@keycloakify/angular-email 0.0.8 → 1.0.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.
- package/README.md +55 -12
- package/bin/build.mjs +1 -1
- package/fesm2022/keycloakify-angular-email-tailwindcss-preset-email-css-processor.mjs +82 -0
- package/fesm2022/keycloakify-angular-email-tailwindcss-preset-email-css-processor.mjs.map +1 -0
- package/fesm2022/keycloakify-angular-email.mjs +44 -31
- package/fesm2022/keycloakify-angular-email.mjs.map +1 -1
- package/index.d.ts +1 -3
- package/node/index.cjs +1 -1
- package/node/index.d.ts +1 -0
- package/node/index.js +1 -1
- package/package.json +44 -7
- package/tailwindcss-preset-email/clients.css +44 -0
- package/tailwindcss-preset-email/css-processor/css-processor.ts +90 -0
- package/tailwindcss-preset-email/css-processor/index.d.ts +3 -0
- package/tailwindcss-preset-email/css-processor/index.ts +1 -0
- package/tailwindcss-preset-email/css-processor/ng-package.json +6 -0
- package/tailwindcss-preset-email/index.css +25 -0
- package/tailwindcss-preset-email/mso.css +433 -0
- package/tailwindcss-preset-email/theme/borders.css +15 -0
- package/tailwindcss-preset-email/theme/colors.css +249 -0
- package/tailwindcss-preset-email/theme/filters.css +13 -0
- package/tailwindcss-preset-email/theme/shadows.css +23 -0
- package/tailwindcss-preset-email/theme/spacing.css +21 -0
- package/tailwindcss-preset-email/theme/text.css +28 -0
- package/fesm2022/keycloakify-angular-email-tailwindcss-preset-email.mjs +0 -1229
- package/fesm2022/keycloakify-angular-email-tailwindcss-preset-email.mjs.map +0 -1
- 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
|
|
53
|
-
|
|
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, '
|
|
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', '
|
|
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, '
|
|
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
|
-
/**
|
|
205
|
-
|
|
213
|
+
/** Optional hook to manipulate the extracted CSS. Useful for PostCSS processing */
|
|
214
|
+
cssProcessor?: (css: string, html: 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
|
|
250
|
+
Just a tailwind v4 preset, inspired by [@maizzle/tailwindcss](https://github.com/maizzle/tailwindcss)
|
|
241
251
|
|
|
242
|
-
|
|
252
|
+
```css
|
|
253
|
+
/* styles.css */
|
|
254
|
+
@import '@keycloakify/angular-email/tailwindcss-preset-email';
|
|
255
|
+
```
|
|
243
256
|
|
|
244
|
-
|
|
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
|
-
|
|
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
|
|
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,82 @@
|
|
|
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, html) => {
|
|
9
|
+
try {
|
|
10
|
+
const inlineSafelist = html
|
|
11
|
+
? Array.from(new Set((html.match(/class=["']([^"']+)["']/g) ?? [])
|
|
12
|
+
.flatMap((m) => m
|
|
13
|
+
.replace(/^class=["']|["']$/g, '')
|
|
14
|
+
.trim()
|
|
15
|
+
.split(/\s+/))
|
|
16
|
+
.filter(Boolean)))
|
|
17
|
+
.slice(0, 5000) // safety cap
|
|
18
|
+
.map((c) => c.replace(/\\/g, '\\\\').replace(/"/g, '\\"'))
|
|
19
|
+
.join(' ')
|
|
20
|
+
: '';
|
|
21
|
+
const styleWithSources = inlineSafelist.length > 0 ? `@source inline("${inlineSafelist}");\n${style}` : style;
|
|
22
|
+
const result = await postcss(tailwindcss({ base: '' }), properties({ preserve: false }), calc({ preserve: false }), {
|
|
23
|
+
postcssPlugin: 'inline-tw-vars-from-layer',
|
|
24
|
+
Once(root) {
|
|
25
|
+
const defaultVars = new Map();
|
|
26
|
+
// 1. Find the default variables in @layer properties
|
|
27
|
+
root.walkAtRules('layer', (layerRule) => {
|
|
28
|
+
if (layerRule.params === 'properties') {
|
|
29
|
+
layerRule.walkDecls(/^--tw-/, (decl) => {
|
|
30
|
+
defaultVars.set(decl.prop, decl.value);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
// 2. Walk all rules and replace var() usages
|
|
35
|
+
root.walkDecls((decl) => {
|
|
36
|
+
decl.value = decl.value.replace(/var\((--tw-[^,)]+)\)/g, (match, varName) => {
|
|
37
|
+
return defaultVars.has(varName) ? defaultVars.get(varName) : match;
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
}, {
|
|
42
|
+
postcssPlugin: 'remove-unused-tw-vars',
|
|
43
|
+
Once(root) {
|
|
44
|
+
root.walkDecls(/^--tw-/, (decl) => {
|
|
45
|
+
decl.remove();
|
|
46
|
+
});
|
|
47
|
+
root.walkAtRules('property', (atRule) => {
|
|
48
|
+
if (atRule.params.startsWith('--tw-')) {
|
|
49
|
+
atRule.remove();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
root.walkAtRules('layer', (atRule) => {
|
|
53
|
+
if (atRule.params === 'properties') {
|
|
54
|
+
atRule.remove();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
}, logical({
|
|
59
|
+
blockDirection: 'top-to-bottom',
|
|
60
|
+
inlineDirection: 'left-to-right',
|
|
61
|
+
}), preset({
|
|
62
|
+
stage: 4,
|
|
63
|
+
minimumVendorImplementations: 3,
|
|
64
|
+
logical: {
|
|
65
|
+
blockDirection: 'top-to-bottom',
|
|
66
|
+
inlineDirection: 'left-to-right',
|
|
67
|
+
},
|
|
68
|
+
})).process(styleWithSources, { map: false, from: undefined });
|
|
69
|
+
return result.css;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
console.error('CSS processing failed:', error);
|
|
73
|
+
throw new Error(`Failed to process CSS: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Generated bundle index. Do not edit.
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
export { cssProcessor };
|
|
82
|
+
//# 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, html: string) => {\n try {\n const inlineSafelist = html\n ? Array.from(\n new Set(\n (html.match(/class=[\"']([^\"']+)[\"']/g) ?? [])\n .flatMap((m) =>\n m\n .replace(/^class=[\"']|[\"']$/g, '')\n .trim()\n .split(/\\s+/),\n )\n .filter(Boolean),\n ),\n )\n .slice(0, 5000) // safety cap\n .map((c) => c.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"'))\n .join(' ')\n : '';\n const styleWithSources = inlineSafelist.length > 0 ? `@source inline(\"${inlineSafelist}\");\\n${style}` : style;\n const result = await postcss(\n tailwindcss({ base: '' }),\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(styleWithSources, { 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":"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"}
|
|
@@ -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
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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, html, 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.
|
|
@@ -732,7 +739,7 @@ const renderNgComponent = async (component, selector, props, signalInputsPrefix)
|
|
|
732
739
|
const normalizeNgHtml = (html, selector) => {
|
|
733
740
|
const nsHtmlRegex = new RegExp(`<(?!(\\/?(${HTMLElements.join('|')})(?![-\\w])))[^>]+>`, 'gm');
|
|
734
741
|
return html
|
|
735
|
-
.replace(new RegExp(`[\\s\\S]*?<${selector}[\\s\\S]*?>([\\s\\S]*?)<\\/${selector}>[\\s\\S]
|
|
742
|
+
.replace(new RegExp(`[\\s\\S]*?<${selector}[\\s\\S]*?>([\\s\\S]*?)<\\/${selector}>[\\s\\S]*?.+`, 'gm'), '$1')
|
|
736
743
|
.replace(nsHtmlRegex, '');
|
|
737
744
|
};
|
|
738
745
|
/**
|
|
@@ -748,9 +755,7 @@ const normalizeNgHtml = (html, selector) => {
|
|
|
748
755
|
* @returns The transformed HTML string.
|
|
749
756
|
*/
|
|
750
757
|
const applyHtmlTransformations = (html) => {
|
|
751
|
-
const $ = cheerio.load(html
|
|
752
|
-
xml: { lowerCaseAttributeNames: false, lowerCaseTags: false },
|
|
753
|
-
});
|
|
758
|
+
const $ = cheerio.load(html);
|
|
754
759
|
replacePlaceholders($);
|
|
755
760
|
return $.html()
|
|
756
761
|
.replace(/\s+/g, ' ')
|
|
@@ -779,25 +784,33 @@ const replacePlaceholders = ($) => {
|
|
|
779
784
|
/**
|
|
780
785
|
* Applies the styles from the specified CSS file paths to the HTML document.
|
|
781
786
|
*
|
|
782
|
-
* @param html - The HTML string to process.
|
|
783
787
|
* @param style - The raw content of <style> tags
|
|
784
|
-
* @param
|
|
788
|
+
* @param cssProcessor - Optional hook for manipulate the css extracted.
|
|
789
|
+
* Useful for postcss processing
|
|
785
790
|
*
|
|
786
791
|
*/
|
|
787
|
-
const parseStyles = async (
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
792
|
+
const parseStyles = async (style, html, cssProcessor) => {
|
|
793
|
+
const normalized = normalizeCssInput(style);
|
|
794
|
+
if (!cssProcessor)
|
|
795
|
+
return normalized;
|
|
796
|
+
const processed = await cssProcessor(style, html);
|
|
797
|
+
return processed;
|
|
798
|
+
};
|
|
799
|
+
/**
|
|
800
|
+
* Normalize css
|
|
801
|
+
*
|
|
802
|
+
* @param input css to normalize
|
|
803
|
+
* @returns normalized css
|
|
804
|
+
*/
|
|
805
|
+
const normalizeCssInput = (input) => {
|
|
806
|
+
return input
|
|
807
|
+
.replace(/\s+/g, ' ')
|
|
808
|
+
.replace(/>\s+</g, '><')
|
|
809
|
+
.replaceAll('<', '<')
|
|
810
|
+
.replaceAll('>', '>')
|
|
811
|
+
.replaceAll('$', '$')
|
|
812
|
+
.replaceAll('"', '"')
|
|
813
|
+
.replaceAll('&', '&');
|
|
801
814
|
};
|
|
802
815
|
/**
|
|
803
816
|
* Inlines CSS into the provided HTML string using Maizzle and Tailwind CSS.
|