<commit_before>import path from 'path';
import glob from 'glob';
import fs from 'fs';
import async from 'async';

import { MonicError } from './error';
import { FileStructure } from './file';
import { SourceMapConsumer } from 'source-map';

/**
 * Parser class
 */
export default class Parser {
	/**
	 * @param {string} nl - EOL symbol
	 * @param {!Array} replacers - an array of transform functions
	 * @param {boolean} sourceMaps - if is true, then will be enabled support for source maps
	 */
	constructor({nl, replacers, sourceMaps}) {
		this.nl = nl;
		this.replacers = replacers;
		this.sourceMaps = sourceMaps;
		this.realpathCache = {};
		this.cache = {};
	}

	/**
	 * Checks a file for existence
	 * and returns an absolute path to it
	 *
	 * @param {string} file - the file path
	 * @param {function(Error, string=)} callback - a callback function
	 */
	normalizePath(file, callback) {
		file = path.normalize(path.resolve(file));

		if (this.realpathCache[file]) {
			callback(null, file);

		} else {
			async.waterfall([
				(next) => fs.stat(file, next),
				(stat, next) => {
					if (!stat.isFile()) {
						return next(new Error(`"${file}" is not a file`));
					}

					this.realpathCache[file] = true;
					next(null, file);
				}

			], callback);
		}
	}

	/**
	 * Parses URL string with glob
	 *
	 * @param {string} base - a path to a base file
	 * @param {string} url - a path to a file
	 * @param {function(Error, Array=)} callback - a callback function
	 */
	parseURL(base, url, callback) {
		const
			parts = url.split('::'),
			dirname = path.dirname(base);

		glob(path.join(dirname, parts[0]), null, (err, files) => {
			if (err) {
				return callback(err);
			}

			callback(
				null,

				$C(files).reduce((res, el) => {
					parts[0] = path.relative(dirname, el);
					res.push(parts.slice());
					return res;
				}, [])

			);
		});
	}

	/**
	 * Parses a file and returns it structure
	 *
	 * @param {string} file - the file path
	 * @param {function(Error, FileStructure=, string=)} callback - a callback function
	 */
	parseFile(file, callback) {
		async.waterfall([
			(next) => this.normalizePath(file, next),

			(src, next) => {
				if (this.cache[src]) {
					return next(null, src, this.cache[src]);
				}

				fs.readFile(src, (err, content) => {
					next(err, src, String(content));
				});
			},

			(src, content, next) => {
				if (typeof content !== 'string') {
					return next(null, content, src);
				}

				this.parse(src, content, next);
			}

		], callback);
	}

	/**
	 * Parses a text and returns it structure
	 *
	 * @param {string} file - a path to a file
	 * @param {string} content - the source text
	 * @param {function(Error, FileStructure=, string=)} callback - a callback function
	 */
	parse(file, content, callback) {
		if (this.cache[file]) {
			return callback(null, this.cache[file], file);
		}

		const
			actions = [];

		$C(this.replacers).forEach((replacer) => {
			actions.push((next) => {
				if (replacer.length > 2) {
					replacer(content, file, (err, res) => next(err, err ? undefined : content = res));

				} else {
					content = replacer(content, file);
					next();
				}
			});
		});

		let sourceMap;
		if (this.sourceMaps) {
			content = content.replace(/\/\/# sourceMappingURL=(.*?)\s*$/, (sstr, url) => {
				actions.push((next) => {
					fs.readFile(path.normalize(path.resolve(path.dirname(file), url)), (err, str) => {
						try {
							sourceMap = new SourceMapConsumer(JSON.parse(str));
							content = content.replace(sstr, this.nl);

						} catch (ignore) {

						} finally {
							next();
						}
					});
				});

				return sstr;
			});
		}

		async.series(actions, (err) => {
			if (err) {
				return callback(err);
			}

			const
				fileStructure = new FileStructure({file, nl: this.nl}),
				lines = content.split(/\r?\n|\r/);

			this.cache[file] = fileStructure;
			const parseLines = (start) => {
				const
					errors = [];

				let
					info,
					i;

				function appendError(err) {
					const
						msg = err.message;

					errors.push(new MonicError(msg, file, i + 1));
					fileStructure.error(msg, info);
				}

				function asyncParseCallback(err) {
					if (err) {
						appendError(err);
					}

					parseLines(i + 1);
				}

				let original;
				if (sourceMap) {
					let
						originalMap = [];

					sourceMap.eachMapping((el) => {
						originalMap.push({
							generated: {
								line: el.generatedLine,
								column: el.generatedColumn
							},

							original: {
								line: el.originalLine,
								column: el.originalColumn
							},

							source: el.source,
							name: el.name
						});
					});

					if (sourceMap.sourcesContent) {
						$C(sourceMap.sourcesContent).forEach((content, i) => {
							const
								src = sourceMap.sources[i];

							$C(originalMap).forEach((el) => {
								if (el.source === src) {
									el.sourcesContent = content;
								}
							});
						});
					}

					original = $C(originalMap).group('generated > line');
				}

				for (i = start; i < lines.length; i++) {
					const
						line = lines[i],
						val = line + (i < lines.length - 1 ? this.nl : '');

					if (this.sourceMaps) {
						if (original) {
							info = original[i] || {ignore: true};

						} else {
							info = {
								generated: {
									column: 0
								},

								original: {
									line: i + 1,
									column: 0
								},

								source: file,
								sourcesContent: content,
								line
							};
						}
					}

					if (line.match(/^\s*\/\/#(.*)/)) {
						if (RegExp.$1) {
							const
								command = RegExp.$1.split(' '),
								dir = String(command.shift());

							const
								key = '_' + dir,
								params = command.join(' ');

							if (/^(?:include|without)$/.test(dir)) {
								return this[key](fileStructure, params, asyncParseCallback);

							} else if (this[key]) {
								try {
									this[key](fileStructure, params);

								} catch (err) {
									appendError(err);
								}

							} else {
								fileStructure.addCode(val, info);
							}
						}

					} else {
						fileStructure.addCode(val, info);
					}
				}

				callback(null, fileStructure, file);
			};

			parseLines(0);
		});
	}

	/**
	 * Directive #include
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 * @param {string} value - a value of the directive
	 * @param {function(Error=)} callback - a callback function
	 */
	_include(file, value, callback) {
		this.parseURL(file.file, value, (err, arr) => {
			if (err) {
				return callback(err);
			}

			const
				actions = [];

			$C(arr).forEach((paramsParts) => {
				actions.push((next) => action(paramsParts, next));
			});

			async.series(actions, callback);
		});

		function action(paramsParts, next) {
			const
				includeFileName = paramsParts.shift();

			paramsParts = $C(paramsParts).reduce((res, el) => {
				res[el] = true;
				return res;
			}, {});

			if (includeFileName) {
				this.parseFile(file.getRelativePathOf(includeFileName), (err, includeFile) => {
					if (err) {
						return next(err);
					}

					file.addInclude(includeFile, paramsParts);
					next();
				});

			} else {
				$C(paramsParts).forEach((el, key) => {
					file.root.labels[key] = true;
				});

				next();
			}
		}
	}

	/**
	 * Directive #without
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 * @param {string} value - a value of the directive
	 * @param {function(Error=)} callback - a callback function
	 */
	_without(file, value, callback) {
		this.parseURL(file.file, value, (err, arr) => {
			if (err) {
				return callback(err);
			}

			const
				actions = [];

			$C(arr).forEach((paramsParts) => {
				actions.push((next) => action(paramsParts, next));
			});

			async.series(actions, callback);
		});

		function action(paramsParts, next) {
			const
				includedFile = file.getRelativePathOf(paramsParts.shift());

			paramsParts = $C(paramsParts).reduce((res, el) => {
				res[el] = true;
				return res;
			}, {});

			this.parseFile(includedFile, (err, includeFile) => {
				if (err) {
					return next(err);
				}

				file.addWithout(includeFile, paramsParts);
				next();
			});
		}
	}

	/**
	 * Directive #label
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 * @param {string} value - a value of the directive
	 */
	_label(file, value) {
		file.beginLabel(value);
	}

	/**
	 * Directive #endlabel
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 */
	_endlabel(file) {
		file.endLabel();
	}

	/**
	 * Directive #if
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 * @param {string} value - a value of the directive
	 */
	_if(file, value) {
		value = value.trim();

		if (!value) {
			throw new Error('Bad "if" directive');
		}

		const
			args = value.split(/\s+/);

		let res = true;
		if (args.length > 1 && args[0] === 'not') {
			res = false;
			args.shift();
		}

		file.beginIf(args[0], res);
	}

	/**
	 * Directive #endif
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 */
	_endif(file) {
		file.endIf();
	}

	/**
	 * Directive #set
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 * @param {string} value - a value of the directive
	 */
	_set(file, value) {
		value = value.trim();

		if (!value) {
			throw new Error('Bad "set" directive');
		}

		file.addSet(value);
	}

	/**
	 * Directive #unset
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 * @param {string} value - a value of the directive
	 */
	_unset(file, value) {
		value = value.trim();

		if (!value) {
			throw new Error('Bad "unset" directive');
		}

		file.addUnset(value);
	}
}
<commit_msg>Bugfix<commit_after>import path from 'path';
import glob from 'glob';
import fs from 'fs';
import async from 'async';

import { MonicError } from './error';
import { FileStructure } from './file';
import { SourceMapConsumer } from 'source-map';

/**
 * Parser class
 */
export default class Parser {
	/**
	 * @param {string} nl - EOL symbol
	 * @param {!Array} replacers - an array of transform functions
	 * @param {boolean} sourceMaps - if is true, then will be enabled support for source maps
	 */
	constructor({nl, replacers, sourceMaps}) {
		this.nl = nl;
		this.replacers = replacers;
		this.sourceMaps = sourceMaps;
		this.realpathCache = {};
		this.cache = {};
	}

	/**
	 * Checks a file for existence
	 * and returns an absolute path to it
	 *
	 * @param {string} file - the file path
	 * @param {function(Error, string=)} callback - a callback function
	 */
	normalizePath(file, callback) {
		file = path.normalize(path.resolve(file));

		if (this.realpathCache[file]) {
			callback(null, file);

		} else {
			async.waterfall([
				(next) => fs.stat(file, next),
				(stat, next) => {
					if (!stat.isFile()) {
						return next(new Error(`"${file}" is not a file`));
					}

					this.realpathCache[file] = true;
					next(null, file);
				}

			], callback);
		}
	}

	/**
	 * Parses URL string with glob
	 *
	 * @param {string} base - a path to a base file
	 * @param {string} url - a path to a file
	 * @param {function(Error, Array=)} callback - a callback function
	 */
	parseURL(base, url, callback) {
		const
			parts = url.split('::'),
			dirname = path.dirname(base);

		glob(path.join(dirname, parts[0]), null, (err, files) => {
			if (err) {
				return callback(err);
			}

			callback(
				null,

				$C(files).reduce((res, el) => {
					parts[0] = path.relative(dirname, el);
					res.push(parts.slice());
					return res;
				}, [])

			);
		});
	}

	/**
	 * Parses a file and returns it structure
	 *
	 * @param {string} file - the file path
	 * @param {function(Error, FileStructure=, string=)} callback - a callback function
	 */
	parseFile(file, callback) {
		async.waterfall([
			(next) => this.normalizePath(file, next),

			(src, next) => {
				if (this.cache[src]) {
					return next(null, src, this.cache[src]);
				}

				fs.readFile(src, (err, content) => {
					next(err, src, String(content));
				});
			},

			(src, content, next) => {
				if (typeof content !== 'string') {
					return next(null, content, src);
				}

				this.parse(src, content, next);
			}

		], callback);
	}

	/**
	 * Parses a text and returns it structure
	 *
	 * @param {string} file - a path to a file
	 * @param {string} content - the source text
	 * @param {function(Error, FileStructure=, string=)} callback - a callback function
	 */
	parse(file, content, callback) {
		if (this.cache[file]) {
			return callback(null, this.cache[file], file);
		}

		const
			actions = [];

		$C(this.replacers).forEach((replacer) => {
			actions.push((next) => {
				if (replacer.length > 2) {
					replacer(content, file, (err, res) => next(err, err ? undefined : content = res));

				} else {
					content = replacer(content, file);
					next();
				}
			});
		});

		let sourceMap;
		if (this.sourceMaps) {
			content = content.replace(/\/\/# sourceMappingURL=(.*?)\s*$/, (sstr, url) => {
				actions.push((next) => {
					fs.readFile(path.normalize(path.resolve(path.dirname(file), url)), (err, str) => {
						try {
							sourceMap = new SourceMapConsumer(JSON.parse(str));
							content = content.replace(sstr, this.nl);

						} catch (ignore) {

						} finally {
							next();
						}
					});
				});

				return sstr;
			});
		}

		async.series(actions, (err) => {
			if (err) {
				return callback(err);
			}

			const
				fileStructure = new FileStructure({file, nl: this.nl}),
				lines = content.split(/\r?\n|\r/);

			this.cache[file] = fileStructure;
			const parseLines = (start) => {
				const
					errors = [];

				let
					info,
					i;

				function appendError(err) {
					const
						msg = err.message;

					errors.push(new MonicError(msg, file, i + 1));
					fileStructure.error(msg, info);
				}

				function asyncParseCallback(err) {
					if (err) {
						appendError(err);
					}

					parseLines(i + 1);
				}

				let original;
				if (sourceMap) {
					let
						originalMap = [];

					sourceMap.eachMapping((el) => {
						originalMap.push({
							generated: {
								line: el.generatedLine,
								column: el.generatedColumn
							},

							original: {
								line: el.originalLine,
								column: el.originalColumn
							},

							source: el.source,
							name: el.name
						});
					});

					if (sourceMap.sourcesContent) {
						$C(sourceMap.sourcesContent).forEach((content, i) => {
							const
								src = sourceMap.sources[i];

							$C(originalMap).forEach((el) => {
								if (el.source === src) {
									el.sourcesContent = content;
								}
							});
						});
					}

					original = $C(originalMap).group('generated > line');
				}

				for (i = start; i < lines.length; i++) {
					const
						line = lines[i],
						val = line + (i < lines.length - 1 ? this.nl : '');

					if (this.sourceMaps) {
						if (original) {
							info = original[i] || {ignore: true};

						} else {
							info = {
								generated: {
									column: 0
								},

								original: {
									line: i + 1,
									column: 0
								},

								source: file,
								sourcesContent: content,
								line
							};
						}
					}

					if (line.match(/^\s*\/\/#(.*)/)) {
						if (RegExp.$1) {
							const
								command = RegExp.$1.split(' '),
								dir = String(command.shift());

							const
								key = '_' + dir,
								params = command.join(' ');

							if (/^(?:include|without)$/.test(dir)) {
								return this[key](fileStructure, params, asyncParseCallback);

							} else if (this[key]) {
								try {
									this[key](fileStructure, params);

								} catch (err) {
									appendError(err);
								}

							} else {
								fileStructure.addCode(val, info);
							}
						}

					} else {
						fileStructure.addCode(val, info);
					}
				}

				callback(null, fileStructure, file);
			};

			parseLines(0);
		});
	}

	/**
	 * Directive #include
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 * @param {string} value - a value of the directive
	 * @param {function(Error=)} callback - a callback function
	 */
	_include(file, value, callback) {
		this.parseURL(file.file, value, (err, arr) => {
			if (err) {
				return callback(err);
			}

			const
				actions = [];

			$C(arr).forEach((paramsParts) => {
				actions.push((next) => action.call(this, paramsParts, next));
			});

			async.series(actions, callback);
		});

		function action(paramsParts, next) {
			const
				includeFileName = paramsParts.shift();

			paramsParts = $C(paramsParts).reduce((res, el) => {
				res[el] = true;
				return res;
			}, {});

			if (includeFileName) {
				this.parseFile(file.getRelativePathOf(includeFileName), (err, includeFile) => {
					if (err) {
						return next(err);
					}

					file.addInclude(includeFile, paramsParts);
					next();
				});

			} else {
				$C(paramsParts).forEach((el, key) => {
					file.root.labels[key] = true;
				});

				next();
			}
		}
	}

	/**
	 * Directive #without
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 * @param {string} value - a value of the directive
	 * @param {function(Error=)} callback - a callback function
	 */
	_without(file, value, callback) {
		this.parseURL(file.file, value, (err, arr) => {
			if (err) {
				return callback(err);
			}

			const
				actions = [];

			$C(arr).forEach((paramsParts) => {
				actions.push((next) => action.call(this, paramsParts, next));
			});

			async.series(actions, callback);
		});

		function action(paramsParts, next) {
			const
				includedFile = file.getRelativePathOf(paramsParts.shift());

			paramsParts = $C(paramsParts).reduce((res, el) => {
				res[el] = true;
				return res;
			}, {});

			this.parseFile(includedFile, (err, includeFile) => {
				if (err) {
					return next(err);
				}

				file.addWithout(includeFile, paramsParts);
				next();
			});
		}
	}

	/**
	 * Directive #label
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 * @param {string} value - a value of the directive
	 */
	_label(file, value) {
		file.beginLabel(value);
	}

	/**
	 * Directive #endlabel
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 */
	_endlabel(file) {
		file.endLabel();
	}

	/**
	 * Directive #if
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 * @param {string} value - a value of the directive
	 */
	_if(file, value) {
		value = value.trim();

		if (!value) {
			throw new Error('Bad "if" directive');
		}

		const
			args = value.split(/\s+/);

		let res = true;
		if (args.length > 1 && args[0] === 'not') {
			res = false;
			args.shift();
		}

		file.beginIf(args[0], res);
	}

	/**
	 * Directive #endif
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 */
	_endif(file) {
		file.endIf();
	}

	/**
	 * Directive #set
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 * @param {string} value - a value of the directive
	 */
	_set(file, value) {
		value = value.trim();

		if (!value) {
			throw new Error('Bad "set" directive');
		}

		file.addSet(value);
	}

	/**
	 * Directive #unset
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 * @param {string} value - a value of the directive
	 */
	_unset(file, value) {
		value = value.trim();

		if (!value) {
			throw new Error('Bad "unset" directive');
		}

		file.addUnset(value);
	}
}
<|endoftext|>


<commit_before>'use strict'

const EventEmitter = require('events')
const espree = require('espree')
const HtmlParser = require('htmlparser2').Parser
const path = require('path')

const RE_VISIBILITY = /(public|protected|private)/
const RE_KEYWORDS = /@\**\s*([a-z0-9_-]+)(\s+(-\s+)?([\wÀ-ÿ\s*{}[\]()='"`_^$#&²~|\\£¤€%µ,?;.:/!§<>+¨-]+))?/ig

const DEFAULT_VISIBILITY = 'public'
const MODEL_KEYWORD = 'model'
const DEFAULT_OPTIONS = {
  range: false,
//   comment: true,
  attachComment: true,

  // create a top-level tokens array containing all tokens
  tokens: true,

  // set to 3, 5 (default), 6, 7, or 8 to specify the version of ECMAScript syntax you want to use.
  // You can also set to 2015 (same as 6), 2016 (same as 7), or 2017 (same as 8) to use the year-based naming.
  ecmaVersion: 7,

  // specify which type of script you're parsing (script or module, default is script)
  sourceType: 'module'
}

const getVisibility = (keywords, defaultVisibility) => {
  const keyword = keywords.find((keyword) => RE_VISIBILITY.test(keyword.name))

  if (keyword) {
    return keyword.name
  }

  return defaultVisibility
}

const parseComment = (text, defaultVisibility) => {
  const result = {
    keywords: [],
    visibility: defaultVisibility || DEFAULT_VISIBILITY,
    description: '',
    describeModel: false
  }

  let index = 0
  let indexDescription = text.length

  while (index < text.length && index !== -1) {
    const matches = RE_KEYWORDS.exec(text)

    if (!matches) {
      break
    }

    if (index === 0) {
      indexDescription = matches.index
    }

    index = matches.index

    result.keywords.push({
      name: matches[1],
      description: matches[4]
        .replace(/\s*\*\s/g, '')
        .replace(/\s+/g, ' ')
        .replace(/\s*\*\//g, '')
    })
  }

  result.description = text.substring(0, indexDescription)
    .replace(/\/*\*\s*/g, '')
    .replace(/\n\s+/g, ' ')
    .replace(/\/\s*$/g, '')
    .trim()

  if (index) {
    result.visibility = getVisibility(result.keywords, result.visibility)
    result.describeModel = !!result.keywords.find((keyword) => keyword.name === MODEL_KEYWORD)
  }

  return result
}

const getComment = (property, defaultVisibility) => {
  defaultVisibility = defaultVisibility || DEFAULT_VISIBILITY

  let lastComment = null

  if (property.leadingComments) {
    lastComment = property.leadingComments.pop().value
  }

  if (property.trailingComments) {
    lastComment = property.trailingComments.pop().value
  }

  if (lastComment) {
    return parseComment(lastComment, defaultVisibility)
  }

  return {
    visibility: defaultVisibility,
    description: null,
    keywords: []
  }
}

class NodeFunction {
  constructor (node) {
    Object.assign(this, node)
  }
}

const value = (property) => {
  /* istanbul ignore if */
  if (property.key.type === 'Literal') {
    property.key.name = property.key.value
  }

  switch (property.value.type) {
    case 'Literal':
      return { [property.key.name]: property.value.value }

    case 'Identifier':
      return { [property.key.name]: property.value.name }

    case 'ObjectExpression':
      return { [property.key.name]: values(property) }

    case 'FunctionExpression':
    case 'ArrowFunctionExpression':
      return { [property.key.name]: new NodeFunction(property.value) }
  }

  return { [property.key.name]: property.value }
}

const values = (entry) => {
  const values = {}

  entry.value.properties.forEach((property) => {
    if (property.value.type === 'ObjectExpression') {
      Object.assign(values, {
        [property.key.name]: value(property)
      })
    } else {
      Object.assign(values, value(property))
    }
  })

  return values
}

const tokensInterval = (tokens, range) => {
  return tokens.filter((item) => {
    return item.range[0] > range[0] && item.range[1] < range[1]
  })
}

const getIdentifierValue = (tokens, identifierName, rangeLimit) => {
  const range = [ tokens[0].range[0], rangeLimit ]
  const searchingTokens = tokensInterval(tokens, range).reverse()
  const tokenIndex = searchingTokens.findIndex((item, i, array) => {
    if (item.type === 'Identifier' && item.value === identifierName) {
      const nextToken = array[i - 1]

      return nextToken.type === 'Punctuator' && nextToken.value === '='
    }

    return false
  })

  /* istanbul ignore if */
  if (tokenIndex === -1) {
    return null
  }

  const valueToken = searchingTokens[tokenIndex - 2]

  switch (valueToken.type) {
    case 'String':
      return valueToken.value.replace(/['"]/g, '')

    case 'Identifier':
      return getIdentifierValue(
        tokens, valueToken.value, valueToken.range[0])
  }

  /* istanbul ignore next */
  return null
}

const unCamelcase = (text) => {
  const chars = []

  text.split('').forEach((char) => {
    if (char === char.toUpperCase()) {
      char = char.toLowerCase()

      if (chars.length) {
        chars.push('-')
      }
    }

    chars.push(char)
  })

  return chars.join('')
}

const first = (o) => o[Object.keys(o)[0]]

const parseOptions = (options) => {
  options = options || {}

  /* istanbul ignore if */
  if (!options.source) {
    throw new Error('options.source.filename is required')
  }

  /* istanbul ignore if */
  if (!options.source.script) {
    throw new Error('options.source.script is required')
  }
}

class Parser extends EventEmitter {
  constructor (options) {
    parseOptions(options)

    for (let key in DEFAULT_OPTIONS) {
      options[key] = DEFAULT_OPTIONS[key]
    }

    super()

    this.ast = espree.parse(options.source.script, options)
    this.componentName = null
    this.template = options.source.template
    this.filename = options.filename
    this.eventsEmmited = {}
    this.defaultMethodVisibility = options.defaultMethodVisibility
  }

  extractProperties (property) {
    switch (property.value.type) {
      case 'ObjectExpression':
        property.value.properties.forEach((p) => {
          let defaultVisibility = DEFAULT_VISIBILITY

          if (property.key.name === 'methods' && this.defaultMethodVisibility) {
            defaultVisibility = this.defaultMethodVisibility
          }

          const entry = getComment(p, defaultVisibility)

          entry.value = value(p)
          entry.name = Object.keys(entry.value)[0]
          entry.value = entry.value[entry.name]

          if (entry.value instanceof NodeFunction) {
            entry.params = entry.value.params
          } else if (property.key.name === 'props') {
            if (entry.describeModel) {
              entry.name = 'v-model'
            } else {
              entry.name = unCamelcase(entry.name)
            }
          }

          delete entry.describeModel

          this.emit(property.key.name, entry)
          this.subWalk(entry.value)
        })
        break

      default:
        const propertyValue = value(property)
        const entryValue = first(propertyValue)

        if (propertyValue.hasOwnProperty('name') && !this.componentName) {
          this.componentName = propertyValue.name
          break
        }

        this.subWalk(entryValue)
    }
  }

  walk () {
    process.nextTick(() => {
      this.ast.body.forEach((body) => {
        const entry = getComment(body, this.defaultMethodVisibility)

        if (entry.description) {
          this.emit('description', entry.description)
        }

        if (entry.keywords.length) {
          this.emit('keywords', entry.keywords)
        }

        if (body.type !== 'ExportDefaultDeclaration' && body.type !== 'ExpressionStatement') {
          return
        }

        if (body.declaration != null) {
          body.declaration.properties.forEach((property) => this.extractProperties(property))
        } else if (body.expression != null) {
          body.expression.right.properties.forEach((property) => this.extractProperties(property))
        }

        if (this.componentName === null && this.filename) {
          const filename = path.parse(this.filename).name

          this.componentName = unCamelcase(filename)
        }

        this.emit('name', this.componentName)

        if (this.template) {
          this.parseTemplate()
        }
      })

      this.emit('end')
    })

    return this
  }

  subWalk (entry) {
    if (entry instanceof NodeFunction) {
      const tokens = tokensInterval(this.ast.tokens, entry.range)

      for (let i = 0; i < tokens.length; i++) {
        const token = tokens[i]

        if (token.type === 'Identifier' && token.value === '$emit') {
          if (!tokens[i + 2]) {
            break
          }

          const next = tokens[i + 2]
          const event = {
            name: null,
            description: null,
            visibility: DEFAULT_VISIBILITY,
            keywords: []
          }

          switch (next.type) {
            case 'String':
              event.name = next.value.replace(/['"]/g, '')
              break

            case 'Identifier':
              event.name = getIdentifierValue(tokens, next.value, next.range[0])
              break
          }

          if (!event.name) {
            event.name = '****unhandled-event-name****'
          } else {
            if (this.eventsEmmited.hasOwnProperty(event.name)) {
              continue
            }

            this.eventsEmmited[event.name] = true
          }

          const range = [
            tokens[i - 3].range[1],
            tokens[i - 2].range[0]
          ]

          event.description = this.ast.comments.reverse().find((c) => {
            return c.range[0] > range[0] && c.range[1] < range[1]
          })

          if (event.description) {
            const comment = parseComment(event.description.value)

            event.visibility = comment.visibility
            event.description = comment.description
            event.keywords = comment.keywords
          }

          this.emit('event', event)

          i += 2
        }
      }
    }
  }

  parseTemplate () {
    let lastComment = null

    const parser = new HtmlParser({
      oncomment: (data) => {
        lastComment = data.trim()
      },
      ontext: (text) => {
        if (text.trim()) {
          lastComment = null
        }
      },
      onopentag: (name, attrs) => {
        if (name === 'slot') {
          this.emit('slot', {
            name: attrs.name || 'default',
            description: lastComment
          })

          lastComment = null
        }
      }
    })

    parser.write(this.template)
    parser.end()
  }
}

module.exports = Parser
module.exports.DEFAULT_VISIBILITY= DEFAULT_VISIBILITY
module.exports.getVisibility = getVisibility
module.exports.parseComment = parseComment
module.exports.getComment = getComment
<commit_msg>Improve keywords parsing<commit_after>'use strict'

const EventEmitter = require('events')
const espree = require('espree')
const HtmlParser = require('htmlparser2').Parser
const path = require('path')

const RE_VISIBILITY = /(public|protected|private)/
const RE_KEYWORDS = /@\**\s*([a-z0-9_-]+)(\s+(-\s+)?([\wÀ-ÿ\s*{}[\]()='"`_^$#&²~|\\£¤€%µ,?;.:/!§<>+¨-]+))?/ig

const DEFAULT_VISIBILITY = 'public'
const MODEL_KEYWORD = 'model'
const DEFAULT_OPTIONS = {
  range: false,
//   comment: true,
  attachComment: true,

  // create a top-level tokens array containing all tokens
  tokens: true,

  // set to 3, 5 (default), 6, 7, or 8 to specify the version of ECMAScript syntax you want to use.
  // You can also set to 2015 (same as 6), 2016 (same as 7), or 2017 (same as 8) to use the year-based naming.
  ecmaVersion: 7,

  // specify which type of script you're parsing (script or module, default is script)
  sourceType: 'module'
}

const getVisibility = (keywords, defaultVisibility) => {
  const keyword = keywords.find((keyword) => RE_VISIBILITY.test(keyword.name))

  if (keyword) {
    return keyword.name
  }

  return defaultVisibility
}

const parseComment = (text, defaultVisibility) => {
  const result = {
    keywords: [],
    visibility: defaultVisibility || DEFAULT_VISIBILITY,
    description: '',
    describeModel: false
  }

  let index = 0
  let indexDescription = text.length

  while (index < text.length && index !== -1) {
    const matches = RE_KEYWORDS.exec(text)

    if (!matches) {
      break
    }

    if (index === 0) {
      indexDescription = matches.index
    }

    index = matches.index

    result.keywords.push({
      name: matches[1],
      description: matches[4]
        .replace(/\s*\*\s/g, '')
        .replace(/\s+/g, ' ')
        .replace(/\s*\*\//g, '')
        .trim()
    })
  }

  result.description = text.substring(0, indexDescription)
    .replace(/\/*\*\s*/g, '')
    .replace(/\n\s+/g, ' ')
    .replace(/\/\s*$/g, '')
    .trim()

  if (index) {
    result.visibility = getVisibility(result.keywords, result.visibility)
    result.describeModel = !!result.keywords.find((keyword) => keyword.name === MODEL_KEYWORD)
  }

  return result
}

const getComment = (property, defaultVisibility) => {
  defaultVisibility = defaultVisibility || DEFAULT_VISIBILITY

  let lastComment = null

  if (property.leadingComments) {
    lastComment = property.leadingComments.pop().value
  }

  if (property.trailingComments) {
    lastComment = property.trailingComments.pop().value
  }

  if (lastComment) {
    return parseComment(lastComment, defaultVisibility)
  }

  return {
    visibility: defaultVisibility,
    description: null,
    keywords: []
  }
}

class NodeFunction {
  constructor (node) {
    Object.assign(this, node)
  }
}

const value = (property) => {
  /* istanbul ignore if */
  if (property.key.type === 'Literal') {
    property.key.name = property.key.value
  }

  switch (property.value.type) {
    case 'Literal':
      return { [property.key.name]: property.value.value }

    case 'Identifier':
      return { [property.key.name]: property.value.name }

    case 'ObjectExpression':
      return { [property.key.name]: values(property) }

    case 'FunctionExpression':
    case 'ArrowFunctionExpression':
      return { [property.key.name]: new NodeFunction(property.value) }
  }

  return { [property.key.name]: property.value }
}

const values = (entry) => {
  const values = {}

  entry.value.properties.forEach((property) => {
    if (property.value.type === 'ObjectExpression') {
      Object.assign(values, {
        [property.key.name]: value(property)
      })
    } else {
      Object.assign(values, value(property))
    }
  })

  return values
}

const tokensInterval = (tokens, range) => {
  return tokens.filter((item) => {
    return item.range[0] > range[0] && item.range[1] < range[1]
  })
}

const getIdentifierValue = (tokens, identifierName, rangeLimit) => {
  const range = [ tokens[0].range[0], rangeLimit ]
  const searchingTokens = tokensInterval(tokens, range).reverse()
  const tokenIndex = searchingTokens.findIndex((item, i, array) => {
    if (item.type === 'Identifier' && item.value === identifierName) {
      const nextToken = array[i - 1]

      return nextToken.type === 'Punctuator' && nextToken.value === '='
    }

    return false
  })

  /* istanbul ignore if */
  if (tokenIndex === -1) {
    return null
  }

  const valueToken = searchingTokens[tokenIndex - 2]

  switch (valueToken.type) {
    case 'String':
      return valueToken.value.replace(/['"]/g, '')

    case 'Identifier':
      return getIdentifierValue(
        tokens, valueToken.value, valueToken.range[0])
  }

  /* istanbul ignore next */
  return null
}

const unCamelcase = (text) => {
  const chars = []

  text.split('').forEach((char) => {
    if (char === char.toUpperCase()) {
      char = char.toLowerCase()

      if (chars.length) {
        chars.push('-')
      }
    }

    chars.push(char)
  })

  return chars.join('')
}

const first = (o) => o[Object.keys(o)[0]]

const parseOptions = (options) => {
  options = options || {}

  /* istanbul ignore if */
  if (!options.source) {
    throw new Error('options.source.filename is required')
  }

  /* istanbul ignore if */
  if (!options.source.script) {
    throw new Error('options.source.script is required')
  }
}

class Parser extends EventEmitter {
  constructor (options) {
    parseOptions(options)

    for (let key in DEFAULT_OPTIONS) {
      options[key] = DEFAULT_OPTIONS[key]
    }

    super()

    this.ast = espree.parse(options.source.script, options)
    this.componentName = null
    this.template = options.source.template
    this.filename = options.filename
    this.eventsEmmited = {}
    this.defaultMethodVisibility = options.defaultMethodVisibility
  }

  extractProperties (property) {
    switch (property.value.type) {
      case 'ObjectExpression':
        property.value.properties.forEach((p) => {
          let defaultVisibility = DEFAULT_VISIBILITY

          if (property.key.name === 'methods' && this.defaultMethodVisibility) {
            defaultVisibility = this.defaultMethodVisibility
          }

          const entry = getComment(p, defaultVisibility)

          entry.value = value(p)
          entry.name = Object.keys(entry.value)[0]
          entry.value = entry.value[entry.name]

          if (entry.value instanceof NodeFunction) {
            entry.params = entry.value.params
          } else if (property.key.name === 'props') {
            if (entry.describeModel) {
              entry.name = 'v-model'
            } else {
              entry.name = unCamelcase(entry.name)
            }
          }

          delete entry.describeModel

          this.emit(property.key.name, entry)
          this.subWalk(entry.value)
        })
        break

      default:
        const propertyValue = value(property)
        const entryValue = first(propertyValue)

        if (propertyValue.hasOwnProperty('name') && !this.componentName) {
          this.componentName = propertyValue.name
          break
        }

        this.subWalk(entryValue)
    }
  }

  walk () {
    process.nextTick(() => {
      this.ast.body.forEach((body) => {
        const entry = getComment(body, this.defaultMethodVisibility)

        if (entry.description) {
          this.emit('description', entry.description)
        }

        if (entry.keywords.length) {
          this.emit('keywords', entry.keywords)
        }

        if (body.type !== 'ExportDefaultDeclaration' && body.type !== 'ExpressionStatement') {
          return
        }

        if (body.declaration != null) {
          body.declaration.properties.forEach((property) => this.extractProperties(property))
        } else if (body.expression != null) {
          body.expression.right.properties.forEach((property) => this.extractProperties(property))
        }

        if (this.componentName === null && this.filename) {
          const filename = path.parse(this.filename).name

          this.componentName = unCamelcase(filename)
        }

        this.emit('name', this.componentName)

        if (this.template) {
          this.parseTemplate()
        }
      })

      this.emit('end')
    })

    return this
  }

  subWalk (entry) {
    if (entry instanceof NodeFunction) {
      const tokens = tokensInterval(this.ast.tokens, entry.range)

      for (let i = 0; i < tokens.length; i++) {
        const token = tokens[i]

        if (token.type === 'Identifier' && token.value === '$emit') {
          if (!tokens[i + 2]) {
            break
          }

          const next = tokens[i + 2]
          const event = {
            name: null,
            description: null,
            visibility: DEFAULT_VISIBILITY,
            keywords: []
          }

          switch (next.type) {
            case 'String':
              event.name = next.value.replace(/['"]/g, '')
              break

            case 'Identifier':
              event.name = getIdentifierValue(tokens, next.value, next.range[0])
              break
          }

          if (!event.name) {
            event.name = '****unhandled-event-name****'
          } else {
            if (this.eventsEmmited.hasOwnProperty(event.name)) {
              continue
            }

            this.eventsEmmited[event.name] = true
          }

          const range = [
            tokens[i - 3].range[1],
            tokens[i - 2].range[0]
          ]

          event.description = this.ast.comments.reverse().find((c) => {
            return c.range[0] > range[0] && c.range[1] < range[1]
          })

          if (event.description) {
            const comment = parseComment(event.description.value)

            event.visibility = comment.visibility
            event.description = comment.description
            event.keywords = comment.keywords
          }

          this.emit('event', event)

          i += 2
        }
      }
    }
  }

  parseTemplate () {
    let lastComment = null

    const parser = new HtmlParser({
      oncomment: (data) => {
        lastComment = data.trim()
      },
      ontext: (text) => {
        if (text.trim()) {
          lastComment = null
        }
      },
      onopentag: (name, attrs) => {
        if (name === 'slot') {
          this.emit('slot', {
            name: attrs.name || 'default',
            description: lastComment
          })

          lastComment = null
        }
      }
    })

    parser.write(this.template)
    parser.end()
  }
}

module.exports = Parser
module.exports.DEFAULT_VISIBILITY= DEFAULT_VISIBILITY
module.exports.getVisibility = getVisibility
module.exports.parseComment = parseComment
module.exports.getComment = getComment
<|endoftext|>


<commit_before>'use strict'

const EventEmitter = require('events')
const espree = require('espree')
const HtmlParser = require('htmlparser2').Parser
const path = require('path')

const RE_TRIM_COMMENT = /\*\s*/g
const RE_VISIBILITY = /@(public|protected|private)/g
const RE_MODEL = /\s?@model\s?/

const DEFAULT_VISIBILITY = 'public'
const DEFAULT_OPTIONS = {
  range: false,
//   comment: true,
  attachComment: true,

  // create a top-level tokens array containing all tokens
  tokens: true,

  // set to 3, 5 (default), 6, 7, or 8 to specify the version of ECMAScript syntax you want to use.
  // You can also set to 2015 (same as 6), 2016 (same as 7), or 2017 (same as 8) to use the year-based naming.
  ecmaVersion: 7,

  // specify which type of script you're parsing (script or module, default is script)
  sourceType: 'module'
}

const hasModelDecoration = (text) => {
  return RE_MODEL.test(text)
}

const getCommentVisibility = (text, defaultVisibility) => {
  const matches = RE_VISIBILITY.exec(text)

  if (matches) {
    return matches[1]
  }

  return defaultVisibility
}

const parseComment = (text, defaultVisibility = DEFAULT_VISIBILITY) => {
  const parsedComment = {
    visibility: getCommentVisibility(text, defaultVisibility),
    describeModel: hasModelDecoration(text),
    description: text.replace(RE_VISIBILITY, '')
  }

  if (parsedComment.describeModel) {
    parsedComment.description = text.replace(RE_MODEL, '').trim()
  }

  parsedComment.description = parsedComment.description
    .replace(RE_TRIM_COMMENT, '').trim()

  return parsedComment
}

const getComment = (property, defaultVisibility = DEFAULT_VISIBILITY) => {
  let lastComment = null

  if (property.leadingComments) {
    lastComment = property.leadingComments.pop().value
  }

  if (property.trailingComments) {
    lastComment = property.trailingComments.pop().value
  }

  if (lastComment) {
    return parseComment(lastComment, defaultVisibility)
  }

  return {
    visibility: defaultVisibility,
    description: null
  }
}

class NodeFunction {
  constructor (node) {
    Object.assign(this, node)
  }
}

const value = (property) => {
  if (property.key.type === 'Literal') {
    property.key.name = property.key.value
  }

  switch (property.value.type) {
    case 'Literal':
      return { [property.key.name]: property.value.value }

    case 'Identifier':
      return { [property.key.name]: property.value.name }

    case 'ObjectExpression':
      return { [property.key.name]: values(property) }

    case 'FunctionExpression':
    case 'ArrowFunctionExpression':
      return { [property.key.name]: new NodeFunction(property.value) }
  }

  return { [property.key.name]: property.value }
}

const values = (entry) => {
  const values = {}

  entry.value.properties.forEach((property) => {
    if (property.value.type === 'ObjectExpression') {
      Object.assign(values, {
        [property.key.name]: value(property)
      })
    } else {
      Object.assign(values, value(property))
    }
  })

  return values
}

const tokensInterval = (tokens, range) => {
  return tokens.filter((item) => {
    return item.range[0] > range[0] && item.range[1] < range[1]
  })
}

const getIdentifierValue = (tokens, identifierName, rangeLimit) => {
  const range = [ tokens[0].range[0], rangeLimit ]
  const searchingTokens = tokensInterval(tokens, range).reverse()
  const tokenIndex = searchingTokens.findIndex((item, i, array) => {
    if (item.type === 'Identifier' && item.value === identifierName) {
      const nextToken = array[i - 1]

      return nextToken.type === 'Punctuator' && nextToken.value === '='
    }

    return false
  })

  /* istanbul ignore if */
  if (tokenIndex === -1) {
    return null
  }

  const valueToken = searchingTokens[tokenIndex - 2]

  switch (valueToken.type) {
    case 'String':
      return valueToken.value.replace(/['"]/g, '')

    case 'Identifier':
      return getIdentifierValue(
        tokens, valueToken.value, valueToken.range[0])
  }

  return null
}

const unCamelcase = (text) => {
  const chars = []

  text.split('').forEach((char) => {
    if (char === char.toUpperCase()) {
      char = char.toLowerCase()

      if (chars.length) {
        chars.push('-')
      }
    }

    chars.push(char)
  })

  return chars.join('')
}

const first = (o) => o[Object.keys(o)[0]]

const parseOptions = (options) => {
  options = options || {}

  /* istanbul ignore if */
  if (!options.source) {
    throw new Error('options.source.filename is required')
  }

  /* istanbul ignore if */
  if (!options.source.script) {
    throw new Error('options.source.script is required')
  }
}

class Parser extends EventEmitter {
  constructor (options) {
    parseOptions(options)

    for (let key in DEFAULT_OPTIONS) {
      options[key] = DEFAULT_OPTIONS[key]
    }

    super()

    this.ast = espree.parse(options.source.script, options)
    this.componentName = null
    this.template = options.source.template
    this.filename = options.filename
    this.eventsEmmited = {}
    this.methodsDefaultPrivate = options.methodsDefaultPrivate
  }

  extractProperties (property) {
    switch (property.value.type) {
      case 'ObjectExpression':
        property.value.properties.forEach((p) => {
          let defaultVisibility = DEFAULT_VISIBILITY
          if (property.key.name === 'methods') {
            defaultVisibility = this.methodsDefaultPrivate ? 'private' : DEFAULT_VISIBILITY
          }
          const entry = getComment(p, defaultVisibility)

          entry.value = value(p)
          entry.name = Object.keys(entry.value)[0]
          entry.value = entry.value[entry.name]

          if (entry.value instanceof NodeFunction) {
            entry.params = entry.value.params
          } else if (property.key.name === 'props') {
            if (entry.describeModel) {
              entry.name = 'v-model'
            } else {
              entry.name = unCamelcase(entry.name)
            }
          }

          delete entry.describeModel

          this.emit(property.key.name, entry)
          this.subWalk(entry.value)
        })
        break

      default:
        const propertyValue = value(property)
        const entryValue = first(propertyValue)

        if (propertyValue.hasOwnProperty('name') && !this.componentName) {
          this.componentName = propertyValue.name
          break
        }

        this.subWalk(entryValue)
    }
  }

  walk () {
    process.nextTick(() => {
      this.ast.body.forEach((body) => {
        const entry = getComment(body)
        const description = entry ? entry.description : null
        this.emit('description', description)

        if (body.type !== 'ExportDefaultDeclaration' && body.type !== 'ExpressionStatement') {
          return
        }

        if (body.declaration != null) {
          body.declaration.properties.forEach((property) => this.extractProperties(property))
        } else if (body.expression != null) {
          body.expression.right.properties.forEach((property) => this.extractProperties(property))
        }

        if (this.componentName === null && this.filename) {
          const filename = path.parse(this.filename).name

          this.componentName = unCamelcase(filename)
        }

        this.emit('name', this.componentName)

        if (this.template) {
          this.parseTemplate()
        }
      })

      this.emit('end')
    })

    return this
  }

  subWalk (entry) {
    if (entry instanceof NodeFunction) {
      const tokens = tokensInterval(this.ast.tokens, entry.range)

      for (let i = 0; i < tokens.length; i++) {
        const token = tokens[i]

        if (token.type === 'Identifier' && token.value === '$emit') {
          if (!tokens[i + 2]) {
            break
          }

          const next = tokens[i + 2]
          const event = {
            name: null,
            description: null,
            visibility: DEFAULT_VISIBILITY
          }

          switch (next.type) {
            case 'String':
              event.name = next.value.replace(/['"]/g, '')
              break

            case 'Identifier':
              event.name = getIdentifierValue(tokens, next.value, next.range[0])
              break
          }

          if (!event.name) {
            event.name = '****unhandled-event-name****'
          } else {
            if (this.eventsEmmited.hasOwnProperty(event.name)) {
              continue
            }

            this.eventsEmmited[event.name] = true
          }

          const range = [
            tokens[i - 3].range[1],
            tokens[i - 2].range[0]
          ]

          event.description = this.ast.comments.reverse().find((c) => {
            return c.range[0] > range[0] && c.range[1] < range[1]
          })

          if (event.description) {
            const comment = parseComment(event.description.value)

            event.visibility = comment.visibility
            event.description = comment.description
          }

          this.emit('event', event)

          i += 2
        }
      }
    }
  }

  parseTemplate () {
    let lastComment = null

    const parser = new HtmlParser({
      oncomment: (data) => {
        lastComment = data.trim()
      },
      ontext: (text) => {
        if (text.trim()) {
          lastComment = null
        }
      },
      onopentag: (name, attrs) => {
        if (name === 'slot') {
          this.emit('slot', {
            name: attrs.name || 'default',
            description: lastComment
          })

          lastComment = null
        }
      }
    })

    parser.write(this.template)
    parser.end()
  }
}

module.exports = Parser
<commit_msg>clean up<commit_after>'use strict'

const EventEmitter = require('events')
const espree = require('espree')
const HtmlParser = require('htmlparser2').Parser
const path = require('path')

const RE_TRIM_COMMENT = /\*\s*/g
const RE_VISIBILITY = /@(public|protected|private)/g
const RE_MODEL = /\s?@model\s?/

const DEFAULT_VISIBILITY = 'public'
const DEFAULT_OPTIONS = {
  range: false,
//   comment: true,
  attachComment: true,

  // create a top-level tokens array containing all tokens
  tokens: true,

  // set to 3, 5 (default), 6, 7, or 8 to specify the version of ECMAScript syntax you want to use.
  // You can also set to 2015 (same as 6), 2016 (same as 7), or 2017 (same as 8) to use the year-based naming.
  ecmaVersion: 7,

  // specify which type of script you're parsing (script or module, default is script)
  sourceType: 'module'
}

const hasModelDecoration = (text) => {
  return RE_MODEL.test(text)
}

const getCommentVisibility = (text, defaultVisibility) => {
  const matches = RE_VISIBILITY.exec(text)

  if (matches) {
    return matches[1]
  }

  return defaultVisibility
}

const parseComment = (text, defaultVisibility = DEFAULT_VISIBILITY) => {
  const parsedComment = {
    visibility: getCommentVisibility(text, defaultVisibility),
    describeModel: hasModelDecoration(text),
    description: text.replace(RE_VISIBILITY, '')
  }

  if (parsedComment.describeModel) {
    parsedComment.description = text.replace(RE_MODEL, '').trim()
  }

  parsedComment.description = parsedComment.description
    .replace(RE_TRIM_COMMENT, '').trim()

  return parsedComment
}

const getComment = (property, defaultVisibility = DEFAULT_VISIBILITY) => {
  let lastComment = null

  if (property.leadingComments) {
    lastComment = property.leadingComments.pop().value
  }

  if (property.trailingComments) {
    lastComment = property.trailingComments.pop().value
  }

  if (lastComment) {
    return parseComment(lastComment, defaultVisibility)
  }

  return {
    visibility: defaultVisibility,
    description: null
  }
}

class NodeFunction {
  constructor (node) {
    Object.assign(this, node)
  }
}

const value = (property) => {
  if (property.key.type === 'Literal') {
    property.key.name = property.key.value
  }

  switch (property.value.type) {
    case 'Literal':
      return { [property.key.name]: property.value.value }

    case 'Identifier':
      return { [property.key.name]: property.value.name }

    case 'ObjectExpression':
      return { [property.key.name]: values(property) }

    case 'FunctionExpression':
    case 'ArrowFunctionExpression':
      return { [property.key.name]: new NodeFunction(property.value) }
  }

  return { [property.key.name]: property.value }
}

const values = (entry) => {
  const values = {}

  entry.value.properties.forEach((property) => {
    if (property.value.type === 'ObjectExpression') {
      Object.assign(values, {
        [property.key.name]: value(property)
      })
    } else {
      Object.assign(values, value(property))
    }
  })

  return values
}

const tokensInterval = (tokens, range) => {
  return tokens.filter((item) => {
    return item.range[0] > range[0] && item.range[1] < range[1]
  })
}

const getIdentifierValue = (tokens, identifierName, rangeLimit) => {
  const range = [ tokens[0].range[0], rangeLimit ]
  const searchingTokens = tokensInterval(tokens, range).reverse()
  const tokenIndex = searchingTokens.findIndex((item, i, array) => {
    if (item.type === 'Identifier' && item.value === identifierName) {
      const nextToken = array[i - 1]

      return nextToken.type === 'Punctuator' && nextToken.value === '='
    }

    return false
  })

  /* istanbul ignore if */
  if (tokenIndex === -1) {
    return null
  }

  const valueToken = searchingTokens[tokenIndex - 2]

  switch (valueToken.type) {
    case 'String':
      return valueToken.value.replace(/['"]/g, '')

    case 'Identifier':
      return getIdentifierValue(
        tokens, valueToken.value, valueToken.range[0])
  }

  return null
}

const unCamelcase = (text) => {
  const chars = []

  text.split('').forEach((char) => {
    if (char === char.toUpperCase()) {
      char = char.toLowerCase()

      if (chars.length) {
        chars.push('-')
      }
    }

    chars.push(char)
  })

  return chars.join('')
}

const first = (o) => o[Object.keys(o)[0]]

const parseOptions = (options) => {
  options = options || {}

  /* istanbul ignore if */
  if (!options.source) {
    throw new Error('options.source.filename is required')
  }

  /* istanbul ignore if */
  if (!options.source.script) {
    throw new Error('options.source.script is required')
  }
}

class Parser extends EventEmitter {
  constructor (options) {
    parseOptions(options)

    for (let key in DEFAULT_OPTIONS) {
      options[key] = DEFAULT_OPTIONS[key]
    }

    super()

    this.ast = espree.parse(options.source.script, options)
    this.componentName = null
    this.template = options.source.template
    this.filename = options.filename
    this.eventsEmmited = {}
    this.methodsDefaultPrivate = options.methodsDefaultPrivate
  }

  extractProperties (property) {
    switch (property.value.type) {
      case 'ObjectExpression':
        property.value.properties.forEach((p) => {
          let defaultVisibility = DEFAULT_VISIBILITY
          if (property.key.name === 'methods' && this.methodsDefaultPrivate) {
            defaultVisibility = 'private'
          }
          const entry = getComment(p, defaultVisibility)

          entry.value = value(p)
          entry.name = Object.keys(entry.value)[0]
          entry.value = entry.value[entry.name]

          if (entry.value instanceof NodeFunction) {
            entry.params = entry.value.params
          } else if (property.key.name === 'props') {
            if (entry.describeModel) {
              entry.name = 'v-model'
            } else {
              entry.name = unCamelcase(entry.name)
            }
          }

          delete entry.describeModel

          this.emit(property.key.name, entry)
          this.subWalk(entry.value)
        })
        break

      default:
        const propertyValue = value(property)
        const entryValue = first(propertyValue)

        if (propertyValue.hasOwnProperty('name') && !this.componentName) {
          this.componentName = propertyValue.name
          break
        }

        this.subWalk(entryValue)
    }
  }

  walk () {
    process.nextTick(() => {
      this.ast.body.forEach((body) => {
        const entry = getComment(body)
        const description = entry ? entry.description : null
        this.emit('description', description)

        if (body.type !== 'ExportDefaultDeclaration' && body.type !== 'ExpressionStatement') {
          return
        }

        if (body.declaration != null) {
          body.declaration.properties.forEach((property) => this.extractProperties(property))
        } else if (body.expression != null) {
          body.expression.right.properties.forEach((property) => this.extractProperties(property))
        }

        if (this.componentName === null && this.filename) {
          const filename = path.parse(this.filename).name

          this.componentName = unCamelcase(filename)
        }

        this.emit('name', this.componentName)

        if (this.template) {
          this.parseTemplate()
        }
      })

      this.emit('end')
    })

    return this
  }

  subWalk (entry) {
    if (entry instanceof NodeFunction) {
      const tokens = tokensInterval(this.ast.tokens, entry.range)

      for (let i = 0; i < tokens.length; i++) {
        const token = tokens[i]

        if (token.type === 'Identifier' && token.value === '$emit') {
          if (!tokens[i + 2]) {
            break
          }

          const next = tokens[i + 2]
          const event = {
            name: null,
            description: null,
            visibility: DEFAULT_VISIBILITY
          }

          switch (next.type) {
            case 'String':
              event.name = next.value.replace(/['"]/g, '')
              break

            case 'Identifier':
              event.name = getIdentifierValue(tokens, next.value, next.range[0])
              break
          }

          if (!event.name) {
            event.name = '****unhandled-event-name****'
          } else {
            if (this.eventsEmmited.hasOwnProperty(event.name)) {
              continue
            }

            this.eventsEmmited[event.name] = true
          }

          const range = [
            tokens[i - 3].range[1],
            tokens[i - 2].range[0]
          ]

          event.description = this.ast.comments.reverse().find((c) => {
            return c.range[0] > range[0] && c.range[1] < range[1]
          })

          if (event.description) {
            const comment = parseComment(event.description.value)

            event.visibility = comment.visibility
            event.description = comment.description
          }

          this.emit('event', event)

          i += 2
        }
      }
    }
  }

  parseTemplate () {
    let lastComment = null

    const parser = new HtmlParser({
      oncomment: (data) => {
        lastComment = data.trim()
      },
      ontext: (text) => {
        if (text.trim()) {
          lastComment = null
        }
      },
      onopentag: (name, attrs) => {
        if (name === 'slot') {
          this.emit('slot', {
            name: attrs.name || 'default',
            description: lastComment
          })

          lastComment = null
        }
      }
    })

    parser.write(this.template)
    parser.end()
  }
}

module.exports = Parser
<|endoftext|>


<commit_before>/*globals $, define */

// This is for displaying and editing attributes of different types
// They will be able to:
//  - render
//  - edit
define([
], function(
) {
    var Field = function(parentEl, name, value, width, y) {
        this.$parent = parentEl;
        this.name = name;
        this.value = value;
        this.createLabel(width, y);
        this.createContent(width, y, this.value);
    };

    Field.PADDING = 12;
    Field.BUTTON_MARGIN = 12;
    Field.prototype.EMPTY_MSG = '<none>';
    Field.prototype.createLabel = function(width, y) {
        // Attribute name
        var leftCol = -width/2 + Field.PADDING;

        this.$label = this.$parent.append('text')
            .attr('y', y)
            .attr('x', leftCol)
            .attr('font-style', 'italic')  // FIXME: move this to css
            .attr('class', 'attr-title')
            .attr('text-anchor', 'start')
            .attr('dominant-baseline', 'middle')
            .text(`${this.name}: `)
            .on('click', () => this.onLabelClick());
    };

    Field.prototype.isEmpty = function() {
        return this.value === undefined || this.value === null || this.value === '';
    };

    Field.prototype.createContent = function(width, y, content) {
        var x = width/2 - Field.PADDING;

        if (this.hasIcon()) {
            x -= Field.BUTTON_MARGIN;
        }

        content = !this.isEmpty() ? content.toString() : this.EMPTY_MSG;
        this.$content = this.$parent.append('text')
            .attr('y', y)
            .attr('x', x)
            .attr('text-anchor', 'end')  // FIXME: move this to css
            .attr('dominant-baseline', 'middle')
            .text(content)
            .on('click', this.onClick.bind(this));

        if (this.isEmpty()) {
            this.$content.attr('font-style', 'italic');
        }
    };

    Field.prototype.setValue = function(value) {
        this.value = value;
        this.$content.text(value);
    };

    Field.prototype.hasIcon = function() {
        return !!this.$icon;
    };

    Field.prototype.onLabelClick =
    Field.prototype.onClick = function() {
        // Override in subclass
    };

    Field.prototype.width = function() {
        var elements = [this.$label[0][0]];

        if (this.$content) {  // may not be loaded in async setters
            elements.push(this.$content[0][0]);
        }

        if (this.$icon) {
            elements.push(this.$icon[0]);
        }

        return elements.map(el => el.getBoundingClientRect().width)
            .reduce((a, b) => a+b, 0);
    };

    Field.prototype.destroy = function() {
        if (this.$icon) {
            this.$icon.remove();
        }
    };

    Field.prototype.createIcon = function(glyphicon, opts) {
        var html = this.$content[0][0],
            position = html.getBoundingClientRect(),
            parentHtml = $('body'),
            icon = $('<span/>'),
            container;

        opts = opts || {};
        container = opts.container || $('<div/>');
        container.css('top', position.top);
        container.css('left', position.right + 5);
        container.css('position', 'absolute');
        container.attr('id', 'download-file-icon');
        if (opts.url) {
            container.attr('href', opts.url);
        }

        if (opts.zoom) {
            icon.css('zoom', (opts.zoom*100)+'%');
        }

        icon.attr('class', 'glyphicon ' + glyphicon);
        container.append(icon);
        $(parentHtml).append(container);

        this.$icon = container;
        return container;
    };

    Field.prototype.render = function(zoom) {
        if (this.$icon) {
            this.$icon.remove();
        }
        this.createIcon('glyphicon-remove', {zoom: zoom});
    };

    return Field;
});
<commit_msg>Added class for attribute content. Fixes #207 (#208)<commit_after>/*globals $, define */

// This is for displaying and editing attributes of different types
// They will be able to:
//  - render
//  - edit
define([
], function(
) {
    var Field = function(parentEl, name, value, width, y) {
        this.$parent = parentEl;
        this.name = name;
        this.value = value;
        this.createLabel(width, y);
        this.createContent(width, y, this.value);
    };

    Field.PADDING = 12;
    Field.BUTTON_MARGIN = 12;
    Field.prototype.EMPTY_MSG = '<none>';
    Field.prototype.createLabel = function(width, y) {
        // Attribute name
        var leftCol = -width/2 + Field.PADDING;

        this.$label = this.$parent.append('text')
            .attr('y', y)
            .attr('x', leftCol)
            .attr('font-style', 'italic')  // FIXME: move this to css
            .attr('class', 'attr-title')
            .attr('text-anchor', 'start')
            .attr('dominant-baseline', 'middle')
            .text(`${this.name}: `)
            .on('click', () => this.onLabelClick());
    };

    Field.prototype.isEmpty = function() {
        return this.value === undefined || this.value === null || this.value === '';
    };

    Field.prototype.createContent = function(width, y, content) {
        var x = width/2 - Field.PADDING;

        if (this.hasIcon()) {
            x -= Field.BUTTON_MARGIN;
        }

        content = !this.isEmpty() ? content.toString() : this.EMPTY_MSG;
        this.$content = this.$parent.append('text')
            .attr('y', y)
            .attr('x', x)
            .attr('class', 'attr-content')
            .attr('text-anchor', 'end')  // FIXME: move this to css
            .attr('dominant-baseline', 'middle')
            .text(content)
            .on('click', this.onClick.bind(this));

        if (this.isEmpty()) {
            this.$content.attr('font-style', 'italic');
        }
    };

    Field.prototype.setValue = function(value) {
        this.value = value;
        this.$content.text(value);
    };

    Field.prototype.hasIcon = function() {
        return !!this.$icon;
    };

    Field.prototype.onLabelClick =
    Field.prototype.onClick = function() {
        // Override in subclass
    };

    Field.prototype.width = function() {
        var elements = [this.$label[0][0]];

        if (this.$content) {  // may not be loaded in async setters
            elements.push(this.$content[0][0]);
        }

        if (this.$icon) {
            elements.push(this.$icon[0]);
        }

        return elements.map(el => el.getBoundingClientRect().width)
            .reduce((a, b) => a+b, 0);
    };

    Field.prototype.destroy = function() {
        if (this.$icon) {
            this.$icon.remove();
        }
    };

    Field.prototype.createIcon = function(glyphicon, opts) {
        var html = this.$content[0][0],
            position = html.getBoundingClientRect(),
            parentHtml = $('body'),
            icon = $('<span/>'),
            container;

        opts = opts || {};
        container = opts.container || $('<div/>');
        container.css('top', position.top);
        container.css('left', position.right + 5);
        container.css('position', 'absolute');
        container.attr('id', 'download-file-icon');
        if (opts.url) {
            container.attr('href', opts.url);
        }

        if (opts.zoom) {
            icon.css('zoom', (opts.zoom*100)+'%');
        }

        icon.attr('class', 'glyphicon ' + glyphicon);
        container.append(icon);
        $(parentHtml).append(container);

        this.$icon = container;
        return container;
    };

    Field.prototype.render = function(zoom) {
        if (this.$icon) {
            this.$icon.remove();
        }
        this.createIcon('glyphicon-remove', {zoom: zoom});
    };

    return Field;
});
<|endoftext|>


<commit_before>var parse5 = require('parse5')
var parser = new parse5.Parser(null, { locationInfo: true })

module.exports = function (content) {
  this.cacheable()
  var cb = this.async()

  var components = {}
  var fragment = parser.parseFragment(content)

  function parseComponent (fragment) {

    var output = {
      template: [],
      style: [],
      script: [],
      styleImports: [],
      hasLocalStyles: false
    }

    fragment.childNodes.forEach(function (node) {
      var type = node.tagName
      var lang = getAttribute(node, 'lang')
      var src = getAttribute(node, 'src')
      var scoped = getAttribute(node, 'scoped') != null

      if (scoped) output.hasLocalStyles = true

      if (type === 'component') {
        throw new Error(
          '[vue-loader] Nested component is not supported.'
        )
      }

      if (src) {
        if (type !== 'style') {
          throw new Error(
            '[vue-loader] src import is only supported for <style> tags.'
          )
        }
        output.styleImports.push({
          src: src,
          lang: lang
        })
        return output
      }

      if (!node.childNodes || !node.childNodes.length) {
        return output
      }

      if (!output[type]) {
        return output
      }

      if (
        (type === 'script' || type === 'template') &&
        output[type].length > 0
      ) {
        throw new Error(
          '[vue-loader] Only one <script> or <template> tag is ' +
          'allowed inside a Vue component.'
        )
      }

      // Work around changes in parse5 >= 1.2.0
      if (node.childNodes[0].nodeName === '#document-fragment') {
        node = node.childNodes[0]
        if (!node.childNodes.length) {
          return output
        }
      }

      var start = node.childNodes[0].__location.start
      var end = node.childNodes[node.childNodes.length - 1].__location.end
      output[type].push({
        lang: lang,
        content: content.substring(start, end).trim()
      })
    })

    return output
  }

  // multi components
  function isMulti (fragment) {

    var multi = false

    fragment.childNodes.forEach(function (node) {
      if (node.tagName === 'component') multi = true
    })

    return multi
  }

  try {
    if (isMulti(fragment)) {

      components._multiComponents_ = true

      fragment.childNodes.forEach(function (node) {
        var type = node.tagName
        var requires = splitRequires(getAttribute(node, 'requires'))

        if (type === 'component') {
          var name = getAttribute(node, 'name')

          // You must specify a name for component
          if (!name || !name.length) {
            throw new Error(
              '[vue-loader] You must specify a name attribute for component tag.'
            )
          }

          if (name === '_multiComponents_') {
            throw new Error(
              "[vue-loader] Can not use `_multiComponents_` as component's name. It is reserved for internal use."
            )
          }

          components[name] = { component: parseComponent(node), requires: requires }

        }

        if (type === 'style' || type === 'template' || type === 'script') {
          throw new Error(
            '[vue-loader] Nested component is not supported.'
          )
        }

      })

    } else {
      components = parseComponent(fragment)
    }
  } catch (err) {
    return cb(err)
  }

  cb(null, 'module.exports = ' + JSON.stringify(components))
}

function splitRequires (string) {

  var result = []

  if (string && string.length > 0) {
    string.split(/[ ,]+/).forEach(function (s) {
      if (s && s.length > 0) result.push(s)
    })
  }
  return result
}

function getAttribute (node, name) {
  if (node.attrs) {
    var i = node.attrs.length
    var attr
    while (i--) {
      attr = node.attrs[i]
      if (attr.name === name) {
        return attr.value
      }
    }
  }
}
<commit_msg>fix scoped styles<commit_after>var parse5 = require('parse5')
var parser = new parse5.Parser(null, { locationInfo: true })

module.exports = function (content) {
  this.cacheable()
  var cb = this.async()

  var components = {}
  var fragment = parser.parseFragment(content)

  function parseComponent (fragment) {

    var output = {
      template: [],
      style: [],
      script: [],
      styleImports: [],
      hasLocalStyles: false
    }

    fragment.childNodes.forEach(function (node) {
      var type = node.tagName
      var lang = getAttribute(node, 'lang')
      var src = getAttribute(node, 'src')
      var scoped = getAttribute(node, 'scoped') != null

      if (scoped) output.hasLocalStyles = true

      if (type === 'component') {
        throw new Error(
          '[vue-loader] Nested component is not supported.'
        )
      }

      if (src) {
        if (type !== 'style') {
          throw new Error(
            '[vue-loader] src import is only supported for <style> tags.'
          )
        }
        output.styleImports.push({
          src: src,
          scoped: scoped,
          lang: lang
        })
        return output
      }

      if (!node.childNodes || !node.childNodes.length) {
        return output
      }

      if (!output[type]) {
        return output
      }

      if (
        (type === 'script' || type === 'template') &&
        output[type].length > 0
      ) {
        throw new Error(
          '[vue-loader] Only one <script> or <template> tag is ' +
          'allowed inside a Vue component.'
        )
      }

      // Work around changes in parse5 >= 1.2.0
      if (node.childNodes[0].nodeName === '#document-fragment') {
        node = node.childNodes[0]
        if (!node.childNodes.length) {
          return output
        }
      }

      var start = node.childNodes[0].__location.start
      var end = node.childNodes[node.childNodes.length - 1].__location.end
      output[type].push({
        lang: lang,
        scoped: scoped,
        content: content.substring(start, end).trim()
      })
    })

    return output
  }

  // multi components
  function isMulti (fragment) {

    var multi = false

    fragment.childNodes.forEach(function (node) {
      if (node.tagName === 'component') multi = true
    })

    return multi
  }

  try {
    if (isMulti(fragment)) {

      components._multiComponents_ = true

      fragment.childNodes.forEach(function (node) {
        var type = node.tagName
        var requires = splitRequires(getAttribute(node, 'requires'))

        if (type === 'component') {
          var name = getAttribute(node, 'name')

          // You must specify a name for component
          if (!name || !name.length) {
            throw new Error(
              '[vue-loader] You must specify a name attribute for component tag.'
            )
          }

          if (name === '_multiComponents_') {
            throw new Error(
              "[vue-loader] Can not use `_multiComponents_` as component's name. It is reserved for internal use."
            )
          }

          components[name] = { component: parseComponent(node), requires: requires }

        }

        if (type === 'style' || type === 'template' || type === 'script') {
          throw new Error(
            '[vue-loader] Nested component is not supported.'
          )
        }

      })

    } else {
      components = parseComponent(fragment)
    }
  } catch (err) {
    return cb(err)
  }

  cb(null, 'module.exports = ' + JSON.stringify(components))
}

function splitRequires (string) {

  var result = []

  if (string && string.length > 0) {
    string.split(/[ ,]+/).forEach(function (s) {
      if (s && s.length > 0) result.push(s)
    })
  }
  return result
}

function getAttribute (node, name) {
  if (node.attrs) {
    var i = node.attrs.length
    var attr
    while (i--) {
      attr = node.attrs[i]
      if (attr.name === name) {
        return attr.value
      }
    }
  }
}
<|endoftext|>


<commit_before>/*!
 * comment.js
 * Copyright (c) 2014 Denis Ciccale (@tdecs)
 * Released under the MIT license
 * https://github.com/dciccale/comment.js/blob/master/LICENSE.txt
 */

'use strict';

var path = require('path');

var tags = require('./tags/tags');

var REGEX_START_COMMENT = /^\s*\/\*\\\s*$/;
var REGEX_END_COMMENT = /^\s*\\\*\/\s*$/;
var REGEX_ROW_DATA = /^\s*(\S)(?:(?!\n)\s(.*))?$/;
var REGEX_LINES = /\n/;

var Parser = function (options) {
  this.tags = tags;
  this.filemap = options.filemap;
  this.section = {
    data: {},
    current: [],
    prev: [],
    mode: ''
  };
  this.sections = [];
  this.commentmap = {};
  this.toc = [];
  this.utoc = {};
  this.tocData = {};
  this.blockData = {};
  this.lvl = [];
  this.root = {};
  this.pointer = null;
};

Parser.prototype.parse = function (filemap) {
  filemap = filemap || this.filemap;

  this.transform(this.extract(filemap));

  return {
    docsname: this.docsname,
    sections: this.sections,
    toc: this.toc
  };
};

// Extract all comment blocks from a file
Parser.prototype.extract = function (filemap) {
  var filename, lines, l, commentlines, comment, i, linenum, line, content, docsname;

  filemap = filemap || this.filemap;

  for (filename in filemap) {

    // The docs name uses the first file name (to improve)
    docsname = docsname || filename;

    // Normalize line endings
    content = filemap[filename].replace(/\r\n?/gm, '\n');

    lines = content.split(REGEX_LINES);
    l = lines.length;

    // Parse each line
    for (i = 0; i < l; i++) {
      line = lines[i];

      if (REGEX_START_COMMENT.test(line)) {
        commentlines = [];

        while (i < l && !REGEX_END_COMMENT.test(line)) {
          commentlines.push(line);
          i++;
          line = lines[i];
          linenum = i + 2;
        }

        // Remove starting comment /*\
        commentlines.shift();

        comment = commentlines.join('\n');
        this.commentmap[filename] = this.commentmap[filename] || [];
        this.commentmap[filename].push({comment: comment, line: linenum, filename: filename});
      }
    }
  }

  this.docsname = path.basename(docsname);

  return this.commentmap;
};

// Process a full comment block, line by line
Parser.prototype.processBlock = function (block) {
  var blocklines = block.comment.split(REGEX_LINES);
  var l = blocklines.length;
  var firstline = false;
  var i, j, ll, line, data, symbol, value, tag, title;

  for (i = 0; i < l; i++) {
    line = blocklines[i];
    data = line.match(REGEX_ROW_DATA);

    if (i === 0) {
      firstline = true;
      this.pointer = this.root;
    }

    if (data) {
      symbol = data[1];
      value = data[2];

      if (symbol === tags.get('text') && firstline) {
        firstline = false;

        // Get namespaces
        title = value.split('.');
        ll = title.length;
        for (j = 0; j < ll; j++) {
          this.pointer = this.pointer[title[j]] = this.pointer[title[j]] || {};
        }

        this.section.data = {
          name: value,
          title: value.replace(/\./g, '-'),
          line: block.line,
          filename: path.basename(block.filename),
          srclink: path.basename(block.filename, path.extname(block.filename)),
          level: ++title.length,
        };

        this.section.current = this.section.prev = [this.section.data];

      } else {
        tag = tags.get(symbol);

        // Change the mode when not matching the current one
        if (this.section.mode !== tag.name) {
          this.section.current = this.section.prev;
        }

        // Process the value
        tag.process(value, this.section);

        this.section.mode = tag.name;
      }
    }
  }

  // Map the section data by name to generate the toc later
  this.tocData[this.section.data.name] = this.section.data;

  // Map each section by name to be able to order it later according the toc
  this.blockData[this.section.data.name] = this.section.prev;
};

Parser.prototype.transform = function (commentmap) {
  var file, blocks, i, l;

  commentmap = commentmap || this.commentmap;

  for (file in commentmap) {
    blocks = commentmap[file];

    // Process all comment blocks for the current file
    l = blocks.length;
    for (i = 0; i < l; i++) {
      this.processBlock(blocks[i]);
    }

    // Generate the toc
    this.generateTOC(this.root);
  }
};

// Generate the toc
Parser.prototype.generateTOC = function (pointer) {

  var levels = [];
  var brackets = '';
  var i, l, node, level, name, sectionData, isMethod, indent;

  for (node in pointer) {
    levels.push(node);
  }

  // Sort alphabetically and format data
  levels = levels.sort();
  l = levels.length;
  for (i = 0; i < l; i++) {
    level = levels[i];

    this.lvl.push(level);

    name = this.lvl.join('.');
    sectionData = this.tocData[name];
    isMethod = sectionData.type && sectionData.type.indexOf('method') + 1;
    indent = this.lvl.length - 1;

    if (isMethod) {
      if (sectionData.params && sectionData.params.length) {
        if (sectionData.params.length === 1) {
          brackets = '(' + sectionData.params[0].join(', ') + ')';
        } else {
          brackets = '(\u2026)';
        }
      } else {
        brackets = '()';
      }
    }

    sectionData.brackets = brackets;

    // Prevent duplicates
    if (!this.utoc[name]) {
      this.sections.push(this.blockData[name]);
      this.toc.push({
        indent: indent,
        name: name,
        type: sectionData.type,
        brackets: isMethod ? '()' : ''
      });
      this.utoc[name] = 1;
    }

    this.generateTOC(pointer[level]);
    this.lvl.pop();
  }
};

module.exports = Parser;
<commit_msg>clear brackets if it not a method<commit_after>/*!
 * comment.js
 * Copyright (c) 2014 Denis Ciccale (@tdecs)
 * Released under the MIT license
 * https://github.com/dciccale/comment.js/blob/master/LICENSE.txt
 */

'use strict';

var path = require('path');

var tags = require('./tags/tags');

var REGEX_START_COMMENT = /^\s*\/\*\\\s*$/;
var REGEX_END_COMMENT = /^\s*\\\*\/\s*$/;
var REGEX_ROW_DATA = /^\s*(\S)(?:(?!\n)\s(.*))?$/;
var REGEX_LINES = /\n/;

var Parser = function (options) {
  this.tags = tags;
  this.filemap = options.filemap;
  this.section = {
    data: {},
    current: [],
    prev: [],
    mode: ''
  };
  this.sections = [];
  this.commentmap = {};
  this.toc = [];
  this.utoc = {};
  this.tocData = {};
  this.blockData = {};
  this.lvl = [];
  this.root = {};
  this.pointer = null;
};

Parser.prototype.parse = function (filemap) {
  filemap = filemap || this.filemap;

  this.transform(this.extract(filemap));

  return {
    docsname: this.docsname,
    sections: this.sections,
    toc: this.toc
  };
};

// Extract all comment blocks from a file
Parser.prototype.extract = function (filemap) {
  var filename, lines, l, commentlines, comment, i, linenum, line, content, docsname;

  filemap = filemap || this.filemap;

  for (filename in filemap) {

    // The docs name uses the first file name (to improve)
    docsname = docsname || filename;

    // Normalize line endings
    content = filemap[filename].replace(/\r\n?/gm, '\n');

    lines = content.split(REGEX_LINES);
    l = lines.length;

    // Parse each line
    for (i = 0; i < l; i++) {
      line = lines[i];

      if (REGEX_START_COMMENT.test(line)) {
        commentlines = [];

        while (i < l && !REGEX_END_COMMENT.test(line)) {
          commentlines.push(line);
          i++;
          line = lines[i];
          linenum = i + 2;
        }

        // Remove starting comment /*\
        commentlines.shift();

        comment = commentlines.join('\n');
        this.commentmap[filename] = this.commentmap[filename] || [];
        this.commentmap[filename].push({comment: comment, line: linenum, filename: filename});
      }
    }
  }

  this.docsname = path.basename(docsname);

  return this.commentmap;
};

// Process a full comment block, line by line
Parser.prototype.processBlock = function (block) {
  var blocklines = block.comment.split(REGEX_LINES);
  var l = blocklines.length;
  var firstline = false;
  var i, j, ll, line, data, symbol, value, tag, title;

  for (i = 0; i < l; i++) {
    line = blocklines[i];
    data = line.match(REGEX_ROW_DATA);

    if (i === 0) {
      firstline = true;
      this.pointer = this.root;
    }

    if (data) {
      symbol = data[1];
      value = data[2];

      if (symbol === tags.get('text') && firstline) {
        firstline = false;

        // Get namespaces
        title = value.split('.');
        ll = title.length;
        for (j = 0; j < ll; j++) {
          this.pointer = this.pointer[title[j]] = this.pointer[title[j]] || {};
        }

        this.section.data = {
          name: value,
          title: value.replace(/\./g, '-'),
          line: block.line,
          filename: path.basename(block.filename),
          srclink: path.basename(block.filename, path.extname(block.filename)),
          level: ++title.length,
        };

        this.section.current = this.section.prev = [this.section.data];

      } else {
        tag = tags.get(symbol);

        // Change the mode when not matching the current one
        if (this.section.mode !== tag.name) {
          this.section.current = this.section.prev;
        }

        // Process the value
        tag.process(value, this.section);

        this.section.mode = tag.name;
      }
    }
  }

  // Map the section data by name to generate the toc later
  this.tocData[this.section.data.name] = this.section.data;

  // Map each section by name to be able to order it later according the toc
  this.blockData[this.section.data.name] = this.section.prev;
};

Parser.prototype.transform = function (commentmap) {
  var file, blocks, i, l;

  commentmap = commentmap || this.commentmap;

  for (file in commentmap) {
    blocks = commentmap[file];

    // Process all comment blocks for the current file
    l = blocks.length;
    for (i = 0; i < l; i++) {
      this.processBlock(blocks[i]);
    }

    // Generate the toc
    this.generateTOC(this.root);
  }
};

// Generate the toc
Parser.prototype.generateTOC = function (pointer) {

  var levels = [];
  var brackets = '';
  var i, l, node, level, name, sectionData, isMethod, indent;

  for (node in pointer) {
    levels.push(node);
  }

  // Sort alphabetically and format data
  levels = levels.sort();
  l = levels.length;
  for (i = 0; i < l; i++) {
    level = levels[i];

    this.lvl.push(level);

    name = this.lvl.join('.');
    sectionData = this.tocData[name];
    isMethod = sectionData.type && sectionData.type.indexOf('method') + 1;
    indent = this.lvl.length - 1;

    if (isMethod) {
      if (sectionData.params && sectionData.params.length) {
        if (sectionData.params.length === 1) {
          brackets = '(' + sectionData.params[0].join(', ') + ')';
        } else {
          brackets = '(\u2026)';
        }
      } else {
        brackets = '()';
      }
    } else {
      brackets = '';
    }

    sectionData.brackets = brackets;

    // Prevent duplicates
    if (!this.utoc[name]) {
      this.sections.push(this.blockData[name]);
      this.toc.push({
        indent: indent,
        name: name,
        type: sectionData.type,
        brackets: isMethod ? '()' : ''
      });
      this.utoc[name] = 1;
    }

    this.generateTOC(pointer[level]);
    this.lvl.pop();
  }
};

module.exports = Parser;
<|endoftext|>


<commit_before>'use strict';

// glorious streaming json parser, built specifically for the twitter streaming api
// assumptions:
//   1) ninjas are mammals
//   2) tweets come in chunks of text, surrounded by {}'s, separated by line breaks
//   3) only one tweet per chunk
//
//   p = new parser.instance()
//   p.addListener('object', function...)
//   p.receive(data)
//   p.receive(data)
//   ...

var EventEmitter = require('events').EventEmitter;

var Parser = module.exports = function Parser() {
  // Make sure we call our parents constructor
  EventEmitter.call(this);
  this.buffer = '';
  return this;
};

// The parser emits events!
Parser.prototype = Object.create(EventEmitter.prototype);

Parser.END        = '\r\n';
Parser.END_LENGTH = 2;

Parser.prototype.receive = function receive(buffer) {
  this.buffer += buffer.toString('utf8');
  var index, json;

  // We have END?
  while ((index = this.buffer.indexOf(Parser.END)) > -1) {
    json = this.buffer.slice(0, index);
    this.buffer = this.buffer.slice(index + Parser.END_LENGTH);
    if (json.length > 0) {
      try {
        json = JSON.parse(json);
        switch(json.event){
          case 'follow':
            this.emit('follow', json);
            break;
          case 'favorite':
            this.emit('favorite', json);
            break;
          case 'unfavorite':
            this.emit('unfavorite', json);
            break;
          case 'block':
            this.emit('block', json);
            break;
          case 'unblock':
            this.emit('unblock', json);
            break;
          case 'list_created':
            this.emit('list_created', json);
            break;
          case 'list_destroyed':
            this.emit('list_destroyed', json);
            break;
          case 'list_updated':
            this.emit('list_updated', json);
            break;
          case 'list_member_added':
            this.emit('list_member_added', json);
            break;
          case 'list_member_removed':
            this.emit('list_member_removed', json);
            break;
          case 'list_user_subscribed':
            this.emit('list_user_subscribed', json);
            break;
          case 'list_user_unsubscribed':
            this.emit('list_user_unsubscribed', json);
            break;
          case 'user_update':
            this.emit('user_update', json);
            break;
          default:
            this.emit('data', json);
            break;
        }
      } catch (error) {
        error.source = json;
        this.emit('error', error);
      }
    }
  }
};
<commit_msg>Add event 'unfollow' and 'quoted_tweet' received on a stream<commit_after>'use strict';

// glorious streaming json parser, built specifically for the twitter streaming api
// assumptions:
//   1) ninjas are mammals
//   2) tweets come in chunks of text, surrounded by {}'s, separated by line breaks
//   3) only one tweet per chunk
//
//   p = new parser.instance()
//   p.addListener('object', function...)
//   p.receive(data)
//   p.receive(data)
//   ...

var EventEmitter = require('events').EventEmitter;

var Parser = module.exports = function Parser() {
  // Make sure we call our parents constructor
  EventEmitter.call(this);
  this.buffer = '';
  return this;
};

// The parser emits events!
Parser.prototype = Object.create(EventEmitter.prototype);

Parser.END        = '\r\n';
Parser.END_LENGTH = 2;

Parser.prototype.receive = function receive(buffer) {
  this.buffer += buffer.toString('utf8');
  var index, json;

  // We have END?
  while ((index = this.buffer.indexOf(Parser.END)) > -1) {
    json = this.buffer.slice(0, index);
    this.buffer = this.buffer.slice(index + Parser.END_LENGTH);
    if (json.length > 0) {
      try {
        json = JSON.parse(json);
        switch(json.event){
          case 'follow':
            this.emit('follow', json);
            break;
          case 'unfollow':
            this.emit('unfollow', json);
            break;
          case 'favorite':
            this.emit('favorite', json);
            break;
          case 'unfavorite':
            this.emit('unfavorite', json);
            break;
          case 'block':
            this.emit('block', json);
            break;
          case 'unblock':
            this.emit('unblock', json);
            break;
          case 'list_created':
            this.emit('list_created', json);
            break;
          case 'list_destroyed':
            this.emit('list_destroyed', json);
            break;
          case 'list_updated':
            this.emit('list_updated', json);
            break;
          case 'list_member_added':
            this.emit('list_member_added', json);
            break;
          case 'list_member_removed':
            this.emit('list_member_removed', json);
            break;
          case 'list_user_subscribed':
            this.emit('list_user_subscribed', json);
            break;
          case 'list_user_unsubscribed':
            this.emit('list_user_unsubscribed', json);
            break;
          case 'quoted_tweet':
            this.emit('quoted_tweet', json);
            break;
          case 'user_update':
            this.emit('user_update', json);
            break;
          default:
            this.emit('data', json);
            break;
        }
      } catch (error) {
        error.source = json;
        this.emit('error', error);
      }
    }
  }
};
<|endoftext|>