From 5fa79ebcdb1a0f2137e16f2ad6b5548d2d719149 Mon Sep 17 00:00:00 2001 From: John Hildenbiddle Date: Wed, 14 Oct 2020 19:11:54 -0500 Subject: [PATCH] Fix Vue content detection and global options - Add detection of Vue HTML directives to trigger mounting - Fix bug that processed markdown child nodes as global Vue instances even if vueGlobalOptions is not specified - Fix bug that prevented passing vueGlobalOptions to global instances if `data()` prop did not exist - Update Vue config check to test for existence of object keys instead of just the existence object - Update tests --- src/core/render/index.js | 56 +++++---- test/e2e/vue.test.js | 264 +++++++++++++++++++++++---------------- 2 files changed, 185 insertions(+), 135 deletions(-) diff --git a/src/core/render/index.js b/src/core/render/index.js index a76b4992..bedd8bb0 100644 --- a/src/core/render/index.js +++ b/src/core/render/index.js @@ -70,7 +70,6 @@ function renderMain(html) { if ( !vueGlobalData && docsifyConfig.vueGlobalOptions && - docsifyConfig.vueGlobalOptions.data && typeof docsifyConfig.vueGlobalOptions.data === 'function' ) { vueGlobalData = docsifyConfig.vueGlobalOptions.data(); @@ -110,35 +109,44 @@ function renderMain(html) { dom.find(markdownElm, cssSelector), vueConfig, ]) - .filter(([elm, vueConfig]) => elm) + .filter( + ([elm, vueConfig]) => elm && Object.keys(vueConfig || {}).length + ) ); // vueGlobalOptions - vueMountData.push( - ...dom - .findAll('.markdown-section > *') - // Remove duplicates - .filter(elm => !vueMountData.some(([e, c]) => e === elm)) - .map(elm => [ - elm, - !vueGlobalData - ? {} - : // Replace data() return value with shared data object. This - // mimics the behavior of a global store when using the same - // configuration with multiple Vue instances. - Object.assign({}, docsifyConfig.vueGlobalOptions || {}, { - data() { - return vueGlobalData; - }, - }), - ]) - ); + if (Object.keys(docsifyConfig.vueGlobalOptions || {}).length) { + vueMountData.push( + ...dom + .findAll('.markdown-section > *') + // Remove duplicates + .filter(elm => !vueMountData.some(([e, c]) => e === elm)) + .map(elm => [ + elm, + !vueGlobalData + ? docsifyConfig.vueGlobalOptions + : // Replace vueGlobalOptions data() return value with shared data + // object. This provides a global store for all Vue instances + // that receive vueGlobalOptions as their configuration. + Object.assign({}, docsifyConfig.vueGlobalOptions, { + data() { + return vueGlobalData; + }, + }), + ]) + ); + } for (const [mountElm, vueConfig] of vueMountData) { - const isValidTag = mountElm.tagName !== 'SCRIPT'; - const hasBrackets = /{{2}[^{}]*}{2}/.test(mountElm.outerHTML); + const isVueMount = + // Valid tag + mountElm.tagName !== 'SCRIPT' && + // Matches curly braces or HTML directives + /{{2}[^{}]*}{2}|\sv-(bind|cloak|else|else-if|for|html|if|is|model|on|once|pre|show|slot|text)=/.test( + mountElm.outerHTML + ); - if (isValidTag && hasBrackets && !isMountedVue(mountElm)) { + if (isVueMount && !isMountedVue(mountElm)) { if (vueVersion === 2) { new window.Vue(vueConfig).$mount(mountElm); } else if (vueVersion === 3) { diff --git a/test/e2e/vue.test.js b/test/e2e/vue.test.js index 395eac57..1bbd3bc0 100644 --- a/test/e2e/vue.test.js +++ b/test/e2e/vue.test.js @@ -1,141 +1,183 @@ +const stripIndent = require('common-tags/lib/stripIndent'); const docsifyInit = require('../helpers/docsify-init'); -describe('Vue.js Rendering', function() { - const vueURLs = [ - `${NODE_MODULES_URL}/vue2/dist/vue.js`, - `${NODE_MODULES_URL}/vue3/dist/vue.global.js`, - ]; +const vueURLs = [ + `${NODE_MODULES_URL}/vue2/dist/vue.js`, + `${NODE_MODULES_URL}/vue3/dist/vue.global.js`, +]; - // Tests - // --------------------------------------------------------------------------- - test(`ignores Vue content when window.Vue is not present`, async () => { - await docsifyInit({ - markdown: { - homepage: ` -
test{{ i }}
- `, - }, - }); - - await page.evaluate(() => { - return 'Vue' in window === false; - }); - await expect(page).toEqualText('#test', 'test{{ i }}'); - }); - - describe('Basic rendering', function() { - for (const vueURL of vueURLs) { - const vueVersion = vueURL.match(/vue(\d+)/)[1]; // vue2|vue3 - - for (const executeScript of ['unspecified', true, false]) { - test(`handles Vue v${vueVersion}.x basic rendering when executeScript is ${executeScript}`, async () => { - const docsifyInitConfig = { - markdown: { - homepage: ` -
test{{ i }}
- `, - }, - scriptURLs: vueURL, - }; - - if (executeScript !== 'unspecified') { - docsifyInitConfig.config = { - executeScript, +describe('Vue.js Compatibility', function() { + function getSharedConfig() { + const config = { + config: { + vueGlobalOptions: { + data: function() { + return { + counter: 0, + msg: 'vueglobaloptions', }; - } - - await docsifyInit(docsifyInitConfig); - - await expect(page).toEqualText('#test', 'test12345'); - }); - } - } - }); - - describe('Advanced usage', function() { - const testData = { - vue2: { - markdown: ` -
- - {{ counter }} -
- - - `, + }, + }, + vueOptions: { + '#vueoptions': { + data: function() { + return { + counter: 0, + msg: 'vueoptions', + }; + }, + }, + }, }, - vue3: { - markdown: ` -
+ markdown: { + homepage: stripIndent` + # {{ i }} + +
+

---

+ + {{ counter }} +
+ +
+

---

+ + {{ counter }} +
+ +
+

---

{{ counter }}
`, }, }; + return config; + } + + // Tests + // --------------------------------------------------------------------------- + describe('Ignores Vue', function() { + test(`content when Vue is not present`, async () => { + const docsifyInitConfig = getSharedConfig(); + + await docsifyInit(docsifyInitConfig); + await page.evaluate(() => { + return 'Vue' in window === false; + }); + await expect(page).toEqualText('h1', '{{ i }}'); + await expect(page).toEqualText('#vueglobaloptions p', '---'); + await expect(page).toEqualText('#vueoptions p', '---'); + await expect(page).toEqualText('#vuescript p', '---'); + }); + + test(`content when vueOptions and vueGlobalOptions are undefined`, async () => { + const docsifyInitConfig = getSharedConfig(); + + docsifyInitConfig.config.vueGlobalOptions = undefined; + docsifyInitConfig.config.vueOptions = undefined; + docsifyInitConfig.scriptURLs = vueURLs[0]; + + await docsifyInit(docsifyInitConfig); + await expect(page).toEqualText('h1', '{{ i }}'); + await expect(page).toEqualText('#vueglobaloptions p', '---'); + await expect(page).toEqualText('#vueoptions p', '---'); + await expect(page).toEqualText('#vuescript p', 'vuescript'); + }); + + test(`content when vueGlobalOptions data is undefined`, async () => { + const docsifyInitConfig = getSharedConfig(); + + docsifyInitConfig.config.vueGlobalOptions.data = undefined; + docsifyInitConfig.scriptURLs = vueURLs[0]; + + await docsifyInit(docsifyInitConfig); + await expect(page).toEqualText('h1', '{{ i }}'); + await expect(page).toEqualText('#vueoptions p', 'vueoptions'); + await expect(page).toEqualText('#vueglobaloptions p', '---'); + await expect(page).toEqualText('#vuescript p', 'vuescript'); + }); + + test(`content when vueOptions data is undefined`, async () => { + const docsifyInitConfig = getSharedConfig(); + + docsifyInitConfig.config.vueOptions['#vueoptions'].data = undefined; + docsifyInitConfig.scriptURLs = vueURLs[0]; + + await docsifyInit(docsifyInitConfig); + await expect(page).toEqualText('h1', '12345'); + await expect(page).toEqualText('#vueoptions p', 'vueglobaloptions'); + await expect(page).toEqualText('#vueglobaloptions p', 'vueglobaloptions'); + await expect(page).toEqualText('#vuescript p', 'vuescript'); + }); + + test(`