From f63c57540d2707e7ebcc0f4439b51197bd88d004 Mon Sep 17 00:00:00 2001 From: robertu <4065233+robertu7@users.noreply.github.com> Date: Thu, 13 Jun 2024 21:40:47 +0800 Subject: [PATCH 1/2] feat: allow to truncate long link text and remove protocol --- package-lock.json | 33 ++++++++++-- package.json | 4 +- src/transformers/normalize.test.ts | 46 ++++++++++++++--- src/transformers/normalize.ts | 80 +++++++++++++++++++++++++++--- 4 files changed, 147 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index eabc03a..1e835d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@matters/matters-editor", - "version": "0.2.5-alpha.3", + "version": "0.2.5-alpha.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@matters/matters-editor", - "version": "0.2.5-alpha.3", + "version": "0.2.5-alpha.6", "license": "MIT", "dependencies": { "@tiptap/core": "2.4.0", @@ -49,6 +49,7 @@ "remark-rehype": "^11.1.0", "remark-stringify": "^11.0.0", "unified": "^11.0.4", + "validator": "^13.12.0", "zeed-dom": "^0.13.3" }, "devDependencies": { @@ -66,6 +67,7 @@ "@types/node": "^20.11.25", "@types/react": "^17.0.53", "@types/react-dom": "^17.0.19", + "@types/validator": "^13.11.10", "benny": "^3.7.1", "common-tags": "^1.8.2", "eslint": "^9.4.0", @@ -88,7 +90,7 @@ "vitest": "^1.6.0" }, "engines": { - "node": ">=18.18 <19.0" + "node": ">=18.19 <19.0" }, "peerDependencies": { "react": ">=17.0.0", @@ -1833,6 +1835,12 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" }, + "node_modules/@types/validator": { + "version": "13.11.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.10.tgz", + "integrity": "sha512-e2PNXoXLr6Z+dbfx5zSh9TRlXJrELycxiaXznp4S5+D2M3b9bqJEitNHA5923jhnB2zzFiZHa2f0SI1HoIahpg==", + "dev": true + }, "node_modules/@types/yargs": { "version": "17.0.22", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.22.tgz", @@ -9617,6 +9625,14 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vfile": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", @@ -11247,6 +11263,12 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" }, + "@types/validator": { + "version": "13.11.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.10.tgz", + "integrity": "sha512-e2PNXoXLr6Z+dbfx5zSh9TRlXJrELycxiaXznp4S5+D2M3b9bqJEitNHA5923jhnB2zzFiZHa2f0SI1HoIahpg==", + "dev": true + }, "@types/yargs": { "version": "17.0.22", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.22.tgz", @@ -16961,6 +16983,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==" + }, "vfile": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", diff --git a/package.json b/package.json index 28303d4..f0f6c02 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@matters/matters-editor", - "version": "0.2.5-alpha.5", + "version": "0.2.5-alpha.6", "description": "Editor for matters.news", "author": "https://github.com/thematters", "homepage": "https://github.com/thematters/matters-editor", @@ -74,6 +74,7 @@ "remark-rehype": "^11.1.0", "remark-stringify": "^11.0.0", "unified": "^11.0.4", + "validator": "^13.12.0", "zeed-dom": "^0.13.3" }, "devDependencies": { @@ -91,6 +92,7 @@ "@types/node": "^20.11.25", "@types/react": "^17.0.53", "@types/react-dom": "^17.0.19", + "@types/validator": "^13.11.10", "benny": "^3.7.1", "common-tags": "^1.8.2", "eslint": "^9.4.0", diff --git a/src/transformers/normalize.test.ts b/src/transformers/normalize.test.ts index 25d68d3..294334f 100644 --- a/src/transformers/normalize.test.ts +++ b/src/transformers/normalize.test.ts @@ -1,15 +1,27 @@ import { stripIndent } from 'common-tags' import { describe, expect, test } from 'vitest' -import { normalizeArticleHTML, normalizeCommentHTML } from './normalize' - -const expectNormalizeArticleHTML = (input: string, output: string) => { - const result = normalizeArticleHTML(input) +import { + normalizeArticleHTML, + normalizeCommentHTML, + NormalizeOptions, +} from './normalize' + +const expectNormalizeArticleHTML = ( + input: string, + output: string, + options?: NormalizeOptions, +) => { + const result = normalizeArticleHTML(input, options) expect(result.trim()).toBe(output) } -const expectNormalizeCommentHTML = (input: string, output: string) => { - const result = normalizeCommentHTML(input) +const expectNormalizeCommentHTML = ( + input: string, + output: string, + options?: NormalizeOptions, +) => { + const result = normalizeCommentHTML(input, options) expect(result.trim()).toBe(output) } @@ -318,6 +330,28 @@ describe('Normalization: Comment', () => { '

abc

', '

abc

', ) + + const longURL = + 'https://medium.com/yihan-huang-studio/%E4%BA%BA%E6%A0%BC%E6%8A%BD%E9%9B%A2%E7%9A%84%E5%B9%BB%E8%A6%BA%E6%B0%A3%E5%91%B3-%E4%BB%A5%E9%B4%89%E7%89%87%E5%85%A5%E9%A6%99%E7%9A%84-boudicca-wode-630a5b253bb3' + + expectNormalizeCommentHTML( + `

${longURL}

`, + `

medium.com/yihan-hua...

`, + { truncate: { maxLength: 20, keepProtocol: false } }, + ) + + expectNormalizeCommentHTML( + `

${longURL}

`, + `

https://medium.com/y...

`, + { truncate: { maxLength: 20, keepProtocol: true } }, + ) + + expect(() => + normalizeCommentHTML( + `

${longURL}

`, + { truncate: { maxLength: 0, keepProtocol: true } }, + ), + ).toThrow('maxLength must be greater than 0') }) test('bolds is not supported', () => { diff --git a/src/transformers/normalize.ts b/src/transformers/normalize.ts index f5c67ff..9b6aeaa 100644 --- a/src/transformers/normalize.ts +++ b/src/transformers/normalize.ts @@ -1,6 +1,7 @@ import type { Extensions } from '@tiptap/core' import { getSchema } from '@tiptap/core' import { DOMParser, DOMSerializer, Node } from '@tiptap/pm/model' +import isURL from 'validator/lib/isURL' import { createHTMLDocument, parseHTML, type VHTMLDocument } from 'zeed-dom' import { @@ -10,6 +11,13 @@ import { Mention, } from '../editors/extensions' +export type NormalizeOptions = { + truncate?: { + maxLength: number + keepProtocol: boolean + } +} + export const makeNormalizer = (extensions: Extensions) => { const schema = getSchema(extensions) @@ -30,20 +38,80 @@ export const makeNormalizer = (extensions: Extensions) => { } } -export const normalizeArticleHTML = (html: string): string => { +// match HTML anchor tags and truncate the text +export const truncateLinkText = ( + html: string, + { maxLength, keepProtocol }: { maxLength: number; keepProtocol: boolean }, +): string => { + const regex = /]*?)>(.*?)<\/a>/gi + + return html.replace(regex, (match, attributes: string, text: string) => { + if (!isURL(text)) { + return match + } + + let truncatedText = text + + if (!keepProtocol) { + truncatedText = text.replace(/(^\w+:|^)\/\//, '') + } + + if (maxLength === 0) { + throw new Error('maxLength must be greater than 0') + } + + if (maxLength && truncatedText.length > maxLength) { + truncatedText = truncatedText.slice(0, maxLength) + '...' + } + + return `${truncatedText}` + }) +} + +export const normalizeArticleHTML = ( + html: string, + options?: NormalizeOptions, +): string => { const extensions = makeArticleEditorExtensions({}) const normalizer = makeNormalizer([...extensions, Mention]) - return normalizer(html) + + let normalizedHtml = normalizer(html) + + if (options?.truncate) { + normalizedHtml = truncateLinkText(html, options.truncate) + } + + return normalizedHtml } -export const normalizeCommentHTML = (html: string): string => { +export const normalizeCommentHTML = ( + html: string, + options?: NormalizeOptions, +): string => { const extensions = makeCommentEditorExtensions({}) const normalizer = makeNormalizer([...extensions, Mention]) - return normalizer(html) + + let normalizedHtml = normalizer(html) + + if (options?.truncate) { + normalizedHtml = truncateLinkText(html, options.truncate) + } + + return normalizedHtml } -export const normalizeJournalHTML = (html: string): string => { +export const normalizeJournalHTML = ( + html: string, + options?: NormalizeOptions, +): string => { const extensions = makeJournalEditorExtensions({}) const normalizer = makeNormalizer([...extensions, Mention]) - return normalizer(html) + + let normalizedHtml = normalizer(html) + + if (options?.truncate) { + normalizedHtml = truncateLinkText(html, options.truncate) + } + + return normalizedHtml } From c267518b8a0ed00e1585f34d23244a24e3696948 Mon Sep 17 00:00:00 2001 From: robertu <4065233+robertu7@users.noreply.github.com> Date: Thu, 13 Jun 2024 21:44:03 +0800 Subject: [PATCH 2/2] fix: throw error if maxLength is less than zero --- src/transformers/normalize.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transformers/normalize.ts b/src/transformers/normalize.ts index 9b6aeaa..85e24ee 100644 --- a/src/transformers/normalize.ts +++ b/src/transformers/normalize.ts @@ -56,7 +56,7 @@ export const truncateLinkText = ( truncatedText = text.replace(/(^\w+:|^)\/\//, '') } - if (maxLength === 0) { + if (maxLength <= 0) { throw new Error('maxLength must be greater than 0') }