Added infrastructure for generating HTML snippets.

master
Pacman Ghost 6 years ago
parent 0f5c0be51f
commit dc535d9d6d
  1. 1
      .pylintrc
  2. 1
      MANIFEST.in
  3. 1
      vasl_templates/webapp/config/constants.py
  4. 3
      vasl_templates/webapp/data/default-templates/scenario.j2
  5. 1
      vasl_templates/webapp/data/default-templates/victory_conditions.j2
  6. 23
      vasl_templates/webapp/generate.py
  7. 37
      vasl_templates/webapp/static/css/main.css
  8. 96
      vasl_templates/webapp/static/growl/jquery.growl.css
  9. 311
      vasl_templates/webapp/static/growl/jquery.growl.js
  10. 577
      vasl_templates/webapp/static/jinja.js
  11. 0
      vasl_templates/webapp/static/jquery-3.3.1.min.js
  12. 80
      vasl_templates/webapp/static/main.js
  13. 112
      vasl_templates/webapp/static/utils.js
  14. 22
      vasl_templates/webapp/templates/main.html
  15. 6
      vasl_templates/webapp/tests/conftest.py
  16. 96
      vasl_templates/webapp/tests/test_generate.py
  17. 20
      vasl_templates/webapp/tests/utils.py

@ -135,6 +135,7 @@ disable=print-statement,
exception-escape,
comprehension-escape,
bad-whitespace,
bad-continuation,
invalid-name,
wrong-import-position

@ -1,3 +1,4 @@
recursive-include vasl_templates/webapp/config *.*
recursive-include vasl_templates/webapp/data *.*
recursive-include vasl_templates/webapp/static *.*
recursive-include vasl_templates/webapp/templates *.*

@ -7,3 +7,4 @@ APP_VERSION = "v0.1"
APP_DESCRIPTION = "Generate HTML for use in VASL scenarios."
BASE_DIR = os.path.abspath( os.path.join( os.path.split(__file__)[0], ".." ) )
DATA_DIR = os.path.join( BASE_DIR, "data" )

@ -0,0 +1,3 @@
name = [{{SCENARIO_NAME}}]
loc = [{{SCENARIO_LOCATION}}]
date = [{{SCENARIO_DATE}}]

@ -1,3 +1,26 @@
""" Webapp handlers. """
import os
from flask import jsonify
from vasl_templates.webapp import app
from vasl_templates.webapp.config.constants import DATA_DIR
# ---------------------------------------------------------------------
@app.route( "/templates" )
def get_templates():
"""Get the specified templates."""
# load the default templates
templates = {}
dname = os.path.join( DATA_DIR, "default-templates" )
for fname in os.listdir(dname):
if os.path.splitext(fname)[1] != ".j2":
continue
fname2 = os.path.join( dname, fname )
with open(fname2,"r") as fp:
templates[os.path.splitext(fname)[0]] = fp.read()
return jsonify( templates )

@ -4,7 +4,12 @@ body { height: 100% ; }
/* -------------------------------------------------------------------- */
#tabs { position: absolute ; top: 5px ; bottom: 5px ; left: 5px ; right: 5px ; }
#tabs {
display: none ;
position: absolute ; top: 5px ; bottom: 5px ; left: 5px ; right: 5px ;
}
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
#tabs-scenario {
display: grid ; display: -ms-grid ;
@ -53,6 +58,32 @@ body { height: 100% ; }
height: 100% ;
}
fieldset { height: calc(100% - 10px) ; margin: 0 5px 5px 5px ; }
fieldset { height: calc(100% - 30px) ; margin: 0 5px 5px 5px ; padding: 10px ; }
#tabs-other fieldset { border: none ; }
fieldset legend { padding: 0 0.2em 0 0.2em ; }
fieldset legend { padding: 0 0.2em 0 0.2em ; font-style: italic ; font-weight: bold ; }
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
.label { font-weight: bold ; }
input[type="text"] { margin-bottom: 0.25em ; }
#panel-scenario .form-grid {
display: grid ; display: -ms-grid ;
grid-template-columns: 5em 1fr ; -ms-grid-columns: 5em 1fr ;
}
#panel-scenario input[type="button"] { float: right ; }
/* FUDGE! IE hackamathon follows (nb: <label> doesn't work, we use <div> for labels instead :-/) */
#panel-scenario div[data-labelfor="scenario_name"] { -ms-grid-row: 1 ; -ms-grid-column: 1 ; }
#panel-scenario input[name="scenario_name"] { -ms-grid-row: 1 ; -ms-grid-column: 2 ; }
#panel-scenario div[data-labelfor="scenario_location"] { -ms-grid-row: 2 ; -ms-grid-column: 1 ; }
#panel-scenario input[name="scenario_location"] { -ms-grid-row: 2 ; -ms-grid-column: 2 ; }
#panel-scenario div[data-labelfor="scenario_date"] { -ms-grid-row: 3 ; -ms-grid-column: 1 ; }
#panel-scenario div.scenario_date { -ms-grid-row: 3 ; -ms-grid-column: 2 ; }
#panel-vc textarea[name="victory_conditions"] { width: calc(100% - 2px) ; height: calc(100% - 1.5em) ; resize: none ; }
#panel-vc input[type="button"] { float: right ; }
/* -------------------------------------------------------------------- */
.growl-title { display: none ; }
.growl ul { margin-left: 1em ; }

@ -0,0 +1,96 @@
/* jQuery Growl
* Copyright 2015 Kevin Sylvestre
* 1.3.5
*/
.ontop, #growls-default, #growls-tl, #growls-tr, #growls-bl, #growls-br, #growls-tc, #growls-bc, #growls-cc, #growls-cl, #growls-cr {
z-index: 50000;
position: fixed; }
#growls-default {
top: 10px;
right: 10px; }
#growls-tl {
top: 10px;
left: 10px; }
#growls-tr {
top: 10px;
right: 10px; }
#growls-bl {
bottom: 10px;
left: 10px; }
#growls-br {
bottom: 10px;
right: 10px; }
#growls-tc {
top: 10px;
right: 10px;
left: 10px; }
#growls-bc {
bottom: 10px;
right: 10px;
left: 10px; }
#growls-cc {
top: 50%;
left: 50%;
margin-left: -125px; }
#growls-cl {
top: 50%;
left: 10px; }
#growls-cr {
top: 50%;
right: 10px; }
#growls-tc .growl, #growls-bc .growl {
margin-left: auto;
margin-right: auto; }
.growl {
opacity: 0.8;
filter: alpha(opacity=80);
position: relative;
border-radius: 4px;
-webkit-transition: all 0.4s ease-in-out;
-moz-transition: all 0.4s ease-in-out;
transition: all 0.4s ease-in-out; }
.growl.growl-incoming {
opacity: 0;
filter: alpha(opacity=0); }
.growl.growl-outgoing {
opacity: 0;
filter: alpha(opacity=0); }
.growl.growl-small {
width: 200px;
padding: 5px;
margin: 5px; }
.growl.growl-medium {
width: 250px;
padding: 10px;
margin: 10px; }
.growl.growl-large {
width: 300px;
padding: 15px;
margin: 15px; }
.growl.growl-default {
color: #FFF;
background: #7f8c8d; }
.growl.growl-error {
color: #FFF;
background: #C0392B; }
.growl.growl-notice {
color: #FFF;
background: #2ECC71; }
.growl.growl-warning {
color: #FFF;
background: #F39C12; }
.growl .growl-close {
cursor: pointer;
float: right;
font-size: 14px;
line-height: 18px;
font-weight: normal;
font-family: helvetica, verdana, sans-serif; }
.growl .growl-title {
font-size: 18px;
line-height: 24px; }
.growl .growl-message {
font-size: 14px;
line-height: 18px; }

@ -0,0 +1,311 @@
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
// Generated by CoffeeScript 2.1.0
(function () {
/*
jQuery Growl
Copyright 2015 Kevin Sylvestre
1.3.5
*/
"use strict";
var $, Animation, Growl;
$ = jQuery;
Animation = function () {
var Animation = function () {
function Animation() {
_classCallCheck(this, Animation);
}
_createClass(Animation, null, [{
key: "transition",
value: function transition($el) {
var el, ref, result, type;
el = $el[0];
ref = this.transitions;
for (type in ref) {
result = ref[type];
if (el.style[type] != null) {
return result;
}
}
}
}]);
return Animation;
}();
;
Animation.transitions = {
"webkitTransition": "webkitTransitionEnd",
"mozTransition": "mozTransitionEnd",
"oTransition": "oTransitionEnd",
"transition": "transitionend"
};
return Animation;
}();
Growl = function () {
var Growl = function () {
_createClass(Growl, null, [{
key: "growl",
value: function growl() {
var settings = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
return new Growl(settings);
}
}]);
function Growl() {
var settings = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
_classCallCheck(this, Growl);
this.render = this.render.bind(this);
this.bind = this.bind.bind(this);
this.unbind = this.unbind.bind(this);
this.mouseEnter = this.mouseEnter.bind(this);
this.mouseLeave = this.mouseLeave.bind(this);
this.click = this.click.bind(this);
this.close = this.close.bind(this);
this.cycle = this.cycle.bind(this);
this.waitAndDismiss = this.waitAndDismiss.bind(this);
this.present = this.present.bind(this);
this.dismiss = this.dismiss.bind(this);
this.remove = this.remove.bind(this);
this.animate = this.animate.bind(this);
this.$growls = this.$growls.bind(this);
this.$growl = this.$growl.bind(this);
this.html = this.html.bind(this);
this.content = this.content.bind(this);
this.container = this.container.bind(this);
this.settings = $.extend({}, Growl.settings, settings);
this.initialize(this.settings.location);
this.render();
}
_createClass(Growl, [{
key: "initialize",
value: function initialize(location) {
var id;
id = 'growls-' + location;
return $('body:not(:has(#' + id + '))').append('<div id="' + id + '" />');
}
}, {
key: "render",
value: function render() {
var $growl;
$growl = this.$growl();
this.$growls(this.settings.location).append($growl);
if (this.settings.fixed) {
this.present();
} else {
this.cycle();
}
}
}, {
key: "bind",
value: function bind() {
var $growl = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.$growl();
$growl.on("click", this.click);
if (this.settings.delayOnHover) {
$growl.on("mouseenter", this.mouseEnter);
$growl.on("mouseleave", this.mouseLeave);
}
return $growl.on("contextmenu", this.close).find("." + this.settings.namespace + "-close").on("click", this.close);
}
}, {
key: "unbind",
value: function unbind() {
var $growl = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.$growl();
$growl.off("click", this.click);
if (this.settings.delayOnHover) {
$growl.off("mouseenter", this.mouseEnter);
$growl.off("mouseleave", this.mouseLeave);
}
return $growl.off("contextmenu", this.close).find("." + this.settings.namespace + "-close").off("click", this.close);
}
}, {
key: "mouseEnter",
value: function mouseEnter(event) {
var $growl;
$growl = this.$growl();
return $growl.stop(true, true);
}
}, {
key: "mouseLeave",
value: function mouseLeave(event) {
return this.waitAndDismiss();
}
}, {
key: "click",
value: function click(event) {
if (this.settings.url != null) {
event.preventDefault();
event.stopPropagation();
return window.open(this.settings.url);
}
}
}, {
key: "close",
value: function close(event) {
var $growl;
event.preventDefault();
event.stopPropagation();
$growl = this.$growl();
return $growl.stop().queue(this.dismiss).queue(this.remove);
}
}, {
key: "cycle",
value: function cycle() {
var $growl;
$growl = this.$growl();
return $growl.queue(this.present).queue(this.waitAndDismiss());
}
}, {
key: "waitAndDismiss",
value: function waitAndDismiss() {
var $growl;
$growl = this.$growl();
return $growl.delay(this.settings.duration).queue(this.dismiss).queue(this.remove);
}
}, {
key: "present",
value: function present(callback) {
var $growl;
$growl = this.$growl();
this.bind($growl);
return this.animate($growl, this.settings.namespace + "-incoming", 'out', callback);
}
}, {
key: "dismiss",
value: function dismiss(callback) {
var $growl;
$growl = this.$growl();
this.unbind($growl);
return this.animate($growl, this.settings.namespace + "-outgoing", 'in', callback);
}
}, {
key: "remove",
value: function remove(callback) {
this.$growl().remove();
return typeof callback === "function" ? callback() : void 0;
}
}, {
key: "animate",
value: function animate($element, name) {
var direction = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'in';
var callback = arguments[3];
var transition;
transition = Animation.transition($element);
$element[direction === 'in' ? 'removeClass' : 'addClass'](name);
$element.offset().position;
$element[direction === 'in' ? 'addClass' : 'removeClass'](name);
if (callback == null) {
return;
}
if (transition != null) {
$element.one(transition, callback);
} else {
callback();
}
}
}, {
key: "$growls",
value: function $growls(location) {
var base;
if (this.$_growls == null) {
this.$_growls = [];
}
return (base = this.$_growls)[location] != null ? base[location] : base[location] = $('#growls-' + location);
}
}, {
key: "$growl",
value: function $growl() {
return this.$_growl != null ? this.$_growl : this.$_growl = $(this.html());
}
}, {
key: "html",
value: function html() {
return this.container(this.content());
}
}, {
key: "content",
value: function content() {
return "<div class='" + this.settings.namespace + "-close'>" + this.settings.close + "</div>\n<div class='" + this.settings.namespace + "-title'>" + this.settings.title + "</div>\n<div class='" + this.settings.namespace + "-message'>" + this.settings.message + "</div>";
}
}, {
key: "container",
value: function container(content) {
return "<div class='" + this.settings.namespace + " " + this.settings.namespace + "-" + this.settings.style + " " + this.settings.namespace + "-" + this.settings.size + "'>\n " + content + "\n</div>";
}
}]);
return Growl;
}();
;
Growl.settings = {
namespace: 'growl',
duration: 3200,
close: "&#215;",
location: "default",
style: "default",
size: "medium",
delayOnHover: true
};
return Growl;
}();
this.Growl = Growl;
$.growl = function () {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
return Growl.growl(options);
};
$.growl.error = function () {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var settings;
settings = {
title: "Error!",
style: "error"
};
return $.growl($.extend(settings, options));
};
$.growl.notice = function () {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var settings;
settings = {
title: "Notice!",
style: "notice"
};
return $.growl($.extend(settings, options));
};
$.growl.warning = function () {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var settings;
settings = {
title: "Warning!",
style: "warning"
};
return $.growl($.extend(settings, options));
};
}).call(this);

@ -0,0 +1,577 @@
/*!
* Jinja Templating for JavaScript v0.1.8
* https://github.com/sstur/jinja-js
*
* This is a slimmed-down Jinja2 implementation [http://jinja.pocoo.org/]
*
* In the interest of simplicity, it deviates from Jinja2 as follows:
* - Line statements, cycle, super, macro tags and block nesting are not implemented
* - auto escapes html by default (the filter is "html" not "e")
* - Only "html" and "safe" filters are built in
* - Filters are not valid in expressions; `foo|length > 1` is not valid
* - Expression Tests (`if num is odd`) not implemented (`is` translates to `==` and `isnot` to `!=`)
*
* Notes:
* - if property is not found, but method '_get' exists, it will be called with the property name (and cached)
* - `{% for n in obj %}` iterates the object's keys; get the value with `{% for n in obj %}{{ obj[n] }}{% endfor %}`
* - subscript notation `a[0]` takes literals or simple variables but not `a[item.key]`
* - `.2` is not a valid number literal; use `0.2`
*
*/
/*global require, exports, module, define */
var jinja;
(function(definition) {
if (typeof exports === 'object' && typeof module === 'object') {
// CommonJS/Node
definition(require, exports, module);
//backwards compatibility
if (typeof define === 'function') {
define('jinja', function() {
this.exports = module.exports;
});
}
return;
}
if (typeof define === 'function') {
//AMD or Other
return define.amd ? define(['require', 'exports'], definition) : define('jinja', definition);
}
definition(function() {}, jinja = {});
})(function(require, jinja) {
"use strict";
var STRINGS = /'(\\.|[^'])*'|"(\\.|[^"'"])*"/g;
var IDENTS_AND_NUMS = /([$_a-z][$\w]*)|([+-]?\d+(\.\d+)?)/g;
var NUMBER = /^[+-]?\d+(\.\d+)?$/;
//non-primitive literals (array and object literals)
var NON_PRIMITIVES = /\[[@#~](,[@#~])*\]|\[\]|\{([@i]:[@#~])(,[@i]:[@#~])*\}|\{\}/g;
//bare identifiers such as variables and in object literals: {foo: 'value'}
var IDENTIFIERS = /[$_a-z][$\w]*/ig;
var VARIABLES = /i(\.i|\[[@#i]\])*/g;
var ACCESSOR = /(\.i|\[[@#i]\])/g;
var OPERATORS = /(===?|!==?|>=?|<=?|&&|\|\||[+\-\*\/%])/g;
//extended (english) operators
var EOPS = /(^|[^$\w])(and|or|not|is|isnot)([^$\w]|$)/g;
var LEADING_SPACE = /^\s+/;
var TRAILING_SPACE = /\s+$/;
var START_TOKEN = /\{\{\{|\{\{|\{%|\{#/;
var TAGS = {
'{{{': /^('(\\.|[^'])*'|"(\\.|[^"'"])*"|.)+?\}\}\}/,
'{{': /^('(\\.|[^'])*'|"(\\.|[^"'"])*"|.)+?\}\}/,
'{%': /^('(\\.|[^'])*'|"(\\.|[^"'"])*"|.)+?%\}/,
'{#': /^('(\\.|[^'])*'|"(\\.|[^"'"])*"|.)+?#\}/
};
var delimeters = {
'{%': 'directive',
'{{': 'output',
'{#': 'comment'
};
var operators = {
and: '&&',
or: '||',
not: '!',
is: '==',
isnot: '!='
};
var constants = {
'true': true,
'false': false,
'null': null
};
function Parser() {
this.nest = [];
this.compiled = [];
this.childBlocks = 0;
this.parentBlocks = 0;
this.isSilent = false;
}
Parser.prototype.push = function(line) {
if (!this.isSilent) {
this.compiled.push(line);
}
};
Parser.prototype.parse = function(src) {
this.tokenize(src);
return this.compiled;
};
Parser.prototype.tokenize = function(src) {
var lastEnd = 0, parser = this, trimLeading = false;
matchAll(src, START_TOKEN, function(open, index, src) {
//here we match the rest of the src against a regex for this tag
var match = src.slice(index + open.length).match(TAGS[open]);
match = (match ? match[0] : '');
//here we sub out strings so we don't get false matches
var simplified = match.replace(STRINGS, '@');
//if we don't have a close tag or there is a nested open tag
if (!match || ~simplified.indexOf(open)) {
return index + 1;
}
var inner = match.slice(0, 0 - open.length);
//check for white-space collapse syntax
if (inner.charAt(0) == '-') var wsCollapseLeft = true;
if (inner.slice(-1) == '-') var wsCollapseRight = true;
inner = inner.replace(/^-|-$/g, '').trim();
//if we're in raw mode and we are not looking at an "endraw" tag, move along
if (parser.rawMode && (open + inner) != '{%endraw') {
return index + 1;
}
var text = src.slice(lastEnd, index);
lastEnd = index + open.length + match.length;
if (trimLeading) text = trimLeft(text);
if (wsCollapseLeft) text = trimRight(text);
if (wsCollapseRight) trimLeading = true;
if (open == '{{{') {
//liquid-style: make {{{x}}} => {{x|safe}}
open = '{{';
inner += '|safe';
}
parser.textHandler(text);
parser.tokenHandler(open, inner);
});
var text = src.slice(lastEnd);
if (trimLeading) text = trimLeft(text);
this.textHandler(text);
};
Parser.prototype.textHandler = function(text) {
this.push('write(' + JSON.stringify(text) + ');');
};
Parser.prototype.tokenHandler = function(open, inner) {
var type = delimeters[open];
if (type == 'directive') {
this.compileTag(inner);
} else
if (type == 'output') {
var extracted = this.extractEnt(inner, STRINGS, '@');
//replace || operators with ~
extracted.src = extracted.src.replace(/\|\|/g, '~').split('|');
//put back || operators
extracted.src = extracted.src.map(function(part) {
return part.split('~').join('||');
});
var parts = this.injectEnt(extracted, '@');
if (parts.length > 1) {
var filters = parts.slice(1).map(this.parseFilter.bind(this));
this.push('filter(' + this.parseExpr(parts[0]) + ',' + filters.join(',') + ');');
} else {
this.push('filter(' + this.parseExpr(parts[0]) + ');');
}
}
};
Parser.prototype.compileTag = function(str) {
var directive = str.split(' ')[0];
var handler = tagHandlers[directive];
if (!handler) {
throw new Error('Invalid tag: ' + str);
}
handler.call(this, str.slice(directive.length).trim());
};
Parser.prototype.parseFilter = function(src) {
src = src.trim();
var match = src.match(/[:(]/);
var i = match ? match.index : -1;
if (i < 0) return JSON.stringify([src]);
var name = src.slice(0, i);
var args = src.charAt(i) == ':' ? src.slice(i + 1) : src.slice(i + 1, -1);
args = this.parseExpr(args, {terms: true});
return '[' + JSON.stringify(name) + ',' + args + ']';
};
Parser.prototype.extractEnt = function(src, regex, placeholder) {
var subs = [], isFunc = typeof placeholder == 'function';
src = src.replace(regex, function(str) {
var replacement = isFunc ? placeholder(str) : placeholder;
if (replacement) {
subs.push(str);
return replacement;
}
return str;
});
return {src: src, subs: subs};
};
Parser.prototype.injectEnt = function(extracted, placeholder) {
var src = extracted.src, subs = extracted.subs, isArr = Array.isArray(src);
var arr = (isArr) ? src : [src];
var re = new RegExp('[' + placeholder + ']', 'g'), i = 0;
arr.forEach(function(src, index) {
arr[index] = src.replace(re, function() {
return subs[i++];
});
});
return isArr ? arr : arr[0];
};
//replace complex literals without mistaking subscript notation with array literals
Parser.prototype.replaceComplex = function(s) {
var parsed = this.extractEnt(s, /i(\.i|\[[@#i]\])+/g, 'v');
parsed.src = parsed.src.replace(NON_PRIMITIVES, '~');
return this.injectEnt(parsed, 'v');
};
//parse expression containing literals (including objects/arrays) and variables (including dot and subscript notation)
//valid expressions: `a + 1 > b.c or c == null`, `a and b[1] != c`, `(a < b) or (c < d and e)`, 'a || [1]`
Parser.prototype.parseExpr = function(src, opts) {
opts = opts || {};
//extract string literals -> @
var parsed1 = this.extractEnt(src, STRINGS, '@');
//note: this will catch {not: 1} and a.is; could we replace temporarily and then check adjacent chars?
parsed1.src = parsed1.src.replace(EOPS, function(s, before, op, after) {
return (op in operators) ? before + operators[op] + after : s;
});
//sub out non-string literals (numbers/true/false/null) -> #
// the distinction is necessary because @ can be object identifiers, # cannot
var parsed2 = this.extractEnt(parsed1.src, IDENTS_AND_NUMS, function(s) {
return (s in constants || NUMBER.test(s)) ? '#' : null;
});
//sub out object/variable identifiers -> i
var parsed3 = this.extractEnt(parsed2.src, IDENTIFIERS, 'i');
//remove white-space
parsed3.src = parsed3.src.replace(/\s+/g, '');
//the rest of this is simply to boil the expression down and check validity
var simplified = parsed3.src;
//sub out complex literals (objects/arrays) -> ~
// the distinction is necessary because @ and # can be subscripts but ~ cannot
while (simplified != (simplified = this.replaceComplex(simplified)));
//now @ represents strings, # represents other primitives and ~ represents non-primitives
//replace complex variables (those with dot/subscript accessors) -> v
while (simplified != (simplified = simplified.replace(/i(\.i|\[[@#i]\])+/, 'v')));
//empty subscript or complex variables in subscript, are not permitted
simplified = simplified.replace(/[iv]\[v?\]/g, 'x');
//sub in "i" for @ and # and ~ and v (now "i" represents all literals, variables and identifiers)
simplified = simplified.replace(/[@#~v]/g, 'i');
//sub out operators
simplified = simplified.replace(OPERATORS, '%');
//allow 'not' unary operator
simplified = simplified.replace(/!+[i]/g, 'i');
var terms = opts.terms ? simplified.split(',') : [simplified];
terms.forEach(function(term) {
//simplify logical grouping
while (term != (term = term.replace(/\(i(%i)*\)/g, 'i')));
if (!term.match(/^i(%i)*$/)) {
throw new Error('Invalid expression: ' + src);
}
});
parsed3.src = parsed3.src.replace(VARIABLES, this.parseVar.bind(this));
parsed2.src = this.injectEnt(parsed3, 'i');
parsed1.src = this.injectEnt(parsed2, '#');
return this.injectEnt(parsed1, '@');
};
Parser.prototype.parseVar = function(src) {
var args = Array.prototype.slice.call(arguments);
var str = args.pop(), index = args.pop();
//quote bare object identifiers (might be a reserved word like {while: 1})
if (src == 'i' && str.charAt(index + 1) == ':') {
return '"i"';
}
var parts = ['"i"'];
src.replace(ACCESSOR, function(part) {
if (part == '.i') {
parts.push('"i"');
} else
if (part == '[i]') {
parts.push('get("i")');
} else {
parts.push(part.slice(1, -1));
}
});
return 'get(' + parts.join(',') + ')';
};
//escapes a name to be used as a javascript identifier
Parser.prototype.escName = function(str) {
return str.replace(/\W/g, function(s) {
return '$' + s.charCodeAt(0).toString(16);
});
};
Parser.prototype.parseQuoted = function(str) {
if (str.charAt(0) == "'") {
str = str.slice(1, -1).replace(/\\.|"/, function(s) {
if (s == "\\'") return "'";
return s.charAt(0) == '\\' ? s : ('\\' + s);
});
str = '"' + str + '"';
}
//todo: try/catch or deal with invalid characters (linebreaks, control characters)
return JSON.parse(str);
};
//the context 'this' inside tagHandlers is the parser instance
var tagHandlers = {
'if': function(expr) {
this.push('if (' + this.parseExpr(expr) + ') {');
this.nest.unshift('if');
},
'else': function() {
if (this.nest[0] == 'for') {
this.push('}, function() {');
} else {
this.push('} else {');
}
},
'elseif': function(expr) {
this.push('} else if (' + this.parseExpr(expr) + ') {');
},
'endif': function() {
this.nest.shift();
this.push('}');
},
'for': function(str) {
var i = str.indexOf(' in ');
var name = str.slice(0, i).trim();
var expr = str.slice(i + 4).trim();
this.push('each(' + this.parseExpr(expr) + ',' + JSON.stringify(name) + ',function() {');
this.nest.unshift('for');
},
'endfor': function() {
this.nest.shift();
this.push('});');
},
'raw': function() {
this.rawMode = true;
},
'endraw': function() {
this.rawMode = false;
},
'set': function(stmt) {
var i = stmt.indexOf('=');
var name = stmt.slice(0, i).trim();
var expr = stmt.slice(i + 1).trim();
this.push('set(' + JSON.stringify(name) + ',' + this.parseExpr(expr) + ');');
},
'block': function(name) {
if (this.isParent) {
++this.parentBlocks;
var blockName = 'block_' + (this.escName(name) || this.parentBlocks);
this.push('block(typeof ' + blockName + ' == "function" ? ' + blockName + ' : function() {');
} else
if (this.hasParent) {
this.isSilent = false;
++this.childBlocks;
blockName = 'block_' + (this.escName(name) || this.childBlocks);
this.push('function ' + blockName + '() {');
}
this.nest.unshift('block');
},
'endblock': function() {
this.nest.shift();
if (this.isParent) {
this.push('});');
} else
if (this.hasParent) {
this.push('}');
this.isSilent = true;
}
},
'extends': function(name) {
name = this.parseQuoted(name);
var parentSrc = this.readTemplateFile(name);
this.isParent = true;
this.tokenize(parentSrc);
this.isParent = false;
this.hasParent = true;
//silence output until we enter a child block
this.isSilent = true;
},
'include': function(name) {
name = this.parseQuoted(name);
var incSrc = this.readTemplateFile(name);
this.isInclude = true;
this.tokenize(incSrc);
this.isInclude = false;
}
};
//liquid style
tagHandlers.assign = tagHandlers.set;
//python/django style
tagHandlers.elif = tagHandlers.elseif;
var getRuntime = function runtime(data, opts) {
var defaults = {autoEscape: 'html'};
var _toString = Object.prototype.toString;
var _hasOwnProperty = Object.prototype.hasOwnProperty;
var getKeys = Object.keys || function(obj) {
var keys = [];
for (var n in obj) if (_hasOwnProperty.call(obj, n)) keys.push(n);
return keys;
};
var isArray = Array.isArray || function(obj) {
return _toString.call(obj) === '[object Array]';
};
var create = Object.create || function(obj) {
function F() {}
F.prototype = obj;
return new F();
};
var toString = function(val) {
if (val == null) return '';
return (typeof val.toString == 'function') ? val.toString() : _toString.call(val);
};
var extend = function(dest, src) {
var keys = getKeys(src);
for (var i = 0, len = keys.length; i < len; i++) {
var key = keys[i];
dest[key] = src[key];
}
return dest;
};
//get a value, lexically, starting in current context; a.b -> get("a","b")
var get = function() {
var val, n = arguments[0], c = stack.length;
while (c--) {
val = stack[c][n];
if (typeof val != 'undefined') break;
}
for (var i = 1, len = arguments.length; i < len; i++) {
if (val == null) continue;
n = arguments[i];
val = (_hasOwnProperty.call(val, n)) ? val[n] : (typeof val._get == 'function' ? (val[n] = val._get(n)) : null);
}
return (val == null) ? null : val;
};
var set = function(n, val) {
stack[stack.length - 1][n] = val;
};
var push = function(ctx) {
stack.push(ctx || {});
};
var pop = function() {
stack.pop();
};
var write = function(str) {
output.push(str);
};
var filter = function(val) {
for (var i = 1, len = arguments.length; i < len; i++) {
var arr = arguments[i], name = arr[0], filter = filters[name];
if (filter) {
arr[0] = val;
//now arr looks like [val, arg1, arg2]
val = filter.apply(data, arr);
} else {
throw new Error('Invalid filter: ' + name);
}
}
if (opts.autoEscape && name != opts.autoEscape && name != 'safe') {
//auto escape if not explicitly safe or already escaped
val = filters[opts.autoEscape].call(data, val);
}
output.push(val);
};
var each = function(obj, loopvar, fn1, fn2) {
if (obj == null) return;
var arr = isArray(obj) ? obj : getKeys(obj), len = arr.length;
var ctx = {loop: {length: len, first: arr[0], last: arr[len - 1]}};
push(ctx);
for (var i = 0; i < len; i++) {
extend(ctx.loop, {index: i + 1, index0: i});
fn1(ctx[loopvar] = arr[i]);
}
if (len == 0 && fn2) fn2();
pop();
};
var block = function(fn) {
push();
fn();
pop();
};
var render = function() {
return output.join('');
};
data = data || {};
opts = extend(defaults, opts || {});
var filters = extend({
html: function(val) {
return toString(val)
.split('&').join('&amp;')
.split('<').join('&lt;')
.split('>').join('&gt;')
.split('"').join('&quot;');
},
safe: function(val) {
return val;
}
}, opts.filters || {});
var stack = [create(data || {})], output = [];
return {get: get, set: set, push: push, pop: pop, write: write, filter: filter, each: each, block: block, render: render};
};
var runtime;
jinja.compile = function(markup, opts) {
opts = opts || {};
var parser = new Parser();
parser.readTemplateFile = this.readTemplateFile;
var code = [];
code.push('function render($) {');
code.push('var get = $.get, set = $.set, push = $.push, pop = $.pop, write = $.write, filter = $.filter, each = $.each, block = $.block;');
code.push.apply(code, parser.parse(markup));
code.push('return $.render();');
code.push('}');
code = code.join('\n');
if (opts.runtime === false) {
var fn = new Function('data', 'options', 'return (' + code + ')(runtime(data, options))');
} else {
runtime = runtime || (runtime = getRuntime.toString());
fn = new Function('data', 'options', 'return (' + code + ')((' + runtime + ')(data, options))');
}
return {render: fn};
};
jinja.render = function(markup, data, opts) {
var tmpl = jinja.compile(markup);
return tmpl.render(data, opts);
};
jinja.templateFiles = [];
jinja.readTemplateFile = function(name) {
var templateFiles = this.templateFiles || [];
var templateFile = templateFiles[name];
if (templateFile == null) {
throw new Error('Template file not found: ' + name);
}
return templateFile;
};
/*!
* Helpers
*/
function trimLeft(str) {
return str.replace(LEADING_SPACE, '');
}
function trimRight(str) {
return str.replace(TRAILING_SPACE, '');
}
function matchAll(str, reg, fn) {
//copy as global
reg = new RegExp(reg.source, 'g' + (reg.ignoreCase ? 'i' : '') + (reg.multiline ? 'm' : ''));
var match;
while ((match = reg.exec(str))) {
var result = fn(match[0], match.index, str);
if (typeof result == 'number') {
reg.lastIndex = result;
}
}
}
});

@ -1,3 +1,10 @@
var gDefaultTemplates = {} ;
// NOTE: These fields aren't mandatory in the sense that snippet generation will fail
// if they're not set, but they're really, really, really expected to be there.
var _MANDATORY_PARAMS = {
scenario: { "SCENARIO_NAME": "scenario name", "SCENARIO_DATE": "scenario date" },
}
// --------------------------------------------------------------------
@ -6,9 +13,16 @@ $(document).ready( function () {
// initialize
$("#tabs").tabs( {
heightStyle: "fill",
} ) ;
} ).show() ;
var navHeight = $("#tabs .ui-tabs-nav").height() ;
// get the default templates
$.getJSON( gGetTemplatesUrl, function(data) {
gDefaultTemplates = data ;
} ).fail( function( xhr, status, errorMsg ) {
showErrorMsg( "Can't get the default templates:<pre>" + escapeHTML(errorMsg) + "</pre>" ) ;
} ) ;
// FUDGE! CSS grids don't seem to update their layout vertically when
// inside a jQuery tab control - we do it manually :-/
var prevHeight = [] ;
@ -25,4 +39,68 @@ $(document).ready( function () {
} ) ;
} ) ;
$(window).trigger( "resize" ) ;
// handle requests to generate HTML snippets
$("input[type='button'].generate").click( function() {
generate_snippet( $(this) )
} ) ;
} ) ;
// --------------------------------------------------------------------
function generate_snippet( $btn )
{
// collect all the template parameters
var params = {} ;
add_param = function($elem) { params[ $elem.attr("name").toUpperCase() ] = $elem.val() ; }
$("input[type='text'].param").each( function() { add_param($(this)) ; } ) ;
$("textarea.param").each( function() { add_param($(this)) ; } ) ;
// check for mandatory parameters
var template_id = $btn.data( "id" ) ;
if ( template_id in _MANDATORY_PARAMS ) {
var missing_params = [] ;
for ( var param_id in _MANDATORY_PARAMS[template_id] ) {
if ( ! (param_id in params && params[param_id].length > 0) )
missing_params.push( _MANDATORY_PARAMS[template_id][param_id] ) ;
}
if ( missing_params.length > 0 ) {
var buf = [ "Missing parameters:<ul>" ] ;
for ( var i=0 ; i < missing_params.length ; ++i )
buf.push( "<li>" + escapeHTML(missing_params[i]) ) ;
buf.push( "</ul>" ) ;
showWarningMsg( buf.join("") ) ;
}
}
// get the template to generate the snippet from
if ( ! (template_id in gDefaultTemplates) ) {
showErrorMsg( "Unknown template: " + escapeHTML(template_id) ) ;
return ;
}
try {
var func = jinja.compile( gDefaultTemplates[template_id] ).render ;
}
catch( ex ) {
showErrorMsg( "Can't compile template:<pre>" + escapeHTML(ex) + "</pre>" ) ;
return ;
}
// process the template
try {
var val = func( params ) ;
}
catch( ex ) {
showErrorMsg( "Can't process template <em>\"" + template_id + "\"</em>:<pre>" + escapeHTML(ex) + "</pre>" ) ;
return ;
}
try {
copyToClipboard( val ) ;
}
catch( ex ) {
showErrorMsg( "Can't copy to the clipboard:<pre>" + escapeHTML(ex) + "</pre>" ) ;
return ;
}
showInfoMsg( "The HTML snippet has been copied to the clipboard." ) ;
}

@ -0,0 +1,112 @@
// --------------------------------------------------------------------
function copyToClipboard( val )
{
// IE-specific code path to prevent textarea being shown while dialog is visible
if ( window.clipboardData && window.clipboardData.setData ) {
clipboardData.setData( "Text", val ) ;
return ;
}
if ( document.queryCommandSupported && document.queryCommandSupported("copy") ) {
// create a textarea to hold the content
var textarea = document.createElement( "textarea" ) ;
textarea.style.position = "fixed" ; // prevent scrolling to bottom in MS Edge
document.body.appendChild( textarea ) ;
textarea.textContent = val ;
// copy the textarea content to the clipboard
textarea.select() ;
try {
document.execCommand( "copy" ) ;
if ( getUrlParam("log-clipboard") )
console.log( "CLIPBOARD:", val ) ;
}
catch( ex ) {
showErrorMsg( "Can't copy to the clipboard:<pre>" + escapeHTML(ex) + "</pre>" ) ;
}
finally {
document.body.removeChild( textarea ) ;
}
}
}
// --------------------------------------------------------------------
function showInfoMsg( msg )
{
// show the informational message
$.growl( {
style: "notice",
title: null,
message: msg,
location: "br",
} ) ;
storeMsgForTestSuite( "_last-info_", msg ) ;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function showWarningMsg( msg )
{
// show the warning message
$.growl( {
style: "warning",
title: null,
message: msg,
location: "br",
} ) ;
storeMsgForTestSuite( "_last-warning_", msg ) ;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function showErrorMsg( msg )
{
// show the error message
$.growl( {
style: "error",
title: null,
message: msg,
location: "br",
fixed: true,
} ) ;
storeMsgForTestSuite( "_last-error_", msg ) ;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function storeMsgForTestSuite( id, msg )
{
// store a message for the test suite
if ( ! getUrlParam( "store_msgs" ) )
return ;
var $elem = $( "#"+id ) ;
if ( $elem.length === 0 ) {
// NOTE: The <div> we store the message in must be visible, otherwise
// Selenium doesn't return any text for it :-/
$elem = $( "<div id='" + id + "' style='z-index-999;'></div>" ) ;
$("body").append( $elem ) ;
}
$elem.html( msg ) ;
}
// --------------------------------------------------------------------
function getUrlParam( param )
{
// look for the specified URL parameter
var url = window.location.search.substring( 1 ) ;
var params = url.split( "&" ) ;
for ( var i=0 ; i < params.length ; i++ ) {
var keyval = params[i].split( "=" ) ;
if ( keyval[0] == param )
return keyval[1] ;
}
}
function escapeHTML( val )
{
// escape HTML
return new Option(val).innerHTML ;
}

@ -6,6 +6,7 @@
<title> {{APP_NAME}} </title>
<link rel="shortcut icon" href="{{url_for('static', filename='images/favicon.ico')}}">
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='jquery-ui/jquery-ui.min.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='growl/jquery.growl.css')}}" />
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/main.css')}}" />
</head>
@ -22,10 +23,21 @@
<div id="tabs-scenario">
<div id="panel-scenario">
<fieldset> <legend>Scenario</legend>
<div class="form-grid">
<div class="label" data-labelfor="scenario_name">Name:</div> <input name="scenario_name" type="text" class="param">
<div class="label" data-labelfor="scenario_location">Location:</div> <input name="scenario_location" type="text" class="param">
<div class="label" data-labelfor="scenario_date">Date:</div>
<div class="scenario_date">
<input name="scenario_date" type="text" size="10" class="param">
<input type="button" class="generate" data-id="scenario" value="Go">
</div>
</div>
</fieldset>
</div>
<div id="panel-vc">
<fieldset> <legend>VC</legend>
<fieldset> <legend>Victory Conditions</legend>
<textarea name="victory_conditions" type="text" class="param"> </textarea>
<input type="button" class="generate" data-id="victory_conditions" value="Go">
</fieldset>
</div>
<div id="panel-ssr">
@ -74,8 +86,14 @@
</div>
</body>
<script src="{{url_for('static',filename='jquery/jquery-3.3.1.min.js')}}"></script>
<script src="{{url_for('static',filename='jquery-3.3.1.min.js')}}"></script>
<script src="{{url_for('static',filename='jquery-ui/jquery-ui.min.js')}}"></script>
<script src="{{url_for('static',filename='jinja.js')}}"></script>
<script src="{{url_for('static',filename='growl/jquery.growl.js')}}"></script>
<script>
gGetTemplatesUrl = "{{url_for('get_templates')}}" ;
</script>
<script src="{{url_for('static',filename='main.js')}}"></script>
<script src="{{url_for('static',filename='utils.js')}}"></script>
</html>

@ -11,6 +11,7 @@ from flask import url_for
from vasl_templates.webapp import app
app.testing = True
from vasl_templates.webapp.tests import utils
FLASK_WEBAPP_PORT = 5001
@ -22,10 +23,10 @@ def webapp():
# initialize
# WTF?! https://github.com/pallets/flask/issues/824
def make_webapp_url( endpoint ):
def make_webapp_url( endpoint, **kwargs ):
"""Generate a webapp URL."""
with app.test_request_context():
url = url_for( endpoint, _external=True )
url = url_for( endpoint, _external=True, **kwargs )
return url.replace( "localhost/", "localhost:{}/".format(FLASK_WEBAPP_PORT) )
app.url_for = make_webapp_url
@ -68,6 +69,7 @@ def webdriver():
)
# return the webdriver to the caller
utils._webdriver = driver #pylint: disable=protected-access
yield driver
# clean up

@ -1,3 +1,97 @@
""" Test response generation. """
""" Test HTML snippet generation. """
from vasl_templates.webapp.tests.utils import get_clipboard, get_stored_msg, find_child
# ---------------------------------------------------------------------
# initialize
def _test_snippet( webdriver, template_id, params, expected, expected2 ):
"""Do a single test."""
# set the template parameters
for key,val in params.items():
elem = find_child( webdriver, "input[name='{}']".format(key) )
if not elem:
elem = find_child( webdriver, "textarea[name='{}']".format(key) )
elem.clear()
if val:
elem.send_keys( val )
# generate the snippet
submit = find_child( webdriver, "input[class='generate'][data-id='{}']".format(template_id) )
submit.click()
snippet = get_clipboard()
lines = [ l.strip() for l in snippet.split("\n") ]
snippet = " | ".join( l for l in lines if l )
assert snippet == expected
# check warnings for mandatory parameters
last_warning = get_stored_msg( "_last-warning_" ) or ""
param_names = [ "scenario name", "scenario location", "scenario date" ]
for pname in param_names:
if pname in expected2:
assert pname in last_warning
else:
assert pname not in last_warning
# ---------------------------------------------------------------------
def test_scenario_snippets( webapp, webdriver ):
"""Test HTML snippet generation."""
# initialize
webdriver.get( webapp.url_for( "main", store_msgs=1 ) )
# generate a SCENARIO snippet
_test_snippet( webdriver, "scenario", {
"scenario_name": "my scenario",
"scenario_location": "here",
"scenario_date": "now",
},
"name = [my scenario] | loc = [here] | date = [now]",
[]
)
# generate a SCENARIO snippet with some fields missing
_test_snippet( webdriver, "scenario", {
"scenario_name": "my scenario",
"scenario_location": None,
"scenario_date": None,
},
"name = [my scenario] | loc = [] | date = []",
[ "scenario date" ],
)
# generate a SCENARIO snippet with all fields missing
_test_snippet( webdriver, "scenario", {
"scenario_name": None,
"scenario_location": None,
"scenario_date": None,
},
"name = [] | loc = [] | date = []",
[ "scenario name", "scenario date" ],
)
# ---------------------------------------------------------------------
def test_vc_snippets( webapp, webdriver ):
"""Test HTML snippet generation."""
# initialize
webdriver.get( webapp.url_for( "main", store_msgs=1 ) )
# generate a VC snippet
_test_snippet( webdriver, "victory_conditions", {
"victory_conditions": "Kill 'Em All!",
},
"VC: Kill 'Em All!",
[]
)
# generate a VC snippet
_test_snippet( webdriver, "victory_conditions", {
"victory_conditions": "",
},
"VC:",
[]
)

@ -1,7 +1,27 @@
""" Helper utilities. """
from PyQt5.QtWidgets import QApplication
from selenium.common.exceptions import NoSuchElementException
_webdriver = None
# ---------------------------------------------------------------------
def get_stored_msg( msg_id ):
"""Get a message stored for us by the front-end."""
elem = find_child( _webdriver, "#"+msg_id )
if not elem:
return None
return elem.text
# ---------------------------------------------------------------------
def get_clipboard() :
"""Get the contents of the clipboard."""
app = QApplication( [] ) #pylint: disable=unused-variable
clipboard = QApplication.clipboard()
return clipboard.text()
# ---------------------------------------------------------------------
def find_child( elem, sel ):

Loading…
Cancel
Save