silex-website-builder-goodevent
Version:
Free and easy website builder for everyone.
284 lines (256 loc) • 10.5 kB
text/typescript
/**
* 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.<Object>}} 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: '<!doctype html>' + 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 <div data-silex-href="..." by <a href="..."
// do a first pass, in order to avoid replacing the elements in the <a> 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;
}
}