////////////////////////////////////////////////// // 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 {Constants} from '../../Constants.js'; export default class DomTools { /** * This method is the entry point for modifying all the URLs in a dom tree * with a function you provide * The algorithm will call your function with the URLs found in the stylsheets, the html markup, and the JSON data stored by Silex */ static transformPaths(dom, fn) { // images, videos, stylesheets, iframes... ['src', 'href'].forEach((attr) => { const elements = dom.window.document.querySelectorAll(`[${attr}]`); for (const el of elements) { if (el.tagName.toLowerCase() === 'a' || el.getAttribute('data-silex-href')) { // do nothing with links continue; } if (el.tagName.toLowerCase() === 'link' && el.getAttribute('rel').toLowerCase() !== 'stylesheet' && el.getAttribute('rel').toLowerCase() !== 'shortcut icon' ) { // do nothing with tags unless it is an external stylesheet or the favicon continue; } if (el.hasAttribute('data-silex-static')) { continue; } const val = el.getAttribute(attr); const newVal = fn(val, el, el.parentElement === dom.window.document.head); if (newVal) { el.setAttribute(attr, newVal); } } }); // CSS rules // FIXME: it would be safer (?) to use CSSStyleSheet::ownerNode instead of browsing the DOM // see the bug in jsdom: https://github.com/jsdom/jsdom/issues/992 const tags = dom.window.document.querySelectorAll('style'); const stylesheets = dom.window.document.styleSheets; const matches = []; for (let stylesheetIdx = 0; stylesheetIdx < stylesheets.length; stylesheetIdx++) { const stylesheet = stylesheets[stylesheetIdx]; if (tags[stylesheetIdx]) { // seems to happen sometimes? const tag = tags[stylesheetIdx]; const cssText = DomTools.transformStylesheet(stylesheet, tag.parentElement === dom.window.document.head, fn); matches.push({ tag, innerHTML: cssText, }); } } matches.forEach(({tag, innerHTML}) => tag.innerHTML = innerHTML); // JSON object of Silex (components and styles) DomTools.transformJson(dom, fn); } /** * if value conatains `url('...')` this will be "transformed" by the provided function `fn` * @param {string} value, e.g. "transparent" or "100px" or "url('image/photo%20page%20accueil.png')" * @param {?CSSStyleSheet} stylesheet or null if the value comes from the JSON object holding silex data * @param {boolean} isInHead, true if the stylesheet is in the head tag * @param {function} fn */ static transformValueUrlKeyword(value, stylesheet, isInHead, fn) { if (typeof value === 'string' && value.indexOf('url(') === 0) { // support url(...), url('...'), url("...") return `url(${ value.replace(/url\('(.*)'\)|url\("(.*)"\)|url\((.*)\)/, (str, match1, match2, match3) => { const match = match1 || match2 || match3; return fn(match, stylesheet, isInHead) || match; }) })`; } return null; } /** * FIXME: this removes comments from CSS */ static transformStylesheet(stylesheet: CSSStyleSheet, isInHead, fn, isMediaQuerySubRule = false) { let cssText = ''; for (const sheetOrRule of stylesheet.cssRules) { // have to play with types const rule: CSSStyleRule = sheetOrRule as CSSStyleRule; const sheet: CSSStyleSheet = sheetOrRule as any; if (rule.style) { for (const valName of Array.from(rule.style)) { const value = rule.style[valName as string]; rule.style[valName as string] = DomTools.transformValueUrlKeyword(value, stylesheet, isInHead, fn) || value; } } else if (sheet.cssRules) { // case of a mediaquery DomTools.transformStylesheet(sheet, isInHead, fn, true); } else { } if (!isMediaQuerySubRule) { // if it is a media query then the parent rule will be written cssText += rule.cssText; } } return cssText; } /** * Transform the JSON object stored by Silex in the DOM * It contains all the components data, the elements styles, etc. * This is even more important than the URLs in the dom and stylesheets since it is re-applyed by Silex when the site is loaded in the editor */ static transformJson(dom, fn) { const jsonData = DomTools.getProperties(dom.window.document); if (jsonData) { for (const dataObjName in jsonData) { const dataObj = jsonData[dataObjName]; for (const elementId in dataObj) { const elementData = dataObj[elementId]; for (const propName in elementData) { const propValue = elementData[propName]; const valueUrlKeyword = DomTools.transformValueUrlKeyword(propValue, null, true, fn); if (valueUrlKeyword) { elementData[propName] = valueUrlKeyword; } else { if (['src', 'href'].indexOf(propName) >= 0) { elementData[propName] = fn(propValue) || propValue; } } } } } DomTools.setProperties(dom.window.document, jsonData); } } /** * Load the styles from the json saved in a script tag * This code comes from the client side class property.js */ static getProperties(doc) { const styleTag = doc.querySelector('.' + Constants.JSON_STYLE_TAG_CLASS_NAME); if (styleTag != null ) { return JSON.parse(styleTag.innerHTML)[0]; } // no JSON styles array found in the dom // this is ok when publishing return null; } /** * Saves the styles to a script tag * This code comes from the client side class property.js */ static setProperties(doc, value) { const styleTag = doc.querySelector('.' + Constants.JSON_STYLE_TAG_CLASS_NAME); if (styleTag != null ) { styleTag.innerHTML = JSON.stringify([value]); } else { console.error('Error: no JSON styles array found in the dom'); } } /** * Split the user editable head tag and silex head tags * the head tag edited by the user is a portion of the real head tag * it is delimited by specific comments * it can not be interpreted while editing, in case it has bad HTML tags, it could break the whole site, insert tags into the body instead of the head... * @param {string} headString initial head tag * @return {{html: string, userHead: string}} split initial head tag and user editable head tag */ static extractUserHeadTag(headString) { const regExp = new RegExp(Constants.HEAD_TAG_START + '([\\\s\\\S.]*)' + Constants.HEAD_TAG_STOP); const found = headString.match(regExp); if (found) { return { userHead: found[1], html: headString.replace(regExp, ''), }; } return { userHead: '', html: headString, }; } /** * insert the HEAD tag back into an HTML string * the head tag edited by the user is a portion of the real head tag * it is delimited by specific comments * it can not be interpreted while editing, in case it has bad HTML tags, it could break the whole site, insert tags into the body instead of the head... * @param {string} htmlString * @param {string} userHead * @return {string} the provided string with the user's head tags */ static insertUserHeadTag(htmlString, userHead) { if (userHead) { return htmlString.replace(/<\/head>/i, Constants.HEAD_TAG_START + userHead + Constants.HEAD_TAG_STOP + ''); } else { return htmlString; } } }