Merge pull request #13
chore: Sync package version v1.16.1 - Introduce language-specific icons in code blocks using react-icons - Implement custom handleCodeTitles rehype plugin to support data-title attribute - Refactor PreMdx component to display language icons and titles - Add comprehensive styling for code blocks including header, actions, and syntax highlighting - Update dependencies to include react-icons and remove unused install package - Bump version to 1.16.1 in both root and dist package.json files
This commit is contained in:
@@ -25,7 +25,7 @@ export default function Home() {
|
||||
)}
|
||||
>
|
||||
<AnimatedShinyText className="inline-flex items-center justify-center px-4 py-1 transition ease-out hover:text-neutral-100 hover:duration-300 hover:dark:text-neutral-200">
|
||||
<span>🚀 New Version - Release v1.16.0</span>
|
||||
<span>🚀 New Version - Release v1.16.1</span>
|
||||
<ArrowRightIcon className="ml-1 size-3 transition-transform duration-300 ease-in-out group-hover:translate-x-0.5" />
|
||||
</AnimatedShinyText>
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,108 @@
|
||||
import { ComponentProps } from "react";
|
||||
import { type ComponentProps } from "react";
|
||||
import Copy from "./CopyMdx";
|
||||
import {
|
||||
SiJavascript,
|
||||
SiTypescript,
|
||||
SiReact,
|
||||
SiPython,
|
||||
SiGo,
|
||||
SiPhp,
|
||||
SiRuby,
|
||||
SiSwift,
|
||||
SiKotlin,
|
||||
SiHtml5,
|
||||
SiCss3,
|
||||
SiSass,
|
||||
SiPostgresql,
|
||||
SiGraphql,
|
||||
SiYaml,
|
||||
SiToml,
|
||||
SiDocker,
|
||||
SiNginx,
|
||||
SiGit,
|
||||
SiGnubash,
|
||||
SiMarkdown,
|
||||
} from "react-icons/si";
|
||||
import { FaJava, FaCode } from "react-icons/fa";
|
||||
import { TbJson } from "react-icons/tb";
|
||||
|
||||
type PreProps = ComponentProps<"pre"> & {
|
||||
raw?: string;
|
||||
"data-title"?: string;
|
||||
};
|
||||
|
||||
// Component to display an icon based on the programming language
|
||||
const LanguageIcon = ({ lang }: { lang: string }) => {
|
||||
const iconProps = { className: "w-4 h-4" };
|
||||
const languageToIconMap: Record<string, JSX.Element> = {
|
||||
gitignore: <SiGit {...iconProps} />,
|
||||
docker: <SiDocker {...iconProps} />,
|
||||
dockerfile: <SiDocker {...iconProps} />,
|
||||
nginx: <SiNginx {...iconProps} />,
|
||||
sql: <SiPostgresql {...iconProps} />,
|
||||
graphql: <SiGraphql {...iconProps} />,
|
||||
yaml: <SiYaml {...iconProps} />,
|
||||
yml: <SiYaml {...iconProps} />,
|
||||
toml: <SiToml {...iconProps} />,
|
||||
json: <TbJson {...iconProps} />,
|
||||
md: <SiMarkdown {...iconProps} />,
|
||||
markdown: <SiMarkdown {...iconProps} />,
|
||||
bash: <SiGnubash {...iconProps} />,
|
||||
sh: <SiGnubash {...iconProps} />,
|
||||
shell: <SiGnubash {...iconProps} />,
|
||||
swift: <SiSwift {...iconProps} />,
|
||||
kotlin: <SiKotlin {...iconProps} />,
|
||||
kt: <SiKotlin {...iconProps} />,
|
||||
kts: <SiKotlin {...iconProps} />,
|
||||
rb: <SiRuby {...iconProps} />,
|
||||
ruby: <SiRuby {...iconProps} />,
|
||||
php: <SiPhp {...iconProps} />,
|
||||
go: <SiGo {...iconProps} />,
|
||||
py: <SiPython {...iconProps} />,
|
||||
python: <SiPython {...iconProps} />,
|
||||
java: <FaJava {...iconProps} />,
|
||||
tsx: <SiReact {...iconProps} />,
|
||||
typescript: <SiTypescript {...iconProps} />,
|
||||
ts: <SiTypescript {...iconProps} />,
|
||||
jsx: <SiReact {...iconProps} />,
|
||||
js: <SiJavascript {...iconProps} />,
|
||||
javascript: <SiJavascript {...iconProps} />,
|
||||
html: <SiHtml5 {...iconProps} />,
|
||||
css: <SiCss3 {...iconProps} />,
|
||||
scss: <SiSass {...iconProps} />,
|
||||
sass: <SiSass {...iconProps} />,
|
||||
};
|
||||
return languageToIconMap[lang] || <FaCode {...iconProps} />;
|
||||
};
|
||||
|
||||
// Function to extract the language from className
|
||||
function getLanguage(className: string = ""): string {
|
||||
const match = className.match(/language-(\w+)/);
|
||||
return match ? match[1] : "default";
|
||||
}
|
||||
|
||||
export default function Pre({ children, raw, ...rest }: PreProps) {
|
||||
const { "data-title": title, className, ...restProps } = rest;
|
||||
const language = getLanguage(className);
|
||||
const hasTitle = !!title;
|
||||
|
||||
export default function Pre({
|
||||
children,
|
||||
raw,
|
||||
...rest
|
||||
}: ComponentProps<"pre"> & { raw?: string }) {
|
||||
return (
|
||||
<div className="my-5 relative">
|
||||
<div className="absolute top-3 right-2.5 z-10 sm:block hidden">
|
||||
<Copy content={raw!} />
|
||||
<div className="code-block-container">
|
||||
<div className="code-block-actions">
|
||||
{raw && <Copy content={raw} />}
|
||||
</div>
|
||||
<div className="relative">
|
||||
<pre {...rest}>{children}</pre>
|
||||
{hasTitle && (
|
||||
<div className="code-block-header">
|
||||
<div className="flex items-center gap-2">
|
||||
<LanguageIcon lang={language} />
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="code-block-body">
|
||||
<pre className={className} {...restProps}>
|
||||
{children}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ import rehypeSlug from "rehype-slug";
|
||||
import rehypeCodeTitles from "rehype-code-titles";
|
||||
import { page_routes, ROUTES } from "./routes-config";
|
||||
import { visit } from "unist-util-visit";
|
||||
import type { Node } from "unist";
|
||||
import type { Node, Parent } from "unist";
|
||||
import matter from "gray-matter";
|
||||
|
||||
// Type definitions for unist-util-visit
|
||||
@@ -16,6 +16,7 @@ interface Element extends Node {
|
||||
type: string;
|
||||
tagName?: string;
|
||||
properties?: Record<string, unknown> & {
|
||||
className?: string[];
|
||||
raw?: string;
|
||||
};
|
||||
children?: Node[];
|
||||
@@ -77,6 +78,49 @@ const components = {
|
||||
AccordionGroup
|
||||
};
|
||||
|
||||
// helper function to handle rehype code titles, since by default we can't inject into the className of rehype-code-titles
|
||||
const handleCodeTitles = () => (tree: Node) => {
|
||||
visit(tree, "element", (node: Element, index: number | null, parent: Parent | null) => {
|
||||
// Ensure the visited node is valid
|
||||
if (!parent || index === null || node.tagName !== 'div') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is the title div from rehype-code-titles
|
||||
const isTitleDiv = node.properties?.className?.includes('rehype-code-title');
|
||||
if (!isTitleDiv) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the next <pre> element, skipping over other nodes like whitespace text
|
||||
let nextElement = null;
|
||||
for (let i = index + 1; i < parent.children.length; i++) {
|
||||
const sibling = parent.children[i];
|
||||
if (sibling.type === 'element') {
|
||||
nextElement = sibling as Element;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If the next element is a <pre>, move the title to it
|
||||
if (nextElement && nextElement.tagName === 'pre') {
|
||||
const titleNode = node.children?.[0] as TextNode;
|
||||
if (titleNode && titleNode.type === 'text') {
|
||||
if (!nextElement.properties) {
|
||||
nextElement.properties = {};
|
||||
}
|
||||
nextElement.properties['data-title'] = titleNode.value;
|
||||
|
||||
// Remove the original title div
|
||||
parent.children.splice(index, 1);
|
||||
|
||||
// Return the same index to continue visiting from the correct position
|
||||
return index;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// can be used for other pages like blogs, Guides etc
|
||||
async function parseMdx<Frontmatter>(rawMdx: string) {
|
||||
return await compileMDX<Frontmatter>({
|
||||
@@ -87,6 +131,7 @@ async function parseMdx<Frontmatter>(rawMdx: string) {
|
||||
rehypePlugins: [
|
||||
preProcess,
|
||||
rehypeCodeTitles,
|
||||
handleCodeTitles,
|
||||
rehypePrism,
|
||||
rehypeSlug,
|
||||
rehypeAutolinkHeadings,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "docubook",
|
||||
"version": "1.16.0",
|
||||
"version": "1.16.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -30,13 +30,13 @@
|
||||
"framer-motion": "^12.4.1",
|
||||
"geist": "^1.3.1",
|
||||
"gray-matter": "^4.0.3",
|
||||
"install": "^0.13.0",
|
||||
"lucide-react": "^0.511.0",
|
||||
"next": "^14.2.6",
|
||||
"next-mdx-remote": "^5.0.0",
|
||||
"next-themes": "^0.3.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"rehype-autolink-headings": "^7.1.0",
|
||||
"rehype-code-titles": "^1.2.0",
|
||||
"rehype-prism-plus": "^2.0.0",
|
||||
|
||||
@@ -90,6 +90,7 @@ pre>code {
|
||||
display: grid;
|
||||
max-width: inherit !important;
|
||||
padding: 14px 0 !important;
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
.code-line {
|
||||
|
||||
@@ -93,3 +93,87 @@
|
||||
border: none;
|
||||
border-radius: 8px; /* Sudut melengkung pada iframe */
|
||||
}
|
||||
|
||||
/* ======================================================================== */
|
||||
/* Custom styling for code blocks */
|
||||
/* ======================================================================== */
|
||||
|
||||
.code-block-container {
|
||||
position: relative;
|
||||
margin: 1.5rem 0;
|
||||
border: 1px solid hsl(var(--border));
|
||||
overflow: hidden;
|
||||
font-size: 0.875rem;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.code-block-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background-color: hsl(var(--muted));
|
||||
padding: 0.5rem 1rem;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-family: monospace;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.code-block-actions {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.75rem;
|
||||
z-index: 10;
|
||||
}
|
||||
.code-block-actions button {
|
||||
color: hsl(var(--muted-foreground));
|
||||
transition: color 0.2s ease-in-out;
|
||||
}
|
||||
.code-block-actions button:hover {
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
|
||||
.code-block-body pre[class*="language-"] {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.line-numbers-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 3rem;
|
||||
padding-top: 1rem;
|
||||
text-align: right;
|
||||
color: var(--line-number-color);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.line-highlight {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: hsl(var(--primary) / 0.1);
|
||||
border-left: 2px solid hsl(var(--primary));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.code-block-body pre[data-line-numbers="true"] .line-highlight {
|
||||
padding-left: 3.5rem;
|
||||
}
|
||||
|
||||
.code-block-body::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
}
|
||||
.code-block-body::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.code-block-body::-webkit-scrollbar-thumb {
|
||||
background: hsl(var(--border));
|
||||
border-radius: 4px;
|
||||
}
|
||||
.code-block-body::-webkit-scrollbar-thumb:hover {
|
||||
background: hsl(var(--muted));
|
||||
}
|
||||
Reference in New Issue
Block a user