From fb3296e9b0d6ff0cd8d5b8c2f4417e48802590a9 Mon Sep 17 00:00:00 2001 From: Pekka Helesuo Date: Wed, 28 Aug 2024 16:01:40 +0300 Subject: [PATCH 1/4] add anchor of the first heading of the .md-file into metadata --- scripts/generateDocumentationMetadata.js | 26 +++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/scripts/generateDocumentationMetadata.js b/scripts/generateDocumentationMetadata.js index a54afa7..10da530 100644 --- a/scripts/generateDocumentationMetadata.js +++ b/scripts/generateDocumentationMetadata.js @@ -26,6 +26,27 @@ function sortByParagraphNumber(a, b) { return aParts.length - bParts.length; } +/** + * Return the content of the first heading of a markdwon file slugified. + */ +function getAnchorForFile(filePath) { + try { + const content = fs.readFileSync(filePath, 'utf-8'); + const headingMatch = content.match(/^(#{1,3}) (.+)/m); + + if (headingMatch && headingMatch[2]) { + const headingText = headingMatch[2].trim(); + const slug = slugify(headingText); + return slug; + } else { + return null; + } + } catch (error) { + console.error('Error reading file!:', error); + return null; + } +} + function listContentsRecursively(fullPath, docsRelativePath, results = []) { const filesAndDirectories = fs.readdirSync(fullPath, { withFileTypes: true }); filesAndDirectories.sort(sortByParagraphNumber); @@ -44,15 +65,18 @@ function listContentsRecursively(fullPath, docsRelativePath, results = []) { }); } else { if (path.extname(itemPath).toLowerCase() === '.md') { + let fileNameWithoutExtension = path.parse(item.name).name; if (fileNameWithoutExtension.indexOf('-') > -1) { fileNameWithoutExtension = fileNameWithoutExtension.split('-')[1] || fileNameWithoutExtension; } const slug = slugify(fileNameWithoutExtension); + const anchor = getAnchorForFile(itemPath); results.push({ path: itemRelativePath, fileName: item.name, - slug + slug, + anchor: anchor ? anchor : null }); } } From 94230d0ce76d2880620952d107b33732c1d08f0a Mon Sep 17 00:00:00 2001 From: Pekka Helesuo Date: Mon, 2 Sep 2024 12:51:30 +0300 Subject: [PATCH 2/4] documentation - section internal links processing at runtime --- .../docs/[version]/[slug]/page.tsx | 2 +- lib/markdownToHtml.ts | 83 ++++++++++++++++++- lib/utils.ts | 15 ++-- types/types.ts | 3 +- 4 files changed, 94 insertions(+), 9 deletions(-) diff --git a/app/documentation/docs/[version]/[slug]/page.tsx b/app/documentation/docs/[version]/[slug]/page.tsx index c54afb1..1b3ab80 100644 --- a/app/documentation/docs/[version]/[slug]/page.tsx +++ b/app/documentation/docs/[version]/[slug]/page.tsx @@ -62,7 +62,7 @@ export default async function SingleDocPage({ } await indexJSON.forEach(async (element: MarkdownFileMetadata) => { - const { html, anchorLinks } = await readAndConcatMarkdownFiles(element, imagesRuntimePath); + const { html, anchorLinks } = await readAndConcatMarkdownFiles(element, imagesRuntimePath, indexJSON, element.title); element.anchorLinks = anchorLinks; element.html = html; }); diff --git a/lib/markdownToHtml.ts b/lib/markdownToHtml.ts index d18275e..d71d9bb 100644 --- a/lib/markdownToHtml.ts +++ b/lib/markdownToHtml.ts @@ -1,4 +1,4 @@ -import { DocAnchorLinksType } from '@/types/types'; +import { DocAnchorLinksType, MarkdownFileMetadata } from '@/types/types'; import slugify from 'slugify'; import hljs from 'highlight.js' @@ -148,6 +148,7 @@ export const processMigrationGuideLinks = (markdownContent: string): string => { return result; } +/* const findLinksToMDDocs = (mdString: string) => { //find all html-style links that have data-internal-anchor - attribute set //and replace thos with links to internal anchors @@ -201,4 +202,82 @@ const parseRef = (ref: string): string => { } return '#' + slugify(ref); -} \ No newline at end of file +} + +*/ + + + +/** + * Creates a slugified url for use in documentation section + * @param {string} url Original href of link + * @param {Object[]} indexJSON The metadata JSON-structure of the documentation + * @param {string} activeSectionTitle Title of the active section when linking within same folder + * @returns {string} Return a new anchor to use as href + */ +const createSlugifiedUrlToAnchor = (url: string, indexJSON: MarkdownFileMetadata[], activeSectionTitle: string) => { + if (url.startsWith('../')) { + url = url.replace('../', ''); + } + const urlParts = url.split('/'); + const fileName = urlParts.pop(); + const directory = urlParts.length > 0 ? urlParts.join('/') : ''; + const findChildAnchor = (children: MarkdownFileMetadata[], fileName: string) => { + const child = children.find(child => child.fileName === fileName); + return child ? child.anchor : null; + }; + + if (!fileName) { + return ''; + } + + // No path provided -> find active section by title + if (!directory) { + const activeSection = indexJSON.find(item => item.title === activeSectionTitle); + if (activeSection) { + const anchor = findChildAnchor(activeSection.children, fileName); + if (anchor) { + return `${activeSection.slug}#${anchor}`; + } + + //console.log('No corresponding anchor found under active section: ', activeSectionTitle, fileName); + } + } else { + // Find section corresponding to path + const matchingTopLevel = indexJSON.find(item => item.title === directory); + if (matchingTopLevel) { + const anchor = findChildAnchor(matchingTopLevel.children, fileName); + if (anchor) { + return `${matchingTopLevel.slug}#${anchor}`; + } + + //console.log('No corresponding anchor found under section: ', matchingTopLevel, fileName); + } + } + + return url; +} + +/** + * Find relative links to .md docs in content and replace href with an anchor we've parsed in documentation metadata + * @param {string} markdownContent - The content of the markdown file + * @param {JSON-object} indexJSON documentation version full metadata + * @param {activeSectionTitle} title of current section when linking within the same folder + * @returns {string} content with links replaced + */ +export const processInternalMDLinks = (markdownContent: string, indexJSON: MarkdownFileMetadata[], activeSectionTitle: string): string => { + // Regular expression to match Markdown links + const relativeLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; + const replacedContent = markdownContent.replace(relativeLinkRegex, (match, text, url) => { + // Check if the URL is a relative link ending with .md + if (url.endsWith('.md') && !url.startsWith('http://') && !url.startsWith('https://') && !url.startsWith('www.')) { + const anchor = createSlugifiedUrlToAnchor(url, indexJSON, activeSectionTitle); + return `[${text}](${anchor})`; + } + // Return the original match if it's not a relative .md link + return match; + }); + + return replacedContent; +} + diff --git a/lib/utils.ts b/lib/utils.ts index 218e3f8..8c16f9a 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -34,7 +34,7 @@ export const readMarkdownFile = async function(filePath: string, imagesPath: str return markdown; }; -export const readAndConcatMarkdownFiles = async function(parentItem: MarkdownFileMetadata, imagesPath: string = '') { +export const readAndConcatMarkdownFiles = async function(parentItem: MarkdownFileMetadata, imagesPath: string = '', indexJSON: MarkdownFileMetadata[] = [], activeSectionSlug: string = '') { let markdownAll = ''; parentItem.children.forEach(element => { const cwdPath = path.resolve(process.cwd()); @@ -53,7 +53,7 @@ export const readAndConcatMarkdownFiles = async function(parentItem: MarkdownFil markdownAll += content +'\r\n\r\n'; }); - markdownAll = processMarkdown(markdownAll, imagesPath); + markdownAll = processMarkdown(markdownAll, imagesPath, indexJSON, activeSectionSlug); const compiled = compileMarkdownToHTML(markdownAll, parentItem.ordinal || '1'); // inject script to make mermaid js work its magic @@ -90,16 +90,21 @@ const replaceLevelOneHeadingsWithLevelTwo = (markdown: string): string => { return replacedString; } -const processMarkdown = (markdown: string, imagesPath: string) => { +const processMarkdown = (markdown: string, imagesPath: string, indexJSON: MarkdownFileMetadata[] = [], activeSectionTitle: string = '') => { markdown = updateMarkdownImagePaths(markdown, imagesPath); markdown = updateMarkdownHtmlStyleTags(markdown); markdown = processHeaders(markdown); // migration guide first, specific treatment for that markdown = processMigrationGuideLinks(markdown); + + //documentation internal links to other mds to work with the compiled version + //Note! At this point the following condition is true only for documentation-section + if (indexJSON && activeSectionTitle) { + markdown = processInternalMDLinks(markdown, indexJSON, activeSectionTitle); + } + //process rest of the links from md style -> , children: Array, - html: string + html: string, + anchor?: string } export type VersionedResourceLink = { From 9681bd67a145d4977441be28c29bc609f5398fe9 Mon Sep 17 00:00:00 2001 From: Pekka Helesuo Date: Mon, 2 Sep 2024 15:56:20 +0300 Subject: [PATCH 3/4] add unit tests --- lib/markdownToHtml.test.ts | 79 +++++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/lib/markdownToHtml.test.ts b/lib/markdownToHtml.test.ts index 58c3d12..8f1376d 100644 --- a/lib/markdownToHtml.test.ts +++ b/lib/markdownToHtml.test.ts @@ -1,5 +1,6 @@ -import { badgeTemplates, insertIdsToHeaders, processAllLinks, processCodeBlocks, processHeaders, processJavascriptBlocks, processMigrationGuideLinks, processTripleQuoteCodeBlocks, updateMarkdownHtmlStyleTags, updateMarkdownImagePaths } from "./markdownToHtml"; +import { MarkdownFileMetadata } from "@/types/types"; +import { badgeTemplates, insertIdsToHeaders, processAllLinks, processCodeBlocks, processHeaders, processInternalMDLinks, processJavascriptBlocks, processMigrationGuideLinks, processTripleQuoteCodeBlocks, updateMarkdownHtmlStyleTags, updateMarkdownImagePaths } from "./markdownToHtml"; import slugify from 'slugify'; const createTestHtml = () => { @@ -236,4 +237,80 @@ describe('markdownToHtml tests', () => { expect(processed).toEqual(markdown); }); }) + + describe("processInternalMDLinks", function() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const indexJSON: any[] = [ + { + "slug": "section-one", + "title": "Section One", + "children": [ + { + "fileName": "file1.md", + "anchor": "anchor1" + }, + { + "fileName": "file2.md", + "anchor": "anchor2" + } + ] + }, + { + "slug": "section-two", + "title": "Section Two", + "children": [ + { + "fileName": "file3.md", + "anchor": "anchor3" + }, + { + "fileName": "file4.md", + "anchor": "anchor4" + } + ] + } + ]; + + it("replaces a relative link with the correct link from indexJSON", function() { + const markdownContent = "Check this [link](file1.md) in the content."; + const activeSectionTitle = "Section One"; + const result = processInternalMDLinks(markdownContent, indexJSON, activeSectionTitle); + expect(result).toBe("Check this [link](section-one#anchor1) in the content."); + }); + + it("does not replace external links", function() { + const markdownContent = "Visit [Google](https://www.google.com) for more info."; + const activeSectionTitle = "Section One"; + const result = processInternalMDLinks(markdownContent, indexJSON, activeSectionTitle); + expect(result).toBe("Visit [Google](https://www.google.com) for more info."); + }); + + it("replaces a relative link with a path and correct link from indexJSON", function() { + const markdownContent = "More info [here](../Section Two/file3.md)."; + const activeSectionTitle = "Section One"; + const result = processInternalMDLinks(markdownContent, indexJSON, activeSectionTitle); + expect(result).toBe("More info [here](section-two#anchor3)."); + }); + + it("does not replace a link if no matching file is found in indexJSON", function() { + const markdownContent = "Check this [link](nonexistent.md) in the content."; + const activeSectionTitle = "Section One"; + const result = processInternalMDLinks(markdownContent, indexJSON, activeSectionTitle); + expect(result).toBe("Check this [link](nonexistent.md) in the content."); + }); + + it("handles multiple links in the same content", function() { + const markdownContent = "Links: [file1](file1.md), [file4](../Section Two/file4.md), and [external](https://www.example.com)."; + const activeSectionTitle = "Section One"; + const result = processInternalMDLinks(markdownContent, indexJSON, activeSectionTitle); + expect(result).toBe("Links: [file1](section-one#anchor1), [file4](section-two#anchor4), and [external](https://www.example.com)."); + }); + + it("leaves content unchanged if there are no links", function() { + const markdownContent = "This content has no links."; + const activeSectionTitle = "Section One"; + const result = processInternalMDLinks(markdownContent, indexJSON, activeSectionTitle); + expect(result).toBe("This content has no links."); + }); + }); }); \ No newline at end of file From 2fbcc3a167bf1a5d34acc6d2bf7136fd1319c920 Mon Sep 17 00:00:00 2001 From: Pekka Helesuo Date: Mon, 2 Sep 2024 16:04:39 +0300 Subject: [PATCH 4/4] remove unused import --- lib/markdownToHtml.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/markdownToHtml.test.ts b/lib/markdownToHtml.test.ts index 8f1376d..6f6b3ae 100644 --- a/lib/markdownToHtml.test.ts +++ b/lib/markdownToHtml.test.ts @@ -1,5 +1,4 @@ -import { MarkdownFileMetadata } from "@/types/types"; import { badgeTemplates, insertIdsToHeaders, processAllLinks, processCodeBlocks, processHeaders, processInternalMDLinks, processJavascriptBlocks, processMigrationGuideLinks, processTripleQuoteCodeBlocks, updateMarkdownHtmlStyleTags, updateMarkdownImagePaths } from "./markdownToHtml"; import slugify from 'slugify';