diff --git a/src/lib/path.ts b/src/lib/path.ts new file mode 100644 index 000000000..42ccd32fb --- /dev/null +++ b/src/lib/path.ts @@ -0,0 +1,15 @@ +export function joinPath(...parts: string[]) { + const separator = '/'; + + return parts + .map((part: string, index: number) => { + if (index) { + part = part.replace(new RegExp('^' + separator), ''); + } + if (index !== parts.length - 1) { + part = part.replace(new RegExp(separator + '$'), ''); + } + return part; + }) + .join(separator); +} diff --git a/src/lib/roadmap.ts b/src/lib/roadmap.ts index aa375b2b6..2b2d0600f 100644 --- a/src/lib/roadmap.ts +++ b/src/lib/roadmap.ts @@ -35,37 +35,4 @@ export async function getRoadmapIds() { return fileName.replace(".md", ""); }); -} - -export interface TopicFileType { - frontMatter: Record; - file: string; - url: string; - Content: any; -}; - -export async function getTopicPathMapping() { - const contentFiles = await import.meta.glob( - "/src/roadmaps/*/content/**/*.md", { - eager: true - } -); - - const mapping: Record = {}; - - Object.keys(contentFiles).forEach((filePath) => { - // => Sample Paths - // /src/roadmaps/vue/content/102-ecosystem/102-ssr/101-nuxt-js.md - // /src/roadmaps/vue/content/102-ecosystem/102-ssr/index.md - const url = filePath - .replace("/src/roadmaps/", "") // Remove the base `/src/roadmaps` from path - .replace("/content", "") // Remove the `/[roadmapName]/content` - .replace(/\/\d+-/g, "/") // Remove ordering info `/101-ecosystem` - .replace(/\/index\.md$/, "") // Make the `/index.md` to become the parent folder only - .replace(/\.md$/, ""); // Remove `.md` from the end of file - - mapping[url] = contentFiles[filePath] as any; - }); - - return mapping; -} +} \ No newline at end of file diff --git a/src/lib/topic.ts b/src/lib/topic.ts new file mode 100644 index 000000000..893c1bece --- /dev/null +++ b/src/lib/topic.ts @@ -0,0 +1,148 @@ +import { joinPath } from './path'; +import type { RoadmapFrontmatter } from './roadmap'; + +// Generates URL from the topic file path e.g. +// -> /src/roadmaps/vue/content/102-ecosystem/102-ssr/101-nuxt-js.md +// /vue/ecosystem/ssr/nuxt-js +// -> /src/roadmaps/vue/content/102-ecosystem +// /vue/ecosystem +function generateTopicUrl(filePath: string) { + return filePath + .replace('/src/roadmaps/', '/') // Remove the base `/src/roadmaps` from path + .replace('/content', '') // Remove the `/[roadmapId]/content` + .replace(/\/\d+-/g, '/') // Remove ordering info `/101-ecosystem` + .replace(/\/index\.md$/, '') // Make the `/index.md` to become the parent folder only + .replace(/\.md$/, ''); // Remove `.md` from the end of file +} + +/** + * Generates breadcrumbs for the given topic URL from the given topic file details + * + * @param topicUrl Topic URL for which breadcrumbs are required + * @param topicFiles Topic file mapping to read the topic data from + */ +function generateBreadcrumbs( + topicUrl: string, + topicFiles: Record +): BreadcrumbItem[] { + // We need to collect all the pages with permalinks to generate breadcrumbs + // e.g. /backend/internet/how-does-internet-work/http + // /backend + // /backend/internet + // /backend/internet/how-does-internet-work + // /backend/internet/how-does-internet-work/http + + const urlParts = topicUrl.split('/'); + const breadcrumbUrls = []; + const subLinks = []; + + for (let counter = 0; counter < urlParts.length; counter++) { + subLinks.push(urlParts[counter]); + + // Skip the following + // -> [ '' ] + // -> [ '', 'vue' ] + if (subLinks.length > 2) { + breadcrumbUrls.push(subLinks.join('/')); + } + } + + const breadcrumbs = breadcrumbUrls.map((breadCrumbUrl): BreadcrumbItem => { + const topicFile = topicFiles[breadCrumbUrl]; + const topicFileContent = topicFile.file; + + const firstHeading = topicFileContent?.getHeadings()?.[0]; + + return { title: firstHeading?.text, url: breadCrumbUrl }; + }); + + return breadcrumbs; +} + +type BreadcrumbItem = { + title: string; + url: string; +}; + +type FileHeadingType = { + depth: number; + slug: string; + text: string; +}; + +export interface TopicFileContentType { + frontMatter: Record; + file: string; + url: string; + Content: any; + getHeadings: () => FileHeadingType[]; +} + +export interface TopicFileType { + url: string; + file: TopicFileContentType; + roadmap: RoadmapFrontmatter; + roadmapId: string; + breadcrumbs: BreadcrumbItem[]; +} + +export async function getTopicFiles(): Promise> { + const contentFiles = await import.meta.glob( + '/src/roadmaps/*/content/**/*.md', + { + eager: true, + } + ); + + const mapping: Record = {}; + + for (let filePath of Object.keys(contentFiles)) { + const fileContent: TopicFileContentType = contentFiles[filePath] as any; + const fileHeadings = fileContent.getHeadings(); + const firstHeading = fileHeadings[0]; + + const [, roadmapId, pathInsideContent] = + filePath.match(/^\/src\/roadmaps\/(.+)?\/content\/(.+)?$/) || []; + + const topicUrl = generateTopicUrl(filePath); + + const currentRoadmap = await import( + `../roadmaps/${roadmapId}/${roadmapId}.md` + ); + + mapping[topicUrl] = { + url: topicUrl, + file: fileContent, + roadmap: currentRoadmap.frontmatter, + roadmapId: roadmapId, + breadcrumbs: [], + }; + } + + // Populate breadcrumbs inside the mapping + Object.keys(mapping).forEach((topicUrl) => { + const { + roadmap: currentRoadmap, + roadmapId, + file: currentTopic, + } = mapping[topicUrl]; + const roadmapUrl = `/${roadmapId}`; + + // Breadcrumbs for the file + const breadcrumbs: BreadcrumbItem[] = [ + { + title: currentRoadmap.featuredTitle, + url: `${roadmapUrl}`, + }, + { + title: 'Topics', + url: `${roadmapUrl}/topics`, + }, + ...generateBreadcrumbs(topicUrl, mapping), + ]; + + mapping[topicUrl].breadcrumbs = breadcrumbs; + }); + + return mapping; +} diff --git a/src/pages/[...topicId].astro b/src/pages/[...topicId].astro index d082228a8..81d7d7187 100644 --- a/src/pages/[...topicId].astro +++ b/src/pages/[...topicId].astro @@ -1,27 +1,27 @@ --- -import MarkdownContent from "../components/MarkdownContent/MarkdownContent.astro"; -import BaseLayout from "../layouts/BaseLayout.astro"; -import { getTopicPathMapping, TopicFileType } from "../lib/roadmap"; +import MarkdownContent from '../components/MarkdownContent/MarkdownContent.astro'; +import BaseLayout from '../layouts/BaseLayout.astro'; +import { getTopicFiles, TopicFileType } from '../lib/topic'; export async function getStaticPaths() { - const topicPathMapping = await getTopicPathMapping(); + const topicPathMapping = await getTopicFiles(); // console.log(topicPathMapping); return Object.keys(topicPathMapping).map((topicSlug) => ({ - params: { topicId: topicSlug }, + params: { topicId: topicSlug.replace(/^\//, '') }, props: topicPathMapping[topicSlug], })); } const { topicId } = Astro.params; -const props: TopicFileType = Astro.props as any; +const { file } = Astro.props as TopicFileType; ---
- +
diff --git a/src/roadmaps/computer-science/content/117-databases/106-dcl.md b/src/roadmaps/computer-science/content/117-databases/106-dcl.md index 8c186c1f2..7ecd0c898 100644 --- a/src/roadmaps/computer-science/content/117-databases/106-dcl.md +++ b/src/roadmaps/computer-science/content/117-databases/106-dcl.md @@ -1,4 +1,4 @@ -DCL (Data Control Language): +# DCL (Data Control Language): DCL includes commands such as GRANT and REVOKE which mainly deal with the rights, permissions, and other controls of the database system.