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