@@ -3,123 +3,178 @@ const path = require("path");
33const woff2base64 = require ( "woff2base64" ) ;
44const css = require ( "css" ) ;
55
6+ const componentPath = "src/" ;
7+ const bcFont = {
8+ font : {
9+ "BCSans-Regular.woff2" : fs . readFileSync (
10+ "src/scss/fonts/BCSans-Regular.woff2" ,
11+ ) ,
12+ } ,
13+ options : {
14+ fontFamily : "BCSans" ,
15+ style : "normal" ,
16+ } ,
17+ } ;
18+
619async function * walk ( dir ) {
7- for await ( const d of await fs . promises . opendir ( dir ) ) {
20+ const allowedExtensions = [ ".tsx" , ".jsx" , ".ts" , ".js" , ".html" ] ;
21+
22+ const test = await fs . promises . opendir ( dir ) ;
23+ for await ( const d of test ) {
824 const entry = path . join ( dir , d . name ) ;
25+ const fileName = d . name . toLowerCase ( ) ;
926 switch ( true ) {
1027 case d . isDirectory ( ) :
1128 yield * walk ( entry ) ;
1229 break ;
13- case d . isFile ( ) && d . name . toLowerCase ( ) . endsWith ( ".tsx" ) :
30+ case d . isFile ( ) &&
31+ allowedExtensions . some ( ( ext ) => fileName . endsWith ( ext ) ) :
1432 yield entry ;
1533 break ;
1634 }
1735 }
1836}
1937
20- async function extractIconList ( dir ) {
38+ // Refuses instances of material-icons and material-icons-outlined.
39+ async function invalidateUsesOfMaterialIconClass ( srcDir ) {
40+ const errors = [ ] ;
41+ for await ( const p of walk ( srcDir ) ) {
42+ if ( p . indexOf ( "mat-icon" ) >= 0 ) {
43+ continue ;
44+ }
45+
46+ const fileErrors = Array . from (
47+ fs . readFileSync ( p ) . toString ( ) . split ( "\n" ) . entries ( ) ,
48+ )
49+ . filter ( ( [ row , line ] ) => line . indexOf ( "material-icons" ) >= 0 )
50+ . map ( ( [ row , line ] ) => {
51+ return { file : p , lineNumber : row + 1 , line : line } ;
52+ } ) ;
53+ errors . push ( ...fileErrors ) ;
54+ }
55+
56+ return errors ;
57+ }
58+
59+ // Extracts the material icon symbols used by the read along component. Returns
60+ // a sorted list of icon names.
61+ //
62+ // Throws an exception if JSX expressions are used in <MatIcon /> elements.
63+ async function extractIconList ( srcDir ) {
2164 const matIconRe = / < M a t I c o n .* ?> ( ( .| \n ) + ?) < \/ M a t I c o n > / gm;
2265 const iconNames = new Set ( ) ;
23- for await ( const p of walk ( dir ) ) {
66+ for await ( const p of walk ( srcDir ) ) {
2467 const tsx = fs . readFileSync ( p ) . toString ( ) ;
25- for ( const result of tsx . matchAll ( matIconRe ) ) {
26- if ( ! result || result . length < 2 ) {
68+ for ( const icon of tsx . matchAll ( matIconRe ) ) {
69+ if ( ! icon || icon . length < 2 ) {
2770 continue ;
2871 }
2972
30- const iconName = result [ 1 ] . trim ( ) ;
73+ const iconName = icon [ 1 ] . trim ( ) ;
3174 if ( ! iconName . startsWith ( "{" ) && ! iconName . endsWith ( "}" ) ) {
3275 iconNames . add ( iconName ) ;
3376 continue ;
3477 }
3578
3679 throw (
37- "MatIcon does not support JSX expressions, please \n" +
38- "rewrite your expression in the following format: \n\n" +
39- "{cond ? <MatIcon>true</MatIcon> : <MatIcon>false</MatIcon>\n"
80+ "MatIcon does not support JSX expressions, please " +
81+ `rewrite your expression: (file: ${ p } ) \n\n` +
82+ ` ${ icon [ 0 ] } \n\n` +
83+ "using the following format: \n" +
84+ " {cond ? (<MatIcon>true</MatIcon>) : (<MatIcon>false</MatIcon>)\n"
4085 ) ;
4186 }
4287 }
4388
4489 return Array . from ( iconNames . values ( ) ) . sort ( ) ;
4590}
4691
47- async function extractFontsFromGoogle ( ) {
92+ /**
93+ * Uses Google's font v2 API to fetch the CSS font-face declarations.
94+ *
95+ * Returns an array of woff2base64 font definitions.
96+ */
97+ async function fontsFromGoogle ( srcDir ) {
4898 // load the list of used material-icon.
49- const knownIcons = await extractIconList ( "src/components/" ) ;
99+ const knownIcons = await extractIconList ( srcDir ) ;
50100
51101 // fetch and parse CSS definition from Google.
52- const parsedCss = await fetch (
102+ const iconUrl =
53103 "https://fonts.googleapis.com/css2?family=Material+Icons&family=Material+Icons+Outlined&display=swap&icon_names=" +
54- knownIcons . join ( "," ) ,
55- )
56- . then ( ( resp ) => resp . text ( ) )
57- . then ( ( text ) => css . parse ( text ) ) ;
104+ knownIcons . join ( "," ) ;
105+
106+ const parsedCss = await fetch ( iconUrl )
107+ . then ( ( resp ) => {
108+ if ( resp . status === 200 ) {
109+ return resp . text ( ) ;
110+ }
111+ throw `${ resp . statusText } (${ resp . status } ): could not fetch font information from Google` ;
112+ } )
113+ . then ( ( text ) => css . parse ( text ) )
114+ . catch ( ( err ) => console . log ( err ) ) ;
115+ if ( ! parsedCss ) {
116+ return null ;
117+ }
58118
59119 const urlExtract = / u r l \( ( .+ ?) \) / ;
60120
61121 // extract the woff2base64 information from the parsed CSS
62- return Promise . all (
63- parsedCss . stylesheet . rules
64- . filter ( ( r ) => r . type === "font-face" )
65- . map ( ( fontRule ) => {
66- // flatten array of declarations to a single object.
67- return Object . values ( fontRule . declarations )
68- . filter ( ( decl ) => decl . type === "declaration" )
69- . reduce ( ( acc , decl ) => {
70- acc [ decl . property ] = decl . value ;
71- return acc ;
72- } , { } ) ;
73- } )
74- . map ( ( font ) => {
75- // extract url from src field.
76- const fontUrl = urlExtract . exec ( font . src ) ;
77- if ( ! fontUrl ) {
78- throw `error: could not find font URL for ${ font [ "font-family" ] } ` ;
79- }
80- font . src = fontUrl [ 1 ] ;
81- return font ;
82- } )
83- . map ( async ( font ) => {
84- // convert the object to the woff2base62 format by fetching the
85- // font from google.
86- let fontFamily = font [ "font-family" ] ;
87- if ( fontFamily . startsWith ( "'" ) && fontFamily . endsWith ( "'" ) ) {
88- fontFamily = fontFamily . slice ( 1 , fontFamily . length - 1 ) ;
89- }
90-
91- const fontFilename = fontFamily . replaceAll ( " " , "" ) + ".woff2" ;
92- const fontContent = { } ;
93- const resp = await fetch ( font . src ) ;
94- fontContent [ fontFilename ] = Buffer . from ( await resp . arrayBuffer ( ) ) ;
95-
96- return {
97- font : fontContent ,
98- options : {
99- fontFamily : fontFamily ,
100- style : font [ "font-style " ] ?? "normal" ,
101- weight : parseInt ( font [ "font-weight" ] ?? "400" ) ,
102- } ,
103- } ;
104- } ) ,
105- ) ;
122+ const fonts = parsedCss . stylesheet . rules
123+ . filter ( ( r ) => r . type === "font-face" )
124+ . map ( ( fontRule ) => {
125+ // flatten array of declarations to a single object.
126+ return Object . values ( fontRule . declarations )
127+ . filter ( ( decl ) => decl . type === "declaration" )
128+ . reduce ( ( acc , decl ) => {
129+ acc [ decl . property ] = decl . value ;
130+ return acc ;
131+ } , { } ) ;
132+ } )
133+ . map ( ( font ) => {
134+ // extract url from src field.
135+ const fontUrl = urlExtract . exec ( font . src ) ;
136+ if ( ! fontUrl ) {
137+ throw `error: could not find font URL for ${ font [ "font-family" ] } ` ;
138+ }
139+ font . src = fontUrl [ 1 ] ;
140+ return font ;
141+ } )
142+ . map ( async ( font ) => {
143+ // convert the object to the woff2base62 format by fetching the
144+ // font from google.
145+ let fontFamily = font [ "font-family" ] ;
146+ if ( fontFamily . startsWith ( "'" ) && fontFamily . endsWith ( "'" ) ) {
147+ fontFamily = fontFamily . slice ( 1 , fontFamily . length - 1 ) ;
148+ }
149+
150+ const fontFilename = fontFamily . replaceAll ( " " , "" ) + ".woff2" ;
151+ const fontContent = { } ;
152+ const resp = await fetch ( font . src ) ;
153+ fontContent [ fontFilename ] = Buffer . from ( await resp . arrayBuffer ( ) ) ;
154+
155+ return {
156+ font : fontContent ,
157+ options : {
158+ fontFamily : fontFamily ,
159+ style : font [ "font-style" ] ?? "normal" ,
160+ weight : parseInt ( font [ "font-weight " ] ?? "400" ) ,
161+ } ,
162+ } ;
163+ } ) ;
164+
165+ return Promise . all ( fonts ) ;
106166}
107167
108- const bcFont = {
109- font : {
110- "BCSans-Regular.woff2" : fs . readFileSync (
111- "src/scss/fonts/BCSans-Regular.woff2" ,
112- ) ,
113- } ,
114- options : {
115- fontFamily : "BCSans" ,
116- style : "normal" ,
117- } ,
118- } ;
168+ /**
169+ * Generate embeddable font files for use with Studio Web.
170+ */
171+ async function main ( ) {
172+ const fonts = await fontsFromGoogle ( componentPath ) ;
173+ if ( ! fonts ) {
174+ process . exit ( 1 ) ;
175+ }
119176
120- extractFontsFromGoogle ( ) . then ( ( fonts ) => {
121177 fonts . push ( bcFont ) ;
122-
123178 const b64Css = fonts
124179 . map ( ( x ) => woff2base64 ( x . font , x . options ) . woff2 )
125180 . join ( "\n" ) ;
@@ -128,5 +183,46 @@ extractFontsFromGoogle().then((fonts) => {
128183 "../../dist/packages/web-component/dist/fonts.b64.css" ,
129184 b64Css ,
130185 ) ;
186+
131187 fs . writeFileSync ( "../studio-web/src/assets/fonts.b64.css" , b64Css ) ;
132- } ) ;
188+ }
189+
190+ /**
191+ * Validate the use of <MatIcon /> type. We currently don't support
192+ * JSX expression.
193+ *
194+ * Verify there are no additional uses of class="material-icon".
195+ */
196+ async function validate ( ) {
197+ let exitCode = 0 ;
198+
199+ // Verify there are no JSX expression inside of <MatIcon /> elements.
200+ try {
201+ await extractIconList ( componentPath ) ;
202+ } catch ( err ) {
203+ console . log ( err ) ;
204+ exitCode = 1 ;
205+ }
206+
207+ // refuse all instances of material-icons or material-icons-outlined.
208+ const errors = await invalidateUsesOfMaterialIconClass ( componentPath ) ;
209+ if ( errors . length > 0 ) {
210+ exitCode = 1 ;
211+ console . log (
212+ "error: detected usage of material-icons or material-icons-outlined. Please" +
213+ " replace these with the <MatIcon /> component:\n" ,
214+ ) ;
215+ errors . forEach ( ( err ) => {
216+ console . log ( `${ err . file } :${ err . lineNumber } - ${ err . line . trim ( ) } ` ) ;
217+ } ) ;
218+ }
219+
220+ process . exit ( exitCode ) ;
221+ }
222+
223+ const isValidate = process . argv . some ( ( arg ) => arg === "--validate" ) ;
224+ if ( isValidate ) {
225+ validate ( ) ;
226+ } else {
227+ main ( ) ;
228+ }
0 commit comments