From 0e8dd9ef4aee5caf58373547765821bd7a945802 Mon Sep 17 00:00:00 2001
From: Shawky Ebrahim Ahmed
<101745968+shawkyebrahim2514@users.noreply.github.com>
Date: Mon, 25 Nov 2024 13:47:25 +0200
Subject: [PATCH] Supporting links with Markdown Syntax
---
react-frontend/README.md | 37 +++++++++
.../HTMLMarkdown/AncherLinkMarkdown.tsx | 35 ++++++++
.../components/HTMLMarkdown/SpanMarkdown.tsx | 81 +++++++++++++++----
.../src/components/HTMLMarkdown/index.tsx | 2 +
.../src/components/MainSection/Header.tsx | 4 +-
5 files changed, 142 insertions(+), 17 deletions(-)
create mode 100644 react-frontend/src/components/HTMLMarkdown/AncherLinkMarkdown.tsx
diff --git a/react-frontend/README.md b/react-frontend/README.md
index 3314312..eb162bd 100644
--- a/react-frontend/README.md
+++ b/react-frontend/README.md
@@ -76,6 +76,43 @@ Into
```
+> Links => Normal markdown notations
+
+- The normal syntax `[Text](Link)` will be rendered as a text with an icon bext to it
+- The syntax `[[Text]](Link)` will be rendered as a lnik button with the text inside it
+
+```markdown
+
+[Google](https://google.com)
+
+---
+
+[[Google]](https://google.com)
+
+```
+
+Into
+
+```html
+
+
+
+
Google
+
+
+
+
+
+
+
+
+
+ Google
+
+
+
+```
+
> Using Markdown Notations: Highlight Text
- Use the syntax `**Text Here**` to make the text bold and with the base color
diff --git a/react-frontend/src/components/HTMLMarkdown/AncherLinkMarkdown.tsx b/react-frontend/src/components/HTMLMarkdown/AncherLinkMarkdown.tsx
new file mode 100644
index 0000000..c5cb261
--- /dev/null
+++ b/react-frontend/src/components/HTMLMarkdown/AncherLinkMarkdown.tsx
@@ -0,0 +1,35 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import Button from '../Button';
+import Header from '../MainSection/Header';
+import type { Element } from 'hast'
+import { faLink } from '@fortawesome/free-solid-svg-icons';
+
+type AncherLinkMarkdownProps = {
+ node?: Element,
+} & React.HTMLAttributes;
+
+const AncherLinkMarkdown = ({ node, ...props }: AncherLinkMarkdownProps) => {
+ console.log("Title: ", props.children);
+ console.log("Link: ", node?.properties?.href);
+ const title = props.children as string;
+ const link = node?.properties?.href as string;
+ const matchButtonLink = /\[(.*)\]/.exec(title);
+ if (matchButtonLink) {
+ return (
+ }
+ text={matchButtonLink[1]}
+ size='md'
+ onClick={() => { window.open(link, "_blank") }}
+ pointer={true}
+ />
+ )
+ }
+ return (
+
+ )
+}
+
+export default AncherLinkMarkdown
\ No newline at end of file
diff --git a/react-frontend/src/components/HTMLMarkdown/SpanMarkdown.tsx b/react-frontend/src/components/HTMLMarkdown/SpanMarkdown.tsx
index 3881b4e..831e0ac 100644
--- a/react-frontend/src/components/HTMLMarkdown/SpanMarkdown.tsx
+++ b/react-frontend/src/components/HTMLMarkdown/SpanMarkdown.tsx
@@ -1,8 +1,11 @@
import { CSSProperties, useMemo } from 'react';
import { useThemeContext } from '../../contexts/ThemeContext';
-import type { Element, Text } from 'hast'
+import type { Element, Text, Node, RootContent } from 'hast'
import { visit } from 'unist-util-visit';
import { v4 as uuidv4 } from 'uuid';
+import { Fragment, jsx, jsxs } from 'react/jsx-runtime'
+import { markdownComponents } from '.';
+import { toJsxRuntime } from 'hast-util-to-jsx-runtime'
type SpanElementType = "highlight-area" | "highlight-text";
type SpanColorType = "base" | "secondary";
@@ -12,31 +15,79 @@ type SpanMarkdownProps = {
node?: Element,
} & React.HTMLAttributes;
+const isTextNode = (node: Node): node is Text => {
+ return node?.type === 'text';
+};
+
const SpanMarkdown = ({ node, ...props }: SpanMarkdownProps) => {
const { theme } = useThemeContext();
let className = (() => props?.className?.split(' ') || [])();
- let spanText = (() => {
- let text = '';
- visit(node as Element, 'text', (textNode: Text) => {
- text = textNode.value;
- const matchHighlightAreaWithSecondaryColor = text.match(/^!-(.*?)-!/g);
- const matchHighlightAreaWithBaseColor = text.match(/^-(.*?)-/g);
- const matchHighlightTextWithSecondaryColor = text.match(/^!(.*?)!/g);
+ let content = (() => {
+ let currentNodes: Node[] = node?.children || [];
+ // No children element found, only a text node
+ if (currentNodes.length === 1 && isTextNode(currentNodes[0])) {
+ let text = currentNodes[0].value;
+ visit(node as Element, 'text', (textNode: Text) => {
+ text = textNode.value;
+ // console.log("text: ", text);
+ const matchHighlightAreaWithSecondaryColor = text.match(/^!-(.*?)-!/g);
+ const matchHighlightAreaWithBaseColor = text.match(/^-(.*?)-/g);
+ const matchHighlightTextWithSecondaryColor = text.match(/^!(.*?)!/g);
+ if (matchHighlightAreaWithSecondaryColor) {
+ className.push("highlight-area");
+ className.push("secondary");
+ text = text.substring(2, matchHighlightAreaWithSecondaryColor[0].length - 2);
+ } else if (matchHighlightAreaWithBaseColor) {
+ className.push("highlight-area");
+ text = text.substring(1, matchHighlightAreaWithBaseColor[0].length - 1);
+ } else if (matchHighlightTextWithSecondaryColor) {
+ className.push("secondary");
+ text = text.substring(1, matchHighlightTextWithSecondaryColor[0].length - 1);
+ }
+ });
+ return text;
+ }
+ let firstNodeChild = currentNodes[0];
+ let lastNodeChild = currentNodes[currentNodes.length - 1];
+ if (isTextNode(firstNodeChild) && isTextNode(lastNodeChild) &&
+ firstNodeChild.value === lastNodeChild.value.split('').reverse().join('')) {
+ let text = firstNodeChild.value;
+ const matchHighlightAreaWithSecondaryColor = /^!-/.exec(text);
+ const matchHighlightAreaWithBaseColor = /^-/.exec(text);
+ const matchHighlightTextWithSecondaryColor = /^!/.exec(text);
if (matchHighlightAreaWithSecondaryColor) {
className.push("highlight-area");
className.push("secondary");
- text = text.substring(2, matchHighlightAreaWithSecondaryColor[0].length - 2);
} else if (matchHighlightAreaWithBaseColor) {
className.push("highlight-area");
- text = text.substring(1, matchHighlightAreaWithBaseColor[0].length - 1);
} else if (matchHighlightTextWithSecondaryColor) {
className.push("secondary");
- text = text.substring(1, matchHighlightTextWithSecondaryColor[0].length - 1);
}
- });
- return text;
+ console.log("text: ", text);
+ console.log("className: ", className);
+ console.log("matchHighlightAreaWithSecondaryColor: ", matchHighlightAreaWithSecondaryColor);
+ console.log("matchHighlightAreaWithBaseColor: ", matchHighlightAreaWithBaseColor);
+ console.log("matchHighlightTextWithSecondaryColor: ", matchHighlightTextWithSecondaryColor);
+
+ // Delete the first and last node
+ currentNodes.shift();
+ currentNodes.pop();
+ }
+ return currentNodes;
})();
+ const contentJSXElementsFromAST = useMemo(() => (
+ typeof content === 'string' ? content :
+ content.map((element) => toJsxRuntime(element as RootContent, {
+ Fragment, jsx, jsxs, passNode: true, components: {
+ ...markdownComponents,
+ br: () => null,
+ }
+ }))
+ ), [content]);
+
+ // console.log("Content: ", content);
+
const textStyle = useMemo((): CSSProperties => {
return {
position: "relative",
@@ -125,8 +176,8 @@ const SpanMarkdown = ({ node, ...props }: SpanMarkdownProps) => {
return className?.includes("secondary") ? "secondary" : "base";
}, [className]);
const { style, children } = useMemo((): { style: CSSProperties, children: React.ReactNode } => {
- return targetElement[spanElement][colorType](props.style ?? {}, spanText);
- }, [colorType, spanText, props.style, spanElement, targetElement]);
+ return targetElement[spanElement][colorType](props.style ?? {}, contentJSXElementsFromAST);
+ }, [colorType, contentJSXElementsFromAST, props.style, spanElement, targetElement]);
return (
diff --git a/react-frontend/src/components/HTMLMarkdown/index.tsx b/react-frontend/src/components/HTMLMarkdown/index.tsx
index 9224be0..8d5179f 100644
--- a/react-frontend/src/components/HTMLMarkdown/index.tsx
+++ b/react-frontend/src/components/HTMLMarkdown/index.tsx
@@ -9,6 +9,7 @@ import LiMarkdown from './LiMarkdown';
import HeadingMarkdown from './HeadingMarkdown';
import SpanMarkdown from './SpanMarkdown';
import BlockquoteMarkdown from './BlockquoteMarkdown';
+import AncherLinkMarkdown from './AncherLinkMarkdown';
type Props = {
readonly markdown: string
@@ -27,6 +28,7 @@ export const markdownComponents : Components = {
span: ({ node, ...props }) => ,
strong: ({ node, ...props }) => ,
blockquote: ({ node, ...props }) => ,
+ a: ({ node, ...props }) => ,
};
function HTMLMarkdown({ markdown }: Props) {
diff --git a/react-frontend/src/components/MainSection/Header.tsx b/react-frontend/src/components/MainSection/Header.tsx
index 0074be9..641695e 100644
--- a/react-frontend/src/components/MainSection/Header.tsx
+++ b/react-frontend/src/components/MainSection/Header.tsx
@@ -35,7 +35,7 @@ export default function Header({ title, link, subtitle }: HeaderProps) {
}, [link]);
return (
-
+ <>