"use client";
import React, { useEffect, useRef, useState } from "react";
export type EditorJsBlock = {
id: string
type: string
data: {
text?: string
level?: number
style?: "ordered" | "unordered"
items?: string[]
}
}
// Helper function to decode HTML entities
function decodeHtmlEntities(text: string): string {
const textarea = typeof document !== 'undefined'
? document.createElement('textarea')
: null;
if (textarea) {
textarea.innerHTML = text;
return textarea.value;
}
// Fallback for server-side rendering
return text
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/ /g, ' ');
}
// Helper function to check if text contains HTML table elements
function isTableFragment(text: string): boolean {
const tablePatterns = [
/
/i,
//i,
//i,
//i,
/| /i,
/ | /i,
//i,
/ pattern.test(text));
}
// Helper function to merge table fragments
function mergeTableBlocks(blocks: EditorJsBlock[]): EditorJsBlock[] {
const merged: EditorJsBlock[] = [];
let tableBuffer: string[] = [];
let tableStartId: string | null = null;
let isInTable = false;
for (let i = 0; i < blocks.length; i++) {
const block = blocks[i];
const text = block.data?.text || "";
const decodedText = decodeHtmlEntities(text);
// Check if this starts a table
if (/ tags and preserve whitespace
const cleanedText = decodedText
.replace(/ /gi, '')
.replace(/^\s+|\s+$/g, ''); // Trim leading/trailing whitespace only
if (cleanedText) {
tableBuffer.push(cleanedText);
}
// Check if this closes the table
if (/<\/table>/i.test(decodedText)) {
isInTable = false;
// Create merged table block
const mergedHTML = tableBuffer.join('');
merged.push({
id: tableStartId || block.id,
type: "table",
data: {
text: mergedHTML
}
});
tableBuffer = [];
tableStartId = null;
}
} else {
// If we have accumulated table content, create a merged block
if (tableBuffer.length > 0 && tableStartId) {
// Join without extra newlines to preserve HTML structure
const mergedHTML = tableBuffer.join('');
merged.push({
id: tableStartId,
type: "table",
data: {
text: mergedHTML
}
});
tableBuffer = [];
tableStartId = null;
isInTable = false;
}
// Add the non-table block
merged.push(block);
}
}
// Handle any remaining table content
if (tableBuffer.length > 0 && tableStartId) {
const mergedHTML = tableBuffer.join('');
merged.push({
id: tableStartId,
type: "table",
data: {
text: mergedHTML
}
});
}
return merged;
}
function TableWithScrollCheck({
blockId,
html,
}: {
blockId: string;
html: string;
}) {
const wrapperRef = useRef(null);
const [hasOverflow, setHasOverflow] = useState(false);
useEffect(() => {
const checkOverflow = () => {
if (wrapperRef.current) {
const hasScroll =
wrapperRef.current.scrollWidth > wrapperRef.current.clientWidth;
setHasOverflow(hasScroll);
}
};
checkOverflow();
window.addEventListener("resize", checkOverflow);
const observer = new MutationObserver(checkOverflow);
if (wrapperRef.current) {
observer.observe(wrapperRef.current, { childList: true, subtree: true });
}
return () => {
window.removeEventListener("resize", checkOverflow);
observer.disconnect();
};
}, [html]);
return (
{hasOverflow && (
Swipe left to view additional table content
)}
);
}
export default function EditorRenderer({
content,
}: {
content: string | null | undefined;
}) {
let blocks: EditorJsBlock[] = [];
if (content) {
try {
const parsed = JSON.parse(content)
if (Array.isArray(parsed?.blocks)) {
blocks = parsed.blocks as EditorJsBlock[]
}
} catch (_) {}
}
if (!blocks.length) {
return (
Data not found
)
}
// Merge table fragments before rendering
const mergedBlocks = mergeTableBlocks(blocks);
const renderBlock = (block: EditorJsBlock) => {
const { type, data } = block
const html = decodeHtmlEntities(data?.text || "")
switch (type) {
case "table":
return (
);
case "header": {
// Never render `h1` from CMS content. Each page template should own the
// single visible H1 for SEO/accessibility.
const level = Math.min(Math.max(Number(data?.level) || 3, 2), 6)
return React.createElement(
`h${level}`,
{
key: block.id,
className: "text-2xl font-semibold leading-8 tracking-[-0.06px] my-6",
dangerouslySetInnerHTML: { __html: html },
}
)
}
case "list": {
const style = data?.style || "unordered"
const items = Array.isArray(data?.items) ? data.items : []
const ListTag = style === "ordered" ? "ol" : "ul"
const listClassName = style === "ordered"
? "list-decimal list-inside my-4 space-y-2 text-lg leading-7 tracking-[-0.045px]"
: "list-disc list-inside my-4 space-y-2 text-lg leading-7 tracking-[-0.045px]"
return (
{items.map((item, idx) => (
))}
)
}
case "paragraph":
default:
// Check if this paragraph contains table HTML that wasn't merged
if (isTableFragment(html)) {
return (
);
}
return (
)
}
}
return (
<>
{mergedBlocks.map(renderBlock)}
>
)
}
|