diff options
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 @@ -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 Binary files differnew file mode 100644 index 000000000..ff28f5fc1 --- /dev/null +++ b/searx/static/courgette/img/bg-body-index.jpg diff --git a/searx/static/img/favicon.png b/searx/static/courgette/img/favicon.png Binary files differindex cefbac496..cefbac496 100644 --- a/searx/static/img/favicon.png +++ b/searx/static/courgette/img/favicon.png diff --git a/searx/static/img/github_ribbon.png b/searx/static/courgette/img/github_ribbon.png Binary files differindex 146ef8a80..146ef8a80 100644 --- a/searx/static/img/github_ribbon.png +++ b/searx/static/courgette/img/github_ribbon.png diff --git a/searx/static/img/icon_github.ico b/searx/static/courgette/img/icon_github.ico Binary files differindex 133f0ca35..133f0ca35 100644 --- a/searx/static/img/icon_github.ico +++ b/searx/static/courgette/img/icon_github.ico diff --git a/searx/static/img/icon_soundcloud.ico b/searx/static/courgette/img/icon_soundcloud.ico Binary files differindex 4130bea1b..4130bea1b 100644 --- a/searx/static/img/icon_soundcloud.ico +++ b/searx/static/courgette/img/icon_soundcloud.ico diff --git a/searx/static/img/icon_stackoverflow.ico b/searx/static/courgette/img/icon_stackoverflow.ico Binary files differindex b2242bc6c..b2242bc6c 100644 --- a/searx/static/img/icon_stackoverflow.ico +++ b/searx/static/courgette/img/icon_stackoverflow.ico diff --git a/searx/static/img/icon_twitter.ico b/searx/static/courgette/img/icon_twitter.ico Binary files differindex b4a71699a..b4a71699a 100644 --- a/searx/static/img/icon_twitter.ico +++ b/searx/static/courgette/img/icon_twitter.ico diff --git a/searx/static/img/icon_vimeo.ico b/searx/static/courgette/img/icon_vimeo.ico Binary files differindex 4fe4336da..4fe4336da 100644 --- a/searx/static/img/icon_vimeo.ico +++ b/searx/static/courgette/img/icon_vimeo.ico diff --git a/searx/static/img/icon_wikipedia.ico b/searx/static/courgette/img/icon_wikipedia.ico Binary files differindex 911fa76f6..911fa76f6 100644 --- a/searx/static/img/icon_wikipedia.ico +++ b/searx/static/courgette/img/icon_wikipedia.ico diff --git a/searx/static/img/icon_youtube.ico b/searx/static/courgette/img/icon_youtube.ico Binary files differindex 977887dbb..977887dbb 100644 --- a/searx/static/img/icon_youtube.ico +++ b/searx/static/courgette/img/icon_youtube.ico diff --git a/searx/static/courgette/img/preference-icon.png b/searx/static/courgette/img/preference-icon.png Binary files differnew file mode 100644 index 000000000..039db04a8 --- /dev/null +++ b/searx/static/courgette/img/preference-icon.png diff --git a/searx/static/courgette/img/search-icon.png b/searx/static/courgette/img/search-icon.png Binary files differnew file mode 100644 index 000000000..52c267842 --- /dev/null +++ b/searx/static/courgette/img/search-icon.png diff --git a/searx/static/img/searx.png b/searx/static/courgette/img/searx.png Binary files differindex e162da502..e162da502 100644 --- a/searx/static/img/searx.png +++ b/searx/static/courgette/img/searx.png 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 Binary files differnew file mode 100644 index 000000000..cefbac496 --- /dev/null +++ b/searx/static/default/img/favicon.png diff --git a/searx/static/default/img/github_ribbon.png b/searx/static/default/img/github_ribbon.png Binary files differnew file mode 100644 index 000000000..146ef8a80 --- /dev/null +++ b/searx/static/default/img/github_ribbon.png diff --git a/searx/static/default/img/icon_github.ico b/searx/static/default/img/icon_github.ico Binary files differnew file mode 100644 index 000000000..133f0ca35 --- /dev/null +++ b/searx/static/default/img/icon_github.ico diff --git a/searx/static/default/img/icon_soundcloud.ico b/searx/static/default/img/icon_soundcloud.ico Binary files differnew file mode 100644 index 000000000..4130bea1b --- /dev/null +++ b/searx/static/default/img/icon_soundcloud.ico diff --git a/searx/static/default/img/icon_stackoverflow.ico b/searx/static/default/img/icon_stackoverflow.ico Binary files differnew file mode 100644 index 000000000..b2242bc6c --- /dev/null +++ b/searx/static/default/img/icon_stackoverflow.ico diff --git a/searx/static/default/img/icon_twitter.ico b/searx/static/default/img/icon_twitter.ico Binary files differnew file mode 100644 index 000000000..b4a71699a --- /dev/null +++ b/searx/static/default/img/icon_twitter.ico diff --git a/searx/static/default/img/icon_vimeo.ico b/searx/static/default/img/icon_vimeo.ico Binary files differnew file mode 100644 index 000000000..4fe4336da --- /dev/null +++ b/searx/static/default/img/icon_vimeo.ico diff --git a/searx/static/default/img/icon_wikipedia.ico b/searx/static/default/img/icon_wikipedia.ico Binary files differnew file mode 100644 index 000000000..911fa76f6 --- /dev/null +++ b/searx/static/default/img/icon_wikipedia.ico diff --git a/searx/static/default/img/icon_youtube.ico b/searx/static/default/img/icon_youtube.ico Binary files differnew file mode 100644 index 000000000..977887dbb --- /dev/null +++ b/searx/static/default/img/icon_youtube.ico diff --git a/searx/static/img/preference-icon.png b/searx/static/default/img/preference-icon.png Binary files differindex f74635788..f74635788 100644 --- a/searx/static/img/preference-icon.png +++ b/searx/static/default/img/preference-icon.png diff --git a/searx/static/img/search-icon.png b/searx/static/default/img/search-icon.png Binary files differindex 1222421b2..1222421b2 100644 --- a/searx/static/img/search-icon.png +++ b/searx/static/default/img/search-icon.png diff --git a/searx/static/default/img/searx.png b/searx/static/default/img/searx.png Binary files differnew file mode 100644 index 000000000..e162da502 --- /dev/null +++ b/searx/static/default/img/searx.png 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,"&").replace(/"/g,""");};Document.implement({newElement:function(e,g){if(g&&g.checked!=null){g.defaultChecked=g.checked; +}if(d&&g){e="<"+e;if(g.name){e+=' name="'+c(g.name)+'"';}if(g.type){e+=' type="'+c(g.type)+'"';}e+=">";delete g.name;delete g.type;}return this.id(this.createElement(e)).set(g); +}});})();(function(){Slick.uidOf(window);Slick.uidOf(document);Document.implement({newTextNode:function(e){return this.createTextNode(e);},getDocument:function(){return this; +},getWindow:function(){return this.window;},id:(function(){var e={string:function(E,D,l){E=Slick.find(l,"#"+E.replace(/(\W)/g,"\\$1"));return(E)?e.element(E,D):null; +},element:function(D,E){Slick.uidOf(D);if(!E&&!D.$family&&!(/^(?:object|embed)$/i).test(D.tagName)){var l=D.fireEvent;D._fireEvent=function(F,G){return l(F,G); +};Object.append(D,Element.Prototype);}return D;},object:function(D,E,l){if(D.toElement){return e.element(D.toElement(l),E);}return null;}};e.textnode=e.whitespace=e.window=e.document=function(l){return l; +};return function(D,F,E){if(D&&D.$family&&D.uniqueNumber){return D;}var l=typeOf(D);return(e[l])?e[l](D,F,E||document):null;};})()});if(window.$==null){Window.implement("$",function(e,l){return document.id(e,l,this.document); +});}Window.implement({getDocument:function(){return this.document;},getWindow:function(){return this;}});[Document,Element].invoke("implement",{getElements:function(e){return Slick.search(this,e,new Elements); +},getElement:function(e){return document.id(Slick.find(this,e));}});var m={contains:function(e){return Slick.contains(this,e);}};if(!document.contains){Document.implement(m); +}if(!document.createElement("div").contains){Element.implement(m);}var r=function(E,D){if(!E){return D;}E=Object.clone(Slick.parse(E));var l=E.expressions; +for(var e=l.length;e--;){l[e][0].combinator=D;}return E;};Object.forEach({getNext:"~",getPrevious:"!~",getParent:"!"},function(e,l){Element.implement(l,function(D){return this.getElement(r(D,e)); +});});Object.forEach({getAllNext:"~",getAllPrevious:"!~",getSiblings:"~~",getChildren:">",getParents:"!"},function(e,l){Element.implement(l,function(D){return this.getElements(r(D,e)); +});});Element.implement({getFirst:function(e){return document.id(Slick.search(this,r(e,">"))[0]);},getLast:function(e){return document.id(Slick.search(this,r(e,">")).getLast()); +},getWindow:function(){return this.ownerDocument.window;},getDocument:function(){return this.ownerDocument;},getElementById:function(e){return document.id(Slick.find(this,"#"+(""+e).replace(/(\W)/g,"\\$1"))); +},match:function(e){return !e||Slick.match(this,e);}});if(window.$$==null){Window.implement("$$",function(e){if(arguments.length==1){if(typeof e=="string"){return Slick.search(this.document,e,new Elements); +}else{if(Type.isEnumerable(e)){return new Elements(e);}}}return new Elements(arguments);});}var w={before:function(l,e){var D=e.parentNode;if(D){D.insertBefore(l,e); +}},after:function(l,e){var D=e.parentNode;if(D){D.insertBefore(l,e.nextSibling);}},bottom:function(l,e){e.appendChild(l);},top:function(l,e){e.insertBefore(l,e.firstChild); +}};w.inside=w.bottom;var j={},d={};var k={};Array.forEach(["type","value","defaultValue","accessKey","cellPadding","cellSpacing","colSpan","frameBorder","rowSpan","tabIndex","useMap"],function(e){k[e.toLowerCase()]=e; +});k.html="innerHTML";k.text=(document.createElement("div").textContent==null)?"innerText":"textContent";Object.forEach(k,function(l,e){d[e]=function(D,E){D[l]=E; +};j[e]=function(D){return D[l];};});var x=["compact","nowrap","ismap","declare","noshade","checked","disabled","readOnly","multiple","selected","noresize","defer","defaultChecked","autofocus","controls","autoplay","loop"]; +var h={};Array.forEach(x,function(e){var l=e.toLowerCase();h[l]=e;d[l]=function(D,E){D[e]=!!E;};j[l]=function(D){return !!D[e];};});Object.append(d,{"class":function(e,l){("className" in e)?e.className=(l||""):e.setAttribute("class",l); +},"for":function(e,l){("htmlFor" in e)?e.htmlFor=l:e.setAttribute("for",l);},style:function(e,l){(e.style)?e.style.cssText=l:e.setAttribute("style",l); +},value:function(e,l){e.value=(l!=null)?l:"";}});j["class"]=function(e){return("className" in e)?e.className||null:e.getAttribute("class");};var f=document.createElement("button"); +try{f.type="button";}catch(z){}if(f.type!="button"){d.type=function(e,l){e.setAttribute("type",l);};}f=null;var p=document.createElement("input");p.value="t"; +p.type="submit";if(p.value!="t"){d.type=function(l,e){var D=l.value;l.type=e;l.value=D;};}p=null;var q=(function(e){e.random="attribute";return(e.getAttribute("random")=="attribute"); +})(document.createElement("div"));Element.implement({setProperty:function(l,D){var E=d[l.toLowerCase()];if(E){E(this,D);}else{if(q){var e=this.retrieve("$attributeWhiteList",{}); +}if(D==null){this.removeAttribute(l);if(q){delete e[l];}}else{this.setAttribute(l,""+D);if(q){e[l]=true;}}}return this;},setProperties:function(e){for(var l in e){this.setProperty(l,e[l]); +}return this;},getProperty:function(F){var D=j[F.toLowerCase()];if(D){return D(this);}if(q){var l=this.getAttributeNode(F),E=this.retrieve("$attributeWhiteList",{}); +if(!l){return null;}if(l.expando&&!E[F]){var G=this.outerHTML;if(G.substr(0,G.search(/\/?['"]?>(?![^<]*<['"])/)).indexOf(F)<0){return null;}E[F]=true;}}var e=Slick.getAttribute(this,F); +return(!e&&!Slick.hasAttribute(this,F))?null:e;},getProperties:function(){var e=Array.from(arguments);return e.map(this.getProperty,this).associate(e); +},removeProperty:function(e){return this.setProperty(e,null);},removeProperties:function(){Array.each(arguments,this.removeProperty,this);return this;},set:function(D,l){var e=Element.Properties[D]; +(e&&e.set)?e.set.call(this,l):this.setProperty(D,l);}.overloadSetter(),get:function(l){var e=Element.Properties[l];return(e&&e.get)?e.get.apply(this):this.getProperty(l); +}.overloadGetter(),erase:function(l){var e=Element.Properties[l];(e&&e.erase)?e.erase.apply(this):this.removeProperty(l);return this;},hasClass:function(e){return this.className.clean().contains(e," "); +},addClass:function(e){if(!this.hasClass(e)){this.className=(this.className+" "+e).clean();}return this;},removeClass:function(e){this.className=this.className.replace(new RegExp("(^|\\s)"+e+"(?:\\s|$)"),"$1"); +return this;},toggleClass:function(e,l){if(l==null){l=!this.hasClass(e);}return(l)?this.addClass(e):this.removeClass(e);},adopt:function(){var E=this,e,G=Array.flatten(arguments),F=G.length; +if(F>1){E=e=document.createDocumentFragment();}for(var D=0;D<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 }}%"> </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() @@ -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))) |