Skip to content

Commit f440f26

Browse files
committed
feat: inline styles to html
0 parents  commit f440f26

File tree

12 files changed

+350
-0
lines changed

12 files changed

+350
-0
lines changed

.gitignore

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
*.local
12+
13+
# Editor directories and files
14+
.vscode/*
15+
!.vscode/extensions.json
16+
.idea
17+
.DS_Store
18+
*.suo
19+
*.ntvs*
20+
*.njsproj
21+
*.sln
22+
*.sw?

.npmignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
src
2+
tests
3+
.prettierrc
4+
tsconfig.json
5+
bun.lockb

.prettierrc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"semi": false,
3+
"tabWidth": 2,
4+
"useTabs": false,
5+
"singleQuote": true,
6+
"trailingComma": "none",
7+
"quoteProps": "as-needed",
8+
"arrowParens": "always",
9+
"endOfLine": "lf"
10+
}

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2023 RayJason <https://github.com/RayJason>
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Inline HTML Styles
2+
Inline (Tailwind) CSS stylesheets into HTML style attributes.
3+
4+
## Features
5+
- Unit conversion (rem -> px)
6+
- Convert CSS stylesheets variables to static
7+
- Simplify CSS stylesheets calc() expressions
8+
- Inline CSS stylesheets into HTML style attributes
9+
10+
## Applicable
11+
When you use Vue / React and **TailwindCSS** to develop a static page for the following scenarios. You can use this method to inline the CSS stylesheet into HTML style attributes.
12+
- Email
13+
- Wechat Articles
14+
15+
Of course, you should compile to Static Site Generation (SSG). Then pass the HTML and CSS strings into this method.
16+
17+
## Install
18+
```bash
19+
bun add inline-html-styles
20+
# or
21+
pnpm add inline-html-styles
22+
# or
23+
yarn add inline-html-styles
24+
# or
25+
npm install inline-html-styles
26+
```
27+
You can also add -D to install it as a development dependency, depending on your project or usage scenario.
28+
29+
30+
## Usage
31+
```js
32+
import inlineStyles from 'inline-html-styles'
33+
34+
const html = `<div class="flex h-full flex-col items-center justify-center space-y-2">
35+
<h1 class="text-2xl font-medium">Inline Styles</h1>
36+
<p>PostCSS + Juice + TypeScript + TailwindCSS</p>
37+
</div>`
38+
39+
const css = `.flex {
40+
display: flex;
41+
}
42+
.h-full {
43+
height: 100%;
44+
}
45+
.flex-col {
46+
flex-direction: column;
47+
}
48+
.items-center {
49+
align-items: center;
50+
}
51+
.justify-center {
52+
justify-content: center;
53+
}
54+
.space-y-2 > :not([hidden]) ~ :not([hidden]) {
55+
--tw-space-y-reverse: 0;
56+
margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
57+
margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
58+
}
59+
.text-2xl {
60+
font-size: 1.5rem;
61+
line-height: 2rem;
62+
}
63+
.font-medium {
64+
font-weight: 500;
65+
}`
66+
67+
const result = inlineStyles(html, css)
68+
console.log(result)
69+
```
70+
71+
### Result
72+
```html
73+
<div style="display: flex; height: 100%; flex-direction: column; align-items: center; justify-content: center">
74+
<h1 style="font-size: 24px; line-height: 32px; font-weight: 500">Inline Styles</h1>
75+
<p style="margin-top: 8px; margin-bottom: 0px">PostCSS + Juice + TypeScript + TailwindCSS</p>
76+
</div>
77+
```

bun.lockb

47.8 KB
Binary file not shown.

dist/index.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* 将 CSS 处理并内联到 HTML 中
3+
* Process and inline CSS into HTML
4+
* @param {string} html
5+
* @param {string} css
6+
* @param {boolean} remToPx 是否将 rem 转换为 px / Convert CSS units from rem to px?
7+
* @returns {string}
8+
*/
9+
declare const inlineStyles: (html: string, css: string, remToPx?: boolean) => string;
10+
export default inlineStyles;

dist/index.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"name": "inline-html-styles",
3+
"version": "1.0.0",
4+
"description": "Inline (Tailwind) CSS stylesheets into HTML style attributes.",
5+
"author": "RayJason",
6+
"license": "MIT",
7+
"main": "./dist/index.js",
8+
"types": "./dist/index.d.ts",
9+
"exports": {
10+
".": {
11+
"types": "./dist/index.d.ts",
12+
"default": "./dist/index.js"
13+
},
14+
"./package.json": "./package.json"
15+
},
16+
"scripts": {
17+
"build": "tsc && terser dist/index.js -o dist/index.js --compress --mangle",
18+
"test": "vitest"
19+
},
20+
"keywords": [
21+
"inline-style",
22+
"css",
23+
"html",
24+
"email",
25+
"esnext",
26+
"bun",
27+
"juice",
28+
"postcss",
29+
"postcss-calc",
30+
"typescript",
31+
"vitest",
32+
"inline-css"
33+
],
34+
"dependencies": {
35+
"juice": "^9.1.0",
36+
"postcss": "^8.4.30",
37+
"postcss-calc": "^9.0.1"
38+
},
39+
"devDependencies": {
40+
"terser": "^5.20.0",
41+
"typescript": "^5.2.2",
42+
"vitest": "^0.34.5"
43+
}
44+
}

src/index.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import juice from 'juice'
2+
import postcss from 'postcss'
3+
import calc from 'postcss-calc'
4+
5+
// 定义正则表达式
6+
// Define regular expression
7+
const VARIABLE_DEFINITION_REGEX = /(--[\w-]+)\s*:\s*([^;]+);/g
8+
const VARIABLE_USAGE_REGEX = /var\((\s*--[a-zA-Z0-9-_]+\s*)(?:\)|,\s*(.*)\))/g
9+
const REM_UNIT_REGEX = /([\d.]+)rem/g
10+
const CLASS_ATTRIBUTE_REGEX = / class="[^"]*"/g
11+
12+
/**
13+
* 将 CSS 变量转换为静态值
14+
* Convert CSS variables to static values
15+
* @param {string} style
16+
* @returns {string}
17+
*/
18+
const handleCssVariables = (style: string): string => {
19+
const variableDefinitions = new Map<string, string>()
20+
let styleWithoutDefinitions = style.replace(
21+
VARIABLE_DEFINITION_REGEX,
22+
(_, variable, value) => {
23+
variableDefinitions.set(variable.trim(), value.trim())
24+
return ''
25+
}
26+
)
27+
28+
let maxCycles = 1000
29+
while (VARIABLE_USAGE_REGEX.test(styleWithoutDefinitions) && maxCycles > 0) {
30+
maxCycles--
31+
styleWithoutDefinitions = styleWithoutDefinitions.replace(
32+
VARIABLE_USAGE_REGEX,
33+
(_, variable, fallback) => {
34+
const key = variable.trim()
35+
return variableDefinitions.get(key) || fallback?.trim() || ''
36+
}
37+
)
38+
}
39+
40+
if (maxCycles <= 0) {
41+
throw new Error('Max Cycles for replacement exceeded')
42+
}
43+
44+
return styleWithoutDefinitions || ''
45+
}
46+
47+
/**
48+
* 将 CSS 处理并内联到 HTML 中
49+
* Process and inline CSS into HTML
50+
* @param {string} html
51+
* @param {string} css
52+
* @param {boolean} remToPx 是否将 rem 转换为 px / Convert CSS units from rem to px?
53+
* @returns {string}
54+
*/
55+
const inlineStyles = (
56+
html: string,
57+
css: string,
58+
remToPx: boolean = true
59+
): string => {
60+
// 将 CSS 单位 rem 转换为 px
61+
// Convert CSS units from rem to px
62+
const basePx = 16
63+
let cssWithUnitConversion = css
64+
if (remToPx) {
65+
cssWithUnitConversion = css.replace(
66+
REM_UNIT_REGEX,
67+
(_, value) => `${parseFloat(value) * basePx}px`
68+
)
69+
}
70+
71+
// 将 CSS 变量转换为常量
72+
// Convert CSS variables to static
73+
const cssWithoutVariables = handleCssVariables(cssWithUnitConversion)
74+
75+
// 简化 calc() 表达式
76+
// Simplify calc() expressions
77+
const simplifiedCss = postcss().use(calc({})).process(cssWithoutVariables).css
78+
79+
// 将 CSS 内联到 HTML 中
80+
// Inline CSS into HTML
81+
const htmlWithInlinedCss = juice(html, {
82+
extraCss: simplifiedCss
83+
})
84+
85+
// 移除 HTML 中的 class 属性
86+
// remove class attributes from HTML
87+
const htmlWithoutClassAttributes = htmlWithInlinedCss.replace(
88+
CLASS_ATTRIBUTE_REGEX,
89+
''
90+
)
91+
92+
return htmlWithoutClassAttributes
93+
}
94+
95+
export default inlineStyles

0 commit comments

Comments
 (0)