UNPKG

tandem-front-end

Version:

Visual editor for web components

418 lines (355 loc) 9.27 kB
import { EMPTY_ARRAY, KeyValue } from "tandem-common"; import { ComputedStyleInfo } from "paperclip"; // TODO - compute this information based on CSS properties export enum CSSBackgroundType { SOLID, LINEAR_GRADIENT, IMAGE } export enum BackgroundBlendMode { SATURATION = "saturation" } export type CSSBaseBackground<TType extends CSSBackgroundType> = { type: TType; blendMode?: BackgroundBlendMode; }; export type CSSSolidBackground = { color: string; } & CSSBaseBackground<CSSBackgroundType.SOLID>; export type CSSLinearGradientColorStop = { color: string; stop: number; }; export type CSSLinearGradientBackground = { degree: string; stops: CSSLinearGradientColorStop[]; } & CSSBaseBackground<CSSBackgroundType.LINEAR_GRADIENT>; export type CSSImageBackground = { // https://www.w3schools.com/cssref/pr_background-image.asp uri: string; // https://www.w3schools.com/CSSref/pr_background-repeat.asp repeat?: string; // https://www.w3schools.com/csSref/css3_pr_background-size.asp size?: string; position?: string; } & CSSBaseBackground<CSSBackgroundType.IMAGE>; export type CSSBackground = | CSSSolidBackground | CSSLinearGradientBackground | CSSImageBackground; export const computeCSSBackgrounds = ({ style }: ComputedStyleInfo): CSSBackground[] => { const source = style["background-image"]; if (!source) { return EMPTY_ARRAY; } const backgrounds = parseCSSBackroundValue(source); let newBackgrounds = []; for (let i = 0, n = backgrounds.length; i < n; i++) { let newBackground = backgrounds[i]; switch (newBackground.type) { case CSSBackgroundType.IMAGE: { newBackground = { ...newBackground, repeat: getCSSParamValue(style["background-repeat"], i) || "repeat", size: getCSSParamValue(style["background-size"], i) || "auto", position: getCSSParamValue(style["background-position"], i) || "0px" }; break; } } newBackgrounds.push(newBackground); } return newBackgrounds; }; const getCSSParamValue = (value: string, index: number) => { return value && value.split(/\s*,\s*/)[index]; }; export const parseCSSBackroundValue = (value: string) => { const scanner = new Scanner(value); const tokenizer = new Tokenizer(scanner); return getBackgroundExpressions(tokenizer); }; const getBackgroundExpressions = (tokenizer: Tokenizer): CSSBackground[] => { const backgrounds = []; while (!tokenizer.ended()) { tokenizer.eatWhitespace(); backgrounds.push(getBackgroundExpression(tokenizer)); tokenizer.next(); } return backgrounds; }; const getBackgroundExpression = (tokenizer: Tokenizer): CSSBackground => { const keyword = tokenizer.next(); const t = tokenizer.next(); if (t.type === TokenType.L_PAREN) { const params = getParams(tokenizer); if (keyword.value === "linear-gradient") { if (params.length === 2 && params[0] === params[1]) { const solid: CSSSolidBackground = { color: params[0], type: CSSBackgroundType.SOLID }; return solid; } else { let degree: string; if (/deg$/.test(params[0])) { degree = params.shift(); } const stops: CSSLinearGradientColorStop[] = []; for (const param of params) { const [, color, stop] = param.match(/(.*?)([\d\.]+%)$/); stops.push({ color: color.trim(), stop: Number(stop.trim().replace("%", "")) }); } const linearGradient: CSSLinearGradientBackground = { stops, type: CSSBackgroundType.LINEAR_GRADIENT, degree }; return linearGradient; } } else if (keyword.value === "url") { const image: CSSImageBackground = { uri: params.join(","), type: CSSBackgroundType.IMAGE }; return image; } else { throw new Error(`unexpected keyword ${keyword.value}`); } } return null; }; const getParams = (tokenizer: Tokenizer): string[] => { const params: string[] = []; let buffer: string = ""; while (!tokenizer.ended()) { const curr = tokenizer.next(); if (curr.type === TokenType.R_PAREN) { params.push(buffer.trim()); break; } if (curr.type === TokenType.COMMA) { params.push(buffer.trim()); buffer = ""; continue; } let value = curr.value; if (!tokenizer.ended()) { if (curr.type === TokenType.APOS || curr.type === TokenType.QUOTE) { value = getString(tokenizer); } else if (tokenizer.peek().type === TokenType.L_PAREN) { tokenizer.next(); // eat ( value = `${value}(${getParams(tokenizer).join(", ")})`; } } buffer += value; } return params; }; const getString = (tokenizer: Tokenizer): string => { let buffer: string = ""; while (!tokenizer.ended()) { const curr = tokenizer.next(); if (curr.type === TokenType.QUOTE || curr.type === TokenType.APOS) { break; } buffer += curr.value; } return buffer; }; export const stringifyCSSBackground = (background: CSSBackground) => { switch (background.type) { case CSSBackgroundType.IMAGE: { return `url("${background.uri}")`; } case CSSBackgroundType.LINEAR_GRADIENT: { return `linear-gradient()`; } case CSSBackgroundType.SOLID: { return `linear-gradient(${background.color}, ${background.color})`; } } }; export const getCSSBackgroundsStyle = (backgrounds: CSSBackground[]) => { let style: KeyValue<string> = {}; for (const background of backgrounds) { switch (background.type) { case CSSBackgroundType.IMAGE: { style = addStyle( style, "background-image", stringifyCSSBackground(background) ); style = addStyle( style, "background-repeat", background.repeat || "repeat" ); style = addStyle(style, "background-size", background.size || "auto"); style = addStyle( style, "background-position", background.position || "0px" ); break; } case CSSBackgroundType.LINEAR_GRADIENT: { style = addStyle( style, "background-image", stringifyCSSBackground(background) ); break; } case CSSBackgroundType.SOLID: { style = addStyle( style, "background-image", stringifyCSSBackground(background) ); break; } } } return style; }; const addStyle = ( style: KeyValue<string>, key: string, value: string ): KeyValue<string> => { if (!value) { return style; } if (!style[key]) { return { ...style, [key]: value }; } else { return { ...style, [key]: style[key] + ", " + value }; } }; enum TokenType { KEYWORD, L_PAREN, WHITESPACE, R_PAREN, COMMA, NUMBER, CHAR, APOS, QUOTE } type Token = { type: TokenType; value: string; }; class Tokenizer { private _stack: Token[] = []; constructor(private _scanner: Scanner) {} ended() { return this._scanner.ended() && !this._stack.length; } putBack() { this._stack.unshift(this.current()); } next() { if (this._stack.length === 0) { this._stack.push(this._next()); } return this._stack.shift(); } eatWhitespace() { if (!this.ended() && this.peek(1).type === TokenType.WHITESPACE) { this.next(); } } peek(count: number = 1) { let diff = count - this._stack.length; while (diff > 0) { this._stack.push(this._next()); diff--; } return this._stack[0]; } _next(): Token { const c = this._scanner.next(); // whitespace if (/[\s\r\n\t]/.test(c)) { return { type: TokenType.WHITESPACE, value: c + this._scanner.scan(/^[\s\n\r\t]+/) }; } if (c === '"') return { type: TokenType.QUOTE, value: c }; if (c === "'") return { type: TokenType.APOS, value: c }; if (c === "(") return { type: TokenType.L_PAREN, value: c }; if (c === ")") return { type: TokenType.R_PAREN, value: c }; if (/[a-zA-Z_-]/.test(c)) return { type: TokenType.KEYWORD, value: c + this._scanner.scan(/^[a-zA-Z_-]+/) }; if (/[0-9\.]/.test(c)) return { type: TokenType.NUMBER, value: c + this._scanner.scan(/^[0-9\.]+/) }; if (c === ",") return { type: TokenType.COMMA, value: c }; return { type: TokenType.CHAR, value: c }; } current(): Token { return this._stack[0]; } } class Scanner { private _position: number = 0; constructor(private _source: string) {} next() { return this.scan(/./); } ended() { return this._position >= this._source.length; } scan(pattern: RegExp) { const match = this._source.substr(this._position).match(pattern); if (!match) { return ""; } const value = match[0]; this._position += value.length; return value; } }