diff --git a/docs/devGuide/design/projectStructure.md b/docs/devGuide/design/projectStructure.md
index 5000eb9d62..dbe85e7c68 100644
--- a/docs/devGuide/design/projectStructure.md
+++ b/docs/devGuide/design/projectStructure.md
@@ -32,7 +32,7 @@ The MarkBind project is developed in a
These will not be converted:
google.com
markbind.org
diff --git a/packages/cli/test/functional/test_site/expected/testLinks.page-vue-render.js b/packages/cli/test/functional/test_site/expected/testLinks.page-vue-render.js index 3f39ae3faf..073f28f12d 100644 --- a/packages/cli/test/functional/test_site/expected/testLinks.page-vue-render.js +++ b/packages/cli/test/functional/test_site/expected/testLinks.page-vue-render.js @@ -11,7 +11,7 @@ with(this){return _c('div',{staticClass:"bg-info display-4 text-center text-whit with(this){return _c('p',[_c('strong',[_v("Relative Link Test")]),_v(" This is a relative Intra-Site link in a layout (see "),_c('a',{attrs:{"href":"/test_site/index.html#heading-with-hidden-keyword"}},[_v("link")]),_v(")")])} },function anonymous( ) { -with(this){return _c('div',{staticClass:"fixed-header-padding",attrs:{"id":"content-wrapper"}},[_c('h1',{attrs:{"id":"autolinks"}},[_c('span',{staticClass:"anchor",attrs:{"id":"autolinks"}}),_v("Autolinks"),_c('a',{staticClass:"fa fa-anchor",attrs:{"href":"#autolinks","onclick":"event.stopPropagation()"}})]),_v(" "),_c('p',[_v("A URL with "),_c('code',{pre:true,attrs:{"class":"line-numbers hljs inline no-lang"}},[_v("http(s)://")]),_v(" head or an email address in plain text will be auto converted into clickable links.")]),_v(" "),_c('p',[_v("This functionality is inherited from markdown-it, with the setting of "),_c('code',{pre:true,attrs:{"class":"line-numbers hljs inline no-lang"}},[_v("fuzzyLink")]),_v(" turned off.")]),_v(" "),_c('p',[_c('strong',[_v("These will be converted:")])]),_v(" "),_c('p',[_c('a',{attrs:{"href":"https://www.google.com"}},[_v("https://www.google.com")])]),_v(" "),_c('p',[_c('a',{attrs:{"href":"https://markbind.org"}},[_v("https://markbind.org")])]),_v(" "),_c('p',[_c('a',{attrs:{"href":"/test_site/mailto:foobar@gmail.com"}},[_v("foobar@gmail.com")])]),_v(" "),_c('p',[_c('strong',[_v("These will not be converted:")])]),_v(" "),_c('p',[_v("google.com")]),_v(" "),_c('p',[_v("markbind.org")]),_v(" "),_c('p',[_v("foo@bar")]),_v(" "),_c('p',[_c('strong',[_v("Tricks to prevent autolink:")])]),_v(" "),_c('p',[_c('code',{pre:true,attrs:{"class":"line-numbers hljs inline no-lang"}},[_v("https://markbind.org")])]),_v(" "),_c('p',[_v("https://"),_c('span'),_v("markbind.org")]),_v(" "),_c('i',{staticClass:"fa fa-arrow-circle-up fa-lg d-print-none",attrs:{"id":"scroll-top-button","onclick":"handleScrollTop()","aria-hidden":"true"}})])} +with(this){return _c('div',{staticClass:"fixed-header-padding",attrs:{"id":"content-wrapper"}},[_c('h1',{attrs:{"id":"autolinks"}},[_c('span',{staticClass:"anchor",attrs:{"id":"autolinks"}}),_v("Autolinks"),_c('a',{staticClass:"fa fa-anchor",attrs:{"href":"#autolinks","onclick":"event.stopPropagation()"}})]),_v(" "),_c('p',[_v("A URL with "),_c('code',{pre:true,attrs:{"class":"line-numbers hljs inline no-lang"}},[_v("http(s)://")]),_v(" head or an email address in plain text will be auto converted into clickable links.")]),_v(" "),_c('p',[_v("This functionality is inherited from markdown-it, with the setting of "),_c('code',{pre:true,attrs:{"class":"line-numbers hljs inline no-lang"}},[_v("fuzzyLink")]),_v(" turned off.")]),_v(" "),_c('p',[_c('strong',[_v("These will be converted:")])]),_v(" "),_c('p',[_c('a',{attrs:{"href":"https://www.google.com"}},[_v("https://www.google.com")])]),_v(" "),_c('p',[_c('a',{attrs:{"href":"https://markbind.org"}},[_v("https://markbind.org")])]),_v(" "),_c('p',[_c('a',{attrs:{"href":"mailto:foobar@gmail.com"}},[_v("foobar@gmail.com")])]),_v(" "),_c('p',[_c('strong',[_v("These will not be converted:")])]),_v(" "),_c('p',[_v("google.com")]),_v(" "),_c('p',[_v("markbind.org")]),_v(" "),_c('p',[_v("foo@bar")]),_v(" "),_c('p',[_c('strong',[_v("Tricks to prevent autolink:")])]),_v(" "),_c('p',[_c('code',{pre:true,attrs:{"class":"line-numbers hljs inline no-lang"}},[_v("https://markbind.org")])]),_v(" "),_c('p',[_v("https://"),_c('span'),_v("markbind.org")]),_v(" "),_c('i',{staticClass:"fa fa-arrow-circle-up fa-lg d-print-none",attrs:{"id":"scroll-top-button","onclick":"handleScrollTop()","aria-hidden":"true"}})])} },function anonymous( ) { with(this){return _c('div',[_c('footer',[_c('h1',{attrs:{"id":"heading-in-footer-should-not-be-indexed"}},[_c('span',{staticClass:"anchor",attrs:{"id":"heading-in-footer-should-not-be-indexed"}}),_v("Heading in footer should not be indexed"),_c('a',{staticClass:"fa fa-anchor",attrs:{"href":"#heading-in-footer-should-not-be-indexed","onclick":"event.stopPropagation()"}})]),_v(" "),_c('div',{staticClass:"text-center"},[_v("\n This is a dynamic height footer that supports markdown "),_c('span',[_v("😄")]),_v("!\n ")])])])} diff --git a/packages/core/src/html/SiteLinkManager.js b/packages/core/src/html/SiteLinkManager.js index 89317c2343..dbd9b5aee9 100644 --- a/packages/core/src/html/SiteLinkManager.js +++ b/packages/core/src/html/SiteLinkManager.js @@ -58,6 +58,10 @@ class SiteLinkManager { } const resourcePath = linkProcessor.getDefaultTagsResourcePath(node); + if (!linkProcessor.isIntraLink(resourcePath)) { + return 'Should not validate'; + } + this._addToCollection(resourcePath, cwf); return 'Intralink collected to be validated later'; } diff --git a/packages/core/src/html/linkProcessor.js b/packages/core/src/html/linkProcessor.js index cc6832edfd..c2e675ec81 100644 --- a/packages/core/src/html/linkProcessor.js +++ b/packages/core/src/html/linkProcessor.js @@ -37,12 +37,24 @@ function getResourcePathFromRoot(rootPath, fullResourcePath) { return fsUtil.ensurePosix(path.relative(rootPath, fullResourcePath)); } +/** + * @param {string} resourcePath parsed from the node's relevant attribute + * @returns {boolean} whether the resourcePath is a valid intra-site link + */ +function isIntraLink(resourcePath) { + const MAILTO_OR_TEL_REGEX = /^(?:mailto:|tel:)/i; + return resourcePath + && !urlUtil.isUrl(resourcePath) + && !resourcePath.startsWith('#') + && !MAILTO_OR_TEL_REGEX.test(resourcePath); +} + function _convertRelativeLink(node, cwf, rootPath, baseUrl, resourcePath, linkAttribName) { - if (!resourcePath) { + if (!isIntraLink(resourcePath)) { return; } - if (path.isAbsolute(resourcePath) || urlUtil.isUrl(resourcePath) || resourcePath.startsWith('#')) { + if (path.isAbsolute(resourcePath)) { // Do not rewrite. return; } @@ -148,7 +160,7 @@ function isValidFileAsset(resourcePath, config) { * @returns {string} these string return values are for unit testing purposes only */ function validateIntraLink(resourcePath, cwf, config) { - if (!resourcePath || urlUtil.isUrl(resourcePath) || resourcePath.startsWith('#')) { + if (!isIntraLink(resourcePath)) { return 'Not Intralink'; } @@ -246,4 +258,5 @@ module.exports = { convertMdExtToHtmlExt, validateIntraLink, collectSource, + isIntraLink, }; diff --git a/packages/core/src/utils/fsUtil.js b/packages/core/src/utils/fsUtil.js index 9c829547d5..7558f9641d 100644 --- a/packages/core/src/utils/fsUtil.js +++ b/packages/core/src/utils/fsUtil.js @@ -9,7 +9,10 @@ module.exports = { fileExists(filePath) { try { - return fs.statSync(filePath).isFile(); + // use decodeURIComponent to deal with space (%20) in file path, e.g + // from docs\images\dev%20diagrams\architecture.png + // to docs\images\dev diagrams\architecture.png + return fs.statSync(decodeURIComponent(filePath)).isFile(); } catch (err) { return false; } diff --git a/packages/core/test/unit/html/SiteLinkManager.test.js b/packages/core/test/unit/html/SiteLinkManager.test.js index aecd4d0557..8c0d2e232d 100644 --- a/packages/core/test/unit/html/SiteLinkManager.test.js +++ b/packages/core/test/unit/html/SiteLinkManager.test.js @@ -27,6 +27,26 @@ test('Test invalid URL link ', () => { expect(siteLinkManager.collectIntraLinkToValidate(mockNode, mockCwf)).toEqual(EXPECTED_RESULT); }); +test('Test mailto URL link', () => { + const siteLinkManager = getNewSiteLinkManager(); + const mockLink = 'Test'; + const mockNode = cheerio.parseHTML(mockLink)[0]; + + const EXPECTED_RESULT = 'Should not validate'; + + expect(siteLinkManager.collectIntraLinkToValidate(mockNode, mockCwf)).toEqual(EXPECTED_RESULT); +}); + +test('Test tel URL link', () => { + const siteLinkManager = getNewSiteLinkManager(); + const mockLink = 'Test'; + const mockNode = cheerio.parseHTML(mockLink)[0]; + + const EXPECTED_RESULT = 'Should not validate'; + + expect(siteLinkManager.collectIntraLinkToValidate(mockNode, mockCwf)).toEqual(EXPECTED_RESULT); +}); + test('Test link for disabled intralink validation', () => { const siteLinkManager = getNewSiteLinkManager(); const mockLink = 'Test'; diff --git a/packages/core/test/unit/html/linkProcessor.test.js b/packages/core/test/unit/html/linkProcessor.test.js index 06430583f0..a851245a42 100644 --- a/packages/core/test/unit/html/linkProcessor.test.js +++ b/packages/core/test/unit/html/linkProcessor.test.js @@ -11,6 +11,7 @@ const json = { './css/main.css': '3', './devGuide/index.html': '4', './rawFile': '5', + './spaced folder/img.png': '6', }; fs.vol.fromJSON(json, './src'); @@ -180,6 +181,24 @@ test('Test valid file asset links (png)', () => { expect(linkProcessor.validateIntraLink(mockResourcePath, mockCwf, mockConfig)).toEqual(EXPECTED_RESULT); }); +test('Test valid file asset links (with %20 in the file path)', () => { + const mockLink = 'Test'; + const mockNode = cheerio.parseHTML(mockLink)[0]; + const mockResourcePath = linkProcessor.getDefaultTagsResourcePath(mockNode); + const EXPECTED_RESULT = 'Intralink is a valid File Asset'; + + expect(linkProcessor.validateIntraLink(mockResourcePath, mockCwf, mockConfig)).toEqual(EXPECTED_RESULT); +}); + +test('Test valid file asset links (with space in the file path)', () => { + const mockLink = 'Test'; + const mockNode = cheerio.parseHTML(mockLink)[0]; + const mockResourcePath = linkProcessor.getDefaultTagsResourcePath(mockNode); + const EXPECTED_RESULT = 'Intralink is a valid File Asset'; + + expect(linkProcessor.validateIntraLink(mockResourcePath, mockCwf, mockConfig)).toEqual(EXPECTED_RESULT); +}); + test('Test valid file asset links (css)', () => { // should be checked as file asset const mockLink = 'Test'; @@ -201,3 +220,21 @@ test('Test invalid link for non-existent file asset (css)', () => { expect(linkProcessor.validateIntraLink(mockResourcePath, mockCwf, mockConfig)).toEqual(EXPECTED_RESULT); }); + +test('Test non intralinks (mailto)', () => { + const mockLink = 'Test Email'; + const mockNode = cheerio.parseHTML(mockLink)[0]; + const mockResourcePath = linkProcessor.getDefaultTagsResourcePath(mockNode); + const EXPECTED_RESULT = 'Not Intralink'; + + expect(linkProcessor.validateIntraLink(mockResourcePath, mockCwf, mockConfig)).toEqual(EXPECTED_RESULT); +}); + +test('Test non intralinks (tel)', () => { + const mockLink = 'Test Phone'; + const mockNode = cheerio.parseHTML(mockLink)[0]; + const mockResourcePath = linkProcessor.getDefaultTagsResourcePath(mockNode); + const EXPECTED_RESULT = 'Not Intralink'; + + expect(linkProcessor.validateIntraLink(mockResourcePath, mockCwf, mockConfig)).toEqual(EXPECTED_RESULT); +});