• 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  if (deviceType[1] === 'default') {
393    return mediaStatus['device-type'] === 'phone';
394  } else {
395    return deviceType[1] === mediaStatus['device-type'];
396  }
397}
398
399/**
400 * Parse screen shape condition, such as: (round-screen: true).
401 * @param {string} condition - Screen shape condition.
402 * @param {Object} mediaStatus - Device info.
403 * @param {FailReason} failReason - Parse fail reason.
404 * @return {boolean}
405 */
406function parseScreenShapeCondition(condition: string, mediaStatus: object, failReason: FailReason): boolean {
407  const shape = condition.match(/[a-z-]+/g);
408  if (!shape || shape.length !== 2) {
409    failReason.type = MEDIAERROR.SYNTAX;
410    return false;
411  }
412  return shape[1] === mediaStatus['round-screen'].toString();
413}
414
415/**
416 * parse dark mode condition, such as: (dark-mode: true)
417 * @param {String} condition: dark condition
418 * @param {Object} mediaStatus: device info
419 * @param {Object} failReason: parse fail reason
420 */
421function parseDarkModeCondition(condition: string, mediaStatus: object, failReason: FailReason): boolean {
422  const darkMode = condition.match(/[a-z-]+/g);
423  if (!darkMode || darkMode.length !== 2) {
424    failReason.type = MEDIAERROR.SYNTAX;
425    return false;
426  }
427  return darkMode[1] === mediaStatus['dark-mode'].toString();
428}
429
430/**
431 * parse aspect ratio condition, such as: (aspect-ratio: 8/3)
432 * @param {String} condition: (device)?-aspect-ratio condition
433 * @param {Object} mediaStatus: aspect-ratio, device-width, device-height
434 * @param {Object} failReason: parse fail reason
435 */
436function parseAspectRatioCondition(condition: string, mediaStatus: object, failReason: FailReason): boolean {
437  let conditionValue;
438  const aspectRatio = condition.match(/[a-z-\d-\/]+/g);
439  let relationship;
440  if (aspectRatio[0].match(/^(max-)/)) {
441    relationship = '<=';
442  } else if (aspectRatio[0].match(/^(min-)/)) {
443    relationship = '>=';
444  } else {
445    relationship = '==';
446  }
447  let statusValue;
448  if (aspectRatio[0].match(/device/)) {
449    Log.info('query device status');
450    statusValue = mediaStatus['device-width'] / mediaStatus['device-height'];
451  } else {
452    Log.info('query page status');
453    statusValue = mediaStatus['aspect-ratio'];
454  }
455  const numbers = aspectRatio[1].split('/');
456  if (numbers.length === 2) {
457    conditionValue = parseInt(numbers[0]) / parseInt(numbers[1]);
458  } else {
459    failReason.type = MEDIAERROR.SYNTAX;
460    return false;
461  }
462  return calculateExpression(statusValue, relationship, conditionValue, failReason);
463}
464
465/**
466 * Transfer unit the same with condition value unit.
467 * @param {number} value - Device value should be transfer unit the same with condition value.
468 * @param {string} unit - Condition value unit, such as: dpi/dpcm/dppx.
469 * @return {number}
470 */
471function transferValue(value: number, unit: string): number {
472  let transfer: number;
473  switch (unit) {
474    case 'dpi':
475      transfer = 96;
476      break;
477    case 'dpcm':
478      transfer = 36;
479      break;
480    default:
481      transfer = 1;
482  }
483  return value * transfer;
484}
485
486/**
487 * Calculate expression result.
488 * @param {number|string} leftValue - Number device value. String condition value.
489 * @param {string} relationship - >=/>/<=/<
490 * @param {number|string} rightValue - Number device value. String condition value.
491 * @param {FailReason} failReason - Parse fail reason.
492 * @return {boolean}
493 */
494function calculateExpression(leftValue: number | string, relationship: string,
495  rightValue: number | string, failReason: FailReason): boolean {
496  let lvalue: number | string;
497  let rvalue: number | string;
498  if (typeof leftValue === 'string') {
499    lvalue = leftValue.match(/[\d]+\.[\d]+/) ? parseFloat(leftValue) : parseInt(leftValue);
500    rvalue = rightValue;
501  } else if (typeof rightValue === 'string') {
502    lvalue = leftValue;
503    rvalue = rightValue.match(/[\d]+\.[\d]+/) ? parseFloat(rightValue) : parseInt(rightValue);
504  } else if (typeof rightValue === 'number') {
505    lvalue = leftValue;
506    rvalue = rightValue;
507  } else {
508    failReason.type = MEDIAERROR.SYNTAX;
509    return false;
510  }
511  switch (relationship) {
512    case '>=':
513      return lvalue >= rvalue;
514    case '>':
515      return lvalue > rvalue;
516    case '<=':
517      return lvalue <= rvalue;
518    case '<':
519      return lvalue < rvalue;
520    case '==':
521      return lvalue === rvalue;
522    default:
523      failReason.type = MEDIAERROR.SYNTAX;
524  }
525  return false;
526}
527