diff --git a/storyteller/README.md b/storyteller/README.md new file mode 100644 index 0000000..61cb06d --- /dev/null +++ b/storyteller/README.md @@ -0,0 +1,5 @@ +# Storyteller + +Storyteller is a toolkit for building multi-page, single-column static websites composed of text and images. All text is input in Markdown format so no knowledge of HTML is required. + +For more information go to the [Storyteller website](https://storyteller20.neocities.org). diff --git a/storyteller/bottle.py b/storyteller/bottle.py new file mode 100644 index 0000000..df867c1 --- /dev/null +++ b/storyteller/bottle.py @@ -0,0 +1,4425 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Bottle is a fast and simple micro-framework for small web applications. It +offers request dispatching (Routes) with URL parameter support, templates, +a built-in HTTP Server and adapters for many third party WSGI/HTTP-server and +template engines - all in a single file and with no dependencies other than the +Python Standard Library. + +Homepage and documentation: http://bottlepy.org/ + +Copyright (c) 2009-2018, Marcel Hellkamp. +License: MIT (see LICENSE for details) +""" + +import sys + +__author__ = 'Marcel Hellkamp' +__version__ = '0.13-dev' +__license__ = 'MIT' + +############################################################################### +# Command-line interface ###################################################### +############################################################################### +# INFO: Some server adapters need to monkey-patch std-lib modules before they +# are imported. This is why some of the command-line handling is done here, but +# the actual call to _main() is at the end of the file. + + +def _cli_parse(args): # pragma: no coverage + from argparse import ArgumentParser + + parser = ArgumentParser(prog=args[0], usage="%(prog)s [options] package.module:app") + opt = parser.add_argument + opt("--version", action="store_true", help="show version number.") + opt("-b", "--bind", metavar="ADDRESS", help="bind socket to ADDRESS.") + opt("-s", "--server", default='wsgiref', help="use SERVER as backend.") + opt("-p", "--plugin", action="append", help="install additional plugin/s.") + opt("-c", "--conf", action="append", metavar="FILE", + help="load config values from FILE.") + opt("-C", "--param", action="append", metavar="NAME=VALUE", + help="override config values.") + opt("--debug", action="store_true", help="start server in debug mode.") + opt("--reload", action="store_true", help="auto-reload on file changes.") + opt('app', help='WSGI app entry point.', nargs='?') + + cli_args = parser.parse_args(args[1:]) + + return cli_args, parser + + +def _cli_patch(cli_args): # pragma: no coverage + parsed_args, _ = _cli_parse(cli_args) + opts = parsed_args + if opts.server: + if opts.server.startswith('gevent'): + import gevent.monkey + gevent.monkey.patch_all() + elif opts.server.startswith('eventlet'): + import eventlet + eventlet.monkey_patch() + + +if __name__ == '__main__': + _cli_patch(sys.argv) + +############################################################################### +# Imports and Python 2/3 unification ########################################## +############################################################################### + + +import base64, calendar, cgi, email.utils, functools, hmac, imp, itertools,\ + mimetypes, os, re, tempfile, threading, time, warnings, weakref, hashlib + +from types import FunctionType +from datetime import date as datedate, datetime, timedelta +from tempfile import TemporaryFile +from traceback import format_exc, print_exc +from unicodedata import normalize + +try: + from ujson import dumps as json_dumps, loads as json_lds +except ImportError: + from json import dumps as json_dumps, loads as json_lds + +# inspect.getargspec was removed in Python 3.6, use +# Signature-based version where we can (Python 3.3+) +try: + from inspect import signature + def getargspec(func): + params = signature(func).parameters + args, varargs, keywords, defaults = [], None, None, [] + for name, param in params.items(): + if param.kind == param.VAR_POSITIONAL: + varargs = name + elif param.kind == param.VAR_KEYWORD: + keywords = name + else: + args.append(name) + if param.default is not param.empty: + defaults.append(param.default) + return (args, varargs, keywords, tuple(defaults) or None) +except ImportError: + try: + from inspect import getfullargspec + def getargspec(func): + spec = getfullargspec(func) + kwargs = makelist(spec[0]) + makelist(spec.kwonlyargs) + return kwargs, spec[1], spec[2], spec[3] + except ImportError: + from inspect import getargspec + + +py = sys.version_info +py3k = py.major > 2 + + +# Workaround for the "print is a keyword/function" Python 2/3 dilemma +# and a fallback for mod_wsgi (resticts stdout/err attribute access) +try: + _stdout, _stderr = sys.stdout.write, sys.stderr.write +except IOError: + _stdout = lambda x: sys.stdout.write(x) + _stderr = lambda x: sys.stderr.write(x) + +# Lots of stdlib and builtin differences. +if py3k: + import http.client as httplib + import _thread as thread + from urllib.parse import urljoin, SplitResult as UrlSplitResult + from urllib.parse import urlencode, quote as urlquote, unquote as urlunquote + urlunquote = functools.partial(urlunquote, encoding='latin1') + from http.cookies import SimpleCookie, Morsel, CookieError + from collections.abc import MutableMapping as DictMixin + import pickle + from io import BytesIO + import configparser + + basestring = str + unicode = str + json_loads = lambda s: json_lds(touni(s)) + callable = lambda x: hasattr(x, '__call__') + imap = map + + def _raise(*a): + raise a[0](a[1]).with_traceback(a[2]) +else: # 2.x + import httplib + import thread + from urlparse import urljoin, SplitResult as UrlSplitResult + from urllib import urlencode, quote as urlquote, unquote as urlunquote + from Cookie import SimpleCookie, Morsel, CookieError + from itertools import imap + import cPickle as pickle + from StringIO import StringIO as BytesIO + import ConfigParser as configparser + from collections import MutableMapping as DictMixin + unicode = unicode + json_loads = json_lds + exec(compile('def _raise(*a): raise a[0], a[1], a[2]', '', 'exec')) + +# Some helpers for string/byte handling +def tob(s, enc='utf8'): + if isinstance(s, unicode): + return s.encode(enc) + return b'' if s is None else bytes(s) + + +def touni(s, enc='utf8', err='strict'): + if isinstance(s, bytes): + return s.decode(enc, err) + return unicode("" if s is None else s) + + +tonat = touni if py3k else tob + + + +# A bug in functools causes it to break if the wrapper is an instance method +def update_wrapper(wrapper, wrapped, *a, **ka): + try: + functools.update_wrapper(wrapper, wrapped, *a, **ka) + except AttributeError: + pass + +# These helpers are used at module level and need to be defined first. +# And yes, I know PEP-8, but sometimes a lower-case classname makes more sense. + + +def depr(major, minor, cause, fix): + text = "Warning: Use of deprecated feature or API. (Deprecated in Bottle-%d.%d)\n"\ + "Cause: %s\n"\ + "Fix: %s\n" % (major, minor, cause, fix) + if DEBUG == 'strict': + raise DeprecationWarning(text) + warnings.warn(text, DeprecationWarning, stacklevel=3) + return DeprecationWarning(text) + + +def makelist(data): # This is just too handy + if isinstance(data, (tuple, list, set, dict)): + return list(data) + elif data: + return [data] + else: + return [] + + +class DictProperty(object): + """ Property that maps to a key in a local dict-like attribute. """ + + def __init__(self, attr, key=None, read_only=False): + self.attr, self.key, self.read_only = attr, key, read_only + + def __call__(self, func): + functools.update_wrapper(self, func, updated=[]) + self.getter, self.key = func, self.key or func.__name__ + return self + + def __get__(self, obj, cls): + if obj is None: return self + key, storage = self.key, getattr(obj, self.attr) + if key not in storage: storage[key] = self.getter(obj) + return storage[key] + + def __set__(self, obj, value): + if self.read_only: raise AttributeError("Read-Only property.") + getattr(obj, self.attr)[self.key] = value + + def __delete__(self, obj): + if self.read_only: raise AttributeError("Read-Only property.") + del getattr(obj, self.attr)[self.key] + + +class cached_property(object): + """ A property that is only computed once per instance and then replaces + itself with an ordinary attribute. Deleting the attribute resets the + property. """ + + def __init__(self, func): + update_wrapper(self, func) + self.func = func + + def __get__(self, obj, cls): + if obj is None: return self + value = obj.__dict__[self.func.__name__] = self.func(obj) + return value + + +class lazy_attribute(object): + """ A property that caches itself to the class object. """ + + def __init__(self, func): + functools.update_wrapper(self, func, updated=[]) + self.getter = func + + def __get__(self, obj, cls): + value = self.getter(cls) + setattr(cls, self.__name__, value) + return value + +############################################################################### +# Exceptions and Events ####################################################### +############################################################################### + + +class BottleException(Exception): + """ A base class for exceptions used by bottle. """ + pass + +############################################################################### +# Routing ###################################################################### +############################################################################### + + +class RouteError(BottleException): + """ This is a base class for all routing related exceptions """ + + +class RouteReset(BottleException): + """ If raised by a plugin or request handler, the route is reset and all + plugins are re-applied. """ + + +class RouterUnknownModeError(RouteError): + + pass + + +class RouteSyntaxError(RouteError): + """ The route parser found something not supported by this router. """ + + +class RouteBuildError(RouteError): + """ The route could not be built. """ + + +def _re_flatten(p): + """ Turn all capturing groups in a regular expression pattern into + non-capturing groups. """ + if '(' not in p: + return p + return re.sub(r'(\\*)(\(\?P<[^>]+>|\((?!\?))', lambda m: m.group(0) if + len(m.group(1)) % 2 else m.group(1) + '(?:', p) + + +class Router(object): + """ A Router is an ordered collection of route->target pairs. It is used to + efficiently match WSGI requests against a number of routes and return + the first target that satisfies the request. The target may be anything, + usually a string, ID or callable object. A route consists of a path-rule + and a HTTP method. + + The path-rule is either a static path (e.g. `/contact`) or a dynamic + path that contains wildcards (e.g. `/wiki/`). The wildcard syntax + and details on the matching order are described in docs:`routing`. + """ + + default_pattern = '[^/]+' + default_filter = 're' + + #: The current CPython regexp implementation does not allow more + #: than 99 matching groups per regular expression. + _MAX_GROUPS_PER_PATTERN = 99 + + def __init__(self, strict=False): + self.rules = [] # All rules in order + self._groups = {} # index of regexes to find them in dyna_routes + self.builder = {} # Data structure for the url builder + self.static = {} # Search structure for static routes + self.dyna_routes = {} + self.dyna_regexes = {} # Search structure for dynamic routes + #: If true, static routes are no longer checked first. + self.strict_order = strict + self.filters = { + 're': lambda conf: (_re_flatten(conf or self.default_pattern), + None, None), + 'int': lambda conf: (r'-?\d+', int, lambda x: str(int(x))), + 'float': lambda conf: (r'-?[\d.]+', float, lambda x: str(float(x))), + 'path': lambda conf: (r'.+?', None, None) + } + + def add_filter(self, name, func): + """ Add a filter. The provided function is called with the configuration + string as parameter and must return a (regexp, to_python, to_url) tuple. + The first element is a string, the last two are callables or None. """ + self.filters[name] = func + + rule_syntax = re.compile('(\\\\*)' + '(?:(?::([a-zA-Z_][a-zA-Z_0-9]*)?()(?:#(.*?)#)?)' + '|(?:<([a-zA-Z_][a-zA-Z_0-9]*)?(?::([a-zA-Z_]*)' + '(?::((?:\\\\.|[^\\\\>])+)?)?)?>))') + + def _itertokens(self, rule): + offset, prefix = 0, '' + for match in self.rule_syntax.finditer(rule): + prefix += rule[offset:match.start()] + g = match.groups() + if g[2] is not None: + depr(0, 13, "Use of old route syntax.", + "Use instead of :name in routes.") + if len(g[0]) % 2: # Escaped wildcard + prefix += match.group(0)[len(g[0]):] + offset = match.end() + continue + if prefix: + yield prefix, None, None + name, filtr, conf = g[4:7] if g[2] is None else g[1:4] + yield name, filtr or 'default', conf or None + offset, prefix = match.end(), '' + if offset <= len(rule) or prefix: + yield prefix + rule[offset:], None, None + + def add(self, rule, method, target, name=None): + """ Add a new rule or replace the target for an existing rule. """ + anons = 0 # Number of anonymous wildcards found + keys = [] # Names of keys + pattern = '' # Regular expression pattern with named groups + filters = [] # Lists of wildcard input filters + builder = [] # Data structure for the URL builder + is_static = True + + for key, mode, conf in self._itertokens(rule): + if mode: + is_static = False + if mode == 'default': mode = self.default_filter + mask, in_filter, out_filter = self.filters[mode](conf) + if not key: + pattern += '(?:%s)' % mask + key = 'anon%d' % anons + anons += 1 + else: + pattern += '(?P<%s>%s)' % (key, mask) + keys.append(key) + if in_filter: filters.append((key, in_filter)) + builder.append((key, out_filter or str)) + elif key: + pattern += re.escape(key) + builder.append((None, key)) + + self.builder[rule] = builder + if name: self.builder[name] = builder + + if is_static and not self.strict_order: + self.static.setdefault(method, {}) + self.static[method][self.build(rule)] = (target, None) + return + + try: + re_pattern = re.compile('^(%s)$' % pattern) + re_match = re_pattern.match + except re.error as e: + raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, e)) + + if filters: + + def getargs(path): + url_args = re_match(path).groupdict() + for name, wildcard_filter in filters: + try: + url_args[name] = wildcard_filter(url_args[name]) + except ValueError: + raise HTTPError(400, 'Path has wrong format.') + return url_args + elif re_pattern.groupindex: + + def getargs(path): + return re_match(path).groupdict() + else: + getargs = None + + flatpat = _re_flatten(pattern) + whole_rule = (rule, flatpat, target, getargs) + + if (flatpat, method) in self._groups: + if DEBUG: + msg = 'Route <%s %s> overwrites a previously defined route' + warnings.warn(msg % (method, rule), RuntimeWarning) + self.dyna_routes[method][ + self._groups[flatpat, method]] = whole_rule + else: + self.dyna_routes.setdefault(method, []).append(whole_rule) + self._groups[flatpat, method] = len(self.dyna_routes[method]) - 1 + + self._compile(method) + + def _compile(self, method): + all_rules = self.dyna_routes[method] + comborules = self.dyna_regexes[method] = [] + maxgroups = self._MAX_GROUPS_PER_PATTERN + for x in range(0, len(all_rules), maxgroups): + some = all_rules[x:x + maxgroups] + combined = (flatpat for (_, flatpat, _, _) in some) + combined = '|'.join('(^%s$)' % flatpat for flatpat in combined) + combined = re.compile(combined).match + rules = [(target, getargs) for (_, _, target, getargs) in some] + comborules.append((combined, rules)) + + def build(self, _name, *anons, **query): + """ Build an URL by filling the wildcards in a rule. """ + builder = self.builder.get(_name) + if not builder: + raise RouteBuildError("No route with that name.", _name) + try: + for i, value in enumerate(anons): + query['anon%d' % i] = value + url = ''.join([f(query.pop(n)) if n else f for (n, f) in builder]) + return url if not query else url + '?' + urlencode(query) + except KeyError as E: + raise RouteBuildError('Missing URL argument: %r' % E.args[0]) + + def match(self, environ): + """ Return a (target, url_args) tuple or raise HTTPError(400/404/405). """ + verb = environ['REQUEST_METHOD'].upper() + path = environ['PATH_INFO'] or '/' + + if verb == 'HEAD': + methods = ['PROXY', verb, 'GET', 'ANY'] + else: + methods = ['PROXY', verb, 'ANY'] + + for method in methods: + if method in self.static and path in self.static[method]: + target, getargs = self.static[method][path] + return target, getargs(path) if getargs else {} + elif method in self.dyna_regexes: + for combined, rules in self.dyna_regexes[method]: + match = combined(path) + if match: + target, getargs = rules[match.lastindex - 1] + return target, getargs(path) if getargs else {} + + # No matching route found. Collect alternative methods for 405 response + allowed = set([]) + nocheck = set(methods) + for method in set(self.static) - nocheck: + if path in self.static[method]: + allowed.add(method) + for method in set(self.dyna_regexes) - allowed - nocheck: + for combined, rules in self.dyna_regexes[method]: + match = combined(path) + if match: + allowed.add(method) + if allowed: + allow_header = ",".join(sorted(allowed)) + raise HTTPError(405, "Method not allowed.", Allow=allow_header) + + # No matching route and no alternative method found. We give up + raise HTTPError(404, "Not found: " + repr(path)) + + +class Route(object): + """ This class wraps a route callback along with route specific metadata and + configuration and applies Plugins on demand. It is also responsible for + turning an URL path rule into a regular expression usable by the Router. + """ + + def __init__(self, app, rule, method, callback, + name=None, + plugins=None, + skiplist=None, **config): + #: The application this route is installed to. + self.app = app + #: The path-rule string (e.g. ``/wiki/``). + self.rule = rule + #: The HTTP method as a string (e.g. ``GET``). + self.method = method + #: The original callback with no plugins applied. Useful for introspection. + self.callback = callback + #: The name of the route (if specified) or ``None``. + self.name = name or None + #: A list of route-specific plugins (see :meth:`Bottle.route`). + self.plugins = plugins or [] + #: A list of plugins to not apply to this route (see :meth:`Bottle.route`). + self.skiplist = skiplist or [] + #: Additional keyword arguments passed to the :meth:`Bottle.route` + #: decorator are stored in this dictionary. Used for route-specific + #: plugin configuration and meta-data. + self.config = app.config._make_overlay() + self.config.load_dict(config) + + @cached_property + def call(self): + """ The route callback with all plugins applied. This property is + created on demand and then cached to speed up subsequent requests.""" + return self._make_callback() + + def reset(self): + """ Forget any cached values. The next time :attr:`call` is accessed, + all plugins are re-applied. """ + self.__dict__.pop('call', None) + + def prepare(self): + """ Do all on-demand work immediately (useful for debugging).""" + self.call + + def all_plugins(self): + """ Yield all Plugins affecting this route. """ + unique = set() + for p in reversed(self.app.plugins + self.plugins): + if True in self.skiplist: break + name = getattr(p, 'name', False) + if name and (name in self.skiplist or name in unique): continue + if p in self.skiplist or type(p) in self.skiplist: continue + if name: unique.add(name) + yield p + + def _make_callback(self): + callback = self.callback + for plugin in self.all_plugins(): + try: + if hasattr(plugin, 'apply'): + callback = plugin.apply(callback, self) + else: + callback = plugin(callback) + except RouteReset: # Try again with changed configuration. + return self._make_callback() + if not callback is self.callback: + update_wrapper(callback, self.callback) + return callback + + def get_undecorated_callback(self): + """ Return the callback. If the callback is a decorated function, try to + recover the original function. """ + func = self.callback + func = getattr(func, '__func__' if py3k else 'im_func', func) + closure_attr = '__closure__' if py3k else 'func_closure' + while hasattr(func, closure_attr) and getattr(func, closure_attr): + attributes = getattr(func, closure_attr) + func = attributes[0].cell_contents + + # in case of decorators with multiple arguments + if not isinstance(func, FunctionType): + # pick first FunctionType instance from multiple arguments + func = filter(lambda x: isinstance(x, FunctionType), + map(lambda x: x.cell_contents, attributes)) + func = list(func)[0] # py3 support + return func + + def get_callback_args(self): + """ Return a list of argument names the callback (most likely) accepts + as keyword arguments. If the callback is a decorated function, try + to recover the original function before inspection. """ + return getargspec(self.get_undecorated_callback())[0] + + def get_config(self, key, default=None): + """ Lookup a config field and return its value, first checking the + route.config, then route.app.config.""" + depr(0, 13, "Route.get_config() is deprectated.", + "The Route.config property already includes values from the" + " application config for missing keys. Access it directly.") + return self.config.get(key, default) + + def __repr__(self): + cb = self.get_undecorated_callback() + return '<%s %r %r>' % (self.method, self.rule, cb) + +############################################################################### +# Application Object ########################################################### +############################################################################### + + +class Bottle(object): + """ Each Bottle object represents a single, distinct web application and + consists of routes, callbacks, plugins, resources and configuration. + Instances are callable WSGI applications. + + :param catchall: If true (default), handle all exceptions. Turn off to + let debugging middleware handle exceptions. + """ + + @lazy_attribute + def _global_config(cls): + cfg = ConfigDict() + cfg.meta_set('catchall', 'validate', bool) + return cfg + + def __init__(self, **kwargs): + #: A :class:`ConfigDict` for app specific configuration. + self.config = self._global_config._make_overlay() + self.config._add_change_listener( + functools.partial(self.trigger_hook, 'config')) + + self.config.update({ + "catchall": True + }) + + if kwargs.get('catchall') is False: + depr(0, 13, "Bottle(catchall) keyword argument.", + "The 'catchall' setting is now part of the app " + "configuration. Fix: `app.config['catchall'] = False`") + self.config['catchall'] = False + if kwargs.get('autojson') is False: + depr(0, 13, "Bottle(autojson) keyword argument.", + "The 'autojson' setting is now part of the app " + "configuration. Fix: `app.config['json.enable'] = False`") + self.config['json.disable'] = True + + self._mounts = [] + + #: A :class:`ResourceManager` for application files + self.resources = ResourceManager() + + self.routes = [] # List of installed :class:`Route` instances. + self.router = Router() # Maps requests to :class:`Route` instances. + self.error_handler = {} + + # Core plugins + self.plugins = [] # List of installed plugins. + self.install(JSONPlugin()) + self.install(TemplatePlugin()) + + #: If true, most exceptions are caught and returned as :exc:`HTTPError` + catchall = DictProperty('config', 'catchall') + + __hook_names = 'before_request', 'after_request', 'app_reset', 'config' + __hook_reversed = {'after_request'} + + @cached_property + def _hooks(self): + return dict((name, []) for name in self.__hook_names) + + def add_hook(self, name, func): + """ Attach a callback to a hook. Three hooks are currently implemented: + + before_request + Executed once before each request. The request context is + available, but no routing has happened yet. + after_request + Executed once after each request regardless of its outcome. + app_reset + Called whenever :meth:`Bottle.reset` is called. + """ + if name in self.__hook_reversed: + self._hooks[name].insert(0, func) + else: + self._hooks[name].append(func) + + def remove_hook(self, name, func): + """ Remove a callback from a hook. """ + if name in self._hooks and func in self._hooks[name]: + self._hooks[name].remove(func) + return True + + def trigger_hook(self, __name, *args, **kwargs): + """ Trigger a hook and return a list of results. """ + return [hook(*args, **kwargs) for hook in self._hooks[__name][:]] + + def hook(self, name): + """ Return a decorator that attaches a callback to a hook. See + :meth:`add_hook` for details.""" + + def decorator(func): + self.add_hook(name, func) + return func + + return decorator + + def _mount_wsgi(self, prefix, app, **options): + segments = [p for p in prefix.split('/') if p] + if not segments: + raise ValueError('WSGI applications cannot be mounted to "/".') + path_depth = len(segments) + + def mountpoint_wrapper(): + try: + request.path_shift(path_depth) + rs = HTTPResponse([]) + + def start_response(status, headerlist, exc_info=None): + if exc_info: + _raise(*exc_info) + if py3k: + # Errors here mean that the mounted WSGI app did not + # follow PEP-3333 (which requires latin1) or used a + # pre-encoding other than utf8 :/ + status = status.encode('latin1').decode('utf8') + headerlist = [(k, v.encode('latin1').decode('utf8')) + for (k, v) in headerlist] + rs.status = status + for name, value in headerlist: + rs.add_header(name, value) + return rs.body.append + + body = app(request.environ, start_response) + rs.body = itertools.chain(rs.body, body) if rs.body else body + return rs + finally: + request.path_shift(-path_depth) + + options.setdefault('skip', True) + options.setdefault('method', 'PROXY') + options.setdefault('mountpoint', {'prefix': prefix, 'target': app}) + options['callback'] = mountpoint_wrapper + + self.route('/%s/<:re:.*>' % '/'.join(segments), **options) + if not prefix.endswith('/'): + self.route('/' + '/'.join(segments), **options) + + def _mount_app(self, prefix, app, **options): + if app in self._mounts or '_mount.app' in app.config: + depr(0, 13, "Application mounted multiple times. Falling back to WSGI mount.", + "Clone application before mounting to a different location.") + return self._mount_wsgi(prefix, app, **options) + + if options: + depr(0, 13, "Unsupported mount options. Falling back to WSGI mount.", + "Do not specify any route options when mounting bottle application.") + return self._mount_wsgi(prefix, app, **options) + + if not prefix.endswith("/"): + depr(0, 13, "Prefix must end in '/'. Falling back to WSGI mount.", + "Consider adding an explicit redirect from '/prefix' to '/prefix/' in the parent application.") + return self._mount_wsgi(prefix, app, **options) + + self._mounts.append(app) + app.config['_mount.prefix'] = prefix + app.config['_mount.app'] = self + for route in app.routes: + route.rule = prefix + route.rule.lstrip('/') + self.add_route(route) + + def mount(self, prefix, app, **options): + """ Mount an application (:class:`Bottle` or plain WSGI) to a specific + URL prefix. Example:: + + parent_app.mount('/prefix/', child_app) + + :param prefix: path prefix or `mount-point`. + :param app: an instance of :class:`Bottle` or a WSGI application. + + Plugins from the parent application are not applied to the routes + of the mounted child application. If you need plugins in the child + application, install them separately. + + While it is possible to use path wildcards within the prefix path + (:class:`Bottle` childs only), it is highly discouraged. + + The prefix path must end with a slash. If you want to access the + root of the child application via `/prefix` in addition to + `/prefix/`, consider adding a route with a 307 redirect to the + parent application. + """ + + if not prefix.startswith('/'): + raise ValueError("Prefix must start with '/'") + + if isinstance(app, Bottle): + return self._mount_app(prefix, app, **options) + else: + return self._mount_wsgi(prefix, app, **options) + + def merge(self, routes): + """ Merge the routes of another :class:`Bottle` application or a list of + :class:`Route` objects into this application. The routes keep their + 'owner', meaning that the :data:`Route.app` attribute is not + changed. """ + if isinstance(routes, Bottle): + routes = routes.routes + for route in routes: + self.add_route(route) + + def install(self, plugin): + """ Add a plugin to the list of plugins and prepare it for being + applied to all routes of this application. A plugin may be a simple + decorator or an object that implements the :class:`Plugin` API. + """ + if hasattr(plugin, 'setup'): plugin.setup(self) + if not callable(plugin) and not hasattr(plugin, 'apply'): + raise TypeError("Plugins must be callable or implement .apply()") + self.plugins.append(plugin) + self.reset() + return plugin + + def uninstall(self, plugin): + """ Uninstall plugins. Pass an instance to remove a specific plugin, a type + object to remove all plugins that match that type, a string to remove + all plugins with a matching ``name`` attribute or ``True`` to remove all + plugins. Return the list of removed plugins. """ + removed, remove = [], plugin + for i, plugin in list(enumerate(self.plugins))[::-1]: + if remove is True or remove is plugin or remove is type(plugin) \ + or getattr(plugin, 'name', True) == remove: + removed.append(plugin) + del self.plugins[i] + if hasattr(plugin, 'close'): plugin.close() + if removed: self.reset() + return removed + + def reset(self, route=None): + """ Reset all routes (force plugins to be re-applied) and clear all + caches. If an ID or route object is given, only that specific route + is affected. """ + if route is None: routes = self.routes + elif isinstance(route, Route): routes = [route] + else: routes = [self.routes[route]] + for route in routes: + route.reset() + if DEBUG: + for route in routes: + route.prepare() + self.trigger_hook('app_reset') + + def close(self): + """ Close the application and all installed plugins. """ + for plugin in self.plugins: + if hasattr(plugin, 'close'): plugin.close() + + def run(self, **kwargs): + """ Calls :func:`run` with the same parameters. """ + run(self, **kwargs) + + def match(self, environ): + """ Search for a matching route and return a (:class:`Route`, urlargs) + tuple. The second value is a dictionary with parameters extracted + from the URL. Raise :exc:`HTTPError` (404/405) on a non-match.""" + return self.router.match(environ) + + def get_url(self, routename, **kargs): + """ Return a string that matches a named route """ + scriptname = request.environ.get('SCRIPT_NAME', '').strip('/') + '/' + location = self.router.build(routename, **kargs).lstrip('/') + return urljoin(urljoin('/', scriptname), location) + + def add_route(self, route): + """ Add a route object, but do not change the :data:`Route.app` + attribute.""" + self.routes.append(route) + self.router.add(route.rule, route.method, route, name=route.name) + if DEBUG: route.prepare() + + def route(self, + path=None, + method='GET', + callback=None, + name=None, + apply=None, + skip=None, **config): + """ A decorator to bind a function to a request URL. Example:: + + @app.route('/hello/') + def hello(name): + return 'Hello %s' % name + + The ```` part is a wildcard. See :class:`Router` for syntax + details. + + :param path: Request path or a list of paths to listen to. If no + path is specified, it is automatically generated from the + signature of the function. + :param method: HTTP method (`GET`, `POST`, `PUT`, ...) or a list of + methods to listen to. (default: `GET`) + :param callback: An optional shortcut to avoid the decorator + syntax. ``route(..., callback=func)`` equals ``route(...)(func)`` + :param name: The name for this route. (default: None) + :param apply: A decorator or plugin or a list of plugins. These are + applied to the route callback in addition to installed plugins. + :param skip: A list of plugins, plugin classes or names. Matching + plugins are not installed to this route. ``True`` skips all. + + Any additional keyword arguments are stored as route-specific + configuration and passed to plugins (see :meth:`Plugin.apply`). + """ + if callable(path): path, callback = None, path + plugins = makelist(apply) + skiplist = makelist(skip) + + def decorator(callback): + if isinstance(callback, basestring): callback = load(callback) + for rule in makelist(path) or yieldroutes(callback): + for verb in makelist(method): + verb = verb.upper() + route = Route(self, rule, verb, callback, + name=name, + plugins=plugins, + skiplist=skiplist, **config) + self.add_route(route) + return callback + + return decorator(callback) if callback else decorator + + def get(self, path=None, method='GET', **options): + """ Equals :meth:`route`. """ + return self.route(path, method, **options) + + def post(self, path=None, method='POST', **options): + """ Equals :meth:`route` with a ``POST`` method parameter. """ + return self.route(path, method, **options) + + def put(self, path=None, method='PUT', **options): + """ Equals :meth:`route` with a ``PUT`` method parameter. """ + return self.route(path, method, **options) + + def delete(self, path=None, method='DELETE', **options): + """ Equals :meth:`route` with a ``DELETE`` method parameter. """ + return self.route(path, method, **options) + + def patch(self, path=None, method='PATCH', **options): + """ Equals :meth:`route` with a ``PATCH`` method parameter. """ + return self.route(path, method, **options) + + def error(self, code=500, callback=None): + """ Register an output handler for a HTTP error code. Can + be used as a decorator or called directly :: + + def error_handler_500(error): + return 'error_handler_500' + + app.error(code=500, callback=error_handler_500) + + @app.error(404) + def error_handler_404(error): + return 'error_handler_404' + + """ + + def decorator(callback): + if isinstance(callback, basestring): callback = load(callback) + self.error_handler[int(code)] = callback + return callback + + return decorator(callback) if callback else decorator + + def default_error_handler(self, res): + return tob(template(ERROR_PAGE_TEMPLATE, e=res, template_settings=dict(name='__ERROR_PAGE_TEMPLATE'))) + + def _handle(self, environ): + path = environ['bottle.raw_path'] = environ['PATH_INFO'] + if py3k: + environ['PATH_INFO'] = path.encode('latin1').decode('utf8', 'ignore') + + environ['bottle.app'] = self + request.bind(environ) + response.bind() + + try: + while True: # Remove in 0.14 together with RouteReset + out = None + try: + self.trigger_hook('before_request') + route, args = self.router.match(environ) + environ['route.handle'] = route + environ['bottle.route'] = route + environ['route.url_args'] = args + out = route.call(**args) + break + except HTTPResponse as E: + out = E + break + except RouteReset: + depr(0, 13, "RouteReset exception deprecated", + "Call route.call() after route.reset() and " + "return the result.") + route.reset() + continue + finally: + if isinstance(out, HTTPResponse): + out.apply(response) + try: + self.trigger_hook('after_request') + except HTTPResponse as E: + out = E + out.apply(response) + except (KeyboardInterrupt, SystemExit, MemoryError): + raise + except Exception as E: + if not self.catchall: raise + stacktrace = format_exc() + environ['wsgi.errors'].write(stacktrace) + environ['wsgi.errors'].flush() + out = HTTPError(500, "Internal Server Error", E, stacktrace) + out.apply(response) + + return out + + def _cast(self, out, peek=None): + """ Try to convert the parameter into something WSGI compatible and set + correct HTTP headers when possible. + Support: False, str, unicode, dict, HTTPResponse, HTTPError, file-like, + iterable of strings and iterable of unicodes + """ + + # Empty output is done here + if not out: + if 'Content-Length' not in response: + response['Content-Length'] = 0 + return [] + # Join lists of byte or unicode strings. Mixed lists are NOT supported + if isinstance(out, (tuple, list))\ + and isinstance(out[0], (bytes, unicode)): + out = out[0][0:0].join(out) # b'abc'[0:0] -> b'' + # Encode unicode strings + if isinstance(out, unicode): + out = out.encode(response.charset) + # Byte Strings are just returned + if isinstance(out, bytes): + if 'Content-Length' not in response: + response['Content-Length'] = len(out) + return [out] + # HTTPError or HTTPException (recursive, because they may wrap anything) + # TODO: Handle these explicitly in handle() or make them iterable. + if isinstance(out, HTTPError): + out.apply(response) + out = self.error_handler.get(out.status_code, + self.default_error_handler)(out) + return self._cast(out) + if isinstance(out, HTTPResponse): + out.apply(response) + return self._cast(out.body) + + # File-like objects. + if hasattr(out, 'read'): + if 'wsgi.file_wrapper' in request.environ: + return request.environ['wsgi.file_wrapper'](out) + elif hasattr(out, 'close') or not hasattr(out, '__iter__'): + return WSGIFileWrapper(out) + + # Handle Iterables. We peek into them to detect their inner type. + try: + iout = iter(out) + first = next(iout) + while not first: + first = next(iout) + except StopIteration: + return self._cast('') + except HTTPResponse as E: + first = E + except (KeyboardInterrupt, SystemExit, MemoryError): + raise + except Exception as error: + if not self.catchall: raise + first = HTTPError(500, 'Unhandled exception', error, format_exc()) + + # These are the inner types allowed in iterator or generator objects. + if isinstance(first, HTTPResponse): + return self._cast(first) + elif isinstance(first, bytes): + new_iter = itertools.chain([first], iout) + elif isinstance(first, unicode): + encoder = lambda x: x.encode(response.charset) + new_iter = imap(encoder, itertools.chain([first], iout)) + else: + msg = 'Unsupported response type: %s' % type(first) + return self._cast(HTTPError(500, msg)) + if hasattr(out, 'close'): + new_iter = _closeiter(new_iter, out.close) + return new_iter + + def wsgi(self, environ, start_response): + """ The bottle WSGI-interface. """ + try: + out = self._cast(self._handle(environ)) + # rfc2616 section 4.3 + if response._status_code in (100, 101, 204, 304)\ + or environ['REQUEST_METHOD'] == 'HEAD': + if hasattr(out, 'close'): out.close() + out = [] + start_response(response._wsgi_status_line(), response.headerlist) + return out + except (KeyboardInterrupt, SystemExit, MemoryError): + raise + except Exception as E: + if not self.catchall: raise + err = '

Critical error while processing request: %s

' \ + % html_escape(environ.get('PATH_INFO', '/')) + if DEBUG: + err += '

Error:

\n
\n%s\n
\n' \ + '

Traceback:

\n
\n%s\n
\n' \ + % (html_escape(repr(E)), html_escape(format_exc())) + environ['wsgi.errors'].write(err) + environ['wsgi.errors'].flush() + headers = [('Content-Type', 'text/html; charset=UTF-8')] + start_response('500 INTERNAL SERVER ERROR', headers, sys.exc_info()) + return [tob(err)] + + def __call__(self, environ, start_response): + """ Each instance of :class:'Bottle' is a WSGI application. """ + return self.wsgi(environ, start_response) + + def __enter__(self): + """ Use this application as default for all module-level shortcuts. """ + default_app.push(self) + return self + + def __exit__(self, exc_type, exc_value, traceback): + default_app.pop() + + def __setattr__(self, name, value): + if name in self.__dict__: + raise AttributeError("Attribute %s already defined. Plugin conflict?" % name) + self.__dict__[name] = value + + +############################################################################### +# HTTP and WSGI Tools ########################################################## +############################################################################### + + +class BaseRequest(object): + """ A wrapper for WSGI environment dictionaries that adds a lot of + convenient access methods and properties. Most of them are read-only. + + Adding new attributes to a request actually adds them to the environ + dictionary (as 'bottle.request.ext.'). This is the recommended + way to store and access request-specific data. + """ + + __slots__ = ('environ', ) + + #: Maximum size of memory buffer for :attr:`body` in bytes. + MEMFILE_MAX = 102400 + + def __init__(self, environ=None): + """ Wrap a WSGI environ dictionary. """ + #: The wrapped WSGI environ dictionary. This is the only real attribute. + #: All other attributes actually are read-only properties. + self.environ = {} if environ is None else environ + self.environ['bottle.request'] = self + + @DictProperty('environ', 'bottle.app', read_only=True) + def app(self): + """ Bottle application handling this request. """ + raise RuntimeError('This request is not connected to an application.') + + @DictProperty('environ', 'bottle.route', read_only=True) + def route(self): + """ The bottle :class:`Route` object that matches this request. """ + raise RuntimeError('This request is not connected to a route.') + + @DictProperty('environ', 'route.url_args', read_only=True) + def url_args(self): + """ The arguments extracted from the URL. """ + raise RuntimeError('This request is not connected to a route.') + + @property + def path(self): + """ The value of ``PATH_INFO`` with exactly one prefixed slash (to fix + broken clients and avoid the "empty path" edge case). """ + return '/' + self.environ.get('PATH_INFO', '').lstrip('/') + + @property + def method(self): + """ The ``REQUEST_METHOD`` value as an uppercase string. """ + return self.environ.get('REQUEST_METHOD', 'GET').upper() + + @DictProperty('environ', 'bottle.request.headers', read_only=True) + def headers(self): + """ A :class:`WSGIHeaderDict` that provides case-insensitive access to + HTTP request headers. """ + return WSGIHeaderDict(self.environ) + + def get_header(self, name, default=None): + """ Return the value of a request header, or a given default value. """ + return self.headers.get(name, default) + + @DictProperty('environ', 'bottle.request.cookies', read_only=True) + def cookies(self): + """ Cookies parsed into a :class:`FormsDict`. Signed cookies are NOT + decoded. Use :meth:`get_cookie` if you expect signed cookies. """ + cookies = SimpleCookie(self.environ.get('HTTP_COOKIE', '')).values() + return FormsDict((c.key, c.value) for c in cookies) + + def get_cookie(self, key, default=None, secret=None, digestmod=hashlib.sha256): + """ Return the content of a cookie. To read a `Signed Cookie`, the + `secret` must match the one used to create the cookie (see + :meth:`BaseResponse.set_cookie`). If anything goes wrong (missing + cookie or wrong signature), return a default value. """ + value = self.cookies.get(key) + if secret: + # See BaseResponse.set_cookie for details on signed cookies. + if value and value.startswith('!') and '?' in value: + sig, msg = map(tob, value[1:].split('?', 1)) + hash = hmac.new(tob(secret), msg, digestmod=digestmod).digest() + if _lscmp(sig, base64.b64encode(hash)): + dst = pickle.loads(base64.b64decode(msg)) + if dst and dst[0] == key: + return dst[1] + return default + return value or default + + @DictProperty('environ', 'bottle.request.query', read_only=True) + def query(self): + """ The :attr:`query_string` parsed into a :class:`FormsDict`. These + values are sometimes called "URL arguments" or "GET parameters", but + not to be confused with "URL wildcards" as they are provided by the + :class:`Router`. """ + get = self.environ['bottle.get'] = FormsDict() + pairs = _parse_qsl(self.environ.get('QUERY_STRING', '')) + for key, value in pairs: + get[key] = value + return get + + @DictProperty('environ', 'bottle.request.forms', read_only=True) + def forms(self): + """ Form values parsed from an `url-encoded` or `multipart/form-data` + encoded POST or PUT request body. The result is returned as a + :class:`FormsDict`. All keys and values are strings. File uploads + are stored separately in :attr:`files`. """ + forms = FormsDict() + forms.recode_unicode = self.POST.recode_unicode + for name, item in self.POST.allitems(): + if not isinstance(item, FileUpload): + forms[name] = item + return forms + + @DictProperty('environ', 'bottle.request.params', read_only=True) + def params(self): + """ A :class:`FormsDict` with the combined values of :attr:`query` and + :attr:`forms`. File uploads are stored in :attr:`files`. """ + params = FormsDict() + for key, value in self.query.allitems(): + params[key] = value + for key, value in self.forms.allitems(): + params[key] = value + return params + + @DictProperty('environ', 'bottle.request.files', read_only=True) + def files(self): + """ File uploads parsed from `multipart/form-data` encoded POST or PUT + request body. The values are instances of :class:`FileUpload`. + + """ + files = FormsDict() + files.recode_unicode = self.POST.recode_unicode + for name, item in self.POST.allitems(): + if isinstance(item, FileUpload): + files[name] = item + return files + + @DictProperty('environ', 'bottle.request.json', read_only=True) + def json(self): + """ If the ``Content-Type`` header is ``application/json`` or + ``application/json-rpc``, this property holds the parsed content + of the request body. Only requests smaller than :attr:`MEMFILE_MAX` + are processed to avoid memory exhaustion. + Invalid JSON raises a 400 error response. + """ + ctype = self.environ.get('CONTENT_TYPE', '').lower().split(';')[0] + if ctype in ('application/json', 'application/json-rpc'): + b = self._get_body_string(self.MEMFILE_MAX) + if not b: + return None + try: + return json_loads(b) + except (ValueError, TypeError): + raise HTTPError(400, 'Invalid JSON') + return None + + def _iter_body(self, read, bufsize): + maxread = max(0, self.content_length) + while maxread: + part = read(min(maxread, bufsize)) + if not part: break + yield part + maxread -= len(part) + + @staticmethod + def _iter_chunked(read, bufsize): + err = HTTPError(400, 'Error while parsing chunked transfer body.') + rn, sem, bs = tob('\r\n'), tob(';'), tob('') + while True: + header = read(1) + while header[-2:] != rn: + c = read(1) + header += c + if not c: raise err + if len(header) > bufsize: raise err + size, _, _ = header.partition(sem) + try: + maxread = int(tonat(size.strip()), 16) + except ValueError: + raise err + if maxread == 0: break + buff = bs + while maxread > 0: + if not buff: + buff = read(min(maxread, bufsize)) + part, buff = buff[:maxread], buff[maxread:] + if not part: raise err + yield part + maxread -= len(part) + if read(2) != rn: + raise err + + @DictProperty('environ', 'bottle.request.body', read_only=True) + def _body(self): + try: + read_func = self.environ['wsgi.input'].read + except KeyError: + self.environ['wsgi.input'] = BytesIO() + return self.environ['wsgi.input'] + body_iter = self._iter_chunked if self.chunked else self._iter_body + body, body_size, is_temp_file = BytesIO(), 0, False + for part in body_iter(read_func, self.MEMFILE_MAX): + body.write(part) + body_size += len(part) + if not is_temp_file and body_size > self.MEMFILE_MAX: + body, tmp = TemporaryFile(mode='w+b'), body + body.write(tmp.getvalue()) + del tmp + is_temp_file = True + self.environ['wsgi.input'] = body + body.seek(0) + return body + + def _get_body_string(self, maxread): + """ Read body into a string. Raise HTTPError(413) on requests that are + to large. """ + if self.content_length > maxread: + raise HTTPError(413, 'Request entity too large') + data = self.body.read(maxread + 1) + if len(data) > maxread: + raise HTTPError(413, 'Request entity too large') + return data + + @property + def body(self): + """ The HTTP request body as a seek-able file-like object. Depending on + :attr:`MEMFILE_MAX`, this is either a temporary file or a + :class:`io.BytesIO` instance. Accessing this property for the first + time reads and replaces the ``wsgi.input`` environ variable. + Subsequent accesses just do a `seek(0)` on the file object. """ + self._body.seek(0) + return self._body + + @property + def chunked(self): + """ True if Chunked transfer encoding was. """ + return 'chunked' in self.environ.get( + 'HTTP_TRANSFER_ENCODING', '').lower() + + #: An alias for :attr:`query`. + GET = query + + @DictProperty('environ', 'bottle.request.post', read_only=True) + def POST(self): + """ The values of :attr:`forms` and :attr:`files` combined into a single + :class:`FormsDict`. Values are either strings (form values) or + instances of :class:`cgi.FieldStorage` (file uploads). + """ + post = FormsDict() + # We default to application/x-www-form-urlencoded for everything that + # is not multipart and take the fast path (also: 3.1 workaround) + if not self.content_type.startswith('multipart/'): + body = tonat(self._get_body_string(self.MEMFILE_MAX), 'latin1') + for key, value in _parse_qsl(body): + post[key] = value + return post + + safe_env = {'QUERY_STRING': ''} # Build a safe environment for cgi + for key in ('REQUEST_METHOD', 'CONTENT_TYPE', 'CONTENT_LENGTH'): + if key in self.environ: safe_env[key] = self.environ[key] + args = dict(fp=self.body, environ=safe_env, keep_blank_values=True) + + if py3k: + args['encoding'] = 'utf8' + post.recode_unicode = False + data = cgi.FieldStorage(**args) + self['_cgi.FieldStorage'] = data #http://bugs.python.org/issue18394 + data = data.list or [] + for item in data: + if item.filename: + post[item.name] = FileUpload(item.file, item.name, + item.filename, item.headers) + else: + post[item.name] = item.value + return post + + @property + def url(self): + """ The full request URI including hostname and scheme. If your app + lives behind a reverse proxy or load balancer and you get confusing + results, make sure that the ``X-Forwarded-Host`` header is set + correctly. """ + return self.urlparts.geturl() + + @DictProperty('environ', 'bottle.request.urlparts', read_only=True) + def urlparts(self): + """ The :attr:`url` string as an :class:`urlparse.SplitResult` tuple. + The tuple contains (scheme, host, path, query_string and fragment), + but the fragment is always empty because it is not visible to the + server. """ + env = self.environ + http = env.get('HTTP_X_FORWARDED_PROTO') \ + or env.get('wsgi.url_scheme', 'http') + host = env.get('HTTP_X_FORWARDED_HOST') or env.get('HTTP_HOST') + if not host: + # HTTP 1.1 requires a Host-header. This is for HTTP/1.0 clients. + host = env.get('SERVER_NAME', '127.0.0.1') + port = env.get('SERVER_PORT') + if port and port != ('80' if http == 'http' else '443'): + host += ':' + port + path = urlquote(self.fullpath) + return UrlSplitResult(http, host, path, env.get('QUERY_STRING'), '') + + @property + def fullpath(self): + """ Request path including :attr:`script_name` (if present). """ + return urljoin(self.script_name, self.path.lstrip('/')) + + @property + def query_string(self): + """ The raw :attr:`query` part of the URL (everything in between ``?`` + and ``#``) as a string. """ + return self.environ.get('QUERY_STRING', '') + + @property + def script_name(self): + """ The initial portion of the URL's `path` that was removed by a higher + level (server or routing middleware) before the application was + called. This script path is returned with leading and tailing + slashes. """ + script_name = self.environ.get('SCRIPT_NAME', '').strip('/') + return '/' + script_name + '/' if script_name else '/' + + def path_shift(self, shift=1): + """ Shift path segments from :attr:`path` to :attr:`script_name` and + vice versa. + + :param shift: The number of path segments to shift. May be negative + to change the shift direction. (default: 1) + """ + script, path = path_shift(self.environ.get('SCRIPT_NAME', '/'), self.path, shift) + self['SCRIPT_NAME'], self['PATH_INFO'] = script, path + + @property + def content_length(self): + """ The request body length as an integer. The client is responsible to + set this header. Otherwise, the real length of the body is unknown + and -1 is returned. In this case, :attr:`body` will be empty. """ + return int(self.environ.get('CONTENT_LENGTH') or -1) + + @property + def content_type(self): + """ The Content-Type header as a lowercase-string (default: empty). """ + return self.environ.get('CONTENT_TYPE', '').lower() + + @property + def is_xhr(self): + """ True if the request was triggered by a XMLHttpRequest. This only + works with JavaScript libraries that support the `X-Requested-With` + header (most of the popular libraries do). """ + requested_with = self.environ.get('HTTP_X_REQUESTED_WITH', '') + return requested_with.lower() == 'xmlhttprequest' + + @property + def is_ajax(self): + """ Alias for :attr:`is_xhr`. "Ajax" is not the right term. """ + return self.is_xhr + + @property + def auth(self): + """ HTTP authentication data as a (user, password) tuple. This + implementation currently supports basic (not digest) authentication + only. If the authentication happened at a higher level (e.g. in the + front web-server or a middleware), the password field is None, but + the user field is looked up from the ``REMOTE_USER`` environ + variable. On any errors, None is returned. """ + basic = parse_auth(self.environ.get('HTTP_AUTHORIZATION', '')) + if basic: return basic + ruser = self.environ.get('REMOTE_USER') + if ruser: return (ruser, None) + return None + + @property + def remote_route(self): + """ A list of all IPs that were involved in this request, starting with + the client IP and followed by zero or more proxies. This does only + work if all proxies support the ```X-Forwarded-For`` header. Note + that this information can be forged by malicious clients. """ + proxy = self.environ.get('HTTP_X_FORWARDED_FOR') + if proxy: return [ip.strip() for ip in proxy.split(',')] + remote = self.environ.get('REMOTE_ADDR') + return [remote] if remote else [] + + @property + def remote_addr(self): + """ The client IP as a string. Note that this information can be forged + by malicious clients. """ + route = self.remote_route + return route[0] if route else None + + def copy(self): + """ Return a new :class:`Request` with a shallow :attr:`environ` copy. """ + return Request(self.environ.copy()) + + def get(self, value, default=None): + return self.environ.get(value, default) + + def __getitem__(self, key): + return self.environ[key] + + def __delitem__(self, key): + self[key] = "" + del (self.environ[key]) + + def __iter__(self): + return iter(self.environ) + + def __len__(self): + return len(self.environ) + + def keys(self): + return self.environ.keys() + + def __setitem__(self, key, value): + """ Change an environ value and clear all caches that depend on it. """ + + if self.environ.get('bottle.request.readonly'): + raise KeyError('The environ dictionary is read-only.') + + self.environ[key] = value + todelete = () + + if key == 'wsgi.input': + todelete = ('body', 'forms', 'files', 'params', 'post', 'json') + elif key == 'QUERY_STRING': + todelete = ('query', 'params') + elif key.startswith('HTTP_'): + todelete = ('headers', 'cookies') + + for key in todelete: + self.environ.pop('bottle.request.' + key, None) + + def __repr__(self): + return '<%s: %s %s>' % (self.__class__.__name__, self.method, self.url) + + def __getattr__(self, name): + """ Search in self.environ for additional user defined attributes. """ + try: + var = self.environ['bottle.request.ext.%s' % name] + return var.__get__(self) if hasattr(var, '__get__') else var + except KeyError: + raise AttributeError('Attribute %r not defined.' % name) + + def __setattr__(self, name, value): + if name == 'environ': return object.__setattr__(self, name, value) + key = 'bottle.request.ext.%s' % name + if key in self.environ: + raise AttributeError("Attribute already defined: %s" % name) + self.environ[key] = value + + def __delattr__(self, name): + try: + del self.environ['bottle.request.ext.%s' % name] + except KeyError: + raise AttributeError("Attribute not defined: %s" % name) + + +def _hkey(key): + if '\n' in key or '\r' in key or '\0' in key: + raise ValueError("Header names must not contain control characters: %r" % key) + return key.title().replace('_', '-') + + +def _hval(value): + value = tonat(value) + if '\n' in value or '\r' in value or '\0' in value: + raise ValueError("Header value must not contain control characters: %r" % value) + return value + + +class HeaderProperty(object): + def __init__(self, name, reader=None, writer=None, default=''): + self.name, self.default = name, default + self.reader, self.writer = reader, writer + self.__doc__ = 'Current value of the %r header.' % name.title() + + def __get__(self, obj, _): + if obj is None: return self + value = obj.get_header(self.name, self.default) + return self.reader(value) if self.reader else value + + def __set__(self, obj, value): + obj[self.name] = self.writer(value) if self.writer else value + + def __delete__(self, obj): + del obj[self.name] + + +class BaseResponse(object): + """ Storage class for a response body as well as headers and cookies. + + This class does support dict-like case-insensitive item-access to + headers, but is NOT a dict. Most notably, iterating over a response + yields parts of the body and not the headers. + + :param body: The response body as one of the supported types. + :param status: Either an HTTP status code (e.g. 200) or a status line + including the reason phrase (e.g. '200 OK'). + :param headers: A dictionary or a list of name-value pairs. + + Additional keyword arguments are added to the list of headers. + Underscores in the header name are replaced with dashes. + """ + + default_status = 200 + default_content_type = 'text/html; charset=UTF-8' + + # Header blacklist for specific response codes + # (rfc2616 section 10.2.3 and 10.3.5) + bad_headers = { + 204: frozenset(('Content-Type', 'Content-Length')), + 304: frozenset(('Allow', 'Content-Encoding', 'Content-Language', + 'Content-Length', 'Content-Range', 'Content-Type', + 'Content-Md5', 'Last-Modified')) + } + + def __init__(self, body='', status=None, headers=None, **more_headers): + self._cookies = None + self._headers = {} + self.body = body + self.status = status or self.default_status + if headers: + if isinstance(headers, dict): + headers = headers.items() + for name, value in headers: + self.add_header(name, value) + if more_headers: + for name, value in more_headers.items(): + self.add_header(name, value) + + def copy(self, cls=None): + """ Returns a copy of self. """ + cls = cls or BaseResponse + assert issubclass(cls, BaseResponse) + copy = cls() + copy.status = self.status + copy._headers = dict((k, v[:]) for (k, v) in self._headers.items()) + if self._cookies: + cookies = copy._cookies = SimpleCookie() + for k,v in self._cookies.items(): + cookies[k] = v.value + cookies[k].update(v) # also copy cookie attributes + return copy + + def __iter__(self): + return iter(self.body) + + def close(self): + if hasattr(self.body, 'close'): + self.body.close() + + @property + def status_line(self): + """ The HTTP status line as a string (e.g. ``404 Not Found``).""" + return self._status_line + + @property + def status_code(self): + """ The HTTP status code as an integer (e.g. 404).""" + return self._status_code + + def _set_status(self, status): + if isinstance(status, int): + code, status = status, _HTTP_STATUS_LINES.get(status) + elif ' ' in status: + if '\n' in status or '\r' in status or '\0' in status: + raise ValueError('Status line must not include control chars.') + status = status.strip() + code = int(status.split()[0]) + else: + raise ValueError('String status line without a reason phrase.') + if not 100 <= code <= 999: + raise ValueError('Status code out of range.') + self._status_code = code + self._status_line = str(status or ('%d Unknown' % code)) + + def _get_status(self): + return self._status_line + + status = property( + _get_status, _set_status, None, + ''' A writeable property to change the HTTP response status. It accepts + either a numeric code (100-999) or a string with a custom reason + phrase (e.g. "404 Brain not found"). Both :data:`status_line` and + :data:`status_code` are updated accordingly. The return value is + always a status string. ''') + del _get_status, _set_status + + @property + def headers(self): + """ An instance of :class:`HeaderDict`, a case-insensitive dict-like + view on the response headers. """ + hdict = HeaderDict() + hdict.dict = self._headers + return hdict + + def __contains__(self, name): + return _hkey(name) in self._headers + + def __delitem__(self, name): + del self._headers[_hkey(name)] + + def __getitem__(self, name): + return self._headers[_hkey(name)][-1] + + def __setitem__(self, name, value): + self._headers[_hkey(name)] = [_hval(value)] + + def get_header(self, name, default=None): + """ Return the value of a previously defined header. If there is no + header with that name, return a default value. """ + return self._headers.get(_hkey(name), [default])[-1] + + def set_header(self, name, value): + """ Create a new response header, replacing any previously defined + headers with the same name. """ + self._headers[_hkey(name)] = [_hval(value)] + + def add_header(self, name, value): + """ Add an additional response header, not removing duplicates. """ + self._headers.setdefault(_hkey(name), []).append(_hval(value)) + + def iter_headers(self): + """ Yield (header, value) tuples, skipping headers that are not + allowed with the current response status code. """ + return self.headerlist + + def _wsgi_status_line(self): + """ WSGI conform status line (latin1-encodeable) """ + if py3k: + return self._status_line.encode('utf8').decode('latin1') + return self._status_line + + @property + def headerlist(self): + """ WSGI conform list of (header, value) tuples. """ + out = [] + headers = list(self._headers.items()) + if 'Content-Type' not in self._headers: + headers.append(('Content-Type', [self.default_content_type])) + if self._status_code in self.bad_headers: + bad_headers = self.bad_headers[self._status_code] + headers = [h for h in headers if h[0] not in bad_headers] + out += [(name, val) for (name, vals) in headers for val in vals] + if self._cookies: + for c in self._cookies.values(): + out.append(('Set-Cookie', _hval(c.OutputString()))) + if py3k: + out = [(k, v.encode('utf8').decode('latin1')) for (k, v) in out] + return out + + content_type = HeaderProperty('Content-Type') + content_length = HeaderProperty('Content-Length', reader=int, default=-1) + expires = HeaderProperty( + 'Expires', + reader=lambda x: datetime.utcfromtimestamp(parse_date(x)), + writer=lambda x: http_date(x)) + + @property + def charset(self, default='UTF-8'): + """ Return the charset specified in the content-type header (default: utf8). """ + if 'charset=' in self.content_type: + return self.content_type.split('charset=')[-1].split(';')[0].strip() + return default + + def set_cookie(self, name, value, secret=None, digestmod=hashlib.sha256, **options): + """ Create a new cookie or replace an old one. If the `secret` parameter is + set, create a `Signed Cookie` (described below). + + :param name: the name of the cookie. + :param value: the value of the cookie. + :param secret: a signature key required for signed cookies. + + Additionally, this method accepts all RFC 2109 attributes that are + supported by :class:`cookie.Morsel`, including: + + :param maxage: maximum age in seconds. (default: None) + :param expires: a datetime object or UNIX timestamp. (default: None) + :param domain: the domain that is allowed to read the cookie. + (default: current domain) + :param path: limits the cookie to a given path (default: current path) + :param secure: limit the cookie to HTTPS connections (default: off). + :param httponly: prevents client-side javascript to read this cookie + (default: off, requires Python 2.6 or newer). + :param samesite: Control or disable third-party use for this cookie. + Possible values: `lax`, `strict` or `none` (default). + + If neither `expires` nor `maxage` is set (default), the cookie will + expire at the end of the browser session (as soon as the browser + window is closed). + + Signed cookies may store any pickle-able object and are + cryptographically signed to prevent manipulation. Keep in mind that + cookies are limited to 4kb in most browsers. + + Warning: Pickle is a potentially dangerous format. If an attacker + gains access to the secret key, he could forge cookies that execute + code on server side if unpickled. Using pickle is discouraged and + support for it will be removed in later versions of bottle. + + Warning: Signed cookies are not encrypted (the client can still see + the content) and not copy-protected (the client can restore an old + cookie). The main intention is to make pickling and unpickling + save, not to store secret information at client side. + """ + if not self._cookies: + self._cookies = SimpleCookie() + + # Monkey-patch Cookie lib to support 'SameSite' parameter + # https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-4.1 + if py < (3, 8, 0): + Morsel._reserved.setdefault('samesite', 'SameSite') + + if secret: + if not isinstance(value, basestring): + depr(0, 13, "Pickling of arbitrary objects into cookies is " + "deprecated.", "Only store strings in cookies. " + "JSON strings are fine, too.") + encoded = base64.b64encode(pickle.dumps([name, value], -1)) + sig = base64.b64encode(hmac.new(tob(secret), encoded, + digestmod=digestmod).digest()) + value = touni(tob('!') + sig + tob('?') + encoded) + elif not isinstance(value, basestring): + raise TypeError('Secret key required for non-string cookies.') + + # Cookie size plus options must not exceed 4kb. + if len(name) + len(value) > 3800: + raise ValueError('Content does not fit into a cookie.') + + self._cookies[name] = value + + for key, value in options.items(): + if key in ('max_age', 'maxage'): # 'maxage' variant added in 0.13 + key = 'max-age' + if isinstance(value, timedelta): + value = value.seconds + value.days * 24 * 3600 + if key == 'expires': + value = http_date(value) + if key in ('same_site', 'samesite'): # 'samesite' variant added in 0.13 + key, value = 'samesite', (value or "none").lower() + if value not in ('lax', 'strict', 'none'): + raise CookieError("Invalid value for SameSite") + if key in ('secure', 'httponly') and not value: + continue + self._cookies[name][key] = value + + def delete_cookie(self, key, **kwargs): + """ Delete a cookie. Be sure to use the same `domain` and `path` + settings as used to create the cookie. """ + kwargs['max_age'] = -1 + kwargs['expires'] = 0 + self.set_cookie(key, '', **kwargs) + + def __repr__(self): + out = '' + for name, value in self.headerlist: + out += '%s: %s\n' % (name.title(), value.strip()) + return out + + +def _local_property(): + ls = threading.local() + + def fget(_): + try: + return ls.var + except AttributeError: + raise RuntimeError("Request context not initialized.") + + def fset(_, value): + ls.var = value + + def fdel(_): + del ls.var + + return property(fget, fset, fdel, 'Thread-local property') + + +class LocalRequest(BaseRequest): + """ A thread-local subclass of :class:`BaseRequest` with a different + set of attributes for each thread. There is usually only one global + instance of this class (:data:`request`). If accessed during a + request/response cycle, this instance always refers to the *current* + request (even on a multithreaded server). """ + bind = BaseRequest.__init__ + environ = _local_property() + + +class LocalResponse(BaseResponse): + """ A thread-local subclass of :class:`BaseResponse` with a different + set of attributes for each thread. There is usually only one global + instance of this class (:data:`response`). Its attributes are used + to build the HTTP response at the end of the request/response cycle. + """ + bind = BaseResponse.__init__ + _status_line = _local_property() + _status_code = _local_property() + _cookies = _local_property() + _headers = _local_property() + body = _local_property() + + +Request = BaseRequest +Response = BaseResponse + + +class HTTPResponse(Response, BottleException): + def __init__(self, body='', status=None, headers=None, **more_headers): + super(HTTPResponse, self).__init__(body, status, headers, **more_headers) + + def apply(self, other): + other._status_code = self._status_code + other._status_line = self._status_line + other._headers = self._headers + other._cookies = self._cookies + other.body = self.body + + +class HTTPError(HTTPResponse): + default_status = 500 + + def __init__(self, + status=None, + body=None, + exception=None, + traceback=None, **more_headers): + self.exception = exception + self.traceback = traceback + super(HTTPError, self).__init__(body, status, **more_headers) + +############################################################################### +# Plugins ###################################################################### +############################################################################### + + +class PluginError(BottleException): + pass + + +class JSONPlugin(object): + name = 'json' + api = 2 + + def __init__(self, json_dumps=json_dumps): + self.json_dumps = json_dumps + + def setup(self, app): + app.config._define('json.enable', default=True, validate=bool, + help="Enable or disable automatic dict->json filter.") + app.config._define('json.ascii', default=False, validate=bool, + help="Use only 7-bit ASCII characters in output.") + app.config._define('json.indent', default=True, validate=bool, + help="Add whitespace to make json more readable.") + app.config._define('json.dump_func', default=None, + help="If defined, use this function to transform" + " dict into json. The other options no longer" + " apply.") + + def apply(self, callback, route): + dumps = self.json_dumps + if not self.json_dumps: return callback + + def wrapper(*a, **ka): + try: + rv = callback(*a, **ka) + except HTTPResponse as resp: + rv = resp + + if isinstance(rv, dict): + #Attempt to serialize, raises exception on failure + json_response = dumps(rv) + #Set content type only if serialization successful + response.content_type = 'application/json' + return json_response + elif isinstance(rv, HTTPResponse) and isinstance(rv.body, dict): + rv.body = dumps(rv.body) + rv.content_type = 'application/json' + return rv + + return wrapper + + +class TemplatePlugin(object): + """ This plugin applies the :func:`view` decorator to all routes with a + `template` config parameter. If the parameter is a tuple, the second + element must be a dict with additional options (e.g. `template_engine`) + or default variables for the template. """ + name = 'template' + api = 2 + + def setup(self, app): + app.tpl = self + + def apply(self, callback, route): + conf = route.config.get('template') + if isinstance(conf, (tuple, list)) and len(conf) == 2: + return view(conf[0], **conf[1])(callback) + elif isinstance(conf, str): + return view(conf)(callback) + else: + return callback + + +#: Not a plugin, but part of the plugin API. TODO: Find a better place. +class _ImportRedirect(object): + def __init__(self, name, impmask): + """ Create a virtual package that redirects imports (see PEP 302). """ + self.name = name + self.impmask = impmask + self.module = sys.modules.setdefault(name, imp.new_module(name)) + self.module.__dict__.update({ + '__file__': __file__, + '__path__': [], + '__all__': [], + '__loader__': self + }) + sys.meta_path.append(self) + + def find_module(self, fullname, path=None): + if '.' not in fullname: return + packname = fullname.rsplit('.', 1)[0] + if packname != self.name: return + return self + + def load_module(self, fullname): + if fullname in sys.modules: return sys.modules[fullname] + modname = fullname.rsplit('.', 1)[1] + realname = self.impmask % modname + __import__(realname) + module = sys.modules[fullname] = sys.modules[realname] + setattr(self.module, modname, module) + module.__loader__ = self + return module + +############################################################################### +# Common Utilities ############################################################# +############################################################################### + + +class MultiDict(DictMixin): + """ This dict stores multiple values per key, but behaves exactly like a + normal dict in that it returns only the newest value for any given key. + There are special methods available to access the full list of values. + """ + + def __init__(self, *a, **k): + self.dict = dict((k, [v]) for (k, v) in dict(*a, **k).items()) + + def __len__(self): + return len(self.dict) + + def __iter__(self): + return iter(self.dict) + + def __contains__(self, key): + return key in self.dict + + def __delitem__(self, key): + del self.dict[key] + + def __getitem__(self, key): + return self.dict[key][-1] + + def __setitem__(self, key, value): + self.append(key, value) + + def keys(self): + return self.dict.keys() + + if py3k: + + def values(self): + return (v[-1] for v in self.dict.values()) + + def items(self): + return ((k, v[-1]) for k, v in self.dict.items()) + + def allitems(self): + return ((k, v) for k, vl in self.dict.items() for v in vl) + + iterkeys = keys + itervalues = values + iteritems = items + iterallitems = allitems + + else: + + def values(self): + return [v[-1] for v in self.dict.values()] + + def items(self): + return [(k, v[-1]) for k, v in self.dict.items()] + + def iterkeys(self): + return self.dict.iterkeys() + + def itervalues(self): + return (v[-1] for v in self.dict.itervalues()) + + def iteritems(self): + return ((k, v[-1]) for k, v in self.dict.iteritems()) + + def iterallitems(self): + return ((k, v) for k, vl in self.dict.iteritems() for v in vl) + + def allitems(self): + return [(k, v) for k, vl in self.dict.iteritems() for v in vl] + + def get(self, key, default=None, index=-1, type=None): + """ Return the most recent value for a key. + + :param default: The default value to be returned if the key is not + present or the type conversion fails. + :param index: An index for the list of available values. + :param type: If defined, this callable is used to cast the value + into a specific type. Exception are suppressed and result in + the default value to be returned. + """ + try: + val = self.dict[key][index] + return type(val) if type else val + except Exception: + pass + return default + + def append(self, key, value): + """ Add a new value to the list of values for this key. """ + self.dict.setdefault(key, []).append(value) + + def replace(self, key, value): + """ Replace the list of values with a single value. """ + self.dict[key] = [value] + + def getall(self, key): + """ Return a (possibly empty) list of values for a key. """ + return self.dict.get(key) or [] + + #: Aliases for WTForms to mimic other multi-dict APIs (Django) + getone = get + getlist = getall + + +class FormsDict(MultiDict): + """ This :class:`MultiDict` subclass is used to store request form data. + Additionally to the normal dict-like item access methods (which return + unmodified data as native strings), this container also supports + attribute-like access to its values. Attributes are automatically de- + or recoded to match :attr:`input_encoding` (default: 'utf8'). Missing + attributes default to an empty string. """ + + #: Encoding used for attribute values. + input_encoding = 'utf8' + #: If true (default), unicode strings are first encoded with `latin1` + #: and then decoded to match :attr:`input_encoding`. + recode_unicode = True + + def _fix(self, s, encoding=None): + if isinstance(s, unicode) and self.recode_unicode: # Python 3 WSGI + return s.encode('latin1').decode(encoding or self.input_encoding) + elif isinstance(s, bytes): # Python 2 WSGI + return s.decode(encoding or self.input_encoding) + else: + return s + + def decode(self, encoding=None): + """ Returns a copy with all keys and values de- or recoded to match + :attr:`input_encoding`. Some libraries (e.g. WTForms) want a + unicode dictionary. """ + copy = FormsDict() + enc = copy.input_encoding = encoding or self.input_encoding + copy.recode_unicode = False + for key, value in self.allitems(): + copy.append(self._fix(key, enc), self._fix(value, enc)) + return copy + + def getunicode(self, name, default=None, encoding=None): + """ Return the value as a unicode string, or the default. """ + try: + return self._fix(self[name], encoding) + except (UnicodeError, KeyError): + return default + + def __getattr__(self, name, default=unicode()): + # Without this guard, pickle generates a cryptic TypeError: + if name.startswith('__') and name.endswith('__'): + return super(FormsDict, self).__getattr__(name) + return self.getunicode(name, default=default) + +class HeaderDict(MultiDict): + """ A case-insensitive version of :class:`MultiDict` that defaults to + replace the old value instead of appending it. """ + + def __init__(self, *a, **ka): + self.dict = {} + if a or ka: self.update(*a, **ka) + + def __contains__(self, key): + return _hkey(key) in self.dict + + def __delitem__(self, key): + del self.dict[_hkey(key)] + + def __getitem__(self, key): + return self.dict[_hkey(key)][-1] + + def __setitem__(self, key, value): + self.dict[_hkey(key)] = [_hval(value)] + + def append(self, key, value): + self.dict.setdefault(_hkey(key), []).append(_hval(value)) + + def replace(self, key, value): + self.dict[_hkey(key)] = [_hval(value)] + + def getall(self, key): + return self.dict.get(_hkey(key)) or [] + + def get(self, key, default=None, index=-1): + return MultiDict.get(self, _hkey(key), default, index) + + def filter(self, names): + for name in (_hkey(n) for n in names): + if name in self.dict: + del self.dict[name] + + +class WSGIHeaderDict(DictMixin): + """ This dict-like class wraps a WSGI environ dict and provides convenient + access to HTTP_* fields. Keys and values are native strings + (2.x bytes or 3.x unicode) and keys are case-insensitive. If the WSGI + environment contains non-native string values, these are de- or encoded + using a lossless 'latin1' character set. + + The API will remain stable even on changes to the relevant PEPs. + Currently PEP 333, 444 and 3333 are supported. (PEP 444 is the only one + that uses non-native strings.) + """ + #: List of keys that do not have a ``HTTP_`` prefix. + cgikeys = ('CONTENT_TYPE', 'CONTENT_LENGTH') + + def __init__(self, environ): + self.environ = environ + + def _ekey(self, key): + """ Translate header field name to CGI/WSGI environ key. """ + key = key.replace('-', '_').upper() + if key in self.cgikeys: + return key + return 'HTTP_' + key + + def raw(self, key, default=None): + """ Return the header value as is (may be bytes or unicode). """ + return self.environ.get(self._ekey(key), default) + + def __getitem__(self, key): + val = self.environ[self._ekey(key)] + if py3k: + if isinstance(val, unicode): + val = val.encode('latin1').decode('utf8') + else: + val = val.decode('utf8') + return val + + def __setitem__(self, key, value): + raise TypeError("%s is read-only." % self.__class__) + + def __delitem__(self, key): + raise TypeError("%s is read-only." % self.__class__) + + def __iter__(self): + for key in self.environ: + if key[:5] == 'HTTP_': + yield _hkey(key[5:]) + elif key in self.cgikeys: + yield _hkey(key) + + def keys(self): + return [x for x in self] + + def __len__(self): + return len(self.keys()) + + def __contains__(self, key): + return self._ekey(key) in self.environ + +_UNSET = object() + +class ConfigDict(dict): + """ A dict-like configuration storage with additional support for + namespaces, validators, meta-data, overlays and more. + + This dict-like class is heavily optimized for read access. All read-only + methods as well as item access should be as fast as the built-in dict. + """ + + __slots__ = ('_meta', '_change_listener', '_overlays', '_virtual_keys', '_source', '__weakref__') + + def __init__(self): + self._meta = {} + self._change_listener = [] + #: Weak references of overlays that need to be kept in sync. + self._overlays = [] + #: Config that is the source for this overlay. + self._source = None + #: Keys of values copied from the source (values we do not own) + self._virtual_keys = set() + + def load_module(self, path, squash=True): + """Load values from a Python module. + + Example modue ``config.py``:: + + DEBUG = True + SQLITE = { + "db": ":memory:" + } + + + >>> c = ConfigDict() + >>> c.load_module('config') + {DEBUG: True, 'SQLITE.DB': 'memory'} + >>> c.load_module("config", False) + {'DEBUG': True, 'SQLITE': {'DB': 'memory'}} + + :param squash: If true (default), dictionary values are assumed to + represent namespaces (see :meth:`load_dict`). + """ + config_obj = load(path) + obj = {key: getattr(config_obj, key) for key in dir(config_obj) + if key.isupper()} + + if squash: + self.load_dict(obj) + else: + self.update(obj) + return self + + def load_config(self, filename, **options): + """ Load values from an ``*.ini`` style config file. + + A configuration file consists of sections, each led by a + ``[section]`` header, followed by key/value entries separated by + either ``=`` or ``:``. Section names and keys are case-insensitive. + Leading and trailing whitespace is removed from keys and values. + Values can be omitted, in which case the key/value delimiter may + also be left out. Values can also span multiple lines, as long as + they are indented deeper than the first line of the value. Commands + are prefixed by ``#`` or ``;`` and may only appear on their own on + an otherwise empty line. + + Both section and key names may contain dots (``.``) as namespace + separators. The actual configuration parameter name is constructed + by joining section name and key name together and converting to + lower case. + + The special sections ``bottle`` and ``ROOT`` refer to the root + namespace and the ``DEFAULT`` section defines default values for all + other sections. + + With Python 3, extended string interpolation is enabled. + + :param filename: The path of a config file, or a list of paths. + :param options: All keyword parameters are passed to the underlying + :class:`python:configparser.ConfigParser` constructor call. + + """ + options.setdefault('allow_no_value', True) + if py3k: + options.setdefault('interpolation', + configparser.ExtendedInterpolation()) + conf = configparser.ConfigParser(**options) + conf.read(filename) + for section in conf.sections(): + for key in conf.options(section): + value = conf.get(section, key) + if section not in ['bottle', 'ROOT']: + key = section + '.' + key + self[key.lower()] = value + return self + + def load_dict(self, source, namespace=''): + """ Load values from a dictionary structure. Nesting can be used to + represent namespaces. + + >>> c = ConfigDict() + >>> c.load_dict({'some': {'namespace': {'key': 'value'} } }) + {'some.namespace.key': 'value'} + """ + for key, value in source.items(): + if isinstance(key, basestring): + nskey = (namespace + '.' + key).strip('.') + if isinstance(value, dict): + self.load_dict(value, namespace=nskey) + else: + self[nskey] = value + else: + raise TypeError('Key has type %r (not a string)' % type(key)) + return self + + def update(self, *a, **ka): + """ If the first parameter is a string, all keys are prefixed with this + namespace. Apart from that it works just as the usual dict.update(). + + >>> c = ConfigDict() + >>> c.update('some.namespace', key='value') + """ + prefix = '' + if a and isinstance(a[0], basestring): + prefix = a[0].strip('.') + '.' + a = a[1:] + for key, value in dict(*a, **ka).items(): + self[prefix + key] = value + + def setdefault(self, key, value): + if key not in self: + self[key] = value + return self[key] + + def __setitem__(self, key, value): + if not isinstance(key, basestring): + raise TypeError('Key has type %r (not a string)' % type(key)) + + self._virtual_keys.discard(key) + + value = self.meta_get(key, 'filter', lambda x: x)(value) + if key in self and self[key] is value: + return + + self._on_change(key, value) + dict.__setitem__(self, key, value) + + for overlay in self._iter_overlays(): + overlay._set_virtual(key, value) + + def __delitem__(self, key): + if key not in self: + raise KeyError(key) + if key in self._virtual_keys: + raise KeyError("Virtual keys cannot be deleted: %s" % key) + + if self._source and key in self._source: + # Not virtual, but present in source -> Restore virtual value + dict.__delitem__(self, key) + self._set_virtual(key, self._source[key]) + else: # not virtual, not present in source. This is OUR value + self._on_change(key, None) + dict.__delitem__(self, key) + for overlay in self._iter_overlays(): + overlay._delete_virtual(key) + + def _set_virtual(self, key, value): + """ Recursively set or update virtual keys. Do nothing if non-virtual + value is present. """ + if key in self and key not in self._virtual_keys: + return # Do nothing for non-virtual keys. + + self._virtual_keys.add(key) + if key in self and self[key] is not value: + self._on_change(key, value) + dict.__setitem__(self, key, value) + for overlay in self._iter_overlays(): + overlay._set_virtual(key, value) + + def _delete_virtual(self, key): + """ Recursively delete virtual entry. Do nothing if key is not virtual. + """ + if key not in self._virtual_keys: + return # Do nothing for non-virtual keys. + + if key in self: + self._on_change(key, None) + dict.__delitem__(self, key) + self._virtual_keys.discard(key) + for overlay in self._iter_overlays(): + overlay._delete_virtual(key) + + def _on_change(self, key, value): + for cb in self._change_listener: + if cb(self, key, value): + return True + + def _add_change_listener(self, func): + self._change_listener.append(func) + return func + + def meta_get(self, key, metafield, default=None): + """ Return the value of a meta field for a key. """ + return self._meta.get(key, {}).get(metafield, default) + + def meta_set(self, key, metafield, value): + """ Set the meta field for a key to a new value. """ + self._meta.setdefault(key, {})[metafield] = value + + def meta_list(self, key): + """ Return an iterable of meta field names defined for a key. """ + return self._meta.get(key, {}).keys() + + def _define(self, key, default=_UNSET, help=_UNSET, validate=_UNSET): + """ (Unstable) Shortcut for plugins to define own config parameters. """ + if default is not _UNSET: + self.setdefault(key, default) + if help is not _UNSET: + self.meta_set(key, 'help', help) + if validate is not _UNSET: + self.meta_set(key, 'validate', validate) + + def _iter_overlays(self): + for ref in self._overlays: + overlay = ref() + if overlay is not None: + yield overlay + + def _make_overlay(self): + """ (Unstable) Create a new overlay that acts like a chained map: Values + missing in the overlay are copied from the source map. Both maps + share the same meta entries. + + Entries that were copied from the source are called 'virtual'. You + can not delete virtual keys, but overwrite them, which turns them + into non-virtual entries. Setting keys on an overlay never affects + its source, but may affect any number of child overlays. + + Other than collections.ChainMap or most other implementations, this + approach does not resolve missing keys on demand, but instead + actively copies all values from the source to the overlay and keeps + track of virtual and non-virtual keys internally. This removes any + lookup-overhead. Read-access is as fast as a build-in dict for both + virtual and non-virtual keys. + + Changes are propagated recursively and depth-first. A failing + on-change handler in an overlay stops the propagation of virtual + values and may result in an partly updated tree. Take extra care + here and make sure that on-change handlers never fail. + + Used by Route.config + """ + # Cleanup dead references + self._overlays[:] = [ref for ref in self._overlays if ref() is not None] + + overlay = ConfigDict() + overlay._meta = self._meta + overlay._source = self + self._overlays.append(weakref.ref(overlay)) + for key in self: + overlay._set_virtual(key, self[key]) + return overlay + + + + +class AppStack(list): + """ A stack-like list. Calling it returns the head of the stack. """ + + def __call__(self): + """ Return the current default application. """ + return self.default + + def push(self, value=None): + """ Add a new :class:`Bottle` instance to the stack """ + if not isinstance(value, Bottle): + value = Bottle() + self.append(value) + return value + new_app = push + + @property + def default(self): + try: + return self[-1] + except IndexError: + return self.push() + + +class WSGIFileWrapper(object): + def __init__(self, fp, buffer_size=1024 * 64): + self.fp, self.buffer_size = fp, buffer_size + for attr in ('fileno', 'close', 'read', 'readlines', 'tell', 'seek'): + if hasattr(fp, attr): setattr(self, attr, getattr(fp, attr)) + + def __iter__(self): + buff, read = self.buffer_size, self.read + while True: + part = read(buff) + if not part: return + yield part + + +class _closeiter(object): + """ This only exists to be able to attach a .close method to iterators that + do not support attribute assignment (most of itertools). """ + + def __init__(self, iterator, close=None): + self.iterator = iterator + self.close_callbacks = makelist(close) + + def __iter__(self): + return iter(self.iterator) + + def close(self): + for func in self.close_callbacks: + func() + + +class ResourceManager(object): + """ This class manages a list of search paths and helps to find and open + application-bound resources (files). + + :param base: default value for :meth:`add_path` calls. + :param opener: callable used to open resources. + :param cachemode: controls which lookups are cached. One of 'all', + 'found' or 'none'. + """ + + def __init__(self, base='./', opener=open, cachemode='all'): + self.opener = opener + self.base = base + self.cachemode = cachemode + + #: A list of search paths. See :meth:`add_path` for details. + self.path = [] + #: A cache for resolved paths. ``res.cache.clear()`` clears the cache. + self.cache = {} + + def add_path(self, path, base=None, index=None, create=False): + """ Add a new path to the list of search paths. Return False if the + path does not exist. + + :param path: The new search path. Relative paths are turned into + an absolute and normalized form. If the path looks like a file + (not ending in `/`), the filename is stripped off. + :param base: Path used to absolutize relative search paths. + Defaults to :attr:`base` which defaults to ``os.getcwd()``. + :param index: Position within the list of search paths. Defaults + to last index (appends to the list). + + The `base` parameter makes it easy to reference files installed + along with a python module or package:: + + res.add_path('./resources/', __file__) + """ + base = os.path.abspath(os.path.dirname(base or self.base)) + path = os.path.abspath(os.path.join(base, os.path.dirname(path))) + path += os.sep + if path in self.path: + self.path.remove(path) + if create and not os.path.isdir(path): + os.makedirs(path) + if index is None: + self.path.append(path) + else: + self.path.insert(index, path) + self.cache.clear() + return os.path.exists(path) + + def __iter__(self): + """ Iterate over all existing files in all registered paths. """ + search = self.path[:] + while search: + path = search.pop() + if not os.path.isdir(path): continue + for name in os.listdir(path): + full = os.path.join(path, name) + if os.path.isdir(full): search.append(full) + else: yield full + + def lookup(self, name): + """ Search for a resource and return an absolute file path, or `None`. + + The :attr:`path` list is searched in order. The first match is + returend. Symlinks are followed. The result is cached to speed up + future lookups. """ + if name not in self.cache or DEBUG: + for path in self.path: + fpath = os.path.join(path, name) + if os.path.isfile(fpath): + if self.cachemode in ('all', 'found'): + self.cache[name] = fpath + return fpath + if self.cachemode == 'all': + self.cache[name] = None + return self.cache[name] + + def open(self, name, mode='r', *args, **kwargs): + """ Find a resource and return a file object, or raise IOError. """ + fname = self.lookup(name) + if not fname: raise IOError("Resource %r not found." % name) + return self.opener(fname, mode=mode, *args, **kwargs) + + +class FileUpload(object): + def __init__(self, fileobj, name, filename, headers=None): + """ Wrapper for file uploads. """ + #: Open file(-like) object (BytesIO buffer or temporary file) + self.file = fileobj + #: Name of the upload form field + self.name = name + #: Raw filename as sent by the client (may contain unsafe characters) + self.raw_filename = filename + #: A :class:`HeaderDict` with additional headers (e.g. content-type) + self.headers = HeaderDict(headers) if headers else HeaderDict() + + content_type = HeaderProperty('Content-Type') + content_length = HeaderProperty('Content-Length', reader=int, default=-1) + + def get_header(self, name, default=None): + """ Return the value of a header within the mulripart part. """ + return self.headers.get(name, default) + + @cached_property + def filename(self): + """ Name of the file on the client file system, but normalized to ensure + file system compatibility. An empty filename is returned as 'empty'. + + Only ASCII letters, digits, dashes, underscores and dots are + allowed in the final filename. Accents are removed, if possible. + Whitespace is replaced by a single dash. Leading or tailing dots + or dashes are removed. The filename is limited to 255 characters. + """ + fname = self.raw_filename + if not isinstance(fname, unicode): + fname = fname.decode('utf8', 'ignore') + fname = normalize('NFKD', fname) + fname = fname.encode('ASCII', 'ignore').decode('ASCII') + fname = os.path.basename(fname.replace('\\', os.path.sep)) + fname = re.sub(r'[^a-zA-Z0-9-_.\s]', '', fname).strip() + fname = re.sub(r'[-\s]+', '-', fname).strip('.-') + return fname[:255] or 'empty' + + def _copy_file(self, fp, chunk_size=2 ** 16): + read, write, offset = self.file.read, fp.write, self.file.tell() + while 1: + buf = read(chunk_size) + if not buf: break + write(buf) + self.file.seek(offset) + + def save(self, destination, overwrite=False, chunk_size=2 ** 16): + """ Save file to disk or copy its content to an open file(-like) object. + If *destination* is a directory, :attr:`filename` is added to the + path. Existing files are not overwritten by default (IOError). + + :param destination: File path, directory or file(-like) object. + :param overwrite: If True, replace existing files. (default: False) + :param chunk_size: Bytes to read at a time. (default: 64kb) + """ + if isinstance(destination, basestring): # Except file-likes here + if os.path.isdir(destination): + destination = os.path.join(destination, self.filename) + if not overwrite and os.path.exists(destination): + raise IOError('File exists.') + with open(destination, 'wb') as fp: + self._copy_file(fp, chunk_size) + else: + self._copy_file(destination, chunk_size) + +############################################################################### +# Application Helper ########################################################### +############################################################################### + + +def abort(code=500, text='Unknown Error.'): + """ Aborts execution and causes a HTTP error. """ + raise HTTPError(code, text) + + +def redirect(url, code=None): + """ Aborts execution and causes a 303 or 302 redirect, depending on + the HTTP protocol version. """ + if not code: + code = 303 if request.get('SERVER_PROTOCOL') == "HTTP/1.1" else 302 + res = response.copy(cls=HTTPResponse) + res.status = code + res.body = "" + res.set_header('Location', urljoin(request.url, url)) + raise res + + +def _file_iter_range(fp, offset, bytes, maxread=1024 * 1024, close=False): + """ Yield chunks from a range in a file, optionally closing it at the end. + No chunk is bigger than maxread. """ + fp.seek(offset) + while bytes > 0: + part = fp.read(min(bytes, maxread)) + if not part: + break + bytes -= len(part) + yield part + if close: + fp.close() + + +def static_file(filename, root, + mimetype=True, + download=False, + charset='UTF-8', + etag=None, + headers=None): + """ Open a file in a safe way and return an instance of :exc:`HTTPResponse` + that can be sent back to the client. + + :param filename: Name or path of the file to send, relative to ``root``. + :param root: Root path for file lookups. Should be an absolute directory + path. + :param mimetype: Provide the content-type header (default: guess from + file extension) + :param download: If True, ask the browser to open a `Save as...` dialog + instead of opening the file with the associated program. You can + specify a custom filename as a string. If not specified, the + original filename is used (default: False). + :param charset: The charset for files with a ``text/*`` mime-type. + (default: UTF-8) + :param etag: Provide a pre-computed ETag header. If set to ``False``, + ETag handling is disabled. (default: auto-generate ETag header) + :param headers: Additional headers dict to add to the response. + + While checking user input is always a good idea, this function provides + additional protection against malicious ``filename`` parameters from + breaking out of the ``root`` directory and leaking sensitive information + to an attacker. + + Read-protected files or files outside of the ``root`` directory are + answered with ``403 Access Denied``. Missing files result in a + ``404 Not Found`` response. Conditional requests (``If-Modified-Since``, + ``If-None-Match``) are answered with ``304 Not Modified`` whenever + possible. ``HEAD`` and ``Range`` requests (used by download managers to + check or continue partial downloads) are also handled automatically. + + """ + + root = os.path.join(os.path.abspath(root), '') + filename = os.path.abspath(os.path.join(root, filename.strip('/\\'))) + headers = headers or {} + + if not filename.startswith(root): + return HTTPError(403, "Access denied.") + if not os.path.exists(filename) or not os.path.isfile(filename): + return HTTPError(404, "File does not exist.") + if not os.access(filename, os.R_OK): + return HTTPError(403, "You do not have permission to access this file.") + + if mimetype is True: + if download and download is not True: + mimetype, encoding = mimetypes.guess_type(download) + else: + mimetype, encoding = mimetypes.guess_type(filename) + if encoding: + headers['Content-Encoding'] = encoding + + if mimetype: + if (mimetype[:5] == 'text/' or mimetype == 'application/javascript')\ + and charset and 'charset' not in mimetype: + mimetype += '; charset=%s' % charset + headers['Content-Type'] = mimetype + + if download: + download = os.path.basename(filename if download is True else download) + headers['Content-Disposition'] = 'attachment; filename="%s"' % download + + stats = os.stat(filename) + headers['Content-Length'] = clen = stats.st_size + headers['Last-Modified'] = email.utils.formatdate(stats.st_mtime, + usegmt=True) + headers['Date'] = email.utils.formatdate(time.time(), usegmt=True) + + getenv = request.environ.get + + if etag is None: + etag = '%d:%d:%d:%d:%s' % (stats.st_dev, stats.st_ino, stats.st_mtime, + clen, filename) + etag = hashlib.sha1(tob(etag)).hexdigest() + + if etag: + headers['ETag'] = etag + check = getenv('HTTP_IF_NONE_MATCH') + if check and check == etag: + return HTTPResponse(status=304, **headers) + + ims = getenv('HTTP_IF_MODIFIED_SINCE') + if ims: + ims = parse_date(ims.split(";")[0].strip()) + if ims is not None and ims >= int(stats.st_mtime): + return HTTPResponse(status=304, **headers) + + body = '' if request.method == 'HEAD' else open(filename, 'rb') + + headers["Accept-Ranges"] = "bytes" + range_header = getenv('HTTP_RANGE') + if range_header: + ranges = list(parse_range_header(range_header, clen)) + if not ranges: + return HTTPError(416, "Requested Range Not Satisfiable") + offset, end = ranges[0] + headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end - 1, clen) + headers["Content-Length"] = str(end - offset) + if body: body = _file_iter_range(body, offset, end - offset, close=True) + return HTTPResponse(body, status=206, **headers) + return HTTPResponse(body, **headers) + +############################################################################### +# HTTP Utilities and MISC (TODO) ############################################### +############################################################################### + + +def debug(mode=True): + """ Change the debug level. + There is only one debug level supported at the moment.""" + global DEBUG + if mode: warnings.simplefilter('default') + DEBUG = bool(mode) + + +def http_date(value): + if isinstance(value, basestring): + return value + if isinstance(value, datetime): + # aware datetime.datetime is converted to UTC time + # naive datetime.datetime is treated as UTC time + value = value.utctimetuple() + elif isinstance(value, datedate): + # datetime.date is naive, and is treated as UTC time + value = value.timetuple() + if not isinstance(value, (int, float)): + # convert struct_time in UTC to UNIX timestamp + value = calendar.timegm(value) + return email.utils.formatdate(value, usegmt=True) + + +def parse_date(ims): + """ Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. """ + try: + ts = email.utils.parsedate_tz(ims) + return calendar.timegm(ts[:8] + (0, )) - (ts[9] or 0) + except (TypeError, ValueError, IndexError, OverflowError): + return None + + +def parse_auth(header): + """ Parse rfc2617 HTTP authentication header string (basic) and return (user,pass) tuple or None""" + try: + method, data = header.split(None, 1) + if method.lower() == 'basic': + user, pwd = touni(base64.b64decode(tob(data))).split(':', 1) + return user, pwd + except (KeyError, ValueError): + return None + + +def parse_range_header(header, maxlen=0): + """ Yield (start, end) ranges parsed from a HTTP Range header. Skip + unsatisfiable ranges. The end index is non-inclusive.""" + if not header or header[:6] != 'bytes=': return + ranges = [r.split('-', 1) for r in header[6:].split(',') if '-' in r] + for start, end in ranges: + try: + if not start: # bytes=-100 -> last 100 bytes + start, end = max(0, maxlen - int(end)), maxlen + elif not end: # bytes=100- -> all but the first 99 bytes + start, end = int(start), maxlen + else: # bytes=100-200 -> bytes 100-200 (inclusive) + start, end = int(start), min(int(end) + 1, maxlen) + if 0 <= start < end <= maxlen: + yield start, end + except ValueError: + pass + + +#: Header tokenizer used by _parse_http_header() +_hsplit = re.compile('(?:(?:"((?:[^"\\\\]|\\\\.)*)")|([^;,=]+))([;,=]?)').findall + +def _parse_http_header(h): + """ Parses a typical multi-valued and parametrised HTTP header (e.g. Accept headers) and returns a list of values + and parameters. For non-standard or broken input, this implementation may return partial results. + :param h: A header string (e.g. ``text/html,text/plain;q=0.9,*/*;q=0.8``) + :return: List of (value, params) tuples. The second element is a (possibly empty) dict. + """ + values = [] + if '"' not in h: # INFO: Fast path without regexp (~2x faster) + for value in h.split(','): + parts = value.split(';') + values.append((parts[0].strip(), {})) + for attr in parts[1:]: + name, value = attr.split('=', 1) + values[-1][1][name.strip()] = value.strip() + else: + lop, key, attrs = ',', None, {} + for quoted, plain, tok in _hsplit(h): + value = plain.strip() if plain else quoted.replace('\\"', '"') + if lop == ',': + attrs = {} + values.append((value, attrs)) + elif lop == ';': + if tok == '=': + key = value + else: + attrs[value] = '' + elif lop == '=' and key: + attrs[key] = value + key = None + lop = tok + return values + + +def _parse_qsl(qs): + r = [] + for pair in qs.replace(';', '&').split('&'): + if not pair: continue + nv = pair.split('=', 1) + if len(nv) != 2: nv.append('') + key = urlunquote(nv[0].replace('+', ' ')) + value = urlunquote(nv[1].replace('+', ' ')) + r.append((key, value)) + return r + + +def _lscmp(a, b): + """ Compares two strings in a cryptographically safe way: + Runtime is not affected by length of common prefix. """ + return not sum(0 if x == y else 1 + for x, y in zip(a, b)) and len(a) == len(b) + + +def cookie_encode(data, key, digestmod=None): + """ Encode and sign a pickle-able object. Return a (byte) string """ + depr(0, 13, "cookie_encode() will be removed soon.", + "Do not use this API directly.") + digestmod = digestmod or hashlib.sha256 + msg = base64.b64encode(pickle.dumps(data, -1)) + sig = base64.b64encode(hmac.new(tob(key), msg, digestmod=digestmod).digest()) + return tob('!') + sig + tob('?') + msg + + +def cookie_decode(data, key, digestmod=None): + """ Verify and decode an encoded string. Return an object or None.""" + depr(0, 13, "cookie_decode() will be removed soon.", + "Do not use this API directly.") + data = tob(data) + if cookie_is_encoded(data): + sig, msg = data.split(tob('?'), 1) + digestmod = digestmod or hashlib.sha256 + hashed = hmac.new(tob(key), msg, digestmod=digestmod).digest() + if _lscmp(sig[1:], base64.b64encode(hashed)): + return pickle.loads(base64.b64decode(msg)) + return None + + +def cookie_is_encoded(data): + """ Return True if the argument looks like a encoded cookie.""" + depr(0, 13, "cookie_is_encoded() will be removed soon.", + "Do not use this API directly.") + return bool(data.startswith(tob('!')) and tob('?') in data) + + +def html_escape(string): + """ Escape HTML special characters ``&<>`` and quotes ``'"``. """ + return string.replace('&', '&').replace('<', '<').replace('>', '>')\ + .replace('"', '"').replace("'", ''') + + +def html_quote(string): + """ Escape and quote a string to be used as an HTTP attribute.""" + return '"%s"' % html_escape(string).replace('\n', ' ')\ + .replace('\r', ' ').replace('\t', ' ') + + +def yieldroutes(func): + """ Return a generator for routes that match the signature (name, args) + of the func parameter. This may yield more than one route if the function + takes optional keyword arguments. The output is best described by example:: + + a() -> '/a' + b(x, y) -> '/b//' + c(x, y=5) -> '/c/' and '/c//' + d(x=5, y=6) -> '/d' and '/d/' and '/d//' + """ + path = '/' + func.__name__.replace('__', '/').lstrip('/') + spec = getargspec(func) + argc = len(spec[0]) - len(spec[3] or []) + path += ('/<%s>' * argc) % tuple(spec[0][:argc]) + yield path + for arg in spec[0][argc:]: + path += '/<%s>' % arg + yield path + + +def path_shift(script_name, path_info, shift=1): + """ Shift path fragments from PATH_INFO to SCRIPT_NAME and vice versa. + + :return: The modified paths. + :param script_name: The SCRIPT_NAME path. + :param script_name: The PATH_INFO path. + :param shift: The number of path fragments to shift. May be negative to + change the shift direction. (default: 1) + """ + if shift == 0: return script_name, path_info + pathlist = path_info.strip('/').split('/') + scriptlist = script_name.strip('/').split('/') + if pathlist and pathlist[0] == '': pathlist = [] + if scriptlist and scriptlist[0] == '': scriptlist = [] + if 0 < shift <= len(pathlist): + moved = pathlist[:shift] + scriptlist = scriptlist + moved + pathlist = pathlist[shift:] + elif 0 > shift >= -len(scriptlist): + moved = scriptlist[shift:] + pathlist = moved + pathlist + scriptlist = scriptlist[:shift] + else: + empty = 'SCRIPT_NAME' if shift < 0 else 'PATH_INFO' + raise AssertionError("Cannot shift. Nothing left from %s" % empty) + new_script_name = '/' + '/'.join(scriptlist) + new_path_info = '/' + '/'.join(pathlist) + if path_info.endswith('/') and pathlist: new_path_info += '/' + return new_script_name, new_path_info + + +def auth_basic(check, realm="private", text="Access denied"): + """ Callback decorator to require HTTP auth (basic). + TODO: Add route(check_auth=...) parameter. """ + + def decorator(func): + + @functools.wraps(func) + def wrapper(*a, **ka): + user, password = request.auth or (None, None) + if user is None or not check(user, password): + err = HTTPError(401, text) + err.add_header('WWW-Authenticate', 'Basic realm="%s"' % realm) + return err + return func(*a, **ka) + + return wrapper + + return decorator + +# Shortcuts for common Bottle methods. +# They all refer to the current default application. + + +def make_default_app_wrapper(name): + """ Return a callable that relays calls to the current default app. """ + + @functools.wraps(getattr(Bottle, name)) + def wrapper(*a, **ka): + return getattr(app(), name)(*a, **ka) + + return wrapper + + +route = make_default_app_wrapper('route') +get = make_default_app_wrapper('get') +post = make_default_app_wrapper('post') +put = make_default_app_wrapper('put') +delete = make_default_app_wrapper('delete') +patch = make_default_app_wrapper('patch') +error = make_default_app_wrapper('error') +mount = make_default_app_wrapper('mount') +hook = make_default_app_wrapper('hook') +install = make_default_app_wrapper('install') +uninstall = make_default_app_wrapper('uninstall') +url = make_default_app_wrapper('get_url') + +############################################################################### +# Server Adapter ############################################################### +############################################################################### + +# Before you edit or add a server adapter, please read: +# - https://github.com/bottlepy/bottle/pull/647#issuecomment-60152870 +# - https://github.com/bottlepy/bottle/pull/865#issuecomment-242795341 + +class ServerAdapter(object): + quiet = False + + def __init__(self, host='127.0.0.1', port=8080, **options): + self.options = options + self.host = host + self.port = int(port) + + def run(self, handler): # pragma: no cover + pass + + def __repr__(self): + args = ', '.join(['%s=%s' % (k, repr(v)) + for k, v in self.options.items()]) + return "%s(%s)" % (self.__class__.__name__, args) + + +class CGIServer(ServerAdapter): + quiet = True + + def run(self, handler): # pragma: no cover + from wsgiref.handlers import CGIHandler + + def fixed_environ(environ, start_response): + environ.setdefault('PATH_INFO', '') + return handler(environ, start_response) + + CGIHandler().run(fixed_environ) + + +class FlupFCGIServer(ServerAdapter): + def run(self, handler): # pragma: no cover + import flup.server.fcgi + self.options.setdefault('bindAddress', (self.host, self.port)) + flup.server.fcgi.WSGIServer(handler, **self.options).run() + + +class WSGIRefServer(ServerAdapter): + def run(self, app): # pragma: no cover + from wsgiref.simple_server import make_server + from wsgiref.simple_server import WSGIRequestHandler, WSGIServer + import socket + + class FixedHandler(WSGIRequestHandler): + def address_string(self): # Prevent reverse DNS lookups please. + return self.client_address[0] + + def log_request(*args, **kw): + if not self.quiet: + return WSGIRequestHandler.log_request(*args, **kw) + + handler_cls = self.options.get('handler_class', FixedHandler) + server_cls = self.options.get('server_class', WSGIServer) + + if ':' in self.host: # Fix wsgiref for IPv6 addresses. + if getattr(server_cls, 'address_family') == socket.AF_INET: + + class server_cls(server_cls): + address_family = socket.AF_INET6 + + self.srv = make_server(self.host, self.port, app, server_cls, + handler_cls) + self.port = self.srv.server_port # update port actual port (0 means random) + try: + self.srv.serve_forever() + except KeyboardInterrupt: + self.srv.server_close() # Prevent ResourceWarning: unclosed socket + raise + + +class CherryPyServer(ServerAdapter): + def run(self, handler): # pragma: no cover + depr(0, 13, "The wsgi server part of cherrypy was split into a new " + "project called 'cheroot'.", "Use the 'cheroot' server " + "adapter instead of cherrypy.") + from cherrypy import wsgiserver # This will fail for CherryPy >= 9 + + self.options['bind_addr'] = (self.host, self.port) + self.options['wsgi_app'] = handler + + certfile = self.options.get('certfile') + if certfile: + del self.options['certfile'] + keyfile = self.options.get('keyfile') + if keyfile: + del self.options['keyfile'] + + server = wsgiserver.CherryPyWSGIServer(**self.options) + if certfile: + server.ssl_certificate = certfile + if keyfile: + server.ssl_private_key = keyfile + + try: + server.start() + finally: + server.stop() + + +class CherootServer(ServerAdapter): + def run(self, handler): # pragma: no cover + from cheroot import wsgi + from cheroot.ssl import builtin + self.options['bind_addr'] = (self.host, self.port) + self.options['wsgi_app'] = handler + certfile = self.options.pop('certfile', None) + keyfile = self.options.pop('keyfile', None) + chainfile = self.options.pop('chainfile', None) + server = wsgi.Server(**self.options) + if certfile and keyfile: + server.ssl_adapter = builtin.BuiltinSSLAdapter( + certfile, keyfile, chainfile) + try: + server.start() + finally: + server.stop() + + +class WaitressServer(ServerAdapter): + def run(self, handler): + from waitress import serve + serve(handler, host=self.host, port=self.port, _quiet=self.quiet, **self.options) + + +class PasteServer(ServerAdapter): + def run(self, handler): # pragma: no cover + from paste import httpserver + from paste.translogger import TransLogger + handler = TransLogger(handler, setup_console_handler=(not self.quiet)) + httpserver.serve(handler, + host=self.host, + port=str(self.port), **self.options) + + +class MeinheldServer(ServerAdapter): + def run(self, handler): + from meinheld import server + server.listen((self.host, self.port)) + server.run(handler) + + +class FapwsServer(ServerAdapter): + """ Extremely fast webserver using libev. See http://www.fapws.org/ """ + + def run(self, handler): # pragma: no cover + import fapws._evwsgi as evwsgi + from fapws import base, config + port = self.port + if float(config.SERVER_IDENT[-2:]) > 0.4: + # fapws3 silently changed its API in 0.5 + port = str(port) + evwsgi.start(self.host, port) + # fapws3 never releases the GIL. Complain upstream. I tried. No luck. + if 'BOTTLE_CHILD' in os.environ and not self.quiet: + _stderr("WARNING: Auto-reloading does not work with Fapws3.\n") + _stderr(" (Fapws3 breaks python thread support)\n") + evwsgi.set_base_module(base) + + def app(environ, start_response): + environ['wsgi.multiprocess'] = False + return handler(environ, start_response) + + evwsgi.wsgi_cb(('', app)) + evwsgi.run() + + +class TornadoServer(ServerAdapter): + """ The super hyped asynchronous server by facebook. Untested. """ + + def run(self, handler): # pragma: no cover + import tornado.wsgi, tornado.httpserver, tornado.ioloop + container = tornado.wsgi.WSGIContainer(handler) + server = tornado.httpserver.HTTPServer(container) + server.listen(port=self.port, address=self.host) + tornado.ioloop.IOLoop.instance().start() + + +class AppEngineServer(ServerAdapter): + """ Adapter for Google App Engine. """ + quiet = True + + def run(self, handler): + depr(0, 13, "AppEngineServer no longer required", + "Configure your application directly in your app.yaml") + from google.appengine.ext.webapp import util + # A main() function in the handler script enables 'App Caching'. + # Lets makes sure it is there. This _really_ improves performance. + module = sys.modules.get('__main__') + if module and not hasattr(module, 'main'): + module.main = lambda: util.run_wsgi_app(handler) + util.run_wsgi_app(handler) + + +class TwistedServer(ServerAdapter): + """ Untested. """ + + def run(self, handler): + from twisted.web import server, wsgi + from twisted.python.threadpool import ThreadPool + from twisted.internet import reactor + thread_pool = ThreadPool() + thread_pool.start() + reactor.addSystemEventTrigger('after', 'shutdown', thread_pool.stop) + factory = server.Site(wsgi.WSGIResource(reactor, thread_pool, handler)) + reactor.listenTCP(self.port, factory, interface=self.host) + if not reactor.running: + reactor.run() + + +class DieselServer(ServerAdapter): + """ Untested. """ + + def run(self, handler): + from diesel.protocols.wsgi import WSGIApplication + app = WSGIApplication(handler, port=self.port) + app.run() + + +class GeventServer(ServerAdapter): + """ Untested. Options: + + * See gevent.wsgi.WSGIServer() documentation for more options. + """ + + def run(self, handler): + from gevent import pywsgi, local + if not isinstance(threading.local(), local.local): + msg = "Bottle requires gevent.monkey.patch_all() (before import)" + raise RuntimeError(msg) + if self.quiet: + self.options['log'] = None + address = (self.host, self.port) + server = pywsgi.WSGIServer(address, handler, **self.options) + if 'BOTTLE_CHILD' in os.environ: + import signal + signal.signal(signal.SIGINT, lambda s, f: server.stop()) + server.serve_forever() + + +class GunicornServer(ServerAdapter): + """ Untested. See http://gunicorn.org/configure.html for options. """ + + def run(self, handler): + from gunicorn.app.base import BaseApplication + + if self.host.startswith("unix:"): + config = {'bind': self.host} + else: + config = {'bind': "%s:%d" % (self.host, self.port)} + + config.update(self.options) + + class GunicornApplication(BaseApplication): + def load_config(self): + for key, value in config.items(): + self.cfg.set(key, value) + + def load(self): + return handler + + GunicornApplication().run() + + +class EventletServer(ServerAdapter): + """ Untested. Options: + + * `backlog` adjust the eventlet backlog parameter which is the maximum + number of queued connections. Should be at least 1; the maximum + value is system-dependent. + * `family`: (default is 2) socket family, optional. See socket + documentation for available families. + """ + + def run(self, handler): + from eventlet import wsgi, listen, patcher + if not patcher.is_monkey_patched(os): + msg = "Bottle requires eventlet.monkey_patch() (before import)" + raise RuntimeError(msg) + socket_args = {} + for arg in ('backlog', 'family'): + try: + socket_args[arg] = self.options.pop(arg) + except KeyError: + pass + address = (self.host, self.port) + try: + wsgi.server(listen(address, **socket_args), handler, + log_output=(not self.quiet)) + except TypeError: + # Fallback, if we have old version of eventlet + wsgi.server(listen(address), handler) + + +class BjoernServer(ServerAdapter): + """ Fast server written in C: https://github.com/jonashaag/bjoern """ + + def run(self, handler): + from bjoern import run + run(handler, self.host, self.port) + +class AsyncioServerAdapter(ServerAdapter): + """ Extend ServerAdapter for adding custom event loop """ + def get_event_loop(self): + pass + +class AiohttpServer(AsyncioServerAdapter): + """ Asynchronous HTTP client/server framework for asyncio + https://pypi.python.org/pypi/aiohttp/ + https://pypi.org/project/aiohttp-wsgi/ + """ + + def get_event_loop(self): + import asyncio + return asyncio.new_event_loop() + + def run(self, handler): + import asyncio + from aiohttp_wsgi.wsgi import serve + self.loop = self.get_event_loop() + asyncio.set_event_loop(self.loop) + + if 'BOTTLE_CHILD' in os.environ: + import signal + signal.signal(signal.SIGINT, lambda s, f: self.loop.stop()) + + serve(handler, host=self.host, port=self.port) + + +class AiohttpUVLoopServer(AiohttpServer): + """uvloop + https://github.com/MagicStack/uvloop + """ + def get_event_loop(self): + import uvloop + return uvloop.new_event_loop() + +class AutoServer(ServerAdapter): + """ Untested. """ + adapters = [WaitressServer, PasteServer, TwistedServer, CherryPyServer, + CherootServer, WSGIRefServer] + + def run(self, handler): + for sa in self.adapters: + try: + return sa(self.host, self.port, **self.options).run(handler) + except ImportError: + pass + + +server_names = { + 'cgi': CGIServer, + 'flup': FlupFCGIServer, + 'wsgiref': WSGIRefServer, + 'waitress': WaitressServer, + 'cherrypy': CherryPyServer, + 'cheroot': CherootServer, + 'paste': PasteServer, + 'fapws3': FapwsServer, + 'tornado': TornadoServer, + 'gae': AppEngineServer, + 'twisted': TwistedServer, + 'diesel': DieselServer, + 'meinheld': MeinheldServer, + 'gunicorn': GunicornServer, + 'eventlet': EventletServer, + 'gevent': GeventServer, + 'bjoern': BjoernServer, + 'aiohttp': AiohttpServer, + 'uvloop': AiohttpUVLoopServer, + 'auto': AutoServer, +} + +############################################################################### +# Application Control ########################################################## +############################################################################### + + +def load(target, **namespace): + """ Import a module or fetch an object from a module. + + * ``package.module`` returns `module` as a module object. + * ``pack.mod:name`` returns the module variable `name` from `pack.mod`. + * ``pack.mod:func()`` calls `pack.mod.func()` and returns the result. + + The last form accepts not only function calls, but any type of + expression. Keyword arguments passed to this function are available as + local variables. Example: ``import_string('re:compile(x)', x='[a-z]')`` + """ + module, target = target.split(":", 1) if ':' in target else (target, None) + if module not in sys.modules: __import__(module) + if not target: return sys.modules[module] + if target.isalnum(): return getattr(sys.modules[module], target) + package_name = module.split('.')[0] + namespace[package_name] = sys.modules[package_name] + return eval('%s.%s' % (module, target), namespace) + + +def load_app(target): + """ Load a bottle application from a module and make sure that the import + does not affect the current default application, but returns a separate + application object. See :func:`load` for the target parameter. """ + global NORUN + NORUN, nr_old = True, NORUN + tmp = default_app.push() # Create a new "default application" + try: + rv = load(target) # Import the target module + return rv if callable(rv) else tmp + finally: + default_app.remove(tmp) # Remove the temporary added default application + NORUN = nr_old + + +_debug = debug + + +def run(app=None, + server='wsgiref', + host='127.0.0.1', + port=8080, + interval=1, + reloader=False, + quiet=False, + plugins=None, + debug=None, + config=None, **kargs): + """ Start a server instance. This method blocks until the server terminates. + + :param app: WSGI application or target string supported by + :func:`load_app`. (default: :func:`default_app`) + :param server: Server adapter to use. See :data:`server_names` keys + for valid names or pass a :class:`ServerAdapter` subclass. + (default: `wsgiref`) + :param host: Server address to bind to. Pass ``0.0.0.0`` to listens on + all interfaces including the external one. (default: 127.0.0.1) + :param port: Server port to bind to. Values below 1024 require root + privileges. (default: 8080) + :param reloader: Start auto-reloading server? (default: False) + :param interval: Auto-reloader interval in seconds (default: 1) + :param quiet: Suppress output to stdout and stderr? (default: False) + :param options: Options passed to the server adapter. + """ + if NORUN: return + if reloader and not os.environ.get('BOTTLE_CHILD'): + import subprocess + lockfile = None + try: + fd, lockfile = tempfile.mkstemp(prefix='bottle.', suffix='.lock') + os.close(fd) # We only need this file to exist. We never write to it + while os.path.exists(lockfile): + args = [sys.executable] + sys.argv + environ = os.environ.copy() + environ['BOTTLE_CHILD'] = 'true' + environ['BOTTLE_LOCKFILE'] = lockfile + p = subprocess.Popen(args, env=environ) + while p.poll() is None: # Busy wait... + os.utime(lockfile, None) # I am alive! + time.sleep(interval) + if p.poll() != 3: + if os.path.exists(lockfile): os.unlink(lockfile) + sys.exit(p.poll()) + except KeyboardInterrupt: + pass + finally: + if os.path.exists(lockfile): + os.unlink(lockfile) + return + + try: + if debug is not None: _debug(debug) + app = app or default_app() + if isinstance(app, basestring): + app = load_app(app) + if not callable(app): + raise ValueError("Application is not callable: %r" % app) + + for plugin in plugins or []: + if isinstance(plugin, basestring): + plugin = load(plugin) + app.install(plugin) + + if config: + app.config.update(config) + + if server in server_names: + server = server_names.get(server) + if isinstance(server, basestring): + server = load(server) + if isinstance(server, type): + server = server(host=host, port=port, **kargs) + if not isinstance(server, ServerAdapter): + raise ValueError("Unknown or unsupported server: %r" % server) + + server.quiet = server.quiet or quiet + if not server.quiet: + _stderr("Bottle v%s server starting up (using %s)...\n" % + (__version__, repr(server))) + if server.host.startswith("unix:"): + _stderr("Listening on %s\n" % server.host) + else: + _stderr("Listening on http://%s:%d/\n" % + (server.host, server.port)) + _stderr("Hit Ctrl-C to quit.\n\n") + + if reloader: + lockfile = os.environ.get('BOTTLE_LOCKFILE') + bgcheck = FileCheckerThread(lockfile, interval) + with bgcheck: + server.run(app) + if bgcheck.status == 'reload': + sys.exit(3) + else: + server.run(app) + except KeyboardInterrupt: + pass + except (SystemExit, MemoryError): + raise + except: + if not reloader: raise + if not getattr(server, 'quiet', quiet): + print_exc() + time.sleep(interval) + sys.exit(3) + + +class FileCheckerThread(threading.Thread): + """ Interrupt main-thread as soon as a changed module file is detected, + the lockfile gets deleted or gets too old. """ + + def __init__(self, lockfile, interval): + threading.Thread.__init__(self) + self.daemon = True + self.lockfile, self.interval = lockfile, interval + #: Is one of 'reload', 'error' or 'exit' + self.status = None + + def run(self): + exists = os.path.exists + mtime = lambda p: os.stat(p).st_mtime + files = dict() + + for module in list(sys.modules.values()): + path = getattr(module, '__file__', '') or '' + if path[-4:] in ('.pyo', '.pyc'): path = path[:-1] + if path and exists(path): files[path] = mtime(path) + + while not self.status: + if not exists(self.lockfile)\ + or mtime(self.lockfile) < time.time() - self.interval - 5: + self.status = 'error' + thread.interrupt_main() + for path, lmtime in list(files.items()): + if not exists(path) or mtime(path) > lmtime: + self.status = 'reload' + thread.interrupt_main() + break + time.sleep(self.interval) + + def __enter__(self): + self.start() + + def __exit__(self, exc_type, *_): + if not self.status: self.status = 'exit' # silent exit + self.join() + return exc_type is not None and issubclass(exc_type, KeyboardInterrupt) + +############################################################################### +# Template Adapters ############################################################ +############################################################################### + + +class TemplateError(BottleException): + pass + + +class BaseTemplate(object): + """ Base class and minimal API for template adapters """ + extensions = ['tpl', 'html', 'thtml', 'stpl'] + settings = {} #used in prepare() + defaults = {} #used in render() + + def __init__(self, + source=None, + name=None, + lookup=None, + encoding='utf8', **settings): + """ Create a new template. + If the source parameter (str or buffer) is missing, the name argument + is used to guess a template filename. Subclasses can assume that + self.source and/or self.filename are set. Both are strings. + The lookup, encoding and settings parameters are stored as instance + variables. + The lookup parameter stores a list containing directory paths. + The encoding parameter should be used to decode byte strings or files. + The settings parameter contains a dict for engine-specific settings. + """ + self.name = name + self.source = source.read() if hasattr(source, 'read') else source + self.filename = source.filename if hasattr(source, 'filename') else None + self.lookup = [os.path.abspath(x) for x in lookup] if lookup else [] + self.encoding = encoding + self.settings = self.settings.copy() # Copy from class variable + self.settings.update(settings) # Apply + if not self.source and self.name: + self.filename = self.search(self.name, self.lookup) + if not self.filename: + raise TemplateError('Template %s not found.' % repr(name)) + if not self.source and not self.filename: + raise TemplateError('No template specified.') + self.prepare(**self.settings) + + @classmethod + def search(cls, name, lookup=None): + """ Search name in all directories specified in lookup. + First without, then with common extensions. Return first hit. """ + if not lookup: + raise depr(0, 12, "Empty template lookup path.", "Configure a template lookup path.") + + if os.path.isabs(name): + raise depr(0, 12, "Use of absolute path for template name.", + "Refer to templates with names or paths relative to the lookup path.") + + for spath in lookup: + spath = os.path.abspath(spath) + os.sep + fname = os.path.abspath(os.path.join(spath, name)) + if not fname.startswith(spath): continue + if os.path.isfile(fname): return fname + for ext in cls.extensions: + if os.path.isfile('%s.%s' % (fname, ext)): + return '%s.%s' % (fname, ext) + + @classmethod + def global_config(cls, key, *args): + """ This reads or sets the global settings stored in class.settings. """ + if args: + cls.settings = cls.settings.copy() # Make settings local to class + cls.settings[key] = args[0] + else: + return cls.settings[key] + + def prepare(self, **options): + """ Run preparations (parsing, caching, ...). + It should be possible to call this again to refresh a template or to + update settings. + """ + raise NotImplementedError + + def render(self, *args, **kwargs): + """ Render the template with the specified local variables and return + a single byte or unicode string. If it is a byte string, the encoding + must match self.encoding. This method must be thread-safe! + Local variables may be provided in dictionaries (args) + or directly, as keywords (kwargs). + """ + raise NotImplementedError + + +class MakoTemplate(BaseTemplate): + def prepare(self, **options): + from mako.template import Template + from mako.lookup import TemplateLookup + options.update({'input_encoding': self.encoding}) + options.setdefault('format_exceptions', bool(DEBUG)) + lookup = TemplateLookup(directories=self.lookup, **options) + if self.source: + self.tpl = Template(self.source, lookup=lookup, **options) + else: + self.tpl = Template(uri=self.name, + filename=self.filename, + lookup=lookup, **options) + + def render(self, *args, **kwargs): + for dictarg in args: + kwargs.update(dictarg) + _defaults = self.defaults.copy() + _defaults.update(kwargs) + return self.tpl.render(**_defaults) + + +class CheetahTemplate(BaseTemplate): + def prepare(self, **options): + from Cheetah.Template import Template + self.context = threading.local() + self.context.vars = {} + options['searchList'] = [self.context.vars] + if self.source: + self.tpl = Template(source=self.source, **options) + else: + self.tpl = Template(file=self.filename, **options) + + def render(self, *args, **kwargs): + for dictarg in args: + kwargs.update(dictarg) + self.context.vars.update(self.defaults) + self.context.vars.update(kwargs) + out = str(self.tpl) + self.context.vars.clear() + return out + + +class Jinja2Template(BaseTemplate): + def prepare(self, filters=None, tests=None, globals={}, **kwargs): + from jinja2 import Environment, FunctionLoader + self.env = Environment(loader=FunctionLoader(self.loader), **kwargs) + if filters: self.env.filters.update(filters) + if tests: self.env.tests.update(tests) + if globals: self.env.globals.update(globals) + if self.source: + self.tpl = self.env.from_string(self.source) + else: + self.tpl = self.env.get_template(self.name) + + def render(self, *args, **kwargs): + for dictarg in args: + kwargs.update(dictarg) + _defaults = self.defaults.copy() + _defaults.update(kwargs) + return self.tpl.render(**_defaults) + + def loader(self, name): + if name == self.filename: + fname = name + else: + fname = self.search(name, self.lookup) + if not fname: return + with open(fname, "rb") as f: + return (f.read().decode(self.encoding), fname, lambda: False) + + +class SimpleTemplate(BaseTemplate): + def prepare(self, + escape_func=html_escape, + noescape=False, + syntax=None, **ka): + self.cache = {} + enc = self.encoding + self._str = lambda x: touni(x, enc) + self._escape = lambda x: escape_func(touni(x, enc)) + self.syntax = syntax + if noescape: + self._str, self._escape = self._escape, self._str + + @cached_property + def co(self): + return compile(self.code, self.filename or '', 'exec') + + @cached_property + def code(self): + source = self.source + if not source: + with open(self.filename, 'rb') as f: + source = f.read() + try: + source, encoding = touni(source), 'utf8' + except UnicodeError: + raise depr(0, 11, 'Unsupported template encodings.', 'Use utf-8 for templates.') + parser = StplParser(source, encoding=encoding, syntax=self.syntax) + code = parser.translate() + self.encoding = parser.encoding + return code + + def _rebase(self, _env, _name=None, **kwargs): + _env['_rebase'] = (_name, kwargs) + + def _include(self, _env, _name=None, **kwargs): + env = _env.copy() + env.update(kwargs) + if _name not in self.cache: + self.cache[_name] = self.__class__(name=_name, lookup=self.lookup, syntax=self.syntax) + return self.cache[_name].execute(env['_stdout'], env) + + def execute(self, _stdout, kwargs): + env = self.defaults.copy() + env.update(kwargs) + env.update({ + '_stdout': _stdout, + '_printlist': _stdout.extend, + 'include': functools.partial(self._include, env), + 'rebase': functools.partial(self._rebase, env), + '_rebase': None, + '_str': self._str, + '_escape': self._escape, + 'get': env.get, + 'setdefault': env.setdefault, + 'defined': env.__contains__ + }) + exec(self.co, env) + if env.get('_rebase'): + subtpl, rargs = env.pop('_rebase') + rargs['base'] = ''.join(_stdout) #copy stdout + del _stdout[:] # clear stdout + return self._include(env, subtpl, **rargs) + return env + + def render(self, *args, **kwargs): + """ Render the template using keyword arguments as local variables. """ + env = {} + stdout = [] + for dictarg in args: + env.update(dictarg) + env.update(kwargs) + self.execute(stdout, env) + return ''.join(stdout) + + +class StplSyntaxError(TemplateError): + pass + + +class StplParser(object): + """ Parser for stpl templates. """ + _re_cache = {} #: Cache for compiled re patterns + + # This huge pile of voodoo magic splits python code into 8 different tokens. + # We use the verbose (?x) regex mode to make this more manageable + + _re_tok = _re_inl = r'''( + [urbURB]* + (?: ''(?!') + |""(?!") + |'{6} + |"{6} + |'(?:[^\\']|\\.)+?' + |"(?:[^\\"]|\\.)+?" + |'{3}(?:[^\\]|\\.|\n)+?'{3} + |"{3}(?:[^\\]|\\.|\n)+?"{3} + ) + )''' + + _re_inl = _re_tok.replace(r'|\n', '') # We re-use this string pattern later + + _re_tok += r''' + # 2: Comments (until end of line, but not the newline itself) + |(\#.*) + + # 3: Open and close (4) grouping tokens + |([\[\{\(]) + |([\]\}\)]) + + # 5,6: Keywords that start or continue a python block (only start of line) + |^([\ \t]*(?:if|for|while|with|try|def|class)\b) + |^([\ \t]*(?:elif|else|except|finally)\b) + + # 7: Our special 'end' keyword (but only if it stands alone) + |((?:^|;)[\ \t]*end[\ \t]*(?=(?:%(block_close)s[\ \t]*)?\r?$|;|\#)) + + # 8: A customizable end-of-code-block template token (only end of line) + |(%(block_close)s[\ \t]*(?=\r?$)) + + # 9: And finally, a single newline. The 10th token is 'everything else' + |(\r?\n) + ''' + + # Match the start tokens of code areas in a template + _re_split = r'''(?m)^[ \t]*(\\?)((%(line_start)s)|(%(block_start)s))''' + # Match inline statements (may contain python strings) + _re_inl = r'''%%(inline_start)s((?:%s|[^'"\n])*?)%%(inline_end)s''' % _re_inl + + # add the flag in front of the regexp to avoid Deprecation warning (see Issue #949) + # verbose and dot-matches-newline mode + _re_tok = '(?mx)' + _re_tok + _re_inl = '(?mx)' + _re_inl + + + default_syntax = '<% %> % {{ }}' + + def __init__(self, source, syntax=None, encoding='utf8'): + self.source, self.encoding = touni(source, encoding), encoding + self.set_syntax(syntax or self.default_syntax) + self.code_buffer, self.text_buffer = [], [] + self.lineno, self.offset = 1, 0 + self.indent, self.indent_mod = 0, 0 + self.paren_depth = 0 + + def get_syntax(self): + """ Tokens as a space separated string (default: <% %> % {{ }}) """ + return self._syntax + + def set_syntax(self, syntax): + self._syntax = syntax + self._tokens = syntax.split() + if syntax not in self._re_cache: + names = 'block_start block_close line_start inline_start inline_end' + etokens = map(re.escape, self._tokens) + pattern_vars = dict(zip(names.split(), etokens)) + patterns = (self._re_split, self._re_tok, self._re_inl) + patterns = [re.compile(p % pattern_vars) for p in patterns] + self._re_cache[syntax] = patterns + self.re_split, self.re_tok, self.re_inl = self._re_cache[syntax] + + syntax = property(get_syntax, set_syntax) + + def translate(self): + if self.offset: raise RuntimeError('Parser is a one time instance.') + while True: + m = self.re_split.search(self.source, pos=self.offset) + if m: + text = self.source[self.offset:m.start()] + self.text_buffer.append(text) + self.offset = m.end() + if m.group(1): # Escape syntax + line, sep, _ = self.source[self.offset:].partition('\n') + self.text_buffer.append(self.source[m.start():m.start(1)] + + m.group(2) + line + sep) + self.offset += len(line + sep) + continue + self.flush_text() + self.offset += self.read_code(self.source[self.offset:], + multiline=bool(m.group(4))) + else: + break + self.text_buffer.append(self.source[self.offset:]) + self.flush_text() + return ''.join(self.code_buffer) + + def read_code(self, pysource, multiline): + code_line, comment = '', '' + offset = 0 + while True: + m = self.re_tok.search(pysource, pos=offset) + if not m: + code_line += pysource[offset:] + offset = len(pysource) + self.write_code(code_line.strip(), comment) + break + code_line += pysource[offset:m.start()] + offset = m.end() + _str, _com, _po, _pc, _blk1, _blk2, _end, _cend, _nl = m.groups() + if self.paren_depth > 0 and (_blk1 or _blk2): # a if b else c + code_line += _blk1 or _blk2 + continue + if _str: # Python string + code_line += _str + elif _com: # Python comment (up to EOL) + comment = _com + if multiline and _com.strip().endswith(self._tokens[1]): + multiline = False # Allow end-of-block in comments + elif _po: # open parenthesis + self.paren_depth += 1 + code_line += _po + elif _pc: # close parenthesis + if self.paren_depth > 0: + # we could check for matching parentheses here, but it's + # easier to leave that to python - just check counts + self.paren_depth -= 1 + code_line += _pc + elif _blk1: # Start-block keyword (if/for/while/def/try/...) + code_line = _blk1 + self.indent += 1 + self.indent_mod -= 1 + elif _blk2: # Continue-block keyword (else/elif/except/...) + code_line = _blk2 + self.indent_mod -= 1 + elif _cend: # The end-code-block template token (usually '%>') + if multiline: multiline = False + else: code_line += _cend + elif _end: + self.indent -= 1 + self.indent_mod += 1 + else: # \n + self.write_code(code_line.strip(), comment) + self.lineno += 1 + code_line, comment, self.indent_mod = '', '', 0 + if not multiline: + break + + return offset + + def flush_text(self): + text = ''.join(self.text_buffer) + del self.text_buffer[:] + if not text: return + parts, pos, nl = [], 0, '\\\n' + ' ' * self.indent + for m in self.re_inl.finditer(text): + prefix, pos = text[pos:m.start()], m.end() + if prefix: + parts.append(nl.join(map(repr, prefix.splitlines(True)))) + if prefix.endswith('\n'): parts[-1] += nl + parts.append(self.process_inline(m.group(1).strip())) + if pos < len(text): + prefix = text[pos:] + lines = prefix.splitlines(True) + if lines[-1].endswith('\\\\\n'): lines[-1] = lines[-1][:-3] + elif lines[-1].endswith('\\\\\r\n'): lines[-1] = lines[-1][:-4] + parts.append(nl.join(map(repr, lines))) + code = '_printlist((%s,))' % ', '.join(parts) + self.lineno += code.count('\n') + 1 + self.write_code(code) + + @staticmethod + def process_inline(chunk): + if chunk[0] == '!': return '_str(%s)' % chunk[1:] + return '_escape(%s)' % chunk + + def write_code(self, line, comment=''): + code = ' ' * (self.indent + self.indent_mod) + code += line.lstrip() + comment + '\n' + self.code_buffer.append(code) + + +def template(*args, **kwargs): + """ + Get a rendered template as a string iterator. + You can use a name, a filename or a template string as first parameter. + Template rendering arguments can be passed as dictionaries + or directly (as keyword arguments). + """ + tpl = args[0] if args else None + for dictarg in args[1:]: + kwargs.update(dictarg) + adapter = kwargs.pop('template_adapter', SimpleTemplate) + lookup = kwargs.pop('template_lookup', TEMPLATE_PATH) + tplid = (id(lookup), tpl) + if tplid not in TEMPLATES or DEBUG: + settings = kwargs.pop('template_settings', {}) + if isinstance(tpl, adapter): + TEMPLATES[tplid] = tpl + if settings: TEMPLATES[tplid].prepare(**settings) + elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl: + TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings) + else: + TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings) + if not TEMPLATES[tplid]: + abort(500, 'Template (%s) not found' % tpl) + return TEMPLATES[tplid].render(kwargs) + + +mako_template = functools.partial(template, template_adapter=MakoTemplate) +cheetah_template = functools.partial(template, + template_adapter=CheetahTemplate) +jinja2_template = functools.partial(template, template_adapter=Jinja2Template) + + +def view(tpl_name, **defaults): + """ Decorator: renders a template for a handler. + The handler can control its behavior like that: + + - return a dict of template vars to fill out the template + - return something other than a dict and the view decorator will not + process the template, but return the handler result as is. + This includes returning a HTTPResponse(dict) to get, + for instance, JSON with autojson or other castfilters. + """ + + def decorator(func): + + @functools.wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + if isinstance(result, (dict, DictMixin)): + tplvars = defaults.copy() + tplvars.update(result) + return template(tpl_name, **tplvars) + elif result is None: + return template(tpl_name, defaults) + return result + + return wrapper + + return decorator + + +mako_view = functools.partial(view, template_adapter=MakoTemplate) +cheetah_view = functools.partial(view, template_adapter=CheetahTemplate) +jinja2_view = functools.partial(view, template_adapter=Jinja2Template) + +############################################################################### +# Constants and Globals ######################################################## +############################################################################### + +TEMPLATE_PATH = ['./', './views/'] +TEMPLATES = {} +DEBUG = False +NORUN = False # If set, run() does nothing. Used by load_app() + +#: A dict to map HTTP status codes (e.g. 404) to phrases (e.g. 'Not Found') +HTTP_CODES = httplib.responses.copy() +HTTP_CODES[418] = "I'm a teapot" # RFC 2324 +HTTP_CODES[428] = "Precondition Required" +HTTP_CODES[429] = "Too Many Requests" +HTTP_CODES[431] = "Request Header Fields Too Large" +HTTP_CODES[451] = "Unavailable For Legal Reasons" # RFC 7725 +HTTP_CODES[511] = "Network Authentication Required" +_HTTP_STATUS_LINES = dict((k, '%d %s' % (k, v)) + for (k, v) in HTTP_CODES.items()) + +#: The default template used for error pages. Override with @error() +ERROR_PAGE_TEMPLATE = """ +%%try: + %%from %s import DEBUG, request + + + + Error: {{e.status}} + + + +

Error: {{e.status}}

+

Sorry, the requested URL {{repr(request.url)}} + caused an error:

+
{{e.body}}
+ %%if DEBUG and e.exception: +

Exception:

+ %%try: + %%exc = repr(e.exception) + %%except: + %%exc = '' %% type(e.exception).__name__ + %%end +
{{exc}}
+ %%end + %%if DEBUG and e.traceback: +

Traceback:

+
{{e.traceback}}
+ %%end + + +%%except ImportError: + ImportError: Could not generate the error page. Please add bottle to + the import path. +%%end +""" % __name__ + +#: A thread-safe instance of :class:`LocalRequest`. If accessed from within a +#: request callback, this instance always refers to the *current* request +#: (even on a multi-threaded server). +request = LocalRequest() + +#: A thread-safe instance of :class:`LocalResponse`. It is used to change the +#: HTTP response for the *current* request. +response = LocalResponse() + +#: A thread-safe namespace. Not used by Bottle. +local = threading.local() + +# Initialize app stack (create first empty Bottle app now deferred until needed) +# BC: 0.6.4 and needed for run() +apps = app = default_app = AppStack() + +#: A virtual package that redirects import statements. +#: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`. +ext = _ImportRedirect('bottle.ext' if __name__ == '__main__' else + __name__ + ".ext", 'bottle_%s').module + + +def _main(argv): # pragma: no coverage + args, parser = _cli_parse(argv) + + def _cli_error(cli_msg): + parser.print_help() + _stderr('\nError: %s\n' % cli_msg) + sys.exit(1) + + if args.version: + _stdout('Bottle %s\n' % __version__) + sys.exit(0) + if not args.app: + _cli_error("No application entry point specified.") + + sys.path.insert(0, '.') + sys.modules.setdefault('bottle', sys.modules['__main__']) + + host, port = (args.bind or 'localhost'), 8080 + if ':' in host and host.rfind(']') < host.rfind(':'): + host, port = host.rsplit(':', 1) + host = host.strip('[]') + + config = ConfigDict() + + for cfile in args.conf or []: + try: + if cfile.endswith('.json'): + with open(cfile, 'rb') as fp: + config.load_dict(json_loads(fp.read())) + else: + config.load_config(cfile) + except configparser.Error as parse_error: + _cli_error(parse_error) + except IOError: + _cli_error("Unable to read config file %r" % cfile) + except (UnicodeError, TypeError, ValueError) as error: + _cli_error("Unable to parse config file %r: %s" % (cfile, error)) + + for cval in args.param or []: + if '=' in cval: + config.update((cval.split('=', 1),)) + else: + config[cval] = True + + run(args.app, + host=host, + port=int(port), + server=args.server, + reloader=args.reload, + plugins=args.plugin, + debug=args.debug, + config=config) + + +if __name__ == '__main__': # pragma: no coverage + _main(sys.argv) diff --git a/storyteller/codemirror-ecs.js b/storyteller/codemirror-ecs.js new file mode 100644 index 0000000..58d762c --- /dev/null +++ b/storyteller/codemirror-ecs.js @@ -0,0 +1,63 @@ +(function (mod) { + if (typeof exports == `object` && typeof module == `object`) // CommonJS + mod(require(`../../lib/codemirror`)); + else if (typeof define == `function` && define.amd) // AMD + define([`../../lib/codemirror`], mod); + else // Plain browser env + mod(CodeMirror); +})(function (CodeMirror) { + "use strict"; + + + CodeMirror.defineMode(`ecs`, function () { + return { + startState: function () { + return { + inString: false, + inComment: false + }; + }, + token: function (stream, state) { + stream.eatSpace(); + // If a string or a comment starts here + if (!state.inString && stream.peek() === `\``) { + stream.next(); // Skip quote + state.inString = true; + } else if (!state.inComment && stream.peek() === `!`) { + stream.next(); // Skip shriek + state.inComment = true; + } + + if (state.inString) { + if (stream.skipTo(`\``)) { // Quote found on this line + stream.next(); // Skip quote + state.inString = false; // Clear flag + } else { + stream.skipToEnd(); + state.inString = false; // Clear flag + } + return `string`; // Return the token style + } + + else if (state.inComment) { + stream.skipToEnd(); + state.inComment = false; + return `comment`; // Return the token style + } + + else { + if (stream.match(/[A-Z][A-Za-z0-9-_]*/, true)) { + return `attribute`; + } + if (stream.match(/[0-9]+/, true)) { + return `number`; + } + stream.skipTo(` `) || stream.skipToEnd(); + return null; // Unstyled token + } + } + }; + }); + + CodeMirror.defineMIME(`text/x-ecs`, `ecs`); +}); \ No newline at end of file diff --git a/storyteller/favicon.gif b/storyteller/favicon.gif new file mode 100644 index 0000000..e69de29 diff --git a/storyteller/favicon.ico b/storyteller/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/storyteller/icons/arrow-back.png b/storyteller/icons/arrow-back.png new file mode 100644 index 0000000..a375adc Binary files /dev/null and b/storyteller/icons/arrow-back.png differ diff --git a/storyteller/icons/arrow-forward.png b/storyteller/icons/arrow-forward.png new file mode 100644 index 0000000..0b128a4 Binary files /dev/null and b/storyteller/icons/arrow-forward.png differ diff --git a/storyteller/icons/home.png b/storyteller/icons/home.png new file mode 100644 index 0000000..9e229dc Binary files /dev/null and b/storyteller/icons/home.png differ diff --git a/storyteller/icons/info.png b/storyteller/icons/info.png new file mode 100644 index 0000000..e11e721 Binary files /dev/null and b/storyteller/icons/info.png differ diff --git a/storyteller/index.html b/storyteller/index.html new file mode 100644 index 0000000..1d875b2 --- /dev/null +++ b/storyteller/index.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + diff --git a/storyteller/rest.php b/storyteller/rest.php new file mode 100644 index 0000000..11e2533 --- /dev/null +++ b/storyteller/rest.php @@ -0,0 +1,167 @@ + 1) { + $props[$ss[0]] = $ss[1]; + } + } + fclose($file); + } + + $requestURI = substr($_SERVER['REQUEST_URI'], 1); + $request = explode("/", $requestURI); + $first = array_shift($request); + $path = getcwd() . '/' . join('/', $request); + $method = $_SERVER['REQUEST_METHOD']; + // First, the commands that don't require a database connection. + switch ($method) { + case 'GET': + switch ($first) { + case '': + print file_get_contents(getcwd() . "/index.html"); + exit; + case 'scripted': + print file_get_contents(getcwd() . "/scripted.html"); + exit; + case 'list': + // List the contents of a directory + // Endpoint: {site root}/list/{path} + $files = scandir($path); + $dd = ''; + $ff = ''; + // First list all the directories + foreach ($files as $file) { + if (strpos($file, '.') !== 0) { + if (is_dir("$path/$file")) { + if ($dd) { + $dd .= ','; + } + $dd .= "\"$file\""; + } + } + } + // Now do the ordinary files + foreach ($files as $file) { + if (strpos($file, '.') !== 0) { + if (!is_dir("$path/$file")) { + if ($ff) { + $ff .= ','; + } + $ff .= "\"$file\""; + } + } + } + print '{"dirs":[' . $dd . '],"files":[' . $ff . ']}'; + exit; + default: + print file_get_contents($requestURI); + exit; + } + break; + case 'POST': + switch ($first) { + case 'makedirs': + // Create a directory + // Endpoint: {site root}/mkdir/{path} + logger("Create directory $path"); + mkdir($path, 0777, true); + exit; + case 'save': + // Save data to a file + // Endpoint: {site root}/save/{path} + $p = strrpos($path, '/'); + $dir = substr($path, 0, $p); + mkdir($dir, 0777, true); + header("Content-Type: application/text"); + $content = stripslashes(file_get_contents("php://input")); + file_put_contents(getcwd() . '/' . join('/', $request), $content); + exit; + case 'delete': + // Delete a file in the resources folder + // Endpoint: {site root}/delete/{path} + $path = getcwd() . '/' . join('/', $request); + if (is_dir($path)) { + rmdir($path); + } else { + unlink($path); + } + exit; + case 'upload': + // Upload a file (an image) to the current directory + // Endpoint: {site root}/upload/{path} + $path = $_POST['path']; + $path = explode("/", $path); + array_shift($path); + $path = "resources/" . join('/', $path); + mkdir($path, 0777, true); + logger("path: $path"); + $fileName = $_FILES['source']['name']; + $tempName = $_FILES['source']['tmp_name']; + $fileType = $_FILES['source']['type']; + $fileSize = $_FILES['source']['size']; + $fileError = $_FILES['source']['error']; + if (!move_uploaded_file($tempName, "$path/$fileName")) { + unlink($tempName); + http_response_code(400); + logger("Failed to upload $tempName to $fileName.\ntempName: $tempName\nfileType: $fileType\nfileSize:$fileSize\nfileError: $fileError"); + } else { + logger("File $fileName uploaded successfully to $path/$fileName"); + $size = getimagesize("$path/$fileName"); + logger("$path/$fileName: width:".$size[0].", height:".$size[1]); + if ($size[0] > 1024) { + logger("mogrify -resize 1024x1024 $path/$fileName"); + system("mogrify -resize 1024x1024 $path/$fileName"); + } + } + exit; + case 'email': + // Send an email + // Endpoint: {site root}/email + header("Content-Type: application/text"); + $value = stripslashes(file_get_contents("php://input")); + $json = json_decode($value); + $from = $json->from; + $to = $json->to; + $subject = $json->subject; + $message = $json->message; + $headers = "MIME-Version: 1.0\r\n"; + $headers .= "Content-Type: text/html; charset=ISO-8859-1\r\n"; + $headers .= "From: $from\r\n"; + mail($to, $subject, $message, "$headers\r\n"); + print "$headers\r\n$message"; + exit; + default: + http_response_code(404); + return; + } + break; + } + + //////////////////////////////////////////////////////////////////////////// + // Log a message. + function logger($message) + { + $timestamp = time(); + $date = date("Y/m/d H:i", $timestamp); + if (!file_exists("log")) mkdir("log"); + $file = "log/".date("Y", $timestamp); + if (!file_exists($file)) mkdir($file); + $file.= "/".date("Ymd", $timestamp).".txt"; + $fp = fopen($file, "a+") or die("Can't open $file"); + fwrite($fp, "$date: $message\n"); + fclose($fp); + } +?> diff --git a/storyteller/rest.py b/storyteller/rest.py new file mode 100644 index 0000000..8c00880 --- /dev/null +++ b/storyteller/rest.py @@ -0,0 +1,75 @@ +import bottle, subprocess, os, json +from bottle import Bottle, run, get, post, request, response, static_file + +app = Bottle() + +############################################################################### +# Endpoints for EasyCoder script editing + +# Endpoint: GET localhost:/list +# Lists all files in the given directory +@app.get('/list/') +def listScripts(path): + print(f'List files in {path}') + dd = [] + ff = [] + for file in os.listdir(path): + if os.path.isdir(os.path.join(path, file)): + dd.append(file) + else: + ff.append(file) + dd.sort() + ff.sort() + d = json.dumps(dd) + f = json.dumps(ff) + return '{"dirs":' + d + ',"files":' + f + '}' + +# Endpoint: POST localhost:/saveScript/ +# Writes the POST body to a named file in the given directory +@app.post('/save/') +def saveScript(path): + print(f'Save {path}') + f = open(path, 'w+') + f.write(request.body.read().decode('utf-8')) + f.close() + return + +# Endpoint: POST localhost:/makedirs/ +# Creates (nested) directories in the given directory +@app.post('/makedirs/') +def makedirs(path): + try: + os.makedirs(path, 0o777, True) + except Exception as err: + print(err) + return + +# Endpoint: POST localhost:/delete/ +# Deletes a named script in the given directory +@app.post('/delete/') +def deleteScript(path): + print(f'Delete {path}') + os.remove(path) + return + +############################################################################### +# Generic endpoints + +# Endpoint: GET localhost:/ +# Gets a file +@app.get('/') +def getFile(path): + print(f'getFile {path}') + response = bottle.static_file(path, root='.') + if response.status[0:3] in ['200', '304']: + response.set_header("Cache-Control", "public, max-age=0") + return response + +# Endpoint: GET localhost:17000 +# Gets the default home page +@app.get('/') +def index(): + return getFile('index.html') + +if __name__ == '__main__': + app.run(host='localhost', port=17001, debug=True) diff --git a/storyteller/scripted.ecs b/storyteller/scripted.ecs new file mode 100644 index 0000000..3f48008 --- /dev/null +++ b/storyteller/scripted.ecs @@ -0,0 +1,79 @@ +! Script Editor + + script ScriptEditor + + div Body + div Container + div Buttons + div ContentDiv + textarea ContentEditor + span Status + button Save + variable File + variable Content + variable Current + + set the title to `Script Editor` + + create Body + create Container in Body + set the style of Container to `width:70%;margin:0 auto;background #ffe` + + create Buttons in Container + set the style of Buttons to `text-align:center` + + create Save in Buttons + set the text of Save to `Save` + create Status in Buttons + set the style of Status to `position:absolute;float:right;padding-left:2em;color:green` + + create ContentDiv in Container + set the style of ContentDiv to `width:100%;height:95%;border:1px solid lightgray` + + create ContentEditor in ContentDiv + set the style of ContentEditor to `width:100%;height:100%` + + codemirror init basic profile `/codemirror-ecs.js` + require css `https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.46.0/addon/dialog/dialog.css` + require js `https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.46.0/addon/dialog/dialog.js` + require js `https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.46.0/addon/search/search.js` + require js `https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.46.0/addon/search/searchcursor.js` + require js `https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.46.0/addon/search/jump-to-line.js` + + put empty into Current + + on click Save + begin + codemirror close ContentEditor + put the content of ContentEditor into Content + if Content is not Current + begin + rest post Content to `save/storyteller.txt` + put Content into Current + set the content of Status to `Script saved` + fork to ResetStatus + end + else + begin + set the content of Status to `Nothing has changed` + fork to ResetStatus + end + codemirror attach to ContentEditor + end + +SelectFile: + put `storyteller.txt` into File + rest get Content from `/` cat File + put Content into Current + set the content of Status to `Script '` cat File cat `' loaded` + fork to ResetStatus + set the title to File + + codemirror attach to ContentEditor + codemirror set content of ContentEditor to Content + stop + +ResetStatus: + wait 2 + set the content of Status to `` + stop diff --git a/storyteller/scripted.html b/storyteller/scripted.html new file mode 100644 index 0000000..f92c0a5 --- /dev/null +++ b/storyteller/scripted.html @@ -0,0 +1,21 @@ + + + + + Script Editor + + + + + + + diff --git a/storyteller/storyteller.js b/storyteller/storyteller.js new file mode 100644 index 0000000..b2d1362 --- /dev/null +++ b/storyteller/storyteller.js @@ -0,0 +1,10614 @@ +const EasyCoder_Browser = { + + name: `EasyCoder_Browser`, + + A: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `a`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + Alert: { + + compile: (compiler) => { + const lino = compiler.getLino(); + const value = compiler.getNextValue(); + compiler.addCommand({ + domain: `browser`, + keyword: `alert`, + lino, + value + }); + return true; + }, + + run: (program) => { + const command = program[program.pc]; + const value = program.getFormattedValue(command.value); + alert(value); + return command.pc + 1; + } + }, + + Attach: { + + compile: (compiler) => { + const lino = compiler.getLino(); + compiler.next(); + if (compiler.isSymbol()) { + // const symbol = compiler.getProgram()[compiler.getSymbol().pc]; + const symbol = compiler.getSymbolRecord(); + let type = symbol.keyword; + switch (type) { + case `a`: + case `blockquote`: + case `button`: + case `canvas`: + case `div`: + case `fieldset`: + case `file`: + case `form`: + case `h1`: + case `h2`: + case `h3`: + case `h4`: + case `h5`: + case `h6`: + case `image`: + case `img`: + case `input`: + case `label`: + case `legend`: + case `li`: + case `option`: + case `p`: + case `pre`: + case `select`: + case `span`: + case `table`: + case `td`: + case `text`: + case `textarea`: + case `tr`: + case `ul`: + compiler.next(); + if (compiler.tokenIs(`to`)) { + let cssID = null; + if (compiler.nextTokenIs(`body`)) { + if (type=== `div`) { + cssId = `body`; + compiler.next(); + } else { + throw Error(`Body variable must be a div`); + } + } + else cssId = compiler.getValue(); + let onError = 0; + if (compiler.tokenIs(`or`)) { + compiler.next(); + onError = compiler.getPc() + 1; + compiler.completeHandler(); + } + compiler.addCommand({ + domain: `browser`, + keyword: `attach`, + lino, + type, + symbol: symbol.name, + cssId, + onError + }); + return true; + } + break; + default: + compiler.addWarning(`type '${symbol.keyword}' not recognized in browser 'attach'`); + return false; + } + } + compiler.addWarning(`Unrecognised syntax in 'attach'`); + return false; + }, + + run: (program) => { + const command = program[program.pc]; + let content = null; + let element = null; + if (command.cssId === `body`) { + element = document.body; + } else { + content = program.value.evaluate(program, command.cssId).content; + element = document.getElementById(content); + } + if (!element) { + if (command.onError) { + program.run(command.onError); + } else { + program.runtimeError(command.lino, `No such element: '${content}'`); + } + return 0; + } + const target = program.getSymbolRecord(command.symbol); + target.element[target.index] = element; + target.value[target.index] = { + type: `constant`, + numeric: false, + content + }; + if (command.type === `popup`) { + // Register a popup + program.popups.push(element.id); + // Handle closing of the popup + window.onclick = function (event) { + if (program.popups.includes(event.target.id)) { + event.target.style.display = `none`; + } + }; + } + return command.pc + 1; + } + }, + + Audioclip: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `audioclip`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + BLOCKQUOTE: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `blockquote`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + BUTTON: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `button`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + CANVAS: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `canvas`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + Clear: { + + compile: (compiler) => { + const lino = compiler.getLino(); + if (compiler.nextTokenIs(`body`)) { + compiler.next(); + compiler.addCommand({ + domain: `browser`, + keyword: `clear`, + lino, + name: null + }); + return true; + } + if (compiler.isSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + if (symbolRecord.extra === `dom`) { + compiler.next(); + compiler.addCommand({ + domain: `browser`, + keyword: `clear`, + lino, + name: symbolRecord.name + }); + return true; + } + } + return false; + }, + + run: (program) => { + const command = program[program.pc]; + if (command.name) { + const targetRecord = program.getSymbolRecord(command.name); + const target = targetRecord.element[targetRecord.index]; + switch (targetRecord.keyword) { + case `input`: + case `textarea`: + target.value = ``; + break; + default: + target.innerHTML = ``; + break; + } + } else { + document.body.innerHTML = ``; + } + return command.pc + 1; + } + }, + + Convert: { + + compile: (compiler) => { + const lino = compiler.getLino(); + if (compiler.nextTokenIs(`whitespace`)) { + if (compiler.nextTokenIs(`in`)) { + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + if (symbolRecord.isVHolder) { + if (compiler.nextTokenIs(`to`)) { + const mode = compiler.nextToken(); + compiler.next(); + compiler.addCommand({ + domain: `browser`, + keyword: `convert`, + lino, + name: symbolRecord.name, + mode + }); + return true; + } + } + } + } + } + return false; + }, + + run: (program) => { + const command = program[program.pc]; + const targetRecord = program.getSymbolRecord(command.name); + const content = targetRecord.value[targetRecord.index].content; + let value = content; + switch (command.mode) { + case `print`: + value = value.split(`%0a`).join(`\n`).split(`%0A`).join(`\n`).split(`%0d`).join(``).split(`$0D`).join(``); + break; + case `html`: + value = value.split(`%0a`).join(`
`).split(`%0A`).join(`
`).split(`%0d`).join(``).split(`$0D`).join(``); + break; + } + targetRecord.value[targetRecord.index].content = value; + return command.pc + 1; + } + }, + + Create: { + + compile: (compiler) => { + const lino = compiler.getLino(); + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + const keyword = symbolRecord.keyword; + if (keyword === `audioclip`) { + if (compiler.nextTokenIs(`from`)) { + const value = compiler.getNextValue(); + compiler.addCommand({ + domain: `browser`, + keyword: `create`, + type: `audioclip`, + name: symbolRecord.name, + lino, + value + }); + return true; + } + return false; + } + if ([`a`, + `blockquote`, + `button`, + `canvas`, + `div`, + `fieldset`, + `file`, + `form`, + `h1`, + `h2`, + `h3`, + `h4`, + `h5`, + `h6`, + `hr`, + `image`, + `img`, + `input`, + `label`, + `legend`, + `li`, + `option`, + `p`, + `pre`, + `progress`, + `select`, + `span`, + `table`, + `tr`, + `td`, + `text`, + `textarea`, + `ul` + ].includes(keyword)) { + if (compiler.nextTokenIs(`in`)) { + if (compiler.nextTokenIs(`body`)) { + compiler.next(); + compiler.addCommand({ + domain: `browser`, + keyword: `create`, + lino, + name: symbolRecord.name, + parent: `body` + }); + return true; + } + if (compiler.isSymbol()) { + const parentRecord = compiler.getSymbolRecord(); + compiler.next(); + compiler.addCommand({ + domain: `browser`, + keyword: `create`, + lino, + name: symbolRecord.name, + parent: parentRecord.name + }); + return true; + } + } else { + const imports = compiler.imports; + if (imports && imports.length > 0) { + // This section is used by Codex to force run in Run panel, which must be the first import + compiler.addCommand({ + domain: `browser`, + keyword: `create`, + lino, + name: symbolRecord.name, + parent: imports[0], + imported: true + }); + return true; + } else { + compiler.addCommand({ + domain: `browser`, + keyword: `create`, + lino, + name: symbolRecord.name, + parent: `body` + }); + return true; + } + } + } + } + return false; + }, + + run: (program) => { + const command = program[program.pc]; + const targetRecord = program.getSymbolRecord(command.name); + switch (command.type) { + case `audioclip`: + targetRecord.value[targetRecord.index] = command.value; + break; + default: + let parent; + if (command.parent === `body`) { + parent = document.body; + } else { + const p = command.imported ? EasyCoder.scripts[program.parent] : program; + const parentRecord = p.getSymbolRecord(command.parent); + if (!parentRecord.element[parentRecord.index]) { + program.runtimeError(command.pc, `Element ${parentRecord.name} does not exist.`); + } + parent = parentRecord.element[parentRecord.index]; + } + targetRecord.element[targetRecord.index] = document.createElement(targetRecord.keyword); + targetRecord.element[targetRecord.index].id = + `ec-${targetRecord.name}-${targetRecord.index}-${EasyCoder.elementId++}`; + if (targetRecord.keyword === `a`) { + targetRecord.element[targetRecord.index].setAttribute(`href`, `#`); + } + parent.appendChild(targetRecord.element[targetRecord.index]); + break; + } + return command.pc + 1; + } + }, + + Disable: { + + compile: (compiler) => { + const lino = compiler.getLino(); + if (compiler.nextIsSymbol()) { + const symbol = compiler.getToken(); + compiler.next(); + compiler.addCommand({ + domain: `browser`, + keyword: `disable`, + lino, + symbol + }); + return true; + } + compiler.addWarning(`Unrecognised syntax in 'disable'`); + return false; + }, + + run: (program) => { + const command = program[program.pc]; + const symbol = program.getSymbolRecord(command.symbol); + const target = document.getElementById(symbol.value[symbol.index].content); + target.disabled = `true`; + return command.pc + 1; + } + }, + + DIV: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `div`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + Enable: { + + compile: (compiler) => { + const lino = compiler.getLino(); + if (compiler.nextIsSymbol()) { + const symbol = compiler.getToken(); + compiler.next(); + compiler.addCommand({ + domain: `browser`, + keyword: `enable`, + lino, + symbol + }); + return true; + } + compiler.addWarning(`Unrecognised syntax in 'enable'`); + return false; + }, + + run: (program) => { + const command = program[program.pc]; + const symbol = program.getSymbolRecord(command.symbol); + const target = document.getElementById(symbol.value[symbol.index].content); + target.disabled = false; + return command.pc + 1; + } + }, + + FIELDSET: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `fieldset`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + FILE: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `file`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + Focus: { + + compile: (compiler) => { + const lino = compiler.getLino(); + if (compiler.nextIsSymbol()) { + const symbol = compiler.getToken(); + compiler.next(); + compiler.addCommand({ + domain: `browser`, + keyword: `focus`, + lino, + symbol + }); + return true; + } + compiler.addWarning(`Unrecognised syntax in 'focus'`); + return false; + }, + + run: (program) => { + const command = program[program.pc]; + const symbol = program.getSymbolRecord(command.symbol); + const element = symbol.element[symbol.index]; + element.focus(); + return command.pc + 1; + } + }, + + FORM: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `form`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + Get: { + + compile: (compiler) => { + const lino = compiler.getLino(); + if (compiler.nextIsSymbol()) { + const target = compiler.getToken(); + let targetRecord = compiler.getSymbolRecord(); + if (compiler.nextTokenIs(`from`)) { + if (compiler.nextTokenIs(`storage`)) { + if (compiler.nextTokenIs(`as`)) { + const key = compiler.getNextValue(); + compiler.addCommand({ + domain: `browser`, + keyword: `get`, + action: `getStorage`, + lino, + target, + key + }); + return true; + } else { + compiler.addCommand({ + domain: `browser`, + keyword: `get`, + action: `listStorage`, + lino, + target + }); + return true; + } + } + if (compiler.isSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + if (symbolRecord.keyword === `select`) { + if (targetRecord.keyword === `option`) { + compiler.next(); + compiler.addCommand({ + domain: `browser`, + keyword: `get`, + action: `getOption`, + lino, + target, + select: symbolRecord.name + }); + return true; + } + throw Error(`Invalid variable type`); + } + if (symbolRecord.keyword !== `form`) { + throw Error(`Invalid variable type`); + } + compiler.next(); + compiler.addCommand({ + domain: `browser`, + keyword: `get`, + action: `getForm`, + lino, + target, + form: symbolRecord.name + }); + return true; + } + else { + let targetRecord = compiler.getSymbolRecord(target); + } + } + } + compiler.addWarning(`Unrecognised syntax in 'get'`); + return false; + }, + + run: (program) => { + const command = program[program.pc]; + const targetRecord = program.getSymbolRecord(command.target); + switch (command.action) { + case `getForm`: + const formRecord = program.getSymbolRecord(command.form); + const form = document.getElementById(formRecord.value[formRecord.index].content); + const data = new FormData(form); + const content = {}; + for (const entry of data) { + content[entry[0]] = entry[1].replace(/\r/g, ``).replace(/\n/g, `%0a`); + } + targetRecord.value[targetRecord.index] = { + type: `constant`, + numeric: false, + content: JSON.stringify(content) + }; + break; + case `listStorage`: + const items = []; + for (let i = 0, len = window.localStorage.length; i < len; i++) { + items.push(localStorage.key(i)); + } + targetRecord.value[targetRecord.index] = { + type: `constant`, + numeric: false, + content: JSON.stringify(items) + }; + break; + case `getStorage`: + let value = window.localStorage.getItem(program.getValue(command.key)); + if (typeof value === `undefined`) { + value = null; + } + targetRecord.value[targetRecord.index] = { + type: `constant`, + numeric: false, + content: value + }; + break; + case `getOption`: + let selectRecord = program.getSymbolRecord(command.select); + let select = selectRecord.element[selectRecord.index]; + let option = select.options[select.selectedIndex]; + targetRecord.element[targetRecord.index] = option; + break; + } + return command.pc + 1; + } + }, + + H1: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `h1`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + H2: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `h2`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + H3: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `h3`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + H4: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `h4`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + H5: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `h5`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + H6: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `h6`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + Highlight: { + + compile: (compiler) => { + const lino = compiler.getLino(); + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + if (symbolRecord.extra === `dom`) { + compiler.next(); + compiler.addCommand({ + domain: `browser`, + keyword: `highlight`, + lino, + name: symbolRecord.name + }); + return true; + } + } + return false; + }, + + run: (program) => { + const command = program[program.pc]; + const targetRecord = program.getSymbolRecord(command.name); + const element = targetRecord.element[targetRecord.index]; + element.select(); + return command.pc + 1; + } + }, + + History: { + + compile: (compiler) => { + const lino = compiler.getLino(); + const type = compiler.nextToken(); + switch (type) { + case `push`: + case `set`: + case `replace`: + compiler.next(); + let url = ``; + let state = ``; + while (true) { + const token = compiler.getToken(); + if (token === `url`) { + url = compiler.getNextValue(); + } else if (token === `state`) { + state = compiler.getNextValue(); + } else { + break; + } + } + compiler.addCommand({ + domain: `browser`, + keyword: `history`, + lino, + type, + url, + state + }); + return true; + case `pop`: + case `back`: + case `forward`: + compiler.next(); + compiler.addCommand({ + domain: `browser`, + keyword: `history`, + lino, + type + }); + return true; + } + return false; + }, + + run: (program) => { + if (!program.script) { + program.script = `script${Date.now()/1000}`; + } + const command = program[program.pc]; + let state = program.getValue(command.state); + if (!state) { + state = `{"script":"${program.script}"}`; + } + const url = program.getValue(command.url); + switch (command.type) { + case `push`: + if (!window.history.state) { + program.runtimeError(command.lino, `No state history; you need to call 'history set' on the parent`); + return 0; + } + window.history.pushState(state, ``, url); + break; + case `set`: + case `replace`: + window.history.replaceState(state, ``, url); + break; + case `pop`: + case `back`: + window.history.back(); + break; + case `forward`: + window.history.forward(); + break; + } + return command.pc + 1; + } + }, + + HR: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `hr`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + IMAGE: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `image`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + IMG: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `img`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + INPUT: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `input`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + LABEL: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `label`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + LEGEND: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `legend`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + LI: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `li`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + Location: { + + compile: (compiler) => { + const lino = compiler.getLino(); + let newWindow = false; + if (compiler.nextTokenIs(`new`)) { + newWindow = true; + compiler.next(); + } + const location = compiler.getValue(); + compiler.addCommand({ + domain: `browser`, + keyword: `location`, + lino, + location, + newWindow + }); + return true; + }, + + run: (program) => { + const command = program[program.pc]; + const location = program.getValue(command.location); + if (command.newWindow) { + window.open(location, `_blank`); + } else { + window.location = location; + } + return command.pc + 1; + } + }, + + Mail: { + + compile: (compiler) => { + const lino = compiler.getLino(); + if (compiler.nextTokenIs(`to`)) { + const to = compiler.getNextValue(); + let subject = ``; + let body = ``; + if (compiler.tokenIs(`subject`)) { + subject = compiler.getNextValue(); + if (compiler.tokenIs(`body`) || compiler.tokenIs(`message`)) { + compiler.next(); + body = compiler.getValue(); + } + } + compiler.addCommand({ + domain: `browser`, + keyword: `mail`, + lino, + to, + subject, + body + }); + return true; + } + return false; + }, + + run: (program) => { + const command = program[program.pc]; + if (command.subject) { + window.location.href = `mailto:${program.getValue(command.to)}` + + `?subject=${program.getValue(command.subject)}&body=${encodeURIComponent(program.getValue(command.body))}`; + } else { + window.location.href = `mailto:${program.getValue(command.to)}`; + } + return command.pc + 1; + } + }, + + On: { + + compile: (compiler) => { + const lino = compiler.getLino(); + const action = compiler.nextToken(); + switch (action) { + case `change`: + compiler.next(); + if (compiler.isSymbol()) { + const symbol = compiler.getSymbolRecord(); + compiler.next(); + if (symbol.extra !== `dom`) { + return false; + } + compiler.addCommand({ + domain: `browser`, + keyword: `on`, + lino, + action, + symbol: symbol.name + }); + return compiler.completeHandler(); + } + break; + case `click`: + if (compiler.nextTokenIs(`document`)) { + compiler.next(); + compiler.addCommand({ + domain: `browser`, + keyword: `on`, + lino, + action: `clickDocument` + }); + return compiler.completeHandler(); + } + if (compiler.isSymbol()) { + const symbol = compiler.getSymbolRecord(); + compiler.next(); + if (symbol.extra !== `dom`) { + return false; + } + compiler.addCommand({ + domain: `browser`, + keyword: `on`, + lino, + action, + symbol: symbol.name + }); + return compiler.completeHandler(); + } + break; + case `key`: + case `leave`: + compiler.next(); + compiler.addCommand({ + domain: `browser`, + keyword: `on`, + lino, + action + }); + return compiler.completeHandler(); + case `window`: + if (compiler.nextTokenIs(`resize`)) { + compiler.next(); + compiler.addCommand({ + domain: `browser`, + keyword: `on`, + lino, + action: `windowResize` + }); + return compiler.completeHandler(); + } + return false; + case `browser`: + case `restore`: + if (action === `browser` && !compiler.nextTokenIs(`back`)) { + return false; + } + compiler.next(); + compiler.addCommand({ + domain: `browser`, + keyword: `on`, + lino, + action: `browserBack` + }); + return compiler.completeHandler(); + case `swipe`: + if ([`left`, `right`].includes(compiler.nextToken())) { + const direction = compiler.getToken(); + compiler.next(); + compiler.addCommand({ + domain: `browser`, + keyword: `on`, + lino, + action: `swipe`, + direction + }); + return compiler.completeHandler(); + } + return false; + case `pick`: + if (compiler.nextIsSymbol()) { + const symbol = compiler.getSymbolRecord(); + compiler.next(); + if (symbol.extra !== `dom`) { + return false; + } + compiler.addCommand({ + domain: `browser`, + keyword: `on`, + lino, + action, + symbol: symbol.name + }); + return compiler.completeHandler(); + } + return false; + case `drag`: + case `drop`: + compiler.next(); + compiler.addCommand({ + domain: `browser`, + keyword: `on`, + lino, + action + }); + return compiler.completeHandler(); + } + compiler.addWarning(`Unrecognised syntax in 'on'`); + return false; + }, + + run: (program) => { + const command = program[program.pc]; + switch (command.action) { + case `change`: + targetRecord = program.getSymbolRecord(command.symbol); + targetRecord.program = program.script; + targetRecord.element.forEach(function (target, index) { + target.targetRecord = targetRecord; + target.targetIndex = index; + target.targetPc = command.pc + 2; + target.addEventListener(`change`, (event) => { + event.stopPropagation(); + if (program.length > 0) { + const eventTarget = event.target; + if (typeof eventTarget.targetRecord !== `undefined`) { + eventTarget.targetRecord.index = eventTarget.targetIndex; + setTimeout(function () { + EasyCoder.timestamp = Date.now(); + let p = EasyCoder.scripts[eventTarget.targetRecord.program]; + p.run(eventTarget.targetPc); + }, 1); + } + } + }); + }); + break; + case `click`: + targetRecord = program.getSymbolRecord(command.symbol); + targetRecord.program = program.script; + targetRecord.element.forEach(function (target, index) { + target.targetRecord = targetRecord; + target.targetIndex = index; + target.targetPc = command.pc + 2; + target.onclick = function (event) { + event.stopPropagation(); + if (program.length > 0) { + const eventTarget = event.target; + eventTarget.blur(); + if (typeof eventTarget.targetRecord !== `undefined`) { + eventTarget.targetRecord.index = eventTarget.targetIndex; + setTimeout(function () { + EasyCoder.timestamp = Date.now(); + let p = EasyCoder.scripts[eventTarget.targetRecord.program]; + p.run(eventTarget.targetPc); + }, 1); + } + } + return false; + }; + }); + break; + case `clickDocument`: + program.targetPc = command.pc + 2; + const interceptClickEvent = (e) => { + EasyCoder.timestamp = Date.now(); + let target = e.target || e.srcElement; + let href = ``; + while (target.parentNode) { + if (target.tagName === `A`) { + href = target.href; + program.docPath = href.slice(-(href.length - window.location.href.length)); + break; + } + target = target.parentNode; + } + while (target.parentNode) { + if (target.id.indexOf(`ec-`) === 0) { + let id = target.id.slice(3); + let pos = id.indexOf(`-`); + program.varName = id.slice(0, pos); + id = id.slice(pos + 1); + pos = id.indexOf(`-`); + program.varIndex = parseInt(id.slice(0, pos)); + break; + } + target = target.parentNode; + } + if (href.indexOf(window.location.href) === 0) { + program.run(program.targetPc); + e.preventDefault(); + } + }; + if (document.addEventListener) { + document.addEventListener(`click`, interceptClickEvent); + } else if (document.attachEvent) { + document.attachEvent(`onclick`, interceptClickEvent); + } + break; + case `swipe`: + let xDown; + const getTouches = (evt) => { + return evt.touches || // browser API + evt.originalEvent.touches; // jQuery + }; + const handleTouchStart = (evt) => { + const firstTouch = getTouches(evt)[0]; + xDown = firstTouch.clientX; + }; + const handleTouchMove = (evt) => { + evt.stopImmediatePropagation(); + if (!xDown) { + return; + } + const xUp = evt.touches[0].clientX; + const xDiff = xDown - xUp; + if (Math.abs(xDiff) > 150) { + xDown = null; + if (xDiff > 0 && program.onSwipeLeft) { + program.run(program.onSwipeLeft); + } else if (xDiff < 0 && program.onSwipeRight) { + program.run(program.onSwipeRight); + } + } + }; + switch (command.direction) { + case `left`: + program.onSwipeLeft = command.pc + 2; + break; + case `right`: + program.onSwipeRight = command.pc + 2; + break; + } + document.addEventListener(`touchstart`, handleTouchStart, false); + document.addEventListener(`touchmove`, handleTouchMove, false); + break; + case `pick`: + const pickRecord = program.getSymbolRecord(command.symbol); + document.pickRecord = pickRecord; + pickRecord.element.forEach(function (element, index) { + document.pickIndex = index; + element.pickIndex = index; + // Set up the mouse down and up listeners + element.mouseDownPc = command.pc + 2; + // Check if touch device + let isTouchDevice = `ontouchstart` in element; + if (isTouchDevice) { + element.addEventListener(`touchstart`, function (e) { + const element = e.targetTouches[0].target; + document.pickX = e.touches[0].clientX; + document.pickY = e.touches[0].clientY; + element.blur(); + setTimeout(function () { + document.pickRecord.index = element.pickIndex; + program.run(element.mouseDownPc); + }, 1); + }, false); + element.addEventListener(`touchmove`, function (e) { + document.dragX = e.touches[0].clientX; + document.dragY = e.touches[0].clientY; + setTimeout(function () { + program.run(document.mouseMovePc); + }, 1); + return false; + }, false); + element.addEventListener(`touchend`, function () { + setTimeout(function () { + program.run(document.mouseUpPc); + }, 1); + return false; + }); + } else { + element.onmousedown = function (event) { + let e = event ? event : window.event; + e.stopPropagation(); + // IE uses srcElement, others use target + if (program.length > 0) { + const element = e.target ? e.target : e.srcElement; + element.offsetX = e.offsetX; + element.offsetY = e.offsetY; + document.pickX = e.clientX; + document.pickY = e.clientY; + element.blur(); + setTimeout(function () { + document.pickRecord.index = element.pickIndex; + program.run(element.mouseDownPc); + }, 1); + } + document.onmousemove = function (event) { + let e = event ? event : window.event; + e.stopPropagation(); + document.dragX = e.clientX; + document.dragY = e.clientY; + if (document.onmousemove) { + setTimeout(function () { + program.run(document.mouseMovePc); + }, 1); + } + return false; + }; + window.onmouseup = function () { + document.onmousemove = null; + document.onmouseup = null; + setTimeout(function () { + if (program && program.run) { + program.run(document.mouseUpPc); + } + }, 1); + return false; + }; + return false; + }; + } + }); + break; + case `drag`: + // Set up the move listener + document.mouseMovePc = command.pc + 2; + break; + case `drop`: + // Set up the move listener + document.mouseUpPc = command.pc + 2; + break; + case `key`: + if (typeof document.onKeyListeners === `undefined`) { + document.onKeyListeners = []; + } + if (!document.onKeyListeners.includes(program)) { + document.onKeyListeners.push(program); + } + program.onKeyPc = command.pc + 2; + document.onkeypress = function (event) { + for (const program of document.onKeyListeners) { + program.key = event.key; + try { + setTimeout(function () { + program.run(program.onKeyPc); + }, 1); + } catch (err) { + console.log(`Error: ${err.message}`); + } + } + return true; + }; + break; + case `windowResize`: + program.onWindowResize = command.pc + 2; + window.addEventListener('resize', function() { + program.run(program.onWindowResize); + }); + break; + case `browserBack`: + program.onBrowserBack = command.pc + 2; + break; + case `leave`: + window.addEventListener(`beforeunload`, function () { + program.run(command.pc + 2); + }); + break; + default: + break; + } + return command.pc + 1; + } + }, + + OPTION: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `option`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + P: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `p`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + Play: { + + compile: (compiler) => { + const lino = compiler.getLino(); + if (compiler.nextIsSymbol()) { + const targetRecord = compiler.getSymbolRecord(); + if (targetRecord.keyword === `audioclip`) { + compiler.next(); + compiler.addCommand({ + domain: `browser`, + keyword: `play`, + lino, + target: targetRecord.name + }); + return true; + } + } + return false; + }, + + run: (program) => { + const command = program[program.pc]; + const targetRecord = program.getSymbolRecord(command.target); + const url = program.value.evaluate(program, targetRecord.value[targetRecord.index]).content; + new Audio(url).play(); + return command.pc + 1; + } + }, + + PRE: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `pre`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + PROGRESS: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `progress`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + Put: { + + compile: (compiler) => { + const lino = compiler.getLino(); + // Get the value + const value = compiler.getNextValue(); + if (compiler.tokenIs(`into`)) { + if (compiler.nextTokenIs(`storage`)) { + if (compiler.nextTokenIs(`as`)) { + const key = compiler.getNextValue(); + compiler.addCommand({ + domain: `browser`, + keyword: `put`, + lino, + value, + key + }); + return true; + } + } + } + return false; + }, + + // runtime + + run: (program) => { + const command = program[program.pc]; + window.localStorage.setItem(program.getValue(command.key), program.getValue(command.value)); + return command.pc + 1; + } + }, + + Remove: { + + compile: (compiler) => { + const lino = compiler.getLino(); + if (compiler.nextTokenIs(`element`)) { + if (compiler.nextIsSymbol()) { + const element = compiler.getSymbolRecord(); + if (element.extra != `dom`) { + compiler.warning(`'${element.name}' is not a DOM element`); + return false; + } + compiler.next(); + compiler.addCommand({ + domain: `browser`, + keyword: `remove`, + type: `removeElement`, + lino, + element: element.name + }); + return true; + } + } + if (compiler.tokenIs(`attribute`)) { + const attribute = compiler.getNextValue(); + if (compiler.tokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const targetRecord = compiler.getSymbolRecord(); + if (targetRecord.extra !== `dom`) { + throw new Error(`Inappropriate type '${targetRecord.keyword}'`); + } + compiler.next(); + compiler.addCommand({ + domain: `browser`, + keyword: `remove`, + type: `removeAttribute`, + lino, + attribute, + target: targetRecord.name + }); + return true; + } + } + } + try { + const key = compiler.getValue(); + if (compiler.tokenIs(`from`)) { + if (compiler.nextTokenIs(`storage`)) { + compiler.next(); + compiler.addCommand({ + domain: `browser`, + keyword: `remove`, + type: `removeStorage`, + key + }); + return true; + } + } + } catch (err) { + return false; + } + return false; + }, + + // runtime + + run: (program) => { + const command = program[program.pc]; + switch (command.type) { + case `removeAttribute`: + const attribute = program.getValue(command.attribute); + const targetRecord = program.getSymbolRecord(command.target); + target = targetRecord.element[targetRecord.index]; + target.removeAttribute(attribute); + break; + case `removeElement`: + const elementRecord = program.getSymbolRecord(command.element); + const element = elementRecord.element[elementRecord.index]; + element.parentElement.removeChild(element); + break; + case `removeStorage`: + const key = program.getValue(command.key); + window.localStorage.removeItem(key); + break; + } + return command.pc + 1; + } + }, + + Request: { + + compile: (compiler) => { + const lino = compiler.getLino(); + if (compiler.nextToken() === `fullscreen`) { + let option = ``; + if (compiler.nextToken() === `exit`) { + option = `exit`; + compiler.next(); + } + compiler.addCommand({ + domain: `browser`, + keyword: `request`, + lino, + option + }); + return true; + } + return false; + }, + + run: (program) => { + const command = program[program.pc]; + if (command.option === `exit`) { + document.exitFullscreen(); + } else { + document.documentElement.requestFullscreen(); + } + return command.pc + 1; + } + }, + + SELECT: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `select`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + Scroll: { + + compile: (compiler) => { + const lino = compiler.getLino(); + let name = null; + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + name = symbolRecord.name; + compiler.next(); + } + if (compiler.tokenIs(`to`)) { + const to = compiler.getNextValue(); + compiler.addCommand({ + domain: `browser`, + keyword: `scroll`, + lino, + name, + to + }); + return true; + } + return false; + }, + + run: (program) => { + const command = program[program.pc]; + const to = program.getValue(command.to); + if (command.name) { + const symbolRecord = program.getSymbolRecord(command.name); + symbolRecord.element[symbolRecord.index].scrollTo(0, to); + } else { + window.scrollTo(0, to); + } + return command.pc + 1; + } + }, + + SECTION: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `section`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + Set: { + + compile: (compiler) => { + const lino = compiler.getLino(); + if (compiler.nextIsSymbol()) { + const targetRecord = compiler.getSymbolRecord(); + const target = targetRecord.name; + if (targetRecord.extra === `dom`) { + const token = compiler.nextToken(); + if (token === `from`) { + if (compiler.nextIsSymbol()) { + if (targetRecord.keyword === `select`) { + const sourceRecord = compiler.getSymbolRecord(); + if (sourceRecord.keyword === `variable`) { + var display = null; + if (compiler.nextTokenIs(`as`)) { + display = compiler.getNextValue(); + } + compiler.addCommand({ + domain: `browser`, + keyword: `set`, + lino, + type: `setSelect`, + select: target, + source: sourceRecord.name, + display + }); + return true; + } + return false; + } + const source = compiler.getToken(); + compiler.next(); + compiler.addCommand({ + domain: `browser`, + keyword: `set`, + lino, + type: `setContentVar`, + source, + target + }); + return true; + } + } + } + } else { + let token = compiler.getToken(); + if (token === `the`) { + token = compiler.nextToken(); + } + if (token === `title`) { + if (compiler.nextTokenIs(`to`)) { + const value = compiler.getNextValue(); + compiler.addCommand({ + domain: `browser`, + keyword: `set`, + lino, + type: `setTitle`, + value + }); + return true; + } + } else if (token === `content`) { + if (compiler.nextTokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const target = compiler.getToken(); + if (compiler.nextTokenIs(`from`)) { + if (compiler.nextIsSymbol()) { + const source = compiler.getToken(); + compiler.next(); + compiler.addCommand({ + domain: `browser`, + keyword: `set`, + lino, + type: `setContentVar`, + source, + target + }); + return true; + } + } + if (compiler.tokenIs(`to`)) { + const value = compiler.getNextValue(); + compiler.addCommand({ + domain: `browser`, + keyword: `set`, + lino, + type: `setContent`, + value, + target + }); + return true; + } + } + throw new Error(`'${compiler.getToken()}' is not a symbol`); + } + } else if (token === `class`) { + if (compiler.nextTokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const symbol = compiler.getSymbolRecord(); + if (symbol.extra === `dom`) { + if (compiler.nextTokenIs(`to`)) { + const value = compiler.getNextValue(); + compiler.addCommand({ + domain: `browser`, + keyword: `set`, + lino, + type: `setClass`, + symbolName: symbol.name, + value + }); + return true; + } + } + } + } + } else if (token === `id`) { + if (compiler.nextTokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const symbol = compiler.getSymbolRecord(); + if (symbol.extra === `dom`) { + if (compiler.nextTokenIs(`to`)) { + const value = compiler.getNextValue(); + compiler.addCommand({ + domain: `browser`, + keyword: `set`, + lino, + type: `setId`, + symbolName: symbol.name, + value + }); + return true; + } + } + } + } + } else if (token === `text`) { + if (compiler.nextTokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const symbol = compiler.getSymbolRecord(); + switch (symbol.keyword) { + case `button`: + case `input`: + case `span`: + case `label`: + case `legend`: + if (compiler.nextTokenIs(`to`)) { + const value = compiler.getNextValue(); + compiler.addCommand({ + domain: `browser`, + keyword: `set`, + lino, + type: `setText`, + symbolName: symbol.name, + value + }); + return true; + } + break; + default: + break; + } + } + } + } else if (token === `size`) { + if (compiler.nextTokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const symbol = compiler.getSymbolRecord(); + switch (symbol.keyword) { + case `input`: + if (compiler.nextTokenIs(`to`)) { + const value = compiler.getNextValue(); + compiler.addCommand({ + domain: `browser`, + keyword: `set`, + lino, + type: `setSize`, + symbolName: symbol.name, + value + }); + return true; + } + } + } + } + } else if (token === `attribute`) { + compiler.next(); + const attributeName = compiler.getValue(); + if (compiler.tokenIs(`of`)) { + if (compiler.nextIsSymbol(true)) { + const symbolRecord = compiler.getSymbolRecord(); + const symbolName = symbolRecord.name; + compiler.next(); + let attributeValue = { + type: `boolean`, + content: true + }; + if (compiler.tokenIs(`to`)) { + attributeValue = compiler.getNextValue(); + } + compiler.addCommand({ + domain: `browser`, + keyword: `set`, + lino, + type: `setAttribute`, + symbolName, + attributeName, + attributeValue + }); + return true; + } + } + } else if (token === `attributes`) { + if (compiler.nextTokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + const symbolName = symbolRecord.name; + if (symbolRecord.extra !== `dom`) { + compiler.warning(`'${symbolName}' is not a DOM type`); + return false; + } + if (compiler.nextTokenIs(`to`)) { + const attributes = compiler.getNextValue(); + if (attributes) { + compiler.addCommand({ + domain: `browser`, + keyword: `set`, + lino, + type: `setAttributes`, + symbolName, + attributes + }); + return true; + } + } + } + } + compiler.warning(`'${compiler.getToken()}' is not a symbol`); + return false; + } else if (token === `style`) { + if (compiler.nextTokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + const symbolName = symbolRecord.name; + if (symbolRecord.extra !== `dom`) { + compiler.warning(`'${symbolName}' is not a DOM type`); + return false; + } + if (compiler.nextTokenIs(`to`)) { + const styleValue = compiler.getNextValue(); + if (styleValue) { + compiler.addCommand({ + domain: `browser`, + keyword: `set`, + lino, + type: `setStyles`, + symbolName, + styleValue + }); + return true; + } + } + } + compiler.warning(`'${compiler.getToken()}' is not a symbol`); + return false; + } + const styleName = compiler.getValue(); + let type = `setStyle`; + let symbolName = ``; + token = compiler.getToken(); + if (token === `of`) { + if (compiler.nextToken() === `body`) { + type = `setBodyStyle`; + } else if (compiler.isSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + symbolName = symbolRecord.name; + if (symbolRecord.extra !== `dom`) { + throw Error(`'${symbolName}' is not a DOM type`); + } + } else { + throw Error(`'${compiler.getToken()}' is not a known symbol`); + } + if (compiler.nextTokenIs(`to`)) { + const styleValue = compiler.getNextValue(); + if (styleValue) { + compiler.addCommand({ + domain: `browser`, + keyword: `set`, + lino, + type, + symbolName, + styleName, + styleValue + }); + return true; + } + } + } + else if (token === `to`) { + const styleValue = compiler.getNextValue(); + if (styleValue) { + compiler.addCommand({ + domain: `browser`, + keyword: `set`, + lino, + type: `setHeadStyle`, + styleName, + styleValue + }); + return true; + } + } + } else if (token === `default`) { + if (compiler.nextTokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + if (symbolRecord.keyword === `select`) { + if (compiler.nextTokenIs(`to`)) { + const value = compiler.getNextValue(); + compiler.addCommand({ + domain: `browser`, + keyword: `set`, + lino, + type: `setDefault`, + name: symbolRecord.name, + value + }); + return true; + } + } + } + } + } + } + compiler.addWarning(`Unrecognised syntax in 'set'`); + return false; + }, + + run: (program) => { + const command = program[program.pc]; + let symbol; + let value; + let target; + let targetId; + let targetRecord; + let cssId; + let selectRecord; + switch (command.type) { + case `setContentVar`: + const sourceVar = program.getSymbolRecord(command.source); + targetRecord = program.getSymbolRecord(command.target); + const source = document.getElementById(sourceVar.value[sourceVar.index].content); + target = targetRecord.element[targetRecord.index]; + if (!target) { + targetId = program.getValue(targetRecord.value[targetRecord.index]); + target = document.getElementById(targetId); + } + target.innerHTML = source.innerHTML; + break; + case `setContent`: + value = program.getValue(command.value); + targetRecord = program.getSymbolRecord(command.target); + target = targetRecord.element[targetRecord.index]; + if (!target) { + cssId = targetRecord.value[targetRecord.index].content; + if (!cssId) { + program.runtimeError(command.lino, + `Variable '${targetRecord.name}' has not been attached to a DOM element.`); + return 0; + } + target = document.getElementById(cssId); + } + targetRecord.element[targetRecord.index] = target; + switch (targetRecord.keyword) { + case `text`: + case `textarea`: + target.value = value; + break; + case `input`: + target.value = value; + break; + default: + target.innerHTML = value; + break; + } + break; + case `setSelect`: + // The source is assumed to be an array + sourceRecord = program.getSymbolRecord(command.source); + const sourceData = program.getValue(sourceRecord.value[sourceRecord.index]); + var itemArray = ``; + try { + itemArray = JSON.parse(sourceData); + } catch (err) { + program.runtimeError(command.lino, `Can't parse JSON`); + return 0; + } + // The target is assumed to be a SELECT + selectRecord = program.getSymbolRecord(command.select); + const select = selectRecord.element[selectRecord.index]; + select.options.length = 0; + // Get the name of the display field + const display = program.getValue(command.display); + // For each item, set the title and inner HTML + itemArray.forEach(function (item) { + const title = display ? program.decode(item[display]) : null; + const opt = document.createElement(`option`); + const innerHTML = title ? title : item; + opt.innerHTML = innerHTML; + const value = title ? JSON.stringify(item) : item; + opt.value = value; + select.appendChild(opt); + }); + if (display) { + select.selectedIndex = itemArray.indexOf(display); + } else { + select.selectedIndex = -1; + } + break; + case `setClass`: + symbol = program.getSymbolRecord(command.symbolName); + target = symbol.element[symbol.index]; + if (!target) { + targetId = program.getValue(symbol.value[symbol.index]); + target = document.getElementById(targetId); + } + program.getValue(command.value).split(` `).forEach(function(item) { + target.classList.remove(item); + target.classList.add(item); + }); + break; + case `setId`: + symbol = program.getSymbolRecord(command.symbolName); + target = symbol.element[symbol.index]; + if (!target) { + targetId = program.getValue(symbol.value[symbol.index]); + target = document.getElementById(targetId); + } + target.id = program.getValue(command.value); + break; + case `setText`: + symbol = program.getSymbolRecord(command.symbolName); + target = symbol.element[symbol.index]; + if (!target) { + targetId = program.getValue(symbol.value[symbol.index]); + target = document.getElementById(targetId); + } + value = program.getValue(command.value); + switch (symbol.keyword) { + case `button`: + case `span`: + case `label`: + case `legend`: + target.innerHTML = value; + break; + case `input`: + target.value = value; + break; + default: + break; + } + break; + case `setSize`: + symbol = program.getSymbolRecord(command.symbolName); + if (symbol.keyword === `input`) { + target = symbol.element[symbol.index]; + if (!target) { + targetId = program.getValue(symbol.value[symbol.index]); + target = document.getElementById(targetId); + } + target.size = program.getValue(command.value); + } else { + program.runtimeError(command.lino, `Inappropriate variable type '${symbol.name}'`); + } + break; + case `setAttribute`: + symbol = program.getSymbolRecord(command.symbolName); + target = symbol.element[symbol.index]; + if (!target) { + targetId = program.getValue(symbol.value[symbol.index]); + target = document.getElementById(targetId); + } + const attributeName = program.getValue(command.attributeName); + if (command.attributeValue.type === `boolean`) { + target.setAttribute(attributeName, command.attributeValue.content); + } else { + target.setAttribute(attributeName, program.getValue(command.attributeValue)); + } + break; + case `setAttributes`: + symbol = program.getSymbolRecord(command.symbolName); + target = symbol.element[symbol.index]; + if (!target) { + targetId = program.getValue(symbol.value[symbol.index]); + target = document.getElementById(targetId); + } + for (let n = target.attributes.length - 1; n >= 0; n--) { + target.removeAttribute(target.attributes[n].name); + } + let attributes = program.getValue(command.attributes); + let list = attributes.split(" "); + for (let n = 0; n < list.length; n++) { + let attribute = list[n]; + let p = attribute.indexOf(`=`); + if (p > 0) { + target.setAttribute(attribute.substr(0, p), attribute.substr(p + 1)); + } + else { + target.setAttribute(attribute, attribute); + } + } + break; + case `setStyle`: + case `setStyles`: + symbol = program.getSymbolRecord(command.symbolName); + target = symbol.element[symbol.index]; + if (!target) { + const symbolElement = symbol.value[symbol.index]; + if (!symbolElement.type) { + program.runtimeError(command.lino, `Variable '${symbol.name}' is not attached to a DOM element.`); + return 0; + } + targetId = program.getValue(symbolElement); + target = document.getElementById(targetId); + } + const styleValue = program.getValue(command.styleValue); + if (!symbol.value[symbol.index]) { + program.runtimeError(command.lino, `Variable '${symbol.name}' has not been assigned.`); + return 0; + } + switch (command.type) { + case `setStyle`: + target.style[command.styleName.content] = styleValue; + break; + case `setStyles`: + target.style.cssText = styleValue; + break; + } + break; + case `setHeadStyle`: + const headStyleName = program.getValue(command.styleName); + const headStyleValue = program.getValue(command.styleValue); + var style = document.createElement('style'); + style.innerHTML = `${headStyleName} ${headStyleValue}`; + for (let i = 0; i < document.head.childNodes.length; i++) { + let node = document.head.childNodes[i]; + if (node.tagName === `STYLE`) { + let data = node.innerHTML; + if (data.indexOf(`${headStyleName} `) === 0) { + document.head.removeChild(node); + break; + } + } + } + document.head.appendChild(style); + break; + case `setBodyStyle`: + const bodyStyleValue = program.getValue(command.styleValue); + switch (command.styleName.content) { + case `background`: + document.body.style.background = bodyStyleValue; + break; + default: + program.runtimeError(command.lino, + `Unsupported body attribute '${command.styleName.content}'`); + return 0; + } + break; + case `setTitle`: + document.title = program.getValue(command.value); + break; + case `setDefault`: + selectRecord = program.getSymbolRecord(command.name); + value = program.getValue(command.value); + const element = selectRecord.element[selectRecord.index]; + for (let n = 0; n < element.options.length; n++) { + if (element.options[n].value === value) { + element.selectedIndex = n; + break; + } + } + break; + default: + break; + } + return command.pc + 1; + } + }, + + SPAN: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `span`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + TABLE: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `table`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + TR: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `tr`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + TD: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `td`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + TEXTAREA: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `textarea`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + Trace: { + + compile: (compiler) => { + const lino = compiler.getLino(); + const variables = []; + if (compiler.nextIsSymbol()) { + while (compiler.isSymbol()) { + variables.push(compiler.getToken()); + compiler.next(); + } + let alignment = `horizontal`; + if (compiler.tokenIs(`horizontal`) || compiler.tokenIs(`vertical`)) { + alignment = compiler.getToken(); + compiler.next(); + } + compiler.addCommand({ + domain: `browser`, + keyword: `trace`, + variant: `setup`, + lino, + variables, + alignment + }); + return true; + } + compiler.addCommand({ + domain: `browser`, + keyword: `trace`, + variant: `run`, + lino + }); + return true; + }, + + run: (program) => { + const command = program[program.pc]; + switch (command.variant) { + case `setup`: + console.log(`Set up tracer`); + program.tracer = { + variables: command.variables, + alignment: command.alignment + }; + break; + case `run`: + console.log(`Run tracer`); + if (!program.tracer) { + program.tracer = { + variables: [], + alignment: `horizontal` + }; + } + if (!program.tracing) { + const tracer = document.getElementById(`easycoder-tracer`); + if (tracer) { + tracer.innerHTML = + `
` + + `` + + `
` + + `
`; + tracer.style.display = `none`; + } + program.tracing = true; + } + program.stop = false; + break; + } + return program.pc + 1; + } + }, + + UL: { + + compile: (compiler) => { + compiler.compileVariable(`browser`, `ul`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + Upload: { + + compile: (compiler) => { + const lino = compiler.getLino(); + if (compiler.nextIsSymbol()) { + const file = compiler.getToken(); + if (compiler.nextTokenIs(`to`)) { + const path = compiler.getNextValue(); + if (compiler.tokenIs(`with`)) { + if (compiler.nextIsSymbol()) { + const progress = compiler.getToken(); + if (compiler.nextTokenIs(`and`)) { + if (compiler.nextIsSymbol()) { + const status = compiler.getToken(); + compiler.next(); + compiler.addCommand({ + domain: `browser`, + keyword: `upload`, + lino, + file, + path, + progress, + status + }); + return true; + } + } + } + } + } + } + return false; + }, + + run: (program) => { + const command = program[program.pc]; + const fileSpec = program.getSymbolRecord(command.file); + const path = program.getValue(command.path); + const progressSpec = program.getSymbolRecord(command.progress); + const statusSpec = program.getSymbolRecord(command.status); + + const file = fileSpec.element[fileSpec.index]; + const progress = progressSpec.element[progressSpec.index]; + const status = statusSpec.element[statusSpec.index]; + + const setProgress = (value) => { + if (progress) { + progress.value = value; + } + }; + const setStatus = (value) => { + if (status) { + status.innerHTML = value; + } + }; + + const source = file.files[0]; + if (source) { + const formData = new FormData(); + formData.append(`source`, source); + formData.append(`path`, path); + const ajax = new XMLHttpRequest(); + ajax.upload.addEventListener(`progress`, function (event) { + const percent = Math.round((event.loaded / event.total) * 100); + setProgress(percent); + setStatus(`${Math.round(percent)}%...`); + }, false); + ajax.addEventListener(`load`, function (event) { + const response = event.target.responseText; + setProgress(0); + setStatus(``); + console.log(response); + }, false); + ajax.addEventListener(`error`, function () { + setStatus(`Upload failed`); + console.log(`Upload failed`); + }, false); + ajax.addEventListener(`abort`, function () { + setStatus(`Upload aborted`); + console.log(`Upload aborted`); + }, false); + ajax.onreadystatechange = function () { + if (this.readyState === 4) { + const command = program.ajaxCommand; + const status = this.status; + switch (status) { + case 200: + program.run(command.pc + 1); + break; + case 0: + break; + default: + try { + program.runtimeError(command.lino, `Error ${status}`); + } catch (err) { + program.reportError(err, program); + } + break; + } + } + }; + program.ajaxCommand = command; + const postpath = path.startsWith(`http`) ? path : `${window.location.origin}/${EasyCoder_Plugins.rest()}/${path}`; + ajax.open(`POST`, postpath); + ajax.send(formData); + } + return 0; + } + }, + + getHandler: (name) => { + switch (name) { + case `a`: + return EasyCoder_Browser.A; + case `alert`: + return EasyCoder_Browser.Alert; + case `attach`: + return EasyCoder_Browser.Attach; + case `audioclip`: + return EasyCoder_Browser.Audioclip; + case `blockquote`: + return EasyCoder_Browser.BLOCKQUOTE; + case `button`: + return EasyCoder_Browser.BUTTON; + case `canvas`: + return EasyCoder_Browser.CANVAS; + case `clear`: + return EasyCoder_Browser.Clear; + case `convert`: + return EasyCoder_Browser.Convert; + case `create`: + return EasyCoder_Browser.Create; + case `disable`: + return EasyCoder_Browser.Disable; + case `div`: + return EasyCoder_Browser.DIV; + case `enable`: + return EasyCoder_Browser.Enable; + case `fieldset`: + return EasyCoder_Browser.FIELDSET; + case `file`: + return EasyCoder_Browser.FILE; + case `focus`: + return EasyCoder_Browser.Focus; + case `form`: + return EasyCoder_Browser.FORM; + case `fullscreen`: + return EasyCoder_Browser.FullScreen; + case `get`: + return EasyCoder_Browser.Get; + case `h1`: + return EasyCoder_Browser.H1; + case `h2`: + return EasyCoder_Browser.H2; + case `h3`: + return EasyCoder_Browser.H3; + case `h4`: + return EasyCoder_Browser.H4; + case `h5`: + return EasyCoder_Browser.H5; + case `h6`: + return EasyCoder_Browser.H6; + case `highlight`: + return EasyCoder_Browser.Highlight; + case `history`: + return EasyCoder_Browser.History; + case `hr`: + return EasyCoder_Browser.HR; + case `image`: + return EasyCoder_Browser.IMAGE; + case `img`: + return EasyCoder_Browser.IMG; + case `input`: + return EasyCoder_Browser.INPUT; + case `label`: + return EasyCoder_Browser.LABEL; + case `legend`: + return EasyCoder_Browser.LEGEND; + case `li`: + return EasyCoder_Browser.LI; + case `location`: + return EasyCoder_Browser.Location; + case `mail`: + return EasyCoder_Browser.Mail; + case `on`: + return EasyCoder_Browser.On; + case `option`: + return EasyCoder_Browser.OPTION; + case `p`: + return EasyCoder_Browser.P; + case `play`: + return EasyCoder_Browser.Play; + case `pre`: + return EasyCoder_Browser.PRE; + case `progress`: + return EasyCoder_Browser.PROGRESS; + case `put`: + return EasyCoder_Browser.Put; + case `remove`: + return EasyCoder_Browser.Remove; + case `request`: + return EasyCoder_Browser.Request; + case `select`: + return EasyCoder_Browser.SELECT; + case `scroll`: + return EasyCoder_Browser.Scroll; + case `section`: + return EasyCoder_Browser.SECTION; + case `set`: + return EasyCoder_Browser.Set; + case `span`: + return EasyCoder_Browser.SPAN; + case `table`: + return EasyCoder_Browser.TABLE; + case `tr`: + return EasyCoder_Browser.TR; + case `td`: + return EasyCoder_Browser.TD; + case `textarea`: + return EasyCoder_Browser.TEXTAREA; + case `trace`: + return EasyCoder_Browser.Trace; + case `ul`: + return EasyCoder_Browser.UL; + case `upload`: + return EasyCoder_Browser.Upload; + default: + return null; + } + }, + + run: (program) => { + const command = program[program.pc]; + const handler = EasyCoder_Browser.getHandler(command.keyword); + if (!handler) { + program.runtimeError(command.lino, `Unknown keyword '${command.keyword}' in 'browser' package`); + } + return handler.run(program); + }, + + value: { + + compile: (compiler) => { + if (compiler.isSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + if (compiler.nextTokenIs(`exists`)) { + if (symbolRecord.extra === `dom`) { + compiler.next(); + return { + domain: `browser`, + type: `exists`, + value: symbolRecord.name + }; + } + return null; + } + switch (symbolRecord.keyword) { + case `file`: + case `input`: + case `select`: + case `textarea`: + return { + domain: `browser`, + type: symbolRecord.keyword, + value: symbolRecord.name + }; + } + return null; + } + + if (compiler.tokenIs(`the`)) { + compiler.next(); + } + let offset = false; + if (compiler.tokenIs(`offset`)) { + offset = true; + compiler.next(); + } + + let type = compiler.getToken(); + let text; + let attribute; + switch (type) { + case `mobile`: + case `portrait`: + case `landscape`: + case `br`: + case `location`: + case `key`: + case `hostname`: + compiler.next(); + return { + domain: `browser`, + type + }; + case `content`: + case `text`: + if (compiler.nextTokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const symbol = compiler.getSymbolRecord(); + compiler.next(); + return { + domain: `browser`, + type: `contentOf`, + symbol: symbol.name + }; + } + throw new Error(`'${compiler.getToken()}' is not a symbol`); + } + return null; + case `selected`: + let arg = compiler.nextToken(); + if ([`index`, `item`].includes(arg)) { + if ([`in`, `of`].includes(compiler.nextToken())) { + if (compiler.nextIsSymbol()) { + const symbol = compiler.getSymbolRecord(); + if ([`ul`, `ol`, `select`].includes(symbol.keyword)) { + compiler.next(); + return { + domain: `browser`, + type: `selected`, + symbol: symbol.name, + arg + }; + } + } + } + } + return null; + case `color`: + compiler.next(); + const value = compiler.getValue(); + return { + domain: `browser`, + type, + value + }; + case `attribute`: + attribute = compiler.getNextValue(); + if (compiler.tokenIs(`of`)) { + compiler.next(); + if (compiler.isSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + if (symbolRecord.extra === `dom`) { + compiler.next(); + return { + domain: `browser`, + type: `attributeOf`, + attribute, + symbol: symbolRecord.name + }; + } + } + } + return null; + case `style`: + const style = compiler.getNextValue(); + if (compiler.tokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + if (symbolRecord.extra === `dom`) { + compiler.next(); + return { + domain: `browser`, + type, + style, + target: symbolRecord.name + }; + } + } + } + return null; + case `confirm`: + text = compiler.getNextValue(); + return { + domain: `browser`, + type: `confirm`, + text + }; + case `prompt`: + text = compiler.getNextValue(); + let pre = null; + if (compiler.tokenIs(`with`)) { + pre = compiler.getNextValue(); + } + return { + domain: `browser`, + type: `prompt`, + text, + pre + }; + case `screen`: + attribute = compiler.nextToken(); + if ([`width`, `height`].includes(attribute)) { + compiler.next(); + return { + domain: `browser`, + type, + attribute + }; + } + break; + case `top`: + case `bottom`: + case `left`: + case `right`: + case `width`: + case `height`: + return EasyCoder_Browser.value.getCoord(compiler, type, offset); + case `scroll`: + if (compiler.nextTokenIs(`position`)) { + compiler.next(); + return { + domain: `browser`, + type: `scrollPosition` + }; + } + return null; + case `document`: + if (compiler.nextTokenIs(`path`)) { + compiler.next(); + return { + domain: `browser`, + type: `docPath` + }; + } + return null; + case `parent`: + switch (compiler.nextToken()) { + case `name`: + compiler.next(); + return { + domain: `browser`, + type: `varName` + }; + case `index`: + compiler.next(); + return { + domain: `browser`, + type: `varIndex` + }; + } + return null; + case `history`: + if (compiler.nextTokenIs(`state`)) { + compiler.next(); + return { + domain: `browser`, + type: `historyState` + }; + } + return null; + case `pick`: + case `drag`: + if (compiler.nextTokenIs(`position`)) { + compiler.next(); + return { + domain: `browser`, + type: `${type}Position` + }; + } + } + return null; + }, + + getCoord: (compiler, type, offset) => { + if (compiler.nextTokenIs(`of`)) { + if (compiler.nextTokenIs(`window`)) { + compiler.next(); + return { + domain: `browser`, + type, + symbol: `window`, + offset + }; + } + let symbolRecord = null; + if (compiler.isSymbol()) { + symbolRecord = compiler.getSymbolRecord(); + if (symbolRecord.extra === `dom`) { + compiler.next(); + return { + domain: `browser`, + type, + symbol: symbolRecord.name, + offset + }; + } + } + } + return null; + }, + + get: (program, value) => { + let symbolRecord; + let element; + let target; + let content; + switch (value.type) { + case `file`: + case `input`: + case `select`: + case `textarea`: + symbolRecord = program.getSymbolRecord(value.value); + target = symbolRecord.element[symbolRecord.index]; + return { + type: `constant`, + numeric: false, + content: target.value + }; + case `exists`: + symbolRecord = program.getSymbolRecord(value.value); + return { + domain: `browser`, + type: `boolean`, + content: typeof symbolRecord.element[symbolRecord.index] !== `undefined` + }; + case `mobile`: + return { + domain: `browser`, + type: `boolean`, + content: (typeof window.orientation !== `undefined`) || (navigator.userAgent.indexOf(`IEMobile`) !== -1) + }; + case `portrait`: + return { + domain: `browser`, + type: `boolean`, + content: document.documentElement.clientWidth < document.documentElement.clientHeight + }; + case `landscape`: + return { + domain: `browser`, + type: `boolean`, + content: document.documentElement.clientWidth >= document.documentElement.clientHeight + }; + case `br`: + return { + type: `constant`, + numeric: false, + content: decodeURIComponent(`%3Cbr%20%2F%3E`) + }; + case `attributeOf`: + symbolRecord = program.getSymbolRecord(value.symbol); + const attribute = program.getValue(value.attribute); + target = symbolRecord.element[symbolRecord.index]; + if (attribute.indexOf(`data-`) === 0) { + return program.getSimpleValue(target.dataset[attribute.substr(5)]); + } + return program.getSimpleValue(target[attribute]); + case `style`: + symbolRecord = program.getSymbolRecord(value.target); + const style = program.getValue(value.style); + target = symbolRecord.element[symbolRecord.index]; + return program.getSimpleValue(target.style[style]); + case `confirm`: + return { + type: `boolean`, + content: window.confirm(program.getValue(value.text)) + }; + case `prompt`: + const text = program.getValue(value.text); + const pre = program.getValue(value.pre); + return { + type: `constant`, + numeric: false, + content: pre ? window.prompt(text, pre) : window.prompt(text) + }; + case `contentOf`: + symbolRecord = program.getSymbolRecord(value.symbol); + target = symbolRecord.element[symbolRecord.index]; + switch (symbolRecord.keyword) { + case `input`: + case `textarea`: + content = target.value; + break; + case `pre`: + content = target.innerHTML; + break; + default: + content = target.innerHTML.split(`\n`).join(``); + break; + } + return { + type: `constant`, + numeric: false, + content + }; + case `selected`: + symbolRecord = program.getSymbolRecord(value.symbol); + target = symbolRecord.element[symbolRecord.index]; + let selectedIndex = target.selectedIndex; + let selectedText = selectedIndex >= 0 ? target.options[selectedIndex].text : ``; + content = (value.arg === `index`) ? selectedIndex : selectedText; + return { + type: `constant`, + numeric: false, + content + }; + case `top`: + if (value.symbol == `window`) { + return { + type: `constant`, + numeric: true, + content: window.screenY + }; + } + symbolRecord = program.getSymbolRecord(value.symbol); + element = symbolRecord.element[symbolRecord.index]; + content = Math.round(value.offset ? element.offsetTop : element.getBoundingClientRect().top); + return { + type: `constant`, + numeric: true, + content + }; + case `bottom`: + if (value.symbol == `window`) { + return { + type: `constant`, + numeric: true, + content: window.screenY + window.innerHeight + }; + } + symbolRecord = program.getSymbolRecord(value.symbol); + content = Math.round(symbolRecord.element[symbolRecord.index].getBoundingClientRect().bottom); + return { + type: `constant`, + numeric: true, + content + }; + case `left`: + if (value.symbol == `window`) { + return { + type: `constant`, + numeric: true, + content: window.screenLeft + }; + } + symbolRecord = program.getSymbolRecord(value.symbol); + element = symbolRecord.element[symbolRecord.index]; + content = Math.round(value.offset ? element.offsetLeft : element.getBoundingClientRect().left); + return { + type: `constant`, + numeric: true, + content + }; + case `right`: + if (value.symbol == `window`) { + return { + type: `constant`, + numeric: true, + content: window.screenX + window.innerWidth + }; + } + symbolRecord = program.getSymbolRecord(value.symbol); + content = Math.round(symbolRecord.element[symbolRecord.index].getBoundingClientRect().right); + return { + type: `constant`, + numeric: true, + content + }; + case `width`: + if (value.symbol == `window`) { + return { + type: `constant`, + numeric: true, + content: window.innerWidth + }; + } + symbolRecord = program.getSymbolRecord(value.symbol); + content = Math.round(symbolRecord.element[symbolRecord.index].getBoundingClientRect().width); + return { + type: `constant`, + numeric: true, + content + }; + case `height`: + if (value.symbol == `window`) { + return { + type: `constant`, + numeric: true, + content: window.innerHeight + }; + } + symbolRecord = program.getSymbolRecord(value.symbol); + content = Math.round(symbolRecord.element[symbolRecord.index].getBoundingClientRect().height); + return { + type: `constant`, + numeric: true, + content + }; + case `color`: + const styleValue = program.value.evaluate(program, value.value).content; + const hex = styleValue.toString(16).padStart(6, `0`); + return { + type: `constant`, + numeric: false, + content: `#${hex}` + }; + case `docPath`: + return { + type: `constant`, + numeric: false, + content: program.docPath + }; + case `location`: + return { + type: `constant`, + numeric: false, + content: window.location.href + }; + case `historyState`: + return { + type: `constant`, + numeric: false, + content: window.history.state + }; + case `scrollPosition`: + return { + type: `constant`, + numeric: true, + content: scrollPosition + }; + case `varName`: + return { + type: `constant`, + numeric: false, + content: program.varName + }; + case `varIndex`: + return { + type: `constant`, + numeric: true, + content: program.varIndex + }; + case `key`: + return { + type: `constant`, + numeric: false, + content: program.key + }; + case `hostname`: + return { + type: `constant`, + numeric: false, + content: location.hostname + }; + case `screen`: + return { + type: `constant`, + numeric: true, + content: screen[value.attribute] + }; + case `pickPosition`: + return { + type: `constant`, + numeric: false, + content: JSON.stringify({ + "x": document.pickX, + "y": document.pickY + }) + }; + case `dragPosition`: + return { + type: `constant`, + numeric: false, + content: JSON.stringify({ + "x": document.dragX, + "y": document.dragY + }) + }; + } + } + }, + + condition: { + + compile: (compiler) => { + if (compiler.tokenIs(`confirm`)) { + const value = compiler.getNextValue(); + return { + domain: `browser`, + type: `confirm`, + value + }; + } else if (compiler.tokenIs(`element`)) { + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + if (symbolRecord.extra === `dom`) { + const token = compiler.nextToken(); + if (token === `has`) { + if (compiler.nextTokenIs(`the`)) { + compiler.next(); + } + if (compiler.tokenIs(`focus`)) { + compiler.next(); + return { + domain: `browser`, + type: `focus`, + element: symbolRecord.name + }; + } + } else if (token === `contains`) { + const position = compiler.getNextValue(); + return { + domain: `browser`, + type: `contains`, + element: symbolRecord.name, + position + }; + } + } + } + } + return null; + }, + + test: (program, condition) => { + switch (condition.type) { + case `confirm`: + return confirm(program.getValue(condition.value)); + case `focus`: + const focusRecord = program.getSymbolRecord(condition.element); + return focusRecord.element[focusRecord.index] === document.activeElement; + case `contains`: + const containsRecord = program.getSymbolRecord(condition.element); + const element = containsRecord.element[containsRecord.index]; + const bounds = element.getBoundingClientRect(); + const left = Math.round(bounds.left); + const right = Math.round(bounds.right); + const top = Math.round(bounds.top); + const bottom = Math.round(bounds.bottom); + const position = JSON.parse(program.getValue(condition.position)); + const x = position.x; + const y = position.y; + if (x >= left && x <= right && y >= top && y <= bottom) { + return true; + } + return false; + } + } + }, + + setStyles: (id, styleString) => { + const element = document.getElementById(id); + const styles = styleString.split(`;`); + for (const item of styles) { + const style = item.split(`:`); + element.setAttribute(style[0], style[1]); + } + } +}; + +let scrollPosition = 0; + +window.addEventListener(`scroll`, function () { + scrollPosition = this.scrollY; +}); + +window.onpopstate = function (event) { + window.EasyCoder.timestamp = Date.now(); + const state = JSON.parse(event.state); + if (state && state.script) { + const program = window.EasyCoder.scripts[state.script]; + if (program) { + if (program.onBrowserBack) { + program.run(program.onBrowserBack); + } + } else { + console.log(`No script property in window state object`); + } + } +}; +const EasyCoder_Json = { + + name: `EasyCoder_JSON`, + + Json: { + + compile: (compiler) => { + const lino = compiler.getLino(); + const request = compiler.nextToken(); + let item; + switch (request) { + case `set`: + compiler.next(); + if (compiler.isSymbol()) { + const targetRecord = compiler.getSymbolRecord(); + if (targetRecord.keyword === `variable`) { + if (compiler.nextTokenIs(`to`)) { + const type = compiler.nextToken(); + if (`["array","object"]`.includes(type)) { + compiler.next(); + compiler.addCommand({ + domain: `json`, + keyword: `json`, + lino, + request: `setVariable`, + target: targetRecord.name, + type + }); + return true; + } + } + } else if (targetRecord.keyword === `select`) { + if (compiler.nextTokenIs(`from`)) { + compiler.next(); + if (compiler.isSymbol()) { + const sourceRecord = compiler.getSymbolRecord(); + if (sourceRecord.keyword === `variable`) { + var display = null; + if (compiler.nextTokenIs(`as`)) { + display = compiler.getNextValue(); + } + compiler.addCommand({ + domain: `json`, + keyword: `json`, + lino, + request: `setList`, + target: targetRecord.name, + source: sourceRecord.name, + display + }); + return true; + } + } + } + } + break; + } + break; + case `sort`: + case `shuffle`: + case `format`: + if (compiler.nextIsSymbol()) { + const targetRecord = compiler.getSymbolRecord(); + if (targetRecord.keyword === `variable`) { + compiler.next(); + compiler.addCommand({ + domain: `json`, + keyword: `json`, + lino, + request, + target: targetRecord.name + }); + return true; + } + } + break; + case `parse`: + if (compiler.nextTokenIs(`url`)) { + const source = compiler.getNextValue(); + if (compiler.tokenIs(`as`)) { + if (compiler.nextIsSymbol()) { + const targetRecord = compiler.getSymbolRecord(); + if (targetRecord.keyword === `variable`) { + compiler.next(); + compiler.addCommand({ + domain: `json`, + keyword: `json`, + lino, + request, + source, + target: targetRecord.name + }); + return true; + } + } + } + } + break; + case `delete`: + const what = compiler.nextToken(); + if ([`property`, `element`].includes(what)) { + const value = compiler.getNextValue(); + if ([`from`, `of`].includes(compiler.getToken())) { + if (compiler.nextIsSymbol()) { + const targetRecord = compiler.getSymbolRecord(); + if (targetRecord.keyword === `variable`) { + compiler.next(); + compiler.addCommand({ + domain: `json`, + keyword: `json`, + lino, + request, + what, + value, + target: targetRecord.name + }); + return true; + } + } + } + } + break; + case `rename`: + const oldName = compiler.getNextValue(); + if (compiler.tokenIs(`to`)) { + const newName = compiler.getNextValue(); + if (compiler.tokenIs(`in`)) { + if (compiler.nextIsSymbol()) { + const targetRecord = compiler.getSymbolRecord(); + if (targetRecord.keyword === `variable`) { + compiler.next(); + compiler.addCommand({ + domain: `json`, + keyword: `json`, + lino, + request, + oldName, + newName, + target: targetRecord.name + }); + return true; + } + } + } + } + break; + case `add`: + item = compiler.getNextValue(); + if (compiler.tokenIs(`to`)) { + if (compiler.nextIsSymbol()) { + const targetRecord = compiler.getSymbolRecord(); + if (targetRecord.keyword === `variable`) { + compiler.next(); + compiler.addCommand({ + domain: `json`, + keyword: `json`, + lino, + request, + item, + target: targetRecord.name + }); + return true; + } + } + } + break; + case `split`: + item = compiler.getNextValue(); + let on = `\n`; + if (compiler.tokenIs(`on`)) { + on = compiler.getNextValue(); + } + if ([`giving`, `into`].includes(compiler.getToken())) { + if (compiler.nextIsSymbol()) { + const targetRecord = compiler.getSymbolRecord(); + if (targetRecord.keyword === `variable`) { + compiler.next(); + compiler.addCommand({ + domain: `json`, + keyword: `json`, + lino, + request, + item, + on, + target: targetRecord.name + }); + return true; + } + } + } + break; + case `replace`: + if (compiler.nextTokenIs(`element`)) { + const index = compiler.getNextValue(); + if (compiler.tokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const targetRecord = compiler.getSymbolRecord(); + if (targetRecord.keyword === `variable`) { + if ([`by`, `with`].includes(compiler.nextToken())) { + const value = compiler.getNextValue(); + compiler.addCommand({ + domain: `json`, + keyword: `json`, + lino, + request, + target: targetRecord.name, + index, + value + }); + return true; + } + } + } + } + } + break; + } + compiler.addWarning(`Unrecognised json command syntax`); + return false; + }, + + run: (program) => { + const command = program[program.pc]; + let sourceRecord; + let targetRecord; + let record; + let content; + let array; + switch (command.request) { + case `setVariable`: + targetRecord = program.getSymbolRecord(command.target); + content = (command.type === `array`) ? `[]` : `{}`; + targetRecord.value[targetRecord.index] = { + type: `constant`, + numeric: false, + content + }; + break; + case `setList`: + // The source is assumed to be a JSON array + sourceRecord = program.getSymbolRecord(command.source); + const sourceData = program.getValue(sourceRecord.value[sourceRecord.index]); + var itemArray = ``; + try { + itemArray = JSON.parse(sourceData); + } catch (err) { + program.runtimeError(command.lino, `Can't parse JSON`); + return 0; + } + // The target is assumed to be a SELECT + targetRecord = program.getSymbolRecord(command.target); + const target = targetRecord.element[targetRecord.index]; + target.options.length = 0; + // Get the name of the display field + const display = program.getValue(command.display); + // For each item, set the title and inner HTML + itemArray.forEach(function (item) { + const title = display ? program.decode(item[display]) : null; + const opt = document.createElement(`option`); + const innerHTML = title ? title : item; + opt.innerHTML = innerHTML; + const value = title ? JSON.stringify(item) : item; + opt.value = value; + target.appendChild(opt); + }); + target.selectedIndex = -1; + break; + case `sort`: + targetRecord = program.getSymbolRecord(command.target); + const list = program.getValue(targetRecord.value[targetRecord.index]); + content = list ? JSON.stringify(JSON.parse(list).sort()) : null; + targetRecord.value[targetRecord.index] = { + type: `constant`, + numeric: false, + content + }; + break; + case `shuffle`: + targetRecord = program.getSymbolRecord(command.target); + array = JSON.parse(program.getValue(targetRecord.value[targetRecord.index])); + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + targetRecord.value[targetRecord.index] = { + type: `constant`, + numeric: false, + content: JSON.stringify(array) + }; + break; + case `format`: + targetRecord = program.getSymbolRecord(command.target); + const val = JSON.parse(program.getValue(targetRecord.value[targetRecord.index])); + targetRecord.value[targetRecord.index] = { + type: `constant`, + numeric: false, + content: JSON.stringify(val, null, 2) + }; + break; + case `parse`: + var source = program.getValue(command.source); + targetRecord = program.getSymbolRecord(command.target); + content = { + url: source + }; + var n = source.indexOf(`://`); + if (n >= 0) { + n += 3; + content.protocol = source.substr(0, n); + source = source.substr(n); + } + n = source.indexOf(`?`); + if (n > 0) { + content.domain = source.substr(0, n); + content.arg = source.substr(n + 1); + } else { + content.domain = source; + } + if (content.domain.endsWith(`/`)) { + content.domain = content.domain.substr(0, content.domain.length - 1); + } + n = content.domain.indexOf(`/`); + if (n > 0) { + content.path = content.domain.substr(n + 1); + content.domain = content.domain.substr(0, n); + } + else { + content.path = ``; + } + targetRecord.value[targetRecord.index] = { + type: `constant`, + numeric: false, + content: JSON.stringify(content, null, 2) + }; + break; + case `delete`: + switch (command.what) { + case `property`: + const name = program.getValue(command.value); + targetRecord = program.getSymbolRecord(command.target); + record = JSON.parse(targetRecord.value[targetRecord.index].content); + delete record[name]; + targetRecord.value[targetRecord.index].content = JSON.stringify(record); + break; + case `element`: + const element = program.getValue(command.value); + targetRecord = program.getSymbolRecord(command.target); + record = JSON.parse(targetRecord.value[targetRecord.index].content); + record.splice(element, 1); + targetRecord.value[targetRecord.index].content = JSON.stringify(record); + break; + } + break; + case `rename`: + const oldName = program.getValue(command.oldName); + const newName = program.getValue(command.newName); + targetRecord = program.getSymbolRecord(command.target); + record = JSON.parse(targetRecord.value[targetRecord.index].content); + content = record[oldName]; + delete record[oldName]; + record[newName] = content; + targetRecord.value[targetRecord.index].content = JSON.stringify(record); + break; + case `add`: + content = program.getValue(command.item); + targetRecord = program.getSymbolRecord(command.target); + const existing = targetRecord.value[targetRecord.index].content; + record = existing ? JSON.parse(existing) : []; + record.push((`[`, `{`).includes(content[0]) ? JSON.parse(content) :content); + targetRecord.value[targetRecord.index] = { + type: `constant`, + numeric: false, + content: JSON.stringify(record) + }; + break; + case `split`: + content = program.getValue(command.item); + const on = program.getValue(command.on); + targetRecord = program.getSymbolRecord(command.target); + targetRecord.value[targetRecord.index] = { + type: `constant`, + numeric: false, + content: JSON.stringify(content.split(on)) + }; + break; + case `replace`: + targetRecord = program.getSymbolRecord(command.target); + const index = program.getValue(command.index); + const value = program.getValue(command.value); + const current = targetRecord.value[targetRecord.index].content; + record = current ? JSON.parse(current) : []; + if (index > record.length - 1) { + program.runtimeError(command.lino, `Index out of range`); + } + record[index] = value; + targetRecord.value[targetRecord.index].content = JSON.stringify(record); + break; + } + return command.pc + 1; + } + }, + + getHandler: (name) => { + switch (name) { + case `json`: + return EasyCoder_Json.Json; + default: + return null; + } + }, + + run: (program) => { + const command = program[program.pc]; + const handler = EasyCoder_Json.getHandler(command.keyword); + if (!handler) { + program.runtimeError(command.lino, `Unknown keyword '${command.keyword}' in 'json' package`); + } + return handler.run(program); + }, + + value: { + + compile: (compiler) => { + if (compiler.tokenIs(`the`)) { + compiler.next(); + } + if (compiler.tokenIs(`json`)) { + const type = compiler.nextToken(); + if ([`size`, `count`, `keys`].includes(type)) { + compiler.skip(`of`); + if (compiler.isSymbol()) { + const target = compiler.getSymbolRecord(); + compiler.next(); + if (target.isVHolder) { + return { + domain: `json`, + type, + name: target.name + }; + } + } + } else if (type === `index`) { + if (compiler.nextTokenIs(`of`)) { + const item = compiler.getNextValue(); + if (compiler.tokenIs(`in`)) { + const list = compiler.getNextValue(); + return { + domain: `json`, + type, + item, + list + }; + } + } + } + } + return null; + }, + + get: (program, value) => { + let symbolRecord; + let data; + let content; + switch (value.type) { + case `size`: + case `count`: + symbolRecord = program.getSymbolRecord(value.name); + data = program.getValue(symbolRecord.value[symbolRecord.index]); + let array; + try { + array = JSON.parse(data); + } catch (err) { + array = []; + } + return { + type: `constant`, + numeric: true, + content: array ? array.length : 0 + }; + case `keys`: + symbolRecord = program.getSymbolRecord(value.name); + data = program.getValue(symbolRecord.value[symbolRecord.index]); + content = data ? JSON.stringify(Object.keys(JSON.parse(data)).sort()) : `[]`; + return { + type: `constant`, + numeric: false, + content + }; + case `index`: + const item = program.getValue(value.item); + const list = JSON.parse(program.getValue(value.list)); + content = list.findIndex(function (value) { + return value === item; + }); + return { + type: `constant`, + numeric: true, + content + }; + } + } + }, + + condition: { + + compile: () => {}, + + test: () => {} + } +}; +const EasyCoder_Rest = { + + name: `EasyCoder_Rest`, + + Rest: { + + compile: (compiler) => { + const lino = compiler.getLino(); + const request = compiler.nextToken(); + switch (request) { + case `get`: + if (compiler.nextIsSymbol(true)) { + const targetRecord = compiler.getSymbolRecord(); + if (targetRecord.keyword === `variable`) { + if (compiler.nextTokenIs(`from`)) { + const url = compiler.getNextValue(); + let fixup = compiler.getPc(); + compiler.addCommand({ + domain: `rest`, + keyword: `rest`, + lino, + request: `get`, + target: targetRecord.name, + url, + onError: null + }); + if (compiler.tokenIs(`or`)) { + compiler.next(); + compiler.getCommandAt(fixup).onError = compiler.getPc() + 1; + compiler.completeHandler(); + } + return true; + } + } + } + break; + case `post`: + let value = null; + if (compiler.nextTokenIs(`to`)) { + compiler.next(); + } else { + value = compiler.getValue(); + if (compiler.tokenIs(`to`)) { + compiler.next(); + } else { + break; + } + } + const url = compiler.getValue(); + if (!url) { + throw new Error(command.lino, `No URL present`); + } + let target = null; + if (compiler.tokenIs(`giving`)) { + if (compiler.nextIsSymbol()) { + const targetRecord = compiler.getSymbolRecord(); + if (targetRecord.isVHolder) { + target = targetRecord.name; + compiler.next(); + } else { + throw new Error(`'${targetRecord.name}' cannot hold a value`); + } + } + } + compiler.addCommand({ + domain: `rest`, + keyword: `rest`, + lino, + request: `post`, + value, + url, + target, + onError: compiler.getPc() + 2 + }); + onError = null; + if (compiler.tokenIs(`or`)) { + compiler.next(); + // onError = compiler.getPc() + 1; + compiler.completeHandler(); + } + return true; + } + return false; + }, + + createCORSRequest: (method, url) => { + var xhr = new XMLHttpRequest(); + if (`withCredentials` in xhr) { + + // Check if the XMLHttpRequest object has a "withCredentials" property. + // "withCredentials" only exists on XMLHTTPRequest2 objects. + xhr.open(method, url, true); + + } else if (typeof XDomainRequest != `undefined`) { + + // Otherwise, check if XDomainRequest. + // XDomainRequest only exists in IE, and is IE's way of making CORS requests. + xhr = new XDomainRequest(); + xhr.open(method, url); + + } else { + + // Otherwise, CORS is not supported by the browser. + xhr = null; + + } + return xhr; + }, + + run: (program) => { + const command = program[program.pc]; + const url = program.getValue(command.url); + const rest = EasyCoder_Plugins.rest(); + const path = url.startsWith(`http`) ? url + : url[0] === `/` ? url.substr(1) + : `${window.location.origin}${rest ? `/${rest}` : ``}/${url}`; + + const request = EasyCoder_Rest.Rest.createCORSRequest(command.request, path); + if (!request) { + program.runtimeError(command.lino, `CORS not supported`); + return; + } + request.script = program.script; + request.pc = program.pc; + + request.onload = function () { + let s = request.script; + let p = EasyCoder.scripts[s]; + let pc = request.pc; + let c = p[pc]; + if (200 <= request.status && request.status < 400) { + var content = request.responseText.trim(); + if (c.target) { + const targetRecord = program.getSymbolRecord(command.target); + targetRecord.value[targetRecord.index] = { + type: `constant`, + numeric: false, + content + }; + targetRecord.used = true; + } + p.run(c.pc + 1); + } else { + const error = `${request.status} ${request.statusText}`; + if (c.onError) { + p.errorMessage = `Exception trapped: ${error}`; + p.run(c.onError); + } else { + p.runtimeError(c.lino, `Error: ${error}`); + } + } + }; + + request.onerror = function () { + if (command.onError) { + program.errorMessage = this.responseText; + program.run(command.onError); + } else { + const error = this.responseText; + program.runtimeError(command.lino, error); + } + }; + + switch (command.request) { + case `get`: + // console.log(`GET from ${path}`); + request.send(); + break; + case `post`: + const value = program.getValue(command.value); + console.log(`POST to ${path}`); + //console.log(`value=${value}`); + request.setRequestHeader(`Content-type`, `application/json; charset=UTF-8`); + request.send(value); + break; + } + return 0; + } + }, + + getHandler: (name) => { + switch (name) { + case `rest`: + return EasyCoder_Rest.Rest; + default: + return null; + } + }, + + run: (program) => { + const command = program[program.pc]; + const handler = EasyCoder_Rest.getHandler(command.keyword); + if (!handler) { + program.runtimeError(command.lino, `Unknown keyword '${command.keyword}' in 'rest' package`); + } + return handler.run(program); + }, + + value: { + + compile: () => { + return null; + }, + + get: () => { + return null; + } + }, + + condition: { + + compile: () => {}, + + test: () => {} + } +}; + +const EasyCoder_GMap = { + + name: `EasyCoder_GMap`, + + Create: { + + compile: compiler => { + const lino = compiler.getLino(); + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + const type = symbolRecord.keyword; + switch (type) { + case `gmap`: + if (compiler.nextTokenIs(`in`)) { + if (compiler.nextIsSymbol()) { + const parentRecord = compiler.getSymbolRecord(); + if (parentRecord.keyword === `div`) { + compiler.next(); + compiler.addCommand({ + domain: `gmap`, + keyword: `create`, + type, + lino, + name: symbolRecord.name, + parent: parentRecord.name + }); + return true; + } + } + } + return false; + case `marker`: + if (compiler.nextTokenIs(`in`)) { + if (compiler.nextIsSymbol()) { + const parentRecord = compiler.getSymbolRecord(); + if (parentRecord.keyword === `gmap`) { + compiler.next(); + compiler.addCommand({ + domain: `gmap`, + keyword: `create`, + type, + lino, + name: symbolRecord.name, + map: parentRecord.name + }); + return true; + } + } + } + return false; + } + } + return false; + }, + + run: program => { + const command = program[program.pc]; + const symbolRecord = program.getSymbolRecord(command.name); + switch (command.type) { + case `gmap`: + symbolRecord.parent = program.getSymbolRecord(command.parent); + symbolRecord.markers = []; + break; + case `marker`: + const mapRecord = program.getSymbolRecord(command.map); + const element = new google.maps.Marker({ + map: mapRecord.map + }); + symbolRecord.element[symbolRecord.index] = element; + mapRecord.markers.push(element); + element.addListener(`click`, function () { + program.run(symbolRecord.onClick); + }); + break; + } + return command.pc + 1; + } + }, + + GMap: { + + compile: compiler => { + compiler.compileVariable(`gmap`, `gmap`); + return true; + }, + + run: program => { + return program[program.pc].pc + 1; + } + }, + + On: { + + compile: compiler => { + const lino = compiler.getLino(); + const action = compiler.nextToken(); + if ([`click`, `move`, `type`, `zoom`].includes(action)) { + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + if (symbolRecord.keyword === `gmap` || (symbolRecord.keyword === `marker` && action === `click`)) { + compiler.next(); + compiler.addCommand({ + domain: `gmap`, + keyword: `on`, + lino, + action, + name: symbolRecord.name + }); + return compiler.completeHandler(); + } + } + } + return false; + }, + + run: (program) => { + const command = program[program.pc]; + const symbolRecord = program.getSymbolRecord(command.name); + switch (command.action) { + case `click`: + if (symbolRecord.keyword === `marker`) { + symbolRecord.element.forEach(function (marker, index) { + marker.targetRecord = symbolRecord; + marker.targetIndex = index; + marker.targetPc = command.pc + 2; + marker.addListener(`click`, function () { + if (program.length > 0) { + marker.targetRecord.index = marker.targetIndex; + setTimeout(function () { + EasyCoder.timestamp = Date.now(); + program.run(marker.targetPc); + }, 1); + } + return false; + }); + }); + } else { + symbolRecord.onClick = command.pc + 2; + } + break; + case `move`: + symbolRecord.onMove = command.pc + 2; + break; + case `type`: + symbolRecord.onType = command.pc + 2; + break; + case `zoom`: + symbolRecord.onZoom = command.pc + 2; + break; + default: + program.runtimeError(command.lino, `Unknown action '${command.action}'`); + return 0; + } + return command.pc + 1; + } + }, + + Marker: { + + compile: compiler => { + compiler.compileVariable(`gmap`, `marker`); + return true; + }, + + run: program => { + return program[program.pc].pc + 1; + } + }, + + Remove: { + + compile: compiler => { + const lino = compiler.getLino(); + if (compiler.nextTokenIs(`markers`)) { + if (compiler.nextTokenIs(`from`)) { + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + if (symbolRecord.keyword === `gmap`) { + compiler.next(); + compiler.addCommand({ + domain: `gmap`, + keyword: `remove`, + lino, + name: symbolRecord.name + }); + return true; + } + } + } + } + return false; + }, + + run: program => { + const command = program[program.pc]; + const mapRecord = program.getSymbolRecord(command.name); + for (const marker of mapRecord.markers) { + marker.setMap(null); + } + mapRecord.markers = []; + return command.pc + 1; + } + }, + + Set: { + + compile: compiler => { + const lino = compiler.getLino(); + compiler.skip(`the`); + const attribute = compiler.getToken(); + if ([`key`, `latitude`, `longitude`, `type`, `zoom`].includes(attribute)) { + if (compiler.nextTokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + if (symbolRecord.keyword === `gmap`) { + if (compiler.nextTokenIs(`to`)) { + const value = compiler.getNextValue(); + compiler.addCommand({ + domain: `gmap`, + keyword: `set`, + lino, + name: symbolRecord.name, + attribute, + value + }); + return true; + } + } + } + } + } else if ([`label`, `title`, `position`, `color`].includes(attribute)) { + if (compiler.nextTokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + if (symbolRecord.keyword === `marker`) { + if (compiler.nextTokenIs(`to`)) { + const value = compiler.getNextValue(); + compiler.addCommand({ + domain: `gmap`, + keyword: `set`, + lino, + name: symbolRecord.name, + attribute, + value + }); + return true; + } + } + } + } + } + return false; + }, + + run: program => { + function pinSymbol(color) { + return { + path: `M 0,0 C -2,-20 -10,-22 -10,-30 A 10,10 0 1,1 10,-30 C 10,-22 2,-20 0,0 z`, + fillColor: color, + fillOpacity: 1, + strokeColor: `#000`, + strokeWeight: 2, + scale: 1, + labelOrigin: new google.maps.Point(0, -28) + }; + } + const command = program[program.pc]; + const symbolRecord = program.getSymbolRecord(command.name); + if ([`key`, `latitude`, `longitude`, `type`, `zoom`].includes(command.attribute)) { + symbolRecord[command.attribute] = program.getValue(command.value); + } else if (command.attribute === `label`) { + symbolRecord.label = program.getValue(command.value); + const marker = symbolRecord.element[symbolRecord.index]; + marker.setLabel(symbolRecord.label); + } else if (command.attribute === `title`) { + symbolRecord.title = program.getValue(command.value); + const marker = symbolRecord.element[symbolRecord.index]; + marker.setTitle(symbolRecord.title); + } else if (command.attribute === `color`) { + symbolRecord.color = program.getValue(command.value); + const marker = symbolRecord.element[symbolRecord.index]; + marker.setIcon(pinSymbol(symbolRecord.color)); + } else if (command.attribute === `position`) { + const value = JSON.parse(program.getValue(command.value)); + symbolRecord.latitude = value.latitude; + symbolRecord.longitude = value.longitude; + const lat = parseFloat(value.latitude); + const lng = parseFloat(value.longitude); + symbolRecord.element[symbolRecord.index].setPosition(new google.maps.LatLng(lat, lng)); + } + return command.pc + 1; + } + }, + + Show: { + + compile: compiler => { + const lino = compiler.getLino(); + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + const type = symbolRecord.keyword; + if (type === `gmap`) { + compiler.next(); + compiler.addCommand({ + domain: `gmap`, + keyword: `show`, + lino, + name: symbolRecord.name + }); + return true; + } + } + return false; + }, + + run: program => { + const command = program[program.pc]; + const mapRecord = program.getSymbolRecord(command.name); + if (mapRecord.keyword !== `gmap`) { + return 0; + } + const parentElement = mapRecord.parent.element[mapRecord.parent.index]; + if (typeof EasyCoder_GMap.loaded === `undefined`) { + const script = document.createElement(`script`); + script.src = `https://maps.googleapis.com/maps/api/js?key=${mapRecord.key}`; + script.async = true; + script.defer = true; + script.onload = function () { + EasyCoder_GMap.setupMap(parentElement, mapRecord, program); + program.run(command.pc + 1); + EasyCoder_GMap.loaded = true; + }; + parentElement.insertBefore(script, null); + return 0; + } + EasyCoder_GMap.setupMap(parentElement, mapRecord, program); + return command.pc + 1; + } + }, + + setupMap: (parentElement, mapRecord, program) => { + const lat = parseFloat(mapRecord.latitude); + const lng = parseFloat(mapRecord.longitude); + const zoom = parseFloat(mapRecord.zoom); + mapRecord.map = new google.maps.Map(parentElement, { + center: { + lat, + lng + }, + zoom, + gestureHandling: `greedy` + }); + mapRecord.map.markers = []; + if (mapRecord.type === `hybrid`) { + mapRecord.map.setMapTypeId(google.maps.MapTypeId.SATELLITE); + } + mapRecord.map.addListener(`center_changed`, function () { + program.run(mapRecord.onMove); + }); + mapRecord.map.addListener(`zoom_changed`, function () { + program.run(mapRecord.onZoom); + }); + mapRecord.map.addListener(`maptypeid_changed`, function () { + program.run(mapRecord.onType); + }); + mapRecord.map.addListener(`click`, function (event) { + mapRecord.clickPosition = { + latitude: event.latLng.lat().toString(), + longitude: event.latLng.lng().toString() + }; + program.run(mapRecord.onClick); + }); + }, + + Update: { + + compile: compiler => { + const lino = compiler.getLino(); + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + if (symbolRecord.keyword === `gmap`) { + compiler.next(); + compiler.addCommand({ + domain: `gmap`, + keyword: `update`, + lino, + name: symbolRecord.name + }); + return true; + } + } + return false; + }, + + run: program => { + const command = program[program.pc]; + const mapRecord = program.getSymbolRecord(command.name); + mapRecord.map.setCenter(new google.maps.LatLng(mapRecord.latitude, mapRecord.longitude)); + mapRecord.map.setZoom(parseFloat(mapRecord.zoom)); + return command.pc + 1; + } + }, + + getHandler: name => { + switch (name) { + case `create`: + return EasyCoder_GMap.Create; + case `gmap`: + return EasyCoder_GMap.GMap; + case `marker`: + return EasyCoder_GMap.Marker; + case `on`: + return EasyCoder_GMap.On; + case `remove`: + return EasyCoder_GMap.Remove; + case `set`: + return EasyCoder_GMap.Set; + case `show`: + return EasyCoder_GMap.Show; + case `update`: + return EasyCoder_GMap.Update; + default: + return null; + } + }, + + run: program => { + const command = program[program.pc]; + const handler = EasyCoder_GMap.getHandler(command.keyword); + if (!handler) { + program.runtimeError(command.lino, `Unknown keyword '${command.keyword}' in 'gmap' package`); + } + return handler.run(program); + }, + + value: { + + compile: compiler => { + if (compiler.tokenIs(`the`)) { + compiler.next(); + } + const type = compiler.getToken(); + if (type === `click`) { + if (compiler.nextTokenIs(`position`)) { + if (compiler.nextTokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const mapRecord = compiler.getSymbolRecord(); + if (mapRecord.keyword === `gmap`) { + compiler.next(); + return { + domain: `gmap`, + type, + name: mapRecord.name + }; + } + } + } + } + } + if (compiler.nextTokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + if (symbolRecord.keyword === `gmap` && [`latitude`, `longitude`, `type`, `zoom`, `bounds`].includes(type) || + symbolRecord.keyword === `marker` && [`latitude`, `longitude`, `title`].includes(type)) { + compiler.next(); + return { + domain: `gmap`, + type, + name: symbolRecord.name + }; + } + } + } + return null; + }, + + get: (program, value) => { + var symbolRecord; + switch (value.type) { + case `latitude`: + symbolRecord = program.getSymbolRecord(value.name); + switch (symbolRecord.keyword) { + case `gmap`: + return { + type: `constant`, + numeric: false, + content: program.getSymbolRecord(value.name).map.getCenter().lat().toString() + }; + case `marker`: + return { + type: `constant`, + numeric: false, + content: program.getSymbolRecord(value.name).marker.getPosition().lat().toString() + }; + } + break; + case `longitude`: + symbolRecord = program.getSymbolRecord(value.name); + switch (symbolRecord.keyword) { + case `gmap`: + return { + type: `constant`, + numeric: false, + content: program.getSymbolRecord(value.name).map.getCenter().lng().toString() + }; + case `marker`: + return { + type: `constant`, + numeric: false, + content: program.getSymbolRecord(value.name).marker.getPosition().lng().toString() + }; + } + break; + case `type`: + return { + type: `constant`, + numeric: false, + content: program.getSymbolRecord(value.name).map.getMapTypeId() + }; + case `zoom`: + return { + type: `constant`, + numeric: false, + content: program.getSymbolRecord(value.name).map.getZoom().toString() + }; + case `bounds`: + const map = program.getSymbolRecord(value.name).map; + const bounds = map ? JSON.stringify(map.getBounds()) : ``; + return { + type: `constant`, + numeric: false, + content: bounds + }; + case `title`: + return { + type: `constant`, + numeric: false, + content: program.getSymbolRecord(value.name).marker.getTitle() + }; + case `click`: + return { + type: `constant`, + numeric: false, + content: JSON.stringify(program.getSymbolRecord(value.name).clickPosition) + }; + } + return null; + } + }, + + condition: { + + compile: () => {}, + + test: () => {} + } +}; +const EasyCoder_Showdown = { + + name: `EasyCoder_Showdown`, + + Load: { + + compile: compiler => { + const lino = compiler.getLino(); + if (compiler.nextTokenIs(`showdown`)) { + compiler.next(); + compiler.addCommand({ + domain: `showdown`, + keyword: `load`, + lino + }); + return true; + } + return false; + }, + + run: program => { + const command = program[program.pc]; + if (program.isUndefined(this.showdown_loaded)) { + program.require(`js`, `https://unpkg.com/showdown@1.9.1/./dist/showdown.js`, function () { + this.showdown_loaded = true; + EasyCoder_Showdown.setupExtension(); + program.run(command.pc + 1); + }); + } + else { + EasyCoder_Showdown.setupExtension(); + return command.pc + 1; + } + return 0; + } + }, + + setupExtension: () => { + showdown.extension(`Extension`, { + type: `lang`, + filter: function (text, converter) { + const callback = program.getSymbolRecord(converter.callback); + return text.replace(/~([^~]+)~/g, function (match, group) { + callback.payload = group; + program.run(callback.cb); + return callback.payload; + }); + } + }); + }, + + getHandler: (name) => { + switch (name) { + case `load`: + return EasyCoder_Showdown.Load; + default: + return null; + } + }, + + run: program => { + const command = program[program.pc]; + const handler = EasyCoder_Showdown.getHandler(command.keyword); + if (!handler) { + program.runtimeError(command.lino, `Unknown keyword '${command.keyword}' in 'showdown' package`); + } + return handler.run(program); + }, + + value: { + + compile: compiler => { + if (compiler.tokenIs(`showdown`)) { + if (compiler.nextTokenIs(`decode`)) { + const value = compiler.getNextValue(); + let callback = null; + if (compiler.tokenIs(`with`)) { + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + if (symbolRecord.keyword === `callback`) { + callback = symbolRecord.name; + compiler.next(); + } + } + } + return { + domain: `showdown`, + type: `decode`, + value, + callback + }; + } + } + return null; + }, + + get: (program, value) => { + const converter = new showdown.Converter({ + extensions: [`Extension`] + }); + switch (value.type) { + case `decode`: + converter.callback = value.callback; + const markdown = program.getValue(value.value); + const content = converter.makeHtml(markdown); + return { + type: `constant`, + numeric: false, + content + }; + } + } + }, + + condition: { + + compile: () => {}, + + test: () => {} + } +}; +const EasyCoder_CodeMirror = { + + name: `EasyCoder_CodeMirror`, + + CodeMirror: { + + compile: (compiler) => { + const lino = compiler.getLino(); + const action = compiler.nextToken(); + switch (action) { + case `init`: + const mode = compiler.nextToken(); + let profile = ``; + if (compiler.nextTokenIs(`profile`)) { + profile = compiler.getNextValue(); + } + compiler.addCommand({ + domain: `codemirror`, + keyword: `codemirror`, + lino, + action, + mode, + profile + }); + return true; + case `attach`: + if (compiler.nextTokenIs(`to`)) { + if (compiler.nextIsSymbol()) { + const editor = compiler.getToken(); + let mode = `ecs`; + if (compiler.nextTokenIs(`mode`)) { + mode = compiler.nextToken(); + compiler.next(); + } + compiler.addCommand({ + domain: `codemirror`, + keyword: `codemirror`, + lino, + action, + editor, + mode + }); + return true; + } + } + break; + case `set`: + if (compiler.nextTokenIs(`content`)) { + if (compiler.nextTokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const editor = compiler.getSymbolRecord(); + if (compiler.nextTokenIs(`to`)) { + const value = compiler.getNextValue(); + compiler.addCommand({ + domain: `codemirror`, + keyword: `codemirror`, + lino, + action: `setContent`, + editor: editor.name, + value + }); + return true; + } + } + } + } + break; + case `close`: + if (compiler.nextIsSymbol()) { + const editor = compiler.getSymbolRecord(); + compiler.next(); + compiler.addCommand({ + domain: `codemirror`, + keyword: `codemirror`, + lino, + action: `close`, + editor: editor.name + }); + return true; + } + return false; + default: + throw new Error(`Unrecognized action '${action}'`); + } + return false; + }, + + run: (program) => { + const command = program[program.pc]; + var editor; + switch (command.action) { + case `init`: + switch (command.mode) { + case `basic`: + program.require(`css`, `https://codemirror.net/lib/codemirror.css`, + function () { + program.require(`js`, `https://codemirror.net/lib/codemirror.js`, + function () { + if (command.profile) { + program.require(`js`, program.getValue(command.profile), + function () { + program.run(command.pc + 1); + }); + } else { + program.run(command.pc + 1); + } + }); + }); + return 0; + } + break; + case `attach`: + try { + editor = program.getSymbolRecord(command.editor); + const element = document.getElementById(editor.element[editor.index].id); + editor.editor = CodeMirror.fromTextArea(element, { + mode: command.mode, + theme: `default`, + lineNumbers: true + }); + editor.editor.setSize(`100%`, `100%`); + } catch (err) { alert(err); } + break; + case `setContent`: + editor = program.getSymbolRecord(command.editor); + const value = program.getValue(command.value); + editor.editor.setValue(value); + break; + case `close`: + editor = program.getSymbolRecord(command.editor); + editor.editor.toTextArea(); + break; + } + return command.pc + 1; + } + }, + + getHandler: (name) => { + switch (name) { + case `codemirror`: + return EasyCoder_CodeMirror.CodeMirror; + default: + return null; + } + }, + + run: (program) => { + const command = program[program.pc]; + const handler = EasyCoder_CodeMirror.getHandler(command.keyword); + if (!handler) { + program.runtimeError(command.lino, + `Unknown keyword '${command.keyword}' in 'codemirror' package`); + } + return handler.run(program); + }, + + value: { + + compile: () => { + return null; + }, + get: () => {} + }, + + condition: { + + compile: () => {}, + test: () => {} + } +}; +const EasyCoder_SVG = { + + name: `EasyCoder_SVG`, + + Circle: { + + compile: (compiler) => { + compiler.compileVariable(`svg`, `circle`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + Create: { + + compile: (compiler) => { + const lino = compiler.getLino(); + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + compiler.next(); + switch (symbolRecord.keyword) { + case `svg`: + if (compiler.tokenIs(`in`)) { + if (compiler.nextIsSymbol()) { + const parent = compiler.getToken(); + compiler.next(); + var style = null; + var flag = true; + while (flag) { + const token = compiler.getToken(); + compiler.next(); + switch (token) { + case `style`: + style = compiler.getValue(); + break; + default: + compiler.prev(); + flag = false; + break; + } + } + if (!style) { + style = { + type: `constant`, + numeric: false, + content: `width:100%;height:100%` + }; + } + compiler.addCommand({ + domain: `svg`, + keyword: `create`, + lino, + type: `svg`, + name: symbolRecord.name, + style, + parent + }); + return true; + } + } + break; + case `group`: + if (compiler.tokenIs(`in`)) { + if (compiler.nextIsSymbol()) { + const parentRecord = compiler.getSymbolRecord(); + if (![`svg`, `group`].includes(parentRecord.keyword)) { + throw new Error(`Inappropriate type '${parentRecord.keyword}'`); + } + compiler.next(); + compiler.addCommand({ + domain: `svg`, + keyword: `create`, + lino, + type: `group`, + name: symbolRecord.name, + parent: parentRecord.name + }); + return true; + } + } + break; + case `circle`: + case `ellipse`: + case `line`: + case `rect`: + case `svgtext`: + if (compiler.tokenIs(`in`)) { + if (compiler.nextIsSymbol()) { + const parentRecord = compiler.getSymbolRecord(); + if (![`svg`, `group`].includes(parentRecord.keyword)) { + throw new Error(`Inappropriate type '${parentRecord.keyword}'`); + } + compiler.next(); + var text; + flag = true; + while (flag) { + const token = compiler.getToken(); + compiler.next(); + switch (token) { + case `style`: + style = compiler.getValue(); + break; + case `text`: + text = compiler.getValue(); + break; + default: + compiler.prev(); + flag = false; + break; + } + } + compiler.addCommand({ + domain: `svg`, + keyword: `create`, + lino, + type: symbolRecord.keyword === `svgtext` ? `text` : symbolRecord.keyword, + name: symbolRecord.name, + style, + text, + parent: parentRecord.name + }); + return true; + } + } + break; + } + } + return false; + }, + + run: (program) => { + const ns = `http://www.w3.org/2000/svg`; + const command = program[program.pc]; + var parentRecord = program.getSymbolRecord(command.parent); + var group; + const symbolRecord = program.getSymbolRecord(command.name); + if (command.type === `group`) { + symbolRecord.parent = command.parent; + symbolRecord.x = 0; + symbolRecord.y = 0; + } else { + if (parentRecord.keyword === `group`) { + group = parentRecord; + // Add this element to the group + const groupElement = group.value[group.index]; + if (!groupElement.content) { + groupElement.content = []; + } + groupElement.content.push({ + name: symbolRecord.name, + index: symbolRecord.index + }); + // Find the real parent + while (parentRecord.keyword === `group`) { + parentRecord = program.getSymbolRecord(parentRecord.parent); + } + } + const container = parentRecord.element[parentRecord.index]; + const element = document.createElementNS(ns, command.type); + symbolRecord.element[symbolRecord.index] = element; + container.appendChild(element); + // Set the id + const id = `ec-` + symbolRecord.name + `-` + symbolRecord.index; + element.setAttribute(`id`, id); + if (symbolRecord.keyword === `svgtext`) { + element.textContent = program.value.evaluate(program, command.text).content; + } + symbolRecord.value[symbolRecord.index] = { + type: `constant`, + numeric: false, + content: id + }; + if (command.style) { + const style = program.value.evaluate(program, command.style).content; + program.domain.browser.setStyles(id, style); + // Store the location of this shape + const value = symbolRecord.value[symbolRecord.index]; + switch (symbolRecord.keyword) { + case `circle`: + case `ellipse`: + value.x = element.getAttribute(`cx`); + value.y = element.getAttribute(`cy`); + break; + case `line`: + value.x = element.getAttribute(`x1`); + value.y = element.getAttribute(`y1`); + value.x2 = element.getAttribute(`x2`); + value.y2 = element.getAttribute(`y2`); + break; + case `rect`: + case `svgtext`: + value.x = element.getAttribute(`x`); + value.y = element.getAttribute(`y`); + break; + } + if (group) { + // Record the group name and index + value.groupName = group.name; + value.groupIndex = group.index; + } + } + } + return program[program.pc].pc + 1; + } + }, + + Ellipse: { + + compile: (compiler) => { + compiler.compileVariable(`svg`, `ellipse`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + Group: { + + compile: (compiler) => { + compiler.compileVariable(`svg`, `group`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + Line: { + + compile: (compiler) => { + compiler.compileVariable(`svg`, `line`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + Move: { + + compile: (compiler) => { + const lino = compiler.getLino(); + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + if (compiler.nextTokenIs(`to`)) { + const x = compiler.getNextValue(); + const y = compiler.getValue(); + compiler.addCommand({ + domain: `svg`, + keyword: `move`, + lino, + type: `moveTo`, + name: symbolRecord.name, + x, + y + }); + return true; + } + } + return false; + }, + + run: (program) => { + const command = program[program.pc]; + const newX = program.value.evaluate(program, command.x).content; + const newY = program.value.evaluate(program, command.y).content; + const symbolRecord = program.getSymbolRecord(command.name); + switch (symbolRecord.keyword) { + case `group`: + for (const item of symbolRecord.value[symbolRecord.index].content) { + const itemRecord = program.getSymbolRecord(item.name); + const value = itemRecord.value[item.index]; + const element = document.getElementById(value.content); + const x = parseInt(value.x) + newX; + const y = parseInt(value.y) + newY; + switch (itemRecord.keyword) { + case `circle`: + case `ellipse`: + element.setAttribute(`cx`, x); + element.setAttribute(`cy`, y); + break; + case `line`: + element.setAttribute(`x1`, x); + element.setAttribute(`y1`, y); + element.setAttribute(`x2`, parseInt(value.x2) + newX); + element.setAttribute(`y2`, parseInt(value.y2) + newY); + break; + case `rect`: + case `svgtext`: + element.setAttribute(`x`, x); + element.setAttribute(`y`, y); + break; + } + } + symbolRecord.x = newX; + symbolRecord.y = newY; + break; + case `circle`: + case `ellipse`: + case `line`: + case `rect`: + case `svgtext`: + var px = 0; + var py = 0; + const symRec = symbolRecord.value[symbolRecord.index]; + if (symRec.groupName) { + const parentRecord = program.getSymbolRecord(symRec.groupName); + px = parentRecord.x; + py = parentRecord.y; + } + const symbolValue = symbolRecord.value[symbolRecord.index]; + const element = document.getElementById(symbolRecord.value[symbolRecord.index].content); + switch (symbolRecord.keyword) { + case `circle`: + case `ellipse`: + element.setAttribute(`cx`, px + newX); + element.setAttribute(`cy`, py + newY); + break; + case `line`: + element.setAttribute(`x1`, px + newX); + element.setAttribute(`y1`, py + newY); + const dx = parseInt(symbolValue.x2) - parseInt(symbolValue.x1); + const dy = parseInt(symbolValue.y2) - parseInt(symbolValue.y1); + element.setAttribute(`x2`, px + dx + newX); + element.setAttribute(`y2`, py + dy + newY); + break; + case `rect`: + case `svgtext`: + element.setAttribute(`x`, px + newX); + element.setAttribute(`y`, py + newY); + break; + } + symbolValue.x = newX; + symbolValue.y = newY; + break; + } + return program[program.pc].pc + 1; + } + }, + + On: { + + compile: (compiler) => { + const lino = compiler.getLino(); + const action = compiler.nextToken(); + switch (action) { + case `click`: + if (compiler.nextIsSymbol()) { + const symbol = compiler.getSymbolRecord(); + compiler.next(); + if (symbol.keyword !== `group`) { + return false; + } + compiler.addCommand({ + domain: `svg`, + keyword: `on`, + lino, + action, + symbol: symbol.name + }); + // Add a 'goto' to skip the action + const goto = compiler.getPc(); + compiler.addCommand({ + domain: `core`, + keyword: `goto`, + goto: 0 + }); + // Add the action + compiler.compileOne(); + // Fixup the 'goto' + compiler.getCommandAt(goto).goto = compiler.getPc(); + return true; + } + } + compiler.addWarning(`Unrecognised syntax in 'on'`); + return false; + }, + + run: (program) => { + const command = program[program.pc]; + const targetItem = program.getSymbolRecord(command.symbol); + switch (command.action) { + case `click`: + if (targetItem.keyword === `group`) { + // Iterate the group array + for (const groupValue of targetItem.value) { + if (groupValue.content) { + for (const value of groupValue.content) { + const contentItem = program.getSymbolRecord(value.name); + const contentValue = contentItem.value[value.index]; + if (contentValue.content) { + const target = document.getElementById(contentValue.content); + target.targetPc = command.pc + 2; + target.contentItem = contentItem; + target.contentIndex = value.index; + target.onclick = function (event) { + event.target.blur(); + const contentItem = event.target.contentItem; + contentItem.index = event.target.contentIndex; + const contentValue = contentItem.value[contentItem.index]; + if (contentValue.groupName) { + targetItem.index = contentValue.groupIndex; + // Set the content indices + const group = targetItem.value[targetItem.index]; + for (const gc of group.content) { + const gi = program.getSymbolRecord(gc.name); + gi.index = gc.index; + } + } + try { + program.run(event.target.targetPc); + } catch (err) { + program.reportError(err, program); + } + return false; + }; + } + } + } + } + } + break; + default: + break; + } + return command.pc + 1; + } + }, + + Rect: { + + compile: (compiler) => { + compiler.compileVariable(`svg`, `rect`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + Set: { + + compile: (compiler) => { + const lino = compiler.getLino(); + var token = compiler.nextToken(); + if (token === `the`) { + token = compiler.nextToken(); + } + if (token === `text`) { + if (compiler.nextTokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const symbol = compiler.getSymbolRecord(); + switch (symbol.keyword) { + case `svgtext`: + if (compiler.nextTokenIs(`to`)) { + compiler.next(); + const value = compiler.getValue(); + compiler.addCommand({ + domain: `svg`, + keyword: `set`, + lino, + type: `setText`, + symbolName: symbol.name, + value + }); + return true; + } + break; + default: + break; + } + } + } + } + return false; + }, + + run: (program) => { + const command = program[program.pc]; + var symbol; + var value; + var target; + switch (command.type) { + case `setText`: + symbol = program.getSymbolRecord(command.symbolName); + target = document.getElementById(symbol.value[symbol.index].content); + value = program.value.evaluate(program, command.value).content; + switch (symbol.keyword) { + case `svgtext`: + target.innerHTML = value; + break; + default: + break; + } + break; + default: + break; + } + return command.pc + 1; + } + }, + + SVG: { + + compile: (compiler) => { + compiler.compileVariable(`svg`, `svg`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + SVGText: { + + compile: (compiler) => { + compiler.compileVariable(`svg`, `svgtext`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + getHandler: (name) => { + switch (name) { + case `circle`: + return EasyCoder_SVG.Circle; + case `create`: + return EasyCoder_SVG.Create; + case `ellipse`: + return EasyCoder_SVG.Ellipse; + case `group`: + return EasyCoder_SVG.Group; + case `line`: + return EasyCoder_SVG.Line; + case `move`: + return EasyCoder_SVG.Move; + case `on`: + return EasyCoder_SVG.On; + case `rect`: + return EasyCoder_SVG.Rect; + case `set`: + return EasyCoder_SVG.Set; + case `svg`: + return EasyCoder_SVG.SVG; + case `svgtext`: + return EasyCoder_SVG.SVGText; + default: + return null; + } + }, + + run: (program) => { + const command = program[program.pc]; + const handler = EasyCoder_SVG.getHandler(command.keyword); + if (!handler) { + program.runtimeError(command.lino, `Unknown keyword '${command.keyword}' in 'svg' package`); + } + return handler.run(program); + }, + + value: { + + compile: (compiler) => { + if (compiler.tokenIs(`the`)) { + compiler.next(); + } + if (compiler.tokenIs(`text`)) { + if (compiler.nextTokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + compiler.next(); + if (symbolRecord.keyword === `svgtext`) { + return { + domain: `svg`, + type: `svgtext`, + name: symbolRecord.name + }; + } + } + } + } + return null; + }, + + get: (program, value) => { + switch (value.type) { + case `svgtext`: + const symbolRecord = program.getSymbolRecord(value.name); + // console.log('symbolRecord: ' + JSON.stringify(symbolRecord.value[symbolRecord.index], null, 2)); + const element = document.getElementById(symbolRecord.value[symbolRecord.index].content); + return { + type: `constant`, + numeric: false, + content: element.innerHTML + }; + } + } + }, + + condition: { + + compile: () => {}, + + test: () => {} + } +}; +const EasyCoder_UI = { + + name: `EasyCoder_UI`, + + monthNames: [ + `January`, + `February`, + `March`, + `April`, + `May`, + `June`, + `July`, + `August`, + `September`, + `October`, + `November`, + `December` + ], + + renderDate: (dateRecord) => { + const date = new Date(dateRecord.timestamp); + const day = date.getDate(); + const month = date.getMonth(); + const year = date.getFullYear(); + + const daysInMonth = [ + 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 + ]; + + if (year % 4 === 0) { + daysInMonth[1] = 29; + } + + // Do the day list + const dayList = dateRecord.day; + while (dayList.firstChild) { + dayList.removeChild(dayList.lastChild); + } + for (var i = 0; i < daysInMonth[month]; i++) { + const option = new Option(String(i)); + option.value = i; + option.text = String(i + 1); + dayList.appendChild(option); + } + dayList.selectedIndex = day - 1; + + // Do the month list + const monthList = dateRecord.month; + while (monthList.firstChild) { + monthList.removeChild(monthList.lastChild); + } + EasyCoder_UI.monthNames.forEach(function (month, index) { + const option = document.createElement(`option`); + option.value = index; + option.text = month; + monthList.appendChild(option); + }); + monthList.selectedIndex = month; + + // Do the year list + const yearList = dateRecord.year; + while (yearList.firstChild) { + yearList.removeChild(yearList.lastChild); + } + const yr = new Date().getUTCFullYear(); + var sel = 0; + for (i = 0; i < 10; i++) { + const option = document.createElement(`option`); + var y = yr - i + 1; + option.value = y; + option.text = String(y); + if (y === year) { + sel = i; + } + yearList.appendChild(option); + } + yearList.selectedIndex = sel; + }, + + Create: { + + compile: (compiler) => { + const lino = compiler.getLino(); + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + const type = symbolRecord.keyword; + if (type === `date`) { + if (compiler.nextTokenIs(`in`)) { + if (compiler.nextIsSymbol()) { + const holderRecord = compiler.getSymbolRecord(); + compiler.next(); + var second = compiler.constant(-1, true); + var minute = compiler.constant(-1, true); + var hour = compiler.constant(-1, true); + var day = compiler.constant(-1, true); + var month = compiler.constant(-1, true); + var year = compiler.constant(-1, true); + while (true) { + const token = compiler.getToken(); + if (token === `second`) { + second = compiler.getNextValue(); + } else if (token === `minute`) { + minute = compiler.getNextValue(); + } else if (token === `hour`) { + hour = compiler.getNextValue(); + } else if (token === `day`) { + day = compiler.getNextValue(); + } else if (token === `month`) { + month = compiler.getNextValue(); + } else if (token === `year`) { + year = compiler.getNextValue(); + } else { + break; + } + } + compiler.addCommand({ + domain: `ui`, + keyword: `create`, + lino, + type, + date: symbolRecord.name, + holder: holderRecord.name, + day, + month, + year, + hour, + minute, + second, + format: `date` + }); + return true; + } + } + } + } + return false; + }, + + run: (program) => { + const command = program[program.pc]; + switch (command.type) { + case `date`: + const dateRecord = program.getSymbolRecord(command.date); + const dayList = document.createElement(`select`); + dayList.id = `ec-day`; + dateRecord.day = dayList; + const monthList = document.createElement(`select`); + dayList.id = `ec-month`; + dateRecord.month = monthList; + const yearList = document.createElement(`select`); + dayList.id = `ec-year`; + dateRecord.year = yearList; + + const holderRecord = program.getSymbolRecord(command.holder); + const holder = holderRecord.element[holderRecord.index]; + while (holder.firstChild) { + holder.removeChild(holder.lastChild); + } + holder.appendChild(dayList); + holder.appendChild(monthList); + holder.appendChild(yearList); + + // Get the requested values + var day = program.getValue(command.day); + var month = program.getValue(command.month); + var year = program.getValue(command.year); + const date = new Date(); + if (day !== -1) { + date.setDate(day); + } + if (month !== -1) { + date.setMonth(month); + } + if (year !== -1) { + date.setYear(year); + } + dateRecord.timestamp = date.getTime(); + EasyCoder_UI.renderDate(dateRecord); + + dayList.dateRecord = dateRecord; + monthList.dateRecord = dateRecord; + yearList.dateRecord = dateRecord; + + dayList.onchange = function () { + const date = new Date(this.dateRecord.timestamp); + date.setDate(this.selectedIndex + 1); + this.dateRecord.timestamp = date.getTime(); + EasyCoder_UI.renderDate(this.dateRecord); + }; + + monthList.onchange = function () { + const date = new Date(this.dateRecord.timestamp); + date.setMonth(this.selectedIndex); + this.dateRecord.timestamp = date.getTime(); + EasyCoder_UI.renderDate(this.dateRecord); + }; + + yearList.onchange = function () { + const date = new Date(this.dateRecord.timestamp); + date.setYear(this[this.selectedIndex].value); + this.dateRecord.timestamp = date.getTime(); + EasyCoder_UI.renderDate(this.dateRecord); + }; + break; + } + + return command.pc + 1; + } + }, + + Date: { + + compile: (compiler) => { + compiler.compileVariable(`ui`, `date`); + return true; + }, + + run: (program) => { + const command = program[program.pc]; + command.value = { + type: `constant`, + numeric: true, + content: Date.now() + }; + return command.pc + 1; + } + }, + + Set: { + + compile: (compiler) => { + const lino = compiler.getLino(); + compiler.skip(`the`); + const token = compiler.getToken(); + switch (token) { + case `date`: + if (compiler.nextTokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const dateRecord = compiler.getSymbolRecord(); + if (dateRecord.keyword === `date`) { + if (compiler.nextTokenIs(`to`)) { + const timestamp = compiler.getNextValue(); + compiler.addCommand({ + domain: `ui`, + keyword: `set`, + lino, + what: `date`, + date: dateRecord.name, + timestamp + }); + return true; + } + } + } + } + break; + } + return false; + }, + + run: (program) => { + const command = program[program.pc]; + switch (command.what) { + case `date`: + const dateRecord = program.getSymbolRecord(command.date); + dateRecord.timestamp = program.getValue(command.timestamp) * 1000; + EasyCoder_UI.renderDate(dateRecord); + break; + } + return command.pc + 1; + } + }, + + getHandler: (name) => { + switch (name) { + case `create`: + return EasyCoder_UI.Create; + case `date`: + return EasyCoder_UI.Date; + case `set`: + return EasyCoder_UI.Set; + default: + return null; + } + }, + + run: (program) => { + const command = program[program.pc]; + const handler = EasyCoder_UI.getHandler(command.keyword); + if (!handler) { + program.runtimeError(command.lino, `Unknown keyword '${command.keyword}' in 'ui' package`); + } + return handler.run(program); + }, + + value: { + + compile: (compiler) => { + if (compiler.isSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + if (symbolRecord.keyword === `date`) { + compiler.next(); + return { + domain: `ui`, + type: `date`, + what: `timestamp`, + value: symbolRecord.name + }; + } + return null; + } + if (compiler.tokenIs(`the`)) { + compiler.next(); + } + const what = compiler.getToken(); + if ([`date`, `timestamp`].includes(what)) { + if (compiler.nextTokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + if (symbolRecord.keyword === `date`) { + compiler.next(); + return { + domain: `ui`, + type: `date`, + what, + value: symbolRecord.name + }; + } + return null; + } + } + return null; + } + // Try other value possibilities + return null; + }, + + get: (program, value) => { + switch (value.type) { + case `date`: + const dateRecord = program.getSymbolRecord(value.value); + const day = dateRecord.day.options[dateRecord.day.selectedIndex].text; + const month = dateRecord.month.options[dateRecord.month.selectedIndex].value; + const year = dateRecord.year.options[dateRecord.year.selectedIndex].value; + const date = new Date(year, month, day, 0, 0, 0, 0); + switch (value.what) { + case `date`: + return { + type: `constant`, + numeric: false, + content: `${day} ${EasyCoder_UI.monthNames[month]} ${year}` + }; + case `timestamp`: + return { + type: `constant`, + numeric: true, + content: date.getTime() / 1000 + }; + } + } + } + }, + + condition: { + + compile: () => {}, + + test: () => {} + } +}; +const EasyCoder_VFX = { + + name: `EasyCoder_VFX`, + + ANIMATION: { + + compile: (compiler) => { + compiler.compileVariable(`vfx`, `animation`, false, `dom`); + return true; + }, + + run: (program) => { + return program[program.pc].pc + 1; + } + }, + + Create: { + + compile: (compiler) => { + const lino = compiler.getLino(); + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + const keyword = symbolRecord.keyword; + if (keyword == `animation`) { + if (compiler.nextTokenIs(`in`)) { + if (compiler.nextIsSymbol()) { + const parentRecord = compiler.getSymbolRecord(); + compiler.next(); + compiler.addCommand({ + domain: `vfx`, + keyword: `create`, + lino, + name: symbolRecord.name, + parent: parentRecord.name + }); + return true; + } + } + } + } + return false; + }, + + run: (program) => { + const command = program[program.pc]; + const targetRecord = program.getSymbolRecord(command.name); + const p = command.imported ? EasyCoder.scripts[program.parent] : program; + const parentRecord = p.getSymbolRecord(command.parent); + if (!parentRecord.element[parentRecord.index]) { + program.runtimeError(command.pc, `Element ${parentRecord.name} does not exist.`); + } + let parent = parentRecord.element[parentRecord.index]; + // Create the container + let container = document.createElement(`div`); + targetRecord.element[targetRecord.index] = container; + targetRecord.element[targetRecord.index].id = + `ec-${targetRecord.name}-${targetRecord.index}-${EasyCoder.elementId++}`; + parent.appendChild(container); + container.style[`position`] = `relative`; + container.style[`overflow`] = `hidden`; + if (typeof targetRecord.animation === `undefined`) { + targetRecord.animation = []; + for (let n = 0; n < targetRecord.elements; n++) { + targetRecord.animation.push({}); + } + } + const image = document.createElement(`img`); + targetRecord.animation[targetRecord.index].image = image; + container.appendChild(image); + image.style[`display`] = `none`; + image.style[`position`] = `absolute`; + image.style[`max-width`] = `none`; + return command.pc + 1; + } + }, + + On: { + + compile: compiler => { + const lino = compiler.getLino(); + const action = compiler.nextToken(); + switch (action) { + case `trigger`: + if (compiler.nextIsSymbol()) { + let symbolRecord = compiler.getSymbolRecord(); + if (symbolRecord.keyword === `animation`) { + compiler.next(); + compiler.addCommand({ + domain: `vfx`, + keyword: `on`, + lino, + action, + target:symbolRecord.name + }); + return compiler.completeHandler(); + } + break; + } + } + return false; + }, + + run: program => { + const command = program[program.pc]; + const cb = command.pc + 2; + const symbolRecord = program.getSymbolRecord(command.target); + switch (command.action) { + case `trigger`: + symbolRecord.onTrigger = cb; + break; + default: + program.runtimeError(command.lino, `Unknown action '${command.action}'`); + return 0; + } + return command.pc + 1; + } + }, + + Set: { + + compile: (compiler) => { + const lino = compiler.getLino(); + let type = compiler.nextToken(); + if (compiler.tokenIs(`the`)) { + type = compiler.nextToken(); + } + if ([`url`, `specification`, `spec`, `opacity`].includes(type)) { + if (compiler.nextTokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + if (compiler.nextTokenIs(`to`)) { + const value = compiler.getNextValue(); + compiler.addCommand({ + domain: `vfx`, + keyword: `set`, + lino, + target: symbolRecord.name, + type, + value + }); + return true; + } + } + } + } + return false; + }, + + run: (program) => { + const command = program[program.pc]; + let targetRecord = program.getSymbolRecord(command.target); + let container = targetRecord.element[targetRecord.index]; + let animation; + switch (command.type) { + case `url`: + let url = program.getValue(command.value); + animation.image.setAttribute(`src`, url); + break; + case `specification`: + case `spec`: + animation = targetRecord.animation[targetRecord.index]; + let spec = JSON.parse(program.getValue(command.value)); + animation.spec = spec; + animation.step = spec.steps; + if (['panzoom'].includes(spec.type)) { + container.style.width = spec.width; + container.style.height = spec.height; + let width = container.getBoundingClientRect().width; + let height = container.getBoundingClientRect().height; + animation.widthS = width * 100 / spec.start.width; + let zoomS = animation.widthS / width; + let heightS = height * zoomS; + animation.leftS = width * zoomS * spec.start.left / 100; + animation.topS = height * zoomS * spec.start.top / 100; + animation.widthF = width * 100 / spec.finish.width; + let zoomF = animation.widthF / width; + let heightF = height * zoomF; + animation.leftF = width * zoomF * spec.finish.left / 100; + animation.topF = height * zoomF * spec.finish.top / 100; + + if (spec.start.width > 100) { + throw new Error(`Start width too great for item ${targetRecord.index}`); + } + if (spec.finish.width > 100) { + throw new Error(`Finish width too great for item ${targetRecord.index}`); + } + if (animation.widthS - animation.leftS < width) { + throw new Error(`Insufficient start width for item ${targetRecord.index}`); + } + if (heightS - animation.topS < height) { + throw new Error(`Insufficient start height for item ${targetRecord.index}`); + } + if (animation.widthF - animation.leftF < width) { + throw new Error(`Insufficient finish width for item ${targetRecord.index}`); + } + if (heightF - animation.topF < height) { + throw new Error(`Insufficient finish height for item ${targetRecord.index}`); + } + animation.left = animation.leftS; + animation.top = animation.topS; + animation.width = animation.widthS; + let image = animation.image; + image.style.left = `-${animation.left}px`; + image.style.top = `-${animation.top}px`; + image.style.width = `${animation.width}px`; + image.setAttribute(`src`, spec.url); + } else { + program.runtimeError(command.lino, `Unknown animation type '${spec.type}'`); + return 0; + } + case `opacity`: + animation = targetRecord.animation[targetRecord.index]; + let image = animation.image; + image.style.opacity = command.value; + break; + } + return command.pc + 1; + } + }, + + Start: { + compile: (compiler) => { + const lino = compiler.getLino(); + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + const keyword = symbolRecord.keyword; + if (keyword == `animation`) { + compiler.next(); + compiler.addCommand({ + domain: `vfx`, + keyword: `start`, + lino, + target: symbolRecord.name + }); + return true; + } + } + return false; + }, + + run: (program) => { + const command = program[program.pc]; + const targetRecord = program.getSymbolRecord(command.target); + const animation = targetRecord.animation[targetRecord.index]; + animation.step = 0; + animation.left = animation.leftS; + animation.top = animation.topS; + animation.width = animation.widthS; + animation.image.style.display = `inline-block`; + return command.pc + 1; + } + }, + + Step: { + compile: (compiler) => { + const lino = compiler.getLino(); + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + const keyword = symbolRecord.keyword; + if (keyword == `animation`) { + compiler.next(); + compiler.addCommand({ + domain: `vfx`, + keyword: `step`, + lino, + target: symbolRecord.name + }); + return true; + } + } + return false; + }, + + run: (program) => { + const command = program[program.pc]; + const targetRecord = program.getSymbolRecord(command.target); + for (targetRecord.index = 0; targetRecord.index < targetRecord.elements; targetRecord.index++) { + const animation = targetRecord.animation[targetRecord.index]; + if (animation.step < animation.spec.steps) { + animation.step++; + let proportion = parseFloat(animation.step) / animation.spec.steps; + animation.left = animation.leftS + (animation.leftF - animation.leftS) * proportion; + animation.top = animation.topS + (animation.topF - animation.topS) * proportion; + animation.width = animation.widthS + (animation.widthF - animation.widthS) * proportion; + const image = animation.image; + image.style.left = `-${animation.left}px`; + image.style.top = `-${animation.top}px`; + image.style.width = `${animation.width}px`; + if (animation.step === animation.spec.trigger) { + program.run(targetRecord.onTrigger); + } + } + } + return command.pc + 1; + } + }, + + getHandler: (name) => { + switch (name) { + case `animation`: + return EasyCoder_VFX.ANIMATION; + case `create`: + return EasyCoder_VFX.Create; + case `on`: + return EasyCoder_VFX.On; + case `set`: + return EasyCoder_VFX.Set; + case `start`: + return EasyCoder_VFX.Start; + case `step`: + return EasyCoder_VFX.Step; + default: + return null; + } + }, + + run: program => { + const command = program[program.pc]; + const handler = EasyCoder_VFX.getHandler(command.keyword); + if (!handler) { + program.runtimeError(command.lino, `Unknown keyword '${command.keyword}' in 'vfx' package`); + } + return handler.run(program); + }, + + value: { + + compile: () => { + return null; + }, + + get: () => { + return null; + } + }, + + condition: { + + compile: () => {}, + + test: () => {} + } +}; +// Plugins for StoryTeller + +// eslint-disable-next-line no-unused-vars +const EasyCoder_Plugins = { + + // eslint-disable-next-line no-unused-vars + getGlobalPlugins: (timestamp, path, setPluginCount, getPlugin, addPlugin) => { + setPluginCount(9); // *** IMPORTANT *** the number of plugins you will be adding + + addPlugin(`browser`, EasyCoder_Browser); + addPlugin(`json`, EasyCoder_Json); + addPlugin(`rest`, EasyCoder_Rest); + addPlugin(`gmap`, EasyCoder_GMap); + addPlugin(`showdown`, EasyCoder_Showdown); + addPlugin(`codemirror`, EasyCoder_CodeMirror); + addPlugin(`svg`, EasyCoder_SVG); + addPlugin(`ui`, EasyCoder_UI); + addPlugin(`vfx`, EasyCoder_VFX); + }, + + rest: () => { + return ``; + } +}; +// eslint-disable-next-line no-unused-vars +const EasyCoder_Compare = (program, value1, value2) => { + + const val1 = program.value.evaluate(program, value1); + const val2 = program.value.evaluate(program, value2); + var v1 = val1.content; + var v2 = val2.content; + if (v1 && val1.numeric) { + if (!val2.numeric) { + v2 = (v2 === `` || v2 === `-` || typeof v2 === `undefined`) ? 0 : parseInt(v2); + } + } else { + if (v2 && val2.numeric) { + v2 = v2.toString(); + } + if (typeof v1 === `undefined`) { + v1 = ``; + } + if (typeof v2 === `undefined`) { + v2 = ``; + } + } + if (v1 > v2) { + return 1; + } + if (v1 < v2) { + return -1; + } + return 0; +}; +// eslint-disable-next-line no-unused-vars +const EasyCoder_Compiler = { + + name: `EasyCoder_Compiler`, + + getTokens: function() { + return this.tokens; + }, + + addWarning: function(message) { + this.warnings.push(message); + }, + + warning: function(message) { + this.addWarning(message); + }, + + unrecognisedSymbol: function(item) { + this.addWarning(`Unrecognised symbol '${item}'`); + }, + + getWarnings: function() { + return this.warnings; + }, + + getIndex: function() { + return this.index; + }, + + next: function(step = 1) { + this.index = this.index + step; + }, + + peek: function() { + return this.tokens[this.index + 1].token; + }, + + more: function() { + return this.index < this.tokens.length; + }, + + getToken: function() { + if (this.index >= this.tokens.length) { + return null; + } + const item = this.tokens[this.index]; + return item ? this.tokens[this.index].token : null; + }, + + nextToken: function() { + this.next(); + return this.getToken(); + }, + + tokenIs: function(token) { + if (this.index >= this.tokens.length) { + return false; + } + return token === this.tokens[this.index].token; + }, + + nextTokenIs: function(token) { + this.next(); + return this.tokenIs(token); + }, + + skip: function(token) { + if (this.index >= this.tokens.length) { + return null; + } + this.next(); + if (this.tokenIs(token)) { + this.next(); + } + }, + + prev: function() { + this.index--; + }, + + getLino: function() { + if (this.index >= this.tokens.length) { + return 0; + } + return this.tokens[this.index].lino; + }, + + getTarget: function(index = this.index) { + return this.tokens[index].token; + }, + + getTargetPc: function(index = this.index) { + return this.symbols[this.getTarget(index)].pc; + }, + + getCommandAt: function(pc) { + return this.program[pc]; + }, + + isSymbol: function(required = false) { + const isSymbol = this.getTarget() in this.symbols; + if (isSymbol) return true; + if (required) { + throw new Error(`Unknown symbol: '${this.getTarget()}'`); + } + return false; + }, + + nextIsSymbol: function(required = false) { + this.next(); + return this.isSymbol(required); + }, + + getSymbol: function(required = false) { + if (this.isSymbol(required)) { + return this.symbols[this.getToken()]; + } + }, + + getSymbolPc: function(required = false) { + return this.getSymbol(required).pc; + }, + + getSymbolRecord: function() { + const record = this.program[this.getSymbolPc(true)]; + record.used = true; + return record; + }, + + getSymbols: function() { + return this.symbols; + }, + + getProgram: function() { + return this.program; + }, + + getPc: function() { + return this.program.length; + }, + + getValue: function() { + return this.value.compile(this); + }, + + getNextValue: function() { + this.next(); + return this.getValue(); + }, + + getCondition: function() { + return this.condition.compile(this); + }, + + constant: function(content, numeric = false) { + return this.value.constant(content, numeric); + }, + + addCommand: function(item) { + const pc = this.program.length; + this.program.push({ + pc, + ...item + }); + }, + + addSymbol: function(name, pc) { + this.symbols[name] = { + pc + }; + }, + + mark: function() { + this.savedMark = this.index; + }, + + rewind: function() { + this.index = this.savedMark; + }, + + rewindTo: function(index) { + this.index = index; + }, + + completeHandler: function() { + const lino = this.getLino(); + // Add a 'goto' to skip the action + const goto = this.getPc(); + this.addCommand({ + domain: `core`, + keyword: `goto`, + lino, + goto: 0 + }); + // Add the action + this.compileOne(); + // If `continue` is set + if (this.continue) { + this.addCommand({ + domain: `core`, + keyword: `goto`, + lino, + goto: this.getPc() + 1 + }); + this.continue = false; + } + // else add a 'stop' + else { + this.addCommand({ + domain: `core`, + keyword: `stop`, + lino, + next: 0 + }); + } + // Fixup the 'goto' + this.getCommandAt(goto).goto = this.getPc(); + return true; + }, + + compileVariable: function(domain, keyword, isVHolder = false, extra = null) { + this.next(); + const lino = this.getLino(); + const item = this.getTokens()[this.getIndex()]; + if (this.symbols[item.token]) { + throw new Error(`Duplicate variable name '${item.token}'`); + } + const pc = this.getPc(); + this.next(); + this.addSymbol(item.token, pc); + const command = { + domain, + keyword, + lino, + isSymbol: true, + used: false, + isVHolder, + name: item.token, + elements: 1, + index: 0, + value: [{}], + element: [], + extra + }; + if (extra === `dom`) { + command.element = []; + } + this.addCommand(command); + return command; + }, + + compileToken: function() { + // Try each domain in turn until one can handle the command + const token = this.getToken(); + if (!token) { + return; + } + // console.log(`Compile ${token}`); + this.mark(); + for (const domainName of Object.keys(this.domain)) { + // console.log(`Try domain ${domainName} for token ${token}`); + const domain = this.domain[domainName]; + if (domain) { + const handler = domain.getHandler(token); + if (handler) { + if (handler.compile(this)) { + return; + } + } + } + this.rewind(); + } + console.log(`No handler found`); + throw new Error(`I don't understand '${token}...'`); + }, + + compileOne: function() { + const keyword = this.getToken(); + if (!keyword) { + return; + } + // console.log(`Compile keyword '${keyword}'`); + this.warnings = []; + const pc = this.program.length; + // First check for a label + if (keyword.endsWith(`:`)) { + const name = keyword.substring(0, keyword.length - 1); + if (this.symbols[name]) { + throw new Error(`Duplicate symbol: '${name}'`); + } + this.symbols[name] = { + pc + }; + this.index++; + } else { + this.compileToken(); + } + }, + + compileFromHere: function(stopOn) { + while (this.index < this.tokens.length) { + const token = this.tokens[this.index]; + const keyword = token.token; + if (keyword === `else`) { + return this.program; + } + this.compileOne(); + if (stopOn.indexOf(keyword) > -1) { + break; + } + } + }, + + compile: function(tokens) { + this.tokens = tokens; + this.index = 0; + this.program = []; + this.program.symbols = {}; + this.symbols = this.program.symbols; + this.warnings = []; + this.compileFromHere([]); + this.addCommand({ + domain: `core`, + keyword: `exit`, + lino: this.getLino(), + next: 0 + }); + // console.log('Symbols: ' + JSON.stringify(this.symbols, null, 2)); + for (const symbol in this.symbols) { + const record = this.program[this.symbols[symbol].pc]; + if (record.isSymbol && !record.used && !record.exporter) { + console.log(`Symbol '${record.name}' has not been used.`); + } + } + return this.program; + } +}; +// eslint-disable-next-line no-unused-vars +const EasyCoder_Condition = { + + name: `EasyCoder_Condition`, + + compile: (compiler) => { + // See if any of the domains can handle it + compiler.mark(); + for (const domainName of Object.keys(compiler.domain)) { + // console.log(`Try domain '${domainName}' for condition`); + const domain = compiler.domain[domainName]; + const code = domain.condition.compile(compiler); + if (code) { + return { + domain: name, + ...code + }; + } + compiler.rewind(); + } + }, + + // runtime + + test: (program, condition) => { + const handler = program.domain[condition.domain]; + return handler.condition.test(program, condition); + } +}; +const EasyCoder_Core = { + + name: `EasyCoder_Core`, + + Add: { + + compile: compiler => { + const lino = compiler.getLino(); + compiler.next(); + // Get the (first) value + const value1 = compiler.getValue(); + if (compiler.tokenIs(`to`)) { + compiler.next(); + // Check if a value holder is next + if (compiler.isSymbol()) { + const symbol = compiler.getSymbol(); + const variable = compiler.getCommandAt(symbol.pc); + if (variable.isVHolder) { + if (compiler.peek() === `giving`) { + // This variable must be treated as a second value + const value2 = compiler.getValue(); + compiler.next(); + const target = compiler.getToken(); + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `add`, + lino, + value1, + value2, + target + }); + } else { + // Here the variable is the target. + const target = compiler.getToken(); + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `add`, + lino, + value1, + target + }); + } + return true; + } + compiler.warning(`core 'add': Expected value holder`); + } else { + // Here we have 2 values so 'giving' must come next + const value2 = compiler.getValue(); + if (compiler.tokenIs(`giving`)) { + compiler.next(); + const target = compiler.getToken(); + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `add`, + lino, + value1, + value2, + target + }); + return true; + } + compiler.warning(`core 'add'': Expected "giving"`); + } + } + return false; + }, + + // runtime + + run: program => { + const command = program[program.pc]; + const value1 = command.value1; + const value2 = command.value2; + const target = program.getSymbolRecord(command.target); + if (target.isVHolder) { + const value = target.value[target.index]; + if (value2) { + const result = program.getValue(value2) + + program.getValue(value1); + target.value[target.index] = { + type: `constant`, + numeric: true, + content: result + }; + } else { + if (!value.numeric && isNaN(value.content)) { + program.nonNumericValueError(command.lino); + } + const result = parseInt(value.content) + parseInt(program.getValue(value1)); + target.value[target.index] = { + type: `constant`, + numeric: true, + content: result + }; + } + } else { + program.variableDoesNotHoldAValueError(command.lino, target.name); + } + return command.pc + 1; + } + }, + + Alias: { + + compile: compiler => { + const lino = compiler.getLino(); + compiler.next(); + if (compiler.isSymbol()) { + const alias = compiler.getToken(); + compiler.next(); + if (compiler.tokenIs(`to`)) { + compiler.next(); + if (compiler.isSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + symbolRecord.used = true; + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `alias`, + lino, + alias, + symbol: symbolRecord.name + }); + return true; + } + } + } + return false; + }, + + run: program => { + const command = program[program.pc]; + const aliasPc = program.symbols[command.alias].pc; + const aliasRecord = program[aliasPc]; + const symbolRecord = program.getSymbolRecord(command.symbol); + program[aliasPc] = { + pc: aliasRecord.pc, + domain: symbolRecord.domain, + keyword: symbolRecord.keyword, + lino: aliasRecord.lino, + name: aliasRecord.name, + alias: command.symbol + }; + return command.pc + 1; + } + }, + + Append: { + + compile: compiler => { + const lino = compiler.getLino(); + const value = compiler.getNextValue(); + if (compiler.tokenIs(`to`)) { + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + if (symbolRecord.isVHolder) { + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `append`, + lino, + value, + select: symbolRecord.name + }); + return true; + } + } + } + return false; + }, + + run: program => { + const command = program[program.pc]; + const array = program.getSymbolRecord(command.select); + try { + const v = program.getValue(command.value); + const value = [`{`, `[`].includes(v[0]) ? JSON.parse(v) : v; + const item = array.value[array.index]; + let a = item.content; + if (a) { + a = JSON.parse(a); + } else { + a = []; + } + a.push(value); + item.content = JSON.stringify(a); + return command.pc + 1; + } catch (err) { + program.runtimeError(command.lino, `JSON: Unable to parse value`); + return false; + } + } + }, + + Begin: { + + compile: compiler => { + compiler.next(); + compiler.compileFromHere([`end`]); + return true; + }, + + run: program => { + return program[program.pc].pc + 1; + } + }, + + Callback: { + + compile: compiler => { + compiler.compileVariable(`core`, `callback`); + return true; + }, + + run: program => { + return program[program.pc].pc + 1; + } + }, + + Clear: { + + compile: compiler => { + const lino = compiler.getLino(); + compiler.next(); + if (compiler.isSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + if (symbolRecord.isVHolder) { + const symbol = compiler.getToken(); + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `clear`, + lino, + symbol + }); + return true; + } + compiler.warning(`'Variable '${symbolRecord.name}' does not hold a value`); + } + return false; + }, + + run: program => { + const command = program[program.pc]; + const symbol = program.getSymbolRecord(command.symbol); + if (symbol.isVHolder) { + const handler = program.domain[symbol.domain]; + handler.value.put(symbol, { + type: `boolean`, + content: false + }); + command.numeric = false; + } else { + program.variableDoesNotHoldAValueError(command.lino, symbol.name); + } + return command.pc + 1; + } + }, + + Close: { + + compile: compiler => { + const lino = compiler.getLino(); + if (compiler.nextIsSymbol()) { + const moduleRecord = compiler.getSymbolRecord(); + if (moduleRecord.keyword === `module`) { + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `close`, + lino, + module: moduleRecord.name + }); + return true; + } + } + return false; + }, + + run: program => { + const command = program[program.pc]; + const moduleRecord = program.getSymbolRecord(command.module); + const p = EasyCoder.scripts[moduleRecord.program]; + p.run(p.onClose); + return command.pc + 1; + } + }, + + Continue: { + + compile: compiler => { + compiler.next(); + compiler.continue = true; + return true; + } + }, + + Debug: { + + compile: compiler => { + const lino = compiler.getLino(); + if (compiler.nextTokenIs(`program`)) { + compiler.next(); + if ([`item`, `pc`].includes(compiler.getToken())) { + const item = compiler.getNextValue(); + compiler.addCommand({ + domain: `core`, + keyword: `debug`, + lino, + item + }); + return true; + } + compiler.addCommand({ + domain: `core`, + keyword: `debug`, + lino, + item: `program` + }); + return true; + } else if (compiler.tokenIs(`symbols`)) { + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `debug`, + lino, + item: `symbols` + }); + return true; + } else if (compiler.tokenIs(`symbol`)) { + const name = compiler.nextToken(); + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `debug`, + lino, + item: `symbol`, + name + }); + return true; + } else if (compiler.tokenIs(`step`)) { + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `debug`, + lino, + item: `step` + }); + return true; + } + return false; + }, + + run: program => { + const command = program[program.pc]; + const item = command.item; + switch (item) { + case `symbols`: + console.log(`Symbols: ${JSON.stringify(program.symbols, null, 2)}`); + break; + case `symbol`: + const record = program.getSymbolRecord(command.name); + const exporter = record.exporter.script; + delete record.exporter; + console.log(`Symbol: ${JSON.stringify(record, null, 2)}`); + record.exporter.script = exporter; + break; + case `step`: + program.debugStep = true; + break; + case `program`: + console.log(`Debug program: ${JSON.stringify(program, null, 2)}`); + break; + default: + if (item.content >= 0) { + console.log(`Debug item ${item.content}: ${JSON.stringify(program[item.content], null, 2)}`); + } + break; + } + return command.pc + 1; + } + }, + + Decode: { + + compile: compiler => { + const lino = compiler.getLino(); + if (compiler.nextIsSymbol()) { + const symbol = compiler.getToken(); + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `decode`, + lino, + symbol + }); + return true; + } + return false; + }, + + run: program => { + const command = program[program.pc]; + const target = program.getSymbolRecord(command.symbol); + if (target.isVHolder) { + const content = program.getValue(target.value[target.index]); + target.value[target.index] = { + type: `constant`, + numeric: false, + content: program.decode(content) + }; + command.numeric = false; + } else { + program.variableDoesNotHoldAValueError(command.lino, target.name); + } + return command.pc + 1; + } + }, + + Divide: { + + compile: compiler => { + const lino = compiler.getLino(); + var target; + if (compiler.nextIsSymbol()) { + // It may be the target + const symbol = compiler.getSymbol(); + target = compiler.getCommandAt(symbol.pc).name; + } + // Get the value even if we have a target + const value1 = compiler.getValue(); + if (compiler.tokenIs(`by`)) { + compiler.next(); + } + // The next item is always a value + const value2 = compiler.getValue(); + // If we now have 'giving' then the target follows + if (compiler.tokenIs(`giving`)) { + compiler.next(); + // Get the target + if (compiler.isSymbol()) { + const symbol = compiler.getSymbol(); + target = compiler.getCommandAt(symbol.pc).name; + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `divide`, + lino, + value1, + value2, + target + }); + return true; + } + compiler.warning(`core 'divide'': Expected value holder`); + } else { + // Here we should already have the target. + if (typeof target === `undefined`) { + compiler.warning(`core 'divide': No target variable given`); + } + compiler.addCommand({ + domain: `core`, + keyword: `divide`, + lino, + value2, + target + }); + return true; + } + return false; + }, + + run: program => { + const command = program[program.pc]; + const value1 = command.value1; + const value2 = command.value2; + const target = program.getSymbolRecord(command.target); + if (target.isVHolder) { + const value = target.value[target.index]; + if (value1) { + const result = program.getValue(value1) / program.getValue(value2); + target.value[target.index] = { + type: `constant`, + numeric: true, + content: Math.trunc(result) + }; + } else { + if (!value.numeric && isNaN(value.content)) { + program.nonNumericValueError(command, lino); + } + const result = parseInt(value.content) / parseInt(program.getValue(value2)); + target.value[target.index] = { + type: `constant`, + numeric: true, + content: Math.trunc(result) + }; + } + } else { + program.variableDoesNotHoldAValueError(command.lino, target.name); + } + return command.pc + 1; + } + }, + + Dummy: { + + compile: compiler => { + const lino = compiler.getLino(); + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `dummy`, + lino + }); + return true; + }, + + run: program => { + return program[program.pc].pc + 1; + } + }, + + Encode: { + + compile: compiler => { + const lino = compiler.getLino(); + compiler.next(); + if (compiler.isSymbol()) { + const symbol = compiler.getToken(); + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `encode`, + lino, + symbol + }); + return true; + } + return false; + }, + + run: program => { + const command = program[program.pc]; + const target = program.getSymbolRecord(command.symbol); + if (target.isVHolder) { + const content = program.getValue(target.value[target.index]); + target.value[target.index] = { + type: `constant`, + numeric: false, + content: program.encode(content) + }; + command.numeric = false; + } else { + program.variableDoesNotHoldAValueError(command.lino, target.name); + } + return command.pc + 1; + } + }, + + End: { + + compile: compiler => { + compiler.next(); + return true; + }, + + run: () => { + return 0; + } + }, + + Exit: { + + compile: compiler => { + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `exit` + }); + return true; + }, + + run: program => { + let parent = EasyCoder.scripts[program.parent]; + let unblocked = program.unblocked; + program.exit(); + if (!unblocked && parent) { + parent.run(parent.nextPc); + parent.nextPc = 0; + } + return 0; + } + }, + + Filter: { + + compile: compiler => { + const lino = compiler.getLino(); + if (compiler.nextIsSymbol()) { + const arrayRecord = compiler.getSymbolRecord(); + if (compiler.nextTokenIs(`with`)) { + const func = compiler.nextToken(); + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `filter`, + lino, + array: arrayRecord.name, + func + }); + return true; + } + } + return false; + }, + + run: program => { + const command = program[program.pc]; + const variable = program.getSymbolRecord(command.array); + const value = variable.value[variable.index].content; + const func = program.getSymbolRecord(command.func).pc; + try { + const array = JSON.parse(value); + const result = array.filter(function (a) { + variable.a = a; + program.run(func); + return variable.v; + }); + variable.value[variable.index].content = JSON.stringify(result); + } catch (err) { + program.runtimeError(command.lino, `Can't parse this array`); + } + return command.pc + 1; + } + }, + + Fork: { + + compile: compiler => { + const lino = compiler.getLino(); + compiler.next(); + if (compiler.nextTokenIs(`to`)) { + compiler.next(); + } + const label = compiler.getToken(); + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `fork`, + lino, + label + }); + return true; + }, + + run: program => { + const command = program[program.pc]; + try { + program.run(program.symbols[command.label].pc); + } catch (err) { + console.log(err.message); + alert(err.message); + } + return command.pc + 1; + } + }, + + Go: { + + compile: compiler => { + const lino = compiler.getLino(); + if (compiler.nextTokenIs(`to`)) { + compiler.next(); + } + const label = compiler.getToken(); + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `go`, + lino, + label + }); + return true; + }, + + run: program => { + const command = program[program.pc]; + if (command.label) { + if (program.verifySymbol(command.label)) { + const pc = program.symbols[command.label]; + if (pc) { + return pc.pc; + } + } + program.runtimeError(command.lino, `Unknown symbol '${command.label}'`); + return 0; + } + return command.goto; + } + }, + + Gosub: { + + compile: compiler => { + const lino = compiler.getLino(); + if (compiler.nextTokenIs(`to`)) { + compiler.next(); + } + const label = compiler.getToken(); + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `gosub`, + lino, + label + }); + return true; + }, + + run: program => { + const command = program[program.pc]; + if (program.verifySymbol(command.label)) { + program.stack.push(program.pc + 1); + return program.symbols[command.label].pc; + } + program.runtimeError(command.lino, `Unknown symbol '${command.label}'`); + return 0; + } + }, + + If: { + + compile: compiler => { + const lino = compiler.getLino(); + compiler.next(); + const condition = compiler.condition.compile(compiler); + const pc = compiler.getPc(); + compiler.addCommand({ + domain: `core`, + keyword: `if`, + lino, + condition + }); + // Get the 'then' code + compiler.compileOne(); + if (!compiler.getToken()) { + compiler.getCommandAt(pc).else = compiler.getPc(); + return true; + } + if (compiler.tokenIs(`else`)) { + const goto = compiler.getPc(); + // Add a 'goto' to skip the 'else' + compiler.addCommand({ + domain: `core`, + keyword: `goto`, + lino, + goto: 0 + }); + // Fixup the link to the 'else' branch + compiler.getCommandAt(pc).else = compiler.getPc(); + // Process the 'else' branch + compiler.next(); + // Add the 'else' branch + compiler.compileOne(true); + // Fixup the 'goto' + compiler.getCommandAt(goto).goto = compiler.getPc(); + } else { + // We're at the next command + compiler.getCommandAt(pc).else = compiler.getPc(); + } + return true; + }, + + run: program => { + const command = program[program.pc]; + const condition = command.condition; + const test = program.condition.test(program, condition); + if (test) { + return command.pc + 1; + } + return command.else; + } + }, + + Import: { + + compile: compiler => { + const imports = compiler.imports; + let caller = EasyCoder.scripts[imports.caller]; + const program = compiler.getProgram(); + if (imports.length) { + for (const name of imports) { + let symbolRecord = caller.getSymbolRecord(name); + const thisType = compiler.nextToken(); + const exportedType = symbolRecord.keyword; + if (thisType === exportedType) { + const command = compiler.compileVariable(symbolRecord.domain, exportedType, true); + const newRecord = program[compiler.getSymbols()[command.name].pc]; + newRecord.element = symbolRecord.element; + newRecord.exporter = symbolRecord.exporter ? symbolRecord.exporter : caller.script; + newRecord.exportedName = symbolRecord.name; + newRecord.extra = symbolRecord.extra; + newRecord.isVHolder = symbolRecord.isVHolder; + if (symbolRecord.program) { + newRecord.program = symbolRecord.program.script; + } + newRecord.imported = true; + if (!compiler.tokenIs(`and`)) { + break; + } + } else { + throw new Error(`Mismatched import variable type for '${symbolRecord.name}'`); + } + } + if (compiler.tokenIs(`and`)) { + throw new Error(`Imports do not match exports`); + } + } else { + compiler.next(); + } + return true; + }, + + run: program => { + const command = program[program.pc]; + return command.pc + 1; + } + }, + + Index: { + + compile: compiler => { + const lino = compiler.getLino(); + // get the variable + if (compiler.nextIsSymbol(true)) { + const symbol = compiler.getToken(); + if (compiler.nextTokenIs(`to`)) { + // get the value + const value = compiler.getNextValue(); + compiler.addCommand({ + domain: `core`, + keyword: `index`, + lino, + symbol, + value + }); + return true; + } + } + return false; + }, + + run: program => { + const command = program[program.pc]; + const symbol = program.getSymbolRecord(command.symbol); + const index = program.getValue(command.value); + if (index >= symbol.elements) { + program.runtimeError(command.lino, + `Array index ${index} is out of range for '${symbol.name}'`); + } + symbol.index = index; + if (symbol.imported) { + const exporterRecord = EasyCoder.symbols[symbol.exporter].getSymbolRecord(symbol.exportedName); + exporterRecord.index = index; + } + return command.pc + 1; + } + }, + + Load: { + + compile: compiler => { + const lino = compiler.getLino(); + const type = compiler.nextToken(); + switch (type) { + case `plugin`: + const name = compiler.getNextValue(); + compiler.addCommand({ + domain: `core`, + keyword: `load`, + lino, + name + }); + return true; + } + return false; + }, + + run: program => { + const command = program[program.pc]; + const name = program.getValue(command.name); + switch (command.keyword) { + case `load`: + if (program.checkPlugin(name)) { + return command.pc + 1; + } + EasyCoder_Plugins.getLocalPlugin( + program.getPluginsPath, + name, + program.getPlugin, + program.addLocalPlugin, + function () { + program.run(command.pc + 1); + }); + return 0; + } + } + }, + + Module: { + + compile: compiler => { + compiler.compileVariable(`core`, `module`); + return true; + }, + + run: program => { + return program[program.pc].pc + 1; + } + }, + + Multiply: { + + compile: compiler => { + const lino = compiler.getLino(); + compiler.next(); + var target; + if (compiler.isSymbol()) { + // It may be the target + const symbol = compiler.getSymbol(); + target = compiler.getCommandAt(symbol.pc).name; + } + // Get the value even if we have a target + const value1 = compiler.getValue(); + if (compiler.tokenIs(`by`)) { + compiler.next(); + } + // The next item is always a value + const value2 = compiler.getValue(); + // If we now have 'giving' then the target follows + if (compiler.tokenIs(`giving`)) { + compiler.next(); + // Get the target + if (compiler.isSymbol()) { + const symbol = compiler.getSymbol(); + target = compiler.getCommandAt(symbol.pc).name; + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `multiply`, + lino, + value1, + value2, + target + }); + return true; + } + compiler.warning(`core multiply: Expected value holder`); + } else { + // Here we should already have the target. + if (typeof target === `undefined`) { + compiler.warning(`core multiply: No target variable given`); + } + compiler.addCommand({ + domain: `core`, + keyword: `multiply`, + lino, + value2, + target + }); + return true; + } + return false; + }, + + run: program => { + const command = program[program.pc]; + const value1 = command.value1; + const value2 = command.value2; + const target = program.getSymbolRecord(command.target); + if (target.isVHolder) { + const value = target.value[target.index]; + if (value1) { + const result = program.getValue(value1) * + program.getValue(value2); + target.value[target.index] = { + type: `constant`, + numeric: true, + content: result + }; + } else { + if (!value.numeric && isNaN(value.content)) { + program.nonNumericValueError(command, lino); + } + const result = parseInt(value.content) * parseInt(program.getValue(value2)); + target.value[target.index] = { + type: `constant`, + numeric: true, + content: result + }; + } + } else { + program.variableDoesNotHoldAValueError(command.lino, target.name); + } + return command.pc + 1; + } + }, + + Negate: { + + compile: compiler => { + const lino = compiler.getLino(); + compiler.next(); + if (compiler.isSymbol()) { + const symbol = compiler.getToken(); + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `negate`, + lino, + symbol + }); + return true; + } + return false; + }, + + run: program => { + const command = program[program.pc]; + const symbol = program.getSymbolRecord(command.symbol); + if (symbol.isVHolder) { + symbol.value[symbol.index] = { + type: `constant`, + numeric: true, + content: -symbol.value[symbol.index].content + }; + } else { + program.variableDoesNotHoldAValueError(command.lino, symbol.name); + } + return command.pc + 1; + } + }, + + On: { + + compile: compiler => { + const lino = compiler.getLino(); + const action = compiler.nextToken(); + switch (action) { + case `close`: + case `message`: + case `error`: + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `on`, + lino, + action + }); + return compiler.completeHandler(); + } + if (compiler.isSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + if (symbolRecord.keyword === `callback`) { + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `on`, + lino, + action: symbolRecord.name + }); + return compiler.completeHandler(); + } + } + return false; + }, + + run: program => { + const command = program[program.pc]; + const cb = command.pc + 2; + switch (command.action) { + case `close`: + program.onClose = cb; + break; + case `message`: + program.onMessage = cb; + break; + case `error`: + program.onError = cb; + break; + default: + const callbacklRecord = program.getSymbolRecord(command.action); + if (callbacklRecord) { + callbacklRecord.cb = cb; + } else { + program.runtimeError(command.lino, `Unknown action '${command.action}'`); + return 0; + } + } + return command.pc + 1; + } + }, + + Print: { + + compile: compiler => { + const lino = compiler.getLino(); + compiler.next(); + const value = compiler.getValue(); + compiler.addCommand({ + domain: `core`, + keyword: `print`, + lino, + value + }); + return true; + }, + + run: program => { + const command = program[program.pc]; + const value = program.getFormattedValue(command.value); + console.log(`-> ` + value); + return command.pc + 1; + } + }, + + Put: { + + compile: compiler => { + const lino = compiler.getLino(); + // Get the value + const value = compiler.getNextValue(); + if (compiler.tokenIs(`into`)) { + if (compiler.nextIsSymbol()) { + const target = compiler.getToken(); + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `put`, + lino, + value, + target + }); + return true; + } + compiler.warning(`core:put: No such variable: '${compiler.getToken()}'`); + } + return false; + }, + + // runtime + + run: program => { + const command = program[program.pc]; + const target = program.getSymbolRecord(command.target); + if (!target.isVHolder) { + program.variableDoesNotHoldAValueError(command.lino, target.name); + } + const value = program.evaluate(command.value); + // target.value[target.index] = value; + target.value[target.index] = { + type: value.type, + numeric: value.numeric, + content: value.content + }; + if (target.imported) { + const exporterRecord = EasyCoder.scripts[target.exporter].getSymbolRecord(target.exportedName); + exporterRecord.value[exporterRecord.index] = value; + } + return command.pc + 1; + } + }, + + Replace: { + + compile: compiler => { + const lino = compiler.getLino(); + const original = compiler.getNextValue(); + if (compiler.tokenIs(`with`)) { + const replacement = compiler.getNextValue(); + if (compiler.tokenIs(`in`)) { + if (compiler.nextIsSymbol()) { + const targetRecord = compiler.getSymbolRecord(); + if (targetRecord.isVHolder) { + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `replace`, + lino, + original, + replacement, + target: targetRecord.name + }); + return true; + } else { + throw new Error(`'${targetRecord.name}' does not hold a value`); + } + } + } + } + return false; + }, + + // runtime + + run: program => { + const command = program[program.pc]; + const original = program.getValue(command.original); + const replacement = program.getValue(command.replacement); + const target = program.getSymbolRecord(command.target); + const value = program.getValue(target.value[target.index]); + const content = value.split(original).join(replacement); + target.value[target.index] = { + type: `constant`, + numeric: false, + content + }; + return command.pc + 1; + } + }, + + Require: { + + compile: compiler => { + const lino = compiler.getLino(); + const type = compiler.nextToken(); + if ([`css`, `js`].includes(type)) { + const url = compiler.getNextValue(); + compiler.addCommand({ + domain: `core`, + keyword: `require`, + lino, + type, + url + }); + return true; + } + throw new Error(`File type must be 'css' or 'js'`); + }, + + // runtime + + run: program => { + const command = program[program.pc]; + program.require(command.type, program.getValue(command.url), + function () { + program.run(command.pc + 1); + }); + return 0; + } + }, + + Return: { + + compile: compiler => { + const lino = compiler.getLino(); + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `return`, + lino + }); + return true; + }, + + // runtime + + run: program => { + return program.stack.pop(); + } + }, + + Run: { + + compile: compiler => { + const lino = compiler.getLino(); + const script = compiler.getNextValue(); + const imports = []; + if (compiler.tokenIs(`with`)) { + while (true) { + if (compiler.nextIsSymbol(true)) { + const symbolRecord = compiler.getSymbolRecord(); + imports.push(symbolRecord.name); + compiler.next(); + if (!compiler.tokenIs(`and`)) { + break; + } + } + } + } + let module; + if (compiler.tokenIs(`as`)) { + if (compiler.nextIsSymbol(true)) { + const moduleRecord = compiler.getSymbolRecord(); + // moduleRecord.program = program.script; + compiler.next(); + if (moduleRecord.keyword !== `module`) { + throw new Error(`'${moduleRecord.name}' is not a module`); + } + module = moduleRecord.name; + } + } + let nowait = false; + if (compiler.tokenIs(`nowait`)) { + compiler.next(); + nowait = true; + } + const pc = compiler.getPc(); + compiler.addCommand({ + domain: `core`, + keyword: `run`, + lino, + script, + imports, + module, + nowait, + then: 0 + }); + // Get the 'then' code, if any + if (compiler.tokenIs(`then`)) { + const goto = compiler.getPc(); + // Add a 'goto' to skip the 'then' + compiler.addCommand({ + domain: `core`, + keyword: `goto`, + goto: 0 + }); + // Fixup the link to the 'then' branch + compiler.getCommandAt(pc).then = compiler.getPc(); + // Process the 'then' branch + compiler.next(); + compiler.compileOne(true); + compiler.addCommand({ + domain: `core`, + keyword: `stop` + }); + // Fixup the 'goto' + compiler.getCommandAt(goto).goto = compiler.getPc(); + } + return true; + }, + + // runtime + + run: program => { + program.nextPc = program.pc + 1; + program.runScript(program); + return 0; + } + }, + + Sanitize: { + + compile: compiler => { + const lino = compiler.getLino(); + if (compiler.nextIsSymbol()) { + const name = compiler.getToken(); + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `sanitize`, + lino, + name + }); + return true; + } + return false; + }, + + run: program => { + const command = program[program.pc]; + const symbolRecord = program.getSymbolRecord(command.name); + const value = symbolRecord.value[symbolRecord.index]; + value.content = JSON.stringify(JSON.parse(value.content)); + return command.pc + 1; + } + }, + + Script: { + + compile: compiler => { + const program = compiler.getProgram(); + program.script = compiler.nextToken(); + compiler.script = program.script; + if (EasyCoder.scripts[program.script]) { + delete compiler.script; + throw new Error(`Script '${program.script}' is already running.`); + } + EasyCoder.scripts[program.script] = program; + compiler.next(); + return true; + }, + + run: program => { + return program[program.pc].pc + 1; + } + }, + + Send: { + + compile: compiler => { + const lino = compiler.getLino(); + let message = ``; + if (!compiler.nextTokenIs(`to`)) { + message = compiler.getValue(); + } + if (compiler.tokenIs(`to`)) { + var recipient; + if (compiler.nextTokenIs(`parent`)) { + recipient = `parent`; + } else if (compiler.isSymbol) { + const moduleRecord = compiler.getSymbolRecord(); + if (moduleRecord.keyword !== `module`) { + throw new Error(`'${moduleRecord.name}' is not a module`); + } + recipient = moduleRecord.name; + } + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `send`, + lino, + message, + recipient + }); + } + return true; + }, + + run: program => { + const command = program[program.pc]; + const message = program.getValue(command.message); + if (command.recipient === `parent`) { + if (program.parent) { + const parent = EasyCoder.scripts[program.parent]; + const onMessage = parent.onMessage; + if (onMessage) { + parent.message = message; + parent.run(parent.onMessage); + } + } + } else { + const recipient = program.getSymbolRecord(command.recipient); + if (recipient.program) { + let rprog = EasyCoder.scripts[recipient.program]; + rprog.message = message; + rprog.run(rprog.onMessage); + } + } + return command.pc + 1; + } + }, + + Set: { + + compile: compiler => { + let name; + const lino = compiler.getLino(); + if (compiler.nextIsSymbol()) { + const targetRecord = compiler.getSymbolRecord(); + if (!targetRecord.isVHolder) { + return false; + } + if (compiler.nextTokenIs(`to`)) { + const token = compiler.nextToken(); + if ([`array`, `object`].includes(token)) { + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `set`, + lino, + request: `setVarTo`, + target: targetRecord.name, + type: token + }); + return true; + } + const value = []; + while (true) { + compiler.mark(); + try { + value.push(compiler.getValue()); + } catch (err) { + compiler.rewind(); + break; + } + } + compiler.addCommand({ + domain: `core`, + keyword: `set`, + lino, + request: `setArray`, + target: targetRecord.name, + value + }); + return true; + } + compiler.addCommand({ + domain: `core`, + keyword: `set`, + lino, + request: `setBoolean`, + target: targetRecord.name + }); + return true; + } + switch (compiler.getToken()) { + case `ready`: + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `set`, + lino, + request: `setReady` + }); + return true; + case `element`: + const index = compiler.getNextValue(); + if (compiler.tokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const targetRecord = compiler.getSymbolRecord(); + if (targetRecord.keyword === `variable`) { + if (compiler.nextTokenIs(`to`)) { + const value = compiler.getNextValue(); + compiler.addCommand({ + domain: `core`, + keyword: `set`, + lino, + request: `setElement`, + target: targetRecord.name, + index, + value + }); + return true; + } + } + } + } + break; + case `property`: + name = compiler.getNextValue(); + if (compiler.tokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const targetRecord = compiler.getSymbolRecord(); + if (targetRecord.keyword === `variable`) { + if (compiler.nextTokenIs(`to`)) { + const value = compiler.getNextValue(); + compiler.addCommand({ + domain: `core`, + keyword: `set`, + lino, + request: `setProperty`, + target: targetRecord.name, + name, + value + }); + return true; + } + } + } + } + break; + case `arg`: + name = compiler.getNextValue(); + if (compiler.tokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const targetRecord = compiler.getSymbolRecord(); + if (compiler.nextTokenIs(`to`)) { + const value = compiler.getNextValue(); + compiler.addCommand({ + domain: `core`, + keyword: `set`, + lino, + request: `setArg`, + target: targetRecord.name, + name, + value + }); + return true; + } + } + } + } + if (compiler.tokenIs(`the`)) { + compiler.next(); + } + switch (compiler.getToken()) { + case `elements`: + compiler.next(); + if (compiler.tokenIs(`of`)) { + compiler.next(); + if (!compiler.isSymbol()) { + throw new Error(`Unknown variable '${compiler.getToken()}'`); + } + const symbol = compiler.getToken(); + compiler.next(); + if (compiler.tokenIs(`to`)) { + compiler.next(); + // get the value + const value = compiler.getValue(); + compiler.addCommand({ + domain: `core`, + keyword: `set`, + lino, + request: `setElements`, + symbol, + value + }); + return true; + } + } + break; + case `encoding`: + if (compiler.nextTokenIs(`to`)) { + const encoding = compiler.getNextValue(); + compiler.addCommand({ + domain: `core`, + keyword: `set`, + request: `encoding`, + lino, + encoding + }); + return true; + } + compiler.addWarning(`Unknown encoding option`); + break; + case `payload`: + if (compiler.nextTokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const callbackRecord = compiler.getSymbolRecord(); + if (callbackRecord.keyword === `callback`) { + if (compiler.nextTokenIs(`to`)) { + const payload = compiler.getNextValue(); + compiler.addCommand({ + domain: `core`, + keyword: `set`, + request: `setPayload`, + lino, + callback: callbackRecord.name, + payload + }); + return true; + } + } + } + } + } + return false; + }, + + run: program => { + let targetRecord; + const command = program[program.pc]; + switch (command.request) { + case `setBoolean`: + const target = program.getSymbolRecord(command.target); + if (target.isVHolder) { + target.value[target.index] = { + type: `boolean`, + content: true + }; + command.numeric = false; + } else { + program.variableDoesNotHoldAValueError(command.lino, target.name); + } + break; + case `setReady`: + let parent = EasyCoder.scripts[program.parent]; + if (parent) { + parent.run(parent.nextPc); + parent.nextPc = 0; + program.unblocked = true; + } + break; + case `setArray`: + targetRecord = program.getSymbolRecord(command.target); + targetRecord.elements = command.value.length; + targetRecord.value = command.value; + break; + case `encoding`: + program.encoding = program.getValue(command.encoding); + break; + case `setElements`: + const symbol = program.getSymbolRecord(command.symbol); + const oldCount = symbol.elements; + symbol.elements = program.getValue(command.value); + symbol.index = 0; + if (symbol.elements > oldCount) { + for (var n = oldCount; n < symbol.elements; n++) { + symbol.value.push({}); + symbol.element.push({}); + } + } else { + symbol.value = symbol.value.slice(0, symbol.elements); + symbol.element = symbol.element.slice(0, symbol.elements); + } + break; + case `setElement`: + targetRecord = program.getSymbolRecord(command.target); + const index = program.getValue(command.index); + const elements = JSON.parse(program.getValue(targetRecord.value[targetRecord.index])); + const value = program.getValue(command.value); + elements[index] = JSON.parse(value); + targetRecord.value[targetRecord.index].content = JSON.stringify(elements); + break; + case `setProperty`: + // This is the name of the property + const itemName = program.getValue(command.name); + // This is the value of the property + let itemValue = program.getValue(command.value); + if (program.isJsonString(itemValue)) { + itemValue = JSON.parse(itemValue); + } + targetRecord = program.getSymbolRecord(command.target); + let targetValue = targetRecord.value[targetRecord.index]; + // Get the existing JSON + if (!targetValue.numeric) { + let content = targetValue.content; + if (content === ``) { + content = {}; + } + else if (program.isJsonString(content)) { + content = JSON.parse(content); + } + // Set the property + content[itemName] = itemValue; + // Put it back + content = JSON.stringify(content); + targetRecord.value[targetRecord.index] = { + type: `constant`, + numeric: false, + content + }; + } + break; + case `setPayload`: + program.getSymbolRecord(command.callback).payload = program.getValue(command.payload); + break; + case `setArg`: + const name = program.getValue(command.name); + targetRecord = program.getSymbolRecord(command.target); + targetRecord[name] = program.getValue(command.value); + break; + case `setVarTo`: + targetRecord = program.getSymbolRecord(command.target); + targetRecord.value[targetRecord.index] = { + type: `constant`, + numeric: false, + content: command.type === `array` ? `[]` : `{}` + }; + break; + default: + break; + } + return command.pc + 1; + } + }, + + Sort: { + + compile: compiler => { + const lino = compiler.getLino(); + if (compiler.nextIsSymbol()) { + const arrayRecord = compiler.getSymbolRecord(); + if (compiler.nextTokenIs(`with`)) { + const func = compiler.nextToken(); + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `sort`, + lino, + array: arrayRecord.name, + func + }); + return true; + } + } + return false; + }, + + run: program => { + const command = program[program.pc]; + const variable = program.getSymbolRecord(command.array); + const value = variable.value[variable.index].content; + const func = program.getSymbolRecord(command.func).pc; + try { + const array = JSON.parse(value); + array.sort(function (a, b) { + variable.a = a; + variable.b = b; + program.run(func); + return variable.v; + }); + variable.value[variable.index].content = JSON.stringify(array); + } catch (err) { + program.runtimeError(command.lino, `Can't parse this array`); + } + return command.pc + 1; + } + }, + + Split: { + + compile: compiler => { + const lino = compiler.getLino(); + item = compiler.getNextValue(); + let on = `\n`; + if (compiler.tokenIs(`on`)) { + on = compiler.getNextValue(); + } + if ([`giving`, `into`].includes(compiler.getToken())) { + if (compiler.nextIsSymbol()) { + const targetRecord = compiler.getSymbolRecord(); + if (targetRecord.keyword === `variable`) { + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `split`, + lino, + item, + on, + target: targetRecord.name + }); + return true; + } + } + } + return false; + }, + + run: program => { + let command = program[program.pc]; + let content = program.getValue(command.item); + let on = program.getValue(command.on); + content = content.split(on); + let elements = content.length; + targetRecord = program.getSymbolRecord(command.target); + targetRecord.elements = elements; + for (let n = 0; n < elements; n++) { + targetRecord.value[n] = { + type: `constant`, + numeric: false, + content: content[n] + }; + } + targetRecord.index = 0; + return command.pc + 1; + } + }, + + Stop: { + + compile: compiler => { + const lino = compiler.getLino(); + compiler.next(); + if (compiler.more() && compiler.isSymbol() && !compiler.getToken().endsWith(`:`)) { + const symbolRecord = compiler.getSymbolRecord(); + if (symbolRecord.keyword === `module`) { + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `stop`, + lino, + name: symbolRecord.name + }); + return true; + } else { + return false; + } + } + compiler.addCommand({ + domain: `core`, + keyword: `stop`, + lino, + next: 0 + }); + return true; + }, + + run: program => { + const command = program[program.pc]; + if (command.name) { + const symbolRecord = program.getSymbolRecord(command.name); + EasyCoder.scripts[symbolRecord.program].exit(); + symbolRecord.program = null; + } else { + return 0; + } + return command.pc + 1; + } + }, + + Take: { + + compile: compiler => { + const lino = compiler.getLino(); + compiler.next(); + // Get the (first) value + const value1 = compiler.getValue(); + if (compiler.tokenIs(`from`)) { + compiler.next(); + if (compiler.isSymbol()) { + const symbol = compiler.getSymbol(); + const variable = compiler.getCommandAt(symbol.pc); + if (variable.isVHolder) { + if (compiler.peek() === `giving`) { + // This variable must be treated as a second value + const value2 = compiler.getValue(); + compiler.next(); + const target = compiler.getToken(); + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `take`, + lino, + value1, + value2, + target + }); + } else { + // Here the variable is the target. + const target = compiler.getToken(); + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `take`, + lino, + value1, + target + }); + } + return true; + } else { + compiler.warning(`core 'take'': Expected value holder`); + } + } else { + // Here we have 2 values so 'giving' must come next + const value2 = compiler.getValue(); + if (compiler.tokenIs(`giving`)) { + compiler.next(); + const target = compiler.getToken(); + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `take`, + lino, + value1, + value2, + target + }); + return true; + } else { + compiler.warning(`core 'take'': Expected "giving"`); + } + } + } + return false; + }, + + run: program => { + const command = program[program.pc]; + const value1 = command.value1; + const value2 = command.value2; + const target = program.getSymbolRecord(command.target); + if (target.isVHolder) { + const value = target.value[target.index]; + if (value2) { + const result = program.getValue(value2) - + program.getValue(value1); + target.value[target.index] = { + type: `constant`, + numeric: true, + content: result + }; + } else { + if (!value.numeric && isNaN(value.content)) { + program.nonNumericValueError(command.lino); + } + const result = parseInt(program.getValue(value)) - parseInt(program.getValue(value1)); + target.value[target.index] = { + type: `constant`, + numeric: true, + content: result + }; + } + } else { + program.variableDoesNotHoldAValueError(command.lino, target.name); + } + return command.pc + 1; + } + }, + + Toggle: { + + compile: compiler => { + const lino = compiler.getLino(); + compiler.next(); + if (compiler.isSymbol()) { + const symbolPc = compiler.getSymbolPc(); + compiler.next(); + compiler.addCommand({ + domain: `core`, + keyword: `toggle`, + lino, + symbol: symbolPc + }); + return true; + } + return false; + }, + + run: program => { + const command = program[program.pc]; + const symbol = program[command.symbol]; + if (symbol.isVHolder) { + const handler = program.domain[symbol.domain]; + const content = handler.value.get(program, symbol.value[symbol.index]).content; + handler.value.put(symbol, { + type: `boolean`, + content: !content + }); + } else { + program.variableDoesNotHoldAValueError(command.lino, symbol.name); + } + return command.pc + 1; + } + }, + + Variable: { + + compile: compiler => { + compiler.compileVariable(`core`, `variable`, true); + return true; + }, + + run: program => { + return program[program.pc].pc + 1; + } + }, + + Wait: { + + compile: compiler => { + const lino = compiler.getLino(); + compiler.next(); + const value = compiler.getValue(compiler); + const scale = compiler.getToken(); + let multiplier = 1000; + switch (scale) { + case `milli`: + case `millis`: + compiler.next(); + multiplier = 1; + break; + case `tick`: + case `ticks`: + compiler.next(); + multiplier = 10; + break; + case `second`: + case `seconds`: + compiler.next(); + multiplier = 1000; + break; + case `minute`: + case `minutes`: + compiler.next(); + multiplier = 60000; + break; + } + compiler.addCommand({ + domain: `core`, + keyword: `wait`, + lino, + value, + multiplier + }); + return true; + }, + + run: program => { + const command = program[program.pc]; + const value = program.getValue(command.value); + setTimeout(function () { + if (program.run) { + program.run(command.pc + 1); + } + }, value * command.multiplier); + return 0; + } + }, + + While: { + + compile: compiler => { + const lino = compiler.getLino(); + compiler.next(); + const condition = compiler.getCondition(); + const pc = compiler.getPc(); + compiler.addCommand({ + domain: `core`, + keyword: `while`, + lino, + condition + }); + // Skip when test fails + const skip = compiler.getPc(); + compiler.addCommand({ + domain: `core`, + keyword: `goto`, + goto: 0 + }); + // Do the body + compiler.compileOne(); + // Repeat the test + compiler.addCommand({ + domain: `core`, + keyword: `goto`, + goto: pc + }); + // Fixup the 'goto' on completion + compiler.getCommandAt(skip).goto = compiler.getPc(); + return true; + }, + + run: program => { + const command = program[program.pc]; + const condition = command.condition; + const test = program.condition.test(program, condition); + if (test) { + return program.pc + 2; + } + return program.pc + 1; + } + }, + + getHandler: (name) => { + switch (name) { + case `add`: + return EasyCoder_Core.Add; + case `alias`: + return EasyCoder_Core.Alias; + case `append`: + return EasyCoder_Core.Append; + case `begin`: + return EasyCoder_Core.Begin; + case `callback`: + return EasyCoder_Core.Callback; + case `clear`: + return EasyCoder_Core.Clear; + case `close`: + return EasyCoder_Core.Close; + case `continue`: + return EasyCoder_Core.Continue; + case `debug`: + return EasyCoder_Core.Debug; + case `decode`: + return EasyCoder_Core.Decode; + case `divide`: + return EasyCoder_Core.Divide; + case `dummy`: + return EasyCoder_Core.Dummy; + case `encode`: + return EasyCoder_Core.Encode; + case `end`: + return EasyCoder_Core.End; + case `exit`: + return EasyCoder_Core.Exit; + case `filter`: + return EasyCoder_Core.Filter; + case `fork`: + return EasyCoder_Core.Fork; + case `go`: + case `goto`: + return EasyCoder_Core.Go; + case `gosub`: + return EasyCoder_Core.Gosub; + case `if`: + return EasyCoder_Core.If; + case `import`: + return EasyCoder_Core.Import; + case `index`: + return EasyCoder_Core.Index; + case `load`: + return EasyCoder_Core.Load; + case `module`: + return EasyCoder_Core.Module; + case `multiply`: + return EasyCoder_Core.Multiply; + case `negate`: + return EasyCoder_Core.Negate; + case `on`: + return EasyCoder_Core.On; + case `print`: + return EasyCoder_Core.Print; + case `put`: + return EasyCoder_Core.Put; + case `replace`: + return EasyCoder_Core.Replace; + case `require`: + return EasyCoder_Core.Require; + case `return`: + return EasyCoder_Core.Return; + case `run`: + return EasyCoder_Core.Run; + case `sanitize`: + return EasyCoder_Core.Sanitize; + case `script`: + return EasyCoder_Core.Script; + case `send`: + return EasyCoder_Core.Send; + case `set`: + return EasyCoder_Core.Set; + case `sort`: + return EasyCoder_Core.Sort; + case `split`: + return EasyCoder_Core.Split; + case `stop`: + return EasyCoder_Core.Stop; + case `take`: + return EasyCoder_Core.Take; + case `toggle`: + return EasyCoder_Core.Toggle; + case `variable`: + return EasyCoder_Core.Variable; + case `wait`: + return EasyCoder_Core.Wait; + case `while`: + return EasyCoder_Core.While; + default: + return false; + } + }, + + run: program => { + // Look up the appropriate handler and call it + // If it's not there throw an error + const command = program[program.pc]; + const handler = EasyCoder_Core.getHandler(command.keyword); + if (!handler) { + program.runtimeError(command.lino, + `Unknown keyword '${command.keyword}' in 'core' package`); + } + return handler.run(program); + }, + + isNegate: (compiler) => { + const token = compiler.getToken(); + if (token === `not`) { + compiler.next(); + return true; + } + return false; + }, + + value: { + + compile: compiler => { + if (compiler.isSymbol()) { + const name = compiler.getToken(); + const symbolRecord = compiler.getSymbolRecord(); + switch (symbolRecord.keyword) { + case `module`: + compiler.next(); + return { + domain: `core`, + type: `module`, + name + }; + case `variable`: + const type = compiler.nextToken(); + if ([`format`, `modulo`].includes(type)) { + const value = compiler.getNextValue(); + return { + domain: `core`, + type, + name, + value + }; + } + return { + domain: `core`, + type: `symbol`, + name + }; + } + return null; + } + + var token = compiler.getToken(); + if (token === `true`) { + compiler.next(); + return { + domain: `core`, + type: `boolean`, + content: true + }; + } + if (token === `false`) { + compiler.next(); + return { + domain: `core`, + type: `boolean`, + content: false + }; + } + if (token === `random`) { + compiler.next(); + const range = compiler.getValue(); + return { + domain: `core`, + type: `random`, + range + }; + } + if (token === `cos`) { + compiler.next(); + const angle_c = compiler.getValue(); + compiler.skip(`radius`); + const radius_c = compiler.getValue(); + return { + domain: `core`, + type: `cos`, + angle_c, + radius_c + }; + } + if (token === `sin`) { + compiler.next(); + const angle_s = compiler.getValue(); + compiler.skip(`radius`); + const radius_s = compiler.getValue(); + return { + domain: `core`, + type: `sin`, + angle_s, + radius_s + }; + } + if (token === `tan`) { + compiler.next(); + const angle_t = compiler.getValue(); + compiler.skip(`radius`); + const radius_t = compiler.getValue(); + return { + domain: `core`, + type: `tan`, + angle_t, + radius_t + }; + } + if ([`now`, `today`, `newline`, `break`, `empty`, `uuid`].includes(token)) { + compiler.next(); + return { + domain: `core`, + type: token + }; + } + if (token === `date`) { + const value = compiler.getNextValue(); + return { + domain: `core`, + type: `date`, + value + }; + } + if ([`encode`, `decode`, `lowercase`, `hash`, `reverse`].includes(token)) { + compiler.next(); + const value = compiler.getValue(); + return { + domain: `core`, + type: token, + value + }; + } + if (token === `element`) { + const element = compiler.getNextValue(); + if (compiler.tokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + compiler.next(); + if (symbolRecord.keyword === `variable`) { + return { + domain: `core`, + type: `element`, + element, + symbol: symbolRecord.name + }; + } + } + } + return null; + } + if (token === `property`) { + const property = compiler.getNextValue(); + if (compiler.tokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + compiler.next(); + if (symbolRecord.keyword === `variable`) { + return { + domain: `core`, + type: `property`, + property, + symbol: symbolRecord.name + }; + } + } + } + return null; + } + if (token === `arg`) { + const value = compiler.getNextValue(); + if (compiler.tokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const target = compiler.getSymbolRecord(); + compiler.next(); + return { + domain: `core`, + type: `arg`, + value, + target: target.name + }; + } + } + } + if ([`character`, `char`].includes(token)) { + let index = compiler.getNextValue(); + if (compiler.tokenIs(`of`)) { + let value = compiler.getNextValue(); + return { + domain: `core`, + type: `char`, + index, + value + }; + } + } + if (compiler.tokenIs(`the`)) { + compiler.next(); + } + const type = compiler.getToken(); + switch (type) { + case `elements`: + if (compiler.nextTokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const name = compiler.getToken(); + compiler.next(); + return { + domain: `core`, + type, + name + }; + } + } + break; + case `index`: + if (compiler.nextTokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + if (compiler.peek() === `in`) { + const value1 = compiler.getValue(); + const value2 = compiler.getNextValue(); + return { + domain: `core`, + type: `indexOf`, + value1, + value2 + }; + } else { + const name = compiler.getToken(); + compiler.next(); + return { + domain: `core`, + type, + name + }; + } + } else { + const value1 = compiler.getValue(); + if (compiler.tokenIs(`in`)) { + const value2 = compiler.getNextValue(); + return { + domain: `core`, + type: `indexOf`, + value1, + value2 + }; + } + } + } + break; + case `value`: + if (compiler.nextTokenIs(`of`)) { + compiler.next(); + const value = compiler.getValue(); + return { + domain: `core`, + type: `valueOf`, + value + }; + } + break; + case `length`: + if (compiler.nextTokenIs(`of`)) { + compiler.next(); + const value = compiler.getValue(); + return { + domain: `core`, + type: `lengthOf`, + value + }; + } + break; + case `left`: + case `right`: + try { + const count = compiler.getNextValue(); + if (compiler.tokenIs(`of`)) { + const value = compiler.getNextValue(); + return { + domain: `core`, + type, + count, + value + }; + } + } catch (err) { + return null; + } + break; + case `from`: + const from = compiler.getNextValue(); + const to = compiler.tokenIs(`to`) ? compiler.getNextValue() : null; + if (compiler.tokenIs(`of`)) { + const value = compiler.getNextValue(); + return { + domain: `core`, + type, + from, + to, + value + }; + } + break; + case `position`: + let nocase = false; + if (compiler.nextTokenIs(`nocase`)) { + nocase = true; + compiler.next(); + } + if (compiler.tokenIs(`of`)) { + var last = false; + if (compiler.nextTokenIs(`the`)) { + if (compiler.nextTokenIs(`last`)) { + compiler.next(); + last = true; + } + } + const needle = compiler.getValue(); + if (compiler.tokenIs(`in`)) { + const haystack = compiler.getNextValue(); + return { + domain: `core`, + type: `position`, + needle, + haystack, + last, + nocase + }; + } + } + break; + case `payload`: + if (compiler.nextTokenIs(`of`)) { + if (compiler.nextIsSymbol()) { + const callbackRecord = compiler.getSymbolRecord(); + if (callbackRecord.keyword === `callback`) { + compiler.next(); + return { + domain: `core`, + type: `payload`, + callback: callbackRecord.name + }; + } + } + } + break; + case `message`: + case `error`: + compiler.next(); + return { + domain: `core`, + type + }; + } + return null; + }, + + get: (program, value) => { + switch (value.type) { + case `boolean`: + return { + type: `boolean`, + numeric: false, + content: value.content + }; + case `elements`: + return { + type: `constant`, + numeric: true, + content: program.getSymbolRecord(value.name).elements + }; + case `index`: + return { + type: `constant`, + numeric: true, + content: program.getSymbolRecord(value.name).index + }; + case `random`: + const range = program.evaluate(value.range); + return { + type: `constant`, + numeric: true, + content: Math.floor((Math.random() * range.content)) + }; + case `cos`: + const angle_c = program.getValue(value.angle_c); + const radius_c = program.getValue(value.radius_c); + return { + type: `constant`, + numeric: true, + content: parseInt(Math.cos(parseFloat(angle_c) * 0.01745329) * radius_c, 10) + }; + case `sin`: + const angle_s = program.getValue(value.angle_s); + const radius_s = program.getValue(value.radius_s); + return { + type: `constant`, + numeric: true, + content: parseInt(Math.sin(parseFloat(angle_s) * 0.01745329) * radius_s, 10) + }; + case `tan`: + const angle_t = program.getValue(value.angle_t); + const radius_t = program.getValue(value.radius_t); + return { + type: `constant`, + numeric: true, + content: parseInt(Math.tan(parseFloat(angle_t) * 0.01745329) * radius_t, 10) + }; + case `valueOf`: + const v = parseInt(program.getValue(value.value)); + return { + type: `constant`, + numeric: true, + content: v ? v : 0 + }; + case `lengthOf`: + return { + type: `constant`, + numeric: true, + content: program.getValue(value.value).length + }; + case `left`: + return { + type: `constant`, + numeric: false, + content: program.getValue(value.value).substr(0, program.getValue(value.count)) + }; + case `right`: + const str = program.getValue(value.value); + return { + type: `constant`, + numeric: false, + content: str.substr(str.length - program.getValue(value.count)) + }; + case `from`: + const from = program.getValue(value.from); + const to = value.to ? program.getValue(value.to) : null; + const fstr = program.getValue(value.value); + return { + type: `constant`, + numeric: false, + content: to ? fstr.substr(from, to) : fstr.substr(from) + }; + case `position`: + let needle = program.getValue(value.needle); + let haystack = program.getValue(value.haystack); + if (value.nocase) { + needle = needle.toLowerCase(); + haystack = haystack.toLowerCase(); + } + return { + type: `constant`, + numeric: true, + content: value.last ? haystack.lastIndexOf(needle) : haystack.indexOf(needle) + }; + case `payload`: + return { + type: `constant`, + numeric: false, + content: program.getSymbolRecord(value.callback).payload + }; + case `modulo`: + const symbolRecord = program.getSymbolRecord(value.name); + const modval = program.evaluate(value.value); + return { + type: `constant`, + numeric: true, + content: symbolRecord.value[symbolRecord.index].content % modval.content + }; + case `format`: + const fmtRecord = program.getSymbolRecord(value.name); + const fmtValue = program.getValue(fmtRecord.value[fmtRecord.index]) * 1000; + try { + const spec = JSON.parse(program.getValue(value.value)); + switch (spec.mode) { + case `time`: + + return { + type: `constant`, + numeric: true, + content: new Date(fmtValue).toLocaleTimeString(spec.locale, spec.options) + }; + case `date`: + default: + const date = new Date(fmtValue); + const content = (spec.format === `iso`) + ? `${date.getFullYear()}-${date.getMonth()+1}-${date.getDate()}` + : date.toLocaleDateString(spec.locale, spec.options); + return { + type: `constant`, + numeric: true, + content + }; + } + } catch (err) { + program.runtimeError(program[program.pc].lino, `Can't parse ${value.value}`); + return null; + } + case `empty`: + return { + type: `constant`, + numeric: false, + content: `` + }; + case `now`: + return { + type: `constant`, + numeric: true, + content: Math.floor(Date.now() / 1000) + }; + case `today`: + const date = new Date(); + date.setHours(0, 0, 0, 0); + return { + type: `constant`, + numeric: true, + content: Math.floor(date.getTime() / 1000) + }; + case `date`: + content = Date.parse(program.getValue(value.value)) / 1000; + if (isNaN(content)) { + program.runtimeError(program[program.pc].lino, `Invalid date format; expecting 'yyyy-mm-dd'`); + return null; + } + return { + type: `constant`, + numeric: true, + content + }; + case `newline`: + return { + type: `constant`, + numeric: false, + content: `\n` + }; + case `break`: + return { + type: `constant`, + numeric: false, + content: `
` + }; + case `uuid`: + return { + type: `constant`, + numeric: false, + content: `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`.replace(/[xy]/g, function(c) { + var r = Math.random() * 16 | 0, v = c == `x` ? r : (r & 0x3 | 0x8); + return v.toString(16); + }) + }; + case `encode`: + return { + type: `constant`, + numeric: false, + content: program.encode(program.getValue(value.value)) + }; + case `decode`: + return { + type: `constant`, + numeric: false, + content: program.decode(program.getValue(value.value)) + }; + case `reverse`: + return { + type: `constant`, + numeric: false, + content: program.getValue(value.value).split(``).reverse().join(``) + }; + case `lowercase`: + return { + type: `constant`, + numeric: false, + content: program.getValue(value.value).toLowerCase() + }; + case `hash`: + const hashval = program.getValue(value.value); + let hash = 0; + if (hashval.length === 0) return hash; + for (let i = 0; i < hashval.length; i++) { + const chr = hashval.charCodeAt(i); + hash = ((hash << 5) - hash) + chr; + // hash |= 0; // Convert to 32bit integer + } + return { + type: `constant`, + numeric: true, + content: hash + }; + case `element`: + const element = program.getValue(value.element); + const elementRecord = program.getSymbolRecord(value.symbol); + var elementContent = ``; + try { + elementContent = JSON.parse(program.getValue(elementRecord.value[elementRecord.index]))[element]; + } catch (err) { + program.runtimeError(program[program.pc].lino, `Can't parse JSON`); + return null; + } + return { + type: `constant`, + numeric: false, + content: typeof elementContent === `object` ? + JSON.stringify(elementContent) : elementContent + }; + case `property`: + const property = program.getValue(value.property); + const propertyRecord = program.getSymbolRecord(value.symbol); + let propertyContent = program.getValue(propertyRecord.value[propertyRecord.index]); + var content = ``; + if (property && propertyContent) { + if (typeof propertyContent === `object`) { + content = propertyContent[property]; + } else if ([`{`, `]`].includes(propertyContent.charAt(0))) { + try { + content = JSON.parse(propertyContent)[property]; + } catch (err) { + console.log(`Can't parse '${propertyContent}': ${err.message}`); + } + } + } + return { + type: `constant`, + numeric: !Array.isArray(content) && !isNaN(content), + content: typeof content === `object` ? JSON.stringify(content) : content + }; + case `module`: + const module = program.getSymbolRecord(value.name); + return { + type: `boolean`, + numeric: false, + content: module.program + }; + case `message`: + content = program.message; + return { + type: `constant`, + numeric: false, + content + }; + case `error`: + content = program.errorMessage; + return { + type: `constant`, + numeric: false, + content + }; + case `indexOf`: + const value1 = program.getValue(value.value1); + const value2 = program.getValue(value.value2); + try { + content = JSON.parse(value2).indexOf(value1); + return { + type: `constant`, + numeric: true, + content + }; + } catch (err) { + program.runtimeError(program[program.pc].lino, `Can't parse ${value2}`); + } + break; + case `arg`: + const name = program.getValue(value.value); + const target = program.getSymbolRecord(value.target); + content = target[name]; + return { + type: `constant`, + numeric: !isNaN(content), + content + }; + case `char`: + let index = program.getValue(value.index); + let string = program.getValue(value.value); + return { + type: `constant`, + numeric: false, + content: string[index] + }; + } + return null; + }, + + put: (symbol, value) => { + symbol.value[symbol.index] = value; + } + }, + + condition: { + + compile: compiler => { + if (compiler.isSymbol()) { + const symbolRecord = compiler.getSymbolRecord(); + if (symbolRecord.keyword === `module`) { + if (compiler.nextTokenIs(`is`)) { + let sense = true; + if (compiler.nextTokenIs(`not`)) { + compiler.next(); + sense = false; + } + if (compiler.tokenIs(`running`)) { + compiler.next(); + return { + domain: `core`, + type: `moduleRunning`, + name: symbolRecord.name, + sense + }; + } + } + return null; + } + } + if (compiler.tokenIs(`not`)) { + const value = compiler.getNextValue(); + return { + domain: `core`, + type: `not`, + value + }; + } + try { + const value1 = compiler.getValue(); + const token = compiler.getToken(); + if (token === `includes`) { + const value2 = compiler.getNextValue(); + return { + domain: `core`, + type: `includes`, + value1, + value2 + }; + } + if (token === `is`) { + compiler.next(); + const negate = EasyCoder_Core.isNegate(compiler); + const test = compiler.getToken(); + switch (test) { + case `numeric`: + compiler.next(); + return { + domain: `core`, + type: `numeric`, + value1, + negate + }; + case `even`: + compiler.next(); + return { + domain: `core`, + type: `even`, + value1 + }; + case `odd`: + compiler.next(); + return { + domain: `core`, + type: `odd`, + value1 + }; + case `greater`: + compiler.next(); + if (compiler.tokenIs(`than`)) { + compiler.next(); + const value2 = compiler.getValue(); + return { + domain: `core`, + type: `greater`, + value1, + value2, + negate + }; + } + return null; + case `less`: + compiler.next(); + if (compiler.tokenIs(`than`)) { + compiler.next(); + const value2 = compiler.getValue(); + return { + domain: `core`, + type: `less`, + value1, + value2, + negate + }; + } + return null; + default: + const value2 = compiler.getValue(); + return { + domain: `core`, + type: `is`, + value1, + value2, + negate + }; + } + } else if (value1) { + // It's a boolean if + return { + domain: `core`, + type: `boolean`, + value: value1 + }; + } + } catch (err) { + compiler.warning(`Can't get a value`); + return 0; + } + return null; + }, + + test: (program, condition) => { + var comparison; + switch (condition.type) { + case `boolean`: + return program.getValue(condition.value); + case `numeric`: + let v = program.getValue(condition.value1); + let test = v === ` ` || isNaN(v); + return condition.negate ? test : !test; + case `even`: + return (program.getValue(condition.value1) % 2) === 0; + case `odd`: + return (program.getValue(condition.value1) % 2) === 1; + case `is`: + comparison = program.compare(program, condition.value1, condition.value2); + return condition.negate ? comparison !== 0 : comparison === 0; + case `greater`: + comparison = program.compare(program, condition.value1, condition.value2); + return condition.negate ? comparison <= 0 : comparison > 0; + case `less`: + comparison = program.compare(program, condition.value1, condition.value2); + return condition.negate ? comparison >= 0 : comparison < 0; + case `not`: + return !program.getValue(condition.value); + case `moduleRunning`: + let moduleRecord = program.getSymbolRecord(condition.name); + if (EasyCoder.scripts.hasOwnProperty(moduleRecord.program) ) { + let p = EasyCoder.scripts[moduleRecord.program]; + return condition.sense ? p.running : !p.running; + } + return !condition.sense; + case `includes`: + const value1 = JSON.parse(program.getValue(condition.value1)); + const value2 = program.getValue(condition.value2); + return value1.includes(value2); + } + return false; + } + } +}; +const EasyCoder = { + + name: `EasyCoder_Main`, + + domain: { + core: EasyCoder_Core + }, + + elementId: 0, + + runtimeError: function (lino, message) { + this.lino = lino; + this.reportError({ + message: `Line ${(lino >= 0) ? lino : ``}: ${message}` + }, this.program); + if (this.program) { + this.program.aborted = true; + } + }, + nonNumericValueError: function (lino) { + this.runtimeError(lino, `Non-numeric value`); + }, + variableDoesNotHoldAValueError: function (lino, name) { + this.runtimeError(lino, `Variable '${name}' does not hold a value`); + }, + + reportError: function (err, program, source) { + if (!err.message) { + console.log(`An error occurred - origin was ${err.path[0]}`); + return; + } + if (!this.compiling && !program) { + const errString = `Error: ${err.message}`; + alert(errString); + console.log(errString); + return; + } + // const compiler = EasyCoder_Compiler; + const { + tokens, + scriptLines + } = source ? source : program.source; + const lino = this.compiling ? tokens[EasyCoder_Compiler.getIndex()].lino : program[program.pc].lino; + var errString = this.compiling ? `Compile error` : `Runtime error in '${program.script}'`; + errString += `:\n`; + var start = lino - 5; + start = start < 0 ? 0 : start; + for (var n = start; n < lino; n++) { + const nn = (`` + (n + 1)).padStart(4, ` `); + errString += nn + ` ` + scriptLines[n].line.split(`\\s`).join(` `) + `\n`; + } + errString += `${err.message}\n`; + const warnings = EasyCoder_Compiler.getWarnings(); + if (warnings.length) { + errString += `Warnings:\n`; + for (const warning of warnings) { + errString += `${warning}\n`; + } + } + console.log(errString); + alert(errString); + }, + + getSymbolRecord: function (name) { + const target = this[this.symbols[name].pc]; + if (target.alias) { + return this.getSymbolRecord(target.alias); + } + if (target.exporter) { + // if (target.exporter != this.script) { + return EasyCoder.scripts[target.exporter].getSymbolRecord(target.exportedName); + // } + } + return target; + }, + + verifySymbol: function (name) { + return this.symbols.hasOwnProperty(name); + }, + + encode: function (value) { + return EasyCoder_Value.encode(value, this.encoding); + }, + + decode: function (value) { + return EasyCoder_Value.decode(value, this.encoding); + }, + + evaluate: function (value) { + return EasyCoder_Value.evaluate(this, value); + }, + + getValue: function (value) { + return EasyCoder_Value.getValue(this, value); + }, + + getFormattedValue: function (value) { + const v = EasyCoder_Value.evaluate(this, value); + if (v.numeric) { + return v.content; + } + if (v.type === `boolean`) { + return v.content ? `true` : `false`; + } + if (this.isJsonString(v.content)) { + try { + const parsed = JSON.parse(v.content); + return JSON.stringify(parsed, null, 2); + } catch (err) { + this.reportError(err); + return `{}`; + } + } + return v.content; + }, + + getSimpleValue: function (content) { + if (content === true || content === false) { + return { + type: `boolean`, + content + }; + } + return { + type: `constant`, + numeric: Number.isInteger(content), + content + }; + }, + + run: function (pc) { + if (pc) { + this.program = this; + EasyCoder_Run.run(this, pc); + } + }, + + exit: function () { + EasyCoder_Run.exit(this); + }, + + register: (program) => { + this.program = program; + }, + + require: function(type, src, cb) { + const element = document.createElement(type === `css` ? `link` : `script`); + switch (type) { + case `css`: + element.type = `text/css`; + element.href = src; + element.rel = `stylesheet`; + break; + case `js`: + element.type = `text/javascript`; + element.src = src; + break; + default: + return; + } + element.onload = function () { + console.log(`${Date.now() - EasyCoder.timestamp} ms: Library ${src} loaded`); + cb(); + }; + document.head.appendChild(element); + }, + + isUndefined: item => { + return typeof item === `undefined`; + }, + + isJsonString: function (str) { + try { + JSON.parse(str); + } catch (e) { + return false; + } + return true; + }, + + runScript: function (program) { + const command = program[program.pc]; + const script = program.getValue(command.script); + const imports = command.imports; + imports.caller = program.script; + const moduleRecord = command.module ? program.getSymbolRecord(command.module) : null; + try { + EasyCoder.tokeniseAndCompile(script.split(`\n`), imports, moduleRecord, this.script, command.then); + } catch (err) { + EasyCoder.reportError(err, program, program.source); + if (program.onError) { + program.run(program.onError); + } else { + let parent = EasyCoder.scripts[program.parent]; + if (parent && parent.onError) { + parent.run(parent.onError); + } + } + return; + } + if (command.nowait) { + EasyCoder.run(program.nextPc); + } + }, + + close: function () {}, + + compileScript: function (source, imports, module, parent) { + const { + tokens + } = source; + this.compiling = true; + const compiler = EasyCoder_Compiler; + this.compiler = compiler; + compiler.value = EasyCoder_Value; + compiler.condition = EasyCoder_Condition; + compiler.domain = this.domain; + compiler.imports = imports; + compiler.continue = false; + const program = EasyCoder_Compiler.compile(tokens); + // console.log('Program: ' + JSON.stringify(program, null, 2)); + this.compiling = false; + + program.EasyCoder = this; + program.value = EasyCoder_Value; + program.condition = EasyCoder_Condition; + program.compare = EasyCoder_Compare; + program.source = source; + program.run = this.run; + program.exit = this.exit; + program.runScript = this.runScript; + program.evaluate = this.evaluate; + program.getValue = this.getValue; + program.getFormattedValue = this.getFormattedValue; + program.getSimpleValue = this.getSimpleValue; + program.encode = this.encode; + program.decode = this.decode; + program.domain = this.domain; + program.require = this.require; + program.isUndefined = this.isUndefined; + program.isJsonString = this.isJsonString; + program.checkPlugin = this.checkPlugin; + program.getPlugin = this.getPlugin; + program.addLocalPlugin = this.addLocalPlugin; + program.getPluginsPath = this.getPluginsPath; + program.getSymbolRecord = this.getSymbolRecord; + program.verifySymbol = this.verifySymbol; + program.runtimeError = this.runtimeError; + program.nonNumericValueError = this.nonNumericValueError; + program.variableDoesNotHoldAValueError = this.variableDoesNotHoldAValueError; + program.reportError = this.reportError; + program.register = this.register; + program.symbols = compiler.getSymbols(); + program.unblocked = false; + program.encoding = `ec`; + program.popups = []; + program.stack = []; + program.queue = [0]; + program.module = module; + program.parent = parent; + if (module) { + module.program = program.script; + } + return program; + }, + + tokeniseFile: function(file) { + const scriptLines = []; + const tokens = []; + let index = 0; + file.forEach(function (line, lino) { + scriptLines.push({ + lino: lino + 1, + line + }); + const len = line.length; + let token = ``; + let inSpace = true; + for (let n = 0; n < len; n++) { + const c = line[n]; + if (c.trim().length == 0) { + if (inSpace) { + continue; + } + tokens.push({ + index, + lino: lino + 1, + token + }); + index++; + token = ``; + inSpace = true; + continue; + } + inSpace = false; + if (c === `\``) { + m = n; + while (++n < line.length) { + if (line[n] === `\``) { + break; + } + } + token = line.substr(m, n - m + 1); + } else if (c == `!`) { + break; + } else { + token += c; + } + } + if (token.length > 0) { + tokens.push({ + index, + lino: lino + 1, + token + }); + } + }); + return {scriptLines, tokens}; + }, + + tokeniseAndCompile: function (file, imports, module, parent, then) { + // console.log('Tokenise script: '); + let program = null; + const startCompile = Date.now(); + const source = this.tokeniseFile(file); + try { + program = this.compileScript(source, imports, module, parent); + this.scriptIndex++; + if (!program.script) { + program.script = this.scriptIndex; + } + const finishCompile = Date.now(); + console.log(`${finishCompile - this.timestamp} ms: ` + + `Compiled ${program.script}: ${source.scriptLines.length} lines (${source.tokens.length} tokens) in ` + + `${finishCompile - startCompile} ms`); + } catch (err) { + if (err.message !== `stop`) { + let parentRecord = EasyCoder.scripts[parent]; + this.reportError(err, parentRecord, source); + if (parentRecord && parentRecord.onError) { + parentRecord.run(parentRecord.onError); + } + // Remove this script + if (EasyCoder_Compiler.script) { + delete EasyCoder.scripts[EasyCoder_Compiler.script]; + delete EasyCoder_Compiler.script; + } + } + return; + } + if (program) { + EasyCoder.scripts[program.script] = program; + if (module) { + module.program = program.script; + } + program.afterExit = then; + program.running = true; + EasyCoder_Run.run(program, 0); + } + }, + + tokenise: function(source) { + const script = source.split(`\n`); + if (!this.tokenising) { + try { + this.tokeniseAndCompile(script); + } catch (err) { + this.reportError(err, null, source); + } + this.tokenising = true; + } + }, + + setPluginCount: function(count) { + EasyCoder.plugins = []; + EasyCoder.pluginCount = count; + }, + + checkPlugin: function(name) { + return EasyCoder.domain[name]; + }, + + getPlugin: function(name, src, onload) { + if (EasyCoder.domain[name]) { + onload(); + return; + } + const script = document.createElement(`script`); + script.type = `text/javascript`; + script.src = `${src}?ver=${EasyCoder.version}`; + script.onload = function () { + console.log(`${Date.now() - EasyCoder.timestamp} ms: Plugin ${src} loaded`); + onload(); + }; + document.head.appendChild(script); + }, + + addGlobalPlugin: function(name, handler) { + // alert(`Add plugin ${name}`); + EasyCoder.plugins.push({ + name, + handler + }); + if (EasyCoder.plugins.length === EasyCoder.pluginCount) { + EasyCoder.plugins.forEach(function (plugin) { + EasyCoder.domain[plugin.name] = plugin.handler; + }); + EasyCoder.tokenise(EasyCoder.source); + } + }, + + addLocalPlugin: function(name, handler, callback) { + EasyCoder.domain[name] = handler; + callback(); + }, + + getPluginsPath: function() { + return EasyCoder.pluginsPath; + }, + + loadPluginJs: function(path) { + console.log(`${Date.now() - this.timestamp} ms: Load ${path}/easycoder/plugins.js`); + const script = document.createElement(`script`); + script.src = `${window.location.origin}${path}/easycoder/plugins.js?ver=${this.version}`; + script.type = `text/javascript`; + script.onload = () => { + EasyCoder_Plugins.getGlobalPlugins( + this.timestamp, + path, + this.setPluginCount, + this.getPlugin, + this.addGlobalPlugin + ); + }; + script.onerror = () => { + if (path) { + this.loadPluginJs(path.slice(0, path.lastIndexOf(`/`))); + } else { + this.reportError({ + message: `Can't load plugins.js` + }, this.program, this.source); + } + }; + document.head.appendChild(script); + this.pluginsPath = path; + }, + + start: function(source) { + this.source = source; + this.scriptIndex = 0; + let pathname = window.location.pathname; + if (pathname.endsWith(`/`)) { + pathname = pathname.slice(0, -1); + } else { + pathname = ``; + } + if (typeof EasyCoder_Plugins === `undefined`) { + this.loadPluginJs(pathname); + } else { + this.pluginsPath = pathname; + EasyCoder_Plugins.getGlobalPlugins( + this.timestamp, + pathname, + this.setPluginCount, + this.getPlugin, + this.addGlobalPlugin + ); + } + } +}; +const EasyCoder_Run = { + + name: `EasyCoder_Run`, + + run: (program, pc) =>{ + + const queue = []; + + const minIndent = (scriptLines) => { + let count = 9999; + scriptLines.forEach(function (element) { + const item = element.line; + let n = 0; + while (n < item.length) { + if (item[n] !== ` `) { + break; + } + n++; + } + if (n > 0 && n < count) { + count = n; + } + }); + return 0; + }; + + if (queue.length) { + queue.push(pc); + return; + } + program.register(program); + queue.push(pc); + while (queue.length > 0) { + program.pc = queue.shift(); + program.watchdog = 0; + while (program.running) { + if (program.watchdog > 1000000) { + program.lino = program[program.pc].lino; + program.reportError( + new Error(`Program runaway intercepted.\nHave you forgotten to increment a loop counter?`, program), + program); + break; + } + program.watchdog++; + const domain = program[program.pc].domain; + if (program.debugStep) { + console.log(`${program.script}: Line ${program[program.pc].lino}: PC: ${program.pc} ${domain}:${program[program.pc].keyword}`); + } + const handler = program.domain[domain]; + if (!handler) { + program.runtimeError(program[program.pc].lino, `Unknown domain '${domain}'`); + break; + } + program.pc = handler.run(program); + if (!program.pc) { + break; + } + if (program.stop) { + program.tracing = false; + break; + } + if (program.tracing) { + const command = program[program.pc]; + const scriptLines = program.source.scriptLines; + const minSpace = minIndent(scriptLines); + const tracer = document.getElementById(`easycoder-tracer`); + if (!tracer) { + program.runtimeError(command.lino, `Element 'easycoder-tracer' was not found`); + return; + } + tracer.style.display = `block`; + tracer.style.visibility = `visible`; + var variables = ``; + if (program.tracer) { + const content = document.getElementById(`easycoder-tracer-content`); + if (content) { + program.tracer.variables.forEach(function (name, index, array) { + const symbol = program.getSymbolRecord(name); + if (symbol.elements > 1) { + variables += `${name}: ${symbol.index}/${symbol.elements}: `; + for (var n = 0; n < symbol.elements; n++) { + const value = symbol.value[n]; + if (value) { + variables += `${value.content} `; + } else { + variables += `undefined `; + } + } + } else { + const value = symbol.value[symbol.index]; + if (value) { + variables += `${name}: ${value.content}`; + } else { + variables += `${name}: undefined`; + } + } + switch (program.tracer.alignment) { + case `horizontal`: + if (index < array.length - 1) { + variables += `, `; + } + break; + case `vertical`: + variables += `
`; + break; + } + }); + variables += `
`; + var trace = ``; + for (var n = 5; n > 0; n--) { + if (command.lino) { + const text = scriptLines[command.lino - n].line.substr(minSpace); + trace += ``; + } + trace += `
`; + } + content.innerHTML = `${variables} ${trace}`; + content.style.display = `block`; + const run = document.getElementById(`easycoder-run-button`); + const step = document.getElementById(`easycoder-step-button`); + + run.onclick = function () { + run.blur(); + program.tracing = false; + const content = document.getElementById(`easycoder-tracer-content`); + content.style.display = `none`; + try { + EasyCoder_Run.run(program, program.resume); + } catch (err) { + const message = `Error in run handler: ` + err.message; + console.log(message); + alert(message); + } + }; + + step.onclick = function () { + console.log(`step`); + step.blur(); + program.tracing = true; + const content = document.getElementById(`easycoder-tracer-content`); + content.style.display = `block`; + try { + program.run(program.resume); + } catch (err) { + const message = `Error in step handler: ` + err.message; + console.log(message); + alert(message); + } + }; + } + + program.resume = program.pc; + program.pc = 0; + } + break; + } + } + } + }, + + exit: (program) => { + if (program.onExit) { + program.run(program.onExit); + } + let parent = program.parent; + let afterExit = program.afterExit; + delete EasyCoder.scripts[program.script]; + if (program.module) { + delete program.module.program; + } + Object.keys(program).forEach(function(key) { + delete program[key]; + }); + if (parent && afterExit) { + EasyCoder.scripts[parent].run(afterExit); + } + } +}; +const EasyCoder_Value = { + + name: `EasyCoder_Value`, + + getItem: (compiler) => { + const token = compiler.getToken(); + if (!token) { + return null; + } + + // Check for a boolean + if (token === `true`) { + compiler.next(); + return { + type: `boolean`, + content: true + }; + } + + if (token === `false`) { + compiler.next(); + return { + type: `boolean`, + content: false + }; + } + + // Check for a string constant + if (token.charAt(0) === `\``) { + compiler.next(); + const value = { + type: `constant`, + numeric: false, + content: token.substring(1, token.length - 1) + }; + return value; + } + + // Check for a numeric constant + if (token.charAt(0).match(/[0-9-]/)) { + const val = eval(token); + if (Number.isInteger(val)) { + compiler.next(); + const value = { + type: `constant`, + numeric: true, + content: val + }; + return value; + } else { + throw new Error(`'${token}' is not an integer`); + } + } + + // See if any of the domains can handle it + const index = compiler.getIndex(); + for (const name of Object.keys(compiler.domain)) { + compiler.rewindTo(index); + const handler = compiler.domain[name]; + const code = handler.value.compile(compiler); + if (code) { + return code; + } + } + return null; + }, + + compile: compiler => { + const token = compiler.getToken(); + const item = EasyCoder_Value.getItem(compiler); + if (!item) { + throw new Error(`Undefined value: '${token}'`); + } + + if (compiler.getToken() === `cat`) { + const value = { + type: `cat`, + numeric: false, + parts: [item] + }; + while (compiler.tokenIs(`cat`)) { + compiler.next(); + value.parts.push(compiler.value.getItem(compiler)); + } + return value; + } + + return item; + }, + + // runtime + + doValue: (program, value) => { + // console.log('Value:doValue:value: '+JSON.stringify(value,null,2)); + // See if it's a constant string, a variable or something else + if (typeof value.type === `undefined`) { + program.runtimeError(program[program.pc].lino, `Undefined value (variable not initialized?)`); + return null; + } + const type = value.type; + switch (type) { + case `cat`: + return { + type: `constant`, + numeric: false, + content: value.parts.reduce(function (acc, part) { + let value = EasyCoder_Value.doValue(program, part); + return acc + (value ? value.content : ``); + }, ``) + }; + case `boolean`: + case `constant`: + return value; + case `symbol`: + const symbol = program.getSymbolRecord(value.name); + if (symbol.isVHolder) { + const symbolValue = symbol.value[symbol.index]; + if (symbolValue) { + const v = symbolValue.content; + if (v === null || typeof v === `undefined`) { + symbolValue.content = symbolValue.numeric ? 0 : ``; + } + return symbolValue; + } else { + return null; + } + } else { + const handler = program.domain[symbol.domain].value; + return handler.get(program, value); + } + default: + break; + } + // Call the given domain to handle a value + const handler = program.domain[value.domain].value; + return handler.get(program, value); + }, + + constant: (content, numeric) => { + return { + type: `constant`, + numeric, + content + }; + }, + + evaluate: (program, value) => { + if (!value) { + return { + type: `constant`, + numeric: false, + content: `` + }; + } + const result = EasyCoder_Value.doValue(program, value); + if (result) { + return result; + } + program.runtimeError(program[program.pc].lino, `Can't decode value: ` + value); + }, + + getValue: (program, value) => { + return EasyCoder_Value.evaluate(program, value).content; + }, + + // tools + + encode: (value, encoding) => { + if (value) { + switch (encoding) { + case `ec`: + return value.replace(/\n/g, `%0a`) + .replace(/\r/g, `%0d`) + .replace(/"/g, `~dq~`) + .replace(/'/g, `~sq~`) + .replace(/\\/g, `~bs~`); + case `url`: + return encodeURIComponent(value.replace(/\s/g, `+`)); + case `sanitize`: + return value.normalize(`NFD`).replace(/[\u0300-\u036f]/g, ``); + default: + return value; + } + } + return value; + }, + + decode: (value, encoding) => { + if (value) { + switch (encoding) { + case `ec`: + return value.replace(/%0a/g, `\n`) + .replace(/%0d/g, `\r`) + .replace(/~dq~/g, `"`) + .replace(/~sq~/g, `'`) + .replace(/~bs~/g, `\\`); + case `url`: + const decoded = decodeURIComponent(value); + return decoded.replace(/\+/g, ` `); + default: + return value; + } + } + return value; + } +}; +EasyCoder.version = `2.6.0`; +EasyCoder.timestamp = Date.now(); + +function EasyCoder_Startup() { + EasyCoder.timestamp = Date.now(); + EasyCoder.scripts = {}; + window.EasyCoder = EasyCoder; + const script = document.getElementById(`easycoder-script`); + if (script) { + script.style.display = `none`; + try { + EasyCoder.start(script.innerText); + } + catch (err) { + EasyCoder.reportError(err); + } + } +}; + +// For browsers +window.onload = EasyCoder_Startup; diff --git a/storyteller/storyteller.txt b/storyteller/storyteller.txt new file mode 100644 index 0000000..467a4f9 --- /dev/null +++ b/storyteller/storyteller.txt @@ -0,0 +1,862 @@ +! Storyteller main script + + script Storyteller + + div Body + div Container + div Content + div TitleDiv + div ButtonBar + div TopicText + div ImageDiv + div Mask + pre Branch + pre Resources + img BigPic + img TopImage + img MidImage + img BottomImage + img HomeButton + img BackButton + img ForwardButton + img InfoButton + img Image + a Link + select Select + option Option + callback DecoratorCallback + variable Mobile + variable CDNPath + variable Args + variable Arg + variable Stories + variable Themes + variable Theme + variable Layout + variable BorderL + variable BorderT + variable BorderR + variable BorderB + variable BorderLeft + variable BorderTop + variable BorderRight + variable BorderBottom + variable AspectW + variable AspectH + variable SID + variable TID + variable CurrentSID + variable CurrentTID + variable Record + variable C + variable N + variable M + variable P + variable Margin + variable WindowWidth + variable WindowHeight + variable Width + variable Height + variable ButtonSize + variable Title + variable Payload + variable Function + variable Data + variable Display + variable Attributes + variable Source + variable Class + variable Classes + variable Options + variable List + variable LinkCount + variable SelectCount + variable ImageCount + variable Value + variable DataID + variable Prefix + variable Topic + variable Item + variable Stack + variable StackPointer + variable FontScale + variable FontSize + variable Path + + clear Mobile + if mobile + begin + if portrait set Mobile + end + + attach Branch to `storyteller-branch` + put the content of Branch into Value + put `https://cdn.jsdelivr.net/gh/easycoder/storyteller@` cat Value into CDNPath + + attach Resources to `storyteller-stories` + put the content of Resources into Stories + put `/` cat Stories cat `/` into Stories + + gosub to GetStyles + + attach Body to body + clear Body + if Mobile + begin + create Container in Body + set style `margin` of Container to `0.5em` + end + else + begin + set the style of Body to `overflow:hidden;width:100vw;height:100vh` + create Container in Body + set the style of Container to `position:relative;overflow:hidden;padding-right:0.5em` + create MidImage in Container + create TopImage in Container + create BottomImage in Container + on window resize gosub to SetStyles + end + + create Content in Container + set style `position` of Content to `absolute` + if Mobile + begin + create ButtonBar in Content + create TitleDiv in Content + end + else + begin + create TitleDiv in Content + create ButtonBar in Content + end + create TopicText in Content + + json parse url the location as Args + put property `arg` of Args into Arg + if Arg is empty + begin + get SID from storage as `id` + if SID is empty put `home` into SID + get TID from storage as `tid` + if TID is empty put `content` into TID + end + else + begin + put property `arg` of Args into Arg + if Arg is not empty + begin + put the position of `=` in Arg into N + if N is not -1 + begin + if left N of Arg is `s` + begin + add 1 to N + put from N of Arg into Stories + put `/` cat Stories cat `/` into Stories + end + else if left N of Arg is `p` + begin + add 1 to N + put from N of Arg into Item + put the position of `/` in Item into N + if N is greater than 0 + begin + put left N of Item into SID + add 1 to N + put from N of Item into TID + end + else put Item into SID + history set url `.` + end + end + end + end + + rest get Title from Stories cat `title.txt?v=` cat now + set the title to Title + + gosub to SetupTheme + gosub to CreateButtons + gosub to SetStyles + + load showdown + on DecoratorCallback go to Decorate + + put 0 into StackPointer + +! View a record, given its Subject and Topic ids. +ViewRecord: + if not Mobile scroll TopicText to 0 + rest get Record from Stories cat SID cat `/content.txt?v=` cat now + or begin + put `home` into SID + put `content` into TID + continue + end + +! Add this topic to the stack + put SID cat `/` cat TID into Stack + if StackPointer is greater than 0 set style `display` of BackButton to `inline-block` + +! Get the content + set the style of TitleDiv to `text-align:center;font-size:1.6em;font-weight:bold` + if Mobile set style `margin-top` of TitleDiv to `0.5em` + rest get Title from Stories cat SID cat `/title.txt?v=` cat now + if Title is empty + put SID into Title + continue + end + set the content of TitleDiv to Title + + rest get Topic from Stories cat SID cat `/` cat TID cat `.txt?v=` cat now + or begin + put `content` into TID + rest get Topic from Stories cat SID cat `/` cat TID cat `.txt?v=` cat now + continue + end + +! Remember where we are + put SID into storage as `id` + put TID into storage as `tid` + put SID into CurrentSID + put TID into CurrentTID + + if Mobile set the style of TopicText to `padding:0.5em` + else set the style of TopicText to + `width:100%;height:calc(100% - 5em);background:none;overflow-y: auto` + cat `;padding-right:1em` + +! Handle the links created by the showdown extension + put 0 into LinkCount + put 0 into ImageCount + put 0 into SelectCount + set the content of TopicText to showdown decode Topic with DecoratorCallback + +! Process links + set the elements of Link to LinkCount + put 0 into N + while N is less than LinkCount + begin + index Link to N + attach Link to `ec-link-` cat N + add 1 to N + end + on click Link + begin + put attribute `data-id` of Link into DataID + if DataID is `theme` + begin + rest get Themes from CDNPath cat `/themes/themes.txt?v=` cat now + json split Themes on newline into Themes + put `Here are the available themes:` cat newline into Item + put 0 into N + while N is less than the json count of Themes + begin + if N is not 0 put Item cat `, ` into Item + put Item cat element N of Themes into Item + add 1 to N + end + put Item cat newline cat newline cat `You are currently using the '` cat Theme cat `' theme.` into Item + put Item cat newline cat `Please type the name of the theme you want to use:` into Item + put prompt Item into Item + if Item is not empty + begin + put Item into Theme + put Theme into storage as `theme` + gosub to SetupTheme + end + end + else + begin + put the position of `-` in DataID into N + if N is greater than 0 + begin + put left N of DataID into Prefix + if Prefix is `S` + begin + put from 2 of DataID into SID + put `content` into TID + go to ViewAnotherRecord + end + else if Prefix is `T` + begin + put from 2 of DataID into TID + go to ViewAnotherRecord + end + else if Prefix is `ST` + begin + put from 3 of DataID into TID + put the position of `/` in TID into N + if N is greater than 0 + begin + put left N of TID into SID + add 1 to N + put from N of TID into TID + go to ViewAnotherRecord + end + end + end + end + end + +! Process images + set the elements of ImageDiv to ImageCount + set the elements of Image to ImageCount + put 0 into N + while N is less than ImageCount + begin + index ImageDiv to N + index Image to N + attach ImageDiv to `ec-imagediv-` cat N + attach Image to `ec-image-` cat N + add 1 to N + end + on click Image + begin + put attribute `data-options` of Image into Options + if the position of `nolink` in Options is -1 + begin + if true !Mobile + begin + create Mask + set the style of Mask to `position:fixed;top:0;left:0;width:100vw;height:150vh;` + cat `text-align:center;background-color:rgba(0,0,0,0.7)` + create BigPic in Mask + set the style of BigPic to + `max-width:93vw;max-height:93vh;margin-top:3%` + put attribute `src` of Image into Source + set attribute `src` of BigPic to Source + end + on click Mask remove element Mask + on click BigPic remove element Mask + end + end + +! Process selectors + set the elements of Select to SelectCount + put 0 into N + while N is less than SelectCount + begin + index Select to N + attach Select to `ec-select-` cat N + put attribute `data-options` of Select into Options + json split Options on `|` into List + put 0 into M + while M is less than the json count of List + begin + create Option in Select + put element M of List into Value + put Value into Display + put the position of `:` in Value into P + if P is not -1 + begin + put left P of Value into Value + add 1 to P + put from P of Display into Display + end + set attribute `data-st` of Option to left 2 of Value + put from 2 of Value into Value + put Display into Attributes + put the position of `!` in Display into P + if P is -1 put empty into Attributes + else + begin + put left P of Display into Display + add 1 to P + put from P of Attributes into Attributes + end + set the content of Option to Display + if Attributes is not empty set the attributes of Option to Attributes + set attribute `value` of Option to Value + add 1 to M + end + add 1 to N + end + on change Select + begin + get Option from Select + put attribute `data-st` of Option into Function + put attribute `value` of Option into Value + if Function is `S-` + begin + put Value into SID + put `content` into TID + go to ViewAnotherRecord + end + else if Function is `T-` + begin + put Value into TID + go to ViewAnotherRecord + end + else if Function is `ST-` + begin + put Value into TID + put the position of `/` in TID into N + if N is greater than 0 + begin + put left N of TID into SID + add 1 to N + put from N of TID into TID + go to ViewAnotherRecord + end + end + end + stop + +! Set up the theme +SetupTheme: + get Theme from storage as `theme` + if Theme is empty rest get Theme from Stories cat `theme.txt` + print Theme + if left 1 of Theme is `/` + begin + put from 1 of Theme into Theme + put empty into Path + end + else put CDNPath into Path + rest get Layout from Path cat `/themes/` cat Theme cat `/theme.json?v=` cat now + or begin + put empty into storage as `theme` + rest get Theme from Stories cat `theme.txt` + if left 1 of Theme is `/` + begin + put from 1 of Theme into Theme + put empty into Path + end + else put CDNPath into Path + rest get Layout from Path cat `/themes/` cat Theme cat `/theme.json?v=` cat now + end + print Path + put property `aspect-w` of Layout into AspectW + put property `aspect-h` of Layout into AspectH + put property `border-l` of Layout into BorderL + put property `border-r` of Layout into BorderR + put property `border-t` of Layout into BorderT + put property `border-b` of Layout into BorderB + put property `font-scale` of Layout into FontScale + if not Mobile + begin + print Path cat `/themes/` cat Theme cat `/mid.jpg` + set attribute `src` of MidImage to Path cat `/themes/` cat Theme cat `/mid.jpg` + set attribute `src` of TopImage to Path cat `/themes/` cat Theme cat `/top.jpg` + set attribute `src` of BottomImage to Path cat `/themes/` cat Theme cat `/bottom.jpg` + end + return + +! Create the buttons at the top of the panel +CreateButtons: + set style `position` of ButtonBar to `relative` + if Mobile + begin + set style `padding` of ButtonBar to `0.25em` + set style `background` of ButtonBar to `#eee` + end + else + begin + set style `margin` of ButtonBar to `0 1em 0.5em 1em` + end + create HomeButton in ButtonBar + set attribute `src` of HomeButton to CDNPath cat `/icons/home.png` + on click HomeButton + begin + put `home` into SID + put `content` into TID + go to ViewAnotherRecord + end + + create BackButton in ButtonBar + set style `display` of BackButton to `none` + set attribute `src` of BackButton to CDNPath cat `/icons/arrow-back.png` + on click BackButton + begin + put the elements of Stack into N + take 1 from N + take 1 from StackPointer + index Stack to StackPointer + put the position of `/` in Stack into N + if N is -1 stop + put left N of Stack into SID + add 1 to N + put from N of Stack into TID + if StackPointer is 0 set style `display` of BackButton to `none` + set style `display` of ForwardButton to `inline-block` + go to ViewRecord + end + + create ForwardButton in ButtonBar + set style `display` of ForwardButton to `none` + set attribute `src` of ForwardButton to CDNPath cat `/icons/arrow-forward.png` + on click ForwardButton + begin + put the elements of Stack into N + take 1 from N + if N is StackPointer stop + add 1 to StackPointer + if StackPointer is N set style `display` of ForwardButton to `none` + set style `display` of BackButton to `inline-block` + index Stack to StackPointer + put the position of `/` in Stack into N + if N is -1 stop + put left N of Stack into SID + add 1 to N + put from N of Stack into TID + go to ViewRecord + end + + create InfoButton in ButtonBar + set attribute `src` of InfoButton to CDNPath cat `/icons/info.png` + on click InfoButton + begin + put `info` into SID + put `content` into TID + go to ViewAnotherRecord + end + + return + +! View another record, given the Subject and Topic ids +ViewAnotherRecord: + if SID is not CurrentSID go to VAR2 + if TID is not CurrentTID go to VAR2 + stop +VAR2: + add 1 to StackPointer + add 1 to StackPointer giving N + set the elements of Stack to N + index Stack to StackPointer + set style `display` of ForwardButton to `none` + go to ViewRecord + +! Responsive design: Compute the size and position of all the screen elements +SetStyles: + put the width of window into WindowWidth + put the height of window into WindowHeight + +! Choose an optimum width based on the window height + put WindowHeight into Height + multiply Height by AspectW giving Width + divide Width by AspectH + +! Make sure the window is wide enough + take Width from WindowWidth giving Margin + divide Margin by 2 + if Margin is less than 0 + begin + put 0 into Margin + put WindowWidth into Width + end + +! Style the Container + set style `left` of Container to Margin cat `px` + set style `top` of Container to 0 + set style `width` of Container to Width cat `px` + set style `height` of Container to Height cat `px` + +! Style the background images + if not Mobile + begin + set the style of MidImage to `position:absolute;left:0;top:0;width:` cat Width + cat `px;height:` cat `calc(` cat Height cat `px - 2vh)` + set the style of TopImage to `position:absolute;left:0;top:0;width:` cat Width cat `px` + set the style of BottomImage to `position:absolute;left0;bottom:2vh;width:` cat Width cat `px` + end + +! Calculate the borders + if Mobile + begin + put 0 into BorderLeft + put 0 into BorderRight + put 0 into BorderTop + put 0 into BorderBottom + end + else + begin + multiply Width by BorderL giving BorderLeft + multiply Width by BorderR giving BorderRight + divide BorderLeft by 100 + divide BorderRight by 100 + take BorderLeft from Width + take BorderRight from Width + + multiply Height by BorderT giving BorderTop + multiply Height by BorderB giving BorderBottom + divide BorderTop by 100 + divide BorderBottom by 100 + take BorderTop from Height + take BorderBottom from Height + end + + divide Width by 20 giving ButtonSize + +! Style the buttons + if Mobile multiply ButtonSize by 2 + set style `position` of HomeButton to `absolute` + set style `left` of HomeButton to `0.25em` + set style `top` of HomeButton to `0.25em` + set style `width` of HomeButton to ButtonSize cat `px` + set style `height` of HomeButton to ButtonSize cat `px` + + set style `height` of ButtonBar to ButtonSize cat `px` + + multiply ButtonSize by 3 giving M + divide M by 2 + put M into N + set style `position` of BackButton to `absolute` + set style `left` of BackButton to `calc(` cat N cat `px + 0.25em)` + set style `top` of BackButton to `0.25em` + set style `width` of BackButton to ButtonSize cat `px` + set style `height` of BackButton to ButtonSize cat `px` + + add M to N + set style `position` of ForwardButton to `absolute` + set style `left` of ForwardButton to `calc(` cat N cat `px + 0.25em)` + set style `top` of ForwardButton to `0.25em` + set style `width` of ForwardButton to ButtonSize cat `px` + set style `height` of ForwardButton to ButtonSize cat `px` + + set style `position` of InfoButton to `absolute` + set style `right` of InfoButton to `0.25em` + set style `top` of InfoButton to `0.25em` + set style `width` of InfoButton to ButtonSize cat `px` + set style `height` of InfoButton to ButtonSize cat `px` + +! Style the content + set style `left` of Content to BorderLeft cat `px` + set style `top` of Content to BorderTop cat `px` + set style `width` of Content to Width cat `px' + set style `height` of Content to Height cat `px' + +! Compute the font size + divide Height by FontScale giving FontSize + if Mobile + begin + multiply FontSize by 5 + divide FontSize by 4 + set style `line-height` of Container to `140%` + end + set style `font-size` of Container to FontSize cat `px` + return + +!------------------------------------------------------------------------------ +! This is the Showndown extension. + +! Decorate is called for every occurrence of ~...~ in the topic data +Decorate: + put the payload of DecoratorCallback into Payload + put the position of `:` in Payload into N + if N is -1 + begin + if Payload is `clear` gosub to ProcessClear + end + else + begin + put left N of Payload into Function + add 1 to N + put from N of Payload into Data + if Function is `sid` gosub to ProcessSID + else if Function is `tid` gosub to ProcessTID + else if Function is `stid` gosub to ProcessSTID + else if Function is `img` gosub to ProcessImage + else if Function is `select` gosub to ProcessSelect + else if Function is `space` gosub to ProcessSpace + else if Function is `comment` gosub to ProcessComment + else if Function is `theme` gosub to ProcessTheme + end + set the payload of DecoratorCallback to Payload + stop + +! Process a request for a new subject +ProcessSID: + put Data into Display + put the position of `:` in Data into N + if N is not -1 + begin + put left N of Data into Data + add 1 to N + put from N of Display into Display + end + put `` cat Display cat `` into Payload + add 1 to LinkCount + return + +! Process a request for a new topic +ProcessTID: + put Data into Display + put the position of `:` in Data into N + if N is not -1 + begin + put left N of Data into Data + add 1 to N + put from N of Display into Display + end + put `` cat Display cat `` into Payload + add 1 to LinkCount + return + +! Process a request for a new subject and topic +ProcessSTID: + put Data into Display + put the position of `:` in Data into N + if N is not -1 + begin + put left N of Data into Data + add 1 to N + put from N of Display into Display + end + put `` cat Display cat `` into Payload + add 1 to LinkCount + return + +! Process an image, including positioning and class information +ProcessImage: + put empty into Options + put the position of `/` in Data into N + if N is -1 put SID into Source + else + begin + put left N of Data into Source + add 1 to N + put from N of Data into Data + end + put the position of `:` in Data into N + put empty into Class + if N is not -1 + begin + put Source cat `/images/` cat left N of Data into Source + add 1 to N + if Data is not empty + begin + if Class is not empty put Class cat ` ` into Class + put Class cat from N of Data into Class + end + put the position of `!` in Class into N + if N is not -1 + begin + put Class into Options + put left N of Class into Class + add 1 to N + put from N of Options into Options + end + end + json split Class on ` ` into Classes + put empty into Class + put 0 into N + while N is less than the json count of Classes + begin + put element N of Classes into C + if C is `border` begin end + else if C is `left` begin end + else if C is `right` begin end + else if C is `center` begin end + else if C is `clear` begin end + else if C is `10%` put `pc10` into C + else if C is `25%` put `pc25` into C + else if C is `33%` put `pc33` into C + else if C is `50%` put `pc50` into C + else if C is `90%` put `pc90` into C + else if C is `100%` put `pc100` into C + else + begin + alert `Unknown style ` cat C + return + end + if Class is not empty put Class cat ` ` into Class + put Class cat C into Class + add 1 to N + end + put `
` cat `
` into Payload + end + else + begin + put Payload cat `">` cat `` + cat `
` into Payload + end + add 1 to ImageCount + return + +! Process a 'clear' +ProcessClear: + put `
` into Payload + return + +! Process a 'select' +ProcessSelect: + put `` into Payload + add 1 to SelectCount + return + +! Process a space +ProcessSpace: + if Mobile put `
` into Payload + else + begin + put empty into Payload + put the value of Data into M + put 0 into N + while N is less than M + begin + put Payload cat ` ` into Payload + add 1 to N + end + end + return + +! Process a 'comment' +ProcessComment: + put empty into Payload + return + +! Process a 'theme' +ProcessTheme: + put Data into Display + put the position of `:` in Data into N + if N is not -1 + begin + put left N of Data into Data + add 1 to N + put from N of Display into Display + end + put `` cat Display cat `` into Payload + add 1 to LinkCount + return + +! Put all the styles needed into the head +GetStyles: +! Set the font size of all selectors + set style `select` to `{font-size:1em}` +! Force its owner to sit below all previous content + set style `.clear` to `{clear:both}` +! Image border and padding + set style `.border` to `{padding:2px;border:1px solid black}` +! Float left with a margin all round + set style `.left` to `{float:left;margin:0.5em}` +! Float right with a margin all round + set style `.right` to `{float:right;margin:0.5em}` +! Put the item in the centre of the page + set style `.center` to `{margin:0 auto}` +! Set the item width to 10% of the page width + set style `.pc10` to `{width:10%}` +! Set the item width to 25% of the page width + set style `.pc25` to `{width:25%}` +! Set the item width to 33% of the page width + set style `.pc33` to `{width:33%}` +! Set the item width to 50% of the page width + set style `.pc50` to `{width:50%}` +! Set the item width to 90% of the page width + set style `.pc90` to `{width:90%}` +! Set the item width to 100% of the page width + set style `.pc100` to `{width:100%}` + return diff --git a/storyteller/storyteller/GetStoryteller/content.txt b/storyteller/storyteller/GetStoryteller/content.txt new file mode 100644 index 0000000..8078032 --- /dev/null +++ b/storyteller/storyteller/GetStoryteller/content.txt @@ -0,0 +1,130 @@ +Links: ~select:-:Select a subject!selected disabled hidden|S-Structure:The structure of a Storyteller website|S-Markdown:Using Markdown|S-Images:Handling images~~space:3~Next: ~sid:Structure:Website structure~ + +# How to set up a Storyteller website + +There are two ways to use Storyteller: + + 1. Open a Neocities account and build your site using the instructions below. + + 1. Download Storyteller files from the [Storyteller repository](https://github.com/easycoder/storyteller) and copy them to your server. This is the option for advanced users. + +## Setting up using Neocities + +Start by opening a [Neocities](https://neocities.org) account. This is pretty simple; the main thing is to decide what you want to call your website and hope nobody else has already taken that name. + +Once you have chosen your name your website will be live and can be seen by anyone who knows its name. All editing of your site is done in the Neocities online editor, so go into that. You'll see some standard files as supplied by the Neocities registration robot. Follow these simple steps: + + 1. Delete all the files and folders listed except `index.html`. + + 1. Click the `Edit` button to edit the `index.html` file. + + 1. Delete its existing contents. + + 1. Copy the HTML text below (between the horizontal lines) and paste it into the editor. + + 1. Click `Save`. + + 1. Click `Dashboard` on the left of the button bar to return to the file listing screen. +
+``` + + + + + + + + + + + + + + + + +``` +
+Now carefully follow these steps: + + 1. Click the `Edit` button for `index.html`. + + 1. Find the text `(Name of my stories folder)` and replace it (and the 2 brackets) with a name you will use for your stories folder. The name can be anything you like (but no spaces, please) so make it appropriate. For example, if you are writing about holidays you could call it `MyHolidays`. + + 1. Click `Save`. + + 1. Back out of the editor to the Dashboard. + + 1. Click the `New Folder` button and type the name you just gave for your stories. This folder is where you will keep all your story files. + + 1. Click its name to go into your stories folder. + + 1. Click `New File` and type the name `title.txt`. Click `Create`. + + 1. Click `Edit` against the new file. + + 1. Type the name of your website as you'd like it to appear in the title bar of the browser. + + 1. Click `Save`. + + 1. Click `Dashboard` on the left of the button bar to return to the file listing screen. + + 1. Click `New File` and type the name `theme.txt`. Click `Create`. + + 1. Click `Edit` against the new file. + + 1. Type the name of a theme chosen from this list of those currently available: + + - `storyteller`, the theme used by this site, with a scroll border and blue sky header + + - `grass`, lush border of thick grass + + - `herbal`, which has a border of what looks like short grass + + - `pencil`, a border design made of 2 pencils bent into a frame + + - `thin-gold`, a thin gold frame + + - `wood1`, a thick wooden frame + + - `wood2`, a much thinner wooden frame + + 1. Click `Save`. + + 1. Click `Dashboard` on the left of the button bar to return to the file listing screen. + +You now have an empty Storyteller website, so now you can actually start building your own website. The way it works is that inside the `stories` folder you can have any number of _subject_ folders, each handling one or more pages on your site. Each of these folders has inside it another folder for any images that will be used by that subject, plus 2 text files: + + - `title.txt` - holds the text of the title that will appear at the top of the page (here it's `Starting Storyteller`). + - `content.txt` - holds the content of your page, which is text plus images, where the images are not directly embedded but are referred to by their names in the `images` folder. + +The system always starts off by looking in the subject folder whose name is `home`, so create a folder called `home` inside your stories folder. Inside this create the `images` folder and the `title.txt` and `content.txt` text files. Click the `Edit` button for `title.txt`, type the title of your website and save it. Then click the `Edit` button for `content.txt` and type a couple of short paragraphs - anything you like just to test the system. Save this file. + +Each Subject starts off with just the one topic (`content.txt`) but you can add as many more as you like. For example, if you want to write a page about a holiday in India you might call it `India.txt`. There'a full description of all this on our ~sid:Structure~ page. + +Now you can view your website by clicking the big box at the top left of the editor screen or the site title to its right. Your content with the title you chose should appear in a new browser window. It it doesn't, look carefully at all the files and make any necessary corrections. On our ~sid:info~ page you'll see contact details if you need to ask questions. + +The best way to learn is often by following examples. So at the bottom of each of these pages you'll see a link that when clicked opens a page containing the _source code_ of the page; in other words what the author of the page was working with. You can use this to check on the precise syntax of any feature such as a right-aligned image or a drop-down list. + +Sometimes your browser may not notice you have made changes to your website and will continue to show you the old version. This is rather annoying and to get round it you will need to learn how to clear the browser cache. This process varies from browser to browser but Google will be able to tell you how to do it. Just search for _firefox clear cache_ or _chrome clear cache_ etc. + +You can do all your editing in Neocities using their built-in editor or you can maintain a corresponding set of files on your computer and upload each one after making changes. If you understand the processes involved you can also run a copy of the website on your own computer by using the `rest.py` webserver provided in our [Github repository](https://github.com/easycoder/storyteller), which makes for a quicker way to make changes and test them before uploading to the live site. (You will also need to download `bottle.py` for this to work.) + +~sid:Structure:The structure of a Storyteller website~ + +~sid:Markdown:Using Markdown in Storyteller~ + +~sid:Images:Handling images~ + +[Click here to see the source code for this page](/storyteller/GetStoryteller/source.txt) diff --git a/storyteller/storyteller/GetStoryteller/source.txt b/storyteller/storyteller/GetStoryteller/source.txt new file mode 100644 index 0000000..c7a3648 --- /dev/null +++ b/storyteller/storyteller/GetStoryteller/source.txt @@ -0,0 +1,126 @@ +Source code of the 'Setting up' page + +Below the line is the page source as typed by the site author. +----------------------------------------------------------------------------------------------------------- +Links: ~select:-:Select a subject!selected disabled hidden|S-Structure:The structure of a Storyteller website|S-Markdown:Using Markdown|S-Images:Handling images~~space:3~Next: ~sid:Structure:Website structure~ + +# How to set up a Storyteller website + +There are two ways to use Storyteller: + + 1. Open a Neocities account and build your site using the instructions below. + + 1. Download Storyteller files from the [Storyteller repository](https://github.com/easycoder/storyteller) and copy them to your server. This is the option for advanced users. + +## Setting up using Neocities + +Start by opening a [Neocities](https://neocities.org) account. This is pretty simple; the main thing is to decide what you want to call your website and hope nobody else has already taken that name. + +Once you have chosen your name your website will be live and can be seen by anyone who knows its name. All editing of your site is done in the Neocities online editor, so go into that. You'll see some standard files as supplied by the Neocities registration robot. Follow these simple steps: + + 1. Delete all the files and folders listed except `index.html`. + + 1. Click the `Edit` button to edit the `index.html` file. + + 1. Delete its existing contents. + + 1. Copy the HTML text below (between the horizontal lines) and paste it into the editor. + + 1. Click `Save`. + + 1. Click `Dashboard` on the left of the button bar to return to the file listing screen. +
+ +``` + + + + + + + + + + +
master
+ + + + +``` +
+Now carefully follow these steps: + + 1. Click the `New Folder` button and type the name `stories`. + + 1. Click its name (`stories`) to go into the folder. + + 1. Click `New File` and type the name `title.txt`. Click `Create`. + + 1. Click `Edit` against the new file. + + 1. Type the name of your website as you'd like it to appear in the title bar of the browser. + + 1. Click `Save`. + + 1. Click `Dashboard` on the left of the button bar to return to the file listing screen. + + 1. Click `New File` and type the name `theme.txt`. Click `Create`. + + 1. Click `Edit` against the new file. + + 1. Type the name of a theme chosen from this list of those currently available: + + - `storyteller`, the theme used by this site, with a scroll border and blue sky header + + - `grass`, lush border of thick grass + + - `herbal`, which has a border of what looks like short grass + + - `pencil`, a border design made of 2 pencils bent into a frame + + - `thin-gold`, a thin gold frame + + - `wood1`, a thick wooden frame + + - `wood2`, a much thinner wooden frame + + 1. Click `Save`. + + 1. Click `Dashboard` on the left of the button bar to return to the file listing screen. + +You now have an empty Storyteller website, so now you can actually start building your own website. The way it works is that inside the `stories` folder you can have any number of _subject_ folders, each handling one or more pages on your site. Each of these folders has inside it another folder for any images that will be used by that subject, plus 2 text files: + + - `title.txt` - holds the text of the title that will appear at the top of the page (here it's `Starting Storyteller`). + - `content.txt` - holds the content of your page, which is text plus images, where the images are not directly embedded but are referred to by their names in the `images` folder. + +The system always starts off by looking in the subject folder whose name is `home`, so create a folder called `home` inside your stories folder. Inside this create the `images` folder and the `title.txt` and `content.txt` text files. Click the `Edit` button for `title.txt`, type the title of your website and save it. Then click the `Edit` button for `content.txt` and type a couple of short paragraphs - anything you like just to test the system. Save this file. + +Each Subject starts off with just the one topic (`content.txt`) but you can add as many more as you like. For example, if you want to write a page about a holiday in India you might call it `India.txt`. There'a full description of all this on our ~sid:Structure~ page. + +Now you can view your website by clicking the big box at the top left of the editor screen or the site title to its right. Your content with the title you chose should appear in a new browser window. It it doesn't, look carefully at all the files and make any necessary corrections. On our ~sid:info~ page you'll see contact details if you need to ask questions. + +The best way to learn is often by following examples. So at the bottom of each of these pages you'll see a link that when clicked opens a page containing the _source code_ of the page; in other words what the author of the page was working with. You can use this to check on the precise syntax of any feature such as a right-aligned image or a drop-down list. + +Sometimes your browser may not notice you have made changes to your website and will continue to show you the old version. This is rather annoying and to get round it you will need to learn how to clear the browser cache. This process varies from browser to browser but Google will be able to tell you how to do it. Just search for _firefox clear cache_ or _chrome clear cache_ etc. + +You can do all your editing in Neocities using their built-in editor or you can maintain a corresponding set of files on your computer and upload each one after making changes. If you understand the processes involved you can also run a copy of the website on your own computer by using the `rest.py` webserver provided in our [Github repository](https://github.com/easycoder/storyteller), which makes for a quicker way to make changes and test them before uploading to the live site. (You will also need to download `bottle.py` for this to work.) + +~sid:Structure:The structure of a Storyteller website~ + +~sid:Markdown:Using Markdown in Storyteller~ + +~sid:Images:Handling images~ + +[Click here to see the source code for this page(/stories/GetStoryteller/source.txt) diff --git a/storyteller/storyteller/GetStoryteller/title.txt b/storyteller/storyteller/GetStoryteller/title.txt new file mode 100644 index 0000000..5f47f1c --- /dev/null +++ b/storyteller/storyteller/GetStoryteller/title.txt @@ -0,0 +1 @@ +Starting Storyteller diff --git a/storyteller/storyteller/Images/content.txt b/storyteller/storyteller/Images/content.txt new file mode 100644 index 0000000..894bdbc --- /dev/null +++ b/storyteller/storyteller/Images/content.txt @@ -0,0 +1,33 @@ +Links: ~select:-:Select a subject!selected disabled hidden|S-GetStoryteller:Getting started with Storyteller|S-Structure:The structure of a Storyteller website|S-Markdown:Using Markdown~~space:3~ + +# How to handle images + +When you are an author, text is the easy part but images present problems, no matter if you are using a word processor or an online editing system. The problems fall into two main groups; where to get good images and how to include them in a document. + +Many people have only a poor understanding of what is meant by _image size_. It's not just the amount of space it takes up in storage; of far more importance is the information relating to the dimensions of a picture. In the world of computers this is not inches or centimeters; it's _pixels_. + +If you look closely at a photo in a newspaper, using a magnifying glass, you may see it's constructed of little colored dots. The more dots you have (for the same size in inches) the harder it is to see the dots. In the old days, computers had screens of maybe 9 inches from corner to corner and they displayed 640 dots across and 480 down. This is something around 75 dots per inch. On a bigger screen the dots become much more noticeable. + +Modern smartphones have a longer dimension of 4.5 inches (12cm) or more and a screen _resolution_ (number of dots of 1920, which equates to about 420 dots per inch. This is far too many for the eye to detect so we get the impression of continuous color. + +So to display an image you might assume it should be big enough to fill such a display. Well, this will certainly work, but in fact the image doesn't have to be quite that big. Partly because we can't see all those dots (pixels) and partly because the way the eye detects color requires far less data than that. You might notice a 'jaggy' effect on a single black diagonal line on a white background, but never in a photo. When you magnify a photo you'll see that edges never go straight from black to white; there are always intermediate shades in there too, and this fools the eye into thinking it's seeing a straight line. + +The reason for all this discussion is that photos take up space. On an account such as Neocities you only have a limited amount of this, so it makes no sense for pictures to be any bigger than they need to be. The bigger they are, the longer they take to download too, so pages with big photos load slower. + +The trouble is, your phone takes photos that may be 6000 or more pixels wide and take up many megabytes of storage each. These photos have massive detail so they can be printed on large sheets of paper and still give superb quality. When they are displayed on a screen such as a phone, however, the phone has to squeeze them into the space available, and it does this by throwing away much of the information, to get 6000 pixels down to 1920 or less. This makes little sense, so it's far better if you do the job before putting the photo on your website. + +All computers - including phones - can edit photos. Most of us simply keep the originals and never use a photo editor. Maybe we'll do a bit of simple rotation or cropping but that's it. You probably don't even know what your computer's photo editor is called, let alone how to use it. Well, the good news is, you don't need to. + +As with most things, photo editing can be done online. On any of several websites you upload a photo from your computer, then you have a selection of things you can do with it, including cropping and rotation. But the main thing is to resize it to dimensions that are more suitable for putting onto a website. Then you save the photo back onto your computer, ready to be uploaded to Neocities. + +The size you pick is always a compromise. Bigger means people can download a better quality copy if they want one; smaller saves space. On a big PC screen it's best to keep the width above 1200 pixels so when people click it the full version will fill the screen, but below 2000 pixels to keep the file size down. I usually settle for 1600 pixels wide. + +I haven't done an exhaustive study of online photo editors, but one that seems pretty easy to use is [Photo Resizer](https://www.photoresizer.com/). Try it and see how you get on. + +~sid:GetStoryteller:Getting started with Storyteller~ + +~sid:Structure:The structure of a Storyteller website~ + +~sid:Markdown:Using Markdown in Storyteller~ + +[Click here to see the source code for this page](/storyteller/Images/source.txt) diff --git a/storyteller/storyteller/Images/source.txt b/storyteller/storyteller/Images/source.txt new file mode 100644 index 0000000..ea5e653 --- /dev/null +++ b/storyteller/storyteller/Images/source.txt @@ -0,0 +1,37 @@ +Source code of the 'Images' page + +Below the line is the page source as typed by the site author. +----------------------------------------------------------------------------------------------------------- +Links: ~select:-:Select a subject!selected disabled hidden|S-GetStoryteller:Getting started with Storyteller|S-Structure:The structure of a Storyteller website|S-Markdown:Using Markdown~~space:3~ + +# How to handle images + +When you are an author, text is the easy part but images present problems, no matter if you are using a word processor or an online editing system. The problems fall into two main groups; where to get good images and how to include them in a document. + +Many people have only a poor understanding of what is meant by _image size_. It's not just the amount of space it takes up in storage; of far more importance is the information relating to the dimensions of a picture. In the world of computers this is not inches or centimeters; it's _pixels_. + +If you look closely at a photo in a newspaper, using a magnifying glass, you may see it's constructed of little colored dots. The more dots you have (for the same size in inches) the harder it is to see the dots. In the old days, computers had screens of maybe 9 inches from corner to corner and they displayed 640 dots across and 480 down. This is something around 75 dots per inch. On a bigger screen the dots become much more noticeable. + +Modern smartphones have a longer dimension of 4.5 inches (12cm) or more and a screen _resolution_ (number of dots of 1920, which equates to about 420 dots per inch. This is far too many for the eye to detect so we get the impression of continuous color. + +So to display an image you might assume it should be big enough to fill such a display. Well, this will certainly work, but in fact the image doesn't have to be quite that big. Partly because we can't see all those dots (pixels) and partly because the way the eye detects color requires far less data than that. You might notice a 'jaggy' effect on a single black diagonal line on a white background, but never in a photo. When you magnify a photo you'll see that edges never go straight from black to white; there are always intermediate shades in there too, and this fools the eye into thinking it's seeing a straight line. + +The reason for all this discussion is that photos take up space. On an account such as Neocities you only have a limited amount of this, so it makes no sense for pictures to be any bigger than they need to be. The bigger they are, the longer they take to download too, so pages with big photos load slower. + +The trouble is, your phone takes photos that may be 6000 or more pixels wide and take up many megabytes of storage each. These photos have massive detail so they can be printed on large sheets of paper and still give superb quality. When they are displayed on a screen such as a phone, however, the phone has to squeeze them into the space available, and it does this by throwing away much of the information, to get 6000 pixels down to 1920 or less. This makes little sense, so it's far better if you do the job before putting the photo on your website. + +All computers - including phones - can edit photos. Most of us simply keep the originals and never use a photo editor. Maybe we'll do a bit of simple rotation or cropping but that's it. You probably don't even know what your computer's photo editor is called, let alone how to use it. Well, the good news is, you don't need to. + +As with most things, photo editing can be done online. On any of several websites you upload a photo from your computer, then you have a selection of things you can do with it, including cropping and rotation. But the main thing is to resize it to dimensions that are more suitable for putting onto a website. Then you save the photo back onto your computer, ready to be uploaded to Neocities. + +The size you pick is always a compromise. Bigger means people can download a better quality copy if they want one; smaller saves space. On a big PC screen it's best to keep the width above 1200 pixels so when people click it the full version will fill the screen, but below 2000 pixels to keep the file size down. I usually settle for 1600 pixels wide. + +I haven't done an exhaustive study of online photo editors, but one that seems pretty easy to use is [Photo Resizer](https://www.photoresizer.com/). Try it and see how you get on. + +~sid:GetStoryteller:Getting started with Storyteller~ + +~sid:Structure:The structure of a Storyteller website~ + +~sid:Markdown:Using Markdown in Storyteller~ + +[Click here to see the source code for this page](/stories/Images/source.txt) diff --git a/storyteller/storyteller/Images/title.txt b/storyteller/storyteller/Images/title.txt new file mode 100644 index 0000000..f449e11 --- /dev/null +++ b/storyteller/storyteller/Images/title.txt @@ -0,0 +1 @@ +Handling images diff --git a/storyteller/storyteller/Markdown/content.txt b/storyteller/storyteller/Markdown/content.txt new file mode 100644 index 0000000..f2060bb --- /dev/null +++ b/storyteller/storyteller/Markdown/content.txt @@ -0,0 +1,60 @@ +Links: ~select:-:Select a subject!selected disabled hidden|S-GetStoryteller:Getting started with Storyteller|S-Structure:The structure of a Storyteller website|S-Images:Handling images~~space:3~Next: ~sid:Images:Handling images~ + +# Using Markdown in Storyteller + +Markdown is a standard, widely used lightweight document formatting system. It’s possible you’ve encountered Markdown without realizing it. Facebook chat, Skype, and Reddit all let you use different flavors of Markdown to format your messages. + +There are many resources on the Web that describe the standard syntax of Markdown. Try Googling _markdown cheatsheet_ or _try markdown_, the latter of which lets you try out various constructs and see the results. + +Storyteller adds custom enhancements to Markdown, to do things that would be impossible otherwise. (They would be pretty hard to do in in HTML too, not to mention inconvenient.) For example, although you can easily define a link to another page outside the current one there's no standard construct that will tell Markdown you want to go to another Topic in the same Subject. So here's a list of our custom additions. Each one is text you put inside a pair of ~ symbols. Please note that StoryTeller is a new product so more custom Markdown additions may be added over the course of time. + + - ~`sid:{name}:{link text}`~ sets up a hyperlink that looks like any other hyperlink, with a blue underline, but when the user clicks it the page changes to the Subject whose name is `{name}`. You can use the first part and leave out the second colon and what follows it; the system will simply use the same text for both. + + - ~`tid:{name}:{link text}`~ sets up a hyperlink that when the user clicks it the page changes to the Topic whose name is `{name}` in the current Subject. In this case the `.txt` part of the name is omitted. As with `sid`, id the colon and the second part are omitted the system will use the same text for both. + + - ~`img:{url}:{class}!{options}`~ Although standard Markdown has markup to add an image, this takes it further. The first part, `{url}` is the URL of the image. If this begins with `http` it's taken to be an external image at the full path given. If not, then it's assumed to be an image in the `images` folder of the current Subject. So the images for each Subject are independent. + + The second part, `class`, separated from the first part by a colon, is a list of space-delimited styles you want to apply to the image, for example to have it float left or right, add a border around it or whatever. The current list of styles available are: + - `border` Place a thin border around the image + - `left` Make the image float to the left + - `right` Make the image float to the right + - `center` Make the image sit in the middle of the page + - `clear` Make the image sit below anything else already on the page + - `10%` Make the width of the image 10% of the page + - `25%` Make the width of the image 25% of the page + - `33%` Make the width of the image 33% of the page + - `50%` Make the width of the image 50% of the page + - `90%` Make the width of the image 90% of the page + - `100%` Make the width of the image 100% of the page + + The final part is optional. When used, an exclamation mark is followed by a list of options. At present there is only one; `nolink`. The standard behavior of a link is to display it as a bigger image when the user clicks it. If you don't want this to happen, use `nolink`. + + - ~`select:item|item|item... ...`~ specifies a drop-down list with as many items as you like, separated from each other by the vertical pipe (|) symbol. Each item is in 2 or 3 parts as follows: + + 1. The first part is the _value_ of the item. This is either a Subject or a Topic; the value has S- or T- prefixed to indicate which. See the example below. + + 1. A colon, then the text that will appear in the drop-down list for the item + + 1. Optionally, an exclamation mark then a list of attributes for the item + + Example: ~select:-:Select a subject!selected disabled hidden|S-Food:Food and drink|S-Transport:Transport links|S-Entertainment:Cinemas, theaters and clubs|T-More:Other resources~ + + In this example, the first option is is a prompt ("Select a subject") which is marked as being selected but also disabled and hidden, so it shows when the selector is closed but not in the list itself. The next 3 options are all Sections and will take you there when selected. The last option is a Topic in the current Subject. + + - ~`clear`~ adds a 'clear', that forces all following content to be below anything already on the page. This is not usually an issue except where you use the `left` or `right` classes for an image, which causes text to flow around the image. Don't try to get the next paragraph to appear in the right place by adding blank lines, as this will not give the same resuts on all browsers. Instead, use a 'clear' before adding the next paragraph. + + - ~`space:{n}`~ adds non-breaking spaces, the number of spaces being the value of `{n}`. If the page is running on a smartphone in portrait mode a single new line is added instead of the spaces. + +## Page style + +As well as content, the page style itself is also a variable. At present we have a single style, that of a bordered page. In future we'll add a convential scrolling page option too. There's also the theme, being the appearance and size of the border. This only applies to the _bordered_ style; scrolling pages may well have their own themes in due course. We'll be offering a selection of themes that you can try out here or even in your own page. To do this you need a special Markdown tag, as follows: + + - ~`theme:{prompt}`~ invokes the theme selector. If you choose a theme from the list offered it overrides the one named in your `theme.txt` file, but only for your current browser and computer. Nobody else is affected. If that's not clear, try it and see by clicking ~theme:here~. + +~sid:GetStoryteller:Getting started with Storyteller~ + +~sid:Structure:The structure of a Storyteller website~ + +~sid:Images:Handling images~ + +[Click here to see the source code for this page](/storyteller/Markdown/source.txt) diff --git a/storyteller/storyteller/Markdown/source.txt b/storyteller/storyteller/Markdown/source.txt new file mode 100644 index 0000000..2041f40 --- /dev/null +++ b/storyteller/storyteller/Markdown/source.txt @@ -0,0 +1,64 @@ +Source code of the 'Markdown' page + +Below the line is the page source as typed by the site author. +----------------------------------------------------------------------------------------------------------- +Links: ~select:-:Select a subject!selected disabled hidden|S-GetStoryteller:Getting started with Storyteller|S-Structure:The structure of a Storyteller website|S-Images:Handling images~~space:3~Next: ~sid:Images:Handling images~ + +# Using Markdown in Storyteller + +Markdown is a standard, widely used lightweight document formatting system. It’s possible you’ve encountered Markdown without realizing it. Facebook chat, Skype, and Reddit all let you use different flavors of Markdown to format your messages. + +There are many resources on the Web that describe the standard syntax of Markdown. Try Googling _markdown cheatsheet_ or _try markdown_, the latter of which lets you try out various constructs and see the results. + +Storyteller adds custom enhancements to Markdown, to do things that would be impossible otherwise. (They would be pretty hard to do in in HTML too, not to mention inconvenient.) For example, although you can easily define a link to another page outside the current one there's no standard construct that will tell Markdown you want to go to another Topic in the same Subject. So here's a list of our custom additions. Each one is text you put inside a pair of ~ symbols. Please note that StoryTeller is a new product so more custom Markdown additions may be added over the course of time. + + - ~`sid:{name}:{link text}`~ sets up a hyperlink that looks like any other hyperlink, with a blue underline, but when the user clicks it the page changes to the Subject whose name is `{name}`. You can use the first part and leave out the second colon and what follows it; the system will simply use the same text for both. + + - ~`tid:{name}:{link text}`~ sets up a hyperlink that when the user clicks it the page changes to the Topic whose name is `{name}` in the current Subject. In this case the `.txt` part of the name is omitted. As with `sid`, id the colon and the second part are omitted the system will use the same text for both. + + - ~`img:{url}:{class}!{options}`~ Although standard Markdown has markup to add an image, this takes it further. The first part, `{url}` is the URL of the image. If this begins with `http` it's taken to be an external image at the full path given. If not, then it's assumed to be an image in the `images` folder of the current Subject. So the images for each Subject are independent. + + The second part, `class`, separated from the first part by a colon, is a list of space-delimited styles you want to apply to the image, for example to have it float left or right, add a border around it or whatever. The current list of styles available are: + - `border` Place a thin border around the image + - `left` Make the image float to the left + - `right` Make the image float to the right + - `center` Make the image sit in the middle of the page + - `clear` Make the image sit below anything else already on the page + - `10%` Make the width of the image 10% of the page + - `25%` Make the width of the image 25% of the page + - `33%` Make the width of the image 33% of the page + - `50%` Make the width of the image 50% of the page + - `90%` Make the width of the image 90% of the page + - `100%` Make the width of the image 100% of the page + + The final part is optional. When used, an exclamation mark is followed by a list of options. At present there is only one; `nolink`. The standard behavior of a link is to display it as a bigger image when the user clicks it. If you don't want this to happen, use `nolink`. + + - ~`select:item|item|item... ...`~ specifies a drop-down list with as many items as you like, separated from each other by the vertical pipe (|) symbol. Each item is in 2 or 3 parts as follows: + + 1. The first part is the _value_ of the item. This is either a Subject or a Topic; the value has S- or T- prefixed to indicate which. See the example below. + + 1. A colon, then the text that will appear in the drop-down list for the item + + 1. Optionally, an exclamation mark then a list of attributes for the item + + Example: ~select:-:Select a subject!selected disabled hidden|S-Food:Food and drink|S-Transport:Transport links|S-Entertainment:Cinemas, theaters and clubs|T-More:Other resources~ + + In this example, the first option is is a prompt ("Select a subject") which is marked as being selected but also disabled and hidden, so it shows when the selector is closed but not in the list itself. The next 3 options are all Sections and will take you there when selected. The last option is a Topic in the current Subject. + + - ~`clear`~ adds a 'clear', that forces all following content to be below anything already on the page. This is not usually an issue except where you use the `left` or `right` classes for an image, which causes text to flow around the image. Don't try to get the next paragraph to appear in the right place by adding blank lines, as this will not give the same resuts on all browsers. Instead, use a 'clear' before adding the next paragraph. + + - ~`space:{n}`~ adds non-breaking spaces, the number of spaces being the value of `{n}`. If the page is running on a smartphone in portrait mode a single new line is added instead of the spaces. + +## Page style + +As well as content, the page style itself is also a variable. At present we have a single style, that of a bordered page. In future we'll add a convential scrolling page option too. There's also the theme, being the appearance and size of the border. This only applies to the _bordered_ style; scrolling pages may well have their own themes in due course. We'll be offering a selection of themes that you can try out here or even in your own page. To do this you need a special Markdown tag, as follows: + + - ~`theme:{prompt}`~ invokes the theme selector. If you choose a theme from the list offered it overrides the one named in your `theme.txt` file, but only for your current browser and computer. Nobody else is affected. If that's not clear, try it and see by clicking ~theme:here~. + +~sid:GetStoryteller:Getting started with Storyteller~ + +~sid:Structure:The structure of a Storyteller website~ + +~sid:Images:Handling images~ + +[Click here to see the source code for this page](/stories/Markdown/source.txt) diff --git a/storyteller/storyteller/Markdown/title.txt b/storyteller/storyteller/Markdown/title.txt new file mode 100644 index 0000000..b5f0b45 --- /dev/null +++ b/storyteller/storyteller/Markdown/title.txt @@ -0,0 +1 @@ +Using Markdown \ No newline at end of file diff --git a/storyteller/storyteller/Structure/content.txt b/storyteller/storyteller/Structure/content.txt new file mode 100644 index 0000000..2e56316 --- /dev/null +++ b/storyteller/storyteller/Structure/content.txt @@ -0,0 +1,33 @@ +Links: ~select:-:Select a subject!selected disabled hidden|S-GetStoryteller:Getting started with Storyteller|S-Markdown:Using Markdown|S-Images:Handling images~~space:3~Next: ~sid:Markdown:Using Markdown~ + +# The structure of a Storyteller website + +A Storyteller website has an `index.html` file and a folder containing your stories. Inside the stories folder are a number of other folders and two text files. One of these is `theme.txt`; it contains the name of the _theme_ you will use for your site. A small number of themes are available. The second text file is `title.txt`, which contains the title that goes in the browser's title bar. + +The folders in your stories folder are _Subjects_, which are groups of pages that belong together. Every Subject can have as many _Topics_ as you like, where a Topic is another page belonging to the same Subject. Every Storyteller website starts with a subject called called `home`, which will be shown when the user clicks the Home button at the top of the screen. The `home` Subject and its various Topics form the home page for the website. You can also add an `info` subject folder and use it to provide information not directly related to the website itself. All other folders in your stories folder are Subjects chosen by the site builder. For example, this site has a number of Subjects in addition to `home`, all of which are linked to at the bottom of each page. + +As an example, suppose we are building a car maintenance website. Doing an oil change, checking the brakes, replacing a hose etc could each be Sections and the steps involved in each one of the jobs could be Topics. If it were a classic car catalog there might be a Section for each car model, with Topics for the various aspects of interest such as History, Specification, Performance and so on. The division is rather arbitrary and entirely up to the site builder. + +## Subject folders + +All Subject folders, including `home` and `info`, contain at least 2 files; `title.txt` and `content.txt`. The first of these, `title.txt`, contains a single line of text comprising the name of the Subject, which is displayed at the top of the page all the time you are in the same Subject. If `title.txt` is empty the system will use the name of the Subject. The page you are in now is part of the `Structure` Subject and its `title.txt` contains the word `Storyteller`. + +The second file, `content.txt`, contains the content of the main Topic. This is the text and images that will be seen by visitors to your website. Rather than expect you to be an HTML expert we use Markdown, a far simpler system that is very widely used for a variety of documentation projects. Markdown is less comprehensive than HTML but is very quick to learn and much more forgiving of mistakes. See our ~sid:Markdown:Markdown Guide~. + +If your Subject has additional Topics, each of these appears in the Subject folder as `{topicname}.txt`. You can have as many as you like. It's your responsibility to take care of linking between Subjects and Topics. In general, the more links the better as it makes it easier for users to discover all the parts of your website. + +## Custom themes + +The themes provided are rather basic and some may like to create their own. You can do this by creating a `themes` folder at the top level of your website, then copying one of the standard theme folders from the GitHub repository, giving it a new name on arrival. Themes comprise the graphic from which the borders are taken (this isn't actually used but we provide it in case it might be useful). Then there are 3 other graphics; the top, middle and bottom of the frame. The top and bottom should be no taller than is needed to include everything. The middle graphic is any section taken from the border graphic. It will be stretched to fit the window and placed beneath the top and the bottom. + +Finally there's a JSON file that defines various constants. First there are the aspect width and height; normally 4:3 but you can experiment with other ratios. Then there are the border percentages for left, top, right and bottom, and finally the font scaling value, which is roughly the number of lines of text to fill the page. The larger this value the smaller the text, but the right value can only be arrived at by experiment. + +To use your theme, put its name into the `theme.txt` file at the top level of your stories folder. Put a forward slash - `/` - in front of the name so the system will take it from your local folder rather than ask the CDN for it. + +~sid:GetStoryteller:Getting started with Storyteller~ + +~sid:Markdown:Using Markdown in Storyteller~ + +~sid:Images:Handling images~ + +[Click here to see the source code for this page](/storyteller/Structure/source.txt) diff --git a/storyteller/storyteller/Structure/source.txt b/storyteller/storyteller/Structure/source.txt new file mode 100644 index 0000000..fd071a8 --- /dev/null +++ b/storyteller/storyteller/Structure/source.txt @@ -0,0 +1,29 @@ +Source code of the 'Structure' page + +Below the line is the page source as typed by the site author. +----------------------------------------------------------------------------------------------------------- +Links: ~select:-:Select a subject!selected disabled hidden|S-GetStoryteller:Getting started with Storyteller|S-Markdown:Using Markdown|S-Images:Handling images~~space:3~Next: ~sid:Markdown:Using Markdown~ + +# The structure of a Storyteller website + +A Storyteller website has an `index.html` file and a `stories` folder. Inside `stories` are a number of other folders and two text files. One of these is `theme.txt`; it contains the name of the _theme_ you will use for your site. A small number of themes are available. The second text file is `title.txt`, which contains the title that goes in the browser's title bar. + +The folders in `stories` are _Subjects_, which are groups of pages that belong together. Every Subject can have as many _Topics_ as you like, where a Topic is another page belonging to the same Subject. Every Storyteller website starts with a subject called called `home`, which will be shown when the user clicks the Home button at the top of the screen. The `home` Subject and its various Topics form the home page for the website. You can also add an `info` subject folder and use it to provide information not directly related to the website itself. All other folders in `stories` are Subjects chosen by the site builder. For example, this site has a number of Subjects in addition to `home`, all of which are linked to at the bottom of each page. (The link for the current Subject is disabled as there's no point in clicking it.). + +As an example, suppose we are building a car maintenance website. Doing an oil change, checking the brakes, replacing a hose etc could each be Sections and the steps involved in each one of the jobs could be Topics. If it were a classic car catalog there might be a Section for each car model, with Topics for the various aspects of interest such as History, Specification, Performance and so on. The division is rather arbitrary and entirely up to the site builder. + +## Subject folders + +All Subject folders, including `home` and `info`, contain at least 2 files; `title.txt` and `content.txt`. The first of these, `title.txt`, contains a single line of text comprising the name of the Subject, which is displayed at the top of the page all the time you are in the same Subject. If `title.txt` is empty the system will use the name of the Subject. The page you are in now is part of the `Structure` Subject and its `title.txt` contains the word `Storyteller`. + +The second file, `content.txt`, contains the content of the main Topic. This is the text and images that will be seen by visitors to your website. Rather than expect you to be an HTML expert we use Markdown, a far simpler system that is very widely used for a variety of documentation projects. Markdown is less comprehensive than HTML but is very quick to learn and much more forgiving of mistakes. See our ~sid:Markdown:Markdown Guide~. + +If your Subject has additional Topics, each of these appears in the Subject folder as `{topicname}.txt`. You can have as many as you like. It's your responsibility to take care of linking between Subjects and Topics. In general, the more links the better as it makes it easier for users to discover all the parts of your website. + +~sid:GetStoryteller:Getting started with Storyteller~ + +~sid:Markdown:Using Markdown in Storyteller~ + +~sid:Images:Handling images~ + +[Click here to see the source code for this page](/stories/Structure/source.txt) diff --git a/storyteller/storyteller/Structure/title.txt b/storyteller/storyteller/Structure/title.txt new file mode 100644 index 0000000..fcead35 --- /dev/null +++ b/storyteller/storyteller/Structure/title.txt @@ -0,0 +1 @@ +Inside Storyteller \ No newline at end of file diff --git a/storyteller/storyteller/home/content.txt b/storyteller/storyteller/home/content.txt new file mode 100644 index 0000000..9e35ece --- /dev/null +++ b/storyteller/storyteller/home/content.txt @@ -0,0 +1,23 @@ +Links: ~select:-:Select a subject!selected disabled hidden|S-GetStoryteller:Getting started with Storyteller|S-Structure:The structure of a Storyteller website|S-Markdown:Using Markdown|S-Images:Handling images~~space:3~Next: ~sid:GetStoryteller:Getting started with Storyteller~ + +# Introducing Storyteller + +Storyteller is a software toolkit for building websites that are mainly text with images, such as this one, for presenting news, information or stories. It's particularly well suited to building 'static' websites that will continue to exist indefinitely without requiring any maintenance, annual hosting fees or domain registration. + +~img:demo0.jpg:right 25%~Personal/family histories and other forms of legacy website need to continue after the author has moved on or died and it often cannot be guaranteed that anyone will pay for hosting. Other good subjects for this kind of presentation are instruction manuals - such as for car maintenance - and cooking or gardening guides. Once created, these sites rarely need changing. + +~img:demo3.jpg:left 25%~No programming experience is needed to build pages yourself using Storyteller; not even of HTML. If you can use a word processor you already have all the skills needed. Everything is done with "Markdown", a much simpler coding system that is mostly plain text, with a few special symbols used to create headings, link to other pages, call external websites and so on. All this is documented here. + +~img:demo5.jpg:right 25%~ In these pages we will show you how to quickly setup a free website on [Neocities](https://neocities.org) to supply information on any subject you choose. Storyteller is for _authors_; people with something to say who need a simple way to get their message out rather than getting bogged down in Web technologies. + +This site is of course built with Storyteller. The images on the page have no relevance; they were just chosen at random. + +~sid:GetStoryteller:Getting started with Storyteller~ + +~sid:Structure:The structure of a Storyteller website~ + +~sid:Markdown:Using Markdown in Storyteller~ + +~sid:Images:Handling images~ + +[Click here to see the source code for this page](/storyteller/home/source.txt) diff --git a/storyteller/storyteller/home/images/demo0.jpg b/storyteller/storyteller/home/images/demo0.jpg new file mode 100644 index 0000000..0f4ad2d Binary files /dev/null and b/storyteller/storyteller/home/images/demo0.jpg differ diff --git a/storyteller/storyteller/home/images/demo3.jpg b/storyteller/storyteller/home/images/demo3.jpg new file mode 100644 index 0000000..7859032 Binary files /dev/null and b/storyteller/storyteller/home/images/demo3.jpg differ diff --git a/storyteller/storyteller/home/images/demo5.jpg b/storyteller/storyteller/home/images/demo5.jpg new file mode 100644 index 0000000..88ffe6b Binary files /dev/null and b/storyteller/storyteller/home/images/demo5.jpg differ diff --git a/storyteller/storyteller/home/images/demo8.jpg b/storyteller/storyteller/home/images/demo8.jpg new file mode 100644 index 0000000..45f73a9 Binary files /dev/null and b/storyteller/storyteller/home/images/demo8.jpg differ diff --git a/storyteller/storyteller/home/source.txt b/storyteller/storyteller/home/source.txt new file mode 100644 index 0000000..675d38a --- /dev/null +++ b/storyteller/storyteller/home/source.txt @@ -0,0 +1,25 @@ +Source code of the 'home' page. + +Below the line is the page source as typed by the site author. +----------------------------------------------------------------------------------------------------------- +Links: ~select:-:Select a subject!selected disabled hidden|S-GetStoryteller:Getting started with Storyteller|S-Structure:The structure of a Storyteller website|S-Markdown:Using Markdown|S-Images:Handling images~~space:3~Next: ~sid:GetStoryteller:Getting started with Storyteller~ + +Storyteller is a software toolkit for building websites that are mainly text with images, such as this one, for presenting news, information or stories. It's particularly well suited to building 'static' websites that will continue to exist indefinitely without requiring any maintenance, annual hosting fees or domain registration. + +~img:demo0.jpg:right 25%~Personal/family histories and other forms of legacy website need to continue after the author has moved on or died and it often cannot be guaranteed that anyone will pay for hosting. Other good subjects for this kind of presentation are instruction manuals - such as for car maintenance - and cooking or gardening guides. Once created, these sites rarely need changing. + +~img:demo3.jpg:left 25%~No programming experience is needed to build pages yourself using Storyteller; not even of HTML. If you can use a word processor you already have all the skills needed. Virtually everything is done with "Markdown", a much simpler coding system that is mostly plain text, with a few special symbols used to create headings, link to other pages, call external websites and so on. All this is documented here. + +~img:demo5.jpg:right 25%~ In these pages we will show you how to quickly setup a free website on [Neocities](https://neocities.org) to supply information on any subject you choose. Storyteller is for _authors_; people with something to say who need a simple way to get their message out rather than getting bogged down in Web technologies. + +This site is of course built with Storyteller. The images on the page have no relevance; they were just chosen at random. + +~sid:GetStoryteller:Getting started with Storyteller~ + +~sid:Structure:The structure of a Storyteller website~ + +~sid:Markdown:Using Markdown in Storyteller~ + +~sid:Images:Handling images~ + +[Click here to see the source code for this page](/stories/home/source.txt) diff --git a/storyteller/storyteller/home/title.txt b/storyteller/storyteller/home/title.txt new file mode 100644 index 0000000..e792b81 --- /dev/null +++ b/storyteller/storyteller/home/title.txt @@ -0,0 +1 @@ +Storyteller diff --git a/storyteller/storyteller/info/content.txt b/storyteller/storyteller/info/content.txt new file mode 100644 index 0000000..1b60867 --- /dev/null +++ b/storyteller/storyteller/info/content.txt @@ -0,0 +1,25 @@ +# About Storyteller + +Storyteller is a system for creating online personal stories and family histories, though it can be used for many other things as well. + +Everyone has a story to tell; the only question is where to tell it. In the past, people wrote diaries if they were able to, and some of the wealthier ones published their memoirs. Some of these documents survive in physical form; others have been digitized. Poorer people in general left no trace of their lives, but today there is no excuse for that; the Internet is there for all if you know where to look. + +Today we rely on websites to store all information and on search engines such as Google to find that information. The storage capacity of the Internet is effectively infinite. In the world of business, what goes in tends to stay there because it takes too long to sort out what is wanted and what is not. This does not necessarily mean everything in there can be found, only that it's in there _somewhere_. That's how things are in 2020 and the signs are that this direction of progress will continue. + +It's not yet quite the same for personal websites, though. These have to be hosted somewhere and in most cases that incurs a cost. Given the small size of an average personal website I think the time will come when the cost of storage will fall to zero for these too, but we're not quite there yet. However, there are a few places you can publish a website without having to pay an annual fee for hosting. This is of course quite important when it comes to memoirs, for these documents are intended to last beyond the lifetime of their authors. + +At the time of writing there are a number of options for hosting your own personal website for free. One of the best known is probably [Wix](https://wix.com), which will let you set up a traditional website largely by using the mouse to select from a list of choices. Many people will find it suits them, though it's easy to get confused by the range of options offered. Also, you are tied to Wix with no possibility of moving your website somewhere else. For most people this is not an issue, but you should be aware of it. + +My own feeling is that the traditional format of websites may not work best for personal memoirs. These are a kind of reference document that is primarily text, with images taking second place. Like newspapers and magazines they tend to use a fairly simple layout that doesn't change, which caters well for the needs of people who don't have a natural flair for design and prefer to be presented with just one way to do things. In many areas of online publishing such as instruction manuals or blogs the preferred tools are all text based and use simple column formats rather than boxes of text and pictures splashed all over the page. + +The options for free hosting are fairly limited and having looked around I've come to the conclusion that [Neocities](https://neocities.org) is probably the easiest one to set up and use. It offers both free and paid-for hosting; the former comes with a limit of 1GB of storage, which is quite enough to host a fairly substantial private website, and it's the one I've chosen to host my own memoirs as well as the documentation pages for Storyteller itself. Other possibilities are [Github Pages](https://pages.github.com/) and [DropPages](https://droppages.com/). + +Neocities provide you with the means to manage your website, in the form of a simple management interface. You get access to every file on your site, with the ability to create new files and folders, delete, rename and edit files. The editing tool is a simple text editor; no tools are provided for selecting images. You are expected to understand HTML and CSS and to know your way around the filing system of your own computer. + +Storyteller makes it a little easier to manage this process, by removing the need for you to directly deal with HTML and CSS files. The text files you create are plain text, not HTML, and they include a few special "tags" that identify features such as headers, hyperlinks or pictures. These special tags are very easy to learn and after a while become second nature to most people. The system is called **_Markdown_**; a Google search will turn up plenty of information about it. Storyteller adds a few special tags of its own; these are documented here on our ~sid:Markdown~ page. + +Storyteller is easy to use and anyone can build their own website with it. Most of the work is in writing the copy and finding the photos that will illustrate that copy. As with this website, yours can be hosted for free on [Neocities](https://neocities.org), where setting up an account is very simple. + +If you have questions, feel free to write to [the author of the Storyteller system](mailto:gtanyware@gmail.com). Since Storyteller is very new and currently under development, if you are intending to try it out I strongly recommend you get in touch, as features tend to change from time to time and the documentation doesn't always keep up. + +[Click here to see the source code for this page](/storyteller/info/source.txt) diff --git a/storyteller/storyteller/info/source.txt b/storyteller/storyteller/info/source.txt new file mode 100644 index 0000000..cfa71c0 --- /dev/null +++ b/storyteller/storyteller/info/source.txt @@ -0,0 +1,29 @@ +Source code of the 'info' page + +Below the line is the page source as typed by the site author. +----------------------------------------------------------------------------------------------------------- +# About Storyteller + +Storyteller is a system for creating online personal stories and family histories, though it can be used for many other things as well. + +Everyone has a story to tell; the only question is where to tell it. In the past, people wrote diaries if they were able to, and some of the wealthier ones published their memoirs. Some of these documents survive in physical form; others have been digitized. + +Today we rely on websites to store all information and on search engines such as Google to find that information. The storage capacity of the Internet is effectively infinite. In the world of business, what goes in tends to stay there because it takes too long to sort out what is wanted and what is not. This does not necessarily mean everything in there can be found, only that it's in there _somewhere_. That's how things are in 2020 and the signs are that this direction of progress will continue. + +It's not yet quite the same for personal websites, though. These have to be hosted somewhere and in most cases that incurs a cost. Given the small size of an average personal website I think the time will come when the cost of storage will fall to zero for these too, but we're not quite there yet. However, there are a few places you can publish a website without having to pay an annual fee for hosting. This is of course quite important when it comes to memoirs, for these documents are intended to last beyond the lifetime of their authors. + +At the time of writing there are a number of options for hosting your own personal website for free. One of the best known is probably [Wix](https://wix.com), which will let you set up a traditional website largely by using the mouse to select from a list of choices. Many people will find it suits them, though it's easy to get confused by the range of options offered. Also, you are tied to Wix with no possibility of moving your website somewhere else. For most people this is not an issue, but you should be aware of it. + +My own feeling is that the traditional format of websites may not work best for personal memoirs. These are a kind of reference document that is primarily text, with images taking second place. Like newspapers and magazines they tend to use a fairly simple layout that doesn't change, which caters well for the needs of people who don't have a natural flair for design and prefer to be presented with just one way to do things. In many areas of online publishing such as instruction manuals or blogs the preferred tools are all text based and use simple column formats rather than boxes of text and pictures splashed all over the page. + +The options for free hosting are fairly limited and having looked around I've come to the conclusion that [Neocities](https://neocities.org) is probably the easiest one to set up and use. It offers both free and paid-for hosting; the former comes with a limit of 1GB of storage, which is quite enough to host a fairly substantial private website, and it's the one I've chosen to host my own memoirs. Other possibilities are [Github Pages](https://pages.github.com/) and [DropPages](https://droppages.com/). + +Neocities provide you with the means to manage your website, in the form of a simple management interface. You get access to every file on your site, with the ability to create new files and folders, delete, rename and edit files. The editing tool is a simple text editor; no tools are provided for selecting images. You are expected to understand HTML and CSS and to know your way around the filing system of your own computer. + +Storyteller makes it a little easier to manage this process, by removing the need for you to directly deal with HTML and CSS files. The text files you create are plain text, not HTML, and they include a few special "tags" that identify features such as headers, hyperlinks or pictures. These special tags are very easy to learn and after a while become second nature to most people. The system is called **_Markdown_**; a Google search will turn up plenty of information about it. Storyteller adds a few special tags of its own; these are documented right here on our ~sid:Markdown~ page. + +Storyteller is easy to use and anyone can build their own website with it. Most of the work is in writing the copy and finding the photos that will illustrate that copy. As with this website, yours can be hosted for free on [Neocities](https://neocities.org), where setting up an account is very simple. + +If you have questions, feel free to write to [the author of the Storyteller system](mailto:gtanyware@gmail.com). Since Storyteller is very new and currently under development, if you are intending to try it out I strongly recommend you get in touch, as features tend to change from time to time and the documentation doesn't always keep up. + +[Click here to see the source code for this page](/stories/info/source.txt) diff --git a/storyteller/storyteller/info/title.txt b/storyteller/storyteller/info/title.txt new file mode 100644 index 0000000..4a8270e --- /dev/null +++ b/storyteller/storyteller/info/title.txt @@ -0,0 +1 @@ + Information diff --git a/storyteller/storyteller/theme.txt b/storyteller/storyteller/theme.txt new file mode 100644 index 0000000..9ab5a8a --- /dev/null +++ b/storyteller/storyteller/theme.txt @@ -0,0 +1 @@ +storyteller diff --git a/storyteller/storyteller/title.txt b/storyteller/storyteller/title.txt new file mode 100644 index 0000000..e792b81 --- /dev/null +++ b/storyteller/storyteller/title.txt @@ -0,0 +1 @@ +Storyteller diff --git a/storyteller/themes/black-line/black-line.jpg b/storyteller/themes/black-line/black-line.jpg new file mode 100644 index 0000000..707a679 Binary files /dev/null and b/storyteller/themes/black-line/black-line.jpg differ diff --git a/storyteller/themes/black-line/bottom.jpg b/storyteller/themes/black-line/bottom.jpg new file mode 100644 index 0000000..bf9d098 Binary files /dev/null and b/storyteller/themes/black-line/bottom.jpg differ diff --git a/storyteller/themes/black-line/mid.jpg b/storyteller/themes/black-line/mid.jpg new file mode 100644 index 0000000..fbd9514 Binary files /dev/null and b/storyteller/themes/black-line/mid.jpg differ diff --git a/storyteller/themes/black-line/theme.json b/storyteller/themes/black-line/theme.json new file mode 100644 index 0000000..acb5b51 --- /dev/null +++ b/storyteller/themes/black-line/theme.json @@ -0,0 +1,9 @@ +{ + "aspect-w":4, + "aspect-h":3, + "border-l":1, + "border-t":1, + "border-r":2, + "border-b":3, + "font-scale":40 +} diff --git a/storyteller/themes/black-line/thin gold.jpg b/storyteller/themes/black-line/thin gold.jpg new file mode 100644 index 0000000..a4c0486 Binary files /dev/null and b/storyteller/themes/black-line/thin gold.jpg differ diff --git a/storyteller/themes/black-line/top.jpg b/storyteller/themes/black-line/top.jpg new file mode 100644 index 0000000..a7ce4f3 Binary files /dev/null and b/storyteller/themes/black-line/top.jpg differ diff --git a/storyteller/themes/grass/bottom.jpg b/storyteller/themes/grass/bottom.jpg new file mode 100644 index 0000000..6415565 Binary files /dev/null and b/storyteller/themes/grass/bottom.jpg differ diff --git a/storyteller/themes/grass/grass.jpg b/storyteller/themes/grass/grass.jpg new file mode 100644 index 0000000..4ead468 Binary files /dev/null and b/storyteller/themes/grass/grass.jpg differ diff --git a/storyteller/themes/grass/mid.jpg b/storyteller/themes/grass/mid.jpg new file mode 100644 index 0000000..1464467 Binary files /dev/null and b/storyteller/themes/grass/mid.jpg differ diff --git a/storyteller/themes/grass/theme.json b/storyteller/themes/grass/theme.json new file mode 100644 index 0000000..b0981b2 --- /dev/null +++ b/storyteller/themes/grass/theme.json @@ -0,0 +1,9 @@ +{ + "aspect-w":4, + "aspect-h":3, + "border-l":12, + "border-t":10, + "border-r":10, + "border-b":10, + "font-scale":40 +} diff --git a/storyteller/themes/grass/top.jpg b/storyteller/themes/grass/top.jpg new file mode 100644 index 0000000..8b1d6cd Binary files /dev/null and b/storyteller/themes/grass/top.jpg differ diff --git a/storyteller/themes/herbal/bottom.jpg b/storyteller/themes/herbal/bottom.jpg new file mode 100644 index 0000000..a2fbeb9 Binary files /dev/null and b/storyteller/themes/herbal/bottom.jpg differ diff --git a/storyteller/themes/herbal/herbal.png b/storyteller/themes/herbal/herbal.png new file mode 100644 index 0000000..ad5458c Binary files /dev/null and b/storyteller/themes/herbal/herbal.png differ diff --git a/storyteller/themes/herbal/herbal.xcf b/storyteller/themes/herbal/herbal.xcf new file mode 100644 index 0000000..8f24799 Binary files /dev/null and b/storyteller/themes/herbal/herbal.xcf differ diff --git a/storyteller/themes/herbal/mid.jpg b/storyteller/themes/herbal/mid.jpg new file mode 100644 index 0000000..2a1a572 Binary files /dev/null and b/storyteller/themes/herbal/mid.jpg differ diff --git a/storyteller/themes/herbal/theme.json b/storyteller/themes/herbal/theme.json new file mode 100644 index 0000000..48b67d9 --- /dev/null +++ b/storyteller/themes/herbal/theme.json @@ -0,0 +1,9 @@ +{ + "aspect-w":4, + "aspect-h":3, + "border-l":8, + "border-t":8, + "border-r":10, + "border-b":11, + "font-scale":40 +} diff --git a/storyteller/themes/herbal/top.jpg b/storyteller/themes/herbal/top.jpg new file mode 100644 index 0000000..74cd161 Binary files /dev/null and b/storyteller/themes/herbal/top.jpg differ diff --git a/storyteller/themes/none/bottom.jpg b/storyteller/themes/none/bottom.jpg new file mode 100644 index 0000000..41059d3 Binary files /dev/null and b/storyteller/themes/none/bottom.jpg differ diff --git a/storyteller/themes/none/mid.jpg b/storyteller/themes/none/mid.jpg new file mode 100644 index 0000000..41059d3 Binary files /dev/null and b/storyteller/themes/none/mid.jpg differ diff --git a/storyteller/themes/none/theme.json b/storyteller/themes/none/theme.json new file mode 100644 index 0000000..c55b44a --- /dev/null +++ b/storyteller/themes/none/theme.json @@ -0,0 +1,9 @@ +{ + "aspect-w":4, + "aspect-h":3, + "border-l":1, + "border-t":1, + "border-r":1, + "border-b":1, + "font-scale":30 +} diff --git a/storyteller/themes/none/top.jpg b/storyteller/themes/none/top.jpg new file mode 100644 index 0000000..41059d3 Binary files /dev/null and b/storyteller/themes/none/top.jpg differ diff --git a/storyteller/themes/pencil/bottom.jpg b/storyteller/themes/pencil/bottom.jpg new file mode 100644 index 0000000..b4726e5 Binary files /dev/null and b/storyteller/themes/pencil/bottom.jpg differ diff --git a/storyteller/themes/pencil/mid.jpg b/storyteller/themes/pencil/mid.jpg new file mode 100644 index 0000000..7247694 Binary files /dev/null and b/storyteller/themes/pencil/mid.jpg differ diff --git a/storyteller/themes/pencil/pencil.png b/storyteller/themes/pencil/pencil.png new file mode 100644 index 0000000..9bb560f Binary files /dev/null and b/storyteller/themes/pencil/pencil.png differ diff --git a/storyteller/themes/pencil/theme.json b/storyteller/themes/pencil/theme.json new file mode 100644 index 0000000..34e402d --- /dev/null +++ b/storyteller/themes/pencil/theme.json @@ -0,0 +1,9 @@ +{ + "aspect-w":4, + "aspect-h":3, + "border-l":10, + "border-t":10, + "border-r":10, + "border-b":10, + "font-scale":40 +} diff --git a/storyteller/themes/pencil/top.jpg b/storyteller/themes/pencil/top.jpg new file mode 100644 index 0000000..3251577 Binary files /dev/null and b/storyteller/themes/pencil/top.jpg differ diff --git a/storyteller/themes/storyteller/bottom.jpg b/storyteller/themes/storyteller/bottom.jpg new file mode 100644 index 0000000..3536cc7 Binary files /dev/null and b/storyteller/themes/storyteller/bottom.jpg differ diff --git a/storyteller/themes/storyteller/frame.xcf b/storyteller/themes/storyteller/frame.xcf new file mode 100644 index 0000000..4082d77 Binary files /dev/null and b/storyteller/themes/storyteller/frame.xcf differ diff --git a/storyteller/themes/storyteller/mid.jpg b/storyteller/themes/storyteller/mid.jpg new file mode 100644 index 0000000..515e93a Binary files /dev/null and b/storyteller/themes/storyteller/mid.jpg differ diff --git a/storyteller/themes/storyteller/sky.jpg b/storyteller/themes/storyteller/sky.jpg new file mode 100644 index 0000000..2e71baf Binary files /dev/null and b/storyteller/themes/storyteller/sky.jpg differ diff --git a/storyteller/themes/storyteller/theme.json b/storyteller/themes/storyteller/theme.json new file mode 100644 index 0000000..68741fc --- /dev/null +++ b/storyteller/themes/storyteller/theme.json @@ -0,0 +1,9 @@ +{ + "aspect-w":4, + "aspect-h":3, + "border-l":8, + "border-t":6, + "border-r":8, + "border-b":15, + "font-scale":40 +} diff --git a/storyteller/themes/storyteller/top.jpg b/storyteller/themes/storyteller/top.jpg new file mode 100644 index 0000000..2447153 Binary files /dev/null and b/storyteller/themes/storyteller/top.jpg differ diff --git a/storyteller/themes/storyteller/top.xcf b/storyteller/themes/storyteller/top.xcf new file mode 100644 index 0000000..6b7cc29 Binary files /dev/null and b/storyteller/themes/storyteller/top.xcf differ diff --git a/storyteller/themes/themes.txt b/storyteller/themes/themes.txt new file mode 100644 index 0000000..d931965 --- /dev/null +++ b/storyteller/themes/themes.txt @@ -0,0 +1,8 @@ +storyteller +grass +herbal +pencil +thin-gold +wood1 +wood2 +none diff --git a/storyteller/themes/thin-gold/bottom.jpg b/storyteller/themes/thin-gold/bottom.jpg new file mode 100644 index 0000000..b22d70a Binary files /dev/null and b/storyteller/themes/thin-gold/bottom.jpg differ diff --git a/storyteller/themes/thin-gold/mid.jpg b/storyteller/themes/thin-gold/mid.jpg new file mode 100644 index 0000000..a6b004d Binary files /dev/null and b/storyteller/themes/thin-gold/mid.jpg differ diff --git a/storyteller/themes/thin-gold/theme.json b/storyteller/themes/thin-gold/theme.json new file mode 100644 index 0000000..c0cbe53 --- /dev/null +++ b/storyteller/themes/thin-gold/theme.json @@ -0,0 +1,10 @@ +{ + "style":"bordered", + "aspect-w":4, + "aspect-h":3, + "border-l":3, + "border-t":4, + "border-r":4, + "border-b":5, + "font-scale":40 +} diff --git a/storyteller/themes/thin-gold/thin gold.jpg b/storyteller/themes/thin-gold/thin gold.jpg new file mode 100644 index 0000000..a4c0486 Binary files /dev/null and b/storyteller/themes/thin-gold/thin gold.jpg differ diff --git a/storyteller/themes/thin-gold/top.jpg b/storyteller/themes/thin-gold/top.jpg new file mode 100644 index 0000000..4b151a0 Binary files /dev/null and b/storyteller/themes/thin-gold/top.jpg differ diff --git a/storyteller/themes/wood1/bottom.jpg b/storyteller/themes/wood1/bottom.jpg new file mode 100644 index 0000000..bea7f25 Binary files /dev/null and b/storyteller/themes/wood1/bottom.jpg differ diff --git a/storyteller/themes/wood1/mid.jpg b/storyteller/themes/wood1/mid.jpg new file mode 100644 index 0000000..d82e063 Binary files /dev/null and b/storyteller/themes/wood1/mid.jpg differ diff --git a/storyteller/themes/wood1/theme.json b/storyteller/themes/wood1/theme.json new file mode 100644 index 0000000..71ea56f --- /dev/null +++ b/storyteller/themes/wood1/theme.json @@ -0,0 +1,9 @@ +{ + "aspect-w":4, + "aspect-h":3, + "border-l":13, + "border-t":14, + "border-r":13, + "border-b":18, + "font-scale":40 +} diff --git a/storyteller/themes/wood1/top.jpg b/storyteller/themes/wood1/top.jpg new file mode 100644 index 0000000..4cb64ec Binary files /dev/null and b/storyteller/themes/wood1/top.jpg differ diff --git a/storyteller/themes/wood1/wood1.png b/storyteller/themes/wood1/wood1.png new file mode 100644 index 0000000..6311fa5 Binary files /dev/null and b/storyteller/themes/wood1/wood1.png differ diff --git a/storyteller/themes/wood2/bottom.jpg b/storyteller/themes/wood2/bottom.jpg new file mode 100644 index 0000000..e795162 Binary files /dev/null and b/storyteller/themes/wood2/bottom.jpg differ diff --git a/storyteller/themes/wood2/mid.jpg b/storyteller/themes/wood2/mid.jpg new file mode 100644 index 0000000..3e7f507 Binary files /dev/null and b/storyteller/themes/wood2/mid.jpg differ diff --git a/storyteller/themes/wood2/theme.json b/storyteller/themes/wood2/theme.json new file mode 100644 index 0000000..e4b06fd --- /dev/null +++ b/storyteller/themes/wood2/theme.json @@ -0,0 +1,9 @@ +{ + "aspect-w":4, + "aspect-h":3, + "border-l":8, + "border-t":9, + "border-r":8, + "border-b":10, + "font-scale":40 +} diff --git a/storyteller/themes/wood2/top.jpg b/storyteller/themes/wood2/top.jpg new file mode 100644 index 0000000..ffbe804 Binary files /dev/null and b/storyteller/themes/wood2/top.jpg differ diff --git a/storyteller/themes/wood2/wood2.png b/storyteller/themes/wood2/wood2.png new file mode 100644 index 0000000..0c6ca05 Binary files /dev/null and b/storyteller/themes/wood2/wood2.png differ