Library rewrite

This commit is contained in:
Niklas von Hertzen
2017-07-29 10:07:42 +08:00
committed by Niklas von Hertzen
parent 83e9b85e1e
commit 8a6fb5f733
70 changed files with 2521 additions and 3219 deletions

366
src/parsing/background.js Normal file
View File

@@ -0,0 +1,366 @@
/* @flow */
'use strict';
import type {Bounds} from '../Bounds';
import type ImageLoader from '../ImageLoader';
import Color from '../Color';
import Length from '../Length';
import Size from '../Size';
import Vector from '../Vector';
export type Background = {
backgroundImage: Array<BackgroundImage>,
backgroundColor: Color
};
export type BackgroundRepeat = $Values<typeof BACKGROUND_REPEAT>;
export type BackgroundSizeTypes = $Values<typeof BACKGROUND_SIZE>;
export type BackgroundSource = {
prefix: string,
method: string,
args: Array<string>
};
export type BackgroundImage = {
source: BackgroundSource,
position: [Length, Length],
size: [BackgroundSize, BackgroundSize],
repeat: BackgroundRepeat
};
export const BACKGROUND_REPEAT = {
REPEAT: 0,
NO_REPEAT: 1,
REPEAT_X: 2,
REPEAT_Y: 3
};
export const BACKGROUND_SIZE = {
AUTO: 0,
CONTAIN: 1,
COVER: 2,
LENGTH: 3
};
const AUTO = 'auto';
class BackgroundSize {
size: ?BackgroundSizeTypes;
value: ?Length;
constructor(size: string) {
switch (size) {
case 'contain':
this.size = BACKGROUND_SIZE.CONTAIN;
break;
case 'cover':
this.size = BACKGROUND_SIZE.COVER;
break;
case 'auto':
this.size = BACKGROUND_SIZE.AUTO;
break;
default:
this.value = new Length(size);
}
}
}
export const calculateBackgroundSize = (
backgroundImage: BackgroundImage,
image: HTMLImageElement,
bounds: Bounds
): Size => {
let width = 0;
let height = 0;
const size = backgroundImage.size;
if (size[0].size === BACKGROUND_SIZE.CONTAIN || size[0].size === BACKGROUND_SIZE.COVER) {
const targetRatio = bounds.width / bounds.height;
const currentRatio = image.width / image.height;
return targetRatio < currentRatio !== (size[0].size === BACKGROUND_SIZE.COVER)
? new Size(bounds.height * currentRatio, bounds.height)
: new Size(bounds.width, bounds.width / currentRatio);
}
if (size[0].value) {
width = size[0].value.getAbsoluteValue(bounds.width);
}
if (size[0].size === BACKGROUND_SIZE.AUTO && size[1].size === BACKGROUND_SIZE.AUTO) {
height = image.height;
} else if (size[1].size === BACKGROUND_SIZE.AUTO) {
height = width / image.width * image.height;
} else if (size[1].value) {
height = size[1].value.getAbsoluteValue(bounds.height);
}
if (size[0].size === BACKGROUND_SIZE.AUTO) {
width = height / image.height * image.width;
}
return new Size(width, height);
};
const AUTO_SIZE = new BackgroundSize(AUTO);
export const calculateBackgroundPosition = (
position: [Length, Length],
size: Size,
bounds: Bounds
): Vector => {
return new Vector(
position[0].getAbsoluteValue(bounds.width - size.width),
position[1].getAbsoluteValue(bounds.height - size.height)
);
};
export const calculateBackgroundRepeatPath = (
background: BackgroundImage,
position: Vector,
size: Size,
bounds: Bounds
) => {
const repeat = background.repeat;
switch (repeat) {
case BACKGROUND_REPEAT.REPEAT_X:
return [
new Vector(Math.round(bounds.left), Math.round(bounds.top + position.y)),
new Vector(
Math.round(bounds.left + bounds.width),
Math.round(bounds.top + position.y)
),
new Vector(
Math.round(bounds.left + bounds.width),
Math.round(size.height + bounds.top + position.y)
),
new Vector(
Math.round(bounds.left),
Math.round(size.height + bounds.top + position.y)
)
];
case BACKGROUND_REPEAT.REPEAT_Y:
return [
new Vector(Math.round(bounds.left + position.x), Math.round(bounds.top)),
new Vector(
Math.round(bounds.left + position.x + size.width),
Math.round(bounds.top)
),
new Vector(
Math.round(bounds.left + position.x + size.width),
Math.round(bounds.height + bounds.top)
),
new Vector(
Math.round(bounds.left + position.x),
Math.round(bounds.height + bounds.top)
)
];
case BACKGROUND_REPEAT.NO_REPEAT:
return [
new Vector(
Math.round(bounds.left + position.x),
Math.round(bounds.top + position.y)
),
new Vector(
Math.round(bounds.left + position.x + size.width),
Math.round(bounds.top + position.y)
),
new Vector(
Math.round(bounds.left + position.x + size.width),
Math.round(bounds.top + position.y + size.height)
),
new Vector(
Math.round(bounds.left + position.x),
Math.round(bounds.top + position.y + size.height)
)
];
default:
return [
new Vector(Math.round(bounds.left), Math.round(bounds.top)),
new Vector(Math.round(bounds.left + bounds.width), Math.round(bounds.top)),
new Vector(
Math.round(bounds.left + bounds.width),
Math.round(bounds.height + bounds.top)
),
new Vector(Math.round(bounds.left), Math.round(bounds.height + bounds.top))
];
}
};
export const parseBackground = (
style: CSSStyleDeclaration,
imageLoader: ImageLoader
): Background => {
return {
backgroundImage: parseBackgroundImages(style, imageLoader),
backgroundColor: new Color(style.backgroundColor)
};
};
const parseBackgroundRepeat = (backgroundRepeat: string): BackgroundRepeat => {
switch (backgroundRepeat) {
case 'no-repeat':
return BACKGROUND_REPEAT.NO_REPEAT;
case 'repeat-x':
case 'repeat no-repeat':
return BACKGROUND_REPEAT.REPEAT_X;
case 'repeat-y':
case 'no-repeat repeat':
return BACKGROUND_REPEAT.REPEAT_Y;
case 'repeat':
return BACKGROUND_REPEAT.REPEAT;
}
if (__DEV__) {
console.error(`Invalid background-repeat value "${backgroundRepeat}"`);
}
return BACKGROUND_REPEAT.REPEAT;
};
const parseBackgroundImages = (
style: CSSStyleDeclaration,
imageLoader: ImageLoader
): Array<BackgroundImage> => {
const sources: Array<BackgroundSource> = parseBackgroundImage(
style.backgroundImage,
imageLoader
);
const positions = style.backgroundPosition.split(',');
const repeats = style.backgroundRepeat.split(',');
const sizes = style.backgroundSize.split(',');
return sources.map((source, index) => {
const size = (sizes[index] || AUTO).trim().split(' ').map(parseBackgroundSize);
const position = (positions[index] || AUTO).trim().split(' ').map(parseBackgoundPosition);
return {
source,
repeat: parseBackgroundRepeat(repeats[index]),
size: size.length < 2 ? [size[0], AUTO_SIZE] : [size[0], size[1]],
position: position.length < 2 ? [position[0], position[0]] : [position[0], position[1]]
};
});
};
const parseBackgroundSize = (size: string): BackgroundSize =>
size === 'auto' ? AUTO_SIZE : new BackgroundSize(size);
const parseBackgoundPosition = (position: string): Length => {
switch (position) {
case 'bottom':
case 'right':
return new Length('100%');
case 'left':
case 'top':
return new Length('0%');
case 'auto':
return new Length('0');
}
return new Length(position);
};
const parseBackgroundImage = (image: string, imageLoader: ImageLoader): Array<BackgroundSource> => {
const whitespace = /^\s$/;
const results = [];
let args = [];
let method = '';
let quote = null;
let definition = '';
let mode = 0;
let numParen = 0;
const appendResult = () => {
let prefix = '';
if (method) {
if (definition.substr(0, 1) === '"') {
definition = definition.substr(1, definition.length - 2);
}
if (definition) {
args.push(definition);
}
const prefix_i = method.indexOf('-', 1) + 1;
if (method.substr(0, 1) === '-' && prefix_i > 0) {
prefix = method.substr(0, prefix_i).toLowerCase();
method = method.substr(prefix_i);
}
method = method.toLowerCase();
if (method === 'url') {
const key = imageLoader.loadImage(args[0]);
args = key ? [key] : [];
}
results.push({
prefix,
method,
args
});
}
args = [];
method = definition = '';
};
image.split('').forEach(c => {
if (mode === 0 && whitespace.test(c)) {
return;
}
switch (c) {
case '"':
if (!quote) {
quote = c;
} else if (quote === c) {
quote = null;
}
break;
case '(':
if (quote) {
break;
} else if (mode === 0) {
mode = 1;
return;
} else {
numParen++;
}
break;
case ')':
if (quote) {
break;
} else if (mode === 1) {
if (numParen === 0) {
mode = 0;
appendResult();
return;
} else {
numParen--;
}
}
break;
case ',':
if (quote) {
break;
} else if (mode === 0) {
appendResult();
return;
} else if (mode === 1) {
if (numParen === 0 && !method.match(/^url$/i)) {
args.push(definition);
definition = '';
return;
}
}
break;
}
if (mode === 0) {
method += c;
} else {
definition += c;
}
});
appendResult();
return results;
};

49
src/parsing/border.js Normal file
View File

@@ -0,0 +1,49 @@
/* @flow */
'use strict';
import Color from '../Color';
export const BORDER_STYLE = {
NONE: 0,
SOLID: 1
};
export type BorderStyle = $Values<typeof BORDER_STYLE>;
export type Border = {
borderColor: Color,
borderStyle: BorderStyle,
borderWidth: number
};
export const BORDER_SIDES = {
TOP: 0,
RIGHT: 1,
BOTTOM: 2,
LEFT: 3
};
export type BorderSide = $Values<typeof BORDER_SIDES>;
const SIDES = Object.keys(BORDER_SIDES).map(s => s.toLowerCase());
const parseBorderStyle = (style: string): BorderStyle => {
switch (style) {
case 'none':
return BORDER_STYLE.NONE;
}
return BORDER_STYLE.SOLID;
};
export const parseBorder = (style: CSSStyleDeclaration): Array<Border> => {
return SIDES.map(side => {
const borderColor = new Color(style.getPropertyValue(`border-${side}-color`));
const borderStyle = parseBorderStyle(style.getPropertyValue(`border-${side}-style`));
const borderWidth = parseInt(style.getPropertyValue(`border-${side}-width`), 10);
return {
borderColor,
borderStyle,
borderWidth: isNaN(borderWidth) ? 0 : borderWidth
};
});
};

View File

@@ -0,0 +1,15 @@
/* @flow */
'use strict';
import Length from '../Length';
const SIDES = ['top-left', 'top-right', 'bottom-right', 'bottom-left'];
export type BorderRadius = Array<Length>;
export const parseBorderRadius = (style: CSSStyleDeclaration): Array<BorderRadius> => {
return SIDES.map(side => {
const value = style.getPropertyValue(`border-${side}-radius`);
const [horizontal, vertical] = value.split(' ').map(Length.create);
return typeof vertical === 'undefined' ? [horizontal, horizontal] : [horizontal, vertical];
});
};

111
src/parsing/display.js Normal file
View File

@@ -0,0 +1,111 @@
/* @flow */
'use strict';
export const DISPLAY = {
NONE: 1 << 0,
BLOCK: 1 << 1,
INLINE: 1 << 2,
RUN_IN: 1 << 3,
FLOW: 1 << 4,
FLOW_ROOT: 1 << 5,
TABLE: 1 << 6,
FLEX: 1 << 7,
GRID: 1 << 8,
RUBY: 1 << 9,
SUBGRID: 1 << 10,
LIST_ITEM: 1 << 11,
TABLE_ROW_GROUP: 1 << 12,
TABLE_HEADER_GROUP: 1 << 13,
TABLE_FOOTER_GROUP: 1 << 14,
TABLE_ROW: 1 << 15,
TABLE_CELL: 1 << 16,
TABLE_COLUMN_GROUP: 1 << 17,
TABLE_COLUMN: 1 << 18,
TABLE_CAPTION: 1 << 19,
RUBY_BASE: 1 << 20,
RUBY_TEXT: 1 << 21,
RUBY_BASE_CONTAINER: 1 << 22,
RUBY_TEXT_CONTAINER: 1 << 23,
CONTENTS: 1 << 24,
INLINE_BLOCK: 1 << 25,
INLINE_LIST_ITEM: 1 << 26,
INLINE_TABLE: 1 << 27,
INLINE_FLEX: 1 << 28,
INLINE_GRID: 1 << 29
};
export type Display = $Values<typeof DISPLAY>;
export type DisplayBit = number;
const parseDisplayValue = (display: string): Display => {
switch (display) {
case 'block':
return DISPLAY.BLOCK;
case 'inline':
return DISPLAY.INLINE;
case 'run-in':
return DISPLAY.RUN_IN;
case 'flow':
return DISPLAY.FLOW;
case 'flow-root':
return DISPLAY.FLOW_ROOT;
case 'table':
return DISPLAY.TABLE;
case 'flex':
return DISPLAY.FLEX;
case 'grid':
return DISPLAY.GRID;
case 'ruby':
return DISPLAY.RUBY;
case 'subgrid':
return DISPLAY.SUBGRID;
case 'list-item':
return DISPLAY.LIST_ITEM;
case 'table-row-group':
return DISPLAY.TABLE_ROW_GROUP;
case 'table-header-group':
return DISPLAY.TABLE_HEADER_GROUP;
case 'table-footer-group':
return DISPLAY.TABLE_FOOTER_GROUP;
case 'table-row':
return DISPLAY.TABLE_ROW;
case 'table-cell':
return DISPLAY.TABLE_CELL;
case 'table-column-group':
return DISPLAY.TABLE_COLUMN_GROUP;
case 'table-column':
return DISPLAY.TABLE_COLUMN;
case 'table-caption':
return DISPLAY.TABLE_CAPTION;
case 'ruby-base':
return DISPLAY.RUBY_BASE;
case 'ruby-text':
return DISPLAY.RUBY_TEXT;
case 'ruby-base-container':
return DISPLAY.RUBY_BASE_CONTAINER;
case 'ruby-text-container':
return DISPLAY.RUBY_TEXT_CONTAINER;
case 'contents':
return DISPLAY.CONTENTS;
case 'inline-block':
return DISPLAY.INLINE_BLOCK;
case 'inline-list-item':
return DISPLAY.INLINE_LIST_ITEM;
case 'inline-table':
return DISPLAY.INLINE_TABLE;
case 'inline-flex':
return DISPLAY.INLINE_FLEX;
case 'inline-grid':
return DISPLAY.INLINE_GRID;
}
return DISPLAY.NONE;
};
const setDisplayBit = (bit: DisplayBit, display: string): DisplayBit => {
return bit | parseDisplayValue(display);
};
export const parseDisplay = (display: string): Display => {
return display.split(' ').reduce(setDisplayBit, 0);
};

26
src/parsing/float.js Normal file
View File

@@ -0,0 +1,26 @@
/* @flow */
'use strict';
export const FLOAT = {
NONE: 0,
LEFT: 1,
RIGHT: 2,
INLINE_START: 3,
INLINE_END: 4
};
export type Float = $Values<typeof FLOAT>;
export const parseCSSFloat = (float: string): Float => {
switch (float) {
case 'left':
return FLOAT.LEFT;
case 'right':
return FLOAT.RIGHT;
case 'inline-start':
return FLOAT.INLINE_START;
case 'inline-end':
return FLOAT.INLINE_END;
}
return FLOAT.NONE;
};

38
src/parsing/font.js Normal file
View File

@@ -0,0 +1,38 @@
/* @flow */
'use strict';
export type Font = {
fontFamily: string,
fontSize: string,
fontStyle: string,
fontVariant: string,
fontWeight: number
};
const parseFontWeight = (weight: string): number => {
switch (weight) {
case 'normal':
return 400;
case 'bold':
return 700;
}
const value = parseInt(weight, 10);
return isNaN(value) ? 400 : value;
};
export const parseFont = (style: CSSStyleDeclaration): Font => {
const fontFamily = style.fontFamily;
const fontSize = style.fontSize;
const fontStyle = style.fontStyle;
const fontVariant = style.fontVariant;
const fontWeight = parseFontWeight(style.fontWeight);
return {
fontFamily,
fontSize,
fontStyle,
fontVariant,
fontWeight
};
};

View File

@@ -0,0 +1,10 @@
/* @flow */
'use strict';
export const parseLetterSpacing = (letterSpacing: string): number => {
if (letterSpacing === 'normal') {
return 0;
}
const value = parseInt(letterSpacing, 10);
return isNaN(value) ? 0 : value;
};

11
src/parsing/padding.js Normal file
View File

@@ -0,0 +1,11 @@
/* @flow */
'use strict';
import Length from '../Length';
const SIDES = ['top', 'right', 'bottom', 'left'];
export type Padding = Array<Length>;
export const parsePadding = (style: CSSStyleDeclaration): Padding => {
return SIDES.map(side => new Length(style.getPropertyValue(`padding-${side}`)));
};

27
src/parsing/position.js Normal file
View File

@@ -0,0 +1,27 @@
/* @flow */
'use strict';
export const POSITION = {
STATIC: 0,
RELATIVE: 1,
ABSOLUTE: 2,
FIXED: 3,
STICKY: 4
};
export type Position = $Values<typeof POSITION>;
export const parsePosition = (position: string): Position => {
switch (position) {
case 'relative':
return POSITION.RELATIVE;
case 'absolute':
return POSITION.ABSOLUTE;
case 'fixed':
return POSITION.FIXED;
case 'sticky':
return POSITION.STICKY;
}
return POSITION.STATIC;
};

View File

@@ -0,0 +1,88 @@
/* @flow */
'use strict';
import Color from '../Color';
export const TEXT_DECORATION_STYLE = {
SOLID: 0,
DOUBLE: 1,
DOTTED: 2,
DASHED: 3,
WAVY: 4
};
export const TEXT_DECORATION = {
NONE: null
};
export const TEXT_DECORATION_LINE = {
UNDERLINE: 1,
OVERLINE: 2,
LINE_THROUGH: 3,
BLINK: 4
};
export type TextDecorationStyle = $Values<typeof TEXT_DECORATION_STYLE>;
export type TextDecorationLine = $Values<typeof TEXT_DECORATION_LINE>;
type TextDecorationLineType = Array<TextDecorationLine> | null;
export type TextDecoration =
| {
textDecorationLine: Array<TextDecorationLine>,
textDecorationStyle: TextDecorationStyle,
textDecorationColor: Color | null
}
| $Values<typeof TEXT_DECORATION>;
const parseLine = (line: string): TextDecorationLine => {
switch (line) {
case 'underline':
return TEXT_DECORATION_LINE.UNDERLINE;
case 'overline':
return TEXT_DECORATION_LINE.OVERLINE;
case 'line-through':
return TEXT_DECORATION_LINE.LINE_THROUGH;
}
return TEXT_DECORATION_LINE.BLINK;
};
const parseTextDecorationLine = (line: string): TextDecorationLineType => {
if (line === 'none') {
return null;
}
return line.split(' ').map(parseLine);
};
const parseTextDecorationStyle = (style: string): TextDecorationStyle => {
switch (style) {
case 'double':
return TEXT_DECORATION_STYLE.DOUBLE;
case 'dotted':
return TEXT_DECORATION_STYLE.DOTTED;
case 'dashed':
return TEXT_DECORATION_STYLE.DASHED;
case 'wavy':
return TEXT_DECORATION_STYLE.WAVY;
}
return TEXT_DECORATION_STYLE.SOLID;
};
export const parseTextDecoration = (style: CSSStyleDeclaration): TextDecoration => {
const textDecorationLine = parseTextDecorationLine(
style.textDecorationLine ? style.textDecorationLine : style.textDecoration
);
if (textDecorationLine === null) {
return TEXT_DECORATION.NONE;
}
const textDecorationColor = style.textDecorationColor
? new Color(style.textDecorationColor)
: null;
const textDecorationStyle = parseTextDecorationStyle(style.textDecorationStyle);
return {
textDecorationLine,
textDecorationColor,
textDecorationStyle
};
};

View File

@@ -0,0 +1,24 @@
/* @flow */
'use strict';
export const TEXT_TRANSFORM = {
NONE: 0,
LOWERCASE: 1,
UPPERCASE: 2,
CAPITALIZE: 3
};
export type TextTransform = $Values<typeof TEXT_TRANSFORM>;
export const parseTextTransform = (textTransform: string): TextTransform => {
switch (textTransform) {
case 'uppercase':
return TEXT_TRANSFORM.UPPERCASE;
case 'lowercase':
return TEXT_TRANSFORM.LOWERCASE;
case 'capitalize':
return TEXT_TRANSFORM.CAPITALIZE;
}
return TEXT_TRANSFORM.NONE;
};

49
src/parsing/transform.js Normal file
View File

@@ -0,0 +1,49 @@
/* @flow */
'use strict';
import Length from '../Length';
const toFloat = (s: string): number => parseFloat(s.trim());
export type Matrix = [number, number, number, number, number, number];
export type TransformOrigin = [Length, Length];
export type Transform = {
transform: Matrix,
transformOrigin: TransformOrigin
} | null;
const MATRIX = /(matrix|matrix3d)\((.+)\)/;
export const parseTransform = (style: CSSStyleDeclaration): Transform => {
// TODO get prefixed values
const transform = parseTransformMatrix(style.transform);
if (transform === null) {
return null;
}
return {
transform,
transformOrigin: parseTransformOrigin(style.transformOrigin)
};
};
const parseTransformOrigin = (origin: string): TransformOrigin => {
const values = origin.split(' ').map(Length.create);
return [values[0], values[1]];
};
const parseTransformMatrix = (transform: string): Matrix | null => {
if (transform === 'none') {
return null;
}
const match = transform.match(MATRIX);
if (match) {
if (match[1] === 'matrix') {
const matrix = match[2].split(',').map(toFloat);
return [matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5]];
} else {
const matrix3d = match[2].split(',').map(toFloat);
return [matrix3d[0], matrix3d[1], matrix3d[4], matrix3d[5], matrix3d[12], matrix3d[13]];
}
}
return null;
};

15
src/parsing/zIndex.js Normal file
View File

@@ -0,0 +1,15 @@
/* @flow */
'use strict';
export type zIndex = {
auto: boolean,
order: number
};
export const parseZIndex = (zIndex: string): zIndex => {
const auto = zIndex === 'auto';
return {
auto,
order: auto ? 0 : parseInt(zIndex, 10)
};
};