From 8d3b7adfb3f0a9d7b0fbcace5ddf171f69540f9a Mon Sep 17 00:00:00 2001 From: Harjun751 Date: Mon, 19 Jan 2026 20:31:31 +0800 Subject: [PATCH 1/6] Add querystring check in validateIntraLink --- packages/core/src/html/linkProcessor.ts | 5 +- .../core/test/unit/html/linkProcessor.test.ts | 85 +++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/packages/core/src/html/linkProcessor.ts b/packages/core/src/html/linkProcessor.ts index 0f66d5841b..47db014460 100644 --- a/packages/core/src/html/linkProcessor.ts +++ b/packages/core/src/html/linkProcessor.ts @@ -178,12 +178,15 @@ export function validateIntraLink(resourcePath: string, ${resourcePath}' found in file '${cwf}'`; resourcePath = urlUtil.stripBaseUrl(resourcePath, config.baseUrl); // eslint-disable-line no-param-reassign - const resourcePathUrl = parse(resourcePath); + const resourcePathUrl = parse(resourcePath); // Parse query strings let hash; if (resourcePathUrl.hash) { hash = resourcePathUrl.hash.substring(1); // remove hash portion (if any) in the resourcePath resourcePath = resourcePathUrl.pathname; // eslint-disable-line no-param-reassign + } else if (resourcePathUrl.query) { + // remove query parameters if present + resourcePath = resourcePathUrl.pathname; // eslint-disable-line no-param-reassign } if (resourcePath.endsWith('/')) { diff --git a/packages/core/test/unit/html/linkProcessor.test.ts b/packages/core/test/unit/html/linkProcessor.test.ts index 5a64caa9dc..433a22d5b4 100644 --- a/packages/core/test/unit/html/linkProcessor.test.ts +++ b/packages/core/test/unit/html/linkProcessor.test.ts @@ -268,3 +268,88 @@ test('Test valid hash link', () => { expect(linkProcessor.validateIntraLink(mockResourcePath, mockCwf, mockConfig, mockMap)) .toEqual(EXPECTED_RESULT); }); + +test('Test link with query parameter', () => { + // should be checked as page + const mockLink = 'Test'; + const mockNode = parseHTML(mockLink)[0] as MbNode; + const mockResourcePath = linkProcessor.getDefaultTagsResourcePath(mockNode); + + const EXPECTED_RESULT = 'Intralink with ".html" extension is a valid Page Source or File Asset'; + + expect(linkProcessor.validateIntraLink(mockResourcePath, mockCwf, mockConfig)).toEqual(EXPECTED_RESULT); +}); + +// test('Test invalid, non-existent link ending with / and query parameters', () => { +// // should be checked as page and file asset +// const mockLink = 'Test'; +// const mockNode = parseHTML(mockLink)[0] as MbNode; +// const mockResourcePath = linkProcessor.getDefaultTagsResourcePath(mockNode); +// +// const EXPECTED_RESULT = 'Intralink ending with "/" is neither a Page Source nor File Asset'; +// +// expect(linkProcessor.validateIntraLink(mockResourcePath, mockCwf, mockConfig)).toEqual(EXPECTED_RESULT); +// }); + +test('Test valid link ending with no extension and query parameters', () => { + // should be checked as page + const mockLink = 'Test'; + const mockNode = parseHTML(mockLink)[0] as MbNode; + const mockResourcePath = linkProcessor.getDefaultTagsResourcePath(mockNode); + + const EXPECTED_RESULT = 'Intralink with no extension is a valid Page Source or File Asset'; + + expect(linkProcessor.validateIntraLink(mockResourcePath, mockCwf, mockConfig)).toEqual(EXPECTED_RESULT); +}); + +test('Test valid link ending with no extension and query parameters', () => { + // should be checked as page and file asset (implicit index resource path will be true) + const mockLink = 'Test'; + const mockNode = parseHTML(mockLink)[0] as MbNode; + const mockResourcePath = linkProcessor.getDefaultTagsResourcePath(mockNode); + + const EXPECTED_RESULT = 'Intralink with no extension is a valid Page Source or File Asset'; + + expect(linkProcessor.validateIntraLink(mockResourcePath, mockCwf, mockConfig)).toEqual(EXPECTED_RESULT); +}); + +test('Test valid link ending with no extension and query parameters', () => { + // should be checked as file asset (raw file) + const mockLink = 'Test'; + const mockNode = parseHTML(mockLink)[0] as MbNode; + const mockResourcePath = linkProcessor.getDefaultTagsResourcePath(mockNode); + + const EXPECTED_RESULT = 'Intralink with no extension is a valid Page Source or File Asset'; + + expect(linkProcessor.validateIntraLink(mockResourcePath, mockCwf, mockConfig)).toEqual(EXPECTED_RESULT); +}); + +test('Test invalid, non-existent link ending with no extension and query parameters', () => { + // should be checked as page, file asset, and raw file asset + const mockLink = 'Test'; + const mockNode = parseHTML(mockLink)[0] as MbNode; + const mockResourcePath = linkProcessor.getDefaultTagsResourcePath(mockNode); + + const EXPECTED_RESULT = 'Intralink with no extension is neither a Page Source nor File Asset'; + + expect(linkProcessor.validateIntraLink(mockResourcePath, mockCwf, mockConfig)).toEqual(EXPECTED_RESULT); +}); + +test('Test valid hash link with query parameters', () => { + const mockLink = 'Test'; + const mockNode = parseHTML(mockLink)[0] as MbNode; + const mockResourcePath = linkProcessor.getDefaultTagsResourcePath(mockNode); + const EXPECTED_RESULT = 'Intralink with ".html" extension is a valid Page Source or File Asset'; + const mockMap = new Map>(); + mockMap.set('/userGuide/raw.md', new Set(['test-1'])); + expect(linkProcessor.validateIntraLink(mockResourcePath, mockCwf, mockConfig, mockMap)) + .toEqual(EXPECTED_RESULT); +}); + +test('Test non valid hash link with query parameters', () => { + const mockLink = 'Test'; + const mockNode = parseHTML(mockLink)[0] as MbNode; + const mockResourcePath = linkProcessor.getDefaultTagsResourcePath(mockNode); + expect(linkProcessor.validateIntraLink(mockResourcePath, mockCwf, mockConfig)) + .toEqual('Intralink with ".html" extension is a valid Page Source or File Asset but hash is not found'); +}); From 5ce7f74b04cdf8715bd7ad5e62ec9849d600096f Mon Sep 17 00:00:00 2001 From: Harjun751 Date: Mon, 19 Jan 2026 21:40:16 +0800 Subject: [PATCH 2/6] Remove redundant test --- .../core/test/unit/html/linkProcessor.test.ts | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/packages/core/test/unit/html/linkProcessor.test.ts b/packages/core/test/unit/html/linkProcessor.test.ts index 433a22d5b4..2e6160fc17 100644 --- a/packages/core/test/unit/html/linkProcessor.test.ts +++ b/packages/core/test/unit/html/linkProcessor.test.ts @@ -270,7 +270,6 @@ test('Test valid hash link', () => { }); test('Test link with query parameter', () => { - // should be checked as page const mockLink = 'Test'; const mockNode = parseHTML(mockLink)[0] as MbNode; const mockResourcePath = linkProcessor.getDefaultTagsResourcePath(mockNode); @@ -280,19 +279,7 @@ test('Test link with query parameter', () => { expect(linkProcessor.validateIntraLink(mockResourcePath, mockCwf, mockConfig)).toEqual(EXPECTED_RESULT); }); -// test('Test invalid, non-existent link ending with / and query parameters', () => { -// // should be checked as page and file asset -// const mockLink = 'Test'; -// const mockNode = parseHTML(mockLink)[0] as MbNode; -// const mockResourcePath = linkProcessor.getDefaultTagsResourcePath(mockNode); -// -// const EXPECTED_RESULT = 'Intralink ending with "/" is neither a Page Source nor File Asset'; -// -// expect(linkProcessor.validateIntraLink(mockResourcePath, mockCwf, mockConfig)).toEqual(EXPECTED_RESULT); -// }); - test('Test valid link ending with no extension and query parameters', () => { - // should be checked as page const mockLink = 'Test'; const mockNode = parseHTML(mockLink)[0] as MbNode; const mockResourcePath = linkProcessor.getDefaultTagsResourcePath(mockNode); @@ -302,30 +289,7 @@ test('Test valid link ending with no extension and query parameters', () => { expect(linkProcessor.validateIntraLink(mockResourcePath, mockCwf, mockConfig)).toEqual(EXPECTED_RESULT); }); -test('Test valid link ending with no extension and query parameters', () => { - // should be checked as page and file asset (implicit index resource path will be true) - const mockLink = 'Test'; - const mockNode = parseHTML(mockLink)[0] as MbNode; - const mockResourcePath = linkProcessor.getDefaultTagsResourcePath(mockNode); - - const EXPECTED_RESULT = 'Intralink with no extension is a valid Page Source or File Asset'; - - expect(linkProcessor.validateIntraLink(mockResourcePath, mockCwf, mockConfig)).toEqual(EXPECTED_RESULT); -}); - -test('Test valid link ending with no extension and query parameters', () => { - // should be checked as file asset (raw file) - const mockLink = 'Test'; - const mockNode = parseHTML(mockLink)[0] as MbNode; - const mockResourcePath = linkProcessor.getDefaultTagsResourcePath(mockNode); - - const EXPECTED_RESULT = 'Intralink with no extension is a valid Page Source or File Asset'; - - expect(linkProcessor.validateIntraLink(mockResourcePath, mockCwf, mockConfig)).toEqual(EXPECTED_RESULT); -}); - test('Test invalid, non-existent link ending with no extension and query parameters', () => { - // should be checked as page, file asset, and raw file asset const mockLink = 'Test'; const mockNode = parseHTML(mockLink)[0] as MbNode; const mockResourcePath = linkProcessor.getDefaultTagsResourcePath(mockNode); From d6d7e636c1e2fd518fbf77f42aa10d3a63446c33 Mon Sep 17 00:00:00 2001 From: Harjun751 Date: Mon, 19 Jan 2026 21:47:55 +0800 Subject: [PATCH 3/6] Remove redundant comment --- packages/core/src/html/linkProcessor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/html/linkProcessor.ts b/packages/core/src/html/linkProcessor.ts index 47db014460..1ce38b1697 100644 --- a/packages/core/src/html/linkProcessor.ts +++ b/packages/core/src/html/linkProcessor.ts @@ -178,7 +178,7 @@ export function validateIntraLink(resourcePath: string, ${resourcePath}' found in file '${cwf}'`; resourcePath = urlUtil.stripBaseUrl(resourcePath, config.baseUrl); // eslint-disable-line no-param-reassign - const resourcePathUrl = parse(resourcePath); // Parse query strings + const resourcePathUrl = parse(resourcePath); let hash; if (resourcePathUrl.hash) { hash = resourcePathUrl.hash.substring(1); From 29e3884280a60d298d73ce20b7a9a16ba88d03aa Mon Sep 17 00:00:00 2001 From: Harjun751 Date: Tue, 20 Jan 2026 08:18:15 +0800 Subject: [PATCH 4/6] Refactor additions --- packages/core/src/html/linkProcessor.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/core/src/html/linkProcessor.ts b/packages/core/src/html/linkProcessor.ts index 1ce38b1697..02b31d99dc 100644 --- a/packages/core/src/html/linkProcessor.ts +++ b/packages/core/src/html/linkProcessor.ts @@ -179,15 +179,8 @@ export function validateIntraLink(resourcePath: string, resourcePath = urlUtil.stripBaseUrl(resourcePath, config.baseUrl); // eslint-disable-line no-param-reassign const resourcePathUrl = parse(resourcePath); - let hash; - if (resourcePathUrl.hash) { - hash = resourcePathUrl.hash.substring(1); - // remove hash portion (if any) in the resourcePath - resourcePath = resourcePathUrl.pathname; // eslint-disable-line no-param-reassign - } else if (resourcePathUrl.query) { - // remove query parameters if present - resourcePath = resourcePathUrl.pathname; // eslint-disable-line no-param-reassign - } + const hash = resourcePathUrl.hash ? resourcePathUrl.hash.substring(1) : undefined; + resourcePath = resourcePathUrl.pathname; // eslint-disable-line no-param-reassign if (resourcePath.endsWith('/')) { // append index.html to e.g. /userGuide/ From 1833f6d4f7392f223b74e17431acd30c927c2f1c Mon Sep 17 00:00:00 2001 From: Harjun751 Date: Tue, 20 Jan 2026 13:43:53 +0800 Subject: [PATCH 5/6] SLAP code, improve quality --- packages/core/src/html/linkProcessor.ts | 109 +++++++++++++++--------- 1 file changed, 68 insertions(+), 41 deletions(-) diff --git a/packages/core/src/html/linkProcessor.ts b/packages/core/src/html/linkProcessor.ts index 02b31d99dc..6d05ceb496 100644 --- a/packages/core/src/html/linkProcessor.ts +++ b/packages/core/src/html/linkProcessor.ts @@ -156,6 +156,62 @@ function isValidFileAsset(resourcePath: string, config: NodeProcessorConfig) { return fsUtil.fileExists(fullResourcePath); } +/** + * Validates paths ending with '/' by checking if they represent valid page sources or file assets + * with implicit index.html + */ +function validatePathEndingWithSlash(pathname: string, config: NodeProcessorConfig, err: string): string { + // append index.html to e.g. /userGuide/ + const implicitResourcePath = `${pathname}index.html`; + if (!isValidPageSource(implicitResourcePath, config) && !isValidFileAsset(implicitResourcePath, config)) { + logger.warn(err); + return 'Intralink ending with "/" is neither a Page Source nor File Asset'; + } + return 'Intralink ending with "/" is a valid Page Source or File Asset'; +} + +/** + * Validates paths without file extensions by checking various possible interpretations + */ +function validatePathWithNoExtension( + pathname: string, config: NodeProcessorConfig, err: string, + hashErr: string, hash: string | undefined, filePathToHashesMap: Map>): string { + // does not end with '/' and no file ext (e.g. /userGuide) + const implicitResourcePath = `${pathname}/index.html`; + const asFileAsset = pathname; + if (!isValidPageSource(implicitResourcePath, config) && !isValidFileAsset(implicitResourcePath, config) + && !isValidFileAsset(asFileAsset, config)) { + logger.warn(err); + return 'Intralink with no extension is neither a Page Source nor File Asset'; + } + if (hash !== undefined + && (!filePathToHashesMap.get(asFileAsset) || !filePathToHashesMap.get(asFileAsset)!.has(hash))) { + logger.warn(hashErr); + return 'Intralink with no extension is a valid Page Source or File Asset but hash is not found'; + } + return 'Intralink with no extension is a valid Page Source or File Asset'; +} + +/** + * Validates paths with .html extensions by checking page sources and file assets + */ +function validatePathWithHtmlExtension( + pathname: string, config: NodeProcessorConfig, err: string, + hashErr: string, hash: string | undefined, filePathToHashesMap: Map>): string { + if (!isValidPageSource(pathname, config) && !isValidFileAsset(pathname, config)) { + logger.warn(err); + return 'Intralink with ".html" extension is neither a Page Source nor File Asset'; + } + if (hash !== undefined) { + const filePath = `${pathname.slice(0, -5)}.md`; + if (!filePathToHashesMap.get(filePath) || !filePathToHashesMap.get(filePath)!.has(hash)) { + logger.warn(hashErr); + return 'Intralink with ".html" extension is a valid Page Source or File Asset but hash is not found'; + } + } + return 'Intralink with ".html" extension is a valid Page Source or File Asset'; +} + /** * Serves as an internal intra-link validator. Checks if the intra-links are valid. * If the intra-links are suspected to be invalid, a warning message will be logged. @@ -176,58 +232,29 @@ export function validateIntraLink(resourcePath: string, '${resourcePath}' found in file '${cwf}'`; const hashErr = `You might have an invalid hash for intra-link! Ignore this warning if it was intended.' ${resourcePath}' found in file '${cwf}'`; - resourcePath = urlUtil.stripBaseUrl(resourcePath, config.baseUrl); // eslint-disable-line no-param-reassign - const resourcePathUrl = parse(resourcePath); + const strippedResourcePath = urlUtil.stripBaseUrl(resourcePath, config.baseUrl); + const resourcePathUrl = parse(strippedResourcePath); const hash = resourcePathUrl.hash ? resourcePathUrl.hash.substring(1) : undefined; - resourcePath = resourcePathUrl.pathname; // eslint-disable-line no-param-reassign - - if (resourcePath.endsWith('/')) { - // append index.html to e.g. /userGuide/ - const implicitResourcePath = `${resourcePath}index.html`; - if (!isValidPageSource(implicitResourcePath, config) && !isValidFileAsset(implicitResourcePath, config)) { - logger.warn(err); - return 'Intralink ending with "/" is neither a Page Source nor File Asset'; - } - return 'Intralink ending with "/" is a valid Page Source or File Asset'; + const { pathname } = resourcePathUrl; + + // Route to appropriate validator based on path characteristics + if (pathname.endsWith('/')) { + return validatePathEndingWithSlash(pathname, config, err); } - const hasNoFileExtension = path.posix.extname(resourcePath) === ''; + const hasNoFileExtension = path.posix.extname(pathname) === ''; if (hasNoFileExtension) { - // does not end with '/' and no file ext (e.g. /userGuide) - const implicitResourcePath = `${resourcePath}/index.html`; - const asFileAsset = resourcePath; - if (!isValidPageSource(implicitResourcePath, config) && !isValidFileAsset(implicitResourcePath, config) - && !isValidFileAsset(asFileAsset, config)) { - logger.warn(err); - return 'Intralink with no extension is neither a Page Source nor File Asset'; - } - if (hash !== undefined - && (!filePathToHashesMap.get(asFileAsset) || !filePathToHashesMap.get(asFileAsset)!.has(hash))) { - logger.warn(hashErr); - return 'Intralink with no extension is a valid Page Source or File Asset but hash is not found'; - } - return 'Intralink with no extension is a valid Page Source or File Asset'; + return validatePathWithNoExtension(pathname, config, err, hashErr, hash, filePathToHashesMap); } - const hasHtmlExt = resourcePath.slice(-5) === '.html'; + const hasHtmlExt = pathname.slice(-5) === '.html'; if (hasHtmlExt) { - if (!isValidPageSource(resourcePath, config) && !isValidFileAsset(resourcePath, config)) { - logger.warn(err); - return 'Intralink with ".html" extension is neither a Page Source nor File Asset'; - } - if (hash !== undefined) { - const filePath = `${resourcePath.slice(0, -5)}.md`; - if (!filePathToHashesMap.get(filePath) || !filePathToHashesMap.get(filePath)!.has(hash)) { - logger.warn(hashErr); - return 'Intralink with ".html" extension is a valid Page Source or File Asset but hash is not found'; - } - } - return 'Intralink with ".html" extension is a valid Page Source or File Asset'; + return validatePathWithHtmlExtension(pathname, config, err, hashErr, hash, filePathToHashesMap); } // basic asset check - if (!isValidFileAsset(resourcePath, config)) { + if (!isValidFileAsset(pathname, config)) { logger.warn(err); return 'Intralink is not a File Asset'; } From 83dd767d087cbc3ffee9f0f9f964818649e8aa0c Mon Sep 17 00:00:00 2001 From: Harjun751 Date: Tue, 20 Jan 2026 14:29:50 +0800 Subject: [PATCH 6/6] Add defensive check in pathname --- packages/core/src/html/linkProcessor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/html/linkProcessor.ts b/packages/core/src/html/linkProcessor.ts index 6d05ceb496..aa64efe9c9 100644 --- a/packages/core/src/html/linkProcessor.ts +++ b/packages/core/src/html/linkProcessor.ts @@ -236,7 +236,7 @@ export function validateIntraLink(resourcePath: string, const strippedResourcePath = urlUtil.stripBaseUrl(resourcePath, config.baseUrl); const resourcePathUrl = parse(strippedResourcePath); const hash = resourcePathUrl.hash ? resourcePathUrl.hash.substring(1) : undefined; - const { pathname } = resourcePathUrl; + const pathname = resourcePathUrl.pathname ?? ''; // Route to appropriate validator based on path characteristics if (pathname.endsWith('/')) {