/** * Silex, live web creation * http://projects.silexlabs.org/?/silex/ * * Copyright (c) 2012 Silex Labs * http://www.silexlabs.org/ * * Silex is available under the GPL license * http://www.silexlabs.org/silex/silex-licensing/ */ import * as Path from 'path'; import { URL } from 'url'; import { Constants } from '../../Constants'; import DomTools from '../utils/DomTools'; export interface File { original: string; srcPath: string; destPath: string; tagName: string; displayName: string; } export interface Action { name: string; path: string; displayName: string; content: string; } /** * @fileoverview Helper class used to cleanup the DOM when publishing a website * */ export class DomPublisher { private doc: HTMLDocument; constructor(private dom, private userHead, private rootUrl, private rootPath, private getDestFolder) { this.doc = dom.window.document; } /** * remove the javascript and css files which firefox inlines * the inlined tags are script type="text/javascript" style="display:none" * @param {Document} doc */ cleanupFirefoxInlines() { // remove inlined scripts and styles ['script', 'style'].forEach((tagName) => { Array.from(this.doc.querySelectorAll(`${tagName}[style="display:none"]`)) .forEach((element) => { element.parentElement.removeChild(element); }); }); } /** * cleanup html page * remove Silex specific data from HTML * create an external CSS file * generates a list of js scripts and assets to be eported with the file * @return {{htmlString: string, cssString: string, jsString: string, files: Array.}} an object with * html: the cleaned up raw HTML {string} or null if an error occured * css: list of css files * js: a script included in the html * files: list of assets files */ cleanup() { // this.cleanupFirefoxInlines(); // remove publication path // remove JSON styles // remove prodotype previews Array.from(this.doc.querySelectorAll(Constants.ELEMENTS_TO_REMOVE_AT_PUBLISH.join(', '))) .forEach((tagToRemove) => { tagToRemove.parentElement.removeChild(tagToRemove); }); // remove data-silex-id // remove data-silex-static (will then be downloaded like any other script, not striped by DomTools.transformPath) // remove data-dependency // do NOT remove data-silex-type because it is used by front-end.js at runtime Array.from(this.doc.querySelectorAll(`[${Constants.TYPE_ATTR}], [${Constants.ELEMENT_ID_ATTR_NAME}], [${Constants.STATIC_ASSET_ATTR}]`)) .forEach((tagToClean) => { tagToClean.removeAttribute(Constants.ELEMENT_ID_ATTR_NAME); tagToClean.removeAttribute(Constants.STATIC_ASSET_ATTR); tagToClean.removeAttribute('data-dependency'); }); } split(newFirstPageName: string): Action[] { this.doc.body.classList.add(Constants.WEBSITE_CONTEXT_PUBLISHED_CLASS_NAME); // remove unused scripts when there is no deeplink navigation anymore ['js/jquery-ui.js', 'js/pageable.js'] .map((path) => this.doc.querySelector(`script[src="${ path }"]`)) .filter((el) => !!el) // when not updated yet to the latest version, the URLs are not relative .forEach((el) => el.parentElement.removeChild(el)); // split in multiple pages const pages = Array.from(this.doc.querySelectorAll(`a[${Constants.TYPE_ATTR}="${Constants.TYPE_PAGE}"]`)); const initialFirstPageName = pages[0].getAttribute('id'); return pages .map((el, idx) => { return { name: el.getAttribute('id'), displayName: el.innerHTML, fileName: el.getAttribute('id') === initialFirstPageName && newFirstPageName ? 'index.html' : (el.getAttribute('id').substr('page-'.length) + '.html'), }; }) .map(({displayName, name, fileName}) => { // clone the document const clone = this.doc.cloneNode(true) as HTMLDocument; // update title (TODO: description and SEO) (clone.head.querySelector('title') || ({} as HTMLTitleElement)).innerHTML += ' - ' + displayName; // remove elements from other pages Array.from(clone.querySelectorAll(`.${Constants.PAGED_CLASS_NAME}`)) .forEach((el) => { if (el.classList.contains(name)) { el.classList.add('page-link-active'); } else { el.parentElement.removeChild(el); } }); // update links Array.from(clone.querySelectorAll('a')) .filter((el) => el.hash.startsWith('#!')) .forEach((el) => { const [pageName, anchor] = el.hash.substr('#!'.length).split('#'); el.href = (pageName === initialFirstPageName && newFirstPageName ? 'index.html' : pageName.substr('page-'.length) + '.html') + (anchor ? '#' + anchor : ''); if (pageName === name) { el.classList.add('page-link-active'); } else { el.classList.remove('page-link-active'); // set when you save the file } }); // remove useless css classes // do not do this before as these classes are needed until the last moment, e.g. to select paged elements Constants.SILEX_CLASS_NAMES_TO_REMOVE_AT_PUBLISH.forEach((className) => { Array.from(clone.getElementsByClassName(className)) .forEach((el: HTMLElement) => el.classList.remove(className)); }); // create a unifile batch action return { name: 'writefile', path: this.rootPath + '/' + this.getDestFolder('.html', null) + '/' + fileName, displayName: fileName, // FIXME: this is not part of a unifile aciton content: '' + clone.documentElement.outerHTML, }; }); } extractAssets(baseUrl: string|URL): {scriptTags: HTMLElement[], styleTags: HTMLElement[], files: File[]} { // all scripts, styles and assets from head => local const files: File[] = []; DomTools.transformPaths(this.dom, (path, el, isInHead) => { // el may be null if the path comes from the JSON object holding Silex data // This is never supposed to happen because the tag holding the JSON object // is removed from the head tag in DomPublisher::cleanup. // But sometimes it appears that the tags are in the body // Maybe we should change cleanup to look for the tagsToRemove also in the body? const tagName = el ? el.tagName : null; const url = new URL(path, baseUrl); if (this.isDownloadable(url)) { const fileName = Path.basename(url.pathname); const destFolder = this.getDestFolder(Path.extname(url.pathname), tagName); if (destFolder) { const destPath = `${destFolder}/${fileName}`; files.push({ original: path, srcPath: url.href, destPath: this.rootPath + '/' + destPath, tagName, displayName: fileName, }); if (tagName) { // not an URL from a style sheet return destPath; } else if (isInHead) { // URL from a style sheet // called from '/css' return '../' + destPath; } // URL from a style sheet // called from './' because it is in the body and not moved to an external CSS return destPath; } } return null; }); // final js script to store in js/script.js const scriptTags = []; Array.from(this.doc.head.querySelectorAll('script')) .forEach((tag) => { if (!tag.src && tag.innerHTML) { tag.parentElement.removeChild(tag); scriptTags.push(tag); } }); // link the user's script if (scriptTags.length > 0) { const scriptTagSrc = this.doc.createElement('script'); scriptTagSrc.src = 'js/script.js'; scriptTagSrc.type = 'text/javascript'; this.doc.head.appendChild(scriptTagSrc); } else { console.info('no script found in head'); } // add head css const styleTags = []; Array.from(this.doc.head.querySelectorAll('style')) .forEach((tag) => { tag.parentElement.removeChild(tag); styleTags.push(tag); }); // link the user's stylesheet if (styleTags.length > 0) { const cssTagSrc = this.doc.createElement('link'); cssTagSrc.href = 'css/styles.css'; cssTagSrc.rel = 'stylesheet'; cssTagSrc.type = 'text/css'; this.doc.head.appendChild(cssTagSrc); } else { console.warn('no styles found in head'); } // put back the user head now that all other scrips and styles are moved to external files this.doc.head.innerHTML += this.userHead; // this.doc.head.appendChild(this.doc.createTextNode(this.userHead)); // cleanup classes used by Silex during edition // DomTools.removeInternalClasses(this.dom); // replace internal links
containers const links = Array.from(this.doc.body.querySelectorAll(`.${Constants.EDITABLE_CLASS_NAME}[${Constants.LINK_ATTR}]`)) .forEach((element: HTMLElement) => { const href = element.getAttribute(Constants.LINK_ATTR); element.removeAttribute(Constants.LINK_ATTR); const replacement = this.doc.createElement('a'); replacement.setAttribute('href', href); replacement.innerHTML = element.innerHTML; for (let attrIdx = 0; attrIdx < element.attributes.length; attrIdx++) { const nodeName = element.attributes.item(attrIdx).nodeName; const nodeValue = element.attributes.item(attrIdx).nodeValue; replacement.setAttribute(nodeName, nodeValue); } // insert the clone at the place of the original and remove the original // FIXME: bug when there is a link in the content of an element with an external link set // see issue https://github.com/silexlabs/Silex/issues/56 element.parentElement.replaceChild(replacement, element); }); return { scriptTags, styleTags, files, }; } /** * det if a given URL is supposed to be downloaded locally * @param {string} url * @return {boolean} true if the url is relative or it is a known domain (sttic.silex.me) */ isDownloadable(url): boolean { // do not download files with GET params since it is probably dynamic return url.search === '' // do not download data:* images && url.protocol !== 'data:' && url.origin === this.rootUrl; } }