You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@answer.apache.org by ro...@apache.org on 2025/05/19 03:34:32 UTC
(answer) 01/01: feat: add Turndown integration for HTML to Markdown conversion in image handling
This is an automated email from the ASF dual-hosted git repository.
robin0716 pushed a commit to branch refactor/turndown
in repository https://gitbox.apache.org/repos/asf/answer.git
commit a924920f278d7f3a2f17f43cb5dbdf401d8aa277
Author: robin <ro...@rnode.me>
AuthorDate: Mon May 19 11:33:52 2025 +0800
feat: add Turndown integration for HTML to Markdown conversion in image handling
---
ui/package.json | 2 ++
ui/pnpm-lock.yaml | 23 ++++++++++++++++++
ui/src/components/Editor/ToolBars/image.tsx | 37 +++++++++++------------------
ui/src/components/Editor/utils/index.ts | 18 ++++++++++++++
4 files changed, 57 insertions(+), 23 deletions(-)
diff --git a/ui/package.json b/ui/package.json
index b88fc12d..842b607d 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -46,6 +46,7 @@
"react-router-dom": "^7.0.2",
"semver": "^7.3.8",
"swr": "^1.3.0",
+ "turndown": "^7.2.0",
"zustand": "^5.0.2"
},
"devDependencies": {
@@ -65,6 +66,7 @@
"@types/qs": "^6.9.7",
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
+ "@types/turndown": "^5.0.5",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"buffer": "6.0.3",
diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml
index 842009a7..cbed2c6e 100644
--- a/ui/pnpm-lock.yaml
+++ b/ui/pnpm-lock.yaml
@@ -95,6 +95,9 @@ importers:
swr:
specifier: ^1.3.0
version: 1.3.0(react@18.3.1)
+ turndown:
+ specifier: ^7.2.0
+ version: 7.2.0
zustand:
specifier: ^5.0.2
version: 5.0.2(@types/react@18.3.16)(immer@9.0.21)(react@18.3.1)(use-sync-external-store@1.2.2(react@18.3.1))
@@ -147,6 +150,9 @@ importers:
'@types/react-dom':
specifier: ^18.0.6
version: 18.3.5(@types/react@18.3.16)
+ '@types/turndown':
+ specifier: ^5.0.5
+ version: 5.0.5
'@typescript-eslint/eslint-plugin':
specifier: ^6.11.0
version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5)
@@ -1409,6 +1415,9 @@ packages:
'@marijn/find-cluster-break@1.0.2':
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
+ '@mixmark-io/domino@2.2.0':
+ resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==}
+
'@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1':
resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==}
@@ -1808,6 +1817,9 @@ packages:
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
+ '@types/turndown@5.0.5':
+ resolution: {integrity: sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w==}
+
'@types/warning@3.0.3':
resolution: {integrity: sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==}
@@ -6454,6 +6466,9 @@ packages:
turbo-stream@2.4.0:
resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==}
+ turndown@7.2.0:
+ resolution: {integrity: sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A==}
+
type-check@0.3.2:
resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==}
engines: {node: '>= 0.8.0'}
@@ -8677,6 +8692,8 @@ snapshots:
'@marijn/find-cluster-break@1.0.2': {}
+ '@mixmark-io/domino@2.2.0': {}
+
'@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1':
dependencies:
eslint-scope: 5.1.1
@@ -9127,6 +9144,8 @@ snapshots:
'@types/trusted-types@2.0.7': {}
+ '@types/turndown@5.0.5': {}
+
'@types/warning@3.0.3': {}
'@types/ws@8.5.13':
@@ -14599,6 +14618,10 @@ snapshots:
turbo-stream@2.4.0: {}
+ turndown@7.2.0:
+ dependencies:
+ '@mixmark-io/domino': 2.2.0
+
type-check@0.3.2:
dependencies:
prelude-ls: 1.1.2
diff --git a/ui/src/components/Editor/ToolBars/image.tsx b/ui/src/components/Editor/ToolBars/image.tsx
index 322da08d..5b31d99d 100644
--- a/ui/src/components/Editor/ToolBars/image.tsx
+++ b/ui/src/components/Editor/ToolBars/image.tsx
@@ -21,6 +21,8 @@ import { useEffect, useState, memo } from 'react';
import { Button, Form, Modal, Tab, Tabs } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
+import TurndownService from 'turndown';
+
import { Modal as AnswerModal } from '@/components';
import ToolItem from '../toolItem';
import { IEditorContext, Editor } from '../types';
@@ -152,11 +154,21 @@ const Image = ({ editorInstance }) => {
e.stopPropagation();
e.preventDefault();
}
+
+ function htmlToMarkdown(htmlStr) {
+ const turndownService = new TurndownService();
+
+ const markdown = turndownService.turndown(htmlStr);
+ return markdown;
+ }
const drop = async (e) => {
const fileList = e.dataTransfer.files;
const bool = verifyImageSize(fileList);
if (!bool) {
+ const htmlStr = e.dataTransfer.getData('text/html');
+ const markdown = htmlToMarkdown(htmlStr);
+ editor.replaceSelection(markdown);
return;
}
@@ -228,30 +240,9 @@ const Image = ({ editorInstance }) => {
return;
}
event.preventDefault();
+ const markdown = htmlToMarkdown(htmlStr);
- let innerText = '';
- const allPTag = new DOMParser()
- .parseFromString(
- htmlStr.replace(
- /<img([\s\S]*?) src\s*=\s*(['"])([\s\S]*?)\2([^>]*)>/gi,
- `<p>\n\n</p>`,
- ),
- 'text/html',
- )
- .querySelectorAll('body p');
-
- allPTag.forEach((p, index) => {
- const text = p.textContent || '';
- if (text !== '') {
- if (index === allPTag.length - 1) {
- innerText += `${p.textContent}`;
- } else {
- innerText += `${p.textContent}${text.endsWith('\n') ? '' : '\n\n'}`;
- }
- }
- });
-
- editor.replaceSelection(innerText);
+ editor.replaceSelection(markdown);
};
const handleClick = () => {
if (!link.value) {
diff --git a/ui/src/components/Editor/utils/index.ts b/ui/src/components/Editor/utils/index.ts
index 3977367c..d9150c09 100644
--- a/ui/src/components/Editor/utils/index.ts
+++ b/ui/src/components/Editor/utils/index.ts
@@ -172,6 +172,24 @@ export const useEditor = ({
placeholder(placeholderText),
EditorView.lineWrapping,
editableCompartment.of(EditorView.editable.of(true)),
+ EditorView.domEventHandlers({
+ paste(event) {
+ const clipboard = event.clipboardData as DataTransfer;
+ const htmlStr = clipboard.getData('text/html');
+ const imgRegex =
+ /<img([\s\S]*?) src\s*=\s*(['"])([\s\S]*?)\2([^>]*)>/;
+
+ return Boolean(htmlStr.match(imgRegex));
+ },
+ drop(event) {
+ const clipboard = event.dataTransfer as DataTransfer;
+ const htmlStr = clipboard.getData('text/html');
+ const imgRegex =
+ /<img([\s\S]*?) src\s*=\s*(['"])([\s\S]*?)\2([^>]*)>/;
+
+ return Boolean(htmlStr.match(imgRegex));
+ },
+ }),
],
});