@silexlabs/silex 3.5.0 → 3.5.1-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.silex.js +0 -3
- package/dist/client/client/config.d.ts +8 -0
- package/dist/client/client/grapesjs/PublicationManager.d.ts +43 -0
- package/dist/client/client/grapesjs/cms/DataSource.d.ts +43 -0
- package/dist/client/client/grapesjs/cms/blocks.d.ts +2 -0
- package/dist/client/client/grapesjs/cms/buttons.d.ts +2 -0
- package/dist/client/client/grapesjs/cms/collection-pages.d.ts +4 -0
- package/dist/client/client/grapesjs/cms/config.d.ts +9 -0
- package/dist/client/client/grapesjs/cms/filters.d.ts +3 -0
- package/dist/client/client/grapesjs/cms/index.d.ts +44 -0
- package/dist/client/client/grapesjs/cms/liquid.d.ts +59 -0
- package/dist/client/client/grapesjs/cms/page-settings.d.ts +6 -0
- package/dist/client/client/grapesjs/cms/publication.d.ts +60 -0
- package/dist/client/client/grapesjs/cms/site-settings.d.ts +3 -0
- package/dist/client/client/grapesjs/cms/states.d.ts +8 -0
- package/dist/client/client/grapesjs/cms/traits.d.ts +8 -0
- package/dist/client/client/grapesjs/settings.d.ts +12 -3
- package/dist/client/client/grapesjs/view-buttons.d.ts +2 -1
- package/dist/client/client/utils.d.ts +1 -0
- package/dist/client/constants.d.ts +2 -1
- package/dist/client/css/admin.css +117 -19
- package/dist/client/index.html +2 -2
- package/dist/client/js/main.js +1974 -267
- package/dist/client/types.d.ts +3 -1
- package/dist/plugins/client/plugins/client/template.js +1 -1
- package/dist/plugins/client/plugins/client/template.js.map +1 -1
- package/dist/plugins/client/types.d.ts +3 -1
- package/dist/plugins/client/types.js +11 -8
- package/dist/plugins/client/types.js.map +1 -1
- package/dist/plugins/server/constants.d.ts +2 -1
- package/dist/plugins/server/constants.js +3 -2
- package/dist/plugins/server/constants.js.map +1 -1
- package/dist/plugins/server/plugins/server/FtpConnector.js +15 -20
- package/dist/plugins/server/plugins/server/FtpConnector.js.map +1 -1
- package/dist/plugins/server/plugins/server/GitlabConnector.js +34 -7
- package/dist/plugins/server/plugins/server/GitlabConnector.js.map +1 -1
- package/dist/plugins/server/server/connectors/FsStorage.js +16 -20
- package/dist/plugins/server/server/connectors/FsStorage.js.map +1 -1
- package/dist/plugins/server/server/utils/websiteDataSerialization.d.ts +4 -0
- package/dist/plugins/server/server/utils/websiteDataSerialization.js +21 -12
- package/dist/plugins/server/server/utils/websiteDataSerialization.js.map +1 -1
- package/dist/plugins/server/types.d.ts +3 -1
- package/dist/plugins/server/types.js +12 -9
- package/dist/plugins/server/types.js.map +1 -1
- package/dist/server/constants.js +3 -2
- package/dist/server/server/connectors/FsStorage.js +14 -18
- package/dist/server/server/utils/websiteDataSerialization.js +16 -11
- package/dist/server/types.js +6 -2
- package/package.json +16 -11
- package/src/css/_variables.scss +2 -1
- package/src/css/grapesjs-plugins.scss +31 -0
- package/src/css/grapesjs.scss +20 -0
- package/src/css/page-panel.scss +25 -6
- package/src/css/silex.scss +64 -5
- package/src/ts/client/config.ts +23 -0
- package/src/ts/client/grapesjs/PublicationManager.ts +159 -1
- package/src/ts/client/grapesjs/breadcrumbs.ts +3 -2
- package/src/ts/client/grapesjs/cms/DataSource.ts +333 -0
- package/src/ts/client/grapesjs/cms/blocks.ts +124 -0
- package/src/ts/client/grapesjs/cms/buttons.ts +163 -0
- package/src/ts/client/grapesjs/cms/collection-pages.ts +214 -0
- package/src/ts/client/grapesjs/cms/config.ts +47 -0
- package/src/ts/client/grapesjs/cms/filters.ts +53 -0
- package/src/ts/client/grapesjs/cms/index.ts +130 -0
- package/src/ts/client/grapesjs/cms/liquid.mock.ts +181 -0
- package/src/ts/client/grapesjs/cms/liquid.test.ts +239 -0
- package/src/ts/client/grapesjs/cms/liquid.ts +325 -0
- package/src/ts/client/grapesjs/cms/page-settings.ts +285 -0
- package/src/ts/client/grapesjs/cms/publication.test.ts +300 -0
- package/src/ts/client/grapesjs/cms/publication.ts +708 -0
- package/src/ts/client/grapesjs/cms/site-settings.ts +79 -0
- package/src/ts/client/grapesjs/cms/states.ts +125 -0
- package/src/ts/client/grapesjs/cms/traits.ts +95 -0
- package/src/ts/client/grapesjs/css-props.ts +15 -16
- package/src/ts/client/grapesjs/flex.ts +59 -39
- package/src/ts/client/grapesjs/img.ts +1 -1
- package/src/ts/client/grapesjs/index.ts +77 -4
- package/src/ts/client/grapesjs/internal-links.ts +4 -2
- package/src/ts/client/grapesjs/page-panel.ts +126 -11
- package/src/ts/client/grapesjs/sectors.ts +18 -10
- package/src/ts/client/grapesjs/semantic.ts +4 -0
- package/src/ts/client/grapesjs/settings.ts +252 -101
- package/src/ts/client/grapesjs/storage.ts +12 -3
- package/src/ts/client/grapesjs/view-buttons.ts +39 -3
- package/src/ts/client/utils.ts +15 -2
- package/src/ts/constants.ts +2 -1
- package/src/ts/plugins/client/template.ts +1 -1
- package/src/ts/plugins/server/FtpConnector.test.ts +1 -0
- package/src/ts/plugins/server/FtpConnector.ts +20 -23
- package/src/ts/plugins/server/GitlabConnector.ts +40 -10
- package/src/ts/server/connectors/FsStorage.test.ts +3 -2
- package/src/ts/server/connectors/FsStorage.ts +21 -23
- package/src/ts/server/utils/websiteDataSerialization.test.ts +45 -13
- package/src/ts/server/utils/websiteDataSerialization.ts +23 -14
- package/src/ts/types.ts +6 -2
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
import dedent from 'dedent'
|
|
2
|
+
import { Component, Page, Editor } from 'grapesjs'
|
|
3
|
+
import { BinaryOperator, Filter, GraphQLOptions, IDataSource, NOTIFICATION_GROUP, Properties, Property, State, StateId, StoredState, Token, UnariOperator, fromStored, getAllDataSources, getDataSource, getPageQuery, getPersistantId, getState, getStateIds, getStateVariableName, toExpression } from '@silexlabs/grapesjs-data-source'
|
|
4
|
+
import { assignBlock, echoBlock, echoBlock1line, getPaginationData, ifBlock, loopBlock } from './liquid'
|
|
5
|
+
import { EleventyPluginOptions, Silex11tyPluginWebsiteSettings } from './index'
|
|
6
|
+
import { PublicationTransformer } from '../../publication-transformers'
|
|
7
|
+
import { ClientConfig } from '../../config'
|
|
8
|
+
import { UNWRAP_ID } from './traits'
|
|
9
|
+
import { EleventyDataSourceId } from './DataSource'
|
|
10
|
+
import { ClientEvent } from '../../events'
|
|
11
|
+
import { WebsiteSettings, ClientSideFile, ClientSideFileType, ClientSideFileWithContent, PublicationData } from '../../../types'
|
|
12
|
+
|
|
13
|
+
const ATTRIBUTE_MULTIPLE_VALUES = ['class', 'style']
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A memoization mechanism to avoid rendering the same component multiple times
|
|
17
|
+
* The cache is cleared every time the publication is done
|
|
18
|
+
* This is a workaround because grapesjs editor.getHtml will call each component's toHtml method multiple times
|
|
19
|
+
*/
|
|
20
|
+
const cache = new Map<string, string>()
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A state with the real tokens instead of the stored tokens
|
|
24
|
+
*/
|
|
25
|
+
interface RealState {
|
|
26
|
+
stateId: StateId,
|
|
27
|
+
label?: string,
|
|
28
|
+
tokens: Token[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getFetchPluginOptions(options: EleventyPluginOptions, settings: Silex11tyPluginWebsiteSettings): object | false {
|
|
32
|
+
if(typeof options.fetchPluginSettings !== 'undefined') {
|
|
33
|
+
return options.fetchPluginSettings
|
|
34
|
+
}
|
|
35
|
+
return settings.eleventyFetch ? { duration: '1s' } : false
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default function (editor: Editor, options: EleventyPluginOptions) {
|
|
39
|
+
/* @ts-ignore There should be a better way to get Silex config */
|
|
40
|
+
const config = window.silex.config as ClientConfig
|
|
41
|
+
// Generate the liquid when the site is published
|
|
42
|
+
config.addPublicationTransformers({
|
|
43
|
+
// Render the components when they are published
|
|
44
|
+
// Will run even with enable11ty = false in order to enable HTML attributes
|
|
45
|
+
renderComponent: (component: Component, toHtml: () => string) => withNotification(() => renderComponent(editor, component, toHtml), editor, component.getId()),
|
|
46
|
+
// Transform the paths to be published according to options.urls
|
|
47
|
+
transformPermalink: options.enable11ty ? (path: string, type: string) => withNotification(() => transformPermalink(editor, path, type, options), editor, null) : undefined,
|
|
48
|
+
// Transform the paths to be published according to options.dir
|
|
49
|
+
transformPath: options.enable11ty ? (path: string, type: string) => withNotification(() => transformPath(editor, path, type, options), editor, null) : undefined,
|
|
50
|
+
// Transform the files content
|
|
51
|
+
//transformFile: (file) => transformFile(file),
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
if (options.enable11ty) {
|
|
55
|
+
// Generate 11ty data files
|
|
56
|
+
// FIXME: should this be in the publication transformers
|
|
57
|
+
editor.on('silex:publish:page', (data, ...args) => {
|
|
58
|
+
withNotification(() => transformPage(editor, data), editor, null)
|
|
59
|
+
})
|
|
60
|
+
editor.on('silex:publish:data', ({ data/*, preventDefault, publicationManager */ }, ...args) => {
|
|
61
|
+
withNotification(() => transformFiles(editor, options, data, config), editor, null)
|
|
62
|
+
})
|
|
63
|
+
editor.on('silex:publish:end', (...args) => {
|
|
64
|
+
cache.clear()
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if the 11ty publication is enabled
|
|
71
|
+
*/
|
|
72
|
+
function enable11ty(): boolean {
|
|
73
|
+
return getAllDataSources()
|
|
74
|
+
.filter(ds => ds.id !== EleventyDataSourceId)
|
|
75
|
+
.length > 0
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Make html attribute
|
|
80
|
+
* Quote strings, no values for boolean
|
|
81
|
+
*/
|
|
82
|
+
function makeAttribute(key: string, value: string | boolean): string {
|
|
83
|
+
switch (typeof value) {
|
|
84
|
+
case 'boolean': return value ? key : ''
|
|
85
|
+
default: return `${key}="${value}"`
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Transform the file name to be published
|
|
91
|
+
*/
|
|
92
|
+
function slugify(text: string | number) {
|
|
93
|
+
return text.toString().toLowerCase()
|
|
94
|
+
.replace(/\s+/g, '-') // Replace spaces with -
|
|
95
|
+
.replace(/[^a-z0-9-]/g, '') // Remove all non-word chars
|
|
96
|
+
.replace(/--+/g, '-') // Replace multiple - with single -
|
|
97
|
+
.replace(/^-+/, '') // Trim - from start of text
|
|
98
|
+
.replace(/-+$/, '') // Trim - from end of text
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getPermalink(page: Page, permalink: Token[], isCollectionPage: boolean, slug: string): string | null {
|
|
102
|
+
const isHome = slug === 'index'
|
|
103
|
+
// User provided a permalink explicitely
|
|
104
|
+
if (permalink && permalink.length > 0) {
|
|
105
|
+
const body = page.getMainComponent() as Component
|
|
106
|
+
return echoBlock1line(body, permalink.map(token => {
|
|
107
|
+
// Replace states which will be one from ./states.ts
|
|
108
|
+
if(token.type === 'state') {
|
|
109
|
+
const state = getState(body, token.storedStateId, true)
|
|
110
|
+
if(!state) throw new Error('State not found on body')
|
|
111
|
+
return {
|
|
112
|
+
...state.expression[0],
|
|
113
|
+
dataSourceId: undefined,
|
|
114
|
+
fieldId: token.label,
|
|
115
|
+
} as Property
|
|
116
|
+
}
|
|
117
|
+
return token
|
|
118
|
+
}))
|
|
119
|
+
} else if (isCollectionPage) {
|
|
120
|
+
// Let 11ty handle the permalink
|
|
121
|
+
return null
|
|
122
|
+
} else if (isHome) {
|
|
123
|
+
// Normal home page
|
|
124
|
+
return '/index.html'
|
|
125
|
+
} else {
|
|
126
|
+
// Use the page name
|
|
127
|
+
return `/${slug}/index.html`
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get the front matter for a given page
|
|
133
|
+
*/
|
|
134
|
+
export function getFrontMatter(page: Page, settings: Silex11tyPluginWebsiteSettings, slug: string, collection: string, lang = ''): string {
|
|
135
|
+
const data = (function() {
|
|
136
|
+
if(!settings.eleventyPageData) return undefined
|
|
137
|
+
const expression = toExpression(settings.eleventyPageData)
|
|
138
|
+
if(expression) {
|
|
139
|
+
if(expression.filter(token => token.type !== 'property').length > 0) {
|
|
140
|
+
console.warn('Expression for pagination data has to contain only properties', expression.map(token => token.type))
|
|
141
|
+
}
|
|
142
|
+
return getPaginationData(expression as Property[])
|
|
143
|
+
} else {
|
|
144
|
+
// Probably not JSON (backward compat)
|
|
145
|
+
return settings.eleventyPageData
|
|
146
|
+
}
|
|
147
|
+
})()
|
|
148
|
+
|
|
149
|
+
const isCollectionPage = !!data && data.length > 0
|
|
150
|
+
const permalinkExpression = toExpression(settings.eleventyPermalink)
|
|
151
|
+
// Here permalinkExpression contains filters and properties. It contains 11ty data source states too
|
|
152
|
+
const permalink = getPermalink(page, permalinkExpression as (Property | Filter)[], isCollectionPage, slug)
|
|
153
|
+
// Escape quotes in permalink
|
|
154
|
+
// because it is in double quotes in the front matter
|
|
155
|
+
?.replace(/"/g, '\\"')
|
|
156
|
+
|
|
157
|
+
return dedent`---
|
|
158
|
+
${data && data.length > 0 ? `pagination:
|
|
159
|
+
addAllPagesToCollections: true
|
|
160
|
+
data: ${data}
|
|
161
|
+
size: ${settings.eleventyPageSize ? settings.eleventyPageSize : '1'}
|
|
162
|
+
${settings.eleventyPageReverse ? 'reverse: true' : ''}
|
|
163
|
+
` : ''}
|
|
164
|
+
${permalink ? `permalink: "${permalink}"` : ''}
|
|
165
|
+
${lang ? `lang: "${lang}"` : ''}
|
|
166
|
+
${collection ? `collection: "${collection}"` : ''}
|
|
167
|
+
${settings?.eleventyNavigationKey ? `eleventyNavigation:
|
|
168
|
+
key: ${settings.eleventyNavigationKey}
|
|
169
|
+
${settings.eleventyNavigationTitle ? `title: ${settings.eleventyNavigationTitle}` : ''}
|
|
170
|
+
${settings.eleventyNavigationOrder ? `order: ${settings.eleventyNavigationOrder}` : ''}
|
|
171
|
+
${settings.eleventyNavigationParent ? `parent: ${settings.eleventyNavigationParent}` : ''}
|
|
172
|
+
${settings.eleventyNavigationUrl ? `url: ${settings.eleventyNavigationUrl}` : ''}
|
|
173
|
+
` : ''}
|
|
174
|
+
`
|
|
175
|
+
// Prettify
|
|
176
|
+
.split('\n')
|
|
177
|
+
.filter(line => line.trim().length > 0)
|
|
178
|
+
.concat(['', '---', ''])
|
|
179
|
+
.join('\n')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get the body states for a given page
|
|
184
|
+
*/
|
|
185
|
+
export function getBodyStates(page: Page): string {
|
|
186
|
+
// Render the body states
|
|
187
|
+
const body = page.getMainComponent() as Component
|
|
188
|
+
const pagination = getState(body, 'pagination', true)
|
|
189
|
+
if (pagination && pagination.expression.length > 0) {
|
|
190
|
+
//const block = getLiquidBlock(body, pagination.expression)
|
|
191
|
+
const bodyId = getPersistantId(body)
|
|
192
|
+
if (bodyId) {
|
|
193
|
+
return dedent`
|
|
194
|
+
{% assign ${getStateVariableName(bodyId, 'pagination')} = pagination %}
|
|
195
|
+
{% assign ${getStateVariableName(bodyId, 'items')} = pagination.items %}
|
|
196
|
+
{% assign ${getStateVariableName(bodyId, 'pages')} = pagination.pages %}
|
|
197
|
+
`
|
|
198
|
+
} else {
|
|
199
|
+
console.error('body has no persistant ID => do not add liquid for 11ty data')
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return ''
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function transformPage(editor: Editor, data: { page: Page, siteSettings: WebsiteSettings, pageSettings: Silex11tyPluginWebsiteSettings }): void {
|
|
206
|
+
// Do nothing if there is no data source, just a static site
|
|
207
|
+
if(!enable11ty()) return
|
|
208
|
+
|
|
209
|
+
const { pageSettings, page } = data
|
|
210
|
+
const body = page.getMainComponent()
|
|
211
|
+
if (pageSettings.eleventySeoTitle) {
|
|
212
|
+
const expression = toExpression(pageSettings.eleventySeoTitle)
|
|
213
|
+
if (expression && expression.length) pageSettings.title = echoBlock(body, expression)
|
|
214
|
+
}
|
|
215
|
+
if (pageSettings.eleventySeoDescription) {
|
|
216
|
+
const expression = toExpression(pageSettings.eleventySeoDescription)
|
|
217
|
+
if (expression && expression.length) pageSettings.description = echoBlock(body, expression)
|
|
218
|
+
}
|
|
219
|
+
if (pageSettings.eleventyFavicon) {
|
|
220
|
+
const expression = toExpression(pageSettings.eleventyFavicon)
|
|
221
|
+
if (expression && expression.length) pageSettings.favicon = echoBlock(body, expression)
|
|
222
|
+
}
|
|
223
|
+
if (pageSettings.eleventyOGImage) {
|
|
224
|
+
const expression = toExpression(pageSettings.eleventyOGImage)
|
|
225
|
+
if (expression && expression.length) pageSettings['og:image'] = echoBlock(body, expression)
|
|
226
|
+
}
|
|
227
|
+
if (pageSettings.eleventyOGTitle) {
|
|
228
|
+
const expression = toExpression(pageSettings.eleventyOGTitle)
|
|
229
|
+
if (expression && expression.length) pageSettings['og:title'] = echoBlock(body, expression)
|
|
230
|
+
}
|
|
231
|
+
if (pageSettings.eleventyOGDescription) {
|
|
232
|
+
const expression = toExpression(pageSettings.eleventyOGDescription)
|
|
233
|
+
if (expression && expression.length) pageSettings['og:description'] = echoBlock(body, expression)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Transform the files to be published
|
|
239
|
+
* This hook is called just before the files are written to the file system
|
|
240
|
+
* Exported for unit tests
|
|
241
|
+
*/
|
|
242
|
+
export function transformFiles(editor: Editor, options: EleventyPluginOptions, data: PublicationData, config: ClientConfig): void {
|
|
243
|
+
// Do nothing if there is no data source, just a static site
|
|
244
|
+
if(!enable11ty()) return
|
|
245
|
+
|
|
246
|
+
editor.Pages.getAll().forEach(page => {
|
|
247
|
+
// Get the page properties
|
|
248
|
+
const slug = slugify(page.getName() || 'index')
|
|
249
|
+
const settings = (page.get('settings') ?? {}) as Silex11tyPluginWebsiteSettings
|
|
250
|
+
const languages = settings.silexLanguagesList?.split(',').map(lang => lang.trim()).filter(lang => !!lang)
|
|
251
|
+
|
|
252
|
+
// Create the data file for this page
|
|
253
|
+
const query = getPageQuery(page, editor)
|
|
254
|
+
// Remove empty data source queries
|
|
255
|
+
Object.entries(query).forEach(([key, value]) => {
|
|
256
|
+
if (value.length === 0) {
|
|
257
|
+
delete query[key]
|
|
258
|
+
}
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// Find the page in the published data
|
|
262
|
+
if (!data.files) throw new Error('No files in publication data')
|
|
263
|
+
const path = transformPath(editor, `/${slug}.html`, ClientSideFileType.HTML, config.cmsConfig as EleventyPluginOptions)
|
|
264
|
+
const pageData = data.files.find(file => file.path === path) as ClientSideFileWithContent | undefined
|
|
265
|
+
if (!pageData) throw new Error(`No file for path ${path}`)
|
|
266
|
+
if (pageData.type !== ClientSideFileType.HTML) throw new Error(`File for path ${path} is not HTML`)
|
|
267
|
+
const dataFile = Object.keys(query).length > 0 ? {
|
|
268
|
+
type: ClientSideFileType.OTHER,
|
|
269
|
+
path: transformPath(editor, `/${slugify(page.getName() || 'index')}.11tydata.mjs`, ClientSideFileType.HTML, config.cmsConfig as EleventyPluginOptions),
|
|
270
|
+
//path: `/${page.getName() || 'index'}.11tydata.mjs`,
|
|
271
|
+
content: getDataFile(editor, page, null, query, options),
|
|
272
|
+
} : null
|
|
273
|
+
|
|
274
|
+
if (languages && languages.length > 0) {
|
|
275
|
+
const pages: ClientSideFileWithContent[] = languages.flatMap(lang => {
|
|
276
|
+
// Change the HTML
|
|
277
|
+
const frontMatter = getFrontMatter(page, settings, slug, page.getName(), lang)
|
|
278
|
+
const bodyStates = getBodyStates(page)
|
|
279
|
+
const pageFile = {
|
|
280
|
+
type: ClientSideFileType.HTML,
|
|
281
|
+
path: path.replace(/\.html$/, `-${lang}.html`),
|
|
282
|
+
content: frontMatter + bodyStates + pageData.content,
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Create the data file for this page
|
|
286
|
+
if (dataFile) {
|
|
287
|
+
return [pageFile, {
|
|
288
|
+
...dataFile,
|
|
289
|
+
path: dataFile.path.replace(/\.11tydata\.mjs$/, `-${lang}.11tydata.mjs`),
|
|
290
|
+
content: getDataFile(editor, page, lang, query, options),
|
|
291
|
+
}] // It is important to keep pageFile first, see bellow
|
|
292
|
+
}
|
|
293
|
+
return pageFile
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
// Update the existing page
|
|
297
|
+
const [existingPage, ...newPages] = pages
|
|
298
|
+
pageData.content = existingPage.content
|
|
299
|
+
pageData.path = existingPage.path
|
|
300
|
+
|
|
301
|
+
// Add the other pages
|
|
302
|
+
data.files.push(...newPages)
|
|
303
|
+
} else {
|
|
304
|
+
// Change the HTML
|
|
305
|
+
const frontMatter = getFrontMatter(page, settings, slug, page.getName())
|
|
306
|
+
const bodyStates = getBodyStates(page)
|
|
307
|
+
|
|
308
|
+
// Update the page before it is published
|
|
309
|
+
const content = frontMatter + bodyStates + pageData.content
|
|
310
|
+
pageData.content = content
|
|
311
|
+
|
|
312
|
+
// Add the data file
|
|
313
|
+
if (dataFile) {
|
|
314
|
+
// There is at least 1 query in this page
|
|
315
|
+
data.files.push(dataFile)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Generate the data file for a given silex page
|
|
323
|
+
* This file will be used by 11ty to generate the final website's page
|
|
324
|
+
* 11ty will use this file to get the data from the data sources
|
|
325
|
+
* - Language
|
|
326
|
+
* - Native fetch or 11ty-fetch plugin
|
|
327
|
+
* - esModule or commonjs
|
|
328
|
+
* - Cache buster
|
|
329
|
+
*
|
|
330
|
+
*/
|
|
331
|
+
function getDataFile(editor: Editor, page: Page, lang: string | null, query: Record<string, string>, options: EleventyPluginOptions): string {
|
|
332
|
+
const esModule = options.esModule === true || typeof options.esModule === 'undefined'
|
|
333
|
+
const fetchPlugin = getFetchPluginOptions(options, editor.getModel().get('settings') || {})
|
|
334
|
+
const fetchImportStatement = fetchPlugin ? (esModule ? 'import EleventyFetch from \'@11ty/eleventy-fetch\'' : 'const EleventyFetch = require(\'@11ty/eleventy-fetch\')') : ''
|
|
335
|
+
const exportStatement = esModule ? 'export default' : 'module.exports ='
|
|
336
|
+
|
|
337
|
+
const content = Object.entries(query).map(([dataSourceId, queryStr]) => {
|
|
338
|
+
const dataSource = getDataSource(dataSourceId)
|
|
339
|
+
if (dataSource) {
|
|
340
|
+
return queryToDataFile(dataSource, queryStr, options, page, lang, fetchPlugin)
|
|
341
|
+
} else {
|
|
342
|
+
console.error('No data source for id', dataSourceId)
|
|
343
|
+
throw new Error(`No data source for id ${dataSourceId}`)
|
|
344
|
+
}
|
|
345
|
+
}).join('\n')
|
|
346
|
+
return `
|
|
347
|
+
${fetchImportStatement}
|
|
348
|
+
${exportStatement} async function (configData) {
|
|
349
|
+
const data = {
|
|
350
|
+
...configData,
|
|
351
|
+
lang: '${lang || ''}',
|
|
352
|
+
}
|
|
353
|
+
const result = {}
|
|
354
|
+
${content}
|
|
355
|
+
return result
|
|
356
|
+
}
|
|
357
|
+
`
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Exported for unit tests
|
|
362
|
+
*/
|
|
363
|
+
export function queryToDataFile(dataSource: IDataSource, queryStr: string, options: EleventyPluginOptions, page: Page, lang: string | null, fetchPlugin: object | false): string {
|
|
364
|
+
if (dataSource.type !== 'graphql') {
|
|
365
|
+
console.info('not graphql', dataSource)
|
|
366
|
+
return ''
|
|
367
|
+
}
|
|
368
|
+
const s2s = (dataSource as GraphQLOptions).serverToServer
|
|
369
|
+
const url = s2s ? s2s.url : dataSource.url
|
|
370
|
+
const urlWithCacheBuster = options.cacheBuster ? `${url}${url.includes('?') ? '&' : '?'}page_id_for_cache=${page.getId()}${lang ? `-${lang}` : ''}` : url
|
|
371
|
+
const method = s2s ? s2s.method : dataSource.method
|
|
372
|
+
const headers = s2s ? s2s.headers : dataSource.headers
|
|
373
|
+
if (headers && !Object.keys(headers).find(key => key.toLowerCase() === 'content-type')) {
|
|
374
|
+
console.warn('11ty plugin for Silex: no content-type in headers of the graphql query. I will set it to application/json for you. To avoid this warning, add a header with key "content-type" and value "application/json" in silex config.')
|
|
375
|
+
headers['content-type'] = 'application/json'
|
|
376
|
+
}
|
|
377
|
+
const headersStr = headers ? Object.entries(headers).map(([key, value]) => `'${key}': \`${value}\`,`).join('\n') : ''
|
|
378
|
+
|
|
379
|
+
const fetchOptions = {
|
|
380
|
+
key: dataSource.id as string,
|
|
381
|
+
method: method || 'POST',
|
|
382
|
+
url: urlWithCacheBuster,
|
|
383
|
+
headers: headersStr,
|
|
384
|
+
query: `JSON.stringify({
|
|
385
|
+
query: \`${queryStr}\`,
|
|
386
|
+
})`, // Let 11ty interpolate the query wich let us add variables in the plugin config
|
|
387
|
+
}
|
|
388
|
+
return fetchPlugin ? makeFetchCallEleventy(fetchOptions, fetchPlugin) : makeFetchCall(fetchOptions)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function makeFetchCall(options: {key: string, url: string, method: string, headers: string, query: string}): string {
|
|
392
|
+
return dedent`
|
|
393
|
+
try {
|
|
394
|
+
const response = await fetch(\`${options.url}\`, {
|
|
395
|
+
|
|
396
|
+
headers: {
|
|
397
|
+
${options.headers}
|
|
398
|
+
},
|
|
399
|
+
method: '${options.method}',
|
|
400
|
+
body: ${options.query}
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
if (!response.ok) {
|
|
404
|
+
throw new Error(\`Error fetching graphql data: HTTP status code \${response.status}, HTTP status text: \${response.statusText}\`)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const json = await response.json()
|
|
408
|
+
|
|
409
|
+
if (json.errors) {
|
|
410
|
+
throw new Error(\`GraphQL error: \\n> \${json.errors.map(e => e.message).join('\\n> ')}\`)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
result['${options.key}'] = json.data
|
|
414
|
+
} catch (e) {
|
|
415
|
+
console.error('11ty plugin for Silex: error fetching graphql data', e, '${options.key}', '${options.url}')
|
|
416
|
+
throw e
|
|
417
|
+
}
|
|
418
|
+
`
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function makeFetchCallEleventy(options: {key: string, url: string, method: string, headers: string, query: string}, fetchPlugin: object): string {
|
|
422
|
+
return dedent`
|
|
423
|
+
try {
|
|
424
|
+
const json = await EleventyFetch(\`${options.url}\`, {
|
|
425
|
+
...${JSON.stringify(fetchPlugin)},
|
|
426
|
+
type: 'json',
|
|
427
|
+
fetchOptions: {
|
|
428
|
+
headers: {
|
|
429
|
+
${options.headers}
|
|
430
|
+
},
|
|
431
|
+
method: '${options.method}',
|
|
432
|
+
body: ${options.query},
|
|
433
|
+
}
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
if (json.errors) {
|
|
437
|
+
throw new Error(\`GraphQL error: \\n> \${json.errors.map(e => e.message).join('\\n> ')}\`)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
result['${options.key}'] = json.data
|
|
441
|
+
} catch (e) {
|
|
442
|
+
console.error('11ty plugin for Silex: error fetching graphql data', e, '${options.key}', '${options.url}')
|
|
443
|
+
throw e
|
|
444
|
+
}
|
|
445
|
+
`
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Make stored states into real states
|
|
450
|
+
* Filter out hidden states and empty expressions
|
|
451
|
+
*/
|
|
452
|
+
function getRealStates(states: { stateId: StateId, state: StoredState }[]): { stateId: StateId, label: string, tokens: State[] }[] {
|
|
453
|
+
return states
|
|
454
|
+
.filter(({ state }) => !state.hidden)
|
|
455
|
+
.filter(({ state }) => state.expression.length > 0)
|
|
456
|
+
// From expression of stored tokens to tokens (with methods not only data)
|
|
457
|
+
.map(({ stateId, state }) => ({
|
|
458
|
+
stateId,
|
|
459
|
+
label: state.label || stateId,
|
|
460
|
+
tokens: state.expression.map(token => {
|
|
461
|
+
const componentId = state.expression[0].type === 'state' ? state.expression[0].componentId : null
|
|
462
|
+
return fromStored(token, componentId)
|
|
463
|
+
}),
|
|
464
|
+
}))
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Check if a state is an attribute
|
|
469
|
+
* Exported for unit tests
|
|
470
|
+
*/
|
|
471
|
+
export function isAttribute(label: string): boolean {
|
|
472
|
+
if (!label) return false
|
|
473
|
+
return !Object.values(Properties).includes(label as Properties)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Build the attributes string for a given component
|
|
478
|
+
* Handle attributes which appear multiple times (class, style)
|
|
479
|
+
* Append to the original attributes
|
|
480
|
+
* Exported for unit tests
|
|
481
|
+
*/
|
|
482
|
+
export function buildAttributes(originalAttributes: Record<string, string>, attributeStates: { stateId: StateId, label: string, value: string }[]): string {
|
|
483
|
+
const attributesArr = Object.entries(originalAttributes)
|
|
484
|
+
// Start with the original attributes
|
|
485
|
+
.map(([label, value]) => ({
|
|
486
|
+
stateId: label,
|
|
487
|
+
label,
|
|
488
|
+
value,
|
|
489
|
+
}))
|
|
490
|
+
// Override or add state attributes
|
|
491
|
+
.concat(attributeStates)
|
|
492
|
+
// Handle attributes which appear multiple times
|
|
493
|
+
.reduce((final, { stateId, label, value }) => {
|
|
494
|
+
const existing = final.find(({ label: existingLabel }) => existingLabel === label)
|
|
495
|
+
if (existing) {
|
|
496
|
+
if (ATTRIBUTE_MULTIPLE_VALUES.includes(label)) {
|
|
497
|
+
// Add to the original value
|
|
498
|
+
existing.value += ' ' + value
|
|
499
|
+
} else {
|
|
500
|
+
// Override the original value
|
|
501
|
+
existing.value = value
|
|
502
|
+
}
|
|
503
|
+
} else {
|
|
504
|
+
// First time we see this attribute
|
|
505
|
+
final.push({
|
|
506
|
+
stateId,
|
|
507
|
+
label,
|
|
508
|
+
value,
|
|
509
|
+
})
|
|
510
|
+
}
|
|
511
|
+
// Return the original array
|
|
512
|
+
return final
|
|
513
|
+
}, [] as ({ stateId: StateId, value: string | boolean, label: string })[])
|
|
514
|
+
// Build final result
|
|
515
|
+
return attributesArr
|
|
516
|
+
// Convert to key="value" string
|
|
517
|
+
.map(({ label, value }) => makeAttribute(label, value))
|
|
518
|
+
// Back to string
|
|
519
|
+
.join(' ')
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function withNotification<T>(cbk: () => T, editor: Editor, componentId: string | null): T {
|
|
523
|
+
try {
|
|
524
|
+
return cbk()
|
|
525
|
+
} catch (e) {
|
|
526
|
+
editor.runCommand('notifications:add', {
|
|
527
|
+
type: 'error',
|
|
528
|
+
message: `Error rendering component: ${e.message}`,
|
|
529
|
+
group: NOTIFICATION_GROUP,
|
|
530
|
+
componentId,
|
|
531
|
+
})
|
|
532
|
+
throw e
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Render the components when they are published
|
|
538
|
+
*/
|
|
539
|
+
function renderComponent(editor: Editor, component: Component, toHtml: () => string): string | undefined {
|
|
540
|
+
if(cache.has(component.getId())) {
|
|
541
|
+
return cache.get(component.getId())
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const statesPrivate = withNotification(() => getRealStates(getStateIds(component, false)
|
|
545
|
+
.map(stateId => ({
|
|
546
|
+
stateId,
|
|
547
|
+
state: getState(component, stateId, false)!,
|
|
548
|
+
}))), editor, component.getId())
|
|
549
|
+
|
|
550
|
+
const statesPublic = withNotification(() => getRealStates(getStateIds(component, true)
|
|
551
|
+
.map(stateId => ({
|
|
552
|
+
stateId,
|
|
553
|
+
state: getState(component, stateId, true)!,
|
|
554
|
+
}))), editor, component.getId())
|
|
555
|
+
|
|
556
|
+
const unwrap = component.get(UNWRAP_ID)
|
|
557
|
+
|
|
558
|
+
if (statesPrivate.length > 0 || statesPublic.length > 0 || unwrap) {
|
|
559
|
+
const tagName = component.get('tagName')?.toLowerCase()
|
|
560
|
+
if (tagName) {
|
|
561
|
+
// Convenience key value object
|
|
562
|
+
const statesObj = statesPrivate
|
|
563
|
+
// Filter out attributes, keep only properties
|
|
564
|
+
.filter(({ label }) => !isAttribute(label))
|
|
565
|
+
// Add states
|
|
566
|
+
.concat(statesPublic)
|
|
567
|
+
.reduce((final, { stateId, label, tokens }) => ({
|
|
568
|
+
...final,
|
|
569
|
+
[stateId]: {
|
|
570
|
+
stateId,
|
|
571
|
+
label,
|
|
572
|
+
tokens,
|
|
573
|
+
},
|
|
574
|
+
}), {} as Record<Properties, RealState>)
|
|
575
|
+
|
|
576
|
+
const hasInnerHtml = !!statesObj.innerHTML?.tokens.length
|
|
577
|
+
const hasCondition = !!statesObj.condition?.tokens.length
|
|
578
|
+
const hasData = !!statesObj.__data?.tokens.length
|
|
579
|
+
|
|
580
|
+
// Style attribute
|
|
581
|
+
const innerHtml = hasInnerHtml ? echoBlock(component, statesObj.innerHTML.tokens) : component.getInnerHTML()
|
|
582
|
+
const operator = component.get('conditionOperator') ?? UnariOperator.TRUTHY
|
|
583
|
+
const binary = operator && Object.values(BinaryOperator).includes(operator)
|
|
584
|
+
const [ifStart, ifEnd] = hasCondition ? ifBlock(component, binary ? {
|
|
585
|
+
expression: statesObj.condition.tokens,
|
|
586
|
+
expression2: statesObj.condition2?.tokens ?? [],
|
|
587
|
+
operator,
|
|
588
|
+
} : {
|
|
589
|
+
expression: statesObj.condition.tokens,
|
|
590
|
+
operator,
|
|
591
|
+
}) : []
|
|
592
|
+
const [forStart, forEnd] = hasData ? loopBlock(component, statesObj.__data.tokens) : []
|
|
593
|
+
const states = statesPublic
|
|
594
|
+
.map(({ stateId, tokens }) => assignBlock(stateId, component, tokens))
|
|
595
|
+
.join('\n')
|
|
596
|
+
const before = (states ?? '') + (forStart ?? '') + (ifStart ?? '')
|
|
597
|
+
const after = (ifEnd ?? '') + (forEnd ?? '')
|
|
598
|
+
|
|
599
|
+
// Attributes
|
|
600
|
+
const originalAttributes = component.get('attributes') as Record<string, string>
|
|
601
|
+
// Add css classes
|
|
602
|
+
originalAttributes.class = component.getClasses().join(' ')
|
|
603
|
+
// Make the list of attributes
|
|
604
|
+
const attributes = buildAttributes(originalAttributes, statesPrivate
|
|
605
|
+
// Filter out properties, keep only attributes
|
|
606
|
+
.filter(({ label }) => isAttribute(label))
|
|
607
|
+
// Make tokens a string
|
|
608
|
+
.map(({ stateId, tokens, label }) => ({
|
|
609
|
+
stateId,
|
|
610
|
+
label,
|
|
611
|
+
value: echoBlock(component, tokens),
|
|
612
|
+
}))
|
|
613
|
+
)
|
|
614
|
+
if (unwrap) {
|
|
615
|
+
const html = `${before}${innerHtml}${after}`
|
|
616
|
+
cache.set(component.getId(), html)
|
|
617
|
+
return html
|
|
618
|
+
} else {
|
|
619
|
+
const html = `${before}<${tagName}${attributes ? ` ${attributes}` : ''}>${innerHtml}</${tagName}>${after}`
|
|
620
|
+
cache.set(component.getId(), html)
|
|
621
|
+
return html
|
|
622
|
+
}
|
|
623
|
+
} else {
|
|
624
|
+
// Not a real component
|
|
625
|
+
// FIXME: understand why
|
|
626
|
+
throw new Error('Why no tagName?')
|
|
627
|
+
}
|
|
628
|
+
} else {
|
|
629
|
+
const html = toHtml()
|
|
630
|
+
cache.set(component.getId(), html)
|
|
631
|
+
return html
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function toPath(path: (string | undefined)[]) {
|
|
636
|
+
return '/' + path
|
|
637
|
+
.filter(p => !!p)
|
|
638
|
+
.map(p => p?.replace(/(^\/|\/$)/g, ''))
|
|
639
|
+
.join('/')
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function transformPermalink(editor: Editor, path: string, type: string, options: EleventyPluginOptions): string {
|
|
643
|
+
// Do nothing if there is no data source, just a static site
|
|
644
|
+
if(!enable11ty()) return path
|
|
645
|
+
|
|
646
|
+
switch (type) {
|
|
647
|
+
case 'html':
|
|
648
|
+
return toPath([
|
|
649
|
+
path
|
|
650
|
+
])
|
|
651
|
+
case 'asset':
|
|
652
|
+
return toPath([
|
|
653
|
+
options.urls?.assets,
|
|
654
|
+
path.replace(/^\/?assets\//, ''),
|
|
655
|
+
])
|
|
656
|
+
case 'css': {
|
|
657
|
+
return toPath([
|
|
658
|
+
options.urls?.css,
|
|
659
|
+
path.replace(/^\.?\/?css\//, ''),
|
|
660
|
+
])
|
|
661
|
+
}
|
|
662
|
+
default:
|
|
663
|
+
console.warn('Unknown file type in transform permalink:', type)
|
|
664
|
+
return path
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function transformPath(editor: Editor, path: string, type: string, options: EleventyPluginOptions): string {
|
|
669
|
+
// Do nothing if there is no data source, just a static site
|
|
670
|
+
if(!enable11ty()) return path
|
|
671
|
+
|
|
672
|
+
switch (type) {
|
|
673
|
+
case 'html':
|
|
674
|
+
return toPath([
|
|
675
|
+
options.dir?.input,
|
|
676
|
+
options.dir?.html,
|
|
677
|
+
path,
|
|
678
|
+
])
|
|
679
|
+
case 'css':
|
|
680
|
+
return toPath([
|
|
681
|
+
options.dir?.input,
|
|
682
|
+
options.dir?.css,
|
|
683
|
+
path.replace(/^\/?css\//, ''),
|
|
684
|
+
])
|
|
685
|
+
case 'asset':
|
|
686
|
+
return toPath([
|
|
687
|
+
options.dir?.input,
|
|
688
|
+
options.dir?.assets,
|
|
689
|
+
path.replace(/^\/?assets\//, ''),
|
|
690
|
+
])
|
|
691
|
+
default:
|
|
692
|
+
console.warn('Unknown file type in transform path:', type)
|
|
693
|
+
return path
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
//function transformFile(file: ClientSideFile/*, options: EleventyPluginOptions*/): ClientSideFile {
|
|
698
|
+
// //const fileWithContent = file as ClientSideFileWithContent
|
|
699
|
+
// switch (file.type) {
|
|
700
|
+
// case 'html':
|
|
701
|
+
// case 'css':
|
|
702
|
+
// case 'asset':
|
|
703
|
+
// return file
|
|
704
|
+
// default:
|
|
705
|
+
// console.warn('Unknown file type in transform file:', file.type)
|
|
706
|
+
// return file
|
|
707
|
+
// }
|
|
708
|
+
//}
|