UNPKG

@silexlabs/silex

Version:

Free and easy website builder for everyone.

553 lines (550 loc) 23.4 kB
/* * Silex website builder, free/libre no-code tool for makers. * Copyright (c) 2023 lexoyo and Silex Labs foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ /** * @fileoverview Handle backward compatibility when a user opens a site for edition * */ //import * as Path from 'path' //import * as fs from 'fs' // //import { Constants } from '../../constants' //import { // ElementData, // ElementState, // ElementType, // Link, // LinkType //} from '../../client/element-store/types' //import { PageData } from '../../client/page-store/types' //import { PersistantData } from '../../client/store/types' //import { // cleanupBefore, // getElementsFromDomBC, // getPagesFromDom, // getSiteFromDom, // writeSiteStyles, // writeStyles //} from './BackwardCompatV2.5.60' //import { getDomElement } from '../../client/element-store/dom' //import { setTagName } from '../../client/utils/dom' //import { writeDataToDom } from '../../client/store/dom' // ///** // * util function for the "fixes", @see fixes // */ //function updateLinks<T extends { link?: Link }>(items: T[]): T[] { // return items // .map((item: T) => { // if(!!item.link && !item.link.linkType) { // // add new props // const link: any = { // ...item.link, // linkType: item.link.type as LinkType, // href: (item.link as any).value, // } // // remove old props // delete link.type // delete link.value // // return the updated element // return { // ...item, // link, // } // } else { // return item // } // }) //} // ///** // * class name for containers which are created with sections // */ //const SECTION_CONTAINER = 'silex-container-content' // //export default class BackwardCompat { // private data: PersistantData = null // private frontEndVersion: string[] // private silexVersion: number[] // // constructor(private rootUrl: string, rootPath = __dirname + '/../../../..') { // /** // * the version of the website is stored in the generator tag as "Silex v-X-Y-Z" // * we get it from package.json // * used for backward compat and for the static files URLs taken from //{{host}}/static/{{Y-Z}} // */ // const packageJson = JSON.parse(fs.readFileSync(Path.resolve(rootPath, 'package.json')).toString()) // this.frontEndVersion = packageJson['version:frontend'].split('.').map((s) => parseInt(s)) // this.silexVersion = packageJson['version:backwardcompat'].split('.').map((s) => parseInt(s)) // // // const components = require('../../../dist/client/libs/prodotype/components/components.json') // console.log(`\nStarting. Version is ${this.silexVersion} for the websites and ${this.frontEndVersion} for Silex\n`) // } // // // remove all tags // // export for tests // removeIfExist(doc: HTMLDocument, selector: string) { // Array.from(doc.querySelectorAll(selector)) // .forEach((tag) => tag.remove()) // } // // // remove all useless css class // // export for tests // removeUselessCSSClass(doc: HTMLDocument, className: string) { // Array.from(doc.querySelectorAll('.' + className)) // .forEach((el) => el.classList.remove(className)) // } // // getVersion(doc): number[] { // // if no generator tag, create one // let metaNode = doc.querySelector('meta[name="generator"]') // if (!metaNode) { // metaNode = doc.createElement('meta') // metaNode.setAttribute('name', 'generator') // doc.head.appendChild(metaNode) // } // // retrieve the website version from generator tag // return (metaNode.getAttribute('content') || '') // .replace('Silex v', '') // .split('.') // .map((str) => parseInt(str, 10) || 0) // } // // hasDataFile(doc): boolean { // return !this.hasToUpdate(this.getVersion(doc), [2, 2, 11]) // } // // /** // * handle backward compatibility issues // * Backwardcompatibility process takes place after opening a file // * @param {Document} doc // * @return {Promise} a Promise, resolve can be called with a warning message // */ // async update(doc: HTMLDocument, data: PersistantData): Promise<[string, PersistantData]> { // // // fix an issue when the style tag has no type, then json is "broken" // // const styleTag = doc.querySelector('.' + Constants.JSON_STYLE_TAG_CLASS_NAME); // // if (styleTag) { styleTag.type = 'text/json'; } // old versions of silex have no json at all so do nothing in that case // // TODO: move this to the data model (e.g. data.site.silexVersion) // // // we need this.data as to2_2_11 will extract it and set it from the dom // this.data = data // // const version = this.getVersion(doc) // const hasToUpdate = this.hasToUpdate(version, this.silexVersion) // // // warn the user // if (this.amIObsolete(version, this.silexVersion)) { // return ['This website has been saved with a newer version of Silex. Continue at your own risks.', this.data] // } else if (this.hasToUpdate(version, [2, 2, 7])) { // return Promise.reject({ // message: 'This website has been saved with an older version of Silex, which is not supported anymore as of March 2018. In order to convert it to a newer version, please go to <a href="https://old.silex.me">old.silex.me</a> to open and then save your website. <a href="https://github.com/silexlabs/Silex/wiki/Website-saved-with-older-version-of-Silex">More about this here</a>', // }) // } else if (hasToUpdate) { // // convert to the latest version // const allActions = { // '2.2.8': await this.to2_2_8(version, doc), // '2.2.9': await this.to2_2_9(version, doc), // '2.2.10': await this.to2_2_10(version, doc), // '2.2.11': await this.to2_2_11(version, doc), // this will set this.data // '2.2.12': await this.to2_2_12(version, doc), // '2.2.13': await this.to2_2_13(version, doc), // '2.2.14': await this.to2_2_14(version, doc), // } // // update the static scripts to match the current server and latest version // this.updateStatic(doc) // // store the latest version // const metaNode = doc.querySelector('meta[name="generator"]') // metaNode.setAttribute('content', 'Silex v' + this.silexVersion.join('.')) // // apply all-time fixes // this.fixes(doc) // // build the report for the user // const report = Object.keys(allActions) // .filter((_version) => allActions[_version].length > 0) // .map((_version) => { // return `<p>Update to version ${ _version }: // <ul>${ allActions[_version].map((_action) => `<li class="no-list">${ _action }</li>`).join('') }</ul> // </p>` // }).join('') // // save data to dom for front-end.js and other scripts // // in case data has been changed // // FIXME: should not have this.data mutated but returned by update scripts // writeDataToDom(doc, this.data) // // needs to reload if silex scripts and stylesheets have been updated // return [` // <p>This website has been updated to Silex latest version.</p> // <p>Before you save it, please check that everything is fine. Saving it with another name could be a good idea too (menu file > save as).</p> // <details> // <summary>Details</summary> // <small> // ${ report } // </small> // </details> // `, // this.data, // ] // } else { // // update the static scripts to match the current server URL // this.updateStatic(doc) // // apply all-time fixes // this.fixes(doc) // // resolve immediately // return ['', this.data] // } // } // // /** // * Check for common errors in editable html files // */ // fixes(doc: HTMLDocument) { // // const pages: HTMLElement[] = Array.from(doc.querySelectorAll(`.${Constants.PAGES_CONTAINER_CLASS_NAME} a[${Constants.TYPE_ATTR}="page"]`)); // // if (pages.length > 0) { // // console.log('Fix error of wrong silex type for', pages.length, 'pages'); // // pages.forEach((page) => page.setAttribute(Constants.TYPE_ATTR, Constants.TYPE_PAGE)); // // } // // // the following is a fix following the beta version of 07-2020 // // for elements and pages: // // link.value becomes href // // link.type becomes linkType // this.data = { // site: this.data.site, // pages: updateLinks<PageData>(this.data.pages), // elements: updateLinks<ElementData>(this.data.elements), // } // // remove juery-ui at publication, in case the website has been updated before the fix of 2.6.2 // Array.from(doc.querySelectorAll('script[src$="pageable.js"], script[src$="jquery-ui.js"]')) // .forEach((tag) => tag.setAttribute(Constants.ATTR_REMOVE_PUBLISH, '')) // // this does not work because sections can not be smaller than their content: // // // resizable sections // // this.data = { // // ...this.data, // // elements: this.data.elements // // .map((el) => el.type === ElementType.SECTION ? { // // ...el, // // enableResize: { // // bottom: true, // // top: true, // // left: false, // // right: false, // // } // // } : el), // // } // // // remove pages which do not exist (was caused by a bug in deletePages, fix 2021-03) // // also remove css classes which are pages (was caused by a bug??) // this.data.elements = this.data.elements.map(el => ({ // ...el, // pageNames: el.pageNames.filter(name => this.data.pages.some(p => p.id === name)), // classList: el.classList.filter(name => !this.data.pages.some(p => p.id === name)), // })) // } // // /** // * update the static scripts to match the current server and latest version // */ // updateStatic(doc: HTMLDocument) { // // update //{{host}}/2.x/... to latest version // Array.from(doc.querySelectorAll('[' + Constants.STATIC_ASSET_ATTR + ']')) // .forEach((element: HTMLElement) => { // const propName = element.hasAttribute('src') ? 'src' : 'href' // if (element.hasAttribute(propName)) { // const newUrl = this.getStaticResourceUrl(element[propName]) // const oldUrl = element.getAttribute(propName) // if (oldUrl !== newUrl) { // element.setAttribute(propName, newUrl) // } // } // }) // } // // /** // * get the complete URL for the static file, // * * on the current Silex server // * * with the latest Silex version // * // * this will result in a URL on the current server, in the `/static/` folder // * // * @example `//localhost:6805/static/2.1/example/example.js` returns `//editor.silex.me/static/2.7/example/unslider.js` // * @example `/static/2.1/example/example.js` returns `//editor.silex.me/static/2.7/example/example.js` // * // * with the current version // * @param {string} url // * @return {string} // */ // getStaticResourceUrl(url: string): string { // const pathRelativeToStaticMatch = url.match(/static\/[0-9]*\.[0-9]*\/(.*)/) // if (pathRelativeToStaticMatch == null) { // console.warn('Error: could not extract the path and file name of static asset', url) // return url // } // const pathRelativeToStatic = pathRelativeToStaticMatch[1] // return `${ this.rootUrl }/static/${ this.frontEndVersion[0] }.${ this.frontEndVersion[1] }/${ pathRelativeToStatic }` // } // // /** // * check if the website has been edited with a newer version of Silex // * @param {Array.<number>} initialVersion the website version // * @param {Array.<number>} targetVersion a given Silex version // * @return {boolean} // */ // amIObsolete(initialVersion: number[], targetVersion: number[]): boolean { // return !!initialVersion[2] && initialVersion[0] > targetVersion[0] || // initialVersion[1] > targetVersion[1] || // initialVersion[2] > targetVersion[2] // } // // /** // * check if the website has to be updated for the given version of Silex // * @param {Array.<number>} initialVersion the website version // * @param {Array.<number>} targetVersion a given Silex version // * @return {boolean} // */ // hasToUpdate(initialVersion: number[], targetVersion: number[]): boolean { // return initialVersion[0] < targetVersion[0] || // initialVersion[1] < targetVersion[1] || // initialVersion[2] < targetVersion[2] // } // // to2_2_8(version: number[], doc: HTMLDocument): Promise<string[]> { // return new Promise((resolve, reject) => { // const actions = [] // if (this.hasToUpdate(version, [2, 2, 8])) { // // cleanup the hamburger menu icon // const menuButton = doc.querySelector('.menu-button') // if (menuButton) { // menuButton.classList.remove('paged-element', 'paged-element-hidden', 'page-page-1', 'prevent-resizable') // menuButton.classList.add('hide-on-desktop') // } // // give the hamburger menu a size (TODO: add to the json model too) // doc.querySelector('.silex-inline-styles').innerHTML += '.silex-id-hamburger-menu {width: 50px;min-height: 40px;}' // // pages need to have href set // Array.from(doc.querySelectorAll('.page-element')) // .forEach((el: HTMLLinkElement) => { // el.setAttribute('href', '#!' + el.getAttribute('id')) // }) // actions.push('I fixed the mobile menu so that it is compatible with the new publication (now multiple pages are generated instead of 1 single page for the whole website).') // } // resolve(actions) // }) // } // // to2_2_9(version: number[], doc: HTMLDocument): Promise<string[]> { // return new Promise((resolve, reject) => { // const actions = [] // if (this.hasToUpdate(version, [2, 2, 9])) { // // remove the hamburger menu icon // const menuButton = doc.querySelector('.menu-button') // if (menuButton) { // menuButton.parentElement.removeChild(menuButton) // actions.push( // 'I removed the mobile menu as there is now a component for that. <a target="_blank" href="https://github.com/silexlabs/Silex/wiki/Hamburger-menu">Read more about the Hamburger Menu component here</a>.', // ) // } // } // resolve(actions) // }) // } // // to2_2_10(version: number[], doc: HTMLDocument): Promise<string[]> { // return new Promise((resolve, reject) => { // const actions = [] // if (this.hasToUpdate(version, [2, 2, 10])) { // // the body is a drop zone, not selectable, not draggable, resizeable // doc.body.classList.add( // Constants.PREVENT_DRAGGABLE_CLASS_NAME, // Constants.PREVENT_RESIZABLE_CLASS_NAME, // Constants.PREVENT_SELECTABLE_CLASS_NAME) // // // each section background and foreground is a drop zone, not selectable, not draggable, resizeable // const changedSections = Array.from(doc.querySelectorAll(`.${ElementType.SECTION}`)) as HTMLElement[] // changedSections.forEach((el: HTMLElement) => el.classList.add( // Constants.PREVENT_DRAGGABLE_CLASS_NAME, // Constants.PREVENT_RESIZABLE_CLASS_NAME, // )) // // // we add classes to the elements so that we can tell the stage component if an element is draggable, resizeable, selectable... // const changedSectionsContent = Array.from(doc.querySelectorAll(`.${ElementType.SECTION}, .${ElementType.SECTION} .${SECTION_CONTAINER}`)) // changedSectionsContent.forEach((el: HTMLElement) => el.classList.add( // Constants.PREVENT_DRAGGABLE_CLASS_NAME, // // Constants.PREVENT_RESIZABLE_LEFT_CLASS_NAME, // // Constants.PREVENT_RESIZABLE_RIGHT_CLASS_NAME // )) // actions.push(`Changed the body and ${changedSections.length} sections with new CSS classes to <a href="https://github.com/silexlabs/stage/" target="_blank">the new stage component.</a>`) // // // types are now with a "-element" suffix // const changedElements = Array.from(doc.querySelectorAll(`[${Constants.TYPE_ATTR}]`)) // changedElements.forEach((el: HTMLElement) => el.setAttribute(Constants.TYPE_ATTR, el.getAttribute(Constants.TYPE_ATTR) + '-element')) // // actions.push(`Updated ${ changedElements.length } elements, changed their types to match the new version of Silex.`) // } // resolve(actions) // }) // } // // to2_2_11(version: number[], doc: HTMLDocument): Promise<string[]> { // return new Promise((resolve, reject) => { // const actions = [] // if (this.hasToUpdate(version, [2, 2, 11])) { // // the body is supposed to be an element too // doc.body.classList.add(Constants.EDITABLE_CLASS_NAME) // actions.push('I made the body editable.') // // const oldBodyId = doc.body.getAttribute('data-silex-id') // if (oldBodyId !== 'body-initial') { // actions.push('I udpated the body class name from "' + oldBodyId + '" to "body-initial".') // doc.body.setAttribute('data-silex-id', 'body-initial') // doc.body.classList.remove(oldBodyId) // doc.body.classList.add('body-initial') // } // // // prepare the dom // cleanupBefore(doc) // // // import elements // const elements = getElementsFromDomBC(doc) // writeStyles(doc, elements) // // // site // const site = getSiteFromDom(doc) // writeSiteStyles(doc, site) // // // pages // const pages = getPagesFromDom(doc) // // if (elements.length && pages.length && site) { // this.data = { // site, // pages, // elements, // } // this.removeIfExist(doc, 'meta[name="website-width"]') // this.removeIfExist(doc, 'meta[name="hostingConnector"]') // this.removeIfExist(doc, 'meta[name="publicationPath"]') // // // remove juery-ui at publication // Array.from(doc.querySelectorAll('script[src$="pageable.js"], script[src$="jquery-ui.js"]')) // .forEach((tag) => tag.setAttribute(Constants.ATTR_REMOVE_PUBLISH, '')) // // ;['prevent-draggable', SECTION_CONTAINER].forEach((className) => this.removeUselessCSSClass(doc, className)) // // actions.push('I updated the model to the latest version of Silex.') // // pages // this.removeIfExist(doc, `.${Constants.PAGES_CONTAINER_CLASS_NAME}`) // actions.push('I removed the old pages system.') // } else { // console.error('Could not import site from v2.2.11', {elements, pages, site}) // } // } // resolve(actions) // }) // } // // to2_2_12(version: number[], doc: HTMLDocument): Promise<string[]> { // return new Promise((resolve, reject) => { // const actions = [] // if (this.hasToUpdate(version, [2, 2, 12])) { // // add empty metadata to the website object // this.data = { // ...this.data, // site: { // ...this.data.site, // data: {}, // } // } // actions.push('add empty metadata to the website object') // // sections are now section tag // const changedSections = Array.from(doc.querySelectorAll(`.${ElementType.SECTION}`)) as HTMLElement[] // changedSections.forEach((el: HTMLElement) => setTagName(el, 'section')) // this.data = { // ...this.data, // elements: this.data.elements // .map((el) => ({ // ...el, // tagName: getDomElement(doc, el as ElementState) ? getDomElement(doc, el as ElementState).tagName : 'DIV', // })) // } // actions.push('All sections have a &lt;SECTION&gt; tag name') // } // resolve(actions) // }) // } // // to2_2_13(version: number[], doc: HTMLDocument): Promise<string[]> { // return new Promise((resolve, reject) => { // const actions = [] // if (this.hasToUpdate(version, [2, 2, 13])) { // Array.from(doc.querySelectorAll('[data-silex-href]')) // .forEach((tag: HTMLElement) => { // const newEl = setTagName(tag, 'A') as HTMLLinkElement // newEl.href = tag.getAttribute('data-silex-href') // newEl.removeAttribute('data-silex-href') // }) // actions.push('Replaced old Silex links with standard HTML links.') // } // resolve(actions) // }) // } // // to2_2_14(version: number[], doc: HTMLDocument): Promise<string[]> { // return new Promise((resolve, reject) => { // const actions = [] // if (this.hasToUpdate(version, [2, 2, 14])) { // // do not fix this: remove w3c warning "The type attribute is unnecessary for JavaScript resources" // // because silex uses script type attr in WebsiteRouter // // instead we remove the type at publication time in DomPublisher // // remove w3c warning "The type attribute for the style element is not needed and should be omitted." // const styles = Array.from(doc.querySelectorAll('style[type="text/css"]')) // if(styles.length) { // styles.forEach(el => el.removeAttribute('type')) // actions.push(`Fixed ${styles.length} w3c warning 'The type attribute for the style element is not needed and should be omitted.'`) // } // // This should not be useful (previous BC bug?) // // Remove empty attributes // // Sometimes we have style="", title=""... // let numEmpty = 0 // const attrs = [ // 'href', // 'style', // 'rel', // 'target', // 'type', // 'title', // ] // attrs.forEach((attr: string) => { // Array.from(doc.querySelectorAll(`[${attr}]`)) // .forEach(el => { // if(el.hasAttribute(attr) && el.getAttribute(attr) === '') { // el.removeAttribute(attr) // numEmpty++ // } // }) // }) // if(numEmpty) actions.push(`Removed ${numEmpty} empty attributes (${attrs.join(', ')})`) // // // This should not be useful (previous BC bug?) // // Remove the href attribute from non <a> tags // // Sometimes we have href="null" on non a tags // const tagsWithWrongHref = Array.from(doc.querySelectorAll('[href]:not(a, link, base)')) // tagsWithWrongHref.forEach(el => { // el.removeAttribute('href') // }) // if(tagsWithWrongHref.length) actions.push(`Removed ${tagsWithWrongHref.length} href attributes on non links tags`) // } // resolve(actions) // }) // } //} //