diff --git a/lib/extend.js b/lib/extend.js index 2840bc3..0d1f956 100644 --- a/lib/extend.js +++ b/lib/extend.js @@ -1,28 +1,22 @@ const utils = require('./utils'); -module.exports = (dom, extendingAppManifest) => { - const doc = dom.window.document; - const _$ = doc.querySelector.bind(doc); - const _$$ = doc.querySelectorAll.bind(doc); - utils.injectBundleIntoDom(dom, extendingAppManifest.bundle); - _$('[export]').setAttribute('export', extendingAppManifest.appName); - Array.from(_$$('[resource]')).forEach(e => - e.setAttribute('resource', 'resource') - ); - Array.from(_$$('script:not([resource]),style:not([resource]),link:not([resource])')).forEach(e => - e.parentElement.removeChild(e) - ); +module.exports = ($, extendingAppManifest) => { + utils.injectBundleIntoDom($, extendingAppManifest.bundle); + $('[export]').attr('export', extendingAppManifest.appName); + + $('script:not([resource]),style:not([resource]),link:not([resource])').remove(); + if (extendingAppManifest.overrides && extendingAppManifest.overrides.length) { extendingAppManifest.overrides.forEach(override => { - const elem = _$(`${override.tag}[public="${override.target}"]`); + const selector = `${override.tag}[public="${override.target}"]`; + const $elem = $(selector); let content = override.content; const superTagPattern = /()|(.*<\/\s*super>)/g; if (superTagPattern.test(content)) { - content = content.replace(superTagPattern, elem.innerHTML); + content = content.replace(superTagPattern, $elem.html()); } const tagName = override.target && override.target !== '' ? override.target : override.tag; - utils.replaceElement(dom, elem, tagName, content); + utils.replaceElement($, selector, tagName, content); }); } - return dom; }; diff --git a/lib/import.js b/lib/import.js index f5d0bd7..eccbe12 100644 --- a/lib/import.js +++ b/lib/import.js @@ -1,27 +1,21 @@ const utils = require('./utils'); -module.exports = (dom, importedApps = {}) => { +module.exports = ($, importedApps = {}) => { const injected = []; - const doc = dom.window.document; - const _$$ = doc.querySelectorAll.bind(doc); - Array.from(_$$('head [import]')).forEach(elem => - elem.parentNode.removeChild(elem) - ); - Array.from(_$$('fragment')).forEach(elem => { - const appName = utils.getAttr(elem, 'name'); + + $('head [import]').remove(); + + $('fragment').each((index, elem) => { + const appName = $(elem).attr('name'); if (importedApps[appName] && importedApps[appName].content) { // inject content - utils.replaceElement(dom, elem, appName, importedApps[appName].content); - if (injected.indexOf(appName) === -1) { - utils.injectBundleIntoDom(dom, importedApps[appName].bundle); + utils.replaceElement($, elem, appName, importedApps[appName].content); + if (!injected.includes(appName)) { + utils.injectBundleIntoDom($, importedApps[appName].bundle); injected.push(appName); } } else { - elem.parentNode.removeChild(elem); + $(elem).remove(elem); } }); - Array.from(_$$('[resource]')).forEach(e => - e.setAttribute('resource', 'resource') - ); - return dom; }; diff --git a/lib/manifest.js b/lib/manifest.js index 73e738e..dfa0fea 100644 --- a/lib/manifest.js +++ b/lib/manifest.js @@ -1,38 +1,40 @@ const utils = require('./utils'); -const JSDOM = require('JSDOM').JSDOM; module.exports = html => { if (!html || typeof html !== 'string') { return; } - const dom = utils.parse(html); - const _$ = dom.window.document.querySelector.bind(dom.window.document); - const _$$ = dom.window.document.querySelectorAll.bind(dom.window.document); - const manifest = utils.cleanObject({ - appName: utils.getAttr(_$('head meta[export]'), 'export'), - baseUrl: utils.getAttr(_$('head meta[public-url]'), 'public-url'), - extending: utils.getAttr(_$('head meta[export]'), 'extends') || undefined, - alias: utils.getAttr(_$('head meta[export-as]'), 'export-as') || undefined, - type: utils.getAttr(_$('head meta[of-type]'), 'of-type') || 'page', - uses: Array.from(_$$('head meta[import]')).map(elem => - utils.getAttr(elem, 'import') - ), - bundle: Array.from(_$$('[resource]')).map(elem => - utils.cleanObject(utils.toBundleItem(elem)) - ), - overrides: Array.from(_$$('[override]')).map(elem => - utils.cleanObject(utils.toOverrideItem(elem)) - ), - publics: Array.from(_$$('[public]')).map( - elem => utils.getAttr(elem, 'public') || elem.tagName - ), - content: utils.toContent(new JSDOM(dom.serialize())) || '', + const $ = utils.parse(html); + + const manifest = { + appName: $('head meta[export]').attr('export'), + baseUrl: $('head meta[public-url]').attr('public-url'), + extending: $('head meta[extends]').attr('extends'), + alias: $('head meta[export-as]').attr('export-as'), + type: $('head meta[of-type]').attr('of-type') || 'page', + uses: [], + bundle: [], + overrides: [], + publics: [], + content: utils.toContent(utils.parse(utils.serialize($))) || '', raw: utils.minify(html), - }); + }; + + $('meta[import]').each((index, elem) => { + manifest.uses.push($(elem).attr('import'))}); + $('[resource]').each((index, elem) => + manifest.bundle.push(utils.toBundleItem($, elem)) + ); + $('[override]').each((index, elem) => + manifest.overrides.push(utils.toOverrideItem($, elem)) + ); + $('[public]').each((index, elem) => + manifest.publics.push($(elem).attr('public') || elem.name) + ); if (!manifest.appName) { throw new Error(' is not available'); } - return manifest; + return utils.cleanObject(manifest); }; diff --git a/lib/mfhtml.js b/lib/mfhtml.js index e0a0b30..fee030e 100644 --- a/lib/mfhtml.js +++ b/lib/mfhtml.js @@ -29,7 +29,7 @@ class MFHTML { const manifest = generateManifest(html); this.graph[manifest.appName] = manifest; } catch (e) { - throw new Error('Not able to generate manifest for given html'); + throw new Error(e); } } else { throw new Error('mfhml.register requires an html'); @@ -39,26 +39,30 @@ class MFHTML { /** * Searches and retrieves the app by name and returns the generated html * @param {string} appName - * @param {null | string} baseUrl + * @param scriptToInject */ - get(appName, baseUrl = null) { + get(appName, scriptToInject) { const foundAppManifest = this.getManifest(appName); - const appBaseUrl = baseUrl || foundAppManifest.baseUrl; - let appDom = UrlFix(utils.parse(foundAppManifest.raw), appBaseUrl); + const appBaseUrl = foundAppManifest.baseUrl || ''; + let appDom = utils.parse(foundAppManifest.raw); + UrlFix(appDom, appBaseUrl); if (foundAppManifest.extending) { if (this.getAppNames().includes(foundAppManifest.extending)) { const superDom = utils.parse(this.get(foundAppManifest.extending)); foundAppManifest.bundle = foundAppManifest.bundle.map(bundle => ({ ...bundle, - path: bundle.path ? utils.combineUrl(appBaseUrl, bundle.path) : bundle.path, + path: bundle.path + ? utils.combineUrl(appBaseUrl, bundle.path) + : bundle.path, })); - appDom = Extend(superDom, foundAppManifest); + Extend(superDom, foundAppManifest); + appDom = superDom; } else { throw new Error(`undefined super '${foundAppManifest.extending}'`); } } if (foundAppManifest.uses && foundAppManifest.uses.length) { - appDom = Import( + Import( appDom, foundAppManifest.uses.reduce((importedAppManifest, importedAppName) => { if (this.getAppNames().includes(importedAppName)) { @@ -72,7 +76,11 @@ class MFHTML { }, {}) ); } - return utils.serialize(utils.cleanPrivates(appDom)); + utils.cleanPrivates(appDom); + if (scriptToInject) { + utils.injectScript(appDom, scriptToInject); + } + return utils.serialize(appDom); } /** diff --git a/lib/url-fixer.js b/lib/url-fixer.js index 7035ebf..ba59692 100644 --- a/lib/url-fixer.js +++ b/lib/url-fixer.js @@ -37,19 +37,18 @@ const ElementsWithUrlSelectors = { */ // will ignore inline styles -module.exports = (dom, baseUri) => { +module.exports = ($, baseUri) => { if (baseUri) { - _$$ = dom.window.document.querySelectorAll.bind(dom.window.document); Object.keys(ElementsWithUrlSelectors).forEach(attr => { const tags = ElementsWithUrlSelectors[attr]; const selector = tags.map(tag => `${tag}[${attr}]`).join(', '); - Array.from(_$$(selector)).forEach(elem => { - const attrValue = elem.getAttribute(attr); + $(selector).each((index, elem) => { + const $elem = $(elem); + const attrValue = $elem.attr(attr); if (attrValue && /https?:\/\//g.test(attrValue) !== true) { - elem.setAttribute(attr, utils.combineUrl(baseUri, attrValue)); + $elem.attr(attr, utils.combineUrl(baseUri, attrValue)); } }); }); } - return dom; }; diff --git a/lib/utils.js b/lib/utils.js index 2f9a57c..4f5038c 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,4 +1,4 @@ -const JSDOM = require('jsdom').JSDOM; +const cheerio = require('cheerio'); const htmlMinify = require('html-minifier').minify; const minifyOptions = { collapseWhitespace: true, @@ -7,22 +7,16 @@ const minifyOptions = { collapseBooleanAttributes: true, }; -const parse = html => new JSDOM(html); +const parse = html => cheerio.load(html); const minify = html => htmlMinify(html, minifyOptions); -const serialize = dom => dom && dom.serialize && minify(dom.serialize(dom)); +const serialize = $ => $ && minify($.html()); -const getAttr = (elem, attr) => elem && elem.getAttribute(attr); - -const setAttr = (elem, attr, value) => elem.setAttribute(attr, value); - -const replaceElement = (dom, elem, tag, content) => { +const replaceElement = ($, elemSelector, tag, content) => { if (content) { - const doc = dom.window.document; - const wrapper = doc.createElement(tag); - wrapper.innerHTML = content; - elem && elem.parentNode.replaceChild(wrapper, elem); + const wrapper = `<${tag}>${content}`; + $(elemSelector).replaceWith(wrapper); } }; @@ -43,32 +37,21 @@ const cleanObject = obj => return res; }, {}); -const toBundleItem = elem => ({ - type: elem.tagName, - path: - elem.tagName === 'LINK' - ? elem.getAttribute('href') - : elem.getAttribute('src'), - content: minify(elem.innerHTML), - attributes: Object.assign( - {}, - ...Array.from(elem.attributes, ({ name, value }) => ({ [name]: value })) - ), +const toBundleItem = ($, elem) => ({ + type: elem.name.toUpperCase(), + content: minify($(elem).html()), + attributes: cleanObject(Object.assign({}, elem.attribs || {})), }); -const toOverrideItem = elem => ({ - tag: elem.tagName, - target: elem.getAttribute('override') || '', - content: elem.innerHTML, +const toOverrideItem = ($, elem) => ({ + tag: elem.name.toUpperCase(), + target: $(elem).attr('override'), + content: $(elem).html(), }); -const toContent = dom => { - Array.from(dom.window.document.getElementsByTagName('script')).forEach( - elem => { - elem.parentNode.removeChild(elem); - } - ); - return minify(dom.window.document.body.innerHTML); +const toContent = $ => { + $('script').remove(); + return minify($('body').html()); }; const combineUrl = (prefix, urlPart) => @@ -76,59 +59,49 @@ const combineUrl = (prefix, urlPart) => .join('/') .replace('/./', '/'); -const injectBundleIntoDom = (dom, bundle) => { +const attributesToString = attributes => + Object.entries(attributes) + .map(([atr, value]) => `${atr}="${value}"`) + .join(' '); + +const injectBundleIntoDom = ($, bundle) => { if (!bundle || !bundle.length) { return; } - const doc = dom.window.document; - const head = doc.getElementsByTagName('head')[0]; - const body = doc.body; + const headerTypes = ['LINK', 'STYLE']; + const bodyTypes = ['SCRIPT']; + const headContent = bundle + .filter(({ type }) => headerTypes.includes(type)) + .map( + ({ type, content, attributes }) => + `<${type} ${attributesToString(attributes)}>${content}` + ) + .join(''); + const bodyContent = bundle + .filter(({ type }) => bodyTypes.includes(type)) + .map( + ({ type, content, attributes }) => + `<${type} ${attributesToString(attributes)}>${content}` + ) + .join(''); + $('head').append(headContent); + $('body').append(bodyContent); +}; - bundle.forEach(item => { - const typesWithContent = ['SCRIPT', 'STYLE']; - const typesWithPath = ['SCRIPT', 'LINK']; - const { type, path, content, attributes } = item; - const itemElem = doc.createElement(type); - Object.entries(attributes).forEach(([attr, value]) => { - itemElem.setAttribute(attr, value); - }); - itemElem.setAttribute('resource', 'resource'); - if (typesWithPath.some(t => t === type) && path) { - itemElem.setAttribute(type === 'LINK' ? 'href' : 'src', path); - } - if (typesWithContent.some(t => t === type) && content) { - const textNode = doc.createTextNode(content); - itemElem.appendChild(textNode); - } - switch (type) { - case 'LINK': - case 'STYLE': - head.appendChild(itemElem); - break; - case 'SCRIPT': - body.appendChild(itemElem); - break; - default: - throw new Error('Unexpected type for bundle item ' + type); - } - }); +const cleanPrivates = $ => { + $('[private]').remove(); }; -const cleanPrivates = dom => { - const doc = dom.window.document; - const _$$ = doc.querySelectorAll.bind(doc); - Array.from(_$$('[private]')).forEach(elem => - elem.parentNode.removeChild(elem) - ); - return dom; +const injectScript = ($, script) => { + $('script') + .first() + .before(script); }; module.exports = { parse, minify, serialize, - getAttr, - setAttr, replaceElement, cleanObject, toBundleItem, @@ -137,4 +110,5 @@ module.exports = { combineUrl, injectBundleIntoDom, cleanPrivates, + injectScript, }; diff --git a/package.json b/package.json index b64ae3d..896e3b6 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,8 @@ "test": "mocha --watch --recursive" }, "dependencies": { - "html-minifier": "^3.5.21", - "jsdom": "^13.2.0" + "cheerio": "^1.0.0-rc.3", + "html-minifier": "^3.5.21" }, "devDependencies": { "chai": "^4.2.0", diff --git a/test/extend/app.html.js b/test/extend/app.html.js index fb97c72..9514865 100644 --- a/test/extend/app.html.js +++ b/test/extend/app.html.js @@ -1,4 +1,4 @@ -exports.SomeApp = /*html*/ ` +exports.SomeApp = ` @@ -12,7 +12,7 @@ exports.SomeApp = /*html*/ ` `; -exports.SomeApp1 = /*html*/ ` +exports.SomeApp1 = ` @@ -31,18 +31,18 @@ exports.SomeApp1 = /*html*/ ` `; -exports.Expected = /*html*/ ` +exports.Expected = ` SomeApp Title - - + +
Header Override
Content Override
- - + + `; diff --git a/test/extend/extend.js b/test/extend/extend.js index d51cf5f..8ce44f7 100644 --- a/test/extend/extend.js +++ b/test/extend/extend.js @@ -11,8 +11,8 @@ const someAppManifest = manifest(someApp); describe('Extend', () => { it('should extend SomeApp1 with SomeApp and produced expected html', () => { - const processed = extend(someApp1Parsed, someAppManifest); - const serialized = utils.serialize(processed); + extend(someApp1Parsed, someAppManifest); + const serialized = utils.serialize(someApp1Parsed); expect(serialized).to.be.equal(utils.minify(Expected)); }); }); diff --git a/test/import/app.html.js b/test/import/app.html.js index b5e12c9..b5791df 100644 --- a/test/import/app.html.js +++ b/test/import/app.html.js @@ -1,4 +1,4 @@ -exports.SomeApp = /*html*/ ` +exports.SomeApp = ` @@ -19,7 +19,7 @@ exports.SomeApp = /*html*/ ` `; -exports.SomeApp1 = /*html*/ ` +exports.SomeApp1 = ` @@ -32,13 +32,13 @@ exports.SomeApp1 = /*html*/ ` `; -exports.Expected = /*html*/ ` +exports.Expected = ` SomeApp Title - - + + @@ -50,10 +50,10 @@ exports.Expected = /*html*/ `
Some App 1 body
- - - + + `; diff --git a/test/import/import.js b/test/import/import.js index ac5d4d9..612a26c 100644 --- a/test/import/import.js +++ b/test/import/import.js @@ -11,8 +11,8 @@ const someApp1Manifest = manifest(someApp1); describe('Import', () => { it('should replace fragment and produce expected html', () => { - const processed = _import(someAppParsed, { SomeApp1: someApp1Manifest }); - const serialized = utils.serialize(processed); + _import(someAppParsed, { SomeApp1: someApp1Manifest }); + const serialized = utils.serialize(someAppParsed); expect(serialized).to.be.equal(utils.minify(Expected)); }); }); diff --git a/test/manifest/app.html.js b/test/manifest/app.html.js index 9d897e9..5515991 100644 --- a/test/manifest/app.html.js +++ b/test/manifest/app.html.js @@ -1,4 +1,4 @@ -exports.app = /*html*/ ` +exports.app = ` diff --git a/test/mfhtml/mfhtml.js b/test/mfhtml/mfhtml.js index 219aaed..a6d8ed2 100644 --- a/test/mfhtml/mfhtml.js +++ b/test/mfhtml/mfhtml.js @@ -78,7 +78,7 @@ describe('MFHTML runtime', () => { it('should throw error on register with wrong html', () => { expect(() => mfhtml.register(mock.badHtml)).to.throw( - 'Not able to generate manifest for given html' + ' is not available' ); }); diff --git a/test/mfhtml/mock.html.js b/test/mfhtml/mock.html.js index 6256f05..e4efd09 100644 --- a/test/mfhtml/mock.html.js +++ b/test/mfhtml/mock.html.js @@ -1,13 +1,13 @@ const utils = require('../../lib/utils'); -exports.badHtml = /*html*/ ` +exports.badHtml = ` Bad Html Bad Html Body `; -const noDependencyApp = /*html*/ ` +const noDependencyApp = ` No dependency App @@ -41,7 +41,7 @@ exports.noDependencyAppManifest = { raw: utils.minify(noDependencyApp), }; -exports.App = /*html*/ ` +exports.App = ` @@ -72,7 +72,7 @@ exports.App = /*html*/ ` `; -exports.SomeApp1 = /*html*/ ` +exports.SomeApp1 = ` @@ -85,7 +85,7 @@ exports.SomeApp1 = /*html*/ ` `; -exports.SomeApp2 = /*html*/ ` +exports.SomeApp2 = ` @@ -99,7 +99,7 @@ exports.SomeApp2 = /*html*/ ` `; -exports.SomeApp3 = /*html*/ ` +exports.SomeApp3 = ` @@ -113,7 +113,7 @@ exports.SomeApp3 = /*html*/ ` `; -exports.ExtendableApp = /*html*/ ` +exports.ExtendableApp = ` diff --git a/test/url-fixer/app.html.js b/test/url-fixer/app.html.js index ca78ae7..f74fdd3 100644 --- a/test/url-fixer/app.html.js +++ b/test/url-fixer/app.html.js @@ -14,12 +14,12 @@ exports.SomeApp = ` exports.Expected = ` - - + + image - + `; diff --git a/test/url-fixer/url-fixer.js b/test/url-fixer/url-fixer.js index c2e4690..f532515 100644 --- a/test/url-fixer/url-fixer.js +++ b/test/url-fixer/url-fixer.js @@ -6,52 +6,52 @@ const Expected = require('./app.html').Expected; describe('Url Fix', () => { it('should fix img url', () => { - const dom = utils.parse(someApp); - urlFix(dom, 'http://some.domain.name'); - expect( - dom.window.document.getElementById('img1').getAttribute('src') - ).to.be.equal('http://some.domain.name/assets/someimage.jpg'); + const $ = utils.parse(someApp); + urlFix($, 'http://some.domain.name'); + expect($('#img1').attr('src')).to.be.equal( + 'http://some.domain.name/assets/someimage.jpg' + ); }); it('should fix link url', () => { - const dom = utils.parse(someApp); - urlFix(dom, 'http://some.domain.name'); - expect( - dom.window.document.getElementById('link1').getAttribute('href') - ).to.be.equal('http://some.domain.name/someapp.css'); + const $ = utils.parse(someApp); + urlFix($, 'http://some.domain.name'); + expect($('#link1').attr('href')).to.be.equal( + 'http://some.domain.name/someapp.css' + ); }); it('should not fix link absolute url', () => { - const dom = utils.parse(someApp); - urlFix(dom, 'http://some.domain.name'); - expect( - dom.window.document.getElementById('link2').getAttribute('href') - ).to.be.equal('https://cdn.server.com/someapp.css?13456'); + const $ = utils.parse(someApp); + urlFix($, 'http://some.domain.name'); + expect($('#link2').attr('href')).to.be.equal( + 'https://cdn.server.com/someapp.css?13456' + ); }); it('should fix script url', () => { - const dom = utils.parse(someApp); - urlFix(dom, 'http://some.domain.name'); - expect( - dom.window.document.getElementById('script1').getAttribute('src') - ).to.be.equal('http://some.domain.name/js/someapp.js'); + const $ = utils.parse(someApp); + urlFix($, 'http://some.domain.name'); + expect($('#script1').attr('src')).to.be.equal( + 'http://some.domain.name/js/someapp.js' + ); }); it('should fix video src and poster url', () => { - const dom = utils.parse(someApp); - urlFix(dom, 'http://some.domain.name'); - expect( - dom.window.document.getElementById('video1').getAttribute('poster') - ).to.be.equal('http://some.domain.name/someposter.png'); - expect( - dom.window.document.getElementById('video1').getAttribute('src') - ).to.be.equal('http://some.domain.name/somevideo.mpeg'); + const $ = utils.parse(someApp); + urlFix($, 'http://some.domain.name'); + expect($('#video1').attr('poster')).to.be.equal( + 'http://some.domain.name/someposter.png' + ); + expect($('#video1').attr('src')).to.be.equal( + 'http://some.domain.name/somevideo.mpeg' + ); }); it('should fix SomeApp with http://some.domain.name and produce expected html', () => { - const dom = utils.parse(someApp); - urlFix(dom, 'http://some.domain.name'); - const serialized = utils.serialize(dom); + const $ = utils.parse(someApp); + urlFix($, 'http://some.domain.name'); + const serialized = utils.serialize($); expect(serialized).to.be.equal(utils.minify(Expected)); }); });