diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..a538da9 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,7 @@ +[run] +omit = silpa/tests/* +branch = True +source = silpa + +[report] +ignore_errors = True \ No newline at end of file diff --git a/.gitignore b/.gitignore index 297f5e4..6c70b22 100755 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,7 @@ venv *.swp .\#* \#*\# +*.webassets-cache/ +*silpa.min.* +/.testrepository/ +/cover/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..fd11d1e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "silpa/frontend/static/js/lib"] + path = silpa/frontend/static/js/jquery.ime + url = https://github.com/wikimedia/jquery.ime.git diff --git a/.testr.conf b/.testr.conf new file mode 100644 index 0000000..3427e65 --- /dev/null +++ b/.testr.conf @@ -0,0 +1,4 @@ +[DEFAULT] +test_command=${PYTHON:-python} -m subunit.run discover silpa $LISTOPT $IDOPTION +test_id_option=--load-list $IDFILE +test_list_option=--list diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a285bc8 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +language: python +python: + - "2.7" + # - "3.3" +install: + - pip install -r requirements.txt + - pip install -r requirements-modules.txt + - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install -r requirements-py2.txt; fi + - pip install -r test-requirements.txt +script: make travis +after_success: coveralls +notifications: + email: + - silpa-discuss@nongnu.org + irc: + channels: + - "irc.freenode.net#silpa" + on_success: change + on_failure: change + use_notice: true \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..851ded1 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +travis: + python setup.py testr --coverage \ + --testr-args="--parallel --concurrency=2" + flake8 silpa + +clean: + find . -name "*.pyc" -exec rm -vf {} \; + find -name __pycache__ -delete + find . -name "*~" -exec rm -vf {} \; + +tox: + tox + +flake: + flake8 silpa tests diff --git a/README b/README index e832720..6aa26a3 100644 --- a/README +++ b/README @@ -1,5 +1,9 @@ SILPA ========== + +[![Build Status](https://travis-ci.org/Project-SILPA/Silpa-Flask.svg?branch=development)](https://travis-ci.org/Project-SILPA/Silpa-Flask) +[![Coverage Status](https://coveralls.io/repos/Project-SILPA/Silpa-Flask/badge.png?branch=development)](https://coveralls.io/r/Project-SILPA/Silpa-Flask?branch=development) + SILPA - Indian Language computing platform which provides a web interface for different Indian language computing python modules. This is hosted at diff --git a/core/__init__.py b/core/__init__.py deleted file mode 100755 index 4118946..0000000 --- a/core/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from jsonrpchandler import * -from modulehelper import * diff --git a/core/jsonrpchandler.py b/core/jsonrpchandler.py deleted file mode 100755 index 10ee409..0000000 --- a/core/jsonrpchandler.py +++ /dev/null @@ -1,102 +0,0 @@ -from json import loads, dumps -from modulehelper import modules, modulenames, MODULES, enabled_modules, \ - load_modules - - -class JSONRPCHandlerException(Exception): - pass - - -class JSONRequestNotTranslatable(JSONRPCHandlerException): - pass - - -class BadServiceRequest(JSONRPCHandlerException): - pass - - -class MethodNotFoundException(JSONRPCHandlerException): - def __init__(self, name): - self.methodname = name - - -class JSONRPCHandler(object): - - def __init__(self): - ''' - This should be only once called. Atleast my assumption - ''' - load_modules() - - def translate_request(self, data): - try: - req = loads(data) - except: - raise JSONRequestNotTranslatable - return req - - def translate_result(self, result, error, id_): - if error != None: - error = {"name": error.__class__.__name__, "message": error} - result = None - - try: - data = dumps({"result": result, "id": id_, "error": error}) - except: - error = {"name": "JSONEncodeException", \ - "message": "Result object is not serializable"} - data = dumps({"result": None, "id": id_, "error": error}) - - return data - - def call(self, method, args): - _args = None - for arg in args: - if arg != '': - if _args == None: - _args = [] - _args.append(arg) - - if _args == None: - # No arguments - return method() - else: - return method(*_args) - - def handle_request(self, json): - err = None - meth = None - id_ = '' - result = None - args = None - - try: - req = self.translate_request(json) - except JSONRequestNotTranslatable, e: - err = e - req = {'id': id_} - - if err == None: - try: - id_ = req['id'] - meth = req['method'] - try: - args = req['params'] - except: - pass - except: - err = BadServiceRequest(json) - - module_instance = None - if err == None: - try: - module_instance = MODULES.get(meth.split('.')[0]) - except: - err = MethodNotFoundException(meth.split('.')[-1]) - - method = None - if err == None: - result = self.call(getattr(module_instance, \ - meth.split('.')[-1]), args) - - return self.translate_result(result, err, id_) diff --git a/core/modulehelper.py b/core/modulehelper.py deleted file mode 100755 index 7f6513f..0000000 --- a/core/modulehelper.py +++ /dev/null @@ -1,55 +0,0 @@ -''' - Purpose of this file is to hold variables which is used - across the application so that these variables don't get - initialized multiple times -''' - -_all_ = ['MODULES', 'modules', 'modulenames', 'enabled_modules', \ - 'load_modules', 'BASEURL'] - -import loadconfig -import sys - - -MODULES = {} -BASEURL = loadconfig.get('baseurl') - -modules = loadconfig.get('modules') -modulenames = loadconfig.get('modules_display') - -enabled_modules = [modulenames[x] for x in modules.keys() \ - if modules[x] == "yes"] - - -def load_modules(): - ''' - Load the modules enabled in the configuration file - by user. This function initializes global variable - MODULES which is a dictionary having module name as - key and module itself as value. Which can be used later - to process requests coming for the modules. - ''' - - # Already initialized the modules then don't do it again - if len(MODULES) != 0: - return - - for key in modules.keys(): - if modules.get(key) == 'yes': - mod = None - try: - mod = sys.modules[key] - if not type(mod).__name__ == 'module': - raise KeyError - except KeyError: - try: - mod = __import__(key, globals(), locals(), []) - except ImportError: - # Since we can't use logger from flask as its not yet - # activated we will write it to sys.stderr this should - # go to webserver error.log - print >> sys.stderr, "Failed to import module {0}"\ - .format(key) - pass - if mod: - MODULES[key] = mod.getInstance() diff --git a/docs/installation.rst b/docs/installation.rst index eda6d6f..ccc039f 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -74,7 +74,8 @@ Get the Code $ git clone https://github.com/Project-SILPA/Silpa-Flask.git $ cd Silpa-Flask $ pip install -r requirements.txt - $ python silpa.py + $ python setup.py install + $ python wsgi.py If you want to Install all modules: @@ -112,7 +113,7 @@ You can start the silpa application by .. code-block:: shell-session - python silpa.py + python wsgi.py Running on http://127.0.0.1:5000/ diff --git a/etc/silpa.conf b/etc/silpa.conf new file mode 100644 index 0000000..d33036e --- /dev/null +++ b/etc/silpa.conf @@ -0,0 +1,60 @@ +[main] +# SITE Name +site = SILPA + +# If your site is not hosted in document root please give baseurl +# eg: www.example.org/silpa +# Leave the / if SILPA is hosted directly under documentroot +# leave it as / if you are using WSGI even if you are not under document root +baseurl = / + +[logging] +# Set a logging level +# Allowed values are info,debug,warn,error +log_level = debug + +# Which folder log should be located +log_folder = /tmp + +# log file name +log_name = silpa.log + +[modules] +# These section provides list of modules use +# 'yes' to enable them and 'no' to disable +spellchecker = no +inexactsearch = no +soundex = yes +transliteration = no +hyphenation = no +chardetails = no +payyans = no +indicsyllabifier = no +indicfortune = no +ucasort = no +indicngram = no +shingling = no +textsimilarity = no +indicstemmer = no +katapayadi = no +#scriptrender = yes + +[module_display] +# Section gives names to be displayed on HTML and +# additionally the end point for each module +soundex = Soundex +inexactsearch = ApproxSearch +spellchecker = SpellCheck +transliteration = Transliteration +hyphenation = Hyphenate +chardetails = Chardetails +payyans = Payyans +indicsyllabifier = Syllabalizer +indicfortune = Fortune +ucasort = UCA Sort +indicngram = N-gram +shingling = Shingling +textsimilarity = Similar Texts +indicstemmer = Stemmer +katapayadi = Katapayadi Numbers +scriptrender = Script Render diff --git a/loadconfig.py b/loadconfig.py deleted file mode 100644 index 3aa8d26..0000000 --- a/loadconfig.py +++ /dev/null @@ -1,36 +0,0 @@ -from ConfigParser import RawConfigParser -import os - -_all_ = ['get'] - - -class _SilpaConfig: - def __init__(self): - _config = RawConfigParser() - _config.read(os.path.join(os.path.dirname(__file__), 'silpa.conf')) - - self.site_name = _config.get('main', 'site') - self.baseurl = _config.get('main', 'baseurl') - - self.log_level = _config.get('logging', 'log_level') - - folder = _config.get('logging', 'log_folder') - self.log_folder = folder if folder != "." else os.getcwd() - - name = _config.get('logging', 'log_name') - self.log_name = name if name else 'silpa.log' - - self.modules = {} - for module, status in _config.items('modules'): - self.modules[module] = status if status else "no" - - self.modules_display = {} - for module, name in _config.items('module_display'): - self.modules_display[module] = name - - -_config = _SilpaConfig() - - -def get(key): - return _config.__dict__[key] diff --git a/requirements-modules.txt b/requirements-modules.txt index f7f0df5..a044b77 100644 --- a/requirements-modules.txt +++ b/requirements-modules.txt @@ -5,7 +5,7 @@ -e git+https://github.com/Project-SILPA/Soundex.git#egg=soundex -e git+https://github.com/Project-SILPA/silpa-common.git#egg=silpa_common -e git+https://github.com/Project-SILPA/shingling.git#egg=shingling --e git+https://github.com/Project-SILPA/scriptrender.git#egg=scriptrender +#-e git+https://github.com/Project-SILPA/scriptrender.git#egg=scriptrender -e git+https://github.com/Project-SILPA/payyans.git#egg=payyans -e git+https://github.com/Project-SILPA/normalizer.git#egg=normalizer -e git+https://github.com/Project-SILPA/inexactsearch.git#egg=inexactsearch diff --git a/requirements-py2.txt b/requirements-py2.txt new file mode 100644 index 0000000..af42dda --- /dev/null +++ b/requirements-py2.txt @@ -0,0 +1 @@ +configparser diff --git a/requirements.txt b/requirements.txt index e3e9a71..b0bd47b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,4 @@ Flask +Flask-Assets +cssmin +jsmin>=2.0.6 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..eb8c17f --- /dev/null +++ b/setup.cfg @@ -0,0 +1,27 @@ +[metadata] +name = silpa +summary = Indic language computing platform +description-file = + README.md +author = SILPA Developers +author-email = silpa-discuss@nongnu.org +home-page = http://silpa.org.in +classifier = + Environment :: Web Environment + Framework :: Flask + Intended Audience :: Developers + Intended Audience :: Information Technology + License :: OSI Approved + License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+) + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 3 + +[files] +packages = + silpa + +[build-sphinx] +all_files = 1 +build_dir = docs/build +source-dir = docs \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c1c8784 --- /dev/null +++ b/setup.py @@ -0,0 +1,6 @@ +from setuptools import setup + + +setup( + setup_requires=['pbr'], + pbr=True) diff --git a/silpa.py b/silpa.py deleted file mode 100644 index a249f1a..0000000 --- a/silpa.py +++ /dev/null @@ -1,112 +0,0 @@ -from flask import Flask -from logging import handlers, Formatter -from webbridge import WebBridge -from core.modulehelper import enabled_modules, BASEURL, modules -from jinja2 import PackageLoader, ChoiceLoader -import loadconfig -import logging -import os - - -def register_url(): - ''' - Function all form of URL which will be handled by SILPA service - - This function actually make this flask front end of SILPA independent - of any modules. It doesn't know what modules are present nor doesn't - know how to handle modules all the request will be promptly handed over - to WebBridge - ''' - # / or /baseurl for index page - baseurl = '/' if BASEURL == '/' else BASEURL - app.logger.debug("Registering the URL:{0}".format(baseurl)) - app.add_url_rule(baseurl, view_func=WebBridge.as_view(baseurl)) - # License page - app.logger.debug("Registering the URL:{0}".format(baseurl+"License")) - app.add_url_rule(baseurl+"License", view_func=WebBridge.as_view("license")) - # Credits Page - app.logger.debug("Registering the URL:{0}".format(baseurl+"Credits")) - app.add_url_rule(baseurl+"Credits", view_func=WebBridge.as_view("credits")) - # Contacts Page - app.logger.debug("Registering the URL:{0}".format(baseurl+"Contact")) - app.add_url_rule(baseurl+"Contact", view_func=WebBridge.as_view("contact")) - # Register all enabled modules - # baseurl/modulenames['module'] - for module in enabled_modules: - module_url = baseurl + "/" + module if not baseurl == "/" \ - else baseurl + module - app.logger.debug("Registering the URL:{0}".format(module_url)) - app.add_url_rule(module_url, view_func=WebBridge.as_view(module_url)) - - # JSONRPC url - jsonrpc_url = (baseurl + "/JSONRPC" if not baseurl == "/" - else baseurl + "JSONRPC") - app.logger.debug("Registering the URL:{0}".format(baseurl)) - app.add_url_rule(jsonrpc_url, view_func=WebBridge.as_view(jsonrpc_url)) - - -def add_templates(): - templates = [app.jinja_loader] - for key in modules.keys(): - if modules.get(key) == 'yes': - templates.append(PackageLoader(key)) - app.jinja_loader = ChoiceLoader(templates) - - -def configure_logging(): - ''' - This function configures logging for the SILPA applications using Flask's - internal logger. - - For now log file will be rotated 7 days once and 4 backups will be kept. - This can't be modified using configuration file as of now. - - Default logging level will be ERROR and can be modified from - configuration file. Log folder and file name can also be configured using - configuration file but make sure the path you give is writable for - Webserver user, otherwise this will lead to an error. - ''' - log_level = loadconfig.get('log_level') - log_folder = loadconfig.get('log_folder') - log_name = loadconfig.get('log_name') - filename = os.path.join(log_folder, log_name) - - handler = handlers.TimedRotatingFileHandler(filename, when='D', - interval=7, backupCount=4) - - level = logging.ERROR - - if log_level == "debug": - level = logging.DEBUG - elif log_level == "info": - level = logging.INFO - elif log_level == "warn": - level = logging.WARNING - elif log_level == "error": - level = logging.ERROR - - handler.setLevel(level) - handler.setFormatter(Formatter('%(asctime)s %(levelname)s: %(message)s \ - [in %(pathname)s %(lineno)d]')) - - app.logger.setLevel(level) - app.logger.addHandler(handler) - - -DEBUG = False - -# Basics -app = Flask(__name__) -app.config.from_object(__name__) - -# Logging -configure_logging() - -# Register URL's -register_url() - -# adds templates from imported modules -add_templates() - -if __name__ == '__main__': - app.run('0.0.0.0') diff --git a/silpa/__init__.py b/silpa/__init__.py new file mode 100644 index 0000000..eab6a15 --- /dev/null +++ b/silpa/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +''' + SILPA + ~~~~~ + + SILPA application package +''' diff --git a/silpa/api/__init__.py b/silpa/api/__init__.py new file mode 100644 index 0000000..7f3c109 --- /dev/null +++ b/silpa/api/__init__.py @@ -0,0 +1,27 @@ +from flask import jsonify +from functools import wraps +from .. import factory + + +def route(bp, *args, **kwargs): + def decorator(f): + @bp.route(*args, **kwargs) + @wraps(f) + def wrapper(*args, **kwargs): + result = f(*args, **kwargs) + sc = 200 if 'error' not in result else 400 + return jsonify(result), sc + return f + return decorator + + +def create_app(conffile, settings_override=None): + app = factory.create_app(__name__, __path__, + settings_override, conffile) + app.errorhandler(404)(on_404) + + return app + + +def on_404(e): + return jsonify(dict(error='Not Found')), 404 diff --git a/silpa/api/jsonrpc.py b/silpa/api/jsonrpc.py new file mode 100644 index 0000000..b6648de --- /dev/null +++ b/silpa/api/jsonrpc.py @@ -0,0 +1,131 @@ +from __future__ import print_function +from collections import namedtuple +from flask import Blueprint, request +from . import route +import json +import sys + +JsonRpcError = namedtuple('JsonRpcError', ['code', 'message', 'data']) +JsonRpcRequest = namedtuple('JsonRpcRequest', + ['jsonrpc', 'method', 'params', 'id']) +JsonRpcErrorResponse = namedtuple('JsonRpcErrorResponse', + ['jsonrpc', 'error', 'id']) +JsonRpcResultResponse = namedtuple('JsonRpcResultResponse', + ['jsonrpc', 'result', 'id']) + +PARSE_ERRORS = -32700 +INVALID_REQUEST = -32600 +METHOD_NOT_FOUND = -32601 +INVALID_PARAMS = -32602 +INTERNAL_ERROR = -32603 + + +bp = Blueprint('api_jsonrpc', __name__, url_prefix='/api') + + +@route(bp, '/JSONRPC', methods=['POST']) +def handle_jsonrpc_call(): + if request.data is not None: + rpc_object = JsonRpc(request.data) + + if rpc_object.error_response is not None: + # There was a error, translate and return the dictionary + # object for client + return dict(zip(rpc_object.error_response._fields, + rpc_object.error_response)) + else: + # there was no problem constructing request lets process + # the call + try: + rpc_object() + except Exception as e: + # Possible errors in execution of method + error = JsonRpcError(code=INTERNAL_ERROR, message=e.message, + data=dict(zip(rpc_object.request._fields, + rpc_object.request))) + return dict(jsonrpc="2.0", + error=dict(zip(error._fields, error)), + id=rpc_object.request.id) + else: + if rpc_object.error_response is None: + # success! + return dict(zip(rpc_object.response._fields, + rpc_object.response)) + else: + return dict(zip(rpc_object.error_response._fields, + rpc_object.error_response)) + + +class JsonRpc(object): + __slots__ = ['request', 'response', 'error_response', 'instance_type'] + + def __init__(self, data): + self.error_response = None + try: + self.request = JsonRpcRequest(**json.loads(data)) + except TypeError as e: + error = JsonRpcError(code=INVALID_REQUEST, + message="Not a valid JSON-RPC request", + data='') + error_dict = dict(zip(error._fields, error)) + self.error_response = JsonRpcErrorResponse(jsonrpc="2.0", + error=error_dict, + id='') + except Exception as e: + # Unable to parse json + error = JsonRpcError(code=PARSE_ERRORS, message=e.message, + data="") + self.error_response = JsonRpcErrorResponse(jsonrpc="2.0", + error=dict(zip( + error._fields, + error)), + id='') + + def __call__(self): + # process request here + module, method = self.request.method.split('.') + if module not in sys.modules: + # Module is not yet loaded or the request module is not + # enabled pass an error here. + error = JsonRpcError(code=INTERNAL_ERROR, + message="Requested module is not loaded or not\ + enabled by Admin", + data="{} is not loaded".format(module)) + self.error_response = JsonRpcErrorResponse(jsonrpc="2.0", + error=dict(zip( + error._fields, + error)), + id=self.request.id) + else: + # module is present in sys + mod = sys.modules[module] + if hasattr(mod, 'getInstance'): + instance = getattr(mod, 'getInstance')() + if hasattr(instance, method): + result = getattr(instance, method)(*self.request.params) + self.response = JsonRpcResultResponse(jsonrpc="2.0", + result=result, + id=self.request.id) + else: + # method not found + error = JsonRpcError(code=METHOD_NOT_FOUND, + message="requested method not found", + data="Requested method {}".format( + self.request.method)) + error_dict = dict(zip(error._fields, error)) + self.error_response = JsonRpcErrorResponse( + jsonrpc="2.0", + error=error_dict, + id=self.request.id) + else: + # module doesn't provide an interface to us? + error = JsonRpcError(code=INTERNAL_ERROR, + message="Requested module doesn't provide \ + getInstance interface", + data="{} is the module requested".format( + module)) + self.error_response = JsonRpcErrorResponse(jsonrpc='2.0', + error=dict(zip( + error._fields, + error)), + id=self.request.id) diff --git a/silpa/factory.py b/silpa/factory.py new file mode 100644 index 0000000..a3d64f9 --- /dev/null +++ b/silpa/factory.py @@ -0,0 +1,73 @@ +import pkgutil +import importlib +import logging +import os + + +from flask import Flask, Blueprint +from .loadconfig import Config +from logging import Formatter +from logging.handlers import TimedRotatingFileHandler +from .helper import ModuleConfigHelper + + +def register_blueprints(app, package_name, package_path): + rv = [] + for _, name, _ in pkgutil.iter_modules(package_path): + m = importlib.import_module('{package}.{module}'.format( + package=package_name, module=name)) + for item in dir(m): + item = getattr(m, item) + if isinstance(item, Blueprint): + app.register_blueprint(item) + rv.append(item) + return rv + + +def configure_logging(app, config): + log_level = config.get('logging', 'log_level') + log_folder = config.get('logging', 'log_folder') + log_name = config.get('logging', 'log_name') + + handler = TimedRotatingFileHandler(os.path.join(log_folder, log_name), + when='D', interval=7, backupCount=4) + + level = None + + if log_level == 'debug': + level = logging.DEBUG + elif log_level == 'info': + level = logging.INFO + elif log_level == 'error': + level = logging.ERROR + elif log_level == 'warn': + level = logging.WARNING + + handler.setLevel(level) + handler.setFormatter(Formatter('%(asctime)s %(levelname)s' + + ' %(message)7s - [in %(funcName)s' + + ' at %(pathname)s %(lineno)d]')) + app.logger.setLevel(level) + app.logger.addHandler(handler) + + +def create_app(package_name, package_path, settings_override=None, + conffile="silpa.conf"): + app = Flask(package_name, instance_relative_config=True) + app.config.from_object("silpa.settings") + app.config.from_pyfile('settings.cfg', silent=True) + app.config.from_object(settings_override) + + config = Config(conffile) + configure_logging(app, config) + + # Create ModuleConfigHelper class and pass it config this will + # instantiate class variables + ModuleConfigHelper(config=config) + ModuleConfigHelper.load_modules() + + # Register blueprints at end so we have module,display and other + # stuff created + register_blueprints(app, package_name, package_path) + + return app diff --git a/silpa/frontend/__init__.py b/silpa/frontend/__init__.py new file mode 100644 index 0000000..79a73a0 --- /dev/null +++ b/silpa/frontend/__init__.py @@ -0,0 +1,20 @@ +from .. import factory +from jinja2 import PackageLoader, ChoiceLoader +from ..helper import ModuleConfigHelper +from . import assets + + +def create_app(conffile, settings_override=None): + app = factory.create_app(__name__, __path__, + settings_override, conffile) + assets.init_app(app) + load_module_templates(app) + return app + + +def load_module_templates(app): + modules = ModuleConfigHelper.get_modules() + templates = [app.jinja_loader] + for module in modules: + templates.append(PackageLoader(module)) + app.jinja_loader = ChoiceLoader(templates) diff --git a/silpa/frontend/assets.py b/silpa/frontend/assets.py new file mode 100644 index 0000000..ebd924f --- /dev/null +++ b/silpa/frontend/assets.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +""" + silpa.frontend.assets + ~~~~~~~~~~~~~~~~~~~~~ + + frontend application asset "pipeline" +""" + +from flask.ext.assets import Environment, Bundle + + +css_all = Bundle("css/bootstrap.min.css", + "js/jquery.ime/css/jquery.ime.css", + "css/main.css", filters="cssmin", output="css/silpa.min.css") + + +js_all = Bundle("js/jquery.js", + "js/bootstrap.min.js", + "js/jquery.ime/src/jquery.ime.js", + "js/jquery.ime/src/jquery.ime.inputmethods.js", + "js/jquery.ime/src/jquery.ime.selector.js", + "js/jquery.ime/src/jquery.ime.preferences.js", + filters="jsmin", output="js/silpa.min.js") + + +def init_app(app): + webassets = Environment(app) + webassets.register('css_all', css_all) + webassets.register('js_all', js_all) + webassets.manifest = 'cache' if not app.debug else False + webassets.cache = not app.debug + webassets.debug = app.debug diff --git a/silpa/frontend/pages.py b/silpa/frontend/pages.py new file mode 100644 index 0000000..bb128bb --- /dev/null +++ b/silpa/frontend/pages.py @@ -0,0 +1,41 @@ +from flask import Blueprint, render_template, abort +from ..helper import ModuleConfigHelper + +_BASE_URL = ModuleConfigHelper.get_baseurl() +_modules = ModuleConfigHelper.get_modules() +_modulename_to_display = ModuleConfigHelper.get_module_displaynames() + +_display_module_map = sorted(zip(_modulename_to_display.keys(), + _modulename_to_display.values())) + +bp = Blueprint('frontend', __name__) + + +@bp.route(_BASE_URL, defaults={'page': 'index.html'}) +@bp.route(_BASE_URL + '') +def serve_pages(page): + if page == "index.html": + return render_template('index.html', title='SILPA', + main_page=_BASE_URL, + modules=_display_module_map) + elif page == "License": + return render_template('license.html', title='SILPA License', + main_page=_BASE_URL, + modules=_display_module_map) + elif page == "Credits": + return render_template('credits.html', title='Credits', + main_page=_BASE_URL, + modules=_display_module_map) + elif page == "Contact": + return render_template('contact.html', title='Contact SILPA Team', + main_page=_BASE_URL, + modules=_display_module_map) + else: + # modules requested!. + if page in _modules: + return render_template(page + '.html', + title=page, main_page=_BASE_URL, + modules=_display_module_map) + else: + # Did we encounter something which is not registered by us? + return abort(404) diff --git a/static/css/bootstrap.min.css b/silpa/frontend/static/css/bootstrap.min.css similarity index 100% rename from static/css/bootstrap.min.css rename to silpa/frontend/static/css/bootstrap.min.css diff --git a/static/css/main.css b/silpa/frontend/static/css/main.css similarity index 100% rename from static/css/main.css rename to silpa/frontend/static/css/main.css diff --git a/static/doc/apis.html b/silpa/frontend/static/doc/apis.html similarity index 100% rename from static/doc/apis.html rename to silpa/frontend/static/doc/apis.html diff --git a/static/doc/contact.html b/silpa/frontend/static/doc/contact.html similarity index 100% rename from static/doc/contact.html rename to silpa/frontend/static/doc/contact.html diff --git a/static/doc/contribute.html b/silpa/frontend/static/doc/contribute.html similarity index 100% rename from static/doc/contribute.html rename to silpa/frontend/static/doc/contribute.html diff --git a/static/doc/credits.html b/silpa/frontend/static/doc/credits.html similarity index 100% rename from static/doc/credits.html rename to silpa/frontend/static/doc/credits.html diff --git a/static/doc/faq.html b/silpa/frontend/static/doc/faq.html similarity index 100% rename from static/doc/faq.html rename to silpa/frontend/static/doc/faq.html diff --git a/static/doc/index.html b/silpa/frontend/static/doc/index.html similarity index 100% rename from static/doc/index.html rename to silpa/frontend/static/doc/index.html diff --git a/static/doc/install.html b/silpa/frontend/static/doc/install.html similarity index 100% rename from static/doc/install.html rename to silpa/frontend/static/doc/install.html diff --git a/static/doc/json.html b/silpa/frontend/static/doc/json.html similarity index 100% rename from static/doc/json.html rename to silpa/frontend/static/doc/json.html diff --git a/static/doc/license.html b/silpa/frontend/static/doc/license.html similarity index 100% rename from static/doc/license.html rename to silpa/frontend/static/doc/license.html diff --git a/static/doc/privacy.html b/silpa/frontend/static/doc/privacy.html similarity index 100% rename from static/doc/privacy.html rename to silpa/frontend/static/doc/privacy.html diff --git a/static/doc/source.html b/silpa/frontend/static/doc/source.html similarity index 100% rename from static/doc/source.html rename to silpa/frontend/static/doc/source.html diff --git a/static/doc/todo.html b/silpa/frontend/static/doc/todo.html similarity index 100% rename from static/doc/todo.html rename to silpa/frontend/static/doc/todo.html diff --git a/static/images/ime-active.png b/silpa/frontend/static/images/ime-active.png similarity index 100% rename from static/images/ime-active.png rename to silpa/frontend/static/images/ime-active.png diff --git a/static/images/ime-active.svg b/silpa/frontend/static/images/ime-active.svg similarity index 100% rename from static/images/ime-active.svg rename to silpa/frontend/static/images/ime-active.svg diff --git a/static/images/ime-inactive.png b/silpa/frontend/static/images/ime-inactive.png similarity index 100% rename from static/images/ime-inactive.png rename to silpa/frontend/static/images/ime-inactive.png diff --git a/static/images/ime-inactive.svg b/silpa/frontend/static/images/ime-inactive.svg similarity index 100% rename from static/images/ime-inactive.svg rename to silpa/frontend/static/images/ime-inactive.svg diff --git a/static/images/netdotnet.png b/silpa/frontend/static/images/netdotnet.png similarity index 100% rename from static/images/netdotnet.png rename to silpa/frontend/static/images/netdotnet.png diff --git a/static/images/tick.png b/silpa/frontend/static/images/tick.png similarity index 100% rename from static/images/tick.png rename to silpa/frontend/static/images/tick.png diff --git a/static/images/tick.svg b/silpa/frontend/static/images/tick.svg similarity index 100% rename from static/images/tick.svg rename to silpa/frontend/static/images/tick.svg diff --git a/static/js/bootstrap.min.js b/silpa/frontend/static/js/bootstrap.min.js similarity index 100% rename from static/js/bootstrap.min.js rename to silpa/frontend/static/js/bootstrap.min.js diff --git a/silpa/frontend/static/js/jquery.ime b/silpa/frontend/static/js/jquery.ime new file mode 160000 index 0000000..dd2aa0f --- /dev/null +++ b/silpa/frontend/static/js/jquery.ime @@ -0,0 +1 @@ +Subproject commit dd2aa0f27e554f751abd330d4d66279d838b874b diff --git a/static/js/jquery.js b/silpa/frontend/static/js/jquery.js similarity index 100% rename from static/js/jquery.js rename to silpa/frontend/static/js/jquery.js diff --git a/static/output/.gitignore b/silpa/frontend/static/output/.gitignore similarity index 100% rename from static/output/.gitignore rename to silpa/frontend/static/output/.gitignore diff --git a/templates/contact.html b/silpa/frontend/templates/contact.html similarity index 100% rename from templates/contact.html rename to silpa/frontend/templates/contact.html diff --git a/templates/credits.html b/silpa/frontend/templates/credits.html similarity index 100% rename from templates/credits.html rename to silpa/frontend/templates/credits.html diff --git a/silpa/frontend/templates/index.html b/silpa/frontend/templates/index.html new file mode 100644 index 0000000..03e46d0 --- /dev/null +++ b/silpa/frontend/templates/index.html @@ -0,0 +1,22 @@ +{% extends "silpa.html" %} + +{% block content %} + +
+

Swathanthra Indian Language Processing + Applications(SILPA) is a web platform to host Free Software language + processing applications. It consists of a web framework and a set of + applications for processing Indian languages. It is a platform for + porting existing and upcoming language processing applications to + the web.

+

Silpa can also be used as a python library or as a + webservice from other applications. Silpa is currently under + development. If you are interested in contributing + contact the developers

+

+ Read the project announcement +

+
+{% endblock %} diff --git a/templates/license.html b/silpa/frontend/templates/license.html similarity index 100% rename from templates/license.html rename to silpa/frontend/templates/license.html diff --git a/templates/silpa.html b/silpa/frontend/templates/silpa.html similarity index 70% rename from templates/silpa.html rename to silpa/frontend/templates/silpa.html index dc73acc..f8bfdf2 100644 --- a/templates/silpa.html +++ b/silpa/frontend/templates/silpa.html @@ -6,16 +6,15 @@ - - - - + {% assets "css_all" %} + + {% endassets %} @@ -71,15 +70,15 @@ - - - + {% assets "js_all" %} + + {% endassets %} {% block modulescript %} {% endblock %} diff --git a/silpa/helper.py b/silpa/helper.py new file mode 100644 index 0000000..e77da81 --- /dev/null +++ b/silpa/helper.py @@ -0,0 +1,43 @@ +from __future__ import print_function + +import importlib +import sys + + +class ModuleConfigHelper(object): + module_names = [] + module_display = {} + base_url = None + + def __new__(cls, *args, **kwargs): + config = kwargs['config'] + cls.module_names = {module for module, need in config.items('modules') + if need == 'yes'} + + cls.module_display = {module: display_name for module, display_name in + config.items('module_display') + if module in cls.module_names} + cls.base_url = config.get('main', 'baseurl') + return super(ModuleConfigHelper, cls).__new__(cls) + + @classmethod + def get_modules(cls): + return cls.module_names + + @classmethod + def get_module_displaynames(cls): + return cls.module_display + + @classmethod + def get_baseurl(cls): + return cls.base_url + + @classmethod + def load_modules(cls): + for module in cls.module_names: + try: + importlib.import_module(module) + except ImportError as e: + print("Failed to import {module}: {message}". + format(module=module, message=e.message), + file=sys.stderr) diff --git a/silpa/loadconfig.py b/silpa/loadconfig.py new file mode 100644 index 0000000..1473f50 --- /dev/null +++ b/silpa/loadconfig.py @@ -0,0 +1,43 @@ +__all__ = ['IncompleteConfigError', 'Config'] + +import configparser + + +class IncompleteConfigError(Exception): + def __init__(self, section, option): + self.section = section + self.option = option + + def __str__(self): + if self.option is not None: + return ">> Missing {option} in {section} \ + of config file".format(option=self.option, + section=self.section) + else: + return ">> Missiong section {section} in config file".format( + section=self.section) + + +class Config(configparser.ConfigParser): + + def __init__(self, location="silpa.conf"): + configparser.ConfigParser.__init__(self) + self.read(location) + self.verify() + + def verify(self): + self._verify_item("main", "site") + self._verify_item("main", "baseurl") + self._verify_item("logging", "log_level") + self._verify_item("logging", "log_folder") + self._verify_item("logging", "log_name") + + if not self.has_section("modules"): + raise IncompleteConfigError("modules", None) + + if not self.has_section("module_display"): + raise IncompleteConfigError("module_display", None) + + def _verify_item(self, section, option): + if not self.has_option(section, option): + raise IncompleteConfigError(section, option) diff --git a/silpa/settings.py b/silpa/settings.py new file mode 100644 index 0000000..078f2ca --- /dev/null +++ b/silpa/settings.py @@ -0,0 +1,5 @@ +# Do not edit this file use your own settings.py for testing +# and for production create a file called settings.cfg +DEBUG = False +TESTING = False +JSON_AS_ASCII = False diff --git a/silpa/tests/__init__.py b/silpa/tests/__init__.py new file mode 100644 index 0000000..85e6898 --- /dev/null +++ b/silpa/tests/__init__.py @@ -0,0 +1,23 @@ + +from unittest import TestCase +from .utils import SILPATestCaseMixin + + +class SILPATestCase(TestCase): + pass + + +class SILPAAppTestCase(SILPATestCaseMixin, SILPATestCase): + def _create_app(self): + raise NotImplementedError + + def setUp(self): + super(SILPAAppTestCase, self).setUp() + self.app = self._create_app() + self.client = self.app.test_client() + self.app_context = self.app.app_context() + self.app_context.push() + + def tearDown(self): + super(SILPAAppTestCase, self).tearDown() + self.app_context.pop() diff --git a/silpa/tests/api/__init__.py b/silpa/tests/api/__init__.py new file mode 100644 index 0000000..894aad2 --- /dev/null +++ b/silpa/tests/api/__init__.py @@ -0,0 +1,14 @@ +from silpa.api import create_app +from .. import SILPAAppTestCase, settings +import os + + +class SILPAApiTestCase(SILPAAppTestCase): + + def _create_app(self): + self.conffile = os.path.join(os.path.dirname(__file__), '../resources', + 'silpa.conf') + return create_app(self.conffile, settings) + + def setUp(self): + super(SILPAApiTestCase, self).setUp() diff --git a/silpa/tests/api/test_jsonrpc.py b/silpa/tests/api/test_jsonrpc.py new file mode 100644 index 0000000..260fde5 --- /dev/null +++ b/silpa/tests/api/test_jsonrpc.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +""" + tests.api.jsonrpc_tests + ~~~~~~~~~~~~~~~~~~~~~~~ + + json-rpc api test module +""" + +from . import SILPAApiTestCase +from silpa.api import jsonrpc +import json +import random + + +class JsonRpcApiTestCase(SILPAApiTestCase): + + def assertJsonRpcMethodNotFound(self, response): + response_dict = json.loads(self.assertBadJson(response).data) + self.assertIn('error', response_dict) + error_obj = jsonrpc.JsonRpcError(**response_dict['error']) + self.assertEquals(error_obj.code, jsonrpc.METHOD_NOT_FOUND) + + def assertJsonRpcInvalidRequest(self, response): + response_dict = json.loads(self.assertBadJson(response).data) + self.assertIn('error', response_dict) + error_obj = jsonrpc.JsonRpcError(**response_dict['error']) + self.assertEquals(error_obj.code, jsonrpc.INVALID_REQUEST) + + def assertJsonRpcParseErrors(self, response): + response_dict = json.loads(self.assertBadJson(response).data) + self.assertIn('error', response_dict) + error_obj = jsonrpc.JsonRpcError(**response_dict['error']) + self.assertEquals(error_obj.code, jsonrpc.PARSE_ERRORS) + + def assertJsonRpcInternalError(self, response): + response_dict = json.loads(self.assertBadJson(response).data) + self.assertIn('error', response_dict) + error_obj = jsonrpc.JsonRpcError(**response_dict['error']) + self.assertEquals(error_obj.code, jsonrpc.INTERNAL_ERROR) + + def assertJsonRpcResult(self, response): + response_dict = json.loads(self.assertOkJson(response).data) + self.assertIn('result', response_dict) + + def test_methodnot_found(self): + data = dict(jsonrpc='2.0', + method='transliteration.transliter', + params=['Hello World!', 'kn_IN'], + id=random.randint(1, 1000)) + self.assertJsonRpcMethodNotFound(self.jpost('/api/JSONRPC', data=data)) + + def test_invalidrequest(self): + data = dict(method='transliteration.transliterate', + params=['Hello World!', 'kn_IN'], + id=random.randint(1, 1000)) + self.assertJsonRpcInvalidRequest(self.jpost('/api/JSONRPC', data=data)) + + def test_request_parseerror(self): + self.assertJsonRpcParseErrors(self.post('/api/JSONRPC', data=''' + {"jsonrpc": "2.0", "method": "transliteration.transliterate", "params": + [ + ''')) + + def test_result_jsonrpc(self): + data = dict(jsonrpc='2.0', + method='transliteration.transliterate', + params=['Hello World!', 'kn_IN'], + id=random.randint(1, 1000)) + self.assertJsonRpcResult(self.jpost('/api/JSONRPC', data=data)) + + def test_notfound(self): + r = self.post('/api/JS') + self.assertStatusCode(r, 404) + + def test_module_notloaded(self): + # Test assumes scriptrender module will never be enabled in + # test setup configuration + data = dict(jsonrpc='2.0', + method='scriptrender.render_text', + params=['some text', 'png', 'Black'], + id=random.randint(1, 1000)) + self.assertJsonRpcInternalError(self.jpost('/api/JSONRPC', data=data)) + + def test_no_interface(self): + data = dict(jsonrpc='2.0', + method='flask.Flask', + params=[__name__], + id=random.randint(1, 1000)) + self.assertJsonRpcInternalError(self.jpost('/api/JSONRPC', data=data)) diff --git a/silpa/tests/frontend/__init__.py b/silpa/tests/frontend/__init__.py new file mode 100644 index 0000000..6e8399f --- /dev/null +++ b/silpa/tests/frontend/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +""" + tests.frontend + ~~~~~~~~~~~~~~ + + frontend tests package +""" + +from silpa.frontend import create_app +from .. import SILPAAppTestCase, settings +import os + + +class SILPAFrontEndTestCase(SILPAAppTestCase): + + def _create_app(self): + self.conffile = os.path.join(os.path.dirname(__file__), '../resources', + 'silpa.conf') + return create_app(self.conffile, settings) + + def setUp(self): + super(SILPAFrontEndTestCase, self).setUp() diff --git a/silpa/tests/frontend/test_pages.py b/silpa/tests/frontend/test_pages.py new file mode 100644 index 0000000..8499d4d --- /dev/null +++ b/silpa/tests/frontend/test_pages.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +""" + tests.frontend.mainpage_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + main page frontend tests module +""" + +from . import SILPAFrontEndTestCase + + +class MainPageTestCase(SILPAFrontEndTestCase): + + def test_indexpage(self): + r = self.get('/') + self.assertIn(' SILPA - Indic Language Computing Platform ' + + '', self.assertOk(r).data) + + def test_pages(self): + r = self.get('/License') + self.assertIn('

License

', self.assertOk(r).data) + + r = self.get('/Contact') + self.assertIn('

Contacts

', self.assertOk(r).data) + + r = self.get('/Credits') + self.assertIn('

Credits

', self.assertOk(r).data) + + def test_moduleloaded(self): + from silpa.helper import ModuleConfigHelper + module_display = ModuleConfigHelper.get_module_displaynames() + + r = self.get('/') + self.assertOk(r) + + for m, d in module_display.items(): + # FIXME: Returned by configparser is unicode string for + # some reason breaks with assertIn not sure why :( + self.assertIn(d.encode('utf-8'), r.data) + + # TODO: URL in modules needs to be fixed befor enabling + # below tests, for now skip transliteration which is + # enabled in test conf but url is not fixed + if m != 'transliteration': + r1 = self.get('/' + m) + self.assertIn(' ' + m.encode('utf-8') + + ' - Indic Language Computing Platform' + + ' ', self.assertOk(r1).data) + + def test_pagenotfound(self): + r = self.get('/blablabla') + self.assertStatusCode(r, 404) diff --git a/silpa.conf b/silpa/tests/resources/silpa.conf similarity index 82% rename from silpa.conf rename to silpa/tests/resources/silpa.conf index af65e6d..daff214 100644 --- a/silpa.conf +++ b/silpa/tests/resources/silpa.conf @@ -22,22 +22,22 @@ log_name = silpa.log [modules] # These section provides list of modules use # 'yes' to enable them and 'no' to disable -spellchecker = yes +spellchecker = no inexactsearch = yes soundex = yes transliteration = yes -hyphenation = yes -chardetails = yes -payyans = yes -indicsyllabifier = yes -indicfortune = yes -ucasort = yes -indicngram = yes -shingling = yes -textsimilarity = yes -indicstemmer = yes -katapayadi = yes -scriptrender = yes +hyphenation = no +chardetails = no +payyans = no +indicsyllabifier = no +indicfortune = no +ucasort = no +indicngram = no +shingling = no +textsimilarity = no +indicstemmer = no +katapayadi = no +scriptrender = no [module_display] # Section gives names to be displayed on HTML and diff --git a/silpa/tests/resources/silpa_error.conf b/silpa/tests/resources/silpa_error.conf new file mode 100644 index 0000000..81a1c5e --- /dev/null +++ b/silpa/tests/resources/silpa_error.conf @@ -0,0 +1,60 @@ +[main] +# SITE Name +site = SILPA + +# If your site is not hosted in document root please give baseurl +# eg: www.example.org/silpa +# Leave the / if SILPA is hosted directly under documentroot +# leave it as / if you are using WSGI even if you are not under document root +baseurl = / + +[logging] +# Set a logging level +# Allowed values are info,debug,warn,error +log_level = error + +# Which folder log should be located +log_folder = /tmp + +# log file name +log_name = silpa.log + +[modules] +# These section provides list of modules use +# 'yes' to enable them and 'no' to disable +spellchecker = no +inexactsearch = yes +soundex = yes +transliteration = yes +hyphenation = no +chardetails = no +payyans = no +indicsyllabifier = no +indicfortune = no +ucasort = no +indicngram = no +shingling = no +textsimilarity = no +indicstemmer = no +katapayadi = no +scriptrender = no + +[module_display] +# Section gives names to be displayed on HTML and +# additionally the end point for each module +soundex = Soundex +inexactsearch = ApproxSearch +spellchecker = SpellCheck +transliteration = Transliteration +hyphenation = Hyphenate +chardetails = Chardetails +payyans = Payyans +indicsyllabifier = Syllabalizer +indicfortune = Fortune +ucasort = UCA Sort +indicngram = N-gram +shingling = Shingling +textsimilarity = Similar Texts +indicstemmer = Stemmer +katapayadi = Katapayadi Numbers +scriptrender = Script Render diff --git a/silpa/tests/resources/silpa_info.conf b/silpa/tests/resources/silpa_info.conf new file mode 100644 index 0000000..cf3d44a --- /dev/null +++ b/silpa/tests/resources/silpa_info.conf @@ -0,0 +1,60 @@ +[main] +# SITE Name +site = SILPA + +# If your site is not hosted in document root please give baseurl +# eg: www.example.org/silpa +# Leave the / if SILPA is hosted directly under documentroot +# leave it as / if you are using WSGI even if you are not under document root +baseurl = / + +[logging] +# Set a logging level +# Allowed values are info,debug,warn,error +log_level = info + +# Which folder log should be located +log_folder = /tmp + +# log file name +log_name = silpa.log + +[modules] +# These section provides list of modules use +# 'yes' to enable them and 'no' to disable +spellchecker = no +inexactsearch = yes +soundex = yes +transliteration = yes +hyphenation = no +chardetails = no +payyans = no +indicsyllabifier = no +indicfortune = no +ucasort = no +indicngram = no +shingling = no +textsimilarity = no +indicstemmer = no +katapayadi = no +scriptrender = no + +[module_display] +# Section gives names to be displayed on HTML and +# additionally the end point for each module +soundex = Soundex +inexactsearch = ApproxSearch +spellchecker = SpellCheck +transliteration = Transliteration +hyphenation = Hyphenate +chardetails = Chardetails +payyans = Payyans +indicsyllabifier = Syllabalizer +indicfortune = Fortune +ucasort = UCA Sort +indicngram = N-gram +shingling = Shingling +textsimilarity = Similar Texts +indicstemmer = Stemmer +katapayadi = Katapayadi Numbers +scriptrender = Script Render diff --git a/silpa/tests/resources/silpa_logging_logfolder.conf b/silpa/tests/resources/silpa_logging_logfolder.conf new file mode 100644 index 0000000..9c59721 --- /dev/null +++ b/silpa/tests/resources/silpa_logging_logfolder.conf @@ -0,0 +1,60 @@ +[main] +# SITE Name +site = SILPA + +# If your site is not hosted in document root please give baseurl +# eg: www.example.org/silpa +# Leave the / if SILPA is hosted directly under documentroot +# leave it as / if you are using WSGI even if you are not under document root +baseurl = / + +[logging] +# Set a logging level +# Allowed values are info,debug,warn,error +log_level = debug + +# Which folder log should be located +#log_folder = /tmp + +# log file name +log_name = silpa.log + +[modules] +# These section provides list of modules use +# 'yes' to enable them and 'no' to disable +spellchecker = no +inexactsearch = yes +soundex = yes +transliteration = yes +hyphenation = no +chardetails = no +payyans = no +indicsyllabifier = no +indicfortune = no +ucasort = no +indicngram = no +shingling = no +textsimilarity = no +indicstemmer = no +katapayadi = no +scriptrender = no + +[module_display] +# Section gives names to be displayed on HTML and +# additionally the end point for each module +soundex = Soundex +inexactsearch = ApproxSearch +spellchecker = SpellCheck +transliteration = Transliteration +hyphenation = Hyphenate +chardetails = Chardetails +payyans = Payyans +indicsyllabifier = Syllabalizer +indicfortune = Fortune +ucasort = UCA Sort +indicngram = N-gram +shingling = Shingling +textsimilarity = Similar Texts +indicstemmer = Stemmer +katapayadi = Katapayadi Numbers +scriptrender = Script Render diff --git a/silpa/tests/resources/silpa_logging_loglevel.conf b/silpa/tests/resources/silpa_logging_loglevel.conf new file mode 100644 index 0000000..d41978a --- /dev/null +++ b/silpa/tests/resources/silpa_logging_loglevel.conf @@ -0,0 +1,60 @@ +[main] +# SITE Name +site = SILPA + +# If your site is not hosted in document root please give baseurl +# eg: www.example.org/silpa +# Leave the / if SILPA is hosted directly under documentroot +# leave it as / if you are using WSGI even if you are not under document root +baseurl = / + +[logging] +# Set a logging level +# Allowed values are info,debug,warn,error +#log_level = debug + +# Which folder log should be located +log_folder = /tmp + +# log file name +log_name = silpa.log + +[modules] +# These section provides list of modules use +# 'yes' to enable them and 'no' to disable +spellchecker = no +inexactsearch = yes +soundex = yes +transliteration = yes +hyphenation = no +chardetails = no +payyans = no +indicsyllabifier = no +indicfortune = no +ucasort = no +indicngram = no +shingling = no +textsimilarity = no +indicstemmer = no +katapayadi = no +scriptrender = no + +[module_display] +# Section gives names to be displayed on HTML and +# additionally the end point for each module +soundex = Soundex +inexactsearch = ApproxSearch +spellchecker = SpellCheck +transliteration = Transliteration +hyphenation = Hyphenate +chardetails = Chardetails +payyans = Payyans +indicsyllabifier = Syllabalizer +indicfortune = Fortune +ucasort = UCA Sort +indicngram = N-gram +shingling = Shingling +textsimilarity = Similar Texts +indicstemmer = Stemmer +katapayadi = Katapayadi Numbers +scriptrender = Script Render diff --git a/silpa/tests/resources/silpa_logging_logname.conf b/silpa/tests/resources/silpa_logging_logname.conf new file mode 100644 index 0000000..c2a3d10 --- /dev/null +++ b/silpa/tests/resources/silpa_logging_logname.conf @@ -0,0 +1,60 @@ +[main] +# SITE Name +site = SILPA + +# If your site is not hosted in document root please give baseurl +# eg: www.example.org/silpa +# Leave the / if SILPA is hosted directly under documentroot +# leave it as / if you are using WSGI even if you are not under document root +baseurl = / + +[logging] +# Set a logging level +# Allowed values are info,debug,warn,error +log_level = debug + +# Which folder log should be located +log_folder = /tmp + +# log file name +#log_name = silpa.log + +[modules] +# These section provides list of modules use +# 'yes' to enable them and 'no' to disable +spellchecker = no +inexactsearch = yes +soundex = yes +transliteration = yes +hyphenation = no +chardetails = no +payyans = no +indicsyllabifier = no +indicfortune = no +ucasort = no +indicngram = no +shingling = no +textsimilarity = no +indicstemmer = no +katapayadi = no +scriptrender = no + +[module_display] +# Section gives names to be displayed on HTML and +# additionally the end point for each module +soundex = Soundex +inexactsearch = ApproxSearch +spellchecker = SpellCheck +transliteration = Transliteration +hyphenation = Hyphenate +chardetails = Chardetails +payyans = Payyans +indicsyllabifier = Syllabalizer +indicfortune = Fortune +ucasort = UCA Sort +indicngram = N-gram +shingling = Shingling +textsimilarity = Similar Texts +indicstemmer = Stemmer +katapayadi = Katapayadi Numbers +scriptrender = Script Render diff --git a/silpa/tests/resources/silpa_main_baseurl.conf b/silpa/tests/resources/silpa_main_baseurl.conf new file mode 100644 index 0000000..dddb914 --- /dev/null +++ b/silpa/tests/resources/silpa_main_baseurl.conf @@ -0,0 +1,60 @@ +[main] +# SITE Name +site = SILPA + +# If your site is not hosted in document root please give baseurl +# eg: www.example.org/silpa +# Leave the / if SILPA is hosted directly under documentroot +# leave it as / if you are using WSGI even if you are not under document root +#baseurl = / + +[logging] +# Set a logging level +# Allowed values are info,debug,warn,error +log_level = debug + +# Which folder log should be located +log_folder = /tmp + +# log file name +log_name = silpa.log + +[modules] +# These section provides list of modules use +# 'yes' to enable them and 'no' to disable +spellchecker = no +inexactsearch = yes +soundex = yes +transliteration = yes +hyphenation = no +chardetails = no +payyans = no +indicsyllabifier = no +indicfortune = no +ucasort = no +indicngram = no +shingling = no +textsimilarity = no +indicstemmer = no +katapayadi = no +scriptrender = no + +[module_display] +# Section gives names to be displayed on HTML and +# additionally the end point for each module +soundex = Soundex +inexactsearch = ApproxSearch +spellchecker = SpellCheck +transliteration = Transliteration +hyphenation = Hyphenate +chardetails = Chardetails +payyans = Payyans +indicsyllabifier = Syllabalizer +indicfortune = Fortune +ucasort = UCA Sort +indicngram = N-gram +shingling = Shingling +textsimilarity = Similar Texts +indicstemmer = Stemmer +katapayadi = Katapayadi Numbers +scriptrender = Script Render diff --git a/silpa/tests/resources/silpa_main_site.conf b/silpa/tests/resources/silpa_main_site.conf new file mode 100644 index 0000000..619073c --- /dev/null +++ b/silpa/tests/resources/silpa_main_site.conf @@ -0,0 +1,60 @@ +[main] +# SITE Name +#site = SILPA + +# If your site is not hosted in document root please give baseurl +# eg: www.example.org/silpa +# Leave the / if SILPA is hosted directly under documentroot +# leave it as / if you are using WSGI even if you are not under document root +baseurl = / + +[logging] +# Set a logging level +# Allowed values are info,debug,warn,error +log_level = debug + +# Which folder log should be located +log_folder = /tmp + +# log file name +log_name = silpa.log + +[modules] +# These section provides list of modules use +# 'yes' to enable them and 'no' to disable +spellchecker = no +inexactsearch = yes +soundex = yes +transliteration = yes +hyphenation = no +chardetails = no +payyans = no +indicsyllabifier = no +indicfortune = no +ucasort = no +indicngram = no +shingling = no +textsimilarity = no +indicstemmer = no +katapayadi = no +scriptrender = no + +[module_display] +# Section gives names to be displayed on HTML and +# additionally the end point for each module +soundex = Soundex +inexactsearch = ApproxSearch +spellchecker = SpellCheck +transliteration = Transliteration +hyphenation = Hyphenate +chardetails = Chardetails +payyans = Payyans +indicsyllabifier = Syllabalizer +indicfortune = Fortune +ucasort = UCA Sort +indicngram = N-gram +shingling = Shingling +textsimilarity = Similar Texts +indicstemmer = Stemmer +katapayadi = Katapayadi Numbers +scriptrender = Script Render diff --git a/silpa/tests/resources/silpa_module_display.conf b/silpa/tests/resources/silpa_module_display.conf new file mode 100644 index 0000000..65dd030 --- /dev/null +++ b/silpa/tests/resources/silpa_module_display.conf @@ -0,0 +1,60 @@ +[main] +# SITE Name +site = SILPA + +# If your site is not hosted in document root please give baseurl +# eg: www.example.org/silpa +# Leave the / if SILPA is hosted directly under documentroot +# leave it as / if you are using WSGI even if you are not under document root +baseurl = / + +[logging] +# Set a logging level +# Allowed values are info,debug,warn,error +log_level = debug + +# Which folder log should be located +log_folder = /tmp + +# log file name +log_name = silpa.log + +[modules] +# These section provides list of modules use +# 'yes' to enable them and 'no' to disable +spellchecker = no +inexactsearch = yes +soundex = yes +transliteration = yes +hyphenation = no +chardetails = no +payyans = no +indicsyllabifier = no +indicfortune = no +ucasort = no +indicngram = no +shingling = no +textsimilarity = no +indicstemmer = no +katapayadi = no +scriptrender = no + +# [module_display] +# # Section gives names to be displayed on HTML and +# # additionally the end point for each module +# soundex = Soundex +# inexactsearch = ApproxSearch +# spellchecker = SpellCheck +# transliteration = Transliteration +# hyphenation = Hyphenate +# chardetails = Chardetails +# payyans = Payyans +# indicsyllabifier = Syllabalizer +# indicfortune = Fortune +# ucasort = UCA Sort +# indicngram = N-gram +# shingling = Shingling +# textsimilarity = Similar Texts +# indicstemmer = Stemmer +# katapayadi = Katapayadi Numbers +# scriptrender = Script Render diff --git a/silpa/tests/resources/silpa_modules.conf b/silpa/tests/resources/silpa_modules.conf new file mode 100644 index 0000000..2df261f --- /dev/null +++ b/silpa/tests/resources/silpa_modules.conf @@ -0,0 +1,60 @@ +[main] +# SITE Name +site = SILPA + +# If your site is not hosted in document root please give baseurl +# eg: www.example.org/silpa +# Leave the / if SILPA is hosted directly under documentroot +# leave it as / if you are using WSGI even if you are not under document root +baseurl = / + +[logging] +# Set a logging level +# Allowed values are info,debug,warn,error +log_level = debug + +# Which folder log should be located +log_folder = /tmp + +# log file name +log_name = silpa.log + +# [modules] +# # These section provides list of modules use +# # 'yes' to enable them and 'no' to disable +# spellchecker = no +# inexactsearch = yes +# soundex = yes +# transliteration = yes +# hyphenation = no +# chardetails = no +# payyans = no +# indicsyllabifier = no +# indicfortune = no +# ucasort = no +# indicngram = no +# shingling = no +# textsimilarity = no +# indicstemmer = no +# katapayadi = no +# scriptrender = no + +[module_display] +# Section gives names to be displayed on HTML and +# additionally the end point for each module +soundex = Soundex +inexactsearch = ApproxSearch +spellchecker = SpellCheck +transliteration = Transliteration +hyphenation = Hyphenate +chardetails = Chardetails +payyans = Payyans +indicsyllabifier = Syllabalizer +indicfortune = Fortune +ucasort = UCA Sort +indicngram = N-gram +shingling = Shingling +textsimilarity = Similar Texts +indicstemmer = Stemmer +katapayadi = Katapayadi Numbers +scriptrender = Script Render diff --git a/silpa/tests/resources/silpa_warn.conf b/silpa/tests/resources/silpa_warn.conf new file mode 100644 index 0000000..9713c1d --- /dev/null +++ b/silpa/tests/resources/silpa_warn.conf @@ -0,0 +1,60 @@ +[main] +# SITE Name +site = SILPA + +# If your site is not hosted in document root please give baseurl +# eg: www.example.org/silpa +# Leave the / if SILPA is hosted directly under documentroot +# leave it as / if you are using WSGI even if you are not under document root +baseurl = / + +[logging] +# Set a logging level +# Allowed values are info,debug,warn,error +log_level = warn + +# Which folder log should be located +log_folder = /tmp + +# log file name +log_name = silpa.log + +[modules] +# These section provides list of modules use +# 'yes' to enable them and 'no' to disable +spellchecker = no +inexactsearch = yes +soundex = yes +transliteration = yes +hyphenation = no +chardetails = no +payyans = no +indicsyllabifier = no +indicfortune = no +ucasort = no +indicngram = no +shingling = no +textsimilarity = no +indicstemmer = no +katapayadi = no +scriptrender = no + +[module_display] +# Section gives names to be displayed on HTML and +# additionally the end point for each module +soundex = Soundex +inexactsearch = ApproxSearch +spellchecker = SpellCheck +transliteration = Transliteration +hyphenation = Hyphenate +chardetails = Chardetails +payyans = Payyans +indicsyllabifier = Syllabalizer +indicfortune = Fortune +ucasort = UCA Sort +indicngram = N-gram +shingling = Shingling +textsimilarity = Similar Texts +indicstemmer = Stemmer +katapayadi = Katapayadi Numbers +scriptrender = Script Render diff --git a/silpa/tests/settings.py b/silpa/tests/settings.py new file mode 100644 index 0000000..b8ef5fd --- /dev/null +++ b/silpa/tests/settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +""" + tests.settings + ~~~~~~~~~~~~~~ + + tests settings module +""" + +DEBUG = False +TESTING = True diff --git a/silpa/tests/test_config.py b/silpa/tests/test_config.py new file mode 100644 index 0000000..889d3fb --- /dev/null +++ b/silpa/tests/test_config.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +''' + tests.test_config + ~~~~~~~~~~~~~~~~~ + + Test various configuration parsing +''' + +import testscenarios +import os + +from functools import partial +from silpa.loadconfig import IncompleteConfigError, Config + +resource_path = partial(os.path.join, os.path.dirname(__file__), 'resources') + + +class TestConfigParser(testscenarios.TestWithScenarios): + scenarios = [ + ('main:site Missing Conf', + dict(location=resource_path('silpa_main_site.conf'), + section='main', option='site')), + ('main:baseurl Missing Conf', + dict(location=resource_path('silpa_main_baseurl.conf'), + section='main', option='baseurl')), + ('logging:log_level Missing Conf', + dict(location=resource_path('silpa_logging_loglevel.conf'), + section='logging', option='log_level')), + ('logging:log_folder Missing Conf', + dict(location=resource_path('silpa_logging_logfolder.conf'), + section='logging', option='log_folder')), + ('logging:log_name Missing Conf', + dict(location=resource_path('silpa_logging_logname.conf'), + section='logging', option='log_name')), + ('modules Missing Conf', + dict(location=resource_path('silpa_modules.conf'), + section='modules', option=None)), + ('modules_display Missing Conf', + dict(location=resource_path('silpa_module_display.conf'), + section='module_display', option=None)) + ] + + def test_config_error_handling(self): + with self.assertRaises(IncompleteConfigError) as ic: + Config(self.location) + + e = ic.exception + self.assertEqual(self.option, e.option) + self.assertEqual(self.section, e.section) + + if self.option: + error = e.__str__() + self.assertIn(self.option, error) + self.assertIn(self.section, error) + else: + self.assertIn(self.section, e.__str__()) diff --git a/silpa/tests/test_logconfig.py b/silpa/tests/test_logconfig.py new file mode 100644 index 0000000..197cc9e --- /dev/null +++ b/silpa/tests/test_logconfig.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +''' + silpa.tests.test_logconfig + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Test loglevel configuration code +''' + +import testscenarios +import os +import logging + +from functools import partial +from silpa.api import create_app + +resource_path = partial(os.path.join, os.path.dirname(__file__), + 'resources') + + +class TestLogLevelConfiguration(testscenarios.TestWithScenarios): + scenarios = [ + ('Info level configuration', + dict(location=resource_path('silpa_info.conf'), + level=logging.INFO)), + ('Error level configuration', + dict(location=resource_path('silpa_error.conf'), + level=logging.ERROR)), + ('Warning level configuration', + dict(location=resource_path('silpa_warn.conf'), + level=logging.WARNING)), + ] + + def test_logging_level(self): + app = create_app(self.location) + self.assertEqual(self.level, app.logger.level) diff --git a/silpa/tests/utils.py b/silpa/tests/utils.py new file mode 100644 index 0000000..c725b57 --- /dev/null +++ b/silpa/tests/utils.py @@ -0,0 +1,48 @@ +import json + + +class SILPATestCaseMixin(object): + def _json_data(self, kwargs): + if 'data' in kwargs: + kwargs['data'] = json.dumps(kwargs['data']) + if 'content_type' not in kwargs: + kwargs['content_type'] = 'application/json' + return kwargs + + def _request(self, method, *args, **kwargs): + kwargs.setdefault('content_type', 'text/html') + kwargs.setdefault('follow_redirects', True) + return method(*args, **kwargs) + + def get(self, *args, **kwargs): + return self._request(self.client.get, *args, **kwargs) + + def post(self, *args, **kwargs): + return self._request(self.client.post, *args, **kwargs) + + def jpost(self, *args, **kwargs): + return self._request(self.client.post, *args, + **self._json_data(kwargs)) + + def assertStatusCode(self, response, status_code): + self.assertEquals(status_code, response.status_code) + return response + + def assertOk(self, response): + return self.assertStatusCode(response, 200) + + def assertBadRequest(self, response): + return self.assertStatusCode(response, 400) + + def assertContentType(self, response, content_type): + self.assertEquals(content_type, response.headers['Content-Type']) + return response + + def assertJson(self, response): + return self.assertContentType(response, 'application/json') + + def assertOkJson(self, response): + return self.assertOk(self.assertJson(response)) + + def assertBadJson(self, response): + return self.assertBadRequest(self.assertJson(response)) diff --git a/static/css/jquery.ime.css b/static/css/jquery.ime.css deleted file mode 100644 index 03ba6ed..0000000 --- a/static/css/jquery.ime.css +++ /dev/null @@ -1,220 +0,0 @@ -.imeselector { - position: absolute; - /* @embed */ - background: url('../images/ime-active.png') no-repeat left center; - background-image: -webkit-linear-gradient(transparent, transparent), url('../images/ime-active.svg'); - background-image: -moz-linear-gradient(transparent, transparent), url('../images/ime-active.svg'); - background-image: linear-gradient(transparent, transparent), url('../images/ime-active.svg'); - background-color: rgba(255,255,255,0.75); - background-position: left 3px center; - background-position-x: 3px; - height: 15px; - font-size: small; - padding: 2px 2px 1px 20px; - box-shadow: 0 1px 3px 0 #777; - margin-top: 0; - text-align: left; - font-family: sans-serif; - white-space: nowrap; - z-index: 1000; -} - -.imeselector:hover { - box-shadow: 0 1px 3px 0 #565656; - border-top: none; - background-color: rgba(255,255,255,0.85); -} - -.imeselector a, -.ime-disable { - cursor: pointer; - text-decoration: none; - outline: none; - color: #222222; - line-height: 1em; - padding-top: 4px; - padding-bottom: 4px; -} - -.ime-setting-caret { - margin-left: 2px; - margin-top: 8px; - border-left: 4px solid transparent; - border-right: 4px solid transparent; - border-top: 4px solid #565656; - content: ""; - display: inline-block; - height: 0; - vertical-align: top; - width: 0; - -} - -span.ime-disable-link { - padding-left: 20px; - white-space: nowrap; -} - -span.ime-disable-shortcut { - text-align: right; - margin-left: 10px; - color: #888; - font-size: smaller; - padding-right: 4px; -} - -.ime-list-title, -.ime-lang-title { - color: #39d; - border-bottom: solid 1px #39d; - text-align: left; - font-size: larger; - font-weight: normal; - padding-bottom: 5px; - padding-left: 20px; - padding-top: 9px; - margin: 0 0 1px; -} - -.ime-language-list-wrapper { - position: relative; - padding: 0; - display: block; - overflow-y: auto; - max-height: 150px; -} - -.imeselector-menu { - position: absolute; - top: 14px; - right: 0; - z-index: 1000; - display: none; - float: left; - margin-top: 13px; - min-width: 160px; - padding: 0; - border: 1px solid #888; - background-color: #FFFFFF; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - border-radius: 5px; - -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - -webkit-background-clip: padding-box; - -moz-background-clip: padding; - background-clip: padding-box; - text-align: left; -} - -.imeselector-menu.ime-right { - right: auto; -} - -.imeselector-menu ul { - width: 100%; - padding: 0; - margin: 0; - list-style: none; -} - -/* The triangle shaped callout */ -.imeselector-menu:before { - border-bottom: 7px solid #888; - border-left: 7px solid transparent; - border-right: 7px solid transparent; - content: ""; - display: inline-block; - right: 9px; - position: absolute; - top: -7px; -} - -.imeselector-menu.ime-right:before { - right: auto; - left: 9px; -} - -.imeselector-menu:after { - border-bottom: 6px solid #FFFFFF; - border-left: 6px solid transparent; - border-right: 6px solid transparent; - content: ""; - display: inline-block; - right: 10px; - position: absolute; - top: -6px; -} - - -.imeselector-menu.ime-right:after { - right: auto; - left: 10px; -} - -.imeselector-menu.ime-position-top:before { - border-bottom: 0 none; - border-top: 7px solid #888; - top: auto; - bottom: -7px; -} - -.imeselector-menu.ime-position-top:after { - border-bottom: 0 none; - border-top: 6px solid #FFFFFF; - top: auto; - bottom: -6px; -} - -.imeselector-menu .ime-checked { - /* @embed */ - background: url(../images/tick.png) no-repeat left center; - background-image: -webkit-linear-gradient(transparent, transparent), url('../images/tick.svg'); - background-image: -moz-linear-gradient(transparent, transparent), url('../images/tick.svg'); - background-image: linear-gradient(transparent, transparent), url('../images/tick.svg'); - background-position: left 4px center; - background-position-x: 4px; -} - -.imeselector-menu .ime-help-link { - background-color: #f0f0f0; - border-radius: 0 0 5px 5px; - border-top: 1px solid #ddd; - margin-top: 6px; - padding: 2px 0; - color: #444; -} - -.imeselector-menu .ime-help-link > a:hover { - background-color: #f0f0f0; - color: #000; -} - -.imeselector-menu .selectable-row-item { - display: block; - padding-left: 20px; - padding-right: 20px; - font-weight: normal; - color: #333333; - outline: none; - white-space: nowrap; - position: relative; -} - -.imeselector-menu .selectable-row { - cursor: pointer; -} - -.imeselector-menu .selectable-row:hover { - background-color: #f0f0f0; -} - -.ime-open { - *z-index: 1000; - display: block; -} - -.imeselector-menu li { - position: relative; -} diff --git a/static/js/jquery.ime.js b/static/js/jquery.ime.js deleted file mode 100644 index 00f6dd3..0000000 --- a/static/js/jquery.ime.js +++ /dev/null @@ -1,2412 +0,0 @@ -/*! jquery.ime - v0.1.0+20131012 -* https://github.com/wikimedia/jquery.ime -* Copyright (c) 2013 Santhosh Thottingal; Licensed GPL, MIT */ -( function ( $ ) { - 'use strict'; - - // rangy is defined in the rangy library - /*global rangy */ - - /** - * IME Class - * @param {Function} [options.helpHandler] Called for each input method row in the selector - * @param {Object} options.helpHandler.imeSelector - * @param {String} options.helpHandler.ime Id of the input method - */ - function IME( element, options ) { - this.$element = $( element ); - // This needs to be delayed here since extending language list happens at DOM ready - $.ime.defaults.languages = arrayKeys( $.ime.languages ); - this.options = $.extend( {}, $.ime.defaults, options ); - this.active = false; - this.inputmethod = null; - this.language = null; - this.context = ''; - this.selector = this.$element.imeselector( this.options ); - this.listen(); - } - - IME.prototype = { - constructor: IME, - - /** - * Listen for events and bind to handlers - */ - listen: function () { - this.$element.on( 'keypress.ime', $.proxy( this.keypress, this ) ); - this.$element.on( 'destroy.ime', $.proxy( this.destroy, this ) ); - this.$element.on( 'enable.ime', $.proxy( this.enable, this ) ); - this.$element.on( 'disable.ime', $.proxy( this.disable, this ) ); - }, - - /** - * Transliterate a given string input based on context and input method definition. - * If there are no matching rules defined, returns the original string. - * - * @param {string} input - * @param {string} context - * @param {boolean} altGr whether altGr key is pressed or not - * @returns {string} transliterated string - */ - transliterate: function ( input, context, altGr ) { - var patterns, regex, rule, replacement, i; - - if ( altGr ) { - patterns = this.inputmethod.patterns_x || []; - } else { - patterns = this.inputmethod.patterns || []; - } - - if ( $.isFunction( patterns ) ) { - return patterns.call( this, input, context ); - } - - for ( i = 0; i < patterns.length; i++ ) { - rule = patterns[i]; - regex = new RegExp( rule[0] + '$' ); - - // Last item in the rules. - // It can also be a function, because the replace - // method can have a function as the second argument. - replacement = rule.slice( -1 )[0]; - - // Input string match test - if ( regex.test( input ) ) { - // Context test required? - if ( rule.length === 3 ) { - if ( new RegExp( rule[1] + '$' ).test( context ) ) { - return input.replace( regex, replacement ); - } - } else { - // No context test required. Just replace. - return input.replace( regex, replacement ); - } - } - } - - // No matches, return the input - return input; - }, - - /** - * Keypress handler - * @param {jQuery.Event} e Event - * @returns {Boolean} - */ - keypress: function ( e ) { - var altGr = false, - c, startPos, pos, endPos, divergingPos, input, replacement; - - if ( !this.active ) { - return true; - } - - if ( !this.inputmethod ) { - return true; - } - - // handle backspace - if ( e.which === 8 ) { - // Blank the context - this.context = ''; - return true; - } - - if ( e.altKey || e.altGraphKey ) { - altGr = true; - } - - // Don't process ASCII control characters except linefeed, - // as well as anything involving Ctrl, Meta and Alt, - // but do process extended keymaps - if ( ( e.which < 32 && e.which !== 13 && !altGr ) || e.ctrlKey || e.metaKey ) { - // Blank the context - this.context = ''; - - return true; - } - - c = String.fromCharCode( e.which ); - - // Get the current caret position. The user may have selected text to overwrite, - // so get both the start and end position of the selection. If there is no selection, - // startPos and endPos will be equal. - pos = this.getCaretPosition( this.$element ); - startPos = pos[0]; - endPos = pos[1]; - - // Get the last few characters before the one the user just typed, - // to provide context for the transliteration regexes. - // We need to append c because it hasn't been added to $this.val() yet - input = this.lastNChars( - this.$element.val() || this.$element.text(), - startPos, - this.inputmethod.maxKeyLength - ); - input += c; - - replacement = this.transliterate( input, this.context, altGr ); - - // Update the context - this.context += c; - - if ( this.context.length > this.inputmethod.contextLength ) { - // The buffer is longer than needed, truncate it at the front - this.context = this.context.substring( - this.context.length - this.inputmethod.contextLength - ); - } - - // If replacement equals to input, no replacement is made, because - // there's apparently nothing to do. However, there may be something - // to do if AltGr was pressed. For example, if a layout is built in - // a way that allows typing the original character instead of - // the replacement by pressing it with AltGr. - if ( !altGr && replacement === input ) { - return true; - } - - // Drop a common prefix, if any - divergingPos = this.firstDivergence( input, replacement ); - input = input.substring( divergingPos ); - replacement = replacement.substring( divergingPos ); - replaceText( this.$element, replacement, startPos - input.length + 1, endPos ); - - e.stopPropagation(); - - return false; - }, - - /** - * Check whether the input method is active or not - * @returns {Boolean} - */ - isActive: function () { - return this.active; - }, - - /** - * Disable the input method - */ - disable: function () { - this.active = false; - $.ime.preferences.setIM( 'system' ); - }, - - /** - * Enable the input method - */ - enable: function () { - this.active = true; - }, - - /** - * Toggle the active state of input method - */ - toggle: function () { - this.active = !this.active; - }, - - /** - * Destroy the binding of ime to the editable element - */ - destroy: function () { - $( 'body' ).off( '.ime' ); - this.$element.off( '.ime' ).removeData( 'ime' ).removeData( 'imeselector' ); - }, - - /** - * Get the current input method - * @returns {string} Current input method id - */ - getIM: function () { - return this.inputmethod; - }, - - /** - * Set the current input method - * @param {string} inputmethodId - */ - setIM: function ( inputmethodId ) { - this.inputmethod = $.ime.inputmethods[inputmethodId]; - $.ime.preferences.setIM( inputmethodId ); - }, - - /** - * Set the current Language - * @param {string} languageCode - * @returns {Boolean} - */ - setLanguage: function ( languageCode ) { - if ( !$.ime.languages[languageCode] ) { - debug( 'Language ' + languageCode + ' is not known to jquery.ime.' ); - - return false; - } - - this.language = languageCode; - $.ime.preferences.setLanguage( languageCode ); - return true; - }, - - /** - * Get current language - * @returns {string} - */ - getLanguage: function () { - return this.language; - }, - - /** - * load an input method by given id - * @param {string} inputmethodId - * @return {jQuery.Promise} - */ - load: function ( inputmethodId ) { - var ime = this, - deferred = $.Deferred(), - dependency; - - if ( $.ime.inputmethods[inputmethodId] ) { - return deferred.resolve(); - } - - dependency = $.ime.sources[inputmethodId].depends; - if ( dependency && !$.ime.inputmethods[dependency] ) { - ime.load( dependency ).done( function () { - ime.load( inputmethodId ).done( function () { - deferred.resolve(); - } ); - } ); - - return deferred; - } - - debug( 'Loading ' + inputmethodId ); - deferred = $.getScript( - ime.options.imePath + $.ime.sources[inputmethodId].source - ).done( function () { - debug( inputmethodId + ' loaded' ); - } ).fail( function ( jqxhr, settings, exception ) { - debug( 'Error in loading inputmethod ' + inputmethodId + ' Exception: ' + exception ); - } ); - - return deferred.promise(); - }, - - /** - * Returns an array [start, end] of the beginning - * and the end of the current selection in $element - * @returns {Array} - */ - getCaretPosition: function ( $element ) { - return getCaretPosition( $element ); - }, - - /** - * Set the caret position in the div. - * @param {jQuery} element The content editable div element - * @param {Object} position An object with start and end properties. - * @return {Array} If the cursor could not be placed at given position, how - * many characters had to go back to place the cursor - */ - setCaretPosition: function ( $element, position ) { - return setCaretPosition( $element, position ); - }, - - /** - * Find the point at which a and b diverge, i.e. the first position - * at which they don't have matching characters. - * - * @param a String - * @param b String - * @return Position at which a and b diverge, or -1 if a === b - */ - firstDivergence: function ( a, b ) { - return firstDivergence( a, b ); - }, - - /** - * Get the n characters in str that immediately precede pos - * Example: lastNChars( 'foobarbaz', 5, 2 ) === 'ba' - * - * @param str String to search in - * @param pos Position in str - * @param n Number of characters to go back from pos - * @return Substring of str, at most n characters long, immediately preceding pos - */ - lastNChars: function ( str, pos, n ) { - return lastNChars( str, pos, n ); - } - }; - - /** - * jQuery plugin ime - * @param {Object} option - */ - $.fn.ime = function ( option ) { - return this.each( function () { - var data, - $this = $( this ), - options = typeof option === 'object' && option; - - // Some exclusions: IME shouldn't be applied to textareas with - // these properties. - if ( $this.prop( 'readonly' ) || - $this.prop( 'disabled' ) || - $this.hasClass( 'noime' ) ) { - return; - } - - data = $this.data( 'ime' ); - - if ( !data ) { - data = new IME( this, options ); - $this.data( 'ime', data ); - } - - if ( typeof option === 'string' ) { - data[option](); - } - } ); - }; - - $.ime = {}; - $.ime.inputmethods = {}; - $.ime.sources = {}; - $.ime.preferences = {}; - $.ime.languages = {}; - - var defaultInputMethod = { - contextLength: 0, - maxKeyLength: 1 - }; - - $.ime.register = function ( inputMethod ) { - $.ime.inputmethods[inputMethod.id] = $.extend( {}, defaultInputMethod, inputMethod ); - }; - - // default options - $.ime.defaults = { - imePath: '../', // Relative/Absolute path for the rules folder of jquery.ime - languages: [], // Languages to be used- by default all languages - helpHandler: null // Called for each ime option in the menu - }; - - /** - * private function for debugging - */ - function debug( $obj ) { - if ( window.console && window.console.log ) { - window.console.log( $obj ); - } - } - - /** - * Returns an array [start, end] of the beginning - * and the end of the current selection in $element - */ - function getCaretPosition( $element ) { - var el = $element.get( 0 ), - start = 0, - end = 0, - normalizedValue, - range, - textInputRange, - len, - newLines, - endRange; - - if ( $element.is( '[contenteditable]' ) ) { - return getDivCaretPosition( el ); - } - - if ( typeof el.selectionStart === 'number' && typeof el.selectionEnd === 'number' ) { - start = el.selectionStart; - end = el.selectionEnd; - } else { - // IE - range = document.selection.createRange(); - - if ( range && range.parentElement() === el ) { - len = el.value.length; - normalizedValue = el.value.replace( /\r\n/g, '\n' ); - newLines = normalizedValue.match( /\n/g ); - - // Create a working TextRange that lives only in the input - textInputRange = el.createTextRange(); - textInputRange.moveToBookmark( range.getBookmark() ); - - // Check if the start and end of the selection are at the very end - // of the input, since moveStart/moveEnd doesn't return what we want - // in those cases - endRange = el.createTextRange(); - endRange.collapse( false ); - - if ( textInputRange.compareEndPoints( 'StartToEnd', endRange ) > -1 ) { - if ( newLines ) { - start = end = len - newLines.length; - } else { - start = end = len; - } - } else { - start = -textInputRange.moveStart( 'character', -len ); - - if ( textInputRange.compareEndPoints( 'EndToEnd', endRange ) > -1 ) { - end = len; - } else { - end = -textInputRange.moveEnd( 'character', -len ); - } - } - } - } - - return [start, end]; - } - - /** - * Helper function to get an IE TextRange object for an element - */ - function rangeForElementIE( element ) { - var selection; - - if ( element.nodeName.toLowerCase() === 'input' ) { - selection = element.createTextRange(); - } else { - selection = document.body.createTextRange(); - selection.moveToElementText( element ); - } - - return selection; - } - - function replaceText( $element, replacement, start, end ) { - var selection, - length, - newLines, - scrollTop, - range, - correction, - textNode, - element = $element.get( 0 ); - - if ( $element.is( '[contenteditable]' ) ) { - correction = setCaretPosition( $element, { - start: start, - end: end - } ); - - selection = rangy.getSelection(); - range = selection.getRangeAt( 0 ); - - if ( correction[0] > 0 ) { - replacement = selection.toString().substring( 0, correction[0] ) + replacement; - } - - textNode = document.createTextNode( replacement ); - range.deleteContents(); - range.insertNode( textNode ); - range.commonAncestorContainer.normalize(); - start = end = start + replacement.length - correction[0]; - setCaretPosition( $element, { - start: start, - end: end - } ); - - return; - } - - if ( typeof element.selectionStart === 'number' && typeof element.selectionEnd === 'number' ) { - // IE9+ and all other browsers - scrollTop = element.scrollTop; - - // Replace the whole text of the text area: - // text before + replacement + text after. - // This could be made better if range selection worked on browsers. - // But for complex scripts, browsers place cursor in unexpected places - // and it's not possible to fix cursor programmatically. - // Ref Bug https://bugs.webkit.org/show_bug.cgi?id=66630 - element.value = element.value.substring( 0, start ) + - replacement + - element.value.substring( end, element.value.length ); - - // restore scroll - element.scrollTop = scrollTop; - // set selection - element.selectionStart = element.selectionEnd = start + replacement.length; - } else { - // IE8 and lower - selection = rangeForElementIE(element); - length = element.value.length; - // IE doesn't count \n when computing the offset, so we won't either - newLines = element.value.match( /\n/g ); - - if ( newLines ) { - length = length - newLines.length; - } - - selection.moveStart( 'character', start ); - selection.moveEnd( 'character', end - length ); - - selection.text = replacement; - selection.collapse( false ); - selection.select(); - } - } - - function getDivCaretPosition( element ) { - var charIndex = 0, - start = 0, - end = 0, - foundStart = false, - foundEnd = false, - sel = rangy.getSelection(); - - function traverseTextNodes( node, range ) { - var i, childNodesCount; - - if ( node.nodeType === Node.TEXT_NODE ) { - if ( !foundStart && node === range.startContainer ) { - start = charIndex + range.startOffset; - foundStart = true; - } - - if ( foundStart && node === range.endContainer ) { - end = charIndex + range.endOffset; - foundEnd = true; - } - - charIndex += node.length; - } else { - childNodesCount = node.childNodes.length; - - for ( i = 0; i < childNodesCount; ++i ) { - traverseTextNodes( node.childNodes[i], range ); - if ( foundEnd ) { - break; - } - } - } - } - - if ( sel.rangeCount ) { - traverseTextNodes( element, sel.getRangeAt( 0 ) ); - } - - return [ start, end ]; - } - - function setCaretPosition( $element, position ) { - var currentPosition, - startCorrection = 0, - endCorrection = 0, - element = $element[0]; - - setDivCaretPosition( element, position ); - currentPosition = getDivCaretPosition( element ); - // see Bug https://bugs.webkit.org/show_bug.cgi?id=66630 - while ( position.start !== currentPosition[0] ) { - position.start -= 1; // go back one more position. - if ( position.start < 0 ) { - // never go beyond 0 - break; - } - setDivCaretPosition( element, position ); - currentPosition = getDivCaretPosition( element ); - startCorrection += 1; - } - - while ( position.end !== currentPosition[1] ) { - position.end += 1; // go forward one more position. - setDivCaretPosition( element, position ); - currentPosition = getDivCaretPosition( element ); - endCorrection += 1; - if ( endCorrection > 10 ) { - // XXX avoid rare case of infinite loop here. - break; - } - } - - return [startCorrection, endCorrection]; - } - - /** - * Set the caret position in the div. - * @param {Element} element The content editable div element - */ - function setDivCaretPosition( element, position ) { - var nextCharIndex, - charIndex = 0, - range = rangy.createRange(), - foundStart = false, - foundEnd = false; - - range.collapseToPoint( element, 0 ); - - function traverseTextNodes( node ) { - var i, len; - - if ( node.nodeType === 3 ) { - nextCharIndex = charIndex + node.length; - - if ( !foundStart && position.start >= charIndex && position.start <= nextCharIndex ) { - range.setStart( node, position.start - charIndex ); - foundStart = true; - } - - if ( foundStart && position.end >= charIndex && position.end <= nextCharIndex ) { - range.setEnd( node, position.end - charIndex ); - foundEnd = true; - } - - charIndex = nextCharIndex; - } else { - for ( i = 0, len = node.childNodes.length; i < len; ++i ) { - traverseTextNodes( node.childNodes[i] ); - if ( foundEnd ) { - rangy.getSelection().setSingleRange( range ); - break; - } - } - } - } - - traverseTextNodes( element ); - - } - - /** - * Find the point at which a and b diverge, i.e. the first position - * at which they don't have matching characters. - * - * @param a String - * @param b String - * @return Position at which a and b diverge, or -1 if a === b - */ - function firstDivergence( a, b ) { - var minLength, i; - - minLength = a.length < b.length ? a.length : b.length; - - for ( i = 0; i < minLength; i++ ) { - if ( a.charCodeAt( i ) !== b.charCodeAt( i ) ) { - return i; - } - } - - return -1; - } - - /** - * Get the n characters in str that immediately precede pos - * Example: lastNChars( 'foobarbaz', 5, 2 ) === 'ba' - * - * @param str String to search in - * @param pos Position in str - * @param n Number of characters to go back from pos - * @return Substring of str, at most n characters long, immediately preceding pos - */ - function lastNChars( str, pos, n ) { - if ( n === 0 ) { - return ''; - } else if ( pos <= n ) { - return str.substr( 0, pos ); - } else { - return str.substr( pos - n, n ); - } - } - - function arrayKeys ( obj ) { - return $.map( obj, function( element, index ) { - return index; - } ); - } -}( jQuery ) ); - -( function ( $ ) { - 'use strict'; - - var selectorTemplate, MutationObserver; - - function IMESelector( element, options ) { - this.$element = $( element ); - this.options = $.extend( {}, IMESelector.defaults, options ); - this.active = false; - this.$imeSetting = null; - this.$menu = null; - this.inputmethod = null; - this.timer = null; - this.init(); - this.listen(); - } - - IMESelector.prototype = { - constructor: IMESelector, - - init: function () { - this.prepareSelectorMenu(); - this.position(); - this.$imeSetting.hide(); - }, - - prepareSelectorMenu: function () { - // TODO: In this approach there is a menu for each editable area. - // With correct event mapping we can probably reduce it to one menu. - this.$imeSetting = $( selectorTemplate ); - this.$menu = $( '