<commit_before>(function() {
  var slice = Array.prototype.slice;

  function update(array, args) {
    var arrayLength = array.length, length = args.length;
    while (length--) array[arrayLength + length] = args[length];
    return array;
  };

  function merge(array, args) {
    array = slice.call(array, 0);
    return update(array, args);
  };

  Function.prototype.bind = function(context) {
    if (arguments.length < 2 && typeof arguments[0] === 'undefined') {
      return this;
    }
    var __method = this;
    var args = slice.call(arguments, 1);
    return function() {
      var a = merge(args, arguments);
      return __method.apply(context, a);
    }
  }

})();

function nblck_each(array, body, after) {
	$.each(array, function(i) {
		body(this, i);
	});

	after();
};

function nblck_map(array, func, after) {
	var acc = [];

	nblck_each(array, function(elem, index) {
		acc.push(func(elem, index));
	}, function(){
		after(acc);
	});
};

function ScriptletCenter()
{
    this.scriptlets = {};

    this.scriptlets['insert_tag'] = function(context, params, text, move_forward, done)
    {
        var start_tag = '<'+params.tag;
        var move_cursor = false;

        for (var attr in params.attrs) {
            start_tag += ' '+attr+'="' + params.attrs[attr] + '"';
        }

        start_tag += '>';
        var end_tag = '</'+params.tag+'>';

        if(text.length > 0) {
            // tokenize
            var output = '';
            var token = '';
            for(var index=0; index < text.length; index++)
            {
                if (text[index].match(/\s/)) { // whitespace
                    token += text[index];
                }
                else { // character
                    output += token;
                    if(output == token) output += start_tag;
                    token = '';
                    output += text[index];
                }
            }

            if( output[output.length-1] == '\\' ) {
                output = output.substr(0, output.length-1) + end_tag + '\\';
            } else {
                output += end_tag;
            }
            output += token;
        }
        else {
            if(params.nocontent) {
                output = "<"+params.tag +" />";
            }
            else {
                output = start_tag + end_tag;
                move_cursor = true;
            }
        }

        if (move_cursor) {
            move_forward += params.tag.length+2;
        }

        done(output, move_forward);
    }.bind(this);

    this.scriptlets['lineregexp'] = function(context, params, text, move_forward, done) {
		var self = this;

        var exprs = $.map(params.exprs, function(expr) {
            var opts = "g";
            if(expr.length > 2) {
                opts = expr[2];
            } return {
                rx: new RegExp(expr[0], opts),
                repl: expr[1]
                };
        });

        var partial = true;
        if(!text) done(text, move_forward);

        var changed = 0;
        var lines = text.split('\n');

		nblck_map(lines, function(line, index) {
            var old_line = line;
            $(exprs).each(function() {
                var expr = this;
                line = line.replace(expr.rx, expr.repl);
            });

			$progress.html(index);

            if(old_line != line) changed += 1;
            return line;
        }, function(newlines) {
            if(changed > 0) {
                text = newlines.join('\n');
            };

            done(text, move_forward);
		});
    }.bind(this);

    this.scriptlets['fulltextregexp'] = function(context, params, text, move_forward, done) {
		var self = this;

        var exprs = $.map(params.exprs, function(expr) {
            var opts = "mg";
            if(expr.length > 2) {
                opts = expr[2];
            }
            return {
                rx: new RegExp(expr[0], opts),
                repl: expr[1]
                };
        });

        if(!text) done(text, move_forward);
        var original = text;$

		nblck_each(exprs, function(expr, index) {
			$progress.html(600 + index);
            text = text.replace(expr.rx, expr.repl);
        }, function() {
			done(text, move_forward);
		});
    }.bind(this);

    this.scriptlets['macro'] = function(context, params, text, move_forward, done) {
        var self = this;
		var i = 0;

		function next(text, move_forward) {
        	if (i < params.length) {
				var e = params[i];
				i = i + 1;
				self.scriptlets[e[0]](context, e[1], text, move_forward, next);
			}
			else {
				done(text, move_forward);
			}
        };

		next(text, move_forward);
    }.bind(this);

    this.scriptlets['lowercase'] = function(context, params, text, move_forward, done)
    {
        if(!text) done(text, move_forward);

        var lcase = text.toLowerCase();
        var ucase = text.toUpperCase();

        if(lcase == text) repl = ucase; /* was lowercase */
        else if(ucase != text) repl = lcase; /* neither lower- or upper-case */
        else { /* upper case -> camel-case */
            var words = $.map(lcase.split(/\s/), function(word) {
                if(word.length > 0) {
                    return word[0].toUpperCase() + word.slice(1);
                } else {
                    return '';
                }
            });
            text = words.join(' ');
        }

        done(text, move_forward);
    }.bind(this);

    this.scriptlets["insert_stanza"] = function(context, params, text, move_forward, done) {
        if(text) {
            var verses = text.split('\n');
            text = ''; var buf = ''; var ebuf = '';
            var first = true;

            for(var i=0;  i < verses.length; i++) {
                var verse = verses[i].replace(/^\s+/, "").replace(/\s+$/, "");
                if(verse) {
                    text += (buf ? buf + '/\n' : '') + ebuf;
                    buf = (first ? '<strofa>\n' : '') + verses[i];
                    ebuf = '';
                    first = false;
                } else {
                    ebuf += '\n' + verses[i];
                }
            }
            text = text + buf + '\n</strofa>' + ebuf;
        }
        else {
            move_forward += params.tag.length + 2;
        }

        done(text, move_forward);
    }.bind(this);

}

ScriptletCenter.prototype.callInteractive = function(opts) {
	$progress = $('Executing script');
	var self = this;

	/* This won't work, 'cause the JS below might be synchronous :( */
	/* var timer = setTimeout(function() {
	    $.blockUI({message: $progress});
	    timer = null;
	}, 1000); */

	$.blockUI({message: $progress, showOverlay: false});

    var input = self.XMLEditorSelectedText(opts.context);
	self.scriptlets[opts.action](opts.context, opts.extra, input, 0, function(output, move_forward){
	    /*if(timer)
	        clearTimeout(timer);
	    else */
        if (input != output) {
            self.XMLEditorReplaceSelectedText(opts.context, output)
        }
        if (move_forward) {
            try {
                self.XMLEditorMoveCursorForward(opts.context, move_forward)
            }
            catch(e) {}
        }
	    $.unblockUI(); // done
	});
}

ScriptletCenter.prototype.XMLEditorSelectedText = function(editor) {

    return editor.selection();
};

ScriptletCenter.prototype.XMLEditorReplaceSelectedText = function(editor, replacement)
{
	$progress.html("Replacing text");
	editor.replaceSelection(replacement);
};

ScriptletCenter.prototype.XMLEditorMoveCursorForward = function(panel, n) {
    var pos = panel.cursorPosition();
    panel.selectLines(pos.line, pos.character + n);
};

var scriptletCenter;

$(function() {
    scriptletCenter = new ScriptletCenter();
});<commit_msg>insert tag fix<commit_after>(function() {
  var slice = Array.prototype.slice;

  function update(array, args) {
    var arrayLength = array.length, length = args.length;
    while (length--) array[arrayLength + length] = args[length];
    return array;
  };

  function merge(array, args) {
    array = slice.call(array, 0);
    return update(array, args);
  };

  Function.prototype.bind = function(context) {
    if (arguments.length < 2 && typeof arguments[0] === 'undefined') {
      return this;
    }
    var __method = this;
    var args = slice.call(arguments, 1);
    return function() {
      var a = merge(args, arguments);
      return __method.apply(context, a);
    }
  }

})();

function nblck_each(array, body, after) {
	$.each(array, function(i) {
		body(this, i);
	});

	after();
};

function nblck_map(array, func, after) {
	var acc = [];

	nblck_each(array, function(elem, index) {
		acc.push(func(elem, index));
	}, function(){
		after(acc);
	});
};

function ScriptletCenter()
{
    this.scriptlets = {};

    this.scriptlets['insert_tag'] = function(context, params, text, move_forward, done)
    {
        var start_tag = '<'+params.tag;
        var move_cursor = false;

        for (var attr in params.attrs) {
            start_tag += ' '+attr+'="' + params.attrs[attr] + '"';
        }

        start_tag += '>';
        var end_tag = '</'+params.tag+'>';

        if(text.length > 0) {
            // tokenize
            var output = '';
            var token = '';
            for(var index=0; index < text.length; index++)
            {
                if (text[index].match(/\s/)) { // whitespace
                    token += text[index];
                }
                else { // character
                    output += token;
                    if(output == token) output += start_tag;
                    token = '';
                    output += text[index];
                }
            }

            if( output[output.length-1] == '\\' ) {
                output = output.substr(0, output.length-1) + end_tag + '\\';
            } else {
                output += end_tag;
            }
            output += token;
        }
        else {
            if(params.nocontent) {
                output = "<"+params.tag +" />";
            }
            else {
                output = start_tag + end_tag;
                move_cursor = true;
            }
        }

        if (move_cursor) {
            move_forward -= params.tag.length+3;
        }

        done(output, move_forward);
    }.bind(this);

    this.scriptlets['lineregexp'] = function(context, params, text, move_forward, done) {
		var self = this;

        var exprs = $.map(params.exprs, function(expr) {
            var opts = "g";
            if(expr.length > 2) {
                opts = expr[2];
            } return {
                rx: new RegExp(expr[0], opts),
                repl: expr[1]
                };
        });

        var partial = true;
        if(!text) done(text, move_forward);

        var changed = 0;
        var lines = text.split('\n');

		nblck_map(lines, function(line, index) {
            var old_line = line;
            $(exprs).each(function() {
                var expr = this;
                line = line.replace(expr.rx, expr.repl);
            });

			$progress.html(index);

            if(old_line != line) changed += 1;
            return line;
        }, function(newlines) {
            if(changed > 0) {
                text = newlines.join('\n');
            };

            done(text, move_forward);
		});
    }.bind(this);

    this.scriptlets['fulltextregexp'] = function(context, params, text, move_forward, done) {
		var self = this;

        var exprs = $.map(params.exprs, function(expr) {
            var opts = "mg";
            if(expr.length > 2) {
                opts = expr[2];
            }
            return {
                rx: new RegExp(expr[0], opts),
                repl: expr[1]
                };
        });

        if(!text) done(text, move_forward);
        var original = text;$

		nblck_each(exprs, function(expr, index) {
			$progress.html(600 + index);
            text = text.replace(expr.rx, expr.repl);
        }, function() {
			done(text, move_forward);
		});
    }.bind(this);

    this.scriptlets['macro'] = function(context, params, text, move_forward, done) {
        var self = this;
		var i = 0;

		function next(text, move_forward) {
        	if (i < params.length) {
				var e = params[i];
				i = i + 1;
				self.scriptlets[e[0]](context, e[1], text, move_forward, next);
			}
			else {
				done(text, move_forward);
			}
        };

		next(text, move_forward);
    }.bind(this);

    this.scriptlets['lowercase'] = function(context, params, text, move_forward, done)
    {
        if(!text) done(text, move_forward);

        var lcase = text.toLowerCase();
        var ucase = text.toUpperCase();

        if(lcase == text) repl = ucase; /* was lowercase */
        else if(ucase != text) repl = lcase; /* neither lower- or upper-case */
        else { /* upper case -> camel-case */
            var words = $.map(lcase.split(/\s/), function(word) {
                if(word.length > 0) {
                    return word[0].toUpperCase() + word.slice(1);
                } else {
                    return '';
                }
            });
            text = words.join(' ');
        }

        done(text, move_forward);
    }.bind(this);

    this.scriptlets["insert_stanza"] = function(context, params, text, move_forward, done) {
        if(text) {
            var verses = text.split('\n');
            text = ''; var buf = ''; var ebuf = '';
            var first = true;

            for(var i=0;  i < verses.length; i++) {
                var verse = verses[i].replace(/^\s+/, "").replace(/\s+$/, "");
                if(verse) {
                    text += (buf ? buf + '/\n' : '') + ebuf;
                    buf = (first ? '<strofa>\n' : '') + verses[i];
                    ebuf = '';
                    first = false;
                } else {
                    ebuf += '\n' + verses[i];
                }
            }
            text = text + buf + '\n</strofa>' + ebuf;
        }
        else {
            text = "<strofa></strofa>"
            move_forward -= "</strofa>".length;
        }

        done(text, move_forward);
    }.bind(this);

}

ScriptletCenter.prototype.callInteractive = function(opts) {
	$progress = $('Executing script');
	var self = this;

	/* This won't work, 'cause the JS below might be synchronous :( */
	/* var timer = setTimeout(function() {
	    $.blockUI({message: $progress});
	    timer = null;
	}, 1000); */

	$.blockUI({message: $progress, showOverlay: false});

    var input = self.XMLEditorSelectedText(opts.context);
	self.scriptlets[opts.action](opts.context, opts.extra, input, 0, function(output, move_forward){
	    /*if(timer)
	        clearTimeout(timer);
	    else */
        if (input != output) {
            self.XMLEditorReplaceSelectedText(opts.context, output)
        }
        if (move_forward) {
            try {
                self.XMLEditorMoveCursorForward(opts.context, move_forward)
            }
            catch(e) {}
        }
	    $.unblockUI(); // done
	});
}

ScriptletCenter.prototype.XMLEditorSelectedText = function(editor) {

    return editor.selection();
};

ScriptletCenter.prototype.XMLEditorReplaceSelectedText = function(editor, replacement)
{
	$progress.html("Replacing text");
	editor.replaceSelection(replacement);
};

ScriptletCenter.prototype.XMLEditorMoveCursorForward = function(panel, n) {
    var pos = panel.cursorPosition();
    panel.selectLines(pos.line, pos.character + n);
};

var scriptletCenter;

$(function() {
    scriptletCenter = new ScriptletCenter();
});<|endoftext|>


<commit_before>const { AppSetting } = require('../core/app_setting');
const { BagItProfile } = require('./bagit_profile');
const { TagDefinition } = require('./tag_definition');
const { TestUtil } = require('../core/test_util');
const { Util } = require('../core/util');
const fs = require('fs');
const path = require('path');

beforeEach(() => {
    TestUtil.deleteJsonFile('BagItProfile');
});

test('Constructor sets initial properties', () => {
    let profile = new BagItProfile();
    expect(profile.name).toEqual('New BagIt Profile');
    expect(profile.description).toEqual('New custom BagIt profile');

    profile = new BagItProfile('Test Profile', 'Profile for testing');
    expect(profile.name).toEqual('Test Profile');
    expect(profile.description).toEqual('Profile for testing');

    expect(profile.acceptBagItVersion).toEqual(['0.97']);
    expect(profile.acceptSerialization).toEqual(['tar']);
    expect(profile.allowFetchTxt).toEqual(false);
    expect(profile.allowMiscTopLevelFiles).toEqual(false);
    expect(profile.allowMiscDirectories).toEqual(false);
    expect(profile.bagItProfileInfo).not.toBeNull();
    expect(profile.manifestsRequired).toEqual(['sha256']);
    expect(profile.tagManifestsRequired).toEqual([]);
    expect(profile.tags.length).toEqual(16);
    expect(profile.serialization).toEqual('optional');
    expect(profile.baseProfileId).toEqual(null);
    expect(profile.isBuiltIn).toEqual(false);
});

test('validate() catches invalid properties', () => {
    let profile = new BagItProfile();
    profile.id = '';
    profile.name = '';
    profile.acceptBagItVersion = [];
    profile.manifestsRequired = [];
    profile.tags = [];
    profile.serialization = "Cap'n Crunch";
    let result = profile.validate();
    expect(result.isValid()).toEqual(false);
    expect(result.errors['id']).toEqual('Id cannot be empty.');
    expect(result.errors['name']).toEqual('Name cannot be empty.');
    expect(result.errors['acceptBagItVersion']).toEqual("Profile must accept at least one BagIt version.");
    expect(result.errors['manifestsRequired']).toEqual("Profile must require at least one manifest.");
    expect(result.errors['tags']).toEqual("Profile lacks requirements for bagit.txt tag file.\nProfile lacks requirements for bag-info.txt tag file.");
    expect(result.errors['serialization']).toEqual("Serialization must be one of: required, optional, forbidden.");
});

test('findMatchingTags()', () => {
    let profile = new BagItProfile();
    profile.tags.push(new TagDefinition('custom-tag-file.txt', 'Contact-Name'));
    let tags = profile.findMatchingTags('tagName', 'Contact-Name');
    expect(tags.length).toEqual(2);
    expect(tags[0].tagFile).toEqual('bag-info.txt'); // was set in BagItProfile constructor
    expect(tags[1].tagFile).toEqual('custom-tag-file.txt');

    tags = profile.findMatchingTags('tagFile', 'bag-info.txt');
    expect(tags.length).toEqual(14);

    tags = profile.findMatchingTags('defaultValue', '');
    expect(tags.length).toEqual(15);  // BagIt-Version and Tag-File-Character-Encoding have values

    tags = profile.findMatchingTags('fakeProperty', 'fakeValue');
    expect(tags.length).toEqual(0);
});

test('firstMatchingTag()', () => {
    let profile = new BagItProfile();
    profile.tags.push(new TagDefinition('custom-tag-file.txt', 'Contact-Name'));
    let tag = profile.firstMatchingTag('tagName', 'Contact-Name');
    expect(tag.tagFile).toEqual('bag-info.txt');

    tag = profile.firstMatchingTag('tagFile', 'bagit.txt');
    expect(tag.tagName).toEqual('BagIt-Version');

    tag = profile.firstMatchingTag('defaultValue', '');
    expect(tag.tagName).toEqual('Bag-Count');

    tag = profile.firstMatchingTag('fakeProperty', 'fakeValue');
    expect(tag).toBeUndefined();
});

test('getTagsFromFile()', () => {
    let profile = new BagItProfile();
    let tags = profile.getTagsFromFile('bagit.txt', 'BagIt-Version');
    expect(tags.length).toEqual(1);
    expect(tags[0].tagFile).toEqual('bagit.txt');
    expect(tags[0].tagName).toEqual('BagIt-Version');

    tags = profile.getTagsFromFile('bag-info.txt', 'Contact-Name');
    expect(tags.length).toEqual(1);
    expect(tags[0].tagFile).toEqual('bag-info.txt');
    expect(tags[0].tagName).toEqual('Contact-Name');

    // Yes, the spec says a tag can appear more than once in a tag file.
    profile.tags.push(new TagDefinition('bag-info.txt', 'Contact-Name'));
    tags = profile.getTagsFromFile('bag-info.txt', 'Contact-Name');
    expect(tags.length).toEqual(2);

    tags = profile.getTagsFromFile('bag-info.txt', 'No-Such-Tag');
    expect(tags.length).toEqual(0);
});

test('hasTagFile()', () => {
    let profile = new BagItProfile();
    expect(profile.hasTagFile('bagit.txt')).toEqual(true);
    expect(profile.hasTagFile('bag-info.txt')).toEqual(true);
    expect(profile.hasTagFile('no-file.txt')).toEqual(false);
});

test('suggestBagName()', () => {
    let inst = new AppSetting('Institution Domain', 'aptrust.org');
    inst.save();

    // Make something that looks like an APTrust profile,
    // just because it has an aptrust-info.txt tag file.
    let aptrustProfile = new BagItProfile();
    aptrustProfile.tags.push(new TagDefinition('aptrust-info.txt', 'Access'));
    expect(aptrustProfile.suggestBagName()).toMatch(/^aptrust.org.bag-\d+$/);

    // Make something that looks like a DPN profile,
    // just because it has an dpn-tags/dpn-info.txt tag file.
    let dpnProfile = new BagItProfile();
    dpnProfile.tags.push(new TagDefinition('dpn-tags/dpn-info.txt', 'Member-Id'));
    let bagName = dpnProfile.suggestBagName();
    expect(Util.looksLikeUUID(bagName)).toEqual(true);

    let genericProfile = new BagItProfile();
    expect(genericProfile.suggestBagName()).toMatch(/^bag-\d+$/);
});

test('suggestGenericBagName()', () => {
    expect(BagItProfile.suggestGenericBagName()).toMatch(/^bag-\d+$/);
});

test('nameLooksLegal() accepts valid file names and rejects invalid ones', () => {
    expect(BagItProfile.nameLooksLegal("legal-name")).toEqual(true);
    expect(BagItProfile.nameLooksLegal("Legal_Name")).toEqual(true);
    expect(BagItProfile.nameLooksLegal("Illeg*l_name")).toEqual(false);
    expect(BagItProfile.nameLooksLegal("Illeg?l_name")).toEqual(false);
    expect(BagItProfile.nameLooksLegal("Illeg\\l_name")).toEqual(false);
    expect(BagItProfile.nameLooksLegal("Illeg/l_name")).toEqual(false);
    expect(BagItProfile.nameLooksLegal("Illegal name")).toEqual(false);
    expect(BagItProfile.nameLooksLegal("Illegal:name")).toEqual(false);
    expect(BagItProfile.nameLooksLegal("Illegal\rname")).toEqual(false);
    expect(BagItProfile.nameLooksLegal("Illegal\nname")).toEqual(false);
    expect(BagItProfile.nameLooksLegal("Illegal\tname")).toEqual(false);
});

test('isValidBagName() asserts profile-specific naming rules', () => {
    let inst = new AppSetting('Institution Domain', 'aptrust.org');
    inst.save();

    let aptrustProfile = new BagItProfile();
    aptrustProfile.tags.push(new TagDefinition('aptrust-info.txt', 'Access'));
    expect(aptrustProfile.isValidBagName("aptrust.org.historical-photos-1951")).toEqual(true);
    expect(aptrustProfile.isValidBagName("historical-photos-1951")).toEqual(false);

    let dpnProfile = new BagItProfile();
    dpnProfile.tags.push(new TagDefinition('dpn-tags/dpn-info.txt', 'Member-Id'));
    expect(dpnProfile.isValidBagName(Util.uuid4())).toEqual(true);
    expect(dpnProfile.isValidBagName("BagOfGlass")).toEqual(false);

    let genericProfile = new BagItProfile();
    expect(genericProfile.isValidBagName("BagOfGlass")).toEqual(true);
    expect(genericProfile.isValidBagName("**Bag?Of:Glass**")).toEqual(false);
});

test('tagsGroupedByFile()', () => {
    let profile = new BagItProfile();
    let files = profile.tagsGroupedByFile();
    expect(files['bagit.txt'].length).toEqual(2);
    expect(files['bag-info.txt'].length).toEqual(14);
});

test('getTagFileContents()', () => {
    let profile = new BagItProfile();
    let files = profile.tagsGroupedByFile();
    profile.getTagsFromFile('bag-info.txt', 'External-Description')[0].userValue = 'Bag of Stuff';
    profile.getTagsFromFile('bag-info.txt', 'Bag-Count')[0].userValue = '1';
    profile.getTagsFromFile('bag-info.txt', 'Bag-Size')[0].userValue = '10887';
    profile.getTagsFromFile('bag-info.txt', 'Bagging-Date')[0].userValue = '2018-08-20';
    profile.getTagsFromFile('bag-info.txt', 'Contact-Email')[0].userValue = 'bagger@aptrust.org';
    profile.getTagsFromFile('bag-info.txt', 'Contact-Name')[0].userValue = 'Bagger Vance';
    profile.getTagsFromFile('bag-info.txt', 'Contact-Phone')[0].userValue = '434-555-1212';
    profile.getTagsFromFile('bag-info.txt', 'Bag-Group-Identifier')[0].userValue = 'Stuff Collection';
    profile.getTagsFromFile('bag-info.txt', 'External-Identifier')[0].userValue = 'MYLB/NDA';
    profile.getTagsFromFile('bag-info.txt', 'Internal-Sender-Description')[0].userValue = 'Bag of miscellaneous junk';
    profile.getTagsFromFile('bag-info.txt', 'Internal-Sender-Identifier')[0].userValue = 'NMOT';
    profile.getTagsFromFile('bag-info.txt', 'Organization-Address')[0].userValue = '1234 Main St., Charlottesville, VA 22903';
    profile.getTagsFromFile('bag-info.txt', 'Payload-Oxum')[0].userValue = '10232.4';
    profile.getTagsFromFile('bag-info.txt', 'Source-Organization')[0].userValue = 'Academic Preservation Trust';
    let bagItContents = "BagIt-Version: 0.97\nTag-File-Character-Encoding: UTF-8";
    let bagInfoContents = "External-Description: Bag of Stuff\nBag-Count: 1\nBag-Size: 10887\nBagging-Date: 2018-08-20\nContact-Email: bagger@aptrust.org\nContact-Name: Bagger Vance\nContact-Phone: 434-555-1212\nBag-Group-Identifier: Stuff Collection\nExternal-Identifier: MYLB/NDA\nInternal-Sender-Description: Bag of miscellaneous junk\nInternal-Sender-Identifier: NMOT\nOrganization-Address: 1234 Main St., Charlottesville, VA 22903\nPayload-Oxum: 10232.4\nSource-Organization: Academic Preservation Trust";
    expect(profile.getTagFileContents('bagit.txt')).toEqual(bagItContents);
    expect(profile.getTagFileContents('bag-info.txt')).toEqual(bagInfoContents);
});

test('isCustomTagFile()', () => {
    let profile = new BagItProfile();

    // These are not custom. They're built in to all profiles.
    expect(profile.isCustomTagFile('bagit.txt')).toEqual(false);
    expect(profile.isCustomTagFile('bag-info.txt')).toEqual(false);

    // This won't be custom, because we're not flagging it as custom.
    let aptrustTag = new TagDefinition('aptrust-info.txt', 'Access');
    aptrustTag.userValue = 'Institution';
    profile.tags.push(aptrustTag);
    expect(profile.isCustomTagFile('aptrust-info.txt')).toEqual(false);

    // This will be custom, because we're flagging it as such.
    let customTag = new TagDefinition('custom-tags.txt', 'Sample-Tag');
    customTag.isUserAddedFile = true;
    customTag.isUserAddedTag = true;
    customTag.userValue = 'electra';
    profile.tags.push(customTag);
    expect(profile.isCustomTagFile('custom-tags.txt')).toEqual(true);

    // Check on a tag file that doesn't even exist.
    expect(profile.isCustomTagFile('file-does-not-exist.txt')).toEqual(false);
});

test('tagFileNames()', () => {
    let profile = new BagItProfile();
    expect(profile.tagFileNames()).toEqual(['bag-info.txt', 'bagit.txt']);

    profile.tags.push(new TagDefinition('aptrust-info.txt', 'Access'));
    profile.tags.push(new TagDefinition('custom-tags.txt', 'Sample-Tag'));
    expect(profile.tagFileNames()).toEqual(['aptrust-info.txt', 'bag-info.txt', 'bagit.txt', 'custom-tags.txt']);
});

test('mustBeTarred()', () => {
    let profile = new BagItProfile();
    expect(profile.mustBeTarred()).toEqual(false);
    profile.acceptSerialization = ['application/tar'];
    expect(profile.mustBeTarred()).toEqual(false);
    profile.serialization = 'required';
    expect(profile.mustBeTarred()).toEqual(true);
});

test('fromJson()', () => {
    let jsonFile = path.join(__dirname, '..', 'test', 'profiles', 'aptrust_bagit_profile_2.2.json');
    let jsonString = fs.readFileSync(jsonFile);
    let profile = BagItProfile.fromJson(jsonString);
    expect(profile).not.toBeNull();

    // This doesn't test everything, but we spot check a few properties.
    expect(profile.id).toEqual('043f1c22-c9ff-4112-86f8-8f8f1e6a2dca');
    expect(profile.name).toEqual('APTrust');
    expect(profile.description).toEqual('APTrust 2.0 default BagIt profile.');
    expect(profile.acceptBagItVersion).toEqual(['0.97']);
    expect(profile.acceptSerialization).toEqual(['application/tar']);
    expect(profile.tags.length).toEqual(14);
    expect(profile.allowMiscDirectories).toEqual(true);
    expect(profile.bagItProfileInfo.contactEmail).toEqual('support@aptrust.org');
});

// test('bagTitle()', () => {

// });

// test('bagDescription()', () => {

// });

// test('bagInternalIdentifier()', () => {

// });

// test('copyDefaultTagValuesFrom()', () => {

// });
<commit_msg>Test bagTitle and bagDescription<commit_after>const { AppSetting } = require('../core/app_setting');
const { BagItProfile } = require('./bagit_profile');
const { TagDefinition } = require('./tag_definition');
const { TestUtil } = require('../core/test_util');
const { Util } = require('../core/util');
const fs = require('fs');
const path = require('path');

beforeEach(() => {
    TestUtil.deleteJsonFile('BagItProfile');
});

test('Constructor sets initial properties', () => {
    let profile = new BagItProfile();
    expect(profile.name).toEqual('New BagIt Profile');
    expect(profile.description).toEqual('New custom BagIt profile');

    profile = new BagItProfile('Test Profile', 'Profile for testing');
    expect(profile.name).toEqual('Test Profile');
    expect(profile.description).toEqual('Profile for testing');

    expect(profile.acceptBagItVersion).toEqual(['0.97']);
    expect(profile.acceptSerialization).toEqual(['tar']);
    expect(profile.allowFetchTxt).toEqual(false);
    expect(profile.allowMiscTopLevelFiles).toEqual(false);
    expect(profile.allowMiscDirectories).toEqual(false);
    expect(profile.bagItProfileInfo).not.toBeNull();
    expect(profile.manifestsRequired).toEqual(['sha256']);
    expect(profile.tagManifestsRequired).toEqual([]);
    expect(profile.tags.length).toEqual(16);
    expect(profile.serialization).toEqual('optional');
    expect(profile.baseProfileId).toEqual(null);
    expect(profile.isBuiltIn).toEqual(false);
});

test('validate() catches invalid properties', () => {
    let profile = new BagItProfile();
    profile.id = '';
    profile.name = '';
    profile.acceptBagItVersion = [];
    profile.manifestsRequired = [];
    profile.tags = [];
    profile.serialization = "Cap'n Crunch";
    let result = profile.validate();
    expect(result.isValid()).toEqual(false);
    expect(result.errors['id']).toEqual('Id cannot be empty.');
    expect(result.errors['name']).toEqual('Name cannot be empty.');
    expect(result.errors['acceptBagItVersion']).toEqual("Profile must accept at least one BagIt version.");
    expect(result.errors['manifestsRequired']).toEqual("Profile must require at least one manifest.");
    expect(result.errors['tags']).toEqual("Profile lacks requirements for bagit.txt tag file.\nProfile lacks requirements for bag-info.txt tag file.");
    expect(result.errors['serialization']).toEqual("Serialization must be one of: required, optional, forbidden.");
});

test('findMatchingTags()', () => {
    let profile = new BagItProfile();
    profile.tags.push(new TagDefinition('custom-tag-file.txt', 'Contact-Name'));
    let tags = profile.findMatchingTags('tagName', 'Contact-Name');
    expect(tags.length).toEqual(2);
    expect(tags[0].tagFile).toEqual('bag-info.txt'); // was set in BagItProfile constructor
    expect(tags[1].tagFile).toEqual('custom-tag-file.txt');

    tags = profile.findMatchingTags('tagFile', 'bag-info.txt');
    expect(tags.length).toEqual(14);

    tags = profile.findMatchingTags('defaultValue', '');
    expect(tags.length).toEqual(15);  // BagIt-Version and Tag-File-Character-Encoding have values

    tags = profile.findMatchingTags('fakeProperty', 'fakeValue');
    expect(tags.length).toEqual(0);
});

test('firstMatchingTag()', () => {
    let profile = new BagItProfile();
    profile.tags.push(new TagDefinition('custom-tag-file.txt', 'Contact-Name'));
    let tag = profile.firstMatchingTag('tagName', 'Contact-Name');
    expect(tag.tagFile).toEqual('bag-info.txt');

    tag = profile.firstMatchingTag('tagFile', 'bagit.txt');
    expect(tag.tagName).toEqual('BagIt-Version');

    tag = profile.firstMatchingTag('defaultValue', '');
    expect(tag.tagName).toEqual('Bag-Count');

    tag = profile.firstMatchingTag('fakeProperty', 'fakeValue');
    expect(tag).toBeUndefined();
});

test('getTagsFromFile()', () => {
    let profile = new BagItProfile();
    let tags = profile.getTagsFromFile('bagit.txt', 'BagIt-Version');
    expect(tags.length).toEqual(1);
    expect(tags[0].tagFile).toEqual('bagit.txt');
    expect(tags[0].tagName).toEqual('BagIt-Version');

    tags = profile.getTagsFromFile('bag-info.txt', 'Contact-Name');
    expect(tags.length).toEqual(1);
    expect(tags[0].tagFile).toEqual('bag-info.txt');
    expect(tags[0].tagName).toEqual('Contact-Name');

    // Yes, the spec says a tag can appear more than once in a tag file.
    profile.tags.push(new TagDefinition('bag-info.txt', 'Contact-Name'));
    tags = profile.getTagsFromFile('bag-info.txt', 'Contact-Name');
    expect(tags.length).toEqual(2);

    tags = profile.getTagsFromFile('bag-info.txt', 'No-Such-Tag');
    expect(tags.length).toEqual(0);
});

test('hasTagFile()', () => {
    let profile = new BagItProfile();
    expect(profile.hasTagFile('bagit.txt')).toEqual(true);
    expect(profile.hasTagFile('bag-info.txt')).toEqual(true);
    expect(profile.hasTagFile('no-file.txt')).toEqual(false);
});

test('suggestBagName()', () => {
    let inst = new AppSetting('Institution Domain', 'aptrust.org');
    inst.save();

    // Make something that looks like an APTrust profile,
    // just because it has an aptrust-info.txt tag file.
    let aptrustProfile = new BagItProfile();
    aptrustProfile.tags.push(new TagDefinition('aptrust-info.txt', 'Access'));
    expect(aptrustProfile.suggestBagName()).toMatch(/^aptrust.org.bag-\d+$/);

    // Make something that looks like a DPN profile,
    // just because it has an dpn-tags/dpn-info.txt tag file.
    let dpnProfile = new BagItProfile();
    dpnProfile.tags.push(new TagDefinition('dpn-tags/dpn-info.txt', 'Member-Id'));
    let bagName = dpnProfile.suggestBagName();
    expect(Util.looksLikeUUID(bagName)).toEqual(true);

    let genericProfile = new BagItProfile();
    expect(genericProfile.suggestBagName()).toMatch(/^bag-\d+$/);
});

test('suggestGenericBagName()', () => {
    expect(BagItProfile.suggestGenericBagName()).toMatch(/^bag-\d+$/);
});

test('nameLooksLegal() accepts valid file names and rejects invalid ones', () => {
    expect(BagItProfile.nameLooksLegal("legal-name")).toEqual(true);
    expect(BagItProfile.nameLooksLegal("Legal_Name")).toEqual(true);
    expect(BagItProfile.nameLooksLegal("Illeg*l_name")).toEqual(false);
    expect(BagItProfile.nameLooksLegal("Illeg?l_name")).toEqual(false);
    expect(BagItProfile.nameLooksLegal("Illeg\\l_name")).toEqual(false);
    expect(BagItProfile.nameLooksLegal("Illeg/l_name")).toEqual(false);
    expect(BagItProfile.nameLooksLegal("Illegal name")).toEqual(false);
    expect(BagItProfile.nameLooksLegal("Illegal:name")).toEqual(false);
    expect(BagItProfile.nameLooksLegal("Illegal\rname")).toEqual(false);
    expect(BagItProfile.nameLooksLegal("Illegal\nname")).toEqual(false);
    expect(BagItProfile.nameLooksLegal("Illegal\tname")).toEqual(false);
});

test('isValidBagName() asserts profile-specific naming rules', () => {
    let inst = new AppSetting('Institution Domain', 'aptrust.org');
    inst.save();

    let aptrustProfile = new BagItProfile();
    aptrustProfile.tags.push(new TagDefinition('aptrust-info.txt', 'Access'));
    expect(aptrustProfile.isValidBagName("aptrust.org.historical-photos-1951")).toEqual(true);
    expect(aptrustProfile.isValidBagName("historical-photos-1951")).toEqual(false);

    let dpnProfile = new BagItProfile();
    dpnProfile.tags.push(new TagDefinition('dpn-tags/dpn-info.txt', 'Member-Id'));
    expect(dpnProfile.isValidBagName(Util.uuid4())).toEqual(true);
    expect(dpnProfile.isValidBagName("BagOfGlass")).toEqual(false);

    let genericProfile = new BagItProfile();
    expect(genericProfile.isValidBagName("BagOfGlass")).toEqual(true);
    expect(genericProfile.isValidBagName("**Bag?Of:Glass**")).toEqual(false);
});

test('tagsGroupedByFile()', () => {
    let profile = new BagItProfile();
    let files = profile.tagsGroupedByFile();
    expect(files['bagit.txt'].length).toEqual(2);
    expect(files['bag-info.txt'].length).toEqual(14);
});

test('getTagFileContents()', () => {
    let profile = new BagItProfile();
    let files = profile.tagsGroupedByFile();
    profile.getTagsFromFile('bag-info.txt', 'External-Description')[0].userValue = 'Bag of Stuff';
    profile.getTagsFromFile('bag-info.txt', 'Bag-Count')[0].userValue = '1';
    profile.getTagsFromFile('bag-info.txt', 'Bag-Size')[0].userValue = '10887';
    profile.getTagsFromFile('bag-info.txt', 'Bagging-Date')[0].userValue = '2018-08-20';
    profile.getTagsFromFile('bag-info.txt', 'Contact-Email')[0].userValue = 'bagger@aptrust.org';
    profile.getTagsFromFile('bag-info.txt', 'Contact-Name')[0].userValue = 'Bagger Vance';
    profile.getTagsFromFile('bag-info.txt', 'Contact-Phone')[0].userValue = '434-555-1212';
    profile.getTagsFromFile('bag-info.txt', 'Bag-Group-Identifier')[0].userValue = 'Stuff Collection';
    profile.getTagsFromFile('bag-info.txt', 'External-Identifier')[0].userValue = 'MYLB/NDA';
    profile.getTagsFromFile('bag-info.txt', 'Internal-Sender-Description')[0].userValue = 'Bag of miscellaneous junk';
    profile.getTagsFromFile('bag-info.txt', 'Internal-Sender-Identifier')[0].userValue = 'NMOT';
    profile.getTagsFromFile('bag-info.txt', 'Organization-Address')[0].userValue = '1234 Main St., Charlottesville, VA 22903';
    profile.getTagsFromFile('bag-info.txt', 'Payload-Oxum')[0].userValue = '10232.4';
    profile.getTagsFromFile('bag-info.txt', 'Source-Organization')[0].userValue = 'Academic Preservation Trust';
    let bagItContents = "BagIt-Version: 0.97\nTag-File-Character-Encoding: UTF-8";
    let bagInfoContents = "External-Description: Bag of Stuff\nBag-Count: 1\nBag-Size: 10887\nBagging-Date: 2018-08-20\nContact-Email: bagger@aptrust.org\nContact-Name: Bagger Vance\nContact-Phone: 434-555-1212\nBag-Group-Identifier: Stuff Collection\nExternal-Identifier: MYLB/NDA\nInternal-Sender-Description: Bag of miscellaneous junk\nInternal-Sender-Identifier: NMOT\nOrganization-Address: 1234 Main St., Charlottesville, VA 22903\nPayload-Oxum: 10232.4\nSource-Organization: Academic Preservation Trust";
    expect(profile.getTagFileContents('bagit.txt')).toEqual(bagItContents);
    expect(profile.getTagFileContents('bag-info.txt')).toEqual(bagInfoContents);
});

test('isCustomTagFile()', () => {
    let profile = new BagItProfile();

    // These are not custom. They're built in to all profiles.
    expect(profile.isCustomTagFile('bagit.txt')).toEqual(false);
    expect(profile.isCustomTagFile('bag-info.txt')).toEqual(false);

    // This won't be custom, because we're not flagging it as custom.
    let aptrustTag = new TagDefinition('aptrust-info.txt', 'Access');
    aptrustTag.userValue = 'Institution';
    profile.tags.push(aptrustTag);
    expect(profile.isCustomTagFile('aptrust-info.txt')).toEqual(false);

    // This will be custom, because we're flagging it as such.
    let customTag = new TagDefinition('custom-tags.txt', 'Sample-Tag');
    customTag.isUserAddedFile = true;
    customTag.isUserAddedTag = true;
    customTag.userValue = 'electra';
    profile.tags.push(customTag);
    expect(profile.isCustomTagFile('custom-tags.txt')).toEqual(true);

    // Check on a tag file that doesn't even exist.
    expect(profile.isCustomTagFile('file-does-not-exist.txt')).toEqual(false);
});

test('tagFileNames()', () => {
    let profile = new BagItProfile();
    expect(profile.tagFileNames()).toEqual(['bag-info.txt', 'bagit.txt']);

    profile.tags.push(new TagDefinition('aptrust-info.txt', 'Access'));
    profile.tags.push(new TagDefinition('custom-tags.txt', 'Sample-Tag'));
    expect(profile.tagFileNames()).toEqual(['aptrust-info.txt', 'bag-info.txt', 'bagit.txt', 'custom-tags.txt']);
});

test('mustBeTarred()', () => {
    let profile = new BagItProfile();
    expect(profile.mustBeTarred()).toEqual(false);
    profile.acceptSerialization = ['application/tar'];
    expect(profile.mustBeTarred()).toEqual(false);
    profile.serialization = 'required';
    expect(profile.mustBeTarred()).toEqual(true);
});

test('fromJson()', () => {
    let jsonFile = path.join(__dirname, '..', 'test', 'profiles', 'aptrust_bagit_profile_2.2.json');
    let jsonString = fs.readFileSync(jsonFile);
    let profile = BagItProfile.fromJson(jsonString);
    expect(profile).not.toBeNull();

    // This doesn't test everything, but we spot check a few properties.
    expect(profile.id).toEqual('043f1c22-c9ff-4112-86f8-8f8f1e6a2dca');
    expect(profile.name).toEqual('APTrust');
    expect(profile.description).toEqual('APTrust 2.0 default BagIt profile.');
    expect(profile.acceptBagItVersion).toEqual(['0.97']);
    expect(profile.acceptSerialization).toEqual(['application/tar']);
    expect(profile.tags.length).toEqual(14);
    expect(profile.allowMiscDirectories).toEqual(true);
    expect(profile.bagItProfileInfo.contactEmail).toEqual('support@aptrust.org');
});

test('bagTitle()', () => {
    let profile = new BagItProfile();
    expect(profile.bagTitle()).toEqual('');

    let titleTag1 = new TagDefinition('bag-info.txt', 'Internal-Title');
    titleTag1.userValue = 'First Title';
    profile.tags.push(titleTag1);
    expect(profile.bagTitle()).toEqual('First Title');

    let titleTag2 = new TagDefinition('bag-info.txt', 'Title');
    titleTag2.userValue = 'Second Title';
    profile.tags.push(titleTag2);
    expect(profile.bagTitle()).toEqual('Second Title');
});

test('bagDescription()', () => {
    let profile = new BagItProfile();
    expect(profile.bagTitle()).toEqual('');

    let titleTag1 = new TagDefinition('bag-info.txt', 'Description');
    titleTag1.userValue = 'First Description';
    profile.tags.push(titleTag1);
    expect(profile.bagDescription()).toEqual('First Description');

    let titleTag2 = new TagDefinition('bag-info.txt', 'Internal-Sender-Description');
    titleTag2.userValue = 'Second Description';
    profile.tags.push(titleTag2);
    expect(profile.bagDescription()).toEqual('Second Description');
});

// test('bagInternalIdentifier()', () => {

// });

// test('copyDefaultTagValuesFrom()', () => {

// });
<|endoftext|>


<commit_before>// $Revision$

Ensembl.LayoutManager = new Base();

Ensembl.LayoutManager.extend({
  constructor: null,

  /**
   * Creates events on elements outside of the domain of panels
   */
  initialize: function () {
    this.id = 'LayoutManager';
    this.ajaxContent = $('.ajax');

    Ensembl.EventManager.register('reloadPage', this, this.reloadPage);
    Ensembl.EventManager.register('validateForms', this, this.validateForms);
    Ensembl.EventManager.register('makeZMenu', this, this.makeZMenu);

    $('#local-tools').show();

    $('.modal_link').show().live('click', function () {
      // If ajax is not enabled, make popup config window
      // If ajax is enabled, modalOpen is triggered. If it doesn't returns true then there's no ModalContainer panel, so make popup config window
      if (Ensembl.ajax != 'enabled' || !Ensembl.EventManager.trigger('modalOpen', this)) {
        var name = 'cp_' + window.name;
        var w = window.open(this.href, name.replace(/cp_cp_/, 'cp_'), 'width=950,height=500,resizable,scrollbars');
        w.focus();
      }

      return false;
    });

    $('.popup').live('click', function () {
      var w = window.open(this.href, 'popup_' + window.name, 'width=950,height=500,resizable,scrollbars');
      w.focus();

      return false;
    });

    $('a[rel="external"]').live('click', function () {
      this.target = '_blank';
    });

    // using livequery plugin because .live doesn't support blur, focus, mouseenter, mouseleave, change or submit in IE
    $('form.check').livequery('submit', function () {
      var form = $(this);
      var rtn = form.parents('#modal_panel').length ?
                Ensembl.EventManager.trigger('modalFormSubmit', form) :
                Ensembl.FormValidator.submit(form);

      form = null;
      return rtn;
    });

    $(':input', 'form.check').live('keyup', function () {
      Ensembl.FormValidator.check($(this));
    }).livequery('change', function () {
      Ensembl.FormValidator.check($(this));
    }).livequery('blur', function () { // IE is stupid, so we need blur as well as change if you select from browser-stored values
      Ensembl.FormValidator.check($(this));
    }).each(function () {
      Ensembl.FormValidator.check($(this));
    });

    // For non ajax support - popup window close button
    var close = $('.popup_close');

    if (close.length) {
      if ($('input.panel_type[value=Configurator]').length) {
        close.html('Save and close').click(function () {
          if (Ensembl.EventManager.trigger('updateConfiguration') || window.location.search.match('reset=1')) {
            window.open(Ensembl.replaceTimestamp(this.href), window.name.replace(/^cp_/, '')); // Reload the main page
          }

          window.close();

          return false;
        });
      } else {
        close.hide();
      }
    }

    close = null;

    // Close modal window if the escape key is pressed
    $(document).keyup(function (event) {
      if (event.keyCode == 27) {
        Ensembl.EventManager.trigger('modalClose');
      }
    }).mouseup(function (e) {
      Ensembl.EventManager.trigger('dragStop', e);
    });
  },

  reloadPage: function () {
    var date = new Date();
    var time = date.getTime() + date.getMilliseconds() / 1000;
    var url = window.location.href.replace(/&/g, ';').replace(/#.*$/g, '').replace(/\?time=[^;]+;?/g, '?').replace(/;time=[^;]+;?/g, ';').replace(/[\?;]$/g, '');

    window.location = url + (url.match(/\?/) ? ';' : '?') + 'time=' + time;
  },

  validateForms: function (context) {
    $('form.check', context).find(':input').each(function () {
      Ensembl.FormValidator.check($(this));
    });
  },

  makeZMenu: function (id, params) {
    $('
' +
      '
' +
      '
X
' +
      '
' +
      '

' +
      '
').draggable({ handle: 'thead' }).appendTo('body');

    Ensembl.EventManager.trigger('addPanel', id, 'ZMenu', undefined, undefined, params, 'showExistingZMenu');
  }
});<commit_msg>cleanup<commit_after>// $Revision$

Ensembl.LayoutManager = new Base();

Ensembl.LayoutManager.extend({
  constructor: null,

  /**
   * Creates events on elements outside of the domain of panels
   */
  initialize: function () {
    this.id = 'LayoutManager';

    Ensembl.EventManager.register('reloadPage', this, this.reloadPage);
    Ensembl.EventManager.register('validateForms', this, this.validateForms);
    Ensembl.EventManager.register('makeZMenu', this, this.makeZMenu);

    $('#local-tools').show();

    $('.modal_link').show().live('click', function () {
      // If ajax is not enabled, make popup config window
      // If ajax is enabled, modalOpen is triggered. If it doesn't returns true then there's no ModalContainer panel, so make popup config window
      if (Ensembl.ajax != 'enabled' || !Ensembl.EventManager.trigger('modalOpen', this)) {
        var name = 'cp_' + window.name;
        var w = window.open(this.href, name.replace(/cp_cp_/, 'cp_'), 'width=950,height=500,resizable,scrollbars');
        w.focus();
      }

      return false;
    });

    $('.popup').live('click', function () {
      var w = window.open(this.href, 'popup_' + window.name, 'width=950,height=500,resizable,scrollbars');
      w.focus();

      return false;
    });

    $('a[rel="external"]').live('click', function () {
      this.target = '_blank';
    });

    // using livequery plugin because .live doesn't support blur, focus, mouseenter, mouseleave, change or submit in IE
    $('form.check').livequery('submit', function () {
      var form = $(this);
      var rtn = form.parents('#modal_panel').length ?
                Ensembl.EventManager.trigger('modalFormSubmit', form) :
                Ensembl.FormValidator.submit(form);

      form = null;
      return rtn;
    });

    $(':input', 'form.check').live('keyup', function () {
      Ensembl.FormValidator.check($(this));
    }).livequery('change', function () {
      Ensembl.FormValidator.check($(this));
    }).livequery('blur', function () { // IE is stupid, so we need blur as well as change if you select from browser-stored values
      Ensembl.FormValidator.check($(this));
    }).each(function () {
      Ensembl.FormValidator.check($(this));
    });

    // For non ajax support - popup window close button
    var close = $('.popup_close');

    if (close.length) {
      if ($('input.panel_type[value=Configurator]').length) {
        close.html('Save and close').click(function () {
          if (Ensembl.EventManager.trigger('updateConfiguration') || window.location.search.match('reset=1')) {
            window.open(Ensembl.replaceTimestamp(this.href), window.name.replace(/^cp_/, '')); // Reload the main page
          }

          window.close();

          return false;
        });
      } else {
        close.hide();
      }
    }

    close = null;

    // Close modal window if the escape key is pressed
    $(document).keyup(function (event) {
      if (event.keyCode == 27) {
        Ensembl.EventManager.trigger('modalClose');
      }
    }).mouseup(function (e) {
      Ensembl.EventManager.trigger('dragStop', e);
    });
  },

  reloadPage: function () {
    var date = new Date();
    var time = date.getTime() + date.getMilliseconds() / 1000;
    var url = window.location.href.replace(/&/g, ';').replace(/#.*$/g, '').replace(/\?time=[^;]+;?/g, '?').replace(/;time=[^;]+;?/g, ';').replace(/[\?;]$/g, '');

    window.location = url + (url.match(/\?/) ? ';' : '?') + 'time=' + time;
  },

  validateForms: function (context) {
    $('form.check', context).find(':input').each(function () {
      Ensembl.FormValidator.check($(this));
    });
  },

  makeZMenu: function (id, params) {
    $('
' +
      '
' +
      '
X
' +
      '
' +
      '

' +
      '
').draggable({ handle: 'thead' }).appendTo('body');

    Ensembl.EventManager.trigger('addPanel', id, 'ZMenu', undefined, undefined, params, 'showExistingZMenu');
  }
});<|endoftext|>


<commit_before>// Like Tinytest._runTests, but runs the tests on both the client and
// the server. Sets a 'server' flag on test results that came from the
// server.
//
Tinytest._runTestsEverywhere = function (onReport, onComplete, pathPrefix) {
  var runId = Random.id();
  var localComplete = false;
  var remoteComplete = false;
  var done = false;

  var maybeDone = function () {
    if (!done && localComplete && remoteComplete) {
      done = true;
      onComplete && onComplete();
    }
  };

  Tinytest._runTests(onReport, function () {
    localComplete = true;
    maybeDone();
  }, pathPrefix);

  var handle;

  Meteor.connection.registerStore(Meteor._ServerTestResultsCollection, {
    update: function (msg) {
      // We only should call _runTestsEverywhere once per client-page-load, so
      // we really only should see one runId here.
      if (msg.id !== runId)
        return;
      // This will only work for added & changed messages.
      // hope that is all you get.
      _.each(msg.fields, function (report, key) {
        // Skip the 'complete' report (deal with it last)
        if (key === 'complete') {
          return;
        }
        _.each(report.events, function (event) {
          delete event.cookie; // can't debug a server test on the client..
        });
        report.server = true;
        onReport(report);
      });
      // Now that we've processed all the other messages,
      // check if we have the 'complete' message
      if (msg.fields && _.has(msg.fields, 'complete')) {
        remoteComplete = true;
        handle.stop();
        Meteor.call('tinytest/clearResults', runId);
        maybeDone();
      }
    }
  });

  handle = Meteor.subscribe(Meteor._ServerTestResultsSubscription, runId);

  Meteor.call('tinytest/run', runId, pathPrefix, function (error, result) {
    if (error)
      // XXX better report error
      throw new Error("Test server returned an error");
  });
};
<commit_msg>By default, don't run server & client tests in parallel<commit_after>// Like Tinytest._runTests, but runs the tests on both the client and
// the server. Sets a 'server' flag on test results that came from the
// server.
//
// Options:
//   serial     if true, will not run tests in parallel.  Currently this means
//              running the server tests before running the client tests.
//              Default is currently true (serial operation), but we will likely
//              change this to false in future.
Tinytest._runTestsEverywhere = function (onReport, onComplete, pathPrefix, options) {
  var runId = Random.id();
  var localComplete = false;
  var localStarted = false;
  var remoteComplete = false;
  var done = false;

  options = _.extend({
    serial: true
  }, options);
  var serial = !!options.serial;

  var maybeDone = function () {
    if (!done && localComplete && remoteComplete) {
      done = true;
      onComplete && onComplete();
    }
    if (serial && remoteComplete && !localStarted) {
      startLocalTests();
    }
  };

  var startLocalTests = function() {
    localStarted = true;
    Tinytest._runTests(onReport, function () {
      localComplete = true;
      maybeDone();
    }, pathPrefix);
  };

  var handle;

  Meteor.connection.registerStore(Meteor._ServerTestResultsCollection, {
    update: function (msg) {
      // We only should call _runTestsEverywhere once per client-page-load, so
      // we really only should see one runId here.
      if (msg.id !== runId)
        return;
      // This will only work for added & changed messages.
      // hope that is all you get.
      _.each(msg.fields, function (report, key) {
        // Skip the 'complete' report (deal with it last)
        if (key === 'complete') {
          return;
        }
        _.each(report.events, function (event) {
          delete event.cookie; // can't debug a server test on the client..
        });
        report.server = true;
        onReport(report);
      });
      // Now that we've processed all the other messages,
      // check if we have the 'complete' message
      if (msg.fields && _.has(msg.fields, 'complete')) {
        remoteComplete = true;
        handle.stop();
        Meteor.call('tinytest/clearResults', runId);
        maybeDone();
      }
    }
  });

  handle = Meteor.subscribe(Meteor._ServerTestResultsSubscription, runId);

  Meteor.call('tinytest/run', runId, pathPrefix, function (error, result) {
    if (error)
      // XXX better report error
      throw new Error("Test server returned an error");
  });

  if (!serial) {
    startLocalTests();
  }
};
<|endoftext|>


<commit_before>// Compares .sol files to their .sol.js counterparts,
// determines which .sol files have been updated.

var path = require("path");
var async = require("async");
var fs = require("fs");
var SolidityParser = require("solidity-parser");
var Graph = require("graphlib").Graph;
var isAcyclic = require("graphlib/lib/alg").isAcyclic;
var CompileError = require("./compileerror");
var expect = require("truffle-expect");
var find_contracts = require("truffle-contract-sources");

module.exports = {
  updated: function(options, callback) {
    var self = this;

    expect.options(options, [
      "resolver"
    ]);

    var contracts_directory = options.contracts_directory;
    var build_directory = options.contracts_build_directory;

    function getFiles(done) {
      if (options.files) {
        done(null, options.files);
      } else {
        find_contracts(contracts_directory, done);
      }
    }

    getFiles(function(err, files) {
      async.map(files, fs.stat, function(err, file_stats) {
        if (err) return callback(err);

        var contracts = files.map(function(expected_source_file) {
          var resolved = null;
          try {
            resolved = options.resolver.require(expected_source_file);
          } catch (e) {
            // do nothing; warning, this could squelch real errors
            if (e.message.indexOf("Could not find artifacts for") != 0) throw e;
          }
          return resolved;
        });

        var updated = [];

        for (var i = 0; i < contracts.length; i++) {
          var file_stat = file_stats[i];
          var contract = contracts[i];

          if (contract == null) {
            updated.push(files[i]);
            continue;
          }

          var modified_time = (file_stat.mtime || file_stat.ctime).getTime();

          var built_time = contract.updated_at || 0;

          if (modified_time > built_time) {
            updated.push(files[i]);
          }
        }

        callback(null, updated);
      });
    });
  },

  required_sources: function(options, callback) {
    expect.options(options, [
      "paths",
      "base_path",
      "resolver"
    ]);

    var paths = this.convert_to_absolute_paths(options.paths, options.base_path);

    this.dependency_graph(paths, options.resolver, function(err, dependsGraph) {
      if (err) return callback(err);

      var required = {};

      function include(import_path) {
        //console.log("Including: " + file)

        required[import_path] = dependsGraph.node(import_path);
      }

      function walk_down(import_path) {
        if (required[import_path] === true) {
          return;
        }

        include(import_path);

        var dependencies = dependsGraph.successors(import_path);

        // console.log("At: " + import_path);
        // console.log("   Dependencies: ", dependencies);

        if (dependencies.length > 0) {
          dependencies.forEach(walk_down);
        }
      }

      function walk_from(import_path) {
        var ancestors = dependsGraph.predecessors(import_path);
        var dependencies = dependsGraph.successors(import_path);

        // console.log("At: " + import_path);
        // console.log("   Ancestors: ", ancestors);
        // console.log("   Dependencies: ", dependencies);

        include(import_path);

        if (ancestors.length > 0) {
          ancestors.forEach(walk_from);
        }

        if (dependencies.length > 0) {
          dependencies.forEach(walk_down);
        }
      }

      paths.forEach(walk_from);

      callback(null, required);
    });
  },

  convert_to_absolute_paths: function(paths, base) {
    var self = this;
    return paths.map(function(p) {
      // If it's anabsolute paths, leave it alone.
      if (path.isAbsolute(p)) return p;

      // If it's not explicitly relative, then leave it alone (i.e., it's a module).
      if (!self.isExplicitlyRelative(p)) return p;

      // Path must be explicitly releative, therefore make it absolute.
      return path.resolve(path.join(base, p));
    });
  },

  isExplicitlyRelative: function(import_path) {
    return import_path.indexOf(".") == 0;
  },

  dependency_graph: function(paths, resolver, callback) {
    var self = this;

    // Iterate through all the contracts looking for libraries and building a dependency graph
    var dependsGraph = new Graph();

    var imports_cache = {};

    // For the purposes of determining correct error messages.
    // The second array item denotes which path imported the current path.
    // In the case of the paths passed in, there was none.
    paths = paths.map(function(p) {
      return [p, null];
    });

    async.whilst(function() {
      return paths.length > 0;
    }, function(finished) {
      var current = paths.shift();
      var import_path = current[0];
      var imported_from = current[1];

      if (dependsGraph.hasNode(import_path) && imports_cache[import_path] != null) {
        return finished();
      }

      resolver.resolve(import_path, imported_from, function(err, body, source) {
        if (err) return callback(err);

        // Add the contract to the depends graph.
        dependsGraph.setNode(import_path, body);

        var imports = SolidityParser.parse(body, "imports");

        // Convert explicitly relative dependencies of modules
        // back into module paths. We also use this loop to update
        // the graph edges.
        imports = imports.map(function(dependency_path) {
          // Convert explicitly relative paths
          if (self.isExplicitlyRelative(dependency_path)) {
            dependency_path = source.resolve_dependency_path(import_path, dependency_path);
          }

          // Update graph edges
          if (!dependsGraph.hasEdge(import_path, dependency_path)) {
            dependsGraph.setEdge(import_path, dependency_path);
          }

          // Return an array that denotes a new import and the path it was imported from.
          return [dependency_path, import_path];
        });

        imports_cache[import_path] = imports;

        Array.prototype.push.apply(paths, imports);

        finished();
      });
    },
    function() {
      // Check for cycles in the graph, the dependency graph needs to be a tree otherwise there's an error
      if (!isAcyclic(dependsGraph)) {
        var errorMessage = "Found cyclic dependencies. Adjust your import statements to remove cycles.\n\n";
        dependsGraph.edges().forEach(function(o){
          errorMessage += o.v + " -- depends on --> " + o.w + "\n";
        });
        return callback(new CompileError(errorMessage));
      }
      callback(null, dependsGraph)
    });
  },

  // Parse all source files in the directory and output the names of contracts they definedContracts
  defined_contracts: function(directory, callback) {
    find_contracts(directory, function(err, files) {
      if (err) return callback(err);

      var promises = files.map(function(file) {
        return new Promise(function(accept, reject) {
          fs.readFile(file, "utf8", function(err, body) {
            if (err) return reject(err);

            var ast;

            try {
              ast = SolidityParser.parse(body);
            } catch (e) {
              e.message = "Error parsing " + file + ": " + e.message;
              return reject(e);
            }

            accept(ast);
          });
        });
      });

      Promise.all(promises).then(function(asts) {
        var contract_names = asts.map(function(ast) {
          return ast.body.filter(function(toplevel_item) {
            return toplevel_item.type == "ContractStatement";
          }).map(function(contract_statement) {
            return contract_statement.name;
          });
        });

        contract_names = [].concat.apply([], contract_names);

        callback(null, contract_names);
      }).catch(callback);
    });
  }
};
<commit_msg>Don’t circumvent control flow by calling the callback. Also, better expose parsing errors, if any.<commit_after>// Compares .sol files to their .sol.js counterparts,
// determines which .sol files have been updated.

var path = require("path");
var async = require("async");
var fs = require("fs");
var SolidityParser = require("solidity-parser");
var Graph = require("graphlib").Graph;
var isAcyclic = require("graphlib/lib/alg").isAcyclic;
var CompileError = require("./compileerror");
var expect = require("truffle-expect");
var find_contracts = require("truffle-contract-sources");

module.exports = {
  updated: function(options, callback) {
    var self = this;

    expect.options(options, [
      "resolver"
    ]);

    var contracts_directory = options.contracts_directory;
    var build_directory = options.contracts_build_directory;

    function getFiles(done) {
      if (options.files) {
        done(null, options.files);
      } else {
        find_contracts(contracts_directory, done);
      }
    }

    getFiles(function(err, files) {
      async.map(files, fs.stat, function(err, file_stats) {
        if (err) return callback(err);

        var contracts = files.map(function(expected_source_file) {
          var resolved = null;
          try {
            resolved = options.resolver.require(expected_source_file);
          } catch (e) {
            // do nothing; warning, this could squelch real errors
            if (e.message.indexOf("Could not find artifacts for") != 0) throw e;
          }
          return resolved;
        });

        var updated = [];

        for (var i = 0; i < contracts.length; i++) {
          var file_stat = file_stats[i];
          var contract = contracts[i];

          if (contract == null) {
            updated.push(files[i]);
            continue;
          }

          var modified_time = (file_stat.mtime || file_stat.ctime).getTime();

          var built_time = contract.updated_at || 0;

          if (modified_time > built_time) {
            updated.push(files[i]);
          }
        }

        callback(null, updated);
      });
    });
  },

  required_sources: function(options, callback) {
    expect.options(options, [
      "paths",
      "base_path",
      "resolver"
    ]);

    var paths = this.convert_to_absolute_paths(options.paths, options.base_path);

    this.dependency_graph(paths, options.resolver, function(err, dependsGraph) {
      if (err) return callback(err);

      var required = {};

      function include(import_path) {
        //console.log("Including: " + file)

        required[import_path] = dependsGraph.node(import_path);
      }

      function walk_down(import_path) {
        if (required[import_path] === true) {
          return;
        }

        include(import_path);

        var dependencies = dependsGraph.successors(import_path);

        // console.log("At: " + import_path);
        // console.log("   Dependencies: ", dependencies);

        if (dependencies.length > 0) {
          dependencies.forEach(walk_down);
        }
      }

      function walk_from(import_path) {
        var ancestors = dependsGraph.predecessors(import_path);
        var dependencies = dependsGraph.successors(import_path);

        // console.log("At: " + import_path);
        // console.log("   Ancestors: ", ancestors);
        // console.log("   Dependencies: ", dependencies);

        include(import_path);

        if (ancestors.length > 0) {
          ancestors.forEach(walk_from);
        }

        if (dependencies.length > 0) {
          dependencies.forEach(walk_down);
        }
      }

      paths.forEach(walk_from);

      callback(null, required);
    });
  },

  convert_to_absolute_paths: function(paths, base) {
    var self = this;
    return paths.map(function(p) {
      // If it's anabsolute paths, leave it alone.
      if (path.isAbsolute(p)) return p;

      // If it's not explicitly relative, then leave it alone (i.e., it's a module).
      if (!self.isExplicitlyRelative(p)) return p;

      // Path must be explicitly releative, therefore make it absolute.
      return path.resolve(path.join(base, p));
    });
  },

  isExplicitlyRelative: function(import_path) {
    return import_path.indexOf(".") == 0;
  },

  dependency_graph: function(paths, resolver, callback) {
    var self = this;

    // Iterate through all the contracts looking for libraries and building a dependency graph
    var dependsGraph = new Graph();

    var imports_cache = {};

    // For the purposes of determining correct error messages.
    // The second array item denotes which path imported the current path.
    // In the case of the paths passed in, there was none.
    paths = paths.map(function(p) {
      return [p, null];
    });

    async.whilst(function() {
      return paths.length > 0;
    }, function(finished) {
      var current = paths.shift();
      var import_path = current[0];
      var imported_from = current[1];

      if (dependsGraph.hasNode(import_path) && imports_cache[import_path] != null) {
        return finished();
      }

      resolver.resolve(import_path, imported_from, function(err, body, source) {
        if (err) return finished(err);

        // Add the contract to the depends graph.
        dependsGraph.setNode(import_path, body);

        var imports;

        try {
          imports = SolidityParser.parse(body, "imports");
        } catch (e) {
          e.message = "Error parsing " + import_path + ": " + e.message;
          return finished(e);
        }

        // Convert explicitly relative dependencies of modules
        // back into module paths. We also use this loop to update
        // the graph edges.
        imports = imports.map(function(dependency_path) {
          // Convert explicitly relative paths
          if (self.isExplicitlyRelative(dependency_path)) {
            dependency_path = source.resolve_dependency_path(import_path, dependency_path);
          }

          // Update graph edges
          if (!dependsGraph.hasEdge(import_path, dependency_path)) {
            dependsGraph.setEdge(import_path, dependency_path);
          }

          // Return an array that denotes a new import and the path it was imported from.
          return [dependency_path, import_path];
        });

        imports_cache[import_path] = imports;

        Array.prototype.push.apply(paths, imports);

        finished();
      });
    },
    function() {
      // Check for cycles in the graph, the dependency graph needs to be a tree otherwise there's an error
      if (!isAcyclic(dependsGraph)) {
        var errorMessage = "Found cyclic dependencies. Adjust your import statements to remove cycles.\n\n";
        dependsGraph.edges().forEach(function(o){
          errorMessage += o.v + " -- depends on --> " + o.w + "\n";
        });
        return callback(new CompileError(errorMessage));
      }
      callback(null, dependsGraph)
    });
  },

  // Parse all source files in the directory and output the names of contracts they definedContracts
  defined_contracts: function(directory, callback) {
    find_contracts(directory, function(err, files) {
      if (err) return callback(err);

      var promises = files.map(function(file) {
        return new Promise(function(accept, reject) {
          fs.readFile(file, "utf8", function(err, body) {
            if (err) return reject(err);

            var ast;

            try {
              ast = SolidityParser.parse(body);
            } catch (e) {
              e.message = "Error parsing " + file + ": " + e.message;
              return reject(e);
            }

            accept(ast);
          });
        });
      });

      Promise.all(promises).then(function(asts) {
        var contract_names = asts.map(function(ast) {
          return ast.body.filter(function(toplevel_item) {
            return toplevel_item.type == "ContractStatement";
          }).map(function(contract_statement) {
            return contract_statement.name;
          });
        });

        contract_names = [].concat.apply([], contract_names);

        callback(null, contract_names);
      }).catch(callback);
    });
  }
};
<|endoftext|>


<commit_before>/* eslint no-use-before-define: [2, "nofunc"] */
'use strict';
const fs = require('fs');
const join = require('path').join;

// rows are represented as an array of three strings: name, type, description

function formatTable(rows) {
  const header = ['Name', 'Type', 'Description'];
  const divider = ['----', '----', '-----------'];
  const allRows = [header, divider].concat(rows);
  return allRows.map(row => row.join(' | ')).join('\n');
}

function formatName(path) {
  if (path.length === 0) {
    return '';
  }
  if (path.length === 1) {
    return path[0];
  }
  return '*' + path.slice(0, -1).join('.') + '.* ' + path.slice(-1)[0];
}

function formatType(schema) {
  if (schema.link) {
    return '[' + schema.title + '](#' + schema.link + ')';
  }
  if (schema.format) {
    return schema.format + ' string';
  }
  if (schema.enum) {
    return 'string';
  }
  return schema.type || 'object';
}

function formatRow(schema, path, isRequired, typeOverride) {
  const description = (isRequired ? '' : '*Optional* ')
                    + (schema.description || '');
  return [formatName(path), typeOverride || formatType(schema), description];
}

function flatten(arrays) {
  return [].concat.apply([], arrays);
}

function includes(array, item) {
  return array && array.indexOf(item) !== -1;
}

function sortKeys(keys, requiredKeys) {
  const result = requiredKeys ? requiredKeys.slice() : [];
  result.forEach(key => {
    if (!includes(keys, key)) {
      throw new Error('Property in "required" not in "properties": ' + key);
    }
  });
  keys.sort().forEach(key => {
    if (!includes(result, key)) {
      result.push(key);
    }
  });
  return result;
}

function generateRowsForObject(schema, path, schemas, isRequired) {
  const keys = sortKeys(Object.keys(schema.properties), schema.required);
  const rows = flatten(keys.map(name => {
    const isRequiredField = includes(schema.required, name);
    return generateRowsForSchema(
      schema.properties[name], path.concat([name]), schemas, isRequiredField);
  }));
  return path.length > 0 ?
    [formatRow(schema, path, isRequired)].concat(rows) : rows;
}

function generateRowsForArray(schema, path, schemas, isRequired) {
  const newPath = path.slice(0, -1).concat([path.slice(-1)[0] + '[]']);
  const rows = generateRowsForSchema(schema.items, newPath, schemas, true);
  if (rows.length === 1) {
    const typeOverride = 'array\\<' + rows[0][1] + '\\>';
    return [formatRow(schema, path, isRequired, typeOverride)];
  }
  const firstRow = formatRow(schema, path, isRequired);
  return [firstRow].concat(rows);
}

function removeDuplicates(rows) {
  const hash = {};
  const result = [];
  rows.forEach(row => {
    const key = row.join('|');
    if (!hash[key]) {
      result.push(row);
      hash[key] = true;
    }
  });
  return result;
}

function generateRowsForBranch(type, branchSchemas, path, schemas, isRequired) {
  const rows = flatten(branchSchemas.map(branchSchema =>
    generateRowsForSchema(branchSchema, path, schemas, isRequired)));
  return [['TODO', type, path.join('.')]].concat(removeDuplicates(rows));
}

function generateRowsForCompleteSchema(schema, path, schemas, isRequired) {
  if (schema.link) {
    return [formatRow(schema, path, isRequired)];
  }
  if (schema.type === 'array') {
    if (path.length > 0) {
      return generateRowsForArray(schema, path, schemas, isRequired);
    }
    return generateRowsForSchema(schema.items, path, schemas, true);
  }
  if (schema.properties) {
    return generateRowsForObject(schema, path, schemas, isRequired);
  }
  if (schema.oneOf) {
    return generateRowsForBranch(
      'oneOf', schema.oneOf, path, schemas, isRequired);
  }
  if (schema.anyOf) {
    return generateRowsForBranch(
      'anyOf', schema.anyOf, path, schemas, isRequired);
  }
  return [formatRow(schema, path, isRequired)];
}

function assign(destination, source) {
  for (let key in source) { // eslint-disable-line
    if (source.hasOwnProperty(key)) {
      destination[key] = source[key];
    }
  }
  return destination;
}

function completeSchema(schema, schemas) {
  if (schema.$ref) {
    const refSchema = schemas[schema.$ref];
    if (!refSchema) {
      throw new Error('Could not find schema for: ' + schema.$ref);
    }
    return assign(assign({}, refSchema), schema);
  }
  return schema;
}

function generateRowsForSchema(schema, path, schemas, isRequired) {
  const completedSchema = completeSchema(schema, schemas);
  return generateRowsForCompleteSchema(completedSchema, path, schemas,
    isRequired);
}

function recursivelyListDirectory(directory) {
  const filenames = fs.readdirSync(directory);
  let results = [];
  filenames.forEach(filename => {
    const filepath = join(directory, filename);
    const stat = fs.statSync(filepath);
    if (stat && stat.isDirectory()) {
      results = results.concat(recursivelyListDirectory(filepath));
    } else {
      results.push(filepath);
    }
  });
  return results;
}

function loadSchema(filepath) {
  return JSON.parse(fs.readFileSync(filepath));
}

function loadSchemas(schemaDirectory) {
  const filepaths = recursivelyListDirectory(schemaDirectory);
  const schemas = {};
  filepaths.forEach(filepath => {
    if (filepath.endsWith('.json')) {
      const schema = loadSchema(filepath);
      schemas[schema.id || schema.title] = schema;
    }
  });
  return schemas;
}

function main() {
  if (process.argv.length !== 4) {
    console.error('usage: generate SCHEMA [SCHEMASDIR]');
    process.exit(2);
  }
  const filepath = process.argv[2];
  const schemas = process.argv.length > 3 ? loadSchemas(process.argv[3]) : {};
  const schema = loadSchema(filepath);
  console.log(formatTable(generateRowsForSchema(schema, [], schemas, true)));
}

main();
<commit_msg>Support absolute links<commit_after>/* eslint no-use-before-define: [2, "nofunc"] */
'use strict';
const fs = require('fs');
const join = require('path').join;

// rows are represented as an array of three strings: name, type, description
function includes(array, item) {
  return array && array.indexOf(item) !== -1;
}

function flatten(arrays) {
  return [].concat.apply([], arrays);
}

function formatTable(rows) {
  const header = ['Name', 'Type', 'Description'];
  const divider = ['----', '----', '-----------'];
  const allRows = [header, divider].concat(rows);
  return allRows.map(row => row.join(' | ')).join('\n');
}

function formatName(path) {
  if (path.length === 0) {
    return '';
  }
  if (path.length === 1) {
    return path[0];
  }
  return '*' + path.slice(0, -1).join('.') + '.* ' + path.slice(-1)[0];
}

function formatType(schema) {
  if (schema.link) {
    const prefix = includes(schema.link, '://') ? '' : '#';
    return '[' + schema.title + '](' + prefix + schema.link + ')';
  }
  if (schema.format) {
    return schema.format + ' string';
  }
  if (schema.enum) {
    return 'string';
  }
  return schema.type || 'object';
}

function formatRow(schema, path, isRequired, typeOverride) {
  const description = (isRequired ? '' : '*Optional* ')
                    + (schema.description || '');
  return [formatName(path), typeOverride || formatType(schema), description];
}

function sortKeys(keys, requiredKeys) {
  const result = requiredKeys ? requiredKeys.slice() : [];
  result.forEach(key => {
    if (!includes(keys, key)) {
      throw new Error('Property in "required" not in "properties": ' + key);
    }
  });
  keys.sort().forEach(key => {
    if (!includes(result, key)) {
      result.push(key);
    }
  });
  return result;
}

function generateRowsForObject(schema, path, schemas, isRequired) {
  const keys = sortKeys(Object.keys(schema.properties), schema.required);
  const rows = flatten(keys.map(name => {
    const isRequiredField = includes(schema.required, name);
    return generateRowsForSchema(
      schema.properties[name], path.concat([name]), schemas, isRequiredField);
  }));
  return path.length > 0 ?
    [formatRow(schema, path, isRequired)].concat(rows) : rows;
}

function generateRowsForArray(schema, path, schemas, isRequired) {
  const newPath = path.slice(0, -1).concat([path.slice(-1)[0] + '[]']);
  const rows = generateRowsForSchema(schema.items, newPath, schemas, true);
  if (rows.length === 1) {
    const typeOverride = 'array\\<' + rows[0][1] + '\\>';
    return [formatRow(schema, path, isRequired, typeOverride)];
  }
  const firstRow = formatRow(schema, path, isRequired);
  return [firstRow].concat(rows);
}

function removeDuplicates(rows) {
  const hash = {};
  const result = [];
  rows.forEach(row => {
    const key = row.join('|');
    if (!hash[key]) {
      result.push(row);
      hash[key] = true;
    }
  });
  return result;
}

function generateRowsForBranch(type, branchSchemas, path, schemas, isRequired) {
  const rows = flatten(branchSchemas.map(branchSchema =>
    generateRowsForSchema(branchSchema, path, schemas, isRequired)));
  return [['TODO', type, path.join('.')]].concat(removeDuplicates(rows));
}

function generateRowsForCompleteSchema(schema, path, schemas, isRequired) {
  if (schema.link) {
    return [formatRow(schema, path, isRequired)];
  }
  if (schema.type === 'array') {
    if (path.length > 0) {
      return generateRowsForArray(schema, path, schemas, isRequired);
    }
    return generateRowsForSchema(schema.items, path, schemas, true);
  }
  if (schema.properties) {
    return generateRowsForObject(schema, path, schemas, isRequired);
  }
  if (schema.oneOf) {
    return generateRowsForBranch(
      'oneOf', schema.oneOf, path, schemas, isRequired);
  }
  if (schema.anyOf) {
    return generateRowsForBranch(
      'anyOf', schema.anyOf, path, schemas, isRequired);
  }
  return [formatRow(schema, path, isRequired)];
}

function assign(destination, source) {
  for (let key in source) { // eslint-disable-line
    if (source.hasOwnProperty(key)) {
      destination[key] = source[key];
    }
  }
  return destination;
}

function completeSchema(schema, schemas) {
  if (schema.$ref) {
    const refSchema = schemas[schema.$ref];
    if (!refSchema) {
      throw new Error('Could not find schema for: ' + schema.$ref);
    }
    return assign(assign({}, refSchema), schema);
  }
  return schema;
}

function generateRowsForSchema(schema, path, schemas, isRequired) {
  const completedSchema = completeSchema(schema, schemas);
  return generateRowsForCompleteSchema(completedSchema, path, schemas,
    isRequired);
}

function recursivelyListDirectory(directory) {
  const filenames = fs.readdirSync(directory);
  let results = [];
  filenames.forEach(filename => {
    const filepath = join(directory, filename);
    const stat = fs.statSync(filepath);
    if (stat && stat.isDirectory()) {
      results = results.concat(recursivelyListDirectory(filepath));
    } else {
      results.push(filepath);
    }
  });
  return results;
}

function loadSchema(filepath) {
  return JSON.parse(fs.readFileSync(filepath));
}

function loadSchemas(schemaDirectory) {
  const filepaths = recursivelyListDirectory(schemaDirectory);
  const schemas = {};
  filepaths.forEach(filepath => {
    if (filepath.endsWith('.json')) {
      const schema = loadSchema(filepath);
      schemas[schema.id || schema.title] = schema;
    }
  });
  return schemas;
}

function main() {
  if (process.argv.length !== 4) {
    console.error('usage: generate SCHEMA [SCHEMASDIR]');
    process.exit(2);
  }
  const filepath = process.argv[2];
  const schemas = process.argv.length > 3 ? loadSchemas(process.argv[3]) : {};
  const schema = loadSchema(filepath);
  console.log(formatTable(generateRowsForSchema(schema, [], schemas, true)));
}

main();
<|endoftext|>


<commit_before>/*

generate.js - generate ASCII text representation of PEG grammar

The MIT License (MIT)

Copyright (c) 2015-2016 Dale Schumacher

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

*/
"use strict";

var generate = module.exports;

var q = function quote(o) {
    return JSON.stringify(o);
}

var indentDepth = 0;
var indentWidth = 2;
var indentSpace =
'                                                                ';
var indent = function indent() {
    return indentSpace.substring(0, indentDepth);
};

/*
 * text(grammar[, indent]) - generate ASCII text representation of PEG grammar
 */
generate.text = function text(grammar, width, source) {
    indentWidth = width || indentWidth;
    source = source || '';
    var s = '';

    s += '/* Generated PEG grammar */\n';
    s += '"use strict";\n';
    s += 'var grammar = module.exports;\n';
    s += '\n';
    s += 'var PEG = require("./PEG.js");\n';
    s += '\n';
    s += 'grammar.build = function build(sponsor, log) {\n';
    indentDepth += indentWidth;
    s += indent() + 'var pf = PEG.factory(sponsor);\n';
    s += indent() + 'var ns = pf.namespace(log);\n';
    s += '\n';

    for (var name in grammar) {
        if (grammar.hasOwnProperty(name)) {
            var rule = grammar[name];
            s += '/*\n';
            s += source.substring(rule.start.pos, rule.end.pos);
            s += '*/\n';
            s += indent() + textRule(name, rule);
            s += '\n\n';
        }
    }

    s += indent() + 'return ns;  // return grammar namespace\n';
    indentDepth -= indentWidth;
    s += '};\n';

    return s;
};

var textRule = function textRule(name, rule) {
    var s = '';

    s += 'ns.define(' + q(name) + ',\n';
    indentDepth += indentWidth;
    s += indent() + textExpression(rule.expr) + '\n';
    indentDepth -= indentWidth;
    s += indent() + ');';

    return s;
};

var textExpression = function textExpression(list) {
    if (list.length == 1) {
        return textSequence(list[0]);
    }

    var s = '';
    var a = [];

    s += 'pf.alt([\n';
    indentDepth += indentWidth;
    for (var i = 0; i < list.length; ++i) {
        a[i] = textSequence(list[i]);
    }
    s += indent() + a.join(',\n' + indent()) + '\n';
    indentDepth -= indentWidth;
    s += indent() + '])';

    return s;
};

var textSequence = function textSequence(list) {
    if (list.length == 1) {
        return textTerm(list[0]);
    }

    var s = '';
    var a = [];

    s += 'pf.seq([\n';
    indentDepth += indentWidth;
    for (var i = 0; i < list.length; ++i) {
        a[i] = textTerm(list[i]);
    }
    s += indent() + a.join(',\n' + indent()) + '\n';
    indentDepth -= indentWidth;
    s += indent() + '])';

    return s;
};

var textTerm = function textTerm(term) {
    if ((typeof term === 'object') && (term.length > 0)) {
        return textExpression(term);
    } else if (term.type === 'Literal') {
        if ((term.open === '\'') && (term.close === '\'')) {
            return textString(term.ptrn);
        } else {
            return textLiteral(term.ptrn);
        }
    } else if (term.type === 'Class') {
        return textClass(term.ptrn);
    } else if (term.type === 'Object') {
        return textObject(term.ptrn);
    }

    var s = '';

    if (term.type === 'Name') {
        s += 'ns.call(' + q(term.ptrn) + ')';
    } else if (term.type === 'AND') {
        s += 'pf.follow(\n';
        indentDepth += indentWidth;
        s += indent() + textTerm(term.ptrn) + '\n';
        indentDepth -= indentWidth;
        s += indent() + ')';
    } else if (term.type === 'NOT') {
        s += 'pf.not(\n';
        indentDepth += indentWidth;
        s += indent() + textTerm(term.ptrn) + '\n';
        indentDepth -= indentWidth;
        s += indent() + ')';
    } else if (term.type === 'QUESTION') {
        s += 'pf.opt(\n';
        indentDepth += indentWidth;
        s += indent() + textTerm(term.ptrn) + '\n';
        indentDepth -= indentWidth;
        s += indent() + ')';
    } else if (term.type === 'STAR') {
        s += 'pf.star(\n';
        indentDepth += indentWidth;
        s += indent() + textTerm(term.ptrn) + '\n';
        indentDepth -= indentWidth;
        s += indent() + ')';
    } else if (term.type === 'PLUS') {
        s += 'pf.plus(\n';
        indentDepth += indentWidth;
        s += indent() + textTerm(term.ptrn) + '\n';
        indentDepth -= indentWidth;
        s += indent() + ')';
    } else if (term.type === 'DOT') {
        s += 'pf.any';
    } else {
        s += q(term);
    }

    return s;
};

var textCharacter = function textCharacter(code) {
    var s = '';

    s += 'pf.term(';
    s += q(String.fromCharCode(code));
    s += ')';

    return s;
};

var sq = function sq(list) {
    var s = '';
    for (var i = 0; i < list.length; ++i) {
        s += String.fromCharCode(list[i]);
    }
    return q(s);
};
var textString = function textString(list) {
    var s = '';

    s += 'pf.term(';
    s += sq(list);
    s += ')';

    return s;
};

var textLiteral = function textLiteral(list) {
    if (list.length == 1) {
        return textCharacter(list[0]);
    }

    var s = '';
    var a = [];

    s += 'pf.seq([\n';
    indentDepth += indentWidth;
    for (var i = 0; i < list.length; ++i) {
        a[i] = textCharacter(list[i]);
    }
    s += indent() + a.join(',\n' + indent()) + '\n';
    indentDepth -= indentWidth;
    s += indent() + '])';

    return s;
};

var rq = function rq(code) {
    var s = q(String.fromCharCode(code));
    s = s.substring(1, s.length - 1);  // drop outer quotes
    if ((s === '[') || (s === ']')) {
        s = '\\' + s;
    }
    return s;
};
var textClass = function textClass(list) {
    var s = '';

    s += 'pf.if(function cond(token) {\n';
    indentDepth += indentWidth;
    s += indent() + 'return /[';
    for (var i = 0; i < list.length; ++i) {
        var range = list[i];
        if (typeof range === 'number') {
            s += rq(range);
        } else {
            s += rq(range[0]) + '-' + rq(range[1]);
        }
    }
    s += ']/.test(token);\n';
    indentDepth -= indentWidth;
    s += indent() + '})';

    return s;
};

var textObject = function textObject(list) {
    var s = '';
    var a = [];

    s += 'pf.object({\n';
    indentDepth += indentWidth;
    for (var i = 0; i < list.length; ++i) {
        var rule = list[i];
        if (rule.type === 'Property') {
            a.push(q(rule.name) + ': '
                + sq(rule.value.ptrn));
        }
    }
    s += indent() + a.join(',\n' + indent()) + '\n';
    indentDepth -= indentWidth;
    s += indent() + '})';

    return s;
};
<commit_msg>Generate arrow-functions for if-predicates<commit_after>/*

generate.js - generate ASCII text representation of PEG grammar

The MIT License (MIT)

Copyright (c) 2015-2016 Dale Schumacher

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

*/
"use strict";

var generate = module.exports;

var q = function quote(o) {
    return JSON.stringify(o);
}

var indentDepth = 0;
var indentWidth = 2;
var indentSpace =
'                                                                ';
var indent = function indent() {
    return indentSpace.substring(0, indentDepth);
};

/*
 * text(grammar[, indent]) - generate ASCII text representation of PEG grammar
 */
generate.text = function text(grammar, width, source) {
    indentWidth = width || indentWidth;
    source = source || '';
    var s = '';

    s += '/* Generated PEG grammar */\n';
    s += '"use strict";\n';
    s += 'var grammar = module.exports;\n';
    s += '\n';
    s += 'var PEG = require("./PEG.js");\n';
    s += '\n';
    s += 'grammar.build = function build(sponsor, log) {\n';
    indentDepth += indentWidth;
    s += indent() + 'var pf = PEG.factory(sponsor);\n';
    s += indent() + 'var ns = pf.namespace(log);\n';
    s += '\n';

    for (var name in grammar) {
        if (grammar.hasOwnProperty(name)) {
            var rule = grammar[name];
            s += '/*\n';
            s += source.substring(rule.start.pos, rule.end.pos);
            s += '*/\n';
            s += indent() + textRule(name, rule);
            s += '\n\n';
        }
    }

    s += indent() + 'return ns;  // return grammar namespace\n';
    indentDepth -= indentWidth;
    s += '};\n';

    return s;
};

var textRule = function textRule(name, rule) {
    var s = '';

    s += 'ns.define(' + q(name) + ',\n';
    indentDepth += indentWidth;
    s += indent() + textExpression(rule.expr) + '\n';
    indentDepth -= indentWidth;
    s += indent() + ');';

    return s;
};

var textExpression = function textExpression(list) {
    if (list.length == 1) {
        return textSequence(list[0]);
    }

    var s = '';
    var a = [];

    s += 'pf.alt([\n';
    indentDepth += indentWidth;
    for (var i = 0; i < list.length; ++i) {
        a[i] = textSequence(list[i]);
    }
    s += indent() + a.join(',\n' + indent()) + '\n';
    indentDepth -= indentWidth;
    s += indent() + '])';

    return s;
};

var textSequence = function textSequence(list) {
    if (list.length == 1) {
        return textTerm(list[0]);
    }

    var s = '';
    var a = [];

    s += 'pf.seq([\n';
    indentDepth += indentWidth;
    for (var i = 0; i < list.length; ++i) {
        a[i] = textTerm(list[i]);
    }
    s += indent() + a.join(',\n' + indent()) + '\n';
    indentDepth -= indentWidth;
    s += indent() + '])';

    return s;
};

var textTerm = function textTerm(term) {
    if ((typeof term === 'object') && (term.length > 0)) {
        return textExpression(term);
    } else if (term.type === 'Literal') {
        if ((term.open === '\'') && (term.close === '\'')) {
            return textString(term.ptrn);
        } else {
            return textLiteral(term.ptrn);
        }
    } else if (term.type === 'Class') {
        return textClass(term.ptrn);
    } else if (term.type === 'Object') {
        return textObject(term.ptrn);
    }

    var s = '';

    if (term.type === 'Name') {
        s += 'ns.call(' + q(term.ptrn) + ')';
    } else if (term.type === 'AND') {
        s += 'pf.follow(\n';
        indentDepth += indentWidth;
        s += indent() + textTerm(term.ptrn) + '\n';
        indentDepth -= indentWidth;
        s += indent() + ')';
    } else if (term.type === 'NOT') {
        s += 'pf.not(\n';
        indentDepth += indentWidth;
        s += indent() + textTerm(term.ptrn) + '\n';
        indentDepth -= indentWidth;
        s += indent() + ')';
    } else if (term.type === 'QUESTION') {
        s += 'pf.opt(\n';
        indentDepth += indentWidth;
        s += indent() + textTerm(term.ptrn) + '\n';
        indentDepth -= indentWidth;
        s += indent() + ')';
    } else if (term.type === 'STAR') {
        s += 'pf.star(\n';
        indentDepth += indentWidth;
        s += indent() + textTerm(term.ptrn) + '\n';
        indentDepth -= indentWidth;
        s += indent() + ')';
    } else if (term.type === 'PLUS') {
        s += 'pf.plus(\n';
        indentDepth += indentWidth;
        s += indent() + textTerm(term.ptrn) + '\n';
        indentDepth -= indentWidth;
        s += indent() + ')';
    } else if (term.type === 'DOT') {
        s += 'pf.any';
    } else {
        s += q(term);
    }

    return s;
};

var textCharacter = function textCharacter(code) {
    var s = '';

    s += 'pf.term(';
    s += q(String.fromCharCode(code));
    s += ')';

    return s;
};

var sq = function sq(list) {
    var s = '';
    for (var i = 0; i < list.length; ++i) {
        s += String.fromCharCode(list[i]);
    }
    return q(s);
};
var textString = function textString(list) {
    var s = '';

    s += 'pf.term(';
    s += sq(list);
    s += ')';

    return s;
};

var textLiteral = function textLiteral(list) {
    if (list.length == 1) {
        return textCharacter(list[0]);
    }

    var s = '';
    var a = [];

    s += 'pf.seq([\n';
    indentDepth += indentWidth;
    for (var i = 0; i < list.length; ++i) {
        a[i] = textCharacter(list[i]);
    }
    s += indent() + a.join(',\n' + indent()) + '\n';
    indentDepth -= indentWidth;
    s += indent() + '])';

    return s;
};

var rq = function rq(code) {
    var s = q(String.fromCharCode(code));
    s = s.substring(1, s.length - 1);  // drop outer quotes
    if ((s === '[') || (s === ']')) {
        s = '\\' + s;
    }
    return s;
};
var textClass = function textClass(list) {
    var s = '';

    s += 'pf.if(token => /[';
    for (var i = 0; i < list.length; ++i) {
        var range = list[i];
        if (typeof range === 'number') {
            s += rq(range);
        } else {
            s += rq(range[0]) + '-' + rq(range[1]);
        }
    }
    s += ']/.test(token))';

    return s;
};

var textObject = function textObject(list) {
    var s = '';
    var a = [];

    s += 'pf.object({\n';
    indentDepth += indentWidth;
    for (var i = 0; i < list.length; ++i) {
        var rule = list[i];
        if (rule.type === 'Property') {
            a.push(q(rule.name) + ': '
                + sq(rule.value.ptrn));
        }
    }
    s += indent() + a.join(',\n' + indent()) + '\n';
    indentDepth -= indentWidth;
    s += indent() + '})';

    return s;
};
<|endoftext|>