@keycloakify/angular-email 0.0.8 → 1.0.0

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 (27) hide show
  1. package/README.md +55 -12
  2. package/bin/build.mjs +1 -1
  3. package/fesm2022/keycloakify-angular-email-tailwindcss-preset-email-css-processor.mjs +70 -0
  4. package/fesm2022/keycloakify-angular-email-tailwindcss-preset-email-css-processor.mjs.map +1 -0
  5. package/fesm2022/keycloakify-angular-email.mjs +41 -27
  6. package/fesm2022/keycloakify-angular-email.mjs.map +1 -1
  7. package/index.d.ts +1 -3
  8. package/node/index.cjs +1 -1
  9. package/node/index.d.ts +1 -0
  10. package/node/index.js +1 -1
  11. package/package.json +44 -7
  12. package/tailwindcss-preset-email/clients.css +44 -0
  13. package/tailwindcss-preset-email/css-processor/css-processor.ts +72 -0
  14. package/tailwindcss-preset-email/css-processor/index.d.ts +3 -0
  15. package/tailwindcss-preset-email/css-processor/index.ts +1 -0
  16. package/tailwindcss-preset-email/css-processor/ng-package.json +6 -0
  17. package/tailwindcss-preset-email/index.css +25 -0
  18. package/tailwindcss-preset-email/mso.css +433 -0
  19. package/tailwindcss-preset-email/theme/borders.css +15 -0
  20. package/tailwindcss-preset-email/theme/colors.css +249 -0
  21. package/tailwindcss-preset-email/theme/filters.css +13 -0
  22. package/tailwindcss-preset-email/theme/shadows.css +23 -0
  23. package/tailwindcss-preset-email/theme/spacing.css +21 -0
  24. package/tailwindcss-preset-email/theme/text.css +28 -0
  25. package/fesm2022/keycloakify-angular-email-tailwindcss-preset-email.mjs +0 -1229
  26. package/fesm2022/keycloakify-angular-email-tailwindcss-preset-email.mjs.map +0 -1
  27. package/tailwindcss-preset-email/index.d.ts +0 -191
package/README.md CHANGED
@@ -14,6 +14,11 @@ yarn add @keycloakify/angular-email
14
14
 
15
15
  ## Usage
16
16
 
17
+ | Angular | Tailwind CSS | @keycloakify/angular-email | Maintained |
18
+ | ------- | ------------ | -------------------------- | ------------- |
19
+ | 20 | 4.x+ | 1.x+ | Yes |
20
+ | 20 | 3.x+ | 0.x+ | Bugfixes only |
21
+
17
22
  ### Creating an Email Component
18
23
 
19
24
  see this [example](https://github.com/keycloakify/angular-email/blob/main/projects/showcase/src/app/app.component.ts)
@@ -49,8 +54,11 @@ export const renderToHtml: RenderToHtml<EmailComponentProps> = (props) => {
49
54
  ```sh
50
55
  # cmd
51
56
 
52
- export $EMAIL_COMPONENTS_DIR_PATH="src/emails"
53
- npx keycloakify-angular-email build -p $EMAIL_COMPONENTS_DIR_PATH
57
+ export EMAIL_COMPONENTS_DIR_PATH="src/emails"
58
+ export EMAIL_OUTPUT_DIR_PATH="dist/emails"
59
+ export EMAIL_EXTERNAL_PACKAGES="tailwindcss,@tailwindcss/postcss,postcss,postcss-calc,postcss-custom-properties,postcss-preset-env,postcss-logical"
60
+
61
+ npx keycloakify-angular-email build -p "$EMAIL_COMPONENTS_DIR_PATH" -o "$EMAIL_OUTPUT_DIR_PATH" -e "$EMAIL_EXTERNAL_PACKAGES"
54
62
  ```
55
63
 
56
64
  NB: use `keycloakify-angular-email build` when you don't need to pass dynamic inputs to your components, otherwise see [Standalone Dynamic Rendering](#standalone-dynamic-rendering)
@@ -70,7 +78,6 @@ NB: use `keycloakify-angular-email build` when you don't need to pass dynamic in
70
78
  "files": [],
71
79
  "include": ["*.ts", "**/*.ts"]
72
80
  }
73
-
74
81
  ```
75
82
 
76
83
  ```ts
@@ -117,7 +124,7 @@ export default defineConfig(({ mode }) => ({
117
124
  ...
118
125
  postBuild: async (buildContext) => {
119
126
  await buildEmailTheme({
120
- templatesSrcDirPath: join(import.meta.dirname, '/emails/templates'),
127
+ templatesSrcDirPath: join(import.meta.dirname, 'emails', 'templates'),
121
128
  filterTemplate: (filePath: string) => !!filePath.endsWith('.component.ts'),
122
129
  themeNames: buildContext.themeNames,
123
130
  keycloakifyBuildDirPath: buildContext.keycloakifyBuildDirPath,
@@ -125,10 +132,10 @@ export default defineConfig(({ mode }) => ({
125
132
  cwd: import.meta.dirname,
126
133
  esbuild: {
127
134
  packages: 'bundle',
128
- external: ['juice', 'postcss', 'tailwindcss-v3'],
135
+ external: ['juice', '...other packages you might use to process css'],
129
136
  format: 'esm',
130
137
  outExtension: { '.js': '.mjs' },
131
- plugins: [angularEsbuildPlugin(join(import.meta.dirname, '/emails'))],
138
+ plugins: [angularEsbuildPlugin(join(import.meta.dirname, 'emails'))],
132
139
  },
133
140
  });
134
141
  },
@@ -149,6 +156,7 @@ import { toHTML } from '@keycloakify/angular-email/node';
149
156
  toHTML({
150
157
  filePath: 'path/to/your.component.ts',
151
158
  props: { foo: 'bar' },
159
+ externals: [],
152
160
  })
153
161
  .then((html) => {
154
162
  console.log(html);
@@ -168,6 +176,7 @@ const { toHTML } = require('@keycloakify/angular-email/node');
168
176
  toHTML({
169
177
  filePath: 'path/to/your.component.ts',
170
178
  props: { foo: 'bar' },
179
+ externals: [],
171
180
  })
172
181
  .then((html) => {
173
182
  console.log(html);
@@ -201,8 +210,8 @@ type Render<Input extends Record<string, any>> = {
201
210
  plainText?: boolean;
202
211
  /** format the html output */
203
212
  pretty?: boolean;
204
- /** tailwind v3 configuration object */
205
- tailwindConfig?: Partial<Config>;
213
+ /** Optional hook to manipulate the extracted CSS. Useful for PostCSS processing */
214
+ cssProcessor?: (css: string) => Promise<string>;
206
215
  /** if you use prefix conventions on signal inputs */
207
216
  signalInputsPrefix?: string;
208
217
  };
@@ -232,18 +241,52 @@ toHTML<Input extends Record<string, any>>(options: {
232
241
  filePath: string;
233
242
  props?: Input;
234
243
  root?: string;
244
+ externals?: string[];
235
245
  }) => Promise<string>
236
246
  ```
237
247
 
238
248
  ### @keycloakify/angular-email/tailwindcss-preset-email
239
249
 
240
- Just a tailwind v3 preset, inspired by [@maizzle/tailwindcss-preset-email](https://github.com/maizzle/tailwindcss-preset-email)
250
+ Just a tailwind v4 preset, inspired by [@maizzle/tailwindcss](https://github.com/maizzle/tailwindcss)
241
251
 
242
- **NB**: tailwind v4 is not supported due to high level css generation and poor support in overriding default utilities
252
+ ```css
253
+ /* styles.css */
254
+ @import '@keycloakify/angular-email/tailwindcss-preset-email';
255
+ ```
243
256
 
244
- [add support for disabling core plugins](https://github.com/tailwindlabs/tailwindcss/discussions/16132)
257
+ ```typescript
258
+ // email.component.ts
259
+ ...
260
+ import { Component, ViewEncapsulation } from '@angular/core';
261
+ import { render, RenderToHtml } from '@keycloakify/angular-email';
262
+ // or your custom css processor implementation
263
+ import { cssProcessor } from '@keycloakify/angular-email/tailwindcss-preset-email/css-processor';
245
264
 
246
- [Cannot override tailwind utilities](https://github.com/tailwindlabs/tailwindcss/issues/16856)
265
+
266
+ ...
267
+ @Component({
268
+ ...
269
+ styleUrls: ['styles.css'],
270
+ encapsulation: ViewEncapsulation.None,
271
+ })
272
+ export class EmailComponent {
273
+ ....
274
+ }
275
+
276
+ type EmailComponentProps = {};
277
+
278
+ export const renderToHtml: RenderToHtml<EmailComponentProps> = (props) => {
279
+ return render({
280
+ component: EmailComponent,
281
+ selector: 'app-root',
282
+ props,
283
+ options: {
284
+ pretty: true,
285
+ cssProcessor,
286
+ },
287
+ });
288
+ };
289
+ ```
247
290
 
248
291
  ## Contributing
249
292
 
package/bin/build.mjs CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{angularEsbuildPlugin as h}from"@keycloakify/angular-email/esbuild";import y from"esbuild";import{readdir as b,rm as g,writeFile as w}from"node:fs/promises";import{basename as P,join as c,resolve as p}from"node:path";import{cwd as d,exit as m}from"node:process";import{pathToFileURL as k}from"node:url";import{termost as D}from"termost";import F from"../package.json"with{type:"json"};async function v(t,e,r){const a=await y.build({entryPoints:t,bundle:!0,splitting:!0,outdir:r,absWorkingDir:e,platform:"node",sourcemap:!0,packages:"bundle",external:["juice","postcss","tailwindcss-v3"],format:"esm",outExtension:{".js":".mjs"},target:"node20",plugins:[h(e)],metafile:!0});return Object.entries(a.metafile.outputs).reduce((i,[l,o])=>(o.entryPoint&&(i[p(e,o.entryPoint)]=k(p(e,l)).toString()),i),{})}async function x(t,e){try{const r=async i=>{const l=await b(i,{withFileTypes:!0}),o=[];for(const s of l)s.isFile()?o.push(s):s.isDirectory()&&o.push(...await r(c(s.parentPath,s.name)));return o};return(await r(t)).map(i=>c(i.parentPath,i.name)).filter(i=>e?e(i):!0)}catch(r){throw console.error(`Error scanning directory: ${r.message}`),r}}const j=async(t,e="dist/emails")=>{t||(console.error("emailFilesPath is required!"),m(1));try{console.log(`building emails to ${e}...`);const r=c(d(),e,".tmp");await g(e,{recursive:!0,force:!0});const a=await x(p(d(),t),n=>n.endsWith(".ts"));a.length||(console.error("no templates found!"),m(1));const i=[...a],l=await v(i,d(),r),o=a.map(async n=>{const u=await import(l[n]);return console.log(`- ${n}`),{...u,file:n}}),s=await Promise.all(o);for(const n of s){const u=await n.renderToHtml();await w(`${e}/${P(n.file).split(".")[0]}.html`,u)}await g(r,{recursive:!0,force:!0}),console.log("\x1B[36m%s\x1B[0m","build complete!"),m(0)}catch(r){console.error(r),m(1)}},f=D({name:"keycloakify-angular-email",description:"Keycloakify CLI",version:F.version,onException:t=>{console.error(t),m(1)}});f.option({key:"emailFilesPath",name:{long:"emailFilesPath",short:"p"},description:"",defaultValue:""}),f.option({key:"outDir",name:{long:"outDir",short:"o"},description:"",defaultValue:"dist/emails"}),f.command({name:"build",description:"build angular components to html"}).input({key:"emailFilesPath",type:"text",defaultValue:void 0,label:"what's the path of your email components files?",skip(t){return!!t.emailFilesPath}}).input({key:"outDir",type:"text",defaultValue:"dist/emails",label:"where do you want to output html files?",skip(t){return!!t.outDir}}).task({handler:async t=>{await j(t.emailFilesPath,t.outDir)}});
2
+ import{angularEsbuildPlugin as b}from"@keycloakify/angular-email/esbuild";import k from"esbuild";import{readdir as w,rm as f,writeFile as P}from"node:fs/promises";import{basename as x,join as g,resolve as p}from"node:path";import{cwd as c,exit as m}from"node:process";import{pathToFileURL as F}from"node:url";import{termost as D}from"termost";import v from"../package.json"with{type:"json"};async function E(e,t,i,a=[]){const r=await k.build({entryPoints:e,bundle:!0,splitting:!0,outdir:i,absWorkingDir:t,platform:"node",sourcemap:!0,packages:"bundle",external:["juice",...a],format:"esm",outExtension:{".js":".mjs"},target:"node20",plugins:[b(t)],metafile:!0});return Object.entries(r.metafile.outputs).reduce((l,[s,o])=>(o.entryPoint&&(l[p(t,o.entryPoint)]=F(p(t,s)).toString()),l),{})}async function j(e,t){try{const i=async r=>{const l=await w(r,{withFileTypes:!0}),s=[];for(const o of l)o.isFile()?s.push(o):o.isDirectory()&&s.push(...await i(g(o.parentPath,o.name)));return s};return(await i(e)).map(r=>g(r.parentPath,r.name)).filter(r=>t?t(r):!0)}catch(i){throw console.error(`Error scanning directory: ${i.message}`),i}}const C=async(e,t="dist/emails",i=[])=>{e||(console.error("emailFilesPath is required!"),m(1));try{console.log(`building emails to ${t}...`);const a=g(c(),t,".tmp");await f(t,{recursive:!0,force:!0});const r=await j(p(c(),e),n=>n.endsWith(".ts"));r.length||(console.error("no templates found!"),m(1));const l=[...r],s=await E(l,c(),a,i),o=r.map(async n=>{const u=s[n]??s[p(c(),n)];if(!u)throw new Error(`Failed to locate bundled URL for "${n}". Ensure getTemplates() returns absolute paths and esbuild.metafile entryPoint mapping uses the same base.`);const h=await import(u);return console.log(`- ${n}`),{...h,file:n}}),y=await Promise.all(o);for(const n of y){const u=await n.renderToHtml();await P(`${t}/${x(n.file).split(".")[0]}.html`,u)}await f(a,{recursive:!0,force:!0}),console.log("\x1B[36m%s\x1B[0m","build complete!"),m(0)}catch(a){console.error(a),m(1)}},d=D({name:"keycloakify-angular-email",description:"Keycloakify CLI",version:v.version,onException:e=>{console.error(e),m(1)}});d.option({key:"emailFilesPath",name:{long:"emailFilesPath",short:"p"},description:"input directory for angular email components",defaultValue:""}),d.option({key:"outDir",name:{long:"outDir",short:"o"},description:"output directory for generated html",defaultValue:"dist/emails"}),d.option({key:"externals",name:{long:"externals",short:"e"},description:"Comma-separated packages to mark as external in esbuild (e.g. tailwindcss,postcss,postcss-calc)",defaultValue:""}),d.command({name:"build",description:"build angular components to html"}).input({key:"emailFilesPath",type:"text",defaultValue:void 0,label:"what's the path of your email components files?",skip(e){return!!e.emailFilesPath}}).input({key:"outDir",type:"text",defaultValue:"dist/emails",label:"where do you want to output html files?",skip(e){return!!e.outDir}}).input({key:"externals",type:"text",label:"any additional package to mark as external (comma separated)?",skip(){return!0}}).task({handler:async e=>{let t=[];e.externals&&(t=e.externals.split(",").map(i=>i.trim()).filter(Boolean)),await C(e.emailFilesPath,e.outDir,t)}});
@@ -0,0 +1,70 @@
1
+ import tailwindcss from '@tailwindcss/postcss';
2
+ import postcss from 'postcss';
3
+ import calc from 'postcss-calc';
4
+ import properties from 'postcss-custom-properties';
5
+ import logical from 'postcss-logical';
6
+ import preset from 'postcss-preset-env';
7
+
8
+ const cssProcessor = async (style) => {
9
+ try {
10
+ const result = await postcss(tailwindcss(), properties({ preserve: false }), calc({ preserve: false }), {
11
+ postcssPlugin: 'inline-tw-vars-from-layer',
12
+ Once(root) {
13
+ const defaultVars = new Map();
14
+ // 1. Find the default variables in @layer properties
15
+ root.walkAtRules('layer', (layerRule) => {
16
+ if (layerRule.params === 'properties') {
17
+ layerRule.walkDecls(/^--tw-/, (decl) => {
18
+ defaultVars.set(decl.prop, decl.value);
19
+ });
20
+ }
21
+ });
22
+ // 2. Walk all rules and replace var() usages
23
+ root.walkDecls((decl) => {
24
+ decl.value = decl.value.replace(/var\((--tw-[^,)]+)\)/g, (match, varName) => {
25
+ return defaultVars.has(varName) ? defaultVars.get(varName) : match;
26
+ });
27
+ });
28
+ },
29
+ }, {
30
+ postcssPlugin: 'remove-unused-tw-vars',
31
+ Once(root) {
32
+ root.walkDecls(/^--tw-/, (decl) => {
33
+ decl.remove();
34
+ });
35
+ root.walkAtRules('property', (atRule) => {
36
+ if (atRule.params.startsWith('--tw-')) {
37
+ atRule.remove();
38
+ }
39
+ });
40
+ root.walkAtRules('layer', (atRule) => {
41
+ if (atRule.params === 'properties') {
42
+ atRule.remove();
43
+ }
44
+ });
45
+ },
46
+ }, logical({
47
+ blockDirection: 'top-to-bottom',
48
+ inlineDirection: 'left-to-right',
49
+ }), preset({
50
+ stage: 4,
51
+ minimumVendorImplementations: 3,
52
+ logical: {
53
+ blockDirection: 'top-to-bottom',
54
+ inlineDirection: 'left-to-right',
55
+ },
56
+ })).process(style, { map: false, from: undefined });
57
+ return result.css;
58
+ }
59
+ catch (error) {
60
+ console.error('CSS processing failed:', error);
61
+ throw new Error(`Failed to process CSS: ${error instanceof Error ? error.message : 'Unknown error'}`);
62
+ }
63
+ };
64
+
65
+ /**
66
+ * Generated bundle index. Do not edit.
67
+ */
68
+
69
+ export { cssProcessor };
70
+ //# sourceMappingURL=keycloakify-angular-email-tailwindcss-preset-email-css-processor.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keycloakify-angular-email-tailwindcss-preset-email-css-processor.mjs","sources":["../../../projects/angular-email/tailwindcss-preset-email/css-processor/css-processor.ts","../../../projects/angular-email/tailwindcss-preset-email/css-processor/keycloakify-angular-email-tailwindcss-preset-email-css-processor.ts"],"sourcesContent":["import tailwindcss from '@tailwindcss/postcss';\nimport postcss from 'postcss';\nimport calc from 'postcss-calc';\nimport properties from 'postcss-custom-properties';\nimport logical, { type DirectionFlow } from 'postcss-logical';\nimport preset from 'postcss-preset-env';\n\nexport const cssProcessor = async (style: string) => {\n try {\n const result = await postcss(\n tailwindcss(),\n properties({ preserve: false }),\n calc({ preserve: false }),\n {\n postcssPlugin: 'inline-tw-vars-from-layer',\n Once(root) {\n const defaultVars = new Map<string, string>();\n\n // 1. Find the default variables in @layer properties\n root.walkAtRules('layer', (layerRule) => {\n if (layerRule.params === 'properties') {\n layerRule.walkDecls(/^--tw-/, (decl) => {\n defaultVars.set(decl.prop, decl.value);\n });\n }\n });\n\n // 2. Walk all rules and replace var() usages\n root.walkDecls((decl) => {\n decl.value = decl.value.replace(/var\\((--tw-[^,)]+)\\)/g, (match, varName) => {\n return defaultVars.has(varName) ? defaultVars.get(varName)! : match;\n });\n });\n },\n },\n {\n postcssPlugin: 'remove-unused-tw-vars',\n Once(root) {\n root.walkDecls(/^--tw-/, (decl) => {\n decl.remove();\n });\n root.walkAtRules('property', (atRule) => {\n if (atRule.params.startsWith('--tw-')) {\n atRule.remove();\n }\n });\n root.walkAtRules('layer', (atRule) => {\n if (atRule.params === 'properties') {\n atRule.remove();\n }\n });\n },\n },\n logical({\n blockDirection: 'top-to-bottom' as DirectionFlow,\n inlineDirection: 'left-to-right' as DirectionFlow,\n }),\n preset({\n stage: 4,\n minimumVendorImplementations: 3,\n logical: {\n blockDirection: 'top-to-bottom' as DirectionFlow,\n inlineDirection: 'left-to-right' as DirectionFlow,\n },\n }),\n ).process(style, { map: false, from: undefined });\n return result.css;\n } catch (error) {\n console.error('CSS processing failed:', error);\n throw new Error(`Failed to process CSS: ${error instanceof Error ? error.message : 'Unknown error'}`);\n }\n};\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;;;;MAOa,YAAY,GAAG,OAAO,KAAa,KAAI;AAClD,IAAA,IAAI;QACF,MAAM,MAAM,GAAG,MAAM,OAAO,CAC1B,WAAW,EAAE,EACb,UAAU,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,EAC/B,IAAI,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,EACzB;AACE,YAAA,aAAa,EAAE,2BAA2B;AAC1C,YAAA,IAAI,CAAC,IAAI,EAAA;AACP,gBAAA,MAAM,WAAW,GAAG,IAAI,GAAG,EAAkB;;gBAG7C,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,SAAS,KAAI;AACtC,oBAAA,IAAI,SAAS,CAAC,MAAM,KAAK,YAAY,EAAE;wBACrC,SAAS,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC,IAAI,KAAI;4BACrC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC;AACxC,yBAAC,CAAC;;AAEN,iBAAC,CAAC;;AAGF,gBAAA,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,KAAI;AACtB,oBAAA,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,uBAAuB,EAAE,CAAC,KAAK,EAAE,OAAO,KAAI;AAC1E,wBAAA,OAAO,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,WAAW,CAAC,GAAG,CAAC,OAAO,CAAE,GAAG,KAAK;AACrE,qBAAC,CAAC;AACJ,iBAAC,CAAC;aACH;SACF,EACD;AACE,YAAA,aAAa,EAAE,uBAAuB;AACtC,YAAA,IAAI,CAAC,IAAI,EAAA;gBACP,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC,IAAI,KAAI;oBAChC,IAAI,CAAC,MAAM,EAAE;AACf,iBAAC,CAAC;gBACF,IAAI,CAAC,WAAW,CAAC,UAAU,EAAE,CAAC,MAAM,KAAI;oBACtC,IAAI,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE;wBACrC,MAAM,CAAC,MAAM,EAAE;;AAEnB,iBAAC,CAAC;gBACF,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,MAAM,KAAI;AACnC,oBAAA,IAAI,MAAM,CAAC,MAAM,KAAK,YAAY,EAAE;wBAClC,MAAM,CAAC,MAAM,EAAE;;AAEnB,iBAAC,CAAC;aACH;AACF,SAAA,EACD,OAAO,CAAC;AACN,YAAA,cAAc,EAAE,eAAgC;AAChD,YAAA,eAAe,EAAE,eAAgC;SAClD,CAAC,EACF,MAAM,CAAC;AACL,YAAA,KAAK,EAAE,CAAC;AACR,YAAA,4BAA4B,EAAE,CAAC;AAC/B,YAAA,OAAO,EAAE;AACP,gBAAA,cAAc,EAAE,eAAgC;AAChD,gBAAA,eAAe,EAAE,eAAgC;AAClD,aAAA;AACF,SAAA,CAAC,CACH,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;QACjD,OAAO,MAAM,CAAC,GAAG;;IACjB,OAAO,KAAK,EAAE;AACd,QAAA,OAAO,CAAC,KAAK,CAAC,wBAAwB,EAAE,KAAK,CAAC;AAC9C,QAAA,MAAM,IAAI,KAAK,CAAC,0BAA0B,KAAK,YAAY,KAAK,GAAG,KAAK,CAAC,OAAO,GAAG,eAAe,CAAA,CAAE,CAAC;;AAEzG;;ACvEA;;AAEG;;;;"}
@@ -1,9 +1,9 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { input, computed, ChangeDetectionStrategy, Component, effect, CUSTOM_ELEMENTS_SCHEMA, inject, DOCUMENT, NO_ERRORS_SCHEMA, provideZonelessChangeDetection } from '@angular/core';
3
3
  import { NgTemplateOutlet } from '@angular/common';
4
+ import { bootstrapApplication } from '@angular/platform-browser';
4
5
  import { renderApplication } from '@angular/platform-server';
5
6
  import { provideServerRendering } from '@angular/ssr';
6
- import { bootstrapApplication } from '@angular/platform-browser';
7
7
  import * as cheerio from 'cheerio/slim';
8
8
  import { convert } from 'html-to-text';
9
9
  import juice from 'juice';
@@ -625,6 +625,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImpor
625
625
  args: [{ selector: 'ng-email-text', imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<p\n [style]=\"$textStyle()\"\n [class]=\"$styleClass()\"\n>\n <ng-content></ng-content>\n</p>\n" }]
626
626
  }] });
627
627
 
628
+ /* eslint-disable @typescript-eslint/no-explicit-any */
628
629
  /**
629
630
  * Renders an Angular component to HTML and applies transformations.
630
631
  *
@@ -635,21 +636,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImpor
635
636
  * @returns {Promise<string>} The rendered HTML or plain text.
636
637
  */
637
638
  const render = async ({ component, selector, props, options, }) => {
639
+ const __originalConsoleLog = console.log;
638
640
  console.log = ((log) => (...args) => {
639
641
  if (args[0] !== 'Angular is running in development mode.')
640
642
  log(...args);
641
643
  })(console.log);
642
- const { style, html: normalizedHtml } = await renderNgComponent(component, selector, props, options?.signalInputsPrefix);
643
- const html = applyHtmlTransformations(normalizedHtml);
644
- if (options?.plainText) {
645
- return renderAsPlainText(html);
644
+ try {
645
+ const { style, html: normalizedHtml } = await renderNgComponent(component, selector, props, options?.signalInputsPrefix);
646
+ const html = applyHtmlTransformations(normalizedHtml);
647
+ if (options?.plainText) {
648
+ return renderAsPlainText(html);
649
+ }
650
+ const cssProcessor = options?.cssProcessor;
651
+ const css = await parseStyles(style, cssProcessor);
652
+ const doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
653
+ const markup = await inlineCss(html, css, !!options?.pretty);
654
+ const document = `${doctype}${markup}`;
655
+ return document;
656
+ }
657
+ finally {
658
+ console.log = __originalConsoleLog;
646
659
  }
647
- const tailwindConfig = options?.tailwindConfig;
648
- const css = await parseStyles(html, style, tailwindConfig);
649
- const doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
650
- const markup = await inlineCss(html, css, !!options?.pretty);
651
- const document = `${doctype}${markup}`;
652
- return document;
653
660
  };
654
661
  /**
655
662
  * Converts the given markup string to plain text while skipping certain elements.
@@ -779,25 +786,32 @@ const replacePlaceholders = ($) => {
779
786
  /**
780
787
  * Applies the styles from the specified CSS file paths to the HTML document.
781
788
  *
782
- * @param html - The HTML string to process.
783
789
  * @param style - The raw content of <style> tags
784
- * @param tailwindConfig - Optional Tailwind CSS configuration to use. If not provided, the `tailwindcss-preset-email` preset will be used.
790
+ * @param cssProcessor - Optional hook for manipulate the css extracted.
791
+ * Useful for postcss processing
785
792
  *
786
793
  */
787
- const parseStyles = async (html, style, tailwindConfig) => {
788
- const { tailwindcssPresetEmail } = await import('@keycloakify/angular-email/tailwindcss-preset-email');
789
- const { default: postcss } = await import('postcss');
790
- const { default: tailwindcss } = await import('tailwindcss-v3');
791
- const result = await postcss(tailwindcss({
792
- presets: tailwindConfig ? [tailwindConfig] : [tailwindcssPresetEmail],
793
- content: [
794
- {
795
- raw: html,
796
- extension: 'html',
797
- },
798
- ],
799
- })).process(style, { map: false, from: undefined });
800
- return result.css;
794
+ const parseStyles = async (style, cssProcessor) => {
795
+ const normalized = normalizeCssInput(style);
796
+ if (!cssProcessor)
797
+ return normalized;
798
+ return await cssProcessor(normalized);
799
+ };
800
+ /**
801
+ * Normalize css
802
+ *
803
+ * @param input css to normalize
804
+ * @returns normalized css
805
+ */
806
+ const normalizeCssInput = (input) => {
807
+ return input
808
+ .replace(/\s+/g, ' ')
809
+ .replace(/>\s+</g, '><')
810
+ .replaceAll('&lt;', '<')
811
+ .replaceAll('&gt;', '>')
812
+ .replaceAll('&#x24;', '$')
813
+ .replaceAll('&quot;', '"')
814
+ .replaceAll('&amp;', '&');
801
815
  };
802
816
  /**
803
817
  * Inlines CSS into the provided HTML string using Maizzle and Tailwind CSS.