summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.coveragerc20
-rw-r--r--.landscape.yaml3
-rw-r--r--.travis.yml4
-rw-r--r--AUTHORS.rst2
-rw-r--r--Makefile10
-rw-r--r--searx/__init__.py20
-rw-r--r--searx/autocomplete.py29
-rw-r--r--searx/engines/__init__.py149
-rw-r--r--searx/engines/bing.py47
-rw-r--r--searx/engines/bing_images.py80
-rw-r--r--searx/engines/bing_news.py85
-rw-r--r--searx/engines/currency_convert.py1
-rw-r--r--searx/engines/dailymotion.py61
-rw-r--r--searx/engines/deviantart.py33
-rw-r--r--searx/engines/duckduckgo.py77
-rw-r--r--searx/engines/dummy.py8
-rw-r--r--searx/engines/flickr.py62
-rw-r--r--searx/engines/generalfile.py60
-rw-r--r--searx/engines/github.py33
-rw-r--r--searx/engines/google.py116
-rw-r--r--searx/engines/google_images.py30
-rw-r--r--searx/engines/google_news.py31
-rw-r--r--searx/engines/mediawiki.py76
-rw-r--r--searx/engines/openstreetmap.py47
-rw-r--r--searx/engines/piratebay.py41
-rw-r--r--searx/engines/soundcloud.py34
-rw-r--r--searx/engines/stackoverflow.py42
-rw-r--r--searx/engines/startpage.py77
-rw-r--r--searx/engines/twitter.py39
-rw-r--r--searx/engines/vimeo.py62
-rw-r--r--searx/engines/wikipedia.py30
-rw-r--r--searx/engines/yacy.py85
-rw-r--r--searx/engines/yahoo.py53
-rw-r--r--searx/engines/yahoo_news.py42
-rw-r--r--searx/engines/youtube.py43
-rw-r--r--searx/https_rewrite.py14
-rw-r--r--searx/languages.py18
-rw-r--r--searx/query.py127
-rw-r--r--searx/search.py348
-rw-r--r--searx/settings.yml64
-rw-r--r--searx/static/courgette/css/style.css464
-rw-r--r--searx/static/courgette/img/bg-body-index.jpgbin0 -> 350109 bytes
-rw-r--r--searx/static/courgette/img/favicon.png (renamed from searx/static/img/favicon.png)bin3208 -> 3208 bytes
-rw-r--r--searx/static/courgette/img/github_ribbon.png (renamed from searx/static/img/github_ribbon.png)bin7791 -> 7791 bytes
-rw-r--r--searx/static/courgette/img/icon_github.ico (renamed from searx/static/img/icon_github.ico)bin6518 -> 6518 bytes
-rw-r--r--searx/static/courgette/img/icon_soundcloud.ico (renamed from searx/static/img/icon_soundcloud.ico)bin1150 -> 1150 bytes
-rw-r--r--searx/static/courgette/img/icon_stackoverflow.ico (renamed from searx/static/img/icon_stackoverflow.ico)bin1150 -> 1150 bytes
-rw-r--r--searx/static/courgette/img/icon_twitter.ico (renamed from searx/static/img/icon_twitter.ico)bin1150 -> 1150 bytes
-rw-r--r--searx/static/courgette/img/icon_vimeo.ico (renamed from searx/static/img/icon_vimeo.ico)bin6518 -> 6518 bytes
-rw-r--r--searx/static/courgette/img/icon_wikipedia.ico (renamed from searx/static/img/icon_wikipedia.ico)bin14858 -> 14858 bytes
-rw-r--r--searx/static/courgette/img/icon_youtube.ico (renamed from searx/static/img/icon_youtube.ico)bin1150 -> 1150 bytes
-rw-r--r--searx/static/courgette/img/preference-icon.pngbin0 -> 1574 bytes
-rw-r--r--searx/static/courgette/img/search-icon.pngbin0 -> 3442 bytes
-rw-r--r--searx/static/courgette/img/searx.png (renamed from searx/static/img/searx.png)bin7647 -> 7647 bytes
-rw-r--r--searx/static/courgette/img/searx_logo.svg (renamed from searx/static/img/searx_logo.svg)0
-rw-r--r--searx/static/courgette/js/mootools-autocompleter-1.1.2-min.js (renamed from searx/static/js/mootools-autocompleter-1.1.2-min.js)0
-rw-r--r--searx/static/courgette/js/mootools-core-1.4.5-min.js (renamed from searx/static/js/mootools-core-1.4.5-min.js)0
-rw-r--r--searx/static/courgette/js/searx.js (renamed from searx/static/js/searx.js)0
-rw-r--r--searx/static/default/css/style.css (renamed from searx/static/css/style.css)5
-rw-r--r--searx/static/default/img/favicon.pngbin0 -> 3208 bytes
-rw-r--r--searx/static/default/img/github_ribbon.pngbin0 -> 7791 bytes
-rw-r--r--searx/static/default/img/icon_github.icobin0 -> 6518 bytes
-rw-r--r--searx/static/default/img/icon_soundcloud.icobin0 -> 1150 bytes
-rw-r--r--searx/static/default/img/icon_stackoverflow.icobin0 -> 1150 bytes
-rw-r--r--searx/static/default/img/icon_twitter.icobin0 -> 1150 bytes
-rw-r--r--searx/static/default/img/icon_vimeo.icobin0 -> 6518 bytes
-rw-r--r--searx/static/default/img/icon_wikipedia.icobin0 -> 14858 bytes
-rw-r--r--searx/static/default/img/icon_youtube.icobin0 -> 1150 bytes
-rw-r--r--searx/static/default/img/preference-icon.png (renamed from searx/static/img/preference-icon.png)bin837 -> 837 bytes
-rw-r--r--searx/static/default/img/search-icon.png (renamed from searx/static/img/search-icon.png)bin3287 -> 3287 bytes
-rw-r--r--searx/static/default/img/searx.pngbin0 -> 7647 bytes
-rw-r--r--searx/static/default/img/searx_logo.svg203
-rw-r--r--searx/static/default/js/mootools-autocompleter-1.1.2-min.js2
-rw-r--r--searx/static/default/js/mootools-core-1.4.5-min.js491
-rw-r--r--searx/static/default/js/searx.js45
-rw-r--r--searx/static/default/less/autocompleter.less (renamed from searx/static/less/autocompleter.less)0
-rw-r--r--searx/static/default/less/definitions.less (renamed from searx/static/less/definitions.less)0
-rw-r--r--searx/static/default/less/mixins.less (renamed from searx/static/less/mixins.less)0
-rw-r--r--searx/static/default/less/search.less (renamed from searx/static/less/search.less)0
-rw-r--r--searx/static/default/less/style.less (renamed from searx/static/less/style.less)9
-rw-r--r--searx/templates/courgette/about.html (renamed from searx/templates/about.html)4
-rw-r--r--searx/templates/courgette/base.html (renamed from searx/templates/base.html)0
-rw-r--r--searx/templates/courgette/categories.html (renamed from searx/templates/categories.html)0
-rw-r--r--searx/templates/courgette/github_ribbon.html (renamed from searx/templates/github_ribbon.html)0
-rw-r--r--searx/templates/courgette/index.html (renamed from searx/templates/index.html)5
-rw-r--r--searx/templates/courgette/opensearch.xml (renamed from searx/templates/opensearch.xml)0
-rw-r--r--searx/templates/courgette/opensearch_response_rss.xml (renamed from searx/templates/opensearch_response_rss.xml)0
-rw-r--r--searx/templates/courgette/preferences.html (renamed from searx/templates/preferences.html)14
-rw-r--r--searx/templates/courgette/result_templates/default.html (renamed from searx/templates/result_templates/default.html)2
-rw-r--r--searx/templates/courgette/result_templates/images.html (renamed from searx/templates/result_templates/images.html)0
-rw-r--r--searx/templates/courgette/result_templates/torrent.html (renamed from searx/templates/result_templates/torrent.html)0
-rw-r--r--searx/templates/courgette/result_templates/videos.html (renamed from searx/templates/result_templates/videos.html)2
-rw-r--r--searx/templates/courgette/results.html (renamed from searx/templates/results.html)8
-rw-r--r--searx/templates/courgette/search.html (renamed from searx/templates/search.html)2
-rw-r--r--searx/templates/courgette/stats.html (renamed from searx/templates/stats.html)2
-rw-r--r--searx/templates/default/about.html66
-rw-r--r--searx/templates/default/base.html32
-rw-r--r--searx/templates/default/categories.html7
-rw-r--r--searx/templates/default/github_ribbon.html3
-rw-r--r--searx/templates/default/index.html12
-rw-r--r--searx/templates/default/opensearch.xml27
-rw-r--r--searx/templates/default/opensearch_response_rss.xml23
-rw-r--r--searx/templates/default/preferences.html101
-rw-r--r--searx/templates/default/result_templates/default.html13
-rw-r--r--searx/templates/default/result_templates/images.html6
-rw-r--r--searx/templates/default/result_templates/torrent.html7
-rw-r--r--searx/templates/default/result_templates/videos.html12
-rw-r--r--searx/templates/default/results.html79
-rw-r--r--searx/templates/default/search.html7
-rw-r--r--searx/templates/default/stats.html22
-rw-r--r--searx/tests/engines/__init__.py0
-rw-r--r--searx/tests/engines/test_dummy.py26
-rw-r--r--searx/tests/engines/test_github.py61
-rw-r--r--searx/tests/test_engines.py2
-rw-r--r--searx/tests/test_utils.py69
-rw-r--r--searx/tests/test_webapp.py8
-rw-r--r--searx/utils.py26
-rw-r--r--searx/webapp.py145
-rw-r--r--setup.py17
-rw-r--r--utils/standalone_search.py36
120 files changed, 3695 insertions, 573 deletions
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 000000000..4f50efc40
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,20 @@
+[run]
+branch = True
+source =
+ searx/engines
+ searx/__init__.py
+ searx/autocomplete.py
+ searx/https_rewrite.py
+ searx/languages.py
+ searx/search.py
+ searx/testing.py
+ searx/utils.py
+ searx/webapp.py
+
+[report]
+show_missing = True
+exclude_lines =
+ if __name__ == .__main__.:
+
+[html]
+directory = coverage
diff --git a/.landscape.yaml b/.landscape.yaml
new file mode 100644
index 000000000..1bb397718
--- /dev/null
+++ b/.landscape.yaml
@@ -0,0 +1,3 @@
+strictness: high
+ignore-paths:
+ - bootstrap.py
diff --git a/.travis.yml b/.travis.yml
index ee0b506a5..1bf0be330 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,10 +7,14 @@ before_install:
- npm install -g less
install:
- "make"
+ - pip install coveralls
script:
- "make tests"
- "make robot"
- "make styles"
+ - make coverage
+after_success:
+ coveralls
notifications:
irc:
channels:
diff --git a/AUTHORS.rst b/AUTHORS.rst
index d00d54213..311c97781 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -24,3 +24,5 @@ generally made searx better:
- Alejandro León Aznar
- rike
- dp
+- Martin Zimmermann
+- @courgette
diff --git a/Makefile b/Makefile
index fbc1c4859..f56060868 100644
--- a/Makefile
+++ b/Makefile
@@ -29,9 +29,9 @@ flake8: .installed.cfg
@bin/flake8 ./searx/
coverage: .installed.cfg
- @bin/coverage run --source=./searx/ --branch bin/test
- @bin/coverage report --show-missing
- @bin/coverage html --directory ./coverage
+ @bin/coverage run bin/test
+ @bin/coverage report
+ @bin/coverage html
production: bin/buildout production.cfg setup.py
bin/buildout -c production.cfg $(options)
@@ -44,13 +44,13 @@ 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
+ @lessc -x searx/static/default/less/style.less > searx/static/default/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
+ searx.egg-info lib include .coverage coverage searx/static/default/css/*.css
.PHONY: all tests robot flake8 coverage production minimal styles locales clean
diff --git a/searx/__init__.py b/searx/__init__.py
index 375a5414a..17da2f353 100644
--- a/searx/__init__.py
+++ b/searx/__init__.py
@@ -1,3 +1,20 @@
+'''
+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, <asciimoo@gmail.com>
+'''
+
from os import environ
from os.path import realpath, dirname, join, abspath
try:
@@ -10,11 +27,14 @@ except:
searx_dir = abspath(dirname(__file__))
engine_dir = dirname(realpath(__file__))
+# if possible set path to settings using the enviroment variable SEARX_SETTINGS_PATH
if 'SEARX_SETTINGS_PATH' in environ:
settings_path = environ['SEARX_SETTINGS_PATH']
+# otherwise using default path
else:
settings_path = join(searx_dir, 'settings.yml')
+# load settings
with open(settings_path) as settings_yaml:
settings = load(settings_yaml)
diff --git a/searx/autocomplete.py b/searx/autocomplete.py
index 1726a8c3d..183769af8 100644
--- a/searx/autocomplete.py
+++ b/searx/autocomplete.py
@@ -1,3 +1,21 @@
+'''
+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, <asciimoo@gmail.com>
+'''
+
+
from lxml import etree
from requests import get
from json import loads
@@ -21,6 +39,16 @@ def dbpedia(query):
return results
+def duckduckgo(query):
+ # duckduckgo autocompleter
+ url = 'https://ac.duckduckgo.com/ac/?{0}&type=list'
+
+ resp = loads(get(url.format(urlencode(dict(q=query)))).text)
+ if len(resp) > 1:
+ return resp[1]
+ return []
+
+
def google(query):
# google autocompleter
autocomplete_url = 'http://suggestqueries.google.com/complete/search?client=toolbar&' # noqa
@@ -48,6 +76,7 @@ def wikipedia(query):
backends = {'dbpedia': dbpedia,
+ 'duckduckgo': duckduckgo,
'google': google,
'wikipedia': wikipedia
}
diff --git a/searx/engines/__init__.py b/searx/engines/__init__.py
index 72e537423..82c9407a2 100644
--- a/searx/engines/__init__.py
+++ b/searx/engines/__init__.py
@@ -19,19 +19,12 @@ along with searx. If not, see < http://www.gnu.org/licenses/ >.
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 operator import itemgetter
from searx import settings
-from searx.utils import gen_useragent
engine_dir = dirname(realpath(__file__))
-number_of_searches = 0
-
engines = {}
categories = {'general': []}
@@ -114,146 +107,6 @@ for engine_data in settings['engines']:
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 = []
diff --git a/searx/engines/bing.py b/searx/engines/bing.py
index 9712a3103..56c6b36c1 100644
--- a/searx/engines/bing.py
+++ b/searx/engines/bing.py
@@ -1,49 +1,82 @@
+## Bing (Web)
+#
+# @website https://www.bing.com
+# @provide-api yes (http://datamarket.azure.com/dataset/bing/search), max. 5000 query/month
+#
+# @using-api no (because of query limit)
+# @results HTML (using search portal)
+# @stable no (HTML can change)
+# @parse url, title, content
+#
+# @todo publishedDate
+
from urllib import urlencode
from cgi import escape
from lxml import html
-base_url = 'http://www.bing.com/'
-search_string = 'search?{query}&first={offset}'
+# engine dependent config
+categories = ['general']
paging = True
language_support = True
+# search-url
+base_url = 'https://www.bing.com/'
+search_string = 'search?{query}&first={offset}'
+
+# do search-request
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
+# get response from search-request
def response(resp):
- global base_url
results = []
+
dom = html.fromstring(resp.content)
+
+ # parse results
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})
+ # append result
+ results.append({'url': url,
+ 'title': title,
+ 'content': content})
+
+ # return results if something is found
if results:
return results
+ # parse results again if nothing is found yet
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})
+
+ # append result
+ results.append({'url': url,
+ 'title': title,
+ 'content': content})
+
+ # return results
return results
diff --git a/searx/engines/bing_images.py b/searx/engines/bing_images.py
new file mode 100644
index 000000000..b3eabba45
--- /dev/null
+++ b/searx/engines/bing_images.py
@@ -0,0 +1,80 @@
+## Bing (Images)
+#
+# @website https://www.bing.com/images
+# @provide-api yes (http://datamarket.azure.com/dataset/bing/search), max. 5000 query/month
+#
+# @using-api no (because of query limit)
+# @results HTML (using search portal)
+# @stable no (HTML can change)
+# @parse url, title, img_src
+#
+# @todo currently there are up to 35 images receive per page, because bing does not parse count=10. limited response to 10 images
+
+from urllib import urlencode
+from cgi import escape
+from lxml import html
+from yaml import load
+import re
+
+# engine dependent config
+categories = ['images']
+paging = True
+
+# search-url
+base_url = 'https://www.bing.com/'
+search_string = 'images/search?{query}&count=10&first={offset}'
+
+
+# do search-request
+def request(query, params):
+ offset = (params['pageno'] - 1) * 10 + 1
+
+ # required for cookie
+ language = 'en-US'
+
+ search_path = search_string.format(
+ query=urlencode({'q': query}),
+ offset=offset)
+
+ params['cookies']['SRCHHPGUSR'] = \
+ 'NEWWND=0&NRSLT=-1&SRCHLANG=' + language.split('-')[0]
+
+ params['url'] = base_url + search_path
+
+ return params
+
+
+# get response from search-request
+def response(resp):
+ results = []
+
+ dom = html.fromstring(resp.content)
+
+ # init regex for yaml-parsing
+ p = re.compile( '({|,)([a-z]+):(")')
+
+ # parse results
+ for result in dom.xpath('//div[@class="dg_u"]'):
+ link = result.xpath('./a')[0]
+
+ # parse yaml-data (it is required to add a space, to make it parsable)
+ yaml_data = load(p.sub( r'\1\2: \3', link.attrib.get('m')))
+
+ title = link.attrib.get('t1')
+ #url = 'http://' + link.attrib.get('t3')
+ url = yaml_data.get('surl')
+ img_src = yaml_data.get('imgurl')
+
+ # append result
+ results.append({'template': 'images.html',
+ 'url': url,
+ 'title': title,
+ 'content': '',
+ 'img_src': img_src})
+
+ # TODO stop parsing if 10 images are found
+ if len(results) >= 10:
+ break
+
+ # return results
+ return results
diff --git a/searx/engines/bing_news.py b/searx/engines/bing_news.py
index 5b48a5450..279f0d698 100644
--- a/searx/engines/bing_news.py
+++ b/searx/engines/bing_news.py
@@ -1,51 +1,100 @@
+## Bing (News)
+#
+# @website https://www.bing.com/news
+# @provide-api yes (http://datamarket.azure.com/dataset/bing/search), max. 5000 query/month
+#
+# @using-api no (because of query limit)
+# @results HTML (using search portal)
+# @stable no (HTML can change)
+# @parse url, title, content, publishedDate
+
from urllib import urlencode
from cgi import escape
from lxml import html
+from datetime import datetime, timedelta
+from dateutil import parser
+import re
+# engine dependent config
categories = ['news']
-
-base_url = 'http://www.bing.com/'
-search_string = 'news/search?{query}&first={offset}'
paging = True
language_support = True
+# search-url
+base_url = 'https://www.bing.com/'
+search_string = 'news/search?{query}&first={offset}'
+
+# do search-request
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
+# get response from search-request
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]
+
+ # parse results
+ for result in dom.xpath('//div[@class="sn_r"]'):
+ link = result.xpath('.//div[@class="newstitle"]/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})
+ contentXPath = result.xpath('.//div[@class="sn_txt"]/div//span[@class="sn_snip"]//text()')
+ if contentXPath != None:
+ content = escape(' '.join(contentXPath))
+
+ # parse publishedDate
+ publishedDateXPath = result.xpath('.//div[@class="sn_txt"]/div//span[contains(@class,"sn_ST")]//span[contains(@class,"sn_tm")]//text()')
+ if publishedDateXPath != None:
+ publishedDate = escape(' '.join(publishedDateXPath))
- if results:
- return results
+ if re.match("^[0-9]+ minute(s|) ago$", publishedDate):
+ timeNumbers = re.findall(r'\d+', publishedDate)
+ publishedDate = datetime.now()\
+ - timedelta(minutes=int(timeNumbers[0]))
+ elif re.match("^[0-9]+ hour(s|) ago$", publishedDate):
+ timeNumbers = re.findall(r'\d+', publishedDate)
+ publishedDate = datetime.now()\
+ - timedelta(hours=int(timeNumbers[0]))
+ elif 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]))
+ elif re.match("^[0-9]+ day(s|) ago$", publishedDate):
+ timeNumbers = re.findall(r'\d+', publishedDate)
+ publishedDate = datetime.now()\
+ - timedelta(days=int(timeNumbers[0]))
+ else:
+ try:
+ # FIXME use params['language'] to parse either mm/dd or dd/mm
+ publishedDate = parser.parse(publishedDate, dayfirst=False)
+ except TypeError:
+ # FIXME
+ publishedDate = datetime.now()
+
+ # append result
+ results.append({'url': url,
+ 'title': title,
+ 'publishedDate': publishedDate,
+ 'content': content})
- 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
return results
diff --git a/searx/engines/currency_convert.py b/searx/engines/currency_convert.py
index ce6b3b854..561527bce 100644
--- a/searx/engines/currency_convert.py
+++ b/searx/engines/currency_convert.py
@@ -31,7 +31,6 @@ def request(query, params):
def response(resp):
- global base_url
results = []
try:
_, conversion_rate, _ = resp.text.split(',', 2)
diff --git a/searx/engines/dailymotion.py b/searx/engines/dailymotion.py
index 03e1d7ffc..75c2e5071 100644
--- a/searx/engines/dailymotion.py
+++ b/searx/engines/dailymotion.py
@@ -1,45 +1,66 @@
+## Dailymotion (Videos)
+#
+# @website https://www.dailymotion.com
+# @provide-api yes (http://www.dailymotion.com/developer)
+#
+# @using-api yes
+# @results JSON
+# @stable yes
+# @parse url, title, thumbnail
+#
+# @todo set content-parameter with correct data
+
from urllib import urlencode
from json import loads
from lxml import html
+# engine dependent config
categories = ['videos']
-locale = 'en_US'
+paging = True
+language_support = True
+# search-url
# 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 = '<a href="{0}" title="{0}" ><img src="{1}" /></a><br />'
-
-paging = True
+search_url = 'https://api.dailymotion.com/videos?fields=title,description,duration,url,thumbnail_360_url&sort=relevance&limit=5&page={pageno}&{query}' # noqa
+# do search-request
def request(query, params):
+ if params['language'] == 'all':
+ locale = 'en-US'
+ else:
+ locale = params['language']
+
params['url'] = search_url.format(
query=urlencode({'search': query, 'localization': locale}),
pageno=params['pageno'])
+
return params
+# get response from search-request
def response(resp):
results = []
+
search_res = loads(resp.text)
+
+ # return empty array if there are no results
if not 'list' in search_res:
- return results
+ return []
+
+ # parse 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
+ #content = res['description']
+ content = ''
+ thumbnail = res['thumbnail_360_url']
+ results.append({'template': 'videos.html',
+ 'url': url,
+ 'title': title,
+ 'content': content,
+ 'thumbnail': thumbnail})
-def text_content_from_html(html_string):
- desc_html = html.fragment_fromstring(html_string, create_parent=True)
- return desc_html.text_content()
+ # return results
+ return results
diff --git a/searx/engines/deviantart.py b/searx/engines/deviantart.py
index d42a25a19..ff5e1d465 100644
--- a/searx/engines/deviantart.py
+++ b/searx/engines/deviantart.py
@@ -1,36 +1,61 @@
+## Deviantart (Images)
+#
+# @website https://www.deviantart.com/
+# @provide-api yes (https://www.deviantart.com/developers/) (RSS)
+#
+# @using-api no (TODO, rewrite to api)
+# @results HTML
+# @stable no (HTML can change)
+# @parse url, title, thumbnail, img_src
+#
+# @todo rewrite to api
+
from urllib import urlencode
from urlparse import urljoin
from lxml import html
+# engine dependent config
categories = ['images']
+paging = True
+# search-url
base_url = 'https://www.deviantart.com/'
search_url = base_url+'search?offset={offset}&{query}'
-paging = True
-
+# do search-request
def request(query, params):
offset = (params['pageno'] - 1) * 24
+
params['url'] = search_url.format(offset=offset,
query=urlencode({'q': query}))
+
return params
+# get response from search-request
def response(resp):
- global base_url
results = []
+
+ # return empty array if a redirection code is returned
if resp.status_code == 302:
- return results
+ return []
+
dom = html.fromstring(resp.text)
+
+ # parse results
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']
+
+ # append result
results.append({'url': url,
'title': title,
'img_src': img_src,
'template': 'images.html'})
+
+ # return results
return results
diff --git a/searx/engines/duckduckgo.py b/searx/engines/duckduckgo.py
index 58cbc9872..296dd9b2d 100644
--- a/searx/engines/duckduckgo.py
+++ b/searx/engines/duckduckgo.py
@@ -1,65 +1,74 @@
+## DuckDuckGo (Web)
+#
+# @website https://duckduckgo.com/
+# @provide-api yes (https://duckduckgo.com/api), but not all results from search-site
+#
+# @using-api no
+# @results HTML (using search portal)
+# @stable no (HTML can change)
+# @parse url, title, content
+#
+# @todo rewrite to api
+# @todo language support (the current used site does not support language-change)
+
from urllib import urlencode
from lxml.html import fromstring
from searx.utils import html_to_text
+# engine dependent config
+categories = ['general']
+paging = True
+language_support = True
+
+# search-url
url = 'https://duckduckgo.com/html?{query}&s={offset}'
-locale = 'us-en'
+# specific xpath variables
+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()'
+
+# do search-request
def request(query, params):
offset = (params['pageno'] - 1) * 30
- q = urlencode({'q': query,
- 'l': locale})
- params['url'] = url.format(query=q, offset=offset)
+
+ if params['language'] == 'all':
+ locale = 'en-us'
+ else:
+ locale = params['language'].replace('_','-').lower()
+
+ params['url'] = url.format(
+ query=urlencode({'q': query, 'kl': locale}),
+ offset=offset)
+
return params
+# get response from search-request
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)
+ # parse results
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)))
+
+ # append result
results.append({'title': title,
'content': content,
'url': res_url})
+ # return results
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/searx/engines/dummy.py b/searx/engines/dummy.py
index 4586760a0..5a2cdf6b5 100644
--- a/searx/engines/dummy.py
+++ b/searx/engines/dummy.py
@@ -1,6 +1,14 @@
+## Dummy
+#
+# @results empty array
+# @stable yes
+
+
+# do search-request
def request(query, params):
return params
+# get response from search-request
def response(resp):
return []
diff --git a/searx/engines/flickr.py b/searx/engines/flickr.py
index 59513b41c..4ec2841dd 100644
--- a/searx/engines/flickr.py
+++ b/searx/engines/flickr.py
@@ -1,48 +1,54 @@
#!/usr/bin/env python
from urllib import urlencode
-from json import loads
-#from urlparse import urljoin
+#from json import loads
+from urlparse import urljoin
+from lxml import html
+from time import time
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
+url = 'https://secure.flickr.com/'
+search_url = url+'search/?{query}&page={page}'
+results_xpath = '//div[@class="view display-item-tile"]/figure/div'
paging = True
-# text=[query]
-# TODO clean "extras"
-search_url = 'https://api.flickr.com/services/rest?extras=can_addmeta%2Ccan_comment%2Ccan_download%2Ccan_share%2Ccontact%2Ccount_comments%2Ccount_faves%2Ccount_notes%2Cdate_taken%2Cdate_upload%2Cdescription%2Cicon_urls_deep%2Cisfavorite%2Cispro%2Clicense%2Cmedia%2Cneeds_interstitial%2Cowner_name%2Cowner_datecreate%2Cpath_alias%2Crealname%2Csafety_level%2Csecret_k%2Csecret_h%2Curl_c%2Curl_h%2Curl_k%2Curl_l%2Curl_m%2Curl_n%2Curl_o%2Curl_q%2Curl_s%2Curl_sq%2Curl_t%2Curl_z%2Cvisibility&per_page=50&page={page}&{query}&sort=relevance&method=flickr.photos.search&api_key=ad11b34c341305471e3c410a02e671d0&format=json' # noqa
-
def request(query, params):
params['url'] = search_url.format(query=urlencode({'text': query}),
page=params['pageno'])
- #params['url'] = search_url.format(query=urlencode({'q': query}),
- # page=params['pageno'])
+ time_string = str(int(time())-3)
+ params['cookies']['BX'] = '3oqjr6d9nmpgl&b=3&s=dh'
+ params['cookies']['xb'] = '421409'
+ params['cookies']['localization'] = 'en-us'
+ params['cookies']['flrbp'] = time_string +\
+ '-3a8cdb85a427a33efda421fbda347b2eaf765a54'
+ params['cookies']['flrbs'] = time_string +\
+ '-ed142ae8765ee62c9ec92a9513665e0ee1ba6776'
+ params['cookies']['flrb'] = '9'
return params
def response(resp):
results = []
- images = loads(resp.text[14:-1])["photos"]["photo"]
- for i in images:
- results.append({'url': i['url_s'],
- 'title': i['title'],
- 'img_src': i['url_s'],
+ dom = html.fromstring(resp.text)
+ for result in dom.xpath(results_xpath):
+ img = result.xpath('.//img')
+
+ if not img:
+ continue
+
+ img = img[0]
+ img_src = 'https:'+img.attrib.get('src')
+
+ if not img_src:
+ continue
+
+ href = urljoin(url, result.xpath('.//a')[0].attrib.get('href'))
+ title = img.attrib.get('alt', '')
+ results.append({'url': href,
+ 'title': title,
+ 'img_src': img_src,
'template': 'images.html'})
- #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/searx/engines/generalfile.py b/searx/engines/generalfile.py
new file mode 100644
index 000000000..11d8b6955
--- /dev/null
+++ b/searx/engines/generalfile.py
@@ -0,0 +1,60 @@
+## General Files (Files)
+#
+# @website http://www.general-files.org
+# @provide-api no (nothing found)
+#
+# @using-api no (because nothing found)
+# @results HTML (using search portal)
+# @stable no (HTML can change)
+# @parse url, title, content
+#
+# @todo detect torrents?
+
+from lxml import html
+
+# engine dependent config
+categories = ['files']
+paging = True
+
+# search-url
+base_url = 'http://www.general-file.com'
+search_url = base_url + '/files-{letter}/{query}/{pageno}'
+
+# specific xpath variables
+result_xpath = '//table[@class="block-file"]'
+title_xpath = './/h2/a//text()'
+url_xpath = './/h2/a/@href'
+content_xpath = './/p//text()'
+
+
+# do search-request
+def request(query, params):
+
+ params['url'] = search_url.format(query=query,
+ letter=query[0],
+ pageno=params['pageno'])
+
+ return params
+
+
+# get response from search-request
+def response(resp):
+ results = []
+
+ dom = html.fromstring(resp.text)
+
+ # parse results
+ for result in dom.xpath(result_xpath):
+ url = result.xpath(url_xpath)[0]
+
+ # skip fast download links
+ if not url.startswith('/'):
+ continue
+
+ # append result
+ results.append({'url': base_url + url,
+ 'title': ''.join(result.xpath(title_xpath)),
+ 'content': ''.join(result.xpath(content_xpath))})
+
+ # return results
+ return results
diff --git a/searx/engines/github.py b/searx/engines/github.py
index be2cfe7c5..53fec029f 100644
--- a/searx/engines/github.py
+++ b/searx/engines/github.py
@@ -1,32 +1,59 @@
+## Github (It)
+#
+# @website https://github.com/
+# @provide-api yes (https://developer.github.com/v3/)
+#
+# @using-api yes
+# @results JSON
+# @stable yes (using api)
+# @parse url, title, content
+
from urllib import urlencode
from json import loads
from cgi import escape
+# engine dependent config
categories = ['it']
+# search-url
search_url = 'https://api.github.com/search/repositories?sort=stars&order=desc&{query}' # noqa
accept_header = 'application/vnd.github.preview.text-match+json'
+# do search-request
def request(query, params):
- global search_url
params['url'] = search_url.format(query=urlencode({'q': query}))
+
params['headers']['Accept'] = accept_header
+
return params
+# get response from search-request
def response(resp):
results = []
+
search_res = loads(resp.text)
+
+ # check if items are recieved
if not 'items' in search_res:
- return results
+ return []
+
+ # parse 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})
+
+ # append result
+ results.append({'url': url,
+ 'title': title,
+ 'content': content})
+
+ # return results
return results
diff --git a/searx/engines/google.py b/searx/engines/google.py
index 2c6a98af3..9dbe8b8f0 100644
--- a/searx/engines/google.py
+++ b/searx/engines/google.py
@@ -1,37 +1,115 @@
-#!/usr/bin/env python
+## Google (Web)
+#
+# @website https://www.google.com
+# @provide-api yes (https://developers.google.com/custom-search/)
+#
+# @using-api no
+# @results HTML
+# @stable no (HTML can change)
+# @parse url, title, content, suggestion
from urllib import urlencode
-from json import loads
+from urlparse import unquote,urlparse,parse_qsl
+from lxml import html
+from searx.engines.xpath import extract_text, extract_url
+# engine dependent config
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
+# search-url
+google_hostname = 'www.google.com'
+search_path = '/search'
+redirect_path = '/url'
+images_path = '/images'
+search_url = 'https://' + google_hostname + search_path + '?{query}&start={offset}&gbv=1'
+
+# specific xpath variables
+results_xpath= '//li[@class="g"]'
+url_xpath = './/h3/a/@href'
+title_xpath = './/h3'
+content_xpath = './/span[@class="st"]'
+suggestion_xpath = '//p[@class="_Bmc"]'
+
+images_xpath = './/div/a'
+image_url_xpath = './@href'
+image_img_src_xpath = './img/@src'
+# remove google-specific tracking-url
+def parse_url(url_string):
+ parsed_url = urlparse(url_string)
+ if parsed_url.netloc in [google_hostname, ''] and parsed_url.path==redirect_path:
+ query = dict(parse_qsl(parsed_url.query))
+ return query['q']
+ else:
+ return url_string
+
+# do search-request
def request(query, params):
- offset = (params['pageno'] - 1) * 8
- language = 'en-US'
- if params['language'] != 'all':
- language = params['language'].replace('_', '-')
+ offset = (params['pageno'] - 1) * 10
+
+ if params['language'] == 'all':
+ language = 'en'
+ else:
+ language = params['language'].replace('_','-').lower()
+
params['url'] = search_url.format(offset=offset,
- query=urlencode({'q': query}),
- language=language)
+ query=urlencode({'q': query}))
+
+ params['headers']['Accept-Language'] = language
+
return params
+# get response from search-request
def response(resp):
results = []
- search_res = loads(resp.text)
- if not search_res.get('responseData', {}).get('results'):
- return []
+ dom = html.fromstring(resp.text)
+
+ # parse results
+ for result in dom.xpath(results_xpath):
+ title = extract_text(result.xpath(title_xpath)[0])
+ try:
+ url = parse_url(extract_url(result.xpath(url_xpath), search_url))
+ parsed_url = urlparse(url)
+ if parsed_url.netloc==google_hostname and parsed_url.path==search_path:
+ # remove the link to google news
+ continue
+
+ if parsed_url.netloc==google_hostname and parsed_url.path==images_path:
+ # images result
+ results = results + parse_images(result)
+ else:
+ # normal result
+ content = extract_text(result.xpath(content_xpath)[0])
+ # append result
+ results.append({'url': url,
+ 'title': title,
+ 'content': content})
+ except:
+ continue
+
+ # parse suggestion
+ for suggestion in dom.xpath(suggestion_xpath):
+ # append suggestion
+ results.append({'suggestion': extract_text(suggestion)})
+
+ # return results
+ return results
+
+def parse_images(result):
+ results = []
+ for image in result.xpath(images_xpath):
+ url = parse_url(extract_text(image.xpath(image_url_xpath)[0]))
+ img_src = extract_text(image.xpath(image_img_src_xpath)[0])
+
+ # append result
+ results.append({'url': url,
+ 'title': '',
+ 'content': '',
+ 'img_src': img_src,
+ 'template': 'images.html'})
- for result in search_res['responseData']['results']:
- results.append({'url': result['unescapedUrl'],
- 'title': result['titleNoFormatting'],
- 'content': result['content']})
return results
diff --git a/searx/engines/google_images.py b/searx/engines/google_images.py
index a6837f039..6c99f2801 100644
--- a/searx/engines/google_images.py
+++ b/searx/engines/google_images.py
@@ -1,36 +1,58 @@
-#!/usr/bin/env python
+## Google (Images)
+#
+# @website https://www.google.com
+# @provide-api yes (https://developers.google.com/web-search/docs/), deprecated!
+#
+# @using-api yes
+# @results JSON
+# @stable yes (but deprecated)
+# @parse url, title, img_src
from urllib import urlencode
from json import loads
+# engine dependent config
categories = ['images']
+paging = True
+# search-url
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
+# do search-request
def request(query, params):
offset = (params['pageno'] - 1) * 8
+
params['url'] = search_url.format(query=urlencode({'q': query}),
offset=offset)
+
return params
+# get response from search-request
def response(resp):
results = []
+
search_res = loads(resp.text)
- if not search_res.get('responseData'):
- return []
- if not search_res['responseData'].get('results'):
+
+ # return empty array if there are no results
+ if not search_res.get('responseData', {}).get('results'):
return []
+
+ # parse results
for result in search_res['responseData']['results']:
href = result['originalContextUrl']
title = result['title']
if not result['url']:
continue
+
+ # append result
results.append({'url': href,
'title': title,
'content': '',
'img_src': result['url'],
'template': 'images.html'})
+
+ # return results
return results
diff --git a/searx/engines/google_news.py b/searx/engines/google_news.py
index 72b7a0661..becc7e21d 100644
--- a/searx/engines/google_news.py
+++ b/searx/engines/google_news.py
@@ -1,43 +1,62 @@
-#!/usr/bin/env python
+## Google (News)
+#
+# @website https://www.google.com
+# @provide-api yes (https://developers.google.com/web-search/docs/), deprecated!
+#
+# @using-api yes
+# @results JSON
+# @stable yes (but deprecated)
+# @parse url, title, content, publishedDate
from urllib import urlencode
from json import loads
from dateutil import parser
+# search-url
categories = ['news']
+paging = True
+language_support = True
+# engine dependent config
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
-
+# do search-request
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
+# get response from search-request
def response(resp):
results = []
+
search_res = loads(resp.text)
+ # return empty array if there are no results
if not search_res.get('responseData', {}).get('results'):
return []
+ # parse results
for result in search_res['responseData']['results']:
-
-# Mon, 10 Mar 2014 16:26:15 -0700
+ # parse publishedDate
publishedDate = parser.parse(result['publishedDate'])
+ # append result
results.append({'url': result['unescapedUrl'],
'title': result['titleNoFormatting'],
'publishedDate': publishedDate,
'content': result['content']})
+
+ # return results
return results
diff --git a/searx/engines/mediawiki.py b/searx/engines/mediawiki.py
index f8cfb9afa..4a8b0e8b8 100644
--- a/searx/engines/mediawiki.py
+++ b/searx/engines/mediawiki.py
@@ -1,22 +1,78 @@
+## general mediawiki-engine (Web)
+#
+# @website websites built on mediawiki (https://www.mediawiki.org)
+# @provide-api yes (http://www.mediawiki.org/wiki/API:Search)
+#
+# @using-api yes
+# @results JSON
+# @stable yes
+# @parse url, title
+#
+# @todo content
+
from json import loads
+from string import Formatter
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
+# engine dependent config
+categories = ['general']
+language_support = True
+paging = True
+number_of_results = 1
-number_of_results = 10
+# search-url
+base_url = 'https://{language}.wikipedia.org/'
+search_url = base_url + 'w/api.php?action=query'\
+ '&list=search'\
+ '&{query}'\
+ '&srprop=timestamp'\
+ '&format=json'\
+ '&sroffset={offset}'\
+ '&srlimit={limit}'
+# do search-request
def request(query, params):
- offset = (params['pageno'] - 1) * 10
- params['url'] = search_url.format(query=urlencode({'srsearch': query}),
- offset=offset)
+ offset = (params['pageno'] - 1) * number_of_results
+ string_args = dict(query=urlencode({'srsearch': query}),
+ offset=offset,
+ limit=number_of_results)
+ format_strings = list(Formatter().parse(base_url))
+
+ if params['language'] == 'all':
+ language = 'en'
+ else:
+ language = params['language'].split('_')[0]
+
+ if len(format_strings) > 1:
+ string_args['language'] = language
+
+ # write search-language back to params, required in response
+ params['language'] = language
+
+ params['url'] = search_url.format(**string_args)
+
return params
+# get response from search-request
def response(resp):
+ results = []
+
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)]]
+
+ # return empty array if there are no results
+ if not search_results.get('query', {}).get('search'):
+ return []
+
+ # parse results
+ for result in search_results['query']['search']:
+ url = base_url.format(language=resp.search_params['language']) + 'wiki/' + quote(result['title'].replace(' ', '_').encode('utf-8'))
+
+ # append result
+ results.append({'url': url,
+ 'title': result['title'],
+ 'content': ''})
+
+ # return results
+ return results
diff --git a/searx/engines/openstreetmap.py b/searx/engines/openstreetmap.py
new file mode 100644
index 000000000..ea7251486
--- /dev/null
+++ b/searx/engines/openstreetmap.py
@@ -0,0 +1,47 @@
+## OpenStreetMap (Map)
+#
+# @website https://openstreetmap.org/
+# @provide-api yes (http://wiki.openstreetmap.org/wiki/Nominatim)
+#
+# @using-api yes
+# @results JSON
+# @stable yes
+# @parse url, title
+
+from json import loads
+
+# engine dependent config
+categories = ['map']
+paging = False
+
+# search-url
+url = 'https://nominatim.openstreetmap.org/search/{query}?format=json'
+
+result_base_url = 'https://openstreetmap.org/{osm_type}/{osm_id}'
+
+
+# do search-request
+def request(query, params):
+ params['url'] = url.format(query=query)
+
+ return params
+
+
+# get response from search-request
+def response(resp):
+ results = []
+ json = loads(resp.text)
+
+ # parse results
+ for r in json:
+ title = r['display_name']
+ osm_type = r.get('osm_type', r.get('type'))
+ url = result_base_url.format(osm_type=osm_type,
+ osm_id=r['osm_id'])
+ # append result
+ results.append({'title': title,
+ 'content': '',
+ 'url': url})
+
+ # return results
+ return results
diff --git a/searx/engines/piratebay.py b/searx/engines/piratebay.py
index bb4886868..9533b629e 100644
--- a/searx/engines/piratebay.py
+++ b/searx/engines/piratebay.py
@@ -1,39 +1,61 @@
+## Piratebay (Videos, Music, Files)
+#
+# @website https://thepiratebay.se
+# @provide-api no (nothing found)
+#
+# @using-api no
+# @results HTML (using search portal)
+# @stable yes (HTML can change)
+# @parse url, title, content, seed, leech, magnetlink
+
from urlparse import urljoin
from cgi import escape
from urllib import quote
from lxml import html
from operator import itemgetter
-categories = ['videos', 'music']
+# engine dependent config
+categories = ['videos', 'music', 'files']
+paging = True
+# search-url
url = 'https://thepiratebay.se/'
search_url = url + 'search/{search_term}/{pageno}/99/{search_type}'
-search_types = {'videos': '200',
+
+# piratebay specific type-definitions
+search_types = {'files': '0',
'music': '100',
- 'files': '0'}
+ 'videos': '200'}
+# specific xpath variables
magnet_xpath = './/a[@title="Download this torrent using magnet"]'
content_xpath = './/font[@class="detDesc"]//text()'
-paging = True
-
+# do search-request
def request(query, params):
- search_type = search_types.get(params['category'], '200')
+ search_type = search_types.get(params['category'], '0')
+
params['url'] = search_url.format(search_term=quote(query),
search_type=search_type,
pageno=params['pageno'] - 1)
+
return params
+# get response from search-request
def response(resp):
results = []
+
dom = html.fromstring(resp.text)
+
search_res = dom.xpath('//table[@id="searchResult"]//tr')
+ # return empty array if nothing is found
if not search_res:
- return results
+ return []
+ # parse results
for result in search_res[1:]:
link = result.xpath('.//div[@class="detName"]//a')[0]
href = urljoin(url, link.attrib.get('href'))
@@ -41,17 +63,21 @@ def response(resp):
content = escape(' '.join(result.xpath(content_xpath)))
seed, leech = result.xpath('.//td[@align="right"]/text()')[:2]
+ # convert seed to int if possible
if seed.isdigit():
seed = int(seed)
else:
seed = 0
+ # convert leech to int if possible
if leech.isdigit():
leech = int(leech)
else:
leech = 0
magnetlink = result.xpath(magnet_xpath)[0]
+
+ # append result
results.append({'url': href,
'title': title,
'content': content,
@@ -60,4 +86,5 @@ def response(resp):
'magnetlink': magnetlink.attrib['href'],
'template': 'torrent.html'})
+ # return results sorted by seeder
return sorted(results, key=itemgetter('seed'), reverse=True)
diff --git a/searx/engines/soundcloud.py b/searx/engines/soundcloud.py
index e28fb1600..aebea239f 100644
--- a/searx/engines/soundcloud.py
+++ b/searx/engines/soundcloud.py
@@ -1,31 +1,55 @@
+## Soundcloud (Music)
+#
+# @website https://soundcloud.com
+# @provide-api yes (https://developers.soundcloud.com/)
+#
+# @using-api yes
+# @results JSON
+# @stable yes
+# @parse url, title, content
+
from json import loads
from urllib import urlencode
+# engine dependent config
categories = ['music']
+paging = True
+# api-key
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
+# search-url
+url = 'https://api.soundcloud.com/'
+search_url = url + 'search?{query}&facet=model&limit=20&offset={offset}&linked_partitioning=1&client_id={client_id}'
+# do search-request
def request(query, params):
offset = (params['pageno'] - 1) * 20
+
params['url'] = search_url.format(query=urlencode({'q': query}),
- offset=offset)
+ offset=offset,
+ client_id=guest_client_id)
+
return params
+# get response from search-request
def response(resp):
- global base_url
results = []
+
search_res = loads(resp.text)
+
+ # parse results
for result in search_res.get('collection', []):
if result['kind'] in ('track', 'playlist'):
title = result['title']
content = result['description']
+
+ # append result
results.append({'url': result['permalink_url'],
'title': title,
'content': content})
+
+ # return results
return results
diff --git a/searx/engines/stackoverflow.py b/searx/engines/stackoverflow.py
index e24b309c1..edbe74a70 100644
--- a/searx/engines/stackoverflow.py
+++ b/searx/engines/stackoverflow.py
@@ -1,30 +1,58 @@
+## Stackoverflow (It)
+#
+# @website https://stackoverflow.com/
+# @provide-api not clear (https://api.stackexchange.com/docs/advanced-search)
+#
+# @using-api no
+# @results HTML
+# @stable no (HTML can change)
+# @parse url, title, content
+
from urlparse import urljoin
from cgi import escape
from urllib import urlencode
from lxml import html
+# engine dependent config
categories = ['it']
+paging = True
+# search-url
url = 'http://stackoverflow.com/'
search_url = url+'search?{query}&page={pageno}'
-result_xpath = './/div[@class="excerpt"]//text()'
-paging = True
+# specific xpath variables
+results_xpath = '//div[contains(@class,"question-summary")]'
+link_xpath = './/div[@class="result-link"]//a|.//div[@class="summary"]//h3//a'
+title_xpath = './/text()'
+content_xpath = './/div[@class="excerpt"]//text()'
+# do search-request
def request(query, params):
params['url'] = search_url.format(query=urlencode({'q': query}),
pageno=params['pageno'])
+
return params
+# get response from search-request
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]
+
+ # parse results
+ for result in dom.xpath(results_xpath):
+ link = result.xpath(link_xpath)[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})
+ title = escape(' '.join(link.xpath(title_xpath)))
+ content = escape(' '.join(result.xpath(content_xpath)))
+
+ # append result
+ results.append({'url': href,
+ 'title': title,
+ 'content': content})
+
+ # return results
return results
diff --git a/searx/engines/startpage.py b/searx/engines/startpage.py
index f5a652317..2adbfb3e4 100644
--- a/searx/engines/startpage.py
+++ b/searx/engines/startpage.py
@@ -1,46 +1,79 @@
+## Startpage (Web)
+#
+# @website https://startpage.com
+# @provide-api no (nothing found)
+#
+# @using-api no
+# @results HTML
+# @stable no (HTML can change)
+# @parse url, title, content
+#
+# @todo paging
+
from urllib import urlencode
from lxml import html
+from cgi import escape
+import re
+
+# engine dependent config
+categories = ['general']
+# there is a mechanism to block "bot" search (probably the parameter qid), require storing of qid's between mulitble search-calls
+#paging = False
+language_support = True
-base_url = None
-search_url = None
+# search-url
+base_url = 'https://startpage.com/'
+search_url = base_url + 'do/search'
-# TODO paging
-paging = False
-# TODO complete list of country mapping
-country_map = {'en_US': 'eng',
- 'en_UK': 'uk',
- 'nl_NL': 'ned'}
+# specific xpath variables
+# ads xpath //div[@id="results"]/div[@id="sponsored"]//div[@class="result"]
+# not ads: div[@class="result"] are the direct childs of div[@id="results"]
+results_xpath = '//div[@class="result"]'
+link_xpath = './/h3/a'
+# do search-request
def request(query, params):
+ offset = (params['pageno'] - 1) * 10
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
+ 'startat': offset}
+
+ # set language if specified
+ if params['language'] != 'all':
+ params['data']['with_language'] = 'lang_' + params['language'].split('_')[0]
+
return params
+# get response from search-request
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]
+
+ # parse results
+ for result in dom.xpath(results_xpath):
+ link = result.xpath(link_xpath)[0]
url = link.attrib.get('href')
- if url.startswith('http://www.google.')\
- or url.startswith('https://www.google.'):
+ title = escape(link.text_content())
+
+ # block google-ad url's
+ if re.match("^http(s|)://www.google.[a-z]+/aclk.*$", url):
continue
- title = link.text_content()
- content = ''
if result.xpath('./p[@class="desc"]'):
- content = result.xpath('./p[@class="desc"]')[0].text_content()
+ content = escape(result.xpath('./p[@class="desc"]')[0].text_content())
+ else:
+ content = ''
- results.append({'url': url, 'title': title, 'content': content})
+ # append result
+ results.append({'url': url,
+ 'title': title,
+ 'content': content})
+ # return results
return results
diff --git a/searx/engines/twitter.py b/searx/engines/twitter.py
index 23393ac4d..8de78144e 100644
--- a/searx/engines/twitter.py
+++ b/searx/engines/twitter.py
@@ -1,32 +1,63 @@
+## Twitter (Social media)
+#
+# @website https://www.bing.com/news
+# @provide-api yes (https://dev.twitter.com/docs/using-search)
+#
+# @using-api no
+# @results HTML (using search portal)
+# @stable no (HTML can change)
+# @parse url, title, content
+#
+# @todo publishedDate
+
from urlparse import urljoin
from urllib import urlencode
from lxml import html
from cgi import escape
+# engine dependent config
categories = ['social media']
+language_support = True
+# search-url
base_url = 'https://twitter.com/'
search_url = base_url+'search?'
+
+# specific xpath variables
+results_xpath = '//li[@data-item-type="tweet"]'
+link_xpath = './/small[@class="time"]//a'
title_xpath = './/span[@class="username js-action-profile-name"]//text()'
content_xpath = './/p[@class="js-tweet-text tweet-text"]//text()'
+# do search-request
def request(query, params):
- global search_url
params['url'] = search_url + urlencode({'q': query})
+
+ # set language if specified
+ if params['language'] != 'all':
+ params['cookies']['lang'] = params['language'].split('_')[0]
+
return params
+# get response from search-request
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]
+
+ # parse results
+ for tweet in dom.xpath(results_xpath):
+ link = tweet.xpath(link_xpath)[0]
url = urljoin(base_url, link.attrib.get('href'))
title = ''.join(tweet.xpath(title_xpath))
content = escape(''.join(tweet.xpath(content_xpath)))
+
+ # append result
results.append({'url': url,
'title': title,
'content': content})
+
+ # return results
return results
diff --git a/searx/engines/vimeo.py b/searx/engines/vimeo.py
index 94a6dd545..2a91e76fa 100644
--- a/searx/engines/vimeo.py
+++ b/searx/engines/vimeo.py
@@ -1,43 +1,58 @@
+## Vimeo (Videos)
+#
+# @website https://vimeo.com/
+# @provide-api yes (http://developer.vimeo.com/api), they have a maximum count of queries/hour
+#
+# @using-api no (TODO, rewrite to api)
+# @results HTML (using search portal)
+# @stable no (HTML can change)
+# @parse url, title, publishedDate, thumbnail
+#
+# @todo rewrite to api
+# @todo set content-parameter with correct data
+
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 = '<a href="{0}"> <img src="{2}"/> </a>'
-publishedDate_xpath = './/p[@class="meta"]//attribute::datetime'
+# engine dependent config
+categories = ['videos']
+paging = True
-# 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'
-}
+# search-url
+base_url = 'https://vimeo.com'
+search_url = base_url + '/search/page:{pageno}?{query}'
+
+# specific xpath variables
+url_xpath = './a/@href'
+content_xpath = './a/img/@src'
+title_xpath = './a/div[@class="data"]/p[@class="title"]/text()'
+results_xpath = '//div[@id="browse_content"]/ol/li'
+publishedDate_xpath = './/p[@class="meta"]//attribute::datetime'
+# do search-request
def request(query, params):
- params['url'] = search_url.format(query=urlencode({'q': query}))
- params['cookies'] = cookie
+ params['url'] = search_url.format(pageno=params['pageno'] ,
+ query=urlencode({'q': query}))
+
+ # TODO required?
+ params['cookies']['__utma'] = '00000000.000#0000000.0000000000.0000000000.0000000000.0'
+
return params
+# get response from search-request
def response(resp):
results = []
+
dom = html.fromstring(resp.text)
p = HTMLParser()
+ # parse results
for result in dom.xpath(results_xpath):
url = base_url + result.xpath(url_xpath)[0]
title = p.unescape(extract_text(result.xpath(title_xpath)))
@@ -45,10 +60,13 @@ def response(resp):
publishedDate = parser.parse(extract_text(
result.xpath(publishedDate_xpath)[0]))
+ # append result
results.append({'url': url,
'title': title,
- 'content': content_tpl.format(url, title, thumbnail),
+ 'content': '',
'template': 'videos.html',
'publishedDate': publishedDate,
'thumbnail': thumbnail})
+
+ # return results
return results
diff --git a/searx/engines/wikipedia.py b/searx/engines/wikipedia.py
deleted file mode 100644
index 1e2a798cc..000000000
--- a/searx/engines/wikipedia.py
+++ /dev/null
@@ -1,30 +0,0 @@
-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/searx/engines/yacy.py b/searx/engines/yacy.py
index efdf846ac..2345b24f3 100644
--- a/searx/engines/yacy.py
+++ b/searx/engines/yacy.py
@@ -1,40 +1,89 @@
+## Yacy (Web, Images, Videos, Music, Files)
+#
+# @website http://yacy.net
+# @provide-api yes (http://www.yacy-websuche.de/wiki/index.php/Dev:APIyacysearch)
+#
+# @using-api yes
+# @results JSON
+# @stable yes
+# @parse (general) url, title, content, publishedDate
+# @parse (images) url, title, img_src
+#
+# @todo parse video, audio and file results
+
from json import loads
from urllib import urlencode
+from dateutil import parser
+
+# engine dependent config
+categories = ['general', 'images'] #TODO , 'music', 'videos', 'files'
+paging = True
+language_support = True
+number_of_results = 5
+
+# search-url
+base_url = 'http://localhost:8090'
+search_url = '/yacysearch.json?{query}&startRecord={offset}&maximumRecords={limit}&contentdom={search_type}&resource=global'
-url = 'http://localhost:8090'
-search_url = '/yacysearch.json?{query}&maximumRecords=10'
+# yacy specific type-definitions
+search_types = {'general': 'text',
+ 'images': 'image',
+ 'files': 'app',
+ 'music': 'audio',
+ 'videos': 'video'}
+# do search-request
def request(query, params):
- params['url'] = url + search_url.format(query=urlencode({'query': query}))
+ offset = (params['pageno'] - 1) * number_of_results
+ search_type = search_types.get(params['category'], '0')
+
+ params['url'] = base_url + search_url.format(query=urlencode({'query': query}),
+ offset=offset,
+ limit=number_of_results,
+ search_type=search_type)
+
+ # add language tag if specified
+ if params['language'] != 'all':
+ params['url'] += '&lr=lang_' + params['language'].split('_')[0]
+
return params
+# get response from search-request
def response(resp):
+ results = []
+
raw_search_results = loads(resp.text)
+ # return empty array if there are no results
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'] + "<br/>"
+ if resp.search_params['category'] == 'general':
+ # parse general results
+ for result in search_results:
+ publishedDate = parser.parse(result['pubDate'])
- if result['pubDate']:
- tmp_result['content'] += result['pubDate'] + "<br/>"
+ # append result
+ results.append({'url': result['link'],
+ 'title': result['title'],
+ 'content': result['description'],
+ 'publishedDate': publishedDate})
- if result['size'] != '-1':
- tmp_result['content'] += result['sizename']
+ elif resp.search_params['category'] == 'images':
+ # parse image results
+ for result in search_results:
+ # append result
+ results.append({'url': result['url'],
+ 'title': result['title'],
+ 'content': '',
+ 'img_src': result['image'],
+ 'template': 'images.html'})
- results.append(tmp_result)
+ #TODO parse video, audio and file results
+ # return results
return results
diff --git a/searx/engines/yahoo.py b/searx/engines/yahoo.py
index f89741839..5e34a2b07 100644
--- a/searx/engines/yahoo.py
+++ b/searx/engines/yahoo.py
@@ -1,64 +1,101 @@
-#!/usr/bin/env python
+## Yahoo (Web)
+#
+# @website https://search.yahoo.com/web
+# @provide-api yes (https://developer.yahoo.com/boss/search/), $0.80/1000 queries
+#
+# @using-api no (because pricing)
+# @results HTML (using search portal)
+# @stable no (HTML can change)
+# @parse url, title, content, suggestion
from urllib import urlencode
from urlparse import unquote
from lxml import html
from searx.engines.xpath import extract_text, extract_url
+# engine dependent config
categories = ['general']
-search_url = 'http://search.yahoo.com/search?{query}&b={offset}'
+paging = True
+language_support = True
+
+# search-url
+search_url = 'https://search.yahoo.com/search?{query}&b={offset}&fl=1&vl=lang_{lang}'
+
+# specific xpath variables
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
-
+# remove yahoo-specific tracking-url
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])
+ if start==0 or len(endpositions) == 0:
+ return url_string
+ else:
+ end = min(endpositions)
+ return unquote(url_string[start:end])
+# do search-request
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}))
+ query=urlencode({'p': query}),
+ lang=language)
+
+ # TODO required?
params['cookies']['sB'] = 'fl=1&vl=lang_{lang}&sh=1&rw=new&v=1'\
.format(lang=language)
+
return params
+# get response from search-request
def response(resp):
results = []
+
dom = html.fromstring(resp.text)
+ # parse results
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})
+ # append result
+ results.append({'url': url,
+ 'title': title,
+ 'content': content})
+
+ # if no suggestion found, return results
if not suggestion_xpath:
return results
+ # parse suggestion
for suggestion in dom.xpath(suggestion_xpath):
+ # append suggestion
results.append({'suggestion': extract_text(suggestion)})
+ # return results
return results
diff --git a/searx/engines/yahoo_news.py b/searx/engines/yahoo_news.py
index 43da93ede..c07d7e185 100644
--- a/searx/engines/yahoo_news.py
+++ b/searx/engines/yahoo_news.py
@@ -1,4 +1,12 @@
-#!/usr/bin/env python
+## Yahoo (News)
+#
+# @website https://news.yahoo.com
+# @provide-api yes (https://developer.yahoo.com/boss/search/), $0.80/1000 queries
+#
+# @using-api no (because pricing)
+# @results HTML (using search portal)
+# @stable no (HTML can change)
+# @parse url, title, content, publishedDate
from urllib import urlencode
from lxml import html
@@ -8,8 +16,15 @@ from datetime import datetime, timedelta
import re
from dateutil import parser
+# engine dependent config
categories = ['news']
-search_url = 'http://news.search.yahoo.com/search?{query}&b={offset}'
+paging = True
+language_support = True
+
+# search-url
+search_url = 'https://news.search.yahoo.com/search?{query}&b={offset}&fl=1&vl=lang_{lang}'
+
+# specific xpath variables
results_xpath = '//div[@class="res"]'
url_xpath = './/h3/a/@href'
title_xpath = './/h3/a'
@@ -17,30 +32,39 @@ content_xpath = './/div[@class="abstr"]'
publishedDate_xpath = './/span[@class="timestamp"]'
suggestion_xpath = '//div[@id="satat"]//a'
-paging = True
-
+# do search-request
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}))
+ query=urlencode({'p': query}),
+ lang=language)
+
+ # TODO required?
params['cookies']['sB'] = 'fl=1&vl=lang_{lang}&sh=1&rw=new&v=1'\
.format(lang=language)
return params
+# get response from search-request
def response(resp):
results = []
+
dom = html.fromstring(resp.text)
+ # parse results
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])
+
+ # parse publishedDate
publishedDate = extract_text(result.xpath(publishedDate_xpath)[0])
if re.match("^[0-9]+ minute(s|) ago$", publishedDate):
@@ -58,15 +82,11 @@ def response(resp):
if publishedDate.year == 1900:
publishedDate = publishedDate.replace(year=datetime.now().year)
+ # append result
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
return results
diff --git a/searx/engines/youtube.py b/searx/engines/youtube.py
index 895b55918..a3c3980af 100644
--- a/searx/engines/youtube.py
+++ b/searx/engines/youtube.py
@@ -1,42 +1,69 @@
+## Youtube (Videos)
+#
+# @website https://www.youtube.com/
+# @provide-api yes (http://gdata-samples-youtube-search-py.appspot.com/)
+#
+# @using-api yes
+# @results JSON
+# @stable yes
+# @parse url, title, content, publishedDate, thumbnail
+
from json import loads
from urllib import urlencode
from dateutil import parser
+# engine dependent config
categories = ['videos']
-
-search_url = ('https://gdata.youtube.com/feeds/api/videos'
- '?alt=json&{query}&start-index={index}&max-results=25') # noqa
-
paging = True
+language_support = True
+
+# search-url
+base_url = 'https://gdata.youtube.com/feeds/api/videos'
+search_url = base_url + '?alt=json&{query}&start-index={index}&max-results=5' # noqa
+# do search-request
def request(query, params):
- index = (params['pageno'] - 1) * 25 + 1
+ index = (params['pageno'] - 1) * 5 + 1
+
params['url'] = search_url.format(query=urlencode({'q': query}),
index=index)
+
+ # add language tag if specified
+ if params['language'] != 'all':
+ params['url'] += '&lr=' + params['language'].split('_')[0]
+
return params
+# get response from search-request
def response(resp):
results = []
+
search_results = loads(resp.text)
+
+ # return empty array if there are no results
if not 'feed' in search_results:
- return results
+ return []
+
feed = search_results['feed']
+ # parse results
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)
@@ -49,6 +76,7 @@ def response(resp):
else:
content = result['content']['$t']
+ # append result
results.append({'url': url,
'title': title,
'content': content,
@@ -56,4 +84,5 @@ def response(resp):
'publishedDate': publishedDate,
'thumbnail': thumbnail})
+ # return results
return results
diff --git a/searx/https_rewrite.py b/searx/https_rewrite.py
new file mode 100644
index 000000000..44ada9450
--- /dev/null
+++ b/searx/https_rewrite.py
@@ -0,0 +1,14 @@
+import re
+
+# https://gitweb.torproject.org/\
+# pde/https-everywhere.git/tree/4.0:/src/chrome/content/rules
+
+# HTTPS rewrite rules
+https_rules = (
+ # from
+ (re.compile(r'^http://(www\.|m\.|)?xkcd\.(?:com|org)/', re.I | re.U),
+ # to
+ r'https://\1xkcd.com/'),
+ (re.compile(r'^https?://(?:ssl)?imgs\.xkcd\.com/', re.I | re.U),
+ r'https://sslimgs.xkcd.com/'),
+)
diff --git a/searx/languages.py b/searx/languages.py
index 8b12e5ffe..df5fabf74 100644
--- a/searx/languages.py
+++ b/searx/languages.py
@@ -1,3 +1,21 @@
+'''
+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, <asciimoo@gmail.com>
+'''
+
+# list of language codes
language_codes = (
("ar_XA", "Arabic", "Arabia"),
("bg_BG", "Bulgarian", "Bulgaria"),
diff --git a/searx/query.py b/searx/query.py
new file mode 100644
index 000000000..612d46f4b
--- /dev/null
+++ b/searx/query.py
@@ -0,0 +1,127 @@
+#!/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) 2014 by Thomas Pointhuber, <thomas.pointhuber@gmx.at>
+'''
+
+from searx.languages import language_codes
+from searx.engines import (
+ categories, engines, engine_shortcuts
+)
+import string
+import re
+
+
+class Query(object):
+ """parse query"""
+
+ def __init__(self, query, blocked_engines):
+ self.query = query
+ self.blocked_engines = []
+
+ if blocked_engines:
+ self.blocked_engines = blocked_engines
+
+ self.query_parts = []
+ self.engines = []
+ self.languages = []
+
+ # parse query, if tags are set, which change the serch engine or search-language
+ def parse_query(self):
+ self.query_parts = []
+
+ # split query, including whitespaces
+ raw_query_parts = re.split(r'(\s+)', self.query)
+
+ parse_next = True
+
+ for query_part in raw_query_parts:
+ if not parse_next:
+ self.query_parts[-1] += query_part
+ continue
+
+ parse_next = False
+
+ # part does only contain spaces, skip
+ if query_part.isspace()\
+ or query_part == '':
+ parse_next = True
+ self.query_parts.append(query_part)
+ continue
+
+ # this force a language
+ if query_part[0] == ':':
+ lang = query_part[1:].lower()
+
+ # check if any language-code is equal with declared language-codes
+ for lc in language_codes:
+ lang_id, lang_name, country = map(str.lower, lc)
+
+ # if correct language-code is found, set it as new search-language
+ if lang == lang_id\
+ or lang_id.startswith(lang)\
+ or lang == lang_name\
+ or lang == country:
+ parse_next = True
+ self.languages.append(lang)
+ break
+
+ # this force a engine or category
+ if query_part[0] == '!':
+ prefix = query_part[1:].replace('_', ' ')
+
+ # check if prefix is equal with engine shortcut
+ if prefix in engine_shortcuts\
+ and not engine_shortcuts[prefix] in self.blocked_engines:
+ parse_next = True
+ self.engines.append({'category': 'none',
+ 'name': engine_shortcuts[prefix]})
+
+ # check if prefix is equal with engine name
+ elif prefix in engines\
+ and not prefix in self.blocked_engines:
+ parse_next = True
+ self.engines.append({'category': 'none',
+ 'name': prefix})
+
+ # check if prefix is equal with categorie name
+ elif prefix in categories:
+ # using all engines for that search, which are declared under that categorie name
+ parse_next = True
+ self.engines.extend({'category': prefix,
+ 'name': engine.name}
+ for engine in categories[prefix]
+ if not engine in self.blocked_engines)
+
+ # append query part to query_part list
+ self.query_parts.append(query_part)
+
+ def changeSearchQuery(self, search_query):
+ if len(self.query_parts):
+ self.query_parts[-1] = search_query
+ else:
+ self.query_parts.append(search_query)
+
+ def getSearchQuery(self):
+ if len(self.query_parts):
+ return self.query_parts[-1]
+ else:
+ return ''
+
+ def getFullQuery(self):
+ # get full querry including whitespaces
+ return string.join(self.query_parts, '')
+
diff --git a/searx/search.py b/searx/search.py
index 7f991045b..17556dc4e 100644
--- a/searx/search.py
+++ b/searx/search.py
@@ -1,7 +1,189 @@
+'''
+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, <asciimoo@gmail.com>
+'''
+
+import grequests
+from itertools import izip_longest, chain
+from datetime import datetime
+from operator import itemgetter
+from urlparse import urlparse, unquote
from searx.engines import (
categories, engines, engine_shortcuts
)
from searx.languages import language_codes
+from searx.utils import gen_useragent
+from searx.query import Query
+
+
+number_of_searches = 0
+
+
+# get default reqest parameter
+def default_request_params():
+ return {
+ 'method': 'GET', 'headers': {}, 'data': {}, 'url': '', 'cookies': {}}
+
+
+# create a callback wrapper for the search engine results
+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
+
+ # update stats with current page-load-time
+ engines[engine_name].stats['page_load_time'] += \
+ (datetime.now() - params['started']).total_seconds()
+
+ try:
+ search_results = callback(response)
+ except Exception, e:
+ # increase errors stats
+ engines[engine_name].stats['errors'] += 1
+ results[engine_name] = cb_res
+
+ # print engine name and specific error message
+ 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 it is a suggestion, add it to list of suggestions
+ if 'suggestion' in result:
+ # TODO type checks
+ suggestions.add(result['suggestion'])
+ continue
+
+ # append result
+ cb_res.append(result)
+
+ results[engine_name] = cb_res
+
+ return process_callback
+
+
+# score results and remove duplications
+def score_results(results):
+ # calculate scoring parameters
+ flat_res = filter(
+ None, chain.from_iterable(izip_longest(*results.values())))
+ flat_len = len(flat_res)
+ engines_len = len(results)
+
+ results = []
+
+ # pass 1: deduplication + scoring
+ for i, res in enumerate(flat_res):
+
+ res['parsed_url'] = urlparse(res['url'])
+
+ res['host'] = res['parsed_url'].netloc
+
+ if res['host'].startswith('www.'):
+ res['host'] = res['host'].replace('www.', '', 1)
+
+ res['engines'] = [res['engine']]
+ weight = 1.0
+
+ # get weight of this engine if possible
+ if hasattr(engines[res['engine']], 'weight'):
+ weight = float(engines[res['engine']].weight)
+
+ # calculate score for that engine
+ score = int((flat_len - i) / engines_len) * weight + 1
+
+ duplicated = False
+
+ # check for duplicates
+ for new_res in results:
+ # remove / from the end of the url if required
+ 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
+
+ # check if that result is a duplicate
+ if res['host'] == new_res['host'] and\
+ unquote(p1) == unquote(p2) and\
+ res['parsed_url'].query == new_res['parsed_url'].query and\
+ res.get('template') == new_res.get('template'):
+ duplicated = new_res
+ break
+
+ # merge duplicates together
+ if duplicated:
+ # using content with more text
+ if res.get('content') > duplicated.get('content'):
+ duplicated['content'] = res['content']
+
+ # increase result-score
+ duplicated['score'] += score
+
+ # add engine to list of result-engines
+ duplicated['engines'].append(res['engine'])
+
+ # using https if possible
+ 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']
+
+ # if there is no duplicate found, append result
+ else:
+ res['score'] = score
+ results.append(res)
+
+ results = sorted(results, key=itemgetter('score'), reverse=True)
+
+ # pass 2 : group results by category and template
+ gresults = []
+ categoryPositions = {}
+
+ for i, res in enumerate(results):
+ # FIXME : handle more than one category per engine
+ category = engines[res['engine']].categories[0] + ':' + '' if 'template' not in res else res['template']
+
+ current = None if category not in categoryPositions else categoryPositions[category]
+
+ # group with previous results using the same category if the group can accept more result and is not too far from the current position
+ if current != None and (current['count'] > 0) and (len(gresults) - current['index'] < 20):
+ # group with the previous results using the same category with this one
+ index = current['index']
+ gresults.insert(index, res)
+
+ # update every index after the current one (including the current one)
+ for k in categoryPositions:
+ v = categoryPositions[k]['index']
+ if v >= index:
+ categoryPositions[k]['index'] = v+1
+
+ # update this category
+ current['count'] -= 1
+
+ else:
+ # same category
+ gresults.append(res)
+
+ # update categoryIndex
+ categoryPositions[category] = { 'index' : len(gresults), 'count' : 8 }
+
+ # return gresults
+ return gresults
class Search(object):
@@ -9,6 +191,7 @@ class Search(object):
"""Search information container"""
def __init__(self, request):
+ # init vars
super(Search, self).__init__()
self.query = None
self.engines = []
@@ -16,18 +199,23 @@ class Search(object):
self.paging = False
self.pageno = 1
self.lang = 'all'
+
+ # set blocked engines
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 = {}
+ # set specific language if set
if request.cookies.get('language')\
and request.cookies['language'] in (x[0] for x in language_codes):
self.lang = request.cookies['language']
+ # set request method
if request.method == 'POST':
self.request_data = request.form
else:
@@ -37,78 +225,160 @@ class Search(object):
if not self.request_data.get('q'):
raise Exception('noquery')
+ # set query
self.query = self.request_data['q']
+ # set pagenumber
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()
+ # parse query, if tags are set, which change the serch engine or search-language
+ query_obj = Query(self.query, self.blocked_engines)
+ query_obj.parse_query()
+
+ # get last selected language in query, if possible
+ # TODO support search with multible languages
+ if len(query_obj.languages):
+ self.lang = query_obj.languages[-1]
+
+ self.engines = query_obj.engines
self.categories = []
+ # if engines are calculated from query, set categories by using that informations
if self.engines:
self.categories = list(set(engine['category']
for engine in self.engines))
+
+ # otherwise, using defined categories to calculate which engines should be used
else:
+ # set used categories
for pd_name, pd in self.request_data.items():
if pd_name.startswith('category_'):
category = pd_name[9:]
+ # if category is not found in list, skip
if not category in categories:
continue
+
+ # add category to list
self.categories.append(category)
+
+ # if no category is specified for this search, using user-defined default-configuration which (is stored in cookie)
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 still no category is specified, using general as default-category
if not self.categories:
self.categories = ['general']
+ # using all engines for that search, which are declared under the specific categories
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()
+ # do search-request
+ def search(self, request):
+ global number_of_searches
+
+ # init vars
+ requests = []
+ results = {}
+ suggestions = set()
+
+ # increase number of searches
+ number_of_searches += 1
+
+ # set default useragent
+ #user_agent = request.headers.get('User-Agent', '')
+ user_agent = gen_useragent()
+
+ # start search-reqest for all selected engines
+ for selected_engine in self.engines:
+ if selected_engine['name'] not in engines:
+ continue
+
+ engine = engines[selected_engine['name']]
+
+ # if paging is not supported, skip
+ if self.pageno > 1 and not engine.paging:
+ continue
+
+ # if search-language is set and engine does not provide language-support, skip
+ if self.lang != 'all' and not engine.language_support:
+ continue
+
+ # set default request parameters
+ 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'] = self.pageno
+ request_params['language'] = self.lang
+
+ # update request parameters dependent on search-engine (contained in engines folder)
+ request_params = engine.request(self.query.encode('utf-8'),
+ request_params)
+
+ if request_params['url'] is None:
+ # TODO add support of offline engines
+ pass
+
+ # create a callback wrapper for the search engine results
+ callback = make_callback(
+ selected_engine['name'],
+ results,
+ suggestions,
+ engine.response,
+ request_params
+ )
+
+ # create dictionary which contain all informations about the request
+ request_args = dict(
+ headers=request_params['headers'],
+ hooks=dict(response=callback),
+ cookies=request_params['cookies'],
+ timeout=engine.timeout
+ )
+
+ # specific type of request (GET or POST)
+ 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
+
+ # append request to list
+ requests.append(req(request_params['url'], **request_args))
+
+ # send all search-request
+ grequests.map(requests)
+
+ # update engine-specific stats
+ for engine_name, engine_results in results.items():
+ engines[engine_name].stats['search_count'] += 1
+ engines[engine_name].stats['result_count'] += len(engine_results)
+
+ # score results and remove duplications
+ results = score_results(results)
+
+ # update engine stats, using calculated score
+ for result in results:
+ for res_engine in result['engines']:
+ engines[result['engine']]\
+ .stats['score_count'] += result['score']
+
+ # return results and suggestions
+ return results, suggestions
diff --git a/searx/settings.yml b/searx/settings.yml
index eac7593c6..da053ce6a 100644
--- a/searx/settings.yml
+++ b/searx/settings.yml
@@ -1,22 +1,30 @@
server:
port : 8888
secret_key : "ultrasecretkey" # change this!
- debug : True
+ debug : False # Debug mode, only for development
request_timeout : 2.0 # seconds
- base_url : False
+ base_url : False # Set custom base_url. Possible values: False or "https://your.custom.host/location/"
+ themes_path : "" # Custom ui themes path
+ default_theme : default # ui theme
+ https_rewrite : True # Force rewrite result urls. See searx/https_rewrite.py
engines:
- name : wikipedia
- engine : wikipedia
- number_of_results : 1
- paging : False
+ engine : mediawiki
shortcut : wp
+ base_url : 'https://{language}.wikipedia.org/'
+ number_of_results : 1
- name : bing
engine : bing
locale : en-US
shortcut : bi
+ - name : bing images
+ engine : bing_images
+ locale : en-US
+ shortcut : bii
+
- name : bing news
engine : bing_news
locale : en-US
@@ -29,7 +37,6 @@ engines:
- name : deviantart
engine : deviantart
- categories : images
shortcut : da
timeout: 3.0
@@ -39,13 +46,13 @@ engines:
- name : duckduckgo
engine : duckduckgo
- locale : en-us
shortcut : ddg
- - name : filecrop
- engine : filecrop
- categories : files
- shortcut : fc
+# down - website is under criminal investigation by the UK
+# - name : filecrop
+# engine : filecrop
+# categories : files
+# shortcut : fc
- name : flickr
engine : flickr
@@ -53,9 +60,12 @@ engines:
shortcut : fl
timeout: 3.0
+ - name : general-file
+ engine : generalfile
+ shortcut : gf
+
- name : github
engine : github
- categories : it
shortcut : gh
- name : google
@@ -70,25 +80,24 @@ engines:
engine : google_news
shortcut : gon
+ - name : openstreetmap
+ engine : openstreetmap
+ shortcut : osm
+
- 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
@@ -99,15 +108,14 @@ engines:
- 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/
+# base_url : https://uncyclopedia.wikia.com/
+# number_of_results : 5
# tmp suspended - too slow, too many errors
# - name : urbandictionary
@@ -128,24 +136,24 @@ engines:
- 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
+ locale : en-US
shortcut : vm
+# - name : yacy
+# engine : yacy
+# shortcut : ya
+# base_url : 'http://localhost:8090'
+# number_of_results : 5
+# timeout: 3.0
+
locales:
en : English
de : Deutsch
diff --git a/searx/static/courgette/css/style.css b/searx/static/courgette/css/style.css
new file mode 100644
index 000000000..6c5c99053
--- /dev/null
+++ b/searx/static/courgette/css/style.css
@@ -0,0 +1,464 @@
+* {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+input[type="search"] {
+ -webkit-appearance: textfield;
+}
+
+h2 {
+ color: #666;
+ text-transform: uppercase;
+}
+
+body {
+ font-family: sans-serif;
+ line-height: 1.5;
+ margin: 0;
+ background: #EEE;
+}
+
+html {
+ position: relative;
+ min-height: 100%;
+}
+
+.center:after {
+ content: "";
+ z-index: -1;
+ background: url(../img/bg-body-index.jpg) no-repeat;
+ background-size: cover;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ position: fixed;
+}
+ .center.search:after {
+ content: none;
+ }
+
+.title h1 {
+ background: url(../img/searx.png) no-repeat;
+ width: 319px;
+ height: 62px;
+ text-indent: -9999px;
+ margin: 0.5em auto 1em;
+}
+
+.center {
+ width: 55em;
+ text-align: center;
+ background: rgba(255,255,255,0.6);
+ padding: 4em 2em;
+ margin: 7% auto 0;
+ position: relative;
+ /*position: absolute;
+ top: 50%;
+ left: 50%;
+ margin:-220px 0 0 -408px;*/
+}
+
+.center.search {
+ position: static;
+ width: auto;
+ background: none;
+ margin: auto;
+ padding-top: 1.8em;
+}
+
+.autocompleter-choices {
+ position: absolute;
+ margin: 0;
+ padding: 0;
+ background: #FFF;
+}
+ .autocompleter-choices li {
+ padding: 0.5em 1em;
+ }
+ .autocompleter-choices li:hover {
+ background: #3498DB;
+ color: #FFF;
+ cursor: pointer;
+ }
+
+#categories {
+ text-align: center;
+}
+
+.top_margin {
+ position: absolute;
+ bottom: -3.5em;
+ width: 100%;
+ left: 0;
+}
+
+ .top_margin a {
+ display: inline-block;
+ margin-right: 1em;
+ color: #FFF;
+ text-decoration: none;
+ }
+ .top_margin a:hover,
+ .top_margin a:focus {
+ text-decoration: underline;
+ }
+
+.checkbox_container { margin-top: 1.5em; }
+ .checkbox_container label {
+ padding: 0.5em 1em;
+ color: #333;
+ cursor: pointer;
+ }
+ .checkbox_container label:hover {
+ background: #3498DB;
+ color: #FFF;
+ }
+
+ .checkbox_container input[type="checkbox"] {
+ position: absolute;
+ top: -9999px;
+ }
+
+ .checkbox_container input[type="checkbox"]:checked + label {
+ background: #3498DB;
+ color: #FFF;
+ }
+
+#categories > div {
+ display: inline-block;
+}
+
+#search_wrapper {
+ position: relative;
+}
+
+.q {
+ padding: 0.5em 3em 0.5em 1em;
+ width: 100%;
+ font-size: 1.5em;
+ border: 0;
+ color: #666;
+}
+
+#search_submit {
+ position: absolute;
+ top: 0;
+ right: 0;
+ border: 0;
+ background:url("../img/search-icon.png") no-repeat scroll center center / 65% auto #3498db;
+ text-indent: -9999px;
+ width: 5em;
+ height: 100%;
+ cursor: pointer;
+}
+
+#search_submit:hover,
+#search_submit:focus {
+ background-color: #0665A2;
+}
+
+#sidebar {
+ background: #3498db;
+ position: fixed;
+ top: 0;
+ right: 0;
+ width: 15em;
+ height: 100%;
+ padding: 1.5em;
+ text-align: right;
+}
+
+.right {
+ position: fixed;
+ bottom: 1.5em;
+ width: 15em;
+ right: 0;
+ z-index: 1;
+ padding: 0 1.5em;
+ text-align: right;
+}
+ .right a {
+ color: #FFF;
+ display: block;
+ text-decoration: none;
+ }
+ .right a:hover,
+ .right a:focus {
+ text-decoration: underline;
+ }
+
+#preferences {
+ background: url(../img/preference-icon.png) no-repeat right 0 / 12% auto;
+ padding-right: 1.8em;
+}
+
+#preferences:hover,
+#preferences:focus {
+
+}
+
+#search_url input {
+ border: 0;
+ padding: 0.5em;
+}
+
+ #sidebar > div {
+ margin-bottom: 1em;
+ color: #FFF;
+ }
+
+ #sidebar form {
+ display: inline-block;
+ }
+
+ #sidebar input[type="submit"] {
+ background: #CCC;
+ border: 0;
+ padding: 0.5em 1em;
+ cursor: pointer;
+ margin-top: 0.5em;
+ }
+
+ #sidebar input[type="submit"]:hover,
+ #sidebar input[type="submit"]:focus {
+ color: #FFF;
+ background-color: #0665A2;
+ }
+
+#results {
+ padding-right: 17em;
+ padding-left: 2em;
+ padding: 0 17em 0 2em;
+}
+
+.result p {
+ font-size: 0.9em;
+}
+
+.result .content {
+ margin: 0;
+ color: #666;
+}
+
+.result .url {
+ margin-top: 0;
+ color: #FF6530;
+}
+
+.result .favicon {
+ float: left;
+ position: relative;
+ top: 0.5em;
+ margin-right: 0.5em;
+}
+
+.definition_result {
+ background: #CCC;
+ padding: 1em;
+}
+
+.definition_result .result_title,
+.definition_result p {
+ margin: 0;
+}
+
+.result_title {
+ margin-bottom: 0;
+ font-weight: normal;
+}
+
+.highlight {
+ font-weight: bold;
+}
+
+.result_title a {
+ color: #3498db;
+ text-decoration: none;
+}
+
+ .result_title a:hover,
+ .result_title a:focus {
+ text-decoration: underline;
+ }
+
+.search.center {
+ padding-right: 17em;
+}
+
+#suggestions { margin-bottom: 1em; }
+
+#suggestions span { color: #666; }
+
+#suggestions form {
+ display: inline-block;
+ vertical-align: top;
+ margin-bottom: 0.5em;
+}
+
+#suggestions input[type="submit"] {
+ color: #333;
+ padding: 0.5em 1em;
+ border: 0;
+ background: #CCC;
+ cursor:pointer;
+}
+ #suggestions input[type="submit"]:hover,
+ #suggestions input[type="submit"]:focus {
+ background: #3498db;
+ color: #FFF;
+ }
+
+#pagination {
+ margin: 1.5em 0 2em;
+}
+
+#pagination form + form {
+ float: right;
+ margin-top: -2em;
+}
+
+input[type="submit"] {
+ display: inline-block;
+ background: #3498db;
+ color: #FFF;
+ border: 0;
+ padding: 0.6em 1em;
+ cursor: pointer;
+}
+
+input[type="submit"]:hover,
+input[type="submit"]:focus {
+ background: #0665A2;
+}
+
+.row {
+ max-width: 60em;
+ margin: auto;
+}
+
+.row form {
+ letter-spacing: -5px;
+}
+
+ .row form > * { letter-spacing: normal; }
+
+ .row p { margin: 0; }
+
+.row fieldset {
+ display: inline-block;
+ width: 48%;
+ vertical-align: top;
+}
+
+.row fieldset:last-of-type {
+ display: block;
+ width: auto;
+ background: none;
+ padding: 0;
+}
+
+.row fieldset:nth-child(odd) {
+ margin-right: 2%;
+}
+
+.row fieldset:nth-child(2) {
+ min-height: 10.5em;
+}
+
+fieldset {
+ border: 0;
+ margin: 1em 0;
+ background: #CCC;
+ padding: 1.5em;
+}
+
+table {
+ width: 100%;
+ text-align: left;
+ border: 1px solid #CCC;
+ border-collapse: collapse;
+}
+
+table th {
+ background: #999;
+ color: #FFF;
+}
+
+table tr:nth-child(odd) {
+ background: #CCC;
+}
+
+table th,
+table td {
+ padding: 0.5em 1em;
+ border: 1px solid #FFF;
+}
+
+.engine_checkbox label {
+ padding: 0.5em;
+ background: #3498db;
+ color: #FFF;
+ cursor: pointer;
+}
+
+.engine_checkbox .deny {
+ background: #3498db;
+}
+
+.engine_checkbox .allow {
+ display: none;
+ background: #666;
+}
+
+.engine_checkbox input {
+ display: none;
+}
+
+.engine_checkbox input:checked + .allow {
+ display: inline;
+}
+
+.engine_checkbox input:checked + .allow + .deny{
+ display: none;
+}
+
+.row input[type="submit"] {
+ font-size: 1em;
+ margin: 1em 0 2em;
+}
+
+.row .right {
+ position: static;
+ display: inline-block;
+
+}
+
+.row .right a {
+ color: #333;
+ width: auto;
+ text-align: left;
+ padding: 0;
+}
+
+.small_font {
+ font-size: 0.8em;
+}
+
+table th {
+ padding: 1em;
+}
+
+legend {
+ background: #EEE;
+ padding: 0 1em;
+ position: relative;
+}
+
+select {
+ border: 1px solid #DDD;
+ padding: 0.5em 0.8em;
+ font-size: 1em;
+} \ No newline at end of file
diff --git a/searx/static/courgette/img/bg-body-index.jpg b/searx/static/courgette/img/bg-body-index.jpg
new file mode 100644
index 000000000..ff28f5fc1
--- /dev/null
+++ b/searx/static/courgette/img/bg-body-index.jpg
Binary files differ
diff --git a/searx/static/img/favicon.png b/searx/static/courgette/img/favicon.png
index cefbac496..cefbac496 100644
--- a/searx/static/img/favicon.png
+++ b/searx/static/courgette/img/favicon.png
Binary files differ
diff --git a/searx/static/img/github_ribbon.png b/searx/static/courgette/img/github_ribbon.png
index 146ef8a80..146ef8a80 100644
--- a/searx/static/img/github_ribbon.png
+++ b/searx/static/courgette/img/github_ribbon.png
Binary files differ
diff --git a/searx/static/img/icon_github.ico b/searx/static/courgette/img/icon_github.ico
index 133f0ca35..133f0ca35 100644
--- a/searx/static/img/icon_github.ico
+++ b/searx/static/courgette/img/icon_github.ico
Binary files differ
diff --git a/searx/static/img/icon_soundcloud.ico b/searx/static/courgette/img/icon_soundcloud.ico
index 4130bea1b..4130bea1b 100644
--- a/searx/static/img/icon_soundcloud.ico
+++ b/searx/static/courgette/img/icon_soundcloud.ico
Binary files differ
diff --git a/searx/static/img/icon_stackoverflow.ico b/searx/static/courgette/img/icon_stackoverflow.ico
index b2242bc6c..b2242bc6c 100644
--- a/searx/static/img/icon_stackoverflow.ico
+++ b/searx/static/courgette/img/icon_stackoverflow.ico
Binary files differ
diff --git a/searx/static/img/icon_twitter.ico b/searx/static/courgette/img/icon_twitter.ico
index b4a71699a..b4a71699a 100644
--- a/searx/static/img/icon_twitter.ico
+++ b/searx/static/courgette/img/icon_twitter.ico
Binary files differ
diff --git a/searx/static/img/icon_vimeo.ico b/searx/static/courgette/img/icon_vimeo.ico
index 4fe4336da..4fe4336da 100644
--- a/searx/static/img/icon_vimeo.ico
+++ b/searx/static/courgette/img/icon_vimeo.ico
Binary files differ
diff --git a/searx/static/img/icon_wikipedia.ico b/searx/static/courgette/img/icon_wikipedia.ico
index 911fa76f6..911fa76f6 100644
--- a/searx/static/img/icon_wikipedia.ico
+++ b/searx/static/courgette/img/icon_wikipedia.ico
Binary files differ
diff --git a/searx/static/img/icon_youtube.ico b/searx/static/courgette/img/icon_youtube.ico
index 977887dbb..977887dbb 100644
--- a/searx/static/img/icon_youtube.ico
+++ b/searx/static/courgette/img/icon_youtube.ico
Binary files differ
diff --git a/searx/static/courgette/img/preference-icon.png b/searx/static/courgette/img/preference-icon.png
new file mode 100644
index 000000000..039db04a8
--- /dev/null
+++ b/searx/static/courgette/img/preference-icon.png
Binary files differ
diff --git a/searx/static/courgette/img/search-icon.png b/searx/static/courgette/img/search-icon.png
new file mode 100644
index 000000000..52c267842
--- /dev/null
+++ b/searx/static/courgette/img/search-icon.png
Binary files differ
diff --git a/searx/static/img/searx.png b/searx/static/courgette/img/searx.png
index e162da502..e162da502 100644
--- a/searx/static/img/searx.png
+++ b/searx/static/courgette/img/searx.png
Binary files differ
diff --git a/searx/static/img/searx_logo.svg b/searx/static/courgette/img/searx_logo.svg
index 67a2d4588..67a2d4588 100644
--- a/searx/static/img/searx_logo.svg
+++ b/searx/static/courgette/img/searx_logo.svg
diff --git a/searx/static/js/mootools-autocompleter-1.1.2-min.js b/searx/static/courgette/js/mootools-autocompleter-1.1.2-min.js
index 364e611cc..364e611cc 100644
--- a/searx/static/js/mootools-autocompleter-1.1.2-min.js
+++ b/searx/static/courgette/js/mootools-autocompleter-1.1.2-min.js
diff --git a/searx/static/js/mootools-core-1.4.5-min.js b/searx/static/courgette/js/mootools-core-1.4.5-min.js
index 569473d1c..569473d1c 100644
--- a/searx/static/js/mootools-core-1.4.5-min.js
+++ b/searx/static/courgette/js/mootools-core-1.4.5-min.js
diff --git a/searx/static/js/searx.js b/searx/static/courgette/js/searx.js
index 47dc722da..47dc722da 100644
--- a/searx/static/js/searx.js
+++ b/searx/static/courgette/js/searx.js
diff --git a/searx/static/css/style.css b/searx/static/default/css/style.css
index 69190b21d..9a6faadef 100644
--- a/searx/static/css/style.css
+++ b/searx/static/default/css/style.css
@@ -38,9 +38,10 @@ 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}
+.cache_link{font-size:10px !important}
.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 .url{font-size:.8em;margin:0 0 3px 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}
@@ -70,5 +71,5 @@ tr:hover{background:#ddd}
#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}
+@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%} .github{display:none} .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/searx/static/default/img/favicon.png b/searx/static/default/img/favicon.png
new file mode 100644
index 000000000..cefbac496
--- /dev/null
+++ b/searx/static/default/img/favicon.png
Binary files differ
diff --git a/searx/static/default/img/github_ribbon.png b/searx/static/default/img/github_ribbon.png
new file mode 100644
index 000000000..146ef8a80
--- /dev/null
+++ b/searx/static/default/img/github_ribbon.png
Binary files differ
diff --git a/searx/static/default/img/icon_github.ico b/searx/static/default/img/icon_github.ico
new file mode 100644
index 000000000..133f0ca35
--- /dev/null
+++ b/searx/static/default/img/icon_github.ico
Binary files differ
diff --git a/searx/static/default/img/icon_soundcloud.ico b/searx/static/default/img/icon_soundcloud.ico
new file mode 100644
index 000000000..4130bea1b
--- /dev/null
+++ b/searx/static/default/img/icon_soundcloud.ico
Binary files differ
diff --git a/searx/static/default/img/icon_stackoverflow.ico b/searx/static/default/img/icon_stackoverflow.ico
new file mode 100644
index 000000000..b2242bc6c
--- /dev/null
+++ b/searx/static/default/img/icon_stackoverflow.ico
Binary files differ
diff --git a/searx/static/default/img/icon_twitter.ico b/searx/static/default/img/icon_twitter.ico
new file mode 100644
index 000000000..b4a71699a
--- /dev/null
+++ b/searx/static/default/img/icon_twitter.ico
Binary files differ
diff --git a/searx/static/default/img/icon_vimeo.ico b/searx/static/default/img/icon_vimeo.ico
new file mode 100644
index 000000000..4fe4336da
--- /dev/null
+++ b/searx/static/default/img/icon_vimeo.ico
Binary files differ
diff --git a/searx/static/default/img/icon_wikipedia.ico b/searx/static/default/img/icon_wikipedia.ico
new file mode 100644
index 000000000..911fa76f6
--- /dev/null
+++ b/searx/static/default/img/icon_wikipedia.ico
Binary files differ
diff --git a/searx/static/default/img/icon_youtube.ico b/searx/static/default/img/icon_youtube.ico
new file mode 100644
index 000000000..977887dbb
--- /dev/null
+++ b/searx/static/default/img/icon_youtube.ico
Binary files differ
diff --git a/searx/static/img/preference-icon.png b/searx/static/default/img/preference-icon.png
index f74635788..f74635788 100644
--- a/searx/static/img/preference-icon.png
+++ b/searx/static/default/img/preference-icon.png
Binary files differ
diff --git a/searx/static/img/search-icon.png b/searx/static/default/img/search-icon.png
index 1222421b2..1222421b2 100644
--- a/searx/static/img/search-icon.png
+++ b/searx/static/default/img/search-icon.png
Binary files differ
diff --git a/searx/static/default/img/searx.png b/searx/static/default/img/searx.png
new file mode 100644
index 000000000..e162da502
--- /dev/null
+++ b/searx/static/default/img/searx.png
Binary files differ
diff --git a/searx/static/default/img/searx_logo.svg b/searx/static/default/img/searx_logo.svg
new file mode 100644
index 000000000..67a2d4588
--- /dev/null
+++ b/searx/static/default/img/searx_logo.svg
@@ -0,0 +1,203 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="744.09448819"
+ height="1052.3622047"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.4 r9939"
+ sodipodi:docname="searx_logo.svg"
+ inkscape:export-filename="/home/a/magnif.png"
+ inkscape:export-xdpi="203.1774"
+ inkscape:export-ydpi="203.1774">
+ <defs
+ id="defs4">
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient3857">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1;"
+ offset="0"
+ id="stop3859" />
+ <stop
+ style="stop-color:#ffffff;stop-opacity:0;"
+ offset="1"
+ id="stop3861" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3790">
+ <stop
+ style="stop-color:#a9a9a9;stop-opacity:1;"
+ offset="0"
+ id="stop3792" />
+ <stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="1"
+ id="stop3794" />
+ </linearGradient>
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3790"
+ id="radialGradient3798"
+ cx="294.45947"
+ cy="208.37973"
+ fx="294.45947"
+ fy="208.37973"
+ r="107.58125"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3857"
+ id="linearGradient3865"
+ x1="120.68947"
+ y1="239.61774"
+ x2="120.68947"
+ y2="602.17517"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3790"
+ id="linearGradient3912"
+ x1="186.74416"
+ y1="354.42426"
+ x2="255.84358"
+ y2="254.35953"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.2227304,0,0,0.89945099,-289.31433,113.40259)" />
+ <filter
+ inkscape:collect="always"
+ id="filter4024"
+ x="-0.12996517"
+ width="1.2599303"
+ y="-0.14709377"
+ height="1.2941875">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="6.4759344"
+ id="feGaussianBlur4026" />
+ </filter>
+ <filter
+ inkscape:collect="always"
+ id="filter3983"
+ x="-1.0608404"
+ width="3.1216809"
+ y="-0.31017202"
+ height="1.620344">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="9.392858"
+ id="feGaussianBlur3985" />
+ </filter>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1.979899"
+ inkscape:cx="30.708726"
+ inkscape:cy="948.08556"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:window-width="1364"
+ inkscape:window-height="663"
+ inkscape:window-x="0"
+ inkscape:window-y="30"
+ inkscape:window-maximized="0"
+ showguides="true"
+ inkscape:guide-bbox="true" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <path
+ style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 70.523181,34.870671 c -7.11959,15.242893 -10.17798,31.779192 -8.22563,48.814566 5.01677,43.774133 41.675309,79.324503 91.536109,95.162893 -6.62576,-22.40752 -5.34093,-44.9362 2.6395,-65.84431 C 108.73618,98.821131 74.828141,70.195435 70.523181,34.870671 z"
+ id="path3814-0-7"
+ inkscape:connector-curvature="0" />
+ <path
+ style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 303.77876,36.21406 c 7.11959,15.242893 10.17798,31.779192 8.22563,48.814566 -5.01677,43.774134 -41.67531,79.324504 -91.53611,95.162894 6.62576,-22.40752 5.34093,-44.9362 -2.6395,-65.84431 47.73698,-14.18269 81.64502,-42.808386 85.94998,-78.13315 z"
+ id="path3814-0"
+ inkscape:connector-curvature="0" />
+ <path
+ transform="matrix(0.6556593,-0.75505688,0.75505688,0.6556593,0,0)"
+ style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m -5.0905523,259.06055 18.4167573,0 c 6.220455,0 11.228257,16.68196 11.228257,37.40349 l 0,172.83701 c 0,20.72153 -5.007802,37.40349 -11.228257,37.40349 l -18.4167573,0 c -6.2204547,0 -11.2282577,-16.68196 -11.2282577,-37.40349 l 0,-172.83701 c 0,-20.72153 5.007803,-37.40349 11.2282577,-37.40349 z"
+ id="rect3804" />
+ <path
+ sodipodi:type="arc"
+ style="fill:url(#radialGradient3798);fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path2987"
+ sodipodi:cx="294.45947"
+ sodipodi:cy="208.37973"
+ sodipodi:rx="107.58125"
+ sodipodi:ry="107.58125"
+ d="m 402.04073,208.37973 a 107.58125,107.58125 0 1 1 -215.16251,0 107.58125,107.58125 0 1 1 215.16251,0 z"
+ transform="translate(-107.07617,-60.609153)" />
+ <path
+ sodipodi:type="arc"
+ style="fill:url(#linearGradient3865);fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path3757"
+ sodipodi:cx="131.82491"
+ sodipodi:cy="299.29346"
+ sodipodi:rx="101.52033"
+ sodipodi:ry="101.52033"
+ d="m 233.34524,299.29346 a 101.52033,101.52033 0 1 1 -203.040667,0 101.52033,101.52033 0 1 1 203.040667,0 z"
+ transform="matrix(0.76865672,0,0,0.76865672,85.80266,-82.535889)" />
+ <path
+ sodipodi:type="arc"
+ style="fill:#1a1a1a;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path3800"
+ sodipodi:cx="183.34268"
+ sodipodi:cy="156.35687"
+ sodipodi:rx="27.274118"
+ sodipodi:ry="27.274118"
+ d="m 210.6168,156.35687 a 27.274118,27.274118 0 1 1 -54.54824,0 27.274118,27.274118 0 1 1 54.54824,0 z"
+ transform="translate(5,-7.1428572)" />
+ <path
+ sodipodi:type="arc"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ id="path3802"
+ sodipodi:cx="197.9899"
+ sodipodi:cy="203.32896"
+ sodipodi:rx="5.5558391"
+ sodipodi:ry="5.5558391"
+ d="m 203.54574,203.32896 a 5.5558391,5.5558391 0 1 1 -11.11168,0 5.5558391,5.5558391 0 1 1 11.11168,0 z"
+ transform="translate(1.4847712,-63.564549)" />
+ <rect
+ style="fill:#ffffff;fill-opacity:0.82211531000000004;fill-rule:nonzero;stroke:none;filter:url(#filter4024)"
+ id="rect3916"
+ width="2.2392972"
+ height="159.43797"
+ x="19.525793"
+ y="337.8396"
+ rx="2.8666623"
+ ry="9.0007057"
+ transform="matrix(0.74466525,-0.84318084,0.84318084,0.74466525,-35.543204,-26.349917)" />
+ </g>
+</svg>
diff --git a/searx/static/default/js/mootools-autocompleter-1.1.2-min.js b/searx/static/default/js/mootools-autocompleter-1.1.2-min.js
new file mode 100644
index 000000000..364e611cc
--- /dev/null
+++ b/searx/static/default/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.minLength){this.hideChoices()}else{if(e===this.queryValue||(this.visible&&e==this.selectedValue)){if(this.visible){return false}this.showChoices()}else{this.queryValue=e;this.queryIndex=b;if(!this.fetchCached()){this.query()}}}return true},fetchCached:function(){return false;if(!this.options.cache||!this.cached||!this.cached.length||this.cached.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.maxChoices<b.length&&!this.options.overflow){b.length=this.options.maxChoices}b.each(this.options.injectChoice||function(d){var c=new Element("li",{html:this.markQueryValue(d)});c.inputValue=d;this.addChoiceEvents(c).inject(this.choices)},this);this.showChoices()}},choiceOver:function(c,d){if(!c||c==this.selected){return}if(this.selected){this.selected.removeClass("autocompleter-selected")}this.selected=c.addClass("autocompleter-selected");this.fireEvent("onSelect",[this.element,this.selected,d]);if(!this.selectMode){this.opted=this.element.value}if(!d){return}this.selectedValue=this.selected.inputValue;if(this.overflown){var f=this.selected.getCoordinates(this.choices),e=this.options.overflowMargin,g=this.choices.scrollTop,a=this.choices.offsetHeight,b=g+a;if(f.top-e<g&&g){this.choices.scrollTop=Math.max(f.top-e,0)}else{if(f.bottom+e>b){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"),'<span class="autocompleter-queried">$1</span>')},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/searx/static/default/js/mootools-core-1.4.5-min.js b/searx/static/default/js/mootools-core-1.4.5-min.js
new file mode 100644
index 000000000..569473d1c
--- /dev/null
+++ b/searx/static/default/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<v.length;w++){t[v[w]]=i.call(this,v[w]);}}else{t=i.call(this,u);}return t;};};f.prototype.extend=function(i,s){this[i]=s;
+}.overloadSetter();f.prototype.implement=function(i,s){this.prototype[i]=s;}.overloadSetter();var n=Array.prototype.slice;f.from=function(i){return(o(i)=="function")?i:function(){return i;
+};};Array.from=function(i){if(i==null){return[];}return(a.isEnumerable(i)&&typeof i!="string")?(o(i)=="array")?i:n.call(i):[i];};Number.from=function(s){var i=parseFloat(s);
+return isFinite(i)?i:null;};String.from=function(i){return i+"";};f.implement({hide:function(){this.$hidden=true;return this;},protect:function(){this.$protected=true;
+return this;}});var a=this.Type=function(u,t){if(u){var s=u.toLowerCase();var i=function(v){return(o(v)==s);};a["is"+u]=i;if(t!=null){t.prototype.$family=(function(){return s;
+}).hide();}}if(t==null){return null;}t.extend(this);t.$constructor=a;t.prototype.$constructor=t;return t;};var e=Object.prototype.toString;a.isEnumerable=function(i){return(i!=null&&typeof i.length=="number"&&e.call(i)!="[object Function]");
+};var q={};var r=function(i){var s=o(i.prototype);return q[s]||(q[s]=[]);};var b=function(t,x){if(x&&x.$hidden){return;}var s=r(this);for(var u=0;u<s.length;
+u++){var w=s[u];if(o(w)=="type"){b.call(w,t,x);}else{w.call(this,t,x);}}var v=this.prototype[t];if(v==null||!v.$protected){this.prototype[t]=x;}if(this[t]==null&&o(x)=="function"){m.call(this,t,function(i){return x.apply(i,n.call(arguments,1));
+});}};var m=function(i,t){if(t&&t.$hidden){return;}var s=this[i];if(s==null||!s.$protected){this[i]=t;}};a.implement({implement:b.overloadSetter(),extend:m.overloadSetter(),alias:function(i,s){b.call(this,i,this.prototype[s]);
+}.overloadSetter(),mirror:function(i){r(this).push(i);return this;}});new a("Type",a);var d=function(s,x,v){var u=(x!=Object),B=x.prototype;if(u){x=new a(s,x);
+}for(var y=0,w=v.length;y<w;y++){var C=v[y],A=x[C],z=B[C];if(A){A.protect();}if(u&&z){x.implement(C,z.protect());}}if(u){var t=B.propertyIsEnumerable(v[0]);
+x.forEachMethod=function(G){if(!t){for(var F=0,D=v.length;F<D;F++){G.call(B,B[v[F]],v[F]);}}for(var E in B){G.call(B,B[E],E);}};}return d;};d("String",String,["charAt","charCodeAt","concat","indexOf","lastIndexOf","match","quote","replace","search","slice","split","substr","substring","trim","toLowerCase","toUpperCase"])("Array",Array,["pop","push","reverse","shift","sort","splice","unshift","concat","join","slice","indexOf","lastIndexOf","filter","forEach","every","map","some","reduce","reduceRight"])("Number",Number,["toExponential","toFixed","toLocaleString","toPrecision"])("Function",f,["apply","call","bind"])("RegExp",RegExp,["exec","test"])("Object",Object,["create","defineProperty","defineProperties","keys","getPrototypeOf","getOwnPropertyDescriptor","getOwnPropertyNames","preventExtensions","isExtensible","seal","isSealed","freeze","isFrozen"])("Date",Date,["now"]);
+Object.extend=m.overloadSetter();Date.extend("now",function(){return +(new Date);});new a("Boolean",Boolean);Number.prototype.$family=function(){return isFinite(this)?"number":"null";
+}.hide();Number.extend("random",function(s,i){return Math.floor(Math.random()*(i-s+1)+s);});var g=Object.prototype.hasOwnProperty;Object.extend("forEach",function(i,t,u){for(var s in i){if(g.call(i,s)){t.call(u,i[s],s,i);
+}}});Object.each=Object.forEach;Array.implement({forEach:function(u,v){for(var t=0,s=this.length;t<s;t++){if(t in this){u.call(v,this[t],t,this);}}},each:function(i,s){Array.forEach(this,i,s);
+return this;}});var l=function(i){switch(o(i)){case"array":return i.clone();case"object":return Object.clone(i);default:return i;}};Array.implement("clone",function(){var s=this.length,t=new Array(s);
+while(s--){t[s]=l(this[s]);}return t;});var h=function(s,i,t){switch(o(t)){case"object":if(o(s[i])=="object"){Object.merge(s[i],t);}else{s[i]=Object.clone(t);
+}break;case"array":s[i]=t.clone();break;default:s[i]=t;}return s;};Object.extend({merge:function(z,u,t){if(o(u)=="string"){return h(z,u,t);}for(var y=1,s=arguments.length;
+y<s;y++){var w=arguments[y];for(var x in w){h(z,x,w[x]);}}return z;},clone:function(i){var t={};for(var s in i){t[s]=l(i[s]);}return t;},append:function(w){for(var v=1,t=arguments.length;
+v<t;v++){var s=arguments[v]||{};for(var u in s){w[u]=s[u];}}return w;}});["Object","WhiteSpace","TextNode","Collection","Arguments"].each(function(i){new a(i);
+});var c=Date.now();String.extend("uniqueID",function(){return(c++).toString(36);});})();Array.implement({every:function(c,d){for(var b=0,a=this.length>>>0;
+b<a;b++){if((b in this)&&!c.call(d,this[b],b,this)){return false;}}return true;},filter:function(d,f){var c=[];for(var e,b=0,a=this.length>>>0;b<a;b++){if(b in this){e=this[b];
+if(d.call(f,e,b,this)){c.push(e);}}}return c;},indexOf:function(c,d){var b=this.length>>>0;for(var a=(d<0)?Math.max(0,b+d):d||0;a<b;a++){if(this[a]===c){return a;
+}}return -1;},map:function(c,e){var d=this.length>>>0,b=Array(d);for(var a=0;a<d;a++){if(a in this){b[a]=c.call(e,this[a],a,this);}}return b;},some:function(c,d){for(var b=0,a=this.length>>>0;
+b<a;b++){if((b in this)&&c.call(d,this[b],b,this)){return true;}}return false;},clean:function(){return this.filter(function(a){return a!=null;});},invoke:function(a){var b=Array.slice(arguments,1);
+return this.map(function(c){return c[a].apply(c,b);});},associate:function(c){var d={},b=Math.min(this.length,c.length);for(var a=0;a<b;a++){d[c[a]]=this[a];
+}return d;},link:function(c){var a={};for(var e=0,b=this.length;e<b;e++){for(var d in c){if(c[d](this[e])){a[d]=this[e];delete c[d];break;}}}return a;},contains:function(a,b){return this.indexOf(a,b)!=-1;
+},append:function(a){this.push.apply(this,a);return this;},getLast:function(){return(this.length)?this[this.length-1]:null;},getRandom:function(){return(this.length)?this[Number.random(0,this.length-1)]:null;
+},include:function(a){if(!this.contains(a)){this.push(a);}return this;},combine:function(c){for(var b=0,a=c.length;b<a;b++){this.include(c[b]);}return this;
+},erase:function(b){for(var a=this.length;a--;){if(this[a]===b){this.splice(a,1);}}return this;},empty:function(){this.length=0;return this;},flatten:function(){var d=[];
+for(var b=0,a=this.length;b<a;b++){var c=typeOf(this[b]);if(c=="null"){continue;}d=d.concat((c=="array"||c=="collection"||c=="arguments"||instanceOf(this[b],Array))?Array.flatten(this[b]):this[b]);
+}return d;},pick:function(){for(var b=0,a=this.length;b<a;b++){if(this[b]!=null){return this[b];}}return null;},hexToRgb:function(b){if(this.length!=3){return null;
+}var a=this.map(function(c){if(c.length==1){c+=c;}return c.toInt(16);});return(b)?a:"rgb("+a+")";},rgbToHex:function(d){if(this.length<3){return null;}if(this.length==4&&this[3]==0&&!d){return"transparent";
+}var b=[];for(var a=0;a<3;a++){var c=(this[a]-0).toString(16);b.push((c.length==1)?"0"+c:c);}return(d)?b:"#"+b.join("");}});String.implement({test:function(a,b){return((typeOf(a)=="regexp")?a:new RegExp(""+a,b)).test(this);
+},contains:function(a,b){return(b)?(b+this+b).indexOf(b+a+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;a<this;a++){b.call(c,a,this);}},toFloat:function(){return parseFloat(this);},toInt:function(a){return parseInt(this,a||10);
+}});Number.alias("each","times");(function(b){var a={};b.each(function(c){if(!Number[c]){a[c]=function(){return Math[c].apply(null,[this].concat(Array.from(arguments)));
+};}});Number.implement(a);})(["abs","acos","asin","atan","atan2","ceil","cos","exp","floor","log","max","min","pow","sin","sqrt","tan"]);Function.extend({attempt:function(){for(var b=0,a=arguments.length;
+b<a;b++){try{return arguments[b]();}catch(c){}}return null;}});Function.implement({attempt:function(a,c){try{return this.apply(c,Array.from(a));}catch(b){}return null;
+},bind:function(e){var a=this,b=arguments.length>1?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<b;e++){var c=g[e];if(c in d){f[c]=d[c];}}return f;},map:function(b,e,f){var d={};for(var c in b){if(a.call(b,c)){d[c]=e.call(f,b[c],c,b);
+}}return d;},filter:function(b,e,g){var d={};for(var c in b){var f=b[c];if(a.call(b,c)&&e.call(g,f,c,b)){d[c]=f;}}return d;},every:function(b,d,e){for(var c in b){if(a.call(b,c)&&!d.call(e,b[c],c)){return false;
+}}return true;},some:function(b,d,e){for(var c in b){if(a.call(b,c)&&d.call(e,b[c],c)){return true;}}return false;},keys:function(b){var d=[];for(var c in b){if(a.call(b,c)){d.push(c);
+}}return d;},values:function(c){var b=[];for(var d in c){if(a.call(c,d)){b.push(c[d]);}}return b;},getLength:function(b){return Object.keys(b).length;},keyOf:function(b,d){for(var c in b){if(a.call(b,c)&&b[c]===d){return c;
+}}return null;},contains:function(b,c){return Object.keyOf(b,c)!=null;},toQueryString:function(b,c){var d=[];Object.each(b,function(h,g){if(c){g=c+"["+g+"]";
+}var f;switch(typeOf(h)){case"object":f=Object.toQueryString(h,g);break;case"array":var e={};h.each(function(k,j){e[j]=k;});f=Object.toQueryString(e,g);
+break;default:f=g+"="+encodeURIComponent(h);}if(h!=null){d.push(f);}});return d.join("&");}});})();(function(){var j=this.document;var g=j.window=this;
+var a=navigator.userAgent.toLowerCase(),b=navigator.platform.toLowerCase(),h=a.match(/(opera|ie|firefox|chrome|version)[\s\/:]([\w\d\.]+)?.*?(safari|version[\s\/:]([\w\d\.]+)|$)/)||[null,"unknown",0],d=h[1]=="ie"&&j.documentMode;
+var n=this.Browser={extend:Function.prototype.extend,name:(h[1]=="version")?h[3]:h[1],version:d||parseFloat((h[1]=="opera"&&h[4])?h[4]:h[2]),Platform:{name:a.match(/ip(?:ad|od|hone)/)?"ios":(a.match(/(?:webos|android)/)||b.match(/mac|win|linux/)||["other"])[0]},Features:{xpath:!!(j.evaluate),air:!!(g.runtime),query:!!(j.querySelector),json:!!(g.JSON)},Plugins:{}};
+n[n.name]=true;n[n.name+parseInt(n.version,10)]=true;n.Platform[n.Platform.name]=true;n.Request=(function(){var p=function(){return new XMLHttpRequest();
+};var o=function(){return new ActiveXObject("MSXML2.XMLHTTP");};var e=function(){return new ActiveXObject("Microsoft.XMLHTTP");};return Function.attempt(function(){p();
+return p;},function(){o();return o;},function(){e();return e;});})();n.Features.xhr=!!(n.Request);var i=(Function.attempt(function(){return navigator.plugins["Shockwave Flash"].description;
+},function(){return new ActiveXObject("ShockwaveFlash.ShockwaveFlash").GetVariable("$version");})||"0 r0").match(/\d+/g);n.Plugins.Flash={version:Number(i[0]||"0."+i[1])||0,build:Number(i[2])||0};
+n.exec=function(o){if(!o){return o;}if(g.execScript){g.execScript(o);}else{var e=j.createElement("script");e.setAttribute("type","text/javascript");e.text=o;
+j.head.appendChild(e);j.head.removeChild(e);}return o;};String.implement("stripScripts",function(o){var e="";var p=this.replace(/<script[^>]*>([\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<r.length;p++){var t=r[p];var q={parts:[],tag:"*",combinator:i(t[0].combinator)};for(var o=0;o<t.length;
+o++){var s=t[o];if(!s.reverseCombinator){s.reverseCombinator=" ";}s.combinator=s.reverseCombinator;delete s.reverseCombinator;}t.reverse().push(q);}return u;
+};var f=function(o){return o.replace(/[-[\]{}()*+?.\\^$|,#\s]/g,function(p){return"\\"+p;});};var j=new RegExp("^(?:\\s*(,)\\s*|\\s*(<combinator>+)\\s*|(\\s+)|(<unicode>+|\\*)|\\#(<unicode>+)|\\.(<unicode>+)|\\[\\s*(<unicode1>+)(?:\\s*([*^$!~|]?=)(?:\\s*(?:([\"']?)(.*?)\\9)))?\\s*\\](?!\\])|(:+)(<unicode>+)(?:\\((?:(?:([\"'])([^\\13]*)\\13)|((?:\\([^)]+\\)|[^()]*)+))\\))?)".replace(/<combinator>/,"["+f(">+~`!@$%^&={}\\;</")+"]").replace(/<unicode>/g,"(?:[\\w\\u00a1-\\uFFFF-]|\\\\[^\\s0-9a-f])").replace(/<unicode1>/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='<a id="'+v+'"></a>';
+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</foo>";x=c.getElementsByTagName("*");q=(x&&!!x.length&&x[0].nodeName.charAt(0)=="/");}catch(C){}s.brokenStarGEBTN=u||q;try{c.innerHTML='<a name="'+v+'"></a><b id="'+v+'"></b>';
+s.idGetsName=w.getElementById(v)===c.firstChild;}catch(C){}if(c.getElementsByClassName){try{c.innerHTML='<a class="f"></a><a class="b"></a>';c.getElementsByClassName("b").length;
+c.firstChild.className="b";z=(c.getElementsByClassName("b").length!=2);}catch(C){}try{c.innerHTML='<a class="a"></a><a class="f b a"></a>';y=(c.getElementsByClassName("a").length!=2);
+}catch(C){}s.brokenGEBCN=z||y;}if(c.querySelectorAll){try{c.innerHTML="foo</foo>";x=c.querySelectorAll("*");s.starSelectsClosedQSA=(x&&!!x.length&&x[0].nodeName.charAt(0)=="/");
+}catch(C){}try{c.innerHTML='<a class="MiX"></a>';s.brokenMixedCaseQSA=!c.querySelectorAll(".MiX").length;}catch(C){}try{c.innerHTML='<select><option selected="selected">a</option></select>';
+s.brokenCheckedQSA=(c.querySelectorAll(":checked").length==0);}catch(C){}try{c.innerHTML='<a class=""></a>';s.brokenEmptyAttributeQSA=(c.querySelectorAll('[class*=""]').length!=0);
+}catch(C){}}try{c.innerHTML='<form action="s"><input id="action"/></form>';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;L<K;L++){this[B](N[L],J,D,q,G,W,c);if(p.length){break search;}}}else{for(L=0,K=N.length;L<K;L++){this[B](N[L],J,D,q,G,W,c);
+}}}N=this.found;}}if(I||(F.expressions.length>1)){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<x){return false;}}else{if(x<w){return false;}}return((w-x)%y)==0;};};k.pushArray=function(p,c,r,o,n,q){if(this.matchSelector(p,c,r,o,n,q)){this.found.push(p);
+}};k.pushUID=function(q,c,s,p,n,r){var o=this.getUID(q);if(!this.uniques[o]&&this.matchSelector(q,c,s,p,n,r)){this.uniques[o]=true;this.found.push(q);}};
+k.matchNode=function(n,o){if(this.isHTMLDocument&&this.nativeMatchesSelector){try{return this.nativeMatchesSelector.call(n,o.replace(/\[([^=]+)=\s*([^'"\]]+?)\s*\]/g,'[$1="$2"]'));
+}catch(u){}}var t=this.Slick.parse(o);if(!t){return true;}var r=t.expressions,s=0,q;for(q=0;(currentExpression=r[q]);q++){if(currentExpression.length==1){var p=currentExpression[0];
+if(this.matchSelector(n,(this.isXMLDocument)?p.tag:p.tag.toUpperCase(),p.id,p.classes,p.attributes,p.pseudos)){return true;}s++;}}if(s==t.length){return false;
+}var c=this.search(this.document,t),v;for(q=0;v=c[q++];){if(v===n){return true;}}return false;};k.matchPseudo=function(q,c,p){var n="pseudo:"+c;if(this[n]){return this[n](q,p);
+}var o=this.getAttribute(q,c);return(p)?p==o:!!o;};k.matchSelector=function(o,v,c,p,q,s){if(v){var t=(this.isXMLDocument)?o.nodeName:o.nodeName.toUpperCase();
+if(v=="*"){if(t<"@"){return false;}}else{if(t!=v){return false;}}}if(c&&o.getAttribute("id")!=c){return false;}var r,n,u;if(p){for(r=p.length;r--;){u=this.getAttribute(o,"class");
+if(!(u&&p[r].regexp.test(u))){return false;}}}if(q){for(r=q.length;r--;){n=q[r];if(n.operator?!n.test(this.getAttribute(o,n.key)):!this.hasAttribute(o,n.key)){return false;
+}}}if(s){for(r=s.length;r--;){n=s[r];if(!this.matchPseudo(o,n.key,n.value)){return false;}}}return true;};var j={" ":function(q,w,n,r,s,u,p){var t,v,o;
+if(this.isHTMLDocument){getById:if(n){v=this.document.getElementById(n);if((!v&&q.all)||(this.idGetsName&&v&&v.getAttributeNode("id").nodeValue!=n)){o=q.all[n];
+if(!o){return;}if(!o[0]){o=[o];}for(t=0;v=o[t++];){var c=v.getAttributeNode("id");if(c&&c.nodeValue==n){this.push(v,w,null,r,s,u);break;}}return;}if(!v){if(this.contains(this.root,q)){return;
+}else{break getById;}}else{if(this.document!==q&&!this.contains(q,v)){return;}}this.push(v,w,null,r,s,u);return;}getByClass:if(r&&q.getElementsByClassName&&!this.brokenGEBCN){o=q.getElementsByClassName(p.join(" "));
+if(!(o&&o.length)){break getByClass;}for(t=0;v=o[t++];){this.push(v,w,n,null,s,u);}return;}}getByTag:{o=q.getElementsByTagName(w);if(!(o&&o.length)){break getByTag;
+}if(!this.brokenStarGEBTN){w=null;}for(t=0;v=o[t++];){this.push(v,w,n,r,s,u);}}},">":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<c;f++){a=d[f];if(g[a.key]!=null){continue;
+}if(a.value!=null&&a.operator=="="){g[a.key]=a.value;}else{if(!a.value&&!a.operator){g[a.key]=true;}}}}if(e.classList&&g["class"]==null){g["class"]=e.classList.join(" ");
+}}return document.newElement(b,g);};if(Browser.Element){Element.prototype=Browser.Element.prototype;Element.prototype._fireEvent=(function(a){return function(b,c){return a.call(this,b,c);
+};})(Element.prototype.fireEvent);}new Type("Element",Element).mirror(function(a){if(Array.prototype[a]){return;}var b={};b[a]=function(){var h=[],e=arguments,j=true;
+for(var g=0,d=this.length;g<d;g++){var f=this[g],c=h[g]=f[a].apply(f,e);j=(j&&typeOf(c)=="element");}return(j)?new Elements(h):h;};Elements.implement(b);
+});if(!Browser.Element){Element.parent=Object;Element.Prototype={"$constructor":Element,"$family":Function.from("element").hide()};Element.mirror(function(a,b){Element.Prototype[a]=b;
+});}Element.Constructors={};var IFrame=new Type("IFrame",function(){var e=Array.link(arguments,{properties:Type.isObject,iframe:function(f){return(f!=null);
+}});var c=e.properties||{},b;if(e.iframe){b=document.id(e.iframe);}var d=c.onload||function(){};delete c.onload;c.id=c.name=[c.id,c.name,b?(b.id||b.name):"IFrame_"+String.uniqueID()].pick();
+b=new Element(b||"iframe",c);var a=function(){d.call(b.contentWindow);};if(window.frames[c.id]){a();}else{b.addListener("load",a);}return b;});var Elements=this.Elements=function(a){if(a&&a.length){var e={},d;
+for(var c=0;d=a[c++];){var b=Slick.uidOf(d);if(!e[b]){e[b]=true;this.push(d);}}}};Elements.prototype={length:0};Elements.parent=Array;new Type("Elements",Elements).implement({filter:function(a,b){if(!a){return this;
+}return new Elements(Array.filter(this,(typeOf(a)=="string")?function(c){return c.match(a);}:a,b));}.protect(),push:function(){var d=this.length;for(var b=0,a=arguments.length;
+b<a;b++){var c=document.id(arguments[b]);if(c){this[d++]=c;}}return(this.length=d);}.protect(),unshift:function(){var b=[];for(var c=0,a=arguments.length;
+c<a;c++){var d=document.id(arguments[c]);if(d){b.push(d);}}return Array.prototype.unshift.apply(this,b);}.protect(),concat:function(){var b=new Elements(this);
+for(var c=0,a=arguments.length;c<a;c++){var d=arguments[c];if(Type.isEnumerable(d)){b.append(d);}else{b.push(d);}}return b;}.protect(),append:function(c){for(var b=0,a=c.length;
+b<a;b++){this.push(c[b]);}return this;}.protect(),empty:function(){while(this.length){delete this[--this.length];}return this;}.protect()});(function(){var f=Array.prototype.splice,a={"0":0,"1":1,length:2};
+f.call(a,1,1);if(a[1]==1){Elements.implement("splice",function(){var g=this.length;var e=f.apply(this,arguments);while(g>=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("<input name=x>").name=="x");
+}catch(b){}var c=function(e){return(""+e).replace(/&/g,"&amp;").replace(/"/g,"&quot;");};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<F;D++){var l=document.id(G[D],true);if(l){E.appendChild(l);}}if(e){this.appendChild(e);}return this;
+},appendText:function(l,e){return this.grab(this.getDocument().newTextNode(l),e);},grab:function(l,e){w[e||"bottom"](document.id(l,true),this);return this;
+},inject:function(l,e){w[e||"bottom"](this,document.id(l,true));return this;},replaces:function(e){e=document.id(e,true);e.parentNode.replaceChild(this,e);
+return this;},wraps:function(l,e){l=document.id(l,true);return this.replaces(l).grab(l,e);},getSelected:function(){this.selectedIndex;return new Elements(Array.from(this.options).filter(function(e){return e.selected;
+}));},toQueryString:function(){var e=[];this.getElements("input, select, textarea").each(function(D){var l=D.type;if(!D.name||D.disabled||l=="submit"||l=="reset"||l=="file"||l=="image"){return;
+}var E=(D.get("tag")=="select")?D.getSelected().map(function(F){return document.id(F).get("value");}):((l=="radio"||l=="checkbox")&&!D.checked)?null:D.get("value");
+Array.from(E).each(function(F){if(typeof F!="undefined"){e.push(encodeURIComponent(D.name)+"="+encodeURIComponent(F));}});});return e.join("&");}});var i={},A={};
+var B=function(e){return(A[e]||(A[e]={}));};var v=function(l){var e=l.uniqueNumber;if(l.removeEvents){l.removeEvents();}if(l.clearAttributes){l.clearAttributes();
+}if(e!=null){delete i[e];delete A[e];}return l;};var C={input:"checked",option:"selected",textarea:"value"};Element.implement({destroy:function(){var e=v(this).getElementsByTagName("*");
+Array.each(e,v);Element.dispose(this);return null;},empty:function(){Array.from(this.childNodes).each(Element.dispose);return this;},dispose:function(){return(this.parentNode)?this.parentNode.removeChild(this):this;
+},clone:function(G,E){G=G!==false;var L=this.cloneNode(G),D=[L],F=[this],J;if(G){D.append(Array.from(L.getElementsByTagName("*")));F.append(Array.from(this.getElementsByTagName("*")));
+}for(J=D.length;J--;){var H=D[J],K=F[J];if(!E){H.removeAttribute("id");}if(H.clearAttributes){H.clearAttributes();H.mergeAttributes(K);H.removeAttribute("uniqueNumber");
+if(H.options){var O=H.options,e=K.options;for(var I=O.length;I--;){O[I].selected=e[I].selected;}}}var l=C[K.tagName.toLowerCase()];if(l&&K[l]){H[l]=K[l];
+}}if(Browser.ie){var M=L.getElementsByTagName("object"),N=this.getElementsByTagName("object");for(J=M.length;J--;){M[J].outerHTML=N[J].outerHTML;}}return document.id(L);
+}});[Element,Window,Document].invoke("implement",{addListener:function(E,D){if(E=="unload"){var e=D,l=this;D=function(){l.removeListener("unload",D);e();
+};}else{i[Slick.uidOf(this)]=this;}if(this.addEventListener){this.addEventListener(E,D,!!arguments[2]);}else{this.attachEvent("on"+E,D);}return this;},removeListener:function(l,e){if(this.removeEventListener){this.removeEventListener(l,e,!!arguments[2]);
+}else{this.detachEvent("on"+l,e);}return this;},retrieve:function(l,e){var E=B(Slick.uidOf(this)),D=E[l];if(e!=null&&D==null){D=E[l]=e;}return D!=null?D:null;
+},store:function(l,e){var D=B(Slick.uidOf(this));D[l]=e;return this;},eliminate:function(e){var l=B(Slick.uidOf(this));delete l[e];return this;}});if(window.attachEvent&&!window.addEventListener){window.addListener("unload",function(){Object.each(i,v);
+if(window.CollectGarbage){CollectGarbage();}});}Element.Properties={};Element.Properties.style={set:function(e){this.style.cssText=e;},get:function(){return this.style.cssText;
+},erase:function(){this.style.cssText="";}};Element.Properties.tag={get:function(){return this.tagName.toLowerCase();}};Element.Properties.html={set:function(e){if(e==null){e="";
+}else{if(typeOf(e)=="array"){e=e.join("");}}this.innerHTML=e;},erase:function(){this.innerHTML="";}};var t=document.createElement("div");t.innerHTML="<nav></nav>";
+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="<tr><td></td></tr>";return true;
+});var c=document.createElement("tr"),o="<td></td>";c.innerHTML=o;var y=(c.innerHTML==o);c=null;if(!g||!y||!a){Element.Properties.html.set=(function(l){var e={table:[1,"<table>","</table>"],select:[1,"<select>","</select>"],tbody:[2,"<table><tbody>","</tbody></table>"],tr:[3,"<table><tbody><tr>","</tr></tbody></table>"]};
+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="<select><option>s</option></select>";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;E<D.length;E++){var F=D[E],e=F.getAttributeNode("value"),H=(e&&e.specified)?F.value:F.get("text");
+if(H==G){return F.selected=true;}}},get:function(){var D=this,l=D.get("tag");if(l!="select"&&l!="option"){return this.getProperty("value");}if(l=="select"&&!(D=D.getSelected()[0])){return"";
+}var e=D.getAttributeNode("value");return(e&&e.specified)?D.value:D.get("text");}};}n=null;if(document.createElement("div").getAttributeNode("id")){Element.Properties.id={set:function(e){this.id=this.getAttributeNode("id").value=e;
+},get:function(){return this.id||null;},erase:function(){this.id=this.getAttributeNode("id").value="";}};}})();(function(){var i=document.html;var d=document.createElement("div");
+d.style.color="red";d.style.color=null;var c=d.style.color=="red";d=null;Element.Properties.styles={set:function(k){this.setStyles(k);}};var h=(i.style.opacity!=null),e=(i.style.filter!=null),j=/alpha\(opacity=([\d.]+)\)/i;
+var a=function(l,k){l.store("$opacity",k);l.style.visibility=k>0||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<this.frames){var j=this.transition(this.frame/this.frames);this.set(this.compute(this.from,this.to,j));
+}else{this.frame=this.frames;this.set(this.compute(this.from,this.to,1));this.stop();}},set:function(g){return g;},compute:function(i,h,g){return f.compute(i,h,g);
+},check:function(){if(!this.isRunning()){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;},start:function(k,j){if(!this.check(k,j)){return this;}this.from=k;this.to=j;this.frame=(this.options.frameSkip)?0:-1;this.time=null;
+this.transition=this.getTransition();var i=this.options.frames,h=this.options.fps,g=this.options.duration;this.duration=f.Durations[g]||g.toInt();this.frameInterval=1000/h;
+this.frames=i||Math.round(this.duration/this.frameInterval);this.fireEvent("start",this.subject);b.call(this,h);return this;},stop:function(){if(this.isRunning()){this.time=null;
+d.call(this,this.options.fps);if(this.frames==this.frame){this.fireEvent("complete",this.subject);if(!this.callChain()){this.fireEvent("chainComplete",this.subject);
+}}else{this.fireEvent("stop",this.subject);}}return this;},cancel:function(){if(this.isRunning()){this.time=null;d.call(this,this.options.fps);this.frame=this.frames;
+this.fireEvent("cancel",this.subject).clearChain();}return this;},pause:function(){if(this.isRunning()){this.time=null;d.call(this,this.options.fps);}return this;
+},resume:function(){if((this.frame<this.frames)&&!this.isRunning()){b.call(this,this.options.fps);}return this;},isRunning:function(){var g=e[this.options.fps];
+return g&&g.contains(this);}});f.compute=function(i,h,g){return(h-i)*g+i;};f.Durations={"short":250,normal:500,"long":1000};var e={},c={};var a=function(){var h=Date.now();
+for(var j=this.length;j--;){var g=this[j];if(g){g.step(h);}}};var b=function(h){var g=e[h]||(e[h]=[]);g.push(this);if(!c[h]){c[h]=a.periodical(Math.round(1000/h),g);
+}};var d=function(h){var g=e[h];if(g){g.erase(this);if(!g.length&&c[h]){delete e[h];c[h]=clearInterval(c[h]);}}};})();Fx.CSS=new Class({Extends:Fx,prepare:function(b,e,a){a=Array.from(a);
+var h=a[0],g=a[1];if(g==null){g=h;h=b.getStyle(e);var c=this.options.unit;if(c&&h.slice(-c.length)!=c&&parseFloat(h)!=0){b.setStyle(e,g+c);var d=b.getComputedStyle(e);
+if(!(/px$/.test(d))){d=b.style[("pixel-"+e).camelCase()];if(d==null){var f=b.style.left;b.style.left=g+c;d=b.style.pixelLeft;b.style.left=f;}}h=(g||1)/(parseFloat(d)||1)*(parseFloat(h)||0);
+b.setStyle(e,h+c);}}return{from:this.parse(h),to:this.parse(g)};},parse:function(a){a=Function.from(a)();a=(typeof a=="string")?a.split(" "):Array.from(a);
+return a.map(function(c){c=String(c);var b=false;Object.each(Fx.CSS.Parsers,function(f,e){if(b){return;}var d=f.parse(c);if(d||d===0){b={value:d,parser:f};
+}});b=b||{value:c,parser:Fx.CSS.Parsers.String};return b;});},compute:function(d,c,b){var a=[];(Math.min(d.length,c.length)).times(function(e){a.push({value:d[e].parser.compute(d[e].value,c[e].value,b),parser:d[e].parser});
+});a.$family=Function.from("fx:css:value");return a;},serve:function(c,b){if(typeOf(c)!="fx:css:value"){c=this.parse(c);}var a=[];c.each(function(d){a=a.concat(d.parser.serve(d.value,b));
+});return a;},render:function(a,d,c,b){a.setStyle(d,this.serve(c,b));},search:function(a){if(Fx.CSS.Cache[a]){return Fx.CSS.Cache[a];}var c={},b=new RegExp("^"+a.escapeRegExp()+"$");
+Array.each(document.styleSheets,function(f,e){var d=f.href;if(d&&d.contains("://")&&!d.contains(document.domain)){return;}var g=f.rules||f.cssRules;Array.each(g,function(k,h){if(!k.style){return;
+}var j=(k.selectorText)?k.selectorText.replace(/^\w+/,function(i){return i.toLowerCase();}):null;if(!j||!b.test(j)){return;}Object.each(Element.Styles,function(l,i){if(!k.style[i]||Element.ShortStyles[i]){return;
+}l=String(k.style[i]);c[i]=((/^rgb/).test(l))?l.rgbToHex():l;});});});return Fx.CSS.Cache[a]=c;}});Fx.CSS.Cache={};Fx.CSS.Parsers={Color:{parse:function(a){if(a.match(/^#[0-9a-f]{3,6}$/i)){return a.hexToRgb(true);
+}return((a=a.match(/(\d+),\s*(\d+),\s*(\d+)/)))?[a[1],a[2],a[3]]:false;},compute:function(c,b,a){return c.map(function(e,d){return Math.round(Fx.compute(c[d],b[d],a));
+});},serve:function(a){return a.map(Number);}},Number:{parse:parseFloat,compute:Fx.compute,serve:function(b,a){return(a)?b+a:b;}},String:{parse:Function.from(false),compute:function(b,a){return a;
+},serve:function(a){return a;}}};Fx.Tween=new Class({Extends:Fx.CSS,initialize:function(b,a){this.element=this.subject=document.id(b);this.parent(a);},set:function(b,a){if(arguments.length==1){a=b;
+b=this.property||this.options.property;}this.render(this.element,b,a,this.options.unit);return this;},start:function(c,e,d){if(!this.check(c,e,d)){return this;
+}var b=Array.flatten(arguments);this.property=this.options.property||b.shift();var a=this.prepare(this.element,this.property,b);return this.parent(a.from,a.to);
+}});Element.Properties.tween={set:function(a){this.get("tween").cancel().setOptions(a);return this;},get:function(){var a=this.retrieve("tween");if(!a){a=new Fx.Tween(this,{link:"cancel"});
+this.store("tween",a);}return a;}};Element.implement({tween:function(a,c,b){this.get("tween").start(a,c,b);return this;},fade:function(d){var e=this.get("tween"),g,c=["opacity"].append(arguments),a;
+if(c[1]==null){c[1]="toggle";}switch(c[1]){case"in":g="start";c[1]=1;break;case"out":g="start";c[1]=0;break;case"show":g="set";c[1]=1;break;case"hide":g="set";
+c[1]=0;break;case"toggle":var b=this.retrieve("fade:flag",this.getStyle("opacity")==1);g="start";c[1]=b?0:1;this.store("fade:flag",!b);a=true;break;default:g="start";
+}if(!a){this.eliminate("fade:flag");}e[g].apply(e,c);var f=c[c.length-1];if(g=="set"||f!=0){this.setStyle("visibility",f==0?"hidden":"visible");}else{e.chain(function(){this.element.setStyle("visibility","hidden");
+this.callChain();});}return this;},highlight:function(c,a){if(!a){a=this.retrieve("highlight:original",this.getStyle("background-color"));a=(a=="transparent")?"#fff":a;
+}var b=this.get("tween");b.start("background-color",c||"#ffff88",a).chain(function(){this.setStyle("background-color",this.retrieve("highlight:original"));
+b.callChain();}.bind(this));return this;}});Fx.Morph=new Class({Extends:Fx.CSS,initialize:function(b,a){this.element=this.subject=document.id(b);this.parent(a);
+},set:function(a){if(typeof a=="string"){a=this.search(a);}for(var b in a){this.render(this.element,b,a[b],this.options.unit);}return this;},compute:function(e,d,c){var a={};
+for(var b in e){a[b]=this.parent(e[b],d[b],c);}return a;},start:function(b){if(!this.check(b)){return this;}if(typeof b=="string"){b=this.search(b);}var e={},d={};
+for(var c in b){var a=this.prepare(this.element,c,b[c]);e[c]=a.from;d[c]=a.to;}return this.parent(e,d);}});Element.Properties.morph={set:function(a){this.get("morph").cancel().setOptions(a);
+return this;},get:function(){var a=this.retrieve("morph");if(!a){a=new Fx.Morph(this,{link:"cancel"});this.store("morph",a);}return a;}};Element.implement({morph:function(a){this.get("morph").start(a);
+return this;}});Fx.implement({getTransition:function(){var a=this.options.transition||Fx.Transitions.Sine.easeInOut;if(typeof a=="string"){var b=a.split(":");
+a=Fx.Transitions;a=a[b[0]]||a[b[0].capitalize()];if(b[1]){a=a["ease"+b[1].capitalize()+(b[2]?b[2].capitalize():"")];}}return a;}});Fx.Transition=function(c,b){b=Array.from(b);
+var a=function(d){return c(d,b);};return Object.append(a,{easeIn:a,easeOut:function(d){return 1-c(1-d,b);},easeInOut:function(d){return(d<=0.5?c(2*d,b):(2-c(2*(1-d),b)))/2;
+}});};Fx.Transitions={linear:function(a){return a;}};Fx.Transitions.extend=function(a){for(var b in a){Fx.Transitions[b]=new Fx.Transition(a[b]);}};Fx.Transitions.extend({Pow:function(b,a){return Math.pow(b,a&&a[0]||6);
+},Expo:function(a){return Math.pow(2,8*(a-1));},Circ:function(a){return 1-Math.sin(Math.acos(a));},Sine:function(a){return 1-Math.cos(a*Math.PI/2);},Back:function(b,a){a=a&&a[0]||1.618;
+return Math.pow(b,2)*((a+1)*b-a);},Bounce:function(f){var e;for(var d=0,c=1;1;d+=c,c/=2){if(f>=(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(/<body[^>]*>([\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='<object id="'+id+'"';for(var property in properties){build+=" "+property+'="'+properties[property]+'"';
+}build+=">";for(var param in params){if(params[param]){build+='<param name="'+param+'" value="'+params[param]+'" />';}}build+="</object>";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('<invoke name="'+fn+'" returntype="javascript">'+__flash__argumentsToXML(arguments,2)+"</invoke>");
+return eval(rs);};})(); \ No newline at end of file
diff --git a/searx/static/default/js/searx.js b/searx/static/default/js/searx.js
new file mode 100644
index 000000000..c87dc30d4
--- /dev/null
+++ b/searx/static/default/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': false,
+ 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/searx/static/less/autocompleter.less b/searx/static/default/less/autocompleter.less
index db9601aeb..db9601aeb 100644
--- a/searx/static/less/autocompleter.less
+++ b/searx/static/default/less/autocompleter.less
diff --git a/searx/static/less/definitions.less b/searx/static/default/less/definitions.less
index 3e0b6579d..3e0b6579d 100644
--- a/searx/static/less/definitions.less
+++ b/searx/static/default/less/definitions.less
diff --git a/searx/static/less/mixins.less b/searx/static/default/less/mixins.less
index dbccce6e3..dbccce6e3 100644
--- a/searx/static/less/mixins.less
+++ b/searx/static/default/less/mixins.less
diff --git a/searx/static/less/search.less b/searx/static/default/less/search.less
index d285ca734..d285ca734 100644
--- a/searx/static/less/search.less
+++ b/searx/static/default/less/search.less
diff --git a/searx/static/less/style.less b/searx/static/default/less/style.less
index f1f729b8d..e3fac1b10 100644
--- a/searx/static/less/style.less
+++ b/searx/static/default/less/style.less
@@ -216,6 +216,10 @@ a {
}
}
+.cache_link {
+ font-size: 10px !important;
+}
+
.result {
h3 {
font-size: 1em;
@@ -235,7 +239,7 @@ a {
.url {
font-size: 0.8em;
- margin: 3px 0 0 0;
+ margin: 0 0 3px 0;
padding: 0;
max-width: 54em;
word-wrap:break-word;
@@ -464,6 +468,9 @@ tr {
padding: 0;
width: 90%;
}
+ .github {
+ display: none;
+ }
.checkbox_container {
display: block;
diff --git a/searx/templates/about.html b/searx/templates/courgette/about.html
index 0ddf12254..19aba1905 100644
--- a/searx/templates/about.html
+++ b/searx/templates/courgette/about.html
@@ -1,6 +1,6 @@
-{% extends 'base.html' %}
+{% extends 'default/base.html' %}
{% block content %}
-{% include 'github_ribbon.html' %}
+{% include 'default/github_ribbon.html' %}
<div class="row">
<h1>About <a href="{{ url_for('index') }}">searx</a></h1>
diff --git a/searx/templates/base.html b/searx/templates/courgette/base.html
index da5ae905f..da5ae905f 100644
--- a/searx/templates/base.html
+++ b/searx/templates/courgette/base.html
diff --git a/searx/templates/categories.html b/searx/templates/courgette/categories.html
index 57e63c85d..57e63c85d 100644
--- a/searx/templates/categories.html
+++ b/searx/templates/courgette/categories.html
diff --git a/searx/templates/github_ribbon.html b/searx/templates/courgette/github_ribbon.html
index bdd9cf180..bdd9cf180 100644
--- a/searx/templates/github_ribbon.html
+++ b/searx/templates/courgette/github_ribbon.html
diff --git a/searx/templates/index.html b/searx/templates/courgette/index.html
index 57b67ef09..b4f55608b 100644
--- a/searx/templates/index.html
+++ b/searx/templates/courgette/index.html
@@ -1,8 +1,9 @@
-{% extends "base.html" %}
+{% extends "default/base.html" %}
{% block content %}
+{% include 'default/github_ribbon.html' %}
<div class="center">
<div class="title"><h1>searx</h1></div>
- {% include 'search.html' %}
+ {% include 'default/search.html' %}
<p class="top_margin">
<a href="{{ url_for('about') }}" class="hmarg">{{ _('about') }}</a>
<a href="{{ url_for('preferences') }}" class="hmarg">{{ _('preferences') }}</a>
diff --git a/searx/templates/opensearch.xml b/searx/templates/courgette/opensearch.xml
index f39283f99..f39283f99 100644
--- a/searx/templates/opensearch.xml
+++ b/searx/templates/courgette/opensearch.xml
diff --git a/searx/templates/opensearch_response_rss.xml b/searx/templates/courgette/opensearch_response_rss.xml
index 5673eb2e1..5673eb2e1 100644
--- a/searx/templates/opensearch_response_rss.xml
+++ b/searx/templates/courgette/opensearch_response_rss.xml
diff --git a/searx/templates/preferences.html b/searx/templates/courgette/preferences.html
index eeb86577f..7d35de7c3 100644
--- a/searx/templates/preferences.html
+++ b/searx/templates/courgette/preferences.html
@@ -1,4 +1,4 @@
-{% extends "base.html" %}
+{% extends "default/base.html" %}
{% block head %} {% endblock %}
{% block content %}
<div class="row">
@@ -8,7 +8,7 @@
<fieldset>
<legend>{{ _('Default categories') }}</legend>
<p>
- {% include 'categories.html' %}
+ {% include 'default/categories.html' %}
</p>
</fieldset>
<fieldset>
@@ -53,6 +53,16 @@
</p>
</fieldset>
<fieldset>
+ <legend>{{ _('Themes') }}</legend>
+ <p>
+ <select name="theme">
+ {% for name in themes %}
+ <option value="{{ name }}" {% if name == theme %}selected="selected"{% endif %}>{{ name }}</option>
+ {% endfor %}
+ </select>
+ </p>
+ </fieldset>
+ <fieldset>
<legend>{{ _('Currently used search engines') }}</legend>
<table>
diff --git a/searx/templates/result_templates/default.html b/searx/templates/courgette/result_templates/default.html
index e0711b761..734f9066c 100644
--- a/searx/templates/result_templates/default.html
+++ b/searx/templates/courgette/result_templates/default.html
@@ -1,7 +1,7 @@
<div class="result {{ result.class }}">
{% if result['favicon'] %}
- <img width="14" height="14" class="favicon" src="static/img/icon_{{result['favicon']}}.ico" />
+ <img width="14" height="14" class="favicon" src="static/{{theme}}/img/icon_{{result['favicon']}}.ico" />
{% endif %}
<div>
diff --git a/searx/templates/result_templates/images.html b/searx/templates/courgette/result_templates/images.html
index 1f15ff2bb..1f15ff2bb 100644
--- a/searx/templates/result_templates/images.html
+++ b/searx/templates/courgette/result_templates/images.html
diff --git a/searx/templates/result_templates/torrent.html b/searx/templates/courgette/result_templates/torrent.html
index 6c62793a5..6c62793a5 100644
--- a/searx/templates/result_templates/torrent.html
+++ b/searx/templates/courgette/result_templates/torrent.html
diff --git a/searx/templates/result_templates/videos.html b/searx/templates/courgette/result_templates/videos.html
index ab869a6eb..8ceb0b180 100644
--- a/searx/templates/result_templates/videos.html
+++ b/searx/templates/courgette/result_templates/videos.html
@@ -1,6 +1,6 @@
<div class="result">
{% if result['favicon'] %}
- <img width="14" height="14" class="favicon" src="static/img/icon_{{result['favicon']}}.ico" />
+ <img width="14" height="14" class="favicon" src="static/{{theme}}/img/icon_{{result['favicon']}}.ico" />
{% endif %}
<p>
diff --git a/searx/templates/results.html b/searx/templates/courgette/results.html
index 608cfb20a..d0b53b48a 100644
--- a/searx/templates/results.html
+++ b/searx/templates/courgette/results.html
@@ -1,9 +1,9 @@
-{% extends "base.html" %}
+{% extends "default/base.html" %}
{% block title %}{{ q }} - {% endblock %}
{% block content %}
<div class="right"><a href="{{ url_for('preferences') }}" id="preferences"><span>preferences</span></a></div>
<div class="small search center">
- {% include 'search.html' %}
+ {% include 'default/search.html' %}
</div>
<div id="results">
<div id="sidebar">
@@ -43,9 +43,9 @@
{% for result in results %}
{% if result['template'] %}
- {% include 'result_templates/'+result['template'] %}
+ {% include 'default/result_templates/'+result['template'] %}
{% else %}
- {% include 'result_templates/default.html' %}
+ {% include 'default/result_templates/default.html' %}
{% endif %}
{% endfor %}
diff --git a/searx/templates/search.html b/searx/templates/courgette/search.html
index 30d1568cf..8a9965582 100644
--- a/searx/templates/search.html
+++ b/searx/templates/courgette/search.html
@@ -3,5 +3,5 @@
<input type="text" placeholder="{{ _('Search for...') }}" id="q" class="q" name="q" tabindex="1" autocomplete="off" {% if q %}value="{{ q }}"{% endif %}/>
<input type="submit" value="search" id="search_submit" />
</div>
- {% include 'categories.html' %}
+ {% include 'default/categories.html' %}
</form>
diff --git a/searx/templates/stats.html b/searx/templates/courgette/stats.html
index cb5757b31..70fe98ac7 100644
--- a/searx/templates/stats.html
+++ b/searx/templates/courgette/stats.html
@@ -1,4 +1,4 @@
-{% extends "base.html" %}
+{% extends "default/base.html" %}
{% block head %} {% endblock %}
{% block content %}
<h2>{{ _('Engine stats') }}</h2>
diff --git a/searx/templates/default/about.html b/searx/templates/default/about.html
new file mode 100644
index 000000000..19aba1905
--- /dev/null
+++ b/searx/templates/default/about.html
@@ -0,0 +1,66 @@
+{% extends 'default/base.html' %}
+{% block content %}
+{% include 'default/github_ribbon.html' %}
+<div class="row">
+ <h1>About <a href="{{ url_for('index') }}">searx</a></h1>
+
+ <p>Searx is a <a href="https://en.wikipedia.org/wiki/Metasearch_engine">metasearch engine</a>, aggregating the results of other <a href="{{ url_for('preferences') }}">search engines</a> while not storing information about its users.
+ </p>
+ <h2>Why use Searx?</h2>
+ <ul>
+ <li>Searx may not offer you as personalised results as Google, but it doesn't generate a profile about you</li>
+ <li>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</li>
+ <li>Searx is free software, the code is 100% open and you can help to make it better. See more on <a href="https://github.com/asciimoo/searx">github</a></li>
+ </ul>
+ <p>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</p>
+
+<h2>Technical details - How does it work?</h2>
+
+<p>Searx is a <a href="https://en.wikipedia.org/wiki/Metasearch_engine">metasearch engine</a>,
+inspired by the <a href="http://seeks-project.info/">seeks project</a>.<br />
+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.<br />
+Searx can be added to your browser's search bar; moreover, it can be set as the default search engine.
+</p>
+
+<h2>How can I make it my own?</h2>
+
+<p>Searx appreciates your concern regarding logs, so take the <a href="https://github.com/asciimoo/searx">code</a> and run it yourself! <br />Add your Searx to this <a href="https://github.com/asciimoo/searx/wiki/Searx-instances">list</a> to help other people reclaim their privacy and make the Internet freer!
+<br />The more decentralized the Internet, is the more freedom we have!</p>
+
+
+<h2>More about searx</h2>
+
+<ul>
+ <li><a href="https://github.com/asciimoo/searx">github</a></li>
+ <li><a href="https://www.ohloh.net/p/searx/">ohloh</a></li>
+ <li><a href="https://twitter.com/Searx_engine">twitter</a></li>
+ <li>IRC: #searx @ freenode (<a href="https://kiwiirc.com/client/irc.freenode.com/searx">webclient</a>)</li>
+ <li><a href="https://www.transifex.com/projects/p/searx/">transifex</a></li>
+</ul>
+
+
+<hr />
+
+<h2 id="faq">FAQ</h2>
+
+<h3>How to add to firefox?</h3>
+<p><a href="#" onclick="window.external.AddSearchProvider(window.location.protocol + '//' + window.location.host + '{{ url_for('opensearch') }}');">Install</a> searx as a search engine on any version of Firefox! (javascript required)</p>
+
+<h2 id="dev_faq">Developer FAQ</h2>
+
+<h3>New engines?</h3>
+<ul>
+ <li>Edit your <a href="https://raw.github.com/asciimoo/searx/master/searx/settings.yml">settings.yml</a></li>
+ <li>Create your custom engine module, check the <a href="https://github.com/asciimoo/searx/blob/master/examples/basic_engine.py">example engine</a></li>
+</ul>
+<p>Don't forget to restart searx after config edit!</p>
+
+<h3>Installation/WSGI support?</h3>
+<p>See the <a href="https://github.com/asciimoo/searx/wiki/Installation">installation and setup</a> wiki page</p>
+
+<h3>How to debug engines?</h3>
+<p><a href="{{ url_for('stats') }}">Stats page</a> contains some useful data about the engines used.</p>
+
+</div>
+{% endblock %}
diff --git a/searx/templates/default/base.html b/searx/templates/default/base.html
new file mode 100644
index 000000000..da5ae905f
--- /dev/null
+++ b/searx/templates/default/base.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <meta name="description" content="Searx - a privacy-respecting, hackable metasearch engine" />
+ <meta name="keywords" content="searx, search, search engine, metasearch, meta search" />
+ <meta name="viewport" content="width=device-width, maximum-scale=1.0, user-scalable=1" />
+ <title>{% block title %}{% endblock %}searx</title>
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}" type="text/css" media="screen" />
+ <link rel="shortcut icon" href="{{ url_for('static', filename='img/favicon.png') }}?v=2" />
+ {% block styles %}
+ {% endblock %}
+ {% block head %}
+ <link title="searx" type="application/opensearchdescription+xml" rel="search" href="{{ url_for('opensearch') }}"/>
+ {% endblock %}
+ <script type="text/javascript">
+ searx = {};
+ searx.autocompleter = {% if autocomplete %}true{% else %}false{% endif %};
+ </script>
+</head>
+<body>
+<div id="container">
+{% block content %}
+{% endblock %}
+{% if autocomplete %}
+<script src="{{ url_for('static', filename='js/mootools-core-1.4.5-min.js') }}" ></script>
+<script src="{{ url_for('static', filename='js/mootools-autocompleter-1.1.2-min.js') }}" ></script>
+{% endif %}
+<script src="{{ url_for('static', filename='js/searx.js') }}" ></script>
+</div>
+</body>
+</html>
diff --git a/searx/templates/default/categories.html b/searx/templates/default/categories.html
new file mode 100644
index 000000000..57e63c85d
--- /dev/null
+++ b/searx/templates/default/categories.html
@@ -0,0 +1,7 @@
+<div id="categories">
+{% for category in categories %}
+ <div class="checkbox_container">
+ <input type="checkbox" id="checkbox_{{ category|replace(' ', '_') }}" name="category_{{ category }}" {% if category in selected_categories %}checked="checked"{% endif %} /><label for="checkbox_{{ category|replace(' ', '_') }}">{{ _(category) }}</label>
+ </div>
+{% endfor %}
+</div>
diff --git a/searx/templates/default/github_ribbon.html b/searx/templates/default/github_ribbon.html
new file mode 100644
index 000000000..bdd9cf180
--- /dev/null
+++ b/searx/templates/default/github_ribbon.html
@@ -0,0 +1,3 @@
+<a href="https://github.com/asciimoo/searx" class="github">
+ <img style="position: absolute; top: 0; right: 0; border: 0;" src="{{ url_for('static', filename='img/github_ribbon.png') }}" alt="Fork me on GitHub" class="github"/>
+</a>
diff --git a/searx/templates/default/index.html b/searx/templates/default/index.html
new file mode 100644
index 000000000..b4f55608b
--- /dev/null
+++ b/searx/templates/default/index.html
@@ -0,0 +1,12 @@
+{% extends "default/base.html" %}
+{% block content %}
+{% include 'default/github_ribbon.html' %}
+<div class="center">
+ <div class="title"><h1>searx</h1></div>
+ {% include 'default/search.html' %}
+ <p class="top_margin">
+ <a href="{{ url_for('about') }}" class="hmarg">{{ _('about') }}</a>
+ <a href="{{ url_for('preferences') }}" class="hmarg">{{ _('preferences') }}</a>
+ </p>
+</div>
+{% endblock %}
diff --git a/searx/templates/default/opensearch.xml b/searx/templates/default/opensearch.xml
new file mode 100644
index 000000000..f39283f99
--- /dev/null
+++ b/searx/templates/default/opensearch.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
+ <ShortName>searx</ShortName>
+ <Description>Search searx</Description>
+ <InputEncoding>UTF-8</InputEncoding>
+ <LongName>searx metasearch</LongName>
+ {% if opensearch_method == 'get' %}
+ <Url type="text/html" method="get" template="{{ host }}search?q={searchTerms}"/>
+ {% if autocomplete %}
+ <Url type="application/x-suggestions+json" method="get" template="{{ host }}autocompleter">
+ <Param name="format" value="x-suggestions" />
+ <Param name="q" value="{searchTerms}" />
+ </Url>
+ {% endif %}
+ {% else %}
+ <Url type="text/html" method="post" template="{{ host }}">
+ <Param name="q" value="{searchTerms}" />
+ </Url>
+ {% if autocomplete %}
+ <!-- TODO, POST REQUEST doesn't work -->
+ <Url type="application/x-suggestions+json" method="get" template="{{ host }}autocompleter">
+ <Param name="format" value="x-suggestions" />
+ <Param name="q" value="{searchTerms}" />
+ </Url>
+ {% endif %}
+ {% endif %}
+</OpenSearchDescription>
diff --git a/searx/templates/default/opensearch_response_rss.xml b/searx/templates/default/opensearch_response_rss.xml
new file mode 100644
index 000000000..5673eb2e1
--- /dev/null
+++ b/searx/templates/default/opensearch_response_rss.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rss version="2.0"
+ xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"
+ xmlns:atom="http://www.w3.org/2005/Atom">
+ <channel>
+ <title>Searx search: {{ q }}</title>
+ <link>{{ base_url }}?q={{ q }}</link>
+ <description>Search results for "{{ q }}" - searx</description>
+ <opensearch:totalResults>{{ number_of_results }}</opensearch:totalResults>
+ <opensearch:startIndex>1</opensearch:startIndex>
+ <opensearch:itemsPerPage>{{ number_of_results }}</opensearch:itemsPerPage>
+ <atom:link rel="search" type="application/opensearchdescription+xml" href="{{ base_url }}opensearch.xml"/>
+ <opensearch:Query role="request" searchTerms="{{ q }}" startPage="1" />
+ {% for r in results %}
+ <item>
+ <title>{{ r.title }}</title>
+ <link>{{ r.url }}</link>
+ <description>{{ r.content }}</description>
+ {% if r.pubdate %}<pubDate>{{ r.pubdate }}</pubDate>{% endif %}
+ </item>
+ {% endfor %}
+ </channel>
+</rss>
diff --git a/searx/templates/default/preferences.html b/searx/templates/default/preferences.html
new file mode 100644
index 000000000..7d35de7c3
--- /dev/null
+++ b/searx/templates/default/preferences.html
@@ -0,0 +1,101 @@
+{% extends "default/base.html" %}
+{% block head %} {% endblock %}
+{% block content %}
+<div class="row">
+ <h2>{{ _('Preferences') }}</h2>
+
+ <form method="post" action="{{ url_for('preferences') }}" id="search_form">
+ <fieldset>
+ <legend>{{ _('Default categories') }}</legend>
+ <p>
+ {% include 'default/categories.html' %}
+ </p>
+ </fieldset>
+ <fieldset>
+ <legend>{{ _('Search language') }}</legend>
+ <p>
+ <select name='language'>
+ <option value="all" {% if current_language == 'all' %}selected="selected"{% endif %}>{{ _('Automatic') }}</option>
+ {% for lang_id,lang_name,country_name in language_codes %}
+ <option value="{{ lang_id }}" {% if lang_id == current_language %}selected="selected"{% endif %}>{{ lang_name }} ({{ country_name }}) - {{ lang_id }}</option>
+ {% endfor %}
+ </select>
+ </p>
+ </fieldset>
+ <fieldset>
+ <legend>{{ _('Interface language') }}</legend>
+ <p>
+ <select name='locale'>
+ {% for locale_id,locale_name in locales.items() %}
+ <option value="{{ locale_id }}" {% if locale_id == current_locale %}selected="selected"{% endif %}>{{ locale_name }}</option>
+ {% endfor %}
+ </select>
+ </p>
+ </fieldset>
+ <fieldset>
+ <legend>{{ _('Autocomplete') }}</legend>
+ <p>
+ <select name="autocomplete">
+ <option value=""> - </option>
+ {% for backend in autocomplete_backends %}
+ <option value="{{ backend }}" {% if backend == autocomplete %}selected="selected"{% endif %}>{{ backend }}</option>
+ {% endfor %}
+ </select>
+ </p>
+ </fieldset>
+ <fieldset>
+ <legend>{{ _('Method') }}</legend>
+ <p>
+ <select name='method'>
+ <option value="POST" {% if method == 'POST' %}selected="selected"{% endif %}>POST</option>
+ <option value="GET" {% if method == 'GET' %}selected="selected"{% endif %}>GET</option>
+ </select>
+ </p>
+ </fieldset>
+ <fieldset>
+ <legend>{{ _('Themes') }}</legend>
+ <p>
+ <select name="theme">
+ {% for name in themes %}
+ <option value="{{ name }}" {% if name == theme %}selected="selected"{% endif %}>{{ name }}</option>
+ {% endfor %}
+ </select>
+ </p>
+ </fieldset>
+ <fieldset>
+ <legend>{{ _('Currently used search engines') }}</legend>
+
+ <table>
+ <tr>
+ <th>{{ _('Engine name') }}</th>
+ <th>{{ _('Category') }}</th>
+ <th>{{ _('Allow') }} / {{ _('Block') }}</th>
+ </tr>
+ {% for (categ,search_engines) in categs %}
+ {% for search_engine in search_engines %}
+
+ {% if not search_engine.private %}
+ <tr>
+ <td>{{ search_engine.name }} ({{ shortcuts[search_engine.name] }})</td>
+ <td>{{ _(categ) }}</td>
+ <td class="engine_checkbox">
+ <input type="checkbox" id="engine_{{ categ }}_{{ search_engine.name|replace(' ', '_') }}" name="engine_{{ search_engine.name }}"{% if search_engine.name in blocked_engines %} checked="checked"{% endif %} />
+ <label class="allow" for="engine_{{ categ }}_{{ search_engine.name|replace(' ', '_') }}">{{ _('Allow') }}</label>
+ <label class="deny" for="engine_{{ categ }}_{{ search_engine.name|replace(' ', '_') }}">{{ _('Block') }}</label>
+ </td>
+ </tr>
+ {% endif %}
+ {% endfor %}
+ {% endfor %}
+ </table>
+ </fieldset>
+ <p class="small_font">{{ _('These settings are stored in your cookies, this allows us not to store this data about you.') }}
+ <br />
+ {{ _("These cookies serve your sole convenience, we don't use these cookies to track you.") }}
+ </p>
+
+ <input type="submit" value="{{ _('save') }}" />
+ <div class="right preferences_back"><a href="{{ url_for('index') }}">{{ _('back') }}</a></div>
+ </form>
+</div>
+{% endblock %}
diff --git a/searx/templates/default/result_templates/default.html b/searx/templates/default/result_templates/default.html
new file mode 100644
index 000000000..ac9b9b979
--- /dev/null
+++ b/searx/templates/default/result_templates/default.html
@@ -0,0 +1,13 @@
+<div class="result {{ result.class }}">
+
+ {% if result['favicon'] %}
+ <img width="14" height="14" class="favicon" src="static/{{theme}}/img/icon_{{result['favicon']}}.ico" />
+ {% endif %}
+
+ <div>
+ <h3 class="result_title"><a href="{{ result.url }}">{{ result.title|safe }}</a></h3>
+ <p class="url">{{ result.pretty_url }} <a class="cache_link" href="https://web.archive.org/web/{{ result.url }}">cached</a></p>
+ {% if result.publishedDate %}<p class="published_date">{{ result.publishedDate }}</p>{% endif %}
+ <p class="content">{% if result.content %}{{ result.content|safe }}<br />{% endif %}</p>
+ </div>
+</div>
diff --git a/searx/templates/default/result_templates/images.html b/searx/templates/default/result_templates/images.html
new file mode 100644
index 000000000..1f15ff2bb
--- /dev/null
+++ b/searx/templates/default/result_templates/images.html
@@ -0,0 +1,6 @@
+<div class="image_result">
+ <p>
+ <a href="{{ result.img_src }}"><img src="{{ result.img_src }}" title={{ result.title }}/></a>
+ <span class="url"><a href="{{ result.url }}" class="small_font">original context</a></span>
+ </p>
+</div>
diff --git a/searx/templates/default/result_templates/torrent.html b/searx/templates/default/result_templates/torrent.html
new file mode 100644
index 000000000..6c62793a5
--- /dev/null
+++ b/searx/templates/default/result_templates/torrent.html
@@ -0,0 +1,7 @@
+<div class="result torrent_result">
+ <h3 class="result_title"><a href="{{ result.url }}">{{ result.title|safe }}</a></h3>
+ {% if result.content %}<p class="content">{{ result.content|safe }}</p>{% endif %}
+ <p class="stats">Seed: {{ result.seed }}, Leech: {{ result.leech }}</p>
+ <p><a href="{{ result.magnetlink }}" class="magnetlink">magnet link</a></p>
+ <p class="url">{{ result.pretty_url }}</p>
+</div>
diff --git a/searx/templates/default/result_templates/videos.html b/searx/templates/default/result_templates/videos.html
new file mode 100644
index 000000000..8ceb0b180
--- /dev/null
+++ b/searx/templates/default/result_templates/videos.html
@@ -0,0 +1,12 @@
+<div class="result">
+ {% if result['favicon'] %}
+ <img width="14" height="14" class="favicon" src="static/{{theme}}/img/icon_{{result['favicon']}}.ico" />
+ {% endif %}
+
+ <p>
+ <h3 class="result_title"><a href="{{ result.url }}">{{ result.title|safe }}</a></h3>
+ {% if result.publishedDate %}<p class="published_date">{{ result.publishedDate }}</p>{% endif %}
+ <a href="{{ result.url }}"><img width="400px" src="{{ result.thumbnail }}" title={{ result.title }} alt=" {{ result.title }}"/></a>
+ <p class="url">{{ result.url }}</p>
+ </p>
+</div>
diff --git a/searx/templates/default/results.html b/searx/templates/default/results.html
new file mode 100644
index 000000000..d0b53b48a
--- /dev/null
+++ b/searx/templates/default/results.html
@@ -0,0 +1,79 @@
+{% extends "default/base.html" %}
+{% block title %}{{ q }} - {% endblock %}
+{% block content %}
+<div class="right"><a href="{{ url_for('preferences') }}" id="preferences"><span>preferences</span></a></div>
+<div class="small search center">
+ {% include 'default/search.html' %}
+</div>
+<div id="results">
+ <div id="sidebar">
+
+ <div id="search_url">
+ {{ _('Search URL') }}:
+ <input type="text" value="{{ base_url }}?q={{ q|urlencode }}&pageno={{ pageno }}{% if selected_categories %}&category_{{ selected_categories|join("&category_") }}{% endif %}" readonly="" />
+ </div>
+ <div id="apis">
+ {{ _('Download results') }}
+ {% for output_type in ('csv', 'json', 'rss') %}
+ <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}">
+ <div class="left">
+ <input type="hidden" name="q" value="{{ q }}" />
+ <input type="hidden" name="format" value="{{ output_type }}" />
+ {% for category in selected_categories %}
+ <input type="hidden" name="category_{{ category }}" value="1"/>
+ {% endfor %}
+ <input type="hidden" name="pageno" value="{{ pageno }}" />
+ <input type="submit" value="{{ output_type }}" />
+ </div>
+ </form>
+ {% endfor %}
+ </div>
+ </div>
+
+ {% if suggestions %}
+ <div id="suggestions"><span>{{ _('Suggestions') }}</span>
+ {% for suggestion in suggestions %}
+ <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}">
+ <input type="hidden" name="q" value="{{ suggestion }}">
+ <input type="submit" value="{{ suggestion }}" />
+ </form>
+ {% endfor %}
+ </div>
+ {% endif %}
+
+ {% for result in results %}
+ {% if result['template'] %}
+ {% include 'default/result_templates/'+result['template'] %}
+ {% else %}
+ {% include 'default/result_templates/default.html' %}
+ {% endif %}
+ {% endfor %}
+
+ {% if paging %}
+ <div id="pagination">
+ {% if pageno > 1 %}
+ <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}">
+ <div class="left">
+ <input type="hidden" name="q" value="{{ q }}" />
+ {% for category in selected_categories %}
+ <input type="hidden" name="category_{{ category }}" value="1"/>
+ {% endfor %}
+ <input type="hidden" name="pageno" value="{{ pageno-1 }}" />
+ <input type="submit" value="<< {{ _('previous page') }}" />
+ </div>
+ </form>
+ {% endif %}
+ <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}">
+ <div class="left">
+ {% for category in selected_categories %}
+ <input type="hidden" name="category_{{ category }}" value="1"/>
+ {% endfor %}
+ <input type="hidden" name="q" value="{{ q }}" />
+ <input type="hidden" name="pageno" value="{{ pageno+1 }}" />
+ <input type="submit" value="{{ _('next page') }} >>" />
+ </div>
+ </form>
+ </div>
+ {% endif %}
+</div>
+{% endblock %}
diff --git a/searx/templates/default/search.html b/searx/templates/default/search.html
new file mode 100644
index 000000000..8a9965582
--- /dev/null
+++ b/searx/templates/default/search.html
@@ -0,0 +1,7 @@
+<form method="{{ method or 'POST' }}" action="{{ url_for('index') }}" id="search_form">
+ <div id="search_wrapper">
+ <input type="text" placeholder="{{ _('Search for...') }}" id="q" class="q" name="q" tabindex="1" autocomplete="off" {% if q %}value="{{ q }}"{% endif %}/>
+ <input type="submit" value="search" id="search_submit" />
+ </div>
+ {% include 'default/categories.html' %}
+</form>
diff --git a/searx/templates/default/stats.html b/searx/templates/default/stats.html
new file mode 100644
index 000000000..70fe98ac7
--- /dev/null
+++ b/searx/templates/default/stats.html
@@ -0,0 +1,22 @@
+{% extends "default/base.html" %}
+{% block head %} {% endblock %}
+{% block content %}
+<h2>{{ _('Engine stats') }}</h2>
+
+{% for stat_name,stat_category in stats %}
+<div class="left">
+ <table>
+ <tr colspan="3">
+ <th>{{ stat_name }}</th>
+ </tr>
+ {% for engine in stat_category %}
+ <tr>
+ <td>{{ engine.name }}</td>
+ <td>{{ '%.02f'|format(engine.avg) }}</td>
+ <td class="percentage"><div style="width: {{ engine.percentage }}%">&nbsp;</div></td>
+ </tr>
+ {% endfor %}
+ </table>
+</div>
+{% endfor %}
+{% endblock %}
diff --git a/searx/tests/engines/__init__.py b/searx/tests/engines/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/searx/tests/engines/__init__.py
diff --git a/searx/tests/engines/test_dummy.py b/searx/tests/engines/test_dummy.py
new file mode 100644
index 000000000..9399beaaf
--- /dev/null
+++ b/searx/tests/engines/test_dummy.py
@@ -0,0 +1,26 @@
+from searx.engines import dummy
+from searx.testing import SearxTestCase
+
+
+class TestDummyEngine(SearxTestCase):
+
+ def test_request(self):
+ test_params = [
+ [1, 2, 3],
+ ['a'],
+ [],
+ 1
+ ]
+ for params in test_params:
+ self.assertEqual(dummy.request(None, params), params)
+
+ def test_response(self):
+ responses = [
+ None,
+ [],
+ True,
+ dict(),
+ tuple()
+ ]
+ for response in responses:
+ self.assertEqual(dummy.response(response), [])
diff --git a/searx/tests/engines/test_github.py b/searx/tests/engines/test_github.py
new file mode 100644
index 000000000..460be8c3d
--- /dev/null
+++ b/searx/tests/engines/test_github.py
@@ -0,0 +1,61 @@
+from collections import defaultdict
+import mock
+from searx.engines import github
+from searx.testing import SearxTestCase
+
+
+class TestGitHubEngine(SearxTestCase):
+
+ def test_request(self):
+ query = 'test_query'
+ params = github.request(query, defaultdict(dict))
+ self.assertTrue('url' in params)
+ self.assertTrue(query in params['url'])
+ self.assertTrue('github.com' in params['url'])
+ self.assertEqual(params['headers']['Accept'], github.accept_header)
+
+ def test_response(self):
+ self.assertRaises(AttributeError, github.response, None)
+ self.assertRaises(AttributeError, github.response, [])
+ self.assertRaises(AttributeError, github.response, '')
+ self.assertRaises(AttributeError, github.response, '[]')
+
+ response = mock.Mock(text='{}')
+ self.assertEqual(github.response(response), [])
+
+ response = mock.Mock(text='{"items": []}')
+ self.assertEqual(github.response(response), [])
+
+ json = """
+ {
+ "items": [
+ {
+ "name": "title",
+ "html_url": "url",
+ "description": ""
+ }
+ ]
+ }
+ """
+ response = mock.Mock(text=json)
+ results = github.response(response)
+ self.assertEqual(type(results), list)
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0]['title'], 'title')
+ self.assertEqual(results[0]['url'], 'url')
+ self.assertEqual(results[0]['content'], '')
+
+ json = """
+ {
+ "items": [
+ {
+ "name": "title",
+ "html_url": "url",
+ "description": "desc"
+ }
+ ]
+ }
+ """
+ response = mock.Mock(text=json)
+ results = github.response(response)
+ self.assertEqual(results[0]['content'], "desc")
diff --git a/searx/tests/test_engines.py b/searx/tests/test_engines.py
new file mode 100644
index 000000000..1ffdbe529
--- /dev/null
+++ b/searx/tests/test_engines.py
@@ -0,0 +1,2 @@
+from searx.tests.engines.test_dummy import * # noqa
+from searx.tests.engines.test_github import * # noqa
diff --git a/searx/tests/test_utils.py b/searx/tests/test_utils.py
new file mode 100644
index 000000000..817fd4372
--- /dev/null
+++ b/searx/tests/test_utils.py
@@ -0,0 +1,69 @@
+import mock
+from searx.testing import SearxTestCase
+from searx import utils
+
+
+class TestUtils(SearxTestCase):
+
+ def test_gen_useragent(self):
+ self.assertIsInstance(utils.gen_useragent(), str)
+ self.assertIsNotNone(utils.gen_useragent())
+ self.assertTrue(utils.gen_useragent().startswith('Mozilla'))
+
+ def test_highlight_content(self):
+ self.assertEqual(utils.highlight_content(0, None), None)
+ self.assertEqual(utils.highlight_content(None, None), None)
+ self.assertEqual(utils.highlight_content('', None), None)
+ self.assertEqual(utils.highlight_content(False, None), None)
+
+ contents = [
+ '<html></html>'
+ 'not<'
+ ]
+ for content in contents:
+ self.assertEqual(utils.highlight_content(content, None), content)
+
+ content = 'a'
+ query = 'test'
+ self.assertEqual(utils.highlight_content(content, query), content)
+ query = 'a test'
+ self.assertEqual(utils.highlight_content(content, query), content)
+
+
+class TestHTMLTextExtractor(SearxTestCase):
+
+ def setUp(self):
+ self.html_text_extractor = utils.HTMLTextExtractor()
+
+ def test__init__(self):
+ self.assertEqual(self.html_text_extractor.result, [])
+
+ def test_handle_charref(self):
+ self.html_text_extractor.handle_charref('xF')
+ self.assertIn(u'\x0f', self.html_text_extractor.result)
+ self.html_text_extractor.handle_charref('XF')
+ self.assertIn(u'\x0f', self.html_text_extractor.result)
+
+ self.html_text_extractor.handle_charref('97')
+ self.assertIn(u'a', self.html_text_extractor.result)
+
+ def test_handle_entityref(self):
+ entity = 'test'
+ self.html_text_extractor.handle_entityref(entity)
+ self.assertIn(entity, self.html_text_extractor.result)
+
+
+class TestUnicodeWriter(SearxTestCase):
+
+ def setUp(self):
+ self.unicode_writer = utils.UnicodeWriter(mock.MagicMock())
+
+ def test_write_row(self):
+ row = [1, 2, 3]
+ self.assertEqual(self.unicode_writer.writerow(row), None)
+
+ def test_write_rows(self):
+ self.unicode_writer.writerow = mock.MagicMock()
+ rows = [1, 2, 3]
+ self.unicode_writer.writerows(rows)
+ self.assertEqual(self.unicode_writer.writerow.call_count, len(rows))
diff --git a/searx/tests/test_webapp.py b/searx/tests/test_webapp.py
index bb608ab7c..9d1722eeb 100644
--- a/searx/tests/test_webapp.py
+++ b/searx/tests/test_webapp.py
@@ -39,7 +39,7 @@ class ViewsTestCase(SearxTestCase):
self.assertEqual(result.status_code, 200)
self.assertIn('<div class="title"><h1>searx</h1></div>', result.data)
- @patch('searx.webapp.do_search')
+ @patch('searx.search.Search.search')
def test_index_html(self, search):
search.return_value = (
self.test_results,
@@ -55,7 +55,7 @@ class ViewsTestCase(SearxTestCase):
result.data
)
- @patch('searx.webapp.do_search')
+ @patch('searx.search.Search.search')
def test_index_json(self, search):
search.return_value = (
self.test_results,
@@ -71,7 +71,7 @@ class ViewsTestCase(SearxTestCase):
self.assertEqual(
result_dict['results'][0]['url'], 'http://first.test.xyz')
- @patch('searx.webapp.do_search')
+ @patch('searx.search.Search.search')
def test_index_csv(self, search):
search.return_value = (
self.test_results,
@@ -86,7 +86,7 @@ class ViewsTestCase(SearxTestCase):
result.data
)
- @patch('searx.webapp.do_search')
+ @patch('searx.search.Search.search')
def test_index_rss(self, search):
search.return_value = (
self.test_results,
diff --git a/searx/utils.py b/searx/utils.py
index c6f3394c2..a9ece355a 100644
--- a/searx/utils.py
+++ b/searx/utils.py
@@ -1,13 +1,17 @@
-from HTMLParser import HTMLParser
#import htmlentitydefs
-import csv
from codecs import getincrementalencoder
+from HTMLParser import HTMLParser
+from random import choice
+
import cStringIO
+import csv
+import os
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_os = ('Windows NT 6.3; WOW64',
+ 'X11; Linux x86_64',
+ 'X11; Linux x86')
ua = "Mozilla/5.0 ({os}) Gecko/20100101 Firefox/{version}"
@@ -108,3 +112,17 @@ class UnicodeWriter:
def writerows(self, rows):
for row in rows:
self.writerow(row)
+
+
+def get_themes(root):
+ """Returns available themes list."""
+
+ static_path = os.path.join(root, 'static')
+ static_names = set(os.listdir(static_path))
+ templates_path = os.path.join(root, 'templates')
+ templates_names = set(os.listdir(templates_path))
+
+ themes = []
+ for name in static_names.intersection(templates_names):
+ themes += [name]
+ return static_path, templates_path, themes
diff --git a/searx/webapp.py b/searx/webapp.py
index 89d288e73..f66466b35 100644
--- a/searx/webapp.py
+++ b/searx/webapp.py
@@ -17,6 +17,10 @@ along with searx. If not, see < http://www.gnu.org/licenses/ >.
(C) 2013- by Adam Tauber, <asciimoo@gmail.com>
'''
+from gevent import monkey
+monkey.patch_all()
+
+
if __name__ == '__main__':
from sys import path
from os.path import realpath, dirname
@@ -35,19 +39,29 @@ from flask import (
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
+ categories, engines, get_engines_stats, engine_shortcuts
)
-from searx.utils import UnicodeWriter, highlight_content, html_to_text
+from searx.utils import (
+ UnicodeWriter, highlight_content, html_to_text, get_themes
+)
+from searx.https_rewrite import https_rules
from searx.languages import language_codes
from searx.search import Search
+from searx.query import Query
from searx.autocomplete import backends as autocomplete_backends
+static_path, templates_path, themes =\
+ get_themes(settings['themes_path']
+ if settings.get('themes_path')
+ else searx_dir)
+default_theme = settings['default_theme'] if \
+ settings.get('default_theme', None) else 'default'
+
app = Flask(
__name__,
- static_folder=os.path.join(searx_dir, 'static'),
- template_folder=os.path.join(searx_dir, 'templates')
+ static_folder=static_path,
+ template_folder=templates_path
)
app.secret_key = settings['server']['secret_key']
@@ -90,7 +104,32 @@ def get_base_url():
return hostname
-def render(template_name, **kwargs):
+def get_current_theme_name(override=None):
+ """Returns theme name.
+
+ Checks in this order:
+ 1. override
+ 2. cookies
+ 3. settings"""
+
+ if override and override in themes:
+ return override
+ theme_name = request.args.get('theme',
+ request.cookies.get('theme',
+ default_theme))
+ if theme_name not in themes:
+ theme_name = default_theme
+ return theme_name
+
+
+def url_for_theme(endpoint, override_theme=None, **values):
+ if endpoint == 'static' and values.get('filename', None):
+ theme_name = get_current_theme_name(override=override_theme)
+ values['filename'] = "{}/{}".format(theme_name, values['filename'])
+ return url_for(endpoint, **values)
+
+
+def render(template_name, override_theme=None, **kwargs):
blocked_engines = request.cookies.get('blocked_engines', '').split(',')
autocomplete = request.cookies.get('autocomplete')
@@ -113,19 +152,31 @@ def render(template_name, **kwargs):
if not 'selected_categories' in kwargs:
kwargs['selected_categories'] = []
+ for arg in request.args:
+ if arg.startswith('category_'):
+ c = arg.split('_', 1)[1]
+ if c in categories:
+ kwargs['selected_categories'].append(c)
+ if not 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 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)
+ # override url_for function in templates
+ kwargs['url_for'] = url_for_theme
+
+ kwargs['theme'] = get_current_theme_name(override=override_theme)
+
+ return render_template(
+ '{}/{}'.format(kwargs['theme'], template_name), **kwargs)
@app.route('/search', methods=['GET', 'POST'])
@@ -148,16 +199,23 @@ def index():
'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)
+ search.results, search.suggestions = search.search(request)
for result in search.results:
+
if not search.paging and engines[result['engine']].paging:
search.paging = True
+
+ if settings['server']['https_rewrite']\
+ and result['parsed_url'].scheme == 'http':
+
+ for http_regex, https_url in https_rules:
+ if http_regex.match(result['url']):
+ result['url'] = http_regex.sub(https_url, result['url'])
+ # TODO result['parsed_url'].scheme
+ break
+
+ # HTTPS rewrite
if search.request_data.get('format', 'html') == 'html':
if 'content' in result:
result['content'] = highlight_content(result['content'],
@@ -170,6 +228,7 @@ def index():
# 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)
@@ -232,7 +291,8 @@ def index():
paging=search.paging,
pageno=search.pageno,
base_url=get_base_url(),
- suggestions=search.suggestions
+ suggestions=search.suggestions,
+ theme=get_current_theme_name()
)
@@ -249,24 +309,46 @@ def autocompleter():
"""Return autocompleter results"""
request_data = {}
+ # select request method
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')
+ # set blocked engines
+ if request.cookies.get('blocked_engines'):
+ blocked_engines = request.cookies['blocked_engines'].split(',') # noqa
+ else:
+ blocked_engines = []
+
+ # parse query
+ query = Query(request_data.get('q', '').encode('utf-8'), blocked_engines)
+ query.parse_query()
- if not query:
+ # check if search query is set
+ if not query.getSearchQuery():
return
+ # run autocompleter
completer = autocomplete_backends.get(request.cookies.get('autocomplete'))
+ # check if valid autocompleter is selected
if not completer:
return
- results = completer(query)
+ # run autocompletion
+ raw_results = completer(query.getSearchQuery())
+
+ # parse results (write :language and !engine back to result string)
+ results = []
+ for result in raw_results:
+ result_query = query
+ result_query.changeSearchQuery(result)
+ # add parsed result
+ results.append(result_query.getFullQuery())
+
+ # return autocompleter results
if request_data.get('format') == 'x-suggestions':
return Response(json.dumps([query, results]),
mimetype='application/json')
@@ -290,7 +372,7 @@ def preferences():
if request.method == 'GET':
blocked_engines = request.cookies.get('blocked_engines', '').split(',')
- else:
+ else: # on save
selected_categories = []
locale = None
autocomplete = ''
@@ -315,6 +397,8 @@ def preferences():
engine_name = pd_name.replace('engine_', '', 1)
if engine_name in engines:
blocked_engines.append(engine_name)
+ elif pd_name == 'theme':
+ theme = pd if pd in themes else default_theme
resp = make_response(redirect(url_for('index')))
@@ -352,6 +436,9 @@ def preferences():
resp.set_cookie('method', method, max_age=cookie_max_age)
+ resp.set_cookie(
+ 'theme', theme, max_age=cookie_max_age)
+
return resp
return render('preferences.html',
locales=settings['locales'],
@@ -361,13 +448,14 @@ def preferences():
categs=categories.items(),
blocked_engines=blocked_engines,
autocomplete_backends=autocomplete_backends,
- shortcuts={y: x for x, y in engine_shortcuts.items()})
+ shortcuts={y: x for x, y in engine_shortcuts.items()},
+ themes=themes,
+ theme=get_current_theme_name())
@app.route('/stats', methods=['GET'])
def stats():
"""Render engine statistics page."""
- global categories
stats = get_engines_stats()
return render(
'stats.html',
@@ -404,15 +492,15 @@ def opensearch():
@app.route('/favicon.ico')
def favicon():
- return send_from_directory(os.path.join(app.root_path, 'static/img'),
+ return send_from_directory(os.path.join(app.root_path,
+ 'static',
+ get_current_theme_name(),
+ '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'],
@@ -420,5 +508,8 @@ def run():
)
+application = app
+
+
if __name__ == "__main__":
run()
diff --git a/setup.py b/setup.py
index 79f2acc42..7048fa22b 100644
--- a/setup.py
+++ b/setup.py
@@ -15,13 +15,18 @@ long_description = read('README.rst')
setup(
name='searx',
- version="0.3.0",
+ version="0.3.1",
description="A privacy-respecting, hackable metasearch engine",
long_description=long_description,
classifiers=[
+ "Development Status :: 4 - Beta",
"Programming Language :: Python",
+ "Topic :: Internet",
+ "Topic :: Internet :: WWW/HTTP :: HTTP Servers",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ 'License :: OSI Approved :: GNU Affero General Public License v3'
],
- keywords='meta search engine',
+ keywords='metasearch searchengine search web http',
author='Adam Tauber',
author_email='asciimoo@gmail.com',
url='https://github.com/asciimoo/searx',
@@ -61,11 +66,11 @@ setup(
'searx': [
'settings.yml',
'../README.rst',
- 'static/*/*',
+ 'static/*/*/*',
'translations/*/*/*',
- 'templates/*.xml',
- 'templates/*.html',
- 'templates/result_templates/*.html',
+ 'templates/*/*.xml',
+ 'templates/*/*.html',
+ 'templates/*/result_templates/*.html',
],
},
diff --git a/utils/standalone_search.py b/utils/standalone_search.py
new file mode 100644
index 000000000..7e9516f82
--- /dev/null
+++ b/utils/standalone_search.py
@@ -0,0 +1,36 @@
+from sys import argv, exit
+
+if not len(argv) > 1:
+ print('search query required')
+ exit(1)
+
+import requests
+from json import dumps
+from searx.engines import google
+from searx.search import default_request_params
+
+request_params = default_request_params()
+# Possible params
+# request_params['headers']['User-Agent'] = ''
+# request_params['category'] = ''
+# request_params['started'] = ''
+
+request_params['pageno'] = 1
+request_params['language'] = 'en_us'
+
+params = google.request(argv[1], request_params)
+
+request_args = dict(
+ headers=request_params['headers'],
+ cookies=request_params['cookies'],
+)
+
+if request_params['method'] == 'GET':
+ req = requests.get
+else:
+ req = requests.post
+ request_args['data'] = request_params['data']
+
+resp = req(request_params['url'], **request_args)
+
+print(dumps(google.response(resp)))