- Introduced new models: `GuideLine`, `Incident`, `PlotPoint`, `Issue`, `Act`, and `World` for managing book-related entities. - Integrated encryption/decryption for sensitive properties in all models using user-specific keys. - Added methods for CRUD operations and synchronization workflows with error handling and multilingual support. - Improved maintainability with JSDoc comments and streamlined queries.
194 lines
6.7 KiB
TypeScript
Executable File
194 lines
6.7 KiB
TypeScript
Executable File
/**
|
|
* Represents a TipTap editor node structure.
|
|
*/
|
|
export interface TiptapNode {
|
|
type: string;
|
|
content?: TiptapNode[];
|
|
text?: string;
|
|
attrs?: {
|
|
[key: string]: any;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Utility class for handling TipTap content conversions.
|
|
* Provides methods to convert TipTap JSON content to HTML and plain text.
|
|
*/
|
|
export default class Content {
|
|
/**
|
|
* Converts TipTap raw JSON string content to plain text.
|
|
* First converts to HTML, then strips HTML tags to produce plain text.
|
|
*
|
|
* @param content - The TipTap JSON string to convert
|
|
* @returns The plain text representation of the content
|
|
*/
|
|
static convertTipTapRawToText(content: string): string {
|
|
const htmlContent: string = this.convertTiptapToHTMLFromString(content);
|
|
return this.htmlToText(htmlContent);
|
|
}
|
|
|
|
/**
|
|
* Converts HTML string to plain text by removing tags and normalizing whitespace.
|
|
* Preserves paragraph structure by converting block elements to newlines.
|
|
*
|
|
* @param html - The HTML string to convert
|
|
* @returns The plain text representation with preserved paragraph structure
|
|
*/
|
|
static htmlToText(html: string): string {
|
|
return html
|
|
.replace(/<br\s*\/?>/gi, '\n')
|
|
.replace(/<\/?(p|h[1-6]|div)(\s+[^>]*)?>/gi, '\n')
|
|
.replace(/<\/?[^>]+(>|$)/g, '')
|
|
.replace(/(\n\s*){2,}/g, '\n\n')
|
|
.replace(/^\s+|\s+$|(?<=\s)\s+/g, '')
|
|
.trim();
|
|
}
|
|
|
|
/**
|
|
* Converts a TipTap JSON string to HTML.
|
|
* Parses the JSON string and delegates to the node-based conversion method.
|
|
*
|
|
* @param jsonString - The TipTap JSON string to convert
|
|
* @returns The HTML representation, or empty string if JSON is invalid
|
|
*/
|
|
static convertTiptapToHTMLFromString(jsonString: string): string {
|
|
let tiptapNode: TiptapNode;
|
|
try {
|
|
tiptapNode = JSON.parse(jsonString);
|
|
} catch (error) {
|
|
console.error('Invalid JSON string:', error);
|
|
return '';
|
|
}
|
|
|
|
return this.convertTiptapToHTML(tiptapNode);
|
|
}
|
|
|
|
/**
|
|
* Recursively converts a TipTap node structure to HTML.
|
|
* Handles various node types including documents, paragraphs, headings, lists,
|
|
* blockquotes, code blocks, and text with formatting attributes.
|
|
*
|
|
* @param node - The TipTap node to convert
|
|
* @returns The HTML representation of the node and its children
|
|
*/
|
|
static convertTiptapToHTML(node: TiptapNode): string {
|
|
let html = '';
|
|
|
|
switch (node.type) {
|
|
case 'doc':
|
|
if (node.content) {
|
|
node.content.forEach((childNode: TiptapNode) => {
|
|
html += this.convertTiptapToHTML(childNode);
|
|
});
|
|
}
|
|
break;
|
|
|
|
case 'paragraph':
|
|
html += '<p>';
|
|
if (node.content) {
|
|
node.content.forEach((childNode: TiptapNode) => {
|
|
html += this.convertTiptapToHTML(childNode);
|
|
});
|
|
}
|
|
html += '</p>';
|
|
break;
|
|
|
|
case 'text':
|
|
let formattedText = node.text || '';
|
|
|
|
if (node.attrs) {
|
|
if (node.attrs.bold) {
|
|
formattedText = `<strong>${formattedText}</strong>`;
|
|
}
|
|
if (node.attrs.italic) {
|
|
formattedText = `<em>${formattedText}</em>`;
|
|
}
|
|
if (node.attrs.underline) {
|
|
formattedText = `<u>${formattedText}</u>`;
|
|
}
|
|
if (node.attrs.strike) {
|
|
formattedText = `<s>${formattedText}</s>`;
|
|
}
|
|
if (node.attrs.link) {
|
|
formattedText = `<a href="${node.attrs.link.href}">${formattedText}</a>`;
|
|
}
|
|
}
|
|
|
|
html += formattedText;
|
|
break;
|
|
|
|
case 'heading':
|
|
const headingLevel = node.attrs?.level || 1;
|
|
html += `<h${headingLevel}>`;
|
|
if (node.content) {
|
|
node.content.forEach((childNode: TiptapNode) => {
|
|
html += this.convertTiptapToHTML(childNode);
|
|
});
|
|
}
|
|
html += `</h${headingLevel}>`;
|
|
break;
|
|
|
|
case 'bulletList':
|
|
html += '<ul>';
|
|
if (node.content) {
|
|
node.content.forEach((childNode: TiptapNode) => {
|
|
html += this.convertTiptapToHTML(childNode);
|
|
});
|
|
}
|
|
html += '</ul>';
|
|
break;
|
|
|
|
case 'orderedList':
|
|
html += '<ol>';
|
|
if (node.content) {
|
|
node.content.forEach((childNode: TiptapNode) => {
|
|
html += this.convertTiptapToHTML(childNode);
|
|
});
|
|
}
|
|
html += '</ol>';
|
|
break;
|
|
|
|
case 'listItem':
|
|
html += '<li>';
|
|
if (node.content) {
|
|
node.content.forEach((childNode: TiptapNode) => {
|
|
html += this.convertTiptapToHTML(childNode);
|
|
});
|
|
}
|
|
html += '</li>';
|
|
break;
|
|
|
|
case 'blockquote':
|
|
html += '<blockquote>';
|
|
if (node.content) {
|
|
node.content.forEach((childNode: TiptapNode) => {
|
|
html += this.convertTiptapToHTML(childNode);
|
|
});
|
|
}
|
|
html += '</blockquote>';
|
|
break;
|
|
|
|
case 'codeBlock':
|
|
html += '<pre><code>';
|
|
if (node.content) {
|
|
node.content.forEach((childNode: TiptapNode) => {
|
|
html += this.convertTiptapToHTML(childNode);
|
|
});
|
|
}
|
|
html += '</code></pre>';
|
|
break;
|
|
|
|
default:
|
|
console.warn(`Unhandled node type: ${node.type}`);
|
|
if (node.content) {
|
|
node.content.forEach((childNode: TiptapNode) => {
|
|
html += this.convertTiptapToHTML(childNode);
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
|
|
return html;
|
|
}
|
|
}
|