@silexlabs/silex 3.5.0 → 3.5.1-1

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.
Files changed (91) hide show
  1. package/.silex.js +0 -3
  2. package/dist/client/client/config.d.ts +8 -0
  3. package/dist/client/client/grapesjs/cms/DataSource.d.ts +43 -0
  4. package/dist/client/client/grapesjs/cms/blocks.d.ts +2 -0
  5. package/dist/client/client/grapesjs/cms/buttons.d.ts +2 -0
  6. package/dist/client/client/grapesjs/cms/collection-pages.d.ts +4 -0
  7. package/dist/client/client/grapesjs/cms/config.d.ts +9 -0
  8. package/dist/client/client/grapesjs/cms/filters.d.ts +3 -0
  9. package/dist/client/client/grapesjs/cms/index.d.ts +44 -0
  10. package/dist/client/client/grapesjs/cms/liquid.d.ts +59 -0
  11. package/dist/client/client/grapesjs/cms/page-settings.d.ts +6 -0
  12. package/dist/client/client/grapesjs/cms/publication.d.ts +60 -0
  13. package/dist/client/client/grapesjs/cms/site-settings.d.ts +3 -0
  14. package/dist/client/client/grapesjs/cms/states.d.ts +8 -0
  15. package/dist/client/client/grapesjs/cms/traits.d.ts +8 -0
  16. package/dist/client/client/grapesjs/settings.d.ts +12 -3
  17. package/dist/client/client/grapesjs/view-buttons.d.ts +2 -1
  18. package/dist/client/client/utils.d.ts +1 -0
  19. package/dist/client/constants.d.ts +2 -1
  20. package/dist/client/css/admin.css +88 -14
  21. package/dist/client/index.html +2 -2
  22. package/dist/client/js/main.js +1968 -264
  23. package/dist/client/types.d.ts +3 -1
  24. package/dist/plugins/client/plugins/client/template.js +1 -1
  25. package/dist/plugins/client/plugins/client/template.js.map +1 -1
  26. package/dist/plugins/client/types.d.ts +3 -1
  27. package/dist/plugins/client/types.js +11 -8
  28. package/dist/plugins/client/types.js.map +1 -1
  29. package/dist/plugins/server/constants.d.ts +2 -1
  30. package/dist/plugins/server/constants.js +3 -2
  31. package/dist/plugins/server/constants.js.map +1 -1
  32. package/dist/plugins/server/plugins/server/FtpConnector.js +15 -20
  33. package/dist/plugins/server/plugins/server/FtpConnector.js.map +1 -1
  34. package/dist/plugins/server/plugins/server/GitlabConnector.js +34 -7
  35. package/dist/plugins/server/plugins/server/GitlabConnector.js.map +1 -1
  36. package/dist/plugins/server/server/connectors/FsStorage.js +16 -20
  37. package/dist/plugins/server/server/connectors/FsStorage.js.map +1 -1
  38. package/dist/plugins/server/server/utils/websiteDataSerialization.d.ts +4 -0
  39. package/dist/plugins/server/server/utils/websiteDataSerialization.js +21 -12
  40. package/dist/plugins/server/server/utils/websiteDataSerialization.js.map +1 -1
  41. package/dist/plugins/server/types.d.ts +3 -1
  42. package/dist/plugins/server/types.js +12 -9
  43. package/dist/plugins/server/types.js.map +1 -1
  44. package/dist/server/constants.js +3 -2
  45. package/dist/server/server/connectors/FsStorage.js +14 -18
  46. package/dist/server/server/utils/websiteDataSerialization.js +16 -11
  47. package/dist/server/types.js +6 -2
  48. package/package.json +14 -9
  49. package/src/css/_variables.scss +2 -1
  50. package/src/css/grapesjs-plugins.scss +31 -0
  51. package/src/css/grapesjs.scss +15 -0
  52. package/src/css/page-panel.scss +1 -1
  53. package/src/css/silex.scss +64 -5
  54. package/src/ts/client/config.ts +23 -0
  55. package/src/ts/client/grapesjs/breadcrumbs.ts +3 -2
  56. package/src/ts/client/grapesjs/cms/DataSource.ts +333 -0
  57. package/src/ts/client/grapesjs/cms/blocks.ts +124 -0
  58. package/src/ts/client/grapesjs/cms/buttons.ts +137 -0
  59. package/src/ts/client/grapesjs/cms/collection-pages.ts +205 -0
  60. package/src/ts/client/grapesjs/cms/config.ts +47 -0
  61. package/src/ts/client/grapesjs/cms/filters.ts +53 -0
  62. package/src/ts/client/grapesjs/cms/index.ts +130 -0
  63. package/src/ts/client/grapesjs/cms/liquid.mock.ts +181 -0
  64. package/src/ts/client/grapesjs/cms/liquid.test.ts +239 -0
  65. package/src/ts/client/grapesjs/cms/liquid.ts +325 -0
  66. package/src/ts/client/grapesjs/cms/page-settings.ts +285 -0
  67. package/src/ts/client/grapesjs/cms/publication.test.ts +300 -0
  68. package/src/ts/client/grapesjs/cms/publication.ts +708 -0
  69. package/src/ts/client/grapesjs/cms/site-settings.ts +79 -0
  70. package/src/ts/client/grapesjs/cms/states.ts +125 -0
  71. package/src/ts/client/grapesjs/cms/traits.ts +95 -0
  72. package/src/ts/client/grapesjs/flex.ts +59 -39
  73. package/src/ts/client/grapesjs/img.ts +1 -1
  74. package/src/ts/client/grapesjs/index.ts +77 -4
  75. package/src/ts/client/grapesjs/internal-links.ts +4 -2
  76. package/src/ts/client/grapesjs/page-panel.ts +8 -7
  77. package/src/ts/client/grapesjs/sectors.ts +18 -10
  78. package/src/ts/client/grapesjs/settings.ts +252 -101
  79. package/src/ts/client/grapesjs/storage.ts +6 -1
  80. package/src/ts/client/grapesjs/view-buttons.ts +39 -3
  81. package/src/ts/client/utils.ts +15 -2
  82. package/src/ts/constants.ts +2 -1
  83. package/src/ts/plugins/client/template.ts +1 -1
  84. package/src/ts/plugins/server/FtpConnector.test.ts +1 -0
  85. package/src/ts/plugins/server/FtpConnector.ts +20 -23
  86. package/src/ts/plugins/server/GitlabConnector.ts +40 -10
  87. package/src/ts/server/connectors/FsStorage.test.ts +3 -2
  88. package/src/ts/server/connectors/FsStorage.ts +21 -23
  89. package/src/ts/server/utils/websiteDataSerialization.test.ts +45 -13
  90. package/src/ts/server/utils/websiteDataSerialization.ts +23 -14
  91. 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
+ //}