Add vueComponents support

This commit is contained in:
John Hildenbiddle 2020-10-15 23:25:02 -05:00
parent 5fa79ebcdb
commit a2386e5a59
2 changed files with 189 additions and 120 deletions

View File

@ -66,15 +66,6 @@ function renderMain(html) {
.findAll('.markdown-section > *')
.filter(elm => isMountedVue(elm));
// Store global data() return value as shared data object
if (
!vueGlobalData &&
docsifyConfig.vueGlobalOptions &&
typeof docsifyConfig.vueGlobalOptions.data === 'function'
) {
vueGlobalData = docsifyConfig.vueGlobalOptions.data();
}
// Destroy/unmount existing Vue instances
for (const mountedElm of mountedElms) {
if (vueVersion === 2) {
@ -101,6 +92,27 @@ function renderMain(html) {
// Handle Vue content not mounted by markdown <script>
if ('Vue' in window) {
const vueMountData = [];
const vueComponentNames = Object.keys(docsifyConfig.vueComponents || {});
// Register global vueComponents
if (vueVersion === 2 && vueComponentNames.length) {
vueComponentNames.forEach(name => {
const isNotRegistered = !window.Vue.options.components[name];
if (isNotRegistered) {
window.Vue.component(name, docsifyConfig.vueComponents[name]);
}
});
}
// Store global data() return value as shared data object
if (
!vueGlobalData &&
docsifyConfig.vueGlobalOptions &&
typeof docsifyConfig.vueGlobalOptions.data === 'function'
) {
vueGlobalData = docsifyConfig.vueGlobalOptions.data();
}
// vueOptions
vueMountData.push(
@ -109,48 +121,82 @@ function renderMain(html) {
dom.find(markdownElm, cssSelector),
vueConfig,
])
.filter(
([elm, vueConfig]) => elm && Object.keys(vueConfig || {}).length
)
.filter(([elm, vueConfig]) => elm)
);
// vueGlobalOptions
if (Object.keys(docsifyConfig.vueGlobalOptions || {}).length) {
if (docsifyConfig.vueGlobalOptions || vueComponentNames.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;
},
}),
])
// Detect Vue content
.filter(elm => {
const isVueMount =
// is a component
elm.tagName.toLowerCase() in
(docsifyConfig.vueComponents || {}) ||
// has a component(s)
elm.querySelector(vueComponentNames.join(',') || null) ||
// has brackets
(docsifyConfig.vueGlobalOptions &&
/{{2}[^{}]*}{2}/.test(elm.outerHTML)) ||
// has directive
/{\sv-(bind|cloak|else|else-if|for|html|if|is|model|on|once|pre|show|slot|text)=/.test(
elm.outerHTML
);
return isVueMount;
})
.map(elm => {
// Clone global configuration
const vueConfig = Object.assign(
{},
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.
if (vueGlobalData) {
vueConfig.data = function() {
return vueGlobalData;
};
}
return [elm, vueConfig];
})
);
}
// Mount
for (const [mountElm, vueConfig] of vueMountData) {
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
);
const isVueAttr = 'data-isvue';
const isSkipElm =
// Is an invalid tag
mountElm.matches('pre, script') ||
// Is a mounted instance
isMountedVue(mountElm) ||
// Has mounted instance(s)
mountElm.querySelector(`[${isVueAttr}]`);
if (!isSkipElm) {
mountElm.setAttribute(isVueAttr, '');
if (isVueMount && !isMountedVue(mountElm)) {
if (vueVersion === 2) {
new window.Vue(vueConfig).$mount(mountElm);
} else if (vueVersion === 3) {
window.Vue.createApp(vueConfig).mount(mountElm);
const app = window.Vue.createApp(vueConfig);
// Register global vueComponents
vueComponentNames.forEach(name => {
const config = docsifyConfig.vueComponents[name];
app.component(name, config);
});
app.mount(mountElm);
}
}
}

View File

@ -10,6 +10,18 @@ describe('Vue.js Compatibility', function() {
function getSharedConfig() {
const config = {
config: {
vueComponents: {
'button-counter': {
template: `
<button @click="counter++">{{ counter }}</button>
`,
data: function() {
return {
counter: 0,
};
},
},
},
vueGlobalOptions: {
data: function() {
return {
@ -31,15 +43,17 @@ describe('Vue.js Compatibility', function() {
},
markdown: {
homepage: stripIndent`
# <span v-for="i in 5">{{ i }}</span>
<div id="vuefor"><span v-for="i in 5">{{ i }}</span></div>
<div id="vueoptions">
<button-counter id="vuecomponent">---</button-counter>
<div id="vueglobaloptions">
<p v-text="msg">---</p>
<button v-on:click="counter += 1">+</button>
<span>{{ counter }}<span>
</div>
<div id="vueglobaloptions">
<div id="vueoptions">
<p v-text="msg">---</p>
<button v-on:click="counter += 1">+</button>
<span>{{ counter }}<span>
@ -79,105 +93,114 @@ describe('Vue.js Compatibility', function() {
// 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(`<script> when executeScript is false`, async () => {
const docsifyInitConfig = getSharedConfig();
docsifyInitConfig.config.executeScript = false;
docsifyInitConfig.scriptURLs = vueURLs[0];
await docsifyInit(docsifyInitConfig);
await expect(page).toEqualText('#vuescript p', 'vueglobaloptions');
});
});
describe('Renders Vue', function() {
for (const vueURL of vueURLs) {
const vueVersion = Number(vueURL.match(/vue(\d+)/)[1]); // 2|3
for (const vueURL of vueURLs) {
const vueVersion = Number(vueURL.match(/vue(\d+)/)[1]); // 2|3
describe(`Vue v${vueVersion}`, function() {
for (const executeScript of [true, undefined]) {
test(`Vue v${vueVersion}: renders when executeScript is ${executeScript}`, async () => {
const docsifyInitConfig = getSharedConfig(vueVersion);
test(`renders content when executeScript is ${executeScript}`, async () => {
const docsifyInitConfig = getSharedConfig();
docsifyInitConfig.config.executeScript = executeScript;
docsifyInitConfig.scriptURLs = vueURL;
await docsifyInit(docsifyInitConfig);
// Static data
await expect(page).toEqualText('h1', '12345');
await expect(page).toEqualText('#vueoptions p', 'vueoptions');
// Static
await expect(page).toEqualText('#vuefor', '12345');
await expect(page).toEqualText('#vuecomponent', '0');
await expect(page).toEqualText(
'#vueglobaloptions p',
'vueglobaloptions'
);
await expect(page).toEqualText('#vuescript p', 'vuescript');
// Reactive data
await expect(page).toEqualText('#vueoptions span', '0');
await page.click('#vueoptions button');
await expect(page).toEqualText('#vueoptions span', '1');
await expect(page).toEqualText('#vueglobaloptions span', '0');
await expect(page).toEqualText('#vueoptions p', 'vueoptions');
await expect(page).toEqualText('#vueoptions span', '0');
await expect(page).toEqualText('#vuescript p', 'vuescript');
await expect(page).toEqualText('#vuescript span', '0');
// Reactive
await page.click('#vuecomponent');
await expect(page).toEqualText('#vuecomponent', '1');
await page.click('#vueglobaloptions button');
await expect(page).toEqualText('#vueglobaloptions span', '1');
await expect(page).toEqualText('#vuescript span', '0');
await page.click('#vueoptions button');
await expect(page).toEqualText('#vueoptions span', '1');
await page.click('#vuescript button');
await expect(page).toEqualText('#vuescript span', '1');
});
}
}
});
test(`ignores 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('#vuefor', '{{ i }}');
await expect(page).toEqualText('#vuecomponent', '---');
await expect(page).toEqualText('#vueglobaloptions p', '---');
await expect(page).toEqualText('#vueoptions p', '---');
await expect(page).toEqualText('#vuescript p', '---');
});
test(`ignores content when vueComponents, vueOptions, and vueGlobalOptions are undefined`, async () => {
const docsifyInitConfig = getSharedConfig();
docsifyInitConfig.config.vueComponents = undefined;
docsifyInitConfig.config.vueGlobalOptions = undefined;
docsifyInitConfig.config.vueOptions = undefined;
docsifyInitConfig.scriptURLs = vueURL;
await docsifyInit(docsifyInitConfig);
await expect(page).toEqualText('#vuefor', '{{ i }}');
await expect(page).toEqualText('#vuecomponent', '---');
await expect(page).toEqualText('#vueglobaloptions p', '---');
await expect(page).toEqualText('#vueoptions p', '---');
await expect(page).toEqualText('#vuescript p', 'vuescript');
});
test(`ignores content when vueGlobalOptions is undefined`, async () => {
const docsifyInitConfig = getSharedConfig();
docsifyInitConfig.config.vueGlobalOptions = undefined;
docsifyInitConfig.scriptURLs = vueURL;
await docsifyInit(docsifyInitConfig);
await expect(page).toEqualText('#vuefor', '{{ i }}');
await expect(page).toEqualText('#vuecomponent', '0');
await expect(page).toEqualText('#vueglobaloptions p', '---');
await expect(page).toEqualText('#vueoptions p', 'vueoptions');
await expect(page).toEqualText('#vuescript p', 'vuescript');
});
test(`ignores content when vueOptions is undefined`, async () => {
const docsifyInitConfig = getSharedConfig();
docsifyInitConfig.config.vueOptions['#vueoptions'] = undefined;
docsifyInitConfig.scriptURLs = vueURL;
await docsifyInit(docsifyInitConfig);
await expect(page).toEqualText('#vuefor', '12345');
await expect(page).toEqualText('#vuecomponent', '0');
await expect(page).toEqualText(
'#vueglobaloptions p',
'vueglobaloptions'
);
await expect(page).toEqualText('#vueoptions p', 'vueglobaloptions');
await expect(page).toEqualText('#vuescript p', 'vuescript');
});
test(`ignores <script> when executeScript is false`, async () => {
const docsifyInitConfig = getSharedConfig();
docsifyInitConfig.config.executeScript = false;
docsifyInitConfig.scriptURLs = vueURL;
await docsifyInit(docsifyInitConfig);
await expect(page).toEqualText('#vuescript p', 'vueglobaloptions');
});
});
}
});