diff --git a/packages/core/src/html/linkProcessor.ts b/packages/core/src/html/linkProcessor.ts index 0f66d5841b..aa64efe9c9 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,62 +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); - 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 - } - 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 strippedResourcePath = urlUtil.stripBaseUrl(resourcePath, config.baseUrl); + const resourcePathUrl = parse(strippedResourcePath); + const hash = resourcePathUrl.hash ? resourcePathUrl.hash.substring(1) : undefined; + const pathname = resourcePathUrl.pathname ?? ''; + + // 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'; } diff --git a/packages/core/test/unit/html/linkProcessor.test.ts b/packages/core/test/unit/html/linkProcessor.test.ts index 5a64caa9dc..2e6160fc17 100644 --- a/packages/core/test/unit/html/linkProcessor.test.ts +++ b/packages/core/test/unit/html/linkProcessor.test.ts @@ -268,3 +268,52 @@ test('Test valid hash link', () => { expect(linkProcessor.validateIntraLink(mockResourcePath, mockCwf, mockConfig, mockMap)) .toEqual(EXPECTED_RESULT); }); + +test('Test link with query parameter', () => { + 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 valid link ending with no extension and query parameters', () => { + 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', () => { + 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'); +});