From 9c4a3d97099243d7ba59e5649b4088550a860c77 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Thu, 17 Jul 2025 14:48:28 +0700 Subject: [PATCH] feat: support markdown --- .../launches/general.preview.component.tsx | 2 +- .../src/components/new-launch/editor.tsx | 2 +- .../components/new-launch/manage.modal.tsx | 3 ++- .../providers/devto/devto.provider.tsx | 7 +++++- .../providers/high.order.provider.tsx | 1 + .../src/utils/strip.html.validation.ts | 13 ++++++++-- .../database/prisma/posts/posts.service.ts | 2 +- .../providers-settings/dev.to.settings.dto.ts | 2 +- .../integrations/social/bluesky.provider.ts | 6 ++--- .../integrations/social/threads.provider.ts | 2 +- .../src/integrations/social/x.provider.ts | 2 +- package.json | 1 + pnpm-lock.yaml | 25 +++++++++++++++++++ 13 files changed, 55 insertions(+), 13 deletions(-) diff --git a/apps/frontend/src/components/launches/general.preview.component.tsx b/apps/frontend/src/components/launches/general.preview.component.tsx index 15e716e4..301a13a5 100644 --- a/apps/frontend/src/components/launches/general.preview.component.tsx +++ b/apps/frontend/src/components/launches/general.preview.component.tsx @@ -17,7 +17,7 @@ export const GeneralPreviewComponent: FC<{ const mediaDir = useMediaDirectory(); const renderContent = topValue.map((p) => { - const newContent = stripHtmlValidation(p.content, true) + const newContent = stripHtmlValidation('normal', p.content, true) .replace(/(@.+?)(\s)/gi, (match, match1, match2) => { return `${match1.trim()}${match2}`; }) diff --git a/apps/frontend/src/components/new-launch/editor.tsx b/apps/frontend/src/components/new-launch/editor.tsx index 43991a3c..c35c6df3 100644 --- a/apps/frontend/src/components/new-launch/editor.tsx +++ b/apps/frontend/src/components/new-launch/editor.tsx @@ -578,7 +578,7 @@ export const Editor: FC<{ }); const valueWithoutHtml = useMemo(() => { - return stripHtmlValidation(props.value || ''); + return stripHtmlValidation('normal', props.value || ''); }, [props.value]); const addText = useCallback( diff --git a/apps/frontend/src/components/new-launch/manage.modal.tsx b/apps/frontend/src/components/new-launch/manage.modal.tsx index d0152872..6102d4ad 100644 --- a/apps/frontend/src/components/new-launch/manage.modal.tsx +++ b/apps/frontend/src/components/new-launch/manage.modal.tsx @@ -147,7 +147,7 @@ export const ManageModal: FC = (props) => { return p.values.some((a: any) => { return ( countCharacters( - stripHtmlValidation(a.content), + stripHtmlValidation('normal', a.content), p?.integration?.identifier || '' ) === 0 && a.media?.length === 0 ); @@ -166,6 +166,7 @@ export const ManageModal: FC = (props) => { return; } + console.log(checkAllValid); for (const item of checkAllValid) { if (item.valid === false) { toaster.show('Some fields are not valid', 'warning'); diff --git a/apps/frontend/src/components/new-launch/providers/devto/devto.provider.tsx b/apps/frontend/src/components/new-launch/providers/devto/devto.provider.tsx index 8570d3e4..0898a7b3 100644 --- a/apps/frontend/src/components/new-launch/providers/devto/devto.provider.tsx +++ b/apps/frontend/src/components/new-launch/providers/devto/devto.provider.tsx @@ -81,7 +81,12 @@ const DevtoSettings: FC = () => {
- +
); diff --git a/apps/frontend/src/components/new-launch/providers/high.order.provider.tsx b/apps/frontend/src/components/new-launch/providers/high.order.provider.tsx index 61ac2b2d..d2f07d28 100644 --- a/apps/frontend/src/components/new-launch/providers/high.order.provider.tsx +++ b/apps/frontend/src/components/new-launch/providers/high.order.provider.tsx @@ -180,6 +180,7 @@ export const withProvider = function (params: { identifier: selectedIntegration.integration.identifier, integration: selectedIntegration.integration, valid: await form.trigger(), + err: form.formState.errors, errors: checkValidity ? await checkValidity( value.map((p) => p.media || []), diff --git a/libraries/helpers/src/utils/strip.html.validation.ts b/libraries/helpers/src/utils/strip.html.validation.ts index fd2129ee..dd8c71b3 100644 --- a/libraries/helpers/src/utils/strip.html.validation.ts +++ b/libraries/helpers/src/utils/strip.html.validation.ts @@ -1,4 +1,5 @@ import striptags from 'striptags'; +import { NodeHtmlMarkdown } from 'node-html-markdown'; const bold = { a: '𝗮', @@ -131,10 +132,15 @@ const underlineMap = { }; export const stripHtmlValidation = ( + type: 'normal' | 'markdown' | 'html', value: string, replaceBold = false ): string => { - if (value.indexOf("

") === -1) { + if (type === 'markdown') { + return NodeHtmlMarkdown.translate(value); + } + + if (value.indexOf('

') === -1) { return value; } @@ -145,7 +151,10 @@ export const stripHtmlValidation = ( .replace(/<\/p>/gi, ''); if (replaceBold) { - return striptags(convertLinkedinMention(convertToAscii(html)), ['ul', 'li']); + return striptags(convertLinkedinMention(convertToAscii(html)), [ + 'ul', + 'li', + ]); } // Strip all other tags diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index d2f8140a..7bb42cb8 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -467,7 +467,7 @@ export class PostsService { await Promise.all( (newPosts || []).map(async (p) => ({ id: p.id, - message: stripHtmlValidation(p.content, true), + message: stripHtmlValidation(getIntegration.editor, p.content, true), settings: JSON.parse(p.settings || '{}'), media: await this.updateMedia( p.id, diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/dev.to.settings.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/dev.to.settings.dto.ts index 7817ec3e..f1e5813b 100644 --- a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/dev.to.settings.dto.ts +++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/dev.to.settings.dto.ts @@ -43,5 +43,5 @@ export class DevToSettingsDto { @ArrayMaxSize(4) @Type(() => DevToTagsSettingsDto) @ValidateNested({ each: true }) - tags: DevToTagsSettingsDto[]; + tags: DevToTagsSettingsDto[] = []; } diff --git a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts index 38709136..9346d4ca 100644 --- a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts @@ -313,13 +313,13 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider { if (postDetails?.[0]?.settings?.active_thread_finisher) { const rt = new RichText({ - text: stripHtmlValidation(postDetails?.[0]?.settings?.thread_finisher, true), + text: stripHtmlValidation('normal', postDetails?.[0]?.settings?.thread_finisher, true), }); await rt.detectFacets(agent); await agent.post({ - text: stripHtmlValidation(rt.text, true), + text: stripHtmlValidation('normal', rt.text, true), facets: rt.facets, createdAt: new Date().toISOString(), embed: { @@ -460,7 +460,7 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider { if (getThread.data.thread.post?.likeCount >= +fields.likesAmount) { await timer(2000); const rt = new RichText({ - text: stripHtmlValidation(fields.post, true), + text: stripHtmlValidation('normal', fields.post, true), }); await agent.post({ diff --git a/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts b/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts index b426cfe7..37ed96fa 100644 --- a/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts @@ -502,7 +502,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { const form = new FormData(); form.append('media_type', 'TEXT'); - form.append('text', stripHtmlValidation(fields.post, true)); + form.append('text', stripHtmlValidation('normal', fields.post, true)); form.append('reply_to_id', id); form.append('access_token', integration.token); diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts index c6ac2445..574c648e 100644 --- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts @@ -152,7 +152,7 @@ export class XProvider extends SocialAbstract implements SocialProvider { await timer(2000); await client.v2.tweet({ - text: stripHtmlValidation(fields.post, true), + text: stripHtmlValidation('normal', fields.post, true), reply: { in_reply_to_tweet_id: id }, }); return true; diff --git a/package.json b/package.json index 61696e69..6b988745 100644 --- a/package.json +++ b/package.json @@ -161,6 +161,7 @@ "nestjs-real-ip": "^3.0.1", "next": "^14.2.30", "next-plausible": "^3.12.0", + "node-html-markdown": "^1.3.0", "node-telegram-bot-api": "^0.66.0", "nodemailer": "^6.9.15", "nostr-tools": "^2.10.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f63b7bd6..b4f9dc81 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -360,6 +360,9 @@ importers: next-plausible: specifier: ^3.12.0 version: 3.12.4(next@14.2.30(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.53.2)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + node-html-markdown: + specifier: ^1.3.0 + version: 1.3.0 node-telegram-bot-api: specifier: ^0.66.0 version: 0.66.0(request@2.88.2) @@ -9272,6 +9275,10 @@ packages: hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + header-case@2.0.4: resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==} @@ -11430,6 +11437,13 @@ packages: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true + node-html-markdown@1.3.0: + resolution: {integrity: sha512-OeFi3QwC/cPjvVKZ114tzzu+YoR+v9UXW5RwSXGUqGb0qCl0DvP406tzdL7SFn8pZrMyzXoisfG2zcuF9+zw4g==} + engines: {node: '>=10.0.0'} + + node-html-parser@6.1.13: + resolution: {integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==} + node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -26406,6 +26420,8 @@ snapshots: property-information: 7.1.0 space-separated-tokens: 2.0.2 + he@1.2.0: {} + header-case@2.0.4: dependencies: capital-case: 1.0.4 @@ -29253,6 +29269,15 @@ snapshots: node-gyp-build@4.8.4: {} + node-html-markdown@1.3.0: + dependencies: + node-html-parser: 6.1.13 + + node-html-parser@6.1.13: + dependencies: + css-select: 5.2.2 + he: 1.2.0 + node-int64@0.4.0: {} node-mock-http@1.0.1: {}