Tim van der Lippe | 706ec96 | 2021-06-04 13:24:42 +0100 | [diff] [blame] | 1 | var OffsetToLocation = require('../common/OffsetToLocation'); |
| 2 | var SyntaxError = require('../common/SyntaxError'); |
| 3 | var TokenStream = require('../common/TokenStream'); |
| 4 | var List = require('../common/List'); |
| 5 | var tokenize = require('../tokenizer'); |
| 6 | var constants = require('../tokenizer/const'); |
| 7 | var { findWhiteSpaceStart, cmpStr } = require('../tokenizer/utils'); |
| 8 | var sequence = require('./sequence'); |
| 9 | var noop = function() {}; |
| 10 | |
| 11 | var TYPE = constants.TYPE; |
| 12 | var NAME = constants.NAME; |
| 13 | var WHITESPACE = TYPE.WhiteSpace; |
| 14 | var COMMENT = TYPE.Comment; |
| 15 | var IDENT = TYPE.Ident; |
| 16 | var FUNCTION = TYPE.Function; |
| 17 | var URL = TYPE.Url; |
| 18 | var HASH = TYPE.Hash; |
| 19 | var PERCENTAGE = TYPE.Percentage; |
| 20 | var NUMBER = TYPE.Number; |
| 21 | var NUMBERSIGN = 0x0023; // U+0023 NUMBER SIGN (#) |
| 22 | var NULL = 0; |
| 23 | |
| 24 | function createParseContext(name) { |
| 25 | return function() { |
| 26 | return this[name](); |
| 27 | }; |
| 28 | } |
| 29 | |
| 30 | function processConfig(config) { |
| 31 | var parserConfig = { |
| 32 | context: {}, |
| 33 | scope: {}, |
| 34 | atrule: {}, |
| 35 | pseudo: {} |
| 36 | }; |
| 37 | |
| 38 | if (config.parseContext) { |
| 39 | for (var name in config.parseContext) { |
| 40 | switch (typeof config.parseContext[name]) { |
| 41 | case 'function': |
| 42 | parserConfig.context[name] = config.parseContext[name]; |
| 43 | break; |
| 44 | |
| 45 | case 'string': |
| 46 | parserConfig.context[name] = createParseContext(config.parseContext[name]); |
| 47 | break; |
| 48 | } |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | if (config.scope) { |
| 53 | for (var name in config.scope) { |
| 54 | parserConfig.scope[name] = config.scope[name]; |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | if (config.atrule) { |
| 59 | for (var name in config.atrule) { |
| 60 | var atrule = config.atrule[name]; |
| 61 | |
| 62 | if (atrule.parse) { |
| 63 | parserConfig.atrule[name] = atrule.parse; |
| 64 | } |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | if (config.pseudo) { |
| 69 | for (var name in config.pseudo) { |
| 70 | var pseudo = config.pseudo[name]; |
| 71 | |
| 72 | if (pseudo.parse) { |
| 73 | parserConfig.pseudo[name] = pseudo.parse; |
| 74 | } |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | if (config.node) { |
| 79 | for (var name in config.node) { |
| 80 | parserConfig[name] = config.node[name].parse; |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | return parserConfig; |
| 85 | } |
| 86 | |
| 87 | module.exports = function createParser(config) { |
| 88 | var parser = { |
| 89 | scanner: new TokenStream(), |
| 90 | locationMap: new OffsetToLocation(), |
| 91 | |
| 92 | filename: '<unknown>', |
| 93 | needPositions: false, |
| 94 | onParseError: noop, |
| 95 | onParseErrorThrow: false, |
| 96 | parseAtrulePrelude: true, |
| 97 | parseRulePrelude: true, |
| 98 | parseValue: true, |
| 99 | parseCustomProperty: false, |
| 100 | |
| 101 | readSequence: sequence, |
| 102 | |
| 103 | createList: function() { |
| 104 | return new List(); |
| 105 | }, |
| 106 | createSingleNodeList: function(node) { |
| 107 | return new List().appendData(node); |
| 108 | }, |
| 109 | getFirstListNode: function(list) { |
| 110 | return list && list.first(); |
| 111 | }, |
| 112 | getLastListNode: function(list) { |
| 113 | return list.last(); |
| 114 | }, |
| 115 | |
| 116 | parseWithFallback: function(consumer, fallback) { |
| 117 | var startToken = this.scanner.tokenIndex; |
| 118 | |
| 119 | try { |
| 120 | return consumer.call(this); |
| 121 | } catch (e) { |
| 122 | if (this.onParseErrorThrow) { |
| 123 | throw e; |
| 124 | } |
| 125 | |
| 126 | var fallbackNode = fallback.call(this, startToken); |
| 127 | |
| 128 | this.onParseErrorThrow = true; |
| 129 | this.onParseError(e, fallbackNode); |
| 130 | this.onParseErrorThrow = false; |
| 131 | |
| 132 | return fallbackNode; |
| 133 | } |
| 134 | }, |
| 135 | |
| 136 | lookupNonWSType: function(offset) { |
| 137 | do { |
| 138 | var type = this.scanner.lookupType(offset++); |
| 139 | if (type !== WHITESPACE) { |
| 140 | return type; |
| 141 | } |
| 142 | } while (type !== NULL); |
| 143 | |
| 144 | return NULL; |
| 145 | }, |
| 146 | |
| 147 | eat: function(tokenType) { |
| 148 | if (this.scanner.tokenType !== tokenType) { |
| 149 | var offset = this.scanner.tokenStart; |
| 150 | var message = NAME[tokenType] + ' is expected'; |
| 151 | |
| 152 | // tweak message and offset |
| 153 | switch (tokenType) { |
| 154 | case IDENT: |
| 155 | // when identifier is expected but there is a function or url |
| 156 | if (this.scanner.tokenType === FUNCTION || this.scanner.tokenType === URL) { |
| 157 | offset = this.scanner.tokenEnd - 1; |
| 158 | message = 'Identifier is expected but function found'; |
| 159 | } else { |
| 160 | message = 'Identifier is expected'; |
| 161 | } |
| 162 | break; |
| 163 | |
| 164 | case HASH: |
| 165 | if (this.scanner.isDelim(NUMBERSIGN)) { |
| 166 | this.scanner.next(); |
| 167 | offset++; |
| 168 | message = 'Name is expected'; |
| 169 | } |
| 170 | break; |
| 171 | |
| 172 | case PERCENTAGE: |
| 173 | if (this.scanner.tokenType === NUMBER) { |
| 174 | offset = this.scanner.tokenEnd; |
| 175 | message = 'Percent sign is expected'; |
| 176 | } |
| 177 | break; |
| 178 | |
| 179 | default: |
| 180 | // when test type is part of another token show error for current position + 1 |
| 181 | // e.g. eat(HYPHENMINUS) will fail on "-foo", but pointing on "-" is odd |
| 182 | if (this.scanner.source.charCodeAt(this.scanner.tokenStart) === tokenType) { |
| 183 | offset = offset + 1; |
| 184 | } |
| 185 | } |
| 186 | |
| 187 | this.error(message, offset); |
| 188 | } |
| 189 | |
| 190 | this.scanner.next(); |
| 191 | }, |
| 192 | |
| 193 | consume: function(tokenType) { |
| 194 | var value = this.scanner.getTokenValue(); |
| 195 | |
| 196 | this.eat(tokenType); |
| 197 | |
| 198 | return value; |
| 199 | }, |
| 200 | consumeFunctionName: function() { |
| 201 | var name = this.scanner.source.substring(this.scanner.tokenStart, this.scanner.tokenEnd - 1); |
| 202 | |
| 203 | this.eat(FUNCTION); |
| 204 | |
| 205 | return name; |
| 206 | }, |
| 207 | |
| 208 | getLocation: function(start, end) { |
| 209 | if (this.needPositions) { |
| 210 | return this.locationMap.getLocationRange( |
| 211 | start, |
| 212 | end, |
| 213 | this.filename |
| 214 | ); |
| 215 | } |
| 216 | |
| 217 | return null; |
| 218 | }, |
| 219 | getLocationFromList: function(list) { |
| 220 | if (this.needPositions) { |
| 221 | var head = this.getFirstListNode(list); |
| 222 | var tail = this.getLastListNode(list); |
| 223 | return this.locationMap.getLocationRange( |
| 224 | head !== null ? head.loc.start.offset - this.locationMap.startOffset : this.scanner.tokenStart, |
| 225 | tail !== null ? tail.loc.end.offset - this.locationMap.startOffset : this.scanner.tokenStart, |
| 226 | this.filename |
| 227 | ); |
| 228 | } |
| 229 | |
| 230 | return null; |
| 231 | }, |
| 232 | |
| 233 | error: function(message, offset) { |
| 234 | var location = typeof offset !== 'undefined' && offset < this.scanner.source.length |
| 235 | ? this.locationMap.getLocation(offset) |
| 236 | : this.scanner.eof |
| 237 | ? this.locationMap.getLocation(findWhiteSpaceStart(this.scanner.source, this.scanner.source.length - 1)) |
| 238 | : this.locationMap.getLocation(this.scanner.tokenStart); |
| 239 | |
| 240 | throw new SyntaxError( |
| 241 | message || 'Unexpected input', |
| 242 | this.scanner.source, |
| 243 | location.offset, |
| 244 | location.line, |
| 245 | location.column |
| 246 | ); |
| 247 | } |
| 248 | }; |
| 249 | |
| 250 | config = processConfig(config || {}); |
| 251 | for (var key in config) { |
| 252 | parser[key] = config[key]; |
| 253 | } |
| 254 | |
| 255 | return function(source, options) { |
| 256 | options = options || {}; |
| 257 | |
| 258 | var context = options.context || 'default'; |
| 259 | var onComment = options.onComment; |
| 260 | var ast; |
| 261 | |
| 262 | tokenize(source, parser.scanner); |
| 263 | parser.locationMap.setSource( |
| 264 | source, |
| 265 | options.offset, |
| 266 | options.line, |
| 267 | options.column |
| 268 | ); |
| 269 | |
| 270 | parser.filename = options.filename || '<unknown>'; |
| 271 | parser.needPositions = Boolean(options.positions); |
| 272 | parser.onParseError = typeof options.onParseError === 'function' ? options.onParseError : noop; |
| 273 | parser.onParseErrorThrow = false; |
| 274 | parser.parseAtrulePrelude = 'parseAtrulePrelude' in options ? Boolean(options.parseAtrulePrelude) : true; |
| 275 | parser.parseRulePrelude = 'parseRulePrelude' in options ? Boolean(options.parseRulePrelude) : true; |
| 276 | parser.parseValue = 'parseValue' in options ? Boolean(options.parseValue) : true; |
| 277 | parser.parseCustomProperty = 'parseCustomProperty' in options ? Boolean(options.parseCustomProperty) : false; |
| 278 | |
| 279 | if (!parser.context.hasOwnProperty(context)) { |
| 280 | throw new Error('Unknown context `' + context + '`'); |
| 281 | } |
| 282 | |
| 283 | if (typeof onComment === 'function') { |
| 284 | parser.scanner.forEachToken((type, start, end) => { |
| 285 | if (type === COMMENT) { |
| 286 | const loc = parser.getLocation(start, end); |
| 287 | const value = cmpStr(source, end - 2, end, '*/') |
| 288 | ? source.slice(start + 2, end - 2) |
| 289 | : source.slice(start + 2, end); |
| 290 | |
| 291 | onComment(value, loc); |
| 292 | } |
| 293 | }); |
| 294 | } |
| 295 | |
| 296 | ast = parser.context[context].call(parser, options); |
| 297 | |
| 298 | if (!parser.scanner.eof) { |
| 299 | parser.error(); |
| 300 | } |
| 301 | |
| 302 | return ast; |
| 303 | }; |
| 304 | }; |