diff --git a/__tests__/__snapshots__/index.spec.ts.snap b/__tests__/__snapshots__/index.spec.ts.snap index cb9b77c..bb6a14e 100644 --- a/__tests__/__snapshots__/index.spec.ts.snap +++ b/__tests__/__snapshots__/index.spec.ts.snap @@ -1,5 +1,33 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`zstyl renderTemplate should render template 1`] = ` +" + @media (max-width: 600px) { + display: inline-flex; + + &:hover { + color: black; + } + } + + display: flex; + justify-content: center; + color: green; + + &:hover { + color: red; + } + + &:active { + color: blue; + } + + .hoge { + width: 100%; + } + " +`; + exports[`zstyl toStyleString should create zheleznaya styled component 1`] = ` "*[data-zstyl='id'] { display: flex; justify-content: center; color: green; } @media (max-width: 600px) { diff --git a/__tests__/hash.spec.ts b/__tests__/hash.spec.ts new file mode 100644 index 0000000..968ad7c --- /dev/null +++ b/__tests__/hash.spec.ts @@ -0,0 +1,18 @@ +import { hashString } from "../hash"; + +describe("hash", () => { + it("should calc hash", () => { + const text = ` +*[data-zstyl='id'] { display: flex; justify-content: center; color: green; } +@media (max-width: 600px) { +*[data-zstyl='id'] { display: inline-flex; } +*[data-zstyl='id']:hover { color: black; } +} +*[data-zstyl='id']:hover { color: red; } +*[data-zstyl='id']:active { color: blue; } +*[data-zstyl='id'] .hoge { width: 100%; } + `.trim(); + const actual = hashString(text); + expect(actual).toBe("bJIaaS"); + }); +}); diff --git a/__tests__/index.spec.ts b/__tests__/index.spec.ts index 8543c4b..f140de7 100644 --- a/__tests__/index.spec.ts +++ b/__tests__/index.spec.ts @@ -1,9 +1,9 @@ -import { random, styled, toStyleString } from "../index"; +import { random, styled, toStyleString, renderTemplate } from "../index"; describe("zstyl", () => { - describe("toStyleString", () => { - it("should create zheleznaya styled component", () => { - const styleString = toStyleString("id", { color: "green" })` + describe("renderTemplate", () => { + it("should render template", () => { + const renderedText = renderTemplate({ color: "green" })` @media (max-width: 600px) { display: inline-flex; @@ -28,6 +28,37 @@ describe("zstyl", () => { width: 100%; } `; + expect(renderedText).toMatchSnapshot(); + }); + }); + + describe("toStyleString", () => { + it("should create zheleznaya styled component", () => { + const styleString = toStyleString("id", ` + @media (max-width: 600px) { + display: inline-flex; + + &:hover { + color: black; + } + } + + display: flex; + justify-content: center; + color: green; + + &:hover { + color: red; + } + + &:active { + color: blue; + } + + .hoge { + width: 100%; + } + `.trim()); expect(styleString).toMatchSnapshot(); }); }); diff --git a/hash.ts b/hash.ts new file mode 100644 index 0000000..e91079d --- /dev/null +++ b/hash.ts @@ -0,0 +1,32 @@ +const AD_REPLACER_R = /(a)(d)/gi; + +const charsLength = 52; +function getAlphabeticChar(code: number) { + return String.fromCharCode(code + (code > 25 ? 39 : 97)); +} + +function getAlphabeticString(code: number) { + let name = ''; + let x; + + for (x = Math.abs(code); x > charsLength; x = (x / charsLength) | 0) { + name = getAlphabeticChar(x % charsLength) + name; + } + + return (getAlphabeticChar(x % charsLength) + name).replace(AD_REPLACER_R, "$1-$2"); +} + +// djb2 algorithm +function phash(h: number, text: string) { + let i = text.length; + while(i) { + h = (h * 33) ^ text.charCodeAt(--i); + } + return h; +} + +const SEED = 5381; + +export function hashString(text: string) { + return getAlphabeticString(phash(SEED, text)); +} diff --git a/index.ts b/index.ts index 5f61a73..4cd76a5 100644 --- a/index.ts +++ b/index.ts @@ -1,6 +1,7 @@ -import { h, Component, render } from "zheleznaya"; +import { h, Component } from "zheleznaya"; import { AstRenderer } from "./AstRenderer"; import { StyleSheetParser } from "./BNFStyledParser"; +import { hashString } from "./hash"; const Chars = "abcdefghijklmnopqrstuvwxyz0123456789"; export function random(size: number = 5) { @@ -32,44 +33,51 @@ function toClassSelector(className: string) { return `.${className}`; } -export function toStyleString(id: string, props: T) { - return (template: TemplateStringsArray, ...values: Array<((props: T) => (string | number)) | string | number>) => { - const renderedStyle = template.map((it, i) => `${it}${expand(props, values[i])}`).join(""); - const { ast } = StyleSheetParser.parse(renderedStyle); - return AstRenderer.renderStyleSheetWithId(toSelector(id), ast!); - } +export function toStyleString(id: string, renderedStyle: string) { + const { ast } = StyleSheetParser.parse(renderedStyle); + return AstRenderer.renderStyleSheetWithId(toSelector(id), ast!); } function isServerSide(): boolean { return typeof window === "undefined"; } +export function renderTemplate(props: T & Partial, ) { + return (template: TemplateStringsArray, ...values: Array<((props: T & Partial) => (string | number)) | string | number>) => { + return template.map((it, i) => `${it}${expand(props, values[i])}`).join(""); + } +} + +function updateStyleEl() { + styleEl && (styleEl.innerHTML = Object.values(styles).map((v) => (v)).join("\n")); +} + function generateInnerFunction(tag: U) { return function innerFunction(template: TemplateStringsArray, ...values: Array<((props: T & Partial) => (string | number)) | string | number>): Component> { if (!isServerSide() && !styleEl) init(); - const id = random(); return (props, children) => { - styles[id] = toStyleString(id, props)(template, ...values); - styleEl && (styleEl.innerHTML = Object.values(styles).map((v) => (v)).join("\n")); + const styleText = renderTemplate(props)(template, ...values); + const id = hashString(styleText); + styles[id] = toStyleString(id, styleText); + updateStyleEl(); return h(tag, { ...props, "data-zstyl": id }, ...children) as any; } } } -const renderedStyleWithId = new Map(); export function css(template: TemplateStringsArray, ...values: Array<((props: T & Partial) => (string | number)) | string | number>) { if (!isServerSide() && !styleEl) init(); const renderedStyle = template.map((it, i) => `${it}${expand({} as any, values[i])}`).join(""); - const id = renderedStyleWithId.get(renderedStyle) ?? random(); - renderedStyleWithId.set(renderedStyle, id); + const id = hashString(renderedStyle); const className = `c-zstyl-${id}`; const { ast } = StyleSheetParser.parse(renderedStyle); - styles[className] = AstRenderer.renderStyleSheetWithId(toClassSelector(className), ast!); - styleEl && (styleEl.innerHTML = Object.values(styles).map((v) => (v)).join("\n")); + styles[id] = AstRenderer.renderStyleSheetWithId(toClassSelector(className), ast!); + updateStyleEl(); + return className; }