diff --git a/sources/LICENSE b/sources/LICENSE new file mode 100644 index 0000000..b403467 --- /dev/null +++ b/sources/LICENSE @@ -0,0 +1,14 @@ +searx is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +searx is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with searx. If not, see < http://www.gnu.org/licenses/ >. + +(C) 2013- by Adam Tauber, diff --git a/sources/Makefile b/sources/Makefile new file mode 100644 index 0000000..fbc1c48 --- /dev/null +++ b/sources/Makefile @@ -0,0 +1,56 @@ +# convenience makefile to boostrap & run buildout +# use `make options=-v` to run buildout with extra options + +version = 2.7 +python = bin/python +options = + +all: .installed.cfg + +.installed.cfg: bin/buildout buildout.cfg setup.py + bin/buildout $(options) + +bin/buildout: $(python) buildout.cfg bootstrap.py + $(python) bootstrap.py + @touch $@ + +$(python): + virtualenv -p python$(version) --no-site-packages . + @touch $@ + +tests: .installed.cfg + @bin/test + +robot: .installed.cfg + @bin/robot + +flake8: .installed.cfg + @bin/flake8 setup.py + @bin/flake8 ./searx/ + +coverage: .installed.cfg + @bin/coverage run --source=./searx/ --branch bin/test + @bin/coverage report --show-missing + @bin/coverage html --directory ./coverage + +production: bin/buildout production.cfg setup.py + bin/buildout -c production.cfg $(options) + @echo "* Please modify `readlink --canonicalize-missing ./searx/settings.py`" + @echo "* Hint 1: on production, disable debug mode and change secret_key" + @echo "* Hint 2: searx will be executed at server startup by crontab" + @echo "* Hint 3: to run immediatley, execute 'bin/supervisord'" + +minimal: bin/buildout minimal.cfg setup.py + bin/buildout -c minimal.cfg $(options) + +styles: + @lessc -x searx/static/less/style.less > searx/static/css/style.css + +locales: + @pybabel compile -d searx/translations + +clean: + @rm -rf .installed.cfg .mr.developer.cfg bin parts develop-eggs \ + searx.egg-info lib include .coverage coverage searx/static/css/*.css + +.PHONY: all tests robot flake8 coverage production minimal styles locales clean diff --git a/sources/README.rst b/sources/README.rst new file mode 100644 index 0000000..bda4950 --- /dev/null +++ b/sources/README.rst @@ -0,0 +1,160 @@ +searx +===== + +A privacy-respecting, hackable `metasearch +engine `__. + +List of `running +instances `__. + +See the `wiki `__ for more information. + +|Flattr searx| + +Features +~~~~~~~~ + +- Tracking free +- Supports multiple output formats + - json ``curl https://searx.0x2a.tk/?format=json&q=[query]`` + - csv ``curl https://searx.0x2a.tk/?format=csv&q=[query]`` + - opensearch/rss ``curl https://searx.0x2a.tk/?format=rss&q=[query]`` +- Opensearch support (you can set as default search engine) +- Configurable search engines/categories +- Different search languages +- Duckduckgo like !bang functionality with engine shortcuts +- Parallel queries - relatively fast + +Installation +~~~~~~~~~~~~ + +- clone source: + ``git clone git@github.com:asciimoo/searx.git && cd searx`` +- install dependencies: ``pip install -r requirements.txt`` +- edit your + `settings.yml `__ + (set your ``secret_key``!) +- run ``python searx/webapp.py`` to start the application + +For all the details, follow this `step by step +installation `__ + +Alternative (Recommended) Installation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- clone source: + ``git clone git@github.com:asciimoo/searx.git && cd searx`` +- build in current folder: ``make minimal`` +- run ``bin/searx-run`` to start the application + +Development +~~~~~~~~~~~ + +Just run ``make``. Versions of dependencies are pinned down inside +``versions.cfg`` to produce most stable build. Also remember, NO make +command should be run as root, not even ``make production`` + +Deployment +~~~~~~~~~~ + +- clone source: + ``git clone git@github.com:asciimoo/searx.git && cd searx`` +- build in current folder: ``make production`` +- run ``bin/supervisord`` to start the application + +Upgrading +~~~~~~~~~ + +- inside previously cloned searx directory run: ``git stash`` to + temporarily save any changes you have made +- pull source: ``git pull origin master`` +- re-build in current folder: ``make production`` +- run ``bin/supervisorctl stop searx`` to stop searx, if it does not, + then run ``fuser -k 8888/tcp`` +- run ``bin/supervisorctl reload`` to re-read supervisor config and + start searx + +Command make +~~~~~~~~~~~~ + +``make`` +'''''''' + +Builds development environment with testing support. + +``make tests`` +'''''''''''''' + +Runs tests. You can write tests +`here `__ and +remember 'untested code is broken code'. + +``make robot`` +'''''''''''''' + +Runs robot (Selenium) tests, you must have ``firefox`` installed because +this functional tests actually run the browser and perform operations on +it. Also searx is executed with +`settings\_robot `__. + +``make flake8`` +''''''''''''''' + +'pep8 is a tool to check your Python code against some of the style +conventions in `PEP 8 `__.' + +``make coverage`` +''''''''''''''''' + +Checks coverage of tests, after running this, execute this: +``firefox ./coverage/index.html`` + +``make production`` +''''''''''''''''''' + +Used to make co-called production environment - without tests (you +should ran tests before deploying searx on the server). This installs +supervisord, so if searx crashes, it will try to pick itself up again. +And crontab entry is added to start supervisord at server boot. + +``make minimal`` +'''''''''''''''' + +Minimal build - without test frameworks, the quickest build option. + +``make clean`` +'''''''''''''' + +Deletes several folders and files (see ``Makefile`` for more), so that +next time you run any other ``make`` command it will rebuild everithing. + +TODO +~~~~ + +- Moar engines +- Better ui +- Browser integration +- Documentation +- Fix ``flake8`` errors, ``make flake8`` will be merged into + ``make tests`` when it does not fail anymore +- Tests +- When we have more tests, we can integrate Travis-CI + +Bugs +~~~~ + +Bugs or suggestions? Visit the `issue +tracker `__. + +`License `__ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +More about searx +~~~~~~~~~~~~~~~~ + +- `ohloh `__ +- `twitter `__ +- IRC: #searx @ freenode + +.. |Flattr searx| image:: http://api.flattr.com/button/flattr-badge-large.png + :target: https://flattr.com/submit/auto?user_id=asciimoo&url=https://github.com/asciimoo/searx&title=searx&language=&tags=github&category=software diff --git a/sources/babel.cfg b/sources/babel.cfg new file mode 100644 index 0000000..f0234b3 --- /dev/null +++ b/sources/babel.cfg @@ -0,0 +1,3 @@ +[python: **.py] +[jinja2: **/templates/**.html] +extensions=jinja2.ext.autoescape,jinja2.ext.with_ diff --git a/sources/base.cfg b/sources/base.cfg new file mode 100644 index 0000000..4ed6683 --- /dev/null +++ b/sources/base.cfg @@ -0,0 +1,23 @@ +[buildout] +extends = versions.cfg +versions = versions +unzip = true +newest = false +extends = versions.cfg +versions = versions +prefer-final = true +develop = . + +extensions = + buildout_versions + +eggs = + searx + +parts = + omelette + + +[omelette] +recipe = collective.recipe.omelette +eggs = ${buildout:eggs} diff --git a/sources/bootstrap.py b/sources/bootstrap.py new file mode 100644 index 0000000..d5e8be1 --- /dev/null +++ b/sources/bootstrap.py @@ -0,0 +1,277 @@ +############################################################################## +# +# Copyright (c) 2006 Zope Foundation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Bootstrap a buildout-based project + +Simply run this script in a directory containing a buildout.cfg. +The script accepts buildout command-line options, so you can +use the -c option to specify an alternate configuration file. +""" + +import os, shutil, sys, tempfile, urllib, urllib2, subprocess +from optparse import OptionParser + +if sys.platform == 'win32': + def quote(c): + if ' ' in c: + return '"%s"' % c # work around spawn lamosity on windows + else: + return c +else: + quote = str + +# See zc.buildout.easy_install._has_broken_dash_S for motivation and comments. +stdout, stderr = subprocess.Popen( + [sys.executable, '-Sc', + 'try:\n' + ' import ConfigParser\n' + 'except ImportError:\n' + ' print 1\n' + 'else:\n' + ' print 0\n'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() +has_broken_dash_S = bool(int(stdout.strip())) + +# In order to be more robust in the face of system Pythons, we want to +# run without site-packages loaded. This is somewhat tricky, in +# particular because Python 2.6's distutils imports site, so starting +# with the -S flag is not sufficient. However, we'll start with that: +if not has_broken_dash_S and 'site' in sys.modules: + # We will restart with python -S. + args = sys.argv[:] + args[0:0] = [sys.executable, '-S'] + args = map(quote, args) + os.execv(sys.executable, args) +# Now we are running with -S. We'll get the clean sys.path, import site +# because distutils will do it later, and then reset the path and clean +# out any namespace packages from site-packages that might have been +# loaded by .pth files. +clean_path = sys.path[:] +import site # imported because of its side effects +sys.path[:] = clean_path +for k, v in sys.modules.items(): + if k in ('setuptools', 'pkg_resources') or ( + hasattr(v, '__path__') and + len(v.__path__) == 1 and + not os.path.exists(os.path.join(v.__path__[0], '__init__.py'))): + # This is a namespace package. Remove it. + sys.modules.pop(k) + +is_jython = sys.platform.startswith('java') + +setuptools_source = 'http://peak.telecommunity.com/dist/ez_setup.py' +distribute_source = 'http://python-distribute.org/distribute_setup.py' +distribute_source = 'https://bitbucket.org/pypa/setuptools/raw/f657df1f1ed46596d236376649c99a470662b4ba/distribute_setup.py' + +# parsing arguments +def normalize_to_url(option, opt_str, value, parser): + if value: + if '://' not in value: # It doesn't smell like a URL. + value = 'file://%s' % ( + urllib.pathname2url( + os.path.abspath(os.path.expanduser(value))),) + if opt_str == '--download-base' and not value.endswith('/'): + # Download base needs a trailing slash to make the world happy. + value += '/' + else: + value = None + name = opt_str[2:].replace('-', '_') + setattr(parser.values, name, value) + +usage = '''\ +[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options] + +Bootstraps a buildout-based project. + +Simply run this script in a directory containing a buildout.cfg, using the +Python that you want bin/buildout to use. + +Note that by using --setup-source and --download-base to point to +local resources, you can keep this script from going over the network. +''' + +parser = OptionParser(usage=usage) +parser.add_option("-v", "--version", dest="version", + help="use a specific zc.buildout version") +parser.add_option("-d", "--distribute", + action="store_true", dest="use_distribute", default=False, + help="Use Distribute rather than Setuptools.") +parser.add_option("--setup-source", action="callback", dest="setup_source", + callback=normalize_to_url, nargs=1, type="string", + help=("Specify a URL or file location for the setup file. " + "If you use Setuptools, this will default to " + + setuptools_source + "; if you use Distribute, this " + "will default to " + distribute_source + ".")) +parser.add_option("--download-base", action="callback", dest="download_base", + callback=normalize_to_url, nargs=1, type="string", + help=("Specify a URL or directory for downloading " + "zc.buildout and either Setuptools or Distribute. " + "Defaults to PyPI.")) +parser.add_option("--eggs", + help=("Specify a directory for storing eggs. Defaults to " + "a temporary directory that is deleted when the " + "bootstrap script completes.")) +parser.add_option("-t", "--accept-buildout-test-releases", + dest='accept_buildout_test_releases', + action="store_true", default=False, + help=("Normally, if you do not specify a --version, the " + "bootstrap script and buildout gets the newest " + "*final* versions of zc.buildout and its recipes and " + "extensions for you. If you use this flag, " + "bootstrap and buildout will get the newest releases " + "even if they are alphas or betas.")) +parser.add_option("-c", None, action="store", dest="config_file", + help=("Specify the path to the buildout configuration " + "file to be used.")) + +options, args = parser.parse_args() + +if options.eggs: + eggs_dir = os.path.abspath(os.path.expanduser(options.eggs)) +else: + eggs_dir = tempfile.mkdtemp() + +if options.setup_source is None: + if options.use_distribute: + options.setup_source = distribute_source + else: + options.setup_source = setuptools_source + +if options.accept_buildout_test_releases: + args.insert(0, 'buildout:accept-buildout-test-releases=true') + +try: + import pkg_resources + import setuptools # A flag. Sometimes pkg_resources is installed alone. + if not hasattr(pkg_resources, '_distribute'): + raise ImportError +except ImportError: + ez_code = urllib2.urlopen( + options.setup_source).read().replace('\r\n', '\n') + ez = {} + exec ez_code in ez + setup_args = dict(to_dir=eggs_dir, download_delay=0) + if options.download_base: + setup_args['download_base'] = options.download_base + if options.use_distribute: + setup_args['no_fake'] = True + if sys.version_info[:2] == (2, 4): + setup_args['version'] = '0.6.32' + ez['use_setuptools'](**setup_args) + if 'pkg_resources' in sys.modules: + reload(sys.modules['pkg_resources']) + import pkg_resources + # This does not (always?) update the default working set. We will + # do it. + for path in sys.path: + if path not in pkg_resources.working_set.entries: + pkg_resources.working_set.add_entry(path) + +cmd = [quote(sys.executable), + '-c', + quote('from setuptools.command.easy_install import main; main()'), + '-mqNxd', + quote(eggs_dir)] + +if not has_broken_dash_S: + cmd.insert(1, '-S') + +find_links = options.download_base +if not find_links: + find_links = os.environ.get('bootstrap-testing-find-links') +if not find_links and options.accept_buildout_test_releases: + find_links = 'http://downloads.buildout.org/' +if find_links: + cmd.extend(['-f', quote(find_links)]) + +if options.use_distribute: + setup_requirement = 'distribute' +else: + setup_requirement = 'setuptools' +ws = pkg_resources.working_set +setup_requirement_path = ws.find( + pkg_resources.Requirement.parse(setup_requirement)).location +env = dict( + os.environ, + PYTHONPATH=setup_requirement_path) + +requirement = 'zc.buildout' +version = options.version +if version is None and not options.accept_buildout_test_releases: + # Figure out the most recent final version of zc.buildout. + import setuptools.package_index + _final_parts = '*final-', '*final' + + def _final_version(parsed_version): + for part in parsed_version: + if (part[:1] == '*') and (part not in _final_parts): + return False + return True + index = setuptools.package_index.PackageIndex( + search_path=[setup_requirement_path]) + if find_links: + index.add_find_links((find_links,)) + req = pkg_resources.Requirement.parse(requirement) + if index.obtain(req) is not None: + best = [] + bestv = None + for dist in index[req.project_name]: + distv = dist.parsed_version + if distv >= pkg_resources.parse_version('2dev'): + continue + if _final_version(distv): + if bestv is None or distv > bestv: + best = [dist] + bestv = distv + elif distv == bestv: + best.append(dist) + if best: + best.sort() + version = best[-1].version + +if version: + requirement += '=='+version +else: + requirement += '<2dev' + +cmd.append(requirement) + +if is_jython: + import subprocess + exitcode = subprocess.Popen(cmd, env=env).wait() +else: # Windows prefers this, apparently; otherwise we would prefer subprocess + exitcode = os.spawnle(*([os.P_WAIT, sys.executable] + cmd + [env])) +if exitcode != 0: + sys.stdout.flush() + sys.stderr.flush() + print ("An error occurred when trying to install zc.buildout. " + "Look above this message for any errors that " + "were output by easy_install.") + sys.exit(exitcode) + +ws.add_entry(eggs_dir) +ws.require(requirement) +import zc.buildout.buildout + +# If there isn't already a command in the args, add bootstrap +if not [a for a in args if '=' not in a]: + args.append('bootstrap') + + +# if -c was provided, we push it back into args for buildout's main function +if options.config_file is not None: + args[0:0] = ['-c', options.config_file] + +zc.buildout.buildout.main(args) +if not options.eggs: # clean up temporary egg directory + shutil.rmtree(eggs_dir) diff --git a/sources/buildout.cfg b/sources/buildout.cfg new file mode 100644 index 0000000..b9e6d24 --- /dev/null +++ b/sources/buildout.cfg @@ -0,0 +1,30 @@ +[buildout] +extends = base.cfg +develop = . + +eggs = + searx [test] + +parts += + pyscripts + robot + test + + +[pyscripts] +recipe = zc.recipe.egg:script +eggs = ${buildout:eggs} +interpreter = py +dependent-scripts = true + + +[robot] +recipe = zc.recipe.testrunner +eggs = ${buildout:eggs} +defaults = ['--color', '--auto-progress', '--layer', 'SearxRobotLayer'] + + +[test] +recipe = zc.recipe.testrunner +eggs = ${buildout:eggs} +defaults = ['--color', '--auto-progress', '--layer', 'SearxTestLayer', '--layer', '!SearxRobotLayer'] diff --git a/sources/examples/basic_engine.py b/sources/examples/basic_engine.py new file mode 100644 index 0000000..d786564 --- /dev/null +++ b/sources/examples/basic_engine.py @@ -0,0 +1,25 @@ + +categories = ['general'] # optional + +def request(query, params): + '''pre-request callback + params: + method : POST/GET + headers : {} + data : {} # if method == POST + url : '' + category: 'search category' + pageno : 1 # number of the requested page + ''' + + params['url'] = 'https://host/%s' % query + + return params + + +def response(resp): + '''post-response callback + resp: requests response object + ''' + return [{'url': '', 'title': '', 'content': ''}] + diff --git a/sources/minimal.cfg b/sources/minimal.cfg new file mode 100644 index 0000000..339a293 --- /dev/null +++ b/sources/minimal.cfg @@ -0,0 +1,15 @@ +[buildout] +extends = base.cfg +develop = . + +eggs = + searx + +parts += + pyscripts + + +[pyscripts] +recipe = zc.recipe.egg:script +eggs = ${buildout:eggs} +interpreter = py diff --git a/sources/production.cfg b/sources/production.cfg new file mode 100644 index 0000000..ea40682 --- /dev/null +++ b/sources/production.cfg @@ -0,0 +1,34 @@ +[buildout] +extends = base.cfg +develop = . + +eggs = + searx + +parts += + pyscripts + supervisor + crontab_reboot + + +[pyscripts] +recipe = zc.recipe.egg:script +eggs = ${buildout:eggs} +interpreter = py + + +[supervisor] +recipe = collective.recipe.supervisor +http-socket = unix +user = searxer +password = ohpleasedochangeme +file = /tmp/supervisor.sock +chmod = 0700 +programs = + 50 searx ${buildout:bin-directory}/searx-run + + +[crontab_reboot] +recipe = z3c.recipe.usercrontab +times = @reboot +command = ${buildout:bin-directory}/supervisord diff --git a/sources/requirements.txt b/sources/requirements.txt new file mode 100644 index 0000000..88c1bc7 --- /dev/null +++ b/sources/requirements.txt @@ -0,0 +1,6 @@ +flask +flask-babel +grequests +lxml +pyyaml +python-dateutil diff --git a/sources/searx/__init__.py b/sources/searx/__init__.py new file mode 100644 index 0000000..375a541 --- /dev/null +++ b/sources/searx/__init__.py @@ -0,0 +1,20 @@ +from os import environ +from os.path import realpath, dirname, join, abspath +try: + from yaml import load +except: + from sys import exit, stderr + stderr.write('[E] install pyyaml\n') + exit(2) + +searx_dir = abspath(dirname(__file__)) +engine_dir = dirname(realpath(__file__)) + +if 'SEARX_SETTINGS_PATH' in environ: + settings_path = environ['SEARX_SETTINGS_PATH'] +else: + settings_path = join(searx_dir, 'settings.yml') + + +with open(settings_path) as settings_yaml: + settings = load(settings_yaml) diff --git a/sources/searx/autocomplete.py b/sources/searx/autocomplete.py new file mode 100644 index 0000000..1726a8c --- /dev/null +++ b/sources/searx/autocomplete.py @@ -0,0 +1,53 @@ +from lxml import etree +from requests import get +from json import loads +from urllib import urlencode + + +def dbpedia(query): + # dbpedia autocompleter + autocomplete_url = 'http://lookup.dbpedia.org/api/search.asmx/KeywordSearch?' # noqa + + response = get(autocomplete_url + + urlencode(dict(QueryString=query))) + + results = [] + + if response.ok: + dom = etree.fromstring(response.content) + results = dom.xpath('//a:Result/a:Label//text()', + namespaces={'a': 'http://lookup.dbpedia.org/'}) + + return results + + +def google(query): + # google autocompleter + autocomplete_url = 'http://suggestqueries.google.com/complete/search?client=toolbar&' # noqa + + response = get(autocomplete_url + + urlencode(dict(q=query))) + + results = [] + + if response.ok: + dom = etree.fromstring(response.text) + results = dom.xpath('//suggestion/@data') + + return results + + +def wikipedia(query): + # wikipedia autocompleter + url = 'https://en.wikipedia.org/w/api.php?action=opensearch&{0}&limit=10&namespace=0&format=json' # noqa + + resp = loads(get(url.format(urlencode(dict(q=query)))).text) + if len(resp) > 1: + return resp[1] + return [] + + +backends = {'dbpedia': dbpedia, + 'google': google, + 'wikipedia': wikipedia + } diff --git a/sources/searx/engines/__init__.py b/sources/searx/engines/__init__.py new file mode 100644 index 0000000..72e5374 --- /dev/null +++ b/sources/searx/engines/__init__.py @@ -0,0 +1,330 @@ + +''' +searx is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +searx is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with searx. If not, see < http://www.gnu.org/licenses/ >. + +(C) 2013- by Adam Tauber, +''' + +from os.path import realpath, dirname, splitext, join +import sys +from imp import load_source +from itertools import izip_longest, chain +from operator import itemgetter +from urlparse import urlparse +from datetime import datetime +import grequests +from flask.ext.babel import gettext +from searx import settings +from searx.utils import gen_useragent + +engine_dir = dirname(realpath(__file__)) + +number_of_searches = 0 + +engines = {} + +categories = {'general': []} + +engine_shortcuts = {} + + +def load_module(filename): + modname = splitext(filename)[0] + if modname in sys.modules: + del sys.modules[modname] + filepath = join(engine_dir, filename) + module = load_source(modname, filepath) + module.name = modname + return module + +if not 'engines' in settings or not settings['engines']: + print '[E] Error no engines found. Edit your settings.yml' + exit(2) + +for engine_data in settings['engines']: + engine_name = engine_data['engine'] + engine = load_module(engine_name + '.py') + + for param_name in engine_data: + if param_name == 'engine': + continue + if param_name == 'categories': + if engine_data['categories'] == 'none': + engine.categories = [] + else: + engine.categories = map( + str.strip, engine_data['categories'].split(',')) + continue + setattr(engine, param_name, engine_data[param_name]) + + if not hasattr(engine, 'paging'): + engine.paging = False + + if not hasattr(engine, 'categories'): + engine.categories = ['general'] + + if not hasattr(engine, 'language_support'): + #engine.language_support = False + engine.language_support = True + + if not hasattr(engine, 'timeout'): + #engine.language_support = False + engine.timeout = settings['server']['request_timeout'] + + if not hasattr(engine, 'shortcut'): + #engine.shortcut = ''' + engine.shortcut = '' + + # checking required variables + for engine_attr in dir(engine): + if engine_attr.startswith('_'): + continue + if getattr(engine, engine_attr) is None: + print '[E] Engine config error: Missing attribute "{0}.{1}"'.format(engine.name, engine_attr) # noqa + sys.exit(1) + + engines[engine.name] = engine + engine.stats = { + 'result_count': 0, + 'search_count': 0, + 'page_load_time': 0, + 'score_count': 0, + 'errors': 0 + } + + if hasattr(engine, 'categories'): + for category_name in engine.categories: + categories.setdefault(category_name, []).append(engine) + else: + categories['general'].append(engine) + + if engine.shortcut: + # TODO check duplications + engine_shortcuts[engine.shortcut] = engine.name + + +def default_request_params(): + return { + 'method': 'GET', 'headers': {}, 'data': {}, 'url': '', 'cookies': {}} + + +def make_callback(engine_name, results, suggestions, callback, params): + # creating a callback wrapper for the search engine results + def process_callback(response, **kwargs): + cb_res = [] + response.search_params = params + engines[engine_name].stats['page_load_time'] += \ + (datetime.now() - params['started']).total_seconds() + try: + search_results = callback(response) + except Exception, e: + engines[engine_name].stats['errors'] += 1 + results[engine_name] = cb_res + print '[E] Error with engine "{0}":\n\t{1}'.format( + engine_name, str(e)) + return + for result in search_results: + result['engine'] = engine_name + if 'suggestion' in result: + # TODO type checks + suggestions.add(result['suggestion']) + continue + cb_res.append(result) + results[engine_name] = cb_res + return process_callback + + +def score_results(results): + flat_res = filter( + None, chain.from_iterable(izip_longest(*results.values()))) + flat_len = len(flat_res) + engines_len = len(results) + results = [] + # deduplication + scoring + for i, res in enumerate(flat_res): + res['parsed_url'] = urlparse(res['url']) + res['engines'] = [res['engine']] + weight = 1.0 + if hasattr(engines[res['engine']], 'weight'): + weight = float(engines[res['engine']].weight) + score = int((flat_len - i) / engines_len) * weight + 1 + duplicated = False + for new_res in results: + p1 = res['parsed_url'].path[:-1] if res['parsed_url'].path.endswith('/') else res['parsed_url'].path # noqa + p2 = new_res['parsed_url'].path[:-1] if new_res['parsed_url'].path.endswith('/') else new_res['parsed_url'].path # noqa + if res['parsed_url'].netloc == new_res['parsed_url'].netloc and\ + p1 == p2 and\ + res['parsed_url'].query == new_res['parsed_url'].query and\ + res.get('template') == new_res.get('template'): + duplicated = new_res + break + if duplicated: + if res.get('content') > duplicated.get('content'): + duplicated['content'] = res['content'] + duplicated['score'] += score + duplicated['engines'].append(res['engine']) + if duplicated['parsed_url'].scheme == 'https': + continue + elif res['parsed_url'].scheme == 'https': + duplicated['url'] = res['parsed_url'].geturl() + duplicated['parsed_url'] = res['parsed_url'] + else: + res['score'] = score + results.append(res) + return sorted(results, key=itemgetter('score'), reverse=True) + + +def search(query, request, selected_engines, pageno=1, lang='all'): + global engines, categories, number_of_searches + requests = [] + results = {} + suggestions = set() + number_of_searches += 1 + #user_agent = request.headers.get('User-Agent', '') + user_agent = gen_useragent() + + for selected_engine in selected_engines: + if selected_engine['name'] not in engines: + continue + + engine = engines[selected_engine['name']] + + if pageno > 1 and not engine.paging: + continue + + if lang != 'all' and not engine.language_support: + continue + + request_params = default_request_params() + request_params['headers']['User-Agent'] = user_agent + request_params['category'] = selected_engine['category'] + request_params['started'] = datetime.now() + request_params['pageno'] = pageno + request_params['language'] = lang + request_params = engine.request(query.encode('utf-8'), request_params) + + callback = make_callback( + selected_engine['name'], + results, + suggestions, + engine.response, + request_params + ) + + request_args = dict( + headers=request_params['headers'], + hooks=dict(response=callback), + cookies=request_params['cookies'], + timeout=engine.timeout + ) + + if request_params['method'] == 'GET': + req = grequests.get + else: + req = grequests.post + request_args['data'] = request_params['data'] + + # ignoring empty urls + if not request_params['url']: + continue + + requests.append(req(request_params['url'], **request_args)) + grequests.map(requests) + for engine_name, engine_results in results.items(): + engines[engine_name].stats['search_count'] += 1 + engines[engine_name].stats['result_count'] += len(engine_results) + + results = score_results(results) + + for result in results: + for res_engine in result['engines']: + engines[result['engine']].stats['score_count'] += result['score'] + + return results, suggestions + + +def get_engines_stats(): + # TODO refactor + pageloads = [] + results = [] + scores = [] + errors = [] + scores_per_result = [] + + max_pageload = max_results = max_score = max_errors = max_score_per_result = 0 # noqa + for engine in engines.values(): + if engine.stats['search_count'] == 0: + continue + results_num = \ + engine.stats['result_count'] / float(engine.stats['search_count']) + load_times = engine.stats['page_load_time'] / float(engine.stats['search_count']) # noqa + if results_num: + score = engine.stats['score_count'] / float(engine.stats['search_count']) # noqa + score_per_result = score / results_num + else: + score = score_per_result = 0.0 + max_results = max(results_num, max_results) + max_pageload = max(load_times, max_pageload) + max_score = max(score, max_score) + max_score_per_result = max(score_per_result, max_score_per_result) + max_errors = max(max_errors, engine.stats['errors']) + pageloads.append({'avg': load_times, 'name': engine.name}) + results.append({'avg': results_num, 'name': engine.name}) + scores.append({'avg': score, 'name': engine.name}) + errors.append({'avg': engine.stats['errors'], 'name': engine.name}) + scores_per_result.append({ + 'avg': score_per_result, + 'name': engine.name + }) + + for engine in pageloads: + engine['percentage'] = int(engine['avg'] / max_pageload * 100) + + for engine in results: + engine['percentage'] = int(engine['avg'] / max_results * 100) + + for engine in scores: + engine['percentage'] = int(engine['avg'] / max_score * 100) + + for engine in scores_per_result: + engine['percentage'] = int(engine['avg'] / max_score_per_result * 100) + + for engine in errors: + if max_errors: + engine['percentage'] = int(float(engine['avg']) / max_errors * 100) + else: + engine['percentage'] = 0 + + return [ + ( + gettext('Page loads (sec)'), + sorted(pageloads, key=itemgetter('avg')) + ), + ( + gettext('Number of results'), + sorted(results, key=itemgetter('avg'), reverse=True) + ), + ( + gettext('Scores'), + sorted(scores, key=itemgetter('avg'), reverse=True) + ), + ( + gettext('Scores per result'), + sorted(scores_per_result, key=itemgetter('avg'), reverse=True) + ), + ( + gettext('Errors'), + sorted(errors, key=itemgetter('avg'), reverse=True) + ), + ] diff --git a/sources/searx/engines/bing.py b/sources/searx/engines/bing.py new file mode 100644 index 0000000..9712a31 --- /dev/null +++ b/sources/searx/engines/bing.py @@ -0,0 +1,49 @@ +from urllib import urlencode +from cgi import escape +from lxml import html + +base_url = 'http://www.bing.com/' +search_string = 'search?{query}&first={offset}' +paging = True +language_support = True + + +def request(query, params): + offset = (params['pageno'] - 1) * 10 + 1 + if params['language'] == 'all': + language = 'en-US' + else: + language = params['language'].replace('_', '-') + search_path = search_string.format( + query=urlencode({'q': query, 'setmkt': language}), + offset=offset) + + params['cookies']['SRCHHPGUSR'] = \ + 'NEWWND=0&NRSLT=-1&SRCHLANG=' + language.split('-')[0] + #if params['category'] == 'images': + # params['url'] = base_url + 'images/' + search_path + params['url'] = base_url + search_path + return params + + +def response(resp): + global base_url + results = [] + dom = html.fromstring(resp.content) + for result in dom.xpath('//div[@class="sa_cc"]'): + link = result.xpath('.//h3/a')[0] + url = link.attrib.get('href') + title = ' '.join(link.xpath('.//text()')) + content = escape(' '.join(result.xpath('.//p//text()'))) + results.append({'url': url, 'title': title, 'content': content}) + + if results: + return results + + for result in dom.xpath('//li[@class="b_algo"]'): + link = result.xpath('.//h2/a')[0] + url = link.attrib.get('href') + title = ' '.join(link.xpath('.//text()')) + content = escape(' '.join(result.xpath('.//p//text()'))) + results.append({'url': url, 'title': title, 'content': content}) + return results diff --git a/sources/searx/engines/bing_news.py b/sources/searx/engines/bing_news.py new file mode 100644 index 0000000..5b48a54 --- /dev/null +++ b/sources/searx/engines/bing_news.py @@ -0,0 +1,51 @@ +from urllib import urlencode +from cgi import escape +from lxml import html + +categories = ['news'] + +base_url = 'http://www.bing.com/' +search_string = 'news/search?{query}&first={offset}' +paging = True +language_support = True + + +def request(query, params): + offset = (params['pageno'] - 1) * 10 + 1 + if params['language'] == 'all': + language = 'en-US' + else: + language = params['language'].replace('_', '-') + search_path = search_string.format( + query=urlencode({'q': query, 'setmkt': language}), + offset=offset) + + params['cookies']['SRCHHPGUSR'] = \ + 'NEWWND=0&NRSLT=-1&SRCHLANG=' + language.split('-')[0] + #if params['category'] == 'images': + # params['url'] = base_url + 'images/' + search_path + params['url'] = base_url + search_path + return params + + +def response(resp): + global base_url + results = [] + dom = html.fromstring(resp.content) + for result in dom.xpath('//div[@class="sa_cc"]'): + link = result.xpath('.//h3/a')[0] + url = link.attrib.get('href') + title = ' '.join(link.xpath('.//text()')) + content = escape(' '.join(result.xpath('.//p//text()'))) + results.append({'url': url, 'title': title, 'content': content}) + + if results: + return results + + for result in dom.xpath('//li[@class="b_algo"]'): + link = result.xpath('.//h2/a')[0] + url = link.attrib.get('href') + title = ' '.join(link.xpath('.//text()')) + content = escape(' '.join(result.xpath('.//p//text()'))) + results.append({'url': url, 'title': title, 'content': content}) + return results diff --git a/sources/searx/engines/currency_convert.py b/sources/searx/engines/currency_convert.py new file mode 100644 index 0000000..ce6b3b8 --- /dev/null +++ b/sources/searx/engines/currency_convert.py @@ -0,0 +1,62 @@ +from datetime import datetime +import re + +categories = [] +url = 'http://finance.yahoo.com/d/quotes.csv?e=.csv&f=sl1d1t1&s={query}=X' +weight = 100 + +parser_re = re.compile(r'^\W*(\d+(?:\.\d+)?)\W*([a-z]{3})\W*(?:in)?\W*([a-z]{3})\W*$', re.I) # noqa + + +def request(query, params): + m = parser_re.match(query) + if not m: + # wrong query + return params + try: + ammount, from_currency, to_currency = m.groups() + ammount = float(ammount) + except: + # wrong params + return params + + q = (from_currency + to_currency).upper() + + params['url'] = url.format(query=q) + params['ammount'] = ammount + params['from'] = from_currency + params['to'] = to_currency + + return params + + +def response(resp): + global base_url + results = [] + try: + _, conversion_rate, _ = resp.text.split(',', 2) + conversion_rate = float(conversion_rate) + except: + return results + + title = '{0} {1} in {2} is {3}'.format( + resp.search_params['ammount'], + resp.search_params['from'], + resp.search_params['to'], + resp.search_params['ammount'] * conversion_rate + ) + + content = '1 {0} is {1} {2}'.format(resp.search_params['from'], + conversion_rate, + resp.search_params['to']) + now_date = datetime.now().strftime('%Y%m%d') + url = 'http://finance.yahoo.com/currency/converter-results/{0}/{1}-{2}-to-{3}.html' # noqa + url = url.format( + now_date, + resp.search_params['ammount'], + resp.search_params['from'].lower(), + resp.search_params['to'].lower() + ) + results.append({'title': title, 'content': content, 'url': url}) + + return results diff --git a/sources/searx/engines/dailymotion.py b/sources/searx/engines/dailymotion.py new file mode 100644 index 0000000..03e1d7f --- /dev/null +++ b/sources/searx/engines/dailymotion.py @@ -0,0 +1,45 @@ +from urllib import urlencode +from json import loads +from lxml import html + +categories = ['videos'] +locale = 'en_US' + +# see http://www.dailymotion.com/doc/api/obj-video.html +search_url = 'https://api.dailymotion.com/videos?fields=title,description,duration,url,thumbnail_360_url&sort=relevance&limit=25&page={pageno}&{query}' # noqa + +# TODO use video result template +content_tpl = '
' + +paging = True + + +def request(query, params): + params['url'] = search_url.format( + query=urlencode({'search': query, 'localization': locale}), + pageno=params['pageno']) + return params + + +def response(resp): + results = [] + search_res = loads(resp.text) + if not 'list' in search_res: + return results + for res in search_res['list']: + title = res['title'] + url = res['url'] + if res['thumbnail_360_url']: + content = content_tpl.format(url, res['thumbnail_360_url']) + else: + content = '' + if res['description']: + description = text_content_from_html(res['description']) + content += description[:500] + results.append({'url': url, 'title': title, 'content': content}) + return results + + +def text_content_from_html(html_string): + desc_html = html.fragment_fromstring(html_string, create_parent=True) + return desc_html.text_content() diff --git a/sources/searx/engines/deviantart.py b/sources/searx/engines/deviantart.py new file mode 100644 index 0000000..d42a25a --- /dev/null +++ b/sources/searx/engines/deviantart.py @@ -0,0 +1,36 @@ +from urllib import urlencode +from urlparse import urljoin +from lxml import html + +categories = ['images'] + +base_url = 'https://www.deviantart.com/' +search_url = base_url+'search?offset={offset}&{query}' + +paging = True + + +def request(query, params): + offset = (params['pageno'] - 1) * 24 + params['url'] = search_url.format(offset=offset, + query=urlencode({'q': query})) + return params + + +def response(resp): + global base_url + results = [] + if resp.status_code == 302: + return results + dom = html.fromstring(resp.text) + for result in dom.xpath('//div[contains(@class, "tt-a tt-fh")]'): + link = result.xpath('.//a[contains(@class, "thumb")]')[0] + url = urljoin(base_url, link.attrib.get('href')) + title_links = result.xpath('.//span[@class="details"]//a[contains(@class, "t")]') # noqa + title = ''.join(title_links[0].xpath('.//text()')) + img_src = link.xpath('.//img')[0].attrib['src'] + results.append({'url': url, + 'title': title, + 'img_src': img_src, + 'template': 'images.html'}) + return results diff --git a/sources/searx/engines/duckduckgo.py b/sources/searx/engines/duckduckgo.py new file mode 100644 index 0000000..58cbc98 --- /dev/null +++ b/sources/searx/engines/duckduckgo.py @@ -0,0 +1,65 @@ +from urllib import urlencode +from lxml.html import fromstring +from searx.utils import html_to_text + +url = 'https://duckduckgo.com/html?{query}&s={offset}' +locale = 'us-en' + + +def request(query, params): + offset = (params['pageno'] - 1) * 30 + q = urlencode({'q': query, + 'l': locale}) + params['url'] = url.format(query=q, offset=offset) + return params + + +def response(resp): + result_xpath = '//div[@class="results_links results_links_deep web-result"]' # noqa + url_xpath = './/a[@class="large"]/@href' + title_xpath = './/a[@class="large"]//text()' + content_xpath = './/div[@class="snippet"]//text()' + results = [] + + doc = fromstring(resp.text) + + for r in doc.xpath(result_xpath): + try: + res_url = r.xpath(url_xpath)[-1] + except: + continue + if not res_url: + continue + title = html_to_text(''.join(r.xpath(title_xpath))) + content = html_to_text(''.join(r.xpath(content_xpath))) + results.append({'title': title, + 'content': content, + 'url': res_url}) + + return results + + +#from json import loads +#search_url = url + 'd.js?{query}&p=1&s={offset}' +# +#paging = True +# +# +#def request(query, params): +# offset = (params['pageno'] - 1) * 30 +# q = urlencode({'q': query, +# 'l': locale}) +# params['url'] = search_url.format(query=q, offset=offset) +# return params +# +# +#def response(resp): +# results = [] +# search_res = loads(resp.text[resp.text.find('[{'):-2])[:-1] +# for r in search_res: +# if not r.get('t'): +# continue +# results.append({'title': r['t'], +# 'content': html_to_text(r['a']), +# 'url': r['u']}) +# return results diff --git a/sources/searx/engines/duckduckgo_definitions.py b/sources/searx/engines/duckduckgo_definitions.py new file mode 100644 index 0000000..3037aae --- /dev/null +++ b/sources/searx/engines/duckduckgo_definitions.py @@ -0,0 +1,23 @@ +import json +from urllib import urlencode + +url = 'http://api.duckduckgo.com/?{query}&format=json&pretty=0&no_redirect=1' + + +def request(query, params): + params['url'] = url.format(query=urlencode({'q': query})) + return params + + +def response(resp): + search_res = json.loads(resp.text) + results = [] + if 'Definition' in search_res: + if search_res.get('AbstractURL'): + res = {'title': search_res.get('Heading', ''), + 'content': search_res.get('Definition', ''), + 'url': search_res.get('AbstractURL', ''), + 'class': 'definition_result'} + results.append(res) + + return results diff --git a/sources/searx/engines/dummy.py b/sources/searx/engines/dummy.py new file mode 100644 index 0000000..4586760 --- /dev/null +++ b/sources/searx/engines/dummy.py @@ -0,0 +1,6 @@ +def request(query, params): + return params + + +def response(resp): + return [] diff --git a/sources/searx/engines/filecrop.py b/sources/searx/engines/filecrop.py new file mode 100644 index 0000000..89dc776 --- /dev/null +++ b/sources/searx/engines/filecrop.py @@ -0,0 +1,84 @@ +from urllib import urlencode +from HTMLParser import HTMLParser + +url = 'http://www.filecrop.com/' +search_url = url + '/search.php?{query}&size_i=0&size_f=100000000&engine_r=1&engine_d=1&engine_e=1&engine_4=1&engine_m=1&pos={index}' # noqa + +paging = True + + +class FilecropResultParser(HTMLParser): + def __init__(self): + HTMLParser.__init__(self) + self.__start_processing = False + + self.results = [] + self.result = {} + + self.tr_counter = 0 + self.data_counter = 0 + + def handle_starttag(self, tag, attrs): + + if tag == 'tr': + if ('bgcolor', '#edeff5') in attrs or\ + ('bgcolor', '#ffffff') in attrs: + self.__start_processing = True + + if not self.__start_processing: + return + + if tag == 'label': + self.result['title'] = [attr[1] for attr in attrs + if attr[0] == 'title'][0] + elif tag == 'a' and ('rel', 'nofollow') in attrs\ + and ('class', 'sourcelink') in attrs: + if 'content' in self.result: + self.result['content'] += [attr[1] for attr in attrs + if attr[0] == 'title'][0] + else: + self.result['content'] = [attr[1] for attr in attrs + if attr[0] == 'title'][0] + self.result['content'] += ' ' + elif tag == 'a': + self.result['url'] = url + [attr[1] for attr in attrs + if attr[0] == 'href'][0] + + def handle_endtag(self, tag): + if self.__start_processing is False: + return + + if tag == 'tr': + self.tr_counter += 1 + + if self.tr_counter == 2: + self.__start_processing = False + self.tr_counter = 0 + self.data_counter = 0 + self.results.append(self.result) + self.result = {} + + def handle_data(self, data): + if not self.__start_processing: + return + + if 'content' in self.result: + self.result['content'] += data + ' ' + else: + self.result['content'] = data + ' ' + + self.data_counter += 1 + + +def request(query, params): + index = 1 + (params['pageno'] - 1) * 30 + params['url'] = search_url.format(query=urlencode({'w': query}), + index=index) + return params + + +def response(resp): + parser = FilecropResultParser() + parser.feed(resp.text) + + return parser.results diff --git a/sources/searx/engines/flickr.py b/sources/searx/engines/flickr.py new file mode 100644 index 0000000..265c59a --- /dev/null +++ b/sources/searx/engines/flickr.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python + +from urllib import urlencode +from lxml import html +from urlparse import urljoin + +categories = ['images'] + +url = 'https://secure.flickr.com/' +search_url = url+'search/?{query}&page={page}' +results_xpath = '//div[@id="thumbnails"]//a[@class="rapidnofollow photo-click" and @data-track="photo-click"]' # noqa + +paging = True + + +def request(query, params): + params['url'] = search_url.format(query=urlencode({'q': query}), + page=params['pageno']) + return params + + +def response(resp): + global base_url + results = [] + dom = html.fromstring(resp.text) + for result in dom.xpath(results_xpath): + href = urljoin(url, result.attrib.get('href')) + img = result.xpath('.//img')[0] + title = img.attrib.get('alt', '') + img_src = img.attrib.get('data-defer-src') + if not img_src: + continue + results.append({'url': href, + 'title': title, + 'img_src': img_src, + 'template': 'images.html'}) + return results diff --git a/sources/searx/engines/github.py b/sources/searx/engines/github.py new file mode 100644 index 0000000..be2cfe7 --- /dev/null +++ b/sources/searx/engines/github.py @@ -0,0 +1,32 @@ +from urllib import urlencode +from json import loads +from cgi import escape + +categories = ['it'] + +search_url = 'https://api.github.com/search/repositories?sort=stars&order=desc&{query}' # noqa + +accept_header = 'application/vnd.github.preview.text-match+json' + + +def request(query, params): + global search_url + params['url'] = search_url.format(query=urlencode({'q': query})) + params['headers']['Accept'] = accept_header + return params + + +def response(resp): + results = [] + search_res = loads(resp.text) + if not 'items' in search_res: + return results + for res in search_res['items']: + title = res['name'] + url = res['html_url'] + if res['description']: + content = escape(res['description'][:500]) + else: + content = '' + results.append({'url': url, 'title': title, 'content': content}) + return results diff --git a/sources/searx/engines/google.py b/sources/searx/engines/google.py new file mode 100644 index 0000000..2c6a98a --- /dev/null +++ b/sources/searx/engines/google.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python + +from urllib import urlencode +from json import loads + +categories = ['general'] + +url = 'https://ajax.googleapis.com/' +search_url = url + 'ajax/services/search/web?v=2.0&start={offset}&rsz=large&safe=off&filter=off&{query}&hl={language}' # noqa + +paging = True +language_support = True + + +def request(query, params): + offset = (params['pageno'] - 1) * 8 + language = 'en-US' + if params['language'] != 'all': + language = params['language'].replace('_', '-') + params['url'] = search_url.format(offset=offset, + query=urlencode({'q': query}), + language=language) + return params + + +def response(resp): + results = [] + search_res = loads(resp.text) + + if not search_res.get('responseData', {}).get('results'): + return [] + + for result in search_res['responseData']['results']: + results.append({'url': result['unescapedUrl'], + 'title': result['titleNoFormatting'], + 'content': result['content']}) + return results diff --git a/sources/searx/engines/google_images.py b/sources/searx/engines/google_images.py new file mode 100644 index 0000000..a6837f0 --- /dev/null +++ b/sources/searx/engines/google_images.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python + +from urllib import urlencode +from json import loads + +categories = ['images'] + +url = 'https://ajax.googleapis.com/' +search_url = url + 'ajax/services/search/images?v=1.0&start={offset}&rsz=large&safe=off&filter=off&{query}' # noqa + + +def request(query, params): + offset = (params['pageno'] - 1) * 8 + params['url'] = search_url.format(query=urlencode({'q': query}), + offset=offset) + return params + + +def response(resp): + results = [] + search_res = loads(resp.text) + if not search_res.get('responseData'): + return [] + if not search_res['responseData'].get('results'): + return [] + for result in search_res['responseData']['results']: + href = result['originalContextUrl'] + title = result['title'] + if not result['url']: + continue + results.append({'url': href, + 'title': title, + 'content': '', + 'img_src': result['url'], + 'template': 'images.html'}) + return results diff --git a/sources/searx/engines/google_news.py b/sources/searx/engines/google_news.py new file mode 100644 index 0000000..72b7a06 --- /dev/null +++ b/sources/searx/engines/google_news.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python + +from urllib import urlencode +from json import loads +from dateutil import parser + +categories = ['news'] + +url = 'https://ajax.googleapis.com/' +search_url = url + 'ajax/services/search/news?v=2.0&start={offset}&rsz=large&safe=off&filter=off&{query}&hl={language}' # noqa + +paging = True +language_support = True + + +def request(query, params): + offset = (params['pageno'] - 1) * 8 + language = 'en-US' + if params['language'] != 'all': + language = params['language'].replace('_', '-') + params['url'] = search_url.format(offset=offset, + query=urlencode({'q': query}), + language=language) + return params + + +def response(resp): + results = [] + search_res = loads(resp.text) + + if not search_res.get('responseData', {}).get('results'): + return [] + + for result in search_res['responseData']['results']: + +# Mon, 10 Mar 2014 16:26:15 -0700 + publishedDate = parser.parse(result['publishedDate']) + + results.append({'url': result['unescapedUrl'], + 'title': result['titleNoFormatting'], + 'publishedDate': publishedDate, + 'content': result['content']}) + return results diff --git a/sources/searx/engines/json_engine.py b/sources/searx/engines/json_engine.py new file mode 100644 index 0000000..708b999 --- /dev/null +++ b/sources/searx/engines/json_engine.py @@ -0,0 +1,87 @@ +from urllib import urlencode +from json import loads +from collections import Iterable + +search_url = None +url_query = None +content_query = None +title_query = None +#suggestion_xpath = '' + + +def iterate(iterable): + if type(iterable) == dict: + it = iterable.iteritems() + + else: + it = enumerate(iterable) + for index, value in it: + yield str(index), value + + +def is_iterable(obj): + if type(obj) == str: + return False + if type(obj) == unicode: + return False + return isinstance(obj, Iterable) + + +def parse(query): + q = [] + for part in query.split('/'): + if part == '': + continue + else: + q.append(part) + return q + + +def do_query(data, q): + ret = [] + if not q: + return ret + + qkey = q[0] + + for key, value in iterate(data): + + if len(q) == 1: + if key == qkey: + ret.append(value) + elif is_iterable(value): + ret.extend(do_query(value, q)) + else: + if not is_iterable(value): + continue + if key == qkey: + ret.extend(do_query(value, q[1:])) + else: + ret.extend(do_query(value, q)) + return ret + + +def query(data, query_string): + q = parse(query_string) + + return do_query(data, q) + + +def request(query, params): + query = urlencode({'q': query})[2:] + params['url'] = search_url.format(query=query) + params['query'] = query + return params + + +def response(resp): + results = [] + + json = loads(resp.text) + + urls = query(json, url_query) + contents = query(json, content_query) + titles = query(json, title_query) + for url, title, content in zip(urls, titles, contents): + results.append({'url': url, 'title': title, 'content': content}) + return results diff --git a/sources/searx/engines/mediawiki.py b/sources/searx/engines/mediawiki.py new file mode 100644 index 0000000..f8cfb9a --- /dev/null +++ b/sources/searx/engines/mediawiki.py @@ -0,0 +1,22 @@ +from json import loads +from urllib import urlencode, quote + +url = 'https://en.wikipedia.org/' + +search_url = url + 'w/api.php?action=query&list=search&{query}&srprop=timestamp&format=json&sroffset={offset}' # noqa + +number_of_results = 10 + + +def request(query, params): + offset = (params['pageno'] - 1) * 10 + params['url'] = search_url.format(query=urlencode({'srsearch': query}), + offset=offset) + return params + + +def response(resp): + search_results = loads(resp.text) + res = search_results.get('query', {}).get('search', []) + return [{'url': url + 'wiki/' + quote(result['title'].replace(' ', '_').encode('utf-8')), # noqa + 'title': result['title']} for result in res[:int(number_of_results)]] diff --git a/sources/searx/engines/piratebay.py b/sources/searx/engines/piratebay.py new file mode 100644 index 0000000..ca10851 --- /dev/null +++ b/sources/searx/engines/piratebay.py @@ -0,0 +1,48 @@ +from urlparse import urljoin +from cgi import escape +from urllib import quote +from lxml import html + +categories = ['videos', 'music'] + +url = 'https://thepiratebay.se/' +search_url = url + 'search/{search_term}/{pageno}/99/{search_type}' +search_types = {'videos': '200', + 'music': '100', + 'files': '0'} + +magnet_xpath = './/a[@title="Download this torrent using magnet"]' +content_xpath = './/font[@class="detDesc"]//text()' + +paging = True + + +def request(query, params): + search_type = search_types.get(params['category'], '200') + params['url'] = search_url.format(search_term=quote(query), + search_type=search_type, + pageno=params['pageno'] - 1) + return params + + +def response(resp): + results = [] + dom = html.fromstring(resp.text) + search_res = dom.xpath('//table[@id="searchResult"]//tr') + if not search_res: + return results + for result in search_res[1:]: + link = result.xpath('.//div[@class="detName"]//a')[0] + href = urljoin(url, link.attrib.get('href')) + title = ' '.join(link.xpath('.//text()')) + content = escape(' '.join(result.xpath(content_xpath))) + seed, leech = result.xpath('.//td[@align="right"]/text()')[:2] + magnetlink = result.xpath(magnet_xpath)[0] + results.append({'url': href, + 'title': title, + 'content': content, + 'seed': seed, + 'leech': leech, + 'magnetlink': magnetlink.attrib['href'], + 'template': 'torrent.html'}) + return results diff --git a/sources/searx/engines/soundcloud.py b/sources/searx/engines/soundcloud.py new file mode 100644 index 0000000..e28fb16 --- /dev/null +++ b/sources/searx/engines/soundcloud.py @@ -0,0 +1,31 @@ +from json import loads +from urllib import urlencode + +categories = ['music'] + +guest_client_id = 'b45b1aa10f1ac2941910a7f0d10f8e28' +url = 'https://api.soundcloud.com/' +search_url = url + 'search?{query}&facet=model&limit=20&offset={offset}&linked_partitioning=1&client_id='+guest_client_id # noqa + +paging = True + + +def request(query, params): + offset = (params['pageno'] - 1) * 20 + params['url'] = search_url.format(query=urlencode({'q': query}), + offset=offset) + return params + + +def response(resp): + global base_url + results = [] + search_res = loads(resp.text) + for result in search_res.get('collection', []): + if result['kind'] in ('track', 'playlist'): + title = result['title'] + content = result['description'] + results.append({'url': result['permalink_url'], + 'title': title, + 'content': content}) + return results diff --git a/sources/searx/engines/stackoverflow.py b/sources/searx/engines/stackoverflow.py new file mode 100644 index 0000000..e24b309 --- /dev/null +++ b/sources/searx/engines/stackoverflow.py @@ -0,0 +1,30 @@ +from urlparse import urljoin +from cgi import escape +from urllib import urlencode +from lxml import html + +categories = ['it'] + +url = 'http://stackoverflow.com/' +search_url = url+'search?{query}&page={pageno}' +result_xpath = './/div[@class="excerpt"]//text()' + +paging = True + + +def request(query, params): + params['url'] = search_url.format(query=urlencode({'q': query}), + pageno=params['pageno']) + return params + + +def response(resp): + results = [] + dom = html.fromstring(resp.text) + for result in dom.xpath('//div[@class="question-summary search-result"]'): + link = result.xpath('.//div[@class="result-link"]//a')[0] + href = urljoin(url, link.attrib.get('href')) + title = escape(' '.join(link.xpath('.//text()'))) + content = escape(' '.join(result.xpath(result_xpath))) + results.append({'url': href, 'title': title, 'content': content}) + return results diff --git a/sources/searx/engines/startpage.py b/sources/searx/engines/startpage.py new file mode 100644 index 0000000..f5a6523 --- /dev/null +++ b/sources/searx/engines/startpage.py @@ -0,0 +1,46 @@ +from urllib import urlencode +from lxml import html + +base_url = None +search_url = None + +# TODO paging +paging = False +# TODO complete list of country mapping +country_map = {'en_US': 'eng', + 'en_UK': 'uk', + 'nl_NL': 'ned'} + + +def request(query, params): + query = urlencode({'q': query})[2:] + params['url'] = search_url + params['method'] = 'POST' + params['data'] = {'query': query, + 'startat': (params['pageno'] - 1) * 10} # offset + country = country_map.get(params['language'], 'eng') + params['cookies']['preferences'] = \ + 'lang_homepageEEEs/air/{country}/N1NsslEEE1N1Nfont_sizeEEEmediumN1Nrecent_results_filterEEE1N1Nlanguage_uiEEEenglishN1Ndisable_open_in_new_windowEEE0N1Ncolor_schemeEEEnewN1Nnum_of_resultsEEE10N1N'.format(country=country) # noqa + return params + + +def response(resp): + results = [] + dom = html.fromstring(resp.content) + # ads xpath //div[@id="results"]/div[@id="sponsored"]//div[@class="result"] + # not ads: div[@class="result"] are the direct childs of div[@id="results"] + for result in dom.xpath('//div[@class="result"]'): + link = result.xpath('.//h3/a')[0] + url = link.attrib.get('href') + if url.startswith('http://www.google.')\ + or url.startswith('https://www.google.'): + continue + title = link.text_content() + + content = '' + if result.xpath('./p[@class="desc"]'): + content = result.xpath('./p[@class="desc"]')[0].text_content() + + results.append({'url': url, 'title': title, 'content': content}) + + return results diff --git a/sources/searx/engines/twitter.py b/sources/searx/engines/twitter.py new file mode 100644 index 0000000..23393ac --- /dev/null +++ b/sources/searx/engines/twitter.py @@ -0,0 +1,32 @@ +from urlparse import urljoin +from urllib import urlencode +from lxml import html +from cgi import escape + +categories = ['social media'] + +base_url = 'https://twitter.com/' +search_url = base_url+'search?' +title_xpath = './/span[@class="username js-action-profile-name"]//text()' +content_xpath = './/p[@class="js-tweet-text tweet-text"]//text()' + + +def request(query, params): + global search_url + params['url'] = search_url + urlencode({'q': query}) + return params + + +def response(resp): + global base_url + results = [] + dom = html.fromstring(resp.text) + for tweet in dom.xpath('//li[@data-item-type="tweet"]'): + link = tweet.xpath('.//small[@class="time"]//a')[0] + url = urljoin(base_url, link.attrib.get('href')) + title = ''.join(tweet.xpath(title_xpath)) + content = escape(''.join(tweet.xpath(content_xpath))) + results.append({'url': url, + 'title': title, + 'content': content}) + return results diff --git a/sources/searx/engines/vimeo.py b/sources/searx/engines/vimeo.py new file mode 100644 index 0000000..94a6dd5 --- /dev/null +++ b/sources/searx/engines/vimeo.py @@ -0,0 +1,54 @@ +from urllib import urlencode +from HTMLParser import HTMLParser +from lxml import html +from searx.engines.xpath import extract_text +from dateutil import parser + +base_url = 'http://vimeo.com' +search_url = base_url + '/search?{query}' +url_xpath = None +content_xpath = None +title_xpath = None +results_xpath = '' +content_tpl = ' ' +publishedDate_xpath = './/p[@class="meta"]//attribute::datetime' + +# the cookie set by vimeo contains all the following values, +# but only __utma seems to be requiered +cookie = { + #'vuid':'918282893.1027205400' + # 'ab_bs':'%7B%223%22%3A279%7D' + '__utma': '00000000.000#0000000.0000000000.0000000000.0000000000.0' + # '__utmb':'18302654.1.10.1388942090' + #, '__utmc':'18302654' + #, '__utmz':'18#302654.1388942090.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none)' # noqa + #, '__utml':'search' +} + + +def request(query, params): + params['url'] = search_url.format(query=urlencode({'q': query})) + params['cookies'] = cookie + return params + + +def response(resp): + results = [] + dom = html.fromstring(resp.text) + + p = HTMLParser() + + for result in dom.xpath(results_xpath): + url = base_url + result.xpath(url_xpath)[0] + title = p.unescape(extract_text(result.xpath(title_xpath))) + thumbnail = extract_text(result.xpath(content_xpath)[0]) + publishedDate = parser.parse(extract_text( + result.xpath(publishedDate_xpath)[0])) + + results.append({'url': url, + 'title': title, + 'content': content_tpl.format(url, title, thumbnail), + 'template': 'videos.html', + 'publishedDate': publishedDate, + 'thumbnail': thumbnail}) + return results diff --git a/sources/searx/engines/wikipedia.py b/sources/searx/engines/wikipedia.py new file mode 100644 index 0000000..1e2a798 --- /dev/null +++ b/sources/searx/engines/wikipedia.py @@ -0,0 +1,30 @@ +from json import loads +from urllib import urlencode, quote + +url = 'https://{language}.wikipedia.org/' + +search_url = url + 'w/api.php?action=query&list=search&{query}&srprop=timestamp&format=json&sroffset={offset}' # noqa + +number_of_results = 10 + +language_support = True + + +def request(query, params): + offset = (params['pageno'] - 1) * 10 + if params['language'] == 'all': + language = 'en' + else: + language = params['language'].split('_')[0] + params['language'] = language + params['url'] = search_url.format(query=urlencode({'srsearch': query}), + offset=offset, + language=language) + return params + + +def response(resp): + search_results = loads(resp.text) + res = search_results.get('query', {}).get('search', []) + return [{'url': url.format(language=resp.search_params['language']) + 'wiki/' + quote(result['title'].replace(' ', '_').encode('utf-8')), # noqa + 'title': result['title']} for result in res[:int(number_of_results)]] diff --git a/sources/searx/engines/xpath.py b/sources/searx/engines/xpath.py new file mode 100644 index 0000000..7212030 --- /dev/null +++ b/sources/searx/engines/xpath.py @@ -0,0 +1,106 @@ +from lxml import html +from urllib import urlencode, unquote +from urlparse import urlparse, urljoin +from lxml.etree import _ElementStringResult, _ElementUnicodeResult +from searx.utils import html_to_text + +search_url = None +url_xpath = None +content_xpath = None +title_xpath = None +suggestion_xpath = '' +results_xpath = '' + + +''' +if xpath_results is list, extract the text from each result and concat the list +if xpath_results is a xml element, extract all the text node from it + ( text_content() method from lxml ) +if xpath_results is a string element, then it's already done +''' + + +def extract_text(xpath_results): + if type(xpath_results) == list: + # it's list of result : concat everything using recursive call + if not xpath_results: + raise Exception('Empty url resultset') + result = '' + for e in xpath_results: + result = result + extract_text(e) + return result + elif type(xpath_results) in [_ElementStringResult, _ElementUnicodeResult]: + # it's a string + return ''.join(xpath_results) + else: + # it's a element + return html_to_text(xpath_results.text_content()) + + +def extract_url(xpath_results, search_url): + url = extract_text(xpath_results) + + if url.startswith('//'): + # add http or https to this kind of url //example.com/ + parsed_search_url = urlparse(search_url) + url = parsed_search_url.scheme+url + elif url.startswith('/'): + # fix relative url to the search engine + url = urljoin(search_url, url) + + # normalize url + url = normalize_url(url) + + return url + + +def normalize_url(url): + parsed_url = urlparse(url) + + # add a / at this end of the url if there is no path + if not parsed_url.netloc: + raise Exception('Cannot parse url') + if not parsed_url.path: + url += '/' + + # FIXME : hack for yahoo + if parsed_url.hostname == 'search.yahoo.com'\ + and parsed_url.path.startswith('/r'): + p = parsed_url.path + mark = p.find('/**') + if mark != -1: + return unquote(p[mark+3:]).decode('utf-8') + + return url + + +def request(query, params): + query = urlencode({'q': query})[2:] + params['url'] = search_url.format(query=query) + params['query'] = query + return params + + +def response(resp): + results = [] + dom = html.fromstring(resp.text) + if results_xpath: + for result in dom.xpath(results_xpath): + url = extract_url(result.xpath(url_xpath), search_url) + title = extract_text(result.xpath(title_xpath)[0]) + content = extract_text(result.xpath(content_xpath)[0]) + results.append({'url': url, 'title': title, 'content': content}) + else: + for url, title, content in zip( + (extract_url(x, search_url) for + x in dom.xpath(url_xpath)), + map(extract_text, dom.xpath(title_xpath)), + map(extract_text, dom.xpath(content_xpath)) + ): + results.append({'url': url, 'title': title, 'content': content}) + + if not suggestion_xpath: + return results + for suggestion in dom.xpath(suggestion_xpath): + results.append({'suggestion': extract_text(suggestion)}) + return results diff --git a/sources/searx/engines/yacy.py b/sources/searx/engines/yacy.py new file mode 100644 index 0000000..efdf846 --- /dev/null +++ b/sources/searx/engines/yacy.py @@ -0,0 +1,40 @@ +from json import loads +from urllib import urlencode + +url = 'http://localhost:8090' +search_url = '/yacysearch.json?{query}&maximumRecords=10' + + +def request(query, params): + params['url'] = url + search_url.format(query=urlencode({'query': query})) + return params + + +def response(resp): + raw_search_results = loads(resp.text) + + if not raw_search_results: + return [] + + search_results = raw_search_results.get('channels', {})[0].get('items', []) + + results = [] + + for result in search_results: + tmp_result = {} + tmp_result['title'] = result['title'] + tmp_result['url'] = result['link'] + tmp_result['content'] = '' + + if result['description']: + tmp_result['content'] += result['description'] + "
" + + if result['pubDate']: + tmp_result['content'] += result['pubDate'] + "
" + + if result['size'] != '-1': + tmp_result['content'] += result['sizename'] + + results.append(tmp_result) + + return results diff --git a/sources/searx/engines/yahoo.py b/sources/searx/engines/yahoo.py new file mode 100644 index 0000000..f897418 --- /dev/null +++ b/sources/searx/engines/yahoo.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python + +from urllib import urlencode +from urlparse import unquote +from lxml import html +from searx.engines.xpath import extract_text, extract_url + +categories = ['general'] +search_url = 'http://search.yahoo.com/search?{query}&b={offset}' +results_xpath = '//div[@class="res"]' +url_xpath = './/h3/a/@href' +title_xpath = './/h3/a' +content_xpath = './/div[@class="abstr"]' +suggestion_xpath = '//div[@id="satat"]//a' + +paging = True + + +def parse_url(url_string): + endings = ['/RS', '/RK'] + endpositions = [] + start = url_string.find('http', url_string.find('/RU=')+1) + for ending in endings: + endpos = url_string.rfind(ending) + if endpos > -1: + endpositions.append(endpos) + + end = min(endpositions) + return unquote(url_string[start:end]) + + +def request(query, params): + offset = (params['pageno'] - 1) * 10 + 1 + if params['language'] == 'all': + language = 'en' + else: + language = params['language'].split('_')[0] + params['url'] = search_url.format(offset=offset, + query=urlencode({'p': query})) + params['cookies']['sB'] = 'fl=1&vl=lang_{lang}&sh=1&rw=new&v=1'\ + .format(lang=language) + return params + + +def response(resp): + results = [] + dom = html.fromstring(resp.text) + + for result in dom.xpath(results_xpath): + try: + url = parse_url(extract_url(result.xpath(url_xpath), search_url)) + title = extract_text(result.xpath(title_xpath)[0]) + except: + continue + content = extract_text(result.xpath(content_xpath)[0]) + results.append({'url': url, 'title': title, 'content': content}) + + if not suggestion_xpath: + return results + + for suggestion in dom.xpath(suggestion_xpath): + results.append({'suggestion': extract_text(suggestion)}) + + return results diff --git a/sources/searx/engines/yahoo_news.py b/sources/searx/engines/yahoo_news.py new file mode 100644 index 0000000..43da93e --- /dev/null +++ b/sources/searx/engines/yahoo_news.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python + +from urllib import urlencode +from lxml import html +from searx.engines.xpath import extract_text, extract_url +from searx.engines.yahoo import parse_url +from datetime import datetime, timedelta +import re +from dateutil import parser + +categories = ['news'] +search_url = 'http://news.search.yahoo.com/search?{query}&b={offset}' +results_xpath = '//div[@class="res"]' +url_xpath = './/h3/a/@href' +title_xpath = './/h3/a' +content_xpath = './/div[@class="abstr"]' +publishedDate_xpath = './/span[@class="timestamp"]' +suggestion_xpath = '//div[@id="satat"]//a' + +paging = True + + +def request(query, params): + offset = (params['pageno'] - 1) * 10 + 1 + if params['language'] == 'all': + language = 'en' + else: + language = params['language'].split('_')[0] + params['url'] = search_url.format(offset=offset, + query=urlencode({'p': query})) + params['cookies']['sB'] = 'fl=1&vl=lang_{lang}&sh=1&rw=new&v=1'\ + .format(lang=language) + return params + + +def response(resp): + results = [] + dom = html.fromstring(resp.text) + + for result in dom.xpath(results_xpath): + url = parse_url(extract_url(result.xpath(url_xpath), search_url)) + title = extract_text(result.xpath(title_xpath)[0]) + content = extract_text(result.xpath(content_xpath)[0]) + publishedDate = extract_text(result.xpath(publishedDate_xpath)[0]) + + if re.match("^[0-9]+ minute(s|) ago$", publishedDate): + publishedDate = datetime.now() - timedelta(minutes=int(re.match(r'\d+', publishedDate).group())) # noqa + else: + if re.match("^[0-9]+ hour(s|), [0-9]+ minute(s|) ago$", + publishedDate): + timeNumbers = re.findall(r'\d+', publishedDate) + publishedDate = datetime.now()\ + - timedelta(hours=int(timeNumbers[0]))\ + - timedelta(minutes=int(timeNumbers[1])) + else: + publishedDate = parser.parse(publishedDate) + + if publishedDate.year == 1900: + publishedDate = publishedDate.replace(year=datetime.now().year) + + results.append({'url': url, + 'title': title, + 'content': content, + 'publishedDate': publishedDate}) + + if not suggestion_xpath: + return results + + for suggestion in dom.xpath(suggestion_xpath): + results.append({'suggestion': extract_text(suggestion)}) + + return results diff --git a/sources/searx/engines/youtube.py b/sources/searx/engines/youtube.py new file mode 100644 index 0000000..895b559 --- /dev/null +++ b/sources/searx/engines/youtube.py @@ -0,0 +1,59 @@ +from json import loads +from urllib import urlencode +from dateutil import parser + +categories = ['videos'] + +search_url = ('https://gdata.youtube.com/feeds/api/videos' + '?alt=json&{query}&start-index={index}&max-results=25') # noqa + +paging = True + + +def request(query, params): + index = (params['pageno'] - 1) * 25 + 1 + params['url'] = search_url.format(query=urlencode({'q': query}), + index=index) + return params + + +def response(resp): + results = [] + search_results = loads(resp.text) + if not 'feed' in search_results: + return results + feed = search_results['feed'] + + for result in feed['entry']: + url = [x['href'] for x in result['link'] if x['type'] == 'text/html'] + if not url: + return + # remove tracking + url = url[0].replace('feature=youtube_gdata', '') + if url.endswith('&'): + url = url[:-1] + title = result['title']['$t'] + content = '' + thumbnail = '' + +#"2013-12-31T15:22:51.000Z" + pubdate = result['published']['$t'] + publishedDate = parser.parse(pubdate) + + if result['media$group']['media$thumbnail']: + thumbnail = result['media$group']['media$thumbnail'][0]['url'] + content += ''.format(url, thumbnail) # noqa + + if content: + content += '
' + result['content']['$t'] + else: + content = result['content']['$t'] + + results.append({'url': url, + 'title': title, + 'content': content, + 'template': 'videos.html', + 'publishedDate': publishedDate, + 'thumbnail': thumbnail}) + + return results diff --git a/sources/searx/languages.py b/sources/searx/languages.py new file mode 100644 index 0000000..8b12e5f --- /dev/null +++ b/sources/searx/languages.py @@ -0,0 +1,59 @@ +language_codes = ( + ("ar_XA", "Arabic", "Arabia"), + ("bg_BG", "Bulgarian", "Bulgaria"), + ("cs_CZ", "Czech", "Czech Republic"), + ("de_DE", "German", "Germany"), + ("da_DK", "Danish", "Denmark"), + ("de_AT", "German", "Austria"), + ("de_CH", "German", "Switzerland"), + ("el_GR", "Greek", "Greece"), + ("en_AU", "English", "Australia"), + ("en_CA", "English", "Canada"), + ("en_GB", "English", "United Kingdom"), + ("en_ID", "English", "Indonesia"), + ("en_IE", "English", "Ireland"), + ("en_IN", "English", "India"), + ("en_MY", "English", "Malaysia"), + ("en_NZ", "English", "New Zealand"), + ("en_PH", "English", "Philippines"), + ("en_SG", "English", "Singapore"), + ("en_US", "English", "United States"), + ("en_XA", "English", "Arabia"), + ("en_ZA", "English", "South Africa"), + ("es_AR", "Spanish", "Argentina"), + ("es_CL", "Spanish", "Chile"), + ("es_ES", "Spanish", "Spain"), + ("es_MX", "Spanish", "Mexico"), + ("es_US", "Spanish", "United States"), + ("es_XL", "Spanish", "Latin America"), + ("et_EE", "Estonian", "Estonia"), + ("fi_FI", "Finnish", "Finland"), + ("fr_BE", "French", "Belgium"), + ("fr_CA", "French", "Canada"), + ("fr_CH", "French", "Switzerland"), + ("fr_FR", "French", "France"), + ("he_IL", "Hebrew", "Israel"), + ("hr_HR", "Croatian", "Croatia"), + ("hu_HU", "Hungarian", "Hungary"), + ("it_IT", "Italian", "Italy"), + ("ja_JP", "Japanese", "Japan"), + ("ko_KR", "Korean", "Korea"), + ("lt_LT", "Lithuanian", "Lithuania"), + ("lv_LV", "Latvian", "Latvia"), + ("nb_NO", "Norwegian", "Norway"), + ("nl_BE", "Dutch", "Belgium"), + ("nl_NL", "Dutch", "Netherlands"), + ("pl_PL", "Polish", "Poland"), + ("pt_BR", "Portuguese", "Brazil"), + ("pt_PT", "Portuguese", "Portugal"), + ("ro_RO", "Romanian", "Romania"), + ("ru_RU", "Russian", "Russia"), + ("sk_SK", "Slovak", "Slovak Republic"), + ("sl_SL", "Slovenian", "Slovenia"), + ("sv_SE", "Swedish", "Sweden"), + ("th_TH", "Thai", "Thailand"), + ("tr_TR", "Turkish", "Turkey"), + ("uk_UA", "Ukrainian", "Ukraine"), + ("zh_CN", "Chinese", "China"), + ("zh_HK", "Chinese", "Hong Kong SAR"), + ("zh_TW", "Chinese", "Taiwan")) diff --git a/sources/searx/search.py b/sources/searx/search.py new file mode 100644 index 0000000..7f99104 --- /dev/null +++ b/sources/searx/search.py @@ -0,0 +1,114 @@ +from searx.engines import ( + categories, engines, engine_shortcuts +) +from searx.languages import language_codes + + +class Search(object): + + """Search information container""" + + def __init__(self, request): + super(Search, self).__init__() + self.query = None + self.engines = [] + self.categories = [] + self.paging = False + self.pageno = 1 + self.lang = 'all' + if request.cookies.get('blocked_engines'): + self.blocked_engines = request.cookies['blocked_engines'].split(',') # noqa + else: + self.blocked_engines = [] + self.results = [] + self.suggestions = [] + self.request_data = {} + + if request.cookies.get('language')\ + and request.cookies['language'] in (x[0] for x in language_codes): + self.lang = request.cookies['language'] + + if request.method == 'POST': + self.request_data = request.form + else: + self.request_data = request.args + + # TODO better exceptions + if not self.request_data.get('q'): + raise Exception('noquery') + + self.query = self.request_data['q'] + + pageno_param = self.request_data.get('pageno', '1') + if not pageno_param.isdigit() or int(pageno_param) < 1: + raise Exception('wrong pagenumber') + + self.pageno = int(pageno_param) + + self.parse_query() + + self.categories = [] + + if self.engines: + self.categories = list(set(engine['category'] + for engine in self.engines)) + else: + for pd_name, pd in self.request_data.items(): + if pd_name.startswith('category_'): + category = pd_name[9:] + if not category in categories: + continue + self.categories.append(category) + if not self.categories: + cookie_categories = request.cookies.get('categories', '') + cookie_categories = cookie_categories.split(',') + for ccateg in cookie_categories: + if ccateg in categories: + self.categories.append(ccateg) + if not self.categories: + self.categories = ['general'] + + for categ in self.categories: + self.engines.extend({'category': categ, + 'name': x.name} + for x in categories[categ] + if not x.name in self.blocked_engines) + + def parse_query(self): + query_parts = self.query.split() + modified = False + if query_parts[0].startswith(':'): + lang = query_parts[0][1:].lower() + + for lc in language_codes: + lang_id, lang_name, country = map(str.lower, lc) + if lang == lang_id\ + or lang_id.startswith(lang)\ + or lang == lang_name\ + or lang == country: + self.lang = lang + modified = True + break + + elif query_parts[0].startswith('!'): + prefix = query_parts[0][1:].replace('_', ' ') + + if prefix in engine_shortcuts\ + and not engine_shortcuts[prefix] in self.blocked_engines: + modified = True + self.engines.append({'category': 'none', + 'name': engine_shortcuts[prefix]}) + elif prefix in engines\ + and not prefix in self.blocked_engines: + modified = True + self.engines.append({'category': 'none', + 'name': prefix}) + elif prefix in categories: + modified = True + self.engines.extend({'category': prefix, + 'name': engine.name} + for engine in categories[prefix] + if not engine in self.blocked_engines) + if modified: + self.query = self.query.replace(query_parts[0], '', 1).strip() + self.parse_query() diff --git a/sources/searx/settings.yml b/sources/searx/settings.yml new file mode 100644 index 0000000..eac7593 --- /dev/null +++ b/sources/searx/settings.yml @@ -0,0 +1,156 @@ +server: + port : 8888 + secret_key : "ultrasecretkey" # change this! + debug : True + request_timeout : 2.0 # seconds + base_url : False + +engines: + - name : wikipedia + engine : wikipedia + number_of_results : 1 + paging : False + shortcut : wp + + - name : bing + engine : bing + locale : en-US + shortcut : bi + + - name : bing news + engine : bing_news + locale : en-US + shortcut : bin + + - name : currency + engine : currency_convert + categories : general + shortcut : cc + + - name : deviantart + engine : deviantart + categories : images + shortcut : da + timeout: 3.0 + + - name : ddg definitions + engine : duckduckgo_definitions + shortcut : ddd + + - name : duckduckgo + engine : duckduckgo + locale : en-us + shortcut : ddg + + - name : filecrop + engine : filecrop + categories : files + shortcut : fc + + - name : flickr + engine : flickr + categories : images + shortcut : fl + timeout: 3.0 + + - name : github + engine : github + categories : it + shortcut : gh + + - name : google + engine : google + shortcut : go + + - name : google images + engine : google_images + shortcut : goi + + - name : google news + engine : google_news + shortcut : gon + + - name : piratebay + engine : piratebay + categories : videos, music, files + shortcut : tpb + + - name : soundcloud + engine : soundcloud + categories : music + shortcut : sc + + - name : stackoverflow + engine : stackoverflow + categories : it + shortcut : st + + - name : startpage + engine : startpage + base_url : 'https://startpage.com/' + search_url : 'https://startpage.com/do/search' + shortcut : sp + +# +30% page load time +# - name : ixquick +# engine : startpage +# base_url : 'https://www.ixquick.com/' +# search_url : 'https://www.ixquick.com/do/search' + + - name : twitter + engine : twitter + categories : social media + shortcut : tw + +# maybe in a fun category +# - name : uncyclopedia +# engine : mediawiki +# categories : general +# shortcut : unc +# url : https://uncyclopedia.wikia.com/ + +# tmp suspended - too slow, too many errors +# - name : urbandictionary +# engine : xpath +# search_url : http://www.urbandictionary.com/define.php?term={query} +# url_xpath : //div[@class="word"]//a/@href +# title_xpath : //div[@class="word"]//a +# content_xpath : //div[@class="definition"] +# shortcut : ud + + - name : yahoo + engine : yahoo + shortcut : yh + + - name : yahoo news + engine : yahoo_news + shortcut : yhn + + - name : youtube + engine : youtube + categories : videos + shortcut : yt + + - name : dailymotion + engine : dailymotion + locale : en_US + categories : videos + shortcut : dm + + - name : vimeo + engine : vimeo + categories : videos + results_xpath : //div[@id="browse_content"]/ol/li + url_xpath : ./a/@href + title_xpath : ./a/div[@class="data"]/p[@class="title"]/text() + content_xpath : ./a/img/@src + shortcut : vm + +locales: + en : English + de : Deutsch + hu : Magyar + fr : Français + es : Español + it : Italiano + nl : Nederlands diff --git a/sources/searx/settings_robot.yml b/sources/searx/settings_robot.yml new file mode 100644 index 0000000..98944a8 --- /dev/null +++ b/sources/searx/settings_robot.yml @@ -0,0 +1,19 @@ +server: + port : 11111 + secret_key : "ultrasecretkey" # change this! + debug : False + request_timeout : 3.0 # seconds + base_url: False + +engines: + - name : general_dummy + engine : dummy + categories : general + + - name : dummy_dummy + engine : dummy + categories : dummy + +locales: + en : English + hu : Magyar diff --git a/sources/searx/static/css/style.css b/sources/searx/static/css/style.css new file mode 100644 index 0000000..69190b2 --- /dev/null +++ b/sources/searx/static/css/style.css @@ -0,0 +1,74 @@ +html{font-family:sans-serif;font-size:.9em;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;color:#444;padding:0;margin:0} +body,#container{padding:0;margin:0} +#container{width:100%;position:absolute;top:0} +.search{padding:0;margin:0}.search .checkbox_container label{font-size:.9em;border-bottom:2px solid #e8e7e6} +.search .checkbox_container label:hover{border-bottom:2px solid #3498db} +.search .checkbox_container input[type="checkbox"]:checked+label{border-bottom:2px solid #2980b9} +#search_wrapper{position:relative;width:50em;padding:10px} +.center #search_wrapper{margin-left:auto;margin-right:auto} +.q{background:none repeat scroll 0 0 #fff;border:1px solid #3498db;color:#222;font-size:16px;height:28px;margin:0;outline:medium none;padding:2px;padding-left:8px;padding-right:0 !important;width:100%;z-index:2} +#search_submit{position:absolute;top:13px;right:1px;padding:0;border:0;background:url('../img/search-icon.png') no-repeat;background-size:24px 24px;opacity:.8;width:24px;height:30px;font-size:0} +@media screen and (max-width:50em){#search_wrapper{width:90%;clear:both;overflow:hidden}}ul.autocompleter-choices{position:absolute;margin:0;padding:0;list-style:none;border:1px solid #3498db;border-left-color:#3498db;border-right-color:#3498db;border-bottom-color:#3498db;text-align:left;font-family:Verdana,Geneva,Arial,Helvetica,sans-serif;z-index:50;background-color:#fff;color:#444}ul.autocompleter-choices li{position:relative;margin:-2px 0 0 0;padding:.2em 1.5em .2em 1em;display:block;float:none !important;cursor:pointer;font-weight:normal;white-space:nowrap;font-size:1em;line-height:1.5em}ul.autocompleter-choices li.autocompleter-selected{background-color:#444;color:#fff}ul.autocompleter-choices li.autocompleter-selected span.autocompleter-queried{color:#9fcfff} +ul.autocompleter-choices span.autocompleter-queried{display:inline;float:none;font-weight:bold;margin:0;padding:0} +.row{max-width:800px;margin:20px auto;text-align:justify}.row h1{font-size:3em;margin-top:50px} +.row p{padding:0 10px;max-width:700px} +.row h3,.row ul{margin:4px 8px} +.hmarg{margin:0 20px;border:1px solid #3498db;padding:4px 10px} +a:link.hmarg{color:#3498db} +a:visited.hmarg{color:#3498db} +a:active.hmarg{color:#3498db} +a:hover.hmarg{color:#3498db} +.top_margin{margin-top:60px} +.center{text-align:center} +h1{font-size:5em} +div.title{background:url('../img/searx.png') no-repeat;width:100%;background-position:center}div.title h1{visibility:hidden} +input[type="submit"]{padding:2px 6px;margin:2px 4px;display:inline-block;background:#3498db;color:#fff;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;border:0;cursor:pointer} +input[type="checkbox"]{visibility:hidden} +fieldset{margin:8px;border:1px solid #3498db} +#categories{margin:0 10px} +.checkbox_container{display:inline-block;position:relative;margin:0 3px;padding:0}.checkbox_container input{display:none} +.checkbox_container label,.engine_checkbox label{cursor:pointer;padding:4px 10px;margin:0;display:block;text-transform:capitalize;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} +.checkbox_container input[type="checkbox"]:checked+label{background:#3498db;color:#fff} +.engine_checkbox{padding:4px} +label.allow{background:#e74c3c;padding:4px 8px;color:#fff;display:none} +label.deny{background:#2ecc71;padding:4px 8px;color:#444;display:inline} +.engine_checkbox input[type="checkbox"]:checked+label:nth-child(2)+label{display:none} +.engine_checkbox input[type="checkbox"]:checked+label.allow{display:inline} +a{text-decoration:none;color:#1a11be}a:visited{color:#8e44ad} +.result{margin:19px 0 18px 0;padding:0;clear:both} +.result_title{margin-bottom:0}.result_title a{color:#2980b9;font-weight:normal;font-size:1.1em}.result_title a:hover{text-decoration:underline} +.result_title a:visited{color:#8e44ad} +.result h3{font-size:1em;word-wrap:break-word;margin:5px 0 1px 0;padding:0} +.result .content{font-size:.8em;margin:0;padding:0;max-width:54em;word-wrap:break-word;line-height:1.24} +.result .url{font-size:.8em;margin:3px 0 0 0;padding:0;max-width:54em;word-wrap:break-word;color:#c0392b} +.result .published_date{font-size:.8em;color:#888;margin:5px 20px} +.engines{color:#888} +.small_font{font-size:.8em} +.small p{margin:2px 0} +.right{float:right} +.invisible{display:none} +.left{float:left} +.highlight{color:#094089} +.content .highlight{color:#000} +.image_result{float:left;margin:10px 10px;position:relative;height:160px}.image_result img{border:0;height:160px} +.image_result p{margin:0;padding:0}.image_result p span a{display:none;color:#fff} +.image_result p:hover span a{display:block;position:absolute;bottom:0;right:0;padding:4px;background-color:rgba(0,0,0,0.6);font-size:.7em} +.torrent_result{border-left:10px solid #d3d3d3;padding-left:3px}.torrent_result p{margin:3px;font-size:.8em} +.definition_result{border-left:10px solid #808080;padding-left:3px} +.percentage{position:relative;width:300px}.percentage div{background:#444} +table{width:100%} +td{padding:0 4px} +tr:hover{background:#ddd} +#results{margin:auto;padding:0;width:50em;margin-bottom:20px} +#sidebar{position:absolute;top:100px;right:10px;margin:0 2px 5px 5px;padding:0 2px 2px 2px;width:14em}#sidebar input{padding:0;margin:3px;font-size:.8em;display:inline-block;background:transparent;color:#444;cursor:pointer} +#sidebar input[type="submit"]{text-decoration:underline} +#suggestions{margin-top:20px}#suggestions span{display:inline;margin:0 2px 2px 2px;padding:0} +#suggestions input{padding:0;margin:3px;font-size:.8em;display:inline-block;background:transparent;color:#444;cursor:pointer} +#suggestions input[type="submit"]{text-decoration:underline} +#suggestions form{display:inline} +#search_url{margin-top:8px}#search_url input{border:1px solid #888;padding:4px;color:#444;width:14em;display:block;margin:4px;font-size:.8em} +#preferences{top:10px;padding:0;border:0;background:url('../img/preference-icon.png') no-repeat;background-size:28px 28px;opacity:.8;width:28px;height:30px;display:block}#preferences *{display:none} +#pagination{clear:both;width:40em} +#apis{margin-top:8px;clear:both} +@media screen and (max-width:50em){#categories{font-size:90%;clear:both}#categories .checkbox_container{margin-top:2px;margin:auto} #results{margin:auto;padding:0;width:90%} .checkbox_container{display:block;width:90%}.checkbox_container label{border-bottom:0}}@media screen and (max-width:70em){.right{display:none;postion:fixed !important;top:100px;right:0} #sidebar{position:static;max-width:50em;margin:0 0 2px 0;padding:0;float:none;border:none;width:auto}#sidebar input{border:0} #apis{display:none} #search_url{display:none} .result{border-top:1px solid #e8e7e6;margin:7px 0 6px 0}.result img{max-width:90%;width:auto;height:auto}}.favicon{float:left;margin-right:4px;margin-top:2px} +.preferences_back{background:none repeat scroll 0 0 #3498db;border:0 none;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;cursor:pointer;display:inline-block;margin:2px 4px;padding:4px 6px}.preferences_back a{color:#fff} diff --git a/sources/searx/static/img/favicon.png b/sources/searx/static/img/favicon.png new file mode 100644 index 0000000..cefbac4 Binary files /dev/null and b/sources/searx/static/img/favicon.png differ diff --git a/sources/searx/static/img/github_ribbon.png b/sources/searx/static/img/github_ribbon.png new file mode 100644 index 0000000..146ef8a Binary files /dev/null and b/sources/searx/static/img/github_ribbon.png differ diff --git a/sources/searx/static/img/icon_github.ico b/sources/searx/static/img/icon_github.ico new file mode 100644 index 0000000..133f0ca Binary files /dev/null and b/sources/searx/static/img/icon_github.ico differ diff --git a/sources/searx/static/img/icon_soundcloud.ico b/sources/searx/static/img/icon_soundcloud.ico new file mode 100644 index 0000000..4130bea Binary files /dev/null and b/sources/searx/static/img/icon_soundcloud.ico differ diff --git a/sources/searx/static/img/icon_stackoverflow.ico b/sources/searx/static/img/icon_stackoverflow.ico new file mode 100644 index 0000000..b2242bc Binary files /dev/null and b/sources/searx/static/img/icon_stackoverflow.ico differ diff --git a/sources/searx/static/img/icon_twitter.ico b/sources/searx/static/img/icon_twitter.ico new file mode 100644 index 0000000..b4a7169 Binary files /dev/null and b/sources/searx/static/img/icon_twitter.ico differ diff --git a/sources/searx/static/img/icon_vimeo.ico b/sources/searx/static/img/icon_vimeo.ico new file mode 100644 index 0000000..4fe4336 Binary files /dev/null and b/sources/searx/static/img/icon_vimeo.ico differ diff --git a/sources/searx/static/img/icon_wikipedia.ico b/sources/searx/static/img/icon_wikipedia.ico new file mode 100644 index 0000000..911fa76 Binary files /dev/null and b/sources/searx/static/img/icon_wikipedia.ico differ diff --git a/sources/searx/static/img/icon_youtube.ico b/sources/searx/static/img/icon_youtube.ico new file mode 100644 index 0000000..977887d Binary files /dev/null and b/sources/searx/static/img/icon_youtube.ico differ diff --git a/sources/searx/static/img/preference-icon.png b/sources/searx/static/img/preference-icon.png new file mode 100644 index 0000000..f746357 Binary files /dev/null and b/sources/searx/static/img/preference-icon.png differ diff --git a/sources/searx/static/img/search-icon.png b/sources/searx/static/img/search-icon.png new file mode 100644 index 0000000..1222421 Binary files /dev/null and b/sources/searx/static/img/search-icon.png differ diff --git a/sources/searx/static/img/searx.png b/sources/searx/static/img/searx.png new file mode 100644 index 0000000..e162da5 Binary files /dev/null and b/sources/searx/static/img/searx.png differ diff --git a/sources/searx/static/img/searx_logo.svg b/sources/searx/static/img/searx_logo.svg new file mode 100644 index 0000000..67a2d45 --- /dev/null +++ b/sources/searx/static/img/searx_logo.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/sources/searx/static/js/mootools-autocompleter-1.1.2-min.js b/sources/searx/static/js/mootools-autocompleter-1.1.2-min.js new file mode 100644 index 0000000..364e611 --- /dev/null +++ b/sources/searx/static/js/mootools-autocompleter-1.1.2-min.js @@ -0,0 +1,2 @@ +/*https://github.com/angelsk/mootools-autocompleter*/ +var Autocompleter=new Class({Implements:[Options,Events],options:{minLength:1,markQuery:true,width:"inherit",maxChoices:10,injectChoice:null,customChoices:null,emptyChoices:null,visibleChoices:true,className:"autocompleter-choices",zIndex:42,delay:400,observerOptions:{},fxOptions:{},autoSubmit:false,overflow:false,overflowMargin:25,selectFirst:false,filter:null,filterCase:false,filterSubset:false,forceSelect:false,selectMode:true,choicesMatch:null,multiple:false,separator:", ",separatorSplit:/\s*[,;]\s*/,autoTrim:false,allowDupes:false,cache:true,relative:false},initialize:function(b,a){this.element=$(b);this.setOptions(a);this.build();this.observer=new Observer(this.element,this.prefetch.bind(this),Object.merge({delay:this.options.delay},this.options.observerOptions));this.queryValue=null;if(this.options.filter){this.filter=this.options.filter.bind(this)}var c=this.options.selectMode;this.typeAhead=(c=="type-ahead");this.selectMode=(c===true)?"selection":c;this.cached=[]},build:function(){if($(this.options.customChoices)){this.choices=this.options.customChoices}else{this.choices=new Element("ul",{"class":this.options.className,styles:{zIndex:this.options.zIndex}}).inject(document.body);this.relative=false;if(this.options.relative){this.choices.inject(this.element,"after");this.relative=this.element.getOffsetParent()}this.fix=new OverlayFix(this.choices)}if(!this.options.separator.test(this.options.separatorSplit)){this.options.separatorSplit=this.options.separator}this.fx=(!this.options.fxOptions)?null:new Fx.Tween(this.choices,Object.merge({property:"opacity",link:"cancel",duration:200},this.options.fxOptions)).addEvent("onStart",Chain.prototype.clearChain).set(0);this.element.setProperty("autocomplete","off").addEvent((Browser.ie||Browser.safari||Browser.chrome)?"keydown":"keypress",this.onCommand.bind(this)).addEvent("click",this.onCommand.bind(this,false)).addEvent("focus",this.toggleFocus.bind(this,true)).addEvent("blur",this.toggleFocus.bind(this,false))},destroy:function(){if(this.fix){this.fix.destroy()}this.choices=this.selected=this.choices.destroy()},toggleFocus:function(a){this.focussed=a;if(!a){this.hideChoices(true)}this.fireEvent((a)?"onFocus":"onBlur",[this.element])},onCommand:function(b){if(!b&&this.focussed){return this.prefetch()}if(b&&b.key&&!b.shift){switch(b.key){case"enter":if(this.element.value!=this.opted){return true}if(this.selected&&this.visible){this.choiceSelect(this.selected);return !!(this.options.autoSubmit)}break;case"up":case"down":if(!this.prefetch()&&this.queryValue!==null){var a=(b.key=="up");this.choiceOver((this.selected||this.choices)[(this.selected)?((a)?"getPrevious":"getNext"):((a)?"getLast":"getFirst")](this.options.choicesMatch),true)}return false;case"esc":case"tab":this.hideChoices(true);break}}return true},setSelection:function(f){var g=this.selected.inputValue,h=g;var a=this.queryValue.length,c=g.length;if(g.substr(0,a).toLowerCase()!=this.queryValue.toLowerCase()){a=0}if(this.options.multiple){var e=this.options.separatorSplit;h=this.element.value;a+=this.queryIndex;c+=this.queryIndex;var b=h.substr(this.queryIndex).split(e,1)[0];h=h.substr(0,this.queryIndex)+g+h.substr(this.queryIndex+b.length);if(f){var d=h.split(this.options.separatorSplit).filter(function(j){return this.test(j)},/[^\s,]+/);if(!this.options.allowDupes){d=[].combine(d)}var i=this.options.separator;h=d.join(i)+i;c=h.length}}this.observer.setValue(h);this.opted=h;if(f||this.selectMode=="pick"){a=c}this.element.selectRange(a,c);this.fireEvent("onSelection",[this.element,this.selected,h,g])},showChoices:function(){var c=this.options.choicesMatch,b=this.choices.getFirst(c);this.selected=this.selectedValue=null;if(this.fix){var e=this.element.getCoordinates(this.relative),a=this.options.width||"auto";this.choices.setStyles({left:e.left,top:e.bottom,width:(a===true||a=="inherit")?e.width:a})}if(!b){return}if(!this.visible){this.visible=true;this.choices.setStyle("display","");if(this.fx){this.fx.start(1)}this.fireEvent("onShow",[this.element,this.choices])}if(this.options.selectFirst||this.typeAhead||b.inputValue==this.queryValue){this.choiceOver(b,this.typeAhead)}var d=this.choices.getChildren(c),f=this.options.maxChoices;var i={overflowY:"hidden",height:""};this.overflown=false;if(d.length>f){var j=d[f-1];i.overflowY="scroll";i.height=j.getCoordinates(this.choices).bottom;this.overflown=true}this.choices.setStyles(i);this.fix.show();if(this.options.visibleChoices){var h=document.getScroll(),k=document.getSize(),g=this.choices.getCoordinates();if(g.right>h.x+k.x){h.x=g.right-k.x}if(g.bottom>h.y+k.y){h.y=g.bottom-k.y}window.scrollTo(Math.min(h.x,g.left),Math.min(h.y,g.top))}},hideChoices:function(a){if(a){var c=this.element.value;if(this.options.forceSelect){c=this.opted}if(this.options.autoTrim){c=c.split(this.options.separatorSplit).filter(arguments[0]).join(this.options.separator)}this.observer.setValue(c)}if(!this.visible){return}this.visible=false;if(this.selected){this.selected.removeClass("autocompleter-selected")}this.observer.clear();var b=function(){this.choices.setStyle("display","none");this.fix.hide()}.bind(this);if(this.fx){this.fx.start(0).chain(b)}else{b()}this.fireEvent("onHide",[this.element,this.choices])},prefetch:function(){var f=this.element.value,e=f;if(this.options.multiple){var c=this.options.separatorSplit;var a=f.split(c);var b=this.element.getSelectedRange().start;var g=f.substr(0,b).split(c);var d=g.length-1;b-=g[d].length;e=a[d]}if(e.length=this.options.maxChoices||this.queryValue){return false}this.update(this.filter(this.cached));return true},update:function(b){this.choices.empty();this.cached=b;var a=b&&typeOf(b);if(!a||(a=="array"&&!b.length)||(a=="hash"&&!b.getLength())){(this.options.emptyChoices||this.hideChoices).call(this)}else{if(this.options.maxChoicesb){this.choices.scrollTop=Math.min(f.bottom-a+e,b)}}}if(this.selectMode){this.setSelection()}},choiceSelect:function(a){if(a){this.choiceOver(a)}this.setSelection(true);this.queryValue=false;this.hideChoices()},filter:function(a){return(a||this.tokens).filter(function(b){return this.test(b)},new RegExp(((this.options.filterSubset)?"":"^")+this.queryValue.escapeRegExp(),(this.options.filterCase)?"":"i"))},markQueryValue:function(a){if(!a){return}return(!this.options.markQuery||!this.queryValue)?a:a.replace(new RegExp("("+((this.options.filterSubset)?"":"^")+this.queryValue.escapeRegExp()+")",(this.options.filterCase)?"":"i"),'$1')},addChoiceEvents:function(a){return a.addEvents({mouseover:this.choiceOver.bind(this,a),click:this.choiceSelect.bind(this,a)})}});var OverlayFix=new Class({initialize:function(a){if(Browser.ie){this.element=$(a);this.relative=this.element.getOffsetParent();this.fix=new Element("iframe",{frameborder:"0",scrolling:"no",src:"javascript:false;",styles:{position:"absolute",border:"none",display:"none",filter:"progid:DXImageTransform.Microsoft.Alpha(opacity=0)"}}).inject(this.element,"after")}},show:function(){if(this.fix){var a=this.element.getCoordinates(this.relative);delete a.right;delete a.bottom;this.fix.setStyles(Object.append(a,{display:"",zIndex:(this.element.getStyle("zIndex")||1)-1}))}return this},hide:function(){if(this.fix){this.fix.setStyle("display","none")}return this},destroy:function(){if(this.fix){this.fix=this.fix.destroy()}}});Element.implement({getSelectedRange:function(){if(!Browser.ie){return{start:this.selectionStart,end:this.selectionEnd}}var e={start:0,end:0};var a=this.getDocument().selection.createRange();if(!a||a.parentElement()!=this){return e}var c=a.duplicate();if(this.type=="text"){e.start=0-c.moveStart("character",-100000);e.end=e.start+a.text.length}else{var b=this.value;var d=b.length-b.match(/[\n\r]*$/)[0].length;c.moveToElementText(this);c.setEndPoint("StartToEnd",a);e.end=d-c.text.length;c.setEndPoint("StartToStart",a);e.start=d-c.text.length}return e},selectRange:function(d,a){if(Browser.ie){var c=this.value.substr(d,a-d).replace(/\r/g,"").length;d=this.value.substr(0,d).replace(/\r/g,"").length;var b=this.createTextRange();b.collapse(true);b.moveEnd("character",d+c);b.moveStart("character",d);b.select()}else{this.focus();this.setSelectionRange(d,a)}return this}});Autocompleter.Base=Autocompleter;Autocompleter.Request=new Class({Extends:Autocompleter,options:{postData:{},ajaxOptions:{},postVar:"value"},query:function(){var c=Object.clone(this.options.postData)||{};c[this.options.postVar]=this.queryValue;var b=$(this.options.indicator);if(b){b.setStyle("display","")}var a=this.options.indicatorClass;if(a){this.element.addClass(a)}this.fireEvent("onRequest",[this.element,this.request,c,this.queryValue]);this.request.send({data:c})},queryResponse:function(){var b=$(this.options.indicator);if(b){b.setStyle("display","none")}var a=this.options.indicatorClass;if(a){this.element.removeClass(a)}return this.fireEvent("onComplete",[this.element,this.request])}});Autocompleter.Request.JSON=new Class({Extends:Autocompleter.Request,initialize:function(c,b,a){this.parent(c,a);this.request=new Request.JSON(Object.merge({url:b,link:"cancel"},this.options.ajaxOptions)).addEvent("onComplete",this.queryResponse.bind(this))},queryResponse:function(a){this.parent();this.update(a)}});Autocompleter.Request.HTML=new Class({Extends:Autocompleter.Request,initialize:function(c,b,a){this.parent(c,a);this.request=new Request.HTML(Object.merge({url:b,link:"cancel",update:this.choices},this.options.ajaxOptions)).addEvent("onComplete",this.queryResponse.bind(this))},queryResponse:function(a,b){this.parent();if(!b||!b.length){this.hideChoices()}else{this.choices.getChildren(this.options.choicesMatch).each(this.options.injectChoice||function(c){var d=c.innerHTML;c.inputValue=d;this.addChoiceEvents(c.set("html",this.markQueryValue(d)))},this);this.showChoices()}}});Autocompleter.Ajax={Base:Autocompleter.Request,Json:Autocompleter.Request.JSON,Xhtml:Autocompleter.Request.HTML};var Observer=new Class({Implements:[Options,Events],options:{periodical:false,delay:1000},initialize:function(c,a,b){this.element=$(c)||$$(c);this.addEvent("onFired",a);this.setOptions(b);this.bound=this.changed.bind(this);this.resume()},changed:function(){var a=this.element.get("value");if($equals(this.value,a)){return}this.clear();this.value=a;this.timeout=this.onFired.delay(this.options.delay,this)},setValue:function(a){this.value=a;this.element.set("value",a);return this.clear()},onFired:function(){this.fireEvent("onFired",[this.value,this.element])},clear:function(){clearTimeout(this.timeout||null);return this},pause:function(){if(this.timer){clearInterval(this.timer)}else{this.element.removeEvent("keyup",this.bound)}return this.clear()},resume:function(){this.value=this.element.get("value");if(this.options.periodical){this.timer=this.changed.periodical(this.options.periodical,this)}else{this.element.addEvent("keyup",this.bound)}return this}});var $equals=function(b,a){return(b==a||JSON.encode(b)==JSON.encode(a))};Autocompleter.Local=new Class({Extends:Autocompleter,options:{minLength:0,delay:200},initialize:function(b,c,a){this.parent(b,a);this.tokens=c},query:function(){this.update(this.filter())}}); diff --git a/sources/searx/static/js/mootools-core-1.4.5-min.js b/sources/searx/static/js/mootools-core-1.4.5-min.js new file mode 100644 index 0000000..569473d --- /dev/null +++ b/sources/searx/static/js/mootools-core-1.4.5-min.js @@ -0,0 +1,491 @@ +/* +--- +MooTools: the javascript framework + +web build: + - http://mootools.net/core/76bf47062d6c1983d66ce47ad66aa0e0 + +packager build: + - packager build Core/Core Core/Array Core/String Core/Number Core/Function Core/Object Core/Event Core/Browser Core/Class Core/Class.Extras Core/Slick.Parser Core/Slick.Finder Core/Element Core/Element.Style Core/Element.Event Core/Element.Delegation Core/Element.Dimensions Core/Fx Core/Fx.CSS Core/Fx.Tween Core/Fx.Morph Core/Fx.Transitions Core/Request Core/Request.HTML Core/Request.JSON Core/Cookie Core/JSON Core/DOMReady Core/Swiff + +copyrights: + - [MooTools](http://mootools.net) + +licenses: + - [MIT License](http://mootools.net/license.txt) +... +*/ + +(function(){this.MooTools={version:"1.4.5",build:"ab8ea8824dc3b24b6666867a2c4ed58ebb762cf0"};var o=this.typeOf=function(i){if(i==null){return"null";}if(i.$family!=null){return i.$family(); +}if(i.nodeName){if(i.nodeType==1){return"element";}if(i.nodeType==3){return(/\S/).test(i.nodeValue)?"textnode":"whitespace";}}else{if(typeof i.length=="number"){if(i.callee){return"arguments"; +}if("item" in i){return"collection";}}}return typeof i;};var j=this.instanceOf=function(t,i){if(t==null){return false;}var s=t.$constructor||t.constructor; +while(s){if(s===i){return true;}s=s.parent;}if(!t.hasOwnProperty){return false;}return t instanceof i;};var f=this.Function;var p=true;for(var k in {toString:1}){p=null; +}if(p){p=["hasOwnProperty","valueOf","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","constructor"];}f.prototype.overloadSetter=function(s){var i=this; +return function(u,t){if(u==null){return this;}if(s||typeof u!="string"){for(var v in u){i.call(this,v,u[v]);}if(p){for(var w=p.length;w--;){v=p[w];if(u.hasOwnProperty(v)){i.call(this,v,u[v]); +}}}}else{i.call(this,u,t);}return this;};};f.prototype.overloadGetter=function(s){var i=this;return function(u){var v,t;if(typeof u!="string"){v=u;}else{if(arguments.length>1){v=arguments; +}else{if(s){v=[u];}}}if(v){t={};for(var w=0;w>>0; +b>>0;b>>0;for(var a=(d<0)?Math.max(0,b+d):d||0;a>>0,b=Array(d);for(var a=0;a>>0; +b-1:String(this).indexOf(a)>-1;},trim:function(){return String(this).replace(/^\s+|\s+$/g,""); +},clean:function(){return String(this).replace(/\s+/g," ").trim();},camelCase:function(){return String(this).replace(/-\D/g,function(a){return a.charAt(1).toUpperCase(); +});},hyphenate:function(){return String(this).replace(/[A-Z]/g,function(a){return("-"+a.charAt(0).toLowerCase());});},capitalize:function(){return String(this).replace(/\b[a-z]/g,function(a){return a.toUpperCase(); +});},escapeRegExp:function(){return String(this).replace(/([-.*+?^${}()|[\]\/\\])/g,"\\$1");},toInt:function(a){return parseInt(this,a||10);},toFloat:function(){return parseFloat(this); +},hexToRgb:function(b){var a=String(this).match(/^#?(\w{1,2})(\w{1,2})(\w{1,2})$/);return(a)?a.slice(1).hexToRgb(b):null;},rgbToHex:function(b){var a=String(this).match(/\d{1,3}/g); +return(a)?a.rgbToHex(b):null;},substitute:function(a,b){return String(this).replace(b||(/\\?\{([^{}]+)\}/g),function(d,c){if(d.charAt(0)=="\\"){return d.slice(1); +}return(a[c]!=null)?a[c]:"";});}});Number.implement({limit:function(b,a){return Math.min(a,Math.max(b,this));},round:function(a){a=Math.pow(10,a||0).toFixed(a<0?-a:0); +return Math.round(this*a)/a;},times:function(b,c){for(var a=0;a1?Array.slice(arguments,1):null,d=function(){};var c=function(){var g=e,h=arguments.length;if(this instanceof c){d.prototype=a.prototype; +g=new d;}var f=(!b&&!h)?a.call(g):a.apply(g,b&&h?b.concat(Array.slice(arguments)):b||arguments);return g==e?f:g;};return c;},pass:function(b,c){var a=this; +if(b!=null){b=Array.from(b);}return function(){return a.apply(c,b||arguments);};},delay:function(b,c,a){return setTimeout(this.pass((a==null?[]:a),c),b); +},periodical:function(c,b,a){return setInterval(this.pass((a==null?[]:a),b),c);}});(function(){var a=Object.prototype.hasOwnProperty;Object.extend({subset:function(d,g){var f={}; +for(var e=0,b=g.length;e]*>([\s\S]*?)<\/script>/gi,function(q,r){e+=r+"\n"; +return"";});if(o===true){n.exec(e);}else{if(typeOf(o)=="function"){o(e,p);}}return p;});n.extend({Document:this.Document,Window:this.Window,Element:this.Element,Event:this.Event}); +this.Window=this.$constructor=new Type("Window",function(){});this.$family=Function.from("window").hide();Window.mirror(function(e,o){g[e]=o;});this.Document=j.$constructor=new Type("Document",function(){}); +j.$family=Function.from("document").hide();Document.mirror(function(e,o){j[e]=o;});j.html=j.documentElement;if(!j.head){j.head=j.getElementsByTagName("head")[0]; +}if(j.execCommand){try{j.execCommand("BackgroundImageCache",false,true);}catch(f){}}if(this.attachEvent&&!this.addEventListener){var c=function(){this.detachEvent("onunload",c); +j.head=j.html=j.window=null;};this.attachEvent("onunload",c);}var l=Array.from;try{l(j.html.childNodes);}catch(f){Array.from=function(o){if(typeof o!="string"&&Type.isEnumerable(o)&&typeOf(o)!="array"){var e=o.length,p=new Array(e); +while(e--){p[e]=o[e];}return p;}return l(o);};var k=Array.prototype,m=k.slice;["pop","push","reverse","shift","sort","splice","unshift","concat","join","slice"].each(function(e){var o=k[e]; +Array[e]=function(p){return o.apply(Array.from(p),m.call(arguments,1));};});}})();(function(){var b={};var a=this.DOMEvent=new Type("DOMEvent",function(c,g){if(!g){g=window; +}c=c||g.event;if(c.$extended){return c;}this.event=c;this.$extended=true;this.shift=c.shiftKey;this.control=c.ctrlKey;this.alt=c.altKey;this.meta=c.metaKey; +var i=this.type=c.type;var h=c.target||c.srcElement;while(h&&h.nodeType==3){h=h.parentNode;}this.target=document.id(h);if(i.indexOf("key")==0){var d=this.code=(c.which||c.keyCode); +this.key=b[d];if(i=="keydown"){if(d>111&&d<124){this.key="f"+(d-111);}else{if(d>95&&d<106){this.key=d-96;}}}if(this.key==null){this.key=String.fromCharCode(d).toLowerCase(); +}}else{if(i=="click"||i=="dblclick"||i=="contextmenu"||i=="DOMMouseScroll"||i.indexOf("mouse")==0){var j=g.document;j=(!j.compatMode||j.compatMode=="CSS1Compat")?j.html:j.body; +this.page={x:(c.pageX!=null)?c.pageX:c.clientX+j.scrollLeft,y:(c.pageY!=null)?c.pageY:c.clientY+j.scrollTop};this.client={x:(c.pageX!=null)?c.pageX-g.pageXOffset:c.clientX,y:(c.pageY!=null)?c.pageY-g.pageYOffset:c.clientY}; +if(i=="DOMMouseScroll"||i=="mousewheel"){this.wheel=(c.wheelDelta)?c.wheelDelta/120:-(c.detail||0)/3;}this.rightClick=(c.which==3||c.button==2);if(i=="mouseover"||i=="mouseout"){var k=c.relatedTarget||c[(i=="mouseover"?"from":"to")+"Element"]; +while(k&&k.nodeType==3){k=k.parentNode;}this.relatedTarget=document.id(k);}}else{if(i.indexOf("touch")==0||i.indexOf("gesture")==0){this.rotation=c.rotation; +this.scale=c.scale;this.targetTouches=c.targetTouches;this.changedTouches=c.changedTouches;var f=this.touches=c.touches;if(f&&f[0]){var e=f[0];this.page={x:e.pageX,y:e.pageY}; +this.client={x:e.clientX,y:e.clientY};}}}}if(!this.client){this.client={};}if(!this.page){this.page={};}});a.implement({stop:function(){return this.preventDefault().stopPropagation(); +},stopPropagation:function(){if(this.event.stopPropagation){this.event.stopPropagation();}else{this.event.cancelBubble=true;}return this;},preventDefault:function(){if(this.event.preventDefault){this.event.preventDefault(); +}else{this.event.returnValue=false;}return this;}});a.defineKey=function(d,c){b[d]=c;return this;};a.defineKeys=a.defineKey.overloadSetter(true);a.defineKeys({"38":"up","40":"down","37":"left","39":"right","27":"esc","32":"space","8":"backspace","9":"tab","46":"delete","13":"enter"}); +})();(function(){var a=this.Class=new Type("Class",function(h){if(instanceOf(h,Function)){h={initialize:h};}var g=function(){e(this);if(g.$prototyping){return this; +}this.$caller=null;var i=(this.initialize)?this.initialize.apply(this,arguments):this;this.$caller=this.caller=null;return i;}.extend(this).implement(h); +g.$constructor=a;g.prototype.$constructor=g;g.prototype.parent=c;return g;});var c=function(){if(!this.$caller){throw new Error('The method "parent" cannot be called.'); +}var g=this.$caller.$name,h=this.$caller.$owner.parent,i=(h)?h.prototype[g]:null;if(!i){throw new Error('The method "'+g+'" has no parent.');}return i.apply(this,arguments); +};var e=function(g){for(var h in g){var j=g[h];switch(typeOf(j)){case"object":var i=function(){};i.prototype=j;g[h]=e(new i);break;case"array":g[h]=j.clone(); +break;}}return g;};var b=function(g,h,j){if(j.$origin){j=j.$origin;}var i=function(){if(j.$protected&&this.$caller==null){throw new Error('The method "'+h+'" cannot be called.'); +}var l=this.caller,m=this.$caller;this.caller=m;this.$caller=i;var k=j.apply(this,arguments);this.$caller=m;this.caller=l;return k;}.extend({$owner:g,$origin:j,$name:h}); +return i;};var f=function(h,i,g){if(a.Mutators.hasOwnProperty(h)){i=a.Mutators[h].call(this,i);if(i==null){return this;}}if(typeOf(i)=="function"){if(i.$hidden){return this; +}this.prototype[h]=(g)?i:b(this,h,i);}else{Object.merge(this.prototype,h,i);}return this;};var d=function(g){g.$prototyping=true;var h=new g;delete g.$prototyping; +return h;};a.implement("implement",f.overloadSetter());a.Mutators={Extends:function(g){this.parent=g;this.prototype=d(g);},Implements:function(g){Array.from(g).each(function(j){var h=new j; +for(var i in h){f.call(this,i,h[i],true);}},this);}};})();(function(){this.Chain=new Class({$chain:[],chain:function(){this.$chain.append(Array.flatten(arguments)); +return this;},callChain:function(){return(this.$chain.length)?this.$chain.shift().apply(this,arguments):false;},clearChain:function(){this.$chain.empty(); +return this;}});var a=function(b){return b.replace(/^on([A-Z])/,function(c,d){return d.toLowerCase();});};this.Events=new Class({$events:{},addEvent:function(d,c,b){d=a(d); +this.$events[d]=(this.$events[d]||[]).include(c);if(b){c.internal=true;}return this;},addEvents:function(b){for(var c in b){this.addEvent(c,b[c]);}return this; +},fireEvent:function(e,c,b){e=a(e);var d=this.$events[e];if(!d){return this;}c=Array.from(c);d.each(function(f){if(b){f.delay(b,this,c);}else{f.apply(this,c); +}},this);return this;},removeEvent:function(e,d){e=a(e);var c=this.$events[e];if(c&&!d.internal){var b=c.indexOf(d);if(b!=-1){delete c[b];}}return this; +},removeEvents:function(d){var e;if(typeOf(d)=="object"){for(e in d){this.removeEvent(e,d[e]);}return this;}if(d){d=a(d);}for(e in this.$events){if(d&&d!=e){continue; +}var c=this.$events[e];for(var b=c.length;b--;){if(b in c){this.removeEvent(e,c[b]);}}}return this;}});this.Options=new Class({setOptions:function(){var b=this.options=Object.merge.apply(null,[{},this.options].append(arguments)); +if(this.addEvent){for(var c in b){if(typeOf(b[c])!="function"||!(/^on[A-Z]/).test(c)){continue;}this.addEvent(c,b[c]);delete b[c];}}return this;}});})(); +(function(){var k,n,l,g,a={},c={},m=/\\/g;var e=function(q,p){if(q==null){return null;}if(q.Slick===true){return q;}q=(""+q).replace(/^\s+|\s+$/g,"");g=!!p; +var o=(g)?c:a;if(o[q]){return o[q];}k={Slick:true,expressions:[],raw:q,reverse:function(){return e(this.raw,true);}};n=-1;while(q!=(q=q.replace(j,b))){}k.length=k.expressions.length; +return o[k.raw]=(g)?h(k):k;};var i=function(o){if(o==="!"){return" ";}else{if(o===" "){return"!";}else{if((/^!/).test(o)){return o.replace(/^!/,"");}else{return"!"+o; +}}}};var h=function(u){var r=u.expressions;for(var p=0;p+)\\s*|(\\s+)|(+|\\*)|\\#(+)|\\.(+)|\\[\\s*(+)(?:\\s*([*^$!~|]?=)(?:\\s*(?:([\"']?)(.*?)\\9)))?\\s*\\](?!\\])|(:+)(+)(?:\\((?:(?:([\"'])([^\\13]*)\\13)|((?:\\([^)]+\\)|[^()]*)+))\\))?)".replace(//,"["+f(">+~`!@$%^&={}\\;/g,"(?:[\\w\\u00a1-\\uFFFF-]|\\\\[^\\s0-9a-f])").replace(//g,"(?:[:\\w\\u00a1-\\uFFFF-]|\\\\[^\\s0-9a-f])")); +function b(x,s,D,z,r,C,q,B,A,y,u,F,G,v,p,w){if(s||n===-1){k.expressions[++n]=[];l=-1;if(s){return"";}}if(D||z||l===-1){D=D||" ";var t=k.expressions[n]; +if(g&&t[l]){t[l].reverseCombinator=i(D);}t[++l]={combinator:D,tag:"*"};}var o=k.expressions[n][l];if(r){o.tag=r.replace(m,"");}else{if(C){o.id=C.replace(m,""); +}else{if(q){q=q.replace(m,"");if(!o.classList){o.classList=[];}if(!o.classes){o.classes=[];}o.classList.push(q);o.classes.push({value:q,regexp:new RegExp("(^|\\s)"+f(q)+"(\\s|$)")}); +}else{if(G){w=w||p;w=w?w.replace(m,""):null;if(!o.pseudos){o.pseudos=[];}o.pseudos.push({key:G.replace(m,""),value:w,type:F.length==1?"class":"element"}); +}else{if(B){B=B.replace(m,"");u=(u||"").replace(m,"");var E,H;switch(A){case"^=":H=new RegExp("^"+f(u));break;case"$=":H=new RegExp(f(u)+"$");break;case"~=":H=new RegExp("(^|\\s)"+f(u)+"(\\s|$)"); +break;case"|=":H=new RegExp("^"+f(u)+"(-|$)");break;case"=":E=function(I){return u==I;};break;case"*=":E=function(I){return I&&I.indexOf(u)>-1;};break; +case"!=":E=function(I){return u!=I;};break;default:E=function(I){return !!I;};}if(u==""&&(/^[*$^]=$/).test(A)){E=function(){return false;};}if(!E){E=function(I){return I&&H.test(I); +};}if(!o.attributes){o.attributes=[];}o.attributes.push({key:B,operator:A,value:u,test:E});}}}}}return"";}var d=(this.Slick||{});d.parse=function(o){return e(o); +};d.escapeRegExp=f;if(!this.Slick){this.Slick=d;}}).apply((typeof exports!="undefined")?exports:this);(function(){var k={},m={},d=Object.prototype.toString; +k.isNativeCode=function(c){return(/\{\s*\[native code\]\s*\}/).test(""+c);};k.isXML=function(c){return(!!c.xmlVersion)||(!!c.xml)||(d.call(c)=="[object XMLDocument]")||(c.nodeType==9&&c.documentElement.nodeName!="HTML"); +};k.setDocument=function(w){var p=w.nodeType;if(p==9){}else{if(p){w=w.ownerDocument;}else{if(w.navigator){w=w.document;}else{return;}}}if(this.document===w){return; +}this.document=w;var A=w.documentElement,o=this.getUIDXML(A),s=m[o],r;if(s){for(r in s){this[r]=s[r];}return;}s=m[o]={};s.root=A;s.isXMLDocument=this.isXML(w); +s.brokenStarGEBTN=s.starSelectsClosedQSA=s.idGetsName=s.brokenMixedCaseQSA=s.brokenGEBCN=s.brokenCheckedQSA=s.brokenEmptyAttributeQSA=s.isHTMLDocument=s.nativeMatchesSelector=false; +var q,u,y,z,t;var x,v="slick_uniqueid";var c=w.createElement("div");var n=w.body||w.getElementsByTagName("body")[0]||A;n.appendChild(c);try{c.innerHTML=''; +s.isHTMLDocument=!!w.getElementById(v);}catch(C){}if(s.isHTMLDocument){c.style.display="none";c.appendChild(w.createComment(""));u=(c.getElementsByTagName("*").length>1); +try{c.innerHTML="foo";x=c.getElementsByTagName("*");q=(x&&!!x.length&&x[0].nodeName.charAt(0)=="/");}catch(C){}s.brokenStarGEBTN=u||q;try{c.innerHTML=''; +s.idGetsName=w.getElementById(v)===c.firstChild;}catch(C){}if(c.getElementsByClassName){try{c.innerHTML='';c.getElementsByClassName("b").length; +c.firstChild.className="b";z=(c.getElementsByClassName("b").length!=2);}catch(C){}try{c.innerHTML='';y=(c.getElementsByClassName("a").length!=2); +}catch(C){}s.brokenGEBCN=z||y;}if(c.querySelectorAll){try{c.innerHTML="foo";x=c.querySelectorAll("*");s.starSelectsClosedQSA=(x&&!!x.length&&x[0].nodeName.charAt(0)=="/"); +}catch(C){}try{c.innerHTML='';s.brokenMixedCaseQSA=!c.querySelectorAll(".MiX").length;}catch(C){}try{c.innerHTML=''; +s.brokenCheckedQSA=(c.querySelectorAll(":checked").length==0);}catch(C){}try{c.innerHTML='';s.brokenEmptyAttributeQSA=(c.querySelectorAll('[class*=""]').length!=0); +}catch(C){}}try{c.innerHTML='
';t=(c.firstChild.getAttribute("action")!="s");}catch(C){}s.nativeMatchesSelector=A.matchesSelector||A.mozMatchesSelector||A.webkitMatchesSelector; +if(s.nativeMatchesSelector){try{s.nativeMatchesSelector.call(A,":slick");s.nativeMatchesSelector=null;}catch(C){}}}try{A.slick_expando=1;delete A.slick_expando; +s.getUID=this.getUIDHTML;}catch(C){s.getUID=this.getUIDXML;}n.removeChild(c);c=x=n=null;s.getAttribute=(s.isHTMLDocument&&t)?function(G,E){var H=this.attributeGetters[E]; +if(H){return H.call(G);}var F=G.getAttributeNode(E);return(F)?F.nodeValue:null;}:function(F,E){var G=this.attributeGetters[E];return(G)?G.call(F):F.getAttribute(E); +};s.hasAttribute=(A&&this.isNativeCode(A.hasAttribute))?function(F,E){return F.hasAttribute(E);}:function(F,E){F=F.getAttributeNode(E);return !!(F&&(F.specified||F.nodeValue)); +};var D=A&&this.isNativeCode(A.contains),B=w&&this.isNativeCode(w.contains);s.contains=(D&&B)?function(E,F){return E.contains(F);}:(D&&!B)?function(E,F){return E===F||((E===w)?w.documentElement:E).contains(F); +}:(A&&A.compareDocumentPosition)?function(E,F){return E===F||!!(E.compareDocumentPosition(F)&16);}:function(E,F){if(F){do{if(F===E){return true;}}while((F=F.parentNode)); +}return false;};s.documentSorter=(A.compareDocumentPosition)?function(F,E){if(!F.compareDocumentPosition||!E.compareDocumentPosition){return 0;}return F.compareDocumentPosition(E)&4?-1:F===E?0:1; +}:("sourceIndex" in A)?function(F,E){if(!F.sourceIndex||!E.sourceIndex){return 0;}return F.sourceIndex-E.sourceIndex;}:(w.createRange)?function(H,F){if(!H.ownerDocument||!F.ownerDocument){return 0; +}var G=H.ownerDocument.createRange(),E=F.ownerDocument.createRange();G.setStart(H,0);G.setEnd(H,0);E.setStart(F,0);E.setEnd(F,0);return G.compareBoundaryPoints(Range.START_TO_END,E); +}:null;A=null;for(r in s){this[r]=s[r];}};var f=/^([#.]?)((?:[\w-]+|\*))$/,h=/\[.+[*$^]=(?:""|'')?\]/,g={};k.search=function(U,z,H,s){var p=this.found=(s)?null:(H||[]); +if(!U){return p;}else{if(U.navigator){U=U.document;}else{if(!U.nodeType){return p;}}}var F,O,V=this.uniques={},I=!!(H&&H.length),y=(U.nodeType==9);if(this.document!==(y?U:U.ownerDocument)){this.setDocument(U); +}if(I){for(O=p.length;O--;){V[this.getUID(p[O])]=true;}}if(typeof z=="string"){var r=z.match(f);simpleSelectors:if(r){var u=r[1],v=r[2],A,E;if(!u){if(v=="*"&&this.brokenStarGEBTN){break simpleSelectors; +}E=U.getElementsByTagName(v);if(s){return E[0]||null;}for(O=0;A=E[O++];){if(!(I&&V[this.getUID(A)])){p.push(A);}}}else{if(u=="#"){if(!this.isHTMLDocument||!y){break simpleSelectors; +}A=U.getElementById(v);if(!A){return p;}if(this.idGetsName&&A.getAttributeNode("id").nodeValue!=v){break simpleSelectors;}if(s){return A||null;}if(!(I&&V[this.getUID(A)])){p.push(A); +}}else{if(u=="."){if(!this.isHTMLDocument||((!U.getElementsByClassName||this.brokenGEBCN)&&U.querySelectorAll)){break simpleSelectors;}if(U.getElementsByClassName&&!this.brokenGEBCN){E=U.getElementsByClassName(v); +if(s){return E[0]||null;}for(O=0;A=E[O++];){if(!(I&&V[this.getUID(A)])){p.push(A);}}}else{var T=new RegExp("(^|\\s)"+e.escapeRegExp(v)+"(\\s|$)");E=U.getElementsByTagName("*"); +for(O=0;A=E[O++];){className=A.className;if(!(className&&T.test(className))){continue;}if(s){return A;}if(!(I&&V[this.getUID(A)])){p.push(A);}}}}}}if(I){this.sort(p); +}return(s)?null:p;}querySelector:if(U.querySelectorAll){if(!this.isHTMLDocument||g[z]||this.brokenMixedCaseQSA||(this.brokenCheckedQSA&&z.indexOf(":checked")>-1)||(this.brokenEmptyAttributeQSA&&h.test(z))||(!y&&z.indexOf(",")>-1)||e.disableQSA){break querySelector; +}var S=z,x=U;if(!y){var C=x.getAttribute("id"),t="slickid__";x.setAttribute("id",t);S="#"+t+" "+S;U=x.parentNode;}try{if(s){return U.querySelector(S)||null; +}else{E=U.querySelectorAll(S);}}catch(Q){g[z]=1;break querySelector;}finally{if(!y){if(C){x.setAttribute("id",C);}else{x.removeAttribute("id");}U=x;}}if(this.starSelectsClosedQSA){for(O=0; +A=E[O++];){if(A.nodeName>"@"&&!(I&&V[this.getUID(A)])){p.push(A);}}}else{for(O=0;A=E[O++];){if(!(I&&V[this.getUID(A)])){p.push(A);}}}if(I){this.sort(p); +}return p;}F=this.Slick.parse(z);if(!F.length){return p;}}else{if(z==null){return p;}else{if(z.Slick){F=z;}else{if(this.contains(U.documentElement||U,z)){(p)?p.push(z):p=z; +return p;}else{return p;}}}}this.posNTH={};this.posNTHLast={};this.posNTHType={};this.posNTHTypeLast={};this.push=(!I&&(s||(F.length==1&&F.expressions[0].length==1)))?this.pushArray:this.pushUID; +if(p==null){p=[];}var M,L,K;var B,J,D,c,q,G,W;var N,P,o,w,R=F.expressions;search:for(O=0;(P=R[O]);O++){for(M=0;(o=P[M]);M++){B="combinator:"+o.combinator; +if(!this[B]){continue search;}J=(this.isXMLDocument)?o.tag:o.tag.toUpperCase();D=o.id;c=o.classList;q=o.classes;G=o.attributes;W=o.pseudos;w=(M===(P.length-1)); +this.bitUniques={};if(w){this.uniques=V;this.found=p;}else{this.uniques={};this.found=[];}if(M===0){this[B](U,J,D,q,G,W,c);if(s&&w&&p.length){break search; +}}else{if(s&&w){for(L=0,K=N.length;L1)){this.sort(p);}return(s)?(p[0]||null):p;};k.uidx=1;k.uidk="slick-uniqueid";k.getUIDXML=function(n){var c=n.getAttribute(this.uidk); +if(!c){c=this.uidx++;n.setAttribute(this.uidk,c);}return c;};k.getUIDHTML=function(c){return c.uniqueNumber||(c.uniqueNumber=this.uidx++);};k.sort=function(c){if(!this.documentSorter){return c; +}c.sort(this.documentSorter);return c;};k.cacheNTH={};k.matchNTH=/^([+-]?\d*)?([a-z]+)?([+-]\d+)?$/;k.parseNTHArgument=function(q){var o=q.match(this.matchNTH); +if(!o){return false;}var p=o[2]||false;var n=o[1]||1;if(n=="-"){n=-1;}var c=+o[3]||0;o=(p=="n")?{a:n,b:c}:(p=="odd")?{a:2,b:1}:(p=="even")?{a:2,b:0}:{a:0,b:n}; +return(this.cacheNTH[q]=o);};k.createNTHPseudo=function(p,n,c,o){return function(s,q){var u=this.getUID(s);if(!this[c][u]){var A=s.parentNode;if(!A){return false; +}var r=A[p],t=1;if(o){var z=s.nodeName;do{if(r.nodeName!=z){continue;}this[c][this.getUID(r)]=t++;}while((r=r[n]));}else{do{if(r.nodeType!=1){continue; +}this[c][this.getUID(r)]=t++;}while((r=r[n]));}}q=q||"n";var v=this.cacheNTH[q]||this.parseNTHArgument(q);if(!v){return false;}var y=v.a,x=v.b,w=this[c][u]; +if(y==0){return x==w;}if(y>0){if(w":function(p,c,r,o,n,q){if((p=p.firstChild)){do{if(p.nodeType==1){this.push(p,c,r,o,n,q); +}}while((p=p.nextSibling));}},"+":function(p,c,r,o,n,q){while((p=p.nextSibling)){if(p.nodeType==1){this.push(p,c,r,o,n,q);break;}}},"^":function(p,c,r,o,n,q){p=p.firstChild; +if(p){if(p.nodeType==1){this.push(p,c,r,o,n,q);}else{this["combinator:+"](p,c,r,o,n,q);}}},"~":function(q,c,s,p,n,r){while((q=q.nextSibling)){if(q.nodeType!=1){continue; +}var o=this.getUID(q);if(this.bitUniques[o]){break;}this.bitUniques[o]=true;this.push(q,c,s,p,n,r);}},"++":function(p,c,r,o,n,q){this["combinator:+"](p,c,r,o,n,q); +this["combinator:!+"](p,c,r,o,n,q);},"~~":function(p,c,r,o,n,q){this["combinator:~"](p,c,r,o,n,q);this["combinator:!~"](p,c,r,o,n,q);},"!":function(p,c,r,o,n,q){while((p=p.parentNode)){if(p!==this.document){this.push(p,c,r,o,n,q); +}}},"!>":function(p,c,r,o,n,q){p=p.parentNode;if(p!==this.document){this.push(p,c,r,o,n,q);}},"!+":function(p,c,r,o,n,q){while((p=p.previousSibling)){if(p.nodeType==1){this.push(p,c,r,o,n,q); +break;}}},"!^":function(p,c,r,o,n,q){p=p.lastChild;if(p){if(p.nodeType==1){this.push(p,c,r,o,n,q);}else{this["combinator:!+"](p,c,r,o,n,q);}}},"!~":function(q,c,s,p,n,r){while((q=q.previousSibling)){if(q.nodeType!=1){continue; +}var o=this.getUID(q);if(this.bitUniques[o]){break;}this.bitUniques[o]=true;this.push(q,c,s,p,n,r);}}};for(var i in j){k["combinator:"+i]=j[i];}var l={empty:function(c){var n=c.firstChild; +return !(n&&n.nodeType==1)&&!(c.innerText||c.textContent||"").length;},not:function(c,n){return !this.matchNode(c,n);},contains:function(c,n){return(c.innerText||c.textContent||"").indexOf(n)>-1; +},"first-child":function(c){while((c=c.previousSibling)){if(c.nodeType==1){return false;}}return true;},"last-child":function(c){while((c=c.nextSibling)){if(c.nodeType==1){return false; +}}return true;},"only-child":function(o){var n=o;while((n=n.previousSibling)){if(n.nodeType==1){return false;}}var c=o;while((c=c.nextSibling)){if(c.nodeType==1){return false; +}}return true;},"nth-child":k.createNTHPseudo("firstChild","nextSibling","posNTH"),"nth-last-child":k.createNTHPseudo("lastChild","previousSibling","posNTHLast"),"nth-of-type":k.createNTHPseudo("firstChild","nextSibling","posNTHType",true),"nth-last-of-type":k.createNTHPseudo("lastChild","previousSibling","posNTHTypeLast",true),index:function(n,c){return this["pseudo:nth-child"](n,""+(c+1)); +},even:function(c){return this["pseudo:nth-child"](c,"2n");},odd:function(c){return this["pseudo:nth-child"](c,"2n+1");},"first-of-type":function(c){var n=c.nodeName; +while((c=c.previousSibling)){if(c.nodeName==n){return false;}}return true;},"last-of-type":function(c){var n=c.nodeName;while((c=c.nextSibling)){if(c.nodeName==n){return false; +}}return true;},"only-of-type":function(o){var n=o,p=o.nodeName;while((n=n.previousSibling)){if(n.nodeName==p){return false;}}var c=o;while((c=c.nextSibling)){if(c.nodeName==p){return false; +}}return true;},enabled:function(c){return !c.disabled;},disabled:function(c){return c.disabled;},checked:function(c){return c.checked||c.selected;},focus:function(c){return this.isHTMLDocument&&this.document.activeElement===c&&(c.href||c.type||this.hasAttribute(c,"tabindex")); +},root:function(c){return(c===this.root);},selected:function(c){return c.selected;}};for(var b in l){k["pseudo:"+b]=l[b];}var a=k.attributeGetters={"for":function(){return("htmlFor" in this)?this.htmlFor:this.getAttribute("for"); +},href:function(){return("href" in this)?this.getAttribute("href",2):this.getAttribute("href");},style:function(){return(this.style)?this.style.cssText:this.getAttribute("style"); +},tabindex:function(){var c=this.getAttributeNode("tabindex");return(c&&c.specified)?c.nodeValue:null;},type:function(){return this.getAttribute("type"); +},maxlength:function(){var c=this.getAttributeNode("maxLength");return(c&&c.specified)?c.nodeValue:null;}};a.MAXLENGTH=a.maxLength=a.maxlength;var e=k.Slick=(this.Slick||{}); +e.version="1.1.7";e.search=function(n,o,c){return k.search(n,o,c);};e.find=function(c,n){return k.search(c,n,null,true);};e.contains=function(c,n){k.setDocument(c); +return k.contains(c,n);};e.getAttribute=function(n,c){k.setDocument(n);return k.getAttribute(n,c);};e.hasAttribute=function(n,c){k.setDocument(n);return k.hasAttribute(n,c); +};e.match=function(n,c){if(!(n&&c)){return false;}if(!c||c===n){return true;}k.setDocument(n);return k.matchNode(n,c);};e.defineAttributeGetter=function(c,n){k.attributeGetters[c]=n; +return this;};e.lookupAttributeGetter=function(c){return k.attributeGetters[c];};e.definePseudo=function(c,n){k["pseudo:"+c]=function(p,o){return n.call(p,o); +};return this;};e.lookupPseudo=function(c){var n=k["pseudo:"+c];if(n){return function(o){return n.call(this,o);};}return null;};e.override=function(n,c){k.override(n,c); +return this;};e.isXML=k.isXML;e.uidOf=function(c){return k.getUIDHTML(c);};if(!this.Slick){this.Slick=e;}}).apply((typeof exports!="undefined")?exports:this); +var Element=function(b,g){var h=Element.Constructors[b];if(h){return h(g);}if(typeof b!="string"){return document.id(b).set(g);}if(!g){g={};}if(!(/^[\w-]+$/).test(b)){var e=Slick.parse(b).expressions[0][0]; +b=(e.tag=="*")?"div":e.tag;if(e.id&&g.id==null){g.id=e.id;}var d=e.attributes;if(d){for(var a,f=0,c=d.length;f=this.length){delete this[g--]; +}return e;}.protect());}Array.forEachMethod(function(g,e){Elements.implement(e,g);});Array.mirror(Elements);var d;try{d=(document.createElement("").name=="x"); +}catch(b){}var c=function(e){return(""+e).replace(/&/g,"&").replace(/"/g,""");};Document.implement({newElement:function(e,g){if(g&&g.checked!=null){g.defaultChecked=g.checked; +}if(d&&g){e="<"+e;if(g.name){e+=' name="'+c(g.name)+'"';}if(g.type){e+=' type="'+c(g.type)+'"';}e+=">";delete g.name;delete g.type;}return this.id(this.createElement(e)).set(g); +}});})();(function(){Slick.uidOf(window);Slick.uidOf(document);Document.implement({newTextNode:function(e){return this.createTextNode(e);},getDocument:function(){return this; +},getWindow:function(){return this.window;},id:(function(){var e={string:function(E,D,l){E=Slick.find(l,"#"+E.replace(/(\W)/g,"\\$1"));return(E)?e.element(E,D):null; +},element:function(D,E){Slick.uidOf(D);if(!E&&!D.$family&&!(/^(?:object|embed)$/i).test(D.tagName)){var l=D.fireEvent;D._fireEvent=function(F,G){return l(F,G); +};Object.append(D,Element.Prototype);}return D;},object:function(D,E,l){if(D.toElement){return e.element(D.toElement(l),E);}return null;}};e.textnode=e.whitespace=e.window=e.document=function(l){return l; +};return function(D,F,E){if(D&&D.$family&&D.uniqueNumber){return D;}var l=typeOf(D);return(e[l])?e[l](D,F,E||document):null;};})()});if(window.$==null){Window.implement("$",function(e,l){return document.id(e,l,this.document); +});}Window.implement({getDocument:function(){return this.document;},getWindow:function(){return this;}});[Document,Element].invoke("implement",{getElements:function(e){return Slick.search(this,e,new Elements); +},getElement:function(e){return document.id(Slick.find(this,e));}});var m={contains:function(e){return Slick.contains(this,e);}};if(!document.contains){Document.implement(m); +}if(!document.createElement("div").contains){Element.implement(m);}var r=function(E,D){if(!E){return D;}E=Object.clone(Slick.parse(E));var l=E.expressions; +for(var e=l.length;e--;){l[e][0].combinator=D;}return E;};Object.forEach({getNext:"~",getPrevious:"!~",getParent:"!"},function(e,l){Element.implement(l,function(D){return this.getElement(r(D,e)); +});});Object.forEach({getAllNext:"~",getAllPrevious:"!~",getSiblings:"~~",getChildren:">",getParents:"!"},function(e,l){Element.implement(l,function(D){return this.getElements(r(D,e)); +});});Element.implement({getFirst:function(e){return document.id(Slick.search(this,r(e,">"))[0]);},getLast:function(e){return document.id(Slick.search(this,r(e,">")).getLast()); +},getWindow:function(){return this.ownerDocument.window;},getDocument:function(){return this.ownerDocument;},getElementById:function(e){return document.id(Slick.find(this,"#"+(""+e).replace(/(\W)/g,"\\$1"))); +},match:function(e){return !e||Slick.match(this,e);}});if(window.$$==null){Window.implement("$$",function(e){if(arguments.length==1){if(typeof e=="string"){return Slick.search(this.document,e,new Elements); +}else{if(Type.isEnumerable(e)){return new Elements(e);}}}return new Elements(arguments);});}var w={before:function(l,e){var D=e.parentNode;if(D){D.insertBefore(l,e); +}},after:function(l,e){var D=e.parentNode;if(D){D.insertBefore(l,e.nextSibling);}},bottom:function(l,e){e.appendChild(l);},top:function(l,e){e.insertBefore(l,e.firstChild); +}};w.inside=w.bottom;var j={},d={};var k={};Array.forEach(["type","value","defaultValue","accessKey","cellPadding","cellSpacing","colSpan","frameBorder","rowSpan","tabIndex","useMap"],function(e){k[e.toLowerCase()]=e; +});k.html="innerHTML";k.text=(document.createElement("div").textContent==null)?"innerText":"textContent";Object.forEach(k,function(l,e){d[e]=function(D,E){D[l]=E; +};j[e]=function(D){return D[l];};});var x=["compact","nowrap","ismap","declare","noshade","checked","disabled","readOnly","multiple","selected","noresize","defer","defaultChecked","autofocus","controls","autoplay","loop"]; +var h={};Array.forEach(x,function(e){var l=e.toLowerCase();h[l]=e;d[l]=function(D,E){D[e]=!!E;};j[l]=function(D){return !!D[e];};});Object.append(d,{"class":function(e,l){("className" in e)?e.className=(l||""):e.setAttribute("class",l); +},"for":function(e,l){("htmlFor" in e)?e.htmlFor=l:e.setAttribute("for",l);},style:function(e,l){(e.style)?e.style.cssText=l:e.setAttribute("style",l); +},value:function(e,l){e.value=(l!=null)?l:"";}});j["class"]=function(e){return("className" in e)?e.className||null:e.getAttribute("class");};var f=document.createElement("button"); +try{f.type="button";}catch(z){}if(f.type!="button"){d.type=function(e,l){e.setAttribute("type",l);};}f=null;var p=document.createElement("input");p.value="t"; +p.type="submit";if(p.value!="t"){d.type=function(l,e){var D=l.value;l.type=e;l.value=D;};}p=null;var q=(function(e){e.random="attribute";return(e.getAttribute("random")=="attribute"); +})(document.createElement("div"));Element.implement({setProperty:function(l,D){var E=d[l.toLowerCase()];if(E){E(this,D);}else{if(q){var e=this.retrieve("$attributeWhiteList",{}); +}if(D==null){this.removeAttribute(l);if(q){delete e[l];}}else{this.setAttribute(l,""+D);if(q){e[l]=true;}}}return this;},setProperties:function(e){for(var l in e){this.setProperty(l,e[l]); +}return this;},getProperty:function(F){var D=j[F.toLowerCase()];if(D){return D(this);}if(q){var l=this.getAttributeNode(F),E=this.retrieve("$attributeWhiteList",{}); +if(!l){return null;}if(l.expando&&!E[F]){var G=this.outerHTML;if(G.substr(0,G.search(/\/?['"]?>(?![^<]*<['"])/)).indexOf(F)<0){return null;}E[F]=true;}}var e=Slick.getAttribute(this,F); +return(!e&&!Slick.hasAttribute(this,F))?null:e;},getProperties:function(){var e=Array.from(arguments);return e.map(this.getProperty,this).associate(e); +},removeProperty:function(e){return this.setProperty(e,null);},removeProperties:function(){Array.each(arguments,this.removeProperty,this);return this;},set:function(D,l){var e=Element.Properties[D]; +(e&&e.set)?e.set.call(this,l):this.setProperty(D,l);}.overloadSetter(),get:function(l){var e=Element.Properties[l];return(e&&e.get)?e.get.apply(this):this.getProperty(l); +}.overloadGetter(),erase:function(l){var e=Element.Properties[l];(e&&e.erase)?e.erase.apply(this):this.removeProperty(l);return this;},hasClass:function(e){return this.className.clean().contains(e," "); +},addClass:function(e){if(!this.hasClass(e)){this.className=(this.className+" "+e).clean();}return this;},removeClass:function(e){this.className=this.className.replace(new RegExp("(^|\\s)"+e+"(?:\\s|$)"),"$1"); +return this;},toggleClass:function(e,l){if(l==null){l=!this.hasClass(e);}return(l)?this.addClass(e):this.removeClass(e);},adopt:function(){var E=this,e,G=Array.flatten(arguments),F=G.length; +if(F>1){E=e=document.createDocumentFragment();}for(var D=0;D"; +var a=(t.childNodes.length==1);if(!a){var s="abbr article aside audio canvas datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video".split(" "),b=document.createDocumentFragment(),u=s.length; +while(u--){b.createElement(s[u]);}}t=null;var g=Function.attempt(function(){var e=document.createElement("table");e.innerHTML="";return true; +});var c=document.createElement("tr"),o="";c.innerHTML=o;var y=(c.innerHTML==o);c=null;if(!g||!y||!a){Element.Properties.html.set=(function(l){var e={table:[1,"","
"],select:[1,""],tbody:[2,"","
"],tr:[3,"","
"]}; +e.thead=e.tfoot=e.tbody;return function(D){var E=e[this.get("tag")];if(!E&&!a){E=[0,"",""];}if(!E){return l.call(this,D);}var H=E[0],G=document.createElement("div"),F=G; +if(!a){b.appendChild(G);}G.innerHTML=[E[1],D,E[2]].flatten().join("");while(H--){F=F.firstChild;}this.empty().adopt(F.childNodes);if(!a){b.removeChild(G); +}G=null;};})(Element.Properties.html.set);}var n=document.createElement("form");n.innerHTML="";if(n.firstChild.value!="s"){Element.Properties.value={set:function(G){var l=this.get("tag"); +if(l!="select"){return this.setProperty("value",G);}var D=this.getElements("option");for(var E=0;E0||k==null?"visible":"hidden";};var f=(h?function(l,k){l.style.opacity=k;}:(e?function(l,k){var n=l.style; +if(!l.currentStyle||!l.currentStyle.hasLayout){n.zoom=1;}if(k==null||k==1){k="";}else{k="alpha(opacity="+(k*100).limit(0,100).round()+")";}var m=n.filter||l.getComputedStyle("filter")||""; +n.filter=j.test(m)?m.replace(j,k):m+k;if(!n.filter){n.removeAttribute("filter");}}:a));var g=(h?function(l){var k=l.style.opacity||l.getComputedStyle("opacity"); +return(k=="")?1:k.toFloat();}:(e?function(l){var m=(l.style.filter||l.getComputedStyle("filter")),k;if(m){k=m.match(j);}return(k==null||m==null)?1:(k[1]/100); +}:function(l){var k=l.retrieve("$opacity");if(k==null){k=(l.style.visibility=="hidden"?0:1);}return k;}));var b=(i.style.cssFloat==null)?"styleFloat":"cssFloat"; +Element.implement({getComputedStyle:function(m){if(this.currentStyle){return this.currentStyle[m.camelCase()];}var l=Element.getDocument(this).defaultView,k=l?l.getComputedStyle(this,null):null; +return(k)?k.getPropertyValue((m==b)?"float":m.hyphenate()):null;},setStyle:function(l,k){if(l=="opacity"){if(k!=null){k=parseFloat(k);}f(this,k);return this; +}l=(l=="float"?b:l).camelCase();if(typeOf(k)!="string"){var m=(Element.Styles[l]||"@").split(" ");k=Array.from(k).map(function(o,n){if(!m[n]){return""; +}return(typeOf(o)=="number")?m[n].replace("@",Math.round(o)):o;}).join(" ");}else{if(k==String(Number(k))){k=Math.round(k);}}this.style[l]=k;if((k==""||k==null)&&c&&this.style.removeAttribute){this.style.removeAttribute(l); +}return this;},getStyle:function(q){if(q=="opacity"){return g(this);}q=(q=="float"?b:q).camelCase();var k=this.style[q];if(!k||q=="zIndex"){k=[];for(var p in Element.ShortStyles){if(q!=p){continue; +}for(var o in Element.ShortStyles[p]){k.push(this.getStyle(o));}return k.join(" ");}k=this.getComputedStyle(q);}if(k){k=String(k);var m=k.match(/rgba?\([\d\s,]+\)/); +if(m){k=k.replace(m[0],m[0].rgbToHex());}}if(Browser.opera||Browser.ie){if((/^(height|width)$/).test(q)&&!(/px$/.test(k))){var l=(q=="width")?["left","right"]:["top","bottom"],n=0; +l.each(function(r){n+=this.getStyle("border-"+r+"-width").toInt()+this.getStyle("padding-"+r).toInt();},this);return this["offset"+q.capitalize()]-n+"px"; +}if(Browser.ie&&(/^border(.+)Width|margin|padding/).test(q)&&isNaN(parseFloat(k))){return"0px";}}return k;},setStyles:function(l){for(var k in l){this.setStyle(k,l[k]); +}return this;},getStyles:function(){var k={};Array.flatten(arguments).each(function(l){k[l]=this.getStyle(l);},this);return k;}});Element.Styles={left:"@px",top:"@px",bottom:"@px",right:"@px",width:"@px",height:"@px",maxWidth:"@px",maxHeight:"@px",minWidth:"@px",minHeight:"@px",backgroundColor:"rgb(@, @, @)",backgroundPosition:"@px @px",color:"rgb(@, @, @)",fontSize:"@px",letterSpacing:"@px",lineHeight:"@px",clip:"rect(@px @px @px @px)",margin:"@px @px @px @px",padding:"@px @px @px @px",border:"@px @ rgb(@, @, @) @px @ rgb(@, @, @) @px @ rgb(@, @, @)",borderWidth:"@px @px @px @px",borderStyle:"@ @ @ @",borderColor:"rgb(@, @, @) rgb(@, @, @) rgb(@, @, @) rgb(@, @, @)",zIndex:"@",zoom:"@",fontWeight:"@",textIndent:"@px",opacity:"@"}; +Element.ShortStyles={margin:{},padding:{},border:{},borderWidth:{},borderStyle:{},borderColor:{}};["Top","Right","Bottom","Left"].each(function(q){var p=Element.ShortStyles; +var l=Element.Styles;["margin","padding"].each(function(r){var s=r+q;p[r][s]=l[s]="@px";});var o="border"+q;p.border[o]=l[o]="@px @ rgb(@, @, @)";var n=o+"Width",k=o+"Style",m=o+"Color"; +p[o]={};p.borderWidth[n]=p[o][n]=l[n]="@px";p.borderStyle[k]=p[o][k]=l[k]="@";p.borderColor[m]=p[o][m]=l[m]="rgb(@, @, @)";});})();(function(){Element.Properties.events={set:function(b){this.addEvents(b); +}};[Element,Window,Document].invoke("implement",{addEvent:function(f,h){var i=this.retrieve("events",{});if(!i[f]){i[f]={keys:[],values:[]};}if(i[f].keys.contains(h)){return this; +}i[f].keys.push(h);var g=f,b=Element.Events[f],d=h,j=this;if(b){if(b.onAdd){b.onAdd.call(this,h,f);}if(b.condition){d=function(k){if(b.condition.call(this,k,f)){return h.call(this,k); +}return true;};}if(b.base){g=Function.from(b.base).call(this,f);}}var e=function(){return h.call(j);};var c=Element.NativeEvents[g];if(c){if(c==2){e=function(k){k=new DOMEvent(k,j.getWindow()); +if(d.call(j,k)===false){k.stop();}};}this.addListener(g,e,arguments[2]);}i[f].values.push(e);return this;},removeEvent:function(e,d){var c=this.retrieve("events"); +if(!c||!c[e]){return this;}var h=c[e];var b=h.keys.indexOf(d);if(b==-1){return this;}var g=h.values[b];delete h.keys[b];delete h.values[b];var f=Element.Events[e]; +if(f){if(f.onRemove){f.onRemove.call(this,d,e);}if(f.base){e=Function.from(f.base).call(this,e);}}return(Element.NativeEvents[e])?this.removeListener(e,g,arguments[2]):this; +},addEvents:function(b){for(var c in b){this.addEvent(c,b[c]);}return this;},removeEvents:function(b){var d;if(typeOf(b)=="object"){for(d in b){this.removeEvent(d,b[d]); +}return this;}var c=this.retrieve("events");if(!c){return this;}if(!b){for(d in c){this.removeEvents(d);}this.eliminate("events");}else{if(c[b]){c[b].keys.each(function(e){this.removeEvent(b,e); +},this);delete c[b];}}return this;},fireEvent:function(e,c,b){var d=this.retrieve("events");if(!d||!d[e]){return this;}c=Array.from(c);d[e].keys.each(function(f){if(b){f.delay(b,this,c); +}else{f.apply(this,c);}},this);return this;},cloneEvents:function(e,d){e=document.id(e);var c=e.retrieve("events");if(!c){return this;}if(!d){for(var b in c){this.cloneEvents(e,b); +}}else{if(c[d]){c[d].keys.each(function(f){this.addEvent(d,f);},this);}}return this;}});Element.NativeEvents={click:2,dblclick:2,mouseup:2,mousedown:2,contextmenu:2,mousewheel:2,DOMMouseScroll:2,mouseover:2,mouseout:2,mousemove:2,selectstart:2,selectend:2,keydown:2,keypress:2,keyup:2,orientationchange:2,touchstart:2,touchmove:2,touchend:2,touchcancel:2,gesturestart:2,gesturechange:2,gestureend:2,focus:2,blur:2,change:2,reset:2,select:2,submit:2,paste:2,input:2,load:2,unload:1,beforeunload:2,resize:1,move:1,DOMContentLoaded:1,readystatechange:1,error:1,abort:1,scroll:1}; +Element.Events={mousewheel:{base:(Browser.firefox)?"DOMMouseScroll":"mousewheel"}};if("onmouseenter" in document.documentElement){Element.NativeEvents.mouseenter=Element.NativeEvents.mouseleave=2; +}else{var a=function(b){var c=b.relatedTarget;if(c==null){return true;}if(!c){return false;}return(c!=this&&c.prefix!="xul"&&typeOf(this)!="document"&&!this.contains(c)); +};Element.Events.mouseenter={base:"mouseover",condition:a};Element.Events.mouseleave={base:"mouseout",condition:a};}if(!window.addEventListener){Element.NativeEvents.propertychange=2; +Element.Events.change={base:function(){var b=this.type;return(this.get("tag")=="input"&&(b=="radio"||b=="checkbox"))?"propertychange":"change";},condition:function(b){return this.type!="radio"||(b.event.propertyName=="checked"&&this.checked); +}};}})();(function(){var c=!!window.addEventListener;Element.NativeEvents.focusin=Element.NativeEvents.focusout=2;var k=function(l,m,n,o,p){while(p&&p!=l){if(m(p,o)){return n.call(p,o,p); +}p=document.id(p.parentNode);}};var a={mouseenter:{base:"mouseover"},mouseleave:{base:"mouseout"},focus:{base:"focus"+(c?"":"in"),capture:true},blur:{base:c?"blur":"focusout",capture:true}}; +var b="$delegation:";var i=function(l){return{base:"focusin",remove:function(m,o){var p=m.retrieve(b+l+"listeners",{})[o];if(p&&p.forms){for(var n=p.forms.length; +n--;){p.forms[n].removeEvent(l,p.fns[n]);}}},listen:function(x,r,v,n,t,s){var o=(t.get("tag")=="form")?t:n.target.getParent("form");if(!o){return;}var u=x.retrieve(b+l+"listeners",{}),p=u[s]||{forms:[],fns:[]},m=p.forms,w=p.fns; +if(m.indexOf(o)!=-1){return;}m.push(o);var q=function(y){k(x,r,v,y,t);};o.addEvent(l,q);w.push(q);u[s]=p;x.store(b+l+"listeners",u);}};};var d=function(l){return{base:"focusin",listen:function(m,n,p,q,r){var o={blur:function(){this.removeEvents(o); +}};o[l]=function(s){k(m,n,p,s,r);};q.target.addEvents(o);}};};if(!c){Object.append(a,{submit:i("submit"),reset:i("reset"),change:d("change"),select:d("select")}); +}var h=Element.prototype,f=h.addEvent,j=h.removeEvent;var e=function(l,m){return function(r,q,n){if(r.indexOf(":relay")==-1){return l.call(this,r,q,n); +}var o=Slick.parse(r).expressions[0][0];if(o.pseudos[0].key!="relay"){return l.call(this,r,q,n);}var p=o.tag;o.pseudos.slice(1).each(function(s){p+=":"+s.key+(s.value?"("+s.value+")":""); +});l.call(this,r,q);return m.call(this,p,o.pseudos[0].value,q);};};var g={addEvent:function(v,q,x){var t=this.retrieve("$delegates",{}),r=t[v];if(r){for(var y in r){if(r[y].fn==x&&r[y].match==q){return this; +}}}var p=v,u=q,o=x,n=a[v]||{};v=n.base||p;q=function(B){return Slick.match(B,u);};var w=Element.Events[p];if(w&&w.condition){var l=q,m=w.condition;q=function(C,B){return l(C,B)&&m.call(C,B,v); +};}var z=this,s=String.uniqueID();var A=n.listen?function(B,C){if(!C&&B&&B.target){C=B.target;}if(C){n.listen(z,q,x,B,C,s);}}:function(B,C){if(!C&&B&&B.target){C=B.target; +}if(C){k(z,q,x,B,C);}};if(!r){r={};}r[s]={match:u,fn:o,delegator:A};t[p]=r;return f.call(this,v,A,n.capture);},removeEvent:function(r,n,t,u){var q=this.retrieve("$delegates",{}),p=q[r]; +if(!p){return this;}if(u){var m=r,w=p[u].delegator,l=a[r]||{};r=l.base||m;if(l.remove){l.remove(this,u);}delete p[u];q[m]=p;return j.call(this,r,w);}var o,v; +if(t){for(o in p){v=p[o];if(v.match==n&&v.fn==t){return g.removeEvent.call(this,r,n,t,o);}}}else{for(o in p){v=p[o];if(v.match==n){g.removeEvent.call(this,r,n,v.fn,o); +}}}return this;}};[Element,Window,Document].invoke("implement",{addEvent:e(f,g.addEvent),removeEvent:e(j,g.removeEvent)});})();(function(){var h=document.createElement("div"),e=document.createElement("div"); +h.style.height="0";h.appendChild(e);var d=(e.offsetParent===h);h=e=null;var l=function(m){return k(m,"position")!="static"||a(m);};var i=function(m){return l(m)||(/^(?:table|td|th)$/i).test(m.tagName); +};Element.implement({scrollTo:function(m,n){if(a(this)){this.getWindow().scrollTo(m,n);}else{this.scrollLeft=m;this.scrollTop=n;}return this;},getSize:function(){if(a(this)){return this.getWindow().getSize(); +}return{x:this.offsetWidth,y:this.offsetHeight};},getScrollSize:function(){if(a(this)){return this.getWindow().getScrollSize();}return{x:this.scrollWidth,y:this.scrollHeight}; +},getScroll:function(){if(a(this)){return this.getWindow().getScroll();}return{x:this.scrollLeft,y:this.scrollTop};},getScrolls:function(){var n=this.parentNode,m={x:0,y:0}; +while(n&&!a(n)){m.x+=n.scrollLeft;m.y+=n.scrollTop;n=n.parentNode;}return m;},getOffsetParent:d?function(){var m=this;if(a(m)||k(m,"position")=="fixed"){return null; +}var n=(k(m,"position")=="static")?i:l;while((m=m.parentNode)){if(n(m)){return m;}}return null;}:function(){var m=this;if(a(m)||k(m,"position")=="fixed"){return null; +}try{return m.offsetParent;}catch(n){}return null;},getOffsets:function(){if(this.getBoundingClientRect&&!Browser.Platform.ios){var r=this.getBoundingClientRect(),o=document.id(this.getDocument().documentElement),q=o.getScroll(),t=this.getScrolls(),s=(k(this,"position")=="fixed"); +return{x:r.left.toInt()+t.x+((s)?0:q.x)-o.clientLeft,y:r.top.toInt()+t.y+((s)?0:q.y)-o.clientTop};}var n=this,m={x:0,y:0};if(a(this)){return m;}while(n&&!a(n)){m.x+=n.offsetLeft; +m.y+=n.offsetTop;if(Browser.firefox){if(!c(n)){m.x+=b(n);m.y+=g(n);}var p=n.parentNode;if(p&&k(p,"overflow")!="visible"){m.x+=b(p);m.y+=g(p);}}else{if(n!=this&&Browser.safari){m.x+=b(n); +m.y+=g(n);}}n=n.offsetParent;}if(Browser.firefox&&!c(this)){m.x-=b(this);m.y-=g(this);}return m;},getPosition:function(p){var q=this.getOffsets(),n=this.getScrolls(); +var m={x:q.x-n.x,y:q.y-n.y};if(p&&(p=document.id(p))){var o=p.getPosition();return{x:m.x-o.x-b(p),y:m.y-o.y-g(p)};}return m;},getCoordinates:function(o){if(a(this)){return this.getWindow().getCoordinates(); +}var m=this.getPosition(o),n=this.getSize();var p={left:m.x,top:m.y,width:n.x,height:n.y};p.right=p.left+p.width;p.bottom=p.top+p.height;return p;},computePosition:function(m){return{left:m.x-j(this,"margin-left"),top:m.y-j(this,"margin-top")}; +},setPosition:function(m){return this.setStyles(this.computePosition(m));}});[Document,Window].invoke("implement",{getSize:function(){var m=f(this);return{x:m.clientWidth,y:m.clientHeight}; +},getScroll:function(){var n=this.getWindow(),m=f(this);return{x:n.pageXOffset||m.scrollLeft,y:n.pageYOffset||m.scrollTop};},getScrollSize:function(){var o=f(this),n=this.getSize(),m=this.getDocument().body; +return{x:Math.max(o.scrollWidth,m.scrollWidth,n.x),y:Math.max(o.scrollHeight,m.scrollHeight,n.y)};},getPosition:function(){return{x:0,y:0};},getCoordinates:function(){var m=this.getSize(); +return{top:0,left:0,bottom:m.y,right:m.x,height:m.y,width:m.x};}});var k=Element.getComputedStyle;function j(m,n){return k(m,n).toInt()||0;}function c(m){return k(m,"-moz-box-sizing")=="border-box"; +}function g(m){return j(m,"border-top-width");}function b(m){return j(m,"border-left-width");}function a(m){return(/^(?:body|html)$/i).test(m.tagName); +}function f(m){var n=m.getDocument();return(!n.compatMode||n.compatMode=="CSS1Compat")?n.html:n.body;}})();Element.alias({position:"setPosition"});[Window,Document,Element].invoke("implement",{getHeight:function(){return this.getSize().y; +},getWidth:function(){return this.getSize().x;},getScrollTop:function(){return this.getScroll().y;},getScrollLeft:function(){return this.getScroll().x; +},getScrollHeight:function(){return this.getScrollSize().y;},getScrollWidth:function(){return this.getScrollSize().x;},getTop:function(){return this.getPosition().y; +},getLeft:function(){return this.getPosition().x;}});(function(){var f=this.Fx=new Class({Implements:[Chain,Events,Options],options:{fps:60,unit:false,duration:500,frames:null,frameSkip:true,link:"ignore"},initialize:function(g){this.subject=this.subject||this; +this.setOptions(g);},getTransition:function(){return function(g){return -(Math.cos(Math.PI*g)-1)/2;};},step:function(g){if(this.options.frameSkip){var h=(this.time!=null)?(g-this.time):0,i=h/this.frameInterval; +this.time=g;this.frame+=i;}else{this.frame++;}if(this.frame=(7-4*d)/11){e=c*c-Math.pow((11-6*d-11*f)/4,2);break;}}return e; +},Elastic:function(b,a){return Math.pow(2,10*--b)*Math.cos(20*b*Math.PI*(a&&a[0]||1)/3);}});["Quad","Cubic","Quart","Quint"].each(function(b,a){Fx.Transitions[b]=new Fx.Transition(function(c){return Math.pow(c,a+2); +});});(function(){var d=function(){},a=("onprogress" in new Browser.Request);var c=this.Request=new Class({Implements:[Chain,Events,Options],options:{url:"",data:"",headers:{"X-Requested-With":"XMLHttpRequest",Accept:"text/javascript, text/html, application/xml, text/xml, */*"},async:true,format:false,method:"post",link:"ignore",isSuccess:null,emulation:true,urlEncoded:true,encoding:"utf-8",evalScripts:false,evalResponse:false,timeout:0,noCache:false},initialize:function(e){this.xhr=new Browser.Request(); +this.setOptions(e);this.headers=this.options.headers;},onStateChange:function(){var e=this.xhr;if(e.readyState!=4||!this.running){return;}this.running=false; +this.status=0;Function.attempt(function(){var f=e.status;this.status=(f==1223)?204:f;}.bind(this));e.onreadystatechange=d;if(a){e.onprogress=e.onloadstart=d; +}clearTimeout(this.timer);this.response={text:this.xhr.responseText||"",xml:this.xhr.responseXML};if(this.options.isSuccess.call(this,this.status)){this.success(this.response.text,this.response.xml); +}else{this.failure();}},isSuccess:function(){var e=this.status;return(e>=200&&e<300);},isRunning:function(){return !!this.running;},processScripts:function(e){if(this.options.evalResponse||(/(ecma|java)script/).test(this.getHeader("Content-type"))){return Browser.exec(e); +}return e.stripScripts(this.options.evalScripts);},success:function(f,e){this.onSuccess(this.processScripts(f),e);},onSuccess:function(){this.fireEvent("complete",arguments).fireEvent("success",arguments).callChain(); +},failure:function(){this.onFailure();},onFailure:function(){this.fireEvent("complete").fireEvent("failure",this.xhr);},loadstart:function(e){this.fireEvent("loadstart",[e,this.xhr]); +},progress:function(e){this.fireEvent("progress",[e,this.xhr]);},timeout:function(){this.fireEvent("timeout",this.xhr);},setHeader:function(e,f){this.headers[e]=f; +return this;},getHeader:function(e){return Function.attempt(function(){return this.xhr.getResponseHeader(e);}.bind(this));},check:function(){if(!this.running){return true; +}switch(this.options.link){case"cancel":this.cancel();return true;case"chain":this.chain(this.caller.pass(arguments,this));return false;}return false;},send:function(o){if(!this.check(o)){return this; +}this.options.isSuccess=this.options.isSuccess||this.isSuccess;this.running=true;var l=typeOf(o);if(l=="string"||l=="element"){o={data:o};}var h=this.options; +o=Object.append({data:h.data,url:h.url,method:h.method},o);var j=o.data,f=String(o.url),e=o.method.toLowerCase();switch(typeOf(j)){case"element":j=document.id(j).toQueryString(); +break;case"object":case"hash":j=Object.toQueryString(j);}if(this.options.format){var m="format="+this.options.format;j=(j)?m+"&"+j:m;}if(this.options.emulation&&!["get","post"].contains(e)){var k="_method="+e; +j=(j)?k+"&"+j:k;e="post";}if(this.options.urlEncoded&&["post","put"].contains(e)){var g=(this.options.encoding)?"; charset="+this.options.encoding:"";this.headers["Content-type"]="application/x-www-form-urlencoded"+g; +}if(!f){f=document.location.pathname;}var i=f.lastIndexOf("/");if(i>-1&&(i=f.indexOf("#"))>-1){f=f.substr(0,i);}if(this.options.noCache){f+=(f.contains("?")?"&":"?")+String.uniqueID(); +}if(j&&e=="get"){f+=(f.contains("?")?"&":"?")+j;j=null;}var n=this.xhr;if(a){n.onloadstart=this.loadstart.bind(this);n.onprogress=this.progress.bind(this); +}n.open(e.toUpperCase(),f,this.options.async,this.options.user,this.options.password);if(this.options.user&&"withCredentials" in n){n.withCredentials=true; +}n.onreadystatechange=this.onStateChange.bind(this);Object.each(this.headers,function(q,p){try{n.setRequestHeader(p,q);}catch(r){this.fireEvent("exception",[p,q]); +}},this);this.fireEvent("request");n.send(j);if(!this.options.async){this.onStateChange();}else{if(this.options.timeout){this.timer=this.timeout.delay(this.options.timeout,this); +}}return this;},cancel:function(){if(!this.running){return this;}this.running=false;var e=this.xhr;e.abort();clearTimeout(this.timer);e.onreadystatechange=d; +if(a){e.onprogress=e.onloadstart=d;}this.xhr=new Browser.Request();this.fireEvent("cancel");return this;}});var b={};["get","post","put","delete","GET","POST","PUT","DELETE"].each(function(e){b[e]=function(g){var f={method:e}; +if(g!=null){f.data=g;}return this.send(f);};});c.implement(b);Element.Properties.send={set:function(e){var f=this.get("send").cancel();f.setOptions(e); +return this;},get:function(){var e=this.retrieve("send");if(!e){e=new c({data:this,link:"cancel",method:this.get("method")||"post",url:this.get("action")}); +this.store("send",e);}return e;}};Element.implement({send:function(e){var f=this.get("send");f.send({data:this,url:e||f.options.url});return this;}});})(); +Request.HTML=new Class({Extends:Request,options:{update:false,append:false,evalScripts:true,filter:false,headers:{Accept:"text/html, application/xml, text/xml, */*"}},success:function(f){var e=this.options,c=this.response; +c.html=f.stripScripts(function(h){c.javascript=h;});var d=c.html.match(/]*>([\s\S]*?)<\/body>/i);if(d){c.html=d[1];}var b=new Element("div").set("html",c.html); +c.tree=b.childNodes;c.elements=b.getElements(e.filter||"*");if(e.filter){c.tree=c.elements;}if(e.update){var g=document.id(e.update).empty();if(e.filter){g.adopt(c.elements); +}else{g.set("html",c.html);}}else{if(e.append){var a=document.id(e.append);if(e.filter){c.elements.reverse().inject(a);}else{a.adopt(b.getChildren());}}}if(e.evalScripts){Browser.exec(c.javascript); +}this.onSuccess(c.tree,c.elements,c.html,c.javascript);}});Element.Properties.load={set:function(a){var b=this.get("load").cancel();b.setOptions(a);return this; +},get:function(){var a=this.retrieve("load");if(!a){a=new Request.HTML({data:this,link:"cancel",update:this,method:"get"});this.store("load",a);}return a; +}};Element.implement({load:function(){this.get("load").send(Array.link(arguments,{data:Type.isObject,url:Type.isString}));return this;}});if(typeof JSON=="undefined"){this.JSON={}; +}(function(){var special={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"};var escape=function(chr){return special[chr]||"\\u"+("0000"+chr.charCodeAt(0).toString(16)).slice(-4); +};JSON.validate=function(string){string=string.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,""); +return(/^[\],:{}\s]*$/).test(string);};JSON.encode=JSON.stringify?function(obj){return JSON.stringify(obj);}:function(obj){if(obj&&obj.toJSON){obj=obj.toJSON(); +}switch(typeOf(obj)){case"string":return'"'+obj.replace(/[\x00-\x1f\\"]/g,escape)+'"';case"array":return"["+obj.map(JSON.encode).clean()+"]";case"object":case"hash":var string=[]; +Object.each(obj,function(value,key){var json=JSON.encode(value);if(json){string.push(JSON.encode(key)+":"+json);}});return"{"+string+"}";case"number":case"boolean":return""+obj; +case"null":return"null";}return null;};JSON.decode=function(string,secure){if(!string||typeOf(string)!="string"){return null;}if(secure||JSON.secure){if(JSON.parse){return JSON.parse(string); +}if(!JSON.validate(string)){throw new Error("JSON could not decode the input; security is enabled and the value is not secure.");}}return eval("("+string+")"); +};})();Request.JSON=new Class({Extends:Request,options:{secure:true},initialize:function(a){this.parent(a);Object.append(this.headers,{Accept:"application/json","X-Request":"JSON"}); +},success:function(c){var b;try{b=this.response.json=JSON.decode(c,this.options.secure);}catch(a){this.fireEvent("error",[c,a]);return;}if(b==null){this.onFailure(); +}else{this.onSuccess(b,c);}}});var Cookie=new Class({Implements:Options,options:{path:"/",domain:false,duration:false,secure:false,document:document,encode:true},initialize:function(b,a){this.key=b; +this.setOptions(a);},write:function(b){if(this.options.encode){b=encodeURIComponent(b);}if(this.options.domain){b+="; domain="+this.options.domain;}if(this.options.path){b+="; path="+this.options.path; +}if(this.options.duration){var a=new Date();a.setTime(a.getTime()+this.options.duration*24*60*60*1000);b+="; expires="+a.toGMTString();}if(this.options.secure){b+="; secure"; +}this.options.document.cookie=this.key+"="+b;return this;},read:function(){var a=this.options.document.cookie.match("(?:^|;)\\s*"+this.key.escapeRegExp()+"=([^;]*)"); +return(a)?decodeURIComponent(a[1]):null;},dispose:function(){new Cookie(this.key,Object.merge({},this.options,{duration:-1})).write("");return this;}}); +Cookie.write=function(b,c,a){return new Cookie(b,a).write(c);};Cookie.read=function(a){return new Cookie(a).read();};Cookie.dispose=function(b,a){return new Cookie(b,a).dispose(); +};(function(i,k){var l,f,e=[],c,b,d=k.createElement("div");var g=function(){clearTimeout(b);if(l){return;}Browser.loaded=l=true;k.removeListener("DOMContentLoaded",g).removeListener("readystatechange",a); +k.fireEvent("domready");i.fireEvent("domready");};var a=function(){for(var m=e.length;m--;){if(e[m]()){g();return true;}}return false;};var j=function(){clearTimeout(b); +if(!a()){b=setTimeout(j,10);}};k.addListener("DOMContentLoaded",g);var h=function(){try{d.doScroll();return true;}catch(m){}return false;};if(d.doScroll&&!h()){e.push(h); +c=true;}if(k.readyState){e.push(function(){var m=k.readyState;return(m=="loaded"||m=="complete");});}if("onreadystatechange" in k){k.addListener("readystatechange",a); +}else{c=true;}if(c){j();}Element.Events.domready={onAdd:function(m){if(l){m.call(this);}}};Element.Events.load={base:"load",onAdd:function(m){if(f&&this==i){m.call(this); +}},condition:function(){if(this==i){g();delete Element.Events.load;}return true;}};i.addEvent("load",function(){f=true;});})(window,document);(function(){var Swiff=this.Swiff=new Class({Implements:Options,options:{id:null,height:1,width:1,container:null,properties:{},params:{quality:"high",allowScriptAccess:"always",wMode:"window",swLiveConnect:true},callBacks:{},vars:{}},toElement:function(){return this.object; +},initialize:function(path,options){this.instance="Swiff_"+String.uniqueID();this.setOptions(options);options=this.options;var id=this.id=options.id||this.instance; +var container=document.id(options.container);Swiff.CallBacks[this.instance]={};var params=options.params,vars=options.vars,callBacks=options.callBacks; +var properties=Object.append({height:options.height,width:options.width},options.properties);var self=this;for(var callBack in callBacks){Swiff.CallBacks[this.instance][callBack]=(function(option){return function(){return option.apply(self.object,arguments); +};})(callBacks[callBack]);vars[callBack]="Swiff.CallBacks."+this.instance+"."+callBack;}params.flashVars=Object.toQueryString(vars);if(Browser.ie){properties.classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"; +params.movie=path;}else{properties.type="application/x-shockwave-flash";}properties.data=path;var build='';}}build+="";this.object=((container)?container.empty():new Element("div")).set("html",build).firstChild; +},replaces:function(element){element=document.id(element,true);element.parentNode.replaceChild(this.toElement(),element);return this;},inject:function(element){document.id(element,true).appendChild(this.toElement()); +return this;},remote:function(){return Swiff.remote.apply(Swiff,[this.toElement()].append(arguments));}});Swiff.CallBacks={};Swiff.remote=function(obj,fn){var rs=obj.CallFunction(''+__flash__argumentsToXML(arguments,2)+""); +return eval(rs);};})(); \ No newline at end of file diff --git a/sources/searx/static/js/searx.js b/sources/searx/static/js/searx.js new file mode 100644 index 0000000..47dc722 --- /dev/null +++ b/sources/searx/static/js/searx.js @@ -0,0 +1,45 @@ +if(searx.autocompleter) { + window.addEvent('domready', function() { + new Autocompleter.Request.JSON('q', '/autocompleter', { + postVar:'q', + postData:{ + 'format': 'json' + }, + ajaxOptions:{ + timeout: 5 // Correct option? + }, + 'minLength': 4, + // 'selectMode': 'type-ahead', + cache: true, + delay: 300 + }); + }); +} + +(function (w, d) { + 'use strict'; + function addListener(el, type, fn) { + if (el.addEventListener) { + el.addEventListener(type, fn, false); + } else { + el.attachEvent('on' + type, fn); + } + } + + function placeCursorAtEnd() { + if (this.setSelectionRange) { + var len = this.value.length * 2; + this.setSelectionRange(len, len); + } + } + + addListener(w, 'load', function () { + var qinput = d.getElementById('q'); + if (qinput !== null && qinput.value === "") { + addListener(qinput, 'focus', placeCursorAtEnd); + qinput.focus(); + } + }); + +})(window, document); + diff --git a/sources/searx/static/less/autocompleter.less b/sources/searx/static/less/autocompleter.less new file mode 100644 index 0000000..db9601a --- /dev/null +++ b/sources/searx/static/less/autocompleter.less @@ -0,0 +1,61 @@ +/* + * searx, A privacy-respecting, hackable metasearch engine + */ + +ul { + &.autocompleter-choices { + position: absolute; + margin: 0; + padding: 0; + list-style: none; + border: 1px solid @color-autocompleter-choices-border; + border-left-color: @color-autocompleter-choices-border-left-right; + border-right-color: @color-autocompleter-choices-border-left-right; + border-bottom-color: @color-autocompleter-choices-border-bottom; + text-align: left; + font-family: Verdana, Geneva, Arial, Helvetica, sans-serif; + z-index: 50; + background-color: @color-autocompleter-choices-background; + color: @color-autocompleter-choices-font; + + li { + position: relative; + margin: -2px 0 0 0; + padding: 0.2em 1.5em 0.2em 1em; + display: block; + float: none !important; + cursor: pointer; + font-weight: normal; + white-space: nowrap; + font-size: 1em; + line-height: 1.5em; + + &.autocompleter-selected { + background-color: @color-autocompleter-selected-background; + color: @color-autocompleter-selected-font; + + span.autocompleter-queried { + color: @color-autocompleter-selected-queried-font; + } + } + } + + span.autocompleter-queried { + display: inline; + float: none; + font-weight: bold; + margin: 0; + padding: 0; + } + } +} + +/*.autocompleter-loading { + //background-image: url(images/spinner.gif); + background-repeat: no-repeat; + background-position: right 50%; +}*/ + +/*textarea.autocompleter-loading { + background-position: right bottom; +}*/ diff --git a/sources/searx/static/less/definitions.less b/sources/searx/static/less/definitions.less new file mode 100644 index 0000000..3e0b657 --- /dev/null +++ b/sources/searx/static/less/definitions.less @@ -0,0 +1,113 @@ +/* + * searx, A privacy-respecting, hackable metasearch engine + * + * To change the colors of the site, simple edit this variables + */ + +/// Basic Colors + +@color-base: #3498DB; +@color-base-dark: #2980B9; +@color-base-light: #ECF0F1; +@color-highlight: #094089; +@color-black: #000000; + +/// General + +@color-font: #444; + +@color-url-font: #1a11be; +@color-url-visited-font: #8E44AD; +@results-width: 50em; + + +/// Start-Screen + +// hmarg +@color-hmarg-border: @color-base; +@color-hmarg-font: @color-base; +@color-hmarg-font-hover: @color-base; + + +/// Search-Input + +@color-search-border: @color-base; +@color-search-background: #FFF; +@color-search-font: #222; + +/// Autocompleter + +@color-autocompleter-choices-background: #FFF; +@color-autocompleter-choices-border: @color-base; +@color-autocompleter-choices-border-left-right: @color-base; +@color-autocompleter-choices-border-bottom: @color-base; + +@color-autocompleter-choices-font: #444; + +// Selected +@color-autocompleter-selected-background: #444; +@color-autocompleter-selected-font: #FFF; +@color-autocompleter-selected-queried-font: #9FCFFF; + +/// Categories + +@color-categories-item-selected: @color-base; +@color-categories-item-selected-font: #FFF; + +@color-categories-item-border-selected: @color-base-dark; +@color-categories-item-border-unselected: #E8E7E6; +@color-categories-item-border-unselected-hover: @color-base; + + +/// Results + +@color-suggestions-button-background: @color-base; +@color-suggestions-button-font: #FFF; + +@color-download-button-background: @color-base; +@color-download-button-font: #FFF; + +@color-result-search-background: @color-base-light; + +@color-result-definition-border: gray; +@color-result-torrent-border: lightgray; +@color-result-top-border: #E8E7E6; + +// Link to result +@color-result-link-font: @color-base-dark; +@color-result-link-visited-font: @color-url-visited-font; + +// Url to result +@color-result-url-font: #C0392B; + +// Publish Date +@color-result-publishdate-font: #888; + +// Images +@color-result-image-span-background-hover: rgba(0, 0, 0, 0.6); +@color-result-image-span-font: #FFF; + +// Search-URL +@color-result-search-url-border: #888; +@color-result-search-url-font: #444; + + +/// Settings + +@color-settings-fieldset: @color-base; +@color-settings-tr-hover: #DDD; + +// Labels +@color-settings-label-allowed-background: #E74C3C; +@color-settings-label-allowed-font: #FFF; + +@color-settings-label-deny-background: #2ECC71; +@color-settings-label-deny-font: @color-font; + +@color-settings-return-background: @color-base; +@color-settings-return-font: #FFF; + +/// Other + +@color-engines-font: #888; +@color-percentage-div-background: #444; diff --git a/sources/searx/static/less/mixins.less b/sources/searx/static/less/mixins.less new file mode 100644 index 0000000..dbccce6 --- /dev/null +++ b/sources/searx/static/less/mixins.less @@ -0,0 +1,27 @@ +/* + * searx, A privacy-respecting, hackable metasearch engine + */ + +// Mixins + +.text-size-adjust (@property: 100%) { + -webkit-text-size-adjust: @property; + -ms-text-size-adjust: @property; + -moz-text-size-adjust: @property; + text-size-adjust: @property; +} + +.rounded-corners (@radius: 4px) { + -webkit-border-radius: @radius; + -moz-border-radius: @radius; + border-radius: @radius; +} + +.user-select () { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} diff --git a/sources/searx/static/less/search.less b/sources/searx/static/less/search.less new file mode 100644 index 0000000..d285ca7 --- /dev/null +++ b/sources/searx/static/less/search.less @@ -0,0 +1,68 @@ +/* + * searx, A privacy-respecting, hackable metasearch engine + */ + +.search { + padding: 0; + margin: 0; + .checkbox_container label { + font-size: 0.9em; + border-bottom: 2px solid @color-categories-item-border-unselected; + } + + .checkbox_container label:hover { + border-bottom: 2px solid @color-categories-item-border-unselected-hover; + } + + .checkbox_container input[type="checkbox"]:checked + label { + border-bottom: 2px solid @color-categories-item-border-selected; + } +} + +#search_wrapper { + position: relative; + width: @results-width; + padding: 10px; +} + +.center #search_wrapper { + margin-left: auto; + margin-right: auto; +} + +.q { + background: none repeat scroll 0 0 @color-search-background; + border: 1px solid @color-search-border; + color: @color-search-font; + font-size: 16px; + height: 28px; + margin: 0; + outline: medium none; + padding: 2px; + padding-left: 8px; + padding-right: 0px !important; + width: 100%; + z-index: 2; +} + +#search_submit { + position: absolute; + top: 13px; + right: 1px; + padding: 0; + border: 0; + background: url('../img/search-icon.png') no-repeat; + background-size: 24px 24px; + opacity: 0.8; + width: 24px; + height: 30px; + font-size: 0; +} + +@media screen and (max-width: @results-width) { + #search_wrapper { + width: 90%; + clear:both; + overflow: hidden + } +} diff --git a/sources/searx/static/less/style.less b/sources/searx/static/less/style.less new file mode 100644 index 0000000..f1f729b --- /dev/null +++ b/sources/searx/static/less/style.less @@ -0,0 +1,538 @@ +/* + * searx, A privacy-respecting, hackable metasearch engine + * + * To convert "style.less" to "style.css" run: $make styles + */ + +@import "definitions.less"; + +@import "mixins.less"; + +// Main LESS-Code + +html { + font-family: sans-serif; + font-size: 0.9em; + .text-size-adjust; + color: @color-font; + padding: 0; + margin: 0; +} + +body, #container { + padding: 0; + margin: 0; +} + +#container { + width: 100%; + position: absolute; + top: 0; +} + +// Search-Field + +@import "search.less"; + +// Autocompleter + +@import "autocompleter.less"; + +.row { + max-width: 800px; + margin: 20px auto; + text-align: justify; + + h1 { + font-size: 3em; + margin-top: 50px; + } + + p { + padding: 0 10px; + max-width: 700px; + } + + h3,ul { + margin: 4px 8px; + } +} + +.hmarg { + margin: 0 20px; + border: 1px solid @color-hmarg-border; + padding: 4px 10px; +} + +a { + &:link.hmarg { + color: @color-hmarg-font; + } + + &:visited.hmarg { + color: @color-hmarg-font; + } + + &:active.hmarg { + color: @color-hmarg-font-hover; + } + + &:hover.hmarg { + color: @color-hmarg-font-hover; + } +} + +.top_margin { + margin-top: 60px; +} + +.center { + text-align: center; +} + +h1 { + font-size: 5em; +} + +div.title { + background: url('../img/searx.png') no-repeat; + width: 100%; + background-position: center; + + h1 { + visibility: hidden; + } +} + +input[type="submit"] { + padding: 2px 6px; + margin: 2px 4px; + display: inline-block; + background: @color-download-button-background; + color: @color-download-button-font; + .rounded-corners; + border: 0; + cursor: pointer; +} + +input[type="checkbox"] { + visibility: hidden; +} + +fieldset { + margin: 8px; + border: 1px solid @color-settings-fieldset; +} + +#categories { + margin: 0 10px; +} + +.checkbox_container { + display: inline-block; + position: relative; + margin: 0 3px; + padding: 0px; + + input { + display: none; + } +} + +.checkbox_container label, .engine_checkbox label { + cursor: pointer; + padding: 4px 10px; + margin: 0; + display: block; + text-transform: capitalize; + .user-select; +} + +.checkbox_container input[type="checkbox"]:checked + label { + background: @color-categories-item-selected; + color: @color-categories-item-selected-font; +} + +.engine_checkbox { + padding: 4px; +} + +label { + &.allow { + background: @color-settings-label-allowed-background; + padding: 4px 8px; + color: @color-settings-label-allowed-font; + display: none; + } + + &.deny { + background: @color-settings-label-deny-background; + padding: 4px 8px; + color: @color-settings-label-deny-font; + display: inline; + } +} + +.engine_checkbox input[type="checkbox"]:checked + label { + &:nth-child(2) + label { + display: none; + } + + &.allow { + display: inline; + } +} + +a { + text-decoration: none; + color: @color-url-font; + + &:visited { + color: @color-url-visited-font; + } +} + +.result { + margin: 19px 0 18px 0; + padding: 0; + clear: both; +} + +.result_title { + margin-bottom: 0; + + a { + color: @color-result-link-font; + font-weight: normal; + font-size: 1.1em; + + &:hover { + text-decoration: underline; + } + + &:visited { + color: @color-result-link-visited-font; + } + } +} + +.result { + h3 { + font-size: 1em; + word-wrap:break-word; + margin: 5px 0 1px 0; + padding: 0 + } + + .content { + font-size: 0.8em; + margin: 0; + padding: 0; + max-width: 54em; + word-wrap:break-word; + line-height: 1.24; + } + + .url { + font-size: 0.8em; + margin: 3px 0 0 0; + padding: 0; + max-width: 54em; + word-wrap:break-word; + color: @color-result-url-font; + } + + .published_date { + font-size: 0.8em; + color: @color-result-publishdate-font; + margin: 5px 20px; + } +} + +.engines { + color: @color-engines-font; +} + +.small_font { + font-size: 0.8em; +} + +.small p { + margin: 2px 0; +} + +.right { + float: right; +} + +.invisible { + display: none; +} + +.left { + float: left; +} + +.highlight { + color: @color-highlight; +} + +.content .highlight { + color: @color-black; +} + +.image_result { + float: left; + margin: 10px 10px; + position: relative; + height: 160px; + + img { + border: 0; + height: 160px; + } + + p { + margin: 0; + padding: 0; + + span a { + display: none; + color: @color-result-image-span-font; + } + + &:hover span a { + display: block; + position: absolute; + bottom: 0; + right: 0; + padding: 4px; + background-color: @color-result-image-span-background-hover; + font-size: 0.7em; + } + } +} + +.torrent_result { + border-left: 10px solid @color-result-torrent-border; + padding-left: 3px; + + p { + margin: 3px; + font-size: 0.8em; + } +} + +.definition_result { + border-left: 10px solid @color-result-definition-border; + padding-left: 3px; +} + +.percentage { + position: relative; + width: 300px; + + div { + background: @color-percentage-div-background; + } +} + +table { + width: 100%; +} + +td { + padding: 0 4px; +} + +tr { + &:hover { + background: @color-settings-tr-hover; + } +} + +#results { + margin: auto; + padding: 0; + width: @results-width; + margin-bottom: 20px; +} + +#sidebar { + position: absolute; + top: 100px; + right: 10px; + margin: 0 2px 5px 5px; + padding: 0 2px 2px 2px; + width: 14em; + + input { + padding: 0; + margin: 3px; + font-size: 0.8em; + display: inline-block; + background: transparent; + color: @color-result-search-url-font; + cursor: pointer; + } + input[type="submit"] { + text-decoration: underline; + } +} + +#suggestions { + + margin-top: 20px; + + span { + display: inline; + margin: 0 2px 2px 2px; + padding: 0; + } + input { + padding: 0; + margin: 3px; + font-size: 0.8em; + display: inline-block; + background: transparent; + color: @color-result-search-url-font; + cursor: pointer; + } + input[type="submit"] { + text-decoration: underline; + } + + form { + display: inline; + } +} + +#search_url { + margin-top: 8px; + + input { + border: 1px solid @color-result-search-url-border; + padding: 4px; + color: @color-result-search-url-font; + width: 14em; + display: block; + margin: 4px; + font-size: 0.8em; + } +} + +#preferences { + top: 10px; + padding: 0; + border: 0; + background: url('../img/preference-icon.png') no-repeat; + background-size: 28px 28px; + opacity: 0.8; + width: 28px; + height: 30px; + display: block; + + * { + display: none; + } +} + +#pagination { + clear: both; + width: 40em; +} + +#apis { + margin-top: 8px; + clear: both; +} + +@media screen and (max-width: @results-width) { + + #categories { + font-size: 90%; + clear: both; + + .checkbox_container { + margin-top: 2px; + margin: auto; + } + } + + #results { + margin: auto; + padding: 0; + width: 90%; + } + + .checkbox_container { + display: block; + width: 90%; + //float: left; + + label { + border-bottom: 0; + } + } +} + +@media screen and (max-width: 70em) { + .right { + display: none; + postion: fixed !important; + top: 100px; + right: 0px; + } + + #sidebar { + position: static; + max-width: @results-width; + margin: 0 0 2px 0; + padding: 0; + float: none; + border: none; + width: auto; + input { + border: 0; + } + } + + #apis { + display: none; + } + + #search_url { + display: none; + } + + .result { + border-top: 1px solid @color-result-top-border; + margin: 7px 0 6px 0; + + img { + max-width: 90%; + width: auto; + height: auto + } + } +} + +.favicon { + float: left; + margin-right: 4px; + margin-top: 2px; +} + +.preferences_back { + background: none repeat scroll 0 0 @color-settings-return-background; + border: 0 none; + .rounded-corners; + cursor: pointer; + display: inline-block; + margin: 2px 4px; + padding: 4px 6px; + + a { + color: @color-settings-return-font; + } +} diff --git a/sources/searx/templates/about.html b/sources/searx/templates/about.html new file mode 100644 index 0000000..0ddf122 --- /dev/null +++ b/sources/searx/templates/about.html @@ -0,0 +1,66 @@ +{% extends 'base.html' %} +{% block content %} +{% include 'github_ribbon.html' %} +
+

About searx

+ +

Searx is a metasearch engine, aggregating the results of other search engines while not storing information about its users. +

+

Why use Searx?

+
    +
  • Searx may not offer you as personalised results as Google, but it doesn't generate a profile about you
  • +
  • Searx doesn't care about what you search for, never shares anything with a third party, and it can't be used to compromise you
  • +
  • Searx is free software, the code is 100% open and you can help to make it better. See more on github
  • +
+

If you do care about privacy, want to be a conscious user, or otherwise believe + in digital freedom, make Searx your default search engine or run it on your own server

+ +

Technical details - How does it work?

+ +

Searx is a metasearch engine, +inspired by the seeks project.
+It provides basic privacy by mixing your queries with searches on other platforms without storing search data. Queries are made using a POST request on every browser (except chrome*). Therefore they show up in neither our logs, nor your url history. In case of Chrome* users there is an exception, Searx uses the search bar to perform GET requests.
+Searx can be added to your browser's search bar; moreover, it can be set as the default search engine. +

+ +

How can I make it my own?

+ +

Searx appreciates your concern regarding logs, so take the code and run it yourself!
Add your Searx to this list to help other people reclaim their privacy and make the Internet freer! +
The more decentralized the Internet, is the more freedom we have!

+ + +

More about searx

+ + + + +
+ +

FAQ

+ +

How to add to firefox?

+

Install searx as a search engine on any version of Firefox! (javascript required)

+ +

Developer FAQ

+ +

New engines?

+ +

Don't forget to restart searx after config edit!

+ +

Installation/WSGI support?

+

See the installation and setup wiki page

+ +

How to debug engines?

+

Stats page contains some useful data about the engines used.

+ +
+{% endblock %} diff --git a/sources/searx/templates/base.html b/sources/searx/templates/base.html new file mode 100644 index 0000000..da5ae90 --- /dev/null +++ b/sources/searx/templates/base.html @@ -0,0 +1,32 @@ + + + + + + + + {% block title %}{% endblock %}searx + + + {% block styles %} + {% endblock %} + {% block head %} + + {% endblock %} + + + +
+{% block content %} +{% endblock %} +{% if autocomplete %} + + +{% endif %} + +
+ + diff --git a/sources/searx/templates/categories.html b/sources/searx/templates/categories.html new file mode 100644 index 0000000..57e63c8 --- /dev/null +++ b/sources/searx/templates/categories.html @@ -0,0 +1,7 @@ +
+{% for category in categories %} +
+ +
+{% endfor %} +
diff --git a/sources/searx/templates/github_ribbon.html b/sources/searx/templates/github_ribbon.html new file mode 100644 index 0000000..bdd9cf1 --- /dev/null +++ b/sources/searx/templates/github_ribbon.html @@ -0,0 +1,3 @@ + + Fork me on GitHub + diff --git a/sources/searx/templates/index.html b/sources/searx/templates/index.html new file mode 100644 index 0000000..57b67ef --- /dev/null +++ b/sources/searx/templates/index.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} +{% block content %} +
+

searx

+ {% include 'search.html' %} +

+ {{ _('about') }} + {{ _('preferences') }} +

+
+{% endblock %} diff --git a/sources/searx/templates/opensearch.xml b/sources/searx/templates/opensearch.xml new file mode 100644 index 0000000..f39283f --- /dev/null +++ b/sources/searx/templates/opensearch.xml @@ -0,0 +1,27 @@ + + + searx + Search searx + UTF-8 + searx metasearch + {% if opensearch_method == 'get' %} + + {% if autocomplete %} + + + + + {% endif %} + {% else %} + + + + {% if autocomplete %} + + + + + + {% endif %} + {% endif %} + diff --git a/sources/searx/templates/opensearch_response_rss.xml b/sources/searx/templates/opensearch_response_rss.xml new file mode 100644 index 0000000..5673eb2 --- /dev/null +++ b/sources/searx/templates/opensearch_response_rss.xml @@ -0,0 +1,23 @@ + + + + Searx search: {{ q }} + {{ base_url }}?q={{ q }} + Search results for "{{ q }}" - searx + {{ number_of_results }} + 1 + {{ number_of_results }} + + diff --git a/sources/searx/templates/preferences.html b/sources/searx/templates/preferences.html new file mode 100644 index 0000000..eeb8657 --- /dev/null +++ b/sources/searx/templates/preferences.html @@ -0,0 +1,91 @@ +{% extends "base.html" %} +{% block head %} {% endblock %} +{% block content %} +
+

{{ _('Preferences') }}

+ +
+
+ {{ _('Default categories') }} +

+ {% include 'categories.html' %} +

+
+
+ {{ _('Search language') }} +

+ +

+
+
+ {{ _('Interface language') }} +

+ +

+
+
+ {{ _('Autocomplete') }} +

+ +

+
+
+ {{ _('Method') }} +

+ +

+
+
+ {{ _('Currently used search engines') }} + + + + + + + + {% for (categ,search_engines) in categs %} + {% for search_engine in search_engines %} + + {% if not search_engine.private %} + + + + + + {% endif %} + {% endfor %} + {% endfor %} +
{{ _('Engine name') }}{{ _('Category') }}{{ _('Allow') }} / {{ _('Block') }}
{{ search_engine.name }} ({{ shortcuts[search_engine.name] }}){{ _(categ) }} + + + +
+
+

{{ _('These settings are stored in your cookies, this allows us not to store this data about you.') }} +
+ {{ _("These cookies serve your sole convenience, we don't use these cookies to track you.") }} +

+ + + +
+
+{% endblock %} diff --git a/sources/searx/templates/result_templates/default.html b/sources/searx/templates/result_templates/default.html new file mode 100644 index 0000000..e0711b7 --- /dev/null +++ b/sources/searx/templates/result_templates/default.html @@ -0,0 +1,13 @@ +
+ + {% if result['favicon'] %} + + {% endif %} + +
+

{{ result.title|safe }}

+ {% if result.publishedDate %}

{{ result.publishedDate }}

{% endif %} +

{% if result.content %}{{ result.content|safe }}
{% endif %}

+

{{ result.pretty_url }}

+
+
diff --git a/sources/searx/templates/result_templates/images.html b/sources/searx/templates/result_templates/images.html new file mode 100644 index 0000000..1f15ff2 --- /dev/null +++ b/sources/searx/templates/result_templates/images.html @@ -0,0 +1,6 @@ +
+

+ + original context +

+
diff --git a/sources/searx/templates/result_templates/torrent.html b/sources/searx/templates/result_templates/torrent.html new file mode 100644 index 0000000..6c62793 --- /dev/null +++ b/sources/searx/templates/result_templates/torrent.html @@ -0,0 +1,7 @@ +
+

{{ result.title|safe }}

+ {% if result.content %}

{{ result.content|safe }}

{% endif %} +

Seed: {{ result.seed }}, Leech: {{ result.leech }}

+

magnet link

+

{{ result.pretty_url }}

+
diff --git a/sources/searx/templates/result_templates/videos.html b/sources/searx/templates/result_templates/videos.html new file mode 100644 index 0000000..ab869a6 --- /dev/null +++ b/sources/searx/templates/result_templates/videos.html @@ -0,0 +1,12 @@ +
+ {% if result['favicon'] %} + + {% endif %} + +

+

{{ result.title|safe }}

+ {% if result.publishedDate %}

{{ result.publishedDate }}

{% endif %} +  {{ result.title }} +

{{ result.url }}

+

+
diff --git a/sources/searx/templates/results.html b/sources/searx/templates/results.html new file mode 100644 index 0000000..608cfb2 --- /dev/null +++ b/sources/searx/templates/results.html @@ -0,0 +1,79 @@ +{% extends "base.html" %} +{% block title %}{{ q }} - {% endblock %} +{% block content %} + + +
+ + + {% if suggestions %} +
{{ _('Suggestions') }} + {% for suggestion in suggestions %} +
+ + +
+ {% endfor %} +
+ {% endif %} + + {% for result in results %} + {% if result['template'] %} + {% include 'result_templates/'+result['template'] %} + {% else %} + {% include 'result_templates/default.html' %} + {% endif %} + {% endfor %} + + {% if paging %} + + {% endif %} +
+{% endblock %} diff --git a/sources/searx/templates/search.html b/sources/searx/templates/search.html new file mode 100644 index 0000000..30d1568 --- /dev/null +++ b/sources/searx/templates/search.html @@ -0,0 +1,7 @@ +
+
+ + +
+ {% include 'categories.html' %} +
diff --git a/sources/searx/templates/stats.html b/sources/searx/templates/stats.html new file mode 100644 index 0000000..cb5757b --- /dev/null +++ b/sources/searx/templates/stats.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} +{% block head %} {% endblock %} +{% block content %} +

{{ _('Engine stats') }}

+ +{% for stat_name,stat_category in stats %} +
+ + + + + {% for engine in stat_category %} + + + + + + {% endfor %} +
{{ stat_name }}
{{ engine.name }}{{ '%.02f'|format(engine.avg) }}
 
+
+{% endfor %} +{% endblock %} diff --git a/sources/searx/testing.py b/sources/searx/testing.py new file mode 100644 index 0000000..51c44d8 --- /dev/null +++ b/sources/searx/testing.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +"""Shared testing code.""" + +from plone.testing import Layer +from unittest2 import TestCase + + +import os +import subprocess + + +class SearxTestLayer: + """Base layer for non-robot tests.""" + + __name__ = u'SearxTestLayer' + + def setUp(cls): + pass + setUp = classmethod(setUp) + + def tearDown(cls): + pass + tearDown = classmethod(tearDown) + + def testSetUp(cls): + pass + testSetUp = classmethod(testSetUp) + + def testTearDown(cls): + pass + testTearDown = classmethod(testTearDown) + + +class SearxRobotLayer(Layer): + """Searx Robot Test Layer""" + + def setUp(self): + os.setpgrp() # create new process group, become its leader + + # get program paths + webapp = os.path.join( + os.path.abspath(os.path.dirname(os.path.realpath(__file__))), + 'webapp.py' + ) + exe = os.path.abspath(os.path.dirname(__file__) + '/../bin/py') + + # set robot settings path + os.environ['SEARX_SETTINGS_PATH'] = os.path.abspath( + os.path.dirname(__file__) + '/settings_robot.yml') + + # run the server + self.server = subprocess.Popen( + [exe, webapp], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT + ) + + def tearDown(self): + # send TERM signal to all processes in my group, to stop subprocesses + os.killpg(os.getpgid(self.server.pid), 15) + + # remove previously set environment variable + del os.environ['SEARX_SETTINGS_PATH'] + + +SEARXROBOTLAYER = SearxRobotLayer() + + +class SearxTestCase(TestCase): + """Base test case for non-robot tests.""" + + layer = SearxTestLayer diff --git a/sources/searx/tests/__init__.py b/sources/searx/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sources/searx/tests/robot/__init__.py b/sources/searx/tests/robot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sources/searx/tests/robot/test_basic.robot b/sources/searx/tests/robot/test_basic.robot new file mode 100644 index 0000000..1b8e78f --- /dev/null +++ b/sources/searx/tests/robot/test_basic.robot @@ -0,0 +1,44 @@ +*** Settings *** +Library Selenium2Library timeout=10 implicit_wait=0.5 +Test Setup Open Browser http://localhost:11111/ +Test Teardown Close All Browsers + + +*** Test Cases *** +Front page + Page Should Contain about + Page Should Contain preferences + +About page + Click Element link=about + Page Should Contain Why use Searx? + Page Should Contain Element link=search engines + +Preferences page + Click Element link=preferences + Page Should Contain Preferences + Page Should Contain Default categories + Page Should Contain Currently used search engines + Page Should Contain dummy_dummy + Page Should Contain general_dummy + +Switch category + Go To http://localhost:11111/preferences + Page Should Contain Checkbox category_general + Page Should Contain Checkbox category_dummy + Click Element xpath=//*[.="general"] + Click Element xpath=//*[.="dummy"] + Submit Form id=search_form + Location Should Be http://localhost:11111/ + Checkbox Should Not Be Selected category_general + Checkbox Should Be Selected category_dummy + +Change language + Page Should Contain about + Page Should Contain preferences + Go To http://localhost:11111/preferences + Select From List locale hu + Submit Form id=search_form + Location Should Be http://localhost:11111/ + Page Should Contain rólunk + Page Should Contain beállítások diff --git a/sources/searx/tests/test_robot.py b/sources/searx/tests/test_robot.py new file mode 100644 index 0000000..b48153f --- /dev/null +++ b/sources/searx/tests/test_robot.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +import os +import unittest2 as unittest +from plone.testing import layered +from robotsuite import RobotTestSuite +from searx.testing import SEARXROBOTLAYER + + +def test_suite(): + suite = unittest.TestSuite() + current_dir = os.path.abspath(os.path.dirname(__file__)) + robot_dir = os.path.join(current_dir, 'robot') + tests = [ + os.path.join('robot', f) for f in + os.listdir(robot_dir) if f.endswith('.robot') and + f.startswith('test_') + ] + for test in tests: + suite.addTests([ + layered(RobotTestSuite(test), layer=SEARXROBOTLAYER), + ]) + return suite diff --git a/sources/searx/tests/test_webapp.py b/sources/searx/tests/test_webapp.py new file mode 100644 index 0000000..1d12b3a --- /dev/null +++ b/sources/searx/tests/test_webapp.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- + +import json +from urlparse import ParseResult +from mock import patch +from searx import webapp +from searx.testing import SearxTestCase + + +class ViewsTestCase(SearxTestCase): + + def setUp(self): + webapp.app.config['TESTING'] = True # to get better error messages + self.app = webapp.app.test_client() + + # set some defaults + self.test_results = [ + { + 'content': 'first test content', + 'title': 'First Test', + 'url': 'http://first.test.xyz', + 'engines': ['youtube', 'startpage'], + 'engine': 'startpage', + 'parsed_url': ParseResult(scheme='http', netloc='first.test.xyz', path='/', params='', query='', fragment=''), # noqa + }, { + 'content': 'second test content', + 'title': 'Second Test', + 'url': 'http://second.test.xyz', + 'engines': ['youtube', 'startpage'], + 'engine': 'youtube', + 'parsed_url': ParseResult(scheme='http', netloc='second.test.xyz', path='/', params='', query='', fragment=''), # noqa + }, + ] + + self.maxDiff = None # to see full diffs + + def test_index_empty(self): + result = self.app.post('/') + self.assertEqual(result.status_code, 200) + self.assertIn('

searx

', result.data) + + @patch('searx.webapp.do_search') + def test_index_html(self, search): + search.return_value = ( + self.test_results, + set() + ) + result = self.app.post('/', data={'q': 'test'}) + self.assertIn( + '

First Test

', # noqa + result.data + ) + self.assertIn( + '

first test content

', + result.data + ) + + @patch('searx.webapp.do_search') + def test_index_json(self, search): + search.return_value = ( + self.test_results, + set() + ) + result = self.app.post('/', data={'q': 'test', 'format': 'json'}) + + result_dict = json.loads(result.data) + + self.assertEqual('test', result_dict['query']) + self.assertEqual( + result_dict['results'][0]['content'], 'first test content') + self.assertEqual( + result_dict['results'][0]['url'], 'http://first.test.xyz') + + @patch('searx.webapp.do_search') + def test_index_csv(self, search): + search.return_value = ( + self.test_results, + set() + ) + result = self.app.post('/', data={'q': 'test', 'format': 'csv'}) + + self.assertEqual( + 'title,url,content,host,engine,score\r\n' + 'First Test,http://first.test.xyz,first test content,first.test.xyz,startpage,\r\n' # noqa + 'Second Test,http://second.test.xyz,second test content,second.test.xyz,youtube,\r\n', # noqa + result.data + ) + + @patch('searx.webapp.do_search') + def test_index_rss(self, search): + search.return_value = ( + self.test_results, + set() + ) + result = self.app.post('/', data={'q': 'test', 'format': 'rss'}) + + self.assertIn( + 'Search results for "test" - searx', + result.data + ) + + self.assertIn( + '2', + result.data + ) + + self.assertIn( + 'First Test', + result.data + ) + + self.assertIn( + 'http://first.test.xyz', + result.data + ) + + self.assertIn( + 'first test content', + result.data + ) + + def test_about(self): + result = self.app.get('/about') + self.assertEqual(result.status_code, 200) + self.assertIn('

About searx

', result.data) + + def test_preferences(self): + result = self.app.get('/preferences') + self.assertEqual(result.status_code, 200) + self.assertIn( + '
', + result.data + ) + self.assertIn( + 'Default categories', + result.data + ) + self.assertIn( + 'Interface language', + result.data + ) + + def test_stats(self): + result = self.app.get('/stats') + self.assertEqual(result.status_code, 200) + self.assertIn('

Engine stats

', result.data) + + def test_robots_txt(self): + result = self.app.get('/robots.txt') + self.assertEqual(result.status_code, 200) + self.assertIn('Allow: /', result.data) + + def test_opensearch_xml(self): + result = self.app.get('/opensearch.xml') + self.assertEqual(result.status_code, 200) + self.assertIn('Search searx', result.data) + + def test_favicon(self): + result = self.app.get('/favicon.ico') + self.assertEqual(result.status_code, 200) diff --git a/sources/searx/translations/de/LC_MESSAGES/messages.mo b/sources/searx/translations/de/LC_MESSAGES/messages.mo new file mode 100644 index 0000000..dc29227 Binary files /dev/null and b/sources/searx/translations/de/LC_MESSAGES/messages.mo differ diff --git a/sources/searx/translations/de/LC_MESSAGES/messages.po b/sources/searx/translations/de/LC_MESSAGES/messages.po new file mode 100644 index 0000000..bd4f44d --- /dev/null +++ b/sources/searx/translations/de/LC_MESSAGES/messages.po @@ -0,0 +1,176 @@ +# English translations for . +# Copyright (C) 2014 ORGANIZATION +# This file is distributed under the same license as the project. +# +# Translators: +# pointhi, 2014 +# stf , 2014 +msgid "" +msgstr "" +"Project-Id-Version: searx\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2014-03-18 17:10+0100\n" +"PO-Revision-Date: 2014-03-15 18:40+0000\n" +"Last-Translator: pointhi\n" +"Language-Team: German " +"(http://www.transifex.com/projects/p/searx/language/de/)\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" + +#: searx/webapp.py:167 +msgid "{minutes} minute(s) ago" +msgstr "" + +#: searx/webapp.py:169 +msgid "{hours} hour(s), {minutes} minute(s) ago" +msgstr "" + +#: searx/engines/__init__.py:311 +msgid "Page loads (sec)" +msgstr "Ladezeit (sek)" + +#: searx/engines/__init__.py:315 +msgid "Number of results" +msgstr "Trefferanzahl" + +#: searx/engines/__init__.py:319 +msgid "Scores" +msgstr "Punkte" + +#: searx/engines/__init__.py:323 +msgid "Scores per result" +msgstr "Punkte pro Treffer" + +#: searx/engines/__init__.py:327 +msgid "Errors" +msgstr "Fehler" + +#: searx/templates/index.html:7 +msgid "about" +msgstr "Über uns" + +#: searx/templates/index.html:8 +msgid "preferences" +msgstr "Einstellungen" + +#: searx/templates/preferences.html:5 +msgid "Preferences" +msgstr "Einstellungen" + +#: searx/templates/preferences.html:9 +msgid "Default categories" +msgstr "Standard Kategorien" + +#: searx/templates/preferences.html:15 +msgid "Search language" +msgstr "Suchsprache" + +#: searx/templates/preferences.html:18 +msgid "Automatic" +msgstr "Automatisch" + +#: searx/templates/preferences.html:26 +msgid "Interface language" +msgstr "Oberflächensprache" + +#: searx/templates/preferences.html:36 +msgid "Currently used search engines" +msgstr "Aktuell benutzte Suchmaschinen" + +#: searx/templates/preferences.html:40 +msgid "Engine name" +msgstr "Suchmaschinenname" + +#: searx/templates/preferences.html:41 +msgid "Category" +msgstr "Kategorie" + +#: searx/templates/preferences.html:42 searx/templates/preferences.html:53 +msgid "Allow" +msgstr "Erlauben" + +#: searx/templates/preferences.html:42 searx/templates/preferences.html:54 +msgid "Block" +msgstr "Blockieren" + +#: searx/templates/preferences.html:62 +msgid "" +"These settings are stored in your cookies, this allows us not to store " +"this data about you." +msgstr "" +"Diese Informationen werden in Cookies gespeichert, damit wir keine ihrer " +"persönlichen Daten speichern müssen." + +#: searx/templates/preferences.html:64 +msgid "" +"These cookies serve your sole convenience, we don't use these cookies to " +"track you." +msgstr "" +"Diese Cookies dienen ihrer Gemütlichkeit, wir verwenden sie nicht zum " +"überwachen." + +#: searx/templates/preferences.html:67 +msgid "save" +msgstr "Speichern" + +#: searx/templates/preferences.html:68 +msgid "back" +msgstr "Zurück" + +#: searx/templates/results.html:11 +msgid "Suggestions" +msgstr "Vorschläge" + +#: searx/templates/results.html:22 +msgid "Search URL" +msgstr "Such-URL" + +#: searx/templates/results.html:26 +msgid "Download results" +msgstr "Ergebnisse herunterladen" + +#: searx/templates/results.html:62 +msgid "previous page" +msgstr "vorherige Seite" + +#: searx/templates/results.html:73 +msgid "next page" +msgstr "nächste Seite" + +#: searx/templates/search.html:3 +msgid "Search for..." +msgstr "Suche nach..." + +#: searx/templates/stats.html:4 +msgid "Engine stats" +msgstr "Suchmaschienen Statistiken" + +# categories - manually added +# TODO - automatically add +msgid "files" +msgstr "Dateien" + +msgid "general" +msgstr "Allgemein" + +msgid "music" +msgstr "Musik" + +msgid "social media" +msgstr "Soziale Medien" + +msgid "images" +msgstr "Bilder" + +msgid "videos" +msgstr "Videos" + +msgid "it" +msgstr "IT" + +msgid "news" +msgstr "Neuigkeiten" + diff --git a/sources/searx/translations/en/LC_MESSAGES/messages.mo b/sources/searx/translations/en/LC_MESSAGES/messages.mo new file mode 100644 index 0000000..4f81aa4 Binary files /dev/null and b/sources/searx/translations/en/LC_MESSAGES/messages.mo differ diff --git a/sources/searx/translations/en/LC_MESSAGES/messages.po b/sources/searx/translations/en/LC_MESSAGES/messages.po new file mode 100644 index 0000000..7141c4d --- /dev/null +++ b/sources/searx/translations/en/LC_MESSAGES/messages.po @@ -0,0 +1,169 @@ +# English translations for PROJECT. +# Copyright (C) 2014 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2014. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2014-03-18 17:10+0100\n" +"PO-Revision-Date: 2014-01-30 15:22+0100\n" +"Last-Translator: FULL NAME \n" +"Language-Team: en \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" + +#: searx/webapp.py:167 +msgid "{minutes} minute(s) ago" +msgstr "" + +#: searx/webapp.py:169 +msgid "{hours} hour(s), {minutes} minute(s) ago" +msgstr "" + +#: searx/engines/__init__.py:311 +msgid "Page loads (sec)" +msgstr "" + +#: searx/engines/__init__.py:315 +msgid "Number of results" +msgstr "" + +#: searx/engines/__init__.py:319 +msgid "Scores" +msgstr "" + +#: searx/engines/__init__.py:323 +msgid "Scores per result" +msgstr "" + +#: searx/engines/__init__.py:327 +msgid "Errors" +msgstr "" + +#: searx/templates/index.html:7 +msgid "about" +msgstr "" + +#: searx/templates/index.html:8 +msgid "preferences" +msgstr "" + +#: searx/templates/preferences.html:5 +msgid "Preferences" +msgstr "" + +#: searx/templates/preferences.html:9 +msgid "Default categories" +msgstr "" + +#: searx/templates/preferences.html:15 +msgid "Search language" +msgstr "" + +#: searx/templates/preferences.html:18 +msgid "Automatic" +msgstr "" + +#: searx/templates/preferences.html:26 +msgid "Interface language" +msgstr "" + +#: searx/templates/preferences.html:36 +msgid "Currently used search engines" +msgstr "" + +#: searx/templates/preferences.html:40 +msgid "Engine name" +msgstr "" + +#: searx/templates/preferences.html:41 +msgid "Category" +msgstr "" + +#: searx/templates/preferences.html:42 searx/templates/preferences.html:53 +msgid "Allow" +msgstr "" + +#: searx/templates/preferences.html:42 searx/templates/preferences.html:54 +msgid "Block" +msgstr "" + +#: searx/templates/preferences.html:62 +msgid "" +"These settings are stored in your cookies, this allows us not to store " +"this data about you." +msgstr "" + +#: searx/templates/preferences.html:64 +msgid "" +"These cookies serve your sole convenience, we don't use these cookies to " +"track you." +msgstr "" + +#: searx/templates/preferences.html:67 +msgid "save" +msgstr "" + +#: searx/templates/preferences.html:68 +msgid "back" +msgstr "" + +#: searx/templates/results.html:11 +msgid "Suggestions" +msgstr "" + +#: searx/templates/results.html:22 +msgid "Search URL" +msgstr "" + +#: searx/templates/results.html:26 +msgid "Download results" +msgstr "" + +#: searx/templates/results.html:62 +msgid "previous page" +msgstr "" + +#: searx/templates/results.html:73 +msgid "next page" +msgstr "" + +#: searx/templates/search.html:3 +msgid "Search for..." +msgstr "" + +#: searx/templates/stats.html:4 +msgid "Engine stats" +msgstr "" + +# categories - manually added +# TODO - automatically add +msgid "files" +msgstr "" + +msgid "general" +msgstr "" + +msgid "music" +msgstr "" + +msgid "social media" +msgstr "" + +msgid "images" +msgstr "" + +msgid "videos" +msgstr "" + +msgid "it" +msgstr "" + +msgid "news" +msgstr "" + diff --git a/sources/searx/translations/es/LC_MESSAGES/messages.mo b/sources/searx/translations/es/LC_MESSAGES/messages.mo new file mode 100644 index 0000000..69c0fdf Binary files /dev/null and b/sources/searx/translations/es/LC_MESSAGES/messages.mo differ diff --git a/sources/searx/translations/es/LC_MESSAGES/messages.po b/sources/searx/translations/es/LC_MESSAGES/messages.po new file mode 100644 index 0000000..bd9ecab --- /dev/null +++ b/sources/searx/translations/es/LC_MESSAGES/messages.po @@ -0,0 +1,175 @@ +# English translations for . +# Copyright (C) 2014 ORGANIZATION +# This file is distributed under the same license as the project. +# +# Translators: +# niazle, 2014 +msgid "" +msgstr "" +"Project-Id-Version: searx\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2014-03-18 17:10+0100\n" +"PO-Revision-Date: 2014-03-04 20:40+0000\n" +"Last-Translator: niazle\n" +"Language-Team: Spanish " +"(http://www.transifex.com/projects/p/searx/language/es/)\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" + +#: searx/webapp.py:167 +msgid "{minutes} minute(s) ago" +msgstr "" + +#: searx/webapp.py:169 +msgid "{hours} hour(s), {minutes} minute(s) ago" +msgstr "" + +#: searx/engines/__init__.py:311 +msgid "Page loads (sec)" +msgstr "Tiempo de carga (segundos)" + +#: searx/engines/__init__.py:315 +msgid "Number of results" +msgstr "Número de resultados" + +#: searx/engines/__init__.py:319 +msgid "Scores" +msgstr "Puntuaciones" + +#: searx/engines/__init__.py:323 +msgid "Scores per result" +msgstr "Puntuaciones por resultado" + +#: searx/engines/__init__.py:327 +msgid "Errors" +msgstr "Errores" + +#: searx/templates/index.html:7 +msgid "about" +msgstr "acerca de" + +#: searx/templates/index.html:8 +msgid "preferences" +msgstr "preferencias" + +#: searx/templates/preferences.html:5 +msgid "Preferences" +msgstr "Preferencias" + +#: searx/templates/preferences.html:9 +msgid "Default categories" +msgstr "Categorías predeterminadas" + +#: searx/templates/preferences.html:15 +msgid "Search language" +msgstr "Buscar idioma" + +#: searx/templates/preferences.html:18 +msgid "Automatic" +msgstr "Automático" + +#: searx/templates/preferences.html:26 +msgid "Interface language" +msgstr "Idioma de la interfaz" + +#: searx/templates/preferences.html:36 +msgid "Currently used search engines" +msgstr "Motores de búsqueda actualmente en uso" + +#: searx/templates/preferences.html:40 +msgid "Engine name" +msgstr "Nombre del motor de búsqueda" + +#: searx/templates/preferences.html:41 +msgid "Category" +msgstr "Categoría" + +#: searx/templates/preferences.html:42 searx/templates/preferences.html:53 +msgid "Allow" +msgstr "Permitir" + +#: searx/templates/preferences.html:42 searx/templates/preferences.html:54 +msgid "Block" +msgstr "Bloquear" + +#: searx/templates/preferences.html:62 +msgid "" +"These settings are stored in your cookies, this allows us not to store " +"this data about you." +msgstr "" +"Esta configuración se guarda en sus cookies, lo que nos permite no " +"almacenar dicha información sobre usted." + +#: searx/templates/preferences.html:64 +msgid "" +"These cookies serve your sole convenience, we don't use these cookies to " +"track you." +msgstr "" +"Estas cookies son para su propia comodidad, no las utilizamos para " +"rastrearle." + +#: searx/templates/preferences.html:67 +msgid "save" +msgstr "Guardar" + +#: searx/templates/preferences.html:68 +msgid "back" +msgstr "Atrás" + +#: searx/templates/results.html:11 +msgid "Suggestions" +msgstr "Sugerencias" + +#: searx/templates/results.html:22 +msgid "Search URL" +msgstr "Buscar URL" + +#: searx/templates/results.html:26 +msgid "Download results" +msgstr "Descargar resultados" + +#: searx/templates/results.html:62 +msgid "previous page" +msgstr "Página anterior" + +#: searx/templates/results.html:73 +msgid "next page" +msgstr "Página siguiente" + +#: searx/templates/search.html:3 +msgid "Search for..." +msgstr "" + +#: searx/templates/stats.html:4 +msgid "Engine stats" +msgstr "Estadísticas del motor de búsqueda" + +# categories - manually added +# TODO - automatically add +msgid "files" +msgstr "Archivos" + +msgid "general" +msgstr "General" + +msgid "music" +msgstr "Música" + +msgid "social media" +msgstr "Medios sociales" + +msgid "images" +msgstr "Imágenes" + +msgid "videos" +msgstr "Vídeos" + +msgid "it" +msgstr "TIC" + +msgid "news" +msgstr "noticias" + diff --git a/sources/searx/translations/fr/LC_MESSAGES/messages.mo b/sources/searx/translations/fr/LC_MESSAGES/messages.mo new file mode 100644 index 0000000..09022d0 Binary files /dev/null and b/sources/searx/translations/fr/LC_MESSAGES/messages.mo differ diff --git a/sources/searx/translations/fr/LC_MESSAGES/messages.po b/sources/searx/translations/fr/LC_MESSAGES/messages.po new file mode 100644 index 0000000..37dc5e4 --- /dev/null +++ b/sources/searx/translations/fr/LC_MESSAGES/messages.po @@ -0,0 +1,177 @@ +# English translations for . +# Copyright (C) 2014 ORGANIZATION +# This file is distributed under the same license as the project. +# +# Translators: +# Benjamin Sonntag , 2014 +# FIRST AUTHOR , 2014 +# rike , 2014 +msgid "" +msgstr "" +"Project-Id-Version: searx\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2014-03-18 17:10+0100\n" +"PO-Revision-Date: 2014-03-16 07:40+0000\n" +"Last-Translator: Benjamin Sonntag \n" +"Language-Team: French " +"(http://www.transifex.com/projects/p/searx/language/fr/)\n" +"Plural-Forms: nplurals=2; plural=(n > 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" + +#: searx/webapp.py:167 +msgid "{minutes} minute(s) ago" +msgstr "" + +#: searx/webapp.py:169 +msgid "{hours} hour(s), {minutes} minute(s) ago" +msgstr "" + +#: searx/engines/__init__.py:311 +msgid "Page loads (sec)" +msgstr "Chargement de la page (sec)" + +#: searx/engines/__init__.py:315 +msgid "Number of results" +msgstr "Nombre de résultats" + +#: searx/engines/__init__.py:319 +msgid "Scores" +msgstr "Score" + +#: searx/engines/__init__.py:323 +msgid "Scores per result" +msgstr "Score par résultat" + +#: searx/engines/__init__.py:327 +msgid "Errors" +msgstr "Erreurs" + +#: searx/templates/index.html:7 +msgid "about" +msgstr "À propos" + +#: searx/templates/index.html:8 +msgid "preferences" +msgstr "préférences" + +#: searx/templates/preferences.html:5 +msgid "Preferences" +msgstr "Préférences" + +#: searx/templates/preferences.html:9 +msgid "Default categories" +msgstr "Catégories par défaut" + +#: searx/templates/preferences.html:15 +msgid "Search language" +msgstr "Langue de recherche" + +#: searx/templates/preferences.html:18 +msgid "Automatic" +msgstr "Automatique" + +#: searx/templates/preferences.html:26 +msgid "Interface language" +msgstr "Langue de l'interface" + +#: searx/templates/preferences.html:36 +msgid "Currently used search engines" +msgstr "Moteurs actuellement utilisés" + +#: searx/templates/preferences.html:40 +msgid "Engine name" +msgstr "Nom du moteur" + +#: searx/templates/preferences.html:41 +msgid "Category" +msgstr "Catégorie" + +#: searx/templates/preferences.html:42 searx/templates/preferences.html:53 +msgid "Allow" +msgstr "Autoriser" + +#: searx/templates/preferences.html:42 searx/templates/preferences.html:54 +msgid "Block" +msgstr "Bloquer" + +#: searx/templates/preferences.html:62 +msgid "" +"These settings are stored in your cookies, this allows us not to store " +"this data about you." +msgstr "" +"Ces paramètres sont stockés dans vos cookies ; ceci nous permet de ne pas" +" collecter vos données." + +#: searx/templates/preferences.html:64 +msgid "" +"These cookies serve your sole convenience, we don't use these cookies to " +"track you." +msgstr "" +"Ces cookies existent pour votre confort d'utilisation, nous ne les " +"utilisons pas pour vous espionner." + +#: searx/templates/preferences.html:67 +msgid "save" +msgstr "enregistrer" + +#: searx/templates/preferences.html:68 +msgid "back" +msgstr "retour" + +#: searx/templates/results.html:11 +msgid "Suggestions" +msgstr "Suggestions" + +#: searx/templates/results.html:22 +msgid "Search URL" +msgstr "URL de recherche" + +#: searx/templates/results.html:26 +msgid "Download results" +msgstr "Télécharger les résultats" + +#: searx/templates/results.html:62 +msgid "previous page" +msgstr "page précédente" + +#: searx/templates/results.html:73 +msgid "next page" +msgstr "page suivante" + +#: searx/templates/search.html:3 +msgid "Search for..." +msgstr "Rechercher..." + +#: searx/templates/stats.html:4 +msgid "Engine stats" +msgstr "Statistiques du moteur" + +# categories - manually added +# TODO - automatically add +msgid "files" +msgstr "fichiers" + +msgid "general" +msgstr "général" + +msgid "music" +msgstr "musique" + +msgid "social media" +msgstr "réseaux sociaux" + +msgid "images" +msgstr "images" + +msgid "videos" +msgstr "vidéos" + +msgid "it" +msgstr "Informatique" + +msgid "news" +msgstr "actus" + diff --git a/sources/searx/translations/hu/LC_MESSAGES/messages.mo b/sources/searx/translations/hu/LC_MESSAGES/messages.mo new file mode 100644 index 0000000..64beee1 Binary files /dev/null and b/sources/searx/translations/hu/LC_MESSAGES/messages.mo differ diff --git a/sources/searx/translations/hu/LC_MESSAGES/messages.po b/sources/searx/translations/hu/LC_MESSAGES/messages.po new file mode 100644 index 0000000..52fd2ee --- /dev/null +++ b/sources/searx/translations/hu/LC_MESSAGES/messages.po @@ -0,0 +1,171 @@ +# Hungarian translations for PROJECT. +# Copyright (C) 2014 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2014. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2014-03-18 17:10+0100\n" +"PO-Revision-Date: 2014-01-21 23:33+0100\n" +"Last-Translator: FULL NAME \n" +"Language-Team: hu \n" +"Plural-Forms: nplurals=1; plural=0\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" + +#: searx/webapp.py:167 +msgid "{minutes} minute(s) ago" +msgstr "{minutes} perce" + +#: searx/webapp.py:169 +msgid "{hours} hour(s), {minutes} minute(s) ago" +msgstr "{hours} óra, {minutes} perce" + +#: searx/engines/__init__.py:311 +msgid "Page loads (sec)" +msgstr "Válaszidők (sec)" + +#: searx/engines/__init__.py:315 +msgid "Number of results" +msgstr "Találatok száma" + +#: searx/engines/__init__.py:319 +msgid "Scores" +msgstr "Pontszámok" + +#: searx/engines/__init__.py:323 +msgid "Scores per result" +msgstr "Pontszámok találatonként" + +#: searx/engines/__init__.py:327 +msgid "Errors" +msgstr "Hibák" + +#: searx/templates/index.html:7 +msgid "about" +msgstr "rólunk" + +#: searx/templates/index.html:8 +msgid "preferences" +msgstr "beállítások" + +#: searx/templates/preferences.html:5 +msgid "Preferences" +msgstr "Beállítások" + +#: searx/templates/preferences.html:9 +msgid "Default categories" +msgstr "Alapértelmezett kategóriák" + +#: searx/templates/preferences.html:15 +msgid "Search language" +msgstr "Keresés nyelve" + +#: searx/templates/preferences.html:18 +msgid "Automatic" +msgstr "Automatikus" + +#: searx/templates/preferences.html:26 +msgid "Interface language" +msgstr "Felület nyelve" + +#: searx/templates/preferences.html:36 +msgid "Currently used search engines" +msgstr "Jelenleg használt keresők" + +#: searx/templates/preferences.html:40 +msgid "Engine name" +msgstr "Kereső neve" + +#: searx/templates/preferences.html:41 +msgid "Category" +msgstr "Kategória" + +#: searx/templates/preferences.html:42 searx/templates/preferences.html:53 +msgid "Allow" +msgstr "Engedélyezés" + +#: searx/templates/preferences.html:42 searx/templates/preferences.html:54 +msgid "Block" +msgstr "Tiltás" + +#: searx/templates/preferences.html:62 +msgid "" +"These settings are stored in your cookies, this allows us not to store " +"this data about you." +msgstr "Ezek a beállítások csak a böngésző cookie-jaiban tárolódnak." + +#: searx/templates/preferences.html:64 +msgid "" +"These cookies serve your sole convenience, we don't use these cookies to " +"track you." +msgstr "" +"Ezek a cookie-k csak kényelmi funkciókat látnak el, nem használjuk a " +"felhasználók követésére." + +#: searx/templates/preferences.html:67 +msgid "save" +msgstr "mentés" + +#: searx/templates/preferences.html:68 +msgid "back" +msgstr "vissza" + +#: searx/templates/results.html:11 +msgid "Suggestions" +msgstr "Javaslatok" + +#: searx/templates/results.html:22 +msgid "Search URL" +msgstr "Keresési URL" + +#: searx/templates/results.html:26 +msgid "Download results" +msgstr "Találatok letöltése" + +#: searx/templates/results.html:62 +msgid "previous page" +msgstr "előző oldal" + +#: searx/templates/results.html:73 +msgid "next page" +msgstr "következő oldal" + +#: searx/templates/search.html:3 +msgid "Search for..." +msgstr "Keresés..." + +#: searx/templates/stats.html:4 +msgid "Engine stats" +msgstr "Kereső statisztikák" + +# categories - manually added +# TODO - automatically add +msgid "files" +msgstr "fájlok" + +msgid "general" +msgstr "általános" + +msgid "music" +msgstr "zene" + +msgid "social media" +msgstr "közösségi média" + +msgid "images" +msgstr "képek" + +msgid "videos" +msgstr "videók" + +msgid "it" +msgstr "it" + +msgid "news" +msgstr "hírek" + diff --git a/sources/searx/translations/it/LC_MESSAGES/messages.mo b/sources/searx/translations/it/LC_MESSAGES/messages.mo new file mode 100644 index 0000000..ffd0dc9 Binary files /dev/null and b/sources/searx/translations/it/LC_MESSAGES/messages.mo differ diff --git a/sources/searx/translations/it/LC_MESSAGES/messages.po b/sources/searx/translations/it/LC_MESSAGES/messages.po new file mode 100644 index 0000000..b83ef44 --- /dev/null +++ b/sources/searx/translations/it/LC_MESSAGES/messages.po @@ -0,0 +1,175 @@ +# English translations for . +# Copyright (C) 2014 ORGANIZATION +# This file is distributed under the same license as the project. +# +# Translators: +# dp , 2014 +msgid "" +msgstr "" +"Project-Id-Version: searx\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2014-03-18 17:10+0100\n" +"PO-Revision-Date: 2014-03-05 13:30+0000\n" +"Last-Translator: dp \n" +"Language-Team: Italian " +"(http://www.transifex.com/projects/p/searx/language/it/)\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" + +#: searx/webapp.py:167 +msgid "{minutes} minute(s) ago" +msgstr "" + +#: searx/webapp.py:169 +msgid "{hours} hour(s), {minutes} minute(s) ago" +msgstr "" + +#: searx/engines/__init__.py:311 +msgid "Page loads (sec)" +msgstr " Caricamento della pagina (secondi)" + +#: searx/engines/__init__.py:315 +msgid "Number of results" +msgstr "Risultati ottenuti" + +#: searx/engines/__init__.py:319 +msgid "Scores" +msgstr "Punteggio" + +#: searx/engines/__init__.py:323 +msgid "Scores per result" +msgstr "Punteggio per risultato" + +#: searx/engines/__init__.py:327 +msgid "Errors" +msgstr "Errori" + +#: searx/templates/index.html:7 +msgid "about" +msgstr "informazioni" + +#: searx/templates/index.html:8 +msgid "preferences" +msgstr "preferenze" + +#: searx/templates/preferences.html:5 +msgid "Preferences" +msgstr "Preferenze" + +#: searx/templates/preferences.html:9 +msgid "Default categories" +msgstr "Categorie predefinite" + +#: searx/templates/preferences.html:15 +msgid "Search language" +msgstr "Lingua di ricerca" + +#: searx/templates/preferences.html:18 +msgid "Automatic" +msgstr "Automatico" + +#: searx/templates/preferences.html:26 +msgid "Interface language" +msgstr "Linguaggio dell'interfaccia" + +#: searx/templates/preferences.html:36 +msgid "Currently used search engines" +msgstr "Motori di ricerca attualmente in uso" + +#: searx/templates/preferences.html:40 +msgid "Engine name" +msgstr "Nome del motore" + +#: searx/templates/preferences.html:41 +msgid "Category" +msgstr "Categoria" + +#: searx/templates/preferences.html:42 searx/templates/preferences.html:53 +msgid "Allow" +msgstr "Autorizza" + +#: searx/templates/preferences.html:42 searx/templates/preferences.html:54 +msgid "Block" +msgstr "Blocca" + +#: searx/templates/preferences.html:62 +msgid "" +"These settings are stored in your cookies, this allows us not to store " +"this data about you." +msgstr "" +"Queste impostazioni sono salvate nei tuoi cookie, consentendoci di non " +"conservare dati su di te." + +#: searx/templates/preferences.html:64 +msgid "" +"These cookies serve your sole convenience, we don't use these cookies to " +"track you." +msgstr "" +"I cookie sono funzionali ad un servizio migliore. Non usiamo i cookie per" +" sorvegliarti." + +#: searx/templates/preferences.html:67 +msgid "save" +msgstr "salva" + +#: searx/templates/preferences.html:68 +msgid "back" +msgstr "indietro" + +#: searx/templates/results.html:11 +msgid "Suggestions" +msgstr "Suggerimenti" + +#: searx/templates/results.html:22 +msgid "Search URL" +msgstr "URL della ricerca" + +#: searx/templates/results.html:26 +msgid "Download results" +msgstr "Scarica i risultati" + +#: searx/templates/results.html:62 +msgid "previous page" +msgstr "pagina precedente" + +#: searx/templates/results.html:73 +msgid "next page" +msgstr "pagina successiva" + +#: searx/templates/search.html:3 +msgid "Search for..." +msgstr "" + +#: searx/templates/stats.html:4 +msgid "Engine stats" +msgstr "Statistiche dei motori" + +# categories - manually added +# TODO - automatically add +msgid "files" +msgstr "documenti" + +msgid "general" +msgstr "generale" + +msgid "music" +msgstr "musica" + +msgid "social media" +msgstr "social media" + +msgid "images" +msgstr "immagini" + +msgid "videos" +msgstr "video" + +msgid "it" +msgstr "it" + +msgid "news" +msgstr "notizie" + diff --git a/sources/searx/translations/nl/LC_MESSAGES/messages.mo b/sources/searx/translations/nl/LC_MESSAGES/messages.mo new file mode 100644 index 0000000..6f456e1 Binary files /dev/null and b/sources/searx/translations/nl/LC_MESSAGES/messages.mo differ diff --git a/sources/searx/translations/nl/LC_MESSAGES/messages.po b/sources/searx/translations/nl/LC_MESSAGES/messages.po new file mode 100644 index 0000000..78d8483 --- /dev/null +++ b/sources/searx/translations/nl/LC_MESSAGES/messages.po @@ -0,0 +1,175 @@ +# English translations for . +# Copyright (C) 2014 ORGANIZATION +# This file is distributed under the same license as the project. +# +# Translators: +# André Koot , 2014 +msgid "" +msgstr "" +"Project-Id-Version: searx\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2014-03-18 17:10+0100\n" +"PO-Revision-Date: 2014-03-15 20:20+0000\n" +"Last-Translator: André Koot \n" +"Language-Team: Dutch " +"(http://www.transifex.com/projects/p/searx/language/nl/)\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" + +#: searx/webapp.py:167 +msgid "{minutes} minute(s) ago" +msgstr "" + +#: searx/webapp.py:169 +msgid "{hours} hour(s), {minutes} minute(s) ago" +msgstr "" + +#: searx/engines/__init__.py:311 +msgid "Page loads (sec)" +msgstr "Pagina laadt (sec)" + +#: searx/engines/__init__.py:315 +msgid "Number of results" +msgstr "Aantal zoekresultaten" + +#: searx/engines/__init__.py:319 +msgid "Scores" +msgstr "Scores" + +#: searx/engines/__init__.py:323 +msgid "Scores per result" +msgstr "Scores per zoekresultaat" + +#: searx/engines/__init__.py:327 +msgid "Errors" +msgstr "Fouten" + +#: searx/templates/index.html:7 +msgid "about" +msgstr "over" + +#: searx/templates/index.html:8 +msgid "preferences" +msgstr "voorkeuren" + +#: searx/templates/preferences.html:5 +msgid "Preferences" +msgstr "Voorkeuren" + +#: searx/templates/preferences.html:9 +msgid "Default categories" +msgstr "Standaardcategorieën" + +#: searx/templates/preferences.html:15 +msgid "Search language" +msgstr "Zoektaal" + +#: searx/templates/preferences.html:18 +msgid "Automatic" +msgstr "Automatisch" + +#: searx/templates/preferences.html:26 +msgid "Interface language" +msgstr "Interfacetaal" + +#: searx/templates/preferences.html:36 +msgid "Currently used search engines" +msgstr "Momenteel gebruikte zoekmachines" + +#: searx/templates/preferences.html:40 +msgid "Engine name" +msgstr "Naam zoekmachine" + +#: searx/templates/preferences.html:41 +msgid "Category" +msgstr "Categorie" + +#: searx/templates/preferences.html:42 searx/templates/preferences.html:53 +msgid "Allow" +msgstr "Toestaan" + +#: searx/templates/preferences.html:42 searx/templates/preferences.html:54 +msgid "Block" +msgstr "Blokkeren" + +#: searx/templates/preferences.html:62 +msgid "" +"These settings are stored in your cookies, this allows us not to store " +"this data about you." +msgstr "" +"Deze instellingen worden bewaard in je cookies. Hierdoor hoeven wij niets" +" over jou te bewaren." + +#: searx/templates/preferences.html:64 +msgid "" +"These cookies serve your sole convenience, we don't use these cookies to " +"track you." +msgstr "" +"Deze cookies zijn alleen voor je eigen gemak, we gebruiken deze cookies " +"niet om je te volgen." + +#: searx/templates/preferences.html:67 +msgid "save" +msgstr "bewaren" + +#: searx/templates/preferences.html:68 +msgid "back" +msgstr "terug" + +#: searx/templates/results.html:11 +msgid "Suggestions" +msgstr "Suggesties" + +#: searx/templates/results.html:22 +msgid "Search URL" +msgstr "Zoek URL" + +#: searx/templates/results.html:26 +msgid "Download results" +msgstr "Downloaden zoekresultaten" + +#: searx/templates/results.html:62 +msgid "previous page" +msgstr "vorige pagina" + +#: searx/templates/results.html:73 +msgid "next page" +msgstr "volgende pagina" + +#: searx/templates/search.html:3 +msgid "Search for..." +msgstr "Zoeken naar..." + +#: searx/templates/stats.html:4 +msgid "Engine stats" +msgstr "Zoekmachinestatistieken" + +# categories - manually added +# TODO - automatically add +msgid "files" +msgstr "bestanden" + +msgid "general" +msgstr "algemeen" + +msgid "music" +msgstr "muziek" + +msgid "social media" +msgstr "social media" + +msgid "images" +msgstr "afbeeldingen" + +msgid "videos" +msgstr "video's" + +msgid "it" +msgstr "it" + +msgid "news" +msgstr "nieuws" + diff --git a/sources/searx/utils.py b/sources/searx/utils.py new file mode 100644 index 0000000..e881a8b --- /dev/null +++ b/sources/searx/utils.py @@ -0,0 +1,108 @@ +from HTMLParser import HTMLParser +#import htmlentitydefs +import csv +from codecs import getincrementalencoder +import cStringIO +import re +from random import choice + +ua_versions = ('26.0', '27.0', '28.0') +ua_os = ('Windows NT 6.3; WOW64', 'X11; Linux x86_64; rv:26.0') +ua = "Mozilla/5.0 ({os}) Gecko/20100101 Firefox/{version}" + + +def gen_useragent(): + # TODO + return ua.format(os=choice(ua_os), version=choice(ua_versions)) + + +def highlight_content(content, query): + + if not content: + return None + # ignoring html contents + # TODO better html content detection + if content.find('<') != -1: + return content + + query = query.decode('utf-8') + if content.lower().find(query.lower()) > -1: + query_regex = u'({0})'.format(re.escape(query)) + content = re.sub(query_regex, '\\1', content, flags=re.I | re.U) + else: + regex_parts = [] + for chunk in query.split(): + if len(chunk) == 1: + regex_parts.append(u'\W+{0}\W+'.format(re.escape(chunk))) + else: + regex_parts.append(u'{0}'.format(re.escape(chunk))) + query_regex = u'({0})'.format('|'.join(regex_parts)) + content = re.sub(query_regex, '\\1', content, flags=re.I | re.U) + + return content + + +class HTMLTextExtractor(HTMLParser): + def __init__(self): + HTMLParser.__init__(self) + self.result = [] + + def handle_data(self, d): + self.result.append(d) + + def handle_charref(self, number): + if number[0] in (u'x', u'X'): + codepoint = int(number[1:], 16) + else: + codepoint = int(number) + self.result.append(unichr(codepoint)) + + def handle_entityref(self, name): + #codepoint = htmlentitydefs.name2codepoint[name] + #self.result.append(unichr(codepoint)) + self.result.append(name) + + def get_text(self): + return u''.join(self.result) + + +def html_to_text(html): + s = HTMLTextExtractor() + s.feed(html) + return s.get_text() + + +class UnicodeWriter: + """ + A CSV writer which will write rows to CSV file "f", + which is encoded in the given encoding. + """ + + def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds): + # Redirect output to a queue + self.queue = cStringIO.StringIO() + self.writer = csv.writer(self.queue, dialect=dialect, **kwds) + self.stream = f + self.encoder = getincrementalencoder(encoding)() + + def writerow(self, row): + unicode_row = [] + for col in row: + if type(col) == str or type(col) == unicode: + unicode_row.append(col.encode('utf-8').strip()) + else: + unicode_row.append(col) + self.writer.writerow(unicode_row) + # Fetch UTF-8 output from the queue ... + data = self.queue.getvalue() + data = data.decode("utf-8") + # ... and reencode it into the target encoding + data = self.encoder.encode(data) + # write to the target stream + self.stream.write(data) + # empty queue + self.queue.truncate(0) + + def writerows(self, rows): + for row in rows: + self.writerow(row) diff --git a/sources/searx/webapp.py b/sources/searx/webapp.py new file mode 100644 index 0000000..89d288e --- /dev/null +++ b/sources/searx/webapp.py @@ -0,0 +1,424 @@ +#!/usr/bin/env python + +''' +searx is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +searx is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with searx. If not, see < http://www.gnu.org/licenses/ >. + +(C) 2013- by Adam Tauber, +''' + +if __name__ == '__main__': + from sys import path + from os.path import realpath, dirname + path.append(realpath(dirname(realpath(__file__))+'/../')) + +import json +import cStringIO +import os + +from datetime import datetime, timedelta +from itertools import chain +from flask import ( + Flask, request, render_template, url_for, Response, make_response, + redirect, send_from_directory +) +from flask.ext.babel import Babel, gettext, format_date +from searx import settings, searx_dir +from searx.engines import ( + search as do_search, categories, engines, get_engines_stats, + engine_shortcuts +) +from searx.utils import UnicodeWriter, highlight_content, html_to_text +from searx.languages import language_codes +from searx.search import Search +from searx.autocomplete import backends as autocomplete_backends + + +app = Flask( + __name__, + static_folder=os.path.join(searx_dir, 'static'), + template_folder=os.path.join(searx_dir, 'templates') +) + +app.secret_key = settings['server']['secret_key'] + +babel = Babel(app) + +#TODO configurable via settings.yml +favicons = ['wikipedia', 'youtube', 'vimeo', 'soundcloud', + 'twitter', 'stackoverflow', 'github'] + +cookie_max_age = 60 * 60 * 24 * 365 * 23 # 23 years + + +@babel.localeselector +def get_locale(): + locale = request.accept_languages.best_match(settings['locales'].keys()) + + if request.cookies.get('locale', '') in settings['locales']: + locale = request.cookies.get('locale', '') + + if 'locale' in request.args\ + and request.args['locale'] in settings['locales']: + locale = request.args['locale'] + + if 'locale' in request.form\ + and request.form['locale'] in settings['locales']: + locale = request.form['locale'] + + return locale + + +def get_base_url(): + if settings['server']['base_url']: + hostname = settings['server']['base_url'] + else: + scheme = 'http' + if request.is_secure: + scheme = 'https' + hostname = url_for('index', _external=True, _scheme=scheme) + return hostname + + +def render(template_name, **kwargs): + blocked_engines = request.cookies.get('blocked_engines', '').split(',') + + autocomplete = request.cookies.get('autocomplete') + + if autocomplete not in autocomplete_backends: + autocomplete = None + + nonblocked_categories = (engines[e].categories + for e in engines + if e not in blocked_engines) + + nonblocked_categories = set(chain.from_iterable(nonblocked_categories)) + + if not 'categories' in kwargs: + kwargs['categories'] = ['general'] + kwargs['categories'].extend(x for x in + sorted(categories.keys()) + if x != 'general' + and x in nonblocked_categories) + + if not 'selected_categories' in kwargs: + kwargs['selected_categories'] = [] + cookie_categories = request.cookies.get('categories', '').split(',') + for ccateg in cookie_categories: + if ccateg in categories: + kwargs['selected_categories'].append(ccateg) + if not kwargs['selected_categories']: + kwargs['selected_categories'] = ['general'] + + if not 'autocomplete' in kwargs: + kwargs['autocomplete'] = autocomplete + + kwargs['method'] = request.cookies.get('method', 'POST') + + return render_template(template_name, **kwargs) + + +@app.route('/search', methods=['GET', 'POST']) +@app.route('/', methods=['GET', 'POST']) +def index(): + """Render index page. + + Supported outputs: html, json, csv, rss. + """ + + if not request.args and not request.form: + return render( + 'index.html', + ) + + try: + search = Search(request) + except: + return render( + 'index.html', + ) + + # TODO moar refactor - do_search integration into Search class + search.results, search.suggestions = do_search(search.query, + request, + search.engines, + search.pageno, + search.lang) + + for result in search.results: + if not search.paging and engines[result['engine']].paging: + search.paging = True + if search.request_data.get('format', 'html') == 'html': + if 'content' in result: + result['content'] = highlight_content(result['content'], + search.query.encode('utf-8')) # noqa + result['title'] = highlight_content(result['title'], + search.query.encode('utf-8')) + else: + if 'content' in result: + result['content'] = html_to_text(result['content']).strip() + # removing html content and whitespace duplications + result['title'] = ' '.join(html_to_text(result['title']) + .strip().split()) + if len(result['url']) > 74: + url_parts = result['url'][:35], result['url'][-35:] + result['pretty_url'] = u'{0}[...]{1}'.format(*url_parts) + else: + result['pretty_url'] = result['url'] + + for engine in result['engines']: + if engine in favicons: + result['favicon'] = engine + + # TODO, check if timezone is calculated right + if 'publishedDate' in result: + if result['publishedDate'].replace(tzinfo=None)\ + >= datetime.now() - timedelta(days=1): + timedifference = datetime.now() - result['publishedDate']\ + .replace(tzinfo=None) + minutes = int((timedifference.seconds / 60) % 60) + hours = int(timedifference.seconds / 60 / 60) + if hours == 0: + result['publishedDate'] = gettext(u'{minutes} minute(s) ago').format(minutes=minutes) # noqa + else: + result['publishedDate'] = gettext(u'{hours} hour(s), {minutes} minute(s) ago').format(hours=hours, minutes=minutes) # noqa + else: + result['pubdate'] = result['publishedDate']\ + .strftime('%a, %d %b %Y %H:%M:%S %z') + result['publishedDate'] = format_date(result['publishedDate']) + + if search.request_data.get('format') == 'json': + return Response(json.dumps({'query': search.query, + 'results': search.results}), + mimetype='application/json') + elif search.request_data.get('format') == 'csv': + csv = UnicodeWriter(cStringIO.StringIO()) + keys = ('title', 'url', 'content', 'host', 'engine', 'score') + if search.results: + csv.writerow(keys) + for row in search.results: + row['host'] = row['parsed_url'].netloc + csv.writerow([row.get(key, '') for key in keys]) + csv.stream.seek(0) + response = Response(csv.stream.read(), mimetype='application/csv') + cont_disp = 'attachment;Filename=searx_-_{0}.csv'.format(search.query) + response.headers.add('Content-Disposition', cont_disp) + return response + elif search.request_data.get('format') == 'rss': + response_rss = render( + 'opensearch_response_rss.xml', + results=search.results, + q=search.request_data['q'], + number_of_results=len(search.results), + base_url=get_base_url() + ) + return Response(response_rss, mimetype='text/xml') + + return render( + 'results.html', + results=search.results, + q=search.request_data['q'], + selected_categories=search.categories, + paging=search.paging, + pageno=search.pageno, + base_url=get_base_url(), + suggestions=search.suggestions + ) + + +@app.route('/about', methods=['GET']) +def about(): + """Render about page""" + return render( + 'about.html', + ) + + +@app.route('/autocompleter', methods=['GET', 'POST']) +def autocompleter(): + """Return autocompleter results""" + request_data = {} + + if request.method == 'POST': + request_data = request.form + else: + request_data = request.args + + # TODO fix XSS-vulnerability + query = request_data.get('q', '').encode('utf-8') + + if not query: + return + + completer = autocomplete_backends.get(request.cookies.get('autocomplete')) + + if not completer: + return + + results = completer(query) + + if request_data.get('format') == 'x-suggestions': + return Response(json.dumps([query, results]), + mimetype='application/json') + else: + return Response(json.dumps(results), + mimetype='application/json') + + +@app.route('/preferences', methods=['GET', 'POST']) +def preferences(): + """Render preferences page. + + Settings that are going to be saved as cookies.""" + lang = None + + if request.cookies.get('language')\ + and request.cookies['language'] in (x[0] for x in language_codes): + lang = request.cookies['language'] + + blocked_engines = [] + + if request.method == 'GET': + blocked_engines = request.cookies.get('blocked_engines', '').split(',') + else: + selected_categories = [] + locale = None + autocomplete = '' + method = 'POST' + for pd_name, pd in request.form.items(): + if pd_name.startswith('category_'): + category = pd_name[9:] + if not category in categories: + continue + selected_categories.append(category) + elif pd_name == 'locale' and pd in settings['locales']: + locale = pd + elif pd_name == 'autocomplete': + autocomplete = pd + elif pd_name == 'language' and (pd == 'all' or + pd in (x[0] for + x in language_codes)): + lang = pd + elif pd_name == 'method': + method = pd + elif pd_name.startswith('engine_'): + engine_name = pd_name.replace('engine_', '', 1) + if engine_name in engines: + blocked_engines.append(engine_name) + + resp = make_response(redirect(url_for('index'))) + + user_blocked_engines = request.cookies.get('blocked_engines', '').split(',') # noqa + + if sorted(blocked_engines) != sorted(user_blocked_engines): + resp.set_cookie( + 'blocked_engines', ','.join(blocked_engines), + max_age=cookie_max_age + ) + + if locale: + resp.set_cookie( + 'locale', locale, + max_age=cookie_max_age + ) + + if lang: + resp.set_cookie( + 'language', lang, + max_age=cookie_max_age + ) + + if selected_categories: + # cookie max age: 4 weeks + resp.set_cookie( + 'categories', ','.join(selected_categories), + max_age=cookie_max_age + ) + + resp.set_cookie( + 'autocomplete', autocomplete, + max_age=cookie_max_age + ) + + resp.set_cookie('method', method, max_age=cookie_max_age) + + return resp + return render('preferences.html', + locales=settings['locales'], + current_locale=get_locale(), + current_language=lang or 'all', + language_codes=language_codes, + categs=categories.items(), + blocked_engines=blocked_engines, + autocomplete_backends=autocomplete_backends, + shortcuts={y: x for x, y in engine_shortcuts.items()}) + + +@app.route('/stats', methods=['GET']) +def stats(): + """Render engine statistics page.""" + global categories + stats = get_engines_stats() + return render( + 'stats.html', + stats=stats, + ) + + +@app.route('/robots.txt', methods=['GET']) +def robots(): + return Response("""User-agent: * +Allow: / +Allow: /about +Disallow: /stats +Disallow: /preferences +""", mimetype='text/plain') + + +@app.route('/opensearch.xml', methods=['GET']) +def opensearch(): + method = 'post' + # chrome/chromium only supports HTTP GET.... + if request.headers.get('User-Agent', '').lower().find('webkit') >= 0: + method = 'get' + + ret = render('opensearch.xml', + opensearch_method=method, + host=get_base_url()) + + resp = Response(response=ret, + status=200, + mimetype="application/xml") + return resp + + +@app.route('/favicon.ico') +def favicon(): + return send_from_directory(os.path.join(app.root_path, 'static/img'), + 'favicon.png', + mimetype='image/vnd.microsoft.icon') + + +def run(): + from gevent import monkey + monkey.patch_all() + + app.run( + debug=settings['server']['debug'], + use_debugger=settings['server']['debug'], + port=settings['server']['port'] + ) + + +if __name__ == "__main__": + run() diff --git a/sources/setup.py b/sources/setup.py new file mode 100644 index 0000000..79f2acc --- /dev/null +++ b/sources/setup.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +"""Installer for Searx package.""" + +from setuptools import setup +from setuptools import find_packages + +import os + + +def read(*rnames): + return open(os.path.join(os.path.dirname(__file__), *rnames)).read() + + +long_description = read('README.rst') + +setup( + name='searx', + version="0.3.0", + description="A privacy-respecting, hackable metasearch engine", + long_description=long_description, + classifiers=[ + "Programming Language :: Python", + ], + keywords='meta search engine', + author='Adam Tauber', + author_email='asciimoo@gmail.com', + url='https://github.com/asciimoo/searx', + license='GNU Affero General Public License', + packages=find_packages('.'), + zip_safe=False, + install_requires=[ + 'flask', + 'flask-babel', + 'grequests', + 'lxml', + 'pyyaml', + 'setuptools', + 'python-dateutil', + ], + extras_require={ + 'test': [ + 'coverage', + 'flake8', + 'mock', + 'plone.testing', + 'robotframework', + 'robotframework-debuglibrary', + 'robotframework-httplibrary', + 'robotframework-selenium2library', + 'robotsuite', + 'unittest2', + 'zope.testrunner', + ] + }, + entry_points={ + 'console_scripts': [ + 'searx-run = searx.webapp:run' + ] + }, + package_data={ + 'searx': [ + 'settings.yml', + '../README.rst', + 'static/*/*', + 'translations/*/*/*', + 'templates/*.xml', + 'templates/*.html', + 'templates/result_templates/*.html', + ], + }, + +) diff --git a/sources/utils/update-translations.sh b/sources/utils/update-translations.sh new file mode 100755 index 0000000..bac7d3c --- /dev/null +++ b/sources/utils/update-translations.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +# script to easily update translation language files + +# add new language: +# pybabel init -i messages.pot -d searx/translations -l en + +SEARX_DIR='searx' + +pybabel extract -F babel.cfg -o messages.pot $SEARX_DIR +for f in `ls $SEARX_DIR'/translations/'`; do + pybabel update -i messages.pot -d $SEARX_DIR'/translations/' -l $f + # TODO - need to fix category translations + sed -i 's/#~ //' $SEARX_DIR'/translations/'$f'/LC_MESSAGES/messages.po' +done + +echo '[!] update done, edit .po files if required and run pybabel compile -d searx/translations/' diff --git a/sources/versions.cfg b/sources/versions.cfg new file mode 100644 index 0000000..e0d7f45 --- /dev/null +++ b/sources/versions.cfg @@ -0,0 +1,103 @@ +[versions] +Babel = 1.3 +Flask = 0.10.1 +Flask-Babel = 0.9 +Jinja2 = 2.7.2 +MarkupSafe = 0.18 +WebOb = 1.3.1 +WebTest = 2.0.11 +Werkzeug = 0.9.4 +buildout-versions = 1.7 +collective.recipe.omelette = 0.16 +coverage = 3.7.1 +decorator = 3.4.0 +docutils = 0.11 +flake8 = 2.1.0 +itsdangerous = 0.23 +mccabe = 0.2.1 +mock = 1.0.1 +pep8 = 1.4.6 +plone.testing = 4.0.8 +pyflakes = 0.7.3 +pytz = 2013b +pyyaml = 3.10 +requests = 2.2.0 +robotframework-debuglibrary = 0.3 +robotframework-httplibrary = 0.4.2 +robotframework-selenium2library = 1.5.0 +robotsuite = 1.4.2 +selenium = 2.39.0 +speaklater = 1.3 +unittest2 = 0.5.1 +waitress = 0.8.8 +zc.recipe.testrunner = 2.0.0 + +# Required by: +# WebTest==2.0.11 +beautifulsoup4 = 4.3.2 + +# Required by: +# grequests==0.2.0 +gevent = 1.0 + +# Required by: +# gevent==1.0 +greenlet = 0.4.2 + +# Required by: +# searx==0.1 +grequests = 0.2.0 + +# Required by: +# robotframework-httplibrary==0.4.2 +jsonpatch = 1.3 + +# Required by: +# robotframework-httplibrary==0.4.2 +jsonpointer = 1.1 + +# Required by: +# robotsuite==1.4.2 +# searx==0.1 +lxml = 3.2.5 + +# Required by: +# robotframework-httplibrary==0.4.2 +robotframework = 2.8.3 + +# Required by: +# plone.testing==4.0.8 +# robotsuite==1.4.2 +# searx==0.1 +# zope.exceptions==4.0.6 +# zope.interface==4.0.5 +# zope.testrunner==4.4.1 +setuptools = 2.1 + +# Required by: +# zope.testrunner==4.4.1 +six = 1.6.1 + +# Required by: +# collective.recipe.omelette==0.16 +zc.recipe.egg = 2.0.1 + +# Required by: +# zope.testrunner==4.4.1 +zope.exceptions = 4.0.6 + +# Required by: +# zope.testrunner==4.4.1 +zope.interface = 4.0.5 + +# Required by: +# plone.testing==4.0.8 +zope.testing = 4.1.2 + +# Required by: +# zc.recipe.testrunner==2.0.0 +zope.testrunner = 4.4.1 + +# Required by: +# searx==0.3.0 +python-dateutil = 2.2