1 | /*
|
2 | * The MIT License (MIT)
|
3 | *
|
4 | * Copyright (c) 2015 - present Instructure, Inc.
|
5 | *
|
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
|
7 | * of this software and associated documentation files (the "Software"), to deal
|
8 | * in the Software without restriction, including without limitation the rights
|
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10 | * copies of the Software, and to permit persons to whom the Software is
|
11 | * furnished to do so, subject to the following conditions:
|
12 | *
|
13 | * The above copyright notice and this permission notice shall be included in all
|
14 | * copies or substantial portions of the Software.
|
15 | *
|
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
22 | * SOFTWARE.
|
23 | */
|
24 |
|
25 | /**
|
26 | * ---
|
27 | * category: utilities/themes
|
28 | * ---
|
29 | * Parses a CSS string into an AST object
|
30 | * @module parseCss
|
31 | * @param {String} cssText CSS string to parse
|
32 | * @returns {Object} AST for the CSS string
|
33 | */
|
34 | function parseCss(cssText = '') {
|
35 | const cleaned = cleanCss(cssText)
|
36 | return parseLexed(lex(cleaned), cleaned)
|
37 | }
|
38 |
|
39 | /**
|
40 | * CSSRule types (https://developer.mozilla.org/en-US/docs/Web/API/CSSRule)
|
41 | */
|
42 | const ruleTypes = {
|
43 | style: 1,
|
44 | keyframes: 7,
|
45 | media: 4
|
46 | }
|
47 |
|
48 | /**
|
49 | * Removes comments and import statements from a CSS string
|
50 | * (to prep for parsing and applying transforms)
|
51 | * @param {String} cssText CSS string to parse
|
52 | * @returns {String} cleaned CSS string
|
53 | */
|
54 | function cleanCss(text = '') {
|
55 | // remove comments and imports
|
56 | return text
|
57 | .replace(/\/\*[^*]*\*+([^/*][^*]*\*+)*\//gim, '')
|
58 | .replace(/@import[^;]*;/gim, '')
|
59 | }
|
60 |
|
61 | function lex(text) {
|
62 | const rootNode = {
|
63 | start: 0,
|
64 | end: text.length
|
65 | }
|
66 | let node = rootNode
|
67 | const chars = text.split('')
|
68 |
|
69 | chars.forEach((char, i) => {
|
70 | switch (char) {
|
71 | case '{': {
|
72 | if (!node.rules) {
|
73 | node.rules = []
|
74 | }
|
75 | const parent = node
|
76 | const previous = parent.rules[parent.rules.length - 1]
|
77 | node = {
|
78 | start: i + 1,
|
79 | parent,
|
80 | previous
|
81 | }
|
82 | parent.rules.push(node)
|
83 | break
|
84 | }
|
85 | case '}': {
|
86 | node.end = i + 1
|
87 | node = node.parent || rootNode
|
88 | break
|
89 | }
|
90 | default: {
|
91 | break
|
92 | }
|
93 | }
|
94 | })
|
95 | return rootNode
|
96 | }
|
97 |
|
98 | function parseSelector(node, text) {
|
99 | const start = node.previous ? node.previous.end : node.parent.start
|
100 | const end = node.start - 1
|
101 |
|
102 | let selector = text.substring(start, end)
|
103 |
|
104 | selector = selector.replace(/\s+/g, ' ')
|
105 | selector = selector.substring(selector.lastIndexOf(';') + 1)
|
106 |
|
107 | return selector.trim()
|
108 | }
|
109 |
|
110 | function parseRuleType(selector) {
|
111 | if (selector.indexOf('@') === 0) {
|
112 | if (selector.indexOf('@media') === 0) {
|
113 | return ruleTypes.media
|
114 | } else if (selector.match(/^@[^\s]*keyframes/)) {
|
115 | return ruleTypes.keyframes
|
116 | }
|
117 | } else {
|
118 | return ruleTypes.style
|
119 | }
|
120 | }
|
121 |
|
122 | function parseLexed(node, text = '') {
|
123 | /* eslint-disable no-param-reassign */
|
124 |
|
125 | if (node.parent) {
|
126 | node.selector = parseSelector(node, text)
|
127 | node.type = parseRuleType(node.selector)
|
128 | }
|
129 |
|
130 | node.cssText = text.substring(node.start, node.end - 1).trim()
|
131 |
|
132 | if (node.rules && node.rules.length > 0) {
|
133 | node.rules = node.rules.map((rule) => parseLexed(rule, text))
|
134 | }
|
135 |
|
136 | /* eslint-enable no-param-reassign */
|
137 |
|
138 | return node
|
139 | }
|
140 |
|
141 | export default parseCss
|
142 | export { parseCss, cleanCss, ruleTypes }
|