Skip to content

Commit 5ec4706

Browse files
committed
making the markodown more modular
1 parent a1ea8aa commit 5ec4706

File tree

13 files changed

+796
-686
lines changed

13 files changed

+796
-686
lines changed

components/StyledMarkdown.tsx

Lines changed: 4 additions & 686 deletions
Large diffs are not rendered by default.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import getFindResultsByGlobalRegExp from '~/lib/getFindResultsByGlobalRegExp';
2+
3+
const REGEX_TAB_GROUPS =
4+
/\[tabs-start\s*"(?<label>.*)"\]((?!\[tabs-start).|\n)*\[tabs-end\]/gm;
5+
6+
export type Element = {
7+
type: 'markdown' | 'tabs-group';
8+
markdown: string;
9+
};
10+
11+
export function transformMarkdownLinks(markdown: string): string {
12+
const linkDefinitions: Record<string, string> = {};
13+
14+
// Extract and remove link definitions
15+
markdown = markdown.replace(
16+
/^\[([^\]]+)\]:\s*(.+)$/gm,
17+
(_, key: string, value: string) => {
18+
linkDefinitions[key.toLowerCase()] = value;
19+
return '';
20+
},
21+
);
22+
23+
// Replace reference-style links with inline links
24+
return markdown.replace(
25+
/\[([^\]]+)\]\[([^\]]*)\]/g,
26+
(_, text: string, id: string) => {
27+
const link = linkDefinitions[id.toLowerCase()];
28+
if (link) {
29+
return `[${text}](${link})`;
30+
}
31+
return _; // Return the original string if no link is found
32+
},
33+
);
34+
}
35+
36+
export function parseMarkdown(markdown: string): Element[] {
37+
markdown = transformMarkdownLinks(markdown);
38+
39+
const sortedTabGroups = (
40+
getFindResultsByGlobalRegExp(markdown, REGEX_TAB_GROUPS) || []
41+
).sort((a, b) => (a.index < b.index ? -1 : 1));
42+
43+
let textCuts = sortedTabGroups.map((tabGroup) => ({
44+
index: tabGroup.index,
45+
length: tabGroup.match.length,
46+
}));
47+
48+
let elements: Element[] = [];
49+
let startIndex = 0;
50+
let sliceMore = true;
51+
52+
do {
53+
const endIndex = textCuts[0]?.index || Infinity;
54+
const length = endIndex - startIndex;
55+
const slicedMarkdown = markdown.substr(startIndex, length);
56+
if (slicedMarkdown.length > 0) {
57+
const markdownElement: Element = {
58+
type: 'markdown',
59+
markdown: slicedMarkdown,
60+
};
61+
elements = [...elements, markdownElement];
62+
}
63+
64+
if (textCuts[0]) {
65+
const tabsGroupElement: Element = {
66+
type: 'tabs-group',
67+
markdown: markdown.substr(textCuts[0].index, textCuts[0].length),
68+
};
69+
elements = [...elements, tabsGroupElement];
70+
startIndex = textCuts[0].index + textCuts[0].length;
71+
textCuts = textCuts.slice(1);
72+
} else {
73+
sliceMore = false;
74+
}
75+
} while (sliceMore);
76+
77+
return elements;
78+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React from 'react';
2+
import Markdown from 'markdown-to-jsx';
3+
import { FullMarkdownContext } from '~/context';
4+
import { textOverrides } from './overrides/TextElements';
5+
import { listOverrides } from './overrides/ListElements';
6+
import { tableOverrides } from './overrides/TableElements';
7+
import { codeOverrides } from './overrides/CodeElements';
8+
import { infoBoxOverrides } from './overrides/InfoBoxes';
9+
import { customOverrides } from './overrides/CustomElements';
10+
import { headlineOverrides } from './overrides/HeadlineElements';
11+
import { TableOfContentsWrapper } from './TableOfContents';
12+
13+
// Combine all overrides
14+
const allOverrides = {
15+
...headlineOverrides,
16+
...textOverrides,
17+
...listOverrides,
18+
...tableOverrides,
19+
...codeOverrides,
20+
...infoBoxOverrides,
21+
...customOverrides,
22+
tableofcontent: { component: TableOfContentsWrapper },
23+
};
24+
25+
export function MarkdownRenderer({ markdown }: { markdown: string }) {
26+
return (
27+
<FullMarkdownContext.Provider value={markdown}>
28+
<Markdown
29+
options={{
30+
forceBlock: true,
31+
overrides: allOverrides,
32+
}}
33+
>
34+
{markdown}
35+
</Markdown>
36+
</FullMarkdownContext.Provider>
37+
);
38+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import React, { useContext, useEffect, useState } from 'react';
2+
import Markdown from 'markdown-to-jsx';
3+
import Image from 'next/image';
4+
import slugifyMarkdownHeadline from '~/lib/slugifyMarkdownHeadline';
5+
import { FullMarkdownContext } from '~/context';
6+
7+
const hiddenElements = (...elements: string[]) => {
8+
return elements.reduce((acc, element) => {
9+
return {
10+
...acc,
11+
[element]: { component: () => null },
12+
};
13+
}, {});
14+
};
15+
16+
export function TableOfContents({
17+
markdown,
18+
depth = 2,
19+
}: {
20+
markdown: string;
21+
depth?: number;
22+
}) {
23+
return (
24+
<Markdown
25+
options={{
26+
overrides: {
27+
h1: {
28+
component: ({ children }) => {
29+
const slug = slugifyMarkdownHeadline(children);
30+
return (
31+
<a
32+
href={`#${slug}`}
33+
className='flex cursor-pointer mb-3 max-sm:text-sm text-slate-600 dark:text-slate-300 leading-6 font-medium'
34+
>
35+
{children}
36+
</a>
37+
);
38+
},
39+
},
40+
41+
/* eslint-disable */
42+
h2:
43+
depth === 0
44+
? {
45+
component: ({ children }) => {
46+
const slug = slugifyMarkdownHeadline(children);
47+
return (
48+
<a
49+
href={`#${slug}`}
50+
className='block cursor-pointer mb-3 text-slate-600 dark:text-slate-300 leading-5 font-medium ml-4'
51+
>
52+
{children}
53+
</a>
54+
);
55+
},
56+
}
57+
: depth >= 2
58+
? {
59+
component: ({ children }) => {
60+
const slug = slugifyMarkdownHeadline(children);
61+
const [isChrome, setIsChrome] = useState(false);
62+
63+
useEffect(() => {
64+
const chromeCheck =
65+
/Chrome/.test(navigator.userAgent) &&
66+
/Google Inc/.test(navigator.vendor);
67+
setIsChrome(chromeCheck);
68+
}, []);
69+
70+
return (
71+
// chromeClass
72+
<a
73+
href={`#${slug}`}
74+
className={`block cursor-pointer mb-3 max-sm:text-sm text-slate-600 dark:text-slate-300 leading-4 ] max-sm:-ml-[6px] font-medium ${isChrome ? '-ml-[4.8px]' : '-ml-[6.5px]'}`}
75+
>
76+
<span className='mr-1 text-blue-400 text-[0.7em]'>
77+
&#9679;
78+
</span>
79+
{children}
80+
</a>
81+
);
82+
},
83+
}
84+
: { component: () => null },
85+
h3:
86+
depth >= 3
87+
? {
88+
component: ({ children }) => {
89+
const slug = slugifyMarkdownHeadline(children);
90+
return (
91+
<a
92+
href={`#${slug}`}
93+
className='flex flex-row items-center cursor-pointer mb-3 max-sm:text-sm text-slate-600 dark:text-slate-300 leading-4 ml-[-0.25rem]'
94+
>
95+
<span className='text-blue-400/40 font-extrabold text-[0.8em] max-sm:text-[1.2em] ml-1'>
96+
&mdash;&mdash;
97+
</span>
98+
<span className='mr-1 text-blue-400/90 text-[0.7em] flex justify-center items-center'>
99+
&#9679;
100+
</span>
101+
102+
{children}
103+
</a>
104+
);
105+
},
106+
}
107+
: { component: () => null },
108+
h4:
109+
depth >= 4
110+
? {
111+
component: ({ children }) => {
112+
const slug = slugifyMarkdownHeadline(children);
113+
return (
114+
<a
115+
href={`#${slug}`}
116+
className='flex flex-row items-center cursor-pointer mb-3 max-sm:text-sm text-slate-600 dark:text-slate-300 leading-4 ml-[-0.25rem] '
117+
>
118+
<span className='text-blue-400/40 font-extrabold text-[0.8em] ml-1 max-sm:text-[1.2em]'>
119+
&mdash;&mdash;&mdash;&mdash;
120+
</span>
121+
<span className='mr-1 text-blue-400/90 text-[0.7em] flex justify-center items-center'>
122+
&#9679;
123+
</span>
124+
125+
{children}
126+
</a>
127+
);
128+
},
129+
}
130+
: { component: () => null },
131+
...hiddenElements(
132+
'strong',
133+
'p',
134+
'a',
135+
'ul',
136+
'li',
137+
'table',
138+
'code',
139+
'pre',
140+
'blockquote',
141+
'span',
142+
'div',
143+
'figure',
144+
'Bigquote',
145+
'Regularquote',
146+
'specialBox',
147+
'Infobox',
148+
'Danger',
149+
'Warning',
150+
'Tip',
151+
),
152+
},
153+
}}
154+
>
155+
{markdown}
156+
</Markdown>
157+
);
158+
}
159+
160+
export const TableOfContentsWrapper = ({ depth }: { depth?: number }) => {
161+
// eslint-disable-next-line react-hooks/rules-of-hooks
162+
const fullMarkdown = useContext(FullMarkdownContext);
163+
if (!fullMarkdown) return null;
164+
165+
return (
166+
<>
167+
<div className='flex flex-row gap-2 text-slate-600 dark:text-slate-300 text-h5 max-sm:text-[1rem] items-center'>
168+
<Image
169+
src={'/icons/toc-menu.svg'}
170+
height={'15'}
171+
width={'15'}
172+
alt='menu-icon'
173+
className='max-sm:w-3 max-sm:h-3'
174+
/>
175+
<span>Table of Contents</span>
176+
</div>
177+
<div className='mt-2 bg-slate-50 dark:bg-slate-900 pt-6 pb-3 pr-3 border border-r-0 border-y-0 border-l-blue-400/40 border-l-[2.5px]'>
178+
<TableOfContents
179+
markdown={fullMarkdown}
180+
depth={depth}
181+
/>
182+
</div>
183+
</>
184+
);
185+
};

components/markdown/TabsGroup.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React, { useState } from 'react';
2+
import classnames from 'classnames';
3+
import getFindResultsByGlobalRegExp from '~/lib/getFindResultsByGlobalRegExp';
4+
import { MarkdownRenderer } from './MarkdownRenderer';
5+
6+
const REGEX_TAB_GROUPS =
7+
/\[tabs-start\s*"(?<label>.*)"\]((?!\[tabs-start).|\n)*\[tabs-end\]/gm;
8+
9+
const TAB_REGEX = /(?<=\[tab )\s*"(?<label>.*)"\](?<markdown>(.|\n)*?)\[tab/gm;
10+
11+
export const TabsGroup = ({ markdown }: { markdown: string }) => {
12+
const groupLabel: string | null =
13+
getFindResultsByGlobalRegExp(markdown, REGEX_TAB_GROUPS)?.[0]?.groups?.find(
14+
(g) => g.name === 'label',
15+
)?.match || null;
16+
17+
const tabs = getFindResultsByGlobalRegExp(markdown, TAB_REGEX).map((tab) => {
18+
const label = tab.groups?.find((g) => g.name === 'label')?.match || '';
19+
const markdown = (
20+
tab.groups?.find((g) => g.name === 'markdown')?.match || ''
21+
).trim();
22+
return { label, markdown };
23+
});
24+
25+
const [activeTabIndex, setActiveTabIndex] = useState(0);
26+
const activeTab = tabs[activeTabIndex];
27+
28+
return (
29+
<div>
30+
<div className='flex flex-row items-end mt-4'>
31+
{groupLabel && (
32+
<div className='p-4 text-slate-400 mr-4 text-sm'>{groupLabel}:</div>
33+
)}
34+
<div className='flex flex-row'>
35+
{tabs.map((tab, index) => {
36+
const isActive = index === activeTabIndex;
37+
return (
38+
<div
39+
key={index}
40+
onClick={() => setActiveTabIndex(index)}
41+
className={classnames(
42+
'p-4 px-6 text-slate-700 font-medium border-b-2 rounded-t-lg',
43+
{
44+
'border-blue-400 text-blue-500 bg-blue-50 dark:bg-slate-900 dark:text-white':
45+
isActive,
46+
'border-white/0 cursor-pointer dark:text-white text-slate-700 hover:border-blue-50 hover:bg-blue-50/20':
47+
!isActive,
48+
},
49+
)}
50+
>
51+
{tab.label}
52+
</div>
53+
);
54+
})}
55+
</div>
56+
</div>
57+
<div className='border-slate-100 mb-4 p-6 from-slate-50/50 to-slate-50/100 rounded-xl bg-gradient-to-b dark:from-slate-700/50 dark:to-slate-900/50'>
58+
<MarkdownRenderer markdown={activeTab.markdown} />
59+
</div>
60+
</div>
61+
);
62+
};

components/markdown/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export {
2+
parseMarkdown,
3+
transformMarkdownLinks,
4+
type Element,
5+
} from './MarkdownParser';
6+
export { MarkdownRenderer } from './MarkdownRenderer';
7+
export { TabsGroup } from './TabsGroup';
8+
export { TableOfContents, TableOfContentsWrapper } from './TableOfContents';
9+
10+
// Export overrides for potential customization
11+
export { textOverrides } from './overrides/TextElements';
12+
export { listOverrides } from './overrides/ListElements';
13+
export { tableOverrides } from './overrides/TableElements';
14+
export { codeOverrides } from './overrides/CodeElements';
15+
export { infoBoxOverrides } from './overrides/InfoBoxes';
16+
export { customOverrides } from './overrides/CustomElements';
17+
export { headlineOverrides } from './overrides/HeadlineElements';

0 commit comments

Comments
 (0)