Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3e4cbf1
🔧 chore: update frontend dependencies including tiptap extensions and…
caverav Jan 31, 2025
5adcaf6
✨ feat: Update RichText component with Tiptap editor and custom styling
caverav Jan 31, 2025
3955940
💄 style: Add CSS styles for a rich text editor component
caverav Jan 31, 2025
4d65e63
✨ feat: update frontend dependencies
caverav Jan 31, 2025
18f7f01
✨ feat: Update frontend dependencies, adding "@tiptap/core" version "…
caverav Jan 31, 2025
76c608e
✨ feat: Updated RichText component with additional features like Kate…
caverav Jan 31, 2025
a9b1426
💄 style: Improve editor styling and add new features including table …
caverav Jan 31, 2025
11611a6
✨ feat: update packages in frontend/package.json by adding "@tiptap/p…
caverav Jan 31, 2025
2c743c1
✨ feat: Add CustomImage component and update images URL configuration
caverav Jan 31, 2025
d399932
✨ feat: Add image service with functions for fetching, creating, and …
caverav Jan 31, 2025
e4cb4d5
✨ feat: Add util functions.
caverav Jan 31, 2025
e727293
✨ feat: Add ImageHandler component with image handling logic
caverav Jan 31, 2025
0a463a9
✨ feat: Configure Image extension in RichText editor to handle image …
caverav Feb 1, 2025
01d14d4
refactor(useAuth.tsx): extract API endpoint URL to a constant for con…
caverav Feb 4, 2025
35851ae
🐛 fix(report-generator.js): fix regex to handle optional alt attribut…
caverav Feb 4, 2025
268562f
♻️ refactor(RichText.tsx): remove commented-out code and simplify ret…
caverav Feb 4, 2025
e39f7e9
🔥 remove(frontend): remove unused ImageHandler component and related …
caverav Feb 5, 2025
ea43193
♻️ refactor(report-generator.js): refactor splitHTMLParagraphs for cl…
caverav Feb 5, 2025
e4a3292
🔧 chore: Remove unused tiptap packages and react-quill-new from front…
caverav Feb 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 30 additions & 19 deletions backend/src/lib/report-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -661,36 +661,47 @@ async function prepAuditData(data, settings) {
}

async function splitHTMLParagraphs(data) {
var result = [];
if (!data) return result;
if (!data) return [];

var splitted = data.split(/(<img.+?src=".*?".+?alt=".*?".*?>)/);
const result = [];
const splitted = data.split(/(<img.+?src=".*?".*?(alt=".*?")?.*?>)/);

for (const value of splitted) {
if (!value) continue;

for (var value of splitted) {
if (value.startsWith('<img')) {
var src = value.match(/<img.+src="(.*?)"/) || '';
var alt = value.match(/<img.+alt="(.*?)"/) || '';
if (src && src.length > 1) src = src[1];
if (alt && alt.length > 1) alt = _.unescape(alt[1]);

if (!src.startsWith('data')) {
try {
src = (await Image.getOne(src)).value;
} catch (error) {
src = 'data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA=';
}
}
const { src, alt } = extractImageAttributes(value);
const processedSrc = await processImageSrc(src);

if (result.length === 0) result.push({ text: '', images: [] });
result[result.length - 1].images.push({ image: src, caption: alt });
} else if (value === '') {
continue;
result[result.length - 1].images.push({ image: processedSrc, caption: alt });
} else {
result.push({ text: value, images: [] });
}
}

return result;
}

function extractImageAttributes(value) {
const srcMatch = value.match(/<img.+src="(.*?)"/) || '';
const altMatch = value.match(/<img.+alt="(.*?)"/) || '';
const src = srcMatch.length > 1 ? srcMatch[1] : '';
const alt = altMatch.length > 1 ? _.unescape(altMatch[1]) : '';
return { src, alt };
}

async function processImageSrc(src) {
if (!src.startsWith('data')) {
try {
src = (await Image.getOne(src)).value;
} catch (error) {
src = 'data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA=';
}
}
return src;
}

function replaceSubTemplating(o, originalData = o) {
var regexp = /\{_\{([a-zA-Z0-9\[\]\_\.]{1,})\}_\}/gm;
if (Array.isArray(o))
Expand Down
8 changes: 6 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"preview": "vite preview"
},
"dependencies": {
"@ant-design/icons": "^5.6.0",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@headlessui/react": "^2.1.2",
Expand All @@ -37,18 +38,21 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"dayjs": "^1.11.7",
"diff": "^7.0.0",
"html2canvas": "^1.4.1",
"i18next": "^23.12.2",
"i18next-browser-languagedetector": "^8.0.0",
"jspdf": "^2.5.2",
"katex": "^0.16.21",
"lodash": "^4.17.21",
"lucide-react": "^0.435.0",
"react": "^18.3.1",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18.3.1",
"react-i18next": "^15.0.0",
"react-icons": "^5.3.0",
"react-quill-new": "^3.3.0",
"react-router-dom": "^6.25.1",
"reactjs-tiptap-editor": "^0.1.14",
"sonner": "^1.5.0",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7"
Expand All @@ -68,7 +72,7 @@
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.9.0",
"eslint-plugin-no-secrets": "^1.0.2",
"eslint-plugin-prefer-arrow-functions": "^3.4.1",
"eslint-plugin-prefer-arrow-functions": "3.4.1",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-react-hooks": "^4.6.2",
Expand Down
221 changes: 161 additions & 60 deletions frontend/src/components/text/RichText.tsx
Original file line number Diff line number Diff line change
@@ -1,72 +1,173 @@
import './css/quill.snow.css';
import './css/quill-styles.css';
import 'katex/dist/katex.min.css';
import 'reactjs-tiptap-editor/style.css';

import { Field, Label } from '@headlessui/react';
import ReactQuill from 'react-quill-new';
import { useRef, useState } from 'react';
import RcTiptapEditor, {
Attachment,
BaseKit,
Blockquote,
Bold,
BulletList,
Clear,
Code,
CodeBlock,
Color,
ColumnActionButton,
Emoji,
Excalidraw,
FontFamily,
FontSize,
FormatPainter,
Heading,
Highlight,
History,
Iframe,
Image,
Indent,
Italic,
Katex,
Link,
Mention,
MoreMark,
OrderedList,
SearchAndReplace,
SlashCommand,
Strike,
Table,
TableOfContents,
TaskList,
Underline,
} from 'reactjs-tiptap-editor';

type RichTextEditorProps = {
const imagesUrl = import.meta.env.VITE_API_URL + '/api/images';

const extensions = [
BaseKit.configure({
multiColumn: true,
placeholder: {
showOnlyCurrent: true,
},
characterCount: {
limit: 50_000,
},
}),
History,
SearchAndReplace,
TableOfContents,
FormatPainter.configure({ spacer: true }),
Clear,
FontFamily,
Heading.configure({ spacer: true }),
FontSize,
Bold,
Italic,
Underline,
Strike,
MoreMark,
Katex,
Emoji,
Color.configure({ spacer: true }),
Highlight,
BulletList,
OrderedList,
Indent,
TaskList.configure({
spacer: true,
taskItem: {
nested: true,
},
}),
Link,
Image.configure({
upload: async (file: File) => {
// eslint-disable-next-line sonarjs/prefer-immediate-return -- we need to wait for the file to be read
const base64Value = await new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
if (reader.result) {
resolve(reader.result.toString());
} else {
reject(new Error('File reading failed'));
}
};
reader.onerror = error => reject(error);
});
return base64Value;
},
}),
Blockquote.configure({ spacer: true }),
SlashCommand,
Code.configure({
toolbar: false,
}),
CodeBlock.configure({ defaultTheme: 'dracula' }),
ColumnActionButton,
Table,
Iframe,
Excalidraw,
Mention,
Attachment.configure({
upload: async (file: File) => {
const formData = new FormData();
formData.append('value', await file.text());
const response = await fetch(imagesUrl, {
method: 'POST',
body: formData,
});
const data = await response.json();
return data.value;
},
}),
];

function debounce(func: (value: string) => void, wait: number) {
let timeout: NodeJS.Timeout;
return function (this: unknown, ...args: [string]) {
clearTimeout(timeout);

timeout = setTimeout(() => func.apply(this, args), wait);
};
}

type RichTextProps = {
label: string;
value: string;
onChange: (value: string) => void;
placeholder: string;
onChange: (content: string) => void;
value: string;
};

const RichText: React.FC<RichTextEditorProps> = ({
label,
value,
placeholder,
onChange,
}) => {
const modules = {
toolbar: [
[{ header: [1, 2, 3, false] }, { font: [] }],
[{ size: [] }],
['bold', 'italic', 'underline', 'strike', 'blockquote'],
[
{ list: 'ordered' },
{ list: 'bullet' },
{ indent: '-1' },
{ indent: '+1' },
],
['link', 'image', 'code-block'],
['clean'],
],
clipboard: {
matchVisual: false,
},
};
const RichText = ({ label, onChange, placeholder, value }: RichTextProps) => {
const [content, setContent] = useState(value || placeholder);
const refEditor = useRef(null);

const disable = false;

const onValueChange = debounce((value: string) => {
onChange(value);
setContent(value);
}, 300);

return (
<div className="p-4 w-full rounded-md border-0 text-gray-900 ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
<Field>
<Label className="block font-medium leading-6 text-gray-300">
{label}
</Label>
<ReactQuill
className="w-full overflow-auto bg-white mt-2 p-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
formats={[
'header',
'font',
'size',
'bold',
'italic',
'underline',
'strike',
'blockquote',
'list',
'indent',
'link',
'image',
'code-block',
]}
modules={modules}
onChange={(value: string) => onChange(value)}
placeholder={placeholder}
theme="snow"
value={value}
<main
style={{
padding: '0 20px',
}}
>
<p>{label}</p>
<div className="m-4">
<RcTiptapEditor
content={content}
dark={true}
disabled={disable}
extensions={extensions}
onChangeContent={onValueChange}
output="html"
ref={refEditor}
/>
</Field>
</div>
</div>
</main>
);
};

export default RichText;
Loading