• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict'
2
3var css = require('css')
4var util = require('./lib/util')
5var validateItem = require('./lib/validator').validate
6var fs = require('fs')
7var path = require('path')
8
9var SELECTOR_MATCHER = /^[\.#]?[A-Za-z0-9_\-:]+$/
10var DESCENDANT_SELECTOR_MATCHER = /^([.#]?[A-Za-z0-9_-]+(\s+|\s*>\s*))+([.#]?[A-Za-z0-9_\-:]+)$/
11var IMPORT_MATCHER = /(['"]([^()]+?)['"])|(['"]([^()]+?)['"]\s+(only|not)?\s?(screen)?\s?((and|or|,|not|landscape)?\s?[(]([^()])+[)]\s*)+)/g
12var LENGTH_REGEXP = /^[-+]?\d*\.?\d+(\S*)$/
13const CARD_SELECTOR = /^[\.#][A-Za-z0-9_\-]+$/
14const card = process.env.DEVICE_LEVEL === 'card'
15
16/**
17 * expand margin、padding、border、borderWidth、borderColor、borderStyle properties、animation
18 *
19 * @param {object} subResult
20 * @param {String} camelCasedName
21 * @param {object} ruleResult
22 */
23function expand (subResult, camelCasedName, ruleResult) {
24  if (camelCasedName === 'border') {
25    subResult.value.forEach(function (item) {
26      if (item.type === 'Width' || item.type === 'Color' || item.type === 'Style') {
27        const spliceName = [camelCasedName + 'Top' + item.type, camelCasedName + 'Right' + item.type, camelCasedName +
28          'Bottom' + item.type, camelCasedName + 'Left' + item.type]
29        util.splitAttr(ruleResult, item.value, spliceName)
30      }
31      else {
32        ruleResult[camelCasedName + item.type] = item.value
33      }
34    })
35  }
36  else if (['borderTop', 'borderRight', 'borderBottom', 'borderLeft'].includes(camelCasedName)) {
37    subResult.value.forEach(function (item) {
38      ruleResult[camelCasedName + item.type] = item.value
39    })
40  }
41  else if (camelCasedName === 'margin' || camelCasedName === 'padding') {
42    const spliceName = [camelCasedName + 'Top', camelCasedName + 'Right', camelCasedName + 'Bottom', camelCasedName + 'Left']
43    util.splitAttr(ruleResult, subResult.value, spliceName)
44  }
45  else if (camelCasedName === 'borderWidth') {
46    util.splitAttr(ruleResult, subResult.value, ['borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth'])
47  }
48  else if (camelCasedName === 'borderColor') {
49    util.splitAttr(ruleResult, subResult.value, ['borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor'])
50  }
51  else if (camelCasedName === 'borderStyle') {
52    util.splitAttr(ruleResult, subResult.value, ['borderTopStyle', 'borderRightStyle', 'borderBottomStyle', 'borderLeftStyle'])
53  }
54  else if (camelCasedName === 'borderRadius') {
55    util.splitAttr(ruleResult, subResult.value, ['borderBottomLeftRadius', 'borderBottomRightRadius', 'borderTopLeftRadius', 'borderTopRightRadius'])
56  }
57  else if (camelCasedName === 'gridGap') {
58    util.splitAttr(ruleResult, subResult.value, ['gridRowsGap', 'gridColumnsGap'])
59  }
60  else if (camelCasedName === 'boxShadow') {
61    subResult.value.forEach(function (item) {
62      if (item.type === 'H' || item.type === 'V' || item.type === 'Blur' || item.type === 'Spread' ||
63        item.type === 'Color') {
64        util.splitAttr(ruleResult, item.value, [camelCasedName + item.type])
65      }
66    })
67  }
68  else if (camelCasedName === 'animation') {
69    Object.assign(ruleResult, subResult.value);
70  }
71  else {
72    // never to do
73  }
74}
75
76/**
77 * expand flex style
78 *
79 * @param {object} rule
80 * @param {Array} ruleLog
81 */
82function flexExpand(rule, ruleLog) {
83  for (let i = 0; i < rule.declarations.length; i++) {
84    let declaration = rule.declarations[i]
85    if (declaration.property === 'flex') {
86      let values =  declaration.value.split(/\s+/)
87      rule.declarations.splice(i, 1)
88      if (values.length === 1) {
89        checkFlexOne(rule, ruleLog, declaration, values, i)
90      } else if (values.length === 2) {
91        checkFlexTwo(rule, ruleLog, declaration, values, i)
92      } else if (values.length === 3) {
93        checkFlexThree(rule, ruleLog, declaration, values, i)
94      } else {
95        ruleLog.push({
96          line: declaration.position.start.line,
97          column: declaration.position.start.column,
98          reason: 'ERROR: Value `' + declaration.value + '` of the `' + declaration.property + '` attribute is incorrect.'
99        })
100      }
101    }
102  }
103}
104
105function getUnit(value) {
106  value = value.toString().trim()
107  let match = value.match(LENGTH_REGEXP)
108  if (match) {
109    let unit = match[1]
110    if (unit) {
111      if (unit === 'px') {
112        return "px"
113      }
114    } else {
115      return "none"
116    }
117  }
118  return null
119}
120
121function checkFlexOne(rule, ruleLog, declaration, values, i) {
122  const array = ['none', 'auto', 'initial']
123  if (array.includes(values[0])) {
124    rule.declarations.splice(i, 0, {type: 'declaration', property: 'flex', value: values[0], position: declaration.position})
125  } else if (getUnit(values[0]) === 'px') {
126    rule.declarations.splice(i, 0, {type: 'declaration', property: 'flex-basis', value: values[0], position: declaration.position})
127  } else if (getUnit(values[0]) === 'none') {
128    rule.declarations.splice(i, 0, {type: 'declaration', property: 'flex-grow', value: values[0], position: declaration.position})
129  } else {
130    ruleLog.push({
131      line: declaration.position.start.line,
132      column: declaration.position.start.column,
133      reason: 'ERROR: Value `' + declaration.value + '` of the `' + declaration.property + '` attribute is incorrect.' +
134        'It must be a number, a number with unit `' + 'px`' + ', none, auto, or initial.'
135    })
136  }
137}
138
139function checkFlexTwo(rule, ruleLog, declaration, values, i) {
140  if (getUnit(values[0]) === 'none') {
141    rule.declarations.splice(i, 0, {type: 'declaration', property: 'flex-grow', value: values[0], position: declaration.position})
142    if (getUnit(values[1]) === 'px') {
143      rule.declarations.splice(i, 0, {type: 'declaration', property: 'flex-basis', value: values[1], position: declaration.position})
144    } else if (getUnit(values[1]) === 'none') {
145      rule.declarations.splice(i, 0, {type: 'declaration', property: 'flex-shrink', value: values[1], position: declaration.position})
146    } else {
147      ruleLog.push({
148        line: declaration.position.start.line,
149        column: declaration.position.start.column,
150        reason: 'ERROR: Value `' + declaration.value + '` of the `' + declaration.property + '` attribute is incorrect. Value `' +
151          values[1] + '` must be a number or a number with unit `' + 'px`.'
152      })
153    }
154  } else {
155    ruleLog.push({
156      line: declaration.position.start.line,
157      column: declaration.position.start.column,
158      reason: 'ERROR: Value `' + declaration.value + '` of the `' + declaration.property + '` attribute is incorrect. Value `' +
159        values[0] + '` must be a number.'
160    })
161  }
162}
163
164function checkFlexThree(rule, ruleLog, declaration, values, i) {
165  if (getUnit(values[0]) === 'none' && getUnit(values[1]) === 'none' && getUnit(values[2]) === 'px') {
166    rule.declarations.splice(i, 0, {type: 'declaration', property: 'flex-grow', value: values[0], position: declaration.position})
167    rule.declarations.splice(i, 0, {type: 'declaration', property: 'flex-shrink', value: values[1], position: declaration.position})
168    rule.declarations.splice(i, 0, {type: 'declaration', property: 'flex-basis', value: values[2], position: declaration.position})
169  } else {
170    ruleLog.push({
171      line: declaration.position.start.line,
172      column: declaration.position.start.column,
173      reason: 'ERROR: Value `' + declaration.value + '` of the `' + declaration.property +
174        '` attribute is incorrect. It must be in the format of (1, 1, 1px).'
175    })
176  }
177}
178
179/**
180 * Parse `<style>` code to a JSON Object and log errors & warnings
181 *
182 * @param {string} code
183 * @param {function} done which will be called with
184 * - err:Error
185 * - data.jsonStyle{}: `classname.propname.value`-like object
186 * - data.log[{line, column, reason}]
187 */
188function parse(code, done, resourcePath) {
189  var ast, err, jsonStyle = {}, log = []
190
191  // css parse
192  ast = css.parse(code, {silent: true})
193
194  // catch syntax error
195  if (ast.stylesheet.parsingErrors && ast.stylesheet.parsingErrors.length) {
196    err = ast.stylesheet.parsingErrors
197    err.forEach(function (error) {
198      log.push({line: error.line, column: error.column, reason: error.toString().replace('Error', 'ERROR')})
199    })
200  }
201
202  // walk all
203  /* istanbul ignore else */
204  if (ast && ast.type === 'stylesheet' && ast.stylesheet &&
205      ast.stylesheet.rules && ast.stylesheet.rules.length) {
206    ast.stylesheet.rules.forEach(function (rule) {
207      var type = rule.type
208      var ruleResult = {}
209      var ruleLog = []
210
211      if (type === 'rule') {
212        if (rule.declarations && rule.declarations.length) {
213          flexExpand(rule, ruleLog)
214
215          rule.declarations.forEach(function (declaration) {
216            var subType = declaration.type
217            var name, value, line, column, subResult, camelCasedName
218
219            /* istanbul ignore if */
220            if (subType !== 'declaration') {
221              return
222            }
223
224            name = declaration.property
225            value = declaration.value
226
227            // validate declarations and collect them to result
228            camelCasedName = util.hyphenedToCamelCase(name)
229            subResult = validateItem(camelCasedName, value)
230
231            // expand margin、padding、border、borderWidth、borderColor、borderStyle properties、animation
232            if (subResult.value && Object.values(util.SPLECIAL_ATTR).indexOf(camelCasedName) !== -1) {
233              expand(subResult, camelCasedName, ruleResult)
234            }
235
236            /* istanbul ignore else */
237            if ((typeof subResult.value === 'number' || typeof subResult.value === 'string')
238              && !Object.values(util.SPLECIAL_ATTR).includes(camelCasedName)) {
239              ruleResult[camelCasedName] = subResult.value
240            }
241            if (subResult.log) {
242              subResult.log.line = declaration.position.start.line
243              subResult.log.column = declaration.position.start.column
244              ruleLog.push(subResult.log)
245            }
246          })
247
248          if (card && rule.selectors.length > 1) {
249            log.push({
250              line: rule.position.start.line,
251              column: rule.position.start.column,
252              reason: 'ERROR: The `' + rule.selectors.join(', ') + '` selector is not supported.'
253            })
254          } else {
255            rule.selectors.forEach(function (selector) {
256              const flag = card ? selector.match(CARD_SELECTOR) :
257                selector.match(SELECTOR_MATCHER) || selector.match(DESCENDANT_SELECTOR_MATCHER)
258              if (flag) {
259                var className = selector
260
261                // handle pseudo class
262                var pseudoIndex = className.indexOf(':')
263                if (pseudoIndex > -1) {
264                  var pseudoCls = className.slice(pseudoIndex)
265                  className = className.slice(0, pseudoIndex)
266                  var pseudoRuleResult = {}
267                  Object.keys(ruleResult).forEach(function (prop) {
268                    pseudoRuleResult[prop + pseudoCls] = ruleResult[prop]
269                  })
270                  ruleResult = pseudoRuleResult
271                }
272
273                // merge style
274                Object.keys(ruleResult).forEach(function (prop) {
275                  // handle transition
276                  if (prop.indexOf('transition') === 0 && prop !== 'transition') {
277                    var realProp = prop.replace('transition', '')
278                    realProp = realProp[0].toLowerCase() + realProp.slice(1)
279                    jsonStyle['@TRANSITION'] = jsonStyle['@TRANSITION'] || {}
280                    jsonStyle['@TRANSITION'][className] = jsonStyle['@TRANSITION'][className] || {}
281                    jsonStyle['@TRANSITION'][className][realProp] = ruleResult[prop]
282                  }
283
284                  jsonStyle[className] = jsonStyle[className] || {}
285                  jsonStyle[className][prop] = ruleResult[prop]
286                })
287              } else {
288                log.push({
289                  line: rule.position.start.line,
290                  column: rule.position.start.column,
291                  reason: 'ERROR: The `' + selector + '` selector is not supported.'
292                })
293              }
294            })
295          }
296          log = log.concat(ruleLog)
297        }
298      }
299      /* istanbul ignore else */
300      else if (type === 'font-face') {
301        /* istanbul ignore else */
302        if (rule.declarations && rule.declarations.length) {
303          rule.declarations.forEach(function (declaration) {
304            /* istanbul ignore if */
305            if (declaration.type !== 'declaration') {
306              return
307            }
308            var name = util.hyphenedToCamelCase(declaration.property)
309            var value = declaration.value
310            if (name === 'fontFamily' && '\"\''.indexOf(value[0]) > -1) { // FIXME: delete leading and trailing quotes
311              value = value.slice(1, value.length - 1)
312            }
313            ruleResult[name] = value
314          })
315          if (!jsonStyle['@FONT-FACE']) {
316            jsonStyle['@FONT-FACE'] = []
317          }
318          jsonStyle['@FONT-FACE'].push(ruleResult)
319        }
320      }
321      else if (type === 'import') {
322        parseImport(resourcePath, rule, jsonStyle, log)
323      }
324      else if (type === 'keyframes' && !card) {
325        if (!jsonStyle['@KEYFRAMES']) {
326          jsonStyle['@KEYFRAMES'] = {}
327        }
328        var keyName = rule.name
329        jsonStyle['@KEYFRAMES'][keyName] = []
330        if (rule.keyframes && rule.keyframes.length) {
331          if (card) {
332            log.push({
333              line: rule.position.start.line,
334              column: rule.position.start.column,
335              reason: 'ERROR: The keyframes is not supported!'
336            })
337          } else {
338            rule.keyframes.forEach(function (keyframe) {
339
340              var keyframeType = keyframe.type
341
342              /* istanbul ignore if */
343              if (keyframeType !== 'keyframe') {
344                return
345              }
346
347              if (keyframe.declarations && keyframe.declarations.length) {
348                keyframe.declarations.forEach(function (declaration) {
349                  var subType = declaration.type
350                  var name, value, line, column, subResult, camelCasedName
351
352                  /* istanbul ignore if */
353                  if (subType !== 'declaration') {
354                    return
355                  }
356
357                  name = declaration.property
358                  value = declaration.value
359
360                  // validate declarations and collect them to result
361                  camelCasedName = util.hyphenedToCamelCase(name)
362                  subResult = validateItem(camelCasedName, value)
363
364                  // expand margin、padding、border、borderWidth、borderColor、borderStyle properties
365                  if (subResult.value && Object.values(util.SPLECIAL_ATTR).indexOf(camelCasedName) !== -1) {
366                    expand(subResult, camelCasedName, ruleResult)
367                  }
368                  /* istanbul ignore else */
369                  if ((typeof subResult.value === 'number' || typeof subResult.value === 'string')
370                    && !Object.values(util.SPLECIAL_ATTR).includes(camelCasedName)) {
371                    ruleResult[camelCasedName] = subResult.value
372                  }
373                  if (subResult.log) {
374                    subResult.log.line = declaration.position.start.line
375                    subResult.log.column = declaration.position.start.column
376                    ruleLog.push(subResult.log)
377                  }
378                })
379              }
380
381              if (keyframe.values[0] === 'from') {
382                var keyframeResult = {}
383                Object.keys(ruleResult).forEach(function (prop) {
384                  keyframeResult[prop] = ruleResult[prop]
385                })
386                keyframeResult['time'] = 0
387                jsonStyle['@KEYFRAMES'][keyName].push(keyframeResult)
388              }
389              if (keyframe.values[0] === 'to') {
390                var keyframeResult = {}
391                Object.keys(ruleResult).forEach(function (prop) {
392                  keyframeResult[prop] = ruleResult[prop]
393                })
394                keyframeResult['time'] = 100
395                jsonStyle['@KEYFRAMES'][keyName].push(keyframeResult)
396              }
397              var patt = new RegExp(/^(100|[1-9]?\d)%$/)
398              if (patt.test(keyframe.values[0])) {
399                var keyframeResult = {}
400                Object.keys(ruleResult).forEach(function (prop) {
401                  keyframeResult[prop] = ruleResult[prop]
402                })
403                keyframeResult['time'] = keyframe.values[0].replace("%", "")
404                jsonStyle['@KEYFRAMES'][keyName].push(keyframeResult)
405              }
406            })
407            log = log.concat(ruleLog)
408          }
409        }
410      }
411      else if (type === 'media') {
412        if (!jsonStyle['@MEDIA']) {
413          jsonStyle['@MEDIA'] = []
414        }
415        var condition =  rule.media
416        var mediaObj = {}
417        mediaObj['condition'] = condition
418
419        if (rule.rules && rule.rules.length) {
420          rule.rules.forEach(function(rule) {
421            ruleResult = {}
422            if (rule.type === 'import') {
423              parseImport(resourcePath, rule, mediaObj, log)
424            }
425            if (rule.declarations && rule.declarations.length) {
426              flexExpand(rule, ruleLog)
427              rule.declarations.forEach(function (declaration) {
428                var subType = declaration.type
429                var name, value, line, column, subResult, camelCasedName
430
431                /* istanbul ignore if */
432                if (subType !== 'declaration') {
433                  return
434                }
435
436                name = declaration.property
437                value = declaration.value
438
439                // validate declarations and collect them to result
440                camelCasedName = util.hyphenedToCamelCase(name)
441                subResult = validateItem(camelCasedName, value)
442                // expand margin、padding、border、borderWidth、borderColor、borderStyle properties
443                if (subResult.value && Object.values(util.SPLECIAL_ATTR).indexOf(camelCasedName) !== -1) {
444                  expand(subResult, camelCasedName, ruleResult)
445                }
446
447                /* istanbul ignore else */
448                if ((typeof subResult.value === 'number' || typeof subResult.value === 'string')
449                  && !Object.values(util.SPLECIAL_ATTR).includes(camelCasedName)) {
450                  ruleResult[camelCasedName] = subResult.value
451                }
452                if (subResult.log) {
453                  subResult.log.line = declaration.position.start.line
454                  subResult.log.column = declaration.position.start.column
455                  ruleLog.push(subResult.log)
456                }
457              })
458              rule.selectors.forEach(function (selector) {
459                if (selector.match(SELECTOR_MATCHER) || selector.match(DESCENDANT_SELECTOR_MATCHER)) {
460                  var className = selector
461
462                  // handle pseudo class
463                  var pseudoIndex = className.indexOf(':')
464                  if (pseudoIndex > -1) {
465                    var pseudoCls = className.slice(pseudoIndex)
466                    className = className.slice(0, pseudoIndex)
467                    var pseudoRuleResult = {}
468                    Object.keys(ruleResult).forEach(function (prop) {
469                      pseudoRuleResult[prop + pseudoCls] = ruleResult[prop]
470                    })
471                    ruleResult = pseudoRuleResult
472                  }
473                  // merge style
474                  Object.keys(ruleResult).forEach(function (prop) {
475                    // handle transition
476                    if (prop.indexOf('transition') === 0 && prop !== 'transition') {
477                      var realProp = prop.replace('transition', '')
478                      realProp = realProp[0].toLowerCase() + realProp.slice(1)
479                      mediaObj['@TRANSITION'] = mediaObj['@TRANSITION'] || {}
480                      mediaObj['@TRANSITION'][className] = mediaObj['@TRANSITION'][className] || {}
481                      mediaObj['@TRANSITION'][className][realProp] = ruleResult[prop]
482                    }
483                    mediaObj[className] = mediaObj[className] || {}
484                    mediaObj[className][prop] = ruleResult[prop]
485                  })
486                } else {
487                  log.push({
488                    line: rule.position.start.line,
489                    column: rule.position.start.column,
490                    reason: 'ERROR: The `' + selector + '` selector is not supported.'
491                  })
492                }
493              })
494              log = log.concat(ruleLog)
495            }
496          })
497        }
498        jsonStyle['@MEDIA'].push(mediaObj)
499      }
500    })
501  }
502
503  done(err, {jsonStyle: jsonStyle, log: log})
504}
505
506function parseImport(resourcePath, rule, jsonStyle, log) {
507  if(!resourcePath) {
508    return
509  }
510  let importString = rule.import
511  let importPath
512  let mediaString = ''
513  let source = ''
514  if (importString.match(IMPORT_MATCHER)) {
515    let filePath = importString.match(/['"]([^()]+?)['"]/)
516    importPath = filePath[1]
517    mediaString = importString.replace(importPath, '').replace(/['"]/g, '')
518  }
519  if(/^(\.)|(\.\.)\//.test(importPath)) {
520    resourcePath = resourcePath.substring(0, resourcePath.lastIndexOf(path.sep) + 1);
521    importPath = path.resolve(resourcePath, importPath)
522  }
523  if (fs.existsSync(importPath)) {
524    source = fs.readFileSync(importPath).toString()
525  } else {
526    log.push({
527      line: rule.position.start.line,
528      column: rule.position.start.column,
529      reason: 'ERROR: no such file or directory, open ' + importPath
530    })
531    return
532  }
533  if (mediaString.length !== 0) {
534    source = '@media ' + mediaString + '{\n' + source + '\n}'
535  }
536  parse(source, (err, obj) => {
537    if (err) {
538      throw(err)
539    } else {
540      jsonStyle = Object.assign(jsonStyle, obj.jsonStyle)
541    }
542  }, importPath)
543}
544
545/**
546 * Validate a JSON Object and log errors & warnings
547 *
548 * @param {object} json
549 * @param {function} done which will be called with
550 * - err:Error
551 * - data.jsonStyle{}: `classname.propname.value`-like object
552 * - data.log[{reason}]
553 */
554function validate(json, done) {
555  var log = []
556  var err
557
558  try {
559    json = JSON.parse(JSON.stringify(json))
560  }
561  catch (e) {
562    err = e
563    json = {}
564  }
565
566  Object.keys(json).forEach(function (selector) {
567    var declarations = json[selector]
568
569    Object.keys(declarations).forEach(function (name) {
570      var value = declarations[name]
571      var result = validateItem(name, value)
572
573      if (typeof result.value === 'number' || typeof result.value === 'string') {
574        declarations[name] = result.value
575      }
576      else {
577        delete declarations[name]
578      }
579
580      if (result.log) {
581        log.push(result.log)
582      }
583    })
584  })
585
586  done(err, {
587    jsonStyle: json,
588    log: log
589  })
590}
591
592module.exports = {
593  parse: parse,
594  validate: validate,
595  validateItem: validateItem,
596  util: util,
597  expand: expand,
598  getUnit: getUnit,
599}
600
601