import * as inflection from 'inflection' import * as Style from '../style/index.js' import { stringifyLength, normalizeStringLength } from '../style/values/length.js' export type UnformattedLength = Style.Length | number | string export type ProducedRules = any[] | ((context: Context) => ProducedRules) declare type ASTCommand = [type: string, ...args: any[]] declare type ASTBlock = [type: string, value: string | string[], ...children: any[]] declare type AST = any // format markdown-metadata compatible text object with line wrapping, like: // title: Description // id: #ccc function produceMeta(object: Record, { keyLength = 18, valueLength = 80 } = {}) { var array = [] for (var property in object) { var string = '' if (object[property] == null) { continue } const rawValue = object[property] var value = String(rawValue && Array.isArray(rawValue) ? rawValue.join(', ') : rawValue).trim() if (!value) { continue } var valueActualLength = value.length const title = inflection.titleize(inflection.underscore(property)) string += title string += ':' string += Array(keyLength - title.length).join(' ') for (var i = 0; i < valueActualLength; i += valueLength) { if (i != 0) { string += Array(keyLength + 1).join(' ') } var rest = value.substring(i, i + valueLength) var restLength = rest.length // check if word can be broken, and scroll back to word boundary if (restLength == valueLength) { for (var j = 0; j < restLength; j++) { if (rest.charAt(restLength - j - 1).match(/\W/)) { break } } if (j != restLength) { rest = rest.slice(0, restLength - j) i -= j } } string += rest array.push(string) string = '' } if (string) { array.push(string) } } return array } export function produceStyle(style: Style.Rule) { return [['comment', ...produceDetails(style.type, style.details)], produceRule(style)] } export function produceRule(style: Style.Rule, weight?: number[]) { const propsStyle = produceProps(style.type, style.props) if (!propsStyle) { throw new Error('Cant serialize ' + style.type) } var overrideStyle = style.details.override ? parse(style.details.override) : [] if (style.type == 'font' && overrideStyle.length) { return overrideStyle } if (style.type == 'color') { return [ 'rule', ':root', ['property', ({ rules: styles }: Context) => '--' + Style.Rule.getClassName(style, styles), ...propsStyle] ] } return [ 'rule', Style.Rule.getSelector( style, weight || (style.type == 'theme' ? [1, 0, 0] : style.type == 'block' || style.type == 'text' || style.type == 'inline' ? [1, 4, 0] : [1, 6, 0]) ), ...propsStyle, ...overrideStyle ] } function extendSelectors(left: string | string[], right: [source: string, replacement: string][]): string[] { if (left == null || right == null) return null return [] .concat(left) .map((source) => { return [].concat(right).map(([target, replacement]) => { const index = source?.indexOf(target) if (index > -1) { const next = source[index + target.length] if (next == null || next.match(/[\s>~+:\[\]]/)) { const s = source.replace(/:weight\([^)]+\)/g, '') const t = target.replace(/:weight\([^)]+\)/g, '') // split aliasing selector so only the last part replaces the target const last = replacement.match(/[^+>~\s]+$/)[0] const prefix = replacement.substring(0, replacement.length - last.length) return prefix + s.replace(t, last) } } }) }) .flat() .filter(Boolean) } function combineSelectors(left: string | string[], right: string | string[]): string[] { if (left == null || right == null) return null return [] .concat(left) .map((l) => { return [].concat(right).map((r) => { if (r.includes('&')) { return r.replace('&', l) } else { return l + ' ' + r } }) }) .flat() } function produceReusableReferenceConditionally(type: Style.Type, ids: Style.RuleId[]): ProducedRules { return (context: Context) => { if (context.bundle) { const props = resolve('props', type, ids)(context) const overrides = resolve('overrides', type, ids)(context) return [...produceProps(type, props), ...overrides] } else { const style = resolve('style', type, ids)(context) return style ? [['extend', resolve('selector', style.details.id)(context)]] : [] } } } export function produceProps(type: T, props: any) { switch (type) { case 'collection': return [] case 'graphic': return produceGraphicProps(props) case 'color': return produceColorProps(props) case 'decoration': return produceDecorationProps(props) case 'typography': return produceTypographyProps(props) case 'fill': return produceFillProps(props) case 'font': return produceFontProps(props) case 'spacing': return produceSpacingProps(props) case 'palette': return producePaletteProps(props) case 'layout': return produceLayoutProps(props) case 'grid': return produceGridProps(props) case 'dimensions': return produceDimensionsProps(props) case 'breakpoint': return produceBreakpointProps(props) case 'media': return produceMediaProps(props) case 'lines': return produceLinesProps(props) case 'theme': return produceThemeProps(props) case 'block': return [ ['comment', ...produceMeta(props)], produceReusableReferenceConditionally('decoration', props.decorationIds), produceReusableReferenceConditionally('fill', props.fillIds), produceReusableReferenceConditionally('spacing', props.spacingIds) ] case 'inline': return [ ['comment', ...produceMeta(props)], produceReusableReferenceConditionally('decoration', props.decorationIds), produceReusableReferenceConditionally('typography', props.typographyIds), produceReusableReferenceConditionally('palette', props.paletteIds), produceReusableReferenceConditionally('fill', props.fillIds), produceReusableReferenceConditionally('spacing', props.spacingIds) ] case 'text': return [ ['comment', ...produceMeta(props)], produceReusableReferenceConditionally('typography', props.typographyIds), produceReusableReferenceConditionally('palette', props.paletteIds) //produceReusablePropsConditionally('spacing', props.spacingIds), ] } } interface Context { rules?: Style.Rule[] customRules?: Record prelude?: boolean // ensure that the rules are self-contained and dont refer others bundle?: boolean [key: string]: any } const resolvers = { color(context: Context, ref: Style.Color.Reference | Style.Color.Props) { if (ref && 'red' in ref) { return ref } if (!ref) return const colorRef = ref const color = Style.Set.findByTypeAndId(context?.rules || [], 'font', colorRef.id) return ref.alpha == null ? color?.props : { ...color?.props, alpha: ref.alpha } }, fontVariant(context: Context, ref: Style.Font.VariantReference | Style.Font.Variant): Style.Font.Variant { if (ref && 'familyName' in ref) { return ref } if (!ref) return const fontId = (ref).id const font = Style.Set.findByTypeAndId(context?.rules || [], 'font', fontId) if (font) { var variant = font?.props.variants?.find((variant) => variant.name == ref.name) } return { ...variant, familyName: font?.props.familyName } }, graphic(context: Context, ref: Style.Graphic.Reference | URL) { if (typeof ref == 'string') { return ref } return context && null }, breakpoint(context: Context, ref: Style.Breakpoint.Reference) { if (!ref) return return Style.Set.findByTypeAndId(context?.rules || [], 'breakpoint', ref)?.props?.query || ref }, selector(context: Context, ref: Style.RuleId, isSimplified = false): string[] { if (!ref) return const styles: Style.Rule[] = context?.rules const style = styles.find((s) => s.details.id == ref) if (style) { if (isSimplified) { if (Style.Rule.isInCategory(style, 'element')) { return Style.Rule.getGenericSelector(style, Style.Set.getElementDefinitions(styles)) } else { return [] } } else { return [Style.Rule.getSelector(style)] } } return [] }, style(context: Context, type: Style.Type, ids?: Style.RuleId[]): Style.Rule { const custom = context?.customRules && context.customRules[type] if (custom?.props) { return custom } const styles = [].concat(custom || [], context?.rules || []) var filtered = !ids || ids.length == 0 ? styles.filter((s) => s.type == type) : ids.map((id) => styles.find((s) => s.details.id == id)) return Style.Set.getAspectDefault(filtered) }, props(context: Context, type: Style.Type, ids?: Style.RuleId[]): Style.Rule['props'] { const style = resolvers.style(context, type, ids) return style?.props || Style.TypeDefinitions[type].Props() }, overrides(context: Context, type: Style.Type, ids?: Style.RuleId[]): any[] { const style = resolvers.style(context, type, ids) return style?.details.override ? parse(style.details.override) : [] } } function resolve( type: T, ...args: any[] ): (context: Context) => ReturnType<(typeof resolvers)[T]> { // @ts-ignore return (context: Context) => resolvers[type](context, ...args) } const fallbackColor = 'rgba(0, 0, 0, 0)' // typography spacing takes precedence over row gap var marginTop = `calc( var(---supports--flex-gap, 0) * (var(---self--paragraph-spacing, var(---self--row-gap, 0px)) - var(---self--row-gap, 0px)) + (1 - var(---supports--flex-gap, 0)) * var(---self--paragraph-spacing, var(---self--row-gap, 0px)) )` var marginRight = 'calc((1 - var(---supports--flex-gap, 0)) * var(---self--column-gap, 0px))' // allow layout & dimensions feature negotiate max width var maxWidth = 'min(var(---self--max-available-width, 100%), var(---self--max-width, 100%))' export const produceDecorationProps = (props: Style.Decoration.Props) => [ [ 'property', 'box-shadow', props.boxShadowInset ? 'inset' : null, ['length', props.boxShadowOffsetX], ['length', props.boxShadowOffsetY], ['length', props.boxShadowBlurRadius], ['length', props.boxShadowSpreadRadius], ['or', ['color', resolve('color', props.boxShadowColor)], fallbackColor] ], [ 'property', 'border-radius', ['length', props.borderTopLeftRadius], ['length', props.borderTopRightRadius], ['length', props.borderBottomRightRadius], ['length', props.borderBottomLeftRadius] ], [ 'property', 'border-width', ['length', props.borderTopWidth], ['length', props.borderRightWidth], ['length', props.borderBottomWidth], ['length', props.borderLeftWidth] ], ['property', 'border-style', ['keyword', props.borderStyle, 'none']], ['property', 'border-color', ['or', ['color', resolve('color', props.borderColor)], fallbackColor]] ] export const produceTypographyProps = (props: Style.Typography.Props) => [ ['property', 'text-transform', props.textTransform], ['property', 'line-height', 'var(---typography--line-height, inherit)'], ...produceFontVariantProps(props.fontVariant), ...produceFontSetting(props.base), ...Object.keys(props.overrides || {}) .map((key) => { return [ ['media', resolve('breakpoint', key), ...produceFontSetting(props.overrides[key], true)], ['rule', ':modifier(.-emulate--' + key + ')', ...produceFontSetting(props.overrides[key], true)] ] }) .flat(1) ] export const produceMediaProps = (props: Style.Media.Props) => [ // if image has specified height, avoid stretching it // this will not correctly work in horizontal layout though - need to fix [ 'rule', '&[class*="-block--media"]', ['property', 'width', ['length', props.width, 'auto']], props.height?.unit && ['property', 'flex-grow', '0 !important'], props.height?.unit && ['property', 'flex-shrink', '0 !important'] ], [ 'rule', '> picture', ['property', 'position', 'relative'], ['property', 'overflow', 'hidden'], [ 'property', 'height', props.height?.unit == 'ratio' && props.height?.value != null ? null : ['length', props.height, 'auto'] ], [ 'rule', '> img', ['property', 'object-fit', ['keyword', props.objectFit, 'fill']], [ 'property', 'object-position', ['space-separated', ['length', props.objectPositionX], ['length', props.objectPositionY]] ], [ 'if', (props.height != null && props.width != null) || props.height?.unit == 'ratio', ['property', 'position', 'absolute'], ['property', 'top', 0], ['property', 'left', 0], ['property', 'right', 0], ['property', 'bottom', 0], ['property', 'height', '100%'], ['property', 'width', '100%'] ] ], props.height?.unit == 'ratio' && props.height?.value != null && [ 'rule', '&:before', ['property', 'content', '""'], ['property', 'display', 'block'], ['property', 'padding-top', (1 / props.height?.value) * 100 + '%'] ] ] ] export const produceFontVariantProps = (props: Style.Font.Variant | Style.Font.VariantReference) => [ ['fontVariant', resolve('fontVariant', props)] ] export const produceSpacingProps = (props: Style.Spacing.Props) => [ ['property', 'padding-top', ['length', props.paddingTop]], ['property', 'padding-right', ['length', props.paddingRight]], ['property', 'padding-bottom', ['length', props.paddingBottom]], ['property', 'padding-left', ['length', props.paddingLeft]], ['property', '---spacing--row-gap', ['length', props.rowGap]], ['property', '---spacing--column-gap', ['length', props.columnGap]], [ 'rule', '> *', ['property', '---layout--row-gap', ['length', props.rowGap]], ['property', '---layout--column-gap', ['length', props.columnGap]] ] ] export const producePaletteProps = (props: Style.Palette.Props) => [ ['property', 'color', ['or', ['color', resolve('color', props.textColor)], 'inherit']], ['property', '---typography--text-decoration', props.linkDecoration || 'underline'] ] export const produceFontSetting = (props: Style.Typography.Size, dontOverwrite?: boolean) => [ ['property', 'font-size', ['length', props.fontSize, dontOverwrite ? null : 'inherit']], ['property', 'letter-spacing', ['length', props.letterSpacing, dontOverwrite ? null : 'inherit']], [ 'property', '---typography--line-height', ['lengthWithoutPercentage', props.lineHeight, dontOverwrite ? null : 'initial'] ], [ 'property', '---typography--icon-size', ['lengthWithoutPercentage', props.iconSize, dontOverwrite ? null : 'initial'] ], ['property', '---typography--paragraph-spacing', ['length', props.paragraphSpacing, null]] ] export const produceFontProps = (props: Style.Font.Props) => { if (props.platform == 'google') { return [ [ 'import', 'https://fonts.googleapis.com/css?display=swap&family=' + encodeURIComponent(props.familyName) + (props.variants?.length ? ':' + props.variants.map((v) => v.name).join(',') : '') ] ] } return [] } export const produceDimensionsProps = (props: Style.Dimensions.Props) => [ [ 'property', 'min-width', props.minWidth && props.maxWidth ? ['min', props.minWidth, props.maxWidth] : ['length', props.minWidth, null] ], ['property', '---self--max-width', ['length', props.maxWidth, null]], ['property', 'max-width', maxWidth], ['property', 'min-height', ['length', props.minHeight, null]], ['property', 'max-height', ['length', props.maxHeight, null]] ] export const produceBreakpointProps = (props: Style.Breakpoint.Props) => [ ['media', props.query, ['rule', '[class*="-breakpoint--"]:not(&)', ['property', 'display', 'none']]] ] export const produceThemeComboProps = (props: Style.Theme.Combo, elementType: Style.Type.Element) => [ [ 'rule', [ (context: Context) => [ resolvers.selector(context, props.refId, true).map((s) => [ // cascading style e.g. h1, .-card props.isDefault && s, // combo's cascade style: isntead of `.theme-dark .-card` it's `.-use--deadbeef:not(x) .-card` `&.-use--${props.id} ${s}:weight(0.-1.2)` ]), // combo explicit style .-card-cool.--deadbeef `.--${props.id}:weight(0.4.0)` ] .flat(Infinity) .filter(Boolean) ], produceReusableReferenceConditionally(elementType, [props.refId]), produceReusableReferenceConditionally('decoration', [props.decorationId]), produceReusableReferenceConditionally('fill', [props.fillId]), produceReusableReferenceConditionally('palette', [props.paletteId]), produceReusableReferenceConditionally('typography', [props.typographyId]), produceReusableReferenceConditionally('spacing', [props.spacingId]) ] ] export const produceThemeProps = (props: Style.Theme.Props) => { return [ [ 'rule', [ //'&.-subtheme:weight(0.0.1)', '&' ], ...[ props.blocks.map((c) => produceThemeComboProps(c, 'block')).filter(Boolean), props.texts.map((c) => produceThemeComboProps(c, 'text')).filter(Boolean), props.inlines.map((c) => produceThemeComboProps(c, 'inline')).filter(Boolean) ].flat(2) ] ] } export const produceLinesProps = (props: Style.Lines.Props) => [ ['property', 'text-align', props.textAlign], [ 'if', props.lines != null && props.lines != 1, ['property', '---self--display', '-webkit-box'], ['property', 'display', 'var(---self--display)'], ['property', '-webkit-line-clamp', props.lines], ['property', '-webkit-box-orient', 'vertical'], ['property', 'overflow', 'hidden'] ], [ 'if', props.lines == 1, ['property', 'white-space', 'nowrap'], (props.ellipsis && ['property', 'text-overflow', 'ellipsis']) || null, (props.ellipsis && ['property', 'overflow', 'hidden']) || null ], ['property', 'word-break', props.wordBreak == 'hyphens' ? 'normal' : props.wordBreak], ['property', 'overflow-wrap', props.wordBreak == 'hyphens' ? 'normal' : props.wordBreak], [ 'if', props.wordBreak == 'hyphens', ['property', '-webkit-hyphens', 'auto'], ['property', '-moz-hyphens', 'auto'], ['property', 'hyphens', 'auto'] ] ] export const produceGridProps = (props: Style.Grid.Props) => [ ['property', '---self--display', 'grid'], ['property', 'display', 'var(---self--display)'], [ 'property', 'grid-template-rows', props.rows .map((r) => commands.minmax(r)) .flat() .join(' ') ], [ 'property', 'grid-template-columns', props.columns .map((r) => commands.minmax(r)) .flat() .join(' ') ], ['property', 'row-gap', 'var(---spacing--row-gap)'], ['property', 'column-gap', 'var(---spacing--column-gap)'], ...props.items.map((item, index) => [ 'rule', `& > :nth-child(${index + 1})`, ['property', 'grid-column', item.from.column], ['property', 'grid-column-end', item.to.column], ['property', 'grid-row', item.from.row], ['property', 'grid-row-end', item.to.row], ['property', 'order', item.order], ['property', 'position', 'relative'] ]) ] export const produceLayoutProps = (props: Style.Layout.Props) => [ ['property', '---self--display', 'flex'], ['property', 'display', 'var(---self--display)'], ['property', 'flex-wrap', (props.columnCount == null || props.columnCount > 1) && props.flexWrap ? 'wrap' : 'nowrap'], ['property', 'flex-direction', props.columnCount == null || props.columnCount > 1 ? 'row' : 'column'], // using stretch + flex-wrap + gaps wraps columns prematurely. // use space-between value between to emulate [ 'property', 'justify-content', props.columnCount > 1 && (!props.justifyContent || props.justifyContent == Style.Layout.JustifyContent.stretch) ? 'space-between' : props.justifyContent || 'stretch' ], ['property', 'align-items', props.alignItems || 'flex-start'], ['property', 'row-gap', 'calc(var(---supports--flex-gap, 0) * var(---spacing--row-gap))'], ['property', 'column-gap', 'calc(var(---supports--flex-gap, 0) * var(---spacing--column-gap))'], /* (props.columnCount == null || props.columnCount > 1) && ['property', 'overflow-x', `auto`], */ [ 'rule', '> *', ['property', '---self--vertical-layout', props.columnCount == 1 ? 1 : 0], props.columnCount > 1 && props.justifyContent != Style.Layout.JustifyContent.stretch && [ 'property', '---self--max-available-width', 'var(---self--column-width)' ], props.columnCount > 1 && [ 'property', '---layout--available-width', `calc(100% - var(---layout--column-gap, 0px) * ${props.columnCount - 1})` ], (props.columnCount == null || props.columnCount > 1) && ['property', '---self--paragraph-spacing', 'initial'], ['property', 'margin-top', props.columnCount != null ? marginTop : `0`], (props.columnCount == null || props.columnCount > 1) && ['property', 'margin-right', marginRight], props.columnCount > 1 && ['property', 'flex-basis', 'var(---self--column-width)'], props.columnCount == null && props.justifyContent == Style.Layout.JustifyContent.stretch && ['property', 'flex-basis', '1em'], ['property', 'flex-grow', props.justifyContent == Style.Layout.JustifyContent.stretch ? 1 : 0], ['property', '---self--row-gap', 'initial'], [ 'property', '---self--column-gap', props.columnCount == null || !props.flexWrap ? 'var(---layout--column-gap, 0px)' : 'initial' ] ], props.columnCount != null && (props.flexWrap || props.columnCount == 1) && [ 'rule', `> :not(:nth-child(-n+${props.columnCount || 1}))`, ['property', '---self--row-gap', 'var(---layout--row-gap, 0px)'] ], props.columnCount > 1 && props.flexWrap && [ 'rule', `> :not(:nth-child(${props.columnCount || 1}n))`, ['property', '---self--column-gap', 'var(---layout--column-gap, 0px)'] ], ['property', 'max-width', maxWidth], ['rule', `> :last-child`, ['property', '---self--column-gap', 'initial']], ...((props.columnCount != null && props.weights) || []).map((weight, index) => { const sum = props.weights.reduce((s, v) => s + v, 0) return [ 'rule', `> :nth-child(${props.columnCount}n + ${index + 1})`, ['property', '---self--column-width', `calc(var(---layout--available-width, 100%) / ${sum} * ${weight})`] ] }) ] // first background is pattern // second is gradient // third is color export const produceFillProps = (props: Style.Fill.Props) => { return [ [ 'property', 'background', [ 'or', [ 'comma-separated', [ [ 'if', resolve('graphic', props.backgroundImage), ['image', resolve('graphic', props.backgroundImage)], props.backgroundRepeat, ['length', props.backgroundPositionX], ['length', props.backgroundPositionY], ['if', props.backgroundSize, '/', ['keyword', props.backgroundSize, 'auto']] ] ], [ [ 'if', resolve('color', props.backgroundGradient?.stops[0]?.color), [ 'gradient', props.backgroundGradient?.angle, ...(props.backgroundGradient?.stops || []).map((stop) => [ ['color', resolve('color', stop.color)], ['length', stop.start, null], ['length', stop.end, null] ]) ] ] ], [['if', resolve('color', props.backgroundColor), ['color', resolve('color', props.backgroundColor)]]] ], 'none' ] ], [ 'property', 'backdrop-filter', ['non-empty', ['space-separated', props.blur?.value > 0 ? `blur(${stringifyLength(props.blur)})` : null], 'none'] ], ['property', 'opacity', props.opacity] ] } export function produceDetails(type: string, { id, title, description, exampleContent, instanceId }: Style.Details) { return produceMeta({ type, id, title, description, exampleContent, instanceId }) } export function produceColorProps(color: Style.Color.Props) { return [['color', resolve('color', color)]] } export function produceGraphicProps(props: Style.Graphic.Props) { return ['property', `---graphic--1`, ['graphic']] } const joinTokens = (children: string[] | string) => { if (!Array.isArray(children)) return String(children) const filtered = children.flat().filter((v) => v != null) if (!filtered.length) return null return filtered.join(' ') } export function getSelectorWeight(ids: number, classes: number, tags: number) { if (ids > 0 || classes > 0 || tags > 0) { const _ids = Array(ids).fill('#_') const _classes = Array(Math.max(0, classes)).fill('._') const _tags = Array(tags).fill('x') const main = _tags.slice(0, 1).concat(_classes, _ids).join('') const extras = _tags.slice(1).fill(':not(x)').join('') return `${extras}:not(${main})` } return '' } export function getSelectorWithWeight(selector: string) { var I = 0 var C = 0 var T = 0 var cleaned = selector.replace(/:weight\(\s*(-?\d+)\s*\.\s*(-?\d+)\s*\.\s*(-?\d+)\s*\)/g, (_, a, b, c) => { I += parseInt(a) || 0 C += parseInt(b) || 0 T += parseInt(c) || 0 return '' }) // handle negative class weight for (var i = C; i < 0; i++) { cleaned = cleaned.replace(/\.[^ #:[.]+/, '') } // rearrange pseudo var pseudos = '' cleaned = cleaned.replace( /(:+(before|after|marker|selection|backdrop|placeholder)|:+(first|last)-(letter|line))$/g, (m) => { pseudos += m return '' } ) return cleaned.trim().replace(/\s{2,}/g, ' ') + getSelectorWeight(I, C, T) + pseudos } export const commands = { import: (src: string) => { return [`@import url("${src}");`] }, length: (length: UnformattedLength, defaultValue = '0px') => { return [stringifyLength(length, defaultValue)] }, or: (...args: any[]) => { return args.find((arg) => arg != null) }, 'non-empty': (...args: any[]) => { return args.find((arg) => arg != null && arg != '') }, keyword: (value: any, defaultValue: any) => { if (value == null) { return defaultValue } return [value] }, comment: (...children: any[]) => { if (!children.length) return [] return ['/*', children, '*/'] }, 'comment-single-line': (...children: any[]) => { if (!children.length) return [] return ['//' + children.toString()] }, breakpoint: (name: any[]) => { return name }, property: (name: string, ...values: any[]) => { const value = joinTokens(values) if (!value) return [] return [`${name}: ${value};`] }, fontVariant: (variant: Style.Font.Variant) => { return [ `font-family: ${variant?.familyName ? JSON.stringify(variant.familyName) : 'inherit'};`, `font-weight: ${variant?.weight || 'inherit'};`, `font-style: ${variant?.style || 'inherit'};` ] }, minmax: (value: Style.Grid.CellSize) => { if (value.min == null && value.max == null) { return 'auto' } if (value.min == null) { return `minmax(auto, ${stringifyLength(value.max)})` } if (value.max == null) { return `minmax(${stringifyLength(value.min)}, auto)` } return `minmax(${stringifyLength(value.min)},${stringifyLength(value.max)})` }, min: (a: Style.Length, b: Style.Length) => { return `min(${stringifyLength(a)},${stringifyLength(b)})` }, lengthWithoutPercentage: (length: Style.Length, defaultValue = '0px') => { if (length?.unit == '%') { return commands.length({ value: length.value / 100, unit: 'em' }, defaultValue) } else { return commands.length(length, defaultValue) } }, max: (a: Style.Length, b: Style.Length) => { return `max(${stringifyLength(a)},${stringifyLength(b)})` }, color: (color: Style.Color.Props) => { if (color == null) return [] const { red = 0, green = 0, blue = 0, alpha = 0 } = color return [`rgba(${red}, ${green}, ${blue}, ${alpha})`] }, 'font-face': (...children: any[]) => { return ['@font-face {', children, '}'] }, rule: (selector: string, ...children: any[]) => { if (!selector) return children //if (!children.length) return [] const selectors = [].concat(selector).flat().filter(Boolean) as string[] if (!selectors.length) return // `div + :wrapper(#a) h1` => `#a div + h1` const selectorString = selectors .map((s) => { var wrapper var cleaned = s.replace(/:(wrapper|modifier)\(([^\)]+)\)\s*/g, (m: string, t: string, w: string) => { wrapper = w + (t == 'wrapper' || !s.startsWith('.-theme') ? ' ' : '') return '' }) if (wrapper) { return wrapper + cleaned } else { return s } }) .map(getSelectorWithWeight) .join(',\n') return [`${selectorString} {`, children, `}`] }, media: (query: string, ...children: any[]) => { if (!children.length) return [] const conditions = [].concat(query).filter(Boolean) if (!conditions.length) return children if (!children.length) return [] return [`@media ${conditions.map((c) => `${c}`).join(',\n')} {`, children, `}`] }, extend: (...selectors: string[]) => { return [`@extend ${selectors.join(', ') + ';'}`] }, if: (condition: any, ...children: any[]) => { if (condition) { return children } return [] }, unless: (condition: any, ...children: any[]) => { if (!condition) { return children } return [] }, 'comma-separated': (...children: any[]) => { const values = children.filter((child) => child && Array.isArray(child) && child.length == 0 ? false : child != null ) if (values.length > 0) { return values.map(joinTokens).join(', ') } }, 'space-separated': (...children: any[]) => { const values = children.filter((child) => child && Array.isArray(child) && child.length == 0 ? false : child != null ) if (values.length > 0) { return values.map(joinTokens).join(' ') } }, gradient: (angle: Style.Gradient['angle'], ...stops: string[]) => { if (angle != null) { return [`linear-gradient(${angle}deg, ${stops.map((stop) => joinTokens(stop)).join(', ')})`] } }, image: (url: string) => { if (url) { return [`url("${encodeURI(url.replace(/"/g, '"'))}")`] } }, string: (value: string) => { return `"${value}"` } } export function isBlock(ast: any): ast is ASTBlock { return ast && (ast[0] == 'rule' || ast[0] == 'media' || ast[0] == 'extend') } // Create flat list of rules from nested rules by combining selectors and media queries export function unnest(ast: any, isInside?: boolean, flattenRules = true): any[] { if (ast && Array.isArray(ast)) { if (isBlock(ast)) return unnestBlock(ast, flattenRules) if (isInside) { return [ast] } return ast.map((c) => unnest(c, true, flattenRules)).flat() } else { return [ast] } } export function unnestBlock(ast: ASTBlock, flattenRules = true) { const [type, value, ...children] = ast const regularChildren = children.filter((child) => !isBlock(child)) const blockChildren: ASTBlock[] = children .filter((child: any) => isBlock(child)) .map((b) => unnest(b)) .flat() if (blockChildren.length && flattenRules) { // skip single rules inside media query if ( !( type == 'media' && children.length == 1 && blockChildren.length == 1 && blockChildren[0][0] == 'rule' && !blockChildren[0].slice(2).some((gc) => isBlock(gc)) ) && !(type == 'rule' && children.length == 1 && blockChildren.length == 1 && blockChildren[0][0] == 'extend') ) { var extras: any[] = [] blockChildren.map((child) => { // bubble up extends by dupliucating a rule if (child && child[0] == 'extend' && type == 'rule') { extras.push(...unnestBlock(['rule', value, ...unnestBlock(child)])) } // combine media queries if (child && child[0] == 'media' && type == 'media') { extras.push(...unnestBlock(['media', [value, child[1]].flat().join(' and '), ...child.slice(2)])) } // combine selectors of parent & nested rule if (child && child[0] == 'rule' && type == 'rule') { extras.push(...unnestBlock(['rule', combineSelectors(value, child[1]), ...child.slice(2)])) } // invert query to be on top // may cause unnesting parent rule with children rule of a media query if (child && child[0] == 'media' && type == 'rule') { extras.push(...unnestBlock(['media', child[1], ...unnestBlock(['rule', value, ...child.slice(2)])])) } // retain rule inside media query if (child && child[0] == 'rule' && type == 'media') { extras.push(...unnestBlock(['media', value, child])) } }) if (extras.length) { if (regularChildren.length == 0) { return extras } return [[type, value, ...regularChildren], ...extras] } } } return [ast] } // invoke all functions with context as argument // values returned from functions would be injected back into AST // functions returning arrays splice in the values export function reify(ast: AST, context?: Context): AST { if (ast && Array.isArray(ast)) { return ast .map((child) => { if (typeof child == 'function') { return reify([].concat(child(context)), context) } else { return [reify(child, context)] } }) .flat() } else { return ast } } // walk the AST and process all commands into their results // the result is arrays with string lines, with sub-arrays for indentation export function process(ast: AST, isInside?: boolean): any { if (!ast || !Array.isArray(ast)) { return [ast] } const command = commands[ast[0] as keyof typeof commands] const args = (command ? ast.slice(1) : ast).map((c) => process(c, true)).flat() // @ts-ignore const result = command ? command(...args) : [args] return isInside ? result : result.flat() } export function simplify(ast: AST) { const extenders: [target: string, replacement: string][] = [] const filtered: AST[] = ast.filter((block: AST) => { if (block?.[0] == 'rule' && block.length == 3 && block[2]?.[0] == 'extend') { ;[].concat(block[1]).map((s) => { ;[].concat(block[2][1]).forEach((e) => { extenders.push([e, s]) }) }) return false } else { return true } }) return filtered .reduce((next: AST[], child: AST) => { if (child == null) return next const prev = next[next.length - 1] // merge adjacent media queries with identical expressions if (child[0] == 'media' && prev && prev[0] == 'media' && prev[1].toString() == child[1].toString()) { next[next.length - 1] = [child[0], child[1], ...prev.slice(2), ...child.slice(2)] // extend selectors } else if (child[0] == 'rule' && child[1] != null) { const selectors = [].concat(child[1]) next.push([child[0], selectors.concat(extendSelectors(selectors, extenders)), ...child.slice(2)]) } else { next.push(child) } return next }, []) .map((child: AST) => { // visit rules in media queries to extend them if (child[0] == 'media') { return [ child[0], child[1], ...child.slice(2).map((s: AST) => { if (s[0] == 'rule') { const selectors = [].concat(s[1]) return [s[0], selectors.concat(extendSelectors(selectors, extenders)), ...s.slice(2)] } else { return s } }) ] } return child }) } /* Move @import expression to the top of the file */ function hoist(css: string) { const importStatements: string[] = [] css = css.replace( /@import\s+(url\(\s*(['"]?)([^'"\)]*)\2?\s*\)|(['"])([^'"\)]*)\4?|([^'"\(\)]+))(;|}|$)/g, (_, url, b, c, d, e, f, end) => { importStatements.push(`@import ${url};`) return end == '}' ? '}' : '' } ) return [...importStatements, css].filter(Boolean).join('\n') } // resolve variables, process the ast, and serialize it export function stringify(ast: any, context?: Context) { return hoist(concat(process(simplify(unnest(reify(ast, context)))), '')) } // stringify the lines, processing sub-arrays to add indentation export function concat(lines: (string | null | undefined | boolean | string[])[], identation = ''): string { return lines .filter((line) => line != null && line !== false) .map((line) => { if (Array.isArray(line)) { return concat(line, identation + ' ') } else { return identation + line } }) .join('\n') } export type RuleAST = ['rule', string[], ...AnyAST[]] export type MediaAST = ['media', string, ...AnyAST[]] export type CommentAST = ['comment', string] export type ExtendAST = ['extend', string] export type ImportAST = ['import', string] export type PropertyAST = ['property', string, AST | KeywordAST] export type LengthAST = ['length', { value: number; unit: string }] export type StringAST = ['string', string] export type FontFaceAST = ['font-face', ...(PropertyAST | CommentAST)[]] export type ErrorAST = ['error', string, number, number, string] export type KeywordAST = string export type AnyAST = LeafAST | BranchAST export type TreeAST = AnyAST[] export type LeafAST = CommentAST | LengthAST | StringAST | PropertyAST | ErrorAST | ExtendAST | ImportAST export type BranchAST = RuleAST | MediaAST | FontFaceAST type FontVariant = { name: string familyName: string weight: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 style: 'italic' | 'normal' unicodeRange?: string src?: string } export function getFonts(ast: TreeAST): FontVariant[] { const fontVariants: FontVariant[] = [] for (const rule of ast) { if (rule[0] === 'font-face') { const [, ...properties] = rule const fontVariant: FontVariant = { name: '', familyName: '', weight: 400, style: 'normal' } let fromWeight = 400 let toWeight = 400 for (const property of properties) { if (property[0] === 'property') { const [, propertyName, propertyValue] = property if (propertyName === 'font-family') { fontVariant.familyName = propertyValue[0] == 'string' ? propertyValue[1] : propertyValue } else if (propertyName === 'font-weight') { const weights = propertyValue.split(' ') fromWeight = parseInt(weights[0], 10) toWeight = parseInt(weights[weights.length - 1], 10) } else if (propertyName === 'font-style') { fontVariant.style = propertyValue as 'italic' | 'normal' } else if (propertyName === 'src') { fontVariant.src = propertyValue } else if (propertyName === 'unicode-range') { fontVariant.unicodeRange = propertyValue } } } for (let weight = fromWeight; weight <= toWeight; weight += 100) { fontVariants.push({ ...fontVariant, name: fontVariant.style + ' ' + weight, weight: weight as FontVariant['weight'] }) } } } return fontVariants } export function parse(input: string): TreeAST { const stack: [AnyAST[], ...BranchAST[]] = [[]] let i = 0 function fail(message: string) { let line = 1 let char = 0 while (/\s|:|;|}/.test(input[i])) i-- for (let index = 0; index < i; index++) { if (input[index] === '\n') { line++ char = 0 } else { char++ } } const lines = input.split('\n') const errorLine = lines[line - 1] const errorCharLine = '-'.repeat(char) + '^' const errorMessage = [lines[line - 2] || '', errorLine, errorCharLine, lines[line] || ''].filter(Boolean).join('\n') throw ['error', message, line, char, errorMessage] as ErrorAST } function consumeWhile(predicate: (char: string) => boolean): string { let result = '' while (i < input.length && predicate(input[i])) { result += input[i++] } return result.trim() } function parseComment(): CommentAST | ErrorAST | ErrorAST { i += 2 // Skip /* const content = consumeWhile((c) => !(c === '*' && input[i + 1] === '/')) i += 2 // Skip */ return ['comment', content] } function parseString(quoteType: string): StringAST | ErrorAST { i++ // Skip opening quote let content = '' while (i < input.length && input[i] !== quoteType) { if (input[i] === '\\' && input[i + 1] === quoteType) { i++ // Skip escape character } content += input[i++] } i++ // Skip closing quote return ['string', content] } function parseProperty(): PropertyAST | ErrorAST { const propertyName = consumeWhile((c) => /[a-z0-9-_\s]/i.test(c) && c !== ':') if (input[i] != ':' || !propertyName) { fail('Failed to parse') } i++ // Skip : let value: LeafAST | KeywordAST consumeWhile((c) => /\s/.test(c)) if (input[i] === '"' || input[i] === "'") { value = parseString(input[i]) } else { value = consumeWhile((c) => c !== ';' && c !== '}') if (value.match(/^\s*-?(?:\d+)?\.?\d+(px|em|rem|%|fr|vw|vh|vmax|vmin)$/i)) { value = ['length', normalizeStringLength(value)] } } if (!value) fail('Missing value') consumeWhile((c) => c !== ';' && c !== '}') if (input[i] == ';') i++ // Skip ; return ['property', propertyName, value] } function push(ast: AnyAST) { const last = stack[stack.length - 1] if (last[0] == 'font-face') { if (typeof ast[0] != 'string' || (ast[0] != 'property' && ast[0] != 'comment')) { return fail('Unexpected @font-face children') } last.push(ast) } else { last.push(ast) } } function start(ast: BranchAST) { const last = stack[stack.length - 1] if (last[0] == 'font-face') { return fail('Can not nest inside @font-face') } stack.push(ast) } try { while (i < input.length) { if (input[i] === ';') { i++ // ignore stray semicolons } else if (input[i] === '/' && input[i + 1] === '*') { // @ts-ignore push(parseComment()) } else if (input[i] === '@' && input.slice(i, i + 7) === '@extend') { i += 7 // Skip @extend const selector = consumeWhile((c) => !/;|\}/.test(c)) if (!selector || !/;|\}/.test(input[i])) { fail('Invalid @extend selector') } i++ // Skip ; push(['extend', selector]) } else if (input[i] === '@' && input.slice(i, i + 7) === '@import') { i += 7 // Skip @import const path = consumeWhile((c) => !/;|\n|\}/.test(c)) if (input[i] && !/;|\}|\n}/.test(input[i])) { fail('Invalid @import url') } const match = path.match(/\s*url\(\s*(['"]?)([^'"\)]*)\1?\s*\)|(['"])([^'"\)]*)\3?|([^'"\(\)]+)/) const matchedQuote = match?.[1] || match?.[3] const url = (match?.[2] || match?.[4] || match?.[5]).trim() if (!url) fail('Invalid @import url') if (input[i] == ';') i++ // Skip ; push(['import', url]) } else if (input[i] === '@' && input.slice(i, i + 6) === '@media') { i += 6 // Skip @media var pos = i const mediaQuery = consumeWhile((c) => c !== '{') if ( input[i] !== '{' || !mediaQuery || !/^(?:(?:\s*(?:and|,)\s*)?(?:\(\s*(min|max)-(height|width)\s*:\s*((?:\d+)?\.?\d+)(px|em|rem|%|fr|vmin|vmax|vw|vh)\s*\))?(?:\(\s*orientation\s*:\s*(landscape|portrait)\s*\))?(?:\(\s*(prefers-color-scheme)\s*:\s*(dark|light)\s*\))?(?:(\s*only\s+)?\s*(screen|print)\s*)?\s*)+$/i.test( mediaQuery ) ) { i = pos fail('Invalid media query') } i++ // Skip { start(['media', mediaQuery]) } else if (input[i] === '@' && input.slice(i, i + 10) === '@font-face') { i += 10 // Skip @font-face const fontName = consumeWhile((c) => !/\{/.test(c)) if (fontName) { fail('Invalid @font-face') } i++ // Skip { start(['font-face'] as FontFaceAST) } else if (input[i] === '}') { const last = stack.pop() if (typeof last[0] != 'string') { fail('Unexpected }') } else { push(last) } i++ // Skip } } else if (/\S/.test(input[i])) { var pos = i const selectorOrProperty = consumeWhile((c) => c !== '{' && c !== ';' && c !== '}') if (input[i] === '{') { i++ // Skip the { start(['rule', splitSelector(selectorOrProperty)]) } else { i = pos push(parseProperty()) } } consumeWhile((c) => /\s/.test(c)) } if (stack.length > 1) { fail('Missing closed brace for ' + stack[stack.length - 1][0]) } } catch (e) { if (Array.isArray(e)) { return [e as ErrorAST] } else { try { fail('Error during parsing CSS: ' + e.message) } catch (e) { return e } } } return stack[0] as TreeAST } function splitSelector(selector: string) { const regex = /'[^']*'|"[^"]*"|\([^()]*\)|,/g let parts = [], lastIndex = 0 selector.replace(regex, (match, index) => { if (match === ',') { parts.push(selector.substring(lastIndex, index).trim()) lastIndex = index + 1 } return match }) parts.push(selector.substring(lastIndex).trim()) return parts } const prelude = `[class*="-theme--"] * { /* Important: Disable variables from cascading */ ---layout--available-width: initial; ---layout--columns: initial; ---layout--column-gap: initial; ---layout--row-gap: initial; ---typography--paragraph-spacing: initial; ---self--paragraph-spacing: initial; ---spacing--column-gap: 0; ---spacing--row-gap: 0; ---self--max-available-width: initial; ---self--max-width: initial; ---self--column-width: initial; ---self--column-gap: initial; ---self--row-gap: initial; ---self--vertical-layout: initial; ---self--spacer-height: initial; ---self--icon-size: initial; ---self--display: initial; margin: 0; } [class*="-theme--"] h1, [class*="-theme--"] h2, [class*="-theme--"] h3, [class*="-theme--"] h4, [class*="-theme--"] h5, [class*="-theme--"] p, [class*="-theme--"] ul, [class*="-theme--"] ol, [class*="-theme--"] li { flex-shrink: 0; } [class*="-theme--"]:not(#x) * { word-break: normal; overflow-wrap: normal; box-sizing: border-box; } [class*="-theme--"] .-spacer { flex-grow: 100000 !important; align-self: stretch !important; justify-self: stretch !important; ---self--spacer-height: var(---self--row-gap, var(---layout--row-gap)); height: var(---self--spacer-height, 5px); margin-top: calc(-1 * var(---self--spacer-height)) !important; margin-bottom: calc(-1 * var(---self--spacer-height)) !important; } body { ---supports--flex-gap: 1; } [class*="-theme--"] .-embed, [class*="-theme--"] .-component { width: 100%; } [class*="-theme--"] .-component { position: relative; } [class*="-theme--"] a { text-decoration: none; } .-feaas li { margin-left: 1em; } .-feaas [class*="-image"] { max-width: 100%; } .-feaas var::selection { background: none !important; } .-feaas .-spacer::selection { background: none !important; } .-feaas .-spacer br { opacity: 0; } .-feaas .-spacer, .-feaas picture { caret-color: transparent; } .-feaas .-has--background { position: relative; min-height: 1em; } .-feaas .-has--background > :not(.-image--background) { z-index: 2; position: relative; } .-feaas [data-symbol-ref] { user-select: none; caret-color: rgba(255,255,255,0) !important; } .-feaas { align-items: center; display: flex; flex-direction: column; overflow: auto; } .-feaas > * { width: 100%; max-width: 100%; } .-feaas > [class*="-theme--"] { display: flex; align-items: center; flex-direction: column; } .-feaas > [class*="-theme--"] > *{ width: 100%; max-width: 100%; } [class*="-theme--"] :not(div):not(a) + *, [class*="-theme--"] :not(div):not(a) + ul > :first-child, [class*="-theme--"] :not(div):not(a) + ol > :first-child { ---self--paragraph-spacing: var(---typography--paragraph-spacing); } [class*="-theme--"] ul > li, [class*="-theme--"] ol > li { margin-top: var(---self--paragraph-spacing, 0); } .-feaas [hidden] { display: none !important; } .-feaas var { font-style: inherit; } .-editor-show-hidden .-feaas [hidden]{ display: var(---self--display) !important; opacity: 0.5 !important; } .-editor-grid-mode .-feaas *{ caret-color: rgba(255,255,255,0) !important; } .-editor-grid-mode .-feaas ::selection{ background: rgba(255,255,255,0); } ${stringify([ 'rule', [ '.-card', '.-section > *', '[class*="-theme--"] section > *', '[class*="-spacing--"]', '[class*="-block--"]', '[class*="-card--"]' ], ...produceLayoutProps(Style.Layout.Props()) ])} ${stringify([ 'rule', [ '.-feaas:not(#x) .-card', '[class*="-theme--"]:not(#x) [class*="-spacing--"]', '[class*="-theme--"]:not(#x) [class*="-layout--"]', '[class*="-theme--"]:not(#x) [class*="-block--"]', '[class*="-theme--"]:not(#x) [class*="-card--"]', '[class*="-theme--"]:not(#x) [class*="-section--"]' ], ['rule', '& > :not(picture)', ['property', '---self--column-gap', 0]] ])} ${stringify([ 'rule', [ '[class*="-theme--"]:not(#x) button', '[class*="-theme--"]:not(#x) .-button', '[class*="-theme--"]:not(#x) [class*="-button--"]', '[class*="-theme--"]:not(#x) .-inline', '[class*="-theme--"]:not(#x) [class*="-inline--"]' ], // dont allow empty inline to collapse ['property', '---self--display', 'var(---layout--inline-display, table)'], ['property', 'display', 'var(---self--display)'], ['property', 'gap', '0'], ['property', 'flex-direction', 'row'], ['rule', '&:empty:before', ['property', 'content', '"\\2060"'], ['property', 'display', 'inline-block']], ['property', 'flex-wrap', 'nowrap'], ['property', 'white-space', 'nowrap'], ['property', 'flex-shrink', '0'], ['property', 'flex-grow', '0'], ['property', 'vertical-align', 'middle'], ['property', 'width', 'max-content'], ['property', 'height', 'max-content'] ])} ${stringify([ 'rule', ['[class*="-theme--"]:not(#x) .-inline', '[class*="-theme--"]:not(#x) [class*="-inline--"]'], ['property', 'vertical-align', 'baseline'] ])} .-feaas h1, .-feaas h2, .-feaas h3, .-feaas h4, .-feaas h5, .-feaas h6, .-feaas li, .-feaas p { ---layout--inline-display: inline-flex; } .feaas li { list-style: disc; } ${stringify([ 'rule', ['[class*="-theme--"]:not(#x) var', '[class*="-theme--"]:not(#x) .-variable'], ['property', 'flex-shrink', '0'] ])} /* Builder styles */ .-feaas[contenteditable="true"] .-embed, .-feaas[contenteditable="true"] .-component { user-select: none; } .-feaas[contenteditable="true"] .-spacer { min-width: 5px; min-height: 5px; } .-feaas[contenteditable], .-feaas [contenteditable] { outline: none; } .-feaas[contenteditable="true"]:not(#x) spacer-before { display: inline-block; width: 2px; letter-spacing: -30px; margin-right: -2px; } .-feaas[contenteditable="true"]:not(#x) spacer-before[for="var"] { position: absolute; } .-feaas[contenteditable="true"]:not(#x) spacer-after[for="var"] { position: absolute; } .-feaas[contenteditable="true"]:not(#x) spacer-after { display: inline-block; width: 2px; letter-spacing: -30px; margin-left: -2px; } .-feaas[contenteditable="true"]:not(#x) spacer-inside { display: inline-block; width: 2px; margin-left: -1px; margin-right: -1px; } .-feaas[contenteditable="true"]:not(#x) .-button, .-feaas[contenteditable="true"]:not(#x) [class*="-button--"], .-feaas[contenteditable="true"]:not(#x) .-inline, .-feaas[contenteditable="true"]:not(#x) [class*="-inline--"], .-feaas[contenteditable="true"]:not(#x) .-block, .-feaas[contenteditable="true"]:not(#x) .-embed, .-feaas[contenteditable="true"]:not(#x) .-component, .-feaas[contenteditable="true"]:not(#x) p, .-feaas[contenteditable="true"]:not(#x) li, .-feaas[contenteditable="true"]:not(#x) h1, .-feaas[contenteditable="true"]:not(#x) h2, .-feaas[contenteditable="true"]:not(#x) h3, .-feaas[contenteditable="true"]:not(#x) h4, .-feaas[contenteditable="true"]:not(#x) h5, .-feaas[contenteditable="true"]:not(#x) blockquote, .-feaas[contenteditable="true"]:not(#x) .-container, .-feaas[contenteditable="true"]:not(#x) [class*="-block--"], .-feaas[contenteditable="true"]:not(#x) [class*="-spacing--"], .-feaas[contenteditable="true"]:not(#x) [class*="-layout--"], .-feaas[contenteditable="true"]:not(#x) [class*="-block--"], .-feaas[contenteditable="true"]:not(#x) [class*="-card--"], .-feaas[contenteditable="true"]:not(#x) [class*="-section--"] { min-width: 1em; min-height: 1em; } ${stringify([ 'rule', ['.-feaas picture.-image--background', '.-feaas picture.-image--foreground'], ...produceMediaProps(Style.Media.Props({})), ['property', 'min-height', '24x'], ['property', 'min-width', '24px'], ['property', 'max-width', '100%'] ])} .-feaas:not(#x) picture.-image--background, .-feaas:not(#x) picture.-image--foreground { border-radius: inherit; } .-feaas:not(#x) picture.-image--background, .-feaas:not(#x) picture.-image--icon { user-select: none; pointer-events: none; } .-feaas:not(#x) picture[class*="-image--"] > img { min-height: 0px; width: 100%; height: 100%; display: block; user-drag: none; user-select: none; object-fit: cover; object-position: 50% 50%; border-radius: inherit; } .-feaas:not(#x) picture.-image--icon > img { width: auto; } .-feaas:not(#x) picture.-image--icon { ---spacing--column-gap: inherit; ---self--icon-size: var(---typography--icon-size, var(---typography--line-height, 1.2em)); margin-top: calc((var(---typography--line-height, 1.6em) - var(---self--icon-size)) / 2); height: var(---self--icon-size, auto) !important; margin-right: var(---spacing--column-gap, 0px); display: inline-block; vertical-align: top; } .-feaas:not(#x) picture.-image--foreground { flex-grow: 1; min-height: 16px; } .-feaas:not(#x):not(#x) picture.-image--background { position: absolute; top: 0; bottom: 0; right: 0; left: 0; ---self--row-gap: 0 !important; ---self--column-gap: 0 !important; ---self--column-width: unset !important; border-radius: inherit; } .-feaas:not(#x) a { text-decoration: none; } .-feaas:not(#x) a:not([class*="-button"]):not([class*="-card"]) { text-decoration: var(---typography--text-decoration, none); } .-feaas--preview { font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; } .-feaas--preview * { font-family: inherit; font-size: inherit; line-height: inherit; } .-feaas--preview a:visited, .-feaas--preview a:hover, .-feaas--preview a:active, .-feaas--preview a:link { color: inherit } .-feaas:not(#x) .-component[data-embed-placeholder], .-feaas:not(#x) [class*="-component--"][data-embed-placeholder], .-feaas:not(#x) .-embed[data-embed-placeholder] { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; } .-feaas:not(#x) .-embed[data-embed-placeholder] { background: url('https://feaasstatic.blob.core.windows.net/assets/sample/embed.svg') !important; } .-feaas:not(#x) .-component[data-embed-placeholder], .-feaas:not(#x) [class*="-component--"][data-embed-placeholder] { background: url('https://feaasstatic.blob.core.windows.net/assets/sample/component.svg') !important; } .-embed--meta { padding: 16px; background: #fff; border-radius: 8px; color: #fff; overflow: hidden; background: #5548D9; } .-embed--meta--type { font-size: 12px; line-height: 14px; font-weight: 500; margin-bottom: 4px; } .-embed--meta--label{ font-size: 18px; line-height: 120%; font-weight: 600; } ` export function toText(styles: Style.Rule[], context?: Context): string { return ( stringify(styles.map((style) => produceStyle(style)).flat(1), { ...context, rules: styles }) + (context?.prelude === false ? '' : '\n\n' + prelude) ) } export async function optimize(css: string): Promise { const importRegex = /@import\s*(?:url\()?\s*['"]?([^'"\)]+)['"]?\s*(?:\))?\s*;/g let importMatch // inline css imports. while ((importMatch = importRegex.exec(css)) !== null) { try { const importUrl = importMatch[1] const response = await fetch(importUrl) const importedCss = await response.text() css = css.replace(importMatch[0], await optimize(importedCss)) } catch (e) { console.info('Could not inline', importMatch[1], ':', e) } } return css }