• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2021 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16import { Log } from '../../../utils/index';
17
18const MEDIA_QUERY_RULE = {
19  CONDITION_WITH_SCREEN: /^(((only|not)screen)|screen)((and|or|,)\([\w\/\.:><=-]+\))*$/,
20  CONDITION_WITHOUT_SCREEN: /^\([\w\/\.:><=-]+\)((and|or|,)\([\w\/\.:><=-]+\))*$/,
21  CONDITION_WITH_AND: /^\([\/\.a-z0-9:>=<-]+\)(and\([\/\.a-z0-9:>=<-]+\))+/,
22  CSS_LEVEL4_MULTI: /^\(([\d\.]+(dpi|dppx|dpcm|px)?)(>|<|>=|<=)[a-z0-9:-]+(>|<|>=|<=)([\d\.]+(dpi|dppx|dpcm|px)?)\)$/,
23  CSS_LEVEL4_LEFT: /^\([^m][a-z-]+(>|<|>=|<=)[\d\.]+(dpi|dppx|dpcm|px)?\)$/,
24  CSS_LEVEL4_RIGHT: /^\([\d\.]+(dpi|dppx|dpcm|px)?(>|<|>=|<=)[^m][a-z-]+\)$/,
25  CSS_LEVEL3_RULE: /^\((min|max)-[a-z-]+:[\d\.]+(dpi|dppx|dpcm|px)?\)$/,
26  ORIENTATION_RULE: /^\(orientation:[a-z]+\)/,
27  DEVICETYPE_RULE: /^\(device-type:[a-z]+\)/,
28  SCREEN_SHAPE_RULE: /^\(round-screen:[a-z]+\)/,
29  DARK_MODE: /^\(dark-mode:[a-z]+\)/,
30  ASPECT_RATIO: /^\((min|max)?-?(device)?-?aspect-ratio:[\d(\/)?(\d)*]+\)/,
31  PATTERN: /^\(pattern:[a-z]+\)/
32};
33
34/**
35 * Enum for MEDIA ERROR.
36 * @enum {string}
37 * @readonly
38 */
39/* eslint-disable no-unused-vars */
40enum MEDIAERROR {
41  /**
42   * SYNTAX Type
43   */
44  SYNTAX = 'SYNTAX',
45  /**
46   * NONE Type
47   */
48  NONE = 'NONE',
49}
50/* eslint-enable no-unused-vars */
51
52interface MediaMatchInfo {
53  status: object;
54  result: boolean;
55}
56
57const queryHistoryList: Map<string, MediaMatchInfo> = new Map();
58
59/**
60 * Match media query condition.
61 * @param {string} condition - Media query condition.
62 * @param {Object} mediaStatus - The device information.
63 * @param {boolean} jsQuery
64 * @return {boolean}
65 */
66export function matchMediaQueryCondition(condition: string, mediaStatus: object, jsQuery: boolean): boolean {
67  if (!condition || !mediaStatus) {
68    return false;
69  }
70
71  // If width and height are not initialized, and the query condition includes 'width' or 'height',
72  // return false directly.
73  if (mediaStatus['width'] === 0 && (condition.includes('width') || condition.includes('height'))) {
74    return false;
75  }
76  if (jsQuery && queryHistoryList.has(condition)) {
77    const queryHistory: MediaMatchInfo = queryHistoryList.get(condition);
78    if (queryHistory && JSON.stringify(queryHistory.status) === JSON.stringify(mediaStatus)) {
79      return queryHistory.result;
80    }
81  }
82  const result: boolean = doMatchMediaQueryCondition(condition, mediaStatus);
83  queryHistoryList.set(condition, {status: mediaStatus, result: result});
84  return result;
85}
86
87interface FailReason {
88  type: MEDIAERROR;
89}
90
91/**
92 * Match media query condition.
93 * @param {string} condition - Media query condition.
94 * @param {Object} mediaStatus - The device information.
95 * @return {boolean}
96 */
97function doMatchMediaQueryCondition(condition: string, mediaStatus: object): boolean {
98  const noSpace: string = condition.replace(/\s*/g, '');
99  let inverse: boolean = false;
100  const failReason: FailReason = { type: MEDIAERROR.NONE };
101  let noScreen: string;
102
103  // Check if the media query condition is legal.
104  if (MEDIA_QUERY_RULE.CONDITION_WITH_SCREEN.exec(noSpace)) {
105    if (noSpace.indexOf('notscreen') !== -1) {
106      inverse = true;
107    }
108    const screenPatt: RegExp = /screen[^and:]/g;
109    if (screenPatt.exec(noSpace)) {
110      return !inverse;
111    }
112    noScreen = noSpace.replace(/^(only|not)?screen(and)?/g, '');
113    if (!noScreen) {
114      return !inverse;
115    }
116  } else if (MEDIA_QUERY_RULE.CONDITION_WITHOUT_SCREEN.exec(noSpace)) {
117    noScreen = noSpace;
118  } else {
119    Log.debug('Illegal condition.');
120    failReason.type = MEDIAERROR.SYNTAX;
121    return false;
122  }
123
124  // Replace 'or' with comma ','.
125  const commaCondition: string = noScreen.replace(/or[(]/g, ',(');
126
127  // Remove screen and modifier.
128  const conditionArr: string[] = commaCondition.split(',');
129  const len: number = conditionArr.length;
130  for (let i = 0; i < len; i++) {
131    if (MEDIA_QUERY_RULE.CONDITION_WITH_AND.exec(conditionArr[i])) {
132      const result: boolean = parseAndCondtion(conditionArr[i], mediaStatus, failReason);
133      if (failReason.type === MEDIAERROR.SYNTAX) {
134        return false;
135      }
136      if (i + 1 === len) {
137        return inverse && !result || !inverse && result;
138      }
139    } else {
140      if (parseSingleCondition(conditionArr[i], mediaStatus, failReason)) {
141        return !inverse;
142      }
143      if (failReason.type === MEDIAERROR.SYNTAX) {
144        return false;
145      }
146    }
147  }
148  return inverse;
149}
150
151/**
152 * Parse single condition, such as: (100 < width).
153 * @param {string} condition - Single condition.
154 * @param {Object} mediaStatus - Device info.
155 * @param {FailReason} failReason - Parse fail reason.
156 * @return {boolean}
157 */
158function parseSingleCondition(condition: string, mediaStatus: object, failReason: FailReason): boolean {
159  if (MEDIA_QUERY_RULE.CSS_LEVEL4_MULTI.exec(condition)) {
160    if (parseCss4MultiCondition(condition, mediaStatus, failReason)) {
161      return true;
162    }
163  } else if (MEDIA_QUERY_RULE.CSS_LEVEL4_LEFT.exec(condition)) {
164    if (parseCss4LeftCondtion(condition, mediaStatus, failReason)) {
165      return true;
166    }
167  } else if (MEDIA_QUERY_RULE.CSS_LEVEL4_RIGHT.exec(condition)) {
168    if (parseCss4RightCondition(condition, mediaStatus, failReason)) {
169      return true;
170    }
171  } else if (MEDIA_QUERY_RULE.CSS_LEVEL3_RULE.exec(condition)) {
172    if (parseCss3Condition(condition, mediaStatus, failReason)) {
173      return true;
174    }
175  } else if (MEDIA_QUERY_RULE.DEVICETYPE_RULE.exec(condition)) {
176    if (parseDeviceTypeCondition(condition, mediaStatus, failReason)) {
177      return true;
178    }
179  } else if (MEDIA_QUERY_RULE.ORIENTATION_RULE.exec(condition)) {
180    if (parseOrientationCondition(condition, mediaStatus, failReason)) {
181      return true;
182    }
183  } else if (MEDIA_QUERY_RULE.SCREEN_SHAPE_RULE.exec(condition)) {
184    if (parseScreenShapeCondition(condition, mediaStatus, failReason)) {
185      return true;
186    }
187  } else if (MEDIA_QUERY_RULE.DARK_MODE.exec(condition)) {
188    if (parseDarkModeCondition(condition, mediaStatus, failReason)) {
189      return true;
190    }
191  } else if (MEDIA_QUERY_RULE.ASPECT_RATIO.exec(condition)) {
192    if (parseAspectRatioCondition(condition, mediaStatus, failReason)) {
193      return true;
194    }
195  } else if (MEDIA_QUERY_RULE.PATTERN.exec(condition)) {
196    if (parsePatternCondition(condition, mediaStatus, failReason)) {
197      return true;
198    }
199  } else {
200    Log.debug('Illegal condition');
201    failReason.type = MEDIAERROR.SYNTAX;
202    return false;
203  }
204  return false;
205}
206
207/**
208 * Parse conditions connect with 'and', such as: (100 < width) and (width < 1000).
209 * @param {string} condition - Conditions connect with 'and'.
210 * @param {Object} mediaStatus - Device info.
211 * @param {FailReason} failReason - Parse fail reason.
212 * @return {boolean}
213 */
214function parseAndCondtion(condition: string, mediaStatus: object, failReason: FailReason): boolean {
215  // Split and condition to simple conditions.
216  const noAnd: string = condition.replace(/and[^a-z]/g, ',(');
217  const conditionArr: string[] = noAnd.split(',');
218  if (!conditionArr) {
219    failReason.type = MEDIAERROR.SYNTAX;
220    return false;
221  }
222  for (let i = 0; i < conditionArr.length; i++) {
223    if (!parseSingleCondition(conditionArr[i], mediaStatus, failReason)) {
224      return false;
225    }
226  }
227  return true;
228}
229
230/**
231 * Parse css4 multi-style condition, such as: (100 < width < 1000).
232 * @param {string} condition - Css4 multi-style condition.
233 * @param {Object} mediaStatus - Device info.
234 * @param {FailReason} failReason - Parse fail reason.
235 * @return {boolean}
236 */
237function parseCss4MultiCondition(condition:string, mediaStatus: object, failReason: FailReason): boolean {
238  const patt: RegExp = /([a-z-]+|[\d.a-z]+|[><=]+)/g;
239  const feature = condition.match(patt);
240  if (!feature || feature.length !== 5) {
241    failReason.type = MEDIAERROR.SYNTAX;
242    return false;
243  }
244  const rcondition: string = '(' + feature[0] + feature[1] + feature[2] + ')';
245  const lcondition: string = '(' + feature[2] + feature[3] + feature[4] + ')';
246
247  return parseCss4RightCondition(rcondition, mediaStatus, failReason) &&
248        parseCss4LeftCondtion(lcondition, mediaStatus, failReason);
249}
250
251/**
252 * Parse css4 style condition, device info is in the left, such as: (width < 1000).
253 * @param {string} condition - Css4 style condition.
254 * @param {Object} mediaStatus - Device info.
255 * @param {FailReason} failReason - Parse fail reason.
256 * @return {boolean}
257 */
258function parseCss4LeftCondtion(condition: string, mediaStatus: object, failReason: FailReason): boolean {
259  const feature = condition.match(/[a-z-]+|[0-9.]+/g);
260  if (!feature || feature.length < 2) {
261    failReason.type = MEDIAERROR.SYNTAX;
262    return false;
263  }
264  const conditionValue: string = feature[1];
265  const unit: string = feature.length === 3 ? feature[2] : '';
266  const relationship = condition.match(/[><=]+/g);
267  const statusValue: number = transferValue(mediaStatus[feature[0]], unit);
268  return calculateExpression(statusValue, relationship[0], conditionValue, failReason);
269}
270
271/**
272 * Parse css4 style condition, device info is in the right, such as: (1000 < width).
273 * @param {string} condition - Css4 style condition.
274 * @param {Object} mediaStatus - Device info.
275 * @param {FailReason} failReason - Parse fail reason.
276 * @return {boolean}
277 */
278function parseCss4RightCondition(condition: string, mediaStatus: object, failReason: FailReason): boolean {
279  const feature = condition.match(/[a-z-]+|[0-9.]+/g);
280  if (!feature || feature.length < 2) {
281    failReason.type = MEDIAERROR.SYNTAX;
282    return false;
283  }
284  const conditionValue: string = feature[0];
285  let statusValue: number;
286  let unit: string;
287  if (feature.length === 3) {
288    unit = feature[1];
289    statusValue = transferValue(mediaStatus[feature[2]], unit);
290  } else {
291    unit = '';
292    statusValue = transferValue(mediaStatus[feature[1]], unit);
293  }
294  const relationship = condition.match(/[><=]+/g);
295  return calculateExpression(conditionValue, relationship[0], statusValue, failReason);
296}
297
298/**
299 * Parse css3 style condition, such as: (min-width: 1000).
300 * @param {String} condition - Css3 style condition.
301 * @param {Object} mediaStatus - Device info.
302 * @param {FailReason} failReason - Parse fail reason.
303 * @return {boolean}
304 */
305function parseCss3Condition(condition: string, mediaStatus: object, failReason: FailReason): boolean {
306  const feature = condition.match(/[a-z-]+|[0-9.]+/g);
307  if (!feature || feature.length < 2) {
308    failReason.type = MEDIAERROR.SYNTAX;
309    return false;
310  }
311  const conditionValue: string = feature[1];
312  const unit: string = feature.length === 3 ? feature[2] : '';
313  let relationship: string;
314  if (feature[0].match(/^(max-)/)) {
315    relationship = '<=';
316  } else if (feature[0].match(/^(min-)/)) {
317    relationship = '>=';
318  } else {
319    failReason.type = MEDIAERROR.SYNTAX;
320    return false;
321  }
322  const status: string = feature[0].replace(/(max|min)-/g, '');
323  const statusValue: number = transferValue(mediaStatus[status], unit);
324  return calculateExpression(statusValue, relationship, conditionValue, failReason);
325}
326
327/**
328 * Parse paatern style condition, such as: (pattern: normal).
329 * @param {String} condition - pattern style condition.
330 * @param {Object} mediaStatus - Device info.
331 * @param {FailReason} failReason - Parse fail reason.
332 * @return {boolean}
333 */
334function parsePatternCondition(condition: string, mediaStatus: object, failReason: FailReason): boolean {
335  const pattern = condition.match(/[a-z-]+/g);
336  if (!pattern || pattern.length !== 2) {
337    failReason.type = MEDIAERROR.SYNTAX;
338    return false;
339  }
340  return getVpType(mediaStatus['resolution'], mediaStatus['width']) === pattern[1];
341}
342
343/**
344 * get the type of vp.
345 * @param {number} resolution - the resolution of device.
346 * @param {number} width - the width of view page.
347 */
348function getVpType(resolution: number, width: number): string {
349  const value = width / resolution;
350  if (value > 0 && value < 320) {
351    return 'small';
352  } else if (value >= 320 && value < 600) {
353    return 'normal';
354  } else if (value >= 600 && value < 840) {
355    return 'large';
356  } else if (value >= 840) {
357    return 'xLarge';
358  } else {
359    return '';
360  }
361}
362
363/**
364 * Parse screen orientation condition, such as: (orientation: portrait).
365 * @param {string} condition - Orientation type condition.
366 * @param {Object} mediaStatus - Device info.
367 * @param {FailReason} failReason - Parse fail reason.
368 * @return {boolean}
369 */
370function parseOrientationCondition(condition: string, mediaStatus: object, failReason: FailReason): boolean {
371  const orientaton = condition.match(/[a-z-]+/g);
372  if (!orientaton || orientaton.length !== 2) {
373    failReason.type = MEDIAERROR.SYNTAX;
374    return false;
375  }
376  return orientaton[1] === mediaStatus['orientation'];
377}
378
379/**
380 * Parse device type condition, such as: (device-type: tv).
381 * @param {string} condition - Device type condition.
382 * @param {Object} mediaStatus - Device info.
383 * @param {FailReason} failReason - Parse fail reason.
384 * @return {boolean}
385 */
386function parseDeviceTypeCondition(condition: string, mediaStatus: object, failReason: FailReason): boolean {
387  const deviceType = condition.match(/[a-z-]+/g);
388  if (!deviceType || deviceType.length !== 2) {
389    failReason.type = MEDIAERROR.SYNTAX;
390    return false;
391  }
392  return deviceType[1] === mediaStatus['device-type'];
393}
394
395/**
396 * Parse screen shape condition, such as: (round-screen: true).
397 * @param {string} condition - Screen shape condition.
398 * @param {Object} mediaStatus - Device info.
399 * @param {FailReason} failReason - Parse fail reason.
400 * @return {boolean}
401 */
402function parseScreenShapeCondition(condition: string, mediaStatus: object, failReason: FailReason): boolean {
403  const shape = condition.match(/[a-z-]+/g);
404  if (!shape || shape.length !== 2) {
405    failReason.type = MEDIAERROR.SYNTAX;
406    return false;
407  }
408  return shape[1] === mediaStatus['round-screen'].toString();
409}
410
411/**
412 * parse dark mode condition, such as: (dark-mode: true)
413 * @param {String} condition: dark condition
414 * @param {Object} mediaStatus: device info
415 * @param {Object} failReason: parse fail reason
416 */
417function parseDarkModeCondition(condition: string, mediaStatus: object, failReason: FailReason): boolean {
418  const darkMode = condition.match(/[a-z-]+/g);
419  if (!darkMode || darkMode.length !== 2) {
420    failReason.type = MEDIAERROR.SYNTAX;
421    return false;
422  }
423  return darkMode[1] === mediaStatus['dark-mode'].toString();
424}
425
426/**
427 * parse aspect ratio condition, such as: (aspect-ratio: 8/3)
428 * @param {String} condition: (device)?-aspect-ratio condition
429 * @param {Object} mediaStatus: aspect-ratio, device-width, device-height
430 * @param {Object} failReason: parse fail reason
431 */
432function parseAspectRatioCondition(condition: string, mediaStatus: object, failReason: FailReason): boolean {
433  let conditionValue;
434  const aspectRatio = condition.match(/[a-z-\d-\/]+/g);
435  let relationship;
436  if (aspectRatio[0].match(/^(max-)/)) {
437    relationship = '<=';
438  } else if (aspectRatio[0].match(/^(min-)/)) {
439    relationship = '>=';
440  } else {
441    relationship = '==';
442  }
443  let statusValue;
444  if (aspectRatio[0].match(/device/)) {
445    Log.info('query device status');
446    statusValue = mediaStatus['device-width'] / mediaStatus['device-height'];
447  } else {
448    Log.info('query page status');
449    statusValue = mediaStatus['aspect-ratio'];
450  }
451  const numbers = aspectRatio[1].split('/');
452  if (numbers.length === 2) {
453    conditionValue = parseInt(numbers[0]) / parseInt(numbers[1]);
454  } else {
455    failReason.type = MEDIAERROR.SYNTAX;
456    return false;
457  }
458  return calculateExpression(statusValue, relationship, conditionValue, failReason);
459}
460
461/**
462 * Transfer unit the same with condition value unit.
463 * @param {number} value - Device value should be transfer unit the same with condition value.
464 * @param {string} unit - Condition value unit, such as: dpi/dpcm/dppx.
465 * @return {number}
466 */
467function transferValue(value: number, unit: string): number {
468  let transfer: number;
469  switch (unit) {
470    case 'dpi':
471      transfer = 96;
472      break;
473    case 'dpcm':
474      transfer = 36;
475      break;
476    default:
477      transfer = 1;
478  }
479  return value * transfer;
480}
481
482/**
483 * Calculate expression result.
484 * @param {number|string} leftValue - Number device value. String condition value.
485 * @param {string} relationship - >=/>/<=/<
486 * @param {number|string} rightValue - Number device value. String condition value.
487 * @param {FailReason} failReason - Parse fail reason.
488 * @return {boolean}
489 */
490function calculateExpression(leftValue: number | string, relationship: string,
491  rightValue: number | string, failReason: FailReason): boolean {
492  let lvalue: number | string;
493  let rvalue: number | string;
494  if (typeof leftValue === 'string') {
495    lvalue = leftValue.match(/[\d]+\.[\d]+/) ? parseFloat(leftValue) : parseInt(leftValue);
496    rvalue = rightValue;
497  } else if (typeof rightValue === 'string') {
498    lvalue = leftValue;
499    rvalue = rightValue.match(/[\d]+\.[\d]+/) ? parseFloat(rightValue) : parseInt(rightValue);
500  } else if (typeof rightValue === 'number') {
501    lvalue = leftValue;
502    rvalue = rightValue;
503  } else {
504    failReason.type = MEDIAERROR.SYNTAX;
505    return false;
506  }
507  switch (relationship) {
508    case '>=':
509      return lvalue >= rvalue;
510    case '>':
511      return lvalue > rvalue;
512    case '<=':
513      return lvalue <= rvalue;
514    case '<':
515      return lvalue < rvalue;
516    case '==':
517      return lvalue === rvalue;
518    default:
519      failReason.type = MEDIAERROR.SYNTAX;
520  }
521  return false;
522}
523