- Add multi-language support for registration and user menu components

- Refactor `TextEditor` to include book-closing functionality with updated toolbar buttons
- Replace `generateHTML` with a lightweight custom TipTap-to-HTML renderer
- Update and lock `esbuild` and related dependencies to latest versions
This commit is contained in:
natreex
2025-12-23 23:24:17 -05:00
parent 1f2513d565
commit 0366a2d444
10 changed files with 728 additions and 537 deletions

View File

@@ -1,5 +1,3 @@
import StarterKit from '@tiptap/starter-kit'
import TextAlign from '@tiptap/extension-text-align'
import ChapterRepo, {
ActChapterQuery,
ChapterQueryResult,
@@ -11,7 +9,6 @@ import ChapterRepo, {
} from "../repositories/chapter.repository.js";
import System from "../System.js";
import {getUserEncryptionKey} from "../keyManager.js";
import { generateHTML } from "@tiptap/react";
export interface ChapterContent {
version: number;
@@ -303,25 +300,92 @@ export default class Chapter {
}
static tipTapToHtml(tipTapContent: JSON): string {
const fixNode = (node: Record<string, unknown>): Record<string, unknown> => {
if (!node) return node;
interface TipTapNode {
type?: string;
text?: string;
content?: TipTapNode[];
attrs?: Record<string, unknown>;
marks?: Array<{ type: string; attrs?: Record<string, unknown> }>;
}
if (node.type === 'text' && (!node.text || node.text === '')) {
node.text = '\u00A0';
}
if (Array.isArray(node.content) && node.content.length) {
node.content = node.content.map(fixNode);
}
return node;
const escapeHtml = (text: string): string => {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
};
return generateHTML(fixNode(tipTapContent as unknown as Record<string, unknown>), [
StarterKit,
TextAlign.configure({
types: ['heading', 'paragraph'],
}),
]);
const renderMarks = (text: string, marks?: Array<{ type: string; attrs?: Record<string, unknown> }>): string => {
if (!marks || marks.length === 0) return escapeHtml(text);
let result = escapeHtml(text);
marks.forEach((mark) => {
switch (mark.type) {
case 'bold':
result = `<strong>${result}</strong>`;
break;
case 'italic':
result = `<em>${result}</em>`;
break;
case 'underline':
result = `<u>${result}</u>`;
break;
case 'strike':
result = `<s>${result}</s>`;
break;
case 'code':
result = `<code>${result}</code>`;
break;
case 'link':
const href = mark.attrs?.href || '#';
result = `<a href="${escapeHtml(String(href))}">${result}</a>`;
break;
}
});
return result;
};
const renderNode = (node: TipTapNode): string => {
if (!node) return '';
if (node.type === 'text') {
const textContent = node.text || '\u00A0';
return renderMarks(textContent, node.marks);
}
const children = node.content?.map(renderNode).join('') || '';
const textAlign = node.attrs?.textAlign ? ` style="text-align: ${node.attrs.textAlign}"` : '';
switch (node.type) {
case 'doc':
return children;
case 'paragraph':
return `<p${textAlign}>${children || '\u00A0'}</p>`;
case 'heading':
const level = node.attrs?.level || 1;
return `<h${level}${textAlign}>${children}</h${level}>`;
case 'bulletList':
return `<ul>${children}</ul>`;
case 'orderedList':
return `<ol>${children}</ol>`;
case 'listItem':
return `<li>${children}</li>`;
case 'blockquote':
return `<blockquote>${children}</blockquote>`;
case 'codeBlock':
return `<pre><code>${children}</code></pre>`;
case 'hardBreak':
return '<br />';
case 'horizontalRule':
return '<hr />';
default:
return children;
}
};
const contentNode = tipTapContent as unknown as TipTapNode;
return renderNode(contentNode);
}
}