UNPKG

yadda

Version:
555 lines (471 loc) 18.8 kB
'use strict'; var $ = require('../Array'); var fn = require('../fn'); var StringUtils = require('../StringUtils'); var Localisation = require('../localisation'); var FeatureParser = function (options) { /* eslint-disable no-redeclare */ var defaults = { language: Localisation.default, leftPlaceholderChar: '[', rightPlaceholderChar: ']' }; var options = options && options.is_language ? { language: options } : options || defaults; var language = options.language || defaults.language; var left_placeholder_char = options.leftPlaceholderChar || defaults.leftPlaceholderChar; var right_placeholder_char = options.rightPlaceholderChar || defaults.rightPlaceholderChar; /* eslint-enable no-redeclare */ var FEATURE_REGEX = new RegExp('^\\s*' + language.localise('feature') + ':\\s*(.*)', 'i'); var SCENARIO_REGEX = new RegExp('^\\s*' + language.localise('scenario') + ':\\s*(.*)', 'i'); var BACKGROUND_REGEX = new RegExp('^\\s*' + language.localise('background') + ':\\s*(.*)', 'i'); var EXAMPLES_REGEX = new RegExp('^\\s*' + language.localise('examples') + ':', 'i'); var TEXT_REGEX = new RegExp('^(.*)$', 'i'); var SINGLE_LINE_COMMENT_REGEX = new RegExp('^\\s*#'); var MULTI_LINE_COMMENT_REGEX = new RegExp('^\\s*#{3,}'); var BLANK_REGEX = new RegExp('^(\\s*)$'); var DASH_REGEX = new RegExp('(^\\s*[\\|\u2506]?-{3,})'); var SIMPLE_ANNOTATION_REGEX = new RegExp('^\\s*@([^=]*)$'); var NVP_ANNOTATION_REGEX = new RegExp('^\\s*@([^=]*)=(.*)$'); var specification; var comment; this.parse = function (text, next) { reset(); split(text).each(parse_line); return (next && next(specification.export())) || specification.export(); }; function reset() { specification = new Specification(); comment = false; } function split(text) { return $(text.split(/\r\n|\n/)); } function parse_line(line, index) { var match; var line_number = index + 1; try { // eslint-disable-next-line no-return-assign if ((match = MULTI_LINE_COMMENT_REGEX.test(line))) return (comment = !comment); if (comment) return; if ((match = SINGLE_LINE_COMMENT_REGEX.test(line))) return; if ((match = SIMPLE_ANNOTATION_REGEX.exec(line))) return specification.handle('Annotation', { key: StringUtils.trim(match[1]), value: true }, line_number); if ((match = NVP_ANNOTATION_REGEX.exec(line))) return specification.handle('Annotation', { key: StringUtils.trim(match[1]), value: StringUtils.trim(match[2]) }, line_number); if ((match = FEATURE_REGEX.exec(line))) return specification.handle('Feature', match[1], line_number); if ((match = SCENARIO_REGEX.exec(line))) return specification.handle('Scenario', match[1], line_number); if ((match = BACKGROUND_REGEX.exec(line))) return specification.handle('Background', match[1], line_number); if ((match = EXAMPLES_REGEX.exec(line))) return specification.handle('Examples', line_number); if ((match = BLANK_REGEX.exec(line))) return specification.handle('Blank', match[0], line_number); if ((match = DASH_REGEX.exec(line))) return specification.handle('Dash', match[1], line_number); if ((match = TEXT_REGEX.exec(line))) return specification.handle('Text', match[1], line_number); } catch (e) { e.message = 'Error parsing line ' + line_number + ', "' + line + '".\nOriginal error was: ' + e.message; throw e; } } var Handlers = function (handlers) { // eslint-disable-next-line no-redeclare var handlers = handlers || {}; this.register = function (event, handler) { handlers[event] = handler; }; this.unregister = function () { $(Array.prototype.slice.call(arguments)).each(function (event) { delete handlers[event]; }); }; this.find = function (event) { if (!handlers[event.toLowerCase()]) throw new Error(event + ' is unexpected at this time'); return { handle: handlers[event.toLowerCase()] }; }; }; var Specification = function () { var current_element = this; var feature; var annotations = new Annotations(); var handlers = new Handlers({ text: fn.noop, blank: fn.noop, annotation: stash_annotation, feature: start_feature, scenario: start_scenario, background: start_background, }); function stash_annotation(event, annotation) { handlers.unregister('background'); annotations.stash(annotation.key, annotation.value); } function start_feature(event, title) { // eslint-disable-next-line no-return-assign return (feature = new Feature(title, annotations, new Annotations())); } function start_scenario(event, title, line_number) { feature = new Feature(title, new Annotations(), annotations); return feature.on(event, title, line_number); } var start_background = start_scenario; this.handle = function (event, data, line_number) { current_element = current_element.on(event, data, line_number); }; this.on = function (event, data, line_number) { return handlers.find(event).handle(event, data, line_number) || this; }; this.export = function () { if (!feature) throw new Error('A feature must contain one or more scenarios'); return feature.export(); }; }; var Annotations = function () { var annotations = {}; this.stash = function (key, value) { if (/\s/.test(key)) throw new Error('Invalid annotation: ' + key); annotations[key.toLowerCase()] = value; }; this.export = function () { return annotations; }; }; var Feature = function (title, annotations, stashed_annotations) { var description = []; var scenarios = []; var background = new NullBackground(); var handlers = new Handlers({ text: capture_description, blank: fn.noop, annotation: stash_annotation, scenario: start_scenario, background: start_background, }); var _this = this; function start_background(event, title) { background = new Background(title, _this); stashed_annotations = new Annotations(); return background; } function stash_annotation(event, annotation) { handlers.unregister('background', 'text'); stashed_annotations.stash(annotation.key, annotation.value); } function capture_description(event, text) { description.push(StringUtils.trim(text)); } function start_scenario(event, title) { var scenario = new Scenario(title, background, stashed_annotations, _this); scenarios.push(scenario); stashed_annotations = new Annotations(); return scenario; } function validate() { if (scenarios.length === 0) throw new Error('Feature requires one or more scenarios'); } this.on = function (event, data, line_number) { return handlers.find(event).handle(event, data, line_number) || this; }; this.export = function () { validate(); return { title: title, annotations: annotations.export(), description: description, scenarios: $(scenarios) .collect(function (scenario) { return scenario.export(); }) .flatten() .naked(), }; }; }; var Background = function (title, feature) { var steps = []; var blanks = []; var indentation = 0; var handlers = new Handlers({ text: capture_step, blank: fn.noop, annotation: stash_annotation, scenario: start_scenario, }); function capture_step(event, text, line_number) { handlers.register('dash', enable_multiline_step); steps.push(StringUtils.trim(text)); } function enable_multiline_step(event, text, line_number) { handlers.unregister('dash', 'annotation', 'scenario'); handlers.register('text', start_multiline_step); handlers.register('blank', stash_blanks); indentation = StringUtils.indentation(text); } function start_multiline_step(event, text, line_number) { handlers.register('dash', disable_multiline_step); handlers.register('text', continue_multiline_step); handlers.register('blank', stash_blanks); handlers.register('annotation', stash_annotation); handlers.register('scenario', start_scenario); append_to_step(text, '\n'); } function continue_multiline_step(event, text, line_number) { unstash_blanks(); append_to_step(text, '\n'); } function stash_blanks(event, text, line_number) { blanks.push(text); } function unstash_blanks() { if (!blanks.length) return; append_to_step(blanks.join('\n'), '\n'); blanks = []; } function disable_multiline_step(event, text, line_number) { handlers.unregister('dash'); handlers.register('text', capture_step); handlers.register('blank', fn.noop); unstash_blanks(); } function append_to_step(text, prefix) { if (StringUtils.isNotBlank(text) && StringUtils.indentation(text) < indentation) throw new Error('Indentation error'); steps[steps.length - 1] = steps[steps.length - 1] + prefix + StringUtils.rtrim(text.substr(indentation)); } function stash_annotation(event, annotation, line_number) { validate(); return feature.on(event, annotation, line_number); } function start_scenario(event, data, line_number) { validate(); return feature.on(event, data, line_number); } function validate() { if (steps.length === 0) throw new Error('Background requires one or more steps'); } this.on = function (event, data, line_number) { return handlers.find(event).handle(event, data, line_number) || this; }; this.export = function () { validate(); return { steps: steps, }; }; }; var NullBackground = function () { var handlers = new Handlers(); this.on = function (event, data, line_number) { return handlers.find(event).handle(event, data, line_number) || this; }; this.export = function () { return { steps: [], }; }; }; var Scenario = function (title, background, annotations, feature) { var description = []; var steps = []; var blanks = []; var examples; var indentation = 0; var handlers = new Handlers({ text: capture_step, blank: fn.noop, annotation: start_scenario, scenario: start_scenario, examples: start_examples, }); var _this = this; function capture_step(event, text, line_number) { handlers.register('dash', enable_multiline_step); steps.push(StringUtils.trim(text)); } function enable_multiline_step(event, text, line_number) { handlers.unregister('dash', 'annotation', 'scenario', 'examples'); handlers.register('text', start_multiline_step); handlers.register('blank', stash_blanks); indentation = StringUtils.indentation(text); } function start_multiline_step(event, text, line_number) { handlers.register('dash', disable_multiline_step); handlers.register('text', continue_multiline_step); handlers.register('blank', stash_blanks); handlers.register('annotation', start_scenario); handlers.register('scenario', start_scenario); handlers.register('examples', start_examples); append_to_step(text, '\n'); } function continue_multiline_step(event, text, line_number) { unstash_blanks(); append_to_step(text, '\n'); } function stash_blanks(event, text, line_number) { blanks.push(text); } function unstash_blanks() { if (!blanks.length) return; append_to_step(blanks.join('\n'), '\n'); blanks = []; } function disable_multiline_step(event, text, line_number) { handlers.unregister('dash'); handlers.register('text', capture_step); handlers.register('blank', fn.noop); unstash_blanks(); } function append_to_step(text, prefix) { if (StringUtils.isNotBlank(text) && StringUtils.indentation(text) < indentation) throw new Error('Indentation error'); steps[steps.length - 1] = steps[steps.length - 1] + prefix + StringUtils.rtrim(text.substr(indentation)); } function start_scenario(event, data, line_number) { validate(); return feature.on(event, data, line_number); } function start_examples(event, data, line_number) { validate(); examples = new Examples(_this); return examples; } function validate() { if (steps.length === 0) throw new Error('Scenario requires one or more steps'); } this.on = function (event, data, line_number) { return handlers.find(event).handle(event, data, line_number) || this; }; this.export = function () { validate(); var result = { title: title, annotations: annotations.export(), description: description, steps: background.export().steps.concat(steps), }; return examples ? examples.expand(result) : result; }; }; var Examples = function (scenario) { var headings = []; var examples = $(); var annotations = new Annotations(); var handlers = new Handlers({ blank: fn.noop, dash: start_example_table, text: capture_headings, }); function start_example_table(evnet, data, line_number) { handlers.unregister('blank', 'dash'); } function capture_headings(event, data, line_number) { handlers.register('annotation', stash_annotation); handlers.register('text', capture_singleline_fields); handlers.register('dash', enable_multiline_examples); var pos = 1; headings = split(data) .collect(function (column) { var attributes = { text: StringUtils.trim(column), left: pos, indentation: StringUtils.indentation(column) }; pos += column.length + 1; return attributes; }) .naked(); } function stash_annotation(event, annotation, line_number) { handlers.unregister('blank', 'dash'); annotations.stash(annotation.key, annotation.value); } function capture_singleline_fields(event, data, line_number) { handlers.register('dash', end_example_table); handlers.register('blank', end_example_table); examples.push({ annotations: annotations, fields: parse_fields(data, {}) }); add_meta_fields(line_number); annotations = new Annotations(); } function enable_multiline_examples(event, data, line_number) { handlers.register('text', start_capturing_multiline_fields); handlers.register('dash', stop_capturing_multiline_fields); } function start_capturing_multiline_fields(event, data, line_number) { handlers.register('text', continue_capturing_multiline_fields); handlers.register('dash', stop_capturing_multiline_fields); handlers.register('blank', end_example_table); examples.push({ annotations: annotations, fields: parse_fields(data, {}) }); add_meta_fields(line_number); } function continue_capturing_multiline_fields(event, data, line_number) { parse_fields(data, examples.last().fields); } function stop_capturing_multiline_fields(event, data, line_number) { handlers.register('text', start_capturing_multiline_fields); annotations = new Annotations(); } function end_example_table(event, data, line_number) { handlers.unregister('text', 'dash'); handlers.register('blank', fn.noop); handlers.register('annotation', start_scenario); handlers.register('scenario', start_scenario); } function add_meta_fields(line_number) { var fields = examples.last().fields; $(headings).each(function (heading) { fields[heading.text + '.index'] = [examples.length]; fields[heading.text + '.start.line'] = [line_number]; fields[heading.text + '.start.column'] = [heading.left + heading.indentation]; }); } function parse_fields(row, fields) { split(row, headings.length).each(function (field, index) { var column = headings[index].text; var indentation = headings[index].indentation; var text = StringUtils.rtrim(field.substr(indentation)); if (StringUtils.isNotBlank(field) && StringUtils.indentation(field) < indentation) throw new Error('Indentation error'); fields[column] = (fields[column] || []).concat(text); }); return fields; } function split(row, number_of_fields) { var separator = row.indexOf('\u2506') >= 0 ? '\u2506' : '|'; var fields = $(row.split(separator)); if (number_of_fields !== undefined && number_of_fields !== fields.length) { throw new Error('Incorrect number of fields in example table. Expected ' + number_of_fields + ' but found ' + fields.length); } return fields; } function start_scenario(event, data, line_number) { validate(); return scenario.on(event, data, line_number); } function validate() { if (headings.length === 0) throw new Error('Examples table requires one or more headings'); if (examples.length === 0) throw new Error('Examples table requires one or more rows'); } this.on = function (event, data, line_number) { return handlers.find(event).handle(event, data, line_number) || this; }; this.expand = function (scenario) { validate(); return examples .collect(function (example) { return { title: substitute(example.fields, scenario.title), annotations: shallow_merge(example.annotations.export(), scenario.annotations), description: substitute_all(example, scenario.description), steps: substitute_all(example.fields, scenario.steps), }; }) .naked(); }; function shallow_merge() { var result = {}; $(Array.prototype.slice.call(arguments)).each(function (annotations) { for (var key in annotations) { result[key] = annotations[key]; } }); return result; } function substitute_all(example, lines) { return $(lines) .collect(function (line) { return substitute(example, line); }) .naked(); } function substitute(example, line) { for (var heading in example) { line = line.replace(new RegExp('\\' + left_placeholder_char + '\\s*' + heading + '\\s*\\' + right_placeholder_char, 'g'), StringUtils.rtrim(example[heading].join('\n'))); } return line; } }; }; module.exports = FeatureParser;