diff --git a/.dockerignore b/.dockerignore
deleted file mode 100644
index 05850e9..0000000
--- a/.dockerignore
+++ /dev/null
@@ -1,11 +0,0 @@
-.gitignore
-.git
-.DS_Store
-.idea
-.dockerignore
-venv
-docs
-doc_generator
-gen_docs.sh
-README.md
-Dockerfile
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index f85f6dd..6f33716 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,6 @@
.env
-*.yaml
-*.json
.idea
venv
-__pycache__
-build
+__pycache__/
+_build/
+fileio
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 8d4792a..414d611 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -8,7 +8,7 @@ repos:
additional_dependencies:
- flake8-docstrings
- flake8-sfs
- args: [--max-line-length=120, --extend-ignore=SFS3 D107 SFS301 D100 D104 D401]
+ args: [--max-line-length=120, --extend-ignore=SFS3 D107 D100 D104 D401 SFS101 SFS201]
-
repo: https://github.com/pre-commit/mirrors-isort
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 20c9ca8..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,12 +0,0 @@
-FROM python:3.9-slim
-
-ENV DOCKER=1
-
-RUN mkdir /opt/netscan
-COPY . /opt/netscan
-
-RUN cd /opt/netscan && pip3 install --user -r requirements.txt
-
-WORKDIR /opt/netscan
-
-ENTRYPOINT ["/usr/local/bin/python", "./analyzer.py"]
\ No newline at end of file
diff --git a/README.md b/README.md
index 57f15ed..81dc153 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,43 @@
# NetScan
Network Scanner to analyze devices connecting to the router and alert accordingly.
-NetScan displays the connected devices, Wi-Fi signal info, speed, etc.
-
-This app can display intruders IP addresses, MAC addresses, and lets the user Ping the device, and even Block user from
-Wi-Fi.
+This app can display intruders' IP addresses, MAC addresses, and lets the user Ping the device, and even Block the device.
Block IP Address feature helps the user to remove the specified connections and block the specific IP address.
-### Docker Setup:
+> Blocking device feature is currently available only for `Netgear` router users.
+
+## Coding Standards
+Docstring format: [`Google`](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
+Styling conventions: [`PEP 8`](https://www.python.org/dev/peps/pep-0008/)
+Clean code with pre-commit hooks: [`flake8`](https://flake8.pycqa.org/en/latest/) and
+[`isort`](https://pycqa.github.io/isort/)
+
+## [Release Notes](https://github.com/thevickypedia/netscan/blob/master/release_notes.rst)
+**Requirement**
+```shell
+python -m pip install changelog-generator
+```
+
+**Usage**
+```shell
+changelog reverse -f release_notes.rst -t 'Release Notes'
+```
-###### Commands:
-- `docker build --progress plain -t netscan .`
-- `docker run netscan`
+## Linting
+`PreCommit` will ensure linting, and the doc creation are run on every commit.
-[Additional options that can be added to the `docker build` command.](https://docs.docker.com/engine/reference/commandline/build/#options)
+**Requirement**
+```shell
+pip install sphinx==5.1.1 pre-commit recommonmark
+```
-###### [Reference](https://github.com/MatMaul/pynetgear)
+**Usage**
+```shell
+pre-commit run --all-files
+```
-###### [Runbook](https://thevickypedia.github.io/netscan/)
+## Runbook
+[![made-with-sphinx-doc](https://img.shields.io/badge/Code%20Docs-Sphinx-1f425f.svg)](https://www.sphinx-doc.org/en/master/man/sphinx-autogen.html)
-###### [Repository](https://github.com/thevickypedia/netscan)
+[https://thevickypedia.github.io/netscan/](https://thevickypedia.github.io/netscan/)
diff --git a/analyzer.py b/analyzer.py
index 1fc25ce..da3d3dd 100644
--- a/analyzer.py
+++ b/analyzer.py
@@ -1,323 +1,31 @@
-import importlib
-import json
-import logging
-import os
-import platform
-import subprocess
-import time
-from datetime import datetime, timezone
-from pathlib import PurePath
-from typing import AnyStr, NoReturn, Union
+from typing import NoReturn
-import yaml
-from dotenv import load_dotenv
-from gmailconnector.send_sms import Messenger
-from pynetgear import Device, Netgear
+from modules import att, models, netgear
-importlib.reload(logging)
-LOGGER = logging.getLogger(name=PurePath(__file__).stem)
-log_formatter = logging.Formatter(
- fmt="%(asctime)s - [%(levelname)s] - %(name)s - %(funcName)s - Line: %(lineno)d - %(message)s",
- datefmt='%b-%d-%Y %H:%M:%S'
-)
-handler = logging.StreamHandler()
-handler.setFormatter(fmt=log_formatter)
-LOGGER.setLevel(level=logging.INFO)
-LOGGER.addHandler(hdlr=handler)
-
-def custom_time(*args: logging.Formatter or time.time) -> time.struct_time:
- """Creates custom timezone for ``logging`` which gets used only when invoked by ``Docker``.
-
- This is used only when triggered within a ``docker container`` as it uses UTC timezone.
-
- Args:
- *args: Takes ``Formatter`` object and current epoch time as arguments passed by ``formatTime`` from ``logging``.
-
- Returns:
- struct_time:
- A struct_time object which is a tuple of:
- **current year, month, day, hour, minute, second, weekday, year day and dst** *(Daylight Saving Time)*
- """
- LOGGER.debug(args)
- local_timezone = datetime.now(tz=timezone.utc).astimezone().tzinfo
- return datetime.now().astimezone(tz=local_timezone).timetuple()
-
-
-def extract_str(input_: AnyStr) -> str:
- """Extracts strings from the received input.
+def network_monitor(module: models.SupportedModules, init: bool = True) -> NoReturn:
+ """Monitor devices connected to the network.
Args:
- input_: Takes a string as argument.
-
- Returns:
- str:
- A string after removing special characters.
- """
- return "".join([i for i in input_ if not i.isdigit() and i not in [",", ".", "?", "-", ";", "!", ":"]]).strip()
-
-
-def device_name() -> str:
- """Gets the device name for MacOS and Windows."""
- if platform.system() == 'Darwin':
- system_kernel = subprocess.check_output("sysctl hw.model", shell=True).decode('utf-8').splitlines()
- return extract_str(system_kernel[0].split(':')[1])
- elif platform.system() == 'Windows':
- return subprocess.getoutput("WMIC CSPRODUCT GET VENDOR").replace('Vendor', '').strip()
-
-
-def get_ssid() -> Union[str, None]:
- """Checks the current operating system and runs the appropriate command to get the SSID of the access point.
-
- Returns:
- str:
- SSID of the access point/router which is being accessed.
+ module: Module to scan. Currently, supports any network on a Netgear router or At&t networks.
+ init: Takes a boolean value to create a snapshot file or actually monitor the network.
"""
- if platform.system() == 'Darwin':
- process = subprocess.Popen(
- ["/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport", "-I"],
- stdout=subprocess.PIPE
- )
- out, err = process.communicate()
- if out.decode(encoding='UTF-8').strip() == "AirPort: Off":
- LOGGER.warning(f"{device_name()} WiFi is turned off.")
- return
- if error := process.returncode:
- LOGGER.error(f"Failed to fetch SSID with exit code: {error}\n{err}")
- return
- # noinspection PyTypeChecker
- return dict(map(str.strip, info.split(": ")) for info in out.decode("utf-8").splitlines()[:-1] if
- len(info.split()) == 2).get("SSID")
- elif platform.system() == 'Windows':
- netsh = subprocess.check_output("netsh wlan show interfaces", shell=True)
- for info in netsh.decode('utf-8').split('\n')[:-1]:
- if 'SSID' in info:
- return info.strip('SSID').replace('SSID', '').replace(':', '').strip()
-
-
-def send_sms(msg: str) -> NoReturn:
- """Sens an SMS notification when invoked by the ``run`` method.
-
- Args:
- msg: Message that has to be sent.
- """
- if os.environ.get('gmail_user') and os.environ.get('gmail_pass') and os.environ.get('phone'):
- messenger = Messenger(gmail_user=os.environ.get('gmail_user'), gmail_pass=os.environ.get('gmail_pass'),
- phone=os.environ.get('phone'), subject="Cyber Alert", message=msg)
- response = messenger.send_sms()
- if response.ok:
- LOGGER.info(f"Firewall alert has been sent to {os.environ.get('phone')}")
+ if module == models.SupportedModules.netgear:
+ if init:
+ netgear.LocalIPScan().create_snapshot()
else:
- LOGGER.error(f"Failed to send a notification.\n{response.body}")
-
-
-class LocalIPScan:
- """Connector to scan devices in the same IP range using ``Netgear API``.
-
- >>> LocalIPScan
-
- """
-
- def __init__(self, router_pass: str = None):
- """Gets local host devices connected to the same network range.
-
- Args:
- router_pass: Password to authenticate the API client.
- """
- env_file_path = '.env'
- load_dotenv(dotenv_path=env_file_path)
- if not router_pass:
- if not (router_pass := os.environ.get('router_pass')):
- raise ValueError(
- 'Router password is required.'
- )
- self.ssid = get_ssid() or 'your Network.'
- self.snapshot = 'snapshot.json'
- self.blocked = 'blocked.yaml'
- self.netgear = Netgear(password=router_pass)
-
- def _get_devices(self) -> Device:
- """Scans the Netgear router for connected devices and the devices' information.
-
- Returns:
- Device:
- Returns list of devices connected to the router and the connection information.
- """
- LOGGER.info(f'Getting devices connected to {self.ssid}')
- return self.netgear.get_attached_devices()
-
- def create_snapshot(self) -> NoReturn:
- """Creates a snapshot.json which is used to determine the known and unknown devices."""
- LOGGER.warning(f"Creating a snapshot will capture the current list of devices connected to {self.ssid} at"
- " this moment.")
- LOGGER.warning("This capture will be used to alert/block when new devices are connected. So, "
- f"please review the {self.snapshot} manually and remove the devices that aren't recognized.")
- devices = {}
- for device in self._get_devices():
- devices.update({str(device.ip): [str(device.name), str(device.type), str(device.allow_or_block)]})
- LOGGER.info(f'Number of devices connected: {len(list(devices.keys()))}')
- with open(self.snapshot, 'w') as file:
- json.dump(devices, file, indent=2)
-
- def _get_device_by_name(self, name: str) -> Device:
- """Calls the ``get_devices()`` method and checks if the given device is available in the list.
-
- Args:
- name: Takes device name as argument.
-
- Returns:
- Device:
- Returns device information as a Device object.
- """
- for device in self._get_devices():
- if device.name == name:
- return device
-
- def allow(self, device: Union[str, Device]) -> Union[Device, None]:
- """Allows internet access to a device.
-
- Args:
- device: Takes device name or Device object as an argument.
-
- Returns:
- Device:
- Returns the device object received from ``get_device_by_name()`` method.
- """
- if isinstance(device, str):
- tmp = device
- LOGGER.info(f'Looking information on {device}')
- if not (device := self._get_device_by_name(name=device)):
- LOGGER.error(f'Device: {tmp} is not connected to {self.ssid}')
- return
- LOGGER.info(f'Granting internet access to {device.name}')
- self.netgear.allow_block_device(mac_addr=device.mac, device_status='Allow')
- return device
-
- def block(self, device: Union[str, Device]) -> Union[Device, None]:
- """Blocks internet access to a device.
-
- Args:
- device: Takes device name or Device object as an argument.
-
- Returns:
- Device:
- Returns the device object received from ``get_device_by_name()`` method.
- """
- if isinstance(device, str):
- tmp = device
- LOGGER.info(f'Looking information on {device}')
- if not (device := self._get_device_by_name(name=device)):
- LOGGER.error(f'Device: {tmp} is not connected to {self.ssid}')
- return
- LOGGER.info(f'Blocking internet access to {device.name}')
- self.netgear.allow_block_device(mac_addr=device.mac, device_status='Block')
- return device
-
- def _dump_blocked(self, device: Device) -> NoReturn:
- """Converts device object to a dictionary and dumps it into ``blocked.json`` file.
-
- Args:
- device: Takes Device object as an argument.
- """
- LOGGER.info(f'Details of {device.name} has been stored in {self.blocked}')
- with open(self.blocked, 'a') as file:
- # noinspection PyProtectedMember
- dictionary = {time.time(): device._asdict()}
- yaml.dump(dictionary, file, allow_unicode=True, default_flow_style=False, sort_keys=False)
-
- def _stasher(self, device: Device) -> NoReturn:
- """Checks the ``blocked.json`` file for an existing record of the same device.
-
- If so, logs else calls ``dump_blocked()`` method.
-
- Args:
- device: Takes Device object as an argument.
- """
- blocked_devices = None
- blocked = []
- if os.path.isfile(self.blocked):
- with open(self.blocked) as file:
- if file.read().strip():
- blocked_devices = yaml.load(stream=file, Loader=yaml.FullLoader)
- if blocked_devices:
- for epoch, device_info in blocked_devices.items():
- blocked.append(device_info.get('mac'))
-
- if device.mac not in blocked:
- self._dump_blocked(device=device)
- else:
- LOGGER.info(f'{device.name} is a part of deny list.')
-
- def always_allow(self, device: Device or str) -> NoReturn:
- """Allows internet access to a device.
-
- Saves the device name to ``snapshot.json`` to not block in future.
- Removes the device name from ``blocked.json`` if an entry is present.
-
- Args:
- device: Takes device name or Device object as an argument
- """
- if isinstance(device, Device):
- device = device.name # converts Device object to string
- if not (device := self.allow(device=device)): # converts string to Device object
- return
-
- with open(self.snapshot, 'r+') as file:
- data = json.load(file)
- file.seek(0)
- if device.ip in list(data.keys()):
- LOGGER.info(f'{device.name} is a part of allow list.')
- data[device.ip][-1] = 'Allow'
- LOGGER.info(f'Setting status to Allow for {device.name} in {self.snapshot}')
- else:
- data.update({str(device.ip): [str(device.name), str(device.type), str(device.allow_or_block)]})
- LOGGER.info(f'Adding {device.name} to {self.snapshot}')
-
- json.dump(data, file, indent=2)
- file.truncate()
-
- if os.path.isfile(self.blocked):
- with open(self.blocked, 'r+') as file:
- if blocked_devices := yaml.load(stream=file, Loader=yaml.FullLoader):
- for epoch, device_info in list(blocked_devices.items()): # convert to a list of dict
- if device_info.get('mac') == device.mac:
- LOGGER.info(f'Removing {device.name} from {self.blocked}')
- del blocked_devices[epoch]
- file.seek(0)
- file.truncate()
- if blocked_devices:
- yaml.dump(blocked_devices, file, indent=2)
-
- def run(self, block: bool = False) -> NoReturn:
- """Trigger to initiate a Network Scan and block the devices that are not present in ``snapshot.json`` file."""
- if not os.path.isfile(self.snapshot):
- LOGGER.error(f'{self.snapshot} not found. Please run `LocalIPScan().create_snapshot()` and review it.')
- raise FileNotFoundError(
- f'{self.snapshot} is required'
- )
- with open(self.snapshot) as file:
- device_list = json.load(file)
- threat = ''
- for device in self._get_devices():
- if device.ip not in list(device_list.keys()):
- LOGGER.warning(f'{device.name} with MAC address {device.mac} and a signal strength of {device.signal}% '
- f'has connected to {self.ssid}')
-
- if device.allow_or_block == 'Allow':
- if block:
- self.block(device=device)
- self._stasher(device=device)
- threat += f'\nName: {device.name}\nIP: {device.ip}\nMAC: {device.mac}'
- else:
- LOGGER.info(f'{device.name} does not have internet access.')
-
- if threat:
- send_sms(msg=threat)
+ netgear.LocalIPScan().run()
+ elif module == models.SupportedModules.att:
+ if init:
+ att.create_snapshot()
else:
- LOGGER.info(f'NetScan has completed. No threats found on {self.ssid}')
+ att.run()
+ else:
+ raise ValueError(
+ "\n\nnetwork argument should either be '%s' or '%s'" % (models.SupportedModules.att,
+ models.SupportedModules.netgear)
+ )
if __name__ == '__main__':
- if os.environ.get('DOCKER'):
- logging.Formatter.converter = custom_time
- LocalIPScan().run()
+ network_monitor(module=models.SupportedModules.att, init=False)
diff --git a/doc_generator/_build/doctrees/README.doctree b/doc_generator/_build/doctrees/README.doctree
deleted file mode 100644
index 1882541..0000000
Binary files a/doc_generator/_build/doctrees/README.doctree and /dev/null differ
diff --git a/doc_generator/_build/doctrees/environment.pickle b/doc_generator/_build/doctrees/environment.pickle
deleted file mode 100644
index 7888f72..0000000
Binary files a/doc_generator/_build/doctrees/environment.pickle and /dev/null differ
diff --git a/doc_generator/_build/doctrees/index.doctree b/doc_generator/_build/doctrees/index.doctree
deleted file mode 100644
index c408b2d..0000000
Binary files a/doc_generator/_build/doctrees/index.doctree and /dev/null differ
diff --git a/doc_generator/_build/html/.buildinfo b/doc_generator/_build/html/.buildinfo
deleted file mode 100644
index 229a098..0000000
--- a/doc_generator/_build/html/.buildinfo
+++ /dev/null
@@ -1,4 +0,0 @@
-# Sphinx build info version 1
-# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
-config: fea9902a8e3e3ee2a1be87e8e7a27920
-tags: 645f666f9bcd5a90fca523b33c5a78b7
diff --git a/docs/README.html b/docs/README.html
index 3ff45b9..621cc50 100644
--- a/docs/README.html
+++ b/docs/README.html
@@ -1,10 +1,10 @@
-
+
-
+
NetScan — NetScan documentation
@@ -13,6 +13,7 @@
+
@@ -42,31 +43,50 @@ Navigation
-NetScan
+NetScan
Network Scanner to analyze devices connecting to the router and alert accordingly.
-NetScan displays the connected devices, Wi-Fi signal info, speed, etc.
-This app can display intruders IP addresses, MAC addresses, and lets the user Ping the device, and even Block user from
-Wi-Fi.
+This app can display intruders’ IP addresses, MAC addresses, and lets the user Ping the device, and even Block the device.
Block IP Address feature helps the user to remove the specified connections and block the specific IP address.
-
-Docker Setup:
-
-
-
+
+Blocking device feature is currently available only for Netgear
router users.
+
+
+
+Coding Standards
+Docstring format: Google
+Styling conventions: PEP 8
+Clean code with pre-commit hooks: flake8
and
+isort
-
-
+
+
+Requirement
+ python -m pip install changelog-generator
+
+
+Usage
+ changelog reverse -f release_notes.rst -t 'Release Notes'
+
+
-
-
+
+Linting
+PreCommit
will ensure linting, and the doc creation are run on every commit.
+Requirement
+ pip install sphinx == 5 .1.1 pre-commit recommonmark
+
+
+Usage
+ pre-commit run --all-files
+
+
+
@@ -77,23 +97,24 @@
@@ -133,7 +154,7 @@ Navigation
\ No newline at end of file
diff --git a/docs/_sources/README.md.txt b/docs/_sources/README.md.txt
index 57f15ed..2efca73 100644
--- a/docs/_sources/README.md.txt
+++ b/docs/_sources/README.md.txt
@@ -1,23 +1,48 @@
# NetScan
Network Scanner to analyze devices connecting to the router and alert accordingly.
-NetScan displays the connected devices, Wi-Fi signal info, speed, etc.
-
-This app can display intruders IP addresses, MAC addresses, and lets the user Ping the device, and even Block user from
-Wi-Fi.
+This app can display intruders' IP addresses, MAC addresses, and lets the user Ping the device, and even Block the device.
Block IP Address feature helps the user to remove the specified connections and block the specific IP address.
-### Docker Setup:
+> Blocking device feature is currently available only for `Netgear` router users.
+
+```python
+
+```
+
+
+## Coding Standards
+Docstring format: [`Google`](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
+Styling conventions: [`PEP 8`](https://www.python.org/dev/peps/pep-0008/)
+Clean code with pre-commit hooks: [`flake8`](https://flake8.pycqa.org/en/latest/) and
+[`isort`](https://pycqa.github.io/isort/)
+
+## [Release Notes](https://github.com/thevickypedia/Jarvis/blob/master/release_notes.rst)
+**Requirement**
+```shell
+python -m pip install changelog-generator
+```
+
+**Usage**
+```shell
+changelog reverse -f release_notes.rst -t 'Release Notes'
+```
-###### Commands:
-- `docker build --progress plain -t netscan .`
-- `docker run netscan`
+## Linting
+`PreCommit` will ensure linting, and the doc creation are run on every commit.
-[Additional options that can be added to the `docker build` command.](https://docs.docker.com/engine/reference/commandline/build/#options)
+**Requirement**
+```shell
+pip install sphinx==5.1.1 pre-commit recommonmark
+```
-###### [Reference](https://github.com/MatMaul/pynetgear)
+**Usage**
+```shell
+pre-commit run --all-files
+```
-###### [Runbook](https://thevickypedia.github.io/netscan/)
+## Runbook
+[![made-with-sphinx-doc](https://img.shields.io/badge/Code%20Docs-Sphinx-1f425f.svg)](https://www.sphinx-doc.org/en/master/man/sphinx-autogen.html)
-###### [Repository](https://github.com/thevickypedia/netscan)
+[https://thevickypedia.github.io/netscan/](https://thevickypedia.github.io/netscan/)
diff --git a/docs/_sources/index.rst.txt b/docs/_sources/index.rst.txt
index 85bed8e..d788cb7 100644
--- a/docs/_sources/index.rst.txt
+++ b/docs/_sources/index.rst.txt
@@ -19,6 +19,41 @@ NetScan
:members:
:undoc-members:
+At&t
+====
+
+.. automodule:: modules.att
+ :members:
+ :undoc-members:
+
+Netgear
+=======
+
+.. automodule:: modules.netgear
+ :members:
+ :undoc-members:
+
+Helper
+======
+
+.. automodule:: modules.helper
+ :members:
+ :undoc-members:
+
+Models
+======
+
+.. automodule:: modules.models
+ :members:
+ :undoc-members:
+
+Settings
+========
+
+.. automodule:: modules.settings
+ :members:
+ :undoc-members:
+
Indices and tables
==================
diff --git a/docs/_static/_sphinx_javascript_frameworks_compat.js b/docs/_static/_sphinx_javascript_frameworks_compat.js
new file mode 100644
index 0000000..8549469
--- /dev/null
+++ b/docs/_static/_sphinx_javascript_frameworks_compat.js
@@ -0,0 +1,134 @@
+/*
+ * _sphinx_javascript_frameworks_compat.js
+ * ~~~~~~~~~~
+ *
+ * Compatability shim for jQuery and underscores.js.
+ *
+ * WILL BE REMOVED IN Sphinx 6.0
+ * xref RemovedInSphinx60Warning
+ *
+ */
+
+/**
+ * select a different prefix for underscore
+ */
+$u = _.noConflict();
+
+
+/**
+ * small helper function to urldecode strings
+ *
+ * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL
+ */
+jQuery.urldecode = function(x) {
+ if (!x) {
+ return x
+ }
+ return decodeURIComponent(x.replace(/\+/g, ' '));
+};
+
+/**
+ * small helper function to urlencode strings
+ */
+jQuery.urlencode = encodeURIComponent;
+
+/**
+ * This function returns the parsed url parameters of the
+ * current request. Multiple values per key are supported,
+ * it will always return arrays of strings for the value parts.
+ */
+jQuery.getQueryParameters = function(s) {
+ if (typeof s === 'undefined')
+ s = document.location.search;
+ var parts = s.substr(s.indexOf('?') + 1).split('&');
+ var result = {};
+ for (var i = 0; i < parts.length; i++) {
+ var tmp = parts[i].split('=', 2);
+ var key = jQuery.urldecode(tmp[0]);
+ var value = jQuery.urldecode(tmp[1]);
+ if (key in result)
+ result[key].push(value);
+ else
+ result[key] = [value];
+ }
+ return result;
+};
+
+/**
+ * highlight a given string on a jquery object by wrapping it in
+ * span elements with the given class name.
+ */
+jQuery.fn.highlightText = function(text, className) {
+ function highlight(node, addItems) {
+ if (node.nodeType === 3) {
+ var val = node.nodeValue;
+ var pos = val.toLowerCase().indexOf(text);
+ if (pos >= 0 &&
+ !jQuery(node.parentNode).hasClass(className) &&
+ !jQuery(node.parentNode).hasClass("nohighlight")) {
+ var span;
+ var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg");
+ if (isInSVG) {
+ span = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
+ } else {
+ span = document.createElement("span");
+ span.className = className;
+ }
+ span.appendChild(document.createTextNode(val.substr(pos, text.length)));
+ node.parentNode.insertBefore(span, node.parentNode.insertBefore(
+ document.createTextNode(val.substr(pos + text.length)),
+ node.nextSibling));
+ node.nodeValue = val.substr(0, pos);
+ if (isInSVG) {
+ var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ var bbox = node.parentElement.getBBox();
+ rect.x.baseVal.value = bbox.x;
+ rect.y.baseVal.value = bbox.y;
+ rect.width.baseVal.value = bbox.width;
+ rect.height.baseVal.value = bbox.height;
+ rect.setAttribute('class', className);
+ addItems.push({
+ "parent": node.parentNode,
+ "target": rect});
+ }
+ }
+ }
+ else if (!jQuery(node).is("button, select, textarea")) {
+ jQuery.each(node.childNodes, function() {
+ highlight(this, addItems);
+ });
+ }
+ }
+ var addItems = [];
+ var result = this.each(function() {
+ highlight(this, addItems);
+ });
+ for (var i = 0; i < addItems.length; ++i) {
+ jQuery(addItems[i].parent).before(addItems[i].target);
+ }
+ return result;
+};
+
+/*
+ * backward compatibility for jQuery.browser
+ * This will be supported until firefox bug is fixed.
+ */
+if (!jQuery.browser) {
+ jQuery.uaMatch = function(ua) {
+ ua = ua.toLowerCase();
+
+ var match = /(chrome)[ \/]([\w.]+)/.exec(ua) ||
+ /(webkit)[ \/]([\w.]+)/.exec(ua) ||
+ /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
+ /(msie) ([\w.]+)/.exec(ua) ||
+ ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
+ [];
+
+ return {
+ browser: match[ 1 ] || "",
+ version: match[ 2 ] || "0"
+ };
+ };
+ jQuery.browser = {};
+ jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true;
+}
diff --git a/docs/_static/basic.css b/docs/_static/basic.css
index 4b155a1..bdfb3ef 100644
--- a/docs/_static/basic.css
+++ b/docs/_static/basic.css
@@ -4,7 +4,7 @@
*
* Sphinx stylesheet -- basic theme.
*
- * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
+ * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
@@ -222,7 +222,7 @@ table.modindextable td {
/* -- general body styles --------------------------------------------------- */
div.body {
- min-width: 450px;
+ min-width: 360px;
max-width: 80%;
}
@@ -237,16 +237,6 @@ a.headerlink {
visibility: hidden;
}
-a.brackets:before,
-span.brackets > a:before{
- content: "[";
-}
-
-a.brackets:after,
-span.brackets > a:after {
- content: "]";
-}
-
h1:hover > a.headerlink,
h2:hover > a.headerlink,
h3:hover > a.headerlink,
@@ -334,13 +324,15 @@ aside.sidebar {
p.sidebar-title {
font-weight: bold;
}
-
+nav.contents,
+aside.topic,
div.admonition, div.topic, blockquote {
clear: left;
}
/* -- topics ---------------------------------------------------------------- */
-
+nav.contents,
+aside.topic,
div.topic {
border: 1px solid #ccc;
padding: 7px;
@@ -379,6 +371,8 @@ div.body p.centered {
div.sidebar > :last-child,
aside.sidebar > :last-child,
+nav.contents > :last-child,
+aside.topic > :last-child,
div.topic > :last-child,
div.admonition > :last-child {
margin-bottom: 0;
@@ -386,6 +380,8 @@ div.admonition > :last-child {
div.sidebar::after,
aside.sidebar::after,
+nav.contents::after,
+aside.topic::after,
div.topic::after,
div.admonition::after,
blockquote::after {
@@ -428,10 +424,6 @@ table.docutils td, table.docutils th {
border-bottom: 1px solid #aaa;
}
-table.footnote td, table.footnote th {
- border: 0 !important;
-}
-
th {
text-align: left;
padding-right: 5px;
@@ -614,20 +606,26 @@ ol.simple p,
ul.simple p {
margin-bottom: 0;
}
-
-dl.footnote > dt,
-dl.citation > dt {
+aside.footnote > span,
+div.citation > span {
float: left;
- margin-right: 0.5em;
}
-
-dl.footnote > dd,
-dl.citation > dd {
+aside.footnote > span:last-of-type,
+div.citation > span:last-of-type {
+ padding-right: 0.5em;
+}
+aside.footnote > p {
+ margin-left: 2em;
+}
+div.citation > p {
+ margin-left: 4em;
+}
+aside.footnote > p:last-of-type,
+div.citation > p:last-of-type {
margin-bottom: 0em;
}
-
-dl.footnote > dd:after,
-dl.citation > dd:after {
+aside.footnote > p:last-of-type:after,
+div.citation > p:last-of-type:after {
content: "";
clear: both;
}
@@ -644,10 +642,6 @@ dl.field-list > dt {
padding-right: 5px;
}
-dl.field-list > dt:after {
- content: ":";
-}
-
dl.field-list > dd {
padding-left: 0.5em;
margin-top: 0em;
@@ -731,8 +725,9 @@ dl.glossary dt {
.classifier:before {
font-style: normal;
- margin: 0.5em;
+ margin: 0 0.5em;
content: ":";
+ display: inline-block;
}
abbr, acronym {
@@ -756,6 +751,7 @@ span.pre {
-ms-hyphens: none;
-webkit-hyphens: none;
hyphens: none;
+ white-space: nowrap;
}
div[class*="highlight-"] {
diff --git a/docs/_static/classic.css b/docs/_static/classic.css
index dcae946..92cac9f 100644
--- a/docs/_static/classic.css
+++ b/docs/_static/classic.css
@@ -4,7 +4,7 @@
*
* Sphinx stylesheet -- classic theme.
*
- * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
+ * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
@@ -28,6 +28,7 @@ body {
}
div.document {
+ display: flex;
background-color: #1c4e63;
}
@@ -204,6 +205,8 @@ div.seealso {
background-color: #ffc;
border: 1px solid #ff6;
}
+nav.contents,
+aside.topic,
div.topic {
background-color: #eee;
diff --git a/docs/_static/doctools.js b/docs/_static/doctools.js
index 8cbf1b1..c3db08d 100644
--- a/docs/_static/doctools.js
+++ b/docs/_static/doctools.js
@@ -2,322 +2,263 @@
* doctools.js
* ~~~~~~~~~~~
*
- * Sphinx JavaScript utilities for all documentation.
+ * Base JavaScript utilities for all Sphinx HTML documentation.
*
- * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
+ * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
+"use strict";
-/**
- * select a different prefix for underscore
- */
-$u = _.noConflict();
-
-/**
- * make the code below compatible with browsers without
- * an installed firebug like debugger
-if (!window.console || !console.firebug) {
- var names = ["log", "debug", "info", "warn", "error", "assert", "dir",
- "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace",
- "profile", "profileEnd"];
- window.console = {};
- for (var i = 0; i < names.length; ++i)
- window.console[names[i]] = function() {};
-}
- */
-
-/**
- * small helper function to urldecode strings
- *
- * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL
- */
-jQuery.urldecode = function(x) {
- if (!x) {
- return x
+const _ready = (callback) => {
+ if (document.readyState !== "loading") {
+ callback();
+ } else {
+ document.addEventListener("DOMContentLoaded", callback);
}
- return decodeURIComponent(x.replace(/\+/g, ' '));
};
/**
- * small helper function to urlencode strings
+ * highlight a given string on a node by wrapping it in
+ * span elements with the given class name.
*/
-jQuery.urlencode = encodeURIComponent;
+const _highlight = (node, addItems, text, className) => {
+ if (node.nodeType === Node.TEXT_NODE) {
+ const val = node.nodeValue;
+ const parent = node.parentNode;
+ const pos = val.toLowerCase().indexOf(text);
+ if (
+ pos >= 0 &&
+ !parent.classList.contains(className) &&
+ !parent.classList.contains("nohighlight")
+ ) {
+ let span;
-/**
- * This function returns the parsed url parameters of the
- * current request. Multiple values per key are supported,
- * it will always return arrays of strings for the value parts.
- */
-jQuery.getQueryParameters = function(s) {
- if (typeof s === 'undefined')
- s = document.location.search;
- var parts = s.substr(s.indexOf('?') + 1).split('&');
- var result = {};
- for (var i = 0; i < parts.length; i++) {
- var tmp = parts[i].split('=', 2);
- var key = jQuery.urldecode(tmp[0]);
- var value = jQuery.urldecode(tmp[1]);
- if (key in result)
- result[key].push(value);
- else
- result[key] = [value];
- }
- return result;
-};
+ const closestNode = parent.closest("body, svg, foreignObject");
+ const isInSVG = closestNode && closestNode.matches("svg");
+ if (isInSVG) {
+ span = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
+ } else {
+ span = document.createElement("span");
+ span.classList.add(className);
+ }
-/**
- * highlight a given string on a jquery object by wrapping it in
- * span elements with the given class name.
- */
-jQuery.fn.highlightText = function(text, className) {
- function highlight(node, addItems) {
- if (node.nodeType === 3) {
- var val = node.nodeValue;
- var pos = val.toLowerCase().indexOf(text);
- if (pos >= 0 &&
- !jQuery(node.parentNode).hasClass(className) &&
- !jQuery(node.parentNode).hasClass("nohighlight")) {
- var span;
- var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg");
- if (isInSVG) {
- span = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
- } else {
- span = document.createElement("span");
- span.className = className;
- }
- span.appendChild(document.createTextNode(val.substr(pos, text.length)));
- node.parentNode.insertBefore(span, node.parentNode.insertBefore(
+ span.appendChild(document.createTextNode(val.substr(pos, text.length)));
+ parent.insertBefore(
+ span,
+ parent.insertBefore(
document.createTextNode(val.substr(pos + text.length)),
- node.nextSibling));
- node.nodeValue = val.substr(0, pos);
- if (isInSVG) {
- var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
- var bbox = node.parentElement.getBBox();
- rect.x.baseVal.value = bbox.x;
- rect.y.baseVal.value = bbox.y;
- rect.width.baseVal.value = bbox.width;
- rect.height.baseVal.value = bbox.height;
- rect.setAttribute('class', className);
- addItems.push({
- "parent": node.parentNode,
- "target": rect});
- }
+ node.nextSibling
+ )
+ );
+ node.nodeValue = val.substr(0, pos);
+
+ if (isInSVG) {
+ const rect = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "rect"
+ );
+ const bbox = parent.getBBox();
+ rect.x.baseVal.value = bbox.x;
+ rect.y.baseVal.value = bbox.y;
+ rect.width.baseVal.value = bbox.width;
+ rect.height.baseVal.value = bbox.height;
+ rect.setAttribute("class", className);
+ addItems.push({ parent: parent, target: rect });
}
}
- else if (!jQuery(node).is("button, select, textarea")) {
- jQuery.each(node.childNodes, function() {
- highlight(this, addItems);
- });
- }
+ } else if (node.matches && !node.matches("button, select, textarea")) {
+ node.childNodes.forEach((el) => _highlight(el, addItems, text, className));
}
- var addItems = [];
- var result = this.each(function() {
- highlight(this, addItems);
- });
- for (var i = 0; i < addItems.length; ++i) {
- jQuery(addItems[i].parent).before(addItems[i].target);
- }
- return result;
};
-
-/*
- * backward compatibility for jQuery.browser
- * This will be supported until firefox bug is fixed.
- */
-if (!jQuery.browser) {
- jQuery.uaMatch = function(ua) {
- ua = ua.toLowerCase();
-
- var match = /(chrome)[ \/]([\w.]+)/.exec(ua) ||
- /(webkit)[ \/]([\w.]+)/.exec(ua) ||
- /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
- /(msie) ([\w.]+)/.exec(ua) ||
- ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
- [];
-
- return {
- browser: match[ 1 ] || "",
- version: match[ 2 ] || "0"
- };
- };
- jQuery.browser = {};
- jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true;
-}
+const _highlightText = (thisNode, text, className) => {
+ let addItems = [];
+ _highlight(thisNode, addItems, text, className);
+ addItems.forEach((obj) =>
+ obj.parent.insertAdjacentElement("beforebegin", obj.target)
+ );
+};
/**
* Small JavaScript module for the documentation.
*/
-var Documentation = {
-
- init : function() {
- this.fixFirefoxAnchorBug();
- this.highlightSearchWords();
- this.initIndexTable();
- if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) {
- this.initOnKeyListeners();
- }
+const Documentation = {
+ init: () => {
+ Documentation.highlightSearchWords();
+ Documentation.initDomainIndexTable();
+ Documentation.initOnKeyListeners();
},
/**
* i18n support
*/
- TRANSLATIONS : {},
- PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; },
- LOCALE : 'unknown',
+ TRANSLATIONS: {},
+ PLURAL_EXPR: (n) => (n === 1 ? 0 : 1),
+ LOCALE: "unknown",
// gettext and ngettext don't access this so that the functions
// can safely bound to a different name (_ = Documentation.gettext)
- gettext : function(string) {
- var translated = Documentation.TRANSLATIONS[string];
- if (typeof translated === 'undefined')
- return string;
- return (typeof translated === 'string') ? translated : translated[0];
- },
-
- ngettext : function(singular, plural, n) {
- var translated = Documentation.TRANSLATIONS[singular];
- if (typeof translated === 'undefined')
- return (n == 1) ? singular : plural;
- return translated[Documentation.PLURALEXPR(n)];
- },
-
- addTranslations : function(catalog) {
- for (var key in catalog.messages)
- this.TRANSLATIONS[key] = catalog.messages[key];
- this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')');
- this.LOCALE = catalog.locale;
+ gettext: (string) => {
+ const translated = Documentation.TRANSLATIONS[string];
+ switch (typeof translated) {
+ case "undefined":
+ return string; // no translation
+ case "string":
+ return translated; // translation exists
+ default:
+ return translated[0]; // (singular, plural) translation tuple exists
+ }
},
- /**
- * add context elements like header anchor links
- */
- addContextElements : function() {
- $('div[id] > :header:first').each(function() {
- $('').
- attr('href', '#' + this.id).
- attr('title', _('Permalink to this headline')).
- appendTo(this);
- });
- $('dt[id]').each(function() {
- $('').
- attr('href', '#' + this.id).
- attr('title', _('Permalink to this definition')).
- appendTo(this);
- });
+ ngettext: (singular, plural, n) => {
+ const translated = Documentation.TRANSLATIONS[singular];
+ if (typeof translated !== "undefined")
+ return translated[Documentation.PLURAL_EXPR(n)];
+ return n === 1 ? singular : plural;
},
- /**
- * workaround a firefox stupidity
- * see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075
- */
- fixFirefoxAnchorBug : function() {
- if (document.location.hash && $.browser.mozilla)
- window.setTimeout(function() {
- document.location.href += '';
- }, 10);
+ addTranslations: (catalog) => {
+ Object.assign(Documentation.TRANSLATIONS, catalog.messages);
+ Documentation.PLURAL_EXPR = new Function(
+ "n",
+ `return (${catalog.plural_expr})`
+ );
+ Documentation.LOCALE = catalog.locale;
},
/**
* highlight the search words provided in the url in the text
*/
- highlightSearchWords : function() {
- var params = $.getQueryParameters();
- var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : [];
- if (terms.length) {
- var body = $('div.body');
- if (!body.length) {
- body = $('body');
- }
- window.setTimeout(function() {
- $.each(terms, function() {
- body.highlightText(this.toLowerCase(), 'highlighted');
- });
- }, 10);
- $('' + _('Hide Search Matches') + '
')
- .appendTo($('#searchbox'));
- }
- },
+ highlightSearchWords: () => {
+ const highlight =
+ new URLSearchParams(window.location.search).get("highlight") || "";
+ const terms = highlight.toLowerCase().split(/\s+/).filter(x => x);
+ if (terms.length === 0) return; // nothing to do
- /**
- * init the domain index toggle buttons
- */
- initIndexTable : function() {
- var togglers = $('img.toggler').click(function() {
- var src = $(this).attr('src');
- var idnum = $(this).attr('id').substr(7);
- $('tr.cg-' + idnum).toggle();
- if (src.substr(-9) === 'minus.png')
- $(this).attr('src', src.substr(0, src.length-9) + 'plus.png');
- else
- $(this).attr('src', src.substr(0, src.length-8) + 'minus.png');
- }).css('display', '');
- if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) {
- togglers.click();
- }
+ // There should never be more than one element matching "div.body"
+ const divBody = document.querySelectorAll("div.body");
+ const body = divBody.length ? divBody[0] : document.querySelector("body");
+ window.setTimeout(() => {
+ terms.forEach((term) => _highlightText(body, term, "highlighted"));
+ }, 10);
+
+ const searchBox = document.getElementById("searchbox");
+ if (searchBox === null) return;
+ searchBox.appendChild(
+ document
+ .createRange()
+ .createContextualFragment(
+ '' +
+ '' +
+ Documentation.gettext("Hide Search Matches") +
+ "
"
+ )
+ );
},
/**
* helper function to hide the search marks again
*/
- hideSearchWords : function() {
- $('#searchbox .highlight-link').fadeOut(300);
- $('span.highlighted').removeClass('highlighted');
+ hideSearchWords: () => {
+ document
+ .querySelectorAll("#searchbox .highlight-link")
+ .forEach((el) => el.remove());
+ document
+ .querySelectorAll("span.highlighted")
+ .forEach((el) => el.classList.remove("highlighted"));
+ const url = new URL(window.location);
+ url.searchParams.delete("highlight");
+ window.history.replaceState({}, "", url);
},
/**
- * make the url absolute
+ * helper function to focus on search bar
*/
- makeURL : function(relativeURL) {
- return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL;
+ focusSearchBar: () => {
+ document.querySelectorAll("input[name=q]")[0]?.focus();
},
/**
- * get the current relative url
+ * Initialise the domain index toggle buttons
*/
- getCurrentURL : function() {
- var path = document.location.pathname;
- var parts = path.split(/\//);
- $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() {
- if (this === '..')
- parts.pop();
- });
- var url = parts.join('/');
- return path.substring(url.lastIndexOf('/') + 1, path.length - 1);
+ initDomainIndexTable: () => {
+ const toggler = (el) => {
+ const idNumber = el.id.substr(7);
+ const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`);
+ if (el.src.substr(-9) === "minus.png") {
+ el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`;
+ toggledRows.forEach((el) => (el.style.display = "none"));
+ } else {
+ el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`;
+ toggledRows.forEach((el) => (el.style.display = ""));
+ }
+ };
+
+ const togglerElements = document.querySelectorAll("img.toggler");
+ togglerElements.forEach((el) =>
+ el.addEventListener("click", (event) => toggler(event.currentTarget))
+ );
+ togglerElements.forEach((el) => (el.style.display = ""));
+ if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler);
},
- initOnKeyListeners: function() {
- $(document).keydown(function(event) {
- var activeElementType = document.activeElement.tagName;
- // don't navigate when in search box, textarea, dropdown or button
- if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT'
- && activeElementType !== 'BUTTON' && !event.altKey && !event.ctrlKey && !event.metaKey
- && !event.shiftKey) {
- switch (event.keyCode) {
- case 37: // left
- var prevHref = $('link[rel="prev"]').prop('href');
- if (prevHref) {
- window.location.href = prevHref;
- return false;
+ initOnKeyListeners: () => {
+ // only install a listener if it is really needed
+ if (
+ !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS &&
+ !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS
+ )
+ return;
+
+ const blacklistedElements = new Set([
+ "TEXTAREA",
+ "INPUT",
+ "SELECT",
+ "BUTTON",
+ ]);
+ document.addEventListener("keydown", (event) => {
+ if (blacklistedElements.has(document.activeElement.tagName)) return; // bail for input elements
+ if (event.altKey || event.ctrlKey || event.metaKey) return; // bail with special keys
+
+ if (!event.shiftKey) {
+ switch (event.key) {
+ case "ArrowLeft":
+ if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break;
+
+ const prevLink = document.querySelector('link[rel="prev"]');
+ if (prevLink && prevLink.href) {
+ window.location.href = prevLink.href;
+ event.preventDefault();
}
break;
- case 39: // right
- var nextHref = $('link[rel="next"]').prop('href');
- if (nextHref) {
- window.location.href = nextHref;
- return false;
+ case "ArrowRight":
+ if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break;
+
+ const nextLink = document.querySelector('link[rel="next"]');
+ if (nextLink && nextLink.href) {
+ window.location.href = nextLink.href;
+ event.preventDefault();
}
break;
+ case "Escape":
+ if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break;
+ Documentation.hideSearchWords();
+ event.preventDefault();
}
}
+
+ // some keyboard layouts may need Shift to get /
+ switch (event.key) {
+ case "/":
+ if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break;
+ Documentation.focusSearchBar();
+ event.preventDefault();
+ }
});
- }
+ },
};
// quick alias for translations
-_ = Documentation.gettext;
+const _ = Documentation.gettext;
-$(document).ready(function() {
- Documentation.init();
-});
+_ready(Documentation.init);
diff --git a/docs/_static/documentation_options.js b/docs/_static/documentation_options.js
index 2fa8c97..b57ae3b 100644
--- a/docs/_static/documentation_options.js
+++ b/docs/_static/documentation_options.js
@@ -1,12 +1,14 @@
var DOCUMENTATION_OPTIONS = {
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
VERSION: '',
- LANGUAGE: 'None',
+ LANGUAGE: 'en',
COLLAPSE_INDEX: false,
BUILDER: 'html',
FILE_SUFFIX: '.html',
LINK_SUFFIX: '.html',
HAS_SOURCE: true,
SOURCELINK_SUFFIX: '.txt',
- NAVIGATION_WITH_KEYS: false
+ NAVIGATION_WITH_KEYS: false,
+ SHOW_SEARCH_SUMMARY: true,
+ ENABLE_SEARCH_SHORTCUTS: true,
};
\ No newline at end of file
diff --git a/docs/_static/jquery-3.5.1.js b/docs/_static/jquery-3.6.0.js
similarity index 98%
rename from docs/_static/jquery-3.5.1.js
rename to docs/_static/jquery-3.6.0.js
index 5093733..fc6c299 100644
--- a/docs/_static/jquery-3.5.1.js
+++ b/docs/_static/jquery-3.6.0.js
@@ -1,15 +1,15 @@
/*!
- * jQuery JavaScript Library v3.5.1
+ * jQuery JavaScript Library v3.6.0
* https://jquery.com/
*
* Includes Sizzle.js
* https://sizzlejs.com/
*
- * Copyright JS Foundation and other contributors
+ * Copyright OpenJS Foundation and other contributors
* Released under the MIT license
* https://jquery.org/license
*
- * Date: 2020-05-04T22:49Z
+ * Date: 2021-03-02T17:08Z
*/
( function( global, factory ) {
@@ -76,12 +76,16 @@ var support = {};
var isFunction = function isFunction( obj ) {
- // Support: Chrome <=57, Firefox <=52
- // In some browsers, typeof returns "function" for HTML elements
- // (i.e., `typeof document.createElement( "object" ) === "function"`).
- // We don't want to classify *any* DOM node as a function.
- return typeof obj === "function" && typeof obj.nodeType !== "number";
- };
+ // Support: Chrome <=57, Firefox <=52
+ // In some browsers, typeof returns "function" for HTML elements
+ // (i.e., `typeof document.createElement( "object" ) === "function"`).
+ // We don't want to classify *any* DOM node as a function.
+ // Support: QtWeb <=3.8.5, WebKit <=534.34, wkhtmltopdf tool <=0.12.5
+ // Plus for old WebKit, typeof returns "function" for HTML collections
+ // (e.g., `typeof document.getElementsByTagName("div") === "function"`). (gh-4756)
+ return typeof obj === "function" && typeof obj.nodeType !== "number" &&
+ typeof obj.item !== "function";
+ };
var isWindow = function isWindow( obj ) {
@@ -147,7 +151,7 @@ function toType( obj ) {
var
- version = "3.5.1",
+ version = "3.6.0",
// Define a local copy of jQuery
jQuery = function( selector, context ) {
@@ -401,7 +405,7 @@ jQuery.extend( {
if ( isArrayLike( Object( arr ) ) ) {
jQuery.merge( ret,
typeof arr === "string" ?
- [ arr ] : arr
+ [ arr ] : arr
);
} else {
push.call( ret, arr );
@@ -496,9 +500,9 @@ if ( typeof Symbol === "function" ) {
// Populate the class2type map
jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ),
-function( _i, name ) {
- class2type[ "[object " + name + "]" ] = name.toLowerCase();
-} );
+ function( _i, name ) {
+ class2type[ "[object " + name + "]" ] = name.toLowerCase();
+ } );
function isArrayLike( obj ) {
@@ -518,14 +522,14 @@ function isArrayLike( obj ) {
}
var Sizzle =
/*!
- * Sizzle CSS Selector Engine v2.3.5
+ * Sizzle CSS Selector Engine v2.3.6
* https://sizzlejs.com/
*
* Copyright JS Foundation and other contributors
* Released under the MIT license
* https://js.foundation/
*
- * Date: 2020-03-14
+ * Date: 2021-02-16
*/
( function( window ) {
var i,
@@ -1108,8 +1112,8 @@ support = Sizzle.support = {};
* @returns {Boolean} True iff elem is a non-HTML XML node
*/
isXML = Sizzle.isXML = function( elem ) {
- var namespace = elem.namespaceURI,
- docElem = ( elem.ownerDocument || elem ).documentElement;
+ var namespace = elem && elem.namespaceURI,
+ docElem = elem && ( elem.ownerDocument || elem ).documentElement;
// Support: IE <=8
// Assume HTML when documentElement doesn't yet exist, such as inside loading iframes
@@ -3024,9 +3028,9 @@ var rneedsContext = jQuery.expr.match.needsContext;
function nodeName( elem, name ) {
- return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
+ return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
-};
+}
var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i );
@@ -3997,8 +4001,8 @@ jQuery.extend( {
resolveContexts = Array( i ),
resolveValues = slice.call( arguments ),
- // the master Deferred
- master = jQuery.Deferred(),
+ // the primary Deferred
+ primary = jQuery.Deferred(),
// subordinate callback factory
updateFunc = function( i ) {
@@ -4006,30 +4010,30 @@ jQuery.extend( {
resolveContexts[ i ] = this;
resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;
if ( !( --remaining ) ) {
- master.resolveWith( resolveContexts, resolveValues );
+ primary.resolveWith( resolveContexts, resolveValues );
}
};
};
// Single- and empty arguments are adopted like Promise.resolve
if ( remaining <= 1 ) {
- adoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject,
+ adoptValue( singleValue, primary.done( updateFunc( i ) ).resolve, primary.reject,
!remaining );
// Use .then() to unwrap secondary thenables (cf. gh-3000)
- if ( master.state() === "pending" ||
+ if ( primary.state() === "pending" ||
isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) {
- return master.then();
+ return primary.then();
}
}
// Multiple arguments are aggregated like Promise.all array elements
while ( i-- ) {
- adoptValue( resolveValues[ i ], updateFunc( i ), master.reject );
+ adoptValue( resolveValues[ i ], updateFunc( i ), primary.reject );
}
- return master.promise();
+ return primary.promise();
}
} );
@@ -4180,8 +4184,8 @@ var access = function( elems, fn, key, value, chainable, emptyGet, raw ) {
for ( ; i < len; i++ ) {
fn(
elems[ i ], key, raw ?
- value :
- value.call( elems[ i ], i, fn( elems[ i ], key ) )
+ value :
+ value.call( elems[ i ], i, fn( elems[ i ], key ) )
);
}
}
@@ -5089,10 +5093,7 @@ function buildFragment( elems, context, scripts, selection, ignored ) {
}
-var
- rkeyEvent = /^key/,
- rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/,
- rtypenamespace = /^([^.]*)(?:\.(.+)|)/;
+var rtypenamespace = /^([^.]*)(?:\.(.+)|)/;
function returnTrue() {
return true;
@@ -5387,8 +5388,8 @@ jQuery.event = {
event = jQuery.event.fix( nativeEvent ),
handlers = (
- dataPriv.get( this, "events" ) || Object.create( null )
- )[ event.type ] || [],
+ dataPriv.get( this, "events" ) || Object.create( null )
+ )[ event.type ] || [],
special = jQuery.event.special[ event.type ] || {};
// Use the fix-ed jQuery.Event rather than the (read-only) native event
@@ -5512,12 +5513,12 @@ jQuery.event = {
get: isFunction( hook ) ?
function() {
if ( this.originalEvent ) {
- return hook( this.originalEvent );
+ return hook( this.originalEvent );
}
} :
function() {
if ( this.originalEvent ) {
- return this.originalEvent[ name ];
+ return this.originalEvent[ name ];
}
},
@@ -5656,7 +5657,13 @@ function leverageNative( el, type, expectSync ) {
// Cancel the outer synthetic event
event.stopImmediatePropagation();
event.preventDefault();
- return result.value;
+
+ // Support: Chrome 86+
+ // In Chrome, if an element having a focusout handler is blurred by
+ // clicking outside of it, it invokes the handler synchronously. If
+ // that handler calls `.remove()` on the element, the data is cleared,
+ // leaving `result` undefined. We need to guard against this.
+ return result && result.value;
}
// If this is an inner synthetic event for an event with a bubbling surrogate
@@ -5821,34 +5828,7 @@ jQuery.each( {
targetTouches: true,
toElement: true,
touches: true,
-
- which: function( event ) {
- var button = event.button;
-
- // Add which for key events
- if ( event.which == null && rkeyEvent.test( event.type ) ) {
- return event.charCode != null ? event.charCode : event.keyCode;
- }
-
- // Add which for click: 1 === left; 2 === middle; 3 === right
- if ( !event.which && button !== undefined && rmouseEvent.test( event.type ) ) {
- if ( button & 1 ) {
- return 1;
- }
-
- if ( button & 2 ) {
- return 3;
- }
-
- if ( button & 4 ) {
- return 2;
- }
-
- return 0;
- }
-
- return event.which;
- }
+ which: true
}, jQuery.event.addProp );
jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) {
@@ -5874,6 +5854,12 @@ jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateTyp
return true;
},
+ // Suppress native focus or blur as it's already being fired
+ // in leverageNative.
+ _default: function() {
+ return true;
+ },
+
delegateType: delegateType
};
} );
@@ -6541,6 +6527,10 @@ var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" );
// set in CSS while `offset*` properties report correct values.
// Behavior in IE 9 is more subtle than in newer versions & it passes
// some versions of this test; make sure not to make it pass there!
+ //
+ // Support: Firefox 70+
+ // Only Firefox includes border widths
+ // in computed dimensions. (gh-4529)
reliableTrDimensions: function() {
var table, tr, trChild, trStyle;
if ( reliableTrDimensionsVal == null ) {
@@ -6548,17 +6538,32 @@ var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" );
tr = document.createElement( "tr" );
trChild = document.createElement( "div" );
- table.style.cssText = "position:absolute;left:-11111px";
+ table.style.cssText = "position:absolute;left:-11111px;border-collapse:separate";
+ tr.style.cssText = "border:1px solid";
+
+ // Support: Chrome 86+
+ // Height set through cssText does not get applied.
+ // Computed height then comes back as 0.
tr.style.height = "1px";
trChild.style.height = "9px";
+ // Support: Android 8 Chrome 86+
+ // In our bodyBackground.html iframe,
+ // display for all div elements is set to "inline",
+ // which causes a problem only in Android 8 Chrome 86.
+ // Ensuring the div is display: block
+ // gets around this issue.
+ trChild.style.display = "block";
+
documentElement
.appendChild( table )
.appendChild( tr )
.appendChild( trChild );
trStyle = window.getComputedStyle( tr );
- reliableTrDimensionsVal = parseInt( trStyle.height ) > 3;
+ reliableTrDimensionsVal = ( parseInt( trStyle.height, 10 ) +
+ parseInt( trStyle.borderTopWidth, 10 ) +
+ parseInt( trStyle.borderBottomWidth, 10 ) ) === tr.offsetHeight;
documentElement.removeChild( table );
}
@@ -7022,10 +7027,10 @@ jQuery.each( [ "height", "width" ], function( _i, dimension ) {
// Running getBoundingClientRect on a disconnected node
// in IE throws an error.
( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ?
- swap( elem, cssShow, function() {
- return getWidthOrHeight( elem, dimension, extra );
- } ) :
- getWidthOrHeight( elem, dimension, extra );
+ swap( elem, cssShow, function() {
+ return getWidthOrHeight( elem, dimension, extra );
+ } ) :
+ getWidthOrHeight( elem, dimension, extra );
}
},
@@ -7084,7 +7089,7 @@ jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft,
swap( elem, { marginLeft: 0 }, function() {
return elem.getBoundingClientRect().left;
} )
- ) + "px";
+ ) + "px";
}
}
);
@@ -7223,7 +7228,7 @@ Tween.propHooks = {
if ( jQuery.fx.step[ tween.prop ] ) {
jQuery.fx.step[ tween.prop ]( tween );
} else if ( tween.elem.nodeType === 1 && (
- jQuery.cssHooks[ tween.prop ] ||
+ jQuery.cssHooks[ tween.prop ] ||
tween.elem.style[ finalPropName( tween.prop ) ] != null ) ) {
jQuery.style( tween.elem, tween.prop, tween.now + tween.unit );
} else {
@@ -7468,7 +7473,7 @@ function defaultPrefilter( elem, props, opts ) {
anim.done( function() {
- /* eslint-enable no-loop-func */
+ /* eslint-enable no-loop-func */
// The final step of a "hide" animation is actually hiding the element
if ( !hidden ) {
@@ -7588,7 +7593,7 @@ function Animation( elem, properties, options ) {
tweens: [],
createTween: function( prop, end ) {
var tween = jQuery.Tween( elem, animation.opts, prop, end,
- animation.opts.specialEasing[ prop ] || animation.opts.easing );
+ animation.opts.specialEasing[ prop ] || animation.opts.easing );
animation.tweens.push( tween );
return tween;
},
@@ -7761,7 +7766,8 @@ jQuery.fn.extend( {
anim.stop( true );
}
};
- doAnimation.finish = doAnimation;
+
+ doAnimation.finish = doAnimation;
return empty || optall.queue === false ?
this.each( doAnimation ) :
@@ -8401,8 +8407,8 @@ jQuery.fn.extend( {
if ( this.setAttribute ) {
this.setAttribute( "class",
className || value === false ?
- "" :
- dataPriv.get( this, "__className__" ) || ""
+ "" :
+ dataPriv.get( this, "__className__" ) || ""
);
}
}
@@ -8417,7 +8423,7 @@ jQuery.fn.extend( {
while ( ( elem = this[ i++ ] ) ) {
if ( elem.nodeType === 1 &&
( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) {
- return true;
+ return true;
}
}
@@ -8707,9 +8713,7 @@ jQuery.extend( jQuery.event, {
special.bindType || type;
// jQuery handler
- handle = (
- dataPriv.get( cur, "events" ) || Object.create( null )
- )[ event.type ] &&
+ handle = ( dataPriv.get( cur, "events" ) || Object.create( null ) )[ event.type ] &&
dataPriv.get( cur, "handle" );
if ( handle ) {
handle.apply( cur, data );
@@ -8856,7 +8860,7 @@ var rquery = ( /\?/ );
// Cross-browser xml parsing
jQuery.parseXML = function( data ) {
- var xml;
+ var xml, parserErrorElem;
if ( !data || typeof data !== "string" ) {
return null;
}
@@ -8865,12 +8869,17 @@ jQuery.parseXML = function( data ) {
// IE throws on parseFromString with invalid input.
try {
xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" );
- } catch ( e ) {
- xml = undefined;
- }
+ } catch ( e ) {}
- if ( !xml || xml.getElementsByTagName( "parsererror" ).length ) {
- jQuery.error( "Invalid XML: " + data );
+ parserErrorElem = xml && xml.getElementsByTagName( "parsererror" )[ 0 ];
+ if ( !xml || parserErrorElem ) {
+ jQuery.error( "Invalid XML: " + (
+ parserErrorElem ?
+ jQuery.map( parserErrorElem.childNodes, function( el ) {
+ return el.textContent;
+ } ).join( "\n" ) :
+ data
+ ) );
}
return xml;
};
@@ -8971,16 +8980,14 @@ jQuery.fn.extend( {
// Can add propHook for "elements" to filter or add form elements
var elements = jQuery.prop( this, "elements" );
return elements ? jQuery.makeArray( elements ) : this;
- } )
- .filter( function() {
+ } ).filter( function() {
var type = this.type;
// Use .is( ":disabled" ) so that fieldset[disabled] works
return this.name && !jQuery( this ).is( ":disabled" ) &&
rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) &&
( this.checked || !rcheckableType.test( type ) );
- } )
- .map( function( _i, elem ) {
+ } ).map( function( _i, elem ) {
var val = jQuery( this ).val();
if ( val == null ) {
@@ -9033,7 +9040,8 @@ var
// Anchor tag for parsing the document origin
originAnchor = document.createElement( "a" );
- originAnchor.href = location.href;
+
+originAnchor.href = location.href;
// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport
function addToPrefiltersOrTransports( structure ) {
@@ -9414,8 +9422,8 @@ jQuery.extend( {
// Context for global events is callbackContext if it is a DOM node or jQuery collection
globalEventContext = s.context &&
( callbackContext.nodeType || callbackContext.jquery ) ?
- jQuery( callbackContext ) :
- jQuery.event,
+ jQuery( callbackContext ) :
+ jQuery.event,
// Deferreds
deferred = jQuery.Deferred(),
@@ -9727,8 +9735,10 @@ jQuery.extend( {
response = ajaxHandleResponses( s, jqXHR, responses );
}
- // Use a noop converter for missing script
- if ( !isSuccess && jQuery.inArray( "script", s.dataTypes ) > -1 ) {
+ // Use a noop converter for missing script but not if jsonp
+ if ( !isSuccess &&
+ jQuery.inArray( "script", s.dataTypes ) > -1 &&
+ jQuery.inArray( "json", s.dataTypes ) < 0 ) {
s.converters[ "text script" ] = function() {};
}
@@ -10466,12 +10476,6 @@ jQuery.offset = {
options.using.call( elem, props );
} else {
- if ( typeof props.top === "number" ) {
- props.top += "px";
- }
- if ( typeof props.left === "number" ) {
- props.left += "px";
- }
curElem.css( props );
}
}
@@ -10640,8 +10644,11 @@ jQuery.each( [ "top", "left" ], function( _i, prop ) {
// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods
jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
- jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name },
- function( defaultExtra, funcName ) {
+ jQuery.each( {
+ padding: "inner" + name,
+ content: type,
+ "": "outer" + name
+ }, function( defaultExtra, funcName ) {
// Margin is only for outerHeight, outerWidth
jQuery.fn[ funcName ] = function( margin, value ) {
@@ -10726,7 +10733,8 @@ jQuery.fn.extend( {
}
} );
-jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " +
+jQuery.each(
+ ( "blur focus focusin focusout resize scroll click dblclick " +
"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
"change select submit keydown keypress keyup contextmenu" ).split( " " ),
function( _i, name ) {
@@ -10737,7 +10745,8 @@ jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " +
this.on( name, null, data, fn ) :
this.trigger( name );
};
- } );
+ }
+);
diff --git a/docs/_static/jquery.js b/docs/_static/jquery.js
index b061403..c4c6022 100644
--- a/docs/_static/jquery.js
+++ b/docs/_static/jquery.js
@@ -1,2 +1,2 @@
-/*! jQuery v3.5.1 | (c) JS Foundation and other contributors | jquery.org/license */
-!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.5.1",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML=" ",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML=" ";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function D(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||j,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,j=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML=" ",y.option=!!ce.lastChild;var ge={thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function qe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function Le(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function He(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Oe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):("number"==typeof f.top&&(f.top+="px"),"number"==typeof f.left&&(f.left+="px"),c.css(f))}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=$e(y.pixelPosition,function(e,t){if(t)return t=Be(e,n),Me.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML=" ",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML=" ";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML=" ",y.option=!!ce.lastChild;var ge={thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0 {
+ const [docname, title, anchor, descr, score, filename] = result
+ return score
},
*/
@@ -28,9 +30,11 @@ if (!Scorer) {
// or matches in the last dotted part of the object name
objPartialMatch: 6,
// Additive scores depending on the priority of the object
- objPrio: {0: 15, // used to be importantResults
- 1: 5, // used to be objectResults
- 2: -5}, // used to be unimportantResults
+ objPrio: {
+ 0: 15, // used to be importantResults
+ 1: 5, // used to be objectResults
+ 2: -5, // used to be unimportantResults
+ },
// Used when the priority is not in the mapping.
objPrioDefault: 0,
@@ -39,452 +43,453 @@ if (!Scorer) {
partialTitle: 7,
// query found in terms
term: 5,
- partialTerm: 2
+ partialTerm: 2,
};
}
-if (!splitQuery) {
- function splitQuery(query) {
- return query.split(/\s+/);
+const _removeChildren = (element) => {
+ while (element && element.lastChild) element.removeChild(element.lastChild);
+};
+
+/**
+ * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
+ */
+const _escapeRegExp = (string) =>
+ string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
+
+const _displayItem = (item, highlightTerms, searchTerms) => {
+ const docBuilder = DOCUMENTATION_OPTIONS.BUILDER;
+ const docUrlRoot = DOCUMENTATION_OPTIONS.URL_ROOT;
+ const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX;
+ const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX;
+ const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY;
+
+ const [docName, title, anchor, descr] = item;
+
+ let listItem = document.createElement("li");
+ let requestUrl;
+ let linkUrl;
+ if (docBuilder === "dirhtml") {
+ // dirhtml builder
+ let dirname = docName + "/";
+ if (dirname.match(/\/index\/$/))
+ dirname = dirname.substring(0, dirname.length - 6);
+ else if (dirname === "index/") dirname = "";
+ requestUrl = docUrlRoot + dirname;
+ linkUrl = requestUrl;
+ } else {
+ // normal html builders
+ requestUrl = docUrlRoot + docName + docFileSuffix;
+ linkUrl = docName + docLinkSuffix;
+ }
+ const params = new URLSearchParams();
+ params.set("highlight", [...highlightTerms].join(" "));
+ let linkEl = listItem.appendChild(document.createElement("a"));
+ linkEl.href = linkUrl + "?" + params.toString() + anchor;
+ linkEl.innerHTML = title;
+ if (descr)
+ listItem.appendChild(document.createElement("span")).innerHTML =
+ " (" + descr + ")";
+ else if (showSearchSummary)
+ fetch(requestUrl)
+ .then((responseData) => responseData.text())
+ .then((data) => {
+ if (data)
+ listItem.appendChild(
+ Search.makeSearchSummary(data, searchTerms, highlightTerms)
+ );
+ });
+ Search.output.appendChild(listItem);
+};
+const _finishSearch = (resultCount) => {
+ Search.stopPulse();
+ Search.title.innerText = _("Search Results");
+ if (!resultCount)
+ Search.status.innerText = Documentation.gettext(
+ "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories."
+ );
+ else
+ Search.status.innerText = _(
+ `Search finished, found ${resultCount} page(s) matching the search query.`
+ );
+};
+const _displayNextItem = (
+ results,
+ resultCount,
+ highlightTerms,
+ searchTerms
+) => {
+ // results left, load the summary and display it
+ // this is intended to be dynamic (don't sub resultsCount)
+ if (results.length) {
+ _displayItem(results.pop(), highlightTerms, searchTerms);
+ setTimeout(
+ () => _displayNextItem(results, resultCount, highlightTerms, searchTerms),
+ 5
+ );
}
+ // search finished, update title and status message
+ else _finishSearch(resultCount);
+};
+
+/**
+ * Default splitQuery function. Can be overridden in ``sphinx.search`` with a
+ * custom function per language.
+ *
+ * The regular expression works by splitting the string on consecutive characters
+ * that are not Unicode letters, numbers, underscores, or emoji characters.
+ * This is the same as ``\W+`` in Python, preserving the surrogate pair area.
+ */
+if (typeof splitQuery === "undefined") {
+ var splitQuery = (query) => query
+ .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu)
+ .filter(term => term) // remove remaining empty strings
}
/**
* Search Module
*/
-var Search = {
-
- _index : null,
- _queued_query : null,
- _pulse_status : -1,
-
- htmlToText : function(htmlString) {
- var virtualDocument = document.implementation.createHTMLDocument('virtual');
- var htmlElement = $(htmlString, virtualDocument);
- htmlElement.find('.headerlink').remove();
- docContent = htmlElement.find('[role=main]')[0];
- if(docContent === undefined) {
- console.warn("Content block not found. Sphinx search tries to obtain it " +
- "via '[role=main]'. Could you check your theme or template.");
- return "";
- }
- return docContent.textContent || docContent.innerText;
+const Search = {
+ _index: null,
+ _queued_query: null,
+ _pulse_status: -1,
+
+ htmlToText: (htmlString) => {
+ const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html');
+ htmlElement.querySelectorAll(".headerlink").forEach((el) => { el.remove() });
+ const docContent = htmlElement.querySelector('[role="main"]');
+ if (docContent !== undefined) return docContent.textContent;
+ console.warn(
+ "Content block not found. Sphinx search tries to obtain it via '[role=main]'. Could you check your theme or template."
+ );
+ return "";
},
- init : function() {
- var params = $.getQueryParameters();
- if (params.q) {
- var query = params.q[0];
- $('input[name="q"]')[0].value = query;
- this.performSearch(query);
- }
+ init: () => {
+ const query = new URLSearchParams(window.location.search).get("q");
+ document
+ .querySelectorAll('input[name="q"]')
+ .forEach((el) => (el.value = query));
+ if (query) Search.performSearch(query);
},
- loadIndex : function(url) {
- $.ajax({type: "GET", url: url, data: null,
- dataType: "script", cache: true,
- complete: function(jqxhr, textstatus) {
- if (textstatus != "success") {
- document.getElementById("searchindexloader").src = url;
- }
- }});
- },
+ loadIndex: (url) =>
+ (document.body.appendChild(document.createElement("script")).src = url),
- setIndex : function(index) {
- var q;
- this._index = index;
- if ((q = this._queued_query) !== null) {
- this._queued_query = null;
- Search.query(q);
+ setIndex: (index) => {
+ Search._index = index;
+ if (Search._queued_query !== null) {
+ const query = Search._queued_query;
+ Search._queued_query = null;
+ Search.query(query);
}
},
- hasIndex : function() {
- return this._index !== null;
- },
+ hasIndex: () => Search._index !== null,
- deferQuery : function(query) {
- this._queued_query = query;
- },
+ deferQuery: (query) => (Search._queued_query = query),
- stopPulse : function() {
- this._pulse_status = 0;
- },
+ stopPulse: () => (Search._pulse_status = -1),
- startPulse : function() {
- if (this._pulse_status >= 0)
- return;
- function pulse() {
- var i;
+ startPulse: () => {
+ if (Search._pulse_status >= 0) return;
+
+ const pulse = () => {
Search._pulse_status = (Search._pulse_status + 1) % 4;
- var dotString = '';
- for (i = 0; i < Search._pulse_status; i++)
- dotString += '.';
- Search.dots.text(dotString);
- if (Search._pulse_status > -1)
- window.setTimeout(pulse, 500);
- }
+ Search.dots.innerText = ".".repeat(Search._pulse_status);
+ if (Search._pulse_status >= 0) window.setTimeout(pulse, 500);
+ };
pulse();
},
/**
* perform a search for something (or wait until index is loaded)
*/
- performSearch : function(query) {
+ performSearch: (query) => {
// create the required interface elements
- this.out = $('#search-results');
- this.title = $('' + _('Searching') + ' ').appendTo(this.out);
- this.dots = $(' ').appendTo(this.title);
- this.status = $('
').appendTo(this.out);
- this.output = $('').appendTo(this.out);
-
- $('#search-progress').text(_('Preparing search...'));
- this.startPulse();
+ const searchText = document.createElement("h2");
+ searchText.textContent = _("Searching");
+ const searchSummary = document.createElement("p");
+ searchSummary.classList.add("search-summary");
+ searchSummary.innerText = "";
+ const searchList = document.createElement("ul");
+ searchList.classList.add("search");
+
+ const out = document.getElementById("search-results");
+ Search.title = out.appendChild(searchText);
+ Search.dots = Search.title.appendChild(document.createElement("span"));
+ Search.status = out.appendChild(searchSummary);
+ Search.output = out.appendChild(searchList);
+
+ const searchProgress = document.getElementById("search-progress");
+ // Some themes don't use the search progress node
+ if (searchProgress) {
+ searchProgress.innerText = _("Preparing search...");
+ }
+ Search.startPulse();
// index already loaded, the browser was quick!
- if (this.hasIndex())
- this.query(query);
- else
- this.deferQuery(query);
+ if (Search.hasIndex()) Search.query(query);
+ else Search.deferQuery(query);
},
/**
* execute search (requires search index to be loaded)
*/
- query : function(query) {
- var i;
-
- // stem the searchterms and add them to the correct list
- var stemmer = new Stemmer();
- var searchterms = [];
- var excluded = [];
- var hlterms = [];
- var tmp = splitQuery(query);
- var objectterms = [];
- for (i = 0; i < tmp.length; i++) {
- if (tmp[i] !== "") {
- objectterms.push(tmp[i].toLowerCase());
- }
+ query: (query) => {
+ // stem the search terms and add them to the correct list
+ const stemmer = new Stemmer();
+ const searchTerms = new Set();
+ const excludedTerms = new Set();
+ const highlightTerms = new Set();
+ const objectTerms = new Set(splitQuery(query.toLowerCase().trim()));
+ splitQuery(query.trim()).forEach((queryTerm) => {
+ const queryTermLower = queryTerm.toLowerCase();
+
+ // maybe skip this "word"
+ // stopwords array is from language_data.js
+ if (
+ stopwords.indexOf(queryTermLower) !== -1 ||
+ queryTerm.match(/^\d+$/)
+ )
+ return;
- if ($u.indexOf(stopwords, tmp[i].toLowerCase()) != -1 || tmp[i] === "") {
- // skip this "word"
- continue;
- }
// stem the word
- var word = stemmer.stemWord(tmp[i].toLowerCase());
- // prevent stemmer from cutting word smaller than two chars
- if(word.length < 3 && tmp[i].length >= 3) {
- word = tmp[i];
- }
- var toAppend;
+ let word = stemmer.stemWord(queryTermLower);
// select the correct list
- if (word[0] == '-') {
- toAppend = excluded;
- word = word.substr(1);
- }
+ if (word[0] === "-") excludedTerms.add(word.substr(1));
else {
- toAppend = searchterms;
- hlterms.push(tmp[i].toLowerCase());
+ searchTerms.add(word);
+ highlightTerms.add(queryTermLower);
}
- // only add if not already in the list
- if (!$u.contains(toAppend, word))
- toAppend.push(word);
- }
- var highlightstring = '?highlight=' + $.urlencode(hlterms.join(" "));
-
- // console.debug('SEARCH: searching for:');
- // console.info('required: ', searchterms);
- // console.info('excluded: ', excluded);
+ });
- // prepare search
- var terms = this._index.terms;
- var titleterms = this._index.titleterms;
+ // console.debug("SEARCH: searching for:");
+ // console.info("required: ", [...searchTerms]);
+ // console.info("excluded: ", [...excludedTerms]);
- // array of [filename, title, anchor, descr, score]
- var results = [];
- $('#search-progress').empty();
+ // array of [docname, title, anchor, descr, score, filename]
+ let results = [];
+ _removeChildren(document.getElementById("search-progress"));
// lookup as object
- for (i = 0; i < objectterms.length; i++) {
- var others = [].concat(objectterms.slice(0, i),
- objectterms.slice(i+1, objectterms.length));
- results = results.concat(this.performObjectSearch(objectterms[i], others));
- }
+ objectTerms.forEach((term) =>
+ results.push(...Search.performObjectSearch(term, objectTerms))
+ );
// lookup as search terms in fulltext
- results = results.concat(this.performTermsSearch(searchterms, excluded, terms, titleterms));
+ results.push(...Search.performTermsSearch(searchTerms, excludedTerms));
// let the scorer override scores with a custom scoring function
- if (Scorer.score) {
- for (i = 0; i < results.length; i++)
- results[i][4] = Scorer.score(results[i]);
- }
+ if (Scorer.score) results.forEach((item) => (item[4] = Scorer.score(item)));
// now sort the results by score (in opposite order of appearance, since the
// display function below uses pop() to retrieve items) and then
// alphabetically
- results.sort(function(a, b) {
- var left = a[4];
- var right = b[4];
- if (left > right) {
- return 1;
- } else if (left < right) {
- return -1;
- } else {
+ results.sort((a, b) => {
+ const leftScore = a[4];
+ const rightScore = b[4];
+ if (leftScore === rightScore) {
// same score: sort alphabetically
- left = a[1].toLowerCase();
- right = b[1].toLowerCase();
- return (left > right) ? -1 : ((left < right) ? 1 : 0);
+ const leftTitle = a[1].toLowerCase();
+ const rightTitle = b[1].toLowerCase();
+ if (leftTitle === rightTitle) return 0;
+ return leftTitle > rightTitle ? -1 : 1; // inverted is intentional
}
+ return leftScore > rightScore ? 1 : -1;
});
+ // remove duplicate search results
+ // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept
+ let seen = new Set();
+ results = results.reverse().reduce((acc, result) => {
+ let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(',');
+ if (!seen.has(resultStr)) {
+ acc.push(result);
+ seen.add(resultStr);
+ }
+ return acc;
+ }, []);
+
+ results = results.reverse();
+
// for debugging
//Search.lastresults = results.slice(); // a copy
- //console.info('search results:', Search.lastresults);
+ // console.info("search results:", Search.lastresults);
// print the results
- var resultCount = results.length;
- function displayNextItem() {
- // results left, load the summary and display it
- if (results.length) {
- var item = results.pop();
- var listItem = $(' ');
- var requestUrl = "";
- var linkUrl = "";
- if (DOCUMENTATION_OPTIONS.BUILDER === 'dirhtml') {
- // dirhtml builder
- var dirname = item[0] + '/';
- if (dirname.match(/\/index\/$/)) {
- dirname = dirname.substring(0, dirname.length-6);
- } else if (dirname == 'index/') {
- dirname = '';
- }
- requestUrl = DOCUMENTATION_OPTIONS.URL_ROOT + dirname;
- linkUrl = requestUrl;
-
- } else {
- // normal html builders
- requestUrl = DOCUMENTATION_OPTIONS.URL_ROOT + item[0] + DOCUMENTATION_OPTIONS.FILE_SUFFIX;
- linkUrl = item[0] + DOCUMENTATION_OPTIONS.LINK_SUFFIX;
- }
- listItem.append($(' ').attr('href',
- linkUrl +
- highlightstring + item[2]).html(item[1]));
- if (item[3]) {
- listItem.append($(' (' + item[3] + ') '));
- Search.output.append(listItem);
- setTimeout(function() {
- displayNextItem();
- }, 5);
- } else if (DOCUMENTATION_OPTIONS.HAS_SOURCE) {
- $.ajax({url: requestUrl,
- dataType: "text",
- complete: function(jqxhr, textstatus) {
- var data = jqxhr.responseText;
- if (data !== '' && data !== undefined) {
- listItem.append(Search.makeSearchSummary(data, searchterms, hlterms));
- }
- Search.output.append(listItem);
- setTimeout(function() {
- displayNextItem();
- }, 5);
- }});
- } else {
- // no source available, just display title
- Search.output.append(listItem);
- setTimeout(function() {
- displayNextItem();
- }, 5);
- }
- }
- // search finished, update title and status message
- else {
- Search.stopPulse();
- Search.title.text(_('Search Results'));
- if (!resultCount)
- Search.status.text(_('Your search did not match any documents. Please make sure that all words are spelled correctly and that you\'ve selected enough categories.'));
- else
- Search.status.text(_('Search finished, found %s page(s) matching the search query.').replace('%s', resultCount));
- Search.status.fadeIn(500);
- }
- }
- displayNextItem();
+ _displayNextItem(results, results.length, highlightTerms, searchTerms);
},
/**
* search for object names
*/
- performObjectSearch : function(object, otherterms) {
- var filenames = this._index.filenames;
- var docnames = this._index.docnames;
- var objects = this._index.objects;
- var objnames = this._index.objnames;
- var titles = this._index.titles;
-
- var i;
- var results = [];
-
- for (var prefix in objects) {
- for (var name in objects[prefix]) {
- var fullname = (prefix ? prefix + '.' : '') + name;
- var fullnameLower = fullname.toLowerCase()
- if (fullnameLower.indexOf(object) > -1) {
- var score = 0;
- var parts = fullnameLower.split('.');
- // check for different match types: exact matches of full name or
- // "last name" (i.e. last dotted part)
- if (fullnameLower == object || parts[parts.length - 1] == object) {
- score += Scorer.objNameMatch;
- // matches in last name
- } else if (parts[parts.length - 1].indexOf(object) > -1) {
- score += Scorer.objPartialMatch;
- }
- var match = objects[prefix][name];
- var objname = objnames[match[1]][2];
- var title = titles[match[0]];
- // If more than one term searched for, we require other words to be
- // found in the name/title/description
- if (otherterms.length > 0) {
- var haystack = (prefix + ' ' + name + ' ' +
- objname + ' ' + title).toLowerCase();
- var allfound = true;
- for (i = 0; i < otherterms.length; i++) {
- if (haystack.indexOf(otherterms[i]) == -1) {
- allfound = false;
- break;
- }
- }
- if (!allfound) {
- continue;
- }
- }
- var descr = objname + _(', in ') + title;
-
- var anchor = match[3];
- if (anchor === '')
- anchor = fullname;
- else if (anchor == '-')
- anchor = objnames[match[1]][1] + '-' + fullname;
- // add custom score for some objects according to scorer
- if (Scorer.objPrio.hasOwnProperty(match[2])) {
- score += Scorer.objPrio[match[2]];
- } else {
- score += Scorer.objPrioDefault;
- }
- results.push([docnames[match[0]], fullname, '#'+anchor, descr, score, filenames[match[0]]]);
- }
+ performObjectSearch: (object, objectTerms) => {
+ const filenames = Search._index.filenames;
+ const docNames = Search._index.docnames;
+ const objects = Search._index.objects;
+ const objNames = Search._index.objnames;
+ const titles = Search._index.titles;
+
+ const results = [];
+
+ const objectSearchCallback = (prefix, match) => {
+ const name = match[4]
+ const fullname = (prefix ? prefix + "." : "") + name;
+ const fullnameLower = fullname.toLowerCase();
+ if (fullnameLower.indexOf(object) < 0) return;
+
+ let score = 0;
+ const parts = fullnameLower.split(".");
+
+ // check for different match types: exact matches of full name or
+ // "last name" (i.e. last dotted part)
+ if (fullnameLower === object || parts.slice(-1)[0] === object)
+ score += Scorer.objNameMatch;
+ else if (parts.slice(-1)[0].indexOf(object) > -1)
+ score += Scorer.objPartialMatch; // matches in last name
+
+ const objName = objNames[match[1]][2];
+ const title = titles[match[0]];
+
+ // If more than one term searched for, we require other words to be
+ // found in the name/title/description
+ const otherTerms = new Set(objectTerms);
+ otherTerms.delete(object);
+ if (otherTerms.size > 0) {
+ const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase();
+ if (
+ [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0)
+ )
+ return;
}
- }
+ let anchor = match[3];
+ if (anchor === "") anchor = fullname;
+ else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname;
+
+ const descr = objName + _(", in ") + title;
+
+ // add custom score for some objects according to scorer
+ if (Scorer.objPrio.hasOwnProperty(match[2]))
+ score += Scorer.objPrio[match[2]];
+ else score += Scorer.objPrioDefault;
+
+ results.push([
+ docNames[match[0]],
+ fullname,
+ "#" + anchor,
+ descr,
+ score,
+ filenames[match[0]],
+ ]);
+ };
+ Object.keys(objects).forEach((prefix) =>
+ objects[prefix].forEach((array) =>
+ objectSearchCallback(prefix, array)
+ )
+ );
return results;
},
- /**
- * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
- */
- escapeRegExp : function(string) {
- return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
- },
-
/**
* search for full-text terms in the index
*/
- performTermsSearch : function(searchterms, excluded, terms, titleterms) {
- var docnames = this._index.docnames;
- var filenames = this._index.filenames;
- var titles = this._index.titles;
+ performTermsSearch: (searchTerms, excludedTerms) => {
+ // prepare search
+ const terms = Search._index.terms;
+ const titleTerms = Search._index.titleterms;
+ const docNames = Search._index.docnames;
+ const filenames = Search._index.filenames;
+ const titles = Search._index.titles;
- var i, j, file;
- var fileMap = {};
- var scoreMap = {};
- var results = [];
+ const scoreMap = new Map();
+ const fileMap = new Map();
// perform the search on the required terms
- for (i = 0; i < searchterms.length; i++) {
- var word = searchterms[i];
- var files = [];
- var _o = [
- {files: terms[word], score: Scorer.term},
- {files: titleterms[word], score: Scorer.title}
+ searchTerms.forEach((word) => {
+ const files = [];
+ const arr = [
+ { files: terms[word], score: Scorer.term },
+ { files: titleTerms[word], score: Scorer.title },
];
// add support for partial matches
if (word.length > 2) {
- var word_regex = this.escapeRegExp(word);
- for (var w in terms) {
- if (w.match(word_regex) && !terms[word]) {
- _o.push({files: terms[w], score: Scorer.partialTerm})
- }
- }
- for (var w in titleterms) {
- if (w.match(word_regex) && !titleterms[word]) {
- _o.push({files: titleterms[w], score: Scorer.partialTitle})
- }
- }
+ const escapedWord = _escapeRegExp(word);
+ Object.keys(terms).forEach((term) => {
+ if (term.match(escapedWord) && !terms[word])
+ arr.push({ files: terms[term], score: Scorer.partialTerm });
+ });
+ Object.keys(titleTerms).forEach((term) => {
+ if (term.match(escapedWord) && !titleTerms[word])
+ arr.push({ files: titleTerms[word], score: Scorer.partialTitle });
+ });
}
// no match but word was a required one
- if ($u.every(_o, function(o){return o.files === undefined;})) {
- break;
- }
+ if (arr.every((record) => record.files === undefined)) return;
+
// found search word in contents
- $u.each(_o, function(o) {
- var _files = o.files;
- if (_files === undefined)
- return
-
- if (_files.length === undefined)
- _files = [_files];
- files = files.concat(_files);
-
- // set score for the word in each file to Scorer.term
- for (j = 0; j < _files.length; j++) {
- file = _files[j];
- if (!(file in scoreMap))
- scoreMap[file] = {};
- scoreMap[file][word] = o.score;
- }
+ arr.forEach((record) => {
+ if (record.files === undefined) return;
+
+ let recordFiles = record.files;
+ if (recordFiles.length === undefined) recordFiles = [recordFiles];
+ files.push(...recordFiles);
+
+ // set score for the word in each file
+ recordFiles.forEach((file) => {
+ if (!scoreMap.has(file)) scoreMap.set(file, {});
+ scoreMap.get(file)[word] = record.score;
+ });
});
// create the mapping
- for (j = 0; j < files.length; j++) {
- file = files[j];
- if (file in fileMap && fileMap[file].indexOf(word) === -1)
- fileMap[file].push(word);
- else
- fileMap[file] = [word];
- }
- }
+ files.forEach((file) => {
+ if (fileMap.has(file) && fileMap.get(file).indexOf(word) === -1)
+ fileMap.get(file).push(word);
+ else fileMap.set(file, [word]);
+ });
+ });
// now check if the files don't contain excluded terms
- for (file in fileMap) {
- var valid = true;
-
+ const results = [];
+ for (const [file, wordList] of fileMap) {
// check if all requirements are matched
- var filteredTermCount = // as search terms with length < 3 are discarded: ignore
- searchterms.filter(function(term){return term.length > 2}).length
+
+ // as search terms with length < 3 are discarded
+ const filteredTermCount = [...searchTerms].filter(
+ (term) => term.length > 2
+ ).length;
if (
- fileMap[file].length != searchterms.length &&
- fileMap[file].length != filteredTermCount
- ) continue;
+ wordList.length !== searchTerms.size &&
+ wordList.length !== filteredTermCount
+ )
+ continue;
// ensure that none of the excluded terms is in the search result
- for (i = 0; i < excluded.length; i++) {
- if (terms[excluded[i]] == file ||
- titleterms[excluded[i]] == file ||
- $u.contains(terms[excluded[i]] || [], file) ||
- $u.contains(titleterms[excluded[i]] || [], file)) {
- valid = false;
- break;
- }
- }
+ if (
+ [...excludedTerms].some(
+ (term) =>
+ terms[term] === file ||
+ titleTerms[term] === file ||
+ (terms[term] || []).includes(file) ||
+ (titleTerms[term] || []).includes(file)
+ )
+ )
+ break;
- // if we have still a valid result we can add it to the result list
- if (valid) {
- // select one (max) score for the file.
- // for better ranking, we should calculate ranking by using words statistics like basic tf-idf...
- var score = $u.max($u.map(fileMap[file], function(w){return scoreMap[file][w]}));
- results.push([docnames[file], titles[file], '', null, score, filenames[file]]);
- }
+ // select one (max) score for the file.
+ const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w]));
+ // add result to the result list
+ results.push([
+ docNames[file],
+ titles[file],
+ "",
+ null,
+ score,
+ filenames[file],
+ ]);
}
return results;
},
@@ -492,31 +497,34 @@ var Search = {
/**
* helper function to return a node containing the
* search summary for a given text. keywords is a list
- * of stemmed words, hlwords is the list of normal, unstemmed
+ * of stemmed words, highlightWords is the list of normal, unstemmed
* words. the first one is used to find the occurrence, the
* latter for highlighting it.
*/
- makeSearchSummary : function(htmlText, keywords, hlwords) {
- var text = Search.htmlToText(htmlText);
- var textLower = text.toLowerCase();
- var start = 0;
- $.each(keywords, function() {
- var i = textLower.indexOf(this.toLowerCase());
- if (i > -1)
- start = i;
- });
- start = Math.max(start - 120, 0);
- var excerpt = ((start > 0) ? '...' : '') +
- $.trim(text.substr(start, 240)) +
- ((start + 240 - text.length) ? '...' : '');
- var rv = $('
').text(excerpt);
- $.each(hlwords, function() {
- rv = rv.highlightText(this, 'highlighted');
- });
- return rv;
- }
+ makeSearchSummary: (htmlText, keywords, highlightWords) => {
+ const text = Search.htmlToText(htmlText);
+ if (text === "") return null;
+
+ const textLower = text.toLowerCase();
+ const actualStartPosition = [...keywords]
+ .map((k) => textLower.indexOf(k.toLowerCase()))
+ .filter((i) => i > -1)
+ .slice(-1)[0];
+ const startWithContext = Math.max(actualStartPosition - 120, 0);
+
+ const top = startWithContext === 0 ? "" : "...";
+ const tail = startWithContext + 240 < text.length ? "..." : "";
+
+ let summary = document.createElement("p");
+ summary.classList.add("context");
+ summary.textContent = top + text.substr(startWithContext, 240).trim() + tail;
+
+ highlightWords.forEach((highlightWord) =>
+ _highlightText(summary, highlightWord, "highlighted")
+ );
+
+ return summary;
+ },
};
-$(document).ready(function() {
- Search.init();
-});
+_ready(Search.init);
diff --git a/docs/_static/sidebar.js b/docs/_static/sidebar.js
index 599639f..ae3080b 100644
--- a/docs/_static/sidebar.js
+++ b/docs/_static/sidebar.js
@@ -16,144 +16,55 @@
* Once the browser is closed the cookie is deleted and the position
* reset to the default (expanded).
*
- * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
+ * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
-$(function() {
-
-
-
-
-
+const initialiseSidebar = () => {
+
// global elements used by the functions.
- // the 'sidebarbutton' element is defined as global after its
- // creation, in the add_sidebar_button function
- var bodywrapper = $('.bodywrapper');
- var sidebar = $('.sphinxsidebar');
- var sidebarwrapper = $('.sphinxsidebarwrapper');
+ const bodyWrapper = document.getElementsByClassName("bodywrapper")[0]
+ const sidebar = document.getElementsByClassName("sphinxsidebar")[0]
+ const sidebarWrapper = document.getElementsByClassName('sphinxsidebarwrapper')[0]
+ const sidebarButton = document.getElementById("sidebarbutton")
+ const sidebarArrow = sidebarButton.querySelector('span')
// for some reason, the document has no sidebar; do not run into errors
- if (!sidebar.length) return;
-
- // original margin-left of the bodywrapper and width of the sidebar
- // with the sidebar expanded
- var bw_margin_expanded = bodywrapper.css('margin-left');
- var ssb_width_expanded = sidebar.width();
+ if (typeof sidebar === "undefined") return;
- // margin-left of the bodywrapper and width of the sidebar
- // with the sidebar collapsed
- var bw_margin_collapsed = '.8em';
- var ssb_width_collapsed = '.8em';
+ const flipArrow = element => element.innerText = (element.innerText === "»") ? "«" : "»"
- // colors used by the current theme
- var dark_color = $('.related').css('background-color');
- var light_color = $('.document').css('background-color');
-
- function sidebar_is_collapsed() {
- return sidebarwrapper.is(':not(:visible)');
+ const collapse_sidebar = () => {
+ bodyWrapper.style.marginLeft = ".8em";
+ sidebar.style.width = ".8em"
+ sidebarWrapper.style.display = "none"
+ flipArrow(sidebarArrow)
+ sidebarButton.title = _('Expand sidebar')
+ window.localStorage.setItem("sidebar", "collapsed")
}
- function toggle_sidebar() {
- if (sidebar_is_collapsed())
- expand_sidebar();
- else
- collapse_sidebar();
+ const expand_sidebar = () => {
+ bodyWrapper.style.marginLeft = ""
+ sidebar.style.removeProperty("width")
+ sidebarWrapper.style.display = ""
+ flipArrow(sidebarArrow)
+ sidebarButton.title = _('Collapse sidebar')
+ window.localStorage.setItem("sidebar", "expanded")
}
- function collapse_sidebar() {
- sidebarwrapper.hide();
- sidebar.css('width', ssb_width_collapsed);
- bodywrapper.css('margin-left', bw_margin_collapsed);
- sidebarbutton.css({
- 'margin-left': '0',
- 'height': bodywrapper.height()
- });
- sidebarbutton.find('span').text('»');
- sidebarbutton.attr('title', _('Expand sidebar'));
- document.cookie = 'sidebar=collapsed';
- }
+ sidebarButton.addEventListener("click", () => {
+ (sidebarWrapper.style.display === "none") ? expand_sidebar() : collapse_sidebar()
+ })
- function expand_sidebar() {
- bodywrapper.css('margin-left', bw_margin_expanded);
- sidebar.css('width', ssb_width_expanded);
- sidebarwrapper.show();
- sidebarbutton.css({
- 'margin-left': ssb_width_expanded-12,
- 'height': bodywrapper.height()
- });
- sidebarbutton.find('span').text('«');
- sidebarbutton.attr('title', _('Collapse sidebar'));
- document.cookie = 'sidebar=expanded';
- }
-
- function add_sidebar_button() {
- sidebarwrapper.css({
- 'float': 'left',
- 'margin-right': '0',
- 'width': ssb_width_expanded - 28
- });
- // create the button
- sidebar.append(
- ''
- );
- var sidebarbutton = $('#sidebarbutton');
- light_color = sidebarbutton.css('background-color');
- // find the height of the viewport to center the '<<' in the page
- var viewport_height;
- if (window.innerHeight)
- viewport_height = window.innerHeight;
- else
- viewport_height = $(window).height();
- sidebarbutton.find('span').css({
- 'display': 'block',
- 'margin-top': (viewport_height - sidebar.position().top - 20) / 2
- });
-
- sidebarbutton.click(toggle_sidebar);
- sidebarbutton.attr('title', _('Collapse sidebar'));
- sidebarbutton.css({
- 'color': '#FFFFFF',
- 'border-left': '1px solid ' + dark_color,
- 'font-size': '1.2em',
- 'cursor': 'pointer',
- 'height': bodywrapper.height(),
- 'padding-top': '1px',
- 'margin-left': ssb_width_expanded - 12
- });
-
- sidebarbutton.hover(
- function () {
- $(this).css('background-color', dark_color);
- },
- function () {
- $(this).css('background-color', light_color);
- }
- );
- }
-
- function set_position_from_cookie() {
- if (!document.cookie)
- return;
- var items = document.cookie.split(';');
- for(var k=0; k
-
+
@@ -12,6 +12,7 @@
+
@@ -44,10 +45,12 @@ Index
| B
| C
| D
- | E
+ | F
| G
| L
| M
+ | N
+ | P
| R
| S
@@ -55,9 +58,11 @@ Index
A
B
@@ -82,11 +95,17 @@ B
C
@@ -94,15 +113,21 @@ C
D
-E
+F
@@ -110,7 +135,17 @@ E
G
@@ -118,7 +153,7 @@ G
L
@@ -131,23 +166,106 @@ M
+
+ modules.att
+
+
+
+
+
+ modules.helper
+
+
+
+ modules.models
+
+
+
+ modules.netgear
+
+
+
+ modules.settings
+
+
+N
+
+
+P
+
+
R
S
@@ -169,7 +287,7 @@ Quick search
-
+
@@ -189,7 +307,7 @@ Navigation
\ No newline at end of file
diff --git a/docs/index.html b/docs/index.html
index 7b19791..b43bb34 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -1,10 +1,10 @@
-
+
-
+
Welcome to NetScan’s documentation! — NetScan documentation
@@ -13,6 +13,7 @@
+
@@ -42,157 +43,288 @@ Navigation
-Welcome to NetScan’s documentation!
+Welcome to NetScan’s documentation!
-NetScan
+NetScan
+
+
+analyzer. network_monitor ( module : SupportedModules , init : bool = True ) → NoReturn
+Monitor devices connected to the network.
+
+Parameters:
+
+module – Module to scan. Currently, supports any network on a Netgear router or At&t networks.
+init – Takes a boolean value to create a snapshot file or actually monitor the network.
+
+
+
+
+
+
+
+At&t
-
-class analyzer. LocalIPScan ( router_pass : Optional [ str ] = None )
+
+class modules.att. Device ( dictionary : dict )
+Convert dictionary into a device object.
+
+
+
+
+
+modules.att. create_snapshot ( ) → NoReturn
+Creates a snapshot.json which is used to determine the known and unknown devices.
+
+
+
+
+modules.att. format_key ( key : str ) → str
+Format the key to match the Device object.
+
+
+
+
+modules.att. generate_dataframe ( ) → DataFrame
+Generate a dataframe using the devices information from router web page.
+
+Returns:
+Devices list as a data frame.
+
+Return type:
+DataFrame
+
+
+
+
+
+
+modules.att. get_attached_devices ( ) → Generator [ Device ]
+Get all devices connected to the router.
+
+Yields:
+Generator[Device] – Yields each device information as a Device object.
+
+
+
+
+
+
+modules.att. get_ipaddress ( ) → str
+Get network id from the current IP address.
+
+
+
+
+modules.att. run ( ) → NoReturn
+Trigger to initiate a Network Scan and block the devices that are not present in snapshot.json
file.
+
+
+
+
+Netgear
+
+
+class modules.netgear. LocalIPScan
Connector to scan devices in the same IP range using Netgear API
.
-
-allow ( device : Union [ str , pynetgear.Device ] ) → Optional [ pynetgear.Device ]
+
+allow ( device : Union [ str , Device ] ) → Optional [ Device ]
Allows internet access to a device.
-Parameters
+Parameters:
device – Takes device name or Device object as an argument.
-Returns
+Returns:
Returns the device object received from get_device_by_name()
method.
-Return type
-Device
+Return type:
+Device
-
-always_allow ( device : pynetgear.Device ) → NoReturn
+
+always_allow ( device : Device ) → NoReturn
Allows internet access to a device.
-Saves the device name to snapshot.json
to not block in future.
+
Saves the device name to snapshot.json
to not block in the future.
Removes the device name from blocked.json
if an entry is present.
-Parameters
+Parameters:
device – Takes device name or Device object as an argument
-
-block ( device : Union [ str , pynetgear.Device ] ) → Optional [ pynetgear.Device ]
+
+block ( device : Union [ str , Device ] ) → Optional [ Device ]
Blocks internet access to a device.
-Parameters
+Parameters:
device – Takes device name or Device object as an argument.
-Returns
+Returns:
Returns the device object received from get_device_by_name()
method.
-Return type
-Device
+Return type:
+Device
-
-create_snapshot ( ) → NoReturn
+
+create_snapshot ( ) → NoReturn
Creates a snapshot.json which is used to determine the known and unknown devices.
-
-run ( block : bool = False ) → NoReturn
+
+run ( block : bool = False ) → NoReturn
Trigger to initiate a Network Scan and block the devices that are not present in snapshot.json
file.
+
+
+Helper
-
-analyzer. custom_time ( * args : logging.Formatter ) → time.struct_time
+
+modules.helper. custom_time ( * args : Formatter ) → struct_time
Creates custom timezone for logging
which gets used only when invoked by Docker
.
This is used only when triggered within a docker container
as it uses UTC timezone.
-Parameters
+Parameters:
*args – Takes Formatter
object and current epoch time as arguments passed by formatTime
from logging
.
-Returns
+Returns:
A struct_time object which is a tuple of:
current year, month, day, hour, minute, second, weekday, year day and dst (Daylight Saving Time)
-Return type
+Return type:
struct_time
-
-analyzer. device_name ( ) → str
-Gets the device name for MacOS and Windows.
-
-
-
-
-Extracts strings from the received input.
+
+modules.helper. notify ( msg : str ) → NoReturn
+Send an email notification when there is a threat.
-Parameters
-input_ – Takes a string as argument.
-
-Returns
-A string after removing special characters.
-
-Return type
-str
+Parameters:
+msg – Message that has to be sent.
-
-
-analyzer. get_ssid ( ) → Optional [ str ]
-Checks the current operating system and runs the appropriate command to get the SSID of the access point.
-
-Returns
-SSID of the access point/router which is being accessed.
-
-Return type
-str
-
-
+
+
+Models
+
+
+class modules.models. DeviceStatus ( value )
+Device status strings for allow or block.
+
+
+allow : str = 'Allow'
+
+
+
+
+block : str = 'Block'
+
+
-
-
-analyzer. send_sms ( msg : str ) → NoReturn
-Sens an SMS notification when invoked by the run
method.
-
-Parameters
-msg – Message that has to be sent.
-
-
+
+
+class modules.models. SupportedModules ( value )
+Supported modules are At&t and Netgear.
+
+
+att : str = 'At&t'
+
+
+
+
+netgear : str = 'Netgear'
+
+
+
+
+
+
+Settings
+
+
+class modules.settings. Config
+Wrapper for all the environment variables.
+
+
+blocked = 'fileio/blocked.yaml'
+
+
+
+
+docker = None
+
+
+
+
+gmail_pass = None
+
+
+
+
+gmail_user = None
+
+
+
+
+phone = None
+
+
+
+
+recipient = None
+
+
+
+
+router_pass = None
+
+
+
+
+snapshot = 'fileio/snapshot.json'
+
+
-Indices and tables
+Indices and tables
@@ -256,7 +397,7 @@ Navigation
\ No newline at end of file
diff --git a/docs/objects.inv b/docs/objects.inv
index c7de258..24e60d1 100644
--- a/docs/objects.inv
+++ b/docs/objects.inv
@@ -2,4 +2,4 @@
# Project: NetScan
# Version:
# The remainder of this file is compressed using zlib.
-xڝKN09lSmwHtQ.XZS{Hy&a5'!JPRv?7cLATz]tYz#+0lu2@$c7|}?zjw$/C&9&Vri_jT(L6K0ȱqB%Y:?-KA+ELzB<"wzltFv-onDvez3+5v{:WJHK|^_ָ-#JE R~Pd%-"VuRAW6FJ!V r}I܆rۛ"YE)&7PEdMڇyf o|]REt%Brn1l
lׇ1#ɛs0O;qLc
\ No newline at end of file
diff --git a/docs/py-modindex.html b/docs/py-modindex.html
index f144b59..c1ede0c 100644
--- a/docs/py-modindex.html
+++ b/docs/py-modindex.html
@@ -1,7 +1,7 @@
-
+
@@ -12,16 +12,13 @@
+
-
-
-
+
@@ -97,7 +129,7 @@ Navigation
\ No newline at end of file
diff --git a/docs/search.html b/docs/search.html
index ce516e7..733416b 100644
--- a/docs/search.html
+++ b/docs/search.html
@@ -1,7 +1,7 @@
-
+
@@ -13,6 +13,7 @@
+
@@ -44,13 +45,14 @@ Navigation
Search
-
-
+
+
Please activate JavaScript to enable the search
functionality.
+
@@ -97,7 +99,7 @@
Navigation
\ No newline at end of file
diff --git a/docs/searchindex.js b/docs/searchindex.js
index d67035b..5bdcde1 100644
--- a/docs/searchindex.js
+++ b/docs/searchindex.js
@@ -1 +1 @@
-Search.setIndex({docnames:["README","index"],envversion:{"sphinx.domains.c":2,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":4,"sphinx.domains.index":1,"sphinx.domains.javascript":2,"sphinx.domains.math":2,"sphinx.domains.python":3,"sphinx.domains.rst":2,"sphinx.domains.std":2,sphinx:56},filenames:["README.md","index.rst"],objects:{"":{analyzer:[1,0,0,"-"]},"analyzer.LocalIPScan":{allow:[1,2,1,""],always_allow:[1,2,1,""],block:[1,2,1,""],create_snapshot:[1,2,1,""],run:[1,2,1,""]},analyzer:{LocalIPScan:[1,1,1,""],custom_time:[1,3,1,""],device_name:[1,3,1,""],extract_str:[1,3,1,""],get_ssid:[1,3,1,""],send_sms:[1,3,1,""]}},objnames:{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","method","Python method"],"3":["py","function","Python function"]},objtypes:{"0":"py:module","1":"py:class","2":"py:method","3":"py:function"},terms:{"class":1,"return":1,A:1,access:1,accordingli:0,ad:0,addit:0,address:0,after:1,alert:0,allow:1,always_allow:1,an:1,analyz:[0,1],anystr:1,api:1,app:0,appropri:1,ar:1,arg:1,argument:1,being:1,block:[0,1],bool:1,build:0,can:0,charact:1,check:1,command:1,connect:0,connector:1,contain:1,creat:1,create_snapshot:1,current:1,custom:1,custom_tim:1,dai:1,daylight:1,determin:1,devic:[0,1],device_nam:1,displai:0,docker:1,dst:1,entri:1,epoch:1,etc:0,even:0,extract:1,extract_str:1,fals:1,featur:0,fi:0,file:1,formatt:1,formattim:1,from:[0,1],futur:1,get:1,get_device_by_nam:1,get_ssid:1,ha:1,help:0,hour:1,index:1,info:0,initi:1,input:1,input_:1,internet:1,intrud:0,invok:1,ip:[0,1],json:1,known:1,let:0,localipscan:1,log:1,mac:0,maco:1,messag:1,method:1,minut:1,modul:1,month:1,msg:1,name:1,netgear:1,network:[0,1],none:1,noreturn:1,notif:1,object:1,onli:1,oper:1,option:[0,1],page:1,paramet:1,pass:1,ping:0,plain:0,point:1,present:1,progress:0,pynetgear:1,rang:1,receiv:1,remov:[0,1],router:[0,1],router_pass:1,run:[0,1],same:1,save:1,scan:1,scanner:0,search:1,second:1,sen:1,send_sm:1,sent:1,setup:1,signal:0,sm:1,snapshot:1,special:1,specif:0,specifi:0,speed:0,ssid:1,str:1,string:1,struct_tim:1,system:1,t:0,take:1,thi:[0,1],time:1,timezon:1,trigger:1,tupl:1,type:1,union:1,unknown:1,us:1,user:0,utc:1,weekdai:1,when:1,which:1,wi:0,window:1,within:1,year:1},titles:["NetScan","Welcome to NetScan\u2019s documentation!"],titleterms:{command:0,docker:0,document:1,indic:1,me:1,netscan:[0,1],read:1,refer:0,repositori:0,runbook:0,s:1,setup:0,tabl:1,welcom:1}})
\ No newline at end of file
+Search.setIndex({"docnames": ["README", "index"], "filenames": ["README.md", "index.rst"], "titles": ["NetScan", "Welcome to NetScan\u2019s documentation!"], "terms": {"network": [0, 1], "scanner": 0, "analyz": [0, 1], "devic": [0, 1], "connect": [0, 1], "router": [0, 1], "alert": 0, "accordingli": 0, "thi": [0, 1], "app": 0, "can": 0, "displai": 0, "intrud": 0, "ip": [0, 1], "address": [0, 1], "mac": 0, "let": 0, "user": 0, "ping": 0, "even": 0, "block": [0, 1], "featur": 0, "help": 0, "remov": [0, 1], "specifi": 0, "specif": 0, "i": [0, 1], "current": [0, 1], "avail": 0, "onli": [0, 1], "netgear": 0, "docstr": 0, "format": [0, 1], "googl": 0, "style": 0, "convent": 0, "pep": 0, "8": 0, "clean": 0, "pre": 0, "commit": 0, "hook": 0, "flake8": 0, "isort": 0, "requir": 0, "python": 0, "m": 0, "pip": 0, "instal": 0, "changelog": 0, "gener": [0, 1], "usag": 0, "revers": 0, "f": 0, "release_not": 0, "rst": 0, "t": 0, "precommit": 0, "ensur": 0, "doc": 0, "creation": 0, "ar": [0, 1], "run": [0, 1], "everi": 0, "sphinx": 0, "5": 0, "1": 0, "recommonmark": 0, "all": [0, 1], "file": [0, 1], "http": 0, "thevickypedia": 0, "github": 0, "io": 0, "code": 1, "standard": 1, "releas": 1, "note": 1, "lint": 1, "runbook": 1, "network_monitor": 1, "modul": 1, "supportedmodul": 1, "init": 1, "bool": 1, "true": 1, "noreturn": 1, "monitor": 1, "paramet": 1, "scan": 1, "support": 1, "ani": 1, "take": 1, "boolean": 1, "valu": 1, "creat": 1, "snapshot": 1, "actual": 1, "class": 1, "att": 1, "dictionari": 1, "dict": 1, "convert": 1, "object": 1, "create_snapshot": 1, "json": 1, "which": 1, "us": 1, "determin": 1, "known": 1, "unknown": 1, "format_kei": 1, "kei": 1, "str": 1, "match": 1, "generate_datafram": 1, "datafram": 1, "inform": 1, "from": 1, "web": 1, "page": 1, "return": 1, "list": 1, "data": 1, "frame": 1, "type": 1, "get_attached_devic": 1, "get": 1, "yield": 1, "each": 1, "get_ipaddress": 1, "id": 1, "trigger": 1, "initi": 1, "present": 1, "localipscan": 1, "connector": 1, "same": 1, "rang": 1, "api": 1, "allow": 1, "union": 1, "option": 1, "internet": 1, "access": 1, "name": 1, "an": 1, "argument": 1, "receiv": 1, "get_device_by_nam": 1, "method": 1, "always_allow": 1, "save": 1, "futur": 1, "entri": 1, "fals": 1, "custom_tim": 1, "arg": 1, "formatt": 1, "struct_tim": 1, "custom": 1, "timezon": 1, "log": 1, "when": 1, "invok": 1, "docker": 1, "within": 1, "contain": 1, "utc": 1, "epoch": 1, "time": 1, "pass": 1, "formattim": 1, "A": 1, "tupl": 1, "year": 1, "month": 1, "dai": 1, "hour": 1, "minut": 1, "second": 1, "weekdai": 1, "dst": 1, "daylight": 1, "notifi": 1, "msg": 1, "send": 1, "email": 1, "notif": 1, "threat": 1, "messag": 1, "ha": 1, "sent": 1, "devicestatu": 1, "statu": 1, "string": 1, "config": 1, "wrapper": 1, "environ": 1, "variabl": 1, "fileio": 1, "yaml": 1, "none": 1, "gmail_pass": 1, "gmail_us": 1, "phone": 1, "recipi": 1, "router_pass": 1, "index": 1, "search": 1}, "objects": {"": [[1, 0, 0, "-", "analyzer"]], "analyzer": [[1, 1, 1, "", "network_monitor"]], "modules": [[1, 0, 0, "-", "att"], [1, 0, 0, "-", "helper"], [1, 0, 0, "-", "models"], [1, 0, 0, "-", "netgear"], [1, 0, 0, "-", "settings"]], "modules.att": [[1, 2, 1, "", "Device"], [1, 1, 1, "", "create_snapshot"], [1, 1, 1, "", "format_key"], [1, 1, 1, "", "generate_dataframe"], [1, 1, 1, "", "get_attached_devices"], [1, 1, 1, "", "get_ipaddress"], [1, 1, 1, "", "run"]], "modules.helper": [[1, 1, 1, "", "custom_time"], [1, 1, 1, "", "notify"]], "modules.models": [[1, 2, 1, "", "DeviceStatus"], [1, 2, 1, "", "SupportedModules"]], "modules.models.DeviceStatus": [[1, 3, 1, "", "allow"], [1, 3, 1, "", "block"]], "modules.models.SupportedModules": [[1, 3, 1, "", "att"], [1, 3, 1, "", "netgear"]], "modules.netgear": [[1, 2, 1, "", "LocalIPScan"]], "modules.netgear.LocalIPScan": [[1, 4, 1, "", "allow"], [1, 4, 1, "", "always_allow"], [1, 4, 1, "", "block"], [1, 4, 1, "", "create_snapshot"], [1, 4, 1, "", "run"]], "modules.settings": [[1, 2, 1, "", "Config"]], "modules.settings.Config": [[1, 3, 1, "", "blocked"], [1, 3, 1, "", "docker"], [1, 3, 1, "", "gmail_pass"], [1, 3, 1, "", "gmail_user"], [1, 3, 1, "", "phone"], [1, 3, 1, "", "recipient"], [1, 3, 1, "", "router_pass"], [1, 3, 1, "", "snapshot"]]}, "objtypes": {"0": "py:module", "1": "py:function", "2": "py:class", "3": "py:attribute", "4": "py:method"}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "function", "Python function"], "2": ["py", "class", "Python class"], "3": ["py", "attribute", "Python attribute"], "4": ["py", "method", "Python method"]}, "titleterms": {"netscan": [0, 1], "code": 0, "standard": 0, "releas": 0, "note": 0, "lint": 0, "runbook": 0, "welcom": 1, "": 1, "document": 1, "read": 1, "me": 1, "At": 1, "t": 1, "netgear": 1, "helper": 1, "model": 1, "set": 1, "indic": 1, "tabl": 1}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 6, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 56}})
\ No newline at end of file
diff --git a/doc_generator/Makefile b/docs_gen/Makefile
similarity index 100%
rename from doc_generator/Makefile
rename to docs_gen/Makefile
diff --git a/doc_generator/conf.py b/docs_gen/conf.py
similarity index 100%
rename from doc_generator/conf.py
rename to docs_gen/conf.py
diff --git a/doc_generator/index.rst b/docs_gen/index.rst
similarity index 56%
rename from doc_generator/index.rst
rename to docs_gen/index.rst
index 85bed8e..d788cb7 100644
--- a/doc_generator/index.rst
+++ b/docs_gen/index.rst
@@ -19,6 +19,41 @@ NetScan
:members:
:undoc-members:
+At&t
+====
+
+.. automodule:: modules.att
+ :members:
+ :undoc-members:
+
+Netgear
+=======
+
+.. automodule:: modules.netgear
+ :members:
+ :undoc-members:
+
+Helper
+======
+
+.. automodule:: modules.helper
+ :members:
+ :undoc-members:
+
+Models
+======
+
+.. automodule:: modules.models
+ :members:
+ :undoc-members:
+
+Settings
+========
+
+.. automodule:: modules.settings
+ :members:
+ :undoc-members:
+
Indices and tables
==================
diff --git a/doc_generator/make.bat b/docs_gen/make.bat
similarity index 95%
rename from doc_generator/make.bat
rename to docs_gen/make.bat
index 2119f51..922152e 100644
--- a/doc_generator/make.bat
+++ b/docs_gen/make.bat
@@ -1,35 +1,35 @@
-@ECHO OFF
-
-pushd %~dp0
-
-REM Command file for Sphinx documentation
-
-if "%SPHINXBUILD%" == "" (
- set SPHINXBUILD=sphinx-build
-)
-set SOURCEDIR=.
-set BUILDDIR=_build
-
-if "%1" == "" goto help
-
-%SPHINXBUILD% >NUL 2>NUL
-if errorlevel 9009 (
- echo.
- echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
- echo.installed, then set the SPHINXBUILD environment variable to point
- echo.to the full path of the 'sphinx-build' executable. Alternatively you
- echo.may add the Sphinx directory to PATH.
- echo.
- echo.If you don't have Sphinx installed, grab it from
- echo.http://sphinx-doc.org/
- exit /b 1
-)
-
-%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
-goto end
-
-:help
-%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
-
-:end
-popd
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=.
+set BUILDDIR=_build
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
diff --git a/gen_docs.sh b/gen_docs.sh
index 57b6de7..fe281ef 100644
--- a/gen_docs.sh
+++ b/gen_docs.sh
@@ -4,6 +4,6 @@
set -e
rm -rf docs
mkdir docs
-mkdir -p doc_generator/_static # creates a _static folder if unavailable
-cp README.md doc_generator && cd doc_generator && make clean html && mv _build/html/* ../docs && rm README.md
+mkdir -p docs_gen/_static # creates a _static folder if unavailable
+cp README.md docs_gen && cd docs_gen && make clean html && mv _build/html/* ../docs && rm README.md
touch ../docs/.nojekyll
\ No newline at end of file
diff --git a/modules/att.py b/modules/att.py
new file mode 100644
index 0000000..a0056fb
--- /dev/null
+++ b/modules/att.py
@@ -0,0 +1,144 @@
+import json
+import os
+import socket
+from collections.abc import Generator
+from typing import Any, NoReturn, Optional, Union
+
+import pandas
+import requests
+from pandas import DataFrame
+
+from modules.helper import notify
+from modules.settings import LOGGER, config
+
+SOURCE = "http://{NETWORK_ID}.254/cgi-bin/devices.ha"
+
+
+def get_ipaddress() -> str:
+ """Get network id from the current IP address."""
+ socket_ = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ try:
+ socket_.connect(("8.8.8.8", 80))
+ ip_address = socket_.getsockname()[0]
+ network_id = '.'.join(ip_address.split('.')[0:3])
+ socket_.close()
+ except OSError as error:
+ LOGGER.warning(error)
+ network_id = "192.168.1"
+ return network_id
+
+
+SOURCE = SOURCE.format(NETWORK_ID=get_ipaddress())
+
+
+class Device:
+ """Convert dictionary into a device object.
+
+ >>> Device
+
+ """
+
+ def __init__(self, dictionary: dict):
+ """Set dictionary keys as attributes of Device object.
+
+ Args:
+ dictionary: Takes the input dictionary as an argument.
+ """
+ self.mac_address: Optional[str] = None
+ self.ipv4_address: Optional[str] = None
+ self.name: Optional[str] = None
+ self.last_activity: Optional[str] = None
+ self.status: Optional[str] = None
+ self.allocation: Optional[str] = None
+ self.connection_type: Optional[str] = None
+ self.connection_speed: Optional[Union[float, Any]] = None
+ self.mesh_client: Optional[str] = None
+ for key in dictionary:
+ setattr(self, key, dictionary[key])
+
+
+def generate_dataframe() -> DataFrame:
+ """Generate a dataframe using the devices information from router web page.
+
+ Returns:
+ DataFrame:
+ Devices list as a data frame.
+ """
+ # pandas.set_option('display.max_rows', None)
+ try:
+ response = requests.get(url=SOURCE)
+ except requests.RequestException as error:
+ LOGGER.error(error)
+ raise ConnectionError(error.args)
+ else:
+ if response.ok:
+ html_source = response.text
+ html_tables = pandas.read_html(html_source)
+ return html_tables[0]
+ else:
+ LOGGER.error("[%s] - %s" % (response.status_code, response.text))
+
+
+def format_key(key: str) -> str:
+ """Format the key to match the Device object."""
+ return key.lower().replace(' ', '_').replace('-', '_')
+
+
+def get_attached_devices() -> Generator[Device]:
+ """Get all devices connected to the router.
+
+ Yields:
+ Generator[Device]:
+ Yields each device information as a Device object.
+ """
+ device_info = {}
+ dataframe = generate_dataframe()
+ if dataframe is None:
+ return
+ for value in dataframe.values:
+ if str(value[0]) == "nan":
+ yield Device(device_info)
+ device_info = {}
+ elif value[0] == "IPv4 Address / Name":
+ key = value[0].split('/')
+ val = value[1].split('/')
+ device_info[format_key(key[0].strip())] = val[0].strip()
+ device_info[format_key(key[1].strip())] = val[1].strip()
+ else:
+ device_info[format_key(value[0])] = value[1]
+
+
+def create_snapshot() -> NoReturn:
+ """Creates a snapshot.json which is used to determine the known and unknown devices."""
+ devices = {}
+ for device in get_attached_devices():
+ if device.ipv4_address:
+ devices[device.ipv4_address] = [str(device.name), str(device.connection_type), str(device.last_activity)]
+ LOGGER.info('Number of devices connected: %d' % len(devices.keys()))
+ with open(config.snapshot, 'w') as file:
+ json.dump(devices, file, indent=2)
+
+
+def run() -> NoReturn:
+ """Trigger to initiate a Network Scan and block the devices that are not present in ``snapshot.json`` file."""
+ if not os.path.isfile(config.snapshot):
+ LOGGER.error("'%s' not found. Please run `create_snapshot()` and review it." % config.snapshot)
+ raise FileNotFoundError(
+ "'%s' is required" % config.snapshot
+ )
+ with open(config.snapshot) as file:
+ device_list = json.load(file)
+ stored_ips = list(device_list.keys())
+ threat = ''
+ for device in get_attached_devices():
+ if device.ipv4_address and device.ipv4_address not in stored_ips:
+ # SOURCE = "http://{NETWORK_ID}.254/cgi-bin/devices.ha"
+ LOGGER.warning('{name} [{ip}: {mac}] is connected to your network.'.format(name=device.name,
+ mac=device.mac_address,
+ ip=device.ipv4_address))
+ threat += '\nName: {name}\nIP: {ip}\nMAC: {mac}'.format(name=device.name, mac=device.mac_address,
+ ip=device.ipv4_address)
+ if threat:
+ notify(msg=threat)
+ else:
+ LOGGER.info('NetScan has completed. No threats found on your network.')
diff --git a/modules/helper.py b/modules/helper.py
new file mode 100644
index 0000000..52331ee
--- /dev/null
+++ b/modules/helper.py
@@ -0,0 +1,49 @@
+import logging
+import time
+from datetime import datetime, timezone
+from typing import NoReturn
+
+import gmailconnector
+
+from modules.settings import LOGGER, config
+
+
+def custom_time(*args: logging.Formatter or time.time) -> time.struct_time:
+ """Creates custom timezone for ``logging`` which gets used only when invoked by ``Docker``.
+
+ This is used only when triggered within a ``docker container`` as it uses UTC timezone.
+
+ Args:
+ *args: Takes ``Formatter`` object and current epoch time as arguments passed by ``formatTime`` from ``logging``.
+
+ Returns:
+ struct_time:
+ A struct_time object which is a tuple of:
+ **current year, month, day, hour, minute, second, weekday, year day and dst** *(Daylight Saving Time)*
+ """
+ LOGGER.debug(args)
+ local_timezone = datetime.now(tz=timezone.utc).astimezone().tzinfo
+ return datetime.now().astimezone(tz=local_timezone).timetuple()
+
+
+if config.docker:
+ logging.Formatter.converter = custom_time
+
+
+def notify(msg: str) -> NoReturn:
+ """Send an email notification when there is a threat.
+
+ Args:
+ msg: Message that has to be sent.
+ """
+ if config.gmail_user and config.gmail_pass and config.recipient:
+ emailer = gmailconnector.SendEmail(gmail_user=config.gmail_user,
+ gmail_pass=config.gmail_pass)
+ response = emailer.send_email(recipient=config.recipient,
+ subject=f"Netscan Alert - {datetime.now().strftime('%C')}", body=msg)
+ if response.ok:
+ LOGGER.info("Firewall alert has been sent to '%s'" % config.phone)
+ else:
+ LOGGER.error("Failed to send a notification.\n%s" % response.body)
+ else:
+ LOGGER.info("Env variables not found to trigger notification.")
diff --git a/modules/models.py b/modules/models.py
new file mode 100644
index 0000000..89fb964
--- /dev/null
+++ b/modules/models.py
@@ -0,0 +1,15 @@
+from enum import Enum
+
+
+class DeviceStatus(str, Enum):
+ """Device status strings for allow or block."""
+
+ allow: str = "Allow"
+ block: str = "Block"
+
+
+class SupportedModules(str, Enum):
+ """Supported modules are At&t and Netgear."""
+
+ att: str = "At&t"
+ netgear: str = "Netgear"
diff --git a/modules/netgear.py b/modules/netgear.py
new file mode 100644
index 0000000..7cabbd2
--- /dev/null
+++ b/modules/netgear.py
@@ -0,0 +1,211 @@
+import copy
+import json
+import os
+import time
+from typing import NoReturn, Union
+
+import yaml
+from pynetgear import Device, Netgear
+
+from modules.helper import notify
+from modules.models import DeviceStatus
+from modules.settings import LOGGER, config
+
+
+class LocalIPScan:
+ """Connector to scan devices in the same IP range using ``Netgear API``.
+
+ >>> LocalIPScan
+
+ """
+
+ def __init__(self):
+ """Gets local host devices connected to the same network range."""
+ self.netgear = Netgear(password=config.router_pass)
+
+ def _get_devices(self) -> Device:
+ """Scans the Netgear router for connected devices and the devices' information.
+
+ Returns:
+ Device:
+ Returns list of devices connected to the router and the connection information.
+ """
+ LOGGER.info('Getting devices connected to your network.')
+ if devices := self.netgear.get_attached_devices():
+ return devices
+ else:
+ text = "'router_pass' is invalid" if config.router_pass else "'router_pass' is required for netgear network"
+ raise ValueError("\n\n" + text)
+
+ def create_snapshot(self) -> NoReturn:
+ """Creates a snapshot.json which is used to determine the known and unknown devices."""
+ LOGGER.warning("Creating a snapshot will capture the current list of devices connected to your network at"
+ " this moment.")
+ LOGGER.warning("This capture will be used to alert/block when new devices are connected. So, please review "
+ "the '%s' manually and remove the devices that aren't recognized." % config.snapshot)
+ devices = {}
+ for device in self._get_devices():
+ if device.ip: # Only look for currently connected devices
+ devices[device.ip] = [device.name, device.type, device.allow_or_block]
+ LOGGER.info('Number of devices connected: %d' % len(devices.keys()))
+ with open(config.snapshot, 'w') as file:
+ json.dump(devices, file, indent=2)
+
+ def _get_device_by_name(self, name: str) -> Device:
+ """Calls the ``get_devices()`` method and checks if the given device is available in the list.
+
+ Args:
+ name: Takes device name as argument.
+
+ Returns:
+ Device:
+ Returns device information as a Device object.
+ """
+ for device in self._get_devices():
+ if device.name == name:
+ return device
+
+ def _get_device_obj(self, device: str):
+ """Identify device object using the device name as a string."""
+ if isinstance(device, str):
+ tmp = device
+ LOGGER.info('Looking information on %s' % device)
+ if device := self._get_device_by_name(name=device):
+ return device
+ else:
+ LOGGER.error('Device: %s is not connected to your network.' % tmp)
+ else:
+ return device
+
+ def allow(self, device: Union[str, Device]) -> Union[Device, None]:
+ """Allows internet access to a device.
+
+ Args:
+ device: Takes device name or Device object as an argument.
+
+ Returns:
+ Device:
+ Returns the device object received from ``get_device_by_name()`` method.
+ """
+ if device := self._get_device_obj(device=device):
+ LOGGER.info("Granting internet access to '%s'" % device.name)
+ self.netgear.allow_block_device(mac_addr=device.mac, device_status=DeviceStatus.allow)
+ return device
+
+ def block(self, device: Union[str, Device]) -> Union[Device, None]:
+ """Blocks internet access to a device.
+
+ Args:
+ device: Takes device name or Device object as an argument.
+
+ Returns:
+ Device:
+ Returns the device object received from ``get_device_by_name()`` method.
+ """
+ device = self._get_device_obj(device=device)
+ LOGGER.info("Blocking internet access to '%s'" % device.name)
+ self.netgear.allow_block_device(mac_addr=device.mac, device_status=DeviceStatus.block)
+ return device
+
+ @staticmethod
+ def _dump_blocked(device: Device) -> NoReturn:
+ """Converts device object to a dictionary and dumps it into ``blocked.json`` file.
+
+ Args:
+ device: Takes Device object as an argument.
+ """
+ LOGGER.info("Details of '%s' has been stored in %s" % (device.name, config.blocked))
+ with open(config.blocked, 'a') as file:
+ # noinspection PyProtectedMember
+ dictionary = {time.time(): device._asdict()}
+ yaml.dump(dictionary, file, allow_unicode=True, default_flow_style=False, sort_keys=False)
+
+ @staticmethod
+ def _get_blocked():
+ if os.path.isfile(config.blocked):
+ with open(config.blocked) as file:
+ try:
+ blocked_devices = yaml.load(stream=file, Loader=yaml.FullLoader) or {}
+ except yaml.YAMLError as error:
+ LOGGER.error(error)
+ else:
+ for epoch, device_info in blocked_devices.items():
+ yield device_info.get('mac')
+
+ def always_allow(self, device: Device or str) -> NoReturn:
+ """Allows internet access to a device.
+
+ Saves the device name to ``snapshot.json`` to not block in the future.
+ Removes the device name from ``blocked.json`` if an entry is present.
+
+ Args:
+ device: Takes device name or Device object as an argument
+ """
+ if isinstance(device, Device):
+ device = device.name # converts Device object to string
+ if not (device := self.allow(device=device)): # converts string to Device object
+ return
+
+ with open(config.snapshot, 'r+') as file:
+ data = json.load(file)
+ file.seek(0)
+ if device.ip and device.ip in list(data.keys()):
+ LOGGER.info("'%s' is a part of allow list." % device.name)
+ data[device.ip][-1] = DeviceStatus.allow
+ LOGGER.info("Setting status to Allow for '%s' in %s" % (device.name, config.snapshot))
+ elif device.ip:
+ data[device.ip] = [device.name, device.type, device.allow_or_block]
+ LOGGER.info("Adding '%s' to %s" % (device.name, config.snapshot))
+ json.dump(data, file, indent=2)
+ file.truncate()
+
+ if os.path.isfile(config.blocked):
+ with open(config.blocked) as file:
+ try:
+ blocked_devices = yaml.load(stream=file, Loader=yaml.FullLoader) or {}
+ except yaml.YAMLError as error:
+ LOGGER.error(error)
+ return
+ blocked_copy = copy.deepcopy(blocked_devices)
+ for epoch, device_info in blocked_copy.items(): # convert to a list of dict
+ if device_info.get('mac') == device.mac:
+ LOGGER.info("Removing '%s' from %s" % (device.name, config.blocked))
+ del blocked_devices[epoch]
+ file.seek(0)
+ file.truncate()
+ if blocked_devices:
+ yaml.dump(blocked_devices, file, indent=2)
+
+ def run(self, block: bool = False) -> NoReturn:
+ """Trigger to initiate a Network Scan and block the devices that are not present in ``snapshot.json`` file."""
+ if not os.path.isfile(config.snapshot):
+ LOGGER.error("'%s' not found. Please generate one and review it." % config.snapshot)
+ raise FileNotFoundError(
+ '%s is required' % config.snapshot
+ )
+ with open(config.snapshot) as file:
+ device_list = json.load(file)
+ stored_ips = list(device_list.keys())
+ threat = ''
+ blocked = list(self._get_blocked())
+ for device in self._get_devices():
+ if device.ip and device.ip not in stored_ips:
+ LOGGER.warning("{name} with MAC address {mac} and a signal strength of {signal}% has connected to your "
+ "network.".format(name=device.name, mac=device.mac, signal=device.signal))
+
+ if device.allow_or_block == DeviceStatus.allow:
+ if block:
+ self.block(device=device)
+ if device.mac not in blocked:
+ self._dump_blocked(device=device)
+ else:
+ LOGGER.info("'%s' is a part of deny list." % device.name)
+ threat += '\nName: {name}\nIP: {ip}\nMAC: {mac}'.format(name=device.name, ip=device.ip,
+ mac=device.mac)
+ else:
+ LOGGER.info("'%s' does not have internet access." % device.name)
+
+ if threat:
+ notify(msg=threat)
+ else:
+ LOGGER.info('NetScan has completed. No threats found on your network.')
diff --git a/modules/settings.py b/modules/settings.py
new file mode 100644
index 0000000..24e1990
--- /dev/null
+++ b/modules/settings.py
@@ -0,0 +1,34 @@
+import logging
+import os
+
+import dotenv
+
+dotenv.load_dotenv(dotenv_path=".env")
+
+if not os.path.isdir('fileio'):
+ os.makedirs('fileio')
+
+LOGGER = logging.getLogger(__name__)
+handler = logging.StreamHandler()
+handler.setFormatter(fmt=logging.Formatter(
+ fmt="%(asctime)s - [%(levelname)s] - %(name)s - %(funcName)s - Line: %(lineno)d - %(message)s",
+ datefmt='%b-%d-%Y %H:%M:%S'
+))
+LOGGER.setLevel(level=logging.DEBUG)
+LOGGER.addHandler(hdlr=handler)
+
+
+class Config:
+ """Wrapper for all the environment variables."""
+
+ router_pass = os.environ.get('ROUTER_PASS') or os.environ.get('router_pass')
+ gmail_user = os.environ.get('GMAIL_USER') or os.environ.get('gmail_user')
+ gmail_pass = os.environ.get('GMAIL_PASS') or os.environ.get('gmail_pass')
+ recipient = os.environ.get('RECIPIENT') or os.environ.get('recipient')
+ docker = os.environ.get('DOCKER') or os.environ.get('docker')
+ phone = os.environ.get('PHONE') or os.environ.get('phone')
+ snapshot = os.path.join('fileio', 'snapshot.json')
+ blocked = os.path.join('fileio', 'blocked.yaml')
+
+
+config = Config()
diff --git a/release_notes.rst b/release_notes.rst
new file mode 100644
index 0000000..93845e7
--- /dev/null
+++ b/release_notes.rst
@@ -0,0 +1,66 @@
+Release Notes
+=============
+
+0.1.5 (04/16/2022)
+------------------
+- Update docstrings
+
+0.1.4 (04/16/2022)
+------------------
+- Make snapshot a JSON file instead of yaml
+- Filter block devices more appropriately
+- Change timezone conversion method
+
+0.1.3 (11/14/2021)
+------------------
+- Upgrade `gmailconnector` version
+- Auto create `_static` directory if unavailable
+
+0.1.2 (08/27/2021)
+------------------
+- Update `timezone` when invoked by `docker build`
+
+0.1.1 (08/26/2021)
+------------------
+- Add Dockerfile and update README.md
+
+0.1.0 (08/26/2021)
+------------------
+- Add notifications when a threat is noticed on the connected SSID
+
+0.0.9 (08/26/2021)
+------------------
+- Onboard sphinx autodocs
+- Update README.md and docstrings
+
+0.0.8 (08/25/2021)
+------------------
+- update README.md and remove first person references
+
+0.0.7 (08/25/2021)
+------------------
+- add an option to always allow an unknown device
+
+0.0.6 (08/25/2021)
+------------------
+- add `docstrings`
+
+0.0.5 (08/25/2021)
+------------------
+- Add `ssid` and `logger`
+
+0.0.4 (08/25/2021)
+------------------
+- Create a `LocalIPScan` to analyzer network and block devices
+
+0.0.3 (08/25/2021)
+------------------
+- add requirements.txt
+
+0.0.2 (08/25/2021)
+------------------
+- update README.md and add .gitignore
+
+0.0.1 (08/23/2021)
+------------------
+- Initial commit
diff --git a/requirements.txt b/requirements.txt
index b8e3944..4fc25dc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,8 +1,8 @@
-gmail-connector
-pynetgear==0.7.0
-python_dotenv==0.20.0
-PyYAML==5.4.1
-pytz==2021.1
-Sphinx==4.1.2
-recommonmark==0.7.1
-pre-commit==2.14.0
\ No newline at end of file
+pynetgear==0.10.9
+python-dotenv
+pytz
+PyYAML
+requests
+pandas
+lxml
+gmail-connector
\ No newline at end of file