mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-31 18:37:00 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			604 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			604 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| // http://www.w3.org/TR/CSS21/grammar.html
 | |
| // https://github.com/visionmedia/css-parse/pull/49#issuecomment-30088027
 | |
| var commentre = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//g
 | |
| 
 | |
| module.exports = function(css, options){
 | |
|   options = options || {};
 | |
| 
 | |
|   /**
 | |
|    * Positional.
 | |
|    */
 | |
| 
 | |
|   var lineno = 1;
 | |
|   var column = 1;
 | |
| 
 | |
|   /**
 | |
|    * Update lineno and column based on `str`.
 | |
|    */
 | |
| 
 | |
|   function updatePosition(str) {
 | |
|     var lines = str.match(/\n/g);
 | |
|     if (lines) lineno += lines.length;
 | |
|     var i = str.lastIndexOf('\n');
 | |
|     column = ~i ? str.length - i : column + str.length;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Mark position and patch `node.position`.
 | |
|    */
 | |
| 
 | |
|   function position() {
 | |
|     var start = { line: lineno, column: column };
 | |
|     return function(node){
 | |
|       node.position = new Position(start);
 | |
|       whitespace();
 | |
|       return node;
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Store position information for a node
 | |
|    */
 | |
| 
 | |
|   function Position(start) {
 | |
|     this.start = start;
 | |
|     this.end = { line: lineno, column: column };
 | |
|     this.source = options.source;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Non-enumerable source string
 | |
|    */
 | |
| 
 | |
|   Position.prototype.content = css;
 | |
| 
 | |
|   /**
 | |
|    * Error `msg`.
 | |
|    */
 | |
| 
 | |
|   var errorsList = [];
 | |
| 
 | |
|   function error(msg) {
 | |
|     var err = new Error(options.source + ':' + lineno + ':' + column + ': ' + msg);
 | |
|     err.reason = msg;
 | |
|     err.filename = options.source;
 | |
|     err.line = lineno;
 | |
|     err.column = column;
 | |
|     err.source = css;
 | |
| 
 | |
|     if (options.silent) {
 | |
|       errorsList.push(err);
 | |
|     } else {
 | |
|       throw err;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Parse stylesheet.
 | |
|    */
 | |
| 
 | |
|   function stylesheet() {
 | |
|     var 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() {
 | |
|     var node;
 | |
|     var rules = [];
 | |
|     whitespace();
 | |
|     comments(rules);
 | |
|     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) {
 | |
|     var m = re.exec(css);
 | |
|     if (!m) return;
 | |
|     var str = m[0];
 | |
|     updatePosition(str);
 | |
|     css = css.slice(str.length);
 | |
|     return m;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Parse whitespace.
 | |
|    */
 | |
| 
 | |
|   function whitespace() {
 | |
|     match(/^\s*/);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Parse comments;
 | |
|    */
 | |
| 
 | |
|   function comments(rules) {
 | |
|     var c;
 | |
|     rules = rules || [];
 | |
|     while (c = comment()) {
 | |
|       if (c !== false) {
 | |
|         rules.push(c);
 | |
|       }
 | |
|     }
 | |
|     return rules;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Parse comment.
 | |
|    */
 | |
| 
 | |
|   function comment() {
 | |
|     var pos = position();
 | |
|     if ('/' != css.charAt(0) || '*' != css.charAt(1)) return;
 | |
| 
 | |
|     var i = 2;
 | |
|     while ("" != css.charAt(i) && ('*' != css.charAt(i) || '/' != css.charAt(i + 1))) ++i;
 | |
|     i += 2;
 | |
| 
 | |
|     if ("" === css.charAt(i-1)) {
 | |
|       return error('End of comment missing');
 | |
|     }
 | |
| 
 | |
|     var 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() {
 | |
|     var m = match(/^([^{]+)/);
 | |
|     if (!m) return;
 | |
|     /* @fix Remove all comments from selectors
 | |
|      * http://ostermiller.org/findcomment.html */
 | |
|     return trim(m[0])
 | |
|       .replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/+/g, '')
 | |
|       .replace(/"(?:\\"|[^"])*"|'(?:\\'|[^'])*'/g, function(m) {
 | |
|         return m.replace(/,/g, '\u200C');
 | |
|       })
 | |
|       .split(/\s*(?![^(]*\)),\s*/)
 | |
|       .map(function(s) {
 | |
|         return s.replace(/\u200C/g, ',');
 | |
|       });
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Parse declaration.
 | |
|    */
 | |
| 
 | |
|   function declaration() {
 | |
|     var pos = position();
 | |
| 
 | |
|     // prop
 | |
|     var prop = match(/^(\*?[-#\/\*\\\w]+(\[[0-9a-z_-]+\])?)\s*/);
 | |
|     if (!prop) return;
 | |
|     prop = trim(prop[0]);
 | |
| 
 | |
|     // :
 | |
|     if (!match(/^:\s*/)) return error("property missing ':'");
 | |
| 
 | |
|     // val
 | |
|     var val = match(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+)/);
 | |
| 
 | |
|     var ret = pos({
 | |
|       type: 'declaration',
 | |
|       property: prop.replace(commentre, ''),
 | |
|       value: val ? trim(val[0]).replace(commentre, '') : ''
 | |
|     });
 | |
| 
 | |
|     // ;
 | |
|     match(/^[;\s]*/);
 | |
| 
 | |
|     return ret;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Parse declarations.
 | |
|    */
 | |
| 
 | |
|   function declarations() {
 | |
|     var decls = [];
 | |
| 
 | |
|     if (!open()) return error("missing '{'");
 | |
|     comments(decls);
 | |
| 
 | |
|     // declarations
 | |
|     var decl;
 | |
|     while (decl = declaration()) {
 | |
|       if (decl !== false) {
 | |
|         decls.push(decl);
 | |
|         comments(decls);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (!close()) return error("missing '}'");
 | |
|     return decls;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Parse keyframe.
 | |
|    */
 | |
| 
 | |
|   function keyframe() {
 | |
|     var m;
 | |
|     var vals = [];
 | |
|     var pos = position();
 | |
| 
 | |
|     while (m = match(/^((\d+\.\d+|\.\d+|\d+)%?|[a-z]+)\s*/)) {
 | |
|       vals.push(m[1]);
 | |
|       match(/^,\s*/);
 | |
|     }
 | |
| 
 | |
|     if (!vals.length) return;
 | |
| 
 | |
|     return pos({
 | |
|       type: 'keyframe',
 | |
|       values: vals,
 | |
|       declarations: declarations()
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Parse keyframes.
 | |
|    */
 | |
| 
 | |
|   function atkeyframes() {
 | |
|     var pos = position();
 | |
|     var m = match(/^@([-\w]+)?keyframes\s*/);
 | |
| 
 | |
|     if (!m) return;
 | |
|     var vendor = m[1];
 | |
| 
 | |
|     // identifier
 | |
|     var m = match(/^([-\w]+)\s*/);
 | |
|     if (!m) return error("@keyframes missing name");
 | |
|     var name = m[1];
 | |
| 
 | |
|     if (!open()) return error("@keyframes missing '{'");
 | |
| 
 | |
|     var frame;
 | |
|     var frames = comments();
 | |
|     while (frame = keyframe()) {
 | |
|       frames.push(frame);
 | |
|       frames = frames.concat(comments());
 | |
|     }
 | |
| 
 | |
|     if (!close()) return error("@keyframes missing '}'");
 | |
| 
 | |
|     return pos({
 | |
|       type: 'keyframes',
 | |
|       name: name,
 | |
|       vendor: vendor,
 | |
|       keyframes: frames
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Parse supports.
 | |
|    */
 | |
| 
 | |
|   function atsupports() {
 | |
|     var pos = position();
 | |
|     var m = match(/^@supports *([^{]+)/);
 | |
| 
 | |
|     if (!m) return;
 | |
|     var supports = trim(m[1]);
 | |
| 
 | |
|     if (!open()) return error("@supports missing '{'");
 | |
| 
 | |
|     var style = comments().concat(rules());
 | |
| 
 | |
|     if (!close()) return error("@supports missing '}'");
 | |
| 
 | |
|     return pos({
 | |
|       type: 'supports',
 | |
|       supports: supports,
 | |
|       rules: style
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Parse host.
 | |
|    */
 | |
| 
 | |
|   function athost() {
 | |
|     var pos = position();
 | |
|     var m = match(/^@host\s*/);
 | |
| 
 | |
|     if (!m) return;
 | |
| 
 | |
|     if (!open()) return error("@host missing '{'");
 | |
| 
 | |
|     var style = comments().concat(rules());
 | |
| 
 | |
|     if (!close()) return error("@host missing '}'");
 | |
| 
 | |
|     return pos({
 | |
|       type: 'host',
 | |
|       rules: style
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Parse media.
 | |
|    */
 | |
| 
 | |
|   function atmedia() {
 | |
|     var pos = position();
 | |
|     var m = match(/^@media *([^{]+)/);
 | |
| 
 | |
|     if (!m) return;
 | |
|     var media = trim(m[1]);
 | |
| 
 | |
|     if (!open()) return error("@media missing '{'");
 | |
| 
 | |
|     var style = comments().concat(rules());
 | |
| 
 | |
|     if (!close()) return error("@media missing '}'");
 | |
| 
 | |
|     return pos({
 | |
|       type: 'media',
 | |
|       media: media,
 | |
|       rules: style
 | |
|     });
 | |
|   }
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * Parse custom-media.
 | |
|    */
 | |
| 
 | |
|   function atcustommedia() {
 | |
|     var pos = position();
 | |
|     var m = match(/^@custom-media\s+(--[^\s]+)\s*([^{;]+);/);
 | |
|     if (!m) return;
 | |
| 
 | |
|     return pos({
 | |
|       type: 'custom-media',
 | |
|       name: trim(m[1]),
 | |
|       media: trim(m[2])
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Parse paged media.
 | |
|    */
 | |
| 
 | |
|   function atpage() {
 | |
|     var pos = position();
 | |
|     var m = match(/^@page */);
 | |
|     if (!m) return;
 | |
| 
 | |
|     var sel = selector() || [];
 | |
| 
 | |
|     if (!open()) return error("@page missing '{'");
 | |
|     var decls = comments();
 | |
| 
 | |
|     // declarations
 | |
|     var decl;
 | |
|     while (decl = declaration()) {
 | |
|       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() {
 | |
|     var pos = position();
 | |
|     var m = match(/^@([-\w]+)?document *([^{]+)/);
 | |
|     if (!m) return;
 | |
| 
 | |
|     var vendor = trim(m[1]);
 | |
|     var doc = trim(m[2]);
 | |
| 
 | |
|     if (!open()) return error("@document missing '{'");
 | |
| 
 | |
|     var style = comments().concat(rules());
 | |
| 
 | |
|     if (!close()) return error("@document missing '}'");
 | |
| 
 | |
|     return pos({
 | |
|       type: 'document',
 | |
|       document: doc,
 | |
|       vendor: vendor,
 | |
|       rules: style
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Parse font-face.
 | |
|    */
 | |
| 
 | |
|   function atfontface() {
 | |
|     var pos = position();
 | |
|     var m = match(/^@font-face\s*/);
 | |
|     if (!m) return;
 | |
| 
 | |
|     if (!open()) return error("@font-face missing '{'");
 | |
|     var decls = comments();
 | |
| 
 | |
|     // declarations
 | |
|     var decl;
 | |
|     while (decl = declaration()) {
 | |
|       decls.push(decl);
 | |
|       decls = decls.concat(comments());
 | |
|     }
 | |
| 
 | |
|     if (!close()) return error("@font-face missing '}'");
 | |
| 
 | |
|     return pos({
 | |
|       type: 'font-face',
 | |
|       declarations: decls
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Parse import
 | |
|    */
 | |
| 
 | |
|   var atimport = _compileAtrule('import');
 | |
| 
 | |
|   /**
 | |
|    * Parse charset
 | |
|    */
 | |
| 
 | |
|   var atcharset = _compileAtrule('charset');
 | |
| 
 | |
|   /**
 | |
|    * Parse namespace
 | |
|    */
 | |
| 
 | |
|   var atnamespace = _compileAtrule('namespace');
 | |
| 
 | |
|   /**
 | |
|    * Parse non-block at-rules
 | |
|    */
 | |
| 
 | |
| 
 | |
|   function _compileAtrule(name) {
 | |
|     var re = new RegExp('^@' + name + '\\s*([^;]+);');
 | |
|     return function() {
 | |
|       var pos = position();
 | |
|       var m = match(re);
 | |
|       if (!m) return;
 | |
|       var ret = { type: name };
 | |
|       ret[name] = m[1].trim();
 | |
|       return pos(ret);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Parse at rule.
 | |
|    */
 | |
| 
 | |
|   function atrule() {
 | |
|     if (css[0] != '@') return;
 | |
| 
 | |
|     return atkeyframes()
 | |
|       || atmedia()
 | |
|       || atcustommedia()
 | |
|       || atsupports()
 | |
|       || atimport()
 | |
|       || atcharset()
 | |
|       || atnamespace()
 | |
|       || atdocument()
 | |
|       || atpage()
 | |
|       || athost()
 | |
|       || atfontface();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Parse rule.
 | |
|    */
 | |
| 
 | |
|   function rule() {
 | |
|     var pos = position();
 | |
|     var sel = selector();
 | |
| 
 | |
|     if (!sel) return error('selector missing');
 | |
|     comments();
 | |
| 
 | |
|     return pos({
 | |
|       type: 'rule',
 | |
|       selectors: sel,
 | |
|       declarations: declarations()
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   return addParent(stylesheet());
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Trim `str`.
 | |
|  */
 | |
| 
 | |
| function trim(str) {
 | |
|   return str ? str.replace(/^\s+|\s+$/g, '') : '';
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Adds non-enumerable parent node reference to each node.
 | |
|  */
 | |
| 
 | |
| function addParent(obj, parent) {
 | |
|   var isNode = obj && typeof obj.type === 'string';
 | |
|   var childParent = isNode ? obj : parent;
 | |
| 
 | |
|   for (var k in obj) {
 | |
|     var value = obj[k];
 | |
|     if (Array.isArray(value)) {
 | |
|       value.forEach(function(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;
 | |
| }
 |