|
|
import deindent from '../../utils/deindent';
|
|
|
import { stringify } from '../../utils/stringify';
|
|
|
import fixAttributeCasing from '../../utils/fixAttributeCasing';
|
|
|
import getExpressionPrecedence from '../../utils/getExpressionPrecedence';
|
|
|
import addToSet from '../../utils/addToSet';
|
|
|
import { DomGenerator } from '../dom/index';
|
|
|
import Node from './shared/Node';
|
|
|
import Element from './Element';
|
|
|
import Block from '../dom/Block';
|
|
|
import Expression from './shared/Expression';
|
|
|
|
|
|
export interface StyleProp {
|
|
|
key: string;
|
|
|
value: Node[];
|
|
|
}
|
|
|
|
|
|
export default class Attribute extends Node {
|
|
|
type: 'Attribute';
|
|
|
start: number;
|
|
|
end: number;
|
|
|
|
|
|
compiler: DomGenerator;
|
|
|
parent: Element;
|
|
|
name: string;
|
|
|
isTrue: boolean;
|
|
|
isDynamic: boolean;
|
|
|
chunks: Node[];
|
|
|
dependencies: Set<string>;
|
|
|
expression: Node;
|
|
|
|
|
|
constructor(compiler, parent, info) {
|
|
|
super(compiler, parent, info);
|
|
|
|
|
|
this.name = info.name;
|
|
|
this.isTrue = info.value === true;
|
|
|
|
|
|
this.dependencies = new Set();
|
|
|
|
|
|
this.chunks = this.isTrue
|
|
|
? []
|
|
|
: info.value.map(node => {
|
|
|
if (node.type === 'Text') return node;
|
|
|
|
|
|
const expression = new Expression(compiler, this, node.expression);
|
|
|
|
|
|
addToSet(this.dependencies, expression.dependencies);
|
|
|
return expression;
|
|
|
});
|
|
|
|
|
|
this.isDynamic = this.dependencies.size > 0;
|
|
|
}
|
|
|
|
|
|
render(block: Block) {
|
|
|
const node = this.parent;
|
|
|
const name = fixAttributeCasing(this.name);
|
|
|
|
|
|
if (name === 'style') {
|
|
|
const styleProps = optimizeStyle(this.chunks);
|
|
|
if (styleProps) {
|
|
|
this.renderStyle(block, styleProps);
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
let metadata = node.namespace ? null : attributeLookup[name];
|
|
|
if (metadata && metadata.appliesTo && !~metadata.appliesTo.indexOf(node.name))
|
|
|
metadata = null;
|
|
|
|
|
|
const isIndirectlyBoundValue =
|
|
|
name === 'value' &&
|
|
|
(node.name === 'option' || // TODO check it's actually bound
|
|
|
(node.name === 'input' &&
|
|
|
node.attributes.find(
|
|
|
(attribute: Attribute) =>
|
|
|
attribute.type === 'Binding' && /checked|group/.test(attribute.name)
|
|
|
)));
|
|
|
|
|
|
const propertyName = isIndirectlyBoundValue
|
|
|
? '__value'
|
|
|
: metadata && metadata.propertyName;
|
|
|
|
|
|
// xlink is a special case... we could maybe extend this to generic
|
|
|
// namespaced attributes but I'm not sure that's applicable in
|
|
|
// HTML5?
|
|
|
const method = name.slice(0, 6) === 'xlink:'
|
|
|
? '@setXlinkAttribute'
|
|
|
: '@setAttribute';
|
|
|
|
|
|
const isLegacyInputType = this.compiler.legacy && name === 'type' && this.parent.name === 'input';
|
|
|
|
|
|
const isDataSet = /^data-/.test(name) && !this.compiler.legacy && !node.namespace;
|
|
|
const camelCaseName = isDataSet ? name.replace('data-', '').replace(/(-\w)/g, function (m) {
|
|
|
return m[1].toUpperCase();
|
|
|
}) : name;
|
|
|
|
|
|
if (this.isDynamic) {
|
|
|
let value;
|
|
|
|
|
|
const allDependencies = new Set();
|
|
|
let shouldCache;
|
|
|
let hasChangeableIndex;
|
|
|
|
|
|
// TODO some of this code is repeated in Tag.ts — would be good to
|
|
|
// DRY it out if that's possible without introducing crazy indirection
|
|
|
if (this.chunks.length === 1) {
|
|
|
// single {tag} — may be a non-string
|
|
|
const expression = this.chunks[0];
|
|
|
const { dependencies, snippet, indexes } = expression;
|
|
|
|
|
|
value = snippet;
|
|
|
dependencies.forEach(d => {
|
|
|
allDependencies.add(d);
|
|
|
});
|
|
|
|
|
|
hasChangeableIndex = Array.from(indexes).some(index => block.changeableIndexes.get(index));
|
|
|
|
|
|
shouldCache = (
|
|
|
expression.type !== 'Identifier' ||
|
|
|
block.contexts.has(expression.name) ||
|
|
|
hasChangeableIndex
|
|
|
);
|
|
|
} else {
|
|
|
// '{{foo}} {{bar}}' — treat as string concatenation
|
|
|
value =
|
|
|
(this.chunks[0].type === 'Text' ? '' : `"" + `) +
|
|
|
this.chunks
|
|
|
.map((chunk: Node) => {
|
|
|
if (chunk.type === 'Text') {
|
|
|
return stringify(chunk.data);
|
|
|
} else {
|
|
|
const { dependencies, snippet, indexes } = chunk;
|
|
|
|
|
|
if (Array.from(indexes).some(index => block.changeableIndexes.get(index))) {
|
|
|
hasChangeableIndex = true;
|
|
|
}
|
|
|
|
|
|
dependencies.forEach(d => {
|
|
|
allDependencies.add(d);
|
|
|
});
|
|
|
|
|
|
return getExpressionPrecedence(chunk) <= 13 ? `(${snippet})` : snippet;
|
|
|
}
|
|
|
})
|
|
|
.join(' + ');
|
|
|
|
|
|
shouldCache = true;
|
|
|
}
|
|
|
|
|
|
const isSelectValueAttribute =
|
|
|
name === 'value' && node.name === 'select';
|
|
|
|
|
|
const last = (shouldCache || isSelectValueAttribute) && block.getUniqueName(
|
|
|
`${node.var}_${name.replace(/[^a-zA-Z_$]/g, '_')}_value`
|
|
|
);
|
|
|
|
|
|
if (shouldCache || isSelectValueAttribute) block.addVariable(last);
|
|
|
|
|
|
let updater;
|
|
|
const init = shouldCache ? `${last} = ${value}` : value;
|
|
|
|
|
|
if (isLegacyInputType) {
|
|
|
block.builders.hydrate.addLine(
|
|
|
`@setInputType(${node.var}, ${init});`
|
|
|
);
|
|
|
updater = `@setInputType(${node.var}, ${shouldCache ? last : value});`;
|
|
|
} else if (isSelectValueAttribute) {
|
|
|
// annoying special case
|
|
|
const isMultipleSelect = node.getStaticAttributeValue('multiple');
|
|
|
const i = block.getUniqueName('i');
|
|
|
const option = block.getUniqueName('option');
|
|
|
|
|
|
const ifStatement = isMultipleSelect
|
|
|
? deindent`
|
|
|
${option}.selected = ~${last}.indexOf(${option}.__value);`
|
|
|
: deindent`
|
|
|
if (${option}.__value === ${last}) {
|
|
|
${option}.selected = true;
|
|
|
break;
|
|
|
}`;
|
|
|
|
|
|
updater = deindent`
|
|
|
for (var ${i} = 0; ${i} < ${node.var}.options.length; ${i} += 1) {
|
|
|
var ${option} = ${node.var}.options[${i}];
|
|
|
|
|
|
${ifStatement}
|
|
|
}
|
|
|
`;
|
|
|
|
|
|
block.builders.hydrate.addBlock(deindent`
|
|
|
${last} = ${value};
|
|
|
${updater}
|
|
|
`);
|
|
|
|
|
|
block.builders.update.addLine(`${last} = ${value};`);
|
|
|
} else if (propertyName) {
|
|
|
block.builders.hydrate.addLine(
|
|
|
`${node.var}.${propertyName} = ${init};`
|
|
|
);
|
|
|
updater = `${node.var}.${propertyName} = ${shouldCache ? last : value};`;
|
|
|
} else if (isDataSet) {
|
|
|
block.builders.hydrate.addLine(
|
|
|
`${node.var}.dataset.${camelCaseName} = ${init};`
|
|
|
);
|
|
|
updater = `${node.var}.dataset.${camelCaseName} = ${shouldCache ? last : value};`;
|
|
|
} else {
|
|
|
block.builders.hydrate.addLine(
|
|
|
`${method}(${node.var}, "${name}", ${init});`
|
|
|
);
|
|
|
updater = `${method}(${node.var}, "${name}", ${shouldCache ? last : value});`;
|
|
|
}
|
|
|
|
|
|
if (allDependencies.size || hasChangeableIndex || isSelectValueAttribute) {
|
|
|
const dependencies = Array.from(allDependencies);
|
|
|
const changedCheck = (
|
|
|
( block.hasOutroMethod ? `#outroing || ` : '' ) +
|
|
|
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
|
|
|
);
|
|
|
|
|
|
const updateCachedValue = `${last} !== (${last} = ${value})`;
|
|
|
|
|
|
const condition = shouldCache ?
|
|
|
( dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue ) :
|
|
|
changedCheck;
|
|
|
|
|
|
block.builders.update.addConditional(
|
|
|
condition,
|
|
|
updater
|
|
|
);
|
|
|
}
|
|
|
} else {
|
|
|
const value = this.isTrue
|
|
|
? 'true'
|
|
|
: this.chunks.length === 0 ? `""` : stringify(this.chunks[0].data);
|
|
|
|
|
|
const statement = (
|
|
|
isLegacyInputType
|
|
|
? `@setInputType(${node.var}, ${value});`
|
|
|
: propertyName
|
|
|
? `${node.var}.${propertyName} = ${value};`
|
|
|
: isDataSet
|
|
|
? `${node.var}.dataset.${camelCaseName} = ${value};`
|
|
|
: `${method}(${node.var}, "${name}", ${value});`
|
|
|
);
|
|
|
|
|
|
block.builders.hydrate.addLine(statement);
|
|
|
|
|
|
// special case – autofocus. has to be handled in a bit of a weird way
|
|
|
if (this.value === true && name === 'autofocus') {
|
|
|
block.autofocus = node.var;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (isIndirectlyBoundValue) {
|
|
|
const updateValue = `${node.var}.value = ${node.var}.__value;`;
|
|
|
|
|
|
block.builders.hydrate.addLine(updateValue);
|
|
|
if (this.isDynamic) block.builders.update.addLine(updateValue);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
renderStyle(
|
|
|
block: Block,
|
|
|
styleProps: StyleProp[]
|
|
|
) {
|
|
|
styleProps.forEach((prop: StyleProp) => {
|
|
|
let value;
|
|
|
|
|
|
if (isDynamic(prop.value)) {
|
|
|
const allDependencies = new Set();
|
|
|
let shouldCache;
|
|
|
let hasChangeableIndex;
|
|
|
|
|
|
value =
|
|
|
((prop.value.length === 1 || prop.value[0].type === 'Text') ? '' : `"" + `) +
|
|
|
prop.value
|
|
|
.map((chunk: Node) => {
|
|
|
if (chunk.type === 'Text') {
|
|
|
return stringify(chunk.data);
|
|
|
} else {
|
|
|
const { dependencies, snippet, indexes } = chunk;
|
|
|
|
|
|
if (Array.from(indexes).some(index => block.changeableIndexes.get(index))) {
|
|
|
hasChangeableIndex = true;
|
|
|
}
|
|
|
|
|
|
dependencies.forEach(d => {
|
|
|
allDependencies.add(d);
|
|
|
});
|
|
|
|
|
|
return getExpressionPrecedence(chunk.expression) <= 13 ? `( ${snippet} )` : snippet;
|
|
|
}
|
|
|
})
|
|
|
.join(' + ');
|
|
|
|
|
|
if (allDependencies.size || hasChangeableIndex) {
|
|
|
const dependencies = Array.from(allDependencies);
|
|
|
const condition = (
|
|
|
( block.hasOutroMethod ? `#outroing || ` : '' ) +
|
|
|
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
|
|
|
);
|
|
|
|
|
|
block.builders.update.addConditional(
|
|
|
condition,
|
|
|
`@setStyle(${this.parent.var}, "${prop.key}", ${value});`
|
|
|
);
|
|
|
}
|
|
|
} else {
|
|
|
value = stringify(prop.value[0].data);
|
|
|
}
|
|
|
|
|
|
block.builders.hydrate.addLine(
|
|
|
`@setStyle(${this.parent.var}, "${prop.key}", ${value});`
|
|
|
);
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// source: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
|
|
|
const attributeLookup = {
|
|
|
accept: { appliesTo: ['form', 'input'] },
|
|
|
'accept-charset': { propertyName: 'acceptCharset', appliesTo: ['form'] },
|
|
|
accesskey: { propertyName: 'accessKey' },
|
|
|
action: { appliesTo: ['form'] },
|
|
|
align: {
|
|
|
appliesTo: [
|
|
|
'applet',
|
|
|
'caption',
|
|
|
'col',
|
|
|
'colgroup',
|
|
|
'hr',
|
|
|
'iframe',
|
|
|
'img',
|
|
|
'table',
|
|
|
'tbody',
|
|
|
'td',
|
|
|
'tfoot',
|
|
|
'th',
|
|
|
'thead',
|
|
|
'tr',
|
|
|
],
|
|
|
},
|
|
|
allowfullscreen: { propertyName: 'allowFullscreen', appliesTo: ['iframe'] },
|
|
|
alt: { appliesTo: ['applet', 'area', 'img', 'input'] },
|
|
|
async: { appliesTo: ['script'] },
|
|
|
autocomplete: { appliesTo: ['form', 'input'] },
|
|
|
autofocus: { appliesTo: ['button', 'input', 'keygen', 'select', 'textarea'] },
|
|
|
autoplay: { appliesTo: ['audio', 'video'] },
|
|
|
autosave: { appliesTo: ['input'] },
|
|
|
bgcolor: {
|
|
|
propertyName: 'bgColor',
|
|
|
appliesTo: [
|
|
|
'body',
|
|
|
'col',
|
|
|
'colgroup',
|
|
|
'marquee',
|
|
|
'table',
|
|
|
'tbody',
|
|
|
'tfoot',
|
|
|
'td',
|
|
|
'th',
|
|
|
'tr',
|
|
|
],
|
|
|
},
|
|
|
border: { appliesTo: ['img', 'object', 'table'] },
|
|
|
buffered: { appliesTo: ['audio', 'video'] },
|
|
|
challenge: { appliesTo: ['keygen'] },
|
|
|
charset: { appliesTo: ['meta', 'script'] },
|
|
|
checked: { appliesTo: ['command', 'input'] },
|
|
|
cite: { appliesTo: ['blockquote', 'del', 'ins', 'q'] },
|
|
|
class: { propertyName: 'className' },
|
|
|
code: { appliesTo: ['applet'] },
|
|
|
codebase: { propertyName: 'codeBase', appliesTo: ['applet'] },
|
|
|
color: { appliesTo: ['basefont', 'font', 'hr'] },
|
|
|
cols: { appliesTo: ['textarea'] },
|
|
|
colspan: { propertyName: 'colSpan', appliesTo: ['td', 'th'] },
|
|
|
content: { appliesTo: ['meta'] },
|
|
|
contenteditable: { propertyName: 'contentEditable' },
|
|
|
contextmenu: {},
|
|
|
controls: { appliesTo: ['audio', 'video'] },
|
|
|
coords: { appliesTo: ['area'] },
|
|
|
data: { appliesTo: ['object'] },
|
|
|
datetime: { propertyName: 'dateTime', appliesTo: ['del', 'ins', 'time'] },
|
|
|
default: { appliesTo: ['track'] },
|
|
|
defer: { appliesTo: ['script'] },
|
|
|
dir: {},
|
|
|
dirname: { propertyName: 'dirName', appliesTo: ['input', 'textarea'] },
|
|
|
disabled: {
|
|
|
appliesTo: [
|
|
|
'button',
|
|
|
'command',
|
|
|
'fieldset',
|
|
|
'input',
|
|
|
'keygen',
|
|
|
'optgroup',
|
|
|
'option',
|
|
|
'select',
|
|
|
'textarea',
|
|
|
],
|
|
|
},
|
|
|
download: { appliesTo: ['a', 'area'] },
|
|
|
draggable: {},
|
|
|
dropzone: {},
|
|
|
enctype: { appliesTo: ['form'] },
|
|
|
for: { propertyName: 'htmlFor', appliesTo: ['label', 'output'] },
|
|
|
form: {
|
|
|
appliesTo: [
|
|
|
'button',
|
|
|
'fieldset',
|
|
|
'input',
|
|
|
'keygen',
|
|
|
'label',
|
|
|
'meter',
|
|
|
'object',
|
|
|
'output',
|
|
|
'progress',
|
|
|
'select',
|
|
|
'textarea',
|
|
|
],
|
|
|
},
|
|
|
formaction: { appliesTo: ['input', 'button'] },
|
|
|
headers: { appliesTo: ['td', 'th'] },
|
|
|
height: {
|
|
|
appliesTo: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'],
|
|
|
},
|
|
|
hidden: {},
|
|
|
high: { appliesTo: ['meter'] },
|
|
|
href: { appliesTo: ['a', 'area', 'base', 'link'] },
|
|
|
hreflang: { appliesTo: ['a', 'area', 'link'] },
|
|
|
'http-equiv': { propertyName: 'httpEquiv', appliesTo: ['meta'] },
|
|
|
icon: { appliesTo: ['command'] },
|
|
|
id: {},
|
|
|
indeterminate: { appliesTo: ['input'] },
|
|
|
ismap: { propertyName: 'isMap', appliesTo: ['img'] },
|
|
|
itemprop: {},
|
|
|
keytype: { appliesTo: ['keygen'] },
|
|
|
kind: { appliesTo: ['track'] },
|
|
|
label: { appliesTo: ['track'] },
|
|
|
lang: {},
|
|
|
language: { appliesTo: ['script'] },
|
|
|
loop: { appliesTo: ['audio', 'bgsound', 'marquee', 'video'] },
|
|
|
low: { appliesTo: ['meter'] },
|
|
|
manifest: { appliesTo: ['html'] },
|
|
|
max: { appliesTo: ['input', 'meter', 'progress'] },
|
|
|
maxlength: { propertyName: 'maxLength', appliesTo: ['input', 'textarea'] },
|
|
|
media: { appliesTo: ['a', 'area', 'link', 'source', 'style'] },
|
|
|
method: { appliesTo: ['form'] },
|
|
|
min: { appliesTo: ['input', 'meter'] },
|
|
|
multiple: { appliesTo: ['input', 'select'] },
|
|
|
muted: { appliesTo: ['audio', 'video'] },
|
|
|
name: {
|
|
|
appliesTo: [
|
|
|
'button',
|
|
|
'form',
|
|
|
'fieldset',
|
|
|
'iframe',
|
|
|
'input',
|
|
|
'keygen',
|
|
|
'object',
|
|
|
'output',
|
|
|
'select',
|
|
|
'textarea',
|
|
|
'map',
|
|
|
'meta',
|
|
|
'param',
|
|
|
],
|
|
|
},
|
|
|
novalidate: { propertyName: 'noValidate', appliesTo: ['form'] },
|
|
|
open: { appliesTo: ['details'] },
|
|
|
optimum: { appliesTo: ['meter'] },
|
|
|
pattern: { appliesTo: ['input'] },
|
|
|
ping: { appliesTo: ['a', 'area'] },
|
|
|
placeholder: { appliesTo: ['input', 'textarea'] },
|
|
|
poster: { appliesTo: ['video'] },
|
|
|
preload: { appliesTo: ['audio', 'video'] },
|
|
|
radiogroup: { appliesTo: ['command'] },
|
|
|
readonly: { propertyName: 'readOnly', appliesTo: ['input', 'textarea'] },
|
|
|
rel: { appliesTo: ['a', 'area', 'link'] },
|
|
|
required: { appliesTo: ['input', 'select', 'textarea'] },
|
|
|
reversed: { appliesTo: ['ol'] },
|
|
|
rows: { appliesTo: ['textarea'] },
|
|
|
rowspan: { propertyName: 'rowSpan', appliesTo: ['td', 'th'] },
|
|
|
sandbox: { appliesTo: ['iframe'] },
|
|
|
scope: { appliesTo: ['th'] },
|
|
|
scoped: { appliesTo: ['style'] },
|
|
|
seamless: { appliesTo: ['iframe'] },
|
|
|
selected: { appliesTo: ['option'] },
|
|
|
shape: { appliesTo: ['a', 'area'] },
|
|
|
size: { appliesTo: ['input', 'select'] },
|
|
|
sizes: { appliesTo: ['link', 'img', 'source'] },
|
|
|
span: { appliesTo: ['col', 'colgroup'] },
|
|
|
spellcheck: {},
|
|
|
src: {
|
|
|
appliesTo: [
|
|
|
'audio',
|
|
|
'embed',
|
|
|
'iframe',
|
|
|
'img',
|
|
|
'input',
|
|
|
'script',
|
|
|
'source',
|
|
|
'track',
|
|
|
'video',
|
|
|
],
|
|
|
},
|
|
|
srcdoc: { appliesTo: ['iframe'] },
|
|
|
srclang: { appliesTo: ['track'] },
|
|
|
srcset: { appliesTo: ['img'] },
|
|
|
start: { appliesTo: ['ol'] },
|
|
|
step: { appliesTo: ['input'] },
|
|
|
style: { propertyName: 'style.cssText' },
|
|
|
summary: { appliesTo: ['table'] },
|
|
|
tabindex: { propertyName: 'tabIndex' },
|
|
|
target: { appliesTo: ['a', 'area', 'base', 'form'] },
|
|
|
title: {},
|
|
|
type: {
|
|
|
appliesTo: [
|
|
|
'button',
|
|
|
'command',
|
|
|
'embed',
|
|
|
'object',
|
|
|
'script',
|
|
|
'source',
|
|
|
'style',
|
|
|
'menu',
|
|
|
],
|
|
|
},
|
|
|
usemap: { propertyName: 'useMap', appliesTo: ['img', 'input', 'object'] },
|
|
|
value: {
|
|
|
appliesTo: [
|
|
|
'button',
|
|
|
'option',
|
|
|
'input',
|
|
|
'li',
|
|
|
'meter',
|
|
|
'progress',
|
|
|
'param',
|
|
|
'select',
|
|
|
'textarea',
|
|
|
],
|
|
|
},
|
|
|
volume: { appliesTo: ['audio', 'video'] },
|
|
|
width: {
|
|
|
appliesTo: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'],
|
|
|
},
|
|
|
wrap: { appliesTo: ['textarea'] },
|
|
|
};
|
|
|
|
|
|
Object.keys(attributeLookup).forEach(name => {
|
|
|
const metadata = attributeLookup[name];
|
|
|
if (!metadata.propertyName) metadata.propertyName = name;
|
|
|
});
|
|
|
|
|
|
function optimizeStyle(value: Node[]) {
|
|
|
let expectingKey = true;
|
|
|
let i = 0;
|
|
|
|
|
|
const props: { key: string, value: Node[] }[] = [];
|
|
|
let chunks = value.slice();
|
|
|
|
|
|
while (chunks.length) {
|
|
|
const chunk = chunks[0];
|
|
|
|
|
|
if (chunk.type !== 'Text') return null;
|
|
|
|
|
|
const keyMatch = /^\s*([\w-]+):\s*/.exec(chunk.data);
|
|
|
if (!keyMatch) return null;
|
|
|
|
|
|
const key = keyMatch[1];
|
|
|
|
|
|
const offset = keyMatch.index + keyMatch[0].length;
|
|
|
const remainingData = chunk.data.slice(offset);
|
|
|
|
|
|
if (remainingData) {
|
|
|
chunks[0] = {
|
|
|
start: chunk.start + offset,
|
|
|
end: chunk.end,
|
|
|
type: 'Text',
|
|
|
data: remainingData
|
|
|
};
|
|
|
} else {
|
|
|
chunks.shift();
|
|
|
}
|
|
|
|
|
|
const result = getStyleValue(chunks);
|
|
|
if (!result) return null;
|
|
|
|
|
|
props.push({ key, value: result.value });
|
|
|
chunks = result.chunks;
|
|
|
}
|
|
|
|
|
|
return props;
|
|
|
}
|
|
|
|
|
|
function getStyleValue(chunks: Node[]) {
|
|
|
const value: Node[] = [];
|
|
|
|
|
|
let inUrl = false;
|
|
|
let quoteMark = null;
|
|
|
let escaped = false;
|
|
|
|
|
|
while (chunks.length) {
|
|
|
const chunk = chunks.shift();
|
|
|
|
|
|
if (chunk.type === 'Text') {
|
|
|
let c = 0;
|
|
|
while (c < chunk.data.length) {
|
|
|
const char = chunk.data[c];
|
|
|
|
|
|
if (escaped) {
|
|
|
escaped = false;
|
|
|
} else if (char === '\\') {
|
|
|
escaped = true;
|
|
|
} else if (char === quoteMark) {
|
|
|
quoteMark === null;
|
|
|
} else if (char === '"' || char === "'") {
|
|
|
quoteMark = char;
|
|
|
} else if (char === ')' && inUrl) {
|
|
|
inUrl = false;
|
|
|
} else if (char === 'u' && chunk.data.slice(c, c + 4) === 'url(') {
|
|
|
inUrl = true;
|
|
|
} else if (char === ';' && !inUrl && !quoteMark) {
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
c += 1;
|
|
|
}
|
|
|
|
|
|
if (c > 0) {
|
|
|
value.push({
|
|
|
type: 'Text',
|
|
|
start: chunk.start,
|
|
|
end: chunk.start + c,
|
|
|
data: chunk.data.slice(0, c)
|
|
|
});
|
|
|
}
|
|
|
|
|
|
while (/[;\s]/.test(chunk.data[c])) c += 1;
|
|
|
const remainingData = chunk.data.slice(c);
|
|
|
|
|
|
if (remainingData) {
|
|
|
chunks.unshift({
|
|
|
start: chunk.start + c,
|
|
|
end: chunk.end,
|
|
|
type: 'Text',
|
|
|
data: remainingData
|
|
|
});
|
|
|
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
else {
|
|
|
value.push(chunk);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
chunks,
|
|
|
value
|
|
|
};
|
|
|
}
|
|
|
|
|
|
function isDynamic(value: Node[]) {
|
|
|
return value.length > 1 || value[0].type !== 'Text';
|
|
|
}
|