Aslam97 / shadcn-minimal-tiptap

Minimal Tiptap Editor
https://shadcn-minimal-tiptap.vercel.app
MIT License
759 stars 37 forks source link

Feature Request - MDX Support as an output. #5

Closed shivaydv closed 4 months ago

shivaydv commented 4 months ago

Most of the modern blog website use mdx to render the blog content. It would be great to have the mdx format in the text editor.

k61b commented 4 months ago

I understand the usefulness of MDX for static pages. However, for projects that incorporate this type of editor, using MDX doesn't seem necessary. The editor already provides us with HTML, which we can directly use. Could you please share more about why you need MDX in this context? @shivaydv

Aslam97 commented 4 months ago

As @k61b mentioned, this minimal-tiptap-editor mean to demonstrates how to set up the Tiptap editor for Shadcn/ui in the most minimal way possible.

Also Tiptap Editor has recently released new documentation, including advanced templates. You can check their docs here.

man-bug commented 2 months ago

Hello @shivaydv, I recently tried this out w/ Next.js 14 using next-mdx + remarkgfm, and I had some hacky success. We're parsing HTML with regex which is a notorious anti-pattern, but it's a start:

import * as React from "react";
import { Content } from "@tiptap/core";

export default function MdxEditor() {
    const [editorContent, setEditorContent] = React.useState<Content>("");
    const [processedContent, setProcessedContent] = React.useState<string>("");

    const handleEditorChange = (content: Content) => {
        setEditorContent(content);
        if (typeof content === "string") {
            setProcessedContent(processTipTapHtmlOutput(content));
        } else {
            console.warn("Editor content is not a string, cannot process.");
            setProcessedContent("");
        }
    };

    return (
        <MinimalTiptapEditor
            {...}
            output="html"
            onChange={handleEditorChange}
        />
    )
}

and the hacky util function:

export const processTipTapHtmlOutput = (html: string): string => {
    const parser = new DOMParser();
    const doc = parser.parseFromString(html, "text/html");

    const processElement = (element: Element): void => {
        // Remove all attributes except href, src, and target, rel
        Array.from(element.attributes).forEach((attr) => {
            if (["href", "src", "target", "rel"].includes(attr.name) === false) {
                element.removeAttribute(attr.name);
            }
        });
        // Process children
        element.childNodes.forEach((child) => {
            if (child.nodeType === Node.ELEMENT_NODE) {
                processElement(child as Element);
            }
        });
    };

    processElement(doc.body);

    // Convert the processed HTML back to a string
    let processedHtml = doc.body.innerHTML;

    // Tiptap doesn't self-close these tags :(
    const selfClosingTags = ["br", "hr", "img"];
    selfClosingTags.forEach((tag) => {
        const regex = new RegExp(`<${tag}([^>]*)>`, "g");
        processedHtml = processedHtml.replace(regex, `<${tag}$1 />`);
    });

    // Replace escaped HTML lt/gt chars
    processedHtml = processedHtml.replace(/&lt;/g, "<").replace(/&gt;/g, ">");

    // Remove parent <p> tags around custom components to avoid hydration errors in Next.js
    processedHtml = processedHtml.replace(
        /<p>\s*(<[A-Z][^>]*>[\s\S]*?<\/[A-Z][^>]*>|<[A-Z][^/>]*\/>)\s*<\/p>/g,
        "$1"
    );

    return processedHtml;
};