Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
111 changes: 49 additions & 62 deletions app/(app)/alpha/new/[[...postIdArr]]/_client.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,6 @@ import {
} from "@headlessui/react";
import { ChevronUpIcon } from "@heroicons/react/20/solid";
import Editor from "@/components/editor/editor";
import RenderPost from "@/components/editor/editor/RenderPost";
import useCreatePage from "@/hooks/useCreatePage";
import { usePrompt } from "@/components/PromptService";

Expand DownExpand Up@@ -214,69 +213,57 @@ const Create = () => {
<div className="z-60 absolute bottom-0 left-0 right-0 top-0 bg-black opacity-25" />
</div>
)}
<div className="bg-black">
<div className="mx-auto w-full max-w-7xl flex-grow text-black lg:flex xl:px-8">
{/* Left sidebar & main wrapper */}
<div className="min-w-0 flex-1 xl:flex">
<div className="lg:min-w-0 lg:flex-1">
<div className="h-full min-h-[40rem] px-4 py-0 sm:px-6 lg:px-4 lg:py-6">
{/* Start main area*/}
<div className="relative h-full">
<div className="bg-neutral-900 text-white shadow-xl">
<div className="bg-neutral-900 px-4 py-6 sm:p-6 lg:pb-8">
{!body && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={"{}"} />
)}
/>
)}
{body && body.length > 0 && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={body} />
)}
/>
)}
<div className="mx-auto break-words px-2 pb-4 sm:px-4 md:max-w-3xl">
<div className="prose mx-auto max-w-3xl dark:prose-invert lg:prose-lg">
{!body && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={"{}"} />
)}
/>
)}
{body && body.length > 0 && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={body} />
)}
/>
)}

<div className="flex items-center justify-between">
<>
{saveStatus === "loading" && <p>Auto-saving...</p>}
{saveStatus === "error" && savedTime && (
<p className="text-xs text-red-600 lg:text-sm">
{`Error saving, last saved: ${savedTime.toString()}`}
</p>
)}
{saveStatus === "success" && savedTime && (
<p className="text-xs text-neutral-400 lg:text-sm">
{`Saved: ${savedTime.toString()}`}
</p>
)}
</>
<div />

<div className="flex">
<button
type="button"
disabled={isDisabled}
className="ml-5 inline-flex justify-center bg-gradient-to-r from-orange-400 to-pink-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:from-orange-300 hover:to-pink-500 focus:outline-none focus:ring-2 focus:ring-pink-300 focus:ring-offset-2 disabled:opacity-50"
onClick={() => setOpen(true)}
>
{!data?.published && "Publish"}
{data?.published && "Save Changes"}
</button>
</div>
</div>
</div>
</div>
</div>
{/* End main area */}
</div>
<div className="flex items-center justify-between">
<div>
{saveStatus === "loading" && (
<p className="text-xs lg:text-sm">Auto-saving...</p>
)}
{saveStatus === "error" && savedTime && (
<p className="text-xs text-red-600 lg:text-sm">
Error saving, last saved: {savedTime.toString()}
</p>
)}
{saveStatus === "success" && savedTime && (
<p className="text-xs text-neutral-400 lg:text-sm">
Saved: {savedTime.toString()}
</p>
)}
</div>

<button
type="button"
disabled={isDisabled}
className="ml-5 inline-flex justify-center bg-gradient-to-r from-orange-400 to-pink-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:from-orange-300 hover:to-pink-500 focus:outline-none focus:ring-2 focus:ring-pink-300 focus:ring-offset-2 disabled:opacity-50"
onClick={() => setOpen(true)}
aria-label={
data?.published
? "Save changes to document"
: "Publish document"
}
>
{data?.published ? "Save Changes" : "Publish"}
</button>
</div>
</div>
</div>
Expand Down
52 changes: 45 additions & 7 deletions app/(app)/articles/[slug]/page.tsx
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import React from "react";
import type { RenderableTreeNode } from "@markdoc/markdoc";
import Markdoc from "@markdoc/markdoc";
import Link from "next/link";
import BioBar from "@/components/BioBar/BioBar";
Expand All@@ -13,6 +14,10 @@ import ArticleAdminPanel from "@/components/ArticleAdminPanel/ArticleAdminPanel"
import { type Metadata } from "next";
import { getPost } from "@/server/lib/posts";
import { getCamelCaseFromLower } from "@/utils/utils";
import { generateHTML } from "@tiptap/html";
import { TiptapExtensions } from "@/components/editor/editor/extensions";
import DOMPurify from "isomorphic-dompurify";
import type { JSONContent } from "@tiptap/core";

type Props = { params: { slug: string } };

Expand DownExpand Up@@ -57,6 +62,20 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
};
}

const parseJSON = (str: string): JSONContent | null => {
try {
return JSON.parse(str);
} catch (e) {
return null;
}
};

const renderSanitizedTiptapContent = (jsonContent: JSONContent) => {
const rawHtml = generateHTML(jsonContent, [...TiptapExtensions]);
// Sanitize the HTML
return DOMPurify.sanitize(rawHtml);
};
Comment on lines +73 to +77

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider memoizing the TiptapExtensions array

The renderSanitizedTiptapContent function creates a new array spread of TiptapExtensions on every call, which could impact performance for large articles.

+const tiptapExtensions = [...TiptapExtensions];+
const renderSanitizedTiptapContent = (jsonContent: JSONContent) => {
- const rawHtml = generateHTML(jsonContent, [...TiptapExtensions]);+ const rawHtml = generateHTML(jsonContent, tiptapExtensions);
// Sanitize the HTML
return DOMPurify.sanitize(rawHtml);
};
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constrenderSanitizedTiptapContent=(jsonContent: JSONContent)=>{
constrawHtml=generateHTML(jsonContent,[...TiptapExtensions]);
// Sanitize the HTML
returnDOMPurify.sanitize(rawHtml);
};
consttiptapExtensions=[...TiptapExtensions];
constrenderSanitizedTiptapContent=(jsonContent: JSONContent)=>{
constrawHtml=generateHTML(jsonContent,tiptapExtensions);
// Sanitize the HTML
returnDOMPurify.sanitize(rawHtml);
};


const ArticlePage = async ({ params }: Props) => {
const session = await getServerAuthSession();
const { slug } = params;
Expand All@@ -66,11 +85,24 @@ const ArticlePage = async ({ params }: Props) => {
const post = await getPost({ slug });

if (!post) {
notFound();
return notFound();
}

const ast = Markdoc.parse(post.body);
const content = Markdoc.transform(ast, config);
const parsedBody = parseJSON(post.body);
const isTiptapContent = parsedBody?.type === "doc";

let renderedContent: string | RenderableTreeNode;
Comment thread
dineshsutihar marked this conversation as resolved.

if (isTiptapContent && parsedBody) {
const jsonContent = parsedBody;
renderedContent = renderSanitizedTiptapContent(jsonContent);
} else {
const ast = Markdoc.parse(post.body);
const transformedContent = Markdoc.transform(ast, config);
renderedContent = Markdoc.renderers.react(transformedContent, React, {
components: markdocComponents,
}) as unknown as string;
Comment on lines +91 to +104

@coderabbitaicoderabbitaiBotOct 22, 2024

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

LGTM: Improved content type detection and rendering

The new logic effectively detects and renders both Tiptap and Markdoc content, aligning with the PR objectives.

Remove the unnecessary type assertion in the Markdoc rendering path:

- renderedContent = Markdoc.renderers.react(transformedContent, React, {- components: markdocComponents,- }) as unknown as string;+ renderedContent = Markdoc.renderers.react(transformedContent, React, {+ components: markdocComponents,+ });

This change eliminates the incorrect casting of a React element to string, improving type safety.

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constparsedBody=parseJSON(post.body);
constisTiptapContent=parsedBody?.type==="doc";
letrenderedContent: string|RenderableTreeNode;
if(isTiptapContent&&parsedBody){
constjsonContent=parsedBody;
renderedContent=renderSanitizedTiptapContent(jsonContent);
}else{
constast=Markdoc.parse(post.body);
consttransformedContent=Markdoc.transform(ast,config);
renderedContent=Markdoc.renderers.react(transformedContent,React,{
components: markdocComponents,
})as unknown asstring;
constparsedBody=parseJSON(post.body);
constisTiptapContent=parsedBody?.type==="doc";
letrenderedContent: string|RenderableTreeNode;
if(isTiptapContent&&parsedBody){
constjsonContent=parsedBody;
renderedContent=renderSanitizedTiptapContent(jsonContent);
}else{
constast=Markdoc.parse(post.body);
consttransformedContent=Markdoc.transform(ast,config);
renderedContent=Markdoc.renderers.react(transformedContent,React,{
components: markdocComponents,
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep the type assertion because renderedContent needs to be treated as a string for use in dangerouslySetInnerHTML, and removing it could lead to runtime issues.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dineshsutihar, understood. Thanks for the clarification. Keeping the type assertion ensures correct behavior when using dangerouslySetInnerHTML.

🐰🤔


✏️ Learnings added
Learnt from: dineshsutihar
PR: codu-code/codu#1153
File: app/(app)/articles/[slug]/page.tsx:91-104
Timestamp: 2024-10-22T08:43:13.045Z
Learning: In `app/(app)/articles/[slug]/page.tsx`, when rendering content, the `renderedContent` needs to be treated as a string using a type assertion because it's used with `dangerouslySetInnerHTML`, and removing the type assertion could lead to runtime issues.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

}

return (
<>
Expand All@@ -83,10 +115,16 @@ const ArticlePage = async ({ params }: Props) => {
/>
<div className="mx-auto break-words px-2 pb-4 sm:px-4 md:max-w-3xl">
<article className="prose mx-auto max-w-3xl dark:prose-invert lg:prose-lg">
<h1>{post.title}</h1>
{Markdoc.renderers.react(content, React, {
components: markdocComponents,
})}
{!isTiptapContent && <h1>{post.title}</h1>}

{isTiptapContent ? (
<div
dangerouslySetInnerHTML={{ __html: renderedContent }}
Comment thread
dineshsutihar marked this conversation as resolved.
className="tiptap-content"
/>
) : (
<div>{renderedContent}</div>
)}
</article>
{post.tags.length > 0 && (
<section className="flex flex-wrap gap-3">
Expand Down
3 changes: 2 additions & 1 deletion components/editor/editor/RenderPost.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import React from "react";
import { TiptapExtensions } from "./extensions";
import { EditorContent, useEditor } from "@tiptap/react";
import SlashCommand from "./extensions/slash-command";

interface RenderPostProps {
json: string;
Expand All@@ -11,7 +12,7 @@ const RenderPost = ({ json }: RenderPostProps) => {

const editor = useEditor({
editable: false,
extensions: [...TiptapExtensions],
extensions: [...TiptapExtensions, SlashCommand],
content,
});

Expand Down
1 change: 0 additions & 1 deletion components/editor/editor/extensions/index.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,7 +138,6 @@ export const TiptapExtensions = [
return "type / to see a list of formatting features";
},
}),
SlashCommand,
TextStyle,
Link.configure({
HTMLAttributes: {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
111 changes: 49 additions & 62 deletions app/(app)/alpha/new/[[...postIdArr]]/_client.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,6 @@ import {
} from "@headlessui/react";
import { ChevronUpIcon } from "@heroicons/react/20/solid";
import Editor from "@/components/editor/editor";
import RenderPost from "@/components/editor/editor/RenderPost";
import useCreatePage from "@/hooks/useCreatePage";
import { usePrompt } from "@/components/PromptService";

Expand DownExpand Up@@ -214,69 +213,57 @@ const Create = () => {
<div className="z-60 absolute bottom-0 left-0 right-0 top-0 bg-black opacity-25" />
</div>
)}
<div className="bg-black">
<div className="mx-auto w-full max-w-7xl flex-grow text-black lg:flex xl:px-8">
{/* Left sidebar & main wrapper */}
<div className="min-w-0 flex-1 xl:flex">
<div className="lg:min-w-0 lg:flex-1">
<div className="h-full min-h-[40rem] px-4 py-0 sm:px-6 lg:px-4 lg:py-6">
{/* Start main area*/}
<div className="relative h-full">
<div className="bg-neutral-900 text-white shadow-xl">
<div className="bg-neutral-900 px-4 py-6 sm:p-6 lg:pb-8">
{!body && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={"{}"} />
)}
/>
)}
{body && body.length > 0 && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={body} />
)}
/>
)}
<div className="mx-auto break-words px-2 pb-4 sm:px-4 md:max-w-3xl">
<div className="prose mx-auto max-w-3xl dark:prose-invert lg:prose-lg">
{!body && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={"{}"} />
)}
/>
)}
{body && body.length > 0 && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={body} />
)}
/>
)}

<div className="flex items-center justify-between">
<>
{saveStatus === "loading" && <p>Auto-saving...</p>}
{saveStatus === "error" && savedTime && (
<p className="text-xs text-red-600 lg:text-sm">
{`Error saving, last saved: ${savedTime.toString()}`}
</p>
)}
{saveStatus === "success" && savedTime && (
<p className="text-xs text-neutral-400 lg:text-sm">
{`Saved: ${savedTime.toString()}`}
</p>
)}
</>
<div />

<div className="flex">
<button
type="button"
disabled={isDisabled}
className="ml-5 inline-flex justify-center bg-gradient-to-r from-orange-400 to-pink-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:from-orange-300 hover:to-pink-500 focus:outline-none focus:ring-2 focus:ring-pink-300 focus:ring-offset-2 disabled:opacity-50"
onClick={() => setOpen(true)}
>
{!data?.published && "Publish"}
{data?.published && "Save Changes"}
</button>
</div>
</div>
</div>
</div>
</div>
{/* End main area */}
</div>
<div className="flex items-center justify-between">
<div>
{saveStatus === "loading" && (
<p className="text-xs lg:text-sm">Auto-saving...</p>
)}
{saveStatus === "error" && savedTime && (
<p className="text-xs text-red-600 lg:text-sm">
Error saving, last saved: {savedTime.toString()}
</p>
)}
{saveStatus === "success" && savedTime && (
<p className="text-xs text-neutral-400 lg:text-sm">
Saved: {savedTime.toString()}
</p>
)}
</div>

<button
type="button"
disabled={isDisabled}
className="ml-5 inline-flex justify-center bg-gradient-to-r from-orange-400 to-pink-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:from-orange-300 hover:to-pink-500 focus:outline-none focus:ring-2 focus:ring-pink-300 focus:ring-offset-2 disabled:opacity-50"
onClick={() => setOpen(true)}
aria-label={
data?.published
? "Save changes to document"
: "Publish document"
}
>
{data?.published ? "Save Changes" : "Publish"}
</button>
</div>
</div>
</div>
Expand Down
52 changes: 45 additions & 7 deletions app/(app)/articles/[slug]/page.tsx
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import React from "react";
import type { RenderableTreeNode } from "@markdoc/markdoc";
import Markdoc from "@markdoc/markdoc";
import Link from "next/link";
import BioBar from "@/components/BioBar/BioBar";
Expand All@@ -13,6 +14,10 @@ import ArticleAdminPanel from "@/components/ArticleAdminPanel/ArticleAdminPanel"
import { type Metadata } from "next";
import { getPost } from "@/server/lib/posts";
import { getCamelCaseFromLower } from "@/utils/utils";
import { generateHTML } from "@tiptap/html";
import { TiptapExtensions } from "@/components/editor/editor/extensions";
import DOMPurify from "isomorphic-dompurify";
import type { JSONContent } from "@tiptap/core";

type Props = { params: { slug: string } };

Expand DownExpand Up@@ -57,6 +62,20 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
};
}

const parseJSON = (str: string): JSONContent | null => {
try {
return JSON.parse(str);
} catch (e) {
return null;
}
};

const renderSanitizedTiptapContent = (jsonContent: JSONContent) => {
const rawHtml = generateHTML(jsonContent, [...TiptapExtensions]);
// Sanitize the HTML
return DOMPurify.sanitize(rawHtml);
};
Comment on lines +73 to +77

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider memoizing the TiptapExtensions array

The renderSanitizedTiptapContent function creates a new array spread of TiptapExtensions on every call, which could impact performance for large articles.

+const tiptapExtensions = [...TiptapExtensions];+
const renderSanitizedTiptapContent = (jsonContent: JSONContent) => {
- const rawHtml = generateHTML(jsonContent, [...TiptapExtensions]);+ const rawHtml = generateHTML(jsonContent, tiptapExtensions);
// Sanitize the HTML
return DOMPurify.sanitize(rawHtml);
};
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constrenderSanitizedTiptapContent=(jsonContent: JSONContent)=>{
constrawHtml=generateHTML(jsonContent,[...TiptapExtensions]);
// Sanitize the HTML
returnDOMPurify.sanitize(rawHtml);
};
consttiptapExtensions=[...TiptapExtensions];
constrenderSanitizedTiptapContent=(jsonContent: JSONContent)=>{
constrawHtml=generateHTML(jsonContent,tiptapExtensions);
// Sanitize the HTML
returnDOMPurify.sanitize(rawHtml);
};


const ArticlePage = async ({ params }: Props) => {
const session = await getServerAuthSession();
const { slug } = params;
Expand All@@ -66,11 +85,24 @@ const ArticlePage = async ({ params }: Props) => {
const post = await getPost({ slug });

if (!post) {
notFound();
return notFound();
}

const ast = Markdoc.parse(post.body);
const content = Markdoc.transform(ast, config);
const parsedBody = parseJSON(post.body);
const isTiptapContent = parsedBody?.type === "doc";

let renderedContent: string | RenderableTreeNode;
Comment thread
dineshsutihar marked this conversation as resolved.

if (isTiptapContent && parsedBody) {
const jsonContent = parsedBody;
renderedContent = renderSanitizedTiptapContent(jsonContent);
} else {
const ast = Markdoc.parse(post.body);
const transformedContent = Markdoc.transform(ast, config);
renderedContent = Markdoc.renderers.react(transformedContent, React, {
components: markdocComponents,
}) as unknown as string;
Comment on lines +91 to +104

@coderabbitaicoderabbitaiBotOct 22, 2024

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

LGTM: Improved content type detection and rendering

The new logic effectively detects and renders both Tiptap and Markdoc content, aligning with the PR objectives.

Remove the unnecessary type assertion in the Markdoc rendering path:

- renderedContent = Markdoc.renderers.react(transformedContent, React, {- components: markdocComponents,- }) as unknown as string;+ renderedContent = Markdoc.renderers.react(transformedContent, React, {+ components: markdocComponents,+ });

This change eliminates the incorrect casting of a React element to string, improving type safety.

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constparsedBody=parseJSON(post.body);
constisTiptapContent=parsedBody?.type==="doc";
letrenderedContent: string|RenderableTreeNode;
if(isTiptapContent&&parsedBody){
constjsonContent=parsedBody;
renderedContent=renderSanitizedTiptapContent(jsonContent);
}else{
constast=Markdoc.parse(post.body);
consttransformedContent=Markdoc.transform(ast,config);
renderedContent=Markdoc.renderers.react(transformedContent,React,{
components: markdocComponents,
})as unknown asstring;
constparsedBody=parseJSON(post.body);
constisTiptapContent=parsedBody?.type==="doc";
letrenderedContent: string|RenderableTreeNode;
if(isTiptapContent&&parsedBody){
constjsonContent=parsedBody;
renderedContent=renderSanitizedTiptapContent(jsonContent);
}else{
constast=Markdoc.parse(post.body);
consttransformedContent=Markdoc.transform(ast,config);
renderedContent=Markdoc.renderers.react(transformedContent,React,{
components: markdocComponents,
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep the type assertion because renderedContent needs to be treated as a string for use in dangerouslySetInnerHTML, and removing it could lead to runtime issues.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dineshsutihar, understood. Thanks for the clarification. Keeping the type assertion ensures correct behavior when using dangerouslySetInnerHTML.

🐰🤔


✏️ Learnings added
Learnt from: dineshsutihar
PR: codu-code/codu#1153
File: app/(app)/articles/[slug]/page.tsx:91-104
Timestamp: 2024-10-22T08:43:13.045Z
Learning: In `app/(app)/articles/[slug]/page.tsx`, when rendering content, the `renderedContent` needs to be treated as a string using a type assertion because it's used with `dangerouslySetInnerHTML`, and removing the type assertion could lead to runtime issues.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

}

return (
<>
Expand All@@ -83,10 +115,16 @@ const ArticlePage = async ({ params }: Props) => {
/>
<div className="mx-auto break-words px-2 pb-4 sm:px-4 md:max-w-3xl">
<article className="prose mx-auto max-w-3xl dark:prose-invert lg:prose-lg">
<h1>{post.title}</h1>
{Markdoc.renderers.react(content, React, {
components: markdocComponents,
})}
{!isTiptapContent && <h1>{post.title}</h1>}

{isTiptapContent ? (
<div
dangerouslySetInnerHTML={{ __html: renderedContent }}
Comment thread
dineshsutihar marked this conversation as resolved.
className="tiptap-content"
/>
) : (
<div>{renderedContent}</div>
)}
</article>
{post.tags.length > 0 && (
<section className="flex flex-wrap gap-3">
Expand Down
3 changes: 2 additions & 1 deletion components/editor/editor/RenderPost.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import React from "react";
import { TiptapExtensions } from "./extensions";
import { EditorContent, useEditor } from "@tiptap/react";
import SlashCommand from "./extensions/slash-command";

interface RenderPostProps {
json: string;
Expand All@@ -11,7 +12,7 @@ const RenderPost = ({ json }: RenderPostProps) => {

const editor = useEditor({
editable: false,
extensions: [...TiptapExtensions],
extensions: [...TiptapExtensions, SlashCommand],
content,
});

Expand Down
1 change: 0 additions & 1 deletion components/editor/editor/extensions/index.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,7 +138,6 @@ export const TiptapExtensions = [
return "type / to see a list of formatting features";
},
}),
SlashCommand,
TextStyle,
Link.configure({
HTMLAttributes: {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
111 changes: 49 additions & 62 deletions app/(app)/alpha/new/[[...postIdArr]]/_client.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,6 @@ import {
} from "@headlessui/react";
import { ChevronUpIcon } from "@heroicons/react/20/solid";
import Editor from "@/components/editor/editor";
import RenderPost from "@/components/editor/editor/RenderPost";
import useCreatePage from "@/hooks/useCreatePage";
import { usePrompt } from "@/components/PromptService";

Expand DownExpand Up@@ -214,69 +213,57 @@ const Create = () => {
<div className="z-60 absolute bottom-0 left-0 right-0 top-0 bg-black opacity-25" />
</div>
)}
<div className="bg-black">
<div className="mx-auto w-full max-w-7xl flex-grow text-black lg:flex xl:px-8">
{/* Left sidebar & main wrapper */}
<div className="min-w-0 flex-1 xl:flex">
<div className="lg:min-w-0 lg:flex-1">
<div className="h-full min-h-[40rem] px-4 py-0 sm:px-6 lg:px-4 lg:py-6">
{/* Start main area*/}
<div className="relative h-full">
<div className="bg-neutral-900 text-white shadow-xl">
<div className="bg-neutral-900 px-4 py-6 sm:p-6 lg:pb-8">
{!body && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={"{}"} />
)}
/>
)}
{body && body.length > 0 && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={body} />
)}
/>
)}
<div className="mx-auto break-words px-2 pb-4 sm:px-4 md:max-w-3xl">
<div className="prose mx-auto max-w-3xl dark:prose-invert lg:prose-lg">
{!body && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={"{}"} />
)}
/>
)}
{body && body.length > 0 && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={body} />
)}
/>
)}

<div className="flex items-center justify-between">
<>
{saveStatus === "loading" && <p>Auto-saving...</p>}
{saveStatus === "error" && savedTime && (
<p className="text-xs text-red-600 lg:text-sm">
{`Error saving, last saved: ${savedTime.toString()}`}
</p>
)}
{saveStatus === "success" && savedTime && (
<p className="text-xs text-neutral-400 lg:text-sm">
{`Saved: ${savedTime.toString()}`}
</p>
)}
</>
<div />

<div className="flex">
<button
type="button"
disabled={isDisabled}
className="ml-5 inline-flex justify-center bg-gradient-to-r from-orange-400 to-pink-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:from-orange-300 hover:to-pink-500 focus:outline-none focus:ring-2 focus:ring-pink-300 focus:ring-offset-2 disabled:opacity-50"
onClick={() => setOpen(true)}
>
{!data?.published && "Publish"}
{data?.published && "Save Changes"}
</button>
</div>
</div>
</div>
</div>
</div>
{/* End main area */}
</div>
<div className="flex items-center justify-between">
<div>
{saveStatus === "loading" && (
<p className="text-xs lg:text-sm">Auto-saving...</p>
)}
{saveStatus === "error" && savedTime && (
<p className="text-xs text-red-600 lg:text-sm">
Error saving, last saved: {savedTime.toString()}
</p>
)}
{saveStatus === "success" && savedTime && (
<p className="text-xs text-neutral-400 lg:text-sm">
Saved: {savedTime.toString()}
</p>
)}
</div>

<button
type="button"
disabled={isDisabled}
className="ml-5 inline-flex justify-center bg-gradient-to-r from-orange-400 to-pink-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:from-orange-300 hover:to-pink-500 focus:outline-none focus:ring-2 focus:ring-pink-300 focus:ring-offset-2 disabled:opacity-50"
onClick={() => setOpen(true)}
aria-label={
data?.published
? "Save changes to document"
: "Publish document"
}
>
{data?.published ? "Save Changes" : "Publish"}
</button>
</div>
</div>
</div>
Expand Down
52 changes: 45 additions & 7 deletions app/(app)/articles/[slug]/page.tsx
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import React from "react";
import type { RenderableTreeNode } from "@markdoc/markdoc";
import Markdoc from "@markdoc/markdoc";
import Link from "next/link";
import BioBar from "@/components/BioBar/BioBar";
Expand All@@ -13,6 +14,10 @@ import ArticleAdminPanel from "@/components/ArticleAdminPanel/ArticleAdminPanel"
import { type Metadata } from "next";
import { getPost } from "@/server/lib/posts";
import { getCamelCaseFromLower } from "@/utils/utils";
import { generateHTML } from "@tiptap/html";
import { TiptapExtensions } from "@/components/editor/editor/extensions";
import DOMPurify from "isomorphic-dompurify";
import type { JSONContent } from "@tiptap/core";

type Props = { params: { slug: string } };

Expand DownExpand Up@@ -57,6 +62,20 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
};
}

const parseJSON = (str: string): JSONContent | null => {
try {
return JSON.parse(str);
} catch (e) {
return null;
}
};

const renderSanitizedTiptapContent = (jsonContent: JSONContent) => {
const rawHtml = generateHTML(jsonContent, [...TiptapExtensions]);
// Sanitize the HTML
return DOMPurify.sanitize(rawHtml);
};
Comment on lines +73 to +77

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider memoizing the TiptapExtensions array

The renderSanitizedTiptapContent function creates a new array spread of TiptapExtensions on every call, which could impact performance for large articles.

+const tiptapExtensions = [...TiptapExtensions];+
const renderSanitizedTiptapContent = (jsonContent: JSONContent) => {
- const rawHtml = generateHTML(jsonContent, [...TiptapExtensions]);+ const rawHtml = generateHTML(jsonContent, tiptapExtensions);
// Sanitize the HTML
return DOMPurify.sanitize(rawHtml);
};
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constrenderSanitizedTiptapContent=(jsonContent: JSONContent)=>{
constrawHtml=generateHTML(jsonContent,[...TiptapExtensions]);
// Sanitize the HTML
returnDOMPurify.sanitize(rawHtml);
};
consttiptapExtensions=[...TiptapExtensions];
constrenderSanitizedTiptapContent=(jsonContent: JSONContent)=>{
constrawHtml=generateHTML(jsonContent,tiptapExtensions);
// Sanitize the HTML
returnDOMPurify.sanitize(rawHtml);
};


const ArticlePage = async ({ params }: Props) => {
const session = await getServerAuthSession();
const { slug } = params;
Expand All@@ -66,11 +85,24 @@ const ArticlePage = async ({ params }: Props) => {
const post = await getPost({ slug });

if (!post) {
notFound();
return notFound();
}

const ast = Markdoc.parse(post.body);
const content = Markdoc.transform(ast, config);
const parsedBody = parseJSON(post.body);
const isTiptapContent = parsedBody?.type === "doc";

let renderedContent: string | RenderableTreeNode;
Comment thread
dineshsutihar marked this conversation as resolved.

if (isTiptapContent && parsedBody) {
const jsonContent = parsedBody;
renderedContent = renderSanitizedTiptapContent(jsonContent);
} else {
const ast = Markdoc.parse(post.body);
const transformedContent = Markdoc.transform(ast, config);
renderedContent = Markdoc.renderers.react(transformedContent, React, {
components: markdocComponents,
}) as unknown as string;
Comment on lines +91 to +104

@coderabbitaicoderabbitaiBotOct 22, 2024

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

LGTM: Improved content type detection and rendering

The new logic effectively detects and renders both Tiptap and Markdoc content, aligning with the PR objectives.

Remove the unnecessary type assertion in the Markdoc rendering path:

- renderedContent = Markdoc.renderers.react(transformedContent, React, {- components: markdocComponents,- }) as unknown as string;+ renderedContent = Markdoc.renderers.react(transformedContent, React, {+ components: markdocComponents,+ });

This change eliminates the incorrect casting of a React element to string, improving type safety.

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constparsedBody=parseJSON(post.body);
constisTiptapContent=parsedBody?.type==="doc";
letrenderedContent: string|RenderableTreeNode;
if(isTiptapContent&&parsedBody){
constjsonContent=parsedBody;
renderedContent=renderSanitizedTiptapContent(jsonContent);
}else{
constast=Markdoc.parse(post.body);
consttransformedContent=Markdoc.transform(ast,config);
renderedContent=Markdoc.renderers.react(transformedContent,React,{
components: markdocComponents,
})as unknown asstring;
constparsedBody=parseJSON(post.body);
constisTiptapContent=parsedBody?.type==="doc";
letrenderedContent: string|RenderableTreeNode;
if(isTiptapContent&&parsedBody){
constjsonContent=parsedBody;
renderedContent=renderSanitizedTiptapContent(jsonContent);
}else{
constast=Markdoc.parse(post.body);
consttransformedContent=Markdoc.transform(ast,config);
renderedContent=Markdoc.renderers.react(transformedContent,React,{
components: markdocComponents,
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep the type assertion because renderedContent needs to be treated as a string for use in dangerouslySetInnerHTML, and removing it could lead to runtime issues.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dineshsutihar, understood. Thanks for the clarification. Keeping the type assertion ensures correct behavior when using dangerouslySetInnerHTML.

🐰🤔


✏️ Learnings added
Learnt from: dineshsutihar
PR: codu-code/codu#1153
File: app/(app)/articles/[slug]/page.tsx:91-104
Timestamp: 2024-10-22T08:43:13.045Z
Learning: In `app/(app)/articles/[slug]/page.tsx`, when rendering content, the `renderedContent` needs to be treated as a string using a type assertion because it's used with `dangerouslySetInnerHTML`, and removing the type assertion could lead to runtime issues.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

}

return (
<>
Expand All@@ -83,10 +115,16 @@ const ArticlePage = async ({ params }: Props) => {
/>
<div className="mx-auto break-words px-2 pb-4 sm:px-4 md:max-w-3xl">
<article className="prose mx-auto max-w-3xl dark:prose-invert lg:prose-lg">
<h1>{post.title}</h1>
{Markdoc.renderers.react(content, React, {
components: markdocComponents,
})}
{!isTiptapContent && <h1>{post.title}</h1>}

{isTiptapContent ? (
<div
dangerouslySetInnerHTML={{ __html: renderedContent }}
Comment thread
dineshsutihar marked this conversation as resolved.
className="tiptap-content"
/>
) : (
<div>{renderedContent}</div>
)}
</article>
{post.tags.length > 0 && (
<section className="flex flex-wrap gap-3">
Expand Down
3 changes: 2 additions & 1 deletion components/editor/editor/RenderPost.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import React from "react";
import { TiptapExtensions } from "./extensions";
import { EditorContent, useEditor } from "@tiptap/react";
import SlashCommand from "./extensions/slash-command";

interface RenderPostProps {
json: string;
Expand All@@ -11,7 +12,7 @@ const RenderPost = ({ json }: RenderPostProps) => {

const editor = useEditor({
editable: false,
extensions: [...TiptapExtensions],
extensions: [...TiptapExtensions, SlashCommand],
content,
});

Expand Down
1 change: 0 additions & 1 deletion components/editor/editor/extensions/index.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,7 +138,6 @@ export const TiptapExtensions = [
return "type / to see a list of formatting features";
},
}),
SlashCommand,
TextStyle,
Link.configure({
HTMLAttributes: {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
111 changes: 49 additions & 62 deletions app/(app)/alpha/new/[[...postIdArr]]/_client.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,6 @@ import {
} from "@headlessui/react";
import { ChevronUpIcon } from "@heroicons/react/20/solid";
import Editor from "@/components/editor/editor";
import RenderPost from "@/components/editor/editor/RenderPost";
import useCreatePage from "@/hooks/useCreatePage";
import { usePrompt } from "@/components/PromptService";

Expand DownExpand Up@@ -214,69 +213,57 @@ const Create = () => {
<div className="z-60 absolute bottom-0 left-0 right-0 top-0 bg-black opacity-25" />
</div>
)}
<div className="bg-black">
<div className="mx-auto w-full max-w-7xl flex-grow text-black lg:flex xl:px-8">
{/* Left sidebar & main wrapper */}
<div className="min-w-0 flex-1 xl:flex">
<div className="lg:min-w-0 lg:flex-1">
<div className="h-full min-h-[40rem] px-4 py-0 sm:px-6 lg:px-4 lg:py-6">
{/* Start main area*/}
<div className="relative h-full">
<div className="bg-neutral-900 text-white shadow-xl">
<div className="bg-neutral-900 px-4 py-6 sm:p-6 lg:pb-8">
{!body && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={"{}"} />
)}
/>
)}
{body && body.length > 0 && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={body} />
)}
/>
)}
<div className="mx-auto break-words px-2 pb-4 sm:px-4 md:max-w-3xl">
<div className="prose mx-auto max-w-3xl dark:prose-invert lg:prose-lg">
{!body && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={"{}"} />
)}
/>
)}
{body && body.length > 0 && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={body} />
)}
/>
)}

<div className="flex items-center justify-between">
<>
{saveStatus === "loading" && <p>Auto-saving...</p>}
{saveStatus === "error" && savedTime && (
<p className="text-xs text-red-600 lg:text-sm">
{`Error saving, last saved: ${savedTime.toString()}`}
</p>
)}
{saveStatus === "success" && savedTime && (
<p className="text-xs text-neutral-400 lg:text-sm">
{`Saved: ${savedTime.toString()}`}
</p>
)}
</>
<div />

<div className="flex">
<button
type="button"
disabled={isDisabled}
className="ml-5 inline-flex justify-center bg-gradient-to-r from-orange-400 to-pink-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:from-orange-300 hover:to-pink-500 focus:outline-none focus:ring-2 focus:ring-pink-300 focus:ring-offset-2 disabled:opacity-50"
onClick={() => setOpen(true)}
>
{!data?.published && "Publish"}
{data?.published && "Save Changes"}
</button>
</div>
</div>
</div>
</div>
</div>
{/* End main area */}
</div>
<div className="flex items-center justify-between">
<div>
{saveStatus === "loading" && (
<p className="text-xs lg:text-sm">Auto-saving...</p>
)}
{saveStatus === "error" && savedTime && (
<p className="text-xs text-red-600 lg:text-sm">
Error saving, last saved: {savedTime.toString()}
</p>
)}
{saveStatus === "success" && savedTime && (
<p className="text-xs text-neutral-400 lg:text-sm">
Saved: {savedTime.toString()}
</p>
)}
</div>

<button
type="button"
disabled={isDisabled}
className="ml-5 inline-flex justify-center bg-gradient-to-r from-orange-400 to-pink-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:from-orange-300 hover:to-pink-500 focus:outline-none focus:ring-2 focus:ring-pink-300 focus:ring-offset-2 disabled:opacity-50"
onClick={() => setOpen(true)}
aria-label={
data?.published
? "Save changes to document"
: "Publish document"
}
>
{data?.published ? "Save Changes" : "Publish"}
</button>
</div>
</div>
</div>
Expand Down
52 changes: 45 additions & 7 deletions app/(app)/articles/[slug]/page.tsx
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import React from "react";
import type { RenderableTreeNode } from "@markdoc/markdoc";
import Markdoc from "@markdoc/markdoc";
import Link from "next/link";
import BioBar from "@/components/BioBar/BioBar";
Expand All@@ -13,6 +14,10 @@ import ArticleAdminPanel from "@/components/ArticleAdminPanel/ArticleAdminPanel"
import { type Metadata } from "next";
import { getPost } from "@/server/lib/posts";
import { getCamelCaseFromLower } from "@/utils/utils";
import { generateHTML } from "@tiptap/html";
import { TiptapExtensions } from "@/components/editor/editor/extensions";
import DOMPurify from "isomorphic-dompurify";
import type { JSONContent } from "@tiptap/core";

type Props = { params: { slug: string } };

Expand DownExpand Up@@ -57,6 +62,20 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
};
}

const parseJSON = (str: string): JSONContent | null => {
try {
return JSON.parse(str);
} catch (e) {
return null;
}
};

const renderSanitizedTiptapContent = (jsonContent: JSONContent) => {
const rawHtml = generateHTML(jsonContent, [...TiptapExtensions]);
// Sanitize the HTML
return DOMPurify.sanitize(rawHtml);
};
Comment on lines +73 to +77

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider memoizing the TiptapExtensions array

The renderSanitizedTiptapContent function creates a new array spread of TiptapExtensions on every call, which could impact performance for large articles.

+const tiptapExtensions = [...TiptapExtensions];+
const renderSanitizedTiptapContent = (jsonContent: JSONContent) => {
- const rawHtml = generateHTML(jsonContent, [...TiptapExtensions]);+ const rawHtml = generateHTML(jsonContent, tiptapExtensions);
// Sanitize the HTML
return DOMPurify.sanitize(rawHtml);
};
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constrenderSanitizedTiptapContent=(jsonContent: JSONContent)=>{
constrawHtml=generateHTML(jsonContent,[...TiptapExtensions]);
// Sanitize the HTML
returnDOMPurify.sanitize(rawHtml);
};
consttiptapExtensions=[...TiptapExtensions];
constrenderSanitizedTiptapContent=(jsonContent: JSONContent)=>{
constrawHtml=generateHTML(jsonContent,tiptapExtensions);
// Sanitize the HTML
returnDOMPurify.sanitize(rawHtml);
};


const ArticlePage = async ({ params }: Props) => {
const session = await getServerAuthSession();
const { slug } = params;
Expand All@@ -66,11 +85,24 @@ const ArticlePage = async ({ params }: Props) => {
const post = await getPost({ slug });

if (!post) {
notFound();
return notFound();
}

const ast = Markdoc.parse(post.body);
const content = Markdoc.transform(ast, config);
const parsedBody = parseJSON(post.body);
const isTiptapContent = parsedBody?.type === "doc";

let renderedContent: string | RenderableTreeNode;
Comment thread
dineshsutihar marked this conversation as resolved.

if (isTiptapContent && parsedBody) {
const jsonContent = parsedBody;
renderedContent = renderSanitizedTiptapContent(jsonContent);
} else {
const ast = Markdoc.parse(post.body);
const transformedContent = Markdoc.transform(ast, config);
renderedContent = Markdoc.renderers.react(transformedContent, React, {
components: markdocComponents,
}) as unknown as string;
Comment on lines +91 to +104

@coderabbitaicoderabbitaiBotOct 22, 2024

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

LGTM: Improved content type detection and rendering

The new logic effectively detects and renders both Tiptap and Markdoc content, aligning with the PR objectives.

Remove the unnecessary type assertion in the Markdoc rendering path:

- renderedContent = Markdoc.renderers.react(transformedContent, React, {- components: markdocComponents,- }) as unknown as string;+ renderedContent = Markdoc.renderers.react(transformedContent, React, {+ components: markdocComponents,+ });

This change eliminates the incorrect casting of a React element to string, improving type safety.

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constparsedBody=parseJSON(post.body);
constisTiptapContent=parsedBody?.type==="doc";
letrenderedContent: string|RenderableTreeNode;
if(isTiptapContent&&parsedBody){
constjsonContent=parsedBody;
renderedContent=renderSanitizedTiptapContent(jsonContent);
}else{
constast=Markdoc.parse(post.body);
consttransformedContent=Markdoc.transform(ast,config);
renderedContent=Markdoc.renderers.react(transformedContent,React,{
components: markdocComponents,
})as unknown asstring;
constparsedBody=parseJSON(post.body);
constisTiptapContent=parsedBody?.type==="doc";
letrenderedContent: string|RenderableTreeNode;
if(isTiptapContent&&parsedBody){
constjsonContent=parsedBody;
renderedContent=renderSanitizedTiptapContent(jsonContent);
}else{
constast=Markdoc.parse(post.body);
consttransformedContent=Markdoc.transform(ast,config);
renderedContent=Markdoc.renderers.react(transformedContent,React,{
components: markdocComponents,
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep the type assertion because renderedContent needs to be treated as a string for use in dangerouslySetInnerHTML, and removing it could lead to runtime issues.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dineshsutihar, understood. Thanks for the clarification. Keeping the type assertion ensures correct behavior when using dangerouslySetInnerHTML.

🐰🤔


✏️ Learnings added
Learnt from: dineshsutihar
PR: codu-code/codu#1153
File: app/(app)/articles/[slug]/page.tsx:91-104
Timestamp: 2024-10-22T08:43:13.045Z
Learning: In `app/(app)/articles/[slug]/page.tsx`, when rendering content, the `renderedContent` needs to be treated as a string using a type assertion because it's used with `dangerouslySetInnerHTML`, and removing the type assertion could lead to runtime issues.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

}

return (
<>
Expand All@@ -83,10 +115,16 @@ const ArticlePage = async ({ params }: Props) => {
/>
<div className="mx-auto break-words px-2 pb-4 sm:px-4 md:max-w-3xl">
<article className="prose mx-auto max-w-3xl dark:prose-invert lg:prose-lg">
<h1>{post.title}</h1>
{Markdoc.renderers.react(content, React, {
components: markdocComponents,
})}
{!isTiptapContent && <h1>{post.title}</h1>}

{isTiptapContent ? (
<div
dangerouslySetInnerHTML={{ __html: renderedContent }}
Comment thread
dineshsutihar marked this conversation as resolved.
className="tiptap-content"
/>
) : (
<div>{renderedContent}</div>
)}
</article>
{post.tags.length > 0 && (
<section className="flex flex-wrap gap-3">
Expand Down
3 changes: 2 additions & 1 deletion components/editor/editor/RenderPost.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import React from "react";
import { TiptapExtensions } from "./extensions";
import { EditorContent, useEditor } from "@tiptap/react";
import SlashCommand from "./extensions/slash-command";

interface RenderPostProps {
json: string;
Expand All@@ -11,7 +12,7 @@ const RenderPost = ({ json }: RenderPostProps) => {

const editor = useEditor({
editable: false,
extensions: [...TiptapExtensions],
extensions: [...TiptapExtensions, SlashCommand],
content,
});

Expand Down
1 change: 0 additions & 1 deletion components/editor/editor/extensions/index.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,7 +138,6 @@ export const TiptapExtensions = [
return "type / to see a list of formatting features";
},
}),
SlashCommand,
TextStyle,
Link.configure({
HTMLAttributes: {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
111 changes: 49 additions & 62 deletions app/(app)/alpha/new/[[...postIdArr]]/_client.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,6 @@ import {
} from "@headlessui/react";
import { ChevronUpIcon } from "@heroicons/react/20/solid";
import Editor from "@/components/editor/editor";
import RenderPost from "@/components/editor/editor/RenderPost";
import useCreatePage from "@/hooks/useCreatePage";
import { usePrompt } from "@/components/PromptService";

Expand DownExpand Up@@ -214,69 +213,57 @@ const Create = () => {
<div className="z-60 absolute bottom-0 left-0 right-0 top-0 bg-black opacity-25" />
</div>
)}
<div className="bg-black">
<div className="mx-auto w-full max-w-7xl flex-grow text-black lg:flex xl:px-8">
{/* Left sidebar & main wrapper */}
<div className="min-w-0 flex-1 xl:flex">
<div className="lg:min-w-0 lg:flex-1">
<div className="h-full min-h-[40rem] px-4 py-0 sm:px-6 lg:px-4 lg:py-6">
{/* Start main area*/}
<div className="relative h-full">
<div className="bg-neutral-900 text-white shadow-xl">
<div className="bg-neutral-900 px-4 py-6 sm:p-6 lg:pb-8">
{!body && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={"{}"} />
)}
/>
)}
{body && body.length > 0 && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={body} />
)}
/>
)}
<div className="mx-auto break-words px-2 pb-4 sm:px-4 md:max-w-3xl">
<div className="prose mx-auto max-w-3xl dark:prose-invert lg:prose-lg">
{!body && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={"{}"} />
)}
/>
)}
{body && body.length > 0 && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={body} />
)}
/>
)}

<div className="flex items-center justify-between">
<>
{saveStatus === "loading" && <p>Auto-saving...</p>}
{saveStatus === "error" && savedTime && (
<p className="text-xs text-red-600 lg:text-sm">
{`Error saving, last saved: ${savedTime.toString()}`}
</p>
)}
{saveStatus === "success" && savedTime && (
<p className="text-xs text-neutral-400 lg:text-sm">
{`Saved: ${savedTime.toString()}`}
</p>
)}
</>
<div />

<div className="flex">
<button
type="button"
disabled={isDisabled}
className="ml-5 inline-flex justify-center bg-gradient-to-r from-orange-400 to-pink-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:from-orange-300 hover:to-pink-500 focus:outline-none focus:ring-2 focus:ring-pink-300 focus:ring-offset-2 disabled:opacity-50"
onClick={() => setOpen(true)}
>
{!data?.published && "Publish"}
{data?.published && "Save Changes"}
</button>
</div>
</div>
</div>
</div>
</div>
{/* End main area */}
</div>
<div className="flex items-center justify-between">
<div>
{saveStatus === "loading" && (
<p className="text-xs lg:text-sm">Auto-saving...</p>
)}
{saveStatus === "error" && savedTime && (
<p className="text-xs text-red-600 lg:text-sm">
Error saving, last saved: {savedTime.toString()}
</p>
)}
{saveStatus === "success" && savedTime && (
<p className="text-xs text-neutral-400 lg:text-sm">
Saved: {savedTime.toString()}
</p>
)}
</div>

<button
type="button"
disabled={isDisabled}
className="ml-5 inline-flex justify-center bg-gradient-to-r from-orange-400 to-pink-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:from-orange-300 hover:to-pink-500 focus:outline-none focus:ring-2 focus:ring-pink-300 focus:ring-offset-2 disabled:opacity-50"
onClick={() => setOpen(true)}
aria-label={
data?.published
? "Save changes to document"
: "Publish document"
}
>
{data?.published ? "Save Changes" : "Publish"}
</button>
</div>
</div>
</div>
Expand Down
52 changes: 45 additions & 7 deletions app/(app)/articles/[slug]/page.tsx
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import React from "react";
import type { RenderableTreeNode } from "@markdoc/markdoc";
import Markdoc from "@markdoc/markdoc";
import Link from "next/link";
import BioBar from "@/components/BioBar/BioBar";
Expand All@@ -13,6 +14,10 @@ import ArticleAdminPanel from "@/components/ArticleAdminPanel/ArticleAdminPanel"
import { type Metadata } from "next";
import { getPost } from "@/server/lib/posts";
import { getCamelCaseFromLower } from "@/utils/utils";
import { generateHTML } from "@tiptap/html";
import { TiptapExtensions } from "@/components/editor/editor/extensions";
import DOMPurify from "isomorphic-dompurify";
import type { JSONContent } from "@tiptap/core";

type Props = { params: { slug: string } };

Expand DownExpand Up@@ -57,6 +62,20 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
};
}

const parseJSON = (str: string): JSONContent | null => {
try {
return JSON.parse(str);
} catch (e) {
return null;
}
};

const renderSanitizedTiptapContent = (jsonContent: JSONContent) => {
const rawHtml = generateHTML(jsonContent, [...TiptapExtensions]);
// Sanitize the HTML
return DOMPurify.sanitize(rawHtml);
};
Comment on lines +73 to +77

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider memoizing the TiptapExtensions array

The renderSanitizedTiptapContent function creates a new array spread of TiptapExtensions on every call, which could impact performance for large articles.

+const tiptapExtensions = [...TiptapExtensions];+
const renderSanitizedTiptapContent = (jsonContent: JSONContent) => {
- const rawHtml = generateHTML(jsonContent, [...TiptapExtensions]);+ const rawHtml = generateHTML(jsonContent, tiptapExtensions);
// Sanitize the HTML
return DOMPurify.sanitize(rawHtml);
};
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constrenderSanitizedTiptapContent=(jsonContent: JSONContent)=>{
constrawHtml=generateHTML(jsonContent,[...TiptapExtensions]);
// Sanitize the HTML
returnDOMPurify.sanitize(rawHtml);
};
consttiptapExtensions=[...TiptapExtensions];
constrenderSanitizedTiptapContent=(jsonContent: JSONContent)=>{
constrawHtml=generateHTML(jsonContent,tiptapExtensions);
// Sanitize the HTML
returnDOMPurify.sanitize(rawHtml);
};


const ArticlePage = async ({ params }: Props) => {
const session = await getServerAuthSession();
const { slug } = params;
Expand All@@ -66,11 +85,24 @@ const ArticlePage = async ({ params }: Props) => {
const post = await getPost({ slug });

if (!post) {
notFound();
return notFound();
}

const ast = Markdoc.parse(post.body);
const content = Markdoc.transform(ast, config);
const parsedBody = parseJSON(post.body);
const isTiptapContent = parsedBody?.type === "doc";

let renderedContent: string | RenderableTreeNode;
Comment thread
dineshsutihar marked this conversation as resolved.

if (isTiptapContent && parsedBody) {
const jsonContent = parsedBody;
renderedContent = renderSanitizedTiptapContent(jsonContent);
} else {
const ast = Markdoc.parse(post.body);
const transformedContent = Markdoc.transform(ast, config);
renderedContent = Markdoc.renderers.react(transformedContent, React, {
components: markdocComponents,
}) as unknown as string;
Comment on lines +91 to +104

@coderabbitaicoderabbitaiBotOct 22, 2024

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

LGTM: Improved content type detection and rendering

The new logic effectively detects and renders both Tiptap and Markdoc content, aligning with the PR objectives.

Remove the unnecessary type assertion in the Markdoc rendering path:

- renderedContent = Markdoc.renderers.react(transformedContent, React, {- components: markdocComponents,- }) as unknown as string;+ renderedContent = Markdoc.renderers.react(transformedContent, React, {+ components: markdocComponents,+ });

This change eliminates the incorrect casting of a React element to string, improving type safety.

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constparsedBody=parseJSON(post.body);
constisTiptapContent=parsedBody?.type==="doc";
letrenderedContent: string|RenderableTreeNode;
if(isTiptapContent&&parsedBody){
constjsonContent=parsedBody;
renderedContent=renderSanitizedTiptapContent(jsonContent);
}else{
constast=Markdoc.parse(post.body);
consttransformedContent=Markdoc.transform(ast,config);
renderedContent=Markdoc.renderers.react(transformedContent,React,{
components: markdocComponents,
})as unknown asstring;
constparsedBody=parseJSON(post.body);
constisTiptapContent=parsedBody?.type==="doc";
letrenderedContent: string|RenderableTreeNode;
if(isTiptapContent&&parsedBody){
constjsonContent=parsedBody;
renderedContent=renderSanitizedTiptapContent(jsonContent);
}else{
constast=Markdoc.parse(post.body);
consttransformedContent=Markdoc.transform(ast,config);
renderedContent=Markdoc.renderers.react(transformedContent,React,{
components: markdocComponents,
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep the type assertion because renderedContent needs to be treated as a string for use in dangerouslySetInnerHTML, and removing it could lead to runtime issues.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dineshsutihar, understood. Thanks for the clarification. Keeping the type assertion ensures correct behavior when using dangerouslySetInnerHTML.

🐰🤔


✏️ Learnings added
Learnt from: dineshsutihar
PR: codu-code/codu#1153
File: app/(app)/articles/[slug]/page.tsx:91-104
Timestamp: 2024-10-22T08:43:13.045Z
Learning: In `app/(app)/articles/[slug]/page.tsx`, when rendering content, the `renderedContent` needs to be treated as a string using a type assertion because it's used with `dangerouslySetInnerHTML`, and removing the type assertion could lead to runtime issues.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

}

return (
<>
Expand All@@ -83,10 +115,16 @@ const ArticlePage = async ({ params }: Props) => {
/>
<div className="mx-auto break-words px-2 pb-4 sm:px-4 md:max-w-3xl">
<article className="prose mx-auto max-w-3xl dark:prose-invert lg:prose-lg">
<h1>{post.title}</h1>
{Markdoc.renderers.react(content, React, {
components: markdocComponents,
})}
{!isTiptapContent && <h1>{post.title}</h1>}

{isTiptapContent ? (
<div
dangerouslySetInnerHTML={{ __html: renderedContent }}
Comment thread
dineshsutihar marked this conversation as resolved.
className="tiptap-content"
/>
) : (
<div>{renderedContent}</div>
)}
</article>
{post.tags.length > 0 && (
<section className="flex flex-wrap gap-3">
Expand Down
3 changes: 2 additions & 1 deletion components/editor/editor/RenderPost.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import React from "react";
import { TiptapExtensions } from "./extensions";
import { EditorContent, useEditor } from "@tiptap/react";
import SlashCommand from "./extensions/slash-command";

interface RenderPostProps {
json: string;
Expand All@@ -11,7 +12,7 @@ const RenderPost = ({ json }: RenderPostProps) => {

const editor = useEditor({
editable: false,
extensions: [...TiptapExtensions],
extensions: [...TiptapExtensions, SlashCommand],
content,
});

Expand Down
1 change: 0 additions & 1 deletion components/editor/editor/extensions/index.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,7 +138,6 @@ export const TiptapExtensions = [
return "type / to see a list of formatting features";
},
}),
SlashCommand,
TextStyle,
Link.configure({
HTMLAttributes: {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
111 changes: 49 additions & 62 deletions app/(app)/alpha/new/[[...postIdArr]]/_client.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,6 @@ import {
} from "@headlessui/react";
import { ChevronUpIcon } from "@heroicons/react/20/solid";
import Editor from "@/components/editor/editor";
import RenderPost from "@/components/editor/editor/RenderPost";
import useCreatePage from "@/hooks/useCreatePage";
import { usePrompt } from "@/components/PromptService";

Expand DownExpand Up@@ -214,69 +213,57 @@ const Create = () => {
<div className="z-60 absolute bottom-0 left-0 right-0 top-0 bg-black opacity-25" />
</div>
)}
<div className="bg-black">
<div className="mx-auto w-full max-w-7xl flex-grow text-black lg:flex xl:px-8">
{/* Left sidebar & main wrapper */}
<div className="min-w-0 flex-1 xl:flex">
<div className="lg:min-w-0 lg:flex-1">
<div className="h-full min-h-[40rem] px-4 py-0 sm:px-6 lg:px-4 lg:py-6">
{/* Start main area*/}
<div className="relative h-full">
<div className="bg-neutral-900 text-white shadow-xl">
<div className="bg-neutral-900 px-4 py-6 sm:p-6 lg:pb-8">
{!body && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={"{}"} />
)}
/>
)}
{body && body.length > 0 && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={body} />
)}
/>
)}
<div className="mx-auto break-words px-2 pb-4 sm:px-4 md:max-w-3xl">
<div className="prose mx-auto max-w-3xl dark:prose-invert lg:prose-lg">
{!body && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={"{}"} />
)}
/>
)}
{body && body.length > 0 && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={body} />
)}
/>
)}

<div className="flex items-center justify-between">
<>
{saveStatus === "loading" && <p>Auto-saving...</p>}
{saveStatus === "error" && savedTime && (
<p className="text-xs text-red-600 lg:text-sm">
{`Error saving, last saved: ${savedTime.toString()}`}
</p>
)}
{saveStatus === "success" && savedTime && (
<p className="text-xs text-neutral-400 lg:text-sm">
{`Saved: ${savedTime.toString()}`}
</p>
)}
</>
<div />

<div className="flex">
<button
type="button"
disabled={isDisabled}
className="ml-5 inline-flex justify-center bg-gradient-to-r from-orange-400 to-pink-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:from-orange-300 hover:to-pink-500 focus:outline-none focus:ring-2 focus:ring-pink-300 focus:ring-offset-2 disabled:opacity-50"
onClick={() => setOpen(true)}
>
{!data?.published && "Publish"}
{data?.published && "Save Changes"}
</button>
</div>
</div>
</div>
</div>
</div>
{/* End main area */}
</div>
<div className="flex items-center justify-between">
<div>
{saveStatus === "loading" && (
<p className="text-xs lg:text-sm">Auto-saving...</p>
)}
{saveStatus === "error" && savedTime && (
<p className="text-xs text-red-600 lg:text-sm">
Error saving, last saved: {savedTime.toString()}
</p>
)}
{saveStatus === "success" && savedTime && (
<p className="text-xs text-neutral-400 lg:text-sm">
Saved: {savedTime.toString()}
</p>
)}
</div>

<button
type="button"
disabled={isDisabled}
className="ml-5 inline-flex justify-center bg-gradient-to-r from-orange-400 to-pink-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:from-orange-300 hover:to-pink-500 focus:outline-none focus:ring-2 focus:ring-pink-300 focus:ring-offset-2 disabled:opacity-50"
onClick={() => setOpen(true)}
aria-label={
data?.published
? "Save changes to document"
: "Publish document"
}
>
{data?.published ? "Save Changes" : "Publish"}
</button>
</div>
</div>
</div>
Expand Down
52 changes: 45 additions & 7 deletions app/(app)/articles/[slug]/page.tsx
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import React from "react";
import type { RenderableTreeNode } from "@markdoc/markdoc";
import Markdoc from "@markdoc/markdoc";
import Link from "next/link";
import BioBar from "@/components/BioBar/BioBar";
Expand All@@ -13,6 +14,10 @@ import ArticleAdminPanel from "@/components/ArticleAdminPanel/ArticleAdminPanel"
import { type Metadata } from "next";
import { getPost } from "@/server/lib/posts";
import { getCamelCaseFromLower } from "@/utils/utils";
import { generateHTML } from "@tiptap/html";
import { TiptapExtensions } from "@/components/editor/editor/extensions";
import DOMPurify from "isomorphic-dompurify";
import type { JSONContent } from "@tiptap/core";

type Props = { params: { slug: string } };

Expand DownExpand Up@@ -57,6 +62,20 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
};
}

const parseJSON = (str: string): JSONContent | null => {
try {
return JSON.parse(str);
} catch (e) {
return null;
}
};

const renderSanitizedTiptapContent = (jsonContent: JSONContent) => {
const rawHtml = generateHTML(jsonContent, [...TiptapExtensions]);
// Sanitize the HTML
return DOMPurify.sanitize(rawHtml);
};
Comment on lines +73 to +77

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider memoizing the TiptapExtensions array

The renderSanitizedTiptapContent function creates a new array spread of TiptapExtensions on every call, which could impact performance for large articles.

+const tiptapExtensions = [...TiptapExtensions];+
const renderSanitizedTiptapContent = (jsonContent: JSONContent) => {
- const rawHtml = generateHTML(jsonContent, [...TiptapExtensions]);+ const rawHtml = generateHTML(jsonContent, tiptapExtensions);
// Sanitize the HTML
return DOMPurify.sanitize(rawHtml);
};
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constrenderSanitizedTiptapContent=(jsonContent: JSONContent)=>{
constrawHtml=generateHTML(jsonContent,[...TiptapExtensions]);
// Sanitize the HTML
returnDOMPurify.sanitize(rawHtml);
};
consttiptapExtensions=[...TiptapExtensions];
constrenderSanitizedTiptapContent=(jsonContent: JSONContent)=>{
constrawHtml=generateHTML(jsonContent,tiptapExtensions);
// Sanitize the HTML
returnDOMPurify.sanitize(rawHtml);
};


const ArticlePage = async ({ params }: Props) => {
const session = await getServerAuthSession();
const { slug } = params;
Expand All@@ -66,11 +85,24 @@ const ArticlePage = async ({ params }: Props) => {
const post = await getPost({ slug });

if (!post) {
notFound();
return notFound();
}

const ast = Markdoc.parse(post.body);
const content = Markdoc.transform(ast, config);
const parsedBody = parseJSON(post.body);
const isTiptapContent = parsedBody?.type === "doc";

let renderedContent: string | RenderableTreeNode;
Comment thread
dineshsutihar marked this conversation as resolved.

if (isTiptapContent && parsedBody) {
const jsonContent = parsedBody;
renderedContent = renderSanitizedTiptapContent(jsonContent);
} else {
const ast = Markdoc.parse(post.body);
const transformedContent = Markdoc.transform(ast, config);
renderedContent = Markdoc.renderers.react(transformedContent, React, {
components: markdocComponents,
}) as unknown as string;
Comment on lines +91 to +104

@coderabbitaicoderabbitaiBotOct 22, 2024

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

LGTM: Improved content type detection and rendering

The new logic effectively detects and renders both Tiptap and Markdoc content, aligning with the PR objectives.

Remove the unnecessary type assertion in the Markdoc rendering path:

- renderedContent = Markdoc.renderers.react(transformedContent, React, {- components: markdocComponents,- }) as unknown as string;+ renderedContent = Markdoc.renderers.react(transformedContent, React, {+ components: markdocComponents,+ });

This change eliminates the incorrect casting of a React element to string, improving type safety.

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constparsedBody=parseJSON(post.body);
constisTiptapContent=parsedBody?.type==="doc";
letrenderedContent: string|RenderableTreeNode;
if(isTiptapContent&&parsedBody){
constjsonContent=parsedBody;
renderedContent=renderSanitizedTiptapContent(jsonContent);
}else{
constast=Markdoc.parse(post.body);
consttransformedContent=Markdoc.transform(ast,config);
renderedContent=Markdoc.renderers.react(transformedContent,React,{
components: markdocComponents,
})as unknown asstring;
constparsedBody=parseJSON(post.body);
constisTiptapContent=parsedBody?.type==="doc";
letrenderedContent: string|RenderableTreeNode;
if(isTiptapContent&&parsedBody){
constjsonContent=parsedBody;
renderedContent=renderSanitizedTiptapContent(jsonContent);
}else{
constast=Markdoc.parse(post.body);
consttransformedContent=Markdoc.transform(ast,config);
renderedContent=Markdoc.renderers.react(transformedContent,React,{
components: markdocComponents,
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep the type assertion because renderedContent needs to be treated as a string for use in dangerouslySetInnerHTML, and removing it could lead to runtime issues.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dineshsutihar, understood. Thanks for the clarification. Keeping the type assertion ensures correct behavior when using dangerouslySetInnerHTML.

🐰🤔


✏️ Learnings added
Learnt from: dineshsutihar
PR: codu-code/codu#1153
File: app/(app)/articles/[slug]/page.tsx:91-104
Timestamp: 2024-10-22T08:43:13.045Z
Learning: In `app/(app)/articles/[slug]/page.tsx`, when rendering content, the `renderedContent` needs to be treated as a string using a type assertion because it's used with `dangerouslySetInnerHTML`, and removing the type assertion could lead to runtime issues.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

}

return (
<>
Expand All@@ -83,10 +115,16 @@ const ArticlePage = async ({ params }: Props) => {
/>
<div className="mx-auto break-words px-2 pb-4 sm:px-4 md:max-w-3xl">
<article className="prose mx-auto max-w-3xl dark:prose-invert lg:prose-lg">
<h1>{post.title}</h1>
{Markdoc.renderers.react(content, React, {
components: markdocComponents,
})}
{!isTiptapContent && <h1>{post.title}</h1>}

{isTiptapContent ? (
<div
dangerouslySetInnerHTML={{ __html: renderedContent }}
Comment thread
dineshsutihar marked this conversation as resolved.
className="tiptap-content"
/>
) : (
<div>{renderedContent}</div>
)}
</article>
{post.tags.length > 0 && (
<section className="flex flex-wrap gap-3">
Expand Down
3 changes: 2 additions & 1 deletion components/editor/editor/RenderPost.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import React from "react";
import { TiptapExtensions } from "./extensions";
import { EditorContent, useEditor } from "@tiptap/react";
import SlashCommand from "./extensions/slash-command";

interface RenderPostProps {
json: string;
Expand All@@ -11,7 +12,7 @@ const RenderPost = ({ json }: RenderPostProps) => {

const editor = useEditor({
editable: false,
extensions: [...TiptapExtensions],
extensions: [...TiptapExtensions, SlashCommand],
content,
});

Expand Down
1 change: 0 additions & 1 deletion components/editor/editor/extensions/index.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,7 +138,6 @@ export const TiptapExtensions = [
return "type / to see a list of formatting features";
},
}),
SlashCommand,
TextStyle,
Link.configure({
HTMLAttributes: {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
111 changes: 49 additions & 62 deletions app/(app)/alpha/new/[[...postIdArr]]/_client.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,6 @@ import {
} from "@headlessui/react";
import { ChevronUpIcon } from "@heroicons/react/20/solid";
import Editor from "@/components/editor/editor";
import RenderPost from "@/components/editor/editor/RenderPost";
import useCreatePage from "@/hooks/useCreatePage";
import { usePrompt } from "@/components/PromptService";

Expand DownExpand Up@@ -214,69 +213,57 @@ const Create = () => {
<div className="z-60 absolute bottom-0 left-0 right-0 top-0 bg-black opacity-25" />
</div>
)}
<div className="bg-black">
<div className="mx-auto w-full max-w-7xl flex-grow text-black lg:flex xl:px-8">
{/* Left sidebar & main wrapper */}
<div className="min-w-0 flex-1 xl:flex">
<div className="lg:min-w-0 lg:flex-1">
<div className="h-full min-h-[40rem] px-4 py-0 sm:px-6 lg:px-4 lg:py-6">
{/* Start main area*/}
<div className="relative h-full">
<div className="bg-neutral-900 text-white shadow-xl">
<div className="bg-neutral-900 px-4 py-6 sm:p-6 lg:pb-8">
{!body && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={"{}"} />
)}
/>
)}
{body && body.length > 0 && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={body} />
)}
/>
)}
<div className="mx-auto break-words px-2 pb-4 sm:px-4 md:max-w-3xl">
<div className="prose mx-auto max-w-3xl dark:prose-invert lg:prose-lg">
{!body && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={"{}"} />
)}
/>
)}
{body && body.length > 0 && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={body} />
)}
/>
)}

<div className="flex items-center justify-between">
<>
{saveStatus === "loading" && <p>Auto-saving...</p>}
{saveStatus === "error" && savedTime && (
<p className="text-xs text-red-600 lg:text-sm">
{`Error saving, last saved: ${savedTime.toString()}`}
</p>
)}
{saveStatus === "success" && savedTime && (
<p className="text-xs text-neutral-400 lg:text-sm">
{`Saved: ${savedTime.toString()}`}
</p>
)}
</>
<div />

<div className="flex">
<button
type="button"
disabled={isDisabled}
className="ml-5 inline-flex justify-center bg-gradient-to-r from-orange-400 to-pink-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:from-orange-300 hover:to-pink-500 focus:outline-none focus:ring-2 focus:ring-pink-300 focus:ring-offset-2 disabled:opacity-50"
onClick={() => setOpen(true)}
>
{!data?.published && "Publish"}
{data?.published && "Save Changes"}
</button>
</div>
</div>
</div>
</div>
</div>
{/* End main area */}
</div>
<div className="flex items-center justify-between">
<div>
{saveStatus === "loading" && (
<p className="text-xs lg:text-sm">Auto-saving...</p>
)}
{saveStatus === "error" && savedTime && (
<p className="text-xs text-red-600 lg:text-sm">
Error saving, last saved: {savedTime.toString()}
</p>
)}
{saveStatus === "success" && savedTime && (
<p className="text-xs text-neutral-400 lg:text-sm">
Saved: {savedTime.toString()}
</p>
)}
</div>

<button
type="button"
disabled={isDisabled}
className="ml-5 inline-flex justify-center bg-gradient-to-r from-orange-400 to-pink-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:from-orange-300 hover:to-pink-500 focus:outline-none focus:ring-2 focus:ring-pink-300 focus:ring-offset-2 disabled:opacity-50"
onClick={() => setOpen(true)}
aria-label={
data?.published
? "Save changes to document"
: "Publish document"
}
>
{data?.published ? "Save Changes" : "Publish"}
</button>
</div>
</div>
</div>
Expand Down
52 changes: 45 additions & 7 deletions app/(app)/articles/[slug]/page.tsx
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import React from "react";
import type { RenderableTreeNode } from "@markdoc/markdoc";
import Markdoc from "@markdoc/markdoc";
import Link from "next/link";
import BioBar from "@/components/BioBar/BioBar";
Expand All@@ -13,6 +14,10 @@ import ArticleAdminPanel from "@/components/ArticleAdminPanel/ArticleAdminPanel"
import { type Metadata } from "next";
import { getPost } from "@/server/lib/posts";
import { getCamelCaseFromLower } from "@/utils/utils";
import { generateHTML } from "@tiptap/html";
import { TiptapExtensions } from "@/components/editor/editor/extensions";
import DOMPurify from "isomorphic-dompurify";
import type { JSONContent } from "@tiptap/core";

type Props = { params: { slug: string } };

Expand DownExpand Up@@ -57,6 +62,20 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
};
}

const parseJSON = (str: string): JSONContent | null => {
try {
return JSON.parse(str);
} catch (e) {
return null;
}
};

const renderSanitizedTiptapContent = (jsonContent: JSONContent) => {
const rawHtml = generateHTML(jsonContent, [...TiptapExtensions]);
// Sanitize the HTML
return DOMPurify.sanitize(rawHtml);
};
Comment on lines +73 to +77

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider memoizing the TiptapExtensions array

The renderSanitizedTiptapContent function creates a new array spread of TiptapExtensions on every call, which could impact performance for large articles.

+const tiptapExtensions = [...TiptapExtensions];+
const renderSanitizedTiptapContent = (jsonContent: JSONContent) => {
- const rawHtml = generateHTML(jsonContent, [...TiptapExtensions]);+ const rawHtml = generateHTML(jsonContent, tiptapExtensions);
// Sanitize the HTML
return DOMPurify.sanitize(rawHtml);
};
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constrenderSanitizedTiptapContent=(jsonContent: JSONContent)=>{
constrawHtml=generateHTML(jsonContent,[...TiptapExtensions]);
// Sanitize the HTML
returnDOMPurify.sanitize(rawHtml);
};
consttiptapExtensions=[...TiptapExtensions];
constrenderSanitizedTiptapContent=(jsonContent: JSONContent)=>{
constrawHtml=generateHTML(jsonContent,tiptapExtensions);
// Sanitize the HTML
returnDOMPurify.sanitize(rawHtml);
};


const ArticlePage = async ({ params }: Props) => {
const session = await getServerAuthSession();
const { slug } = params;
Expand All@@ -66,11 +85,24 @@ const ArticlePage = async ({ params }: Props) => {
const post = await getPost({ slug });

if (!post) {
notFound();
return notFound();
}

const ast = Markdoc.parse(post.body);
const content = Markdoc.transform(ast, config);
const parsedBody = parseJSON(post.body);
const isTiptapContent = parsedBody?.type === "doc";

let renderedContent: string | RenderableTreeNode;
Comment thread
dineshsutihar marked this conversation as resolved.

if (isTiptapContent && parsedBody) {
const jsonContent = parsedBody;
renderedContent = renderSanitizedTiptapContent(jsonContent);
} else {
const ast = Markdoc.parse(post.body);
const transformedContent = Markdoc.transform(ast, config);
renderedContent = Markdoc.renderers.react(transformedContent, React, {
components: markdocComponents,
}) as unknown as string;
Comment on lines +91 to +104

@coderabbitaicoderabbitaiBotOct 22, 2024

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

LGTM: Improved content type detection and rendering

The new logic effectively detects and renders both Tiptap and Markdoc content, aligning with the PR objectives.

Remove the unnecessary type assertion in the Markdoc rendering path:

- renderedContent = Markdoc.renderers.react(transformedContent, React, {- components: markdocComponents,- }) as unknown as string;+ renderedContent = Markdoc.renderers.react(transformedContent, React, {+ components: markdocComponents,+ });

This change eliminates the incorrect casting of a React element to string, improving type safety.

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constparsedBody=parseJSON(post.body);
constisTiptapContent=parsedBody?.type==="doc";
letrenderedContent: string|RenderableTreeNode;
if(isTiptapContent&&parsedBody){
constjsonContent=parsedBody;
renderedContent=renderSanitizedTiptapContent(jsonContent);
}else{
constast=Markdoc.parse(post.body);
consttransformedContent=Markdoc.transform(ast,config);
renderedContent=Markdoc.renderers.react(transformedContent,React,{
components: markdocComponents,
})as unknown asstring;
constparsedBody=parseJSON(post.body);
constisTiptapContent=parsedBody?.type==="doc";
letrenderedContent: string|RenderableTreeNode;
if(isTiptapContent&&parsedBody){
constjsonContent=parsedBody;
renderedContent=renderSanitizedTiptapContent(jsonContent);
}else{
constast=Markdoc.parse(post.body);
consttransformedContent=Markdoc.transform(ast,config);
renderedContent=Markdoc.renderers.react(transformedContent,React,{
components: markdocComponents,
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep the type assertion because renderedContent needs to be treated as a string for use in dangerouslySetInnerHTML, and removing it could lead to runtime issues.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dineshsutihar, understood. Thanks for the clarification. Keeping the type assertion ensures correct behavior when using dangerouslySetInnerHTML.

🐰🤔


✏️ Learnings added
Learnt from: dineshsutihar
PR: codu-code/codu#1153
File: app/(app)/articles/[slug]/page.tsx:91-104
Timestamp: 2024-10-22T08:43:13.045Z
Learning: In `app/(app)/articles/[slug]/page.tsx`, when rendering content, the `renderedContent` needs to be treated as a string using a type assertion because it's used with `dangerouslySetInnerHTML`, and removing the type assertion could lead to runtime issues.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

}

return (
<>
Expand All@@ -83,10 +115,16 @@ const ArticlePage = async ({ params }: Props) => {
/>
<div className="mx-auto break-words px-2 pb-4 sm:px-4 md:max-w-3xl">
<article className="prose mx-auto max-w-3xl dark:prose-invert lg:prose-lg">
<h1>{post.title}</h1>
{Markdoc.renderers.react(content, React, {
components: markdocComponents,
})}
{!isTiptapContent && <h1>{post.title}</h1>}

{isTiptapContent ? (
<div
dangerouslySetInnerHTML={{ __html: renderedContent }}
Comment thread
dineshsutihar marked this conversation as resolved.
className="tiptap-content"
/>
) : (
<div>{renderedContent}</div>
)}
</article>
{post.tags.length > 0 && (
<section className="flex flex-wrap gap-3">
Expand Down
3 changes: 2 additions & 1 deletion components/editor/editor/RenderPost.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import React from "react";
import { TiptapExtensions } from "./extensions";
import { EditorContent, useEditor } from "@tiptap/react";
import SlashCommand from "./extensions/slash-command";

interface RenderPostProps {
json: string;
Expand All@@ -11,7 +12,7 @@ const RenderPost = ({ json }: RenderPostProps) => {

const editor = useEditor({
editable: false,
extensions: [...TiptapExtensions],
extensions: [...TiptapExtensions, SlashCommand],
content,
});

Expand Down
1 change: 0 additions & 1 deletion components/editor/editor/extensions/index.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,7 +138,6 @@ export const TiptapExtensions = [
return "type / to see a list of formatting features";
},
}),
SlashCommand,
TextStyle,
Link.configure({
HTMLAttributes: {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
111 changes: 49 additions & 62 deletions app/(app)/alpha/new/[[...postIdArr]]/_client.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,6 @@ import {
} from "@headlessui/react";
import { ChevronUpIcon } from "@heroicons/react/20/solid";
import Editor from "@/components/editor/editor";
import RenderPost from "@/components/editor/editor/RenderPost";
import useCreatePage from "@/hooks/useCreatePage";
import { usePrompt } from "@/components/PromptService";

Expand DownExpand Up@@ -214,69 +213,57 @@ const Create = () => {
<div className="z-60 absolute bottom-0 left-0 right-0 top-0 bg-black opacity-25" />
</div>
)}
<div className="bg-black">
<div className="mx-auto w-full max-w-7xl flex-grow text-black lg:flex xl:px-8">
{/* Left sidebar & main wrapper */}
<div className="min-w-0 flex-1 xl:flex">
<div className="lg:min-w-0 lg:flex-1">
<div className="h-full min-h-[40rem] px-4 py-0 sm:px-6 lg:px-4 lg:py-6">
{/* Start main area*/}
<div className="relative h-full">
<div className="bg-neutral-900 text-white shadow-xl">
<div className="bg-neutral-900 px-4 py-6 sm:p-6 lg:pb-8">
{!body && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={"{}"} />
)}
/>
)}
{body && body.length > 0 && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={body} />
)}
/>
)}
<div className="mx-auto break-words px-2 pb-4 sm:px-4 md:max-w-3xl">
<div className="prose mx-auto max-w-3xl dark:prose-invert lg:prose-lg">
{!body && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={"{}"} />
)}
/>
)}
{body && body.length > 0 && (
<Controller
name="body"
control={control}
render={({ field }) => (
<Editor {...field} initialValue={body} />
)}
/>
)}

<div className="flex items-center justify-between">
<>
{saveStatus === "loading" && <p>Auto-saving...</p>}
{saveStatus === "error" && savedTime && (
<p className="text-xs text-red-600 lg:text-sm">
{`Error saving, last saved: ${savedTime.toString()}`}
</p>
)}
{saveStatus === "success" && savedTime && (
<p className="text-xs text-neutral-400 lg:text-sm">
{`Saved: ${savedTime.toString()}`}
</p>
)}
</>
<div />

<div className="flex">
<button
type="button"
disabled={isDisabled}
className="ml-5 inline-flex justify-center bg-gradient-to-r from-orange-400 to-pink-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:from-orange-300 hover:to-pink-500 focus:outline-none focus:ring-2 focus:ring-pink-300 focus:ring-offset-2 disabled:opacity-50"
onClick={() => setOpen(true)}
>
{!data?.published && "Publish"}
{data?.published && "Save Changes"}
</button>
</div>
</div>
</div>
</div>
</div>
{/* End main area */}
</div>
<div className="flex items-center justify-between">
<div>
{saveStatus === "loading" && (
<p className="text-xs lg:text-sm">Auto-saving...</p>
)}
{saveStatus === "error" && savedTime && (
<p className="text-xs text-red-600 lg:text-sm">
Error saving, last saved: {savedTime.toString()}
</p>
)}
{saveStatus === "success" && savedTime && (
<p className="text-xs text-neutral-400 lg:text-sm">
Saved: {savedTime.toString()}
</p>
)}
</div>

<button
type="button"
disabled={isDisabled}
className="ml-5 inline-flex justify-center bg-gradient-to-r from-orange-400 to-pink-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:from-orange-300 hover:to-pink-500 focus:outline-none focus:ring-2 focus:ring-pink-300 focus:ring-offset-2 disabled:opacity-50"
onClick={() => setOpen(true)}
aria-label={
data?.published
? "Save changes to document"
: "Publish document"
}
>
{data?.published ? "Save Changes" : "Publish"}
</button>
</div>
</div>
</div>
Expand Down
52 changes: 45 additions & 7 deletions app/(app)/articles/[slug]/page.tsx
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import React from "react";
import type { RenderableTreeNode } from "@markdoc/markdoc";
import Markdoc from "@markdoc/markdoc";
import Link from "next/link";
import BioBar from "@/components/BioBar/BioBar";
Expand All@@ -13,6 +14,10 @@ import ArticleAdminPanel from "@/components/ArticleAdminPanel/ArticleAdminPanel"
import { type Metadata } from "next";
import { getPost } from "@/server/lib/posts";
import { getCamelCaseFromLower } from "@/utils/utils";
import { generateHTML } from "@tiptap/html";
import { TiptapExtensions } from "@/components/editor/editor/extensions";
import DOMPurify from "isomorphic-dompurify";
import type { JSONContent } from "@tiptap/core";

type Props = { params: { slug: string } };

Expand DownExpand Up@@ -57,6 +62,20 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
};
}

const parseJSON = (str: string): JSONContent | null => {
try {
return JSON.parse(str);
} catch (e) {
return null;
}
};

const renderSanitizedTiptapContent = (jsonContent: JSONContent) => {
const rawHtml = generateHTML(jsonContent, [...TiptapExtensions]);
// Sanitize the HTML
return DOMPurify.sanitize(rawHtml);
};
Comment on lines +73 to +77

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider memoizing the TiptapExtensions array

The renderSanitizedTiptapContent function creates a new array spread of TiptapExtensions on every call, which could impact performance for large articles.

+const tiptapExtensions = [...TiptapExtensions];+
const renderSanitizedTiptapContent = (jsonContent: JSONContent) => {
- const rawHtml = generateHTML(jsonContent, [...TiptapExtensions]);+ const rawHtml = generateHTML(jsonContent, tiptapExtensions);
// Sanitize the HTML
return DOMPurify.sanitize(rawHtml);
};
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constrenderSanitizedTiptapContent=(jsonContent: JSONContent)=>{
constrawHtml=generateHTML(jsonContent,[...TiptapExtensions]);
// Sanitize the HTML
returnDOMPurify.sanitize(rawHtml);
};
consttiptapExtensions=[...TiptapExtensions];
constrenderSanitizedTiptapContent=(jsonContent: JSONContent)=>{
constrawHtml=generateHTML(jsonContent,tiptapExtensions);
// Sanitize the HTML
returnDOMPurify.sanitize(rawHtml);
};


const ArticlePage = async ({ params }: Props) => {
const session = await getServerAuthSession();
const { slug } = params;
Expand All@@ -66,11 +85,24 @@ const ArticlePage = async ({ params }: Props) => {
const post = await getPost({ slug });

if (!post) {
notFound();
return notFound();
}

const ast = Markdoc.parse(post.body);
const content = Markdoc.transform(ast, config);
const parsedBody = parseJSON(post.body);
const isTiptapContent = parsedBody?.type === "doc";

let renderedContent: string | RenderableTreeNode;
Comment thread
dineshsutihar marked this conversation as resolved.

if (isTiptapContent && parsedBody) {
const jsonContent = parsedBody;
renderedContent = renderSanitizedTiptapContent(jsonContent);
} else {
const ast = Markdoc.parse(post.body);
const transformedContent = Markdoc.transform(ast, config);
renderedContent = Markdoc.renderers.react(transformedContent, React, {
components: markdocComponents,
}) as unknown as string;
Comment on lines +91 to +104

@coderabbitaicoderabbitaiBotOct 22, 2024

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

LGTM: Improved content type detection and rendering

The new logic effectively detects and renders both Tiptap and Markdoc content, aligning with the PR objectives.

Remove the unnecessary type assertion in the Markdoc rendering path:

- renderedContent = Markdoc.renderers.react(transformedContent, React, {- components: markdocComponents,- }) as unknown as string;+ renderedContent = Markdoc.renderers.react(transformedContent, React, {+ components: markdocComponents,+ });

This change eliminates the incorrect casting of a React element to string, improving type safety.

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constparsedBody=parseJSON(post.body);
constisTiptapContent=parsedBody?.type==="doc";
letrenderedContent: string|RenderableTreeNode;
if(isTiptapContent&&parsedBody){
constjsonContent=parsedBody;
renderedContent=renderSanitizedTiptapContent(jsonContent);
}else{
constast=Markdoc.parse(post.body);
consttransformedContent=Markdoc.transform(ast,config);
renderedContent=Markdoc.renderers.react(transformedContent,React,{
components: markdocComponents,
})as unknown asstring;
constparsedBody=parseJSON(post.body);
constisTiptapContent=parsedBody?.type==="doc";
letrenderedContent: string|RenderableTreeNode;
if(isTiptapContent&&parsedBody){
constjsonContent=parsedBody;
renderedContent=renderSanitizedTiptapContent(jsonContent);
}else{
constast=Markdoc.parse(post.body);
consttransformedContent=Markdoc.transform(ast,config);
renderedContent=Markdoc.renderers.react(transformedContent,React,{
components: markdocComponents,
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep the type assertion because renderedContent needs to be treated as a string for use in dangerouslySetInnerHTML, and removing it could lead to runtime issues.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dineshsutihar, understood. Thanks for the clarification. Keeping the type assertion ensures correct behavior when using dangerouslySetInnerHTML.

🐰🤔


✏️ Learnings added
Learnt from: dineshsutihar
PR: codu-code/codu#1153
File: app/(app)/articles/[slug]/page.tsx:91-104
Timestamp: 2024-10-22T08:43:13.045Z
Learning: In `app/(app)/articles/[slug]/page.tsx`, when rendering content, the `renderedContent` needs to be treated as a string using a type assertion because it's used with `dangerouslySetInnerHTML`, and removing the type assertion could lead to runtime issues.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

}

return (
<>
Expand All@@ -83,10 +115,16 @@ const ArticlePage = async ({ params }: Props) => {
/>
<div className="mx-auto break-words px-2 pb-4 sm:px-4 md:max-w-3xl">
<article className="prose mx-auto max-w-3xl dark:prose-invert lg:prose-lg">
<h1>{post.title}</h1>
{Markdoc.renderers.react(content, React, {
components: markdocComponents,
})}
{!isTiptapContent && <h1>{post.title}</h1>}

{isTiptapContent ? (
<div
dangerouslySetInnerHTML={{ __html: renderedContent }}
Comment thread
dineshsutihar marked this conversation as resolved.
className="tiptap-content"
/>
) : (
<div>{renderedContent}</div>
)}
</article>
{post.tags.length > 0 && (
<section className="flex flex-wrap gap-3">
Expand Down
3 changes: 2 additions & 1 deletion components/editor/editor/RenderPost.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import React from "react";
import { TiptapExtensions } from "./extensions";
import { EditorContent, useEditor } from "@tiptap/react";
import SlashCommand from "./extensions/slash-command";

interface RenderPostProps {
json: string;
Expand All@@ -11,7 +12,7 @@ const RenderPost = ({ json }: RenderPostProps) => {

const editor = useEditor({
editable: false,
extensions: [...TiptapExtensions],
extensions: [...TiptapExtensions, SlashCommand],
content,
});

Expand Down
1 change: 0 additions & 1 deletion components/editor/editor/extensions/index.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -138,7 +138,6 @@ export const TiptapExtensions = [
return "type / to see a list of formatting features";
},
}),
SlashCommand,
TextStyle,
Link.configure({
HTMLAttributes: {
Expand Down
Loading