diff --git a/src/ts/client/config.ts b/src/ts/client/config.ts index 48213906c..d738a397d 100644 --- a/src/ts/client/config.ts +++ b/src/ts/client/config.ts @@ -20,7 +20,7 @@ import { getEditor, getEditorConfig } from './grapesjs' import { CLIENT_CONFIG_FILE_NAME, DEFAULT_LANGUAGE, DEFAULT_WEBSITE_ID } from '../constants' import { ConnectorId, WebsiteId } from '../types' import { Editor, EditorConfig, Page } from 'grapesjs' -import { PublicationTransformer } from './publication-transformers' +import { PublicationTransformer, validatePublicationTransformer } from './publication-transformers' import * as api from './api' /** @@ -91,6 +91,16 @@ export class ClientConfig extends Config { * Add a publication transformer(s) */ addPublicationTransformers(transformers: PublicationTransformer | PublicationTransformer[]) { + // Make sure it is an array + if (!Array.isArray(transformers)) { + transformers = [transformers] + } + // Validate + transformers.forEach(transformer => { + validatePublicationTransformer(transformer) + }) + + // Add to the list this.publicationTransformers = this.publicationTransformers.concat(transformers) } } diff --git a/src/ts/client/grapesjs/PublicationManager.ts b/src/ts/client/grapesjs/PublicationManager.ts index b1871aee0..e7d05ff6a 100644 --- a/src/ts/client/grapesjs/PublicationManager.ts +++ b/src/ts/client/grapesjs/PublicationManager.ts @@ -16,12 +16,13 @@ */ import { getPageSlug } from '../../page' -import { ApiConnectorLoggedInPostMessage, ApiConnectorLoginQuery, ApiPublicationPublishBody, ApiPublicationPublishQuery, ApiPublicationPublishResponse, ApiPublicationStatusQuery, ApiPublicationStatusResponse, ClientSideFile, ClientSideFileWithContent, ClientSideFileWithSrc, ConnectorData, ConnectorType, ConnectorUser, JobData, JobStatus, PublicationData, PublicationJobData, PublicationSettings, WebsiteData, WebsiteFile, WebsiteId, WebsiteSettings } from '../../types' +import { ApiConnectorLoggedInPostMessage, ApiConnectorLoginQuery, ApiPublicationPublishBody, ApiPublicationPublishQuery, ApiPublicationPublishResponse, ApiPublicationStatusQuery, ApiPublicationStatusResponse, ClientSideFile, ClientSideFileType, ClientSideFileWithContent, ClientSideFileWithSrc, ConnectorData, ConnectorType, ConnectorUser, JobData, JobStatus, PublicationData, PublicationJobData, PublicationSettings, WebsiteData, WebsiteFile, WebsiteId, WebsiteSettings } from '../../types' import { Editor, ProjectData } from 'grapesjs' import { PublicationUi } from './PublicationUi' import { getUser, logout, publicationStatus, publish } from '../api' import { API_CONNECTOR_LOGIN, API_CONNECTOR_PATH, API_PATH } from '../../constants' import { ClientEvent } from '../events' +import { resetRenderComponents, resetRenderCssRules, transformPermalink, transformFiles, transformPath, renderComponents } from '../publication-transformers' /** * @fileoverview Publication manager for Silex @@ -203,25 +204,27 @@ export class PublicationManager { this.job = null this.dialog && this.dialog.displayPending(this.job, this.status) this.editor.trigger(ClientEvent.PUBLISH_START) + this.setPublicationTransformers() const projectData = this.editor.getProjectData() as WebsiteData const siteSettings = this.editor.getModel().get('settings') as WebsiteSettings // Build the files structure - const files: ClientSideFile[] = (await this.getSiteFiles(siteSettings)) + const files: ClientSideFile[] = (await this.getHtmlFiles(siteSettings)) .flatMap(file => ([{ - path: file.htmlPath, + path: file.htmlPath, // Already "transformed" in getHtmlFiles content: file.html, type: 'html', } as ClientSideFile, { - path: file.cssPath, + path: file.cssPath, // Already "transformed" in getHtmlFiles content: file.css, type: 'css', } as ClientSideFile])) - .concat(projectData.assets.map(asset => ({ - ...asset, - path: `/${asset.src}`, - src: asset.src, - type: 'asset', - }) as ClientSideFile)) + .concat(projectData.assets.map(asset => { + const src = transformPath(this.editor, asset.path, ClientSideFileType.ASSET) + return { + ...asset, + src, + } as ClientSideFile + })) // Create the data to send to the server const data: PublicationData = { @@ -230,6 +233,7 @@ export class PublicationManager { publication: this.settings, files, } + transformFiles(this.editor, data) this.editor.trigger(ClientEvent.PUBLISH_DATA, data) const storageUser = this.editor.getModel().get('user') as ConnectorUser if(!storageUser) throw new Error('User not logged in to a storage connector') @@ -268,25 +272,26 @@ export class PublicationManager { } } - async getSiteFiles(siteSettings: WebsiteSettings): Promise { + async getHtmlFiles(siteSettings: WebsiteSettings): Promise { return this.editor.Pages.getAll().map(page => { const pageSettings = page.get('settings') as WebsiteSettings function getSetting(name) { return (pageSettings || {})[name] || (siteSettings || [])[name] || '' } const component = page.getMainComponent() - - - - - - const slug = page.get('slug') || getPageSlug(page.get('name') || page.get('type')) + // Transform the file paths + const slug = getPageSlug(page.get('name')) + const cssInitialPath = `/css/${slug}.css` + const htmlInitialPath = `/${slug}.html` + const cssPermalink = transformPermalink(this.editor, cssInitialPath, ClientSideFileType.CSS) + const cssPath = transformPath(this.editor, cssInitialPath, ClientSideFileType.CSS) + const htmlPath = transformPath(this.editor, htmlInitialPath, ClientSideFileType.HTML) return { html: ` - + ${siteSettings?.head || ''} ${pageSettings?.head || ''} ${getSetting('title')} @@ -300,8 +305,8 @@ export class PublicationManager { `, css: this.editor.getCss({ component }), - cssPath: `/${slug}.css`, - htmlPath: `/${slug}.html`, + cssPath, + htmlPath, } }) } @@ -313,6 +318,7 @@ export class PublicationManager { } catch (e) { this.status = PublicationStatus.STATUS_ERROR this.dialog && this.dialog.displayError(`An error occured, your site is not published. ${e.message}`, this.job, this.status) + this.resetPublicationTransformers() this.editor.trigger(ClientEvent.PUBLISH_END, { success: false, message: e.message }) this.editor.trigger(ClientEvent.PUBLISH_ERROR, { success: false, message: e.message }) return @@ -320,8 +326,19 @@ export class PublicationManager { if (this.job.status === JobStatus.IN_PROGRESS) { setTimeout(() => this.trackProgress(), 2000) } else { + this.resetPublicationTransformers() this.editor.trigger(ClientEvent.PUBLISH_END, { success: this.job.status === JobStatus.SUCCESS, message: this.job.message }) } this.dialog && this.dialog.displayPending(this.job, this.status) } + + private setPublicationTransformers() { + renderComponents(this.editor) + renderComponents(this.editor) + } + + private resetPublicationTransformers() { + resetRenderComponents(this.editor) + resetRenderCssRules(this.editor) + } } diff --git a/src/ts/client/grapesjs/PublicationUi.ts b/src/ts/client/grapesjs/PublicationUi.ts index 1cc411629..53bad561b 100644 --- a/src/ts/client/grapesjs/PublicationUi.ts +++ b/src/ts/client/grapesjs/PublicationUi.ts @@ -162,7 +162,7 @@ export class PublicationUi { ` : ''} ${this.isSuccess(status) ? html`

Publication success

- ${this.settings.url ? html`

Click here to view the published website

` : ''} + ${this.settings.options.websiteUrl ? html`

Click here to view the published website

` : ''} ` : ''} ${this.isError(status) || this.isLoggedOut(status) ? html`

Publication error

diff --git a/src/ts/client/grapesjs/eleventy.ts b/src/ts/client/grapesjs/eleventy.ts index d9ac38138..d534b4394 100644 --- a/src/ts/client/grapesjs/eleventy.ts +++ b/src/ts/client/grapesjs/eleventy.ts @@ -15,13 +15,19 @@ * along with this program. If not, see . */ -throw 'unused now' +/** + * @deprecated + * Replaced by the plugin 11ty.ts + * This plugin is deprecated. Use the publication renderer plugin instead. + */ +console.warn('This plugin is deprecated. Use the publication renderer plugin instead.') +throw 'This plugin is deprecated. Use the publication renderer plugin instead.' import grapesjs from 'grapesjs/dist/grapes.min.js' import { getPageSlug } from '../../page' -import { Page } from '../../types' import { ClientEvent } from '../events' +import { Page } from 'grapesjs' const pluginName = 'eleventy' @@ -30,7 +36,7 @@ export const eleventyPlugin = grapesjs.plugins.add(pluginName, (editor, opts) => data.pages.forEach((page: Page, idx) => { const file = data.files[idx] file.css = `--- -permalink: /css/${getPageSlug(page.name)}.css +permalink: /css/${getPageSlug(page.get('name'))}.css --- ${file.css} ` diff --git a/src/ts/client/grapesjs/internal-links.ts b/src/ts/client/grapesjs/internal-links.ts index 24d7e5f03..bdd05b15b 100644 --- a/src/ts/client/grapesjs/internal-links.ts +++ b/src/ts/client/grapesjs/internal-links.ts @@ -107,7 +107,7 @@ export const internalLinksPlugin = grapesjs.plugins.add(pluginName, (editor, opt function getDefaultHref(type) { switch(type) { case 'email': return 'mailto:' - case 'page': return getPageLink() + case 'page': return getPageLink(null) case 'url': return 'https://' } } diff --git a/src/ts/client/index.ts b/src/ts/client/index.ts index 90be1df61..b0ee9f56f 100644 --- a/src/ts/client/index.ts +++ b/src/ts/client/index.ts @@ -24,7 +24,6 @@ import { ClientConfig } from './config' import { ClientEvent } from './events' import { initEditor, getEditor } from './grapesjs/index' -import { initPublicationTransformers } from './publication-transformers' // Expose API to calling app as window.silex export * from './expose' @@ -60,10 +59,12 @@ export async function start(options = {}) { } const editor = getEditor() - config.emit(ClientEvent.GRAPESJS_END, { editor }) - // Init publication transformers - initPublicationTransformers(config) + // Store the config in the editor + editor.getModel().set('config', config) + + // Notify plugins + config.emit(ClientEvent.GRAPESJS_END, { editor }) // Init internationalization module editor.I18n.setLocale(config.lang) diff --git a/src/ts/client/publication-transformers.test.ts b/src/ts/client/publication-transformers.test.ts index 4d2bb80e4..54fd2f780 100644 --- a/src/ts/client/publication-transformers.test.ts +++ b/src/ts/client/publication-transformers.test.ts @@ -1,18 +1,17 @@ import { jest, expect, describe, it, beforeEach } from '@jest/globals' import { Mock } from 'jest-mock' import { - transformComponents, - transformStyles, - resetTransformComponents, - resetTransformStyles, + renderComponents, + renderCssRules, + resetRenderComponents, + resetRenderCssRules, PublicationTransformer, - transformPages, - resetTransformPages, - transformFiles + transformFiles, + transformBgImage } from './publication-transformers' import { ClientConfig } from './config' import GrapesJS, { Component, Editor, ObjectStrings, Page } from 'grapesjs' -import { ClientSideFile, PublicationData } from '../types' +import { ClientSideFile, ClientSideFileType, PublicationData } from '../types' describe('publication-transformers', () => { let mockConfig: ClientConfig @@ -53,16 +52,18 @@ describe('publication-transformers', () => { transformer = { renderComponent: jest.fn(), renderCssRule: jest.fn(), - pageToSlug: jest.fn(), - transformFile: jest.fn() + transformFile: jest.fn(), + transformPermalink: jest.fn(), + transformPath: jest.fn(), } as PublicationTransformer mockConfig = { getEditor: jest.fn(() => editor), publicationTransformers: [transformer] } as unknown as ClientConfig + editor.getModel().set('config', mockConfig) }) it('should transform components', () => { const renderComponent = transformer.renderComponent as Mock renderComponent.mockReturnValue('mockHtml') - transformComponents(mockConfig) + renderComponents(editor) const html = editor.getHtml() expect(renderComponent).toBeCalledTimes(6) const results = renderComponent.mock.calls.map(call => call[0] as Component) @@ -75,22 +76,22 @@ describe('publication-transformers', () => { it('should reset transformed pages', () => { const renderComponent = transformer.renderComponent as Mock renderComponent.mockReturnValue('mockHtml') - transformComponents(mockConfig) - resetTransformComponents(mockConfig) + renderComponents(editor) + resetRenderComponents(editor) const html = editor.getHtml() expect(renderComponent).toBeCalledTimes(0) expect(html).toContain('Hello world') }) it('should transform styles', () => { - const renderCssRule = transformer.renderCssRule as Mock - renderCssRule.mockImplementation((rule: any) => { - return { color: 'test'+rule.attributes.style.color } as ObjectStrings + const transformCssRule = transformer.renderCssRule as Mock + transformCssRule.mockImplementation((rule: any) => { + return { color: 'test'+rule.color } as ObjectStrings }) - transformStyles(mockConfig) + renderCssRules(editor) const css = editor.getCss() - expect(renderCssRule).toBeCalledTimes(2) - const results = renderCssRule.mock.results.map(r => r.value as ObjectStrings) + expect(transformCssRule).toBeCalledTimes(2) + const results = transformCssRule.mock.results.map(r => r.value as ObjectStrings) const parent = results.find(c => c.color === 'testred') expect(parent).not.toBeUndefined() const child = results.find(c => c.color === 'testblue') @@ -103,34 +104,13 @@ describe('publication-transformers', () => { const renderCssRule = transformer.renderCssRule as Mock const returned = {color: 'test'} as ObjectStrings renderCssRule.mockReturnValue(returned) - transformStyles(mockConfig) - resetTransformStyles(mockConfig) + renderCssRules(editor) + resetRenderCssRules(editor) const css = editor.getCss() expect(renderCssRule).toBeCalledTimes(0) expect(css).toContain('color:blue') }) - it('should transform pages', () => { - const PAGE_SLUG = 'test page slug' - const pageToSlug = transformer.pageToSlug as Mock - pageToSlug.mockReturnValue(PAGE_SLUG) - transformPages(mockConfig) - const pages = editor.Pages.getAll() - expect(pageToSlug).toBeCalledTimes(1) - expect(pages[0].get('slug')).toBe(PAGE_SLUG) - }) - - it('should reset transformed components', () => { - const PAGE_SLUG = 'test page slug' - const pageToSlug = transformer.pageToSlug as Mock - pageToSlug.mockReturnValue(PAGE_SLUG) - transformPages(mockConfig) - resetTransformPages(mockConfig) - const pages = editor.Pages.getAll() - expect(pageToSlug).toBeCalledTimes(1) - expect(pages[0].get('slug')).not.toBe(PAGE_SLUG) - }) - it('should transform files', () => { const transformFile = transformer.transformFile as Mock const returned = {path: 'test path changed', content: 'test content changed'} as ClientSideFile @@ -138,9 +118,82 @@ describe('publication-transformers', () => { expect(mockData.files?.length).toBe(1) if (mockData.files?.length) { // To avoid ! operator on mockData.files and make the lint happy expect(mockData.files[0]).toBe(mockFile) - transformFiles(mockConfig, mockData) + transformFiles(editor, mockData) expect(transformFile).toBeCalledTimes(1) expect(mockData.files[0]).toBe(returned) } }) + it('should transform permalinks of images src', () => { + editor.addComponents(` + + `) + const transformPermalink = transformer.transformPermalink as Mock + const transformedSrc = 'transformed.png' + transformPermalink.mockReturnValue(transformedSrc) + renderComponents(editor) + const html = editor.getHtml() + expect(transformPermalink).toBeCalledTimes(1) + expect(transformPermalink.mock.calls[0][0]).toBe('test.png') + expect(transformPermalink.mock.calls[0][1]).toBe('asset') + }) + it('should transform a style rule with background image', () => { + expect(transformBgImage(editor, { 'background-image': 'url(test.png)' })).toEqual({ 'background-image': 'url(test.png)' }) + const transformPermalink = transformer.transformPermalink as Mock + const transformedSrc = 'transformed.png' + transformPermalink.mockImplementation((url, type) => (url as string).replace('test.png', transformedSrc)) + expect(transformBgImage(editor, { 'background-image': 'url(test.png)' })).toEqual({ 'background-image': 'url(transformed.png)' }) + expect(transformBgImage(editor, { 'background-image': 'url("test.png")' })).toEqual({ 'background-image': 'url(transformed.png)' }) + }) + + it('should transform permalinks of background images in inline css', () => { + const el = document.createElement('div') + const [comp] = editor.addComponents(el) + comp.setStyle({ 'background-image': 'url(test.png)' }) + //editor.addComponents(` + //
+ //`) + const transformPermalink = transformer.transformPermalink as Mock + const transformedSrc = 'transformed.png' + transformPermalink.mockReturnValue(transformedSrc) + renderComponents(editor) + const css = editor.getCss() + expect(transformPermalink).toBeCalledTimes(1) + expect(transformPermalink.mock.calls[0][0]).toBe('test.png') + expect(transformPermalink.mock.calls[0][1]).toBe('asset') + expect(css).toContain('url(test.png)') + }) + it('should transform permalinks of background images in styles', () => { + editor.addComponents(` +
+ `) + editor.addStyle(` + .test { + background-image: url(test.png); + } + `) + const transformPermalink = transformer.transformPermalink as Mock + const transformedSrc = 'transformed.png' + transformPermalink.mockReturnValue(transformedSrc) + renderCssRules(editor) + const css = editor.getCss() + expect(transformPermalink).toBeCalledTimes(1) + expect(transformPermalink.mock.calls[0][0]).toBe('test.png') + expect(transformPermalink.mock.calls[0][1]).toBe('asset') + expect(css).toContain(`url(${transformedSrc})`) + }) + it('should transform links to pages to match permalinks ', () => { + editor.addComponents(` + test + `) + const transformPermalink = transformer.transformPermalink as Mock + const transformedPermalink = './transformed-page.html' + transformPermalink.mockReturnValue(transformedPermalink) + renderComponents(editor) + const html = editor.getHtml() + expect(transformPermalink).toBeCalledTimes(1) + expect((transformPermalink.mock.calls[0][0])).toBe('./index.html') + expect((transformPermalink.mock.calls[0][1])).toBe(ClientSideFileType.HTML) + expect(html).not.toContain('href="./index.html"') + expect(html).toContain(`href="${transformedPermalink}"`) + }) }) diff --git a/src/ts/client/publication-transformers.ts b/src/ts/client/publication-transformers.ts index 3d35aa7b2..d2f2f1e6c 100644 --- a/src/ts/client/publication-transformers.ts +++ b/src/ts/client/publication-transformers.ts @@ -15,18 +15,24 @@ * along with this program. If not, see . */ -import { Component, CssRule, ObjectStrings, Page } from 'grapesjs' -import { ClientSideFile, PublicationData } from '../types' -import { ClientConfig } from './config' -import { ClientEvent } from './events' +import { Component, CssRule, Editor, ObjectStrings, Page } from 'grapesjs' +import { ClientSideFile, ClientSideFileType, ClientSideFileWithPermalink, PublicationData } from '../types' import { onAll } from './utils' +import { getPageLink, getPageSlug } from '../page' /** * @fileoverview Silex publication transformers are used to control how the site is rendered and published + * Here we call path the path on the connector (either storage or hosting) + * We call permalink the path at which the resource is served + * This is where pages and assets paths and permalinks are set + * Here we also update background images urls, assets url and links to match the new permalinks */ // Properties names used to store the original methods on the components and styles const ATTRIBUTE_METHOD_STORE_HTML = 'tmp-pre-publication-transformer-tohtml' +const ATTRIBUTE_METHOD_STORE_SRC = 'tmp-pre-publication-transformer-src' +const ATTRIBUTE_METHOD_STORE_INLINE_CSS = 'tmp-pre-publication-transformer-inline-css' +const ATTRIBUTE_METHOD_STORE_HREF = 'tmp-pre-publication-transformer-href' const ATTRIBUTE_METHOD_STORE_CSS = 'tmp-pre-publication-transformer-tocss' /** @@ -34,56 +40,76 @@ const ATTRIBUTE_METHOD_STORE_CSS = 'tmp-pre-publication-transformer-tocss' * They are added to the config object with config.addPublicationTransformer() */ export interface PublicationTransformer { - // Override how components render at publication by grapesjs + // Temporarily override how components render at publication by grapesjs renderComponent?(component: Component, toHtml: () => string): string | undefined - // Override how styles render at publication by grapesjs + // Temporarily override how styles render at publication by grapesjs renderCssRule?(rule: CssRule, initialRule: () => ObjectStrings): ObjectStrings | undefined - // Define how pages are named - pageToSlug?(page: Page): string // Transform files after they are rendered and before they are published - transformFile?(file: ClientSideFile, page: Page | null): ClientSideFile + transformFile?(file: ClientSideFile): ClientSideFile + // Define where files are served + transformPermalink?(path: string, type: ClientSideFileType): string + // Define where files are published + transformPath?(path: string, type: ClientSideFileType): string } +export function validatePublicationTransformer(transformer: PublicationTransformer): void { + // List all the properties + const allowedProperties = [ + 'renderComponent', + 'renderCssRule', + 'transformFile', + 'transformPermalink', + 'transformPath', + ] -/** - * Init publication transformers - * Called at startup in the /index.ts file - */ -export async function initPublicationTransformers(config: ClientConfig) { - const editor = config.getEditor() - // Override default rendering of components and styles - // Also create page slugs - editor.on(ClientEvent.PUBLISH_START, () => { - transformComponents(config) - transformStyles(config) - transformPages(config) - }) - // Reset the components and styles rendering - editor.on(ClientEvent.PUBLISH_END, () => { - resetTransformComponents(config) - resetTransformStyles(config) - resetTransformPages(config) + // Check that there are no unknown properties + Object.keys(transformer).forEach(key => { + if(!allowedProperties.includes(key)) { + throw new Error(`Publication transformer: unknown property ${key}`) + } }) - // Transform files after generating all files to be published - editor.on(ClientEvent.PUBLISH_DATA, (data: PublicationData) => { - transformFiles(config, data) + + // Check that the methods are functions + allowedProperties.forEach(key => { + if(typeof transformer[key] !== 'function') { + throw new Error(`Publication transformer: ${key} must be a function`) + } }) } + /** * Alter the components rendering * Exported for unit tests */ -export function transformComponents(config: ClientConfig) { - const editor = config.getEditor() +export function renderComponents(editor: Editor) { + const config = editor.getModel().get('config') onAll(editor, (c: Component) => { if (c.get(ATTRIBUTE_METHOD_STORE_HTML)) { console.warn('Publication transformer: HTML transform already altered', c) } else { const initialToHTML = c.toHTML.bind(c) c[ATTRIBUTE_METHOD_STORE_HTML] = c.toHTML + const initialGetStyle = c.getStyle.bind(c) + c[ATTRIBUTE_METHOD_STORE_INLINE_CSS] = c.getStyle + const href = c.get('attributes').href as string | undefined + if(href?.startsWith('./')) { + //const page = editor.Pages.getAll().find(p => getPageLink(p.getName()) === href) + //if(page) { + c[ATTRIBUTE_METHOD_STORE_HREF] = href + c.set('attributes', { + ...c.get('attributes'), + //href: transformPagePermalin(editor, page), + href: transformPermalink(editor, href, ClientSideFileType.HTML), + }) + //} + } + if(c.get('src')) { + c[ATTRIBUTE_METHOD_STORE_SRC] = c.get('src') + c.set('src', transformPermalink(editor, c.get('src'), ClientSideFileType.ASSET)) + } c.toHTML = () => { - return config.publicationTransformers.reduce((html, transformer) => { + return config.publicationTransformers.reduce((html: string, transformer: PublicationTransformer) => { try { return transformer.renderComponent ? transformer.renderComponent(c, initialToHTML) ?? html : html } catch (e) { @@ -92,6 +118,7 @@ export function transformComponents(config: ClientConfig) { } }, initialToHTML()) } + c.getStyle = () => transformBgImage(editor, initialGetStyle()) } }) } @@ -100,23 +127,24 @@ export function transformComponents(config: ClientConfig) { * Alter the styles rendering * Exported for unit tests */ -export function transformStyles(config: ClientConfig) { - const editor = config.getEditor() - editor.Css.getAll().forEach(c => { - if (c[ATTRIBUTE_METHOD_STORE_CSS]) { - console.warn('Publication transformer: CSS transform already altered', c) +export function renderCssRules(editor: Editor) { + const config = editor.getModel().get('config') + editor.Css.getAll().forEach((style: CssRule) => { + if (style[ATTRIBUTE_METHOD_STORE_CSS]) { + console.warn('Publication transformer: CSS transform already altered', style) } else { - const initialGetStyle = c.getStyle.bind(c) - c[ATTRIBUTE_METHOD_STORE_CSS] = c.getStyle - c.getStyle = () => { + const initialGetStyle = style.getStyle.bind(style) + style[ATTRIBUTE_METHOD_STORE_CSS] = style.getStyle + style.getStyle = () => { try { - return config.publicationTransformers.reduce((style, transformer) => { + const result = config.publicationTransformers.reduce((s: CssRule, transformer: PublicationTransformer) => { return { - ...transformer.renderCssRule ? transformer.renderCssRule(c, initialGetStyle) ?? style : style, + ...transformer.renderCssRule ? transformer.renderCssRule(s, initialGetStyle) ?? s : s, } - }, initialGetStyle()) + }, transformBgImage(editor, initialGetStyle())) + return result } catch (e) { - console.error('Publication transformer: error rendering style', c, e) + console.error('Publication transformer: error rendering style', style, e) return initialGetStyle() } } @@ -125,33 +153,31 @@ export function transformStyles(config: ClientConfig) { } /** - * Create page slugs - * Exported for unit tests + * Transform background image url according to the transformed path of assets */ -export function transformPages(config: ClientConfig) { - const editor = config.getEditor() - editor.Pages.getAll().forEach((page) => { - page.set('slug', config.publicationTransformers.reduce((slug, transformer) => { - try { - return transformer.pageToSlug ? transformer.pageToSlug(page) ?? slug : slug - } catch (e) { - console.error('Publication transformer: error creating page slug', page, e) - return slug - } - }, page.get('slug'))) - }) +export function transformBgImage(editor: Editor, style: ObjectStrings): ObjectStrings { + const url = style['background-image'] + const bgUrl = url?.match(/url\(["']?(.*?)["']?\)/)?.pop() + if (bgUrl) { + return { + ...style, + 'background-image': `url(${transformPermalink(editor, bgUrl, ClientSideFileType.ASSET)})`, + } + } + return style } /** * Transform files * Exported for unit tests */ -export function transformFiles(config: ClientConfig, data: PublicationData) { +export function transformFiles(editor: Editor, data: PublicationData) { + const config = editor.getModel().get('config') data.files = config.publicationTransformers.reduce((files: ClientSideFile[], transformer: PublicationTransformer) => { return files.map((file, idx) => { try { const page = data.pages[idx] ?? null - return transformer.transformFile ? transformer.transformFile(file, page) as ClientSideFile ?? file : file + return transformer.transformFile ? transformer.transformFile(file) as ClientSideFile ?? file : file } catch (e) { console.error('Publication transformer: error transforming file', file, e) return file @@ -160,18 +186,59 @@ export function transformFiles(config: ClientConfig, data: PublicationData) { }, data.files) } -export function resetTransformComponents(config: ClientConfig) { - const editor = config.getEditor() +/** + * Transform files paths + * Exported for unit tests + */ +export function transformPermalink(editor: Editor, path: string, type: ClientSideFileType): string { + const config = editor.getModel().get('config') + return config.publicationTransformers.reduce((result: string, transformer: PublicationTransformer) => { + try { + return transformer.transformPermalink ? transformer.transformPermalink(path, type) ?? result : result + } catch (e) { + console.error('Publication transformer: error transforming path', path, e) + return result + } + }, path) +} + +export function transformPath(editor: Editor, path: string, type: ClientSideFileType): string { + const config = editor.getModel().get('config') + return config.publicationTransformers.reduce((result: string, transformer: PublicationTransformer) => { + try { + return transformer.transformPath ? transformer.transformPath(path, type) ?? result : result + } catch (e) { + console.error('Publication transformer: error transforming path', path, e) + return result + } + }, path) +} + +export function resetRenderComponents(editor: Editor) { onAll(editor, (c: Component) => { if (c[ATTRIBUTE_METHOD_STORE_HTML]) { c.toHTML = c[ATTRIBUTE_METHOD_STORE_HTML] delete c[ATTRIBUTE_METHOD_STORE_HTML] } + if (c[ATTRIBUTE_METHOD_STORE_INLINE_CSS]) { + c.getStyle = c[ATTRIBUTE_METHOD_STORE_INLINE_CSS] + delete c[ATTRIBUTE_METHOD_STORE_INLINE_CSS] + } + if(c[ATTRIBUTE_METHOD_STORE_SRC]) { + c.set('src', c[ATTRIBUTE_METHOD_STORE_SRC]) + delete c[ATTRIBUTE_METHOD_STORE_SRC] + } + if(c[ATTRIBUTE_METHOD_STORE_HREF]) { + c.set('attributes', { + ...c.get('attributes'), + href: c[ATTRIBUTE_METHOD_STORE_HREF], + }) + delete c[ATTRIBUTE_METHOD_STORE_HREF] + } }) } -export function resetTransformStyles(config: ClientConfig) { - const editor = config.getEditor() +export function resetRenderCssRules(editor: Editor) { editor.Css.getAll().forEach(c => { if (c[ATTRIBUTE_METHOD_STORE_CSS]) { c.getStyle = c[ATTRIBUTE_METHOD_STORE_CSS] @@ -179,10 +246,3 @@ export function resetTransformStyles(config: ClientConfig) { } }) } - -export function resetTransformPages(config: ClientConfig) { - const editor = config.getEditor() - editor.Pages.getAll().forEach(page => { - page.unset('slug') - }) -} diff --git a/src/ts/page.ts b/src/ts/page.ts index 07a8a85d1..ecbb705fb 100644 --- a/src/ts/page.ts +++ b/src/ts/page.ts @@ -18,7 +18,7 @@ // Page related functions // This is used on the client and the server export function getPageSlug(pageName) { - return pageName + return (pageName || 'index') .toLowerCase() .replace(/[^a-z0-9 -]/g, '') // Collapse whitespace and replace by - @@ -26,6 +26,6 @@ export function getPageSlug(pageName) { // Collapse dashes .replace(/-+/g, '-') } -export function getPageLink(pageName = 'index') { +export function getPageLink(pageName) { return `./${getPageSlug(pageName)}.html` } diff --git a/src/ts/plugins/client/11ty.ts b/src/ts/plugins/client/11ty.ts index 4e60272d9..377228e17 100644 --- a/src/ts/plugins/client/11ty.ts +++ b/src/ts/plugins/client/11ty.ts @@ -38,9 +38,8 @@ export default (config: ClientConfig, opts: Partial) => { } config.addPublicationTransformers({ - transformFile: (file: ClientSideFile, page: Page) => { + transformFile: (file: ClientSideFile) => { const fileWithContent = file as ClientSideFileWithContent - console.log('Silex: transform file for 11ty', fileWithContent) switch (file.type) { case 'html': return { diff --git a/src/ts/plugins/client/publicationRenderer.ts b/src/ts/plugins/client/publicationRenderer.ts index 317d00520..094fbddb6 100644 --- a/src/ts/plugins/client/publicationRenderer.ts +++ b/src/ts/plugins/client/publicationRenderer.ts @@ -15,8 +15,12 @@ * along with this program. If not, see . */ -// Silex: publication renderer plugin is deprecated. Use the publication renderer plugin instead. -console.warn('Silex: publication renderer plugin is deprecated. Use the publication renderer plugin instead.') +/** + * @deprecated + * This plugin is deprecated. Use the publication renderer plugin instead. + */ +console.warn('This plugin is deprecated. Use the publication renderer plugin instead.') +throw 'This plugin is deprecated. Use the publication renderer plugin instead.' /* Usage: import publicationRenderer from '/node_modules/@silexlabs/silex/dist/plugins/client/plugins/client/publicationRenderer.js' diff --git a/src/ts/plugins/server/FtpConnector.ts b/src/ts/plugins/server/FtpConnector.ts index 9bf704a4a..1c786bcfd 100644 --- a/src/ts/plugins/server/FtpConnector.ts +++ b/src/ts/plugins/server/FtpConnector.ts @@ -64,6 +64,7 @@ export interface FtpOptions { type: ConnectorType path: string assetsFolder: string + cssFolder: string // For publication only authorizeUrl: string authorizePath: string } @@ -238,6 +239,7 @@ export default class FtpConnector implements StorageConnector { this.options = { path: '', assetsFolder: 'assets', + cssFolder: 'css', authorizeUrl: '/api/authorize/ftp/', authorizePath: '/api/authorize/ftp/', ...opts, @@ -493,6 +495,16 @@ export default class FtpConnector implements StorageConnector { id: WebsiteId, files: ConnectorFile[], statusCbk?: StatusCallback, + ): Promise { + return this.writeFile(session, id, files, this.options.assetsFolder, statusCbk) + } + + async writeFile( + session: any, + id: WebsiteId, + files: ConnectorFile[], + relativePath: string, + statusCbk?: StatusCallback, ): Promise { // Connect to FTP server statusCbk && statusCbk({ @@ -517,7 +529,7 @@ export default class FtpConnector implements StorageConnector { message: `Writing file ${file.path}`, status: JobStatus.IN_PROGRESS, }) - const dstPath = join(this.options.path, rootPath, id, this.options.assetsFolder, file.path) + const dstPath = join(this.options.path, rootPath, id, relativePath, file.path) lastFile = file const result = await this.write(ftp, dstPath, file.content, message => { statusCbk && statusCbk({ @@ -576,9 +588,10 @@ export default class FtpConnector implements StorageConnector { const rootPath = this.rootPath(session) const ftp = await this.getClient(this.sessionData(session)) await this.mkdir(ftp, rootPath) - await this.mkdir(ftp, join(rootPath, 'assets')) + await this.mkdir(ftp, join(rootPath, this.options.assetsFolder)) + await this.mkdir(ftp, join(rootPath, this.options.cssFolder)) // Write files - this.writeAssets(session, '', files, async ({status, message}) => { + this.writeFile(session, '', files, '', async ({status, message}) => { // Update the job status job.status = status job.message = message diff --git a/src/ts/types.ts b/src/ts/types.ts index bd4c35da7..7f6d397eb 100644 --- a/src/ts/types.ts +++ b/src/ts/types.ts @@ -23,7 +23,6 @@ export interface PublicationSettings { connector?: ConnectorData, // Set by the postMessage from the login callback page options?: ConnectorOptions, // Options for the publication connector saved with the site - url?: string, // URL to display where the website is published to } export interface WebsiteFile { @@ -172,7 +171,25 @@ export type Selector = string | { */ export type ConnectorId = string -export type ConnectorOptions = object +export type ConnectorOptions = { + websiteUrl?: string, // For publication UI + [key: string]: any, +} + +export enum ClientSideFileType { + HTML = 'html', + ASSET = 'asset', + CSS = 'css', +} + +/** + * Type for a client side file when the content is not available, used to handle file names and paths and urls + */ +export interface ClientSideFileWithPermalink { + path: string, // Path in the connector + permalink: string, // Used to link to the file + type: ClientSideFileType, +} /** * Type for a client side file when the content is available as a string @@ -180,16 +197,16 @@ export type ConnectorOptions = object export interface ClientSideFileWithContent { path: string, content: string, // Not buffer because it's sent from the client in JSON - type: 'html' | 'asset' | 'css', + type: ClientSideFileType } /** - * Type for a client side file when the content is in the storage connector + * Type for a client side file when the content is in the connector */ export interface ClientSideFileWithSrc { path: string, src: string, // Where to download the file, a path for the storage connector - type: 'html' | 'asset' | 'css', + type: ClientSideFileType } /**