This commit is contained in:
gabeszm 2024-12-06 18:21:39 +01:00
commit 0ea1783e86
164 changed files with 12819 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

103
Dockerfile Normal file
View file

@ -0,0 +1,103 @@
FROM python:3.12.6-alpine3.20 AS builder
RUN apk --update add \
build-base \
libxml2-dev \
libxslt-dev \
openssl-dev \
libffi-dev
COPY requirements.txt .
RUN pip install --upgrade pip
RUN pip install --prefix /install --no-warn-script-location --no-cache-dir -r requirements.txt
FROM python:3.12.6-alpine3.20
RUN apk add --update --no-cache tor curl openrc libstdc++
# git go //for obfs4proxy
# libcurl4-openssl-dev
RUN apk -U upgrade
# uncomment to build obfs4proxy
# RUN git clone https://gitlab.com/yawning/obfs4.git
# WORKDIR /obfs4
# RUN go build -o obfs4proxy/obfs4proxy ./obfs4proxy
# RUN cp ./obfs4proxy/obfs4proxy /usr/bin/obfs4proxy
ARG DOCKER_USER=whoogle
ARG DOCKER_USERID=927
ARG config_dir=/config
RUN mkdir -p $config_dir
RUN chmod a+w $config_dir
VOLUME $config_dir
ARG url_prefix=''
ARG username=''
ARG password=''
ARG proxyuser=''
ARG proxypass=''
ARG proxytype=''
ARG proxyloc=''
ARG whoogle_dotenv=''
ARG use_https=''
ARG whoogle_port=5000
ARG twitter_alt='farside.link/nitter'
ARG youtube_alt='farside.link/invidious'
ARG reddit_alt='farside.link/libreddit'
ARG medium_alt='farside.link/scribe'
ARG translate_alt='farside.link/lingva'
ARG imgur_alt='farside.link/rimgo'
ARG wikipedia_alt='farside.link/wikiless'
ARG imdb_alt='farside.link/libremdb'
ARG quora_alt='farside.link/quetre'
ARG so_alt='farside.link/anonymousoverflow'
ENV CONFIG_VOLUME=$config_dir \
WHOOGLE_URL_PREFIX=$url_prefix \
WHOOGLE_USER=$username \
WHOOGLE_PASS=$password \
WHOOGLE_PROXY_USER=$proxyuser \
WHOOGLE_PROXY_PASS=$proxypass \
WHOOGLE_PROXY_TYPE=$proxytype \
WHOOGLE_PROXY_LOC=$proxyloc \
WHOOGLE_DOTENV=$whoogle_dotenv \
HTTPS_ONLY=$use_https \
EXPOSE_PORT=$whoogle_port \
WHOOGLE_ALT_TW=$twitter_alt \
WHOOGLE_ALT_YT=$youtube_alt \
WHOOGLE_ALT_RD=$reddit_alt \
WHOOGLE_ALT_MD=$medium_alt \
WHOOGLE_ALT_TL=$translate_alt \
WHOOGLE_ALT_IMG=$imgur_alt \
WHOOGLE_ALT_WIKI=$wikipedia_alt \
WHOOGLE_ALT_IMDB=$imdb_alt \
WHOOGLE_ALT_QUORA=$quora_alt \
WHOOGLE_ALT_SO=$so_alt
WORKDIR /whoogle
COPY --from=builder /install /usr/local
COPY misc/tor/torrc /etc/tor/torrc
COPY misc/tor/start-tor.sh misc/tor/start-tor.sh
COPY app/ app/
COPY run whoogle.env* ./
# Create user/group to run as
RUN adduser -D -g $DOCKER_USERID -u $DOCKER_USERID $DOCKER_USER
# Fix ownership / permissions
RUN chown -R ${DOCKER_USER}:${DOCKER_USER} /whoogle /var/lib/tor
# Allow writing symlinks to build dir
RUN chown $DOCKER_USERID:$DOCKER_USERID app/static/build
USER $DOCKER_USER:$DOCKER_USER
EXPOSE $EXPOSE_PORT
HEALTHCHECK --interval=30s --timeout=5s \
CMD curl -f http://localhost:${EXPOSE_PORT}/healthz || exit 1
CMD misc/tor/start-tor.sh & ./run

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Ben Busby
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

6
MANIFEST.in Normal file
View file

@ -0,0 +1,6 @@
graft app/static
graft app/templates
graft app/misc
include requirements.txt
recursive-include test
global-exclude *.pyc

21
README.md Normal file
View file

@ -0,0 +1,21 @@
Hi ![](https://user-images.githubusercontent.com/18350557/176309783-0785949b-9127-417c-8b55-ab5a4333674e.gif)My name is GabeszM
===============================================================================================================================
Whoogle verzió: [![Latest Release](https://img.shields.io/github/v/release/benbusby/whoogle-search)](https://github.com/benbusby/shoogle/releases)
Licensz: [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
### Changelog
===============================================================================================================================
2024.11.06
- Frissítve a legújabb 0.9.1-es verzióra
### Skills ami nincs
<p align="left">
<a href="https://git-scm.com/" target="_blank" rel="noreferrer"><img src="https://raw.githubusercontent.com/danielcranney/readme-generator/main/public/icons/skills/git-colored.svg" width="36" height="36" alt="Git" /></a><a href="https://www.php.net/" target="_blank" rel="noreferrer"><img src="https://raw.githubusercontent.com/danielcranney/readme-generator/main/public/icons/skills/php-colored.svg" width="36" height="36" alt="PHP" /></a><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript" target="_blank" rel="noreferrer"><img src="https://raw.githubusercontent.com/danielcranney/readme-generator/main/public/icons/skills/javascript-colored.svg" width="36" height="36" alt="JavaScript" /></a><a href="https://code.visualstudio.com/" target="_blank" rel="noreferrer"><img src="https://raw.githubusercontent.com/danielcranney/readme-generator/main/public/icons/skills/visualstudiocode.svg" width="36" height="36" alt="VS Code" /></a><a href="https://www.sublimetext.com/index2" target="_blank" rel="noreferrer"><img src="https://raw.githubusercontent.com/danielcranney/readme-generator/main/public/icons/skills/sublimetext.svg" width="36" height="36" alt="Sublime Text" /></a><a href="https://developer.mozilla.org/en-US/docs/Glossary/HTML5" target="_blank" rel="noreferrer"><img src="https://raw.githubusercontent.com/danielcranney/readme-generator/main/public/icons/skills/html5-colored.svg" width="36" height="36" alt="HTML5" /></a><a href="https://www.w3.org/TR/CSS/#css" target="_blank" rel="noreferrer"><img src="https://raw.githubusercontent.com/danielcranney/readme-generator/main/public/icons/skills/css3-colored.svg" width="36" height="36" alt="CSS3" /></a><a href="https://www.figma.com/" target="_blank" rel="noreferrer"><img src="https://raw.githubusercontent.com/danielcranney/readme-generator/main/public/icons/skills/figma-colored.svg" width="36" height="36" alt="Figma" /></a><a href="https://www.docker.com/" target="_blank" rel="noreferrer"><img src="https://raw.githubusercontent.com/danielcranney/readme-generator/main/public/icons/skills/docker-colored.svg" width="36" height="36" alt="Docker" /></a><a href="https://apple.com" target="_blank" rel="noreferrer"><img src="https://raw.githubusercontent.com/danielcranney/readme-generator/main/public/icons/skills/macos-colored.svg" width="36" height="36" alt="MacOS" /></a>
</p>

BIN
app 2.zip Normal file

Binary file not shown.

194
app.json Normal file
View file

@ -0,0 +1,194 @@
{
"name": "Whoogle Search",
"description": "A lightweight, privacy-oriented, containerized Google search proxy for desktop/mobile that removes Javascript, AMP links, tracking, and ads/sponsored content",
"repository": "https://github.com/benbusby/whoogle-search",
"logo": "https://raw.githubusercontent.com/benbusby/whoogle-search/master/app/static/img/favicon/ms-icon-150x150.png",
"keywords": [
"search",
"metasearch",
"flask",
"docker",
"heroku",
"adblock",
"degoogle",
"privacy"
],
"stack": "container",
"env": {
"WHOOGLE_URL_PREFIX": {
"description": "The URL prefix to use for the whoogle instance (i.e. \"/whoogle\")",
"value": "",
"required": false
},
"WHOOGLE_USER": {
"description": "The username for basic auth. WHOOGLE_PASS must also be set if used. Leave empty to disable.",
"value": "",
"required": false
},
"WHOOGLE_PASS": {
"description": "The password for basic auth. WHOOGLE_USER must also be set if used. Leave empty to disable.",
"value": "",
"required": false
},
"WHOOGLE_PROXY_USER": {
"description": "The username of the proxy server. Leave empty to disable.",
"value": "",
"required": false
},
"WHOOGLE_PROXY_PASS": {
"description": "The password of the proxy server. Leave empty to disable.",
"value": "",
"required": false
},
"WHOOGLE_PROXY_TYPE": {
"description": "The type of the proxy server. For example \"socks5\". Leave empty to disable.",
"value": "",
"required": false
},
"WHOOGLE_PROXY_LOC": {
"description": "The location of the proxy server (host or ip). Leave empty to disable.",
"value": "",
"required": false
},
"WHOOGLE_ALT_TW": {
"description": "The site to use as a replacement for twitter.com when site alternatives are enabled in the config.",
"value": "farside.link/nitter",
"required": false
},
"WHOOGLE_ALT_YT": {
"description": "The site to use as a replacement for youtube.com when site alternatives are enabled in the config.",
"value": "farside.link/invidious",
"required": false
},
"WHOOGLE_ALT_RD": {
"description": "The site to use as a replacement for reddit.com when site alternatives are enabled in the config.",
"value": "farside.link/libreddit",
"required": false
},
"WHOOGLE_ALT_MD": {
"description": "The site to use as a replacement for medium.com when site alternatives are enabled in the config.",
"value": "farside.link/scribe",
"required": false
},
"WHOOGLE_ALT_TL": {
"description": "The Google Translate alternative to use for all searches following the 'translate ___' structure.",
"value": "farside.link/lingva",
"required": false
},
"WHOOGLE_ALT_IMG": {
"description": "The site to use as a replacement for imgur.com when site alternatives are enabled in the config.",
"value": "farside.link/rimgo",
"required": false
},
"WHOOGLE_ALT_WIKI": {
"description": "The site to use as a replacement for wikipedia.com when site alternatives are enabled in the config.",
"value": "farside.link/wikiless",
"required": false
},
"WHOOGLE_ALT_IMDB": {
"description": "The site to use as a replacement for imdb.com when site alternatives are enabled in the config.",
"value": "farside.link/libremdb",
"required": false
},
"WHOOGLE_ALT_QUORA": {
"description": "The site to use as a replacement for quora.com when site alternatives are enabled in the config.",
"value": "farside.link/quetre",
"required": false
},
"WHOOGLE_ALT_SO": {
"description": "The site to use as a replacement for stackoverflow.com when site alternatives are enabled in the config.",
"value": "farside.link/anonymousoverflow",
"required": false
},
"WHOOGLE_MINIMAL": {
"description": "Remove everything except basic result cards from all search queries (set to 1 or leave blank)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_COUNTRY": {
"description": "[CONFIG] The country to use for restricting search results (use values from https://raw.githubusercontent.com/benbusby/whoogle-search/develop/app/static/settings/countries.json)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_TIME_PERIOD" : {
"description": "[CONFIG] The time period to use for restricting search results",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_LANGUAGE": {
"description": "[CONFIG] The language to use for the interface (use values from https://raw.githubusercontent.com/benbusby/whoogle-search/develop/app/static/settings/languages.json)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_SEARCH_LANGUAGE": {
"description": "[CONFIG] The language to use for search results (use values from https://raw.githubusercontent.com/benbusby/whoogle-search/develop/app/static/settings/languages.json)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_DISABLE": {
"description": "[CONFIG] Disable ability for client to change config (set to 1 or leave blank)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_BLOCK": {
"description": "[CONFIG] Block websites from search results (comma-separated list)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_THEME": {
"description": "[CONFIG] Set theme to 'dark', 'light', or 'system'",
"value": "system",
"required": false
},
"WHOOGLE_CONFIG_SAFE": {
"description": "[CONFIG] Use safe mode for searches (set to 1 or leave blank)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_ALTS": {
"description": "[CONFIG] Use social media alternatives (set to 1 or leave blank)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_NEAR": {
"description": "[CONFIG] Restrict results to only those near a particular city",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_TOR": {
"description": "[CONFIG] Use Tor, if available (set to 1 or leave blank)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_NEW_TAB": {
"description": "[CONFIG] Always open results in new tab (set to 1 or leave blank)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_VIEW_IMAGE": {
"description": "[CONFIG] Enable View Image option (set to 1 or leave blank)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_GET_ONLY": {
"description": "[CONFIG] Search using GET requests only (set to 1 or leave blank)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_STYLE": {
"description": "[CONFIG] Custom CSS styling (paste in CSS or leave blank)",
"value": ":root { /* LIGHT THEME COLORS */ --whoogle-background: #d8dee9; --whoogle-accent: #2e3440; --whoogle-text: #3B4252; --whoogle-contrast-text: #eceff4; --whoogle-secondary-text: #70757a; --whoogle-result-bg: #fff; --whoogle-result-title: #4c566a; --whoogle-result-url: #81a1c1; --whoogle-result-visited: #a3be8c; /* DARK THEME COLORS */ --whoogle-dark-background: #222; --whoogle-dark-accent: #685e79; --whoogle-dark-text: #fff; --whoogle-dark-contrast-text: #000; --whoogle-dark-secondary-text: #bbb; --whoogle-dark-result-bg: #000; --whoogle-dark-result-title: #1967d2; --whoogle-dark-result-url: #4b11a8; --whoogle-dark-result-visited: #bbbbff; }",
"required": false
},
"WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED": {
"description": "[CONFIG] Encrypt preferences token, requires WHOOGLE_CONFIG_PREFERENCES_KEY to be set",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_PREFERENCES_KEY": {
"description": "[CONFIG] Key to encrypt preferences",
"value": "NEEDS_TO_BE_MODIFIED",
"required": false
}
}
}

BIN
app.zip Normal file

Binary file not shown.

BIN
app/.DS_Store vendored Normal file

Binary file not shown.

199
app/__init__.py Executable file
View file

@ -0,0 +1,199 @@
from app.filter import clean_query
from app.request import send_tor_signal
from app.utils.session import generate_key
from app.utils.bangs import gen_bangs_json, load_all_bangs
from app.utils.misc import gen_file_hash, read_config_bool
from base64 import b64encode
from bs4 import MarkupResemblesLocatorWarning
from datetime import datetime, timedelta
from dotenv import load_dotenv
from flask import Flask
import json
import logging.config
import os
from stem import Signal
import threading
import warnings
from werkzeug.middleware.proxy_fix import ProxyFix
from app.utils.misc import read_config_bool
from app.version import __version__
app = Flask(__name__, static_folder=os.path.dirname(
os.path.abspath(__file__)) + '/static')
app.wsgi_app = ProxyFix(app.wsgi_app)
# look for WHOOGLE_ENV, else look in parent directory
dot_env_path = os.getenv(
"WHOOGLE_DOTENV_PATH",
os.path.join(os.path.dirname(os.path.abspath(__file__)), "../whoogle.env"))
# Load .env file if enabled
if os.path.exists(dot_env_path):
load_dotenv(dot_env_path)
app.enc_key = generate_key()
if read_config_bool('HTTPS_ONLY'):
app.config['SESSION_COOKIE_NAME'] = '__Secure-session'
app.config['SESSION_COOKIE_SECURE'] = True
app.config['VERSION_NUMBER'] = __version__
app.config['APP_ROOT'] = os.getenv(
'APP_ROOT',
os.path.dirname(os.path.abspath(__file__)))
app.config['STATIC_FOLDER'] = os.getenv(
'STATIC_FOLDER',
os.path.join(app.config['APP_ROOT'], 'static'))
app.config['BUILD_FOLDER'] = os.path.join(
app.config['STATIC_FOLDER'], 'build')
app.config['CACHE_BUSTING_MAP'] = {}
app.config['LANGUAGES'] = json.load(open(
os.path.join(app.config['STATIC_FOLDER'], 'settings/languages.json'),
encoding='utf-8'))
app.config['COUNTRIES'] = json.load(open(
os.path.join(app.config['STATIC_FOLDER'], 'settings/countries.json'),
encoding='utf-8'))
app.config['TIME_PERIODS'] = json.load(open(
os.path.join(app.config['STATIC_FOLDER'], 'settings/time_periods.json'),
encoding='utf-8'))
app.config['TRANSLATIONS'] = json.load(open(
os.path.join(app.config['STATIC_FOLDER'], 'settings/translations.json'),
encoding='utf-8'))
app.config['THEMES'] = json.load(open(
os.path.join(app.config['STATIC_FOLDER'], 'settings/themes.json'),
encoding='utf-8'))
app.config['HEADER_TABS'] = json.load(open(
os.path.join(app.config['STATIC_FOLDER'], 'settings/header_tabs.json'),
encoding='utf-8'))
app.config['CONFIG_PATH'] = os.getenv(
'CONFIG_VOLUME',
os.path.join(app.config['STATIC_FOLDER'], 'config'))
app.config['DEFAULT_CONFIG'] = os.path.join(
app.config['CONFIG_PATH'],
'config.json')
app.config['CONFIG_DISABLE'] = read_config_bool('WHOOGLE_CONFIG_DISABLE')
app.config['SESSION_FILE_DIR'] = os.path.join(
app.config['CONFIG_PATH'],
'session')
app.config['MAX_SESSION_SIZE'] = 4000 # Sessions won't exceed 4KB
app.config['BANG_PATH'] = os.getenv(
'CONFIG_VOLUME',
os.path.join(app.config['STATIC_FOLDER'], 'bangs'))
app.config['BANG_FILE'] = os.path.join(
app.config['BANG_PATH'],
'bangs.json')
# Ensure all necessary directories exist
if not os.path.exists(app.config['CONFIG_PATH']):
os.makedirs(app.config['CONFIG_PATH'])
if not os.path.exists(app.config['SESSION_FILE_DIR']):
os.makedirs(app.config['SESSION_FILE_DIR'])
if not os.path.exists(app.config['BANG_PATH']):
os.makedirs(app.config['BANG_PATH'])
if not os.path.exists(app.config['BUILD_FOLDER']):
os.makedirs(app.config['BUILD_FOLDER'])
# Session values
app_key_path = os.path.join(app.config['CONFIG_PATH'], 'whoogle.key')
if os.path.exists(app_key_path):
try:
app.config['SECRET_KEY'] = open(app_key_path, 'r').read()
except PermissionError:
app.config['SECRET_KEY'] = str(b64encode(os.urandom(32)))
else:
app.config['SECRET_KEY'] = str(b64encode(os.urandom(32)))
with open(app_key_path, 'w') as key_file:
key_file.write(app.config['SECRET_KEY'])
key_file.close()
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365)
# NOTE: SESSION_COOKIE_SAMESITE must be set to 'lax' to allow the user's
# previous session to persist when accessing the instance from an external
# link. Setting this value to 'strict' causes Whoogle to revalidate a new
# session, and fail, resulting in cookies being disabled.
app.config['SESSION_COOKIE_SAMESITE'] = 'Strict'
# Config fields that are used to check for updates
app.config['RELEASES_URL'] = 'https://github.com/' \
'benbusby/whoogle-search/releases'
app.config['LAST_UPDATE_CHECK'] = datetime.now() - timedelta(hours=24)
app.config['HAS_UPDATE'] = ''
# The alternative to Google Translate is treated a bit differently than other
# social media site alternatives, in that it is used for any translation
# related searches.
translate_url = os.getenv('WHOOGLE_ALT_TL', 'https://farside.link/lingva')
if not translate_url.startswith('http'):
translate_url = 'https://' + translate_url
app.config['TRANSLATE_URL'] = translate_url
app.config['CSP'] = 'default-src \'none\';' \
'frame-src ' + translate_url + ';' \
'manifest-src \'self\';' \
'img-src \'self\' data:;' \
'style-src \'self\' \'unsafe-inline\';' \
'script-src \'self\';' \
'media-src \'self\';' \
'connect-src \'self\';'
# Generate DDG bang filter
generating_bangs = False
if not os.path.exists(app.config['BANG_FILE']):
generating_bangs = True
json.dump({}, open(app.config['BANG_FILE'], 'w'))
bangs_thread = threading.Thread(
target=gen_bangs_json,
args=(app.config['BANG_FILE'],))
bangs_thread.start()
# Build new mapping of static files for cache busting
cache_busting_dirs = ['css', 'js']
for cb_dir in cache_busting_dirs:
full_cb_dir = os.path.join(app.config['STATIC_FOLDER'], cb_dir)
for cb_file in os.listdir(full_cb_dir):
# Create hash from current file state
full_cb_path = os.path.join(full_cb_dir, cb_file)
cb_file_link = gen_file_hash(full_cb_dir, cb_file)
build_path = os.path.join(app.config['BUILD_FOLDER'], cb_file_link)
try:
os.symlink(full_cb_path, build_path)
except FileExistsError:
# Symlink hasn't changed, ignore
pass
# Create mapping for relative path urls
map_path = build_path.replace(app.config['APP_ROOT'], '')
if map_path.startswith('/'):
map_path = map_path[1:]
app.config['CACHE_BUSTING_MAP'][cb_file] = map_path
# Templating functions
app.jinja_env.globals.update(clean_query=clean_query)
app.jinja_env.globals.update(
cb_url=lambda f: app.config['CACHE_BUSTING_MAP'][f.lower()])
# Attempt to acquire tor identity, to determine if Tor config is available
send_tor_signal(Signal.HEARTBEAT)
# Suppress spurious warnings from BeautifulSoup
warnings.simplefilter('ignore', MarkupResemblesLocatorWarning)
from app import routes # noqa
# The gen_bangs_json function takes care of loading bangs, so skip it here if
# it's already being loaded
if not generating_bangs:
load_all_bangs(app.config['BANG_FILE'])
# Disable logging from imported modules
logging.config.dictConfig({
'version': 1,
'disable_existing_loggers': True,
})

3
app/__main__.py Executable file
View file

@ -0,0 +1,3 @@
from .routes import run_app
run_app()

BIN
app/__pycache__/.DS_Store vendored Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

790
app/filter.py Executable file
View file

@ -0,0 +1,790 @@
import cssutils
from bs4 import BeautifulSoup
from bs4.element import ResultSet, Tag
from cryptography.fernet import Fernet
from flask import render_template
import html
import urllib.parse as urlparse
from urllib.parse import parse_qs
import re
from app.models.g_classes import GClasses
from app.request import VALID_PARAMS, MAPS_URL
from app.utils.misc import get_abs_url, read_config_bool
from app.utils.results import (
BLANK_B64, GOOG_IMG, GOOG_STATIC, G_M_LOGO_URL, LOGO_URL, SITE_ALTS,
has_ad_content, filter_link_args, append_anon_view, get_site_alt,
)
from app.models.endpoint import Endpoint
from app.models.config import Config
MAPS_ARGS = ['q', 'daddr']
minimal_mode_sections = ['Top stories', 'Images', 'People also ask']
unsupported_g_pages = [
'google.com/aclk'
'*.googleapis.com'
'*.gstatic.com'
'*.google-analytics.com'
'adservice.google.com'
'support.google.com',
'accounts.google.com',
'policies.google.com',
'google.com/preferences',
'google.com/intl',
'advanced_search',
'tbm=shop',
'ageverification.google.co.kr'
]
unsupported_g_divs = [
'google.com/preferences?hl=',
'ageverification.google.co.kr'
'google.com/aclk?sa='
]
def extract_q(q_str: str, href: str) -> str:
"""Extracts the 'q' element from a result link. This is typically
either the link to a result's website, or a string.
Args:
q_str: The result link to parse
href: The full url to check for standalone 'q' elements first,
rather than parsing the whole query string and then checking.
Returns:
str: The 'q' element of the link, or an empty string
"""
return parse_qs(q_str, keep_blank_values=True)['q'][0] if ('&q=' in href or '?q=' in href) else ''
def build_map_url(href: str) -> str:
"""Tries to extract known args that explain the location in the url. If a
location is found, returns the default url with it. Otherwise, returns the
url unchanged.
Args:
href: The full url to check.
Returns:
str: The parsed url, or the url unchanged.
"""
# parse the url
parsed_url = parse_qs(href)
# iterate through the known parameters and try build the url
for param in MAPS_ARGS:
if param in parsed_url:
return MAPS_URL + "?q=" + parsed_url[param][0]
# query could not be extracted returning unchanged url
return href
def clean_query(query: str) -> str:
"""Strips the blocked site list from the query, if one is being
used.
Args:
query: The query string
Returns:
str: The query string without any "-site:..." filters
"""
return query[:query.find('-site:')] if '-site:' in query else query
def clean_css(css: str, page_url: str) -> str:
"""Removes all remote URLs from a CSS string.
Args:
css: The CSS string
Returns:
str: The filtered CSS, with URLs proxied through Whoogle
"""
sheet = cssutils.parseString(css)
urls = cssutils.getUrls(sheet)
for url in urls:
abs_url = get_abs_url(url, page_url)
if abs_url.startswith('data:'):
continue
css = css.replace(
url,
f'{Endpoint.element}?type=image/png&url={abs_url}'
)
return css
class Filter:
# Limit used for determining if a result is a "regular" result or a list
# type result (such as "people also asked", "related searches", etc)
RESULT_CHILD_LIMIT = 7
def __init__(
self,
user_key: str,
config: Config,
root_url='',
page_url='',
query='',
mobile=False) -> None:
self.soup = None
self.config = config
self.mobile = mobile
self.user_key = user_key
self.page_url = page_url
self.query = query
self.main_divs = ResultSet('')
self._elements = 0
self._av = set()
self.root_url = root_url[:-1] if root_url.endswith('/') else root_url
def __getitem__(self, name):
return getattr(self, name)
@property
def elements(self):
return self._elements
def encrypt_path(self, path, is_element=False) -> str:
# Encrypts path to avoid plaintext results in logs
if is_element:
# Element paths are encrypted separately from text, to allow key
# regeneration once all items have been served to the user
enc_path = Fernet(self.user_key).encrypt(path.encode()).decode()
self._elements += 1
return enc_path
return Fernet(self.user_key).encrypt(path.encode()).decode()
def clean(self, soup) -> BeautifulSoup:
self.soup = soup
self.main_divs = self.soup.find('div', {'id': 'main'})
self.remove_ads()
self.remove_block_titles()
self.remove_block_url()
self.collapse_sections()
self.update_css()
self.update_styling()
self.remove_block_tabs()
# self.main_divs is only populated for the main page of search results
# (i.e. not images/news/etc).
if self.main_divs:
for div in self.main_divs:
self.sanitize_div(div)
for img in [_ for _ in self.soup.find_all('img') if 'src' in _.attrs]:
self.update_element_src(img, 'image/png')
for audio in [_ for _ in self.soup.find_all('audio') if 'src' in _.attrs]:
self.update_element_src(audio, 'audio/mpeg')
audio['controls'] = ''
for link in self.soup.find_all('a', href=True):
self.update_link(link)
self.add_favicon(link)
if self.config.alts:
self.site_alt_swap()
input_form = self.soup.find('form')
if input_form is not None:
input_form['method'] = 'GET' if self.config.get_only else 'POST'
# Use a relative URI for submissions
input_form['action'] = 'search'
# Ensure no extra scripts passed through
for script in self.soup('script'):
script.decompose()
# Update default footer and header
footer = self.soup.find('footer')
if footer:
# Remove divs that have multiple links beyond just page navigation
[_.decompose() for _ in footer.find_all('div', recursive=False)
if len(_.find_all('a', href=True)) > 3]
for link in footer.find_all('a', href=True):
link['href'] = f'{link["href"]}&preferences={self.config.preferences}'
header = self.soup.find('header')
if header:
header.decompose()
self.remove_site_blocks(self.soup)
return self.soup
def sanitize_div(self, div) -> None:
"""Removes escaped script and iframe tags from results
Returns:
None (The soup object is modified directly)
"""
if not div:
return
for d in div.find_all('div', recursive=True):
d_text = d.find(text=True, recursive=False)
# Ensure we're working with tags that contain text content
if not d_text or not d.string:
continue
d.string = html.unescape(d_text)
div_soup = BeautifulSoup(d.string, 'html.parser')
# Remove all valid script or iframe tags in the div
for script in div_soup.find_all('script'):
script.decompose()
for iframe in div_soup.find_all('iframe'):
iframe.decompose()
d.string = str(div_soup)
def add_favicon(self, link) -> None:
"""Adds icons for each returned result, using the result site's favicon
Returns:
None (The soup object is modified directly)
"""
# Skip empty, parentless, or internal links
show_favicons = read_config_bool('WHOOGLE_SHOW_FAVICONS', True)
is_valid_link = link and link.parent and link['href'].startswith('http')
if not show_favicons or not is_valid_link:
return
parent = link.parent
is_result_div = False
# Check each parent to make sure that the div doesn't already have a
# favicon attached, and that the div is a result div
while parent:
p_cls = parent.attrs.get('class') or []
if 'has-favicon' in p_cls or GClasses.scroller_class in p_cls:
return
elif GClasses.result_class_a not in p_cls:
parent = parent.parent
else:
is_result_div = True
break
if not is_result_div:
return
# Construct the html for inserting the icon into the parent div
parsed = urlparse.urlparse(link['href'])
favicon = self.encrypt_path(
f'{parsed.scheme}://{parsed.netloc}/favicon.ico',
is_element=True)
src = f'{self.root_url}/{Endpoint.element}?url={favicon}' + \
'&type=image/x-icon'
html = f'<img class="site-favicon" src="{src}">'
favicon = BeautifulSoup(html, 'html.parser')
link.parent.insert(0, favicon)
# Update all parents to indicate that a favicon has been attached
parent = link.parent
while parent:
p_cls = parent.get('class') or []
p_cls.append('has-favicon')
parent['class'] = p_cls
parent = parent.parent
if GClasses.result_class_a in p_cls:
break
def remove_site_blocks(self, soup) -> None:
if not self.config.block or not soup.body:
return
search_string = ' '.join(['-site:' +
_ for _ in self.config.block.split(',')])
selected = soup.body.findAll(text=re.compile(search_string))
for result in selected:
result.string.replace_with(result.string.replace(
search_string, ''))
def remove_ads(self) -> None:
"""Removes ads found in the list of search result divs
Returns:
None (The soup object is modified directly)
"""
if not self.main_divs:
return
for div in [_ for _ in self.main_divs.find_all('div', recursive=True)]:
div_ads = [_ for _ in div.find_all('span', recursive=True)
if has_ad_content(_.text)]
_ = div.decompose() if len(div_ads) else None
def remove_block_titles(self) -> None:
if not self.main_divs or not self.config.block_title:
return
block_title = re.compile(self.config.block_title)
for div in [_ for _ in self.main_divs.find_all('div', recursive=True)]:
block_divs = [_ for _ in div.find_all('h3', recursive=True)
if block_title.search(_.text) is not None]
_ = div.decompose() if len(block_divs) else None
def remove_block_url(self) -> None:
if not self.main_divs or not self.config.block_url:
return
block_url = re.compile(self.config.block_url)
for div in [_ for _ in self.main_divs.find_all('div', recursive=True)]:
block_divs = [_ for _ in div.find_all('a', recursive=True)
if block_url.search(_.attrs['href']) is not None]
_ = div.decompose() if len(block_divs) else None
def remove_block_tabs(self) -> None:
if self.main_divs:
for div in self.main_divs.find_all(
'div',
attrs={'class': f'{GClasses.main_tbm_tab}'}
):
_ = div.decompose()
else:
# when in images tab
for div in self.soup.find_all(
'div',
attrs={'class': f'{GClasses.images_tbm_tab}'}
):
_ = div.decompose()
def collapse_sections(self) -> None:
"""Collapses long result sections ("people also asked", "related
searches", etc) into "details" elements
These sections are typically the only sections in the results page that
have more than ~5 child divs within a primary result div.
Returns:
None (The soup object is modified directly)
"""
minimal_mode = read_config_bool('WHOOGLE_MINIMAL')
def pull_child_divs(result_div: BeautifulSoup):
try:
return result_div.findChildren(
'div', recursive=False
)[0].findChildren(
'div', recursive=False)
except IndexError:
return []
if not self.main_divs:
return
#töörölni kell People also ask,
search_terms = ["People also search for", "Related searches", "Kapcsolódó keresések", "Mások ezeket keresték még"]
details_list = []
# Loop through results and check for the number of child divs in each
for result in self.main_divs.find_all():
result_children = pull_child_divs(result)
if minimal_mode:
if any(f">{x}</span" in str(s) for s in result_children
for x in minimal_mode_sections):
result.decompose()
continue
for s in result_children:
if ('Twitter ' in str(s)):
result.decompose()
continue
if len(result_children) < self.RESULT_CHILD_LIMIT:
continue
else:
if len(result_children) < self.RESULT_CHILD_LIMIT:
continue
# Find and decompose the first element with an inner HTML text val.
# This typically extracts the title of the section (i.e. "Related
# Searches", "People also ask", etc)
# If there are more than one child tags with text
# parenthesize the rest except the first
label = 'Collapsed Results'
subtitle = None
for elem in result_children:
if elem.text:
content = list(elem.strings)
label = content[0]
if len(content) > 1:
subtitle = '<span> (' + \
''.join(content[1:]) + ')</span>'
elem.decompose()
break
# Determine the class based on the label content
if any(term in label for term in search_terms):
details_class = 'search-recommendations'
details_attrs = {'class': details_class, 'open': 'true'}
else:
details_class = 'other-results'
details_attrs = {'class': details_class}
# Create the new details element to wrap around the result's
# first parent
parent = None
idx = 0
while not parent and idx < len(result_children):
parent = result_children[idx].parent
idx += 1
details = BeautifulSoup(features='html.parser').new_tag('details', attrs=details_attrs)
summary = BeautifulSoup(features='html.parser').new_tag('summary', attrs={'class': "summary_div"})
summary.string = label
if subtitle:
soup = BeautifulSoup(subtitle, 'html.parser')
summary.append(soup)
details.append(summary)
if parent and not minimal_mode:
parent.wrap(details)
elif parent and minimal_mode:
# Remove parent element from document if "minimal mode" is
# enabled
parent.decompose()
for details in details_list:
self.main_divs.append(details)
def update_element_src(self, element: Tag, mime: str, attr='src') -> None:
"""Encrypts the original src of an element and rewrites the element src
to use the "/element?src=" pass-through.
Returns:
None (The soup element is modified directly)
"""
src = element[attr].split(' ')[0]
if src.startswith('//'):
src = 'https:' + src
elif src.startswith('data:'):
return
if src.startswith(LOGO_URL):
# Re-brand with Whoogle logo
element.replace_with(BeautifulSoup(
render_template('logo.html'),
features='html.parser'))
return
elif src.startswith(G_M_LOGO_URL):
# Re-brand with single-letter Whoogle logo
element['src'] = 'static/img/favicon/apple-icon.png'
element.parent['href'] = 'home'
return
elif src.startswith(GOOG_IMG) or GOOG_STATIC in src:
element['src'] = BLANK_B64
return
element[attr] = f'{self.root_url}/{Endpoint.element}?url=' + (
self.encrypt_path(
src,
is_element=True
) + '&type=' + urlparse.quote(mime)
)
def update_css(self) -> None:
"""Updates URLs used in inline styles to be proxied by Whoogle
using the /element endpoint.
Returns:
None (The soup element is modified directly)
"""
# Filter all <style> tags
for style in self.soup.find_all('style'):
style.string = clean_css(style.string, self.page_url)
# TODO: Convert remote stylesheets to style tags and proxy all
# remote requests
# for link in soup.find_all('link', attrs={'rel': 'stylesheet'}):
# print(link)
def update_styling(self) -> None:
# Update CSS classes for result divs
soup = GClasses.replace_css_classes(self.soup)
# Remove unnecessary button(s)
for button in self.soup.find_all('button'):
button.decompose()
# Remove svg logos
for svg in self.soup.find_all('svg'):
svg.decompose()
# Update logo
logo = self.soup.find('a', {'class': 'l'})
if logo and self.mobile:
logo['style'] = ('display:flex; justify-content:center; '
'align-items:center; color:#685e79; '
'font-size:18px; ')
# Fix search bar length on mobile
""" try:
search_bar = self.soup.find('header').find('form').find('div')
search_bar['style'] = 'width: 100%;'
except AttributeError:
pass
# Fix body max width on images tab
style = self.soup.find('style')
div = self.soup.find('div', attrs={
'class': f'{GClasses.images_tbm_tab}'})
if style and div and not self.mobile:
css = style.string
css_html_tag = (
'html{'
'font-family: Roboto, Helvetica Neue, Arial, sans-serif;'
'font-size: 14px;'
'line-height: 20px;'
'text-size-adjust: 100%;'
'word-wrap: break-word;'
'}'
)
css = f"{css_html_tag}{css}"
css = re.sub('body{(.*?)}',
'body{padding:0 8px;margin:0 auto;max-width:900px;}',
css)
style.string = css """
def update_link(self, link: Tag) -> None:
"""Update internal link paths with encrypted path, otherwise remove
unnecessary redirects and/or marketing params from the url
Args:
link: A bs4 Tag element to inspect and update
Returns:
None (the tag is updated directly)
"""
parsed_link = urlparse.urlparse(link['href'])
if '/url?q=' in link['href']:
link_netloc = extract_q(parsed_link.query, link['href'])
else:
link_netloc = parsed_link.netloc
# Remove any elements that direct to unsupported Google pages
if any(url in link_netloc for url in unsupported_g_pages):
# FIXME: The "Shopping" tab requires further filtering (see #136)
# Temporarily removing all links to that tab for now.
# Replaces the /url google unsupported link to the direct url
link['href'] = link_netloc
parent = link.parent
if any(divlink in link_netloc for divlink in unsupported_g_divs):
# Handle case where a search is performed in a different
# language than what is configured. This usually returns a
# div with the same classes as normal search results, but with
# a link to configure language preferences through Google.
# Since we want all language config done through Whoogle, we
# can safely decompose this element.
while parent:
p_cls = parent.attrs.get('class') or []
if f'{GClasses.result_class_a}' in p_cls:
parent.decompose()
break
parent = parent.parent
else:
# Remove cases where google links appear in the footer
while parent:
p_cls = parent.attrs.get('class') or []
if parent.name == 'footer' or f'{GClasses.footer}' in p_cls:
link.decompose()
parent = parent.parent
if link.decomposed:
return
# Replace href with only the intended destination (no "utm" type tags)
href = link['href'].replace('https://www.google.com', '')
result_link = urlparse.urlparse(href)
q = extract_q(result_link.query, href)
if q.startswith('/') and q not in self.query and 'spell=1' not in href:
# Internal google links (i.e. mail, maps, etc) should still
# be forwarded to Google
link['href'] = 'https://google.com' + q
elif q.startswith('https://accounts.google.com'):
# Remove Sign-in link
link.decompose()
return
elif '/search?q=' in href:
# "li:1" implies the query should be interpreted verbatim,
# which is accomplished by wrapping the query in double quotes
if 'li:1' in href:
q = '"' + q + '"'
new_search = 'search?q=' + self.encrypt_path(q)
query_params = parse_qs(urlparse.urlparse(href).query)
for param in VALID_PARAMS:
if param not in query_params:
continue
param_val = query_params[param][0]
new_search += '&' + param + '=' + param_val
link['href'] = new_search
elif 'url?q=' in href:
# Strip unneeded arguments
link['href'] = filter_link_args(q)
# Add alternate viewing options for results,
# if the result doesn't already have an AV link
netloc = urlparse.urlparse(link['href']).netloc
if self.config.anon_view and netloc not in self._av:
self._av.add(netloc)
append_anon_view(link, self.config)
else:
if href.startswith(MAPS_URL):
# Maps links don't work if a site filter is applied
link['href'] = build_map_url(link['href'])
elif (href.startswith('/?') or href.startswith('/search?') or
href.startswith('/imgres?')):
# make sure that tags can be clicked as relative URLs
link['href'] = href[1:]
elif href.startswith('/intl/'):
# do nothing, keep original URL for ToS
pass
elif href.startswith('/preferences'):
# there is no config specific URL, remove this
link.decompose()
return
else:
link['href'] = href
if self.config.new_tab and (
link["href"].startswith("http")
or link["href"].startswith("imgres?")
):
link["target"] = "_blank"
def site_alt_swap(self) -> None:
"""Replaces link locations and page elements if "alts" config
is enabled
"""
for site, alt in SITE_ALTS.items():
if site != "medium.com" and alt != "":
# Ignore medium.com replacements since these are handled
# specifically in the link description replacement, and medium
# results are never given their own "card" result where this
# replacement would make sense.
# Also ignore if the alt is empty, since this is used to indicate
# that the alt is not enabled.
for div in self.soup.find_all('div', text=re.compile(site)):
# Use the number of words in the div string to determine if the
# string is a result description (shouldn't replace domains used
# in desc text).
if len(div.string.split(' ')) == 1:
div.string = div.string.replace(site, alt)
for link in self.soup.find_all('a', href=True):
# Search and replace all link descriptions
# with alternative location
link['href'] = get_site_alt(link['href'])
link_desc = link.find_all(
text=re.compile('|'.join(SITE_ALTS.keys())))
if len(link_desc) == 0:
continue
# Replace link description
link_desc = link_desc[0]
if site not in link_desc or not alt:
continue
new_desc = BeautifulSoup(features='html.parser').new_tag('div')
link_str = str(link_desc)
# Medium links should be handled differently, since 'medium.com'
# is a common substring of domain names, but shouldn't be
# replaced (i.e. 'philomedium.com' should stay as it is).
if 'medium.com' in link_str:
if link_str.startswith('medium.com') or '.medium.com' in link_str:
link_str = SITE_ALTS['medium.com'] + link_str[
link_str.find('medium.com') + len('medium.com'):]
new_desc.string = link_str
else:
new_desc.string = link_str.replace(site, alt)
link_desc.replace_with(new_desc)
def view_image(self, soup) -> BeautifulSoup:
"""Replaces the soup with a new one that handles mobile results and
adds the link of the image full res to the results.
Args:
soup: A BeautifulSoup object containing the image mobile results.
Returns:
BeautifulSoup: The new BeautifulSoup object
"""
# get some tags that are unchanged between mobile and pc versions
cor_suggested = soup.find_all(class_="By0U9")
next_pages = soup.find(class_= "uZgmoc")
results = []
# find results div
results_div = soup.find('div', attrs={'class': "nQvrDb"})
# find all the results (if any)
results_all = []
if results_div:
results_all = results_div.find_all('div', attrs={'class': "lIMUZd"})
for item in results_all:
urls = item.find('a')['href'].split('&imgrefurl=')
# Skip urls that are not two-element lists
if len(urls) != 2:
continue
img_url = urlparse.unquote(urls[0].replace(
f'/{Endpoint.imgres}?imgurl=', ''))
try:
# Try to strip out only the necessary part of the web page link
web_page = urlparse.unquote(urls[1].split('&')[0])
except IndexError:
web_page = urlparse.unquote(urls[1])
# Construct favicon URL from the webpage domain
parsed = urlparse.urlparse(web_page)
favicon_url = f'{parsed.scheme}://{parsed.netloc}/favicon.ico'
img_tbn = urlparse.unquote(item.find('a').find('img')['src'])
results.append({
'domain': urlparse.urlparse(web_page).netloc,
'img_url': img_url,
'web_page': web_page,
'img_tbn': img_tbn,
'favicon': favicon_url
})
# Create a new BeautifulSoup object with the template
soup = BeautifulSoup(render_template('imageresults.html',
length=len(results),
results=results,
view_label="Kép megtekintése"),
features='html.parser')
# replace correction suggested by google object if exists
if len(cor_suggested):
soup.find_all(class_= "By0U9"
)[0].replaceWith(cor_suggested[0])
# replace next page object at the bottom of the page
soup.find_all(class_= "uZgmoc")[0].replaceWith(next_pages)
return soup

BIN
app/models/.DS_Store vendored Executable file

Binary file not shown.

0
app/models/__init__.py Executable file
View file

BIN
app/models/__pycache__/.DS_Store vendored Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

267
app/models/config.py Executable file
View file

@ -0,0 +1,267 @@
from inspect import Attribute
from typing import Optional
from app.utils.misc import read_config_bool
from flask import current_app
import os
from base64 import urlsafe_b64encode, urlsafe_b64decode
from cryptography.fernet import Fernet
import hashlib
import brotli
import logging
import json
import cssutils
from cssutils.css.cssstylesheet import CSSStyleSheet
from cssutils.css.cssstylerule import CSSStyleRule
# removes warnings from cssutils
cssutils.log.setLevel(logging.CRITICAL)
def get_rule_for_selector(stylesheet: CSSStyleSheet,
selector: str) -> Optional[CSSStyleRule]:
"""Search for a rule that matches a given selector in a stylesheet.
Args:
stylesheet (CSSStyleSheet) -- the stylesheet to search
selector (str) -- the selector to search for
Returns:
Optional[CSSStyleRule] -- the rule that matches the selector or None
"""
for rule in stylesheet.cssRules:
if hasattr(rule, "selectorText") and selector == rule.selectorText:
return rule
return None
class Config:
def __init__(self, **kwargs):
app_config = current_app.config
self.url = os.getenv('WHOOGLE_CONFIG_URL', '')
self.lang_search = os.getenv('WHOOGLE_CONFIG_SEARCH_LANGUAGE', '')
self.lang_interface = os.getenv('WHOOGLE_CONFIG_LANGUAGE', default='lang_hu')
self.style_modified = os.getenv(
'WHOOGLE_CONFIG_STYLE', '')
self.block = os.getenv('WHOOGLE_CONFIG_BLOCK', '')
self.block_title = os.getenv('WHOOGLE_CONFIG_BLOCK_TITLE', '')
self.block_url = os.getenv('WHOOGLE_CONFIG_BLOCK_URL', '')
self.country = os.getenv('WHOOGLE_CONFIG_COUNTRY', '')
self.tbs = os.getenv('WHOOGLE_CONFIG_TIME_PERIOD', '')
self.theme = os.getenv('WHOOGLE_CONFIG_THEME', 'dark')
self.safe = read_config_bool('WHOOGLE_CONFIG_SAFE', default=True)
self.dark = read_config_bool('WHOOGLE_CONFIG_DARK') # deprecated
self.alts = read_config_bool('WHOOGLE_CONFIG_ALTS')
self.nojs = read_config_bool('WHOOGLE_CONFIG_NOJS')
self.tor = read_config_bool('WHOOGLE_CONFIG_TOR')
self.near = os.getenv('WHOOGLE_CONFIG_NEAR', '')
self.new_tab = read_config_bool('WHOOGLE_CONFIG_NEW_TAB')
self.view_image = read_config_bool('WHOOGLE_CONFIG_VIEW_IMAGE', default=True)
self.get_only = read_config_bool('WHOOGLE_CONFIG_GET_ONLY')
self.anon_view = read_config_bool('WHOOGLE_CONFIG_ANON_VIEW')
self.preferences_encrypted = read_config_bool('WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED')
self.preferences_key = os.getenv('WHOOGLE_CONFIG_PREFERENCES_KEY', '')
self.accept_language = False
self.safe_keys = [
'lang_search',
'lang_interface',
'country',
'theme',
'alts',
'new_tab',
'view_image',
'block',
'safe',
'nojs',
'anon_view',
'preferences_encrypted',
'tbs'
]
# Skip setting custom config if there isn't one
if kwargs:
mutable_attrs = self.get_mutable_attrs()
for attr in mutable_attrs:
if attr in kwargs.keys():
setattr(self, attr, kwargs[attr])
elif attr not in kwargs.keys() and mutable_attrs[attr] == bool:
setattr(self, attr, False)
def __getitem__(self, name):
return getattr(self, name)
def __setitem__(self, name, value):
return setattr(self, name, value)
def __delitem__(self, name):
return delattr(self, name)
def __contains__(self, name):
return hasattr(self, name)
def get_mutable_attrs(self):
return {name: type(attr) for name, attr in self.__dict__.items()
if not name.startswith("__")
and (type(attr) is bool or type(attr) is str)}
def get_attrs(self):
return {name: attr for name, attr in self.__dict__.items()
if not name.startswith("__")
and (type(attr) is bool or type(attr) is str)}
@property
def style(self) -> str:
"""Returns the default style updated with specified modifications.
Returns:
str -- the new style
"""
style_sheet = cssutils.parseString(
open(os.path.join(current_app.config['STATIC_FOLDER'],
'css/variables.css')).read()
)
modified_sheet = cssutils.parseString(self.style_modified)
for rule in modified_sheet:
rule_default = get_rule_for_selector(style_sheet,
rule.selectorText)
# if modified rule is in default stylesheet, update it
if rule_default is not None:
# TODO: update this in a smarter way to handle :root better
# for now if we change a varialbe in :root all other default
# variables need to be also present
rule_default.style = rule.style
# else add the new rule to the default stylesheet
else:
style_sheet.add(rule)
return str(style_sheet.cssText, 'utf-8')
@property
def preferences(self) -> str:
# if encryption key is not set will uncheck preferences encryption
if self.preferences_encrypted:
self.preferences_encrypted = bool(self.preferences_key)
# add a tag for visibility if preferences token startswith 'e' it means
# the token is encrypted, 'u' means the token is unencrypted and can be
# used by other whoogle instances
encrypted_flag = "e" if self.preferences_encrypted else 'u'
preferences_digest = self._encode_preferences()
return f"{encrypted_flag}{preferences_digest}"
def is_safe_key(self, key) -> bool:
"""Establishes a group of config options that are safe to set
in the url.
Args:
key (str) -- the key to check against
Returns:
bool -- True/False depending on if the key is in the "safe"
array
"""
return key in self.safe_keys
def get_localization_lang(self):
"""Returns the correct language to use for localization, but falls
back to english if not set.
Returns:
str -- the localization language string
"""
if (self.lang_interface and
self.lang_interface in current_app.config['TRANSLATIONS']):
return self.lang_interface
return 'lang_en'
def from_params(self, params) -> 'Config':
"""Modify user config with search parameters. This is primarily
used for specifying configuration on a search-by-search basis on
public instances.
Args:
params -- the url arguments (can be any deemed safe by is_safe())
Returns:
Config -- a modified config object
"""
if 'preferences' in params:
params_new = self._decode_preferences(params['preferences'])
# if preferences leads to an empty dictionary it means preferences
# parameter was not decrypted successfully
if len(params_new):
params = params_new
for param_key in params.keys():
if not self.is_safe_key(param_key):
continue
param_val = params.get(param_key)
if param_val == 'off':
param_val = False
elif isinstance(param_val, str):
if param_val.isdigit():
param_val = int(param_val)
self[param_key] = param_val
return self
def to_params(self, keys: list = []) -> str:
"""Generates a set of safe params for using in Whoogle URLs
Args:
keys (list) -- optional list of keys of URL parameters
Returns:
str -- a set of URL parameters
"""
if not len(keys):
keys = self.safe_keys
param_str = ''
for safe_key in keys:
if not self[safe_key]:
continue
param_str = param_str + f'&{safe_key}={self[safe_key]}'
return param_str
def _get_fernet_key(self, password: str) -> bytes:
hash_object = hashlib.md5(password.encode())
key = urlsafe_b64encode(hash_object.hexdigest().encode())
return key
def _encode_preferences(self) -> str:
preferences_json = json.dumps(self.get_attrs()).encode()
compressed_preferences = brotli.compress(preferences_json)
if self.preferences_encrypted and self.preferences_key:
key = self._get_fernet_key(self.preferences_key)
encrypted_preferences = Fernet(key).encrypt(compressed_preferences)
compressed_preferences = brotli.compress(encrypted_preferences)
return urlsafe_b64encode(compressed_preferences).decode()
def _decode_preferences(self, preferences: str) -> dict:
mode = preferences[0]
preferences = preferences[1:]
try:
decoded_data = brotli.decompress(urlsafe_b64decode(preferences.encode() + b'=='))
if mode == 'e' and self.preferences_key:
# preferences are encrypted
key = self._get_fernet_key(self.preferences_key)
decrypted_data = Fernet(key).decrypt(decoded_data)
decoded_data = brotli.decompress(decrypted_data)
config = json.loads(decoded_data)
except Exception:
config = {}
return config

22
app/models/endpoint.py Executable file
View file

@ -0,0 +1,22 @@
from enum import Enum
class Endpoint(Enum):
autocomplete = 'autocomplete'
home = 'home'
healthz = 'healthz'
config = 'config'
opensearch = 'opensearch.xml'
search = 'search'
search_html = 'search.html'
url = 'url'
imgres = 'imgres'
element = 'element'
window = 'window'
def __str__(self):
return self.value
def in_path(self, path: str) -> bool:
return path.startswith(self.value) or \
path.startswith(f'/{self.value}')

48
app/models/g_classes.py Executable file
View file

@ -0,0 +1,48 @@
from bs4 import BeautifulSoup
class GClasses:
"""A class for tracking obfuscated class names used in Google results that
are directly referenced in Whoogle's filtering code.
Note: Using these should be a last resort. It is always preferred to filter
results using structural cues instead of referencing class names, as these
are liable to change at any moment.
"""
main_tbm_tab = 'KP7LCb'
images_tbm_tab = 'n692Zd'
footer = 'TuS8Ad'
result_class_a = 'ZINbbc'
result_class_b = 'luh4td'
scroller_class = 'idg8be'
line_tag = 'BsXmcf'
result_classes = {
result_class_a: ['Gx5Zad'],
result_class_b: ['fP1Qef']
}
@classmethod
def replace_css_classes(cls, soup: BeautifulSoup) -> BeautifulSoup:
"""Replace updated Google classes with the original class names that
Whoogle relies on for styling.
Args:
soup: The result page as a BeautifulSoup object
Returns:
BeautifulSoup: The new BeautifulSoup
"""
result_divs = soup.find_all('div', {
'class': [_ for c in cls.result_classes.values() for _ in c]
})
for div in result_divs:
new_class = ' '.join(div['class'])
for key, val in cls.result_classes.items():
new_class = ' '.join(new_class.replace(_, key) for _ in val)
div['class'] = new_class.split(' ')
return soup
def __str__(self):
return self.value

352
app/request.py Executable file
View file

@ -0,0 +1,352 @@
from app.models.config import Config
from app.utils.misc import read_config_bool
from datetime import datetime
from defusedxml import ElementTree as ET
import random
import requests
from requests import Response, ConnectionError
import urllib.parse as urlparse
import os
from stem import Signal, SocketError
from stem.connection import AuthenticationFailure
from stem.control import Controller
from stem.connection import authenticate_cookie, authenticate_password
MAPS_URL = 'https://maps.google.com/maps'
AUTOCOMPLETE_URL = ('https://suggestqueries.google.com/'
'complete/search?client=toolbar&')
MOBILE_UA = '{}/5.0 (Android 0; Mobile; rv:54.0) Gecko/54.0 {}/59.0'
DESKTOP_UA = '{}/5.0 (X11; {} x86_64; rv:75.0) Gecko/20100101 {}/75.0'
# Valid query params
VALID_PARAMS = ['tbs', 'tbm', 'start', 'near', 'source', 'nfpr']
class TorError(Exception):
"""Exception raised for errors in Tor requests.
Attributes:
message: a message describing the error that occurred
disable: optionally disables Tor in the user config (note:
this should only happen if the connection has been dropped
altogether).
"""
def __init__(self, message, disable=False) -> None:
self.message = message
self.disable = disable
super().__init__(message)
def send_tor_signal(signal: Signal) -> bool:
use_pass = read_config_bool('WHOOGLE_TOR_USE_PASS')
confloc = './misc/tor/control.conf'
# Check that the custom location of conf is real.
temp = os.getenv('WHOOGLE_TOR_CONF', '')
if os.path.isfile(temp):
confloc = temp
# Attempt to authenticate and send signal.
try:
with Controller.from_port(port=9051) as c:
if use_pass:
with open(confloc, "r") as conf:
# Scan for the last line of the file.
for line in conf:
pass
secret = line.strip('\n')
authenticate_password(c, password=secret)
else:
cookie_path = '/var/lib/tor/control_auth_cookie'
authenticate_cookie(c, cookie_path=cookie_path)
c.signal(signal)
os.environ['TOR_AVAILABLE'] = '1'
return True
except (SocketError, AuthenticationFailure,
ConnectionRefusedError, ConnectionError):
# TODO: Handle Tor authentication (password and cookie)
os.environ['TOR_AVAILABLE'] = '0'
return False
def gen_user_agent(is_mobile) -> str:
user_agent = os.environ.get('WHOOGLE_USER_AGENT', '')
user_agent_mobile = os.environ.get('WHOOGLE_USER_AGENT_MOBILE', '')
if user_agent and not is_mobile:
return user_agent
if user_agent_mobile and is_mobile:
return user_agent_mobile
firefox = random.choice(['Choir', 'Squier', 'Higher', 'Wire']) + 'fox'
linux = random.choice(['Win', 'Sin', 'Gin', 'Fin', 'Kin']) + 'ux'
if is_mobile:
return MOBILE_UA.format("Mozilla", firefox)
return DESKTOP_UA.format("Mozilla", linux, firefox)
def gen_query(query, args, config) -> str:
param_dict = {key: '' for key in VALID_PARAMS}
# Use :past(hour/day/week/month/year) if available
# example search "new restaurants :past month"
lang = ''
if ':past' in query and 'tbs' not in args:
time_range = str.strip(query.split(':past', 1)[-1])
param_dict['tbs'] = '&tbs=' + ('qdr:' + str.lower(time_range[0]))
elif 'tbs' in args or 'tbs' in config:
result_tbs = args.get('tbs') if 'tbs' in args else config['tbs']
param_dict['tbs'] = '&tbs=' + result_tbs
# Occasionally the 'tbs' param provided by google also contains a
# field for 'lr', but formatted strangely. This is a rough solution
# for this.
#
# Example:
# &tbs=qdr:h,lr:lang_1pl
# -- the lr param needs to be extracted and remove the leading '1'
result_params = [_ for _ in result_tbs.split(',') if 'lr:' in _]
if len(result_params) > 0:
result_param = result_params[0]
lang = result_param[result_param.find('lr:') + 3:len(result_param)]
# Ensure search query is parsable
query = urlparse.quote(query)
# Pass along type of results (news, images, books, etc)
if 'tbm' in args:
param_dict['tbm'] = '&tbm=' + args.get('tbm')
# Get results page start value (10 per page, ie page 2 start val = 20)
if 'start' in args:
param_dict['start'] = '&start=' + args.get('start')
# Search for results near a particular city, if available
if config.near:
param_dict['near'] = '&near=' + urlparse.quote(config.near)
# Set language for results (lr) if source isn't set, otherwise use the
# result language param provided in the results
if 'source' in args:
param_dict['source'] = '&source=' + args.get('source')
param_dict['lr'] = ('&lr=' + ''.join(
[_ for _ in lang if not _.isdigit()]
)) if lang else ''
else:
param_dict['lr'] = (
'&lr=' + config.lang_search
) if config.lang_search else ''
# 'nfpr' defines the exclusion of results from an auto-corrected query
if 'nfpr' in args:
param_dict['nfpr'] = '&nfpr=' + args.get('nfpr')
# 'chips' is used in image tabs to pass the optional 'filter' to add to the
# given search term
if 'chips' in args:
param_dict['chips'] = '&chips=' + args.get('chips')
param_dict['gl'] = (
'&gl=' + config.country
) if config.country else ''
param_dict['hl'] = (
'&hl=' + config.lang_interface.replace('lang_', '')
) if config.lang_interface else ''
param_dict['safe'] = '&safe=' + ('active' if config.safe else 'off')
# Block all sites specified in the user config
unquoted_query = urlparse.unquote(query)
for blocked_site in config.block.replace(' ', '').split(','):
if not blocked_site:
continue
block = (' -site:' + blocked_site)
query += block if block not in unquoted_query else ''
for val in param_dict.values():
if not val:
continue
query += val
return query
class Request:
"""Class used for handling all outbound requests, including search queries,
search suggestions, and loading of external content (images, audio, etc).
Attributes:
normal_ua: the user's current user agent
root_path: the root path of the whoogle instance
config: the user's current whoogle configuration
"""
def __init__(self, normal_ua, root_path, config: Config):
self.search_url = 'https://www.google.com/search?gbv=1&num=' + str(
os.getenv('WHOOGLE_RESULTS_PER_PAGE', 15)) + '&q='
# Send heartbeat to Tor, used in determining if the user can or cannot
# enable Tor for future requests
send_tor_signal(Signal.HEARTBEAT)
self.language = (
config.lang_search if config.lang_search else ''
)
self.country = config.country if config.country else ''
# For setting Accept-language Header
self.lang_interface = ''
if config.accept_language:
self.lang_interface = config.lang_interface
self.mobile = bool(normal_ua) and ('Android' in normal_ua
or 'iPhone' in normal_ua)
self.modified_user_agent = gen_user_agent(self.mobile)
if not self.mobile:
self.modified_user_agent_mobile = gen_user_agent(True)
# Set up proxy, if previously configured
proxy_path = os.environ.get('WHOOGLE_PROXY_LOC', '')
if proxy_path:
proxy_type = os.environ.get('WHOOGLE_PROXY_TYPE', '')
proxy_user = os.environ.get('WHOOGLE_PROXY_USER', '')
proxy_pass = os.environ.get('WHOOGLE_PROXY_PASS', '')
auth_str = ''
if proxy_user:
auth_str = f'{proxy_user}:{proxy_pass}@'
proxy_str = f'{proxy_type}://{auth_str}{proxy_path}'
self.proxies = {
'https': proxy_str,
'http': proxy_str
}
else:
self.proxies = {
'http': 'socks5://127.0.0.1:9050',
'https': 'socks5://127.0.0.1:9050'
} if config.tor else {}
self.tor = config.tor
self.tor_valid = False
self.root_path = root_path
def __getitem__(self, name):
return getattr(self, name)
def autocomplete(self, query) -> list:
"""Sends a query to Google's search suggestion service
Args:
query: The in-progress query to send
Returns:
list: The list of matches for possible search suggestions
"""
ac_query = dict(q=query)
if self.language:
ac_query['lr'] = self.language
if self.country:
ac_query['gl'] = self.country
if self.lang_interface:
ac_query['hl'] = self.lang_interface
response = self.send(base_url=AUTOCOMPLETE_URL,
query=urlparse.urlencode(ac_query)).text
if not response:
return []
try:
root = ET.fromstring(response)
return [_.attrib['data'] for _ in
root.findall('.//suggestion/[@data]')]
except ET.ParseError:
# Malformed XML response
return []
def send(self, base_url='', query='', attempt=0,
force_mobile=True, user_agent='') -> Response:
"""Sends an outbound request to a URL. Optionally sends the request
using Tor, if enabled by the user.
Args:
base_url: The URL to use in the request
query: The optional query string for the request
attempt: The number of attempts made for the request
(used for cycling through Tor identities, if enabled)
force_mobile: Optional flag to enable a mobile user agent
(used for fetching full size images in search results)
Returns:
Response: The Response object returned by the requests call
"""
use_client_user_agent = int(os.environ.get('WHOOGLE_USE_CLIENT_USER_AGENT', '0'))
if user_agent and use_client_user_agent == 1:
modified_user_agent = user_agent
else:
if force_mobile and not self.mobile:
modified_user_agent = self.modified_user_agent_mobile
else:
modified_user_agent = self.modified_user_agent
headers = {
'User-Agent': modified_user_agent
}
# Adding the Accept-Language to the Header if possible
if self.lang_interface:
headers.update({'Accept-Language':
self.lang_interface.replace('lang_', '')
+ ';q=1.0'})
# view is suppressed correctly
now = datetime.now()
cookies = {
'CONSENT': 'PENDING+987',
'SOCS': 'CAESHAgBEhIaAB',
}
# Validate Tor conn and request new identity if the last one failed
if self.tor and not send_tor_signal(
Signal.NEWNYM if attempt > 0 else Signal.HEARTBEAT):
raise TorError(
"Tor was previously enabled, but the connection has been "
"dropped. Please check your Tor configuration and try again.",
disable=True)
# Make sure that the tor connection is valid, if enabled
if self.tor:
try:
tor_check = requests.get('https://check.torproject.org/',
proxies=self.proxies, headers=headers)
self.tor_valid = 'Congratulations' in tor_check.text
if not self.tor_valid:
raise TorError(
"Tor connection succeeded, but the connection could "
"not be validated by torproject.org",
disable=True)
except ConnectionError:
raise TorError(
"Error raised during Tor connection validation",
disable=True)
response = requests.get(
(base_url or self.search_url) + query,
proxies=self.proxies,
headers=headers,
cookies=cookies)
# Retry query with new identity if using Tor (max 10 attempts)
if 'form id="captcha-form"' in response.text and self.tor:
attempt += 1
if attempt > 10:
raise TorError("Tor query failed -- max attempts exceeded 10")
return self.send((base_url or self.search_url), query, attempt)
return response

726
app/routes.py Executable file
View file

@ -0,0 +1,726 @@
import argparse
import base64
import io
import json
import os
import pickle
import re
import urllib.parse as urlparse
import uuid
import validators
import sys
import traceback
from datetime import datetime, timedelta
from functools import wraps
import logging
import traceback
from werkzeug.exceptions import HTTPException
from urllib.parse import unquote
import waitress
from app import app
from app.models.config import Config
from app.models.endpoint import Endpoint
from app.request import Request, TorError
from app.utils.bangs import suggest_bang, resolve_bang
from app.utils.misc import empty_gif, placeholder_img, get_proxy_host_url, \
fetch_favicon
from app.filter import Filter
from app.utils.misc import read_config_bool, get_client_ip, get_request_url, \
check_for_update, encrypt_string
from app.utils.widgets import *
from app.utils.results import bold_search_terms,\
add_currency_card, check_currency, get_tabs_content
from app.utils.search import Search, needs_https, has_captcha
from app.utils.session import valid_user_session
from bs4 import BeautifulSoup as bsoup
from flask import Flask, jsonify, make_response, request, redirect, render_template, \
send_file, session, url_for, g
from requests import exceptions
from requests.models import PreparedRequest
from cryptography.fernet import Fernet, InvalidToken
from cryptography.exceptions import InvalidSignature
from werkzeug.datastructures import MultiDict
ac_var = 'WHOOGLE_AUTOCOMPLETE'
autocomplete_enabled = os.getenv(ac_var, '1')
def get_search_name(tbm):
for tab in app.config['HEADER_TABS'].values():
if tab['tbm'] == tbm:
return tab['name']
def auth_required(f):
@wraps(f)
def decorated(*args, **kwargs):
# do not ask password if cookies already present
if (
valid_user_session(session)
and 'cookies_disabled' not in request.args
and session['auth']
):
return f(*args, **kwargs)
auth = request.authorization
# Skip if username/password not set
whoogle_user = os.getenv('WHOOGLE_USER', '')
whoogle_pass = os.getenv('WHOOGLE_PASS', '')
if (not whoogle_user or not whoogle_pass) or (
auth
and whoogle_user == auth.username
and whoogle_pass == auth.password):
session['auth'] = True
return f(*args, **kwargs)
else:
return make_response('Not logged in', 401, {
'WWW-Authenticate': 'Basic realm="Login Required"'})
return decorated
def session_required(f):
@wraps(f)
def decorated(*args, **kwargs):
if not valid_user_session(session):
session.pop('_permanent', None)
# Note: This sets all requests to use the encryption key determined per
# instance on app init. This can be updated in the future to use a key
# that is unique for their session (session['key']) but this should use
# a config setting to enable the session based key. Otherwise there can
# be problems with searches performed by users with cookies blocked if
# a session based key is always used.
g.session_key = app.enc_key
# Clear out old sessions
invalid_sessions = []
for user_session in os.listdir(app.config['SESSION_FILE_DIR']):
file_path = os.path.join(
app.config['SESSION_FILE_DIR'],
user_session)
try:
# Ignore files that are larger than the max session file size
if os.path.getsize(file_path) > app.config['MAX_SESSION_SIZE']:
continue
with open(file_path, 'rb') as session_file:
_ = pickle.load(session_file)
data = pickle.load(session_file)
if isinstance(data, dict) and 'valid' in data:
continue
invalid_sessions.append(file_path)
except Exception:
# Broad exception handling here due to how instances installed
# with pip seem to have issues storing unrelated files in the
# same directory as sessions
pass
for invalid_session in invalid_sessions:
try:
os.remove(invalid_session)
except FileNotFoundError:
# Don't throw error if the invalid session has been removed
pass
return f(*args, **kwargs)
return decorated
@app.before_request
def before_request_func():
session.permanent = True
# Check for latest version if needed
now = datetime.now()
needs_update_check = now - timedelta(hours=24) > app.config['LAST_UPDATE_CHECK']
if read_config_bool('WHOOGLE_UPDATE_CHECK', True) and needs_update_check:
app.config['LAST_UPDATE_CHECK'] = now
app.config['HAS_UPDATE'] = check_for_update(
app.config['RELEASES_URL'],
app.config['VERSION_NUMBER'])
g.request_params = (
request.args if request.method == 'GET' else request.form
)
default_config = json.load(open(app.config['DEFAULT_CONFIG'])) \
if os.path.exists(app.config['DEFAULT_CONFIG']) else {}
# Generate session values for user if unavailable
if not valid_user_session(session):
session['config'] = default_config
session['uuid'] = str(uuid.uuid4())
session['key'] = app.enc_key
session['auth'] = False
# Establish config values per user session
g.user_config = Config(**session['config'])
# Update user config if specified in search args
g.user_config = g.user_config.from_params(g.request_params)
if not g.user_config.url:
g.user_config.url = get_request_url(request.url_root)
g.user_request = Request(
request.headers.get('User-Agent'),
get_request_url(request.url_root),
config=g.user_config)
g.app_location = g.user_config.url
@app.after_request
def after_request_func(resp):
resp.headers['X-Content-Type-Options'] = 'nosniff'
resp.headers['X-Frame-Options'] = 'DENY'
resp.headers['Cache-Control'] = 'max-age=86400'
if os.getenv('WHOOGLE_CSP', False):
resp.headers['Content-Security-Policy'] = app.config['CSP']
if os.environ.get('HTTPS_ONLY', False):
resp.headers['Content-Security-Policy'] += \
'upgrade-insecure-requests'
return resp
@app.errorhandler(404)
def unknown_page(e):
app.logger.warn(e)
return redirect(g.app_location)
@app.route(f'/{Endpoint.healthz}', methods=['GET'])
def healthz():
return ''
@app.route('/', methods=['GET'])
@app.route(f'/{Endpoint.home}', methods=['GET'])
@auth_required
def index():
# Redirect if an error was raised
if 'error_message' in session and session['error_message']:
error_message = session['error_message']
session['error_message'] = ''
return render_template('error.html', error_message=error_message)
return render_template('index.html',
has_update=app.config['HAS_UPDATE'],
languages=app.config['LANGUAGES'],
countries=app.config['COUNTRIES'],
time_periods=app.config['TIME_PERIODS'],
themes=app.config['THEMES'],
autocomplete_enabled=autocomplete_enabled,
translation=app.config['TRANSLATIONS'][
g.user_config.get_localization_lang()
],
logo=render_template(
'logo.html',
dark=g.user_config.dark),
config_disabled=(
app.config['CONFIG_DISABLE'] or
not valid_user_session(session)),
config=g.user_config,
tor_available=int(os.environ.get('TOR_AVAILABLE')),
version_number=app.config['VERSION_NUMBER'])
@app.route(f'/{Endpoint.opensearch}', methods=['GET'])
def opensearch():
opensearch_url = g.app_location
if opensearch_url.endswith('/'):
opensearch_url = opensearch_url[:-1]
# Enforce https for opensearch template
if needs_https(opensearch_url):
opensearch_url = opensearch_url.replace('http://', 'https://', 1)
get_only = g.user_config.get_only or 'Chrome' in request.headers.get(
'User-Agent')
return render_template(
'opensearch.xml',
main_url=opensearch_url,
request_type='' if get_only else 'method="post"',
search_type=request.args.get('tbm'),
search_name=get_search_name(request.args.get('tbm'))
), 200, {'Content-Type': 'application/xml'}
@app.route(f'/{Endpoint.search_html}', methods=['GET'])
def search_html():
search_url = g.app_location
if search_url.endswith('/'):
search_url = search_url[:-1]
return render_template('search.html', url=search_url)
@app.route(f'/{Endpoint.autocomplete}', methods=['GET', 'POST'])
def autocomplete():
if os.getenv(ac_var) and not read_config_bool(ac_var):
return jsonify({})
q = g.request_params.get('q')
if not q:
# FF will occasionally (incorrectly) send the q field without a
# mimetype in the format "b'q=<query>'" through the request.data field
q = str(request.data).replace('q=', '')
# Search bangs if the query begins with "!", but not "! " (feeling lucky)
if q.startswith('!') and len(q) > 1 and not q.startswith('! '):
return jsonify([q, suggest_bang(q)])
if not q and not request.data:
return jsonify({'?': []})
elif request.data:
q = urlparse.unquote_plus(
request.data.decode('utf-8').replace('q=', ''))
# Return a list of suggestions for the query
#
# Note: If Tor is enabled, this returns nothing, as the request is
# almost always rejected
return jsonify([
q,
g.user_request.autocomplete(q) if not g.user_config.tor else []
])
@app.route(f'/{Endpoint.search}', methods=['GET', 'POST'])
@session_required
@auth_required
def search():
if request.method == 'POST':
# Redirect as a GET request with an encrypted query
post_data = MultiDict(request.form)
post_data['q'] = encrypt_string(g.session_key, post_data['q'])
get_req_str = urlparse.urlencode(post_data)
return redirect(url_for('.search') + '?' + get_req_str)
search_util = Search(request, g.user_config, g.session_key)
query = search_util.new_search_query()
bang = resolve_bang(query)
if bang:
return redirect(bang)
# Redirect to home if invalid/blank search
if not query:
return redirect(url_for('.index'))
# Generate response and number of external elements from the page
try:
response = search_util.generate_response()
except TorError as e:
session['error_message'] = e.message + (
"\\n\\nTor config is now disabled!" if e.disable else "")
session['config']['tor'] = False if e.disable else session['config'][
'tor']
return redirect(url_for('.index'))
if search_util.feeling_lucky:
return redirect(response, code=303)
# If the user is attempting to translate a string, determine the correct
# string for formatting the lingva.ml url
localization_lang = g.user_config.get_localization_lang()
translation = app.config['TRANSLATIONS'][localization_lang]
translate_to = localization_lang.replace('lang_', '')
# removing st-card to only use whoogle time selector
soup = bsoup(response, "html.parser");
for x in soup.find_all(attrs={"id": "st-card"}):
x.replace_with("")
response = str(soup)
# Return 503 if temporarily blocked by captcha
if has_captcha(str(response)):
app.logger.error('503 (CAPTCHA)')
fallback_engine = os.environ.get('WHOOGLE_FALLBACK_ENGINE_URL', '')
if (fallback_engine):
return redirect(fallback_engine + query)
return render_template(
'error.html',
blocked=True,
error_message=translation['ratelimit'],
translation=translation,
farside='https://farside.link',
config=g.user_config,
query=urlparse.unquote(query),
params=g.user_config.to_params(keys=['preferences'])), 503
response = bold_search_terms(response, query)
# check for widgets and add if requested
if search_util.widget != '':
html_soup = bsoup(str(response), 'html.parser')
if search_util.widget == 'ip':
response = add_ip_card(html_soup, get_client_ip(request))
elif search_util.widget == 'calculator' and not 'nojs' in request.args:
response = add_calculator_card(html_soup)
# Update tabs content
tabs = get_tabs_content(app.config['HEADER_TABS'],
search_util.full_query,
search_util.search_type,
g.user_config.preferences,
translation)
# Feature to display currency_card
# Since this is determined by more than just the
# query is it not defined as a standard widget
conversion = check_currency(str(response))
if conversion:
html_soup = bsoup(str(response), 'html.parser')
response = add_currency_card(html_soup, conversion)
preferences = g.user_config.preferences
home_url = f"home?preferences={preferences}" if preferences else "home"
cleanresponse = str(response).replace("andlt;","&lt;").replace("andgt;","&gt;")
return render_template(
'display.html',
has_update=app.config['HAS_UPDATE'],
query=urlparse.unquote(query),
search_type=search_util.search_type,
search_name=get_search_name(search_util.search_type),
config=g.user_config,
autocomplete_enabled=autocomplete_enabled,
lingva_url=app.config['TRANSLATE_URL'],
translation=translation,
translate_to=translate_to,
translate_str=query.replace(
'translate', ''
).replace(
translation['translate'], ''
),
is_translation=any(
_ in query.lower() for _ in [translation['translate'], 'translate']
) and not search_util.search_type, # Standard search queries only
response=cleanresponse,
version_number=app.config['VERSION_NUMBER'],
search_header=render_template(
'header.html',
home_url=home_url,
config=g.user_config,
translation=translation,
languages=app.config['LANGUAGES'],
countries=app.config['COUNTRIES'],
time_periods=app.config['TIME_PERIODS'],
logo=render_template('logo.html', dark=g.user_config.dark),
query=urlparse.unquote(query),
search_type=search_util.search_type,
mobile=g.user_request.mobile,
tabs=tabs)).replace(" ", "")
@app.route(f'/{Endpoint.config}', methods=['GET', 'POST', 'PUT'])
@session_required
@auth_required
def config():
config_disabled = (
app.config['CONFIG_DISABLE'] or
not valid_user_session(session))
name = ''
if 'name' in request.args:
name = os.path.normpath(request.args.get('name'))
if not re.match(r'^[A-Za-z0-9_.+-]+$', name):
return make_response('Invalid config name', 400)
if request.method == 'GET':
return json.dumps(g.user_config.__dict__)
elif request.method == 'PUT' and not config_disabled:
if name:
config_pkl = os.path.join(app.config['CONFIG_PATH'], name)
session['config'] = (pickle.load(open(config_pkl, 'rb'))
if os.path.exists(config_pkl)
else session['config'])
return json.dumps(session['config'])
else:
return json.dumps({})
elif not config_disabled:
config_data = request.form.to_dict()
if 'url' not in config_data or not config_data['url']:
config_data['url'] = g.user_config.url
# Save config by name to allow a user to easily load later
if 'name' in request.args:
pickle.dump(
config_data,
open(os.path.join(
app.config['CONFIG_PATH'],
name), 'wb'))
session['config'] = config_data
return redirect(config_data['url'])
else:
return redirect(url_for('.index'), code=403)
@app.route(f'/{Endpoint.imgres}')
@session_required
@auth_required
def imgres():
return redirect(request.args.get('imgurl'))
@app.route(f'/{Endpoint.element}')
@session_required
@auth_required
def element():
element_url = src_url = request.args.get('url')
if element_url.startswith('gAAAAA'):
try:
cipher_suite = Fernet(g.session_key)
src_url = cipher_suite.decrypt(element_url.encode()).decode()
except (InvalidSignature, InvalidToken) as e:
return render_template(
'error.html',
error_message=str(e)), 401
src_type = request.args.get('type')
# Ensure requested element is from a valid domain
domain = urlparse.urlparse(src_url).netloc
if not validators.domain(domain):
return send_file(io.BytesIO(empty_gif), mimetype='image/gif')
try:
response = g.user_request.send(base_url=src_url)
# Display an empty gif if the requested element couldn't be retrieved
if response.status_code != 200 or len(response.content) == 0:
if 'favicon' in src_url:
favicon = fetch_favicon(src_url)
return send_file(io.BytesIO(favicon), mimetype='image/png')
else:
return send_file(io.BytesIO(empty_gif), mimetype='image/gif')
file_data = response.content
tmp_mem = io.BytesIO()
tmp_mem.write(file_data)
tmp_mem.seek(0)
return send_file(tmp_mem, mimetype=src_type)
except exceptions.RequestException:
pass
return send_file(io.BytesIO(empty_gif), mimetype='image/gif')
@app.route(f'/{Endpoint.window}')
@session_required
@auth_required
def window():
target_url = request.args.get('location')
if target_url.startswith('gAAAAA'):
cipher_suite = Fernet(g.session_key)
target_url = cipher_suite.decrypt(target_url.encode()).decode()
content_filter = Filter(
g.session_key,
root_url=request.url_root,
config=g.user_config)
target = urlparse.urlparse(target_url)
# Ensure requested URL has a valid domain
if not validators.domain(target.netloc):
return render_template(
'error.html',
error_message='Invalid location'), 400
host_url = f'{target.scheme}://{target.netloc}'
get_body = g.user_request.send(base_url=target_url).text
results = bsoup(get_body, 'html.parser')
src_attrs = ['src', 'href', 'srcset', 'data-srcset', 'data-src']
# Parse HTML response and replace relative links w/ absolute
for element in results.find_all():
for attr in src_attrs:
if not element.has_attr(attr) or not element[attr].startswith('/'):
continue
element[attr] = host_url + element[attr]
# Replace or remove javascript sources
for script in results.find_all('script', {'src': True}):
if 'nojs' in request.args:
script.decompose()
else:
content_filter.update_element_src(script, 'application/javascript')
# Replace all possible image attributes
img_sources = ['src', 'data-src', 'data-srcset', 'srcset']
for img in results.find_all('img'):
_ = [
content_filter.update_element_src(img, 'image/png', attr=_)
for _ in img_sources if img.has_attr(_)
]
# Replace all stylesheet sources
for link in results.find_all('link', {'href': True}):
content_filter.update_element_src(link, 'text/css', attr='href')
# Use anonymous view for all links on page
for a in results.find_all('a', {'href': True}):
a['href'] = f'{Endpoint.window}?location=' + a['href'] + (
'&nojs=1' if 'nojs' in request.args else '')
# Remove all iframes -- these are commonly used inside of <noscript> tags
# to enforce loading Google Analytics
for iframe in results.find_all('iframe'):
iframe.decompose()
return render_template(
'display.html',
response=results,
translation=app.config['TRANSLATIONS'][
g.user_config.get_localization_lang()
]
)
@app.route('/robots.txt')
def robots():
response = make_response(
'''User-Agent: *
Disallow: /''', 200)
response.mimetype = 'text/plain'
return response
@app.route('/')
def favicon():
return app.send_static_file('img/favicon.ico')
@app.errorhandler(Exception)
def internal_error(e):
query = ''
if request.method == 'POST':
query = request.form.get('q')
else:
query = request.args.get('q')
# Attempt to parse the query
try:
search_util = Search(request, g.user_config, g.session_key)
query = search_util.new_search_query()
except Exception:
pass
print(traceback.format_exc(), file=sys.stderr)
fallback_engine = os.environ.get('WHOOGLE_FALLBACK_ENGINE_URL', '')
if (fallback_engine):
return redirect(fallback_engine + query)
localization_lang = g.user_config.get_localization_lang()
translation = app.config['TRANSLATIONS'][localization_lang]
# Itt kapja meg a hiba részleteit, és ezeket fogja megjeleníteni.
error_detail = str(e) # Konvertálja a hiba objektumot szöveges formátumba
return render_template(
'error.html',
error_message=f'Internal server error (500): {error_detail}', # Ide kerül a pontos hibaüzenet
translation=translation,
farside='https://farside.link',
config=g.user_config,
query=unquote(query),
params=g.user_config.to_params(keys=['preferences'])), 500
def run_app() -> None:
parser = argparse.ArgumentParser(
description='RaveSearch console runner')
parser.add_argument(
'--port',
default=5000,
metavar='<port number>',
help='Specifies a port to run on (default 5000)')
parser.add_argument(
'--host',
default='127.0.0.1',
metavar='<ip address>',
help='Specifies the host address to use (default 127.0.0.1)')
parser.add_argument(
'--unix-socket',
default='',
metavar='</path/to/unix.sock>',
help='Listen for app on unix socket instead of host:port')
parser.add_argument(
'--unix-socket-perms',
default='600',
metavar='<octal permissions>',
help='Octal permissions to use for the Unix domain socket (default 600)')
parser.add_argument(
'--debug',
default=False,
action='store_true',
help='Activates debug mode for the server (default False)')
parser.add_argument(
'--https-only',
default=False,
action='store_true',
help='Enforces HTTPS redirects for all requests')
parser.add_argument(
'--userpass',
default='',
metavar='<username:password>',
help='Sets a username/password basic auth combo (default None)')
parser.add_argument(
'--proxyauth',
default='',
metavar='<username:password>',
help='Sets a username/password for a HTTP/SOCKS proxy (default None)')
parser.add_argument(
'--proxytype',
default='',
metavar='<socks4|socks5|http>',
help='Sets a proxy type for all connections (default None)')
parser.add_argument(
'--proxyloc',
default='',
metavar='<location:port>',
help='Sets a proxy location for all connections (default None)')
args = parser.parse_args()
if args.userpass:
user_pass = args.userpass.split(':')
os.environ['WHOOGLE_USER'] = user_pass[0]
os.environ['WHOOGLE_PASS'] = user_pass[1]
if args.proxytype and args.proxyloc:
if args.proxyauth:
proxy_user_pass = args.proxyauth.split(':')
os.environ['WHOOGLE_PROXY_USER'] = proxy_user_pass[0]
os.environ['WHOOGLE_PROXY_PASS'] = proxy_user_pass[1]
os.environ['WHOOGLE_PROXY_TYPE'] = args.proxytype
os.environ['WHOOGLE_PROXY_LOC'] = args.proxyloc
if args.https_only:
os.environ['HTTPS_ONLY'] = '1'
if args.debug:
app.run(host=args.host, port=args.port, debug=args.debug)
elif args.unix_socket:
waitress.serve(app, unix_socket=args.unix_socket, unix_socket_perms=args.unix_socket_perms)
else:
waitress.serve(
app,
listen="{}:{}".format(args.host, args.port),
url_prefix=os.environ.get('WHOOGLE_URL_PREFIX', ''))

BIN
app/static/.DS_Store vendored Executable file

Binary file not shown.

View file

@ -0,0 +1,14 @@
{
"!i": {
"url": "search?q={}&tbm=isch",
"suggestion": "!i (Whoogle Images)"
},
"!v": {
"url": "search?q={}&tbm=vid",
"suggestion": "!v (Whoogle Videos)"
},
"!n": {
"url": "search?q={}&tbm=nws",
"suggestion": "!n (Whoogle News)"
}
}

1
app/static/bangs/bangs.json Executable file

File diff suppressed because one or more lines are too long

2
app/static/build/.gitignore vendored Executable file
View file

@ -0,0 +1,2 @@
*
!.gitignore

BIN
app/static/config/.DS_Store vendored Executable file

Binary file not shown.

1
app/static/config/whoogle.key Executable file
View file

@ -0,0 +1 @@
b'mr+IkY4OTQpU+KhotYOliihTwVVaabDcbwRW5w7HrlI='

BIN
app/static/css/.DS_Store vendored Normal file

Binary file not shown.

895
app/static/css/dark-theme.css Executable file
View file

@ -0,0 +1,895 @@
:root {
/* DARK THEME COLORS */
--primary: #ed0f59;
--primary-active: #bc134b;
--primary-grad2: #e24c41;
--grad2-active: #a53028;
--primary-grad3: #dc7541;
--grad3-active: #a34e24;
--body-bg: #0a0c0d;
--body-color: #e4e8ea;
--ad-bg: #282e31;
--gray-200: #1b1f21;
--gray-300: #23292c;
--gray-400: #282e31;
--gray-800: #9aa2a6;
--gray-900: #50585c;
--gray-950: #31373a;
--danger: #ff3333;
--danger-active: #e52e2e;
--visited: #8e7263;
}
/* :root {
/* DARK THEME COLORS */
/*--primary: #105d5e;
--primary-active: #137071;
--primary-grad2: #009a6e;
--grad2-active: #00af7e;
--primary-grad3: #b3eda9;
--grad3-active: #d0fdc8;
--primary-grad4: #ebfadb;
--ad-bg: #293e33;
--silver-bg: #767f7d;
--silver-light-bg: #c2cbc9;
--active: #e8e300;
--body-bg: #0a0c0d;
--body-color: #e4e8ea;
--body-color: #edf3f5;
--gray-200: #1b1f21;
--gray-300: #23292c;
--gray-400: #282e31;
--gray-800: #9aa2a6;
--gray-900: #50585c;
--gray-950: #31373a;
--danger: #ff3333;
--danger-active: #e52e2e;
} */
html {
background: var(--body-bg) !important;
color: var(--body-color) !important;
}
body {
background:var(--body-bg) !important;
color: var(--body-color) !important;
}
div {
color: var(--body-color) !important;
}
/* label {
color: var(--whoogle-dark-contrast-text) !important;
}
li a {
color: var(--whoogle-dark-result-url) !important;
}
li {
color: var(--whoogle-dark-text) !important;
}
.anon-view {
color: var(--bo) !important;
padding: 30px;
} */
/* textarea {
background: rgb(29, 29, 29) !important;
color: var(--whoogle-dark-text) !important;
} */
/* Látogatott találat címke */
a:visited h3 div {
color: var(--visited) !important;
}
/* Találat címke */
a:link h3 div {
color: var(--primary-grad3) !important;
}
a:link div {
color: var(--body-color) !important;
}
/* Találat címke hover */
a:link div:hover {
color: var(--grad3-active) !important;
text-decoration: none !important;
}
a:link div:visited {
color: var(--visited) !important;
text-decoration: none !important;
}
a:hover {
text-decoration: none !important;
}
/* Wikipedia cím */
.lU7jec h3 {
font-size: 20px;
font-weight: bold;
line-height: 26px;
color: var(--primary-grad3) !important;
margin: 0 0 24px 0 !important;
}
/* wikipedia favicon */
.BNeawe.s3v9rd.AP7Wnd.has-favicon > .BNeawe.has-favicon > img.site-favicon {
display: none !important;
}
/* Helyek favicon */
div > .other-results > .ZINbbc.xpd.Et0od.pkph0e.has-favicon > .has-favicon > img.site-favicon {
display: none !important;
}
.tAd8D.AP7Wnd {
color: var(--body-color) !important;
}
.tAd8D.AP7Wnd {
padding: 0 !important;
}
div span {
color: var(--primary-grad3) !important;
}
input {
background-color: var(--gray-300) !important;
color: var(--body-color) !important;
}
input:focus-visible {
border: 1px solid var(--primary-active) !important;
outline: none;
border-radius: 100px;
}
select {
background: transparent !important;
color: var(--body-color) !important;
}
/* ---------------------------------------- */
.autocomplete {
background-color: var(--gray-300) !important;
}
.autocomplete:hover {
background-image: linear-gradient(90deg, var(--primary), var(--primary));
background-clip: padding-box, border-box;
background-origin: border-box;
border: 1px solid var(--primary-grad3);
border-radius: 100px;
}
.autocomplete:focus {
background-image: linear-gradient(90deg, var(--primary), var(--primary));
background-clip: padding-box, border-box;
background-origin: border-box;
border: 1px solid var(--primary-grad3);
border-radius: 100px;
}
/* Keresőgomb */
#search-submit {
color: var(--body-color) !important;
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-grad2) 60%, var(--primary-grad3) 100%) !important;
border: 1px solid var(--grad3-active) !important;
border-radius: 100px !important;
}
#search-submit:hover {
color: var(--body-color) !important;
background: linear-gradient(200deg, var(--primary) 0%, var(--primary-grad2) 60%, var(--primary-grad3) 100%) !important;
border: 1px solid var(--grad3-active) !important;
border-radius: 100px !important;
}
.kattide {
color: var(--body-color) !important;
background: linear-gradient(45deg, var(--primary) 0%, var(--primary-grad2) 80%, var(--primary-grad3) 100%);
padding-left: 0.8rem;
padding-right: 0.8rem;
-webkit-box-decoration-break: clone;
box-decoration-break: clone;
}
.kattide:hover {
color: white !important;
}
/* Keresőkategóriák megjelenítése */
.selected-header-button,
button.selected-header-button:active,
button.selected-header-button:focus-within {
color: var(--primary-grad3) !important;
border-bottom: 2px solid var(--primary-grad2) !important;
}
.header-button:hover {
color: var(--primary) !important;
border-bottom: 2px solid var(--primary) !important;
}
/* Keresési találat div keret */
#main > div:focus-within {
border-radius: 8px;
box-shadow: 0 0 6px 1px var(--primary-grad3);
}
.ZINbbc {
background-color: #29292987 !important;
overflow: hidden;
margin-bottom: 25px !important;
padding: 15px !important;
border-radius: 8px !important;
}
/* Ezeket keresték még */
.gGQDvd {
padding: 10px 0 0 10px !important;
height: 0 !important;
}
.kjGX2 {
left: 40px !important;
}
.toMBf {
left: 0 !important;
margin: 0 !important;
}
@media (min-width: 601px) {
.zBAuLc {
line-height: normal;
margin: 0;
padding: 0 0 0 0 !important;
}
}
@media (max-width: 600px) {
.s3v9rd {
padding: 8px 0 0 0 !important;
}
}
.j039Wc {
padding-top: 34px !important;
margin-bottom: -14px !important;
}
/* Keresés tartalom */
.s3v9rd {
font-size: 16px;
padding: 10px 0 0 0;
}
.KP7LCb {
box-shadow: 0 0 0 0 !important;
}
.BVG0Nb {
box-shadow: 0 0 0 0 !important;
background-color: var(--whoogle-dark-page-bg) !important;
}
.ZINbbc.luh4tb {
background: var(--whoogle-dark-result-bg) !important;
margin-bottom: 24px !important;
}
.bRsWnc {
background-color: var(--whoogle-dark-result-bg) !important;
}
.x54gtf {
background-color: var(--whoogle-dark-divider) !important;
}
.Q0HXG {
background-color: var(--whoogle-dark-divider) !important;
}
.LKSyXe {
background-color: var(--whoogle-dark-divider) !important;
}
.tAd8D {
padding: 10px !important;
}
.sa1toc {
background: var(--whoogle-dark-page-bg) !important;
}
.qFvlD {
border: 1px solid !important;
}
.xeDNfc {
display: none !important;
}
.info-text {
color: var(--whoogle-dark-contrast-text) !important;
opacity: 75%;
}
.collapsible {
color: var(--whoogle-dark-text) !important;
}
.collapsible:after {
color: var(--whoogle-dark-text) !important;
}
.active {
background: transparent !important;
border: 2px solid grey !important;
}
.content,
.result-config {
background-color: transparent !important;
color: var(--whoogle-contrast-text) !important;
}
.active:after {
color: var(--whoogle-dark-contrast-text) !important;
}
.link {
color: var(--whoogle-dark-contrast-text);
}
.link-color {
color: var(--whoogle-dark-result-url) !important;
}
/* Keresési ajánlások */
.autocomplete-items {
border: 0;
}
.autocomplete-items div {
color: var(--body-color);
background-color: rgb(51, 51, 51);
border: 0;
}
.autocomplete-items div:first-child {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
}
.autocomplete-items div:last-child {
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
}
.autocomplete-items div:hover {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-grad2) 60%, var(--primary-grad3) 100%) !important;
color: var(--body-color) !important;
}
.autocomplete-active {
background: linear-gradient(200deg, var(--primary) 0%, var(--primary-grad2) 60%, var(--primary-grad3) 100%) !important;
color: var(--body-color) !important;
}
.footer {
margin-bottom: 30px;
color: var(--whoogle-dark-text);
}
.header-div {
margin: 0 0 30px 0;
}
.mobile-search-bar {
background-color: var(--whoogle-dark-result-bg) !important;
color: var(--whoogle-dark-text) !important;
}
.search-bar-desktop {
color: var(--whoogle-dark-text) !important;
}
.ip-text-div,
.update_available,
.cb_label,
.cb {
color: var(--whoogle-dark-secondary-text) !important;
}
.cb:focus {
color: var(--whoogle-dark-contrast-text) !important;
}
.desktop-header,
.mobile-header {
background-color: transparent !important;
}
/* Keresés feletti mini url */
.UPmit.AP7Wnd {
color: #d0bc8e !important;
font-size: 0.8em;
}
.lcJF1d {
margin: 25px 18px 18px 0 !important;
}
.egMi0 {
margin: 0 !important;
}
.qN9Ked {
display: table-caption !important;
}
.kCrYT {
padding: 0 !important;
}
.BsXmcf {
position: relative !important;
height: 0 !important;
background: transparent !important;
}
.Q6Xouf {
max-width: 555px;
margin: 0 auto;
}
/* Helyek cím */
.deIvCb {
font-weight: 600 !important;
font-size: 18px !important;
padding: 0;
}
.nGphre > .site-favicon {
display: none !important;
}
/* Képtalálatok */
.favicon-img {
width: 18px;
height: 18px;
}
.site-info {
margin: 15px 10px 0 10px;
}
.result-img {
width: 100%;
height: 150px;
border-radius: 5px;
padding-top: 15px;
}
.result-item {
padding: 0 0 10px 0;
border-radius: 5px;
background-color: #29292987;
width: calc(33.333% - 16px);
box-sizing: border-box;
text-align: center;
word-wrap: break-word;
}
@media screen and (max-width: 600px) {
.result-item {
width: 80%;
margin: 0 auto;
}
.results {
flex-direction: column;
}
}
.results {
display: flex;
flex-wrap: wrap;
gap: 24px;
margin: 20px 0;
}
.img-thumbnail {
height: 100%;
width: 100%;
/* border-radius: 5px 5px 0 0; */
object-fit: contain;
}
.result-item p {
margin: 10px 0 0;
font-size: 14px;
}
.img-title {
color: var(--primary-grad3);
text-transform: uppercase;
}
.img-url {
color: var(--body-color);
}
.img-url:hover {
text-decoration: underline;
color: var(--grad2-active);
}
/* Stílusok a javaslatokhoz */
.suggestions {
margin: 20px 0;
padding: 10px;
background-color: #e9e9e9;
border: 1px solid #ccc;
border-radius: 5px;
}
/* Stílusok a lapozáshoz */
.pagination {
display: flex;
justify-content: center;
margin: 20px 0;
}
.pagination a {
margin: 0 5px;
padding: 10px 15px;
border: 1px solid #ddd;
background-color: #fff;
text-decoration: none;
color: #007bff;
border-radius: 5px;
}
.pagination a:hover {
background-color: #007bff;
color: #fff;
}
.e3goi {
padding: 10px;
width: 100%;
height: 160px;
}
.TxbwNb {
width: 100%;
height: 100%;
}
.RAyV4b {
width: 100%;
height: 130px;
display: flex;
justify-content: center;
align-items: center;
}
.t0fcAb {
scale: 1.8;
}
.Tor4Ec {
text-align: center;
width: 100%;
height: 20px;
display: block;
padding: 10px 0;
}
.qXLe6d {
font-size: 16px;
font-weight: bold;
}
.FbhRzb {
border-left: thin solid #dadce0;
border-right: thin solid #dadce0;
border-top: thin solid #dadce0;
height: 40px;
overflow: hidden;
}
.n692Zd {
margin-bottom: 10px;
}
.cvifge {
height: 40px;
border-spacing: 0;
width: 100%;
}
.QvGUP {
height: 40px;
padding: 0 8px 0 8px;
vertical-align: top;
}
.O4cRJf {
height: 40px;
width: 100%;
padding: 0;
padding-right: 16px;
}
.O1ePr {
height: 40px;
padding: 0;
vertical-align: top;
}
.kgJEQe {
height: 36px;
width: 98px;
vertical-align: top;
margin-top: 4px;
}
.lXLRf {
vertical-align: top;
}
.MhzMZd {
border: 0;
vertical-align: middle;
font-size: 14px;
height: 40px;
padding: 0;
width: 100%;
padding-left: 16px;
}
.xB0fq {
height: 40px;
border: none;
font-size: 14px;
background-color: #4285f4;
color: #fff;
padding: 0 16px;
margin: 0;
vertical-align: top;
cursor: pointer;
}
.xB0fq:focus {
border: 1px solid #000;
}
.M7pB2 {
border: thin solid #dadce0;
margin: 0 0 3px 0;
font-size: 13px;
font-weight: 500;
height: 40px;
}
.euZec {
width: 100%;
height: 40px;
text-align: center;
border-spacing: 0;
}
table.euZec td {
padding: 0;
width: 25%;
}
.QIqI7 {
display: inline-block;
padding-top: 4px;
font-weight: bold;
color: #4285f4;
}
.EY24We {
border-bottom: 2px solid #4285f4;
}
.CsQyDc {
display: inline-block;
color: #70757a;
}
.TuS8Ad {
font-size: 14px;
}
.HddGcc {
padding: 8px;
color: #70757a;
}
.dzp8ae {
font-weight: bold;
color: #3c4043;
}
.rEM8G {
color: #70757a;
}
.bookcf {
table-layout: fixed;
width: 100%;
border-spacing: 0;
}
.InWNIe {
text-align: center;
}
.uZgmoc {
width: 350px;
display: flex;
border: none;
color: #70757a;
font-size: 14px;
text-align: center;
/* background-color: #29292987; */
border-radius: 8px;
margin: 35px auto;
padding: 10px;
justify-content: center;
}
.frGj1b {
margin: 10px;
font-size: 16px;
border: 1px solid;
border-radius: 10px;
padding: 10px 15px;
}
.BnJWBc {
text-align: center;
padding: 6px 0 13px 0;
height: 35px;
}
.X6ZCif {
color: #202124;
font-size: 11px;
line-height: 16px;
display: inline-block;
padding-top: 2px;
overflow: hidden;
padding-bottom: 4px;
width: 100%;
}
.TwVfHd {
border-radius: 16px;
border: thin solid #dadce0;
display: inline-block;
padding: 8px 8px;
margin-right: 8px;
margin-bottom: 4px;
}
.yekiAe {
background-color: #dadce0;
}
.svla5d {
width: 100%;
height: 100%;
background-color: #29292987;
border-radius: 10px;
margin: 10px 0;
display: grid;
margin: 0 auto;
padding: 10px 0;
}
.ezO2md {
border: thin solid #dadce0;
padding: 12px 16px 12px 16px;
margin-bottom: 10px;
}
.K35ahc {
width: 100%;
}
.owohpf {
text-align: center;
}
.fYyStc {
word-break: break-word;
text-align: center;
font-size: 16px;
padding: 0 0 10px 0;
}
.img_button {
padding: 10px;
margin: 0 auto 10px;
border: 1px solid #84dbff;
border-radius: 6px;
color: #3c4043;
background-color: #2e97c3;
}
.ynsChf {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.Fj3V3b {
color: #1967d2;
font-size: 14px;
line-height: 20px;
}
.FrIlee {
color: #202124;
font-size: 11px;
line-height: 16px;
}
.F9iS2e {
color: #70757a;
font-size: 11px;
line-height: 16px;
}
.WMQ2Le {
color: #70757a;
font-size: 12px;
line-height: 16px;
}
.x3G5ab {
color: #202124;
font-size: 12px;
line-height: 16px;
}
.fuLhoc {
color: #1967d2;
font-size: 18px;
line-height: 24px;
}
.epoveb {
font-size: 32px;
line-height: 40px;
font-weight: 400;
color: #202124;
}
.dXDvrc {
color: #0d652d;
font-size: 14px;
line-height: 20px;
word-wrap: break-word;
}
.dloBPe {
font-weight: bold;
}
.YVIcad {
color: #70757a;
}
.JkVVdd {
color: #ea4335;
}
.oXZRFd {
color: #ea4335;
}
.MQHtg {
color: #fbbc04;
}
.pyMRrb {
color: #1e8e3e;
}
.EtTZid {
color: #1e8e3e;
}
.M3vVJe {
color: #1967d2;
}
.NHQNef {
font-style: italic;
}
.Cb8Z7c {
white-space: pre;
}
a.ZWRArf {
text-decoration: none;
}
a .CVA68e:hover {
text-decoration: underline;
}

9
app/static/css/error.css Executable file
View file

@ -0,0 +1,9 @@
html {
font-size: 1.3rem;
}
@media (max-width: 1000px) {
html {
font-size: 3rem;
}
}

286
app/static/css/header.css Executable file
View file

@ -0,0 +1,286 @@
html {
font-family: "Ubuntu", sans-serif !important;
overflow-x: hidden;
scrollbar-color: var(--primary-grad3) #0000002e !important;
scrollbar-width: thin !important;
}
header {
font-size: 14px;
line-height: 20px;
color: #3c4043;
word-wrap: break-word;
width: 100%;
display: grid;
justify-content: center;
align-content: center;
align-items: center;
}
.header-search {
max-width: 40rem;
margin: auto;
}
.mobile-logo {
width: 100% !important;
padding: 0 !important;
margin: 5px 0 !important;
}
.logo-link,
.logo-letter {
text-decoration: none !important;
letter-spacing: -1px;
text-align: center;
border-radius: 2px 0 0 0;
}
.result-config {
margin-bottom: 10px;
border-radius: 8px;
}
.mobile-logo {
font: 22px/36px Futura, Arial, sans-serif;
padding-left: 5px;
/* display: flex; */
width: 10rem;
align-items: center;
}
.logo-div {
margin-top: 20px;
letter-spacing: -1px;
text-align: center;
font: 22pt Futura, Arial, sans-serif;
padding: 10px 0 5px 0;
height: 37px;
font-smoothing: antialiased;
}
.search-bar-desktop {
border-radius: 8px 8px 0 0;
height: 40px !important;
}
.search-div {
margin-top: 30px;
width: 100%;
}
.search-form {
display: contents;
width: 100%;
margin: 0px;
}
.search-input {
background: none;
margin: 2px 4px 2px 8px;
display: block;
font-size: 16px;
padding: 0 0 0 8px;
flex: 1;
height: 35px;
outline: none;
border: none;
width: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
overflow: hidden;
}
.tracking-link {
font-size: large;
text-align: center;
margin: 15px;
display: block;
}
#mobile-header-logo {
height: 1.75em;
}
.mobile-input-div {
width: 100%;
}
.mobile-search-bar {
display: block;
font-size: 16px;
padding: 0 0 0 20px;
margin-right: -25px;
-webkit-box-flex: 1;
outline: none;
width: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
overflow: hidden;
border: 0px !important;
}
.autocomplete-mobile {
display: -webkit-box;
width: 100%;
}
.desktop-header-logo {
height: 1.65em;
}
.header-autocomplete {
width: 100%;
flex: 1;
}
a {
color: var(--primary-grad2) !important;
text-decoration: none;
tap-highlight-color: rgba(0, 0, 0, 0.1);
}
a:hover {
color: var(--grad2-active) !important;
text-decoration: none;
text-decoration-line: none;
tap-highlight-color: rgba(0, 0, 0, 0.1);
}
.header-tab-div {
overflow: auto;
margin: 30px 0 0 0;
}
.desktop-header {
height: 39px;
display: box;
display: flex;
width: 100%;
}
.header-tab {
box-pack: justify;
font-size: 14px;
line-height: 37px;
justify-content: space-evenly;
}
.desktop-header a,
.desktop-header span {
color: #70757a;
display: block;
flex: none;
padding: 0 16px;
text-align: center;
text-transform: uppercase;
}
span.header-tab-span {
color: #4285f4;
font-weight: bold;
background-color: #1d1d1d;
}
.mobile-header {
height: 65px;
display: table;
margin: 0 auto;
width: 100%;
}
.mobile-header a,
.mobile-header span {
color: #70757a;
text-decoration: none;
display: table-cell;
word-spacing: 30px;
height: auto;
/* padding: 8px 12px 8px 12px; */
}
@media (max-width: 600px) {
.mobile-header a,
.mobile-header span {
padding: 0 22px;
}
}
span.mobile-tab-span {
border-bottom: 2px solid #202124;
color: #202124;
height: 26px;
/* margin: 0 12px; */
/* padding: 0; */
}
.desktop-header input {
margin: 2px 4px 2px 8px;
}
a.header-tab-a:visited {
color: #70757a;
}
.header-tab-div-end {
border-left: none;
}
.adv-search {
font-size: 30px;
margin: 0 0 8px 0;
vertical-align: sub;
}
.adv-search:hover {
cursor: pointer;
}
#adv-search-toggle {
display: none;
}
.result-collapsible {
max-height: 0px;
overflow: hidden;
transition: max-height 0.25s linear;
}
.search-bar-input {
display: block;
font-size: 16px;
padding: 0 0 0 8px;
flex: 1;
height: 35px;
outline: none;
border: none;
width: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
overflow: hidden;
}
#result-country {
max-width: 200px;
}
@media (max-width: 801px) {
.header-tab-div {
margin-bottom: 10px !important;
}
.header-tab-div-2 {
margin: 0;
}
}
@media (min-width: 802px) {
.header-tab-div {
margin-bottom: 10px !important;
}
.header-tab-div-2 {
margin: 50px 20px 0 20px;
}
}
@media (max-width: 600px) {
.header-tab {
font-size: 12px;
}
}

48
app/static/css/input.css Executable file
View file

@ -0,0 +1,48 @@
#search-bar {
height: 48px !important;
background: transparent !important;
}
#search-reset {
all: unset;
margin-left: -40px;
text-align: center;
background-color: transparent !important;
cursor: pointer;
height: 45px;
width: 65px;
}
@media (max-width: 300px) {
#search-reset {
display: none;
}
}
.ZINbbc.xpd.O9g5cc.uUPGi input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.cb {
width: 40%;
overflow: hidden;
text-align: left;
line-height: 28px;
background: transparent;
border-radius: 6px;
border: 1px solid #5f6368;
font-size: 14px !important;
height: 36px;
padding: 0 0 0 12px;
margin: 10px 10px 10px 0;
}
.conversion_box {
margin-top: 15px;
}
.ZINbbc.xpd.O9g5cc.uUPGi input:focus-visible {
outline: 0;
}

205
app/static/css/light-theme.css Executable file
View file

@ -0,0 +1,205 @@
html {
background: var(--whoogle-page-bg) !important;
}
body {
background: var(--whoogle-page-bg) !important;
}
div {
color: var(--whoogle-text) !important;
}
label {
color: var(--whoogle-contrast-text) !important;
}
li a {
color: var(--whoogle-result-url) !important;
}
li {
color: var(--whoogle-text) !important;
}
.anon-view {
color: var(--whoogle-text) !important;
text-decoration: underline;
}
textarea {
background: var(--whoogle-page-bg) !important;
color: var(--whoogle-text) !important;
}
select {
background: var(--whoogle-page-bg) !important;
color: var(--whoogle-text) !important;
}
.ZINbbc {
overflow: hidden;
background-color: var(--whoogle-result-bg) !important;
margin-bottom: 10px !important;
border-radius: 8px !important;
box-shadow: 0 1px 6px rgba(32,33,36,0.28) !important;
}
.BVG0Nb {
background-color: var(--whoogle-result-bg) !important;
}
.ZINbbc.luh4tb {
background: var(--whoogle-result-bg) !important;
margin-bottom: 24px !important;
}
.bRsWnc {
background-color: var(--whoogle-result-bg) !important;
}
.x54gtf {
background-color: var(--whoogle-divider) !important;
}
.Q0HXG {
background-color: var(--whoogle-divider) !important;
}
.LKSyXe {
background-color: var(--whoogle-divider) !important;
}
a:visited h3 div {
color: var(--whoogle-result-visited) !important;
}
a:link h3 div {
color: var(--whoogle-result-title) !important;
}
a:link div {
color: var(--whoogle-result-url) !important;
}
div span {
color: var(--whoogle-secondary-text) !important;
}
input {
background-color: var(--whoogle-page-bg) !important;
color: var(--whoogle-text) !important;
}
#search-bar {
color: var(--whoogle-text) !important;
background-color: var(--whoogle-page-bg);
}
.home-search {
border-color: var(--whoogle-element-bg) !important;
}
.search-container {
background-color: var(--whoogle-page-bg) !important;
}
#search-submit {
border: 1px solid var(--whoogle-element-bg) !important;
background: var(--whoogle-element-bg) !important;
color: var(--whoogle-contrast-text) !important;
}
.info-text {
color: var(--whoogle-contrast-text) !important;
opacity: 75%;
}
.collapsible {
color: var(--whoogle-text) !important;
}
.collapsible:after {
color: var(--whoogle-text);
}
.active {
background-color: var(--whoogle-element-bg) !important;
color: var(--whoogle-contrast-text) !important;
}
.content, .result-config {
background-color: var(--whoogle-element-bg) !important;
color: var(--whoogle-contrast-text) !important;
}
.active:after {
color: var(--whoogle-contrast-text);
}
.link {
color: var(--whoogle-element-bg);
}
.link-color {
color: var(--whoogle-result-url) !important;
}
.autocomplete-items {
border: 1px solid var(--whoogle-element-bg);
}
.autocomplete-items div {
background-color: var(--whoogle-page-bg);
border-bottom: 1px solid var(--whoogle-element-bg);
}
.autocomplete-items div:hover {
background-color: var(--whoogle-element-bg);
color: var(--whoogle-contrast-text) !important;
}
.autocomplete-active {
background-color: var(--whoogle-element-bg) !important;
color: var(--whoogle-contrast-text) !important;
}
.footer {
color: var(--whoogle-text);
}
path {
fill: var(--whoogle-logo);
}
.header-div {
background-color: var(--whoogle-result-bg) !important;
}
#search-reset {
color: var(--whoogle-text) !important;
}
.mobile-search-bar {
background-color: var(--whoogle-result-bg) !important;
color: var(--whoogle-text) !important;
}
.search-bar-desktop {
background-color: var(--whoogle-result-bg) !important;
color: var(--whoogle-text);
border-bottom: 0px;
}
.ip-text-div, .update_available, .cb_label, .cb {
color: var(--whoogle-secondary-text) !important;
}
.cb:focus {
color: var(--whoogle-text) !important;
}
.desktop-header, .mobile-header {
background-color: var(--whoogle-result-bg) !important;
}

30
app/static/css/logo.css Executable file
View file

@ -0,0 +1,30 @@
.cls-1 {
fill: transparent;
}
svg {
height: inherit;
}
a {
height: inherit;
}
@media (max-width: 1000px) {
svg {
margin-top: 0.3em;
height: 70%;
}
}
.rplogo {
max-width: 40rem;
display: block;
margin: 30px auto;
position: relative;
border-radius: inherit;
top: 0;
right: 0;
bottom: 0;
left: 0;
}

381
app/static/css/main.css Executable file
View file

@ -0,0 +1,381 @@
@font-face {
font-family: "Ubuntu";
src: url("fonts/ubuntu.woff2") format("woff");
font-weight: normal;
font-style: normal;
}
body {
font-family: "Ubuntu", sans-serif !important;
max-width: 900px !important;
min-width: 0 !important;
display: flex;
flex-direction: column;
margin: 25px auto !important;
font-size: 1rem;
font-weight: 400;
color: white;
}
body,
main {
margin: 0;
padding: 15px;
}
main {
width: 100%;
margin-bottom: 2rem;
flex: 1;
}
footer {
clear: both;
min-height: 4rem;
width: 100%;
text-align: center;
display: block;
overflow: hidden;
margin-top: 45px;
text-wrap: balance;
}
.logo-container {
background: no-repeat;
min-height: 4rem;
background-position: center;
background-size: contain;
}
.home-search {
background: transparent !important;
}
::placeholder {
font-size: 14px;
}
.social-container {
display: flex;
flex-direction: inherit;
justify-content: center;
}
.social-column {
display: grid;
grid-template-rows: repeat(2, 1fr);
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
grid-auto-flow: dense;
margin-top: 50px;
}
@media (max-width: 500px){
.social-column {
grid-template-columns: repeat(1, 1fr);
}
}
.rpkick {
grid-row-end: span 2;
grid-column-end: span 1;
height: 50px;
width: 200px;
padding: 0 10px;
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 10px;
transition: background-color 0.3s ease-out 0.3ms;
}
.rpkick:hover {
background-color: rgba(83, 250, 22, 0.08);
}
.rptwitch {
grid-row-end: span 2;
grid-column-end: span 1;
height: 50px;
width: 200px;
padding: 0 10px;
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 10px;
transition: background-color 0.3s ease-out 0.3ms;
}
.rptwitch:hover {
background-color: rgba(101, 66, 166, 0.14);
}
.rpdiscord {
grid-row-end: span 2;
grid-column-end: span 1;
height: 50px;
width: 200px;
padding: 0 10px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
transition: background-color 0.3s ease-out 0.3ms;
}
.rpdiscord:hover {
background-color: rgba(98, 102, 243, 0.08);
}
.rpgit {
grid-row-end: span 2;
grid-column-end: span 1;
height: 50px;
width: 200px;
padding: 0 10px;
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 10px;
transition: background-color 0.3s ease-out 0.3ms;
}
.rpgit:hover {
background-color: rgba(204, 204, 204, 0.04);
}
.rpyoutube {
grid-row-end: span 2;
grid-column-end: span 1;
height: 50px;
width: 200px;
padding: 0 10px;
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 10px;
transition: background-color 0.3s ease-out 0.3ms;
}
.rpyoutube:hover {
background-color: rgba(129, 0, 0, 0.054);
}
.rpweb {
grid-row-end: span 2;
grid-column-end: span 1;
height: 50px;
width: 200px;
padding: 0 10px;
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 10px;
transition: background-color 0.3s ease-out 0.3ms;
}
.rpweb:hover {
background-color: rgba(251, 255, 33, 0.049);
}
.search-container {
margin-top: 26vh;
width: 100%;
margin-bottom: 2rem;
flex: 1;
padding: 0;
margin: 0;
text-align: center;
}
#search-form,
#search-fields {
max-width: 650px;
margin: 0 auto;
background: inherit;
border: inherit;
padding: 0;
display: block;
}
.search-items {
width: 100%;
position: relative;
display: flex;
}
#search-bar {
width: 95%;
margin: 0 auto;
height: 3rem;
display: inline-flex;
flex-direction: row;
white-space: nowrap;
font-size: 24px;
border: none !important;
border-radius: 100px;
padding: 0 0 0 15px;
}
#search-submit {
display: inline-flex;
flex-direction: row;
white-space: nowrap;
margin: 0.5rem auto;
cursor: pointer;
width: 100%;
height: 3rem;
font-size: 18px;
justify-content: center;
}
.config-options {
max-height: 370px;
overflow-y: scroll;
}
.config-buttons {
max-height: 30px;
}
.config-div {
margin-top: 10px;
padding: 15px;
overflow: hidden;
}
button::-moz-focus-inner {
border: 0;
}
.collapsible {
background-color: rgba(0, 0, 0, 0);
cursor: pointer;
padding: 10px 35px 10px 35px;
width: 100%;
max-width: 686px;
border: none;
text-align: left;
outline: none;
font-size: 15px;
border-radius: 10px 10px 0 0;
margin: 1.5rem 0 0 0;
}
.collapsible:after {
content: "\002B";
font-weight: bold;
float: right;
margin-left: 5px;
}
.active:after {
content: "\2212";
}
.content {
max-width: 646px;
margin: 0 auto;
padding: 0 18px;
max-height: 0;
overflow: hidden;
transition: max-height 0.2s ease-out;
border-radius: 0 0 10px 10px;
border-left: 2px solid grey;
border-right: 2px solid grey;
}
.open {
padding-top: 20px;
padding-bottom: 60px;
border-bottom: 2px solid grey;
background-color: #1d1d1d !important;
}
.hidden {
display: none;
}
.info-text {
font-style: italic;
font-size: 12px;
}
#config-style {
resize: none;
width: 100%;
height: 100px;
}
.whoogle-logo {
display: none;
}
.whoogle-svg {
width: 80%;
height: initial;
display: block;
margin: auto;
padding-bottom: 10px;
}
.autocomplete {
width: 100%;
margin: 0 auto;
color: #ffffff !important;
border: 1px solid #ffffff29;
border-radius: 100px;
box-shadow: 0 2px 16px 0 rgba(5, 5, 6, 15%), 0 4px 8px 0 rgba(5, 5, 6, 35%);
}
/* .autocomplete:hover {
border: 1px solid #2e97c3;
box-shadow: 0 1px 4px 0 #2e97c3, 0 1px 2px 0 #2e97c3;
}
*/
.autocomplete-items {
width: 650px;
position: absolute;
z-index: 99;
border: 0;
border-radius: 4px;
background-color: #1d1d1d;
overflow: hidden;
transition: opacity 0s ease 0s, visibility 0s ease 0s, all 0.5s ease 0s;
opacity: 1;
visibility: visible;
animation-delay: 0.1s;
text-align: left;
margin-top: 2px;
}
.autocomplete-items div {
padding: 10px;
cursor: pointer;
}
details summary {
padding: 10px;
font-weight: bold;
}
/* Mobile styles */
.BNeawe.UPmit.AP7Wnd.lRVwie {
font-variant: all-small-caps;
text-wrap: balance;
}
.w1C3Le,
.BmP5tf,
.G5NbBd,
.CS4w5b {
padding: 0 !important;
}
.lcJF1d {
margin-left: 30px !important;
}
.egMi0 {
margin: 0 !important;
}
.footer-bottom {
margin-bottom: 30px;
}
.lRVwie {
text-wrap: balance !important;
}

213
app/static/css/search.css Executable file
View file

@ -0,0 +1,213 @@
body {
max-width: 900px !important;
min-width: 0 !important;
display: flex;
flex-direction: column;
margin: 25px auto !important;
color: white;
padding: 0 8px !important;
}
.vvjwJb {
font-size: 20px !important;
}
.autocomplete {
position: relative;
display: -webkit-box;
width: 100%;
margin: 0 auto;
color: #ffffff !important;
border: 1px solid #ffffff29;
border-radius: 100px;
box-shadow: 0 2px 16px 0 rgba(5, 5, 6, 15%), 0 4px 8px 0 rgba(5, 5, 6, 35%);
background: #1d1d1d !important;
}
.autocomplete-items {
position: absolute;
border-bottom: none;
border-top: none;
z-index: 99;
/*position the autocomplete items to be the same width as the container:*/
top: 100%;
left: 0;
right: 0;
}
.autocomplete-items div {
padding: 10px;
cursor: pointer;
}
details summary {
margin-bottom: 20px;
font-weight: bold;
padding-left: 10px;
}
details summary span {
font-weight: normal;
}
#lingva-iframe {
width: 100%;
height: 650px;
border: 0;
}
.ip-address-div {
padding-bottom: 0 !important;
}
.ip-text-div {
padding-top: 0 !important;
}
.footer {
text-align: center;
}
.site-favicon {
float: left;
width: 18px;
height: 18px;
padding: 0 5px 0 0;
}
@media (max-width: 600px) {
.site-favicon {
display: none;
}
}
@media (min-width: 601px) {
.has-favicon .sCuL3 {
padding: 0 0 0 30px !important;
}
}
#flex_text_audio_icon_chunk {
display: none;
}
.rp-credit {
padding: 10px 0 0 0;
margin: 0 auto;
display: table;
text-align: center;
}
audio {
display: block;
margin-right: auto;
padding-bottom: 5px;
}
@media (min-width: 801px) {
body {
max-width: 900px !important;
min-width: 0 !important;
}
}
@media (max-width: 801px) {
details summary {
margin-bottom: 10px !important;
}
}
.header-button {
background-color: inherit;
color: #fff;
cursor: pointer;
padding: 10px 35px;
display: table-cell;
align-items: center;
text-transform: uppercase;
border: none;
border-bottom-width: medium;
border-bottom-style: none;
border-bottom-color: currentcolor;
border-bottom: 2px solid transparent;
}
.sCuL3 {
top: -3px !important;
}
.l97dzf {
font-weight: 600 !important;
padding: 0 !important;
}
.tAd8D.AP7Wnd {
padding: 20px 0 20px 40px !important;
}
.skVgpb {
display: ruby !important;
padding: 10px 0 0 40px !important;
}
.VGHMXd {
display: contents !important;
}
.vbShOe {
padding: 10px 0 0 10px !important;
}
/* .BNeawe.uEec3.AP7Wnd {
display: none !important;
} */
.RWuggc.kCrYT > div > .BNeawe.tAd8D.AP7Wnd {
padding: 20px 0 0 0 !important;
}
.BVG0Nb {
border: none !important;
}
.Xdlr0d {
scrollbar-color: var(--primary-grad3) #0000002e;
scrollbar-width: thin;
}
* > span > .BNeawe.uEec3.AP7Wnd {
display: none !important;
}
.AVsepf {
padding: 0 0 10px 10px !important;
}
.ieB2Dd {
padding: 10px 0px 0px 0px !important;
background-color: transparent !important;
}
.idg8be {
border-spacing: 20px 0 !important;
padding: 0 !important;
}
.pijXPc {
border-radius: 0 !important;
}
.Xdlr0d {
padding: 15px 5px !important;
}
.EtOod>*:first-child {
border-radius: 0 !important;
}
.OxTOff {
border-radius: 0 !important;
}

81
app/static/css/variables.css Executable file
View file

@ -0,0 +1,81 @@
/* Colors */
:root {
/* LIGHT THEME COLORS */
--whoogle-logo: #685e79;
--whoogle-page-bg: #ffffff;
--whoogle-element-bg: #4285f4;
--whoogle-text: #000000;
--whoogle-contrast-text: #ffffff;
--whoogle-secondary-text: #70757a;
--whoogle-result-bg: #ffffff;
--whoogle-result-title: #1967d2;
--whoogle-result-url: #0d652d;
--whoogle-result-visited: #4b11a8;
/* DARK THEME COLORS */
/* --primary: #ff1a66;
--primary-active: #e5175c;
--primary-grad2: #ff5448;
--grad2-active: #e54b41;
--primary-grad3: #ff8448;
--grad3-active: #e57741; */
--body-bg: #0a0c0d;
--body-color: #e4e8ea;
--ad-bg: #282e31;
--gray-200: #1b1f21;
--gray-300: #23292c;
--gray-400: #282e31;
--gray-800: #9aa2a6;
--gray-900: #50585c;
--gray-950: #31373a;
--danger: #ff3333;
--danger-active: #e52e2e;
--aa: #D1D1D1;
--ab: #DBDBDB;
--bb: #85C7F2;
--ba: #636363;
--bc: #4C4C4C;
--qw: #F3E8EE;
--primary-grad2: #BACDB0;
--primary: #729B79;
--primary-grad3: #475B63;
--ew: #2E2C2F;
--re: #2D3142;
--er: #BFC0C0;
--rr: #FFFFFF;
--ee: #EF8354;
--rw: #4F5D75;
}
#whoogle-w {
fill: #4285f4;
}
#whoogle-h {
fill: #ea4335;
}
#whoogle-o-1 {
fill: #fbbc05;
}
#whoogle-o-2 {
fill: #4285f4;
}
#whoogle-g {
fill: #34a853;
}
#whoogle-l {
fill: #ea4335;
}
#whoogle-e {
fill: #fbbc05;
}

BIN
app/static/img/.DS_Store vendored Executable file

Binary file not shown.

BIN
app/static/img/favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
app/static/img/favicon/.DS_Store vendored Executable file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,44 @@
{
"name": "RaveSearch",
"short_name": "RaveSearch",
"display": "fullscreen",
"scope": "/",
"icons": [
{
"src": "favicon-32x32.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "favicon-32x32.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "favicon-32x32.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "android-chrome-192x192.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "android-chrome-192x192.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "android-chrome-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -0,0 +1,523 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="352.000000pt" height="352.000000pt" viewBox="0 0 352.000000 352.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,352.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1300 3362 c0 -104 30 -223 80 -322 25 -48 110 -180 117 -180 3 0 2
15 -1 33 -5 24 -4 27 3 12 5 -11 10 -30 10 -41 1 -12 25 -57 54 -101 92 -139
155 -295 160 -400 2 -40 1 -73 -3 -73 -3 0 -14 23 -24 51 -10 28 -25 53 -32
56 -7 3 -27 29 -44 59 -16 30 -34 54 -39 54 -5 0 -30 34 -56 75 -25 41 -51 77
-56 81 -6 3 2 -12 17 -35 15 -23 25 -43 23 -46 -3 -2 -41 29 -85 70 -85 78
-153 173 -259 365 -60 109 -82 140 -97 140 -17 0 -7 -37 22 -83 38 -61 38 -67
-1 -22 -17 19 -34 33 -39 30 -18 -11 -32 -94 -22 -124 10 -27 9 -30 -8 -28
-28 4 -49 26 -81 83 -16 28 -57 75 -92 105 -34 29 -49 39 -34 22 17 -19 25
-36 21 -46 -4 -12 25 -48 107 -130 119 -118 213 -226 205 -235 -10 -10 -181
135 -210 178 -17 24 -32 38 -34 32 -2 -7 -40 7 -102 38 -60 30 -106 47 -117
44 -15 -5 -15 -4 -4 4 12 9 9 12 -15 18 -16 4 -31 10 -34 14 -6 9 -110 62
-110 56 0 -3 90 -77 200 -166 110 -88 200 -163 200 -165 0 -2 -17 -2 -37 1
-27 3 -14 -4 42 -24 101 -36 140 -56 192 -99 50 -40 152 -168 113 -141 -161
112 -187 120 -43 14 100 -73 124 -96 103 -96 -22 0 -170 81 -265 144 -54 36
-100 65 -102 63 -1 -2 11 -16 27 -32 17 -16 30 -34 30 -41 0 -7 14 -23 32 -35
30 -22 31 -23 10 -32 -31 -14 -141 16 -232 64 -77 40 -220 151 -220 170 0 7
17 9 53 4 67 -9 59 -1 -20 19 -34 9 -112 36 -174 61 -61 25 -113 44 -115 43
-5 -6 67 -113 75 -113 5 0 13 -4 18 -9 5 -5 3 -6 -4 -2 -17 10 -16 0 3 -29 15
-24 15 -25 -2 -26 -34 -1 16 -17 55 -18 19 -1 68 -17 108 -37 71 -35 90 -50
80 -60 -6 -6 -122 32 -162 54 -16 9 -34 17 -40 17 -5 1 -32 10 -60 20 -44 17
-47 20 -25 25 20 5 16 8 -23 14 -27 4 -51 5 -54 3 -2 -3 22 -33 54 -67 l59
-63 88 -2 c86 -3 207 -32 251 -62 11 -7 27 -13 37 -13 10 0 18 -4 18 -8 0 -5
-26 -10 -57 -11 -32 -1 -74 -6 -93 -11 -19 -5 -55 -11 -80 -14 -92 -12 -166
-24 -209 -36 -24 -6 -62 -13 -85 -15 -38 -3 -66 -10 -91 -23 -26 -14 17 25 47
42 28 17 31 17 19 3 -7 -9 -9 -17 -3 -17 5 0 15 7 22 15 11 13 93 55 108 55 3
0 -3 -10 -14 -21 -16 -18 -12 -17 20 5 21 14 47 26 57 26 39 0 25 17 -24 30
-29 7 -57 17 -64 22 -8 6 -18 5 -30 -2 -17 -11 -17 -11 0 -6 11 3 24 3 30 -1
7 -5 28 -14 47 -20 34 -12 33 -12 -23 -8 -32 2 -65 9 -72 15 -8 7 -19 9 -25 5
-20 -12 -9 -23 33 -30 38 -8 39 -9 13 -13 -38 -5 -78 6 -70 19 3 6 -9 3 -27
-7 -36 -18 -155 -132 -147 -140 3 -2 81 10 174 27 155 29 180 31 303 26 74 -4
138 -10 142 -14 5 -5 -70 -29 -167 -53 l-175 -45 53 -9 53 -9 -55 -13 c-30 -7
-66 -13 -80 -14 -42 -1 -1 -20 43 -20 73 0 27 -18 -65 -25 -137 -11 -146 -20
-37 -34 97 -13 125 -19 118 -25 -2 -2 -45 -32 -95 -67 l-90 -64 113 -1 c71 -1
118 -5 126 -13 8 -8 7 -11 -6 -11 -25 0 -206 -98 -267 -145 -50 -39 -89 -75
-79 -75 21 1 179 39 165 39 -31 1 -13 10 38 20 26 5 49 5 52 0 3 -5 13 -5 24
1 24 13 33 13 25 0 -13 -21 49 -8 128 27 76 34 130 66 82 49 -40 -15 -142 -34
-205 -39 l-65 -6 144 71 c188 93 305 117 297 62 -1 -9 -9 -20 -17 -25 -10 -6
-6 -9 15 -9 23 0 27 -3 23 -20 -4 -15 0 -20 14 -20 24 0 24 -9 2 -25 -15 -11
-10 -12 30 -8 38 5 53 1 81 -18 26 -18 32 -27 24 -36 -8 -10 -4 -13 20 -13 40
0 61 -25 33 -40 -12 -6 -21 -16 -21 -22 0 -9 3 -9 12 0 15 15 70 16 85 1 15
-15 -12 -59 -36 -59 -9 0 -23 -5 -31 -11 -11 -7 -6 -9 20 -4 19 4 45 18 58 32
15 16 37 27 59 30 20 2 43 7 52 12 20 10 43 3 35 -11 -3 -5 13 4 37 22 32 23
51 30 70 26 18 -3 32 1 44 14 9 10 30 23 46 29 l29 10 -16 -25 c-9 -13 -15
-27 -12 -29 2 -3 17 16 33 40 28 46 42 58 31 29 -5 -14 -4 -15 9 -4 18 15 21
-1 5 -31 -8 -13 -7 -19 0 -19 6 0 7 -6 1 -17 -4 -10 -25 -67 -45 -128 l-36
-110 5 70 5 69 -39 -66 c-68 -117 -115 -168 -105 -115 3 16 0 14 -10 -8 -8
-16 -45 -60 -83 -96 -37 -37 -78 -80 -91 -95 l-23 -29 22 34 c13 20 18 36 12
40 -6 3 -8 29 -6 56 2 28 4 57 5 65 0 8 11 26 23 40 12 14 30 48 39 77 10 28
31 71 48 95 l31 43 -47 -45 c-25 -24 -46 -40 -46 -36 0 4 -27 -17 -61 -48 -57
-53 -172 -136 -187 -136 -4 0 -23 -13 -42 -28 -45 -37 -80 -56 -80 -43 0 5 5
13 11 17 6 4 25 36 41 73 17 36 37 74 46 84 8 9 12 17 8 17 -4 0 2 17 13 38
23 43 52 112 46 112 -2 0 -34 -39 -72 -86 -43 -54 -95 -106 -142 -140 -73 -54
-91 -62 -91 -45 0 6 -23 0 -51 -11 -34 -15 -46 -24 -37 -29 10 -7 9 -9 -4 -9
-10 0 -32 -14 -50 -31 -20 -18 -25 -26 -13 -21 11 5 40 13 65 16 l45 7 -35
-30 c-40 -35 -31 -40 18 -11 l34 21 -7 -22 c-6 -17 6 -11 51 28 33 28 69 56
82 62 43 22 14 -40 -42 -91 -33 -31 -45 -47 -34 -48 9 0 29 10 45 23 51 41 82
57 113 57 34 0 35 -2 18 -33 -6 -12 21 8 61 46 70 65 91 79 91 56 0 -6 16 1
35 15 37 28 37 28 49 -25 12 -55 -75 -174 -189 -258 -27 -21 -83 -57 -122 -80
-77 -45 -90 -57 -47 -41 14 6 27 10 29 10 2 0 -9 -30 -25 -67 -24 -54 -26 -62
-9 -40 19 25 58 40 44 17 -3 -6 -2 -10 3 -10 13 0 -33 -92 -57 -114 -12 -11
-21 -25 -21 -32 0 -7 -5 -16 -10 -19 -7 -4 -9 2 -4 19 12 50 -14 25 -43 -41
-31 -69 -76 -203 -70 -209 4 -4 110 122 150 177 16 21 33 39 39 39 6 0 3 -10
-8 -21 -11 -13 3 -6 32 14 l51 37 6 -98 c8 -119 35 -220 89 -330 45 -89 68
-118 68 -86 1 24 26 229 39 314 14 86 14 110 1 90 -7 -10 -10 21 -9 97 1 103
2 113 24 133 l24 23 -5 -54 c-3 -30 -7 -74 -9 -99 -4 -41 -3 -42 4 -10 13 51
61 183 72 194 15 16 10 -78 -6 -117 -24 -58 -17 -56 18 5 18 31 41 66 50 76 9
10 17 23 17 28 0 5 24 35 53 68 l54 59 7 -34 c8 -42 0 -133 -21 -221 -17 -78
-12 -109 10 -58 l15 35 10 -25 c29 -73 108 -228 166 -323 36 -60 66 -111 66
-113 0 -3 -6 -2 -12 2 -8 4 -10 3 -5 -2 5 -5 14 -9 20 -9 7 0 26 -25 42 -56
41 -76 129 -218 135 -218 6 0 94 142 135 218 17 31 35 56 42 56 6 0 15 4 20 9
5 5 3 6 -4 2 -7 -4 -13 -5 -13 -2 0 2 30 53 66 113 58 95 137 250 166 323 l10
25 15 -35 c22 -51 27 -20 10 58 -21 88 -29 179 -21 221 l7 34 54 -59 c29 -33
53 -63 53 -68 0 -5 8 -18 17 -28 9 -10 32 -45 50 -76 35 -61 42 -63 18 -5 -16
39 -21 133 -6 117 11 -11 59 -143 72 -194 7 -32 8 -31 4 10 -2 25 -6 69 -9 99
l-5 54 24 -23 c22 -20 23 -30 24 -133 1 -76 -2 -107 -9 -97 -13 20 -13 -4 1
-90 13 -85 38 -290 39 -314 0 -32 23 -3 68 86 54 110 81 211 89 330 l6 98 51
-37 c29 -20 43 -27 32 -14 -11 11 -14 21 -8 21 6 0 23 -18 39 -39 40 -55 146
-181 150 -177 6 6 -39 140 -70 209 -29 66 -55 91 -43 41 5 -17 3 -23 -4 -19
-5 3 -10 12 -10 19 0 7 -9 21 -21 32 -24 22 -70 114 -57 114 5 0 6 5 3 10 -14
23 25 8 44 -18 17 -21 15 -13 -9 41 -16 37 -27 67 -25 67 2 0 15 -4 29 -10 43
-16 30 -4 -46 41 -40 23 -95 59 -123 80 -114 84 -201 203 -189 258 12 53 12
53 49 25 19 -14 35 -21 35 -15 0 23 21 9 91 -56 40 -38 67 -58 61 -46 -17 31
-16 33 18 33 31 0 62 -16 113 -57 16 -13 36 -23 45 -23 11 1 -1 17 -34 48 -56
51 -85 113 -42 91 13 -6 49 -34 82 -62 45 -39 57 -45 51 -28 l-7 22 34 -21
c49 -29 58 -24 18 11 l-35 30 45 -7 c25 -3 54 -11 65 -16 12 -5 7 3 -13 21
-18 17 -40 31 -50 31 -13 0 -14 2 -4 9 9 5 -3 14 -37 29 -28 11 -51 17 -51 11
0 -17 -18 -9 -91 45 -47 34 -99 86 -142 140 -38 47 -70 86 -72 86 -6 0 23 -69
46 -113 11 -20 17 -37 13 -37 -4 0 0 -8 8 -17 9 -10 29 -48 46 -84 16 -37 35
-69 41 -73 6 -4 11 -12 11 -17 0 -13 -35 6 -80 43 -19 15 -38 28 -42 28 -15 0
-130 83 -187 136 -34 31 -61 52 -61 48 0 -4 -21 12 -46 36 l-47 45 31 -43 c17
-24 38 -67 48 -95 9 -29 27 -63 39 -77 12 -14 23 -32 23 -40 1 -8 3 -37 5 -65
2 -27 0 -53 -6 -56 -6 -4 -1 -20 12 -40 l22 -34 -23 29 c-13 15 -54 58 -91 95
-38 36 -75 80 -83 96 -10 22 -13 24 -10 8 10 -53 -37 -2 -105 115 l-39 66 5
-69 5 -70 -36 110 c-20 61 -41 118 -45 128 -6 11 -5 17 1 17 7 0 8 6 0 19 -16
30 -13 46 5 31 13 -11 14 -10 9 4 -11 29 3 17 31 -29 16 -24 31 -43 33 -40 3
2 -3 16 -12 29 l-16 25 29 -10 c16 -6 37 -19 46 -29 12 -13 26 -17 44 -14 19
4 38 -3 70 -26 24 -18 40 -27 37 -22 -8 14 15 21 35 11 9 -5 32 -10 52 -12 22
-3 44 -14 59 -30 13 -14 39 -28 58 -32 26 -5 31 -3 20 4 -8 6 -22 11 -31 11
-24 0 -51 44 -36 59 15 15 70 14 85 -1 9 -9 12 -9 12 0 0 6 -9 16 -21 22 -28
15 -7 40 33 40 24 0 28 3 20 13 -8 9 -2 18 24 36 28 19 43 23 81 18 40 -4 45
-3 30 8 -22 16 -22 25 2 25 14 0 18 5 14 20 -4 17 0 20 23 20 21 0 25 3 15 9
-8 5 -16 16 -17 25 -8 55 109 31 297 -62 l144 -71 -65 6 c-63 5 -165 24 -205
39 -48 17 6 -15 82 -49 79 -35 141 -48 128 -27 -8 13 1 13 25 0 11 -6 21 -6
24 -1 3 5 26 5 52 0 51 -10 69 -19 38 -20 -14 0 144 -38 165 -39 10 0 -29 36
-79 75 -61 47 -242 145 -267 145 -13 0 -14 3 -6 11 8 8 55 12 126 13 l113 1
-90 64 c-50 35 -93 65 -95 67 -7 6 21 12 118 25 109 14 100 23 -37 34 -92 7
-138 25 -65 25 44 0 85 19 43 20 -14 1 -50 7 -80 14 l-55 13 53 9 53 9 -175
45 c-97 24 -172 48 -167 53 4 4 68 10 142 14 123 5 148 3 303 -26 93 -17 171
-29 174 -27 8 8 -111 122 -147 140 -18 10 -30 13 -27 7 8 -13 -32 -24 -70 -19
-26 4 -25 5 14 13 41 7 52 18 32 30 -6 4 -17 2 -25 -5 -7 -6 -40 -13 -72 -15
-56 -4 -57 -4 -23 8 19 6 40 15 47 20 6 4 19 4 30 1 17 -5 17 -5 0 6 -12 7
-22 8 -30 2 -7 -5 -35 -15 -64 -22 -49 -13 -63 -30 -24 -30 10 0 36 -12 57
-26 32 -22 36 -23 20 -5 -11 11 -17 21 -14 21 15 0 97 -42 108 -55 7 -8 17
-15 22 -15 6 0 4 8 -3 17 -12 14 -9 14 19 -3 41 -24 64 -48 36 -38 -10 4 -23
9 -28 12 -5 3 -28 6 -51 8 -23 1 -62 8 -86 14 -43 12 -117 24 -209 36 -25 3
-61 9 -80 14 -19 5 -61 10 -92 11 -32 1 -58 6 -58 11 0 4 8 8 18 8 10 0 26 6
37 13 44 30 165 59 251 62 l88 2 59 63 c32 34 56 64 54 67 -3 2 -27 1 -54 -3
-39 -6 -43 -9 -23 -14 22 -5 19 -8 -25 -25 -28 -10 -54 -19 -60 -20 -5 0 -24
-8 -40 -17 -40 -22 -156 -60 -162 -54 -10 10 9 25 80 60 40 20 89 36 108 37
39 1 89 17 55 18 -17 1 -17 2 -2 26 19 29 20 39 4 29 -8 -4 -10 -3 -5 2 5 5
13 9 18 9 8 0 80 107 75 113 -2 1 -54 -18 -115 -43 -62 -25 -140 -52 -174 -61
-79 -20 -87 -28 -19 -19 35 5 52 3 52 -4 0 -19 -143 -130 -220 -170 -91 -48
-201 -78 -232 -64 -21 9 -20 10 10 32 18 12 32 28 32 35 0 7 13 25 30 41 16
16 28 30 27 32 -2 2 -48 -27 -102 -63 -95 -63 -243 -144 -265 -144 -21 0 3 23
103 96 144 106 118 98 -43 -14 -39 -27 63 101 113 141 52 43 91 63 192 99 56
20 69 27 43 24 -21 -3 -38 -3 -38 -1 0 2 90 77 200 165 110 89 200 163 200
166 0 6 -104 -47 -110 -56 -3 -4 -18 -10 -34 -14 -24 -6 -27 -9 -15 -18 11 -8
11 -9 -4 -4 -11 3 -57 -14 -117 -44 -62 -31 -100 -45 -102 -38 -2 6 -17 -8
-34 -32 -29 -43 -200 -188 -210 -178 -8 9 86 117 205 235 82 82 111 118 107
130 -4 10 4 27 21 46 15 17 0 7 -34 -22 -35 -30 -76 -77 -92 -105 -32 -57 -53
-79 -81 -83 -17 -2 -18 1 -8 28 10 30 -4 113 -22 124 -5 3 -22 -11 -39 -30
-39 -45 -39 -39 -1 22 29 46 39 83 22 83 -15 0 -37 -31 -97 -140 -106 -192
-174 -287 -259 -365 -44 -41 -82 -72 -85 -70 -2 3 8 23 23 46 15 23 23 38 17
35 -5 -4 -31 -40 -56 -81 -26 -41 -51 -75 -56 -75 -5 0 -23 -24 -39 -54 -17
-30 -37 -56 -44 -59 -7 -3 -22 -28 -32 -56 -10 -28 -21 -51 -24 -51 -4 0 -5
33 -3 73 5 105 68 261 161 400 28 44 52 89 53 101 0 11 5 30 10 41 7 15 8 12
3 -12 -3 -18 -4 -33 -1 -33 6 0 92 131 116 178 54 106 84 232 79 332 l-3 65
-24 -65 c-13 -36 -42 -100 -63 -142 -21 -43 -38 -83 -38 -90 0 -7 -10 -26 -21
-43 -12 -16 -18 -22 -15 -11 7 20 -32 116 -47 116 -5 0 -6 -8 -3 -17 6 -14 3
-14 -15 3 -13 11 -19 13 -13 4 4 -8 7 -23 5 -34 -1 -11 2 -16 8 -12 6 4 11 2
11 -3 0 -6 -4 -11 -10 -11 -5 0 -10 -9 -10 -20 0 -11 -6 -20 -12 -20 -10 0
-10 -2 0 -9 10 -6 10 -11 -3 -21 -13 -11 -18 -11 -24 -1 -5 7 -10 12 -12 10
-2 -1 -15 2 -28 6 l-24 7 24 13 c13 7 27 11 30 9 4 -2 5 4 2 15 -3 13 -11 17
-24 14 -10 -3 -27 1 -37 8 -15 12 -16 12 -5 -2 10 -12 10 -18 0 -28 -10 -11
-8 -11 11 -1 13 6 26 9 29 7 2 -3 -5 -10 -16 -17 -21 -11 -21 -11 9 -41 30
-30 40 -69 19 -69 -6 0 -9 9 -6 20 3 11 1 18 -4 14 -5 -3 -9 -12 -9 -19 0 -9
-6 -12 -17 -8 -15 5 -15 3 2 -11 18 -16 18 -17 2 -12 -10 3 -21 6 -23 6 -2 0
-4 9 -4 20 0 11 -4 20 -10 20 -5 0 -10 -6 -10 -13 0 -15 45 -57 61 -57 6 0 7
-5 2 -12 -8 -13 -10 -19 -16 -50 -1 -9 -14 -26 -27 -37 l-25 -20 23 -1 c16 0
21 -4 15 -12 -4 -7 -6 -21 -5 -30 2 -13 -8 -20 -42 -29 l-46 -11 0 -184 c0
-116 -4 -184 -10 -184 -6 0 -10 68 -10 183 l0 182 -31 17 c-17 10 -28 22 -24
27 3 5 1 12 -5 16 -5 3 -10 11 -10 16 0 6 5 7 10 4 14 -9 58 19 93 59 20 22
30 27 34 18 4 -10 11 -5 24 15 10 15 19 33 19 38 -1 6 -5 2 -11 -9 -9 -15 -13
-16 -20 -5 -7 12 -9 11 -9 -1 0 -8 -18 -26 -40 -39 -21 -14 -37 -27 -35 -31 2
-3 -4 -6 -13 -6 -9 -1 -24 -3 -32 -5 -8 -2 -15 3 -15 11 -2 26 5 30 28 18 20
-10 21 -10 8 6 -13 15 -12 16 15 6 25 -9 26 -8 12 3 -15 12 -15 14 0 25 14 10
13 11 -5 2 -30 -14 -48 -13 -37 3 11 14 11 37 1 54 -4 7 1 14 14 18 14 5 18
10 11 17 -6 6 -16 7 -24 2 -20 -13 -40 -14 -57 -3 -12 8 -10 8 8 3 22 -6 23
-4 19 24 -4 26 -2 29 11 22 13 -6 16 -3 16 16 0 13 -3 24 -7 24 -5 0 -8 10 -7
23 0 21 1 21 9 2 6 -14 9 -16 10 -5 2 8 2 18 1 23 -2 13 17 8 34 -9 9 -9 14
-22 12 -29 -3 -6 1 -12 9 -12 7 0 11 4 8 8 -5 9 26 49 39 49 4 0 5 -4 2 -10
-3 -5 -2 -10 3 -10 6 0 12 7 16 15 3 8 13 15 21 15 9 0 13 5 10 10 -3 6 -15
10 -26 10 -12 0 -17 -4 -13 -11 5 -8 1 -8 -14 0 -25 13 -43 15 -23 2 12 -8 12
-12 -2 -26 -15 -15 -19 -15 -39 -1 -12 9 -17 16 -10 16 6 0 12 5 12 11 0 8 -4
8 -13 1 -8 -7 -29 -9 -50 -5 -25 4 -37 2 -37 -6 0 -8 8 -10 22 -5 20 6 21 5 9
-10 -20 -24 -40 -20 -36 9 1 14 -2 28 -7 31 -5 4 8 17 29 30 35 21 36 23 13
24 -15 0 -40 -14 -63 -35 -21 -19 -41 -35 -45 -35 -4 0 2 7 12 15 11 8 16 15
11 15 -4 0 0 10 11 21 19 22 19 22 -1 10 -11 -7 -31 -28 -45 -46 -19 -26 -27
-30 -33 -19 -4 8 -5 19 -2 24 3 6 4 10 1 10 -13 0 -56 -105 -50 -124 4 -15 -1
-12 -15 9 -12 17 -21 36 -21 43 0 7 -17 47 -38 90 -21 42 -49 105 -62 140 -12
34 -24 62 -26 62 -2 0 -4 -30 -4 -68z m320 -182 c0 -5 -2 -10 -4 -10 -3 0 -8
5 -11 10 -3 6 -1 10 4 10 6 0 11 -4 11 -10z m-750 -112 c0 -6 -7 -5 -15 2 -8
7 -15 17 -15 22 0 6 7 5 15 -2 8 -7 15 -17 15 -22z m1780 7 c-7 -9 -15 -13
-18 -10 -3 2 1 11 8 20 7 9 15 13 18 10 3 -2 -1 -11 -8 -20z m-1906 -90 c11
-8 16 -15 10 -15 -5 0 -18 7 -28 15 -11 8 -16 15 -10 15 5 0 18 -7 28 -15z
m922 0 c-11 -8 -25 -15 -30 -15 -6 0 -2 7 8 15 11 8 25 15 30 15 6 0 2 -7 -8
-15z m1108 0 c-10 -8 -23 -15 -28 -15 -6 0 -1 7 10 15 10 8 23 15 28 15 6 0 1
-7 -10 -15z m-1219 -136 c47 -92 56 -131 16 -65 -21 34 -54 116 -46 116 3 0
16 -23 30 -51z m410 10 c-15 -40 -53 -109 -61 -109 -5 0 12 42 41 99 31 62 39
66 20 10z m-736 -44 c0 -5 -13 6 -29 25 -17 19 -30 39 -30 45 0 5 14 -6 30
-25 16 -19 29 -39 29 -45z m1076 30 c-15 -20 -29 -34 -32 -31 -3 2 7 21 22 41
15 19 30 33 33 31 3 -3 -8 -22 -23 -41z m-1074 -105 c57 -55 108 -100 113
-100 11 0 -10 26 -78 98 -28 28 -46 52 -41 52 12 0 121 -103 158 -148 15 -19
27 -28 27 -22 0 18 56 -20 120 -82 68 -67 139 -197 142 -263 1 -22 6 -46 11
-53 5 -7 8 -15 5 -17 -2 -3 -17 21 -34 51 -16 31 -34 56 -39 56 -18 -1 -36 21
-62 71 -37 73 -117 191 -121 178 -2 -6 22 -51 53 -102 65 -104 69 -142 7 -53
-43 59 -199 200 -211 189 -3 -4 19 -29 49 -57 139 -128 161 -150 176 -181 15
-28 9 -25 -44 26 -33 31 -64 57 -69 57 -5 0 -31 21 -59 48 -28 26 -74 67 -103
90 -36 30 -50 49 -46 61 3 12 -10 25 -50 49 -31 18 -53 37 -50 42 3 5 29 0 58
-10 28 -9 65 -21 81 -24 l28 -7 -25 23 c-49 46 -120 128 -109 128 5 0 57 -45
113 -100z m1114 48 c-26 -29 -59 -63 -72 -76 l-25 -23 28 7 c16 3 53 15 81 24
29 10 55 15 58 10 3 -5 -19 -24 -50 -42 -40 -24 -53 -37 -50 -49 4 -12 -10
-31 -46 -61 -29 -23 -75 -64 -103 -90 -28 -27 -54 -48 -59 -48 -5 0 -36 -26
-69 -57 -53 -51 -59 -54 -44 -26 15 31 37 53 176 181 30 28 52 53 49 57 -12
11 -168 -130 -211 -189 -62 -89 -58 -51 7 53 31 51 55 96 53 102 -4 13 -84
-105 -122 -179 -25 -49 -41 -67 -60 -67 -5 0 -23 -26 -40 -57 -16 -32 -32 -56
-34 -53 -3 2 0 10 5 17 5 7 10 31 11 53 3 66 74 196 142 263 64 62 120 100
120 82 0 -6 12 3 27 22 37 45 146 148 158 148 5 0 -13 -24 -41 -52 -68 -72
-89 -98 -78 -98 5 0 56 45 113 100 110 106 155 135 76 48z m-1807 -81 c34 -33
46 -63 13 -33 -11 10 -24 15 -29 12 -13 -8 -112 72 -112 90 0 19 82 -25 128
-69z m2552 69 c0 -18 -99 -98 -112 -90 -5 3 -18 -2 -29 -12 -33 -30 -21 0 14
33 25 24 109 80 125 83 1 0 2 -6 2 -14z m-1386 -32 c17 -6 22 -24 8 -24 -5 0
-17 7 -28 15 -19 15 -8 20 20 9z m112 -9 c-23 -17 -36 -19 -36 -6 0 11 18 19
40 20 11 0 10 -3 -4 -14z m-1408 -99 c60 -28 86 -55 40 -41 -31 10 -105 53
-114 66 -4 7 -3 10 2 8 5 -2 37 -17 72 -33z m2752 19 c-16 -20 -119 -69 -127
-61 -9 8 -6 10 62 44 69 34 82 38 65 17z m-2895 -195 c4 -7 -5 -9 -26 -5 -44
8 -50 15 -11 15 17 0 34 -5 37 -10z m3034 6 c-10 -11 -70 -16 -64 -6 3 5 20
10 37 10 17 0 29 -2 27 -4z m-2205 -15 c3 -5 18 -12 33 -16 36 -8 93 -52 93
-70 0 -7 12 -27 26 -44 14 -17 23 -31 20 -31 -19 0 -105 52 -127 77 -39 43
-72 93 -60 93 5 0 12 -4 15 -9z m124 -20 c32 -16 75 -48 95 -70 20 -23 37 -38
37 -34 0 3 -7 15 -17 25 -9 10 -14 21 -10 24 13 13 131 -71 122 -87 -4 -5 1
-6 9 -3 24 9 66 -74 66 -132 l-1 -49 -14 32 c-8 18 -25 38 -39 44 -14 7 -44
38 -68 71 -24 32 -44 54 -46 48 -2 -6 -22 9 -44 32 -40 42 -40 42 4 -12 24
-30 48 -61 53 -69 14 -24 -82 54 -111 90 -15 19 -46 53 -68 77 -22 23 -37 42
-33 42 4 0 34 -13 65 -29z m1096 -13 c-22 -24 -53 -58 -68 -77 -29 -36 -125
-114 -111 -90 5 8 29 39 53 69 44 54 44 54 4 12 -22 -23 -42 -38 -44 -32 -2 6
-22 -16 -46 -48 -24 -33 -54 -64 -68 -71 -14 -6 -31 -26 -39 -44 l-14 -32 -1
49 c0 58 42 141 66 132 8 -3 13 -2 9 3 -9 16 109 100 122 87 4 -3 -1 -14 -10
-24 -10 -10 -17 -22 -17 -25 0 -4 17 11 37 34 32 36 131 97 158 99 5 0 -9 -19
-31 -42z m95 10 c-39 -58 -73 -89 -121 -114 -61 -31 -65 -30 -34 7 14 17 26
37 26 44 0 18 57 62 93 70 15 4 30 11 33 16 3 5 10 9 15 9 6 0 0 -15 -12 -32z
m-1702 6 c-3 -3 -12 -4 -19 -1 -8 3 -5 6 6 6 11 1 17 -2 13 -5z m2110 0 c-3
-3 -12 -4 -19 -1 -8 3 -5 6 6 6 11 1 17 -2 13 -5z m-1255 -131 c30 -36 30 -47
0 -19 -18 17 -32 31 -32 33 0 9 19 1 32 -14z m408 14 c0 -2 -14 -16 -32 -33
-30 -28 -30 -17 0 19 13 15 32 23 32 14z m-1263 -29 c-2 -7 3 -18 12 -25 14
-11 14 -13 -4 -27 -11 -8 -26 -16 -32 -16 -27 0 -11 -18 22 -24 47 -9 52 -22
14 -41 l-31 -17 32 -19 c20 -13 30 -25 26 -34 -3 -8 0 -15 7 -15 19 -1 -35
-38 -62 -43 -52 -10 -210 51 -234 90 -4 7 -17 13 -27 13 -26 0 -26 17 0 25 11
3 25 15 30 25 6 10 28 29 50 41 22 13 40 27 40 31 0 5 19 17 43 28 49 22 121
27 114 8z m924 -10 c11 -21 11 -22 -4 -9 -10 7 -17 17 -17 22 0 15 9 10 21
-13z m245 -6 c-18 -16 -18 -16 -6 6 6 13 14 21 18 18 3 -4 -2 -14 -12 -24z
m1028 8 c25 -10 46 -23 46 -28 0 -4 18 -18 40 -31 22 -12 44 -31 50 -41 5 -10
19 -22 30 -25 26 -8 26 -25 0 -25 -10 0 -23 -6 -27 -13 -24 -39 -182 -100
-234 -90 -27 5 -81 42 -62 43 7 0 10 7 7 15 -4 9 6 21 26 34 l32 19 -31 17
c-38 19 -33 32 14 41 33 6 49 24 23 24 -7 0 -22 8 -33 16 -18 14 -18 16 -4 27
9 7 14 18 12 25 -7 19 59 15 111 -8z m-1681 -67 c3 -5 -6 -16 -18 -26 -27 -19
-45 -6 -25 18 13 16 36 20 43 8z m1097 -8 c20 -24 2 -37 -25 -18 -12 10 -21
21 -18 26 7 12 30 8 43 -8z m-842 -60 c-3 -3 -9 2 -12 12 -6 14 -5 15 5 6 7
-7 10 -15 7 -18z m555 10 c-3 -9 -8 -14 -10 -11 -3 3 -2 9 2 15 9 16 15 13 8
-4z m-713 -9 c0 -3 -4 -8 -10 -11 -5 -3 -10 -1 -10 4 0 6 5 11 10 11 6 0 10
-2 10 -4z m880 -6 c0 -5 -2 -10 -4 -10 -3 0 -8 5 -11 10 -3 6 -1 10 4 10 6 0
11 -4 11 -10z m-470 -29 c0 -4 9 -7 20 -7 11 0 20 3 20 7 0 4 10 9 21 12 17 4
19 3 10 -6 -20 -20 -11 -30 11 -11 18 16 21 17 15 2 -3 -9 -3 -19 1 -21 4 -2
1 -2 -5 -1 -18 5 -16 -7 4 -21 14 -10 15 -16 5 -28 -18 -21 -55 -30 -48 -11 5
14 1 16 -34 16 -35 0 -39 -2 -34 -16 7 -19 -30 -10 -48 11 -10 12 -9 18 5 28
20 14 22 26 5 21 -7 -1 -10 -1 -6 1 4 2 4 12 1 21 -6 15 -3 14 15 -2 22 -19
31 -9 11 11 -9 9 -7 10 10 6 11 -3 21 -8 21 -12z m-112 -47 c12 -8 12 -19 5
-57 -9 -42 -8 -49 15 -82 16 -23 28 -31 33 -24 4 6 12 8 18 5 25 -16 9 12 -24
41 -19 17 -35 37 -35 45 1 7 14 -2 30 -21 16 -19 31 -33 33 -30 2 2 11 16 20
32 10 15 26 27 37 27 11 0 27 -12 37 -27 9 -16 18 -30 20 -32 2 -3 17 11 33
30 16 19 29 28 30 21 0 -8 -16 -28 -35 -45 -33 -29 -49 -57 -24 -41 6 3 14 1
18 -5 5 -7 17 1 33 24 23 33 24 40 15 82 -7 38 -7 49 5 57 8 6 17 18 20 26 6
17 38 -51 38 -81 0 -11 4 -19 9 -19 5 0 15 -38 21 -83 11 -72 11 -90 -3 -128
l-15 -44 -2 55 c-1 52 -1 54 -17 35 -15 -19 -15 -19 -10 10 2 17 2 38 -2 47
-4 12 -14 4 -38 -37 -31 -53 -31 -54 -7 -45 30 12 31 -2 5 -52 -11 -19 -18
-38 -16 -40 2 -2 -14 -24 -36 -48 -44 -48 -59 -72 -59 -92 1 -7 7 2 15 20 8
17 19 32 25 32 7 0 7 -7 -2 -22 -7 -13 -19 -52 -28 -88 -10 -40 -22 -65 -30
-65 -8 0 -20 25 -30 65 -9 36 -21 75 -28 88 -9 15 -9 22 -2 22 6 0 17 -15 25
-32 8 -18 14 -27 15 -20 0 20 -15 44 -59 92 -22 24 -38 46 -36 48 2 2 -5 21
-16 40 -26 50 -25 64 5 52 24 -9 24 -8 -7 45 -24 41 -34 49 -38 37 -4 -9 -4
-30 -2 -47 5 -29 5 -29 -10 -10 -16 19 -16 17 -17 -35 l-2 -55 -15 44 c-14 38
-14 56 -3 128 6 45 16 83 21 83 5 0 9 8 9 19 0 30 32 98 38 81 3 -8 12 -20 20
-26z m-1196 16 c65 -19 58 -33 -13 -25 -35 3 -73 9 -84 12 -18 6 -18 7 5 14
36 10 51 10 92 -1z m2748 1 c23 -7 23 -8 5 -14 -44 -13 -148 -19 -141 -8 15
23 90 35 136 22z m-1960 -27 c0 -8 -4 -12 -10 -9 -5 3 -10 10 -10 16 0 5 5 9
10 9 6 0 10 -7 10 -16z m1100 7 c0 -6 -4 -13 -10 -16 -5 -3 -10 1 -10 9 0 9 5
16 10 16 6 0 10 -4 10 -9z m-619 -58 c12 -15 12 -16 -6 -9 -16 6 -18 5 -7 -5
17 -18 15 -34 -2 -20 -19 16 -27 51 -11 51 7 0 19 -8 26 -17z m152 -3 c-3 -10
-11 -25 -19 -31 -17 -14 -19 2 -1 20 10 10 8 11 -8 5 -18 -7 -18 -6 -6 9 19
24 40 22 34 -3z m-1363 -40 c-36 -17 -72 -34 -80 -40 -29 -20 -121 -60 -138
-60 -15 1 -14 2 3 15 11 8 25 18 30 23 18 15 142 80 161 85 10 2 35 5 54 5 33
1 31 -1 -30 -28z m2646 -16 c38 -21 74 -42 79 -46 6 -5 19 -15 30 -23 17 -13
18 -14 3 -15 -17 0 -109 40 -138 60 -8 6 -44 24 -80 41 l-65 30 51 -5 c33 -3
75 -18 120 -42z m-1661 -54 c-10 -11 -20 -18 -22 -16 -6 5 30 44 36 39 2 -1
-4 -12 -14 -23z m595 -17 c0 -3 -9 2 -20 12 -11 10 -20 22 -20 27 0 4 9 -1 20
-12 11 -11 20 -23 20 -27z m-926 -14 c-6 -11 -24 -2 -24 11 0 5 7 7 15 4 8 -4
12 -10 9 -15z m1251 2 c-3 -6 -11 -8 -17 -5 -6 4 -5 9 3 15 16 10 23 4 14 -10z
m-1013 -18 c-6 -9 -18 -18 -28 -19 -15 -2 -14 1 6 16 31 24 33 24 22 3z m798
-18 c0 -11 -34 5 -42 19 -9 17 -9 17 16 1 14 -10 26 -19 26 -20z m-990 15 c0
-5 -9 -10 -21 -10 -11 0 -17 5 -14 10 3 6 13 10 21 10 8 0 14 -4 14 -10z
m1175 0 c3 -5 -3 -10 -14 -10 -12 0 -21 5 -21 10 0 6 6 10 14 10 8 0 18 -4 21
-10z m-1139 -15 c-26 -19 -42 -19 -26 0 7 8 20 15 29 15 13 -1 13 -3 -3 -15z
m1094 0 c16 -19 0 -19 -26 0 -16 12 -16 14 -3 15 9 0 22 -7 29 -15z m-1029 -9
c-7 -8 -21 -14 -31 -13 -26 2 -5 27 22 27 17 0 18 -3 9 -14z m977 1 c8 -10 6
-13 -8 -14 -10 -1 -24 5 -31 13 -9 11 -8 14 9 14 11 0 24 -6 30 -13z m-617
-169 c11 -22 18 -42 15 -45 -2 -3 -14 12 -25 34 -11 21 -20 31 -21 23 -1 -22
-30 19 -30 43 0 10 -4 15 -10 12 -11 -7 -14 8 -3 18 9 9 50 -37 74 -85z m-20
75 c13 -16 12 -17 -3 -4 -10 7 -18 15 -18 17 0 8 8 3 21 -13z m279 13 c0 -2
-8 -10 -17 -17 -16 -13 -17 -12 -4 4 13 16 21 21 21 13z m40 -17 c0 -6 -4 -7
-10 -4 -5 3 -10 -2 -10 -12 0 -24 -29 -65 -30 -43 -1 8 -10 -2 -21 -23 -11
-22 -23 -37 -25 -34 -8 7 31 79 58 110 22 24 38 27 38 6z m-374 -116 c-3 -10
-7 -36 -10 -58 l-6 -40 15 34 c9 18 18 31 20 29 9 -9 -43 -187 -72 -248 -39
-80 -41 -62 -13 105 11 66 20 135 20 154 0 25 4 32 15 27 8 -3 15 -1 15 4 0 6
5 10 11 10 6 0 8 -8 5 -17z m384 7 c0 -5 7 -7 15 -4 11 5 15 -2 15 -27 0 -19
9 -88 20 -154 28 -167 26 -185 -13 -105 -29 61 -81 239 -72 248 2 2 11 -11 20
-29 l15 -34 -6 40 c-3 22 -7 48 -10 58 -3 9 -1 17 5 17 6 0 11 -4 11 -10z
m-450 -52 c0 -73 -28 -180 -66 -255 -37 -74 -173 -238 -159 -192 7 24 -24 -15
-69 -84 -9 -16 -19 -27 -22 -25 -6 7 52 178 83 243 16 33 41 79 56 102 15 24
27 47 27 53 0 16 42 101 47 96 3 -3 0 -21 -7 -41 -7 -20 -13 -51 -14 -68 -2
-18 -6 -45 -10 -62 -10 -46 24 30 64 140 18 50 36 100 41 113 14 36 29 25 29
-20z m563 -73 c46 -128 82 -208 71 -160 -4 17 -8 44 -10 62 -1 17 -7 48 -14
68 -7 20 -10 38 -7 41 5 5 47 -80 47 -96 0 -6 12 -29 27 -53 15 -23 40 -69 56
-102 31 -65 89 -236 83 -243 -3 -2 -13 9 -22 25 -45 69 -76 108 -69 84 14 -46
-122 118 -159 192 -38 75 -66 182 -66 255 0 69 19 47 63 -73z m-362 -60 c16
-33 32 -75 35 -92 3 -18 10 -33 14 -33 4 0 11 15 14 33 6 34 55 141 77 169 9
11 19 15 27 10 10 -7 7 -26 -11 -90 -27 -92 -21 -100 14 -16 18 47 24 53 30
37 10 -28 -21 -134 -68 -228 -22 -44 -48 -104 -57 -132 -10 -29 -21 -53 -26
-53 -5 0 -16 24 -26 53 -9 28 -35 88 -57 132 -47 94 -78 200 -68 228 6 16 12
10 30 -37 35 -84 41 -76 14 16 -18 64 -21 83 -11 90 16 10 32 -10 69 -87z
m-412 -60 c-19 -36 -37 -65 -41 -65 -10 0 -10 1 3 41 13 38 58 103 66 95 3 -3
-10 -35 -28 -71z m944 12 c22 -47 29 -77 20 -77 -8 0 -73 119 -73 134 0 16 38
-25 53 -57z m-649 -106 c9 -52 15 -96 12 -98 -9 -9 -36 81 -42 144 -10 100 11
69 30 -46z m362 43 c-6 -60 -33 -150 -42 -141 -5 5 25 188 33 200 12 20 15 1
9 -59z m-755 -119 c-25 -40 -65 -96 -89 -125 -24 -29 -50 -63 -58 -74 -12 -17
-16 -18 -23 -7 -7 11 -9 11 -14 0 -8 -18 2 57 13 96 4 17 14 56 21 89 12 57
33 81 49 56 10 -16 8 -28 -9 -59 -8 -14 -9 -21 -2 -17 6 4 18 21 27 38 8 16
18 27 21 25 3 -3 -2 -20 -10 -36 -9 -17 -12 -31 -7 -31 4 0 13 14 20 31 7 17
31 59 53 92 l41 62 7 -33 c6 -29 1 -44 -40 -107z m1162 18 c17 -35 35 -63 39
-63 3 0 0 14 -9 31 -8 16 -13 33 -10 36 3 2 13 -9 21 -25 9 -17 21 -34 27 -38
7 -4 6 3 -2 17 -17 31 -19 43 -9 59 16 25 37 1 49 -56 7 -33 17 -72 21 -89 11
-39 21 -114 13 -96 -5 11 -7 11 -14 0 -7 -11 -11 -10 -23 7 -8 11 -34 45 -58
74 -24 29 -64 85 -89 125 -38 59 -45 78 -41 105 l5 34 23 -30 c13 -16 39 -57
57 -91z m-1324 -22 c-42 -94 -119 -232 -144 -257 -7 -8 -18 -14 -23 -14 -6 0
-16 -14 -24 -30 -7 -17 -21 -33 -30 -36 -9 -3 -18 -15 -20 -26 -4 -25 -23 -35
-31 -15 -3 8 6 27 19 42 27 32 32 60 9 51 -9 -3 -15 0 -15 10 0 10 6 14 15 10
11 -4 20 4 29 27 44 104 60 130 117 190 55 58 63 56 20 -6 -15 -23 -41 -67
-55 -97 -15 -30 -32 -63 -38 -73 -5 -11 -8 -21 -5 -23 7 -7 74 99 129 204 61
116 78 145 84 140 2 -3 -15 -46 -37 -97z m1479 -24 c61 -117 132 -230 139
-223 3 2 0 12 -5 23 -6 10 -23 43 -38 73 -14 30 -40 74 -55 97 -43 62 -35 64
20 6 57 -60 73 -86 117 -190 9 -23 18 -31 29 -27 9 4 15 0 15 -10 0 -10 -6
-13 -15 -10 -23 9 -18 -19 9 -51 13 -15 22 -34 19 -42 -8 -20 -27 -10 -31 15
-2 11 -11 23 -20 26 -9 3 -23 19 -30 36 -8 16 -18 30 -24 30 -31 0 -122 160
-206 359 -3 9 -1 12 6 8 6 -4 37 -58 70 -120z m-960 26 c23 -77 72 -276 68
-280 -6 -6 -55 86 -72 136 -19 57 -31 181 -17 181 5 0 15 -17 21 -37z m429
-30 c-3 -38 -13 -89 -21 -114 -17 -50 -66 -142 -72 -136 -4 4 45 203 68 280
18 60 32 45 25 -30z m-287 -63 c29 -66 56 -120 60 -120 4 0 31 54 60 120 29
66 56 120 60 120 9 0 -1 -47 -24 -109 -26 -71 -17 -81 14 -16 16 34 29 52 29
41 1 -20 1 -20 16 0 8 10 15 16 15 12 0 -15 -61 -171 -93 -235 -19 -37 -43
-105 -53 -150 -10 -46 -21 -83 -24 -83 -3 0 -14 37 -24 83 -10 45 -34 113 -53
150 -32 64 -93 220 -93 235 0 4 7 -2 15 -12 15 -20 15 -20 16 0 0 11 13 -7 29
-41 31 -65 40 -55 14 16 -23 62 -33 109 -24 109 4 0 31 -54 60 -120z m-190
-25 c16 -35 30 -69 30 -75 0 -6 6 -23 14 -38 7 -15 16 -45 18 -67 3 -22 10
-51 17 -64 23 -42 3 -34 -29 12 -41 57 -83 159 -69 167 9 6 4 39 -16 108 -14
44 5 21 35 -43z m532 28 c-18 -49 -24 -86 -13 -92 14 -9 -28 -110 -69 -168
-32 -46 -52 -54 -29 -12 7 13 14 42 17 64 2 22 11 52 18 67 8 15 14 32 14 38
0 18 61 142 67 136 3 -3 1 -18 -5 -33z m-572 -129 c17 -43 42 -99 55 -124 13
-25 29 -59 34 -75 l11 -30 -25 30 c-13 16 -32 45 -42 64 -10 20 -30 57 -45 83
-30 52 -61 156 -51 171 7 12 23 -18 63 -119z m634 53 c-8 -31 -27 -79 -42
-105 -15 -26 -35 -63 -45 -83 -10 -19 -29 -48 -42 -64 l-25 -30 11 30 c5 16
21 50 34 75 13 25 37 79 54 120 43 110 54 131 62 123 5 -5 2 -34 -7 -66z
m-1314 -2 c-13 -14 -28 -25 -33 -25 -6 0 0 11 13 25 13 14 28 25 33 25 6 0 0
-11 -13 -25z m1960 0 c13 -14 19 -25 13 -25 -5 0 -20 11 -33 25 -13 14 -19 25
-13 25 5 0 20 -11 33 -25z m-1051 -220 c0 -12 -12 5 -30 40 -16 33 -29 69 -28
80 0 12 12 -5 30 -40 16 -33 29 -69 28 -80z m154 43 c-16 -37 -29 -56 -31 -46
-2 10 9 46 25 80 16 37 29 56 31 46 2 -10 -9 -46 -25 -80z m-903 -125 c-1 -27
-9 -66 -20 -88 -11 -22 -19 -47 -19 -55 0 -8 10 5 21 29 21 42 22 43 30 20 5
-13 7 -42 3 -64 -3 -22 -8 -65 -11 -95 l-6 -54 -15 39 c-8 22 -18 56 -20 75
-5 29 -7 32 -13 15 -6 -16 -9 -12 -15 20 -3 22 -11 49 -16 61 -16 33 -7 89 14
88 9 -1 17 3 17 9 0 12 31 45 43 46 4 1 7 -20 7 -46z m753 -16 c20 -34 39 -75
42 -90 9 -35 21 -35 30 0 7 29 75 156 80 151 2 -2 -16 -59 -39 -128 -27 -79
-47 -125 -56 -125 -9 0 -28 41 -51 110 -20 61 -40 118 -44 128 -15 38 5 14 38
-46z m897 46 c11 -10 20 -23 20 -29 0 -7 8 -11 18 -10 20 1 29 -55 13 -88 -5
-12 -13 -39 -16 -61 -6 -32 -9 -36 -15 -20 -6 17 -8 14 -13 -15 -2 -19 -12
-53 -20 -75 l-15 -39 -6 54 c-3 30 -8 73 -11 95 -4 22 -2 51 3 64 8 23 9 22
30 -20 11 -24 21 -37 21 -29 0 8 -8 33 -19 55 -17 35 -28 135 -15 135 3 0 14
-8 25 -17z m-972 -38 c-3 -3 -13 7 -23 22 l-17 28 23 -22 c12 -12 20 -25 17
-28z m286 20 c-9 -14 -19 -23 -22 -20 -5 4 26 45 35 45 2 0 -4 -11 -13 -25z
m-295 -72 l33 -48 -36 39 c-31 34 -44 56 -33 56 2 0 18 -21 36 -47z m303 9
c-12 -16 -29 -35 -39 -43 -9 -8 -2 7 17 33 18 27 35 46 38 44 2 -3 -5 -18 -16
-34z m-380 -89 c30 -31 108 -176 108 -201 0 -6 -85 105 -105 137 -5 9 -20 33
-32 52 -39 62 -25 67 29 12z m488 31 c0 -4 -10 -23 -22 -43 -13 -19 -27 -43
-33 -52 -20 -32 -105 -143 -105 -137 0 25 78 170 108 201 34 35 52 45 52 31z
m-1047 -263 c-16 -20 -32 124 -17 156 5 12 10 -11 14 -67 4 -47 5 -87 3 -89z
m1575 80 c-3 -62 -11 -93 -21 -80 -2 2 -1 42 1 87 3 56 8 80 14 74 6 -6 9 -41
6 -81z"/>
<path d="M1591 2353 c7 -12 15 -20 18 -17 3 2 -3 12 -13 22 -17 16 -18 16 -5
-5z"/>
<path d="M1900 2355 c-7 -9 -11 -17 -9 -20 3 -2 10 5 17 15 14 24 10 26 -8 5z"/>
<path d="M1376 2320 c14 -27 65 -100 70 -100 8 0 -25 55 -51 85 -15 17 -23 23
-19 15z"/>
<path d="M1460 2225 c0 -5 5 -17 10 -25 5 -8 10 -10 10 -5 0 6 -5 17 -10 25
-5 8 -10 11 -10 5z"/>
<path d="M2098 2298 c-28 -37 -51 -78 -44 -78 4 0 45 58 70 98 15 24 -2 11
-26 -20z"/>
<path d="M2026 2215 c-9 -26 -7 -32 5 -12 6 10 9 21 6 23 -2 3 -7 -2 -11 -11z"/>
<path d="M1600 1926 c0 -35 32 -100 41 -84 6 9 15 6 35 -12 l28 -25 -18 29
c-10 16 -34 49 -52 73 -31 41 -34 43 -34 19z"/>
<path d="M1865 1906 c-16 -22 -39 -54 -49 -71 l-20 -30 28 25 c20 18 29 21 35
12 10 -17 44 57 39 85 -2 17 -8 14 -33 -21z"/>
<path d="M1660 1720 c11 -22 23 -40 25 -40 3 0 -4 18 -15 40 -11 22 -23 40
-25 40 -3 0 4 -18 15 -40z"/>
<path d="M1830 1720 c-11 -22 -18 -40 -15 -40 2 0 14 18 25 40 11 22 18 40 15
40 -2 0 -14 -18 -25 -40z"/>
<path d="M1260 1150 c-6 -11 -8 -20 -6 -20 3 0 10 9 16 20 6 11 8 20 6 20 -3
0 -10 -9 -16 -20z"/>
<path d="M2230 1150 c6 -11 13 -20 16 -20 2 0 0 9 -6 20 -6 11 -13 20 -16 20
-2 0 0 -9 6 -20z"/>
<path d="M1640 1505 c0 -5 5 -17 10 -25 5 -8 10 -10 10 -5 0 6 -5 17 -10 25
-5 8 -10 11 -10 5z"/>
<path d="M1846 1495 c-9 -26 -7 -32 5 -12 6 10 9 21 6 23 -2 3 -7 -2 -11 -11z"/>
<path d="M1056 1088 c-9 -12 -15 -32 -15 -43 1 -17 2 -17 6 -2 2 9 9 17 14 17
5 0 9 9 9 19 0 11 6 22 13 24 9 4 9 6 0 6 -6 1 -19 -9 -27 -21z"/>
<path d="M2418 1103 c6 -2 12 -13 12 -24 0 -10 4 -19 9 -19 5 0 12 -8 14 -17
4 -15 5 -15 6 2 1 25 -25 65 -42 64 -9 0 -9 -2 1 -6z"/>
<path d="M932 570 c0 -19 2 -27 5 -17 2 9 2 25 0 35 -3 9 -5 1 -5 -18z"/>
<path d="M2562 570 c0 -19 2 -27 5 -17 2 9 2 25 0 35 -3 9 -5 1 -5 -18z"/>
<path d="M810 3416 c0 -31 191 -431 196 -411 1 6 4 38 6 73 3 49 1 62 -9 58
-8 -3 -15 5 -19 19 -3 14 -10 25 -15 25 -5 0 -9 7 -9 16 0 9 -9 28 -21 42 -11
15 -45 63 -75 106 -30 44 -54 76 -54 72z"/>
<path d="M2622 3324 c-35 -52 -68 -97 -73 -100 -5 -3 -9 -14 -9 -25 0 -10 -4
-19 -9 -19 -5 0 -12 -11 -15 -25 -4 -14 -11 -22 -19 -19 -10 4 -12 -9 -9 -58
2 -35 5 -67 6 -73 3 -13 116 210 161 319 20 48 35 89 33 91 -2 2 -32 -39 -66
-91z"/>
<path d="M1095 3393 c4 -10 19 -68 34 -128 30 -119 18 -99 180 -313 35 -46 86
-124 113 -174 27 -50 68 -116 92 -147 69 -90 117 -162 131 -194 7 -16 15 -25
18 -19 7 11 -47 96 -135 212 -31 41 -70 102 -86 135 -31 62 -77 193 -88 252
-3 18 -12 37 -20 44 -8 6 -14 17 -14 24 0 6 -20 43 -45 81 -55 85 -57 74 -6
-28 36 -72 57 -128 47 -128 -11 0 -98 170 -112 219 -9 31 -37 84 -62 119 -47
63 -60 76 -47 45z"/>
<path d="M2358 3348 c-25 -35 -53 -88 -62 -119 -14 -49 -101 -219 -112 -219
-10 0 11 56 47 128 51 102 49 113 -6 28 -25 -38 -45 -75 -45 -81 0 -7 -6 -18
-14 -24 -8 -7 -17 -26 -20 -44 -11 -59 -57 -190 -88 -252 -16 -33 -55 -94 -86
-135 -88 -116 -142 -201 -135 -212 3 -6 11 3 18 19 14 32 62 104 131 194 24
31 65 97 92 147 27 50 78 128 113 174 162 214 150 194 180 313 15 60 30 118
34 128 13 31 0 18 -47 -45z"/>
<path d="M1810 3250 c0 -13 5 -18 15 -14 12 5 13 10 4 20 -15 19 -19 18 -19
-6z"/>
<path d="M1784 3231 c5 -8 2 -11 -9 -9 -10 2 -20 -2 -22 -9 -3 -7 3 -9 17 -6
26 7 34 19 19 29 -8 5 -9 3 -5 -5z"/>
<path d="M1880 3210 c0 -13 5 -18 15 -14 12 5 13 10 4 20 -15 19 -19 18 -19
-6z"/>
<path d="M1174 3082 c8 -16 22 -35 32 -43 10 -8 5 4 -11 29 -32 48 -42 55 -21
14z"/>
<path d="M1807 3097 c-3 -8 3 -14 15 -14 16 -2 17 1 8 13 -15 17 -17 17 -23 1z"/>
<path d="M2305 3068 c-16 -25 -21 -37 -11 -29 16 14 51 71 43 71 -3 0 -17 -19
-32 -42z"/>
<path d="M1750 3066 c0 -8 -3 -22 -7 -32 -4 -11 -3 -15 4 -11 15 9 23 46 12
53 -5 3 -9 -1 -9 -10z"/>
<path d="M1750 2959 c0 -5 9 -9 20 -9 11 0 20 2 20 4 0 2 -9 6 -20 9 -11 3
-20 1 -20 -4z"/>
<path d="M1707 2799 c7 -7 15 -10 18 -7 3 3 -2 9 -12 12 -14 6 -15 5 -6 -5z"/>
<path d="M820 2720 c-13 -8 -12 -10 3 -10 9 0 17 5 17 10 0 12 -1 12 -20 0z"/>
<path d="M2660 2720 c0 -6 7 -10 15 -10 8 0 15 2 15 4 0 2 -7 6 -15 10 -8 3
-15 1 -15 -4z"/>
<path d="M471 2512 c8 -13 20 -17 37 -14 24 5 24 5 -8 18 -41 18 -42 18 -29
-4z"/>
<path d="M540 2519 c0 -5 9 -9 20 -9 11 0 20 2 20 4 0 2 -9 6 -20 9 -11 3 -20
1 -20 -4z"/>
<path d="M2935 2520 c-19 -8 -19 -9 3 -9 12 -1 22 4 22 9 0 6 -1 10 -2 9 -2 0
-12 -4 -23 -9z"/>
<path d="M2996 2516 l-29 -13 25 -5 c17 -3 29 1 37 14 13 21 7 21 -33 4z"/>
<path d="M580 1854 c-30 -7 -68 -17 -85 -22 -27 -9 -25 -10 25 -10 39 -1 49 2
35 8 -18 8 -18 9 5 10 28 2 114 28 90 28 -8 0 -40 -6 -70 -14z"/>
<path d="M2875 1855 c22 -8 51 -14 65 -15 23 -1 23 -2 5 -10 -14 -6 -4 -9 35
-8 50 0 52 1 25 9 -34 12 -157 40 -165 38 -3 -1 13 -7 35 -14z"/>
<path d="M700 1840 c-8 -5 -40 -10 -70 -11 -40 -1 -48 -3 -30 -8 22 -6 13 -12
-60 -40 -47 -18 -116 -52 -155 -76 -67 -41 -230 -173 -223 -180 4 -5 102 18
108 24 3 4 41 7 85 9 l80 2 -54 -32 c-72 -44 -202 -146 -198 -157 1 -4 -9 -18
-23 -29 -16 -14 -20 -21 -11 -22 8 0 23 7 33 17 14 13 18 13 18 2 1 -11 3 -11
13 1 25 32 134 81 151 68 2 -2 13 6 25 16 12 11 21 16 21 12 0 -5 17 2 38 13
l37 21 -35 -30 -35 -31 30 7 c38 9 146 98 186 154 24 33 27 41 13 36 -33 -13
-158 -46 -173 -46 -7 0 7 18 33 41 25 23 71 74 102 114 30 41 71 87 92 104 34
28 36 43 2 21z m-90 -95 c0 -3 -15 -11 -32 -20 -18 -8 -40 -19 -48 -25 -26
-17 -148 -69 -177 -75 l-28 -6 22 19 c13 11 48 28 78 37 57 18 75 32 28 21
-24 -5 -25 -5 -9 5 48 29 166 61 166 44z m-72 -84 c-6 -17 -90 -81 -112 -85
-48 -10 -139 -16 -133 -9 15 14 97 41 97 32 0 -6 16 2 35 17 19 14 35 22 35
16 0 -6 12 0 26 13 26 24 59 34 52 16z m-83 -171 c-3 -5 -14 -10 -24 -10 -10
0 -24 -7 -31 -15 -7 -8 -18 -15 -25 -15 -7 0 -18 -7 -25 -15 -7 -8 -23 -15
-35 -15 -13 0 -25 -3 -27 -7 -4 -11 -76 -43 -91 -42 -7 0 10 11 36 24 27 12
43 25 35 27 -7 3 2 8 20 12 17 4 32 11 32 16 0 4 8 11 18 14 9 3 28 13 42 21
28 17 85 20 75 5z"/>
<path d="M2802 1820 c21 -17 62 -64 92 -105 31 -40 77 -91 102 -114 26 -23 40
-41 33 -41 -15 0 -140 33 -173 46 -14 5 -11 -3 13 -36 40 -56 148 -145 186
-154 l30 -7 -35 31 -35 30 38 -21 c20 -11 37 -18 37 -13 0 4 8 0 18 -9 9 -8
20 -16 25 -16 42 1 130 -40 154 -71 10 -12 12 -12 13 -1 0 11 4 11 18 -2 10
-10 25 -17 33 -17 9 1 5 8 -11 22 -14 11 -24 25 -23 29 4 11 -126 113 -198
157 l-54 32 80 -2 c44 -2 82 -5 85 -9 6 -6 104 -29 108 -24 7 7 -156 139 -223
180 -38 24 -108 58 -155 76 -72 28 -81 34 -60 39 18 5 9 8 -35 12 -33 2 -69 8
-80 12 -11 4 -3 -7 17 -24z m220 -105 c29 -13 41 -22 26 -19 -47 10 -31 -2 27
-21 30 -9 65 -26 78 -37 l22 -19 -28 6 c-29 6 -151 58 -177 75 -8 6 -30 17
-47 25 -18 9 -33 18 -33 21 0 12 80 -6 132 -31z m-8 -70 c14 -13 26 -19 26
-13 0 6 16 -2 35 -16 19 -15 35 -23 35 -17 0 9 82 -18 97 -32 6 -7 -85 -1
-133 9 -22 4 -106 68 -112 85 -7 18 26 8 52 -16z m106 -160 c14 -8 33 -18 43
-21 9 -3 17 -10 17 -14 0 -5 15 -12 33 -16 17 -4 26 -9 19 -12 -8 -2 8 -15 35
-27 26 -13 43 -24 36 -24 -15 -1 -87 31 -91 42 -2 4 -14 7 -27 7 -12 0 -28 7
-35 15 -7 8 -18 15 -25 15 -7 0 -18 7 -25 15 -7 8 -21 15 -31 15 -10 0 -21 5
-24 10 -10 15 47 12 75 -5z"/>
<path d="M1246 1578 c-43 -51 -83 -104 -90 -118 -16 -29 -37 -92 -33 -96 5 -6
105 131 126 173 11 23 29 48 38 55 15 11 48 78 39 78 -2 0 -38 -42 -80 -92z"/>
<path d="M2183 1640 c6 -19 20 -41 30 -48 9 -7 27 -32 38 -55 21 -42 121 -179
126 -173 3 3 -16 61 -30 93 -9 19 -120 158 -163 203 -12 12 -12 9 -1 -20z"/>
<path d="M980 1637 c-50 -19 -70 -31 -70 -39 0 -5 9 -4 19 2 11 5 32 15 47 20
25 10 26 9 13 -7 -22 -28 -42 -42 -61 -43 -25 0 -33 -20 -8 -20 26 0 93 61 89
82 -3 12 -9 13 -29 5z"/>
<path d="M2490 1631 c0 -24 64 -81 91 -81 24 0 15 20 -9 20 -19 1 -39 15 -61
43 -13 16 -12 17 13 7 15 -5 36 -15 47 -20 10 -6 19 -7 19 -2 0 9 -19 19 -72
41 -25 9 -28 8 -28 -8z"/>
<path d="M1286 1573 c-6 -14 -5 -15 5 -6 7 7 10 15 7 18 -3 3 -9 -2 -12 -12z"/>
<path d="M2200 1581 c0 -6 4 -13 10 -16 6 -3 7 1 4 9 -7 18 -14 21 -14 7z"/>
<path d="M930 1535 c-7 -9 -11 -18 -8 -20 3 -3 11 1 18 10 7 9 11 18 8 20 -3
3 -11 -1 -18 -10z"/>
<path d="M2550 1542 c0 -5 7 -15 15 -22 8 -7 15 -8 15 -2 0 5 -7 15 -15 22 -8
7 -15 8 -15 2z"/>
<path d="M555 1290 l-40 -19 42 0 c41 -1 64 13 47 29 -5 4 -27 -1 -49 -10z"/>
<path d="M2894 1298 c-11 -18 12 -29 52 -26 l39 3 -42 17 c-27 10 -45 13 -49
6z"/>
<path d="M1431 538 c17 -34 99 -148 106 -148 3 0 -2 10 -10 23 -9 12 -35 50
-59 85 -43 64 -58 79 -37 40z"/>
<path d="M2032 498 c-24 -35 -50 -73 -59 -85 -8 -13 -13 -23 -10 -23 7 0 89
114 106 148 21 39 6 24 -37 -40z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 36 KiB

View file

@ -0,0 +1,19 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-256x256.png",
"sizes": "256x256",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

BIN
app/static/img/rp/.DS_Store vendored Executable file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
app/static/img/rp/rpwebsearch.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
app/static/img/rplogo.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
app/static/js/.DS_Store vendored Normal file

Binary file not shown.

127
app/static/js/autocomplete.js Executable file
View file

@ -0,0 +1,127 @@
let searchInput;
let currentFocus;
let originalSearch;
let autocompleteResults;
const handleUserInput = () => {
let xhrRequest = new XMLHttpRequest();
xhrRequest.open("POST", "autocomplete");
xhrRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhrRequest.onload = function () {
if (xhrRequest.readyState === 4 && xhrRequest.status !== 200) {
// Do nothing if failed to fetch autocomplete results
return;
}
// Fill autocomplete with fetched results
autocompleteResults = JSON.parse(xhrRequest.responseText)[1];
updateAutocompleteList();
};
xhrRequest.send('q=' + searchInput.value);
};
const removeActive = suggestion => {
// Remove "autocomplete-active" class from previously active suggestion
for (let i = 0; i < suggestion.length; i++) {
suggestion[i].classList.remove("autocomplete-active");
}
};
const addActive = (suggestion) => {
// Handle navigation outside of suggestion list
if (!suggestion || !suggestion[currentFocus]) {
if (currentFocus >= suggestion.length) {
// Move selection back to the beginning
currentFocus = 0;
} else if (currentFocus < 0) {
// Retrieve original search and remove active suggestion selection
currentFocus = -1;
searchInput.value = originalSearch;
removeActive(suggestion);
return;
} else {
return;
}
}
removeActive(suggestion);
suggestion[currentFocus].classList.add("autocomplete-active");
// Autofill search bar with suggestion content (minus the "bang name" if using a bang operator)
let searchContent = suggestion[currentFocus].textContent;
if (searchContent.indexOf('(') > 0) {
searchInput.value = searchContent.substring(0, searchContent.indexOf('('));
} else {
searchInput.value = searchContent;
}
searchInput.focus();
};
const autocompleteInput = (e) => {
// Handle navigation between autocomplete suggestions
let suggestion = document.getElementById("autocomplete-list");
if (suggestion) suggestion = suggestion.getElementsByTagName("div");
if (e.keyCode === 40) { // down
e.preventDefault();
currentFocus++;
addActive(suggestion);
} else if (e.keyCode === 38) { //up
e.preventDefault();
currentFocus--;
addActive(suggestion);
} else if (e.keyCode === 13) { // enter
e.preventDefault();
if (currentFocus > -1) {
if (suggestion) suggestion[currentFocus].click();
}
} else {
originalSearch = searchInput.value;
}
};
const updateAutocompleteList = () => {
let autocompleteItem, i;
let val = originalSearch;
let autocompleteList = document.getElementById("autocomplete-list");
autocompleteList.innerHTML = "";
if (!val || !autocompleteResults) {
return false;
}
currentFocus = -1;
for (i = 0; i < autocompleteResults.length; i++) {
if (autocompleteResults[i].substr(0, val.length).toUpperCase() === val.toUpperCase()) {
autocompleteItem = document.createElement("div");
autocompleteItem.setAttribute("class", "autocomplete-item");
autocompleteItem.innerHTML = "<strong>" + autocompleteResults[i].substr(0, val.length) + "</strong>";
autocompleteItem.innerHTML += autocompleteResults[i].substr(val.length);
autocompleteItem.innerHTML += "<input type=\"hidden\" value=\"" + autocompleteResults[i] + "\">";
autocompleteItem.addEventListener("click", function () {
searchInput.value = this.getElementsByTagName("input")[0].value;
autocompleteList.innerHTML = "";
document.getElementById("search-form").submit();
});
autocompleteList.appendChild(autocompleteItem);
}
}
};
document.addEventListener("DOMContentLoaded", function() {
let autocompleteList = document.createElement("div");
autocompleteList.setAttribute("id", "autocomplete-list");
autocompleteList.setAttribute("class", "autocomplete-items");
searchInput = document.getElementById("search-bar");
searchInput.parentNode.appendChild(autocompleteList);
searchInput.addEventListener("keydown", (event) => autocompleteInput(event));
document.addEventListener("click", function (e) {
autocompleteList.innerHTML = "";
});
});

88
app/static/js/controller.js Executable file
View file

@ -0,0 +1,88 @@
const setupSearchLayout = () => {
// Setup search field
const searchBar = document.getElementById("search-bar");
const searchBtn = document.getElementById("search-submit");
const arrowKeys = [37, 38, 39, 40];
let searchValue = searchBar.value;
// Automatically focus on search field
searchBar.focus();
searchBar.select();
searchBar.addEventListener("keyup", function(event) {
if (event.keyCode === 13) {
event.preventDefault();
searchBtn.click();
} else if (searchBar.value !== searchValue && !arrowKeys.includes(event.keyCode)) {
searchValue = searchBar.value;
handleUserInput();
}
});
};
const setupConfigLayout = () => {
// Setup whoogle config
const collapsible = document.getElementById("config-collapsible");
collapsible.addEventListener("click", function() {
this.classList.toggle("active");
let content = this.nextElementSibling;
if (content.style.maxHeight) {
content.style.maxHeight = null;
} else {
content.style.maxHeight = "400px";
}
content.classList.toggle("open");
});
};
const loadConfig = event => {
event.preventDefault();
let config = prompt("Enter name of config:");
if (!config) {
alert("Must specify a name for the config to load");
return;
}
let xhrPUT = new XMLHttpRequest();
xhrPUT.open("PUT", "config?name=" + config + ".conf");
xhrPUT.onload = function() {
if (xhrPUT.readyState === 4 && xhrPUT.status !== 200) {
alert("Error loading Whoogle config");
return;
}
location.reload(true);
};
xhrPUT.send();
};
const saveConfig = event => {
event.preventDefault();
let config = prompt("Enter name for this config:");
if (!config) {
alert("Must specify a name for the config to save");
return;
}
let configForm = document.getElementById("config-form");
configForm.action = 'config?name=' + config + ".conf";
configForm.submit();
};
document.addEventListener("DOMContentLoaded", function() {
setTimeout(function() {
document.getElementById("main");
}, 100);
setupSearchLayout();
setupConfigLayout();
document.getElementById("config-load").addEventListener("click", loadConfig);
document.getElementById("config-save").addEventListener("click", saveConfig);
// Focusing on the search input field requires a delay for elements to finish
// loading (seemingly only on FF)
setTimeout(function() { document.getElementById("search-bar").focus(); }, 250);
});

9
app/static/js/currency.js Executable file
View file

@ -0,0 +1,9 @@
const convert = (n1, n2, conversionFactor) => {
// id's for currency input boxes
let id1 = "cb" + n1;
let id2 = "cb" + n2;
// getting the value of the input box that just got filled
let inputBox = document.getElementById(id1).value;
// updating the other input box after conversion
document.getElementById(id2).value = ((inputBox * conversionFactor).toFixed(2));
}

67
app/static/js/header.js Executable file
View file

@ -0,0 +1,67 @@
document.addEventListener("DOMContentLoaded", () => {
const advSearchToggle = document.getElementById("adv-search-toggle");
const advSearchDiv = document.getElementById("adv-search-div");
const searchBar = document.getElementById("search-bar");
const countrySelect = document.getElementById("result-country");
const timePeriodSelect = document.getElementById("result-time-period");
const arrowKeys = [37, 38, 39, 40];
let searchValue = searchBar.value;
countrySelect.onchange = () => {
let str = window.location.href;
n = str.lastIndexOf("/search");
if (n > 0) {
str = str.substring(0, n) + `/search?q=${searchBar.value}`;
str = tackOnParams(str);
window.location.href = str;
}
}
timePeriodSelect.onchange = () => {
let str = window.location.href;
n = str.lastIndexOf("/search");
if (n > 0) {
str = str.substring(0, n) + `/search?q=${searchBar.value}`;
str = tackOnParams(str);
window.location.href = str;
}
}
function tackOnParams(str) {
if (timePeriodSelect.value != "") {
str = str + `&tbs=${timePeriodSelect.value}`;
}
if (countrySelect.value != "") {
str = str + `&country=${countrySelect.value}`;
}
return str;
}
const toggleAdvancedSearch = on => {
if (on) {
advSearchDiv.style.maxHeight = "70px";
} else {
advSearchDiv.style.maxHeight = "0px";
}
localStorage.advSearchToggled = on;
}
try {
toggleAdvancedSearch(JSON.parse(localStorage.advSearchToggled));
} catch (error) {
console.warn("Did not recover advanced search toggle state");
}
advSearchToggle.onclick = () => {
toggleAdvancedSearch(advSearchToggle.checked);
}
searchBar.addEventListener("keyup", function(event) {
if (event.keyCode === 13) {
document.getElementById("search-form").submit();
} else if (searchBar.value !== searchValue && !arrowKeys.includes(event.keyCode)) {
searchValue = searchBar.value;
handleUserInput();
}
});
});

62
app/static/js/keyboard.js Executable file
View file

@ -0,0 +1,62 @@
(function () {
let searchBar, results;
let shift = false;
const keymap = {
ArrowUp: goUp,
ArrowDown: goDown,
ShiftTab: goUp,
Tab: goDown,
k: goUp,
j: goDown,
'/': focusSearch,
};
let activeIdx = -1;
document.addEventListener('DOMContentLoaded', () => {
searchBar = document.querySelector('#search-bar');
results = document.querySelectorAll('#main>div>div>div>a');
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Shift') {
shift = true;
}
if (e.target.tagName === 'INPUT') return true;
if (typeof keymap[e.key] === 'function') {
e.preventDefault();
keymap[`${shift && e.key == 'Tab' ? 'Shift' : ''}${e.key}`]();
}
});
document.addEventListener('keyup', (e) => {
if (e.key === 'Shift') {
shift = false;
}
});
function goUp () {
if (activeIdx > 0) focusResult(activeIdx - 1);
else focusSearch();
}
function goDown () {
if (activeIdx < results.length - 1) focusResult(activeIdx + 1);
}
function focusResult (idx) {
activeIdx = idx;
results[activeIdx].scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
results[activeIdx].focus();
}
function focusSearch () {
if (window.usingCalculator) {
// if this function exists, it means the calculator widget has been displayed
if (usingCalculator()) return;
}
activeIdx = -1;
searchBar.focus();
}
}());

76
app/static/js/utils.js Executable file
View file

@ -0,0 +1,76 @@
const checkForTracking = () => {
const mainDiv = document.getElementById("main");
const searchBar = document.getElementById("search-bar");
// some pages (e.g. images) do not have these
if (!mainDiv || !searchBar)
return;
const query = searchBar.value.replace(/\s+/g, '');
// Note: regex functions for checking for tracking queries were derived
// from here -- https://stackoverflow.com/questions/619977
const matchTracking = {
"ups": {
"link": `https://www.ups.com/track?tracknum=${query}`,
"expr": [
/\b(1Z ?[0-9A-Z]{3} ?[0-9A-Z]{3} ?[0-9A-Z]{2} ?[0-9A-Z]{4} ?[0-9A-Z]{3} ?[0-9A-Z]|[\dT]\d\d\d ?\d\d\d\d ?\d\d\d)\b/
]
},
"usps": {
"link": `https://tools.usps.com/go/TrackConfirmAction_input?origTrackNum=${query}`,
"expr": [
/(\b\d{30}\b)|(\b91\d+\b)|(\b\d{20}\b)/,
/^E\D{1}\d{9}\D{2}$|^9\d{15,21}$/,
/^91[0-9]+$/,
/^[A-Za-z]{2}[0-9]+US$/
]
},
"fedex": {
"link": `https://www.fedex.com/apps/fedextrack/?tracknumbers=${query}`,
"expr": [
/(\b96\d{20}\b)|(\b\d{15}\b)|(\b\d{12}\b)/,
/\b((98\d\d\d\d\d?\d\d\d\d|98\d\d) ?\d\d\d\d ?\d\d\d\d( ?\d\d\d)?)\b/,
/^[0-9]{15}$/
]
}
};
// Creates a link to a UPS/USPS/FedEx tracking page
const createTrackingLink = href => {
let link = document.createElement("a");
link.className = "tracking-link";
link.innerHTML = "View Tracking Info";
link.href = href;
mainDiv.prepend(link);
};
// Compares the query against a set of regex patterns
// for tracking numbers
const compareQuery = provider => {
provider.expr.some(regex => {
if (query.match(regex)) {
createTrackingLink(provider.link);
return true;
}
});
};
for (const key of Object.keys(matchTracking)) {
compareQuery(matchTracking[key]);
}
};
document.addEventListener("DOMContentLoaded", function() {
checkForTracking();
// Clear input if reset button tapped
const searchBar = document.getElementById("search-bar");
const resetBtn = document.getElementById("search-reset");
// some pages (e.g. images) do not have these
if (!searchBar || !resetBtn)
return;
resetBtn.addEventListener("click", event => {
event.preventDefault();
searchBar.value = "";
searchBar.focus();
});
});

View file

@ -0,0 +1,978 @@
[
{
"name": "-------",
"value": ""
},
{
"name": "Hungary",
"value": "HU"
},
{
"name": "Afghanistan",
"value": "AF"
},
{
"name": "Albania",
"value": "AL"
},
{
"name": "Algeria",
"value": "DZ"
},
{
"name": "American Samoa",
"value": "AS"
},
{
"name": "Andorra",
"value": "AD"
},
{
"name": "Angola",
"value": "AO"
},
{
"name": "Anguilla",
"value": "AI"
},
{
"name": "Antarctica",
"value": "AQ"
},
{
"name": "Antigua and Barbuda",
"value": "AG"
},
{
"name": "Argentina",
"value": "AR"
},
{
"name": "Armenia",
"value": "AM"
},
{
"name": "Aruba",
"value": "AW"
},
{
"name": "Australia",
"value": "AU"
},
{
"name": "Austria",
"value": "AT"
},
{
"name": "Azerbaijan",
"value": "AZ"
},
{
"name": "Bahamas",
"value": "BS"
},
{
"name": "Bahrain",
"value": "BH"
},
{
"name": "Bangladesh",
"value": "BD"
},
{
"name": "Barbados",
"value": "BB"
},
{
"name": "Belarus",
"value": "BY"
},
{
"name": "Belgium",
"value": "BE"
},
{
"name": "Belize",
"value": "BZ"
},
{
"name": "Benin",
"value": "BJ"
},
{
"name": "Bermuda",
"value": "BM"
},
{
"name": "Bhutan",
"value": "BT"
},
{
"name": "Bolivia",
"value": "BO"
},
{
"name": "Bosnia and Herzegovina",
"value": "BA"
},
{
"name": "Botswana",
"value": "BW"
},
{
"name": "Bouvet Island",
"value": "BV"
},
{
"name": "Brazil",
"value": "BR"
},
{
"name": "British Indian Ocean Territory",
"value": "IO"
},
{
"name": "Brunei Darussalam",
"value": "BN"
},
{
"name": "Bulgaria",
"value": "BG"
},
{
"name": "Burkina Faso",
"value": "BF"
},
{
"name": "Burundi",
"value": "BI"
},
{
"name": "Cambodia",
"value": "KH"
},
{
"name": "Cameroon",
"value": "CM"
},
{
"name": "Canada",
"value": "CA"
},
{
"name": "Cape Verde",
"value": "CV"
},
{
"name": "Cayman Islands",
"value": "KY"
},
{
"name": "Central African Republic",
"value": "CF"
},
{
"name": "Chad",
"value": "TD"
},
{
"name": "Chile",
"value": "CL"
},
{
"name": "China",
"value": "CN"
},
{
"name": "Christmas Island",
"value": "CX"
},
{
"name": "Cocos (Keeling) Islands",
"value": "CC"
},
{
"name": "Colombia",
"value": "CO"
},
{
"name": "Comoros",
"value": "KM"
},
{
"name": "Congo",
"value": "CG"
},
{
"name": "Congo, Democratic Republic of the",
"value": "CD"
},
{
"name": "Cook Islands",
"value": "CK"
},
{
"name": "Costa Rica",
"value": "CR"
},
{
"name": "Cote D'ivoire",
"value": "CI"
},
{
"name": "Croatia (Hrvatska)",
"value": "HR"
},
{
"name": "Cuba",
"value": "CU"
},
{
"name": "Cyprus",
"value": "CY"
},
{
"name": "Czech Republic",
"value": "CZ"
},
{
"name": "Denmark",
"value": "DK"
},
{
"name": "Djibouti",
"value": "DJ"
},
{
"name": "Dominica",
"value": "DM"
},
{
"name": "Dominican Republic",
"value": "DO"
},
{
"name": "East Timor",
"value": "TP"
},
{
"name": "Ecuador",
"value": "EC"
},
{
"name": "Egypt",
"value": "EG"
},
{
"name": "El Salvador",
"value": "SV"
},
{
"name": "Equatorial Guinea",
"value": "GQ"
},
{
"name": "Eritrea",
"value": "ER"
},
{
"name": "Estonia",
"value": "EE"
},
{
"name": "Ethiopia",
"value": "ET"
},
{
"name": "European Union",
"value": "EU"
},
{
"name": "Falkland Islands (Malvinas)",
"value": "FK"
},
{
"name": "Faroe Islands",
"value": "FO"
},
{
"name": "Fiji",
"value": "FJ"
},
{
"name": "Finland",
"value": "FI"
},
{
"name": "France",
"value": "FR"
},
{
"name": "France, Metropolitan",
"value": "FX"
},
{
"name": "French Guiana",
"value": "GF"
},
{
"name": "French Polynesia",
"value": "PF"
},
{
"name": "French Southern Territories",
"value": "TF"
},
{
"name": "Gabon",
"value": "GA"
},
{
"name": "Gambia",
"value": "GM"
},
{
"name": "Georgia",
"value": "GE"
},
{
"name": "Germany",
"value": "DE"
},
{
"name": "Ghana",
"value": "GH"
},
{
"name": "Gibraltar",
"value": "GI"
},
{
"name": "Greece",
"value": "GR"
},
{
"name": "Greenland",
"value": "GL"
},
{
"name": "Grenada",
"value": "GD"
},
{
"name": "Guadeloupe",
"value": "GP"
},
{
"name": "Guam",
"value": "GU"
},
{
"name": "Guatemala",
"value": "GT"
},
{
"name": "Guinea",
"value": "GN"
},
{
"name": "Guinea-Bissau",
"value": "GW"
},
{
"name": "Guyana",
"value": "GY"
},
{
"name": "Haiti",
"value": "HT"
},
{
"name": "Heard Island and Mcdonald Islands",
"value": "HM"
},
{
"name": "Holy See (Vatican City State)",
"value": "VA"
},
{
"name": "Honduras",
"value": "HN"
},
{
"name": "Hong Kong",
"value": "HK"
},
{
"name": "Hungary",
"value": "HU"
},
{
"name": "Iceland",
"value": "IS"
},
{
"name": "India",
"value": "IN"
},
{
"name": "Indonesia",
"value": "ID"
},
{
"name": "Iran, Islamic Republic of",
"value": "IR"
},
{
"name": "Iraq",
"value": "IQ"
},
{
"name": "Ireland",
"value": "IE"
},
{
"name": "Israel",
"value": "IL"
},
{
"name": "Italy",
"value": "IT"
},
{
"name": "Jamaica",
"value": "JM"
},
{
"name": "Japan",
"value": "JP"
},
{
"name": "Jordan",
"value": "JO"
},
{
"name": "Kazakhstan",
"value": "KZ"
},
{
"name": "Kenya",
"value": "KE"
},
{
"name": "Kiribati",
"value": "KI"
},
{
"name": "Korea, Democratic People's Republic of",
"value": "KP"
},
{
"name": "Korea, Republic of",
"value": "KR"
},
{
"name": "Kuwait",
"value": "KW"
},
{
"name": "Kyrgyzstan",
"value": "KG"
},
{
"name": "Lao People's Democratic Republic",
"value": "LA"
},
{
"name": "Latvia",
"value": "LV"
},
{
"name": "Lebanon",
"value": "LB"
},
{
"name": "Lesotho",
"value": "LS"
},
{
"name": "Liberia",
"value": "LR"
},
{
"name": "Libyan Arab Jamahiriya",
"value": "LY"
},
{
"name": "Liechtenstein",
"value": "LI"
},
{
"name": "Lithuania",
"value": "LT"
},
{
"name": "Luxembourg",
"value": "LU"
},
{
"name": "Macao",
"value": "MO"
},
{
"name": "Macedonia, the Former Yugosalv Republic of",
"value": "MK"
},
{
"name": "Madagascar",
"value": "MG"
},
{
"name": "Malawi",
"value": "MW"
},
{
"name": "Malaysia",
"value": "MY"
},
{
"name": "Maldives",
"value": "MV"
},
{
"name": "Mali",
"value": "ML"
},
{
"name": "Malta",
"value": "MT"
},
{
"name": "Marshall Islands",
"value": "MH"
},
{
"name": "Martinique",
"value": "MQ"
},
{
"name": "Mauritania",
"value": "MR"
},
{
"name": "Mauritius",
"value": "MU"
},
{
"name": "Mayotte",
"value": "YT"
},
{
"name": "Mexico",
"value": "MX"
},
{
"name": "Micronesia, Federated States of",
"value": "FM"
},
{
"name": "Moldova, Republic of",
"value": "MD"
},
{
"name": "Monaco",
"value": "MC"
},
{
"name": "Mongolia",
"value": "MN"
},
{
"name": "Montserrat",
"value": "MS"
},
{
"name": "Morocco",
"value": "MA"
},
{
"name": "Mozambique",
"value": "MZ"
},
{
"name": "Myanmar",
"value": "MM"
},
{
"name": "Namibia",
"value": "NA"
},
{
"name": "Nauru",
"value": "NR"
},
{
"name": "Nepal",
"value": "NP"
},
{
"name": "Netherlands",
"value": "NL"
},
{
"name": "Netherlands Antilles",
"value": "AN"
},
{
"name": "New Caledonia",
"value": "NC"
},
{
"name": "New Zealand",
"value": "NZ"
},
{
"name": "Nicaragua",
"value": "NI"
},
{
"name": "Niger",
"value": "NE"
},
{
"name": "Nigeria",
"value": "NG"
},
{
"name": "Niue",
"value": "NU"
},
{
"name": "Norfolk Island",
"value": "NF"
},
{
"name": "Northern Mariana Islands",
"value": "MP"
},
{
"name": "Norway",
"value": "NO"
},
{
"name": "Oman",
"value": "OM"
},
{
"name": "Pakistan",
"value": "PK"
},
{
"name": "Palau",
"value": "PW"
},
{
"name": "Palestinian Territory",
"value": "PS"
},
{
"name": "Panama",
"value": "PA"
},
{
"name": "Papua New Guinea",
"value": "PG"
},
{
"name": "Paraguay",
"value": "PY"
},
{
"name": "Peru",
"value": "PE"
},
{
"name": "Philippines",
"value": "PH"
},
{
"name": "Pitcairn",
"value": "PN"
},
{
"name": "Poland",
"value": "PL"
},
{
"name": "Portugal",
"value": "PT"
},
{
"name": "Puerto Rico",
"value": "PR"
},
{
"name": "Qatar",
"value": "QA"
},
{
"name": "Reunion",
"value": "RE"
},
{
"name": "Romania",
"value": "RO"
},
{
"name": "Russian Federation",
"value": "RU"
},
{
"name": "Rwanda",
"value": "RW"
},
{
"name": "Saint Helena",
"value": "SH"
},
{
"name": "Saint Kitts and Nevis",
"value": "KN"
},
{
"name": "Saint Lucia",
"value": "LC"
},
{
"name": "Saint Pierre and Miquelon",
"value": "PM"
},
{
"name": "Saint Vincent and the Grenadines",
"value": "VC"
},
{
"name": "Samoa",
"value": "WS"
},
{
"name": "San Marino",
"value": "SM"
},
{
"name": "Sao Tome and Principe",
"value": "ST"
},
{
"name": "Saudi Arabia",
"value": "SA"
},
{
"name": "Senegal",
"value": "SN"
},
{
"name": "Serbia and Montenegro",
"value": "CS"
},
{
"name": "Seychelles",
"value": "SC"
},
{
"name": "Sierra Leone",
"value": "SL"
},
{
"name": "Singapore",
"value": "SG"
},
{
"name": "Slovakia",
"value": "SK"
},
{
"name": "Slovenia",
"value": "SI"
},
{
"name": "Solomon Islands",
"value": "SB"
},
{
"name": "Somalia",
"value": "SO"
},
{
"name": "South Africa",
"value": "ZA"
},
{
"name": "South Georgia and the South Sandwich Islands",
"value": "GS"
},
{
"name": "Spain",
"value": "ES"
},
{
"name": "Sri Lanka",
"value": "LK"
},
{
"name": "Sudan",
"value": "SD"
},
{
"name": "Suriname",
"value": "SR"
},
{
"name": "Svalbard and Jan Mayen",
"value": "SJ"
},
{
"name": "Swaziland",
"value": "SZ"
},
{
"name": "Sweden",
"value": "SE"
},
{
"name": "Switzerland",
"value": "CH"
},
{
"name": "Syrian Arab Republic",
"value": "SY"
},
{
"name": "Taiwan",
"value": "TW"
},
{
"name": "Tajikistan",
"value": "TJ"
},
{
"name": "Tanzania, United Republic of",
"value": "TZ"
},
{
"name": "Thailand",
"value": "TH"
},
{
"name": "Togo",
"value": "TG"
},
{
"name": "Tokelau",
"value": "TK"
},
{
"name": "Tonga",
"value": "TO"
},
{
"name": "Trinidad and Tobago",
"value": "TT"
},
{
"name": "Tunisia",
"value": "TN"
},
{
"name": "Turkey",
"value": "TR"
},
{
"name": "Turkmenistan",
"value": "TM"
},
{
"name": "Turks and Caicos Islands",
"value": "TC"
},
{
"name": "Tuvalu",
"value": "TV"
},
{
"name": "Uganda",
"value": "UG"
},
{
"name": "Ukraine",
"value": "UA"
},
{
"name": "United Arab Emirates",
"value": "AE"
},
{
"name": "United Kingdom",
"value": "UK"
},
{
"name": "United States",
"value": "US"
},
{
"name": "United States Minor Outlying Islands",
"value": "UM"
},
{
"name": "Uruguay",
"value": "UY"
},
{
"name": "Uzbekistan",
"value": "UZ"
},
{
"name": "Vanuatu",
"value": "VU"
},
{
"name": "Venezuela",
"value": "VE"
},
{
"name": "Vietnam",
"value": "VN"
},
{
"name": "Virgin Islands, British",
"value": "VG"
},
{
"name": "Virgin Islands, U.S.",
"value": "VI"
},
{
"name": "Wallis and Futuna",
"value": "WF"
},
{
"name": "Western Sahara",
"value": "EH"
},
{
"name": "Yemen",
"value": "YE"
},
{
"name": "Yugoslavia",
"value": "YU"
},
{
"name": "Zambia",
"value": "ZM"
},
{
"name": "Zimbabwe",
"value": "ZW"
}
]

View file

@ -0,0 +1,32 @@
{
"all": {
"tbm": null,
"href": "search?q={query}",
"name": "All",
"selected": true
},
"images": {
"tbm": "isch",
"href": "search?q={query}",
"name": "Images",
"selected": false
},
"maps": {
"tbm": null,
"href": "https://www.openstreetmap.org/search?query={map_query}",
"name": "Maps",
"selected": false
},
"videos": {
"tbm": "vid",
"href": "search?q={query}",
"name": "Videos",
"selected": false
},
"news": {
"tbm": "nws",
"href": "search?q={query}",
"name": "News",
"selected": false
}
}

View file

@ -0,0 +1,210 @@
[
{
"name": "English",
"value": "lang_en"
},
{
"name": "Hungarian (Magyar)",
"value": "lang_hu"
},
{
"name": "Afrikaans (Afrikaans)",
"value": "lang_af"
},
{
"name": "Arabic (عربى)",
"value": "lang_ar"
},
{
"name": "Armenian (հայերեն)",
"value": "lang_hy"
},
{
"name": "Azerbaijani (Azərbaycanca)",
"value": "lang_az"
},
{
"name": "Belarusian (Беларуская)",
"value": "lang_be"
},
{
"name": "Bulgarian (български)",
"value": "lang_bg"
},
{
"name": "Catalan (Català)",
"value": "lang_ca"
},
{
"name": "Chinese, Simplified (简体中文)",
"value": "lang_zh-CN"
},
{
"name": "Chinese, Traditional (正體中文)",
"value": "lang_zh-TW"
},
{
"name": "Croatian (Hrvatski)",
"value": "lang_hr"
},
{
"name": "Czech (čeština)",
"value": "lang_cs"
},
{
"name": "Danish (Dansk)",
"value": "lang_da"
},
{
"name": "Dutch (Nederlands)",
"value": "lang_nl"
},
{
"name": "Esperanto (Esperanto)",
"value": "lang_eo"
},
{
"name": "Estonian (Eestlane)",
"value": "lang_et"
},
{
"name": "Filipino (Pilipino)",
"value": "lang_tl"
},
{
"name": "Finnish (Suomalainen)",
"value": "lang_fi"
},
{
"name": "French (Français)",
"value": "lang_fr"
},
{
"name": "German (Deutsch)",
"value": "lang_de"
},
{
"name": "Greek (Ελληνικά)",
"value": "lang_el"
},
{
"name": "Hebrew (עִברִית)",
"value": "lang_iw"
},
{
"name": "Hindi (हिंदी)",
"value": "lang_hi"
},
{
"name": "Icelandic (Íslenska)",
"value": "lang_is"
},
{
"name": "Indonesian (Indonesian)",
"value": "lang_id"
},
{
"name": "Italian (Italiano)",
"value": "lang_it"
},
{
"name": "Japanese (日本語)",
"value": "lang_ja"
},
{
"name": "Korean (한국어)",
"value": "lang_ko"
},
{
"name": "Kurdish (Kurdî)",
"value": "lang_ku"
},
{
"name": "Latvian (Latvietis)",
"value": "lang_lv"
},
{
"name": "Lithuanian (Lietuvis)",
"value": "lang_lt"
},
{
"name": "Norwegian (Norwegian)",
"value": "lang_no"
},
{
"name": "Persian (فارسی)",
"value": "lang_fa"
},
{
"name": "Polish (Polskie)",
"value": "lang_pl"
},
{
"name": "Portuguese (Português)",
"value": "lang_pt"
},
{
"name": "Romanian (Română)",
"value": "lang_ro"
},
{
"name": "Russian (русский)",
"value": "lang_ru"
},
{
"name": "Serbian (Српски)",
"value": "lang_sr"
},
{
"name": "Sinhala (සිංහල)",
"value": "lang_si"
},
{
"name": "Slovak (Slovák)",
"value": "lang_sk"
},
{
"name": "Slovenian (Slovenščina)",
"value": "lang_sl"
},
{
"name": "Spanish (Español)",
"value": "lang_es"
},
{
"name": "Swahili (Kiswahili)",
"value": "lang_sw"
},
{
"name": "Swedish (Svenska)",
"value": "lang_sv"
},
{
"name": "Thai (ไทย)",
"value": "lang_th"
},
{
"name": "Turkish (Türk)",
"value": "lang_tr"
},
{
"name": "Ukrainian (Український)",
"value": "lang_uk"
},
{
"name": "Vietnamese (Tiếng Việt)",
"value": "lang_vi"
},
{
"name": "Welsh (Cymraeg)",
"value": "lang_cy"
},
{
"name": "Xhosa (isiXhosa)",
"value": "lang_xh"
},
{
"name": "Zulu (isiZulu)",
"value": "lang_zu"
}
]

View file

@ -0,0 +1,5 @@
[
"light",
"dark",
"system"
]

View file

@ -0,0 +1,8 @@
[
{"name": "Any time", "value": ""},
{"name": "Past hour", "value": "qdr:h"},
{"name": "Past 24 hours", "value": "qdr:d"},
{"name": "Past week", "value": "qdr:w"},
{"name": "Past month", "value": "qdr:m"},
{"name": "Past year", "value": "qdr:y"}
]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,260 @@
<!--
Calculator widget.
This file should contain all required
CSS, HTML, and JS for it.
-->
<style>
#calc-text {
background: var(--whoogle-dark-page-bg);
padding: 8px;
border-radius: 8px;
text-align: right;
font-family: monospace;
font-size: 16px;
color: var(--whoogle-dark-text);
}
#prev-equation {
text-align: right;
}
.error-border {
border: 1px solid red;
}
#calc-btns {
display: grid;
grid-template-columns: repeat(6, 1fr);
grid-template-rows: repeat(5, 1fr);
gap: 5px;
}
#calc-btns button {
background: #313141;
color: var(--whoogle-dark-text);
border: none;
border-radius: 8px;
padding: 8px;
cursor: pointer;
}
#calc-btns button:hover {
background: #414151;
}
#calc-btns .common {
background: #51516a;
}
#calc-btns .common:hover {
background: #61617a;
}
#calc-btn-0 { grid-row: 5; grid-column: 3; }
#calc-btn-1 { grid-row: 4; grid-column: 3; }
#calc-btn-2 { grid-row: 4; grid-column: 4; }
#calc-btn-3 { grid-row: 4; grid-column: 5; }
#calc-btn-4 { grid-row: 3; grid-column: 3; }
#calc-btn-5 { grid-row: 3; grid-column: 4; }
#calc-btn-6 { grid-row: 3; grid-column: 5; }
#calc-btn-7 { grid-row: 2; grid-column: 3; }
#calc-btn-8 { grid-row: 2; grid-column: 4; }
#calc-btn-9 { grid-row: 2; grid-column: 5; }
#calc-btn-EQ { grid-row: 5; grid-column: 5; }
#calc-btn-PT { grid-row: 5; grid-column: 4; }
#calc-btn-BCK { grid-row: 5; grid-column: 6; }
#calc-btn-ADD { grid-row: 4; grid-column: 6; }
#calc-btn-SUB { grid-row: 3; grid-column: 6; }
#calc-btn-MLT { grid-row: 2; grid-column: 6; }
#calc-btn-DIV { grid-row: 1; grid-column: 6; }
#calc-btn-CLR { grid-row: 1; grid-column: 5; }
#calc-btn-PRC{ grid-row: 1; grid-column: 4; }
#calc-btn-RP { grid-row: 1; grid-column: 3; }
#calc-btn-LP { grid-row: 1; grid-column: 2; }
#calc-btn-ABS { grid-row: 1; grid-column: 1; }
#calc-btn-SIN { grid-row: 2; grid-column: 2; }
#calc-btn-COS { grid-row: 3; grid-column: 2; }
#calc-btn-TAN { grid-row: 4; grid-column: 2; }
#calc-btn-SQR { grid-row: 5; grid-column: 2; }
#calc-btn-EXP { grid-row: 2; grid-column: 1; }
#calc-btn-E { grid-row: 3; grid-column: 1; }
#calc-btn-PI { grid-row: 4; grid-column: 1; }
#calc-btn-LOG { grid-row: 5; grid-column: 1; }
</style>
<p id="prev-equation"></p>
<div id="calculator-widget">
<p id="calc-text">0</p>
<div id="calc-btns">
<button id="calc-btn-0" class="common">0</button>
<button id="calc-btn-1" class="common">1</button>
<button id="calc-btn-2" class="common">2</button>
<button id="calc-btn-3" class="common">3</button>
<button id="calc-btn-4" class="common">4</button>
<button id="calc-btn-5" class="common">5</button>
<button id="calc-btn-6" class="common">6</button>
<button id="calc-btn-7" class="common">7</button>
<button id="calc-btn-8" class="common">8</button>
<button id="calc-btn-9" class="common">9</button>
<button id="calc-btn-EQ" class="common">=</button>
<button id="calc-btn-PT" class="common">.</button>
<button id="calc-btn-BCK"></button>
<button id="calc-btn-ADD">+</button>
<button id="calc-btn-SUB">-</button>
<button id="calc-btn-MLT">x</button>
<button id="calc-btn-DIV">/</button>
<button id="calc-btn-CLR">C</button>
<button id="calc-btn-PRC">%</button>
<button id="calc-btn-RP">)</button>
<button id="calc-btn-LP">(</button>
<button id="calc-btn-ABS">|x|</button>
<button id="calc-btn-SIN">sin</button>
<button id="calc-btn-COS">cos</button>
<button id="calc-btn-TAN">tan</button>
<button id="calc-btn-SQR"></button>
<button id="calc-btn-EXP">^</button>
<button id="calc-btn-E"></button>
<button id="calc-btn-PI">π</button>
<button id="calc-btn-LOG">log</button>
</div>
</div>
<script>
// JS does not have this by default.
// from https://www.freecodecamp.org/news/how-to-factorialize-a-number-in-javascript-9263c89a4b38/
function factorial(num) {
if (num < 0)
return -1;
else if (num === 0)
return 1;
else {
return (num * factorial(num - 1));
}
}
// returns true if the user is currently focused on the calculator widget
function usingCalculator() {
let activeElement = document.activeElement;
while (true) {
if (!activeElement) return false;
if (activeElement.id === "calculator-wrapper") return true;
activeElement = activeElement.parentElement;
}
}
const $ = q => document.querySelectorAll(q);
// key bindings for commonly used buttons
const keybindings = {
"0": "0",
"1": "1",
"2": "2",
"3": "3",
"4": "4",
"5": "5",
"6": "6",
"7": "7",
"8": "8",
"9": "9",
"Enter": "EQ",
".": "PT",
"+": "ADD",
"-": "SUB",
"*": "MLT",
"/": "DIV",
"%": "PRC",
"c": "CLR",
"(": "LP",
")": "RP",
"Backspace": "BCK",
}
window.addEventListener("keydown", event => {
if (!usingCalculator()) return;
if (event.key === "Enter" && document.activeElement.id !== "search-bar")
event.preventDefault();
if (keybindings[event.key])
document.getElementById("calc-btn-" + keybindings[event.key]).click();
})
// calculates the string
const calc = () => {
var mathtext = document.getElementById("calc-text");
var statement = mathtext.innerHTML
// remove empty ()
.replace("()", "")
// special constants
.replace("π", "(Math.PI)")
.replace("ℇ", "(Math.E)")
// turns 3(1+2) into 3*(1+2) (for example)
.replace(/(?<=[0-9\)])(?<=[^+\-x*\/%^])\(/, "x(")
// same except reversed
.replace(/\)(?=[0-9\(])(?=[^+\-x*\/%^])/, ")x")
// replace human friendly x with JS *
.replace("x", "*")
// trig & misc functions
.replace("sin", "Math.sin")
.replace("cos", "Math.cos")
.replace("tan", "Math.tan")
.replace("√", "Math.sqrt")
.replace("^", "**")
.replace("abs", "Math.abs")
.replace("log", "Math.log")
;
// add any missing )s to the end
while(true) if (
(statement.match(/\(/g) || []).length >
(statement.match(/\)/g) || []).length
) statement += ")"; else break;
// evaluate the expression.
console.log("calculating [" + statement + "]");
try {
var result = eval(statement);
document.getElementById("prev-equation").innerHTML = mathtext.innerHTML + " = ";
mathtext.innerHTML = result;
mathtext.classList.remove("error-border");
} catch (e) {
mathtext.classList.add("error-border");
console.error(e);
}
}
const updateCalc = (e) => {
// character(s) recieved from button
var c = event.target.innerHTML;
var mathtext = document.getElementById("calc-text");
if (mathtext.innerHTML === "0") mathtext.innerHTML = "";
// special cases
switch (c) {
case "C":
// Clear
mathtext.innerHTML = "0";
break;
case "⬅":
// Delete
mathtext.innerHTML = mathtext.innerHTML.slice(0, -1);
if (mathtext.innerHTML.length === 0) {
mathtext.innerHTML = "0";
}
break;
case "=":
calc()
break;
case "sin":
case "cos":
case "tan":
case "log":
case "√":
mathtext.innerHTML += `${c}(`;
break;
case "|x|":
mathtext.innerHTML += "abs("
break;
case "+":
case "-":
case "x":
case "/":
case "%":
case "^":
if (mathtext.innerHTML.length === 0) mathtext.innerHTML = "0";
// prevent typing 2 operators in a row
if (mathtext.innerHTML.match(/[+\-x\/%^] $/))
mathtext.innerHTML = mathtext.innerHTML.slice(0, -3);
mathtext.innerHTML += ` ${c} `;
break;
default:
mathtext.innerHTML += c;
}
}
for (let i of $("#calc-btns button")) {
i.addEventListener('click', event => {
updateCalc(event);
})
}
</script>

40
app/templates/display.html Executable file
View file

@ -0,0 +1,40 @@
<html>
<head>
<link rel="shortcut icon" href="static/img/favicon.ico" type="image/x-icon">
<link rel="icon" href="static/img/favicon.ico" type="image/x-icon">
{% if not search_type %}
<link rel="search" href="opensearch.xml" type="application/opensearchdescription+xml" title="RaveSearch">
{% else %}
<link rel="search" href="opensearch.xml?tbm={{ search_type }}" type="application/opensearchdescription+xml" title="RaveSearch ({{ search_name }})">
{% endif %}
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer">
<link rel="stylesheet" href="{{ cb_url('logo.css') }}">
<link rel="stylesheet" href="{{ cb_url('input.css') }}">
<link rel="stylesheet" href="{{ cb_url('search.css') }}">
<link rel="stylesheet" href="{{ cb_url('header.css') }}">
<style>
@import "{{ cb_url('dark-theme.css') }}";
</style>
<title>{{ clean_query(query) }} - RaveSearch</title>
</head>
<body>
{{ search_header|safe }}
{% if is_translation %}
<iframe
id="lingva-iframe"
src="{{ lingva_url }}/auto/{{ translate_to }}/{{ translate_str }}">
</iframe>
{% endif %}
{{ response|safe }}
</body>
{% include 'footer.html' %}
{% if autocomplete_enabled == '1' %}
<script src="{{ cb_url('autocomplete.js') }}"></script>
{% endif %}
<script src="{{ cb_url('utils.js') }}"></script>
<script src="{{ cb_url('keyboard.js') }}"></script>
<script src="{{ cb_url('currency.js') }}"></script>
</html>

106
app/templates/error.html Executable file
View file

@ -0,0 +1,106 @@
{% if config.theme %}
{% if config.theme == 'system' %}
<style>
@import "{{ cb_url('light-theme.css') }}" screen;
@import "{{ cb_url('dark-theme.css') }}" screen and (prefers-color-scheme: dark);
</style>
{% else %}
<link rel="stylesheet" href="{{ cb_url(config.theme + '-theme.css') }}"/>
{% endif %}
{% else %}
<link rel="stylesheet" href="{{ cb_url(('dark' if config.dark else 'light') + '-theme.css') }}"/>
{% endif %}
<link rel="stylesheet" href="{{ cb_url('main.css') }}">
<link rel="stylesheet" href="{{ cb_url('error.css') }}">
<style>{{ config.style }}</style>
<div>
<h1>Error</h1>
<p>
{{ error_message }}
</p>
<hr>
{% if query and translation %}
<p>
<h4><a class="link" href="https://farside.link">{{ translation['continue-search'] }}</a></h4>
<ul>
<li>
<a href="https://github.com/benbusby/whoogle-search">Whoogle</a>
<ul>
<li>
<a class="link-color" href="{{farside}}/whoogle/search?q={{query}}{{params}}">
{{farside}}/whoogle/search?q={{query}}
</a>
</li>
</ul>
</li>
<li>
<a href="https://github.com/searxng/searxng">SearXNG</a>
<ul>
<li>
<a class="link-color" href="{{farside}}/searxng/search?q={{query}}">
{{farside}}/searxng/search?q={{query}}
</a>
</li>
</ul>
</li>
</ul>
<hr>
<h4>Other options:</h4>
<ul>
<li>
<a href="https://kagi.com">Kagi</a>
<ul>
<li>Requires account</li>
<li>
<a class="link-color" href="https://kagi.com/search?q={{query}}">
kagi.com/search?q={{query}}
</a>
</li>
</ul>
</li>
<li>
<a href="https://duckduckgo.com">DuckDuckGo</a>
<ul>
<li>
<a class="link-color" href="https://duckduckgo.com/search?q={{query}}">
duckduckgo.com/search?q={{query}}
</a>
</li>
</ul>
</li>
<li>
<a href="https://search.brave.com">Brave Search</a>
<ul>
<li>
<a class="link-color" href="https://search.brave.com/search?q={{query}}">
search.brave.com/search?q={{query}}
</a>
</li>
</ul>
</li>
<li>
<a href="https://ecosia.com">Ecosia</a>
<ul>
<li>
<a class="link-color" href="https://ecosia.com/search?q={{query}}">
ecosia.com/search?q={{query}}
</a>
</li>
</ul>
</li>
<li>
<a href="https://google.com">Google</a>
<ul>
<li>
<a class="link-color" href="https://google.com/search?q={{query}}">
google.com/search?q={{query}}
</a>
</li>
</ul>
</li>
</ul>
<hr>
</p>
{% endif %}
<a class="link" href="home">Return Home</a>
</div>

18
app/templates/footer.html Executable file
View file

@ -0,0 +1,18 @@
</div>
<footer>
<p class="footer">
A RaveSearch a Whoogle v{{ version_number }} alapjait használja ||
<a class="link" href="https://github.com/benbusby/whoogle-search"
>{{ translation['github-link'] }}</a
></p>
<!-- {% if has_update %} ||
<span class="update_available">Update Available 🟢</span>
{% endif %} -->
<span>
<p class="rp-credit" style="color: white">
Szeretnél többet megtudni a keresőről? <a class="kattide" href="https://rp1.hu/leirasok/ravepriest1-kereso/">RavePriest1</a>
</p>
</span>
</footer>

90
app/templates/header.html Executable file
View file

@ -0,0 +1,90 @@
<div class="header-div">
<div id="search_header">
<a id="search_logo" href="{{ home_url }}" tabindex="0">
{{ logo|safe }}
</a>
</div>
<form class="search-form header" id="search-form" method="{{ 'get' if config.get_only else 'post' }}" role="search" action="search">
<div class="header-search">
<div class="autocomplete">
{% if config.preferences %}
<input type="hidden" name="preferences" value="{{ config.preferences }}" />
{% endif %}
<input
id="search-bar"
class="mobile-search-bar"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
name="q"
type="text"
value="{{ clean_query(query) }}"
dir="auto"
/>
<input id="search-reset" type="reset" value="x">
<input name="tbm" value="{{ search_type }}" style="display: none">
<input name="country" value="{{ config.country }}" style="display: none;">
<input type="submit" id="search-submit" style="display: none;">
</div>
</div>
</form>
<div class="header-tab-div">
<div class="mobile-header header-tab">
{% for tab_id, tab_content in tabs.items() %}
{% if tab_content['selected'] %}
<a><button class="header-button selected-header-button">{{ tab_content['name'] }}</button></a>
{% else %}
<a href="{{ tab_content['href'] }}"><button class="header-button">{{ tab_content['name'] }}</button></a>
{% endif %}
{% endfor %}
<label for="adv-search-toggle" id="adv-search-label" class="adv-search"></label>
<input id="adv-search-toggle" type="checkbox">
<div class="header-tab-div-end"></div>
</div>
</div>
</div>
<div class="result-collapsible" id="adv-search-div">
<div class="result-config">
<label for="config-country">{{ translation['config-country'] }}: </label>
<select name="country" id="result-country">
{% for country in countries %}
<option value="{{ country.value }}"
{% if (
config.country != '' and config.country in country.value
) or (
config.country == '' and country.value == '')
%}
selected
{% endif %}>
{{ country.name }}
</option>
{% endfor %}
</select>
<br /><br />
<label for="config-time-period">{{ translation['config-time-period'] }}: </label>
<select name="tbs" id="result-time-period">
{% for time_period in time_periods %}
<option value="{{ time_period.value }}"
{% if (
config.tbs != '' and config.tbs in time_period.value
) or (
config.tbs == '' and time_period.value == '')
%}
selected
{% endif %}>
{{ translation[time_period.value] }}
</option>
{% endfor %}
</select>
</div>
</div>
<script type="text/javascript" src="{{ cb_url('header.js') }}"></script>
<!-- Az oldal alján -->

210
app/templates/header2.html Executable file
View file

@ -0,0 +1,210 @@
{% if mobile %}
<header>
<div class="header-div">
<form class="search-form header"
id="search-form"
method="{{ 'GET' if config.get_only else 'POST' }}">
<a class="logo-link mobile-logo" href="{{ home_url }}">
<div id="mobile-header-logo">
{{ logo|safe }}
</div>
</a>
<div class="H0PQec mobile-input-div">
<div class="autocomplete-mobile esbc autocomplete">
{% if config.preferences %}
<input type="hidden" name="preferences" value="{{ config.preferences }}" />
{% endif %}
<input
id="search-bar"
class="mobile-search-bar"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
class="search-bar-input"
name="q"
type="text"
value="{{ clean_query(query) }}"
dir="auto">
<input id="search-reset" type="reset" value="x">
<input name="tbm" value="{{ search_type }}" style="display: none">
<input name="country" value="{{ config.country }}" style="display: none;">
<input type="submit" style="display: none;">
<div class="sc"></div>
</div>
</div>
</form>
</div>
<div>
<div class="header-tab-div">
<div class="header-tab-div-2">
<div class="header-tab-div-3">
<div class="mobile-header header-tab">
{% for tab_id, tab_content in tabs.items() %}
{% if tab_content['selected'] %}
<span class="mobile-tab-span">{{ tab_content['name'] }}</span>
{% else %}
<a class="header-tab-a" href="{{ tab_content['href'] }}">{{ tab_content['name'] }}</a>
{% endif %}
{% endfor %}
<label for="adv-search-toggle" id="adv-search-label" class="adv-search"></label>
<input id="adv-search-toggle" type="checkbox">
<div class="header-tab-div-end"></div>
</div>
</div>
</div>
</div>
<div class="" id="s">
</div>
</header>
{% else %}
<header>
<div class="logo-div">
<a class="logo-link" href="{{ home_url }}">
<div class="desktop-header-logo">
{{ logo|safe }}
</div>
</a>
</div>
<div class="search-div">
<form id="search-form"
class="search-form"
id="sf"
method="{{ 'GET' if config.get_only else 'POST' }}">
<div class="autocomplete header-autocomplete">
<div style="width: 100%; display: flex">
{% if config.preferences %}
<input type="hidden" name="preferences" value="{{ config.preferences }}" />
{% endif %}
<input
id="search-bar"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
class="search-bar-desktop search-bar-input"
name="q"
spellcheck="false"
type="text"
value="{{ clean_query(query) }}"
dir="auto">
<input name="tbm" value="{{ search_type }}" style="display: none">
<input name="country" value="{{ config.country }}" style="display: none;">
<input name="tbs" value="{{ config.tbs }}" style="display: none;">
<input type="submit" style="display: none;">
<div class="sc"></div>
</div>
</div>
</form>
</div>
</header>
<div>
<div class="header-tab-div">
<div class="header-tab-div-2">
<div class="header-tab-div-3">
<div class="desktop-header header-tab">
{% for tab_id, tab_content in tabs.items() %}
{% if tab_content['selected'] %}
<span class="header-tab-span">{{ tab_content['name'] }}</span>
{% else %}
<a class="header-tab-a" href="{{ tab_content['href'] }}">{{ tab_content['name'] }}</a>
{% endif %}
{% endfor %}
<label for="adv-search-toggle" id="adv-search-label" class="adv-search"></label>
<input id="adv-search-toggle" type="checkbox">
</div>
</div>
</div>
</div>
<div class="" id="s">
</div>
{% endif %}
<div class="result-collapsible" id="adv-search-div">
<div class="result-config">
<label for="config-country">{{ translation['config-country'] }}: </label>
<select name="country" id="result-country">
{% for country in countries %}
<option value="{{ country.value }}"
{% if (
config.country != '' and config.country in country.value
) or (
config.country == '' and country.value == '')
%}
selected
{% endif %}>
{{ country.name }}
</option>
{% endfor %}
</select>
<br />
<label for="config-time-period">{{ translation['config-time-period'] }}: </label>
<select name="tbs" id="result-time-period">
{% for time_period in time_periods %}
<option value="{{ time_period.value }}"
{% if (
config.tbs != '' and config.tbs in time_period.value
) or (
config.tbs == '' and time_period.value == '')
%}
selected
{% endif %}>
{{ translation[time_period.value] }}
</option>
{% endfor %}
</select>
</div>
</div>
<script type="text/javascript" src="{{ cb_url('header.js') }}"></script>
<div id="search_view">
<div class="search_box">
<input id="q" name="q" type="text" placeholder="{{ _('Search for...') }}" tabindex="1" autocomplete="off" autocapitalize="none" spellcheck="false" autocorrect="off" dir="auto" value="{{ q or '' }}">
<button id="clear_search" type="reset" aria-label="{{ _('clear') }}" class="hide_if_nojs"><span>{{ icon_big('close') }}</span><span class="show_if_nojs">{{ _('clear') }}</span></button>
<button id="send_search" type="submit" {%- if search_on_category_select -%}name="category_{{ selected_categories[0]|replace(' ', '_') }}"{%- endif -%} aria-label="{{ _('search') }}"><span class="hide_if_nojs">{{ icon_big('search-outline') }}</span><span class="show_if_nojs">{{ _('search') }}</span></button>
</div>
</div>
{% set display_tooltip = true %}
{% include 'simple/categories.html' %}
</div>
<div class="search_filters">
{% include 'simple/filters/languages.html' %}
{% include 'simple/filters/time_range.html' %}
{% include 'simple/filters/safesearch.html' %}
</div>
<input type="hidden" name="theme" value="{{ theme }}" >
{% if timeout_limit %}<input type="hidden" name="timeout_limit" value="{{ timeout_limit|e }}" >{% endif %}
</form>
<div id="categories" class="search_categories">{{- '' -}}
<div id="categories_container">
{%- if not search_on_category_select or not display_tooltip -%}
{%- for category in categories_as_tabs -%}
<div class="category category_checkbox">{{- '' -}}
<input type="checkbox" id="checkbox_{{ category|replace(' ', '_') }}" name="category_{{ category }}"{% if category in selected_categories %} checked="checked"{% endif %}/>
<label for="checkbox_{{ category|replace(' ', '_') }}" class="tooltips">
{{- icon_big(category_icons[category]) if category in category_icons else icon_big('globe-outline') -}}
<div class="category_name">{{- _(category) -}}</div>
</label>
</div>
{%- endfor -%}
{%- if display_tooltip %}<div class="help">{{ _('Click on the magnifier to perform search') }}</div>{% endif -%}
{%- else -%}
{%- for category in categories_as_tabs -%}{{- '\n' -}}
<button type="submit" name="category_{{ category }}" class="category category_button {% if category in selected_categories %}selected{% endif %}">
{{- icon_big(category_icons[category]) if category in category_icons else icon_big('globe-outline') -}}
<div class="category_name">{{- _(category) -}}</div>{{- '' -}}
</button>{{- '' -}}
{%- endfor -%}
{{- '\n' -}}
{%- endif -%}
</div>{{- '' -}}
</div>

38
app/templates/imageresults.html Executable file
View file

@ -0,0 +1,38 @@
<div>
<div class="results">
{% for result in results %}
<div class="result-item">
<a href="{{ result.img_url }}">
<div class="result-img"><img class="img-thumbnail" src="{{ result.img_tbn }}" alt="Image thumbnail"></div>
</a>
<div class="site-info">
<div class="site-favicon">
<img
class="favicon-img"
src="{{ result.favicon }}"
alt="site icon"
onerror="this.src='/static/img/default-favicon.png';">
</div>
<a href="{{ result.web_page }}">
<span class="img-title">{{ result.domain }}</span>
</a>
</div>
</div>
{% endfor %}
</div>
<div class="uZgmoc">
<!-- next page object -->
</div>
<br />
<div class="lIMUZd">
<div class="By0U9">
<!-- correction suggested -->
</div>
</div>
</div>

303
app/templates/index.html Executable file
View file

@ -0,0 +1,303 @@
<!DOCTYPE html>
<html>
<head>
<link rel="apple-touch-icon" sizes="180x180" href="static/img/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="static/img/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="static/img/favicon/favicon-16x16.png">
<link rel="manifest" href="static/img/favicon/site.webmanifest">
<link rel="mask-icon" href="static/img/favicon/safari-pinned-tab.svg" color="#00566f">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
{% if autocomplete_enabled == '1' %}
<script src="{{ cb_url('autocomplete.js') }}"></script>
{% endif %}
<script type="text/javascript" src="{{ cb_url('controller.js') }}"></script>
<link rel="search" href="opensearch.xml" type="application/opensearchdescription+xml" title="RaveSearch">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ cb_url('logo.css') }}">
<style>
@import "{{ cb_url('dark-theme.css') }}";
</style>
<link rel="stylesheet" href="{{ cb_url('main.css') }}">
<title>RaveSearch</title>
</head>
<body id="main_index">
<main class="search-container">
<div class="logo-container">
{{ logo|safe }}
</div>
<form id="search-form" action="search" method="{{ 'get' if config.get_only else 'post' }}">
<div class="search-fields">
<div class="autocomplete">
{% if config.preferences %}
<input type="hidden" name="preferences" value="{{ config.preferences }}" />
{% endif %}
<input
placeholder="{{ translation['input-text'] }}"
type="text"
name="q"
id="search-bar"
autofocus="autofocus"
autocapitalize="none"
spellcheck="false"
autocorrect="off"
autocomplete="off"
dir="auto" />
</div>
<input type="submit" id="search-submit" value="{{ translation['search'] }}">
</div>
</form>
{% if not config_disabled %}
<button id="config-collapsible" class="collapsible">{{ translation['config'] }}</button>
<div class="content">
<div class="config-fields">
<form id="config-form" action="config" method="post">
<div class="config-options">
<!--<div class="config-div config-div-country">
<label for="config-country">{{ translation['config-country'] }}: </label>
<select name="country" id="config-country">
{% for country in countries %}
<option value="{{ country.value }}"
{% if (
config.country != '' and config.country in country.value
) or (
config.country == '' and country.value == '')
%}
selected
{% endif %}>
{{ country.name }}
</option>
{% endfor %}
</select>
</div> -->
<!-- <div class="config-div">
<label for="config-time-period">{{ translation['config-time-period'] }}</label>
<select name="tbs" id="config-time-period">
{% for time_period in time_periods %}
<option value="{{ time_period.value }}"
{% if (
config.tbs != '' and config.tbs in time_period.value
) or (
config.tbs == '' and time_period.value == '')
%}
selected
{% endif %}>
{{ translation[time_period.value] }}
</option>
{% endfor %}
</select>
</div> -->
<div class="config-div config-div-lang">
<label for="config-lang-interface">{{ translation['config-lang'] }}: </label>
<select name="lang_interface" id="config-lang-interface">
{% for lang in languages %}
<option value="{{ lang.value }}"
{% if lang.value in config.lang_interface %}
selected
{% endif %}>
{{ lang.name }}
</option>
{% endfor %}
</select>
</div>
<div class="config-div config-div-search-lang">
<label for="config-lang-search">{{ translation['config-lang-search'] }}: </label>
<select name="lang_search" id="config-lang-search">
{% for lang in languages %}
<option value="{{ lang.value }}"
{% if lang.value in config.lang_search %}
selected
{% endif %}>
{{ lang.name }}
</option>
{% endfor %}
</select>
</div>
<!-- <div class="config-div config-div-autocomplete">
<label for="config-autocomplete">autocomplete</label>
<input type="checkbox" name="autocomplete" id="config-autocomplete" {{ 'checked' if config.autocomplete else '' }}>
</div> -->
<!-- <div class="config-div config-div-near">
<label for="config-near">{{ translation['config-near'] }}: </label>
<input type="text" name="near" id="config-near"
placeholder="{{ translation['config-near-help'] }}" value="{{ config.near }}">
</div> -->
<!-- <div class="config-div config-div-block">
<label for="config-block">{{ translation['config-block'] }}: </label>
<input type="text" name="block" id="config-block"
placeholder="{{ translation['config-block-help'] }}" value="{{ config.block }}">
</div> -->
<!-- <div class="config-div config-div-block">
<label for="config-block-title">{{ translation['config-block-title'] }}: </label>
<input type="text" name="block_title" id="config-block"
placeholder="{{ translation['config-block-title-help'] }}"
value="{{ config.block_title }}">
</div> -->
<!-- <div class="config-div config-div-block">
<label for="config-block-url">{{ translation['config-block-url'] }}: </label>
<input type="text" name="block_url" id="config-block"
placeholder="{{ translation['config-block-url-help'] }}" value="{{ config.block_url }}">
</div> -->
<div class="config-div config-div-anon-view">
<label for="config-anon-view">{{ translation['config-anon-view'] }}: </label>
<input type="checkbox" name="anon_view" id="config-anon-view" {{ 'checked' if config.anon_view else '' }}>
</div>
<div class="config-div config-div-nojs">
<label for="config-nojs">{{ translation['config-nojs'] }}: </label>
<input type="checkbox" name="nojs" id="config-nojs" {{ 'checked' if config.nojs else '' }}>
</div>
<!-- DEPRECATED -->
<!--<div class="config-div config-div-dark">-->
<!--<label for="config-dark">{{ translation['config-dark'] }}: </label>-->
<!--<input type="checkbox" name="dark" id="config-dark" {{ 'checked' if config.dark else '' }}>-->
<!--</div>-->
<div class="config-div config-div-safe">
<label for="config-safe">{{ translation['config-safe'] }}: </label>
<input type="checkbox" name="safe" id="config-safe" {{ 'checked' if config.safe else '' }}>
</div>
<div class="config-div config-div-alts">
<label class="tooltip" for="config-alts">{{ translation['config-alts'] }}: </label>
<input type="checkbox" name="alts" id="config-alts" {{ 'checked' if config.alts else '' }}>
<div><span class="info-text"> — {{ translation['config-alts-help'] }}</span></div>
</div>
<div class="config-div config-div-new-tab">
<label for="config-new-tab">{{ translation['config-new-tab'] }}: </label>
<input type="checkbox" name="new_tab"
id="config-new-tab" {{ 'checked' if config.new_tab else '' }}>
</div>
<!-- <div class="config-div config-div-view-image">
<label for="config-view-image">{{ translation['config-images'] }}: </label>
<input checked type="checkbox" name="view_image"
id="config-view-image" {{ 'checked' if config.view_image else '' }}>
<div><span class="info-text"> — {{ translation['config-images-help'] }}</span></div>
</div> -->
<div class="config-div config-div-tor">
<label for="config-tor">{{ translation['config-tor'] }}: {{ '' if tor_available else 'Unavailable' }}</label>
<input type="checkbox" name="tor"
id="config-tor" {{ '' if tor_available else 'hidden' }} {{ 'checked' if config.tor else '' }}>
</div>
<div class="config-div config-div-get-only">
<label for="config-get-only">{{ translation['config-get-only'] }}: </label>
<input type="checkbox" name="get_only"
id="config-get-only" {{ 'checked' if config.get_only else '' }}>
</div>
<!-- <div class="config-div config-div-accept-language">
<label for="config-accept-language">Set Accept-Language: </label>
<input type="checkbox" name="accept_language"
id="config-accept-language" {{ 'checked' if config.accept_language else '' }}>
</div> -->
<div class="config-div config-div-root-url">
<label for="config-url">{{ translation['config-url'] }}: </label>
<input type="text" name="url" id="config-url" value="{{ config.url }}">
</div>
<div class="config-div config-div-custom-css">
<a id="css-link"
href="https://github.com/benbusby/whoogle-search/wiki/User-Contributed-CSS-Themes">
{{ translation['config-css'] }}:
</a>
<textarea
name="style_modified"
id="config-style"
autocapitalize="off"
autocomplete="off"
spellcheck="false"
autocorrect="off"
value="">{{ config.style_modified.replace('\t', '') }}</textarea>
</div>
<div class="config-div config-div-pref-url">
<label for="config-pref-encryption">{{ translation['config-pref-encryption'] }}: </label>
<input type="checkbox" name="preferences_encrypted"
id="config-pref-encryption" {{ 'checked' if config.preferences_encrypted and config.preferences_key else '' }}>
<div><span class="info-text"> — {{ translation['config-pref-help'] }}</span></div>
<label for="config-pref-url">{{ translation['config-pref-url'] }}: </label>
<input type="text" name="pref-url" id="config-pref-url" value="{{ config.url }}?preferences={{ config.preferences }}">
</div>
</div>
<div class="config-div config-buttons">
<input type="submit" id="config-load" value="{{ translation['load'] }}">&nbsp;
<input type="submit" id="config-submit" value="{{ translation['apply'] }}">&nbsp;
<input type="submit" id="config-save" value="{{ translation['save-as'] }}">
</div>
</form>
</div>
</div>
{% endif %}
<div class="social-container">
<div class="social-column">
<div class="rpkick">
<a href="https://kick.com/ravepriest1">
<img
src="static/img/rp/rpkicksearch.png"
alt="rpkick"
title="RP Kick"
width="200px"
height="50px"
/></a>
</div>
<div class="rpdiscord">
<a href="https://discord.com/invite/MwUydP4gc5">
<img
src="static/img/rp/rpdiscordsearch.png"
alt="rpdiscord"
title="RP Discord"
width="200px"
height="50px"
/></a>
</div>
<div class="rptwitch">
<a href="https://www.twitch.tv/ravepriest1">
<img
src="static/img/rp/rptwitchsearch.png"
alt="rptwitch"
title="RP Twitch"
width="200px"
height="50px"
/></a>
</div>
<div class="rpgit">
<a href="https://git.rp1.hu/explore/repos">
<img
src="static/img/rp/rpgiteasearch.png"
alt="rpgit"
title="RP Gitea"
width="200px"
height="50px"
/></a>
</div>
<div class="rpyoutube">
<a href="https://www.youtube.com/@RPslair">
<img
src="static/img/rp/rpyoutubesearch.png"
alt="rpyoutube"
title="RP YouTube"
width="200px"
height="50px"
/></a>
</div>
<div class="rpweb">
<a href="https://rp1.hu">
<img
src="static/img/rp/rpwebsearch.png"
alt="rpweb"
title="RP Website"
width="200px"
height="50px"
/></a>
</div>
</div>
</div>
</main>
{% include 'footer.html' %}
</body>
</html>

Some files were not shown because too many files have changed in this diff Show more