Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 69 additions & 46 deletions packages/core/src/html/linkProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, Set<string>>): 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, Set<string>>): 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.
Expand All @@ -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';
}
Expand Down
49 changes: 49 additions & 0 deletions packages/core/test/unit/html/linkProcessor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<a href="/index.html?param=value">Test</a>';
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 = '<a href="/userGuide?param=value">Test</a>';
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 = '<a href="/missingRawFile?param=value">Test</a>';
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 = '<a href="/userGuide/raw.html?param=value#test-1">Test</a>';
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<string, Set<string>>();
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 = '<a href="/userGuide/raw.html?param=value#test-1">Test</a>';
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');
});