Skip to content

Commit

Permalink
Supporting links with Markdown Syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
shawkyebrahim2514 committed Nov 25, 2024
1 parent cc104bf commit 0e8dd9e
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 17 deletions.
37 changes: 37 additions & 0 deletions react-frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

<p>
<div style="display: inline-block; align-items: center; gap: 0.5rem;">
<h3 tabindex="0" style="display: inline; margin-right: 0.5rem; cursor: pointer;">Google</h3>
<h3 tabindex="0" style="display: inline-block; cursor: pointer;">
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="arrow-up-right-from-square" class="svg-inline--fa fa-arrow-up-right-from-square fa-sm " role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M320 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l82.7 0L201.4 265.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L448 109.3l0 82.7c0 17.7 14.3 32 32 32s32-14.3 32-32l0-160c0-17.7-14.3-32-32-32L320 0zM80 32C35.8 32 0 67.8 0 112L0 432c0 44.2 35.8 80 80 80l320 0c44.2 0 80-35.8 80-80l0-112c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 112c0 8.8-7.2 16-16 16L80 448c-8.8 0-16-7.2-16-16l0-320c0-8.8 7.2-16 16-16l112 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L80 32z"></path></svg>
</h3>
</div>
</p>

<p>
<div class="css-1n6xck9">
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="link" class="svg-inline--fa fa-link " role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z"></path></svg>
Google
</div>
</p>

```

> Using Markdown Notations: Highlight Text
- Use the syntax `**Text Here**` to make the text bold and with the base color
Expand Down
35 changes: 35 additions & 0 deletions react-frontend/src/components/HTMLMarkdown/AncherLinkMarkdown.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLAnchorElement>;

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 (
<Button
icon={<FontAwesomeIcon icon={faLink} />}
text={matchButtonLink[1]}
size='md'
onClick={() => { window.open(link, "_blank") }}
pointer={true}
/>
)
}
return (
<Header
title={title}
link={link} />
)
}

export default AncherLinkMarkdown
81 changes: 66 additions & 15 deletions react-frontend/src/components/HTMLMarkdown/SpanMarkdown.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -12,31 +15,79 @@ type SpanMarkdownProps = {
node?: Element,
} & React.HTMLAttributes<HTMLSpanElement>;

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",
Expand Down Expand Up @@ -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 (
<span {...props} style={style}>
Expand Down
2 changes: 2 additions & 0 deletions react-frontend/src/components/HTMLMarkdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,6 +28,7 @@ export const markdownComponents : Components = {
span: ({ node, ...props }) => <SpanMarkdown node={node} {...props} />,
strong: ({ node, ...props }) => <SpanMarkdown node={node} {...props} />,
blockquote: ({ node, ...props }) => <BlockquoteMarkdown node={node} {...props} />,
a: ({ node, ...props }) => <AncherLinkMarkdown node={node} {...props} />,
};

function HTMLMarkdown({ markdown }: Props) {
Expand Down
4 changes: 2 additions & 2 deletions react-frontend/src/components/MainSection/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default function Header({ title, link, subtitle }: HeaderProps) {
}, [link]);

return (
<header>
<>
<div style={headerConatinerStyle}>
<Text
variant={"h3"}
Expand All @@ -60,7 +60,7 @@ export default function Header({ title, link, subtitle }: HeaderProps) {
{subtitle}
</SubtitleText>
}
</header>
</>
)
}

Expand Down

0 comments on commit 0e8dd9e

Please sign in to comment.