Skip to content

Commit

Permalink
SizeList object in preparation to support sizes attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
Dima Voytenko committed Oct 1, 2015
1 parent 4ad0a4e commit cc735dc
Show file tree
Hide file tree
Showing 4 changed files with 321 additions and 6 deletions.
8 changes: 4 additions & 4 deletions src/layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,10 @@ export function parseLength(s) {
if (!s) {
return undefined;
}
if (!/^\d+(px|em|rem|vh|vw|vmin|vmax)?$/.test(s)) {
if (!/^\d+(\.\d+)?(px|em|rem|vh|vw|vmin|vmax)?$/.test(s)) {
return undefined;
}
if (/^\d+$/.test(s)) {
if (/^\d+(\.\d+)?$/.test(s)) {
return s + 'px';
}
return s;
Expand All @@ -119,7 +119,7 @@ export function parseLength(s) {
* @return {!Length}
*/
export function assertLength(length) {
assert(/^\d+(px|em|rem|vh|vw|vmin|vmax)$/.test(length),
assert(/^\d+(\.\d+)?(px|em|rem|vh|vw|vmin|vmax)$/.test(length),
'Invalid length value: %s', length);
return length;
}
Expand All @@ -144,7 +144,7 @@ export function getLengthUnits(length) {
* @return {number}
*/
export function getLengthNumeral(length) {
return parseInt(length, 10);
return parseFloat(length, 10);
}


Expand Down
127 changes: 127 additions & 0 deletions src/size-list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* Copyright 2015 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {assert} from './asserts';
import {assertLength} from './layout';


/**
* A single option within a SizeList.
* @typedef {{
* mediaQuery: (string|undefined),
* size: (!Length)
* }}
*/
var SizeListOption;


/**
* Parses the text representation of "sizes" into SizeList object.
*
* There could be any number of size options within the SizeList. They are tried
* in the order they were defined. The final size option must not have "media"
* condition specified. All other size options must have "media" condition
* specified.
*
* See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#Attributes
* See http://www.w3.org/html/wg/drafts/html/master/semantics.html#attr-img-sizes
* @param {string} s
* @return {!SizeList}
*/
export function parseSizeList(s) {
let sSizes = s.split(',');
assert(sSizes.length > 0, 'sizes has to have at least one size');
let sizes = [];
sSizes.forEach((sSize) => {
sSize = sSize.replace(/\s+/g, ' ').trim();
if (sSize.length == 0) {
return;
}

let mediaStr;
let sizeStr;
let spaceIndex = sSize.lastIndexOf(' ');
if (spaceIndex != -1) {
mediaStr = sSize.substring(0, spaceIndex).trim();
sizeStr = sSize.substring(spaceIndex + 1).trim();
} else {
sizeStr = sSize;
mediaStr = undefined;
}
sizes.push({mediaQuery: mediaStr, size: assertLength(sizeStr)});
});
return new SizeList(sizes);
};


/**
* A SizeList object contains one or more sizes as typically seen in "sizes"
* attribute.
*
* See "select" method for details on how the size selection is performed.
*
* See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#Attributes
* See http://www.w3.org/html/wg/drafts/html/master/semantics.html#attr-img-sizes
*/
export class SizeList {
/**
* @param {!Array<!SizeListOption>} sizes
*/
constructor(sizes) {
assert(sizes.length > 0, 'SizeList must have at least one option');
/** @private @const {!Array<!SizeListOption>} */
this.sizes_ = sizes;

// All sources except for last must have a media query. The last one must
// not.
for (let i = 0; i < sizes.length; i++) {
let option = sizes[i];
if (i < sizes.length - 1) {
assert(option.mediaQuery,
'All options except for the last must have a media condition');
} else {
assert(!option.mediaQuery,
'The last option must not have a media condition');
}
}
}

/**
* Selects the first size that matches media conditions. If no options match,
* the last option is returned.
*
* See http://www.w3.org/html/wg/drafts/html/master/semantics.html#attr-img-sizes
* @param {!Window} win
* @return {!Length}
*/
select(win) {
for (let i = 0; i < this.sizes_.length - 1; i++) {
let option = this.sizes_[i];
if (win.matchMedia(option.mediaQuery).matches) {
return option.size;
}
}
return this.getLast();
}

/**
* Returns the last size in the SizeList, which is the default.
* @return {!Length}
*/
getLast() {
return this.sizes_[this.sizes_.length - 1].size;
}
}
47 changes: 45 additions & 2 deletions test/functional/test-layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
* limitations under the License.
*/

import {Layout, getLengthNumeral, getLengthUnits, parseLength, parseLayout} from
'../../src/layout';
import {Layout, assertLength, getLengthNumeral, getLengthUnits, parseLength,
parseLayout} from '../../src/layout';
import {applyLayout_} from '../../src/custom-element';


Expand Down Expand Up @@ -46,6 +46,13 @@ describe('Layout', () => {
expect(parseLength('10px')).to.equal('10px');
expect(parseLength('10em')).to.equal('10em');
expect(parseLength('10vmin')).to.equal('10vmin');

expect(parseLength(10.1)).to.equal('10.1px');
expect(parseLength('10.2')).to.equal('10.2px');
expect(parseLength('10.1px')).to.equal('10.1px');
expect(parseLength('10.1em')).to.equal('10.1em');
expect(parseLength('10.1vmin')).to.equal('10.1vmin');

expect(parseLength(undefined)).to.equal(undefined);
expect(parseLength(null)).to.equal(undefined);
expect(parseLength('')).to.equal(undefined);
Expand All @@ -55,15 +62,51 @@ describe('Layout', () => {
expect(getLengthUnits('10px')).to.equal('px');
expect(getLengthUnits('10em')).to.equal('em');
expect(getLengthUnits('10vmin')).to.equal('vmin');

expect(getLengthUnits('10.1px')).to.equal('px');
expect(getLengthUnits('10.1em')).to.equal('em');
expect(getLengthUnits('10.1vmin')).to.equal('vmin');
});

it('getLengthNumeral', () => {
expect(getLengthNumeral('10')).to.equal(10);
expect(getLengthNumeral('10px')).to.equal(10);
expect(getLengthNumeral('10em')).to.equal(10);
expect(getLengthNumeral('10vmin')).to.equal(10);

expect(getLengthNumeral('10.1')).to.equal(10.1);
expect(getLengthNumeral('10.1px')).to.equal(10.1);
expect(getLengthNumeral('10.1em')).to.equal(10.1);
expect(getLengthNumeral('10.1vmin')).to.equal(10.1);
});

it('assertLength', () => {
expect(assertLength('10px')).to.equal('10px');
expect(assertLength('10em')).to.equal('10em');
expect(assertLength('10vmin')).to.equal('10vmin');

expect(assertLength('10.1px')).to.equal('10.1px');
expect(assertLength('10.1em')).to.equal('10.1em');
expect(assertLength('10.1vmin')).to.equal('10.1vmin');

expect(function() {
assertLength(10);
}).to.throw(/Invalid length value/);
expect(function() {
assertLength('10');
}).to.throw(/Invalid length value/);
expect(function() {
assertLength(undefined);
}).to.throw(/Invalid length value/);
expect(function() {
assertLength(null);
}).to.throw(/Invalid length value/);
expect(function() {
assertLength('');
}).to.throw(/Invalid length value/);
});


it('layout=nodisplay', () => {
div.setAttribute('layout', 'nodisplay');
expect(applyLayout_(div)).to.equal(Layout.NODISPLAY);
Expand Down
145 changes: 145 additions & 0 deletions test/functional/test-size-list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* Copyright 2015 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {SizeList, parseSizeList} from '../../src/size-list';


describe('SizeList parseSizeList', () => {

it('should accept single option', () => {
var res = parseSizeList(' \n 111px \n ');
expect(res.sizes_.length).to.equal(1);
expect(res.sizes_[0].mediaQuery).to.equal(undefined);
expect(res.sizes_[0].size).to.equal('111px');
});

it('should accept multiple options', () => {
var res = parseSizeList(' \n print 222px \n, 111px \n');
expect(res.sizes_.length).to.equal(2);
expect(res.sizes_[0].mediaQuery).to.equal('print');
expect(res.sizes_[0].size).to.equal('222px');
expect(res.sizes_[1].mediaQuery).to.equal(undefined);
expect(res.sizes_[1].size).to.equal('111px');
});

it('should accept even more multiple options', () => {
var res = parseSizeList(' \n screen 333px, print 222px \n, 111px \n');
expect(res.sizes_.length).to.equal(3);
expect(res.sizes_[0].mediaQuery).to.equal('screen');
expect(res.sizes_[0].size).to.equal('333px');
expect(res.sizes_[1].mediaQuery).to.equal('print');
expect(res.sizes_[1].size).to.equal('222px');
expect(res.sizes_[2].mediaQuery).to.equal(undefined);
expect(res.sizes_[2].size).to.equal('111px');
});

it('should accept complicated media conditions', () => {
var res = parseSizeList(
' \n screen and (min-width: 1000px) \t ' +
' and (max-width: 2000px) 222px \n,' +
' 111px \n');
expect(res.sizes_.length).to.equal(2);
expect(res.sizes_[0].mediaQuery).to.equal(
'screen and (min-width: 1000px) and (max-width: 2000px)');
expect(res.sizes_[0].size).to.equal('222px');
expect(res.sizes_[1].mediaQuery).to.equal(undefined);
expect(res.sizes_[1].size).to.equal('111px');
});

it('should accept different length units', () => {
var res = parseSizeList(' \n 111vw \n ');
expect(res.sizes_.length).to.equal(1);
expect(res.sizes_[0].mediaQuery).to.equal(undefined);
expect(res.sizes_[0].size).to.equal('111vw');
});

it('should fail bad length', () => {
expect(() => {
parseSizeList(' \n 111 \n ');
}).to.throw(/Invalid length value/);
});
});


describe('SizeList construct', () => {

it('should have at least one option', () => {
expect(() => {
new SizeList([]);
}).to.throw(/SizeList must have at least one option/);
});

it('the last option must not have a query', () => {
expect(() => {
new SizeList([{mediaQuery: 'screen', size: '111px'}]);
}).to.throw(/The last option must not have a media condition/);
expect(() => {
new SizeList([{mediaQuery: 'print', size: '222px'},
{mediaQuery: 'screen', size: '111px'}]);
}).to.throw(/The last option must not have a media condition/);
});

it('non-last options must have media query', () => {
expect(() => {
new SizeList([{size: '222px'}, {size: '111px'}]);
}).to.throw(/All options except for the last must have a media condition/);
});
});


describe('SizeList select', () => {
it('should select default last option', () => {
let sizeList = new SizeList([
{mediaQuery: 'media1', size: '444px'},
{mediaQuery: 'media2', size: '333px'},
{mediaQuery: 'media3', size: '222px'},
{size: '111px'}
]);
expect(sizeList.select({matchMedia: () => {
return {};
}})).to.equal('111px');
});

it('should select a matching option', () => {
let sizeList = new SizeList([
{mediaQuery: 'media1', size: '444px'},
{mediaQuery: 'media2', size: '333px'},
{mediaQuery: 'media3', size: '222px'},
{size: '111px'}
]);
expect(sizeList.select({matchMedia: (mq) => {
if (mq == 'media2') {
return {matches: true};
}
return {};
}})).to.equal('333px');
});

it('should select first matching option', () => {
let sizeList = new SizeList([
{mediaQuery: 'media1', size: '444px'},
{mediaQuery: 'media2', size: '333px'},
{mediaQuery: 'media3', size: '222px'},
{size: '111px'}
]);
expect(sizeList.select({matchMedia: (mq) => {
if (mq == 'media1' || mq == 'media2') {
return {matches: true};
}
return {};
}})).to.equal('444px');
});
});

0 comments on commit cc735dc

Please sign in to comment.