UNPKG

@web-atoms/core

Version:
278 lines (230 loc) • 7.75 kB
function *divide (text: string) { const regex = /^(([^\{\n]+\{[\t\x20]*)|([^\n\}]*\}[^\S\n\r]*))$/gm; let m; let sentOnce = false; let lastIndex = 0; let lastMatch: string; while((m = regex.exec(text)) !== null) { // This is necessary to avoid infinite loops with zero-width matches if (m.index === regex.lastIndex) { regex.lastIndex++; } const match = m[0]; if(!sentOnce) { // send first text .. // send current .. sentOnce = true; yield text.substring(lastIndex, m.index); lastIndex = m.index + match.length; lastMatch = match; continue; } if (lastMatch.includes("}")) { yield [lastMatch.trim()]; lastIndex = m.index + match.length; lastMatch = match; continue; } yield [lastMatch, text.substring(lastIndex, m.index), match]; lastIndex = m.index + match.length; lastMatch = match; } if(lastMatch?.includes("}")) { yield [lastMatch.trim()]; } }; let id = 1; const nextId = () => `styled-r${id++}`; let first = document.head.firstElementChild; const markers = { }; const addMarker = (name) => { let m = document.head.querySelector(`meta[name="${name}"]`) as HTMLMetaElement; if (m) { first = m; return markers[name] = m; } m = document.createElement("meta"); m.name = name; if (first) { first.insertAdjacentElement("afterend", m); } else { document.head.insertAdjacentElement("afterbegin", m); } first = m; return markers[name] = m; } addMarker("global-low-marker"); addMarker("global-marker"); addMarker("global-high-marker"); addMarker("local-low-marker"); addMarker("local-marker"); addMarker("local-high-marker"); // export type IStyleFragment = Partial<StyleFragment>; class StyleFragment { static newStyle( { selector = "", content = ""}) { return new StyleFragment( { selector, content }) as Partial<StyleFragment>; } private selector: string; private content: string; private id?: string; private description?: string; private order: string = "low"; constructor({ selector, content }) { this.selector = selector; this.content = content; } expand(selector?) { selector ??= this.selector; let en = divide(this.content); let parts = en.next(); if (parts.done) { if (!this.content) { return ""; } return `${selector} {\n${this.content}\n}`; } const first = parts.value as string; let content = first?.trim() ? `${selector} {\n${first}\n}\n` : ""; let selectorStack = []; while (!(parts = en.next()).done) { const [key, value] = parts.value as string[]; if (key.endsWith("}")) { selector = selectorStack.pop(); if (selector === "@") { content += "\n}\n"; selector = selectorStack.pop(); } continue; } let keySelector = key.replace("{", ""); const isMedia = /\@/.test(keySelector); const replace = !isMedia; const replaced = replace ? selector.split(",").map((x) => keySelector.replace(/\&/g, x).trim()).join(",") : keySelector; // const replaced = keySelector.replace(/\&/g, selector).trim(); // push stack... selectorStack.push(selector); selector = replace ? replaced : selector; if (isMedia) { selectorStack.push("@"); } // only add rule if it is not empty... if (value?.trim()) { content += `${replaced} {\n${value}\n}\n`; } else { if (isMedia) { content += `${replaced} {\n`; } } } return content; } toString() { return this.content.replace(/\\n/g,""); } /** * Installs style globally, without appending it with any class * @param selector global selector if any * @param id id of style element * @param description description if any */ installGlobal(selector: string = "", id: string = this.id || selector, description?: string) { const style = document.createElement("style"); style.textContent = this.expand(selector); if (description) { style.setAttribute("data-desc", description); } switch(this.order) { case "low": document.head.insertBefore(style, markers["global-low-marker"]); break; case "default": case "medium": document.head.insertBefore(style, markers["global-marker"]); break; case "high": document.head.insertBefore(style, markers["global-high-marker"]); break; } style.id = id; } /** * Installs style with an auto generated class name * @param prefix prefix of an element if any * @param description description if any * @returns string */ installLocal(prefix: string = "", description: string = this.description) { const selector = nextId(); const style = document.createElement("style"); const id = `${prefix}.${selector}`; style.id = id; style.textContent = this.expand(id); if (description) { style.setAttribute("data-desc", description); } switch(this.order) { case "low": document.head.insertBefore(style, markers["local-low-marker"]); break; case "default": case "medium": document.head.insertBefore(style, markers["local-marker"]); break; case "high": document.head.insertBefore(style, markers["local-high-marker"]); break; } return selector; } withId(id: string) { this.id = id; return this; } withDescription(description: string) { this.description = description; return this; } /** * Order of installation. * * @param order low | medium | high - default is low * @returns */ withOrder(order: "low" | "medium" | "high") { this.order = order; return this; } } export type IStyleFragments = { [key: string]: StyleFragment; }; export type IStyleFragmentSet = { [key: string]: IStyleFragments; } const styles: IStyleFragmentSet[] = []; const styled = { get styles() { return styles; }, css: (t: TemplateStringsArray, ... a: any[]) => { let r = ""; for (let index = 0; index < t.length; index++) { const element = t[index]; r += element; if (index < a.length) { r += a[index]; } } return StyleFragment.newStyle( { content: r }); }, add(x: IStyleFragmentSet) { styles.push(x); }, }; export default styled; export const svgAsCssDataUrl = (text: string) => `url(${JSON.stringify(`data:image/svg+xml,${text}`)})`;