1
1
import { mkdir , writeFile } from "node:fs/promises" ;
2
+ import hash from "@emotion/hash" ;
2
3
import {
3
4
coreMetas ,
4
5
createScope ,
@@ -10,13 +11,16 @@ import {
10
11
} from "@webstudio-is/sdk" ;
11
12
import { generateWebstudioComponent } from "@webstudio-is/react-sdk" ;
12
13
import {
14
+ findByClasses ,
13
15
findByTags ,
14
16
getAttr ,
15
17
getTextContent ,
16
18
loadHtmlIndices ,
19
+ loadSvgSinglePage ,
17
20
parseHtml ,
18
21
} from "./crawler" ;
19
22
import { possibleStandardNames } from "./possible-standard-names" ;
23
+ import { ignoredTags } from "./overrides" ;
20
24
21
25
const validHtmlAttributes = new Set < string > ( ) ;
22
26
@@ -28,14 +32,7 @@ type Attribute = {
28
32
options ?: string [ ] ;
29
33
} ;
30
34
31
- const overrides : Record <
32
- string ,
33
- false | Record < string , false | Partial < Attribute > >
34
- > = {
35
- template : false ,
36
- link : false ,
37
- script : false ,
38
- style : false ,
35
+ const overrides : Record < string , Record < string , false | Partial < Attribute > > > = {
39
36
"*" : {
40
37
// react has own opinions about it
41
38
style : false ,
@@ -215,16 +212,15 @@ for (const row of rows) {
215
212
if ( / c u s t o m e l e m e n t s / i. test ( tag ) ) {
216
213
continue ;
217
214
}
218
- const tagOverride = overrides [ tag ] ;
219
- if ( tagOverride === false ) {
215
+ if ( ignoredTags . includes ( tag ) ) {
220
216
continue ;
221
217
}
222
218
if ( ! attributesByTag [ tag ] ) {
223
219
attributesByTag [ tag ] = [ ] ;
224
220
}
225
221
const attributes = attributesByTag [ tag ] ;
226
222
if ( ! attributes . some ( ( item ) => item . name === attribute ) ) {
227
- const override = tagOverride ?. [ attribute ] ;
223
+ const override = overrides [ tag ] ?. [ attribute ] ;
228
224
if ( override !== false ) {
229
225
attributes . push ( {
230
226
name : attribute ,
@@ -238,28 +234,150 @@ for (const row of rows) {
238
234
}
239
235
}
240
236
237
+ {
238
+ const svg = await loadSvgSinglePage ( ) ;
239
+ const document = parseHtml ( svg ) ;
240
+ const attributeOptions = new Map < string , string [ ] > ( ) ;
241
+ // find all property definition and extract there keywords
242
+ for ( const propdef of findByClasses ( document , "propdef" ) ) {
243
+ let options : undefined | string [ ] ;
244
+ for ( const row of findByTags ( propdef , "tr" ) ) {
245
+ const [ nameNode , valueNode ] = row . childNodes ;
246
+ const name = getTextContent ( nameNode ) ;
247
+ const list = getTextContent ( valueNode )
248
+ . trim ( )
249
+ . split ( / \s + \| \s + / ) ;
250
+ if (
251
+ name . toLowerCase ( ) . includes ( "value" ) &&
252
+ list . every ( ( item ) => item . match ( / ^ [ a - z A - Z - ] + $ / ) )
253
+ ) {
254
+ options = list ;
255
+ }
256
+ }
257
+ for ( const propNameNode of findByClasses ( propdef , "propdef-title" ) ) {
258
+ const propName = getTextContent ( propNameNode ) . slice ( 1 , - 1 ) ;
259
+ if ( options ) {
260
+ attributeOptions . set ( propName , options ) ;
261
+ }
262
+ }
263
+ }
264
+
265
+ for ( const summary of findByClasses ( document , "element-summary" ) ) {
266
+ const [ tag ] = findByClasses ( summary , "element-summary-name" ) . map ( ( item ) =>
267
+ getTextContent ( item ) . slice ( 1 , - 1 )
268
+ ) ;
269
+ // ignore existing
270
+ if ( attributesByTag [ tag ] || ignoredTags . includes ( tag ) ) {
271
+ continue ;
272
+ }
273
+ const attributes = new Set < string > ( ) ;
274
+ const [ dl ] = findByTags ( summary , "dl" ) ;
275
+ for ( let index = 0 ; index < dl . childNodes . length ; index += 1 ) {
276
+ const child = dl . childNodes [ index ] ;
277
+ if ( getTextContent ( child ) . toLowerCase ( ) . includes ( "attributes" ) ) {
278
+ const dd = dl . childNodes [ index + 1 ] ;
279
+ for ( const attrNameNode of findByClasses ( dd , "attr-name" ) ) {
280
+ const attrName = getTextContent ( attrNameNode ) . slice ( 1 , - 1 ) ;
281
+ // skip events
282
+ if ( attrName . startsWith ( "on" ) || attrName === "style" ) {
283
+ continue ;
284
+ }
285
+ validHtmlAttributes . add ( attrName ) ;
286
+ attributes . add ( attrName ) ;
287
+ }
288
+ }
289
+ }
290
+ attributesByTag [ tag ] = Array . from ( attributes )
291
+ . sort ( )
292
+ . map ( ( name ) => {
293
+ let options = attributeOptions . get ( name ) ;
294
+ if ( name === "externalResourcesRequired" ) {
295
+ options = [ "true" , "false" ] ;
296
+ }
297
+ if ( name === "accumulate" ) {
298
+ options = [ "none" , "sum" ] ;
299
+ }
300
+ if ( name === "additive" ) {
301
+ options = [ "replace" , "sum" ] ;
302
+ }
303
+ if ( name === "preserveAlpha" ) {
304
+ options = [ "true" , "false" ] ;
305
+ }
306
+ if ( options ) {
307
+ return { name, description : "" , type : "select" , options } ;
308
+ }
309
+ return { name, description : "" , type : "string" } ;
310
+ } ) ;
311
+ }
312
+ }
313
+
241
314
// sort tags and attributes
242
315
const tags = Object . keys ( attributesByTag ) . sort ( ) ;
316
+ const attributesByHash = new Map < string , Attribute > ( ) ;
317
+ const reusableAttributesByHash = new Map < string , Attribute > ( ) ;
243
318
for ( const tag of tags ) {
244
319
const attributes = attributesByTag [ tag ] ;
245
320
delete attributesByTag [ tag ] ;
246
- attributes . sort ( ) ;
321
+ attributes . sort ( ( left , right ) => left . name . localeCompare ( right . name ) ) ;
247
322
if ( attributes . length > 0 ) {
323
+ for ( const attribute of attributes ) {
324
+ const attributeHash = hash ( JSON . stringify ( attribute ) ) ;
325
+ if ( attributesByHash . has ( attributeHash ) ) {
326
+ reusableAttributesByHash . set ( attributeHash , attribute ) ;
327
+ } else {
328
+ attributesByHash . set ( attributeHash , attribute ) ;
329
+ }
330
+ }
248
331
attributesByTag [ tag ] = attributes ;
249
332
}
250
333
}
251
334
252
- const attributesContent = `type Attribute = {
335
+ let attributesContent = `type Attribute = {
253
336
name: string,
254
337
description: string,
255
338
required?: boolean,
256
339
type: 'string' | 'boolean' | 'number' | 'select' | 'url',
257
340
options?: string[]
258
341
}
259
342
260
- export const attributesByTag: Record<string, undefined | Attribute[]> = ${ JSON . stringify ( attributesByTag , null , 2 ) } ;
261
343
` ;
262
344
345
+ const attributeVariableByHash = new Map < string , string > ( ) ;
346
+ for ( const [ key , attribute ] of reusableAttributesByHash ) {
347
+ const normalizedName = attribute . name
348
+ . replaceAll ( "-" , "_" )
349
+ . replaceAll ( ":" , "_" ) ;
350
+ const variableName = `attribute_${ normalizedName } _${ key } ` ;
351
+ attributeVariableByHash . set ( key , variableName ) ;
352
+ attributesContent += `const ${ variableName } : Attribute = ${ JSON . stringify ( attribute , null , 2 ) } ;\n\n` ;
353
+ }
354
+
355
+ const serializableAttributesByTag : Record <
356
+ string ,
357
+ Array < string | Attribute >
358
+ > = { } ;
359
+ for ( const tag of tags ) {
360
+ const attributes = attributesByTag [ tag ] ;
361
+ serializableAttributesByTag [ tag ] = attributes . map ( ( attribute ) => {
362
+ const key = hash ( JSON . stringify ( attribute ) ) ;
363
+ const variableName = attributeVariableByHash . get ( key ) ;
364
+ if ( variableName ) {
365
+ return variableName ;
366
+ }
367
+ return attribute ;
368
+ } ) ;
369
+ }
370
+
371
+ attributesContent += `
372
+ export const attributesByTag: Record<string, undefined | Attribute[]> = ${ JSON . stringify ( serializableAttributesByTag , null , 2 ) } ;
373
+ ` ;
374
+ for ( const variableName of attributeVariableByHash . values ( ) ) {
375
+ attributesContent = attributesContent . replaceAll (
376
+ `"${ variableName } "` ,
377
+ variableName
378
+ ) ;
379
+ }
380
+
263
381
await mkdir ( "./src/__generated__" , { recursive : true } ) ;
264
382
await writeFile ( "./src/__generated__/attributes.ts" , attributesContent ) ;
265
383
0 commit comments