UNPKG

@hippy/vue-css-loader

Version:

hippy-vue style loader module for webpack

852 lines (797 loc) 20 kB
/* * Tencent is pleased to support the open source community by making * Hippy available. * * Copyright (C) 2017-2019 THL A29 Limited, a Tencent company. * 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 { camelize } from 'shared/util'; import { tryConvertNumber, warn } from '@vue/util/index'; import translateColor from './color-parser'; const PROPERTIES_MAP: any = { textDecoration: 'textDecorationLine', boxShadowOffset: 'shadowOffset', boxShadowOffsetX: 'shadowOffsetX', boxShadowOffsetY: 'shadowOffsetY', boxShadowOpacity: 'shadowOpacity', boxShadowRadius: 'shadowRadius', boxShadowSpread: 'shadowSpread', boxShadowColor: 'shadowColor', caretColor: 'caret-color', }; // linear-gradient direction description map const LINEAR_GRADIENT_DIRECTION_MAP: any = { totop: '0', totopright: 'totopright', toright: '90', tobottomright: 'tobottomright', tobottom: '180', // default value tobottomleft: 'tobottomleft', toleft: '270', totopleft: 'totopleft', }; const DEGREE_UNIT = { TURN: 'turn', RAD: 'rad', DEG: 'deg', }; const commentRegexp = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//g; /** * Trim `str`. */ function trim(str: any) { return str ? str.trim() : ''; } /** * Adds non-enumerable parent node reference to each node. */ function addParent(obj: any, parent: any) { const isNode = obj && typeof obj.type === 'string'; const childParent = isNode ? obj : parent; Object.keys(obj).forEach((k) => { const value = obj[k]; if (Array.isArray(value)) { value.forEach((v) => { addParent(v, childParent); }); } else if (value && typeof value === 'object') { addParent(value, childParent); } }); if (isNode) { Object.defineProperty(obj, 'parent', { configurable: true, writable: true, enumerable: false, value: parent || null, }); } return obj; } /** * Convert the px unit to pt directly. * We found to the behavior of convert the unit directly is correct. */ function convertPxUnitToPt(value: any) { // If value is number just ignore if (Number.isInteger(value)) { return value; } // If value unit is rpx, don't need to filter if (typeof value === 'string' && value.endsWith('rpx')) { return value; } // If value unit is px, change to use pt as 1:1. if (typeof value === 'string' && value.endsWith('px')) { const num = parseFloat(value.slice(0, value.indexOf('px'))); if (!Number.isNaN(num)) { value = num; } } return value; } /** * Parse the CSS to be AST tree. */ function parseCSS(css: any, options: any) { options = options || {}; /** * Positional. */ let lineno = 1; let column = 1; /** * Update lineno and column based on `str`. */ function updatePosition(str: any) { const lines = str.match(/\n/g); if (lines) lineno += lines.length; const i = str.lastIndexOf('\n'); column = ~i ? str.length - i : column + str.length; } /** * Mark position and patch `node.position`. */ function position() { const start = { line: lineno, column }; return (node: any) => { node.position = new Position(start); whitespace(); return node; }; } /** * Store position information for a node */ class Position { content: any; end: any; source: any; start: any; constructor(start: any) { this.start = start; this.end = { line: lineno, column }; this.source = options.source; this.content = css; } } /** * Error `msg`. */ const errorsList: any = []; function error(msg: any) { const err = new Error(`${options.source}:${lineno}:${column}: ${msg}`); (err as any).reason = msg; (err as any).filename = options.source; (err as any).line = lineno; (err as any).column = column; (err as any).source = css; if (options.silent) { errorsList.push(err); } else { throw err; } } /** * Parse stylesheet. */ function stylesheet() { const rulesList = rules(); return { type: 'stylesheet', stylesheet: { source: options.source, rules: rulesList, parsingErrors: errorsList, }, }; } /** * Opening brace. */ function open() { return match(/^{\s*/); } /** * Closing brace. */ function close() { return match(/^}/); } /** * Parse ruleset. */ function rules() { let node; const rules: any = []; whitespace(); comments(rules); // eslint-disable-next-line no-cond-assign while (css.length && css.charAt(0) !== '}' && (node = atrule() || rule())) { if (node !== false) { rules.push(node); comments(rules); } } return rules; } /** * Match `re` and return captures. */ function match(re: any) { const m = re.exec(css); if (!m) { return null; } const str = m[0]; updatePosition(str); css = css.slice(str.length); return m; } /** * Parse whitespace. */ function whitespace() { match(/^\s*/); } /** * Parse comments; */ function comments(rules: any[] = []): any[] { let c; rules = rules || []; while ((c = comment()) !== null) { if (c !== false) { rules.push(c); } } return rules; } /** * Parse comment. */ function comment() { const pos = position(); if (css.charAt(0) !== '/' || css.charAt(1) !== '*') { return null; } let i = 2; while (css.charAt(i) !== '' && (css.charAt(i) !== '*' || css.charAt(i + 1) !== '/')) { i += 1; } i += 2; if (css.charAt(i - 1) === '') { return error('End of comment missing'); } const str = css.slice(2, i - 2); column += 2; updatePosition(str); css = css.slice(i); column += 2; return pos({ type: 'comment', comment: str, }); } /** * Parse selector. */ function selector() { const m = match(/^([^{]+)/); if (!m) { return null; } /* @fix Remove all comments from selectors * http://ostermiller.org/findcomment.html */ return trim(m[0]) .replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/+/g, '') .replace(/"(?:\\"|[^"])*"|'(?:\\'|[^'])*'/g, (m: any) => m.replace(/,/g, '\u200C')) .split(/\s*(?![^(]*\)),\s*/) .map((s: any) => s.replace(/\u200C/g, ',')); } /** * convert string value to string degree * @param {string} value * @param {string} unit */ function convertToDegree(value: any, unit = DEGREE_UNIT.DEG) { const convertedNumValue = parseFloat(value); let result = value || ''; const [, decimals] = value.split('.'); if (decimals && decimals.length > 2) { result = convertedNumValue.toFixed(2); } switch (unit) { // turn unit case DEGREE_UNIT.TURN: result = `${(convertedNumValue * 360).toFixed(2)}`; break; // radius unit case DEGREE_UNIT.RAD: result = `${(180 / Math.PI * convertedNumValue).toFixed(2)}`; break; default: } return result; } /** * parse gradient angle or direction * @param {string} value */ function getLinearGradientAngle(value: any) { const processedValue = (value || '').replace(/\s*/g, '').toLowerCase(); const reg = /^([+-]?(?=(?<digit>\d+))\k<digit>\.?\d*)+(deg|turn|rad)|(to\w+)$/g; const valueList = reg.exec(processedValue); if (!Array.isArray(valueList)) return; // default direction is to bottom, i.e. 180degree let angle = '180'; const [direction, angleValue, angleUnit] = valueList; if (angleValue && angleUnit) { // angle value angle = convertToDegree(angleValue, angleUnit); } else if (direction && typeof LINEAR_GRADIENT_DIRECTION_MAP[direction] !== 'undefined') { // direction description angle = LINEAR_GRADIENT_DIRECTION_MAP[direction]; } else { warn('linear-gradient direction or angle is invalid, default value [to bottom] would be used'); } return angle; } /** * parse gradient color stop * @param {string} value */ function getLinearGradientColorStop(value: any) { const processedValue = (value || '').replace(/\s+/g, ' ').trim(); const [color, percentage] = processedValue.split(/\s+(?![^(]*?\))/); const percentageCheckReg = /^([+-]?\d+\.?\d*)%$/g; if (color && !percentageCheckReg.exec(color) && !percentage) { return { color: translateColor(color), }; } if (color && percentageCheckReg.exec(percentage)) { return { // color stop ratio ratio: parseFloat(percentage.split('%')[0]) / 100, color: translateColor(color), }; } warn('linear-gradient color stop is invalid'); } /** * parse backgroundImage * @param {string} property * @param {string|Object|number|boolean} value * @returns {(string|{})[]} */ function parseBackgroundImage(property: any, value: any) { let processedValue = value; let processedProperty = property; if (value.indexOf('linear-gradient') === 0) { processedProperty = 'linearGradient'; const valueString = value.substring(value.indexOf('(') + 1, value.lastIndexOf(')')); const tokens = valueString.split(/,(?![^(]*?\))/); const colorStopList: any = []; processedValue = {}; tokens.forEach((value: any, index: any) => { if (index === 0) { // the angle of linear-gradient parameter can be optional const angle = getLinearGradientAngle(value); if (angle) { processedValue.angle = angle; } else { // if angle ignored, default direction is to bottom, i.e. 180degree processedValue.angle = '180'; const colorObject = getLinearGradientColorStop(value); if (colorObject) colorStopList.push(colorObject); } } else { const colorObject = getLinearGradientColorStop(value); if (colorObject) colorStopList.push(colorObject); } }); processedValue.colorStopList = colorStopList; } else { const regexp = /(?:\(['"]?)(.*?)(?:['"]?\))/; const executed = regexp.exec(value); if (executed && executed.length > 1) { [, processedValue] = executed; } } return [processedProperty, processedValue]; } /** * Parse declaration. */ function declaration() { const pos = position(); // prop let prop = match(/^(\*?[-#/*\\\w]+(\[[0-9a-z_-]+\])?)\s*/); if (!prop) { return null; } prop = trim(prop[0]); // : if (!match(/^:\s*/)) { return error('property missing \':\''); } // val const propertyName = prop.replace(commentRegexp, ''); const camelizedProperty = camelize(propertyName); let property = (() => { const property = PROPERTIES_MAP[camelizedProperty]; if (property) { return property; } return camelizedProperty; })(); const val = match(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^)]*?\)|[^};])+)/); let value = val ? trim(val[0]).replace(commentRegexp, '') : ''; switch (property) { case 'backgroundImage': { [property, value] = parseBackgroundImage(property, value); break; } case 'transform': { const regex = /(\w+\s*)(?:\(['"]?)(.*?)(?:['"]?\))/g; const oldValue = value; value = []; let group; while (group = regex.exec(oldValue)) { const key = group[1]; let v = group[2]; if (v.indexOf('.') === 0) { v = `0${v}`; } if (parseFloat(v).toString() === v) { v = parseFloat(v); } const transform: any = {}; transform[key] = v; value.push(transform); }; break; } case 'fontWeight': // Keep string and going on. break; case 'textShadowOffset': { const pos = value.split(' ') .filter((v: any) => v) .map((v: any) => convertPxUnitToPt(v)); const [width] = pos; let [, height] = pos; if (!height) { height = width; } value = { width, height, }; break; } case 'shadowOffset': { const pos = value.split(' ') .filter((v: any) => v) .map((v: any) => convertPxUnitToPt(v)); const [x] = pos; let [, y] = pos; if (!y) { y = x; } // FIXME: should not be width and height, should be x and y. value = { x, y, }; break; } case 'collapsable': value = value !== 'false'; break; default: { value = tryConvertNumber(value); // Convert the px to pt for specific properties value = convertPxUnitToPt(value); } } const ret = pos({ type: 'declaration', value, property, }); // ; match(/^[;\s]*/); return ret; } /** * Parse declarations. */ function declarations() { let decls: any = []; if (!open()) return error('missing \'{\''); comments(decls); // declarations let decl; while ((decl = declaration()) !== null) { if (decl !== false) { if (Array.isArray(decl)) { decls = decls.concat(decl); } else { decls.push(decl); } comments(decls); } } if (!close()) return error('missing \'}\''); return decls; } /** * Parse keyframe. */ function keyframe() { let m; const vals: any[] = []; const pos = position(); while ((m = match(/^((\d+\.\d+|\.\d+|\d+)%?|[a-z]+)\s*/)) !== null) { vals.push(m[1]); match(/^,\s*/); } if (!vals.length) { return null; } return pos({ type: 'keyframe', values: vals, declarations: declarations(), }); } /** * Parse keyframes. */ function atkeyframes() { const pos = position(); let m = match(/^@([-\w]+)?keyframes\s*/); if (!m) { return null; } const vendor = m[1]; // identifier m = match(/^([-\w]+)\s*/); if (!m) { return error('@keyframes missing name'); } const name = m[1]; if (!open()) return error('@keyframes missing \'{\''); let frame; let frames = comments(); while ((frame = keyframe()) !== null) { frames.push(frame); frames = frames.concat(comments()); } if (!close()) return error('@keyframes missing \'}\''); return pos({ type: 'keyframes', name, vendor, keyframes: frames, }); } /** * Parse supports. */ function atsupports() { const pos = position(); const m = match(/^@supports *([^{]+)/); if (!m) { return null; } const supports = trim(m[1]); if (!open()) return error('@supports missing \'{\''); const style = comments().concat(rules()); if (!close()) return error('@supports missing \'}\''); return pos({ type: 'supports', supports, rules: style, }); } /** * Parse host. */ function athost() { const pos = position(); const m = match(/^@host\s*/); if (!m) { return null; } if (!open()) { return error('@host missing \'{\''); } const style = comments().concat(rules()); if (!close()) { return error('@host missing \'}\''); } return pos({ type: 'host', rules: style, }); } /** * Parse media. */ function atmedia() { const pos = position(); const m = match(/^@media *([^{]+)/); if (!m) { return null; } const media = trim(m[1]); if (!open()) { return error('@media missing \'{\''); } const style = comments().concat(rules()); if (!close()) { return error('@media missing \'}\''); } return pos({ type: 'media', media, rules: style, }); } /** * Parse custom-media. */ function atcustommedia() { const pos = position(); const m = match(/^@custom-media\s+(--[^\s]+)\s*([^{;]+);/); if (!m) { return null; } return pos({ type: 'custom-media', name: trim(m[1]), media: trim(m[2]), }); } /** * Parse paged media. */ function atpage() { const pos = position(); const m = match(/^@page */); if (!m) { return null; } const sel = selector() || []; if (!open()) { return error('@page missing \'{\''); } let decls = comments(); // declarations let decl; while ((decl = declaration()) !== null) { decls.push(decl); decls = decls.concat(comments()); } if (!close()) { return error('@page missing \'}\''); } return pos({ type: 'page', selectors: sel, declarations: decls, }); } /** * Parse document. */ function atdocument() { const pos = position(); const m = match(/^@([-\w]+)?document *([^{]+)/); if (!m) { return null; } const vendor = trim(m[1]); const doc = trim(m[2]); if (!open()) { return error('@document missing \'{\''); } const style = comments().concat(rules()); if (!close()) { return error('@document missing \'}\''); } return pos({ type: 'document', document: doc, vendor, rules: style, }); } /** * Parse font-face. */ function atfontface() { const pos = position(); const m = match(/^@font-face\s*/); if (!m) { return null; } if (!open()) { return error('@font-face missing \'{\''); } let decls = comments(); // declarations let decl; while ((decl = declaration()) !== null) { decls.push(decl); decls = decls.concat(comments()); } if (!close()) { return error('@font-face missing \'}\''); } return pos({ type: 'font-face', declarations: decls, }); } /** * Parse import */ const atimport = compileAtRule('import'); /** * Parse charset */ const atcharset = compileAtRule('charset'); /** * Parse namespace */ const atnamespace = compileAtRule('namespace'); /** * Parse non-block at-rules */ function compileAtRule(name: any) { const re = new RegExp(`^@${name}\\s*([^;]+);`); return () => { const pos = position(); const m = match(re); if (!m) { return null; } const ret: any = { type: name }; ret[name] = m[1].trim(); return pos(ret); }; } /** * Parse at rule. */ function atrule() { if (css[0] !== '@') { return null; } return atkeyframes() || atmedia() || atcustommedia() || atsupports() || atimport() || atcharset() || atnamespace() || atdocument() || atpage() || athost() || atfontface(); } /** * Parse rule. */ function rule() { const pos = position(); const sel = selector(); if (!sel) return error('selector missing'); comments(); return pos({ type: 'rule', selectors: sel, declarations: declarations(), }); } // @ts-expect-error TS(2554): Expected 2 arguments, but got 1. return addParent(stylesheet()); } export default parseCSS; export { PROPERTIES_MAP, };