From e7d47e5be139ba041159735e52a728515eb90c03 Mon Sep 17 00:00:00 2001
From: Gaubee <GaubeeBangeel@Gmail.com>
Date: Mon, 21 Apr 2025 17:11:25 +0800
Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=94=A7=20add=20vscode=20recommend=20?=
 =?UTF-8?q?=20config?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .gitignore                      | 3 ++-
 .vscode/extensions.json         | 5 +++++
 .vscode/settings.recommend.json | 8 ++++++++
 3 files changed, 15 insertions(+), 1 deletion(-)
 create mode 100644 .vscode/extensions.json
 create mode 100644 .vscode/settings.recommend.json

diff --git a/.gitignore b/.gitignore
index 246e8b2..c98486e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 node_modules
 scripts/dist
 packages/*/dist
-.turbo
\ No newline at end of file
+.turbo
+.vscode/settings.json
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..a5824a5
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,5 @@
+{
+    "recommendations": [
+        "biomejs.biome"
+    ]
+}
\ No newline at end of file
diff --git a/.vscode/settings.recommend.json b/.vscode/settings.recommend.json
new file mode 100644
index 0000000..d83931f
--- /dev/null
+++ b/.vscode/settings.recommend.json
@@ -0,0 +1,8 @@
+{
+    "[json]": {
+        "editor.defaultFormatter": "biomejs.biome"
+    },
+    "[typescript]": {
+        "editor.defaultFormatter": "biomejs.biome"
+    },
+}
\ No newline at end of file

From 0dbfb4276a155976a6058f9b8d1afd704a759f56 Mon Sep 17 00:00:00 2001
From: Gaubee <GaubeeBangeel@Gmail.com>
Date: Mon, 21 Apr 2025 17:19:13 +0800
Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=A8=20[minify-literals]=20better=20cs?=
 =?UTF-8?q?sText=20support?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

getPlaceholder now supports "strict mode" (return `string[]`), whereas it originally returne `string` ("loose mode").
This better supports CSSminify.
The following Template literals syntax patterns are supported:
```
1. selector
${selector} {
}

2. key
selector {
	 ${key}: value;
}

3. rule
[selector {}]
${rule}
[selector {}]

4. number-literal
selector{
  key: ${param}px;
}

5. value
selector {
	 key: ${value};
	 key: ${value}
}

6. param
selector{
  key: fun(${param}[, ${param}]);
}
```
---
 packages/minify-literals/lib/index.test.ts    |  63 ++++++++-
 packages/minify-literals/lib/index.ts         |  10 +-
 packages/minify-literals/lib/strategy.test.ts |   9 +-
 packages/minify-literals/lib/strategy.ts      | 123 ++++++++++++++++--
 .../package.json                              |   2 +-
 5 files changed, 186 insertions(+), 21 deletions(-)

diff --git a/packages/minify-literals/lib/index.test.ts b/packages/minify-literals/lib/index.test.ts
index 5607a98..6a2b042 100644
--- a/packages/minify-literals/lib/index.test.ts
+++ b/packages/minify-literals/lib/index.test.ts
@@ -12,11 +12,68 @@ import {
 } from "./";
 import { defaultMinifyOptions, defaultStrategy } from "./strategy";
 
+/**
+ * get code snippet from function body
+ */
+const fn_body = (fn: Function, tabSize = 2) => {
+	const fn_str = fn.toString();
+	const body_str = fn_str
+		.slice(fn_str.indexOf("{") + 1, fn_str.lastIndexOf("}"))
+		// remove trailing whitespace
+		.trimEnd()
+		// remove empty lines
+		.replace(/^([\s\t]*$\r?\n)+/gm, "")
+		// use spaces uniformly: current indentStyle is "tab",and tab-width == 2(spaces)
+		.replace(/^\t+/gm, (tabs) => " ".repeat(tabs.length * tabSize));
+	/// remove indent
+	const indent = body_str.match(/\s+/)?.[0];
+	if (!indent) {
+		return body_str.trim();
+	}
+	const indent_reg = new RegExp(`^\\s{${indent.length}}`, "gm");
+	return body_str.replace(indent_reg, "");
+};
 // https://github.com/explodingcamera/esm/issues/1
 describe("handle key value pairs correctly", () => {
-	it("should minify html", async () => {
-		const source = `const css = css\`:host{\${"color"}: \${"red"}}\``;
-		expect((await minifyHTMLLiterals(source))?.code).toMatch('const css = css`:host{${"color"}:${"red"}}`');
+	it("should minify css", async () => {
+		const source = fn_body((css = String.raw) => {
+			const cssText = css`:host{
+				${"color"}: ${"red"};
+			}`;
+		});
+
+		expect((await minifyHTMLLiterals(source))?.code).toMatch(
+			fn_body((css = String.raw) => {
+				const cssText = css`:host{${"color"}:${"red"}}`;
+			}),
+		);
+	});
+
+	it("should minify css with unit", async () => {
+		const source = fn_body((css = String.raw) => {
+			const cssText = css`:host{
+				${"width"}: ${10}px;
+			}`;
+		});
+
+		expect((await minifyHTMLLiterals(source))?.code).toMatch(
+			fn_body((css = String.raw) => {
+				const cssText = css`:host{${"width"}:${10}px}`;
+			}),
+		);
+	});
+	it("should minify css within css-function", async () => {
+		const source = fn_body((css = String.raw) => {
+			const cssText = css`:host{
+				${"width"}: calc(${10}px + ${"var(--data)"});
+			}`;
+		});
+
+		expect((await minifyHTMLLiterals(source))?.code).toMatch(
+			fn_body((css = String.raw) => {
+				const cssText = css`:host{${"width"}:calc(${10}px + ${"var(--data)"})}`;
+			}),
+		);
 	});
 });
 
diff --git a/packages/minify-literals/lib/index.ts b/packages/minify-literals/lib/index.ts
index 93d0699..a09f923 100644
--- a/packages/minify-literals/lib/index.ts
+++ b/packages/minify-literals/lib/index.ts
@@ -210,9 +210,13 @@ export function defaultShouldMinifyCSS(template: Template) {
  */
 export const defaultValidation: Validation = {
 	ensurePlaceholderValid(placeholder) {
-		if (typeof placeholder !== "string" || !placeholder.length) {
-			throw new Error("getPlaceholder() must return a non-empty string");
+		if (typeof placeholder === "string" && placeholder.length > 0) {
+			return;
 		}
+		if (Array.isArray(placeholder) && placeholder.every((ph) => ph.length > 0)) {
+			return;
+		}
+		throw new Error("getPlaceholder() must return a non-empty string | string[]");
 	},
 	ensureHTMLPartsValid(parts, htmlParts) {
 		if (parts.length !== htmlParts.length) {
@@ -291,7 +295,7 @@ export async function minifyHTMLLiterals(source: string, options: Options = {}):
 
 		if (!(minifyHTML || minifyCSS)) return;
 
-		const placeholder = strategy.getPlaceholder(template.parts);
+		const placeholder = strategy.getPlaceholder(template.parts, template.tag);
 		if (validate) {
 			validate.ensurePlaceholderValid(placeholder);
 		}
diff --git a/packages/minify-literals/lib/strategy.test.ts b/packages/minify-literals/lib/strategy.test.ts
index 7ac991d..929f0bf 100644
--- a/packages/minify-literals/lib/strategy.test.ts
+++ b/packages/minify-literals/lib/strategy.test.ts
@@ -26,10 +26,12 @@ describe("strategy", () => {
 			});
 
 			it('should append "_" if placeholder exists in templates', () => {
-				const regularPlaceholder = defaultStrategy.getPlaceholder(parts);
+				const regularPlaceholder = defaultStrategy.getPlaceholder(parts) as string;
+				expect(regularPlaceholder).toBeTypeOf("string");
 				const oneUnderscore = defaultStrategy.getPlaceholder([
 					{ text: regularPlaceholder, start: 0, end: regularPlaceholder.length },
-				]);
+				]) as string;
+				expect(oneUnderscore).toBeTypeOf("string");
 
 				expect(oneUnderscore).not.toEqual(regularPlaceholder);
 				expect(oneUnderscore.includes("_")).toEqual(true);
@@ -53,7 +55,8 @@ describe("strategy", () => {
 			});
 
 			it("should return a value that is preserved by html-minifier when splitting", async () => {
-				const placeholder = defaultStrategy.getPlaceholder(parts);
+				const placeholder = defaultStrategy.getPlaceholder(parts) as string;
+				expect(placeholder).toBeTypeOf("string");
 				const minHtml = await defaultStrategy.minifyHTML(
 					`
           <style>
diff --git a/packages/minify-literals/lib/strategy.ts b/packages/minify-literals/lib/strategy.ts
index dbc9b3a..8e36cf0 100644
--- a/packages/minify-literals/lib/strategy.ts
+++ b/packages/minify-literals/lib/strategy.ts
@@ -1,6 +1,7 @@
 import CleanCSS from "clean-css";
 import { type Options as HTMLOptions, minify } from "html-minifier-terser";
 import type { TemplatePart } from "parse-literals";
+import { randomBytes, randomInt } from "node:crypto";
 
 /**
  * A strategy on how to minify HTML and optionally CSS.
@@ -20,7 +21,7 @@ export interface Strategy<O = any, C = any> {
 	 * @param parts the parts to get a placeholder for
 	 * @returns the placeholder
 	 */
-	getPlaceholder(parts: TemplatePart[]): string;
+	getPlaceholder(parts: TemplatePart[], tag?: string): string | string[];
 	/**
 	 * Combines the parts' HTML text strings together into a single string using
 	 * the provided placeholder. The placeholder indicates where a template
@@ -30,7 +31,7 @@ export interface Strategy<O = any, C = any> {
 	 * @param placeholder the placeholder to use between parts
 	 * @returns the combined parts' text strings
 	 */
-	combineHTMLStrings(parts: TemplatePart[], placeholder: string): string;
+	combineHTMLStrings(parts: TemplatePart[], placeholder: string | string[]): string;
 	/**
 	 * Minfies the provided HTML string.
 	 *
@@ -56,7 +57,7 @@ export interface Strategy<O = any, C = any> {
 	 * @param placeholder the placeholder to split by
 	 * @returns an array of html strings
 	 */
-	splitHTMLByPlaceholder(html: string, placeholder: string): string[];
+	splitHTMLByPlaceholder(html: string, placeholder: string | string[]): string[];
 }
 
 /**
@@ -89,7 +90,87 @@ export const defaultMinifyOptions: HTMLOptions = {
  * <code>clean-css</code> to minify CSS.
  */
 export const defaultStrategy: Strategy<HTMLOptions, CleanCSS.Options> = {
-	getPlaceholder(parts) {
+	getPlaceholder(parts, tag) {
+		const isCss = tag?.toLowerCase().includes("css");
+		if (isCss) {
+			// use strict mode to avoid issues with CSS minification
+			const random = `tmp_${randomBytes(6).toString("hex")}`;
+			const placeholder: string[] = [];
+			const comment = /\/\*[\s\S]*?\*\//g;
+			for (let i = 1; i < parts.length; i++) {
+				const beforeFull = parts[i - 1]!.text;
+				const beforeCss = beforeFull.replace(comment, "");
+				const afterFull = parts[i]!.text;
+				const afterCss = afterFull.replace(comment, "");
+				/**
+				 * 1. selector
+				 * ${selector} {
+				 * }
+				 *
+				 * 2. key
+				 * selector {
+				 * 	 ${key}: value;
+				 * }
+				 *
+				 * 3. rule
+				 * [selector {}]
+				 * ${rule}
+				 * [selector {}]
+				 *
+				 * 4. number-literal
+				 * selector{
+				 *   key: ${param}px;
+				 * }
+				 *
+				 * 5. value
+				 * selector {
+				 * 	 key: ${value};
+				 * 	 key: ${value}
+				 * }
+				 *
+				 * 6. param
+				 * selector{
+				 *   key: fun(${param}[, ${param}]);
+				 * }
+				 */
+
+				const isSelector = /^\s*\{/.test(afterCss);
+				if (isSelector) {
+					placeholder.push(`#${random}`);
+					continue;
+				}
+				const isKey = /^\s*:/.test(afterCss);
+				if (isKey) {
+					placeholder.push(`--${random}`);
+					continue;
+				}
+				const isRule = /\}\s*$/.test(beforeCss) || beforeCss.trim().length === 0;
+				if (isRule) {
+					return `@${random}();`;
+				}
+				const isUnit = /^\w+/.test(afterCss);
+				if (isUnit) {
+					let num: string;
+					while (true) {
+						num = `${randomInt(281474976710655)}`;
+						if (!beforeFull.includes(num)) {
+							break;
+						}
+					}
+					placeholder.push(num);
+					continue;
+				}
+				const isValue = /:\s*$/.test(beforeCss);
+				if (isValue) {
+					placeholder.push(`var(--${random})`);
+					continue;
+				}
+
+				// isParams
+				placeholder.push(`var(--${random})`);
+			}
+			return placeholder;
+		}
 		// Using @ and (); will cause the expression not to be removed in CSS.
 		// However, sometimes the semicolon can be removed (ex: inline styles).
 		// In those cases, we want to make sure that the HTML splitting also
@@ -104,7 +185,10 @@ export const defaultStrategy: Strategy<HTMLOptions, CleanCSS.Options> = {
 	},
 
 	combineHTMLStrings(parts, placeholder) {
-		return parts.map((part) => part.text).join(placeholder);
+		if (typeof placeholder === "string") {
+			return parts.map((part) => part.text).join(placeholder);
+		}
+		return parts.map((part, i) => part.text + (placeholder[i] ?? "")).join("");
 	},
 
 	async minifyHTML(html, options = {}) {
@@ -194,13 +278,30 @@ export const defaultStrategy: Strategy<HTMLOptions, CleanCSS.Options> = {
 		return output.styles;
 	},
 	splitHTMLByPlaceholder(html, placeholder) {
-		const parts = html.split(placeholder);
-		// Make the last character (a semicolon) optional. See above.
-		if (placeholder.endsWith(";")) {
-			const withoutSemicolon = placeholder.substring(0, placeholder.length - 1);
-			for (let i = parts.length - 1; i >= 0; i--) {
-				parts.splice(i, 1, ...(parts[i]?.split(withoutSemicolon) ?? []));
+		let parts: string[];
+		if (typeof placeholder === "string") {
+			parts = html.split(placeholder);
+			// Make the last character (a semicolon) optional. See above.
+			if (placeholder.endsWith(";")) {
+				const withoutSemicolon = placeholder.substring(0, placeholder.length - 1);
+				for (let i = parts.length - 1; i >= 0; i--) {
+					parts.splice(i, 1, ...(parts[i]?.split(withoutSemicolon) ?? []));
+				}
+			}
+		} else {
+			parts = [];
+			// strice mode
+			let pos = 0;
+			let index = -1;
+			for (const ph of placeholder) {
+				index = html.indexOf(ph, pos);
+				if (index === -1) {
+					throw new Error(`placeholder ${ph} not found in html ${html}`);
+				}
+				parts.push(html.slice(pos, index));
+				pos = index + ph.length;
 			}
+			parts.push(html.slice(pos));
 		}
 
 		return parts;
diff --git a/packages/rollup-plugin-minify-template-literals/package.json b/packages/rollup-plugin-minify-template-literals/package.json
index fe26398..689cdef 100644
--- a/packages/rollup-plugin-minify-template-literals/package.json
+++ b/packages/rollup-plugin-minify-template-literals/package.json
@@ -38,7 +38,7 @@
   },
   "dependencies": {
     "@rollup/pluginutils": "^5.1.4",
-    "minify-literals": "^1.0.0"
+    "minify-literals": "workspace:^*"
   },
   "devDependencies": {
     "rollup": "^4.34.9"