From b2e92b1d100e7bec198d82a978200e80bbb2037c Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Fri, 5 Jul 2019 19:14:23 +0700 Subject: [PATCH 001/191] Import the redesigned AnnounceShare under the new name TrackerShare --- plugins/TrackerShare/Test/TestTrackerShare.py | 24 ++ plugins/TrackerShare/Test/conftest.py | 3 + plugins/TrackerShare/Test/pytest.ini | 5 + plugins/TrackerShare/TrackerSharePlugin.py | 363 ++++++++++++++++++ plugins/TrackerShare/__init__.py | 1 + 5 files changed, 396 insertions(+) create mode 100644 plugins/TrackerShare/Test/TestTrackerShare.py create mode 100644 plugins/TrackerShare/Test/conftest.py create mode 100644 plugins/TrackerShare/Test/pytest.ini create mode 100644 plugins/TrackerShare/TrackerSharePlugin.py create mode 100644 plugins/TrackerShare/__init__.py diff --git a/plugins/TrackerShare/Test/TestTrackerShare.py b/plugins/TrackerShare/Test/TestTrackerShare.py new file mode 100644 index 00000000..391f2009 --- /dev/null +++ b/plugins/TrackerShare/Test/TestTrackerShare.py @@ -0,0 +1,24 @@ +import pytest + +from TrackerShare import TrackerSharePlugin +from Peer import Peer +from Config import config + + +@pytest.mark.usefixtures("resetSettings") +@pytest.mark.usefixtures("resetTempSettings") +class TestTrackerShare: + def testAnnounceList(self, file_server): + open("%s/trackers.json" % config.data_dir, "w").write("{}") + tracker_storage = TrackerSharePlugin.tracker_storage + tracker_storage.load() + peer = Peer(file_server.ip, 1544, connection_server=file_server) + assert peer.request("getTrackers")["trackers"] == [] + + tracker_storage.onTrackerFound("zero://%s:15441" % file_server.ip) + assert peer.request("getTrackers")["trackers"] == [] + + # It needs to have at least one successfull announce to be shared to other peers + tracker_storage.onTrackerSuccess("zero://%s:15441" % file_server.ip, 1.0) + assert peer.request("getTrackers")["trackers"] == ["zero://%s:15441" % file_server.ip] + diff --git a/plugins/TrackerShare/Test/conftest.py b/plugins/TrackerShare/Test/conftest.py new file mode 100644 index 00000000..5abd4dd6 --- /dev/null +++ b/plugins/TrackerShare/Test/conftest.py @@ -0,0 +1,3 @@ +from src.Test.conftest import * + +from Config import config diff --git a/plugins/TrackerShare/Test/pytest.ini b/plugins/TrackerShare/Test/pytest.ini new file mode 100644 index 00000000..d09210d1 --- /dev/null +++ b/plugins/TrackerShare/Test/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +python_files = Test*.py +addopts = -rsxX -v --durations=6 +markers = + webtest: mark a test as a webtest. \ No newline at end of file diff --git a/plugins/TrackerShare/TrackerSharePlugin.py b/plugins/TrackerShare/TrackerSharePlugin.py new file mode 100644 index 00000000..244b0be5 --- /dev/null +++ b/plugins/TrackerShare/TrackerSharePlugin.py @@ -0,0 +1,363 @@ +import random +import time +import os +import logging +import json +import atexit +import re + +import gevent + +from Config import config +from Plugin import PluginManager +from util import helper + + +class TrackerStorage(object): + def __init__(self): + self.site_announcer = None + self.log = logging.getLogger("TrackerStorage") + + self.working_tracker_time_interval = 60 * 60 + self.tracker_down_time_interval = 60 * 60 + self.tracker_discover_time_interval = 5 * 60 + + self.working_shared_trackers_limit_per_protocol = {} + self.working_shared_trackers_limit_per_protocol["other"] = 2 + + self.file_path = "%s/trackers.json" % config.data_dir + self.load() + self.time_discover = 0.0 + self.time_success = 0.0 + atexit.register(self.save) + + def initTrackerLimitForProtocol(self): + for s in re.split("[,;]", config.working_shared_trackers_limit_per_protocol): + x = s.split("=", 1) + if len(x) == 1: + x = ["other", x[0]] + try: + self.working_shared_trackers_limit_per_protocol[x[0]] = int(x[1]) + except ValueError: + pass + self.log.debug("Limits per protocol: %s" % self.working_shared_trackers_limit_per_protocol) + + def getTrackerLimitForProtocol(self, protocol): + l = self.working_shared_trackers_limit_per_protocol + return l.get(protocol, l.get("other")) + + def setSiteAnnouncer(self, site_announcer): + if self.site_announcer: + return + self.site_announcer = site_announcer + self.initTrackerLimitForProtocol() + self.recheckValidTrackers() + + def isTrackerAddressValid(self, tracker_address): + if not self.site_announcer: # Not completely initialized, skip check + return True + + address_parts = self.site_announcer.getAddressParts(tracker_address) + if not address_parts: + self.log.debug("Invalid tracker address: %s" % tracker_address) + return False + + handler = self.site_announcer.getTrackerHandler(address_parts["protocol"]) + if not handler: + self.log.debug("Invalid tracker address: Unknown protocol %s: %s" % (address_parts["protocol"], tracker_address)) + return False + + return True + + def recheckValidTrackers(self): + trackers = self.getTrackers() + for address, tracker in list(trackers.items()): + if not self.isTrackerAddressValid(address): + del trackers[address] + + def getNormalizedTrackerProtocol(self, tracker_address): + if not self.site_announcer: + return None + + address_parts = self.site_announcer.getAddressParts(tracker_address) + if not address_parts: + return None + + protocol = address_parts["protocol"] + if protocol == "https": + protocol = "http" + + return protocol + + def getSupportedProtocols(self): + if not self.site_announcer: + return None + + supported_trackers = self.site_announcer.getSupportedTrackers() + + # If a tracker is in our list, but is absent from the results of getSupportedTrackers(), + # it seems to be supported by software, but forbidden by the settings or network configuration. + # We check and remove thoose trackers here, since onTrackerError() is never emitted for them. + trackers = self.getTrackers() + for tracker_address, tracker in list(trackers.items()): + t = max(trackers[tracker_address]["time_added"], + trackers[tracker_address]["time_success"]) + if tracker_address not in supported_trackers and t < time.time() - self.tracker_down_time_interval: + self.log.debug("Tracker %s looks unused, removing." % tracker_address) + del trackers[tracker_address] + + protocols = set() + for tracker_address in supported_trackers: + protocol = self.getNormalizedTrackerProtocol(tracker_address) + if not protocol: + continue + protocols.add(protocol) + + protocols = list(protocols) + + self.log.debug("Supported tracker protocols: %s" % protocols) + + return protocols + + def getDefaultFile(self): + return {"shared": {}} + + def onTrackerFound(self, tracker_address, type="shared", my=False): + if not self.isTrackerAddressValid(tracker_address): + return False + + trackers = self.getTrackers(type) + added = False + if tracker_address not in trackers: + trackers[tracker_address] = { + "time_added": time.time(), + "time_success": 0, + "latency": 99.0, + "num_error": 0, + "my": False + } + self.log.debug("New tracker found: %s" % tracker_address) + added = True + + trackers[tracker_address]["time_found"] = time.time() + trackers[tracker_address]["my"] = my + return added + + def onTrackerSuccess(self, tracker_address, latency): + trackers = self.getTrackers() + if tracker_address not in trackers: + return False + + trackers[tracker_address]["latency"] = latency + trackers[tracker_address]["time_success"] = time.time() + trackers[tracker_address]["num_error"] = 0 + + self.time_success = time.time() + + def onTrackerError(self, tracker_address): + trackers = self.getTrackers() + if tracker_address not in trackers: + return False + + trackers[tracker_address]["time_error"] = time.time() + trackers[tracker_address]["num_error"] += 1 + + if self.time_success < time.time() - self.tracker_down_time_interval / 2: + # Don't drop trackers from the list, if there haven't been any successful announces recently. + # There may be network connectivity issues. + return + + protocol = self.getNormalizedTrackerProtocol(tracker_address) or "" + + nr_working_trackers_for_protocol = len(self.getTrackersPerProtocol(working_only=True).get(protocol, [])) + nr_working_trackers = len(self.getWorkingTrackers()) + + error_limit = 30 + if nr_working_trackers_for_protocol >= self.getTrackerLimitForProtocol(protocol): + error_limit = 10 + if nr_working_trackers >= config.working_shared_trackers_limit: + error_limit = 5 + + if trackers[tracker_address]["num_error"] > error_limit and trackers[tracker_address]["time_success"] < time.time() - self.tracker_down_time_interval: + self.log.debug("Tracker %s looks down, removing." % tracker_address) + del trackers[tracker_address] + + def isTrackerWorking(self, tracker_address, type="shared"): + trackers = self.getTrackers(type) + tracker = trackers[tracker_address] + if not tracker: + return False + + if tracker["time_success"] > time.time() - self.working_tracker_time_interval: + return True + + return False + + def getTrackers(self, type="shared"): + return self.file_content.setdefault(type, {}) + + def getTrackersPerProtocol(self, type="shared", working_only=False): + if not self.site_announcer: + return None + + trackers = self.getTrackers(type) + + trackers_per_protocol = {} + for tracker_address in trackers: + protocol = self.getNormalizedTrackerProtocol(tracker_address) + if not protocol: + continue + if not working_only or self.isTrackerWorking(tracker_address, type=type): + trackers_per_protocol.setdefault(protocol, []).append(tracker_address) + + return trackers_per_protocol + + def getWorkingTrackers(self, type="shared"): + trackers = { + key: tracker for key, tracker in self.getTrackers(type).items() + if self.isTrackerWorking(key, type) + } + return trackers + + def getFileContent(self): + if not os.path.isfile(self.file_path): + open(self.file_path, "w").write("{}") + return self.getDefaultFile() + try: + return json.load(open(self.file_path)) + except Exception as err: + self.log.error("Error loading trackers list: %s" % err) + return self.getDefaultFile() + + def load(self): + self.file_content = self.getFileContent() + + trackers = self.getTrackers() + self.log.debug("Loaded %s shared trackers" % len(trackers)) + for address, tracker in list(trackers.items()): + tracker["num_error"] = 0 + self.recheckValidTrackers() + + def save(self): + s = time.time() + helper.atomicWrite(self.file_path, json.dumps(self.file_content, indent=2, sort_keys=True).encode("utf8")) + self.log.debug("Saved in %.3fs" % (time.time() - s)) + + def enoughWorkingTrackers(self, type="shared"): + supported_protocols = self.getSupportedProtocols() + if not supported_protocols: + return False + + trackers_per_protocol = self.getTrackersPerProtocol(type="shared", working_only=True) + if not trackers_per_protocol: + return False + + total_nr = 0 + + for protocol in supported_protocols: + trackers = trackers_per_protocol.get(protocol, []) + if len(trackers) < self.getTrackerLimitForProtocol(protocol): + self.log.debug("Not enough working trackers for protocol %s: %s < %s" % ( + protocol, len(trackers), self.getTrackerLimitForProtocol(protocol))) + return False + total_nr += len(trackers) + + if total_nr < config.working_shared_trackers_limit: + self.log.debug("Not enough working trackers: %s < %s" % ( + total_nr, config.working_shared_trackers_limit)) + return False + + return True + + def discoverTrackers(self, peers): + if self.enoughWorkingTrackers(type="shared"): + return False + + self.log.debug("Discovering trackers from %s peers..." % len(peers)) + + s = time.time() + num_success = 0 + for peer in peers: + if peer.connection and peer.connection.handshake.get("rev", 0) < 3560: + continue # Not supported + + res = peer.request("getTrackers") + if not res or "error" in res: + continue + + num_success += 1 + + random.shuffle(res["trackers"]) + for tracker_address in res["trackers"]: + if type(tracker_address) is bytes: # Backward compatibilitys + tracker_address = tracker_address.decode("utf8") + added = self.onTrackerFound(tracker_address) + if added: # Only add one tracker from one source + break + + if not num_success and len(peers) < 20: + self.time_discover = 0.0 + + if num_success: + self.save() + + self.log.debug("Trackers discovered from %s/%s peers in %.3fs" % (num_success, len(peers), time.time() - s)) + + +if "tracker_storage" not in locals(): + tracker_storage = TrackerStorage() + + +@PluginManager.registerTo("SiteAnnouncer") +class SiteAnnouncerPlugin(object): + def getTrackers(self): + tracker_storage.setSiteAnnouncer(self) + if tracker_storage.time_discover < time.time() - tracker_storage.tracker_discover_time_interval: + tracker_storage.time_discover = time.time() + gevent.spawn(tracker_storage.discoverTrackers, self.site.getConnectedPeers()) + trackers = super(SiteAnnouncerPlugin, self).getTrackers() + shared_trackers = list(tracker_storage.getTrackers("shared").keys()) + if shared_trackers: + return trackers + shared_trackers + else: + return trackers + + def announceTracker(self, tracker, *args, **kwargs): + tracker_storage.setSiteAnnouncer(self) + res = super(SiteAnnouncerPlugin, self).announceTracker(tracker, *args, **kwargs) + if res: + latency = res + tracker_storage.onTrackerSuccess(tracker, latency) + elif res is False: + tracker_storage.onTrackerError(tracker) + + return res + + +@PluginManager.registerTo("FileRequest") +class FileRequestPlugin(object): + def actionGetTrackers(self, params): + shared_trackers = list(tracker_storage.getWorkingTrackers("shared").keys()) + random.shuffle(shared_trackers) + self.response({"trackers": shared_trackers}) + + +@PluginManager.registerTo("FileServer") +class FileServerPlugin(object): + def portCheck(self, *args, **kwargs): + res = super(FileServerPlugin, self).portCheck(*args, **kwargs) + if res and not config.tor == "always" and "Bootstrapper" in PluginManager.plugin_manager.plugin_names: + for ip in self.ip_external_list: + my_tracker_address = "zero://%s:%s" % (ip, config.fileserver_port) + tracker_storage.onTrackerFound(my_tracker_address, my=True) + return res + + +@PluginManager.registerTo("ConfigPlugin") +class ConfigPlugin(object): + def createArguments(self): + group = self.parser.add_argument_group("TrackerShare plugin") + group.add_argument('--working_shared_trackers_limit', help='Stop discovering new shared trackers after this number of shared trackers reached (total)', default=10, type=int, metavar='limit') + group.add_argument('--working_shared_trackers_limit_per_protocol', help='Stop discovering new shared trackers after this number of shared trackers reached per each supported protocol', default="zero=5,other=2", metavar='limit') + + return super(ConfigPlugin, self).createArguments() diff --git a/plugins/TrackerShare/__init__.py b/plugins/TrackerShare/__init__.py new file mode 100644 index 00000000..0cc19c27 --- /dev/null +++ b/plugins/TrackerShare/__init__.py @@ -0,0 +1 @@ +from . import TrackerSharePlugin From 3910338b28b1ad300c4f6bdfab55a2feb186a7c5 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Fri, 5 Jul 2019 19:16:25 +0700 Subject: [PATCH 002/191] Import plugin: TrackerList --- .../disabled-TrackerList/TrackerListPlugin.py | 107 ++++++++++++++++++ plugins/disabled-TrackerList/__init__.py | 1 + 2 files changed, 108 insertions(+) create mode 100644 plugins/disabled-TrackerList/TrackerListPlugin.py create mode 100644 plugins/disabled-TrackerList/__init__.py diff --git a/plugins/disabled-TrackerList/TrackerListPlugin.py b/plugins/disabled-TrackerList/TrackerListPlugin.py new file mode 100644 index 00000000..c1ced5e1 --- /dev/null +++ b/plugins/disabled-TrackerList/TrackerListPlugin.py @@ -0,0 +1,107 @@ +import time +import os +import logging +import json +import atexit +import re + +import gevent + +from Config import config +from Debug import Debug +from Plugin import PluginManager +from util import helper + +class TrackerList(object): + def __init__(self): + self.log = logging.getLogger("TrackerList") + self.tracker_storage = None + self.last_rescan_time = 0.0 + self.last_rescan_failed = False + + def parse_list(self, data): + for line in data.splitlines(): + line = line.strip() + + if not line: + continue + + if re.match("^udp://", line): + line = re.sub("/announce$", "", line) + + if self.tracker_storage.onTrackerFound(line): + self.log.info("Added tracker: %s" % line) + + def do_rescan(self): + url = config.tracker_list_url + response = None + + self.log.info("Rescanning: %s" % url) + + try: + # FIXME: add support of reading from ZeroNet URLs + if re.match("^http(s)?://", url): + req = helper.httpRequest(url) + response = req.read().decode("utf8") + req.close() + req = None + else: + response = open(url, 'r').read().decode("utf8") + except Exception as err: + self.log.error("Error reading %s: %s" % (url, err)) + self.last_rescan_failed = True + + if response: + self.parse_list(response); + self.last_rescan_failed = False + + def reload(self): + if "AnnounceShare" not in PluginManager.plugin_manager.plugin_names: + return + + rescan_interval = config.tracker_list_rescan_interval + if self.last_rescan_failed: + rescan_interval = (rescan_interval / 2, 60) + + if self.last_rescan_time > time.time() - rescan_interval: + return + + self.last_rescan_time = time.time() + + try: + if "tracker_storage" not in locals(): + from AnnounceShare.AnnounceSharePlugin import tracker_storage + self.tracker_storage = tracker_storage + if self.tracker_storage: + gevent.spawn(self.do_rescan) + except Exception as err: + self.log.error("%s" % Debug.formatException(err)) + + +if "tracker_list" not in locals(): + tracker_list = TrackerList() + + +@PluginManager.registerTo("SiteAnnouncer") +class SiteAnnouncerPlugin(object): + def announceTracker(self, tracker, *args, **kwargs): + tracker_list.reload() + return super(SiteAnnouncerPlugin, self).announceTracker(tracker, *args, **kwargs) + + +@PluginManager.registerTo("FileServer") +class FileServerPlugin(object): + def portCheck(self, *args, **kwargs): + res = super(FileServerPlugin, self).portCheck(*args, **kwargs) + tracker_list.reload() + return res + + +@PluginManager.registerTo("ConfigPlugin") +class ConfigPlugin(object): + def createArguments(self): + group = self.parser.add_argument_group("TrackerList plugin") + group.add_argument('--tracker_list_url', help='URL of local file path, where the list of additional trackers is located', default='https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_ip.txt', metavar='url') + group.add_argument('--tracker_list_rescan_interval', help='Interval in seconds between rescans of the list of additional trackers', default=60 * 60, type=int, metavar='interval') + + return super(ConfigPlugin, self).createArguments() diff --git a/plugins/disabled-TrackerList/__init__.py b/plugins/disabled-TrackerList/__init__.py new file mode 100644 index 00000000..d834cf6b --- /dev/null +++ b/plugins/disabled-TrackerList/__init__.py @@ -0,0 +1 @@ +from . import TrackerListPlugin From 84526a6657021db0c35b50e581a4978f70bebfca Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Fri, 5 Jul 2019 19:28:03 +0700 Subject: [PATCH 003/191] TrackerShare: raise the default limit per protocol limit from 2 to 4 --- plugins/TrackerShare/TrackerSharePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/TrackerShare/TrackerSharePlugin.py b/plugins/TrackerShare/TrackerSharePlugin.py index 244b0be5..45b4aea1 100644 --- a/plugins/TrackerShare/TrackerSharePlugin.py +++ b/plugins/TrackerShare/TrackerSharePlugin.py @@ -358,6 +358,6 @@ class ConfigPlugin(object): def createArguments(self): group = self.parser.add_argument_group("TrackerShare plugin") group.add_argument('--working_shared_trackers_limit', help='Stop discovering new shared trackers after this number of shared trackers reached (total)', default=10, type=int, metavar='limit') - group.add_argument('--working_shared_trackers_limit_per_protocol', help='Stop discovering new shared trackers after this number of shared trackers reached per each supported protocol', default="zero=5,other=2", metavar='limit') + group.add_argument('--working_shared_trackers_limit_per_protocol', help='Stop discovering new shared trackers after this number of shared trackers reached per each supported protocol', default="zero=5,other=4", metavar='limit') return super(ConfigPlugin, self).createArguments() From 33c81a89e99991c783a7e870f15f73a16ac45b4a Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Fri, 5 Jul 2019 19:38:00 +0700 Subject: [PATCH 004/191] TrackerShare: rename the config arguments to avoid the name clash with AnnounceShare's arguments --working_shared_trackers_limit -> --shared_trackers_limit --working_shared_trackers_limit_per_protocol -> --shared_trackers_limit_per_protocol Also modify the help messages so that they were more consistent with how the code really works. --- plugins/TrackerShare/TrackerSharePlugin.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/plugins/TrackerShare/TrackerSharePlugin.py b/plugins/TrackerShare/TrackerSharePlugin.py index 45b4aea1..6b32c4e2 100644 --- a/plugins/TrackerShare/TrackerSharePlugin.py +++ b/plugins/TrackerShare/TrackerSharePlugin.py @@ -22,8 +22,8 @@ class TrackerStorage(object): self.tracker_down_time_interval = 60 * 60 self.tracker_discover_time_interval = 5 * 60 - self.working_shared_trackers_limit_per_protocol = {} - self.working_shared_trackers_limit_per_protocol["other"] = 2 + self.shared_trackers_limit_per_protocol = {} + self.shared_trackers_limit_per_protocol["other"] = 2 self.file_path = "%s/trackers.json" % config.data_dir self.load() @@ -32,18 +32,18 @@ class TrackerStorage(object): atexit.register(self.save) def initTrackerLimitForProtocol(self): - for s in re.split("[,;]", config.working_shared_trackers_limit_per_protocol): + for s in re.split("[,;]", config.shared_trackers_limit_per_protocol): x = s.split("=", 1) if len(x) == 1: x = ["other", x[0]] try: - self.working_shared_trackers_limit_per_protocol[x[0]] = int(x[1]) + self.shared_trackers_limit_per_protocol[x[0]] = int(x[1]) except ValueError: pass - self.log.debug("Limits per protocol: %s" % self.working_shared_trackers_limit_per_protocol) + self.log.debug("Limits per protocol: %s" % self.shared_trackers_limit_per_protocol) def getTrackerLimitForProtocol(self, protocol): - l = self.working_shared_trackers_limit_per_protocol + l = self.shared_trackers_limit_per_protocol return l.get(protocol, l.get("other")) def setSiteAnnouncer(self, site_announcer): @@ -175,7 +175,7 @@ class TrackerStorage(object): error_limit = 30 if nr_working_trackers_for_protocol >= self.getTrackerLimitForProtocol(protocol): error_limit = 10 - if nr_working_trackers >= config.working_shared_trackers_limit: + if nr_working_trackers >= config.shared_trackers_limit: error_limit = 5 if trackers[tracker_address]["num_error"] > error_limit and trackers[tracker_address]["time_success"] < time.time() - self.tracker_down_time_interval: @@ -262,9 +262,9 @@ class TrackerStorage(object): return False total_nr += len(trackers) - if total_nr < config.working_shared_trackers_limit: + if total_nr < config.shared_trackers_limit: self.log.debug("Not enough working trackers: %s < %s" % ( - total_nr, config.working_shared_trackers_limit)) + total_nr, config.shared_trackers_limit)) return False return True @@ -357,7 +357,7 @@ class FileServerPlugin(object): class ConfigPlugin(object): def createArguments(self): group = self.parser.add_argument_group("TrackerShare plugin") - group.add_argument('--working_shared_trackers_limit', help='Stop discovering new shared trackers after this number of shared trackers reached (total)', default=10, type=int, metavar='limit') - group.add_argument('--working_shared_trackers_limit_per_protocol', help='Stop discovering new shared trackers after this number of shared trackers reached per each supported protocol', default="zero=5,other=4", metavar='limit') + group.add_argument('--shared_trackers_limit', help='Discover new shared trackers if this number of shared trackers isn\'t reached (total)', default=10, type=int, metavar='limit') + group.add_argument('--shared_trackers_limit_per_protocol', help='Discover new shared trackers if this number of shared trackers isn\'t reached per each supported protocol', default="zero=5,other=4", metavar='limit') return super(ConfigPlugin, self).createArguments() From 8f8e10a703f5d1030468ffcba5ad20564b6182d0 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Fri, 5 Jul 2019 23:17:13 +0700 Subject: [PATCH 005/191] TrackerShare: Change the log level for several messages from debug to info Increased the log level for messages that are not very annoying and help to keep the track of events. --- plugins/TrackerShare/TrackerSharePlugin.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/TrackerShare/TrackerSharePlugin.py b/plugins/TrackerShare/TrackerSharePlugin.py index 6b32c4e2..3a09c5d2 100644 --- a/plugins/TrackerShare/TrackerSharePlugin.py +++ b/plugins/TrackerShare/TrackerSharePlugin.py @@ -40,7 +40,7 @@ class TrackerStorage(object): self.shared_trackers_limit_per_protocol[x[0]] = int(x[1]) except ValueError: pass - self.log.debug("Limits per protocol: %s" % self.shared_trackers_limit_per_protocol) + self.log.info("Limits per protocol: %s" % self.shared_trackers_limit_per_protocol) def getTrackerLimitForProtocol(self, protocol): l = self.shared_trackers_limit_per_protocol @@ -103,7 +103,7 @@ class TrackerStorage(object): t = max(trackers[tracker_address]["time_added"], trackers[tracker_address]["time_success"]) if tracker_address not in supported_trackers and t < time.time() - self.tracker_down_time_interval: - self.log.debug("Tracker %s looks unused, removing." % tracker_address) + self.log.info("Tracker %s looks unused, removing." % tracker_address) del trackers[tracker_address] protocols = set() @@ -136,7 +136,7 @@ class TrackerStorage(object): "num_error": 0, "my": False } - self.log.debug("New tracker found: %s" % tracker_address) + self.log.info("New tracker found: %s" % tracker_address) added = True trackers[tracker_address]["time_found"] = time.time() @@ -179,7 +179,7 @@ class TrackerStorage(object): error_limit = 5 if trackers[tracker_address]["num_error"] > error_limit and trackers[tracker_address]["time_success"] < time.time() - self.tracker_down_time_interval: - self.log.debug("Tracker %s looks down, removing." % tracker_address) + self.log.info("Tracker %s looks down, removing." % tracker_address) del trackers[tracker_address] def isTrackerWorking(self, tracker_address, type="shared"): @@ -257,13 +257,13 @@ class TrackerStorage(object): for protocol in supported_protocols: trackers = trackers_per_protocol.get(protocol, []) if len(trackers) < self.getTrackerLimitForProtocol(protocol): - self.log.debug("Not enough working trackers for protocol %s: %s < %s" % ( + self.log.info("Not enough working trackers for protocol %s: %s < %s" % ( protocol, len(trackers), self.getTrackerLimitForProtocol(protocol))) return False total_nr += len(trackers) if total_nr < config.shared_trackers_limit: - self.log.debug("Not enough working trackers: %s < %s" % ( + self.log.info("Not enough working trackers: %s < %s" % ( total_nr, config.shared_trackers_limit)) return False @@ -273,7 +273,7 @@ class TrackerStorage(object): if self.enoughWorkingTrackers(type="shared"): return False - self.log.debug("Discovering trackers from %s peers..." % len(peers)) + self.log.info("Discovering trackers from %s peers..." % len(peers)) s = time.time() num_success = 0 @@ -301,7 +301,7 @@ class TrackerStorage(object): if num_success: self.save() - self.log.debug("Trackers discovered from %s/%s peers in %.3fs" % (num_success, len(peers), time.time() - s)) + self.log.info("Trackers discovered from %s/%s peers in %.3fs" % (num_success, len(peers), time.time() - s)) if "tracker_storage" not in locals(): From c8545ce054efbe1189a9ad816e8d043e5f9e11c2 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Fri, 5 Jul 2019 23:41:23 +0700 Subject: [PATCH 006/191] TrackerShare: print the total number of discovered trackers at the end of discovery procedure --- plugins/TrackerShare/TrackerSharePlugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/TrackerShare/TrackerSharePlugin.py b/plugins/TrackerShare/TrackerSharePlugin.py index 3a09c5d2..c89723fe 100644 --- a/plugins/TrackerShare/TrackerSharePlugin.py +++ b/plugins/TrackerShare/TrackerSharePlugin.py @@ -277,6 +277,7 @@ class TrackerStorage(object): s = time.time() num_success = 0 + num_trackers_discovered = 0 for peer in peers: if peer.connection and peer.connection.handshake.get("rev", 0) < 3560: continue # Not supported @@ -293,6 +294,7 @@ class TrackerStorage(object): tracker_address = tracker_address.decode("utf8") added = self.onTrackerFound(tracker_address) if added: # Only add one tracker from one source + num_trackers_discovered += 1 break if not num_success and len(peers) < 20: @@ -301,7 +303,7 @@ class TrackerStorage(object): if num_success: self.save() - self.log.info("Trackers discovered from %s/%s peers in %.3fs" % (num_success, len(peers), time.time() - s)) + self.log.info("Discovered %s new trackers from %s/%s peers in %.3fs" % (num_trackers_discovered, num_success, len(peers), time.time() - s)) if "tracker_storage" not in locals(): From d35a15d674ee1b25cfaf909ae48e64705fcd08bc Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Sat, 6 Jul 2019 01:15:37 +0700 Subject: [PATCH 007/191] TrackerShare: don't delete "my" trackers on errors, but delete them on program restart; add "persistent" flag for manually added trackers --- plugins/TrackerShare/TrackerSharePlugin.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/plugins/TrackerShare/TrackerSharePlugin.py b/plugins/TrackerShare/TrackerSharePlugin.py index c89723fe..e683dbfc 100644 --- a/plugins/TrackerShare/TrackerSharePlugin.py +++ b/plugins/TrackerShare/TrackerSharePlugin.py @@ -122,25 +122,31 @@ class TrackerStorage(object): def getDefaultFile(self): return {"shared": {}} - def onTrackerFound(self, tracker_address, type="shared", my=False): + def onTrackerFound(self, tracker_address, type="shared", my=False, persistent=False): if not self.isTrackerAddressValid(tracker_address): return False trackers = self.getTrackers(type) added = False if tracker_address not in trackers: + # "My" trackers never get deleted on announce errors, but aren't saved between restarts. + # They are to be used as automatically added addresses from the Bootstrap plugin. + # Persistent trackers never get deleted. + # They are to be used for entries manually added by the user. trackers[tracker_address] = { "time_added": time.time(), "time_success": 0, "latency": 99.0, "num_error": 0, - "my": False + "my": False, + "persistent": False } self.log.info("New tracker found: %s" % tracker_address) added = True trackers[tracker_address]["time_found"] = time.time() - trackers[tracker_address]["my"] = my + trackers[tracker_address]["my"] |= my + trackers[tracker_address]["persistent"] |= persistent return added def onTrackerSuccess(self, tracker_address, latency): @@ -162,6 +168,9 @@ class TrackerStorage(object): trackers[tracker_address]["time_error"] = time.time() trackers[tracker_address]["num_error"] += 1 + if trackers[tracker_address]["my"] or trackers[tracker_address]["persistent"]: + return + if self.time_success < time.time() - self.tracker_down_time_interval / 2: # Don't drop trackers from the list, if there haven't been any successful announces recently. # There may be network connectivity issues. @@ -235,7 +244,14 @@ class TrackerStorage(object): trackers = self.getTrackers() self.log.debug("Loaded %s shared trackers" % len(trackers)) for address, tracker in list(trackers.items()): + tracker.setdefault("time_added", time.time()) + tracker.setdefault("time_success", 0) + tracker.setdefault("latency", 99.0) + tracker.setdefault("my", False) + tracker.setdefault("persistent", False) tracker["num_error"] = 0 + if tracker["my"]: + del trackers[address] self.recheckValidTrackers() def save(self): From 37627822deedfd3b13c25345a31e59232cf5fc79 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Sat, 6 Jul 2019 01:39:32 +0700 Subject: [PATCH 008/191] TrackerList: make the plugin compatible with TrackerShare --- .../disabled-TrackerList/TrackerListPlugin.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/plugins/disabled-TrackerList/TrackerListPlugin.py b/plugins/disabled-TrackerList/TrackerListPlugin.py index c1ced5e1..b3bc6515 100644 --- a/plugins/disabled-TrackerList/TrackerListPlugin.py +++ b/plugins/disabled-TrackerList/TrackerListPlugin.py @@ -56,9 +56,6 @@ class TrackerList(object): self.last_rescan_failed = False def reload(self): - if "AnnounceShare" not in PluginManager.plugin_manager.plugin_names: - return - rescan_interval = config.tracker_list_rescan_interval if self.last_rescan_failed: rescan_interval = (rescan_interval / 2, 60) @@ -68,14 +65,19 @@ class TrackerList(object): self.last_rescan_time = time.time() - try: - if "tracker_storage" not in locals(): - from AnnounceShare.AnnounceSharePlugin import tracker_storage - self.tracker_storage = tracker_storage - if self.tracker_storage: - gevent.spawn(self.do_rescan) - except Exception as err: - self.log.error("%s" % Debug.formatException(err)) + if "tracker_storage" not in locals(): + try: + if "TrackerShare" in PluginManager.plugin_manager.plugin_names: + from TrackerShare.TrackerSharePlugin import tracker_storage + self.tracker_storage = tracker_storage + elif "AnnounceShare" in PluginManager.plugin_manager.plugin_names: + from AnnounceShare.AnnounceSharePlugin import tracker_storage + self.tracker_storage = tracker_storage + except Exception as err: + self.log.error("%s" % Debug.formatException(err)) + + if self.tracker_storage: + gevent.spawn(self.do_rescan) if "tracker_list" not in locals(): From 6ee1db4197a5a846ba12e461a2a92e9de4c6f6ed Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Sat, 6 Jul 2019 17:21:14 +0700 Subject: [PATCH 009/191] TrackerList: fix a typo --- plugins/disabled-TrackerList/TrackerListPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/disabled-TrackerList/TrackerListPlugin.py b/plugins/disabled-TrackerList/TrackerListPlugin.py index b3bc6515..b3611173 100644 --- a/plugins/disabled-TrackerList/TrackerListPlugin.py +++ b/plugins/disabled-TrackerList/TrackerListPlugin.py @@ -58,7 +58,7 @@ class TrackerList(object): def reload(self): rescan_interval = config.tracker_list_rescan_interval if self.last_rescan_failed: - rescan_interval = (rescan_interval / 2, 60) + rescan_interval = rescan_interval / 2 if self.last_rescan_time > time.time() - rescan_interval: return From aa6d7a468d1b0d225a4a2cef53101c64c99f922f Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Sun, 7 Jul 2019 14:31:13 +0700 Subject: [PATCH 010/191] TrackerShare: store trackers in shared-trackers.json, since trackers.json is in use by AnnounceShare and may have an incompatible format in the future --- plugins/TrackerShare/TrackerSharePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/TrackerShare/TrackerSharePlugin.py b/plugins/TrackerShare/TrackerSharePlugin.py index e683dbfc..558163ed 100644 --- a/plugins/TrackerShare/TrackerSharePlugin.py +++ b/plugins/TrackerShare/TrackerSharePlugin.py @@ -25,7 +25,7 @@ class TrackerStorage(object): self.shared_trackers_limit_per_protocol = {} self.shared_trackers_limit_per_protocol["other"] = 2 - self.file_path = "%s/trackers.json" % config.data_dir + self.file_path = "%s/shared-trackers.json" % config.data_dir self.load() self.time_discover = 0.0 self.time_success = 0.0 From b7550474a568cc24a0334679f17115a26956f7dd Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Fri, 12 Jul 2019 01:48:52 +0700 Subject: [PATCH 011/191] TrackerZero: copy the Bootstrapper code to a new plugin TrackerZero --- plugins/TrackerZero/TrackerZeroDb.py | 156 +++++++++++++++++++++++ plugins/TrackerZero/TrackerZeroPlugin.py | 155 ++++++++++++++++++++++ plugins/TrackerZero/__init__.py | 1 + 3 files changed, 312 insertions(+) create mode 100644 plugins/TrackerZero/TrackerZeroDb.py create mode 100644 plugins/TrackerZero/TrackerZeroPlugin.py create mode 100644 plugins/TrackerZero/__init__.py diff --git a/plugins/TrackerZero/TrackerZeroDb.py b/plugins/TrackerZero/TrackerZeroDb.py new file mode 100644 index 00000000..b5b57afa --- /dev/null +++ b/plugins/TrackerZero/TrackerZeroDb.py @@ -0,0 +1,156 @@ +import time +import re + +import gevent + +from Config import config +from Db import Db +from util import helper + + +class TrackerZeroDb(Db.Db): + def __init__(self): + self.version = 7 + self.hash_ids = {} # hash -> id cache + super(TrackerZeroDb, self).__init__({"db_name": "TrackerZero"}, "%s/tracker-zero.db" % config.data_dir) + self.foreign_keys = True + self.checkTables() + self.updateHashCache() + gevent.spawn(self.cleanup) + + def cleanup(self): + while 1: + time.sleep(4 * 60) + timeout = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time() - 60 * 40)) + self.execute("DELETE FROM peer WHERE date_announced < ?", [timeout]) + + def updateHashCache(self): + res = self.execute("SELECT * FROM hash") + self.hash_ids = {str(row["hash"]): row["hash_id"] for row in res} + self.log.debug("Loaded %s hash_ids" % len(self.hash_ids)) + + def checkTables(self): + version = int(self.execute("PRAGMA user_version").fetchone()[0]) + self.log.debug("Db version: %s, needed: %s" % (version, self.version)) + if version < self.version: + self.createTables() + else: + self.execute("VACUUM") + + def createTables(self): + # Delete all tables + self.execute("PRAGMA writable_schema = 1") + self.execute("DELETE FROM sqlite_master WHERE type IN ('table', 'index', 'trigger')") + self.execute("PRAGMA writable_schema = 0") + self.execute("VACUUM") + self.execute("PRAGMA INTEGRITY_CHECK") + # Create new tables + self.execute(""" + CREATE TABLE peer ( + peer_id INTEGER PRIMARY KEY ASC AUTOINCREMENT NOT NULL UNIQUE, + type TEXT, + address TEXT, + port INTEGER NOT NULL, + date_added DATETIME DEFAULT (CURRENT_TIMESTAMP), + date_announced DATETIME DEFAULT (CURRENT_TIMESTAMP) + ); + """) + self.execute("CREATE UNIQUE INDEX peer_key ON peer (address, port);") + + self.execute(""" + CREATE TABLE peer_to_hash ( + peer_to_hash_id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, + peer_id INTEGER REFERENCES peer (peer_id) ON DELETE CASCADE, + hash_id INTEGER REFERENCES hash (hash_id) + ); + """) + self.execute("CREATE INDEX peer_id ON peer_to_hash (peer_id);") + self.execute("CREATE INDEX hash_id ON peer_to_hash (hash_id);") + + self.execute(""" + CREATE TABLE hash ( + hash_id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, + hash BLOB UNIQUE NOT NULL, + date_added DATETIME DEFAULT (CURRENT_TIMESTAMP) + ); + """) + self.execute("PRAGMA user_version = %s" % self.version) + + def getHashId(self, hash): + if hash not in self.hash_ids: + self.log.debug("New hash: %s" % repr(hash)) + self.execute("INSERT OR IGNORE INTO hash ?", {"hash": hash}) + self.hash_ids[hash] = self.cur.cursor.lastrowid + return self.hash_ids[hash] + + def peerAnnounce(self, ip_type, address, port=None, hashes=[], onion_signed=False, delete_missing_hashes=False): + hashes_ids_announced = [] + for hash in hashes: + hashes_ids_announced.append(self.getHashId(hash)) + + # Check user + res = self.execute("SELECT peer_id FROM peer WHERE ? LIMIT 1", {"address": address, "port": port}) + + user_row = res.fetchone() + now = time.strftime("%Y-%m-%d %H:%M:%S") + if user_row: + peer_id = user_row["peer_id"] + self.execute("UPDATE peer SET date_announced = ? WHERE peer_id = ?", (now, peer_id)) + else: + self.log.debug("New peer: %s signed: %s" % (address, onion_signed)) + if ip_type == "onion" and not onion_signed: + return len(hashes) + self.execute("INSERT INTO peer ?", {"type": ip_type, "address": address, "port": port, "date_announced": now}) + peer_id = self.cur.cursor.lastrowid + + # Check user's hashes + res = self.execute("SELECT * FROM peer_to_hash WHERE ?", {"peer_id": peer_id}) + hash_ids_db = [row["hash_id"] for row in res] + if hash_ids_db != hashes_ids_announced: + hash_ids_added = set(hashes_ids_announced) - set(hash_ids_db) + hash_ids_removed = set(hash_ids_db) - set(hashes_ids_announced) + if ip_type != "onion" or onion_signed: + for hash_id in hash_ids_added: + self.execute("INSERT INTO peer_to_hash ?", {"peer_id": peer_id, "hash_id": hash_id}) + if hash_ids_removed and delete_missing_hashes: + self.execute("DELETE FROM peer_to_hash WHERE ?", {"peer_id": peer_id, "hash_id": list(hash_ids_removed)}) + + return len(hash_ids_added) + len(hash_ids_removed) + else: + return 0 + + def peerList(self, hash, address=None, onions=[], port=None, limit=30, need_types=["ipv4", "onion"], order=True): + back = {"ipv4": [], "ipv6": [], "onion": []} + if limit == 0: + return back + hashid = self.getHashId(hash) + + if order: + order_sql = "ORDER BY date_announced DESC" + else: + order_sql = "" + where_sql = "hash_id = :hashid" + if onions: + onions_escaped = ["'%s'" % re.sub("[^a-z0-9,]", "", onion) for onion in onions if type(onion) is str] + where_sql += " AND address NOT IN (%s)" % ",".join(onions_escaped) + elif address: + where_sql += " AND NOT (address = :address AND port = :port)" + + query = """ + SELECT type, address, port + FROM peer_to_hash + LEFT JOIN peer USING (peer_id) + WHERE %s + %s + LIMIT :limit + """ % (where_sql, order_sql) + res = self.execute(query, {"hashid": hashid, "address": address, "port": port, "limit": limit}) + + for row in res: + if row["type"] in need_types: + if row["type"] == "onion": + packed = helper.packOnionAddress(row["address"], row["port"]) + else: + packed = helper.packAddress(str(row["address"]), row["port"]) + back[row["type"]].append(packed) + return back diff --git a/plugins/TrackerZero/TrackerZeroPlugin.py b/plugins/TrackerZero/TrackerZeroPlugin.py new file mode 100644 index 00000000..ef455776 --- /dev/null +++ b/plugins/TrackerZero/TrackerZeroPlugin.py @@ -0,0 +1,155 @@ +import time + +from util import helper + +from Plugin import PluginManager +from .TrackerZeroDb import TrackerZeroDb +from Crypt import CryptRsa +from Config import config + +if "db" not in locals().keys(): # Share during reloads + db = TrackerZeroDb() + + +@PluginManager.registerTo("FileRequest") +class FileRequestPlugin(object): + def checkOnionSigns(self, onions, onion_signs, onion_sign_this): + if not onion_signs or len(onion_signs) != len(set(onions)): + return False + + if time.time() - float(onion_sign_this) > 3 * 60: + return False # Signed out of allowed 3 minutes + + onions_signed = [] + # Check onion signs + for onion_publickey, onion_sign in onion_signs.items(): + if CryptRsa.verify(onion_sign_this.encode(), onion_publickey, onion_sign): + onions_signed.append(CryptRsa.publickeyToOnion(onion_publickey)) + else: + break + + # Check if the same onion addresses signed as the announced onces + if sorted(onions_signed) == sorted(set(onions)): + return True + else: + return False + + def actionAnnounce(self, params): + time_started = time.time() + s = time.time() + # Backward compatibility + if "ip4" in params["add"]: + params["add"].append("ipv4") + if "ip4" in params["need_types"]: + params["need_types"].append("ipv4") + + hashes = params["hashes"] + + all_onions_signed = self.checkOnionSigns(params.get("onions", []), params.get("onion_signs"), params.get("onion_sign_this")) + + time_onion_check = time.time() - s + + ip_type = helper.getIpType(self.connection.ip) + + if ip_type == "onion" or self.connection.ip in config.ip_local: + is_port_open = False + elif ip_type in params["add"]: + is_port_open = True + else: + is_port_open = False + + s = time.time() + # Separatley add onions to sites or at once if no onions present + i = 0 + onion_to_hash = {} + for onion in params.get("onions", []): + if onion not in onion_to_hash: + onion_to_hash[onion] = [] + onion_to_hash[onion].append(hashes[i]) + i += 1 + + hashes_changed = 0 + for onion, onion_hashes in onion_to_hash.items(): + hashes_changed += db.peerAnnounce( + ip_type="onion", + address=onion, + port=params["port"], + hashes=onion_hashes, + onion_signed=all_onions_signed + ) + time_db_onion = time.time() - s + + s = time.time() + + if is_port_open: + hashes_changed += db.peerAnnounce( + ip_type=ip_type, + address=self.connection.ip, + port=params["port"], + hashes=hashes, + delete_missing_hashes=params.get("delete") + ) + time_db_ip = time.time() - s + + s = time.time() + # Query sites + back = {} + peers = [] + if params.get("onions") and not all_onions_signed and hashes_changed: + back["onion_sign_this"] = "%.0f" % time.time() # Send back nonce for signing + + if len(hashes) > 500 or not hashes_changed: + limit = 5 + order = False + else: + limit = 30 + order = True + for hash in hashes: + if time.time() - time_started > 1: # 1 sec limit on request + self.connection.log("Announce time limit exceeded after %s/%s sites" % (len(peers), len(hashes))) + break + + hash_peers = db.peerList( + hash, + address=self.connection.ip, onions=list(onion_to_hash.keys()), port=params["port"], + limit=min(limit, params["need_num"]), need_types=params["need_types"], order=order + ) + if "ip4" in params["need_types"]: # Backward compatibility + hash_peers["ip4"] = hash_peers["ipv4"] + del(hash_peers["ipv4"]) + peers.append(hash_peers) + time_peerlist = time.time() - s + + back["peers"] = peers + self.connection.log( + "Announce %s sites (onions: %s, onion_check: %.3fs, db_onion: %.3fs, db_ip: %.3fs, peerlist: %.3fs, limit: %s)" % + (len(hashes), len(onion_to_hash), time_onion_check, time_db_onion, time_db_ip, time_peerlist, limit) + ) + self.response(back) + + +@PluginManager.registerTo("UiRequest") +class UiRequestPlugin(object): + def actionStatsTrackerZero(self): + self.sendHeader() + + # Style + yield """ + + """ + + hash_rows = db.execute("SELECT * FROM hash").fetchall() + for hash_row in hash_rows: + peer_rows = db.execute( + "SELECT * FROM peer LEFT JOIN peer_to_hash USING (peer_id) WHERE hash_id = :hash_id", + {"hash_id": hash_row["hash_id"]} + ).fetchall() + + yield "
%s (added: %s, peers: %s)
" % ( + str(hash_row["hash"]).encode("hex"), hash_row["date_added"], len(peer_rows) + ) + for peer_row in peer_rows: + yield " - {ip4: <30} {onion: <30} added: {date_added}, announced: {date_announced}
".format(**dict(peer_row)) diff --git a/plugins/TrackerZero/__init__.py b/plugins/TrackerZero/__init__.py new file mode 100644 index 00000000..638a8f8a --- /dev/null +++ b/plugins/TrackerZero/__init__.py @@ -0,0 +1 @@ +from . import TrackerZeroPlugin \ No newline at end of file From f4708d9781bfe95522b01e351867fb81ba102177 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Fri, 12 Jul 2019 15:44:23 +0700 Subject: [PATCH 012/191] TrackerZero: add a separate class for not to run complicated code in overloaded methods --- plugins/TrackerZero/TrackerZeroPlugin.py | 38 ++++++++++++++++-------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/plugins/TrackerZero/TrackerZeroPlugin.py b/plugins/TrackerZero/TrackerZeroPlugin.py index ef455776..7b4d3de4 100644 --- a/plugins/TrackerZero/TrackerZeroPlugin.py +++ b/plugins/TrackerZero/TrackerZeroPlugin.py @@ -7,12 +7,10 @@ from .TrackerZeroDb import TrackerZeroDb from Crypt import CryptRsa from Config import config -if "db" not in locals().keys(): # Share during reloads - db = TrackerZeroDb() +class TrackerZero(object): + def __init__(self): + self.log = logging.getLogger("TrackerZero") - -@PluginManager.registerTo("FileRequest") -class FileRequestPlugin(object): def checkOnionSigns(self, onions, onion_signs, onion_sign_this): if not onion_signs or len(onion_signs) != len(set(onions)): return False @@ -34,7 +32,7 @@ class FileRequestPlugin(object): else: return False - def actionAnnounce(self, params): + def actionAnnounce(self, file_request, params): time_started = time.time() s = time.time() # Backward compatibility @@ -49,9 +47,9 @@ class FileRequestPlugin(object): time_onion_check = time.time() - s - ip_type = helper.getIpType(self.connection.ip) + ip_type = helper.getIpType(file_request.connection.ip) - if ip_type == "onion" or self.connection.ip in config.ip_local: + if ip_type == "onion" or file_request.connection.ip in config.ip_local: is_port_open = False elif ip_type in params["add"]: is_port_open = True @@ -84,7 +82,7 @@ class FileRequestPlugin(object): if is_port_open: hashes_changed += db.peerAnnounce( ip_type=ip_type, - address=self.connection.ip, + address=file_request.connection.ip, port=params["port"], hashes=hashes, delete_missing_hashes=params.get("delete") @@ -106,12 +104,12 @@ class FileRequestPlugin(object): order = True for hash in hashes: if time.time() - time_started > 1: # 1 sec limit on request - self.connection.log("Announce time limit exceeded after %s/%s sites" % (len(peers), len(hashes))) + file_request.connection.log("Announce time limit exceeded after %s/%s sites" % (len(peers), len(hashes))) break hash_peers = db.peerList( hash, - address=self.connection.ip, onions=list(onion_to_hash.keys()), port=params["port"], + address=file_request.connection.ip, onions=list(onion_to_hash.keys()), port=params["port"], limit=min(limit, params["need_num"]), need_types=params["need_types"], order=order ) if "ip4" in params["need_types"]: # Backward compatibility @@ -121,11 +119,25 @@ class FileRequestPlugin(object): time_peerlist = time.time() - s back["peers"] = peers - self.connection.log( + file_request.connection.log( "Announce %s sites (onions: %s, onion_check: %.3fs, db_onion: %.3fs, db_ip: %.3fs, peerlist: %.3fs, limit: %s)" % (len(hashes), len(onion_to_hash), time_onion_check, time_db_onion, time_db_ip, time_peerlist, limit) ) - self.response(back) + file_request.response(back) + + +if "db" not in locals().keys(): # Share during reloads + db = TrackerZeroDb() + +if "TrackerZero" not in locals(): + tracker_zero = TrackerZero() + + + +@PluginManager.registerTo("FileRequest") +class FileRequestPlugin(object): + def actionAnnounce(self, params): + tracker_zero.actionAnnounce(self, params) @PluginManager.registerTo("UiRequest") From 9a8519b4872c86326043b684aa70c5e86f540ca8 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Sat, 13 Jul 2019 00:51:31 +0700 Subject: [PATCH 013/191] TrackerZero: read settings from tracker-zero.json; register listened addresses in TrackerShare/AnnounceShare --- plugins/TrackerZero/TrackerZeroPlugin.py | 112 ++++++++++++++++++++++- 1 file changed, 111 insertions(+), 1 deletion(-) diff --git a/plugins/TrackerZero/TrackerZeroPlugin.py b/plugins/TrackerZero/TrackerZeroPlugin.py index 7b4d3de4..53ca6d6c 100644 --- a/plugins/TrackerZero/TrackerZeroPlugin.py +++ b/plugins/TrackerZero/TrackerZeroPlugin.py @@ -1,3 +1,7 @@ +import atexit +import json +import logging +import os import time from util import helper @@ -10,6 +14,53 @@ from Config import config class TrackerZero(object): def __init__(self): self.log = logging.getLogger("TrackerZero") + self.log_once = set() + self.enabled_addresses = [] + self.config_file_path = "%s/tracker-zero.json" % config.data_dir + self.config = None + self.load() + atexit.register(self.save) + + + def logOnce(self, message): + if message in self.log_once: + return + self.log_once.add(message) + self.log.info(message) + + def getDefaultConfig(self): + return { + "settings": { + "enable": False, + "enable_only_in_tor_always_mode": True, + "listen_on_public_ips": False, + "listen_on_temporary_onion_address": False, + "listen_on_persistent_onion_address": True + } + } + + def readJSON(self, file_path, default_value): + if not os.path.isfile(file_path): + try: + self.writeJSON(file_path, default_value) + except Exception as err: + self.log.error("Error writing %s: %s" % (file_path, err)) + return default_value + + try: + return json.load(open(file_path)) + except Exception as err: + self.log.error("Error loading %s: %s" % (file_path, err)) + return default_value + + def writeJSON(self, file_path, value): + helper.atomicWrite(file_path, json.dumps(value, indent=2, sort_keys=True).encode("utf8")) + + def load(self): + self.config = self.readJSON(self.config_file_path, self.getDefaultConfig()) + + def save(self): + self.writeJSON(self.config_file_path, self.config) def checkOnionSigns(self, onions, onion_signs, onion_sign_this): if not onion_signs or len(onion_signs) != len(set(onions)): @@ -126,10 +177,61 @@ class TrackerZero(object): file_request.response(back) + def getTrackerStorage(self): + try: + if "TrackerShare" in PluginManager.plugin_manager.plugin_names: + from TrackerShare.TrackerSharePlugin import tracker_storage + return tracker_storage + elif "AnnounceShare" in PluginManager.plugin_manager.plugin_names: + from AnnounceShare.AnnounceSharePlugin import tracker_storage + return tracker_storage + except Exception as err: + self.log.error("%s" % Debug.formatException(err)) + + return None + + def registerSharedAddresses(self, file_server, port_open): + tracker_storage = self.getTrackerStorage() + if not tracker_storage: + return + + settings = self.config.get("settings", {}) + + if not settings.get("enable"): + self.logOnce("Plugin loaded, but disabled by the settings") + return False + + if settings.get("enable_only_in_tor_always_mode") and not config.tor == "always": + self.logOnce("Plugin loaded, but disabled from running in the modes other than 'tor = always'") + return False + + self.enabled_addresses = [] + + if settings.get("listen_on_public_ips") and port_open and not config.tor == "always": + for ip in file_server.ip_external_list: + my_tracker_address = "zero://%s:%s" % (ip, config.fileserver_port) + if tracker_storage.onTrackerFound(my_tracker_address, my=True): + self.logOnce("listening on public IP: %s" % my_tracker_address) + self.enabled_addresses.append(my_tracker_address) + + if settings.get("listen_on_temporary_onion_address") and file_server.tor_manager.enabled: + onion = file_server.tor_manager.getOnion(config.homepage) + if onion: + my_tracker_address = "zero://%s.onion:%s" % (onion, file_server.tor_manager.fileserver_port) + if tracker_storage.onTrackerFound(my_tracker_address, my=True): + self.logOnce("listening on temporary onion address: %s" % my_tracker_address) + self.enabled_addresses.append(my_tracker_address) + + if settings.get("listen_on_persistent_onion_address") and file_server.tor_manager.enabled: + # FIXME: not implemented + pass + + return len(self.enabled_addresses) > 0 + if "db" not in locals().keys(): # Share during reloads db = TrackerZeroDb() -if "TrackerZero" not in locals(): +if "tracker_zero" not in locals(): tracker_zero = TrackerZero() @@ -140,6 +242,14 @@ class FileRequestPlugin(object): tracker_zero.actionAnnounce(self, params) +@PluginManager.registerTo("FileServer") +class FileServerPlugin(object): + def portCheck(self, *args, **kwargs): + res = super(FileServerPlugin, self).portCheck(*args, **kwargs) + tracker_zero.registerSharedAddresses(self, res) + return res + + @PluginManager.registerTo("UiRequest") class UiRequestPlugin(object): def actionStatsTrackerZero(self): From a36b2c924165cd44ddc8715a0bdae660321b2a31 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Sat, 13 Jul 2019 02:34:07 +0700 Subject: [PATCH 014/191] TrackerZero: add support of persistent onion addresses --- plugins/TrackerZero/TrackerZeroPlugin.py | 62 +++++++++++++++++------- 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/plugins/TrackerZero/TrackerZeroPlugin.py b/plugins/TrackerZero/TrackerZeroPlugin.py index 53ca6d6c..f31bd09b 100644 --- a/plugins/TrackerZero/TrackerZeroPlugin.py +++ b/plugins/TrackerZero/TrackerZeroPlugin.py @@ -1,6 +1,7 @@ import atexit import json import logging +import re import os import time @@ -21,7 +22,6 @@ class TrackerZero(object): self.load() atexit.register(self.save) - def logOnce(self, message): if message in self.log_once: return @@ -190,11 +190,23 @@ class TrackerZero(object): return None - def registerSharedAddresses(self, file_server, port_open): - tracker_storage = self.getTrackerStorage() - if not tracker_storage: + def registerTrackerAddress(self, message, address, port): + _tracker_storage = self.getTrackerStorage() + if not _tracker_storage: return + my_tracker_address = "zero://%s:%s" % (address, port) + if _tracker_storage.onTrackerFound(my_tracker_address, my=True): + self.logOnce("listening on %s: %s" % (message, my_tracker_address)) + self.enabled_addresses.append("%s:%s" % (address, port)) + + def registerTrackerAddresses(self, file_server, port_open): + _tracker_storage = self.getTrackerStorage() + if not _tracker_storage: + return + + tor_manager = file_server.tor_manager + settings = self.config.get("settings", {}) if not settings.get("enable"): @@ -209,22 +221,36 @@ class TrackerZero(object): if settings.get("listen_on_public_ips") and port_open and not config.tor == "always": for ip in file_server.ip_external_list: - my_tracker_address = "zero://%s:%s" % (ip, config.fileserver_port) - if tracker_storage.onTrackerFound(my_tracker_address, my=True): - self.logOnce("listening on public IP: %s" % my_tracker_address) - self.enabled_addresses.append(my_tracker_address) + self.registerTrackerAddress("public IP", ip, config.fileserver_port) - if settings.get("listen_on_temporary_onion_address") and file_server.tor_manager.enabled: - onion = file_server.tor_manager.getOnion(config.homepage) + if settings.get("listen_on_temporary_onion_address") and tor_manager.enabled: + onion = tor_manager.getOnion(config.homepage) if onion: - my_tracker_address = "zero://%s.onion:%s" % (onion, file_server.tor_manager.fileserver_port) - if tracker_storage.onTrackerFound(my_tracker_address, my=True): - self.logOnce("listening on temporary onion address: %s" % my_tracker_address) - self.enabled_addresses.append(my_tracker_address) + self.registerTrackerAddress("temporary onion address", onion, tor_manager.fileserver_port) - if settings.get("listen_on_persistent_onion_address") and file_server.tor_manager.enabled: - # FIXME: not implemented - pass + if settings.get("listen_on_persistent_onion_address") and tor_manager.enabled: + persistent_addresses = self.config.setdefault("persistent_addresses", {}) + if len(persistent_addresses) == 0: + result = tor_manager.makeOnionAndKey() + if result: + onion_address, onion_privatekey = result + persistent_addresses[onion_address] = { + "private_key": onion_privatekey + } + self.registerTrackerAddress("persistent onion address", onion_address, tor_manager.fileserver_port) + else: + for address, d in persistent_addresses.items(): + private_key = d.get("private_key") + if not private_key: + continue + res = tor_manager.request( + "ADD_ONION RSA1024:%s port=%s" % (private_key, tor_manager.fileserver_port) + ) + match = re.search("ServiceID=([A-Za-z0-9]+)", res) + if match: + onion_address = match.groups()[0] + if onion_address == address: + self.registerTrackerAddress("persistent onion address", onion_address, tor_manager.fileserver_port) return len(self.enabled_addresses) > 0 @@ -246,7 +272,7 @@ class FileRequestPlugin(object): class FileServerPlugin(object): def portCheck(self, *args, **kwargs): res = super(FileServerPlugin, self).portCheck(*args, **kwargs) - tracker_zero.registerSharedAddresses(self, res) + tracker_zero.registerTrackerAddresses(self, res) return res From eb6d0c9644980b7e8a85455f543c98675729f589 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Sat, 13 Jul 2019 15:35:16 +0700 Subject: [PATCH 015/191] TrackerZero: add missimg @helper.encodeResponse --- plugins/TrackerZero/TrackerZeroPlugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/TrackerZero/TrackerZeroPlugin.py b/plugins/TrackerZero/TrackerZeroPlugin.py index f31bd09b..7495eea1 100644 --- a/plugins/TrackerZero/TrackerZeroPlugin.py +++ b/plugins/TrackerZero/TrackerZeroPlugin.py @@ -278,6 +278,8 @@ class FileServerPlugin(object): @PluginManager.registerTo("UiRequest") class UiRequestPlugin(object): + + @helper.encodeResponse def actionStatsTrackerZero(self): self.sendHeader() From d57deaa8e4869b80abca12d027c24bb86a08c054 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Sat, 13 Jul 2019 15:36:05 +0700 Subject: [PATCH 016/191] TrackerShare: ignore the udp:// protocol, when UDP is known to be disabled by the config --- plugins/TrackerShare/TrackerSharePlugin.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/TrackerShare/TrackerSharePlugin.py b/plugins/TrackerShare/TrackerSharePlugin.py index 558163ed..156f7fd2 100644 --- a/plugins/TrackerShare/TrackerSharePlugin.py +++ b/plugins/TrackerShare/TrackerSharePlugin.py @@ -75,6 +75,9 @@ class TrackerStorage(object): if not self.isTrackerAddressValid(address): del trackers[address] + def isUdpEnabled(self): + return not (config.disable_udp or config.trackers_proxy != "disable") + def getNormalizedTrackerProtocol(self, tracker_address): if not self.site_announcer: return None @@ -111,6 +114,8 @@ class TrackerStorage(object): protocol = self.getNormalizedTrackerProtocol(tracker_address) if not protocol: continue + if protocol == "udp" and not self.isUdpEnabled(): + continue protocols.add(protocol) protocols = list(protocols) From de5a9ff67bfa3c6722987747cca4aead060763f3 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Sat, 13 Jul 2019 15:38:05 +0700 Subject: [PATCH 017/191] TrackerShare: drop incomplete support of Bootstrapper, we now have TrackerZero, which is able to register its addresses by itself --- plugins/TrackerShare/TrackerSharePlugin.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/plugins/TrackerShare/TrackerSharePlugin.py b/plugins/TrackerShare/TrackerSharePlugin.py index 156f7fd2..eb07b978 100644 --- a/plugins/TrackerShare/TrackerSharePlugin.py +++ b/plugins/TrackerShare/TrackerSharePlugin.py @@ -365,17 +365,6 @@ class FileRequestPlugin(object): self.response({"trackers": shared_trackers}) -@PluginManager.registerTo("FileServer") -class FileServerPlugin(object): - def portCheck(self, *args, **kwargs): - res = super(FileServerPlugin, self).portCheck(*args, **kwargs) - if res and not config.tor == "always" and "Bootstrapper" in PluginManager.plugin_manager.plugin_names: - for ip in self.ip_external_list: - my_tracker_address = "zero://%s:%s" % (ip, config.fileserver_port) - tracker_storage.onTrackerFound(my_tracker_address, my=True) - return res - - @PluginManager.registerTo("ConfigPlugin") class ConfigPlugin(object): def createArguments(self): From 142f5862df1ff383c1f409a7efaf3ad466ce4c91 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Sat, 13 Jul 2019 15:49:21 +0700 Subject: [PATCH 018/191] TrackerZero: ignore "announce" action if the plugin is disabled by its settings --- plugins/TrackerZero/TrackerZeroPlugin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/TrackerZero/TrackerZeroPlugin.py b/plugins/TrackerZero/TrackerZeroPlugin.py index 7495eea1..92070fa5 100644 --- a/plugins/TrackerZero/TrackerZeroPlugin.py +++ b/plugins/TrackerZero/TrackerZeroPlugin.py @@ -84,6 +84,10 @@ class TrackerZero(object): return False def actionAnnounce(self, file_request, params): + if len(self.enabled_addresses) < 1: + file_request.actionUnknown("announce", params) + return + time_started = time.time() s = time.time() # Backward compatibility From fee63a1ed2bf2fc9c4ce24d4a7f3f9824b8765a4 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Sat, 13 Jul 2019 21:13:41 +0700 Subject: [PATCH 019/191] TrackerZero: add the missing .onion suffix --- plugins/TrackerZero/TrackerZeroPlugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/TrackerZero/TrackerZeroPlugin.py b/plugins/TrackerZero/TrackerZeroPlugin.py index 92070fa5..3ae9531f 100644 --- a/plugins/TrackerZero/TrackerZeroPlugin.py +++ b/plugins/TrackerZero/TrackerZeroPlugin.py @@ -230,7 +230,7 @@ class TrackerZero(object): if settings.get("listen_on_temporary_onion_address") and tor_manager.enabled: onion = tor_manager.getOnion(config.homepage) if onion: - self.registerTrackerAddress("temporary onion address", onion, tor_manager.fileserver_port) + self.registerTrackerAddress("temporary onion address", "%s.onion" % onion, tor_manager.fileserver_port) if settings.get("listen_on_persistent_onion_address") and tor_manager.enabled: persistent_addresses = self.config.setdefault("persistent_addresses", {}) @@ -241,7 +241,7 @@ class TrackerZero(object): persistent_addresses[onion_address] = { "private_key": onion_privatekey } - self.registerTrackerAddress("persistent onion address", onion_address, tor_manager.fileserver_port) + self.registerTrackerAddress("persistent onion address", "%s.onion" % onion_address, tor_manager.fileserver_port) else: for address, d in persistent_addresses.items(): private_key = d.get("private_key") @@ -254,7 +254,7 @@ class TrackerZero(object): if match: onion_address = match.groups()[0] if onion_address == address: - self.registerTrackerAddress("persistent onion address", onion_address, tor_manager.fileserver_port) + self.registerTrackerAddress("persistent onion address", "%s.onion" % onion_address, tor_manager.fileserver_port) return len(self.enabled_addresses) > 0 From 2e1b0e093f938576891cc4e019dbb3b37997056f Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Sun, 14 Jul 2019 18:26:38 +0700 Subject: [PATCH 020/191] TrackerZero: fix errors in actionStatsTrackerZero() --- plugins/TrackerZero/TrackerZeroPlugin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/TrackerZero/TrackerZeroPlugin.py b/plugins/TrackerZero/TrackerZeroPlugin.py index 3ae9531f..52912a61 100644 --- a/plugins/TrackerZero/TrackerZeroPlugin.py +++ b/plugins/TrackerZero/TrackerZeroPlugin.py @@ -4,6 +4,7 @@ import logging import re import os import time +import binascii from util import helper @@ -303,7 +304,7 @@ class UiRequestPlugin(object): ).fetchall() yield "
%s (added: %s, peers: %s)
" % ( - str(hash_row["hash"]).encode("hex"), hash_row["date_added"], len(peer_rows) + binascii.hexlify(hash_row["hash"]).decode("utf-8"), hash_row["date_added"], len(peer_rows) ) for peer_row in peer_rows: - yield " - {ip4: <30} {onion: <30} added: {date_added}, announced: {date_announced}
".format(**dict(peer_row)) + yield " - {type: <6} {address: <30} {port: >5} added: {date_added}, announced: {date_announced}
".format(**dict(peer_row)) From 96e935300ce65c94c215deb89798d8f19ef38ef3 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Mon, 15 Jul 2019 14:13:14 +0700 Subject: [PATCH 021/191] TrackerZero: fix error in updateHashCache() Reported to the upstream: https://github.com/HelloZeroNet/ZeroNet/issues/2095 --- plugins/TrackerZero/TrackerZeroDb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/TrackerZero/TrackerZeroDb.py b/plugins/TrackerZero/TrackerZeroDb.py index b5b57afa..e91cf9be 100644 --- a/plugins/TrackerZero/TrackerZeroDb.py +++ b/plugins/TrackerZero/TrackerZeroDb.py @@ -26,7 +26,7 @@ class TrackerZeroDb(Db.Db): def updateHashCache(self): res = self.execute("SELECT * FROM hash") - self.hash_ids = {str(row["hash"]): row["hash_id"] for row in res} + self.hash_ids = {row["hash"]: row["hash_id"] for row in res} self.log.debug("Loaded %s hash_ids" % len(self.hash_ids)) def checkTables(self): From 2811d7c9d4963323c9e12d030d48b46154bd1927 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 17 Jul 2019 23:21:11 +0700 Subject: [PATCH 022/191] TrackerShare: replace self.site_announcer on every call from SiteAnnouncer --- plugins/TrackerShare/TrackerSharePlugin.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/plugins/TrackerShare/TrackerSharePlugin.py b/plugins/TrackerShare/TrackerSharePlugin.py index eb07b978..6ae642d1 100644 --- a/plugins/TrackerShare/TrackerSharePlugin.py +++ b/plugins/TrackerShare/TrackerSharePlugin.py @@ -47,11 +47,15 @@ class TrackerStorage(object): return l.get(protocol, l.get("other")) def setSiteAnnouncer(self, site_announcer): - if self.site_announcer: + if not site_announcer: return - self.site_announcer = site_announcer - self.initTrackerLimitForProtocol() - self.recheckValidTrackers() + + if not self.site_announcer: + self.site_announcer = site_announcer + self.initTrackerLimitForProtocol() + self.recheckValidTrackers() + else: + self.site_announcer = site_announcer def isTrackerAddressValid(self, tracker_address): if not self.site_announcer: # Not completely initialized, skip check From 0d02c3c4dabb82ae918f0d5814520a571e848890 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 17 Jul 2019 23:23:03 +0700 Subject: [PATCH 023/191] TrackerShare: increase default limits: zero=10,other=5; total=20 --- plugins/TrackerShare/TrackerSharePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/TrackerShare/TrackerSharePlugin.py b/plugins/TrackerShare/TrackerSharePlugin.py index 6ae642d1..65cf99b5 100644 --- a/plugins/TrackerShare/TrackerSharePlugin.py +++ b/plugins/TrackerShare/TrackerSharePlugin.py @@ -373,7 +373,7 @@ class FileRequestPlugin(object): class ConfigPlugin(object): def createArguments(self): group = self.parser.add_argument_group("TrackerShare plugin") - group.add_argument('--shared_trackers_limit', help='Discover new shared trackers if this number of shared trackers isn\'t reached (total)', default=10, type=int, metavar='limit') - group.add_argument('--shared_trackers_limit_per_protocol', help='Discover new shared trackers if this number of shared trackers isn\'t reached per each supported protocol', default="zero=5,other=4", metavar='limit') + group.add_argument('--shared_trackers_limit', help='Discover new shared trackers if this number of shared trackers isn\'t reached (total)', default=20, type=int, metavar='limit') + group.add_argument('--shared_trackers_limit_per_protocol', help='Discover new shared trackers if this number of shared trackers isn\'t reached per each supported protocol', default="zero=10,other=5", metavar='limit') return super(ConfigPlugin, self).createArguments() From cb363d2f11174c60b3c8d5b81117025f2315a2fb Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 17 Jul 2019 23:35:39 +0700 Subject: [PATCH 024/191] TrackerShare: move the tracker list cleanup code from getSupportedProtocols() to a separate method --- plugins/TrackerShare/TrackerSharePlugin.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/plugins/TrackerShare/TrackerSharePlugin.py b/plugins/TrackerShare/TrackerSharePlugin.py index 65cf99b5..1e70b4af 100644 --- a/plugins/TrackerShare/TrackerSharePlugin.py +++ b/plugins/TrackerShare/TrackerSharePlugin.py @@ -96,12 +96,7 @@ class TrackerStorage(object): return protocol - def getSupportedProtocols(self): - if not self.site_announcer: - return None - - supported_trackers = self.site_announcer.getSupportedTrackers() - + def deleteUnusedTrackers(self, supported_trackers): # If a tracker is in our list, but is absent from the results of getSupportedTrackers(), # it seems to be supported by software, but forbidden by the settings or network configuration. # We check and remove thoose trackers here, since onTrackerError() is never emitted for them. @@ -113,6 +108,14 @@ class TrackerStorage(object): self.log.info("Tracker %s looks unused, removing." % tracker_address) del trackers[tracker_address] + def getSupportedProtocols(self): + if not self.site_announcer: + return None + + supported_trackers = self.site_announcer.getSupportedTrackers() + + self.deleteUnusedTrackers(supported_trackers) + protocols = set() for tracker_address in supported_trackers: protocol = self.getNormalizedTrackerProtocol(tracker_address) From 95c8f0e97e015695bd619a13382dbcf58fa2aa04 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 17 Jul 2019 23:48:53 +0700 Subject: [PATCH 025/191] TrackerShare: consider UDP disabled if config.tor == "always" --- plugins/TrackerShare/TrackerSharePlugin.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/plugins/TrackerShare/TrackerSharePlugin.py b/plugins/TrackerShare/TrackerSharePlugin.py index 1e70b4af..f213353b 100644 --- a/plugins/TrackerShare/TrackerSharePlugin.py +++ b/plugins/TrackerShare/TrackerSharePlugin.py @@ -80,7 +80,16 @@ class TrackerStorage(object): del trackers[address] def isUdpEnabled(self): - return not (config.disable_udp or config.trackers_proxy != "disable") + if config.disable_udp: + return False + + if config.trackers_proxy != "disable": + return False + + if config.tor == "always": + return False + + return True def getNormalizedTrackerProtocol(self, tracker_address): if not self.site_announcer: From 5f6589cfc231ade0b06e8b29238ec09b06f4ae95 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Thu, 18 Jul 2019 00:01:56 +0700 Subject: [PATCH 026/191] TrackerShare: be more verbose in enoughWorkingTrackers() --- plugins/TrackerShare/TrackerSharePlugin.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/TrackerShare/TrackerSharePlugin.py b/plugins/TrackerShare/TrackerSharePlugin.py index f213353b..31627595 100644 --- a/plugins/TrackerShare/TrackerSharePlugin.py +++ b/plugins/TrackerShare/TrackerSharePlugin.py @@ -289,6 +289,8 @@ class TrackerStorage(object): if not trackers_per_protocol: return False + unmet_conditions = 0 + total_nr = 0 for protocol in supported_protocols: @@ -296,15 +298,15 @@ class TrackerStorage(object): if len(trackers) < self.getTrackerLimitForProtocol(protocol): self.log.info("Not enough working trackers for protocol %s: %s < %s" % ( protocol, len(trackers), self.getTrackerLimitForProtocol(protocol))) - return False + unmet_conditions += 1 total_nr += len(trackers) if total_nr < config.shared_trackers_limit: - self.log.info("Not enough working trackers: %s < %s" % ( + self.log.info("Not enough working trackers (total): %s < %s" % ( total_nr, config.shared_trackers_limit)) - return False + unmet_conditions + 1 - return True + return unmet_conditions == 0 def discoverTrackers(self, peers): if self.enoughWorkingTrackers(type="shared"): From f9706e3dc4912bcca1952de44dfbc83a57ca9299 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Mon, 9 Mar 2020 19:24:57 +0700 Subject: [PATCH 027/191] TrackerZero: don't register the same onion addresses multiple times --- plugins/TrackerZero/TrackerZeroPlugin.py | 30 +++++++++++++++++------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/plugins/TrackerZero/TrackerZeroPlugin.py b/plugins/TrackerZero/TrackerZeroPlugin.py index 52912a61..e90f085d 100644 --- a/plugins/TrackerZero/TrackerZeroPlugin.py +++ b/plugins/TrackerZero/TrackerZeroPlugin.py @@ -18,11 +18,30 @@ class TrackerZero(object): self.log = logging.getLogger("TrackerZero") self.log_once = set() self.enabled_addresses = [] + self.added_onions = set() self.config_file_path = "%s/tracker-zero.json" % config.data_dir self.config = None self.load() atexit.register(self.save) + def addOnion(self, tor_manager, onion, private_key): + # XXX: TorManager hangs if Tor returns a code different from 250 OK, + # so we keep the list of already added onions to avoid adding them twice. + # TODO: Report to the upstream. + + if onion in self.added_onions: + return onion + + res = tor_manager.request( + "ADD_ONION RSA1024:%s port=%s" % (private_key, tor_manager.fileserver_port) + ) + match = re.search("ServiceID=([A-Za-z0-9]+)", res) + if match: + onion_address = match.groups()[0] + self.added_onions.add(onion_address) + return onion_address + return None + def logOnce(self, message): if message in self.log_once: return @@ -248,14 +267,9 @@ class TrackerZero(object): private_key = d.get("private_key") if not private_key: continue - res = tor_manager.request( - "ADD_ONION RSA1024:%s port=%s" % (private_key, tor_manager.fileserver_port) - ) - match = re.search("ServiceID=([A-Za-z0-9]+)", res) - if match: - onion_address = match.groups()[0] - if onion_address == address: - self.registerTrackerAddress("persistent onion address", "%s.onion" % onion_address, tor_manager.fileserver_port) + onion_address = self.addOnion(tor_manager, address, private_key) + if onion_address == address: + self.registerTrackerAddress("persistent onion address", "%s.onion" % onion_address, tor_manager.fileserver_port) return len(self.enabled_addresses) > 0 From 1f34f477efb6585d5563ee2b58a18fb291b0a4ea Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Mon, 26 Oct 2020 13:33:25 +0700 Subject: [PATCH 028/191] TrackerShare: add plugin_info.json --- plugins/TrackerShare/plugin_info.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 plugins/TrackerShare/plugin_info.json diff --git a/plugins/TrackerShare/plugin_info.json b/plugins/TrackerShare/plugin_info.json new file mode 100644 index 00000000..9c93cafc --- /dev/null +++ b/plugins/TrackerShare/plugin_info.json @@ -0,0 +1,5 @@ +{ + "name": "TrackerShare", + "description": "Share possible trackers between clients.", + "default": "enabled" +} From b0005026b44870d4efad8664760dfd082e86ab84 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Mon, 26 Oct 2020 19:52:35 +0700 Subject: [PATCH 029/191] TrackerShare: rename the `shared` key to `trackers` key in the json file and delete the unused `type` parameter --- plugins/TrackerShare/TrackerSharePlugin.py | 36 +++++++++++----------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/plugins/TrackerShare/TrackerSharePlugin.py b/plugins/TrackerShare/TrackerSharePlugin.py index 31627595..a139e82c 100644 --- a/plugins/TrackerShare/TrackerSharePlugin.py +++ b/plugins/TrackerShare/TrackerSharePlugin.py @@ -141,13 +141,13 @@ class TrackerStorage(object): return protocols def getDefaultFile(self): - return {"shared": {}} + return {"trackers": {}} - def onTrackerFound(self, tracker_address, type="shared", my=False, persistent=False): + def onTrackerFound(self, tracker_address, my=False, persistent=False): if not self.isTrackerAddressValid(tracker_address): return False - trackers = self.getTrackers(type) + trackers = self.getTrackers() added = False if tracker_address not in trackers: # "My" trackers never get deleted on announce errors, but aren't saved between restarts. @@ -212,8 +212,8 @@ class TrackerStorage(object): self.log.info("Tracker %s looks down, removing." % tracker_address) del trackers[tracker_address] - def isTrackerWorking(self, tracker_address, type="shared"): - trackers = self.getTrackers(type) + def isTrackerWorking(self, tracker_address): + trackers = self.getTrackers() tracker = trackers[tracker_address] if not tracker: return False @@ -223,29 +223,29 @@ class TrackerStorage(object): return False - def getTrackers(self, type="shared"): - return self.file_content.setdefault(type, {}) + def getTrackers(self): + return self.file_content.setdefault("trackers", {}) - def getTrackersPerProtocol(self, type="shared", working_only=False): + def getTrackersPerProtocol(self, working_only=False): if not self.site_announcer: return None - trackers = self.getTrackers(type) + trackers = self.getTrackers() trackers_per_protocol = {} for tracker_address in trackers: protocol = self.getNormalizedTrackerProtocol(tracker_address) if not protocol: continue - if not working_only or self.isTrackerWorking(tracker_address, type=type): + if not working_only or self.isTrackerWorking(tracker_address): trackers_per_protocol.setdefault(protocol, []).append(tracker_address) return trackers_per_protocol - def getWorkingTrackers(self, type="shared"): + def getWorkingTrackers(self): trackers = { - key: tracker for key, tracker in self.getTrackers(type).items() - if self.isTrackerWorking(key, type) + key: tracker for key, tracker in self.getTrackers().items() + if self.isTrackerWorking(key) } return trackers @@ -280,12 +280,12 @@ class TrackerStorage(object): helper.atomicWrite(self.file_path, json.dumps(self.file_content, indent=2, sort_keys=True).encode("utf8")) self.log.debug("Saved in %.3fs" % (time.time() - s)) - def enoughWorkingTrackers(self, type="shared"): + def enoughWorkingTrackers(self): supported_protocols = self.getSupportedProtocols() if not supported_protocols: return False - trackers_per_protocol = self.getTrackersPerProtocol(type="shared", working_only=True) + trackers_per_protocol = self.getTrackersPerProtocol(working_only=True) if not trackers_per_protocol: return False @@ -309,7 +309,7 @@ class TrackerStorage(object): return unmet_conditions == 0 def discoverTrackers(self, peers): - if self.enoughWorkingTrackers(type="shared"): + if self.enoughWorkingTrackers(): return False self.log.info("Discovering trackers from %s peers..." % len(peers)) @@ -357,7 +357,7 @@ class SiteAnnouncerPlugin(object): tracker_storage.time_discover = time.time() gevent.spawn(tracker_storage.discoverTrackers, self.site.getConnectedPeers()) trackers = super(SiteAnnouncerPlugin, self).getTrackers() - shared_trackers = list(tracker_storage.getTrackers("shared").keys()) + shared_trackers = list(tracker_storage.getTrackers().keys()) if shared_trackers: return trackers + shared_trackers else: @@ -378,7 +378,7 @@ class SiteAnnouncerPlugin(object): @PluginManager.registerTo("FileRequest") class FileRequestPlugin(object): def actionGetTrackers(self, params): - shared_trackers = list(tracker_storage.getWorkingTrackers("shared").keys()) + shared_trackers = list(tracker_storage.getWorkingTrackers().keys()) random.shuffle(shared_trackers) self.response({"trackers": shared_trackers}) From d4239d16f9e9565daa3e1a73ddc7bbb049fa55bf Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Tue, 27 Oct 2020 12:08:54 +0700 Subject: [PATCH 030/191] TrackerShare: send `my` trackers in response to actionGetTrackers even if they don't seem working; add `private` field for hiding trackers --- plugins/TrackerShare/TrackerSharePlugin.py | 60 +++++++++++++++++----- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/plugins/TrackerShare/TrackerSharePlugin.py b/plugins/TrackerShare/TrackerSharePlugin.py index a139e82c..1619fcd6 100644 --- a/plugins/TrackerShare/TrackerSharePlugin.py +++ b/plugins/TrackerShare/TrackerSharePlugin.py @@ -154,13 +154,15 @@ class TrackerStorage(object): # They are to be used as automatically added addresses from the Bootstrap plugin. # Persistent trackers never get deleted. # They are to be used for entries manually added by the user. + # Private trackers never listed to other peer in response of the getTrackers command trackers[tracker_address] = { "time_added": time.time(), "time_success": 0, "latency": 99.0, "num_error": 0, "my": False, - "persistent": False + "persistent": False, + "private": False } self.log.info("New tracker found: %s" % tracker_address) added = True @@ -212,9 +214,29 @@ class TrackerStorage(object): self.log.info("Tracker %s looks down, removing." % tracker_address) del trackers[tracker_address] - def isTrackerWorking(self, tracker_address): - trackers = self.getTrackers() - tracker = trackers[tracker_address] + # Returns the dict of known trackers. + # If condition is None the returned dict can be modified in place, and the + # modifications is reflected in the underlying storage. + # If condition is a function, the dict if filtered by the function, + # and the returned dict has no connection to the underlying storage. + def getTrackers(self, condition = None): + trackers = self.file_content.setdefault("trackers", {}) + + if condition: + trackers = { + key: tracker for key, tracker in trackers.items() + if condition(key) + } + + return trackers + + def resolveTracker(self, tracker): + if isinstance(tracker, str): + tracker = self.getTrackers().get(tracker, None) + return tracker + + def isTrackerWorking(self, tracker): + tracker = self.resolveTracker(tracker) if not tracker: return False @@ -223,8 +245,24 @@ class TrackerStorage(object): return False - def getTrackers(self): - return self.file_content.setdefault("trackers", {}) + def isTrackerShared(self, tracker): + tracker = self.resolveTracker(tracker) + if not tracker: + return False + + if tracker["private"]: + return False + + if tracker["my"]: + return True + + return self.isTrackerWorking(tracker) + + def getWorkingTrackers(self): + return self.getTrackers(self.isTrackerWorking) + + def getSharedTrackers(self): + return self.getTrackers(self.isTrackerShared) def getTrackersPerProtocol(self, working_only=False): if not self.site_announcer: @@ -242,13 +280,6 @@ class TrackerStorage(object): return trackers_per_protocol - def getWorkingTrackers(self): - trackers = { - key: tracker for key, tracker in self.getTrackers().items() - if self.isTrackerWorking(key) - } - return trackers - def getFileContent(self): if not os.path.isfile(self.file_path): open(self.file_path, "w").write("{}") @@ -270,6 +301,7 @@ class TrackerStorage(object): tracker.setdefault("latency", 99.0) tracker.setdefault("my", False) tracker.setdefault("persistent", False) + tracker.setdefault("private", False) tracker["num_error"] = 0 if tracker["my"]: del trackers[address] @@ -378,7 +410,7 @@ class SiteAnnouncerPlugin(object): @PluginManager.registerTo("FileRequest") class FileRequestPlugin(object): def actionGetTrackers(self, params): - shared_trackers = list(tracker_storage.getWorkingTrackers().keys()) + shared_trackers = list(tracker_storage.getSharedTrackers().keys()) random.shuffle(shared_trackers) self.response({"trackers": shared_trackers}) From 3d68a25e130f070e9a140b3c3477d8cc2408b1ae Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Tue, 27 Oct 2020 17:01:40 +0700 Subject: [PATCH 031/191] TrackerShare: refactor --- plugins/TrackerShare/TrackerSharePlugin.py | 109 +++++++++++++++++---- 1 file changed, 88 insertions(+), 21 deletions(-) diff --git a/plugins/TrackerShare/TrackerSharePlugin.py b/plugins/TrackerShare/TrackerSharePlugin.py index 1619fcd6..e6b4454b 100644 --- a/plugins/TrackerShare/TrackerSharePlugin.py +++ b/plugins/TrackerShare/TrackerSharePlugin.py @@ -114,7 +114,7 @@ class TrackerStorage(object): t = max(trackers[tracker_address]["time_added"], trackers[tracker_address]["time_success"]) if tracker_address not in supported_trackers and t < time.time() - self.tracker_down_time_interval: - self.log.info("Tracker %s looks unused, removing." % tracker_address) + self.log.info("Tracker %s seems to be disabled by the configuration, removing." % tracker_address) del trackers[tracker_address] def getSupportedProtocols(self): @@ -158,6 +158,7 @@ class TrackerStorage(object): trackers[tracker_address] = { "time_added": time.time(), "time_success": 0, + "time_error": 0, "latency": 99.0, "num_error": 0, "my": False, @@ -173,32 +174,65 @@ class TrackerStorage(object): return added def onTrackerSuccess(self, tracker_address, latency): - trackers = self.getTrackers() - if tracker_address not in trackers: - return False + tracker = self.resolveTracker(tracker_address) + if not tracker: + return - trackers[tracker_address]["latency"] = latency - trackers[tracker_address]["time_success"] = time.time() - trackers[tracker_address]["num_error"] = 0 + tracker["latency"] = latency + tracker["time_success"] = time.time() + tracker["num_error"] = 0 self.time_success = time.time() def onTrackerError(self, tracker_address): - trackers = self.getTrackers() - if tracker_address not in trackers: - return False - - trackers[tracker_address]["time_error"] = time.time() - trackers[tracker_address]["num_error"] += 1 - - if trackers[tracker_address]["my"] or trackers[tracker_address]["persistent"]: + tracker = self.resolveTracker(tracker_address) + if not tracker: return - if self.time_success < time.time() - self.tracker_down_time_interval / 2: - # Don't drop trackers from the list, if there haven't been any successful announces recently. - # There may be network connectivity issues. + tracker["time_error"] = time.time() + tracker["num_error"] += 1 + + self.considerTrackerDeletion(tracker_address) + + def considerTrackerDeletion(self, tracker_address): + tracker = self.resolveTracker(tracker_address) + if not tracker: return + if tracker["my"] or tracker["persistent"]: + return + + error_limit = self.getSuccessiveErrorLimit(tracker_address) + + if tracker["num_error"] > error_limit: + if self.isTrackerDown(tracker_address): + self.log.info("Tracker %s looks down, removing." % tracker_address) + self.deleteTracker(tracker_address) + elif self.areWayTooManyTrackers(tracker_address): + self.log.info( + "Tracker %s has %d successive errors. Looks like we have too many trackers, so removing." % ( + tracker_address, + tracker["num_error"])) + self.deleteTracker(tracker_address) + + def areWayTooManyTrackers(self, tracker_address): + # Prevent the tracker list overgrowth by hard limiting the maximum size + + protocol = self.getNormalizedTrackerProtocol(tracker_address) or "" + + nr_trackers_for_protocol = len(self.getTrackersPerProtocol().get(protocol, [])) + nr_trackers = len(self.getTrackers()) + + hard_limit_mult = 5 + hard_limit_for_protocol = self.getTrackerLimitForProtocol(protocol) * hard_limit_mult + hard_limit = config.shared_trackers_limit * hard_limit_mult + + if (nr_trackers_for_protocol > hard_limit_for_protocol) and (nr_trackers > hard_limit): + return True + + return False + + def getSuccessiveErrorLimit(self, tracker_address): protocol = self.getNormalizedTrackerProtocol(tracker_address) or "" nr_working_trackers_for_protocol = len(self.getTrackersPerProtocol(working_only=True).get(protocol, [])) @@ -210,9 +244,7 @@ class TrackerStorage(object): if nr_working_trackers >= config.shared_trackers_limit: error_limit = 5 - if trackers[tracker_address]["num_error"] > error_limit and trackers[tracker_address]["time_success"] < time.time() - self.tracker_down_time_interval: - self.log.info("Tracker %s looks down, removing." % tracker_address) - del trackers[tracker_address] + return error_limit # Returns the dict of known trackers. # If condition is None the returned dict can be modified in place, and the @@ -230,11 +262,45 @@ class TrackerStorage(object): return trackers + def deleteTracker(self, tracker): + trackers = self.getTrackers() + if isinstance(tracker, str): + if trackers[tracker]: + del trackers[tracker] + else: + trackers.remove(tracker) + def resolveTracker(self, tracker): if isinstance(tracker, str): tracker = self.getTrackers().get(tracker, None) return tracker + def isTrackerDown(self, tracker): + tracker = self.resolveTracker(tracker) + if not tracker: + return False + + # Don't consider any trackers down if there haven't been any successful announces at all + if self.time_success < 1: + return False + + time_success = max(tracker["time_added"], tracker["time_success"]) + time_error = max(tracker["time_added"], tracker["time_error"]) + + if time_success >= time_error: + return False + + # Deadline is calculated based on the time of the last successful announce, + # not based on the current time. + # There may be network connectivity issues, if there haven't been any + # successful announces recently. + + deadline = self.time_success - self.tracker_down_time_interval + if time_success >= deadline: + return False + + return True + def isTrackerWorking(self, tracker): tracker = self.resolveTracker(tracker) if not tracker: @@ -298,6 +364,7 @@ class TrackerStorage(object): for address, tracker in list(trackers.items()): tracker.setdefault("time_added", time.time()) tracker.setdefault("time_success", 0) + tracker.setdefault("time_error", 0) tracker.setdefault("latency", 99.0) tracker.setdefault("my", False) tracker.setdefault("persistent", False) From b6ae96db5a5bfab3d54d1598b9e8242bf4197d38 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 28 Oct 2020 14:48:30 +0700 Subject: [PATCH 032/191] Implement overriding log levels for separate modules --- src/loglevel_overrides.py | 9 ++++++++ src/main.py | 1 + src/util/SelectiveLogger.py | 43 +++++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 src/loglevel_overrides.py create mode 100644 src/util/SelectiveLogger.py diff --git a/src/loglevel_overrides.py b/src/loglevel_overrides.py new file mode 100644 index 00000000..5622e523 --- /dev/null +++ b/src/loglevel_overrides.py @@ -0,0 +1,9 @@ +# This file is for adding rules for selectively enabling debug logging +# when working on the code. +# Add your rules here and skip this file when committing changes. + +#import re +#from util import SelectiveLogger +# +#SelectiveLogger.addLogLevelRaisingRule("ConnServer") +#SelectiveLogger.addLogLevelRaisingRule(re.compile(r'^Site:')) diff --git a/src/main.py b/src/main.py index 7a0188e7..209bb9d2 100644 --- a/src/main.py +++ b/src/main.py @@ -4,6 +4,7 @@ import sys import stat import time import logging +import loglevel_overrides startup_errors = [] def startupError(msg): diff --git a/src/util/SelectiveLogger.py b/src/util/SelectiveLogger.py new file mode 100644 index 00000000..fcdcba0a --- /dev/null +++ b/src/util/SelectiveLogger.py @@ -0,0 +1,43 @@ +import logging +import re + +log_level_raising_rules = [] + +def addLogLevelRaisingRule(rule, level=None): + if level is None: + level = logging.INFO + log_level_raising_rules.append({ + "rule": rule, + "level": level + }) + +def matchLogLevelRaisingRule(name): + for rule in log_level_raising_rules: + if isinstance(rule["rule"], re.Pattern): + if rule["rule"].search(name): + return rule["level"] + else: + if rule["rule"] == name: + return rule["level"] + return None + +class SelectiveLogger(logging.getLoggerClass()): + def __init__(self, name, level=logging.NOTSET): + return super().__init__(name, level) + + def raiseLevel(self, level): + raised_level = matchLogLevelRaisingRule(self.name) + if raised_level is not None: + if level < raised_level: + level = raised_level + return level + + def isEnabledFor(self, level): + level = self.raiseLevel(level) + return super().isEnabledFor(level) + + def _log(self, level, msg, args, **kwargs): + level = self.raiseLevel(level) + return super()._log(level, msg, args, **kwargs) + +logging.setLoggerClass(SelectiveLogger) From f1d91989d54ad70b1cc9d3b80118ab573dcdb716 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 28 Oct 2020 14:51:17 +0700 Subject: [PATCH 033/191] SiteAnnouncer: make use of a separate logger instance, not the Site's logger --- src/Site/SiteAnnouncer.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index 2fd63e82..b50a01fe 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -1,6 +1,7 @@ import random import time import hashlib +import logging import re import collections @@ -24,6 +25,8 @@ global_stats = collections.defaultdict(lambda: collections.defaultdict(int)) class SiteAnnouncer(object): def __init__(self, site): self.site = site + self.log = logging.getLogger("Site:%s SiteAnnouncer" % self.site.address_short) + self.stats = {} self.fileserver_port = config.fileserver_port self.peer_id = self.site.connection_server.peer_id @@ -74,7 +77,7 @@ class SiteAnnouncer(object): if time.time() - self.time_last_announce < 30 and not force: return # No reannouncing within 30 secs if force: - self.site.log.debug("Force reannounce in mode %s" % mode) + self.log.debug("Force reannounce in mode %s" % mode) self.fileserver_port = config.fileserver_port self.time_last_announce = time.time() @@ -82,7 +85,7 @@ class SiteAnnouncer(object): trackers = self.getAnnouncingTrackers(mode) if config.verbose: - self.site.log.debug("Tracker announcing, trackers: %s" % trackers) + self.log.debug("Tracker announcing, trackers: %s" % trackers) errors = [] slow = [] @@ -96,7 +99,7 @@ class SiteAnnouncer(object): time_announce_allowed = time.time() - 60 * min(30, tracker_stats["num_error"]) if tracker_stats["num_error"] > 5 and tracker_stats["time_request"] > time_announce_allowed and not force: if config.verbose: - self.site.log.debug("Tracker %s looks unreliable, announce skipped (error: %s)" % (tracker, tracker_stats["num_error"])) + self.log.debug("Tracker %s looks unreliable, announce skipped (error: %s)" % (tracker, tracker_stats["num_error"])) continue thread = self.site.greenlet_manager.spawn(self.announceTracker, tracker, mode=mode) threads.append(thread) @@ -129,15 +132,15 @@ class SiteAnnouncer(object): else: announced_to = "%s/%s trackers" % (num_announced, len(threads)) if mode != "update" or config.verbose: - self.site.log.debug( + self.log.debug( "Announced in mode %s to %s in %.3fs, errors: %s, slow: %s" % (mode, announced_to, time.time() - s, errors, slow) ) else: if len(threads) > 1: - self.site.log.error("Announce to %s trackers in %.3fs, failed" % (len(threads), time.time() - s)) + self.log.error("Announce to %s trackers in %.3fs, failed" % (len(threads), time.time() - s)) if len(threads) == 1 and mode != "start": # Move to next tracker - self.site.log.debug("Tracker failed, skipping to next one...") + self.log.debug("Tracker failed, skipping to next one...") self.site.greenlet_manager.spawnLater(1.0, self.announce, force=force, mode=mode, pex=pex) self.updateWebsocket(trackers="announced") @@ -177,7 +180,7 @@ class SiteAnnouncer(object): s = time.time() address_parts = self.getAddressParts(tracker) if not address_parts: - self.site.log.warning("Tracker %s error: Invalid address" % tracker) + self.log.warning("Tracker %s error: Invalid address" % tracker) return False if tracker not in self.stats: @@ -188,7 +191,7 @@ class SiteAnnouncer(object): self.stats[tracker]["time_request"] = time.time() global_stats[tracker]["time_request"] = time.time() if config.verbose: - self.site.log.debug("Tracker announcing to %s (mode: %s)" % (tracker, mode)) + self.log.debug("Tracker announcing to %s (mode: %s)" % (tracker, mode)) if mode == "update": num_want = 10 else: @@ -202,7 +205,7 @@ class SiteAnnouncer(object): else: raise AnnounceError("Unknown protocol: %s" % address_parts["protocol"]) except Exception as err: - self.site.log.warning("Tracker %s announce failed: %s in mode %s" % (tracker, Debug.formatException(err), mode)) + self.log.warning("Tracker %s announce failed: %s in mode %s" % (tracker, Debug.formatException(err), mode)) error = err if error: @@ -249,7 +252,7 @@ class SiteAnnouncer(object): self.site.updateWebsocket(peers_added=added) if config.verbose: - self.site.log.debug( + self.log.debug( "Tracker result: %s://%s (found %s peers, new: %s, total: %s)" % (address_parts["protocol"], address_parts["address"], len(peers), added, len(self.site.peers)) ) @@ -281,7 +284,7 @@ class SiteAnnouncer(object): time.sleep(0.1) if done == query_num: break - self.site.log.debug("Pex result: from %s peers got %s new peers." % (done, total_added)) + self.log.debug("Pex result: from %s peers got %s new peers." % (done, total_added)) def updateWebsocket(self, **kwargs): if kwargs: From 112c778c28cd9390a9ef714d861a080f99d7fa85 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 28 Oct 2020 14:53:56 +0700 Subject: [PATCH 034/191] Peer.py: allow overriding the log level Not a best solution, but with minimal code changes. --- src/Peer/Peer.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index 03cc1f47..d8e28121 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -32,6 +32,8 @@ class Peer(object): self.site = site self.key = "%s:%s" % (ip, port) + self.log_level = logging.DEBUG + self.connection = None self.connection_server = connection_server self.has_hashfield = False # Lazy hashfield object not created yet @@ -59,12 +61,18 @@ class Peer(object): return getattr(self, key) def log(self, text): - if not config.verbose: - return # Only log if we are in debug mode + if self.log_level <= logging.DEBUG: + if not config.verbose: + return # Only log if we are in debug mode + + logger = None + if self.site: - self.site.log.debug("%s:%s %s" % (self.ip, self.port, text)) + logger = self.site.log else: - logging.debug("%s:%s %s" % (self.ip, self.port, text)) + logger = logging.getLogger() + + logger.log(self.log_level, "%s:%s %s" % (self.ip, self.port, text)) # Connect to host def connect(self, connection=None): From 3ca323f8b07dbde15106152922e2770be6f47a24 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 28 Oct 2020 17:56:11 +0700 Subject: [PATCH 035/191] FileServer: move loadTrackersFile() to a separate thread --- src/File/FileServer.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index 5f18c645..186d88a3 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -318,11 +318,21 @@ class FileServer(ConnectionServer): site.sendMyHashfield(3) site.updateHashfield(3) + # Periodic reloading of tracker files + def reloadTrackerFilesThread(self): + # TODO: + # This should probably be more sophisticated. + # We should check if the files have actually changed, + # and do it more often. + interval = 60 * 10 + while 1: + time.sleep(interval) + config.loadTrackersFile() + # Announce sites every 20 min def announceSites(self): time.sleep(5 * 60) # Sites already announced on startup while 1: - config.loadTrackersFile() s = time.time() for address, site in list(self.sites.items()): if not site.isServing(): @@ -387,6 +397,7 @@ class FileServer(ConnectionServer): if check_sites: # Open port, Update sites, Check files integrity gevent.spawn(self.checkSites) + thread_reaload_tracker_files = gevent.spawn(self.reloadTrackerFilesThread) thread_announce_sites = gevent.spawn(self.announceSites) thread_cleanup_sites = gevent.spawn(self.cleanupSites) thread_wakeup_watcher = gevent.spawn(self.wakeupWatcher) From 511a90a5c58b617298da557bb2c2bd773175b28f Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 28 Oct 2020 20:31:48 +0700 Subject: [PATCH 036/191] Redesign Site.needConnections() --- src/File/FileServer.py | 10 ++--- src/Peer/Peer.py | 9 +++-- src/Site/Site.py | 87 +++++++++++++++++++++++++++++++++--------- 3 files changed, 80 insertions(+), 26 deletions(-) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index 186d88a3..c1134800 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -295,12 +295,12 @@ class FileServer(ConnectionServer): elif site.bad_files: site.retryBadFiles() - if time.time() - site.settings.get("modified", 0) < 60 * 60 * 24 * 7: - # Keep active connections if site has been modified witin 7 days - connected_num = site.needConnections(check_site_on_reconnect=True) + # Keep active connections + connected_num = site.needConnections(check_site_on_reconnect=True) - if connected_num < config.connected_limit: # This site has small amount of peers, protect them from closing - peers_protected.update([peer.key for peer in site.getConnectedPeers()]) + if connected_num < config.connected_limit: + # This site has small amount of peers, protect them from closing + peers_protected.update([peer.key for peer in site.getConnectedPeers()]) time.sleep(1) # Prevent too quick request diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index d8e28121..75eb190f 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -74,6 +74,9 @@ class Peer(object): logger.log(self.log_level, "%s:%s %s" % (self.ip, self.port, text)) + def isConnected(self): + return self.connection and self.connection.connected + # Connect to host def connect(self, connection=None): if self.reputation < -10: @@ -249,11 +252,11 @@ class Peer(object): return buff # Send a ping request - def ping(self): + def ping(self, timeout=10.0, tryes=3): response_time = None - for retry in range(1, 3): # Retry 3 times + for retry in range(1, tryes): # Retry 3 times s = time.time() - with gevent.Timeout(10.0, False): # 10 sec timeout, don't raise exception + with gevent.Timeout(timeout, False): res = self.request("ping") if res and "body" in res and res["body"] == b"Pong!": diff --git a/src/Site/Site.py b/src/Site/Site.py index 354fe9c0..fff592cd 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -853,36 +853,87 @@ class Site(object): if self.isServing(): self.announcer.announce(*args, **kwargs) - # Keep connections to get the updates - def needConnections(self, num=None, check_site_on_reconnect=False): - if num is None: - if len(self.peers) < 50: - num = 3 - else: - num = 6 - need = min(len(self.peers), num, config.connected_limit) # Need 5 peer, but max total peers + def getPreferableActiveConnectionCount(self): + if not self.isServing(): + return 0 + age = time.time() - self.settings.get("modified", 0) + count = 0 + + if age < 60 * 60: + count = 10 + elif age < 60 * 60 * 5: + count = 8 + elif age < 60 * 60 * 24: + count = 6 + elif age < 60 * 60 * 24 * 3: + count = 4 + elif age < 60 * 60 * 24 * 7: + count = 2 + + if len(self.peers) < 50: + count = max(count, 5) + + return count + + def tryConnectingToMorePeers(self, more=1, pex=True, try_harder=False): + max_peers = more * 2 + 10 + if try_harder: + max_peers += 10000 + + connected = 0 + for peer in self.getRecentPeers(max_peers): + if not peer.isConnected(): + if pex: + peer.pex() + else: + peer.ping(timeout=2.0, tryes=1) + + if peer.isConnected(): + connected += 1 + + if connected >= more: + break + + return connected + + def bringConnections(self, need=1, check_site_on_reconnect=False, pex=True, try_harder=False): connected = len(self.getConnectedPeers()) - connected_before = connected self.log.debug("Need connections: %s, Current: %s, Total: %s" % (need, connected, len(self.peers))) - if connected < need: # Need more than we have - for peer in self.getRecentPeers(30): - if not peer.connection or not peer.connection.connected: # No peer connection or disconnected - peer.pex() # Initiate peer exchange - if peer.connection and peer.connection.connected: - connected += 1 # Successfully connected - if connected >= need: - break + if connected < need: + connected += self.tryConnectingToMorePeers(more=(need-connected), pex=pex, try_harder=try_harder) self.log.debug( "Connected before: %s, after: %s. Check site: %s." % (connected_before, connected, check_site_on_reconnect) ) if check_site_on_reconnect and connected_before == 0 and connected > 0 and self.connection_server.has_internet: - gevent.spawn(self.update, check_files=False) + self.greenlet_manager.spawn(self.update, check_files=False) + + return connected + + # Keep connections + def needConnections(self, num=None, check_site_on_reconnect=False, pex=True): + if num is None: + num = self.getPreferableActiveConnectionCount() + + need = min(len(self.peers), num, config.connected_limit) + + connected = self.bringConnections( + need=need, + check_site_on_reconnect=check_site_on_reconnect, + pex=pex, + try_harder=False) + + if connected < need: + self.greenlet_manager.spawnLater(1.0, self.bringConnections, + need=need, + check_site_on_reconnect=check_site_on_reconnect, + pex=pex, + try_harder=True) return connected From 8fd88c50f923fe2338fc793e7ea5a18473c56e38 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 28 Oct 2020 23:38:17 +0700 Subject: [PATCH 037/191] Redesign cleanupSites() and all the related stuff and rename it to periodic maintenance. --- src/Config.py | 2 +- src/File/FileServer.py | 68 +++++++------------ src/Peer/Peer.py | 26 +++++++- src/Site/Site.py | 125 +++++++++++++++++++++++++---------- src/util/CircularIterator.py | 34 ++++++++++ src/util/__init__.py | 1 + 6 files changed, 175 insertions(+), 81 deletions(-) create mode 100644 src/util/CircularIterator.py diff --git a/src/Config.py b/src/Config.py index 41c914f2..70b284bf 100644 --- a/src/Config.py +++ b/src/Config.py @@ -253,7 +253,7 @@ class Config(object): self.parser.add_argument('--size_limit', help='Default site size limit in MB', default=10, type=int, metavar='limit') self.parser.add_argument('--file_size_limit', help='Maximum per file size limit in MB', default=10, type=int, metavar='limit') - self.parser.add_argument('--connected_limit', help='Max connected peer per site', default=8, type=int, metavar='connected_limit') + self.parser.add_argument('--connected_limit', help='Max connected peer per site', default=10, type=int, metavar='connected_limit') self.parser.add_argument('--global_connected_limit', help='Max connections', default=512, type=int, metavar='global_connected_limit') self.parser.add_argument('--workers', help='Download workers per site', default=5, type=int, metavar='workers') diff --git a/src/File/FileServer.py b/src/File/FileServer.py index c1134800..fbc3cc85 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -10,6 +10,7 @@ from gevent.server import StreamServer import util from util import helper +from util import CircularIterator from Config import config from .FileRequest import FileRequest from Peer import PeerPortchecker @@ -18,7 +19,6 @@ from Connection import ConnectionServer from Plugin import PluginManager from Debug import Debug - @PluginManager.acceptPlugins class FileServer(ConnectionServer): @@ -259,55 +259,35 @@ class FileServer(ConnectionServer): check_thread.join(timeout=5) self.log.debug("Checksites done in %.3fs" % (time.time() - s)) - def cleanupSites(self): + def sitesMaintenanceThread(self): import gc startup = True - time.sleep(5 * 60) # Sites already cleaned up on startup - peers_protected = set([]) + + short_timeout = 2 + long_timeout = 60 * 5 + + circular_iterator = CircularIterator() + while 1: - # Sites health care every 20 min + if circular_iterator.isWrapped(): + time.sleep(long_timeout) + circular_iterator.resetSuccessiveCount() + gc.collect() # Explicit garbage collection + self.log.debug( - "Running site cleanup, connections: %s, internet: %s, protected peers: %s" % - (len(self.connections), self.has_internet, len(peers_protected)) + "Running site cleanup, connections: %s, internet: %s" % + (len(self.connections), self.has_internet) ) - for address, site in list(self.sites.items()): - if not site.isServing(): - continue + site = circular_iterator.next(list(self.sites.values())) + if site: + done = site.runPeriodicMaintenance(startup=startup) + if done: + time.sleep(short_timeout) + site = None - if not startup: - site.cleanupPeers(peers_protected) - - time.sleep(1) # Prevent too quick request - - peers_protected = set([]) - for address, site in list(self.sites.items()): - if not site.isServing(): - continue - - if site.peers: - with gevent.Timeout(10, exception=False): - site.announcer.announcePex() - - # Last check modification failed - if site.content_updated is False: - site.update() - elif site.bad_files: - site.retryBadFiles() - - # Keep active connections - connected_num = site.needConnections(check_site_on_reconnect=True) - - if connected_num < config.connected_limit: - # This site has small amount of peers, protect them from closing - peers_protected.update([peer.key for peer in site.getConnectedPeers()]) - - time.sleep(1) # Prevent too quick request - - site = None - gc.collect() # Implicit garbage collection - startup = False - time.sleep(60 * 20) + if circular_iterator.isWrapped(): + startup = False def announceSite(self, site): site.announce(mode="update", pex=False) @@ -399,7 +379,7 @@ class FileServer(ConnectionServer): thread_reaload_tracker_files = gevent.spawn(self.reloadTrackerFilesThread) thread_announce_sites = gevent.spawn(self.announceSites) - thread_cleanup_sites = gevent.spawn(self.cleanupSites) + thread_sites_maintenance = gevent.spawn(self.sitesMaintenanceThread) thread_wakeup_watcher = gevent.spawn(self.wakeupWatcher) ConnectionServer.listen(self) diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index 75eb190f..0a518fdc 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -46,6 +46,7 @@ class Peer(object): self.is_tracker_connection = False # Tracker connection instead of normal peer self.reputation = 0 # More likely to connect if larger self.last_content_json_update = 0.0 # Modify date of last received content.json + self.protected = 0 self.connection_error = 0 # Series of connection error self.hash_failed = 0 # Number of bad files from peer @@ -74,9 +75,26 @@ class Peer(object): logger.log(self.log_level, "%s:%s %s" % (self.ip, self.port, text)) + # Site marks its Peers protected, if it has not enough peers connected. + # This is to be used to prevent disconnecting from peers when doing + # a periodic cleanup. + def markProtected(self, interval=60*20): + self.protected = time.time() + interval + + def isProtected(self): + if self.protected > 0: + if self.protected < time.time(): + self.protected = 0 + return self.protected > 0 + def isConnected(self): + if self.connection and not self.connection.connected: + self.connection = None return self.connection and self.connection.connected + def isTtlExpired(self, ttl): + return (time.time() - self.time_found) > ttl + # Connect to host def connect(self, connection=None): if self.reputation < -10: @@ -115,6 +133,11 @@ class Peer(object): self.connection = None return self.connection + def disconnect(self, reason="Unknown"): + if self.connection: + self.connection.close(reason) + self.connection = None + # Check if we have connection to peer def findConnection(self): if self.connection and self.connection.connected: # We have connection to peer @@ -400,8 +423,7 @@ class Peer(object): if self.site and self in self.site.peers_recent: self.site.peers_recent.remove(self) - if self.connection: - self.connection.close(reason) + self.disconnect(reason) # - EVENTS - diff --git a/src/Site/Site.py b/src/Site/Site.py index fff592cd..b941b035 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -40,6 +40,9 @@ class Site(object): self.log = logging.getLogger("Site:%s" % self.address_short) self.addEventListeners() + self.periodic_maintenance_interval = 60 * 20 + self.periodic_maintenance_timestamp = 0 + self.content = None # Load content.json self.peers = {} # Key: ip:port, Value: Peer.Peer self.peers_recent = collections.deque(maxlen=150) @@ -853,6 +856,11 @@ class Site(object): if self.isServing(): self.announcer.announce(*args, **kwargs) + # The engine tries to maintain the number of active connections: + # >= getPreferableActiveConnectionCount() + # and + # <= getActiveConnectionCountLimit() + def getPreferableActiveConnectionCount(self): if not self.isServing(): return 0 @@ -874,8 +882,16 @@ class Site(object): if len(self.peers) < 50: count = max(count, 5) + count = min(count, config.connected_limit) + return count + def getActiveConnectionCountLimit(self): + count_above_preferable = 2 + limit = self.getPreferableActiveConnectionCount() + count_above_preferable + limit = min(limit, config.connected_limit) + return limit + def tryConnectingToMorePeers(self, more=1, pex=True, try_harder=False): max_peers = more * 2 + 10 if try_harder: @@ -920,7 +936,7 @@ class Site(object): if num is None: num = self.getPreferableActiveConnectionCount() - need = min(len(self.peers), num, config.connected_limit) + need = min(len(self.peers), num) connected = self.bringConnections( need=need, @@ -935,8 +951,15 @@ class Site(object): pex=pex, try_harder=True) + if connected < num: + self.markConnectedPeersProtected() + return connected + def markConnectedPeersProtected(self): + for peer in site.getConnectedPeers(): + peer.markProtected() + # Return: Probably peers verified to be connectable recently def getConnectablePeers(self, need_num=5, ignore=[], allow_private=True): peers = list(self.peers.values()) @@ -1022,53 +1045,87 @@ class Site(object): back.append(peer) return back - # Cleanup probably dead peers and close connection if too much - def cleanupPeers(self, peers_protected=[]): + def removeDeadPeers(self): peers = list(self.peers.values()) - if len(peers) > 20: - # Cleanup old peers - removed = 0 - if len(peers) > 1000: - ttl = 60 * 60 * 1 - else: - ttl = 60 * 60 * 4 + if len(peers) <= 20: + return - for peer in peers: - if peer.connection and peer.connection.connected: - continue - if peer.connection and not peer.connection.connected: - peer.connection = None # Dead connection - if time.time() - peer.time_found > ttl: # Not found on tracker or via pex in last 4 hour - peer.remove("Time found expired") - removed += 1 - if removed > len(peers) * 0.1: # Don't remove too much at once - break + removed = 0 + if len(peers) > 1000: + ttl = 60 * 60 * 1 + elif len(peers) > 100: + ttl = 60 * 60 * 4 + else: + ttl = 60 * 60 * 8 - if removed: - self.log.debug("Cleanup peers result: Removed %s, left: %s" % (removed, len(self.peers))) + for peer in peers: + if peer.isConnected() or peer.isProtected(): + continue + if peer.isTtlExpired(ttl): + peer.remove("TTL expired") + removed += 1 + if removed > len(peers) * 0.1: # Don't remove too much at once + break - # Close peers over the limit - closed = 0 - connected_peers = [peer for peer in self.getConnectedPeers() if peer.connection.connected] # Only fully connected peers - need_to_close = len(connected_peers) - config.connected_limit + if removed: + self.log.debug("Cleanup peers result: Removed %s, left: %s" % (removed, len(self.peers))) - if closed < need_to_close: - # Try to keep connections with more sites + # Cleanup probably dead peers and close connection if too much + def cleanupPeers(self): + self.removeDeadPeers() + + limit = self.getActiveConnectionCountLimit() + connected_peers = [peer for peer in self.getConnectedPeers() if peer.isConnected()] # Only fully connected peers + need_to_close = len(connected_peers) - limit + + if need_to_close > 0: + closed = 0 for peer in sorted(connected_peers, key=lambda peer: min(peer.connection.sites, 5)): - if not peer.connection: + if not peer.isConnected(): continue - if peer.key in peers_protected: + if peer.isProtected(): continue if peer.connection.sites > 5: break - peer.connection.close("Cleanup peers") - peer.connection = None + peer.disconnect("Cleanup peers") closed += 1 if closed >= need_to_close: break - if need_to_close > 0: - self.log.debug("Connected: %s, Need to close: %s, Closed: %s" % (len(connected_peers), need_to_close, closed)) + self.log.debug("Connected: %s, Need to close: %s, Closed: %s" % ( + len(connected_peers), need_to_close, closed)) + + def runPeriodicMaintenance(self, startup=False, force=False): + if not self.isServing(): + return False + + scheduled_time = self.periodic_maintenance_timestamp + self.periodic_maintenance_interval + + if time.time() < scheduled_time and not force: + return False + + self.periodic_maintenance_timestamp = time.time() + + self.log.debug("runPeriodicMaintenance: startup=%s" % startup) + + if not startup: + self.cleanupPeers() + + if self.peers: + with gevent.Timeout(10, exception=False): + self.announcer.announcePex() + + # Last check modification failed + if self.content_updated is False: + self.update() + elif self.bad_files: + self.retryBadFiles() + + self.needConnections(check_site_on_reconnect=True) + + self.periodic_maintenance_timestamp = time.time() + + return True # Send hashfield to peers def sendMyHashfield(self, limit=5): diff --git a/src/util/CircularIterator.py b/src/util/CircularIterator.py new file mode 100644 index 00000000..3466092e --- /dev/null +++ b/src/util/CircularIterator.py @@ -0,0 +1,34 @@ +import random + +class CircularIterator: + def __init__(self): + self.successive_count = 0 + self.last_size = 0 + self.index = -1 + + def next(self, items): + self.last_size = len(items) + + if self.last_size == 0: + return None + + if self.index < 0: + self.index = random.randint(0, self.last_size) + else: + self.index += 1 + + self.index = self.index % self.last_size + + self.successive_count += 1 + + return items[self.index] + + def resetSuccessiveCount(self): + self.successive_count = 0 + + def getSuccessiveCount(self): + return self.successive_count + + def isWrapped(self): + return self.successive_count >= self.last_size + diff --git a/src/util/__init__.py b/src/util/__init__.py index ab8a8b88..f00c1459 100644 --- a/src/util/__init__.py +++ b/src/util/__init__.py @@ -1,4 +1,5 @@ from .Cached import Cached +from .CircularIterator import CircularIterator from .Event import Event from .Noparallel import Noparallel from .Pooled import Pooled From adf40dbb6b24234bc72cec77710632ceae52cbd6 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Thu, 29 Oct 2020 12:57:16 +0700 Subject: [PATCH 038/191] Refactor SiteAnnouncer.announce --- src/Site/SiteAnnouncer.py | 193 ++++++++++++++++++++++---------------- 1 file changed, 114 insertions(+), 79 deletions(-) diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index b50a01fe..1440eb57 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -13,6 +13,7 @@ from Debug import Debug from util import helper from greenlet import GreenletExit import util +from util import CircularIterator class AnnounceError(Exception): @@ -30,7 +31,7 @@ class SiteAnnouncer(object): self.stats = {} self.fileserver_port = config.fileserver_port self.peer_id = self.site.connection_server.peer_id - self.last_tracker_id = random.randint(0, 10) + self.tracker_circular_iterator = CircularIterator() self.time_last_announce = 0 def getTrackers(self): @@ -49,15 +50,58 @@ class SiteAnnouncer(object): return trackers - def getAnnouncingTrackers(self, mode): + def shouldTrackerBeTemporarilyIgnored(self, tracker, mode, force): + if not tracker: + return True + + if force: + return False + + now = time.time() + + # Throttle accessing unresponsive trackers + tracker_stats = global_stats[tracker] + delay = min(30 * tracker_stats["num_error"], 60 * 10) + time_announce_allowed = tracker_stats["time_request"] + delay + if now < time_announce_allowed: + return True + + return False + + def getAnnouncingTrackers(self, mode, force): trackers = self.getSupportedTrackers() - if trackers and (mode == "update" or mode == "more"): # Only announce on one tracker, increment the queried tracker id - self.last_tracker_id += 1 - self.last_tracker_id = self.last_tracker_id % len(trackers) - trackers_announcing = [trackers[self.last_tracker_id]] # We only going to use this one + if trackers and (mode == "update" or mode == "more"): + + # Choose just 2 trackers to announce to + + trackers_announcing = [] + + # One is the next in sequence + + self.tracker_circular_iterator.resetSuccessiveCount() + while 1: + tracker = self.tracker_circular_iterator.next(trackers) + if not self.shouldTrackerBeTemporarilyIgnored(tracker, mode, force): + trackers_announcing.append(tracker) + break + if self.tracker_circular_iterator.isWrapped(): + break + + # And one is just random + + shuffled_trackers = random.sample(trackers, len(trackers)) + for tracker in shuffled_trackers: + if tracker in trackers_announcing: + continue + if not self.shouldTrackerBeTemporarilyIgnored(tracker, mode, force): + trackers_announcing.append(tracker) + break else: - trackers_announcing = trackers + trackers_announcing = [ + tracker for tracker in trackers + if not self.shouldTrackerBeTemporarilyIgnored(tracker, mode, force) + ] return trackers_announcing @@ -76,83 +120,18 @@ class SiteAnnouncer(object): def announce(self, force=False, mode="start", pex=True): if time.time() - self.time_last_announce < 30 and not force: return # No reannouncing within 30 secs - if force: - self.log.debug("Force reannounce in mode %s" % mode) + + self.log.debug("announce: force=%s, mode=%s, pex=%s" % (force, mode, pex)) self.fileserver_port = config.fileserver_port self.time_last_announce = time.time() - trackers = self.getAnnouncingTrackers(mode) - - if config.verbose: - self.log.debug("Tracker announcing, trackers: %s" % trackers) - - errors = [] - slow = [] - s = time.time() - threads = [] - num_announced = 0 - - for tracker in trackers: # Start announce threads - tracker_stats = global_stats[tracker] - # Reduce the announce time for trackers that looks unreliable - time_announce_allowed = time.time() - 60 * min(30, tracker_stats["num_error"]) - if tracker_stats["num_error"] > 5 and tracker_stats["time_request"] > time_announce_allowed and not force: - if config.verbose: - self.log.debug("Tracker %s looks unreliable, announce skipped (error: %s)" % (tracker, tracker_stats["num_error"])) - continue - thread = self.site.greenlet_manager.spawn(self.announceTracker, tracker, mode=mode) - threads.append(thread) - thread.tracker = tracker - - time.sleep(0.01) - self.updateWebsocket(trackers="announcing") - - gevent.joinall(threads, timeout=20) # Wait for announce finish - - for thread in threads: - if thread.value is None: - continue - if thread.value is not False: - if thread.value > 1.0: # Takes more than 1 second to announce - slow.append("%.2fs %s" % (thread.value, thread.tracker)) - num_announced += 1 - else: - if thread.ready(): - errors.append(thread.tracker) - else: # Still running - slow.append("30s+ %s" % thread.tracker) - - # Save peers num - self.site.settings["peers"] = len(self.site.peers) - - if len(errors) < len(threads): # At least one tracker finished - if len(trackers) == 1: - announced_to = trackers[0] - else: - announced_to = "%s/%s trackers" % (num_announced, len(threads)) - if mode != "update" or config.verbose: - self.log.debug( - "Announced in mode %s to %s in %.3fs, errors: %s, slow: %s" % - (mode, announced_to, time.time() - s, errors, slow) - ) - else: - if len(threads) > 1: - self.log.error("Announce to %s trackers in %.3fs, failed" % (len(threads), time.time() - s)) - if len(threads) == 1 and mode != "start": # Move to next tracker - self.log.debug("Tracker failed, skipping to next one...") - self.site.greenlet_manager.spawnLater(1.0, self.announce, force=force, mode=mode, pex=pex) - - self.updateWebsocket(trackers="announced") + trackers = self.getAnnouncingTrackers(mode, force) + self.log.debug("Chosen trackers: %s" % trackers) + self.announceToTrackers(trackers, force=force, mode=mode) if pex: - self.updateWebsocket(pex="announcing") - if mode == "more": # Need more peers - self.announcePex(need_num=10) - else: - self.announcePex() - - self.updateWebsocket(pex="announced") + self.announcePex() def getTrackerHandler(self, protocol): return None @@ -258,8 +237,62 @@ class SiteAnnouncer(object): ) return time.time() - s + def announceToTrackers(self, trackers, force=False, mode="start"): + errors = [] + slow = [] + s = time.time() + threads = [] + num_announced = 0 + + for tracker in trackers: # Start announce threads + thread = self.site.greenlet_manager.spawn(self.announceTracker, tracker, mode=mode) + threads.append(thread) + thread.tracker = tracker + + time.sleep(0.01) + self.updateWebsocket(trackers="announcing") + + gevent.joinall(threads, timeout=20) # Wait for announce finish + + for thread in threads: + if thread.value is None: + continue + if thread.value is not False: + if thread.value > 1.0: # Takes more than 1 second to announce + slow.append("%.2fs %s" % (thread.value, thread.tracker)) + num_announced += 1 + else: + if thread.ready(): + errors.append(thread.tracker) + else: # Still running + slow.append("30s+ %s" % thread.tracker) + + # Save peers num + self.site.settings["peers"] = len(self.site.peers) + + if len(errors) < len(threads): # At least one tracker finished + if len(trackers) == 1: + announced_to = trackers[0] + else: + announced_to = "%s/%s trackers" % (num_announced, len(threads)) + if mode != "update" or config.verbose: + self.log.debug( + "Announced in mode %s to %s in %.3fs, errors: %s, slow: %s" % + (mode, announced_to, time.time() - s, errors, slow) + ) + else: + if len(threads) > 1: + self.log.error("Announce to %s trackers in %.3fs, failed" % (len(threads), time.time() - s)) + if len(threads) > 1 and mode != "start": # Move to next tracker + self.log.debug("Tracker failed, skipping to next one...") + self.site.greenlet_manager.spawnLater(5.0, self.announce, force=force, mode=mode, pex=False) + + self.updateWebsocket(trackers="announced") + @util.Noparallel(blocking=False) - def announcePex(self, query_num=2, need_num=5): + def announcePex(self, query_num=2, need_num=10): + self.updateWebsocket(pex="announcing") + peers = self.site.getConnectedPeers() if len(peers) == 0: # Wait 3s for connections time.sleep(3) @@ -286,6 +319,8 @@ class SiteAnnouncer(object): break self.log.debug("Pex result: from %s peers got %s new peers." % (done, total_added)) + self.updateWebsocket(pex="announced") + def updateWebsocket(self, **kwargs): if kwargs: param = {"event": list(kwargs.items())[0]} From 829fd4678133e527d34ac395c6c5bf3da20f8050 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Fri, 30 Oct 2020 14:36:08 +0700 Subject: [PATCH 039/191] Redesign the site updating strategy in Site.py, SiteAnnouncer.py, FileServer.py --- src/Config.py | 2 + src/File/FileServer.py | 245 ++++++++++++++++++++++---------------- src/Site/Site.py | 156 ++++++++++++++++++++---- src/Site/SiteAnnouncer.py | 11 +- 4 files changed, 285 insertions(+), 129 deletions(-) diff --git a/src/Config.py b/src/Config.py index 70b284bf..a3d66395 100644 --- a/src/Config.py +++ b/src/Config.py @@ -257,6 +257,8 @@ class Config(object): self.parser.add_argument('--global_connected_limit', help='Max connections', default=512, type=int, metavar='global_connected_limit') self.parser.add_argument('--workers', help='Download workers per site', default=5, type=int, metavar='workers') + self.parser.add_argument('--expose_no_ownership', help='By default, ZeroNet tries checking updates for own sites more frequently. This can be used by a third party for revealing the network addresses of a site owner. If this option is enabled, ZeroNet performs the checks in the same way for any sites.', type='bool', choices=[True, False], default=False) + self.parser.add_argument('--fileserver_ip', help='FileServer bind address', default="*", metavar='ip') self.parser.add_argument('--fileserver_port', help='FileServer bind port (0: randomize)', default=0, type=int, metavar='port') self.parser.add_argument('--fileserver_port_range', help='FileServer randomization range', default="10000-40000", metavar='port') diff --git a/src/File/FileServer.py b/src/File/FileServer.py index fbc3cc85..8294d179 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -10,7 +10,6 @@ from gevent.server import StreamServer import util from util import helper -from util import CircularIterator from Config import config from .FileRequest import FileRequest from Peer import PeerPortchecker @@ -19,16 +18,24 @@ from Connection import ConnectionServer from Plugin import PluginManager from Debug import Debug +log = logging.getLogger("FileServer") + @PluginManager.acceptPlugins class FileServer(ConnectionServer): def __init__(self, ip=config.fileserver_ip, port=config.fileserver_port, ip_type=config.fileserver_ip_type): self.site_manager = SiteManager.site_manager self.portchecker = PeerPortchecker.PeerPortchecker(self) - self.log = logging.getLogger("FileServer") self.ip_type = ip_type self.ip_external_list = [] + # This is wrong: + # self.log = logging.getLogger("FileServer") + # The value of self.log will be overwritten in ConnectionServer.__init__() + + self.check_pool = gevent.pool.Pool(5) + self.check_start_time = 0 + self.supported_ip_types = ["ipv4"] # Outgoing ip_type support if helper.getIpType(ip) == "ipv6" or self.isIpv6Supported(): self.supported_ip_types.append("ipv6") @@ -52,17 +59,17 @@ class FileServer(ConnectionServer): config.arguments.fileserver_port = port ConnectionServer.__init__(self, ip, port, self.handleRequest) - self.log.debug("Supported IP types: %s" % self.supported_ip_types) + log.debug("Supported IP types: %s" % self.supported_ip_types) if ip_type == "dual" and ip == "::": # Also bind to ipv4 addres in dual mode try: - self.log.debug("Binding proxy to %s:%s" % ("::", self.port)) + log.debug("Binding proxy to %s:%s" % ("::", self.port)) self.stream_server_proxy = StreamServer( ("0.0.0.0", self.port), self.handleIncomingConnection, spawn=self.pool, backlog=100 ) except Exception as err: - self.log.info("StreamServer proxy create error: %s" % Debug.formatException(err)) + log.info("StreamServer proxy create error: %s" % Debug.formatException(err)) self.port_opened = {} @@ -71,8 +78,17 @@ class FileServer(ConnectionServer): self.files_parsing = {} self.ui_server = None + def getSiteAddresses(self): + # Avoid saving the site list on the stack, since a site may be deleted + # from the original list while iterating. + # Use the list of addresses instead. + return [ + site.address for site in + sorted(list(self.sites.values()), key=lambda site: site.settings.get("modified", 0), reverse=True) + ] + def getRandomPort(self, ip, port_range_from, port_range_to): - self.log.info("Getting random port in range %s-%s..." % (port_range_from, port_range_to)) + log.info("Getting random port in range %s-%s..." % (port_range_from, port_range_to)) tried = [] for bind_retry in range(100): port = random.randint(port_range_from, port_range_to) @@ -84,11 +100,11 @@ class FileServer(ConnectionServer): sock.bind((ip, port)) success = True except Exception as err: - self.log.warning("Error binding to port %s: %s" % (port, err)) + log.warning("Error binding to port %s: %s" % (port, err)) success = False sock.close() if success: - self.log.info("Found unused random port: %s" % port) + log.info("Found unused random port: %s" % port) return port else: time.sleep(0.1) @@ -104,16 +120,16 @@ class FileServer(ConnectionServer): sock.connect((ipv6_testip, 80)) local_ipv6 = sock.getsockname()[0] if local_ipv6 == "::1": - self.log.debug("IPv6 not supported, no local IPv6 address") + log.debug("IPv6 not supported, no local IPv6 address") return False else: - self.log.debug("IPv6 supported on IP %s" % local_ipv6) + log.debug("IPv6 supported on IP %s" % local_ipv6) return True except socket.error as err: - self.log.warning("IPv6 not supported: %s" % err) + log.warning("IPv6 not supported: %s" % err) return False except Exception as err: - self.log.error("IPv6 check error: %s" % err) + log.error("IPv6 check error: %s" % err) return False def listenProxy(self): @@ -121,20 +137,20 @@ class FileServer(ConnectionServer): self.stream_server_proxy.serve_forever() except Exception as err: if err.errno == 98: # Address already in use error - self.log.debug("StreamServer proxy listen error: %s" % err) + log.debug("StreamServer proxy listen error: %s" % err) else: - self.log.info("StreamServer proxy listen error: %s" % err) + log.info("StreamServer proxy listen error: %s" % err) # Handle request to fileserver def handleRequest(self, connection, message): if config.verbose: if "params" in message: - self.log.debug( + log.debug( "FileRequest: %s %s %s %s" % (str(connection), message["cmd"], message["params"].get("site"), message["params"].get("inner_path")) ) else: - self.log.debug("FileRequest: %s %s" % (str(connection), message["cmd"])) + log.debug("FileRequest: %s %s" % (str(connection), message["cmd"])) req = FileRequest(self, connection) req.route(message["cmd"], message.get("req_id"), message.get("params")) if not self.has_internet and not connection.is_private_ip: @@ -142,7 +158,7 @@ class FileServer(ConnectionServer): self.onInternetOnline() def onInternetOnline(self): - self.log.info("Internet online") + log.info("Internet online") gevent.spawn(self.checkSites, check_files=False, force_port_check=True) # Reload the FileRequest class to prevent restarts in debug mode @@ -153,7 +169,7 @@ class FileServer(ConnectionServer): def portCheck(self): if config.offline: - self.log.info("Offline mode: port check disabled") + log.info("Offline mode: port check disabled") res = {"ipv4": None, "ipv6": None} self.port_opened = res return res @@ -169,7 +185,7 @@ class FileServer(ConnectionServer): } self.ip_external_list = config.ip_external self.port_opened.update(res) - self.log.info("Server port opened based on configuration ipv4: %s, ipv6: %s" % (res["ipv4"], res["ipv6"])) + log.info("Server port opened based on configuration ipv4: %s, ipv6: %s" % (res["ipv4"], res["ipv6"])) return res self.port_opened = {} @@ -191,7 +207,7 @@ class FileServer(ConnectionServer): else: res_ipv6 = res_ipv6_thread.get() if res_ipv6["opened"] and not helper.getIpType(res_ipv6["ip"]) == "ipv6": - self.log.info("Invalid IPv6 address from port check: %s" % res_ipv6["ip"]) + log.info("Invalid IPv6 address from port check: %s" % res_ipv6["ip"]) res_ipv6["opened"] = False self.ip_external_list = [] @@ -200,7 +216,7 @@ class FileServer(ConnectionServer): self.ip_external_list.append(res_ip["ip"]) SiteManager.peer_blacklist.append((res_ip["ip"], self.port)) - self.log.info("Server port opened ipv4: %s, ipv6: %s" % (res_ipv4["opened"], res_ipv6["opened"])) + log.info("Server port opened ipv4: %s, ipv6: %s" % (res_ipv4["opened"], res_ipv6["opened"])) res = {"ipv4": res_ipv4["opened"], "ipv6": res_ipv6["opened"]} @@ -213,7 +229,7 @@ class FileServer(ConnectionServer): self.ip_external_list.append(ip) res[helper.getIpType(ip)] = True # We have opened port if we have external ip SiteManager.peer_blacklist.append((ip, self.port)) - self.log.debug("External ip found on interfaces: %s" % ip) + log.debug("External ip found on interfaces: %s" % ip) self.port_opened.update(res) @@ -224,79 +240,123 @@ class FileServer(ConnectionServer): # Check site file integrity def checkSite(self, site, check_files=False): - if site.isServing(): - site.announce(mode="startup") # Announce site to tracker - site.update(check_files=check_files) # Update site's content.json and download changed files - site.sendMyHashfield() - site.updateHashfield() + if not site.isServing(): + return + + quick_start = len(site.peers) >= 50 + + if quick_start: + log.debug("Checking site: %s (quick start)" % (site.address)) + else: + log.debug("Checking site: %s" % (site.address)) + + if quick_start: + site.setDelayedStartupAnnounce() + else: + site.announce(mode="startup") + + site.update(check_files=check_files) # Update site's content.json and download changed files + site.sendMyHashfield() + site.updateHashfield() # Check sites integrity @util.Noparallel() def checkSites(self, check_files=False, force_port_check=False): - self.log.debug("Checking sites...") - s = time.time() - sites_checking = False - if not self.port_opened or force_port_check: # Test and open port if not tested yet - if len(self.sites) <= 2: # Don't wait port opening on first startup - sites_checking = True - for address, site in list(self.sites.items()): - gevent.spawn(self.checkSite, site, check_files) + log.info("Checking sites: check_files=%s, force_port_check=%s", check_files, force_port_check) + # Don't wait port opening on first startup. Do the instant check now. + if len(self.sites) <= 2: + sites_checking = True + for address, site in list(self.sites.items()): + gevent.spawn(self.checkSite, site, check_files) + + # Test and open port if not tested yet + if not self.port_opened or force_port_check: self.portCheck() - if not self.port_opened["ipv4"]: self.tor_manager.startOnions() - if not sites_checking: - check_pool = gevent.pool.Pool(5) - # Check sites integrity - for site in sorted(list(self.sites.values()), key=lambda site: site.settings.get("modified", 0), reverse=True): - if not site.isServing(): - continue - check_thread = check_pool.spawn(self.checkSite, site, check_files) # Check in new thread - time.sleep(2) - if site.settings.get("modified", 0) < time.time() - 60 * 60 * 24: # Not so active site, wait some sec to finish - check_thread.join(timeout=5) - self.log.debug("Checksites done in %.3fs" % (time.time() - s)) + site_addresses = self.getSiteAddresses() + + sites_processed = 0 + start_time = time.time() + self.check_start_time = start_time + progress_print_time = time.time() + + # Check sites integrity + for site_address in site_addresses: + site = self.sites.get(site_address, None) + + sites_processed += 1 + + if (not site) or (not site.isServing()): + continue + check_thread = self.check_pool.spawn(self.checkSite, site, check_files) # Check in new thread + + if time.time() - progress_print_time > 60: + progress_print_time = time.time() + time_spent = time.time() - start_time + time_per_site = time_spent / float(sites_processed) + sites_left = len(site_addresses) - sites_processed + time_left = time_per_site * sites_left + log.info("Checking sites: DONE: %d sites in %.2fs (%.2fs per site); LEFT: %d sites in %.2fs", + sites_processed, + time_spent, + time_per_site, + sites_left, + time_left + ) + + if (self.check_start_time != start_time) and self.check_pool.full(): + # Another check is running, throttling... + time.sleep(5) + else: + time.sleep(1) + + log.info("Checking sites: finished in %.3fs" % (time.time() - start_time)) def sitesMaintenanceThread(self): import gc startup = True short_timeout = 2 - long_timeout = 60 * 5 - - circular_iterator = CircularIterator() + long_timeout = 60 * 2 while 1: - if circular_iterator.isWrapped(): - time.sleep(long_timeout) - circular_iterator.resetSuccessiveCount() - gc.collect() # Explicit garbage collection + time.sleep(long_timeout) + gc.collect() # Explicit garbage collection - self.log.debug( - "Running site cleanup, connections: %s, internet: %s" % - (len(self.connections), self.has_internet) + log.debug( + "Starting maintenance cycle: connections=%s, internet=%s", + len(self.connections), self.has_internet + ) + start_time = time.time() + + site_addresses = self.getSiteAddresses() + + sites_processed = 0 + + for site_address in site_addresses: + site = self.sites.get(site_address, None) + if (not site) or (not site.isServing()): + continue + + log.debug("Running maintenance for site: %s", site.address) + + done = site.runPeriodicMaintenance(startup=startup) + site = None + if done: + sites_processed += 1 + time.sleep(short_timeout) + + log.debug("Maintenance cycle finished in %.3fs. Total sites: %d. Processed sites: %d", + time.time() - start_time, + len(site_addresses), + sites_processed ) - site = circular_iterator.next(list(self.sites.values())) - if site: - done = site.runPeriodicMaintenance(startup=startup) - if done: - time.sleep(short_timeout) - site = None - - if circular_iterator.isWrapped(): - startup = False - - def announceSite(self, site): - site.announce(mode="update", pex=False) - active_site = time.time() - site.settings.get("modified", 0) < 24 * 60 * 60 - if site.settings["own"] or active_site: - # Check connections more frequently on own and active sites to speed-up first connections - site.needConnections(check_site_on_reconnect=True) - site.sendMyHashfield(3) - site.updateHashfield(3) + site_addresses = None + startup = False # Periodic reloading of tracker files def reloadTrackerFilesThread(self): @@ -309,24 +369,6 @@ class FileServer(ConnectionServer): time.sleep(interval) config.loadTrackersFile() - # Announce sites every 20 min - def announceSites(self): - time.sleep(5 * 60) # Sites already announced on startup - while 1: - s = time.time() - for address, site in list(self.sites.items()): - if not site.isServing(): - continue - gevent.spawn(self.announceSite, site).join(timeout=10) - time.sleep(1) - taken = time.time() - s - - # Query all trackers one-by-one in 20 minutes evenly distributed - sleep = max(0, 60 * 20 / len(config.trackers) - taken) - - self.log.debug("Site announce tracker done in %.3fs, sleeping for %.3fs..." % (taken, sleep)) - time.sleep(sleep) - # Detects if computer back from wakeup def wakeupWatcher(self): last_time = time.time() @@ -336,7 +378,7 @@ class FileServer(ConnectionServer): is_time_changed = time.time() - max(self.last_request, last_time) > 60 * 3 if is_time_changed: # If taken more than 3 minute then the computer was in sleep mode - self.log.info( + log.info( "Wakeup detected: time warp from %0.f to %0.f (%0.f sleep seconds), acting like startup..." % (last_time, time.time(), time.time() - last_time) ) @@ -344,7 +386,7 @@ class FileServer(ConnectionServer): my_ips = socket.gethostbyname_ex('')[2] is_ip_changed = my_ips != last_my_ips if is_ip_changed: - self.log.info("IP change detected from %s to %s" % (last_my_ips, my_ips)) + log.info("IP change detected from %s to %s" % (last_my_ips, my_ips)) if is_time_changed or is_ip_changed: self.checkSites(check_files=False, force_port_check=True) @@ -362,9 +404,9 @@ class FileServer(ConnectionServer): try: self.stream_server.start() except Exception as err: - self.log.error("Error listening on: %s:%s: %s" % (self.ip, self.port, err)) + log.error("Error listening on: %s:%s: %s" % (self.ip, self.port, err)) if "ui_server" in dir(sys.modules["main"]): - self.log.debug("Stopping UI Server.") + log.debug("Stopping UI Server.") sys.modules["main"].ui_server.stop() return False @@ -378,21 +420,20 @@ class FileServer(ConnectionServer): gevent.spawn(self.checkSites) thread_reaload_tracker_files = gevent.spawn(self.reloadTrackerFilesThread) - thread_announce_sites = gevent.spawn(self.announceSites) thread_sites_maintenance = gevent.spawn(self.sitesMaintenanceThread) thread_wakeup_watcher = gevent.spawn(self.wakeupWatcher) ConnectionServer.listen(self) - self.log.debug("Stopped.") + log.debug("Stopped.") def stop(self): if self.running and self.portchecker.upnp_port_opened: - self.log.debug('Closing port %d' % self.port) + log.debug('Closing port %d' % self.port) try: self.portchecker.portClose(self.port) - self.log.info('Closed port via upnp.') + log.info('Closed port via upnp.') except Exception as err: - self.log.info("Failed at attempt to use upnp to close port: %s" % err) + log.info("Failed at attempt to use upnp to close port: %s" % err) return ConnectionServer.stop(self) diff --git a/src/Site/Site.py b/src/Site/Site.py index b941b035..b01e9aae 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -28,6 +28,38 @@ from File import FileServer from .SiteAnnouncer import SiteAnnouncer from . import SiteManager +class ScaledTimeoutHandler: + def __init__(self, val_min, val_max, handler=None, scaler=None): + self.val_min = val_min + self.val_max = val_max + self.timestamp = 0 + self.handler = handler + self.scaler = scaler + self.log = logging.getLogger("ScaledTimeoutHandler") + + def isExpired(self, scale): + interval = scale * (self.val_max - self.val_min) + self.val_min + expired_at = self.timestamp + interval + now = time.time() + expired = (now > expired_at) + if expired: + self.log.debug( + "Expired: [%d..%d]: scale=%f, interval=%f", + self.val_min, self.val_max, scale, interval) + return expired + + def done(self): + self.timestamp = time.time() + + def run(self, *args, **kwargs): + do_run = kwargs["force"] or self.isExpired(self.scaler()) + if do_run: + result = self.handler(*args, **kwargs) + if result: + self.done() + return result + else: + return None @PluginManager.acceptPlugins class Site(object): @@ -40,8 +72,16 @@ class Site(object): self.log = logging.getLogger("Site:%s" % self.address_short) self.addEventListeners() - self.periodic_maintenance_interval = 60 * 20 - self.periodic_maintenance_timestamp = 0 + self.periodic_maintenance_handlers = [ + ScaledTimeoutHandler(60 * 30, 60 * 2, + handler=self.periodicMaintenanceHandler_announce, + scaler=self.getAnnounceRating), + ScaledTimeoutHandler(60 * 20, 60 * 10, + handler=self.periodicMaintenanceHandler_general, + scaler=self.getActivityRating) + ] + + self.delayed_startup_announce = False self.content = None # Load content.json self.peers = {} # Key: ip:port, Value: Peer.Peer @@ -852,10 +892,63 @@ class Site(object): peer.found(source) return peer + def setDelayedStartupAnnounce(self): + self.delayed_startup_announce = True + + def applyDelayedStartupAnnounce(self): + if self.delayed_startup_announce: + self.delayed_startup_announce = False + self.announce(mode="startup") + return True + return False + def announce(self, *args, **kwargs): if self.isServing(): self.announcer.announce(*args, **kwargs) + def getActivityRating(self, force_safe=False): + age = time.time() - self.settings.get("modified", 0) + + if age < 60 * 60: + rating = 1.0 + elif age < 60 * 60 * 5: + rating = 0.8 + elif age < 60 * 60 * 24: + rating = 0.6 + elif age < 60 * 60 * 24 * 3: + rating = 0.4 + elif age < 60 * 60 * 24 * 7: + rating = 0.2 + else: + rating = 0.0 + + force_safe = force_safe or config.expose_no_ownership + + if (not force_safe) and self.settings["own"]: + rating = min(rating, 0.6) + + return rating + + def getAnnounceRating(self): + # rare frequent + # announces announces + # 0 ------------------- 1 + # activity -------------> -- active site ==> frequent announces + # <---------------- peers -- many peers ==> rare announces + # trackers -------------> -- many trackers ==> frequent announces to iterate over more trackers + + activity_rating = self.getActivityRating(force_safe=True) + + peer_count = len(self.peers) + peer_rating = 1.0 - min(peer_count, 50) / 50.0 + + tracker_count = self.announcer.getSupportedTrackerCount() + tracker_count = max(tracker_count, 1) + tracker_rating = 1.0 - (1.0 / tracker_count) + + v = [activity_rating, peer_rating, tracker_rating] + return sum(v) / float(len(v)) + # The engine tries to maintain the number of active connections: # >= getPreferableActiveConnectionCount() # and @@ -866,18 +959,7 @@ class Site(object): return 0 age = time.time() - self.settings.get("modified", 0) - count = 0 - - if age < 60 * 60: - count = 10 - elif age < 60 * 60 * 5: - count = 8 - elif age < 60 * 60 * 24: - count = 6 - elif age < 60 * 60 * 24 * 3: - count = 4 - elif age < 60 * 60 * 24 * 7: - count = 2 + count = int(10 * self.getActivityRating(force_safe=True)) if len(self.peers) < 50: count = max(count, 5) @@ -957,7 +1039,7 @@ class Site(object): return connected def markConnectedPeersProtected(self): - for peer in site.getConnectedPeers(): + for peer in self.getConnectedPeers(): peer.markProtected() # Return: Probably peers verified to be connectable recently @@ -1099,32 +1181,54 @@ class Site(object): if not self.isServing(): return False - scheduled_time = self.periodic_maintenance_timestamp + self.periodic_maintenance_interval + self.log.debug("runPeriodicMaintenance: startup=%s, force=%s" % (startup, force)) - if time.time() < scheduled_time and not force: + result = False + + for handler in self.periodic_maintenance_handlers: + result = result | bool(handler.run(startup=startup, force=force)) + + return result + + def periodicMaintenanceHandler_general(self, startup=False, force=False): + if not self.isServing(): return False - self.periodic_maintenance_timestamp = time.time() + self.applyDelayedStartupAnnounce() - self.log.debug("runPeriodicMaintenance: startup=%s" % startup) + if not self.peers: + return False + + self.log.debug("periodicMaintenanceHandler_general: startup=%s, force=%s" % (startup, force)) if not startup: self.cleanupPeers() - if self.peers: - with gevent.Timeout(10, exception=False): - self.announcer.announcePex() + self.needConnections(check_site_on_reconnect=True) - # Last check modification failed - if self.content_updated is False: + with gevent.Timeout(10, exception=False): + self.announcer.announcePex() + + self.sendMyHashfield(3) + self.updateHashfield(3) + + if self.content_updated is False: # Last check modification failed self.update() elif self.bad_files: self.retryBadFiles() - self.needConnections(check_site_on_reconnect=True) + return True - self.periodic_maintenance_timestamp = time.time() + def periodicMaintenanceHandler_announce(self, startup=False, force=False): + if not self.isServing(): + return False + self.log.debug("periodicMaintenanceHandler_announce: startup=%s, force=%s" % (startup, force)) + + if self.applyDelayedStartupAnnounce(): + return True + + self.announce(mode="update", pex=False) return True # Send hashfield to peers diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index 1440eb57..5a97807e 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -33,6 +33,7 @@ class SiteAnnouncer(object): self.peer_id = self.site.connection_server.peer_id self.tracker_circular_iterator = CircularIterator() self.time_last_announce = 0 + self.supported_tracker_count = 0 def getTrackers(self): return config.trackers @@ -50,6 +51,12 @@ class SiteAnnouncer(object): return trackers + # Returns a cached value of len(self.getSupportedTrackers()), which can be + # inacurate. + # To be used from Site for estimating available tracker count. + def getSupportedTrackerCount(self): + return self.supported_tracker_count + def shouldTrackerBeTemporarilyIgnored(self, tracker, mode, force): if not tracker: return True @@ -71,6 +78,8 @@ class SiteAnnouncer(object): def getAnnouncingTrackers(self, mode, force): trackers = self.getSupportedTrackers() + self.supported_tracker_count = len(trackers) + if trackers and (mode == "update" or mode == "more"): # Choose just 2 trackers to announce to @@ -116,7 +125,7 @@ class SiteAnnouncer(object): back.append("onion") return back - @util.Noparallel(blocking=False) + @util.Noparallel() def announce(self, force=False, mode="start", pex=True): if time.time() - self.time_last_announce < 30 and not force: return # No reannouncing within 30 secs From d1b9cc826153248f4340284f531ab4258aa927e1 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Fri, 30 Oct 2020 23:28:16 +0700 Subject: [PATCH 040/191] Redesign the Internet outage detection. Improvements in FileServer threads. --- src/Connection/Connection.py | 12 +++ src/Connection/ConnectionServer.py | 65 +++++++++++---- src/File/FileServer.py | 125 +++++++++++++++++++++++++---- src/Site/Site.py | 1 + 4 files changed, 173 insertions(+), 30 deletions(-) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 22bcf29c..27ae3734 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -442,6 +442,7 @@ class Connection(object): def handleMessage(self, message): cmd = message["cmd"] + self.updateOnlineStatus(successful_activity=True) self.last_message_time = time.time() self.last_cmd_recv = cmd if cmd == "response": # New style response @@ -504,6 +505,7 @@ class Connection(object): # Send data to connection def send(self, message, streaming=False): + self.updateOnlineStatus(outgoing_activity=True) self.last_send_time = time.time() if config.debug_socket: self.log("Send: %s, to: %s, streaming: %s, site: %s, inner_path: %s, req_id: %s" % ( @@ -543,6 +545,11 @@ class Connection(object): message = None with self.send_lock: self.sock.sendall(data) + # XXX: Should not be used here: + # self.updateOnlineStatus(successful_activity=True) + # Looks like self.sock.sendall() returns normally, instead of + # raising an Exception (at least, some times). + # So the only way of detecting the network activity is self.handleMessage() except Exception as err: self.close("Send error: %s (cmd: %s)" % (err, stat_key)) return False @@ -633,3 +640,8 @@ class Connection(object): self.sock = None self.unpacker = None self.event_connected = None + + def updateOnlineStatus(self, outgoing_activity=False, successful_activity=False): + self.server.updateOnlineStatus(self, + outgoing_activity=outgoing_activity, + successful_activity=successful_activity) diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index 8d377aca..66b50608 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -41,7 +41,11 @@ class ConnectionServer(object): self.ip_incoming = {} # Incoming connections from ip in the last minute to avoid connection flood self.broken_ssl_ips = {} # Peerids of broken ssl connections self.ips = {} # Connection by ip + self.has_internet = True # Internet outage detection + self.last_outgoing_internet_activity_time = 0 # Last time the application tried to send any data + self.last_successful_internet_activity_time = 0 # Last time the application successfully sent or received any data + self.internet_outage_threshold = 60 * 2 self.stream_server = None self.stream_server_proxy = None @@ -60,6 +64,8 @@ class ConnectionServer(object): self.num_outgoing = 0 self.had_external_incoming = False + + self.timecorrection = 0.0 self.pool = Pool(500) # do not accept more than 500 connections @@ -252,8 +258,8 @@ class ConnectionServer(object): while self.running: run_i += 1 self.ip_incoming = {} # Reset connected ips counter - last_message_time = 0 s = time.time() + self.updateOnlineStatus(None) for connection in self.connections[:]: # Make a copy if connection.ip.endswith(".onion") or config.tor == "always": timeout_multipler = 2 @@ -261,9 +267,6 @@ class ConnectionServer(object): timeout_multipler = 1 idle = time.time() - max(connection.last_recv_time, connection.start_time, connection.last_message_time) - if connection.last_message_time > last_message_time and not connection.is_private_ip: - # Message from local IPs does not means internet connection - last_message_time = connection.last_message_time if connection.unpacker and idle > 30: # Delete the unpacker if not needed @@ -311,18 +314,6 @@ class ConnectionServer(object): # Reset bad action counter every 30 min connection.bad_actions = 0 - # Internet outage detection - if time.time() - last_message_time > max(60, 60 * 10 / max(1, float(len(self.connections)) / 50)): - # Offline: Last message more than 60-600sec depending on connection number - if self.has_internet and last_message_time: - self.has_internet = False - self.onInternetOffline() - else: - # Online - if not self.has_internet: - self.has_internet = True - self.onInternetOnline() - self.timecorrection = self.getTimecorrection() if time.time() - s > 0.01: @@ -353,6 +344,48 @@ class ConnectionServer(object): )) return num_closed + # Internet outage detection + def updateOnlineStatus(self, connection, outgoing_activity=False, successful_activity=False): + + now = time.time() + + if connection and not connection.is_private_ip: + if outgoing_activity: + self.last_outgoing_internet_activity_time = now + if successful_activity: + self.last_successful_internet_activity_time = now + self.setInternetStatus(True) + return + + if not self.last_outgoing_internet_activity_time: + return + + if ( + (self.last_successful_internet_activity_time < now - self.internet_outage_threshold) + and + (self.last_successful_internet_activity_time < self.last_outgoing_internet_activity_time) + ): + self.setInternetStatus(False) + return + + # This is the old algorithm just in case we missed something + idle = now - self.last_successful_internet_activity_time + if idle > max(60, 60 * 10 / max(1, float(len(self.connections)) / 50)): + # Offline: Last successful activity more than 60-600sec depending on connection number + self.setInternetStatus(False) + return + + def setInternetStatus(self, status): + if self.has_internet == status: + return + + self.has_internet = status + + if self.has_internet: + gevent.spawn(self.onInternetOnline) + else: + gevent.spawn(self.onInternetOffline) + def onInternetOnline(self): self.log.info("Internet online") diff --git a/src/File/FileServer.py b/src/File/FileServer.py index 8294d179..c425b3f4 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -238,17 +238,50 @@ class FileServer(ConnectionServer): return res + # Returns False if Internet is immediately available + # Returns True if we've spent some time waiting for Internet + def waitForInternetOnline(self): + if self.has_internet: + return False + + while not self.has_internet: + time.sleep(15) + if self.has_internet: + break + if not self.check_pool.full(): + self.check_pool.spawn(self.updateRandomSite) + + return True + + def updateRandomSite(self, site_addresses=None, check_files=False): + if not site_addresses: + site_addresses = self.getSiteAddresses() + + site_addresses = random.sample(site_addresses, 1) + if len(site_addresses) < 1: + return + + address = site_addresses[0] + site = self.sites.get(address, None) + + if not site or not site.isServing(): + return + + log.debug("Checking randomly chosen site: %s", site.address_short) + + self.checkSite(site, check_files=check_files) + # Check site file integrity def checkSite(self, site, check_files=False): - if not site.isServing(): + if not site or not site.isServing(): return quick_start = len(site.peers) >= 50 if quick_start: - log.debug("Checking site: %s (quick start)" % (site.address)) + log.debug("Checking site: %s (quick start)", site.address_short) else: - log.debug("Checking site: %s" % (site.address)) + log.debug("Checking site: %s", site.address_short) if quick_start: site.setDelayedStartupAnnounce() @@ -291,7 +324,12 @@ class FileServer(ConnectionServer): if (not site) or (not site.isServing()): continue - check_thread = self.check_pool.spawn(self.checkSite, site, check_files) # Check in new thread + + while 1: + self.waitForInternetOnline() + self.check_pool.spawn(self.checkSite, site, check_files) + if not self.waitForInternetOnline(): + break if time.time() - progress_print_time > 60: progress_print_time = time.time() @@ -313,21 +351,25 @@ class FileServer(ConnectionServer): else: time.sleep(1) - log.info("Checking sites: finished in %.3fs" % (time.time() - start_time)) + log.info("Checking sites: finished in %.2fs" % (time.time() - start_time)) - def sitesMaintenanceThread(self): - import gc + def sitesMaintenanceThread(self, mode="full"): startup = True short_timeout = 2 - long_timeout = 60 * 2 + min_long_timeout = 10 + max_long_timeout = 60 * 10 + long_timeout = min_long_timeout + short_cycle_time_limit = 60 * 2 while 1: time.sleep(long_timeout) - gc.collect() # Explicit garbage collection + + start_time = time.time() log.debug( - "Starting maintenance cycle: connections=%s, internet=%s", + "Starting <%s> maintenance cycle: connections=%s, internet=%s", + mode, len(self.connections), self.has_internet ) start_time = time.time() @@ -341,7 +383,7 @@ class FileServer(ConnectionServer): if (not site) or (not site.isServing()): continue - log.debug("Running maintenance for site: %s", site.address) + log.debug("Running maintenance for site: %s", site.address_short) done = site.runPeriodicMaintenance(startup=startup) site = None @@ -349,15 +391,68 @@ class FileServer(ConnectionServer): sites_processed += 1 time.sleep(short_timeout) - log.debug("Maintenance cycle finished in %.3fs. Total sites: %d. Processed sites: %d", + # If we host hundreds of sites, the full maintenance cycle may take very + # long time, especially on startup ( > 1 hour). + # This means we are not able to run the maintenance procedure for active + # sites frequently enough using just a single maintenance thread. + # So we run 2 maintenance threads: + # * One running full cycles. + # * And one running short cycles for the most active sites. + # When the short cycle runs out of the time limit, it restarts + # from the beginning of the site list. + if mode == "short" and time.time() - start_time > short_cycle_time_limit: + break + + log.debug("<%s> maintenance cycle finished in %.2fs. Total sites: %d. Processed sites: %d. Timeout: %d", + mode, time.time() - start_time, len(site_addresses), - sites_processed + sites_processed, + long_timeout ) + if sites_processed: + long_timeout = max(int(long_timeout / 2), min_long_timeout) + else: + long_timeout = min(long_timeout + 1, max_long_timeout) + site_addresses = None startup = False + def keepAliveThread(self): + # This thread is mostly useless on a loaded system, since it never does + # any works, if we have active traffic. + # + # We should initiate some network activity to detect the Internet outage + # and avoid false positives. We normally have some network activity + # initiated by various parts on the application as well as network peers. + # So it's not a problem. + # + # However, if it actually happens that we have no network traffic for + # some time (say, we host just a couple of inactive sites, and no peers + # are interested in connecting to them), we initiate some traffic by + # performing the update for a random site. It's way better than just + # silly pinging a random peer for no profit. + while 1: + threshold = self.internet_outage_threshold / 2.0 + time.sleep(threshold / 2.0) + self.waitForInternetOnline() + last_activity_time = max( + self.last_successful_internet_activity_time, + self.last_outgoing_internet_activity_time) + now = time.time() + if not len(self.sites): + continue + if last_activity_time > now - threshold: + continue + if self.check_pool.full(): + continue + + log.info("No network activity for %.2fs. Running an update for a random site.", + now - last_activity_time + ) + self.check_pool.spawn(self.updateRandomSite) + # Periodic reloading of tracker files def reloadTrackerFilesThread(self): # TODO: @@ -420,7 +515,9 @@ class FileServer(ConnectionServer): gevent.spawn(self.checkSites) thread_reaload_tracker_files = gevent.spawn(self.reloadTrackerFilesThread) - thread_sites_maintenance = gevent.spawn(self.sitesMaintenanceThread) + thread_sites_maintenance_full = gevent.spawn(self.sitesMaintenanceThread, mode="full") + thread_sites_maintenance_short = gevent.spawn(self.sitesMaintenanceThread, mode="short") + thread_keep_alive = gevent.spawn(self.keepAliveThread) thread_wakeup_watcher = gevent.spawn(self.wakeupWatcher) ConnectionServer.listen(self) diff --git a/src/Site/Site.py b/src/Site/Site.py index b01e9aae..13ee730b 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -1177,6 +1177,7 @@ class Site(object): self.log.debug("Connected: %s, Need to close: %s, Closed: %s" % ( len(connected_peers), need_to_close, closed)) + @util.Noparallel(queue=True) def runPeriodicMaintenance(self, startup=False, force=False): if not self.isServing(): return False From e8358ee8f2df8e1b66b9a823a6ea6b28859300bc Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Sat, 31 Oct 2020 03:59:54 +0700 Subject: [PATCH 041/191] More fixes on the way to reliable site updates. --- src/Connection/ConnectionServer.py | 3 ++ src/File/FileServer.py | 70 +++++++++++------------------- src/Site/Site.py | 56 +++++++++++++++++++++--- 3 files changed, 79 insertions(+), 50 deletions(-) diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index 66b50608..2b4586e9 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -386,6 +386,9 @@ class ConnectionServer(object): else: gevent.spawn(self.onInternetOffline) + def isInternetOnline(self): + return self.has_internet + def onInternetOnline(self): self.log.info("Internet online") diff --git a/src/File/FileServer.py b/src/File/FileServer.py index c425b3f4..de14a9c9 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -33,8 +33,8 @@ class FileServer(ConnectionServer): # self.log = logging.getLogger("FileServer") # The value of self.log will be overwritten in ConnectionServer.__init__() - self.check_pool = gevent.pool.Pool(5) - self.check_start_time = 0 + self.update_pool = gevent.pool.Pool(5) + self.update_start_time = 0 self.supported_ip_types = ["ipv4"] # Outgoing ip_type support if helper.getIpType(ip) == "ipv6" or self.isIpv6Supported(): @@ -153,13 +153,12 @@ class FileServer(ConnectionServer): log.debug("FileRequest: %s %s" % (str(connection), message["cmd"])) req = FileRequest(self, connection) req.route(message["cmd"], message.get("req_id"), message.get("params")) - if not self.has_internet and not connection.is_private_ip: - self.has_internet = True - self.onInternetOnline() + if not connection.is_private_ip: + self.setInternetStatus(self, True) def onInternetOnline(self): log.info("Internet online") - gevent.spawn(self.checkSites, check_files=False, force_port_check=True) + gevent.spawn(self.updateSites, force_port_check=True) # Reload the FileRequest class to prevent restarts in debug mode def reload(self): @@ -241,19 +240,19 @@ class FileServer(ConnectionServer): # Returns False if Internet is immediately available # Returns True if we've spent some time waiting for Internet def waitForInternetOnline(self): - if self.has_internet: + if self.isInternetOnline(): return False - while not self.has_internet: + while not self.isInternetOnline(): time.sleep(15) - if self.has_internet: + if self.isInternetOnline(): break - if not self.check_pool.full(): - self.check_pool.spawn(self.updateRandomSite) + if not self.update_pool.full(): + self.update_pool.spawn(self.updateRandomSite) return True - def updateRandomSite(self, site_addresses=None, check_files=False): + def updateRandomSite(self, site_addresses=None): if not site_addresses: site_addresses = self.getSiteAddresses() @@ -269,39 +268,22 @@ class FileServer(ConnectionServer): log.debug("Checking randomly chosen site: %s", site.address_short) - self.checkSite(site, check_files=check_files) + self.updateSite(site) - # Check site file integrity - def checkSite(self, site, check_files=False): + def updateSite(self, site): if not site or not site.isServing(): return + site.considerUpdate() - quick_start = len(site.peers) >= 50 - - if quick_start: - log.debug("Checking site: %s (quick start)", site.address_short) - else: - log.debug("Checking site: %s", site.address_short) - - if quick_start: - site.setDelayedStartupAnnounce() - else: - site.announce(mode="startup") - - site.update(check_files=check_files) # Update site's content.json and download changed files - site.sendMyHashfield() - site.updateHashfield() - - # Check sites integrity @util.Noparallel() - def checkSites(self, check_files=False, force_port_check=False): - log.info("Checking sites: check_files=%s, force_port_check=%s", check_files, force_port_check) + def updateSites(self, force_port_check=False): + log.info("Checking sites: force_port_check=%s", force_port_check) # Don't wait port opening on first startup. Do the instant check now. if len(self.sites) <= 2: sites_checking = True for address, site in list(self.sites.items()): - gevent.spawn(self.checkSite, site, check_files) + gevent.spawn(self.updateSite, site) # Test and open port if not tested yet if not self.port_opened or force_port_check: @@ -313,7 +295,7 @@ class FileServer(ConnectionServer): sites_processed = 0 start_time = time.time() - self.check_start_time = start_time + self.update_start_time = start_time progress_print_time = time.time() # Check sites integrity @@ -327,7 +309,7 @@ class FileServer(ConnectionServer): while 1: self.waitForInternetOnline() - self.check_pool.spawn(self.checkSite, site, check_files) + self.update_pool.spawn(self.updateSite, site) if not self.waitForInternetOnline(): break @@ -345,8 +327,8 @@ class FileServer(ConnectionServer): time_left ) - if (self.check_start_time != start_time) and self.check_pool.full(): - # Another check is running, throttling... + if (self.update_start_time != start_time) and self.update_pool.full(): + # Another updateSites() is running, throttling now... time.sleep(5) else: time.sleep(1) @@ -370,7 +352,7 @@ class FileServer(ConnectionServer): log.debug( "Starting <%s> maintenance cycle: connections=%s, internet=%s", mode, - len(self.connections), self.has_internet + len(self.connections), self.isInternetOnline() ) start_time = time.time() @@ -445,13 +427,13 @@ class FileServer(ConnectionServer): continue if last_activity_time > now - threshold: continue - if self.check_pool.full(): + if self.update_pool.full(): continue log.info("No network activity for %.2fs. Running an update for a random site.", now - last_activity_time ) - self.check_pool.spawn(self.updateRandomSite) + self.update_pool.spawn(self.updateRandomSite) # Periodic reloading of tracker files def reloadTrackerFilesThread(self): @@ -484,7 +466,7 @@ class FileServer(ConnectionServer): log.info("IP change detected from %s to %s" % (last_my_ips, my_ips)) if is_time_changed or is_ip_changed: - self.checkSites(check_files=False, force_port_check=True) + self.updateSites(force_port_check=True) last_time = time.time() last_my_ips = my_ips @@ -512,7 +494,7 @@ class FileServer(ConnectionServer): DebugReloader.watcher.addCallback(self.reload) if check_sites: # Open port, Update sites, Check files integrity - gevent.spawn(self.checkSites) + gevent.spawn(self.updateSites) thread_reaload_tracker_files = gevent.spawn(self.reloadTrackerFilesThread) thread_sites_maintenance_full = gevent.spawn(self.sitesMaintenanceThread, mode="full") diff --git a/src/Site/Site.py b/src/Site/Site.py index 13ee730b..6d72e119 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -76,7 +76,7 @@ class Site(object): ScaledTimeoutHandler(60 * 30, 60 * 2, handler=self.periodicMaintenanceHandler_announce, scaler=self.getAnnounceRating), - ScaledTimeoutHandler(60 * 20, 60 * 10, + ScaledTimeoutHandler(60 * 30, 60 * 5, handler=self.periodicMaintenanceHandler_general, scaler=self.getActivityRating) ] @@ -91,6 +91,8 @@ class Site(object): self.worker_manager = WorkerManager(self) # Handle site download from other peers self.bad_files = {} # SHA check failed files, need to redownload {"inner.content": 1} (key: file, value: failed accept) self.content_updated = None # Content.js update time + self.last_check_files_time = 0 + self.last_online_update = 0 self.notifications = [] # Pending notifications displayed once on page load [error|ok|info, message, timeout] self.page_requested = False # Page viewed in browser self.websockets = [] # Active site websocket connections @@ -564,6 +566,44 @@ class Site(object): self.updateWebsocket(updated=True) + def considerUpdate(self): + if not self.isServing(): + return + + online = self.connection_server.isInternetOnline() + + if online and time.time() - self.last_online_update < 60 * 10: + with gevent.Timeout(10, exception=False): + self.announcer.announcePex() + return + + # TODO: there should be a configuration options controlling: + # * whether to check files on the program startup + # * whether to check files during the run time and how often + check_files = self.last_check_files_time == 0 + + self.last_check_files_time = time.time() + + # quick start, avoiding redundant announces + if len(self.peers) >= 50: + if len(self.getConnectedPeers()) > 4: + pass # Don't run announce() at all + else: + self.setDelayedStartupAnnounce() + else: + self.announce(mode="startup") + + online = online and self.connection_server.isInternetOnline() + + self.update(check_files=check_files) + self.sendMyHashfield() + self.updateHashfield() + + online = online and self.connection_server.isInternetOnline() + + if online: + self.last_online_update = time.time() + # Update site by redownload all content.json def redownloadContents(self): # Download all content.json again @@ -927,6 +967,14 @@ class Site(object): if (not force_safe) and self.settings["own"]: rating = min(rating, 0.6) + if self.content_updated is False: # Last check modification failed + rating += 0.1 + elif self.bad_files: + rating += 0.1 + + if rating > 1.0: + rating = 1.0 + return rating def getAnnounceRating(self): @@ -1210,14 +1258,10 @@ class Site(object): with gevent.Timeout(10, exception=False): self.announcer.announcePex() + self.update() self.sendMyHashfield(3) self.updateHashfield(3) - if self.content_updated is False: # Last check modification failed - self.update() - elif self.bad_files: - self.retryBadFiles() - return True def periodicMaintenanceHandler_announce(self, startup=False, force=False): From ea21b32b93384cc718aada5fd184de5dbbf23b1f Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Sat, 31 Oct 2020 18:05:50 +0700 Subject: [PATCH 042/191] Add explicit invalidation and expiration of site update timestamps --- src/Connection/ConnectionServer.py | 4 ++ src/File/FileServer.py | 109 +++++++++++++++++++++-------- src/Site/Site.py | 101 +++++++++++++++----------- 3 files changed, 142 insertions(+), 72 deletions(-) diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index 2b4586e9..3ec0932d 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -43,6 +43,8 @@ class ConnectionServer(object): self.ips = {} # Connection by ip self.has_internet = True # Internet outage detection + self.internet_online_since = 0 + self.internet_offline_since = 0 self.last_outgoing_internet_activity_time = 0 # Last time the application tried to send any data self.last_successful_internet_activity_time = 0 # Last time the application successfully sent or received any data self.internet_outage_threshold = 60 * 2 @@ -382,8 +384,10 @@ class ConnectionServer(object): self.has_internet = status if self.has_internet: + self.internet_online_since = time.time() gevent.spawn(self.onInternetOnline) else: + self.internet_offline_since = time.time() gevent.spawn(self.onInternetOffline) def isInternetOnline(self): diff --git a/src/File/FileServer.py b/src/File/FileServer.py index de14a9c9..66c8c135 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -33,8 +33,11 @@ class FileServer(ConnectionServer): # self.log = logging.getLogger("FileServer") # The value of self.log will be overwritten in ConnectionServer.__init__() + self.recheck_port = True + self.update_pool = gevent.pool.Pool(5) self.update_start_time = 0 + self.update_sites_task_next_nr = 1 self.supported_ip_types = ["ipv4"] # Outgoing ip_type support if helper.getIpType(ip) == "ipv6" or self.isIpv6Supported(): @@ -158,7 +161,13 @@ class FileServer(ConnectionServer): def onInternetOnline(self): log.info("Internet online") - gevent.spawn(self.updateSites, force_port_check=True) + invalid_interval=( + self.internet_offline_since - self.internet_outage_threshold - random.randint(60 * 5, 60 * 10), + time.time() + ) + self.invalidateUpdateTime(invalid_interval) + self.recheck_port = True + gevent.spawn(self.updateSites) # Reload the FileRequest class to prevent restarts in debug mode def reload(self): @@ -237,6 +246,17 @@ class FileServer(ConnectionServer): return res + @util.Noparallel(queue=True) + def recheckPort(self): + if not self.recheck_port: + return + + if not self.port_opened or self.recheck_port: + self.portCheck() + if not self.port_opened["ipv4"]: + self.tor_manager.startOnions() + self.recheck_port = False + # Returns False if Internet is immediately available # Returns True if we've spent some time waiting for Internet def waitForInternetOnline(self): @@ -250,6 +270,7 @@ class FileServer(ConnectionServer): if not self.update_pool.full(): self.update_pool.spawn(self.updateRandomSite) + self.recheckPort() return True def updateRandomSite(self, site_addresses=None): @@ -270,46 +291,68 @@ class FileServer(ConnectionServer): self.updateSite(site) - def updateSite(self, site): + def updateSite(self, site, check_files=False, dry_run=False): if not site or not site.isServing(): - return - site.considerUpdate() + return False + return site.considerUpdate(check_files=check_files, dry_run=dry_run) - @util.Noparallel() - def updateSites(self, force_port_check=False): - log.info("Checking sites: force_port_check=%s", force_port_check) + def getSite(self, address): + return self.sites.get(address, None) + + def invalidateUpdateTime(self, invalid_interval): + for address in self.getSiteAddresses(): + site = self.getSite(address) + if site: + site.invalidateUpdateTime(invalid_interval) + + def updateSites(self, check_files=False): + task_nr = self.update_sites_task_next_nr + self.update_sites_task_next_nr += 1 + + task_description = "updateSites: #%d, check_files=%s" % (task_nr, check_files) + log.info("%s: started", task_description) # Don't wait port opening on first startup. Do the instant check now. if len(self.sites) <= 2: - sites_checking = True for address, site in list(self.sites.items()): - gevent.spawn(self.updateSite, site) + self.updateSite(site, check_files=check_files) - # Test and open port if not tested yet - if not self.port_opened or force_port_check: - self.portCheck() - if not self.port_opened["ipv4"]: - self.tor_manager.startOnions() + all_site_addresses = self.getSiteAddresses() + site_addresses = [ + address for address in all_site_addresses + if self.updateSite(self.getSite(address), check_files=check_files, dry_run=True) + ] - site_addresses = self.getSiteAddresses() + log.info("%s: chosen %d sites (of %d)", task_description, len(site_addresses), len(all_site_addresses)) sites_processed = 0 + sites_skipped = 0 start_time = time.time() self.update_start_time = start_time progress_print_time = time.time() # Check sites integrity for site_address in site_addresses: - site = self.sites.get(site_address, None) + if check_files: + time.sleep(10) + else: + time.sleep(1) + + site = self.getSite(site_address) + if not self.updateSite(site, check_files=check_files, dry_run=True): + sites_skipped += 1 + continue sites_processed += 1 - if (not site) or (not site.isServing()): - continue - while 1: self.waitForInternetOnline() - self.update_pool.spawn(self.updateSite, site) + thread = self.update_pool.spawn(self.updateSite, + site, check_files=check_files) + if check_files: + # Limit the concurency + # ZeroNet may be laggy when running from HDD. + thread.join(timeout=60) if not self.waitForInternetOnline(): break @@ -319,21 +362,17 @@ class FileServer(ConnectionServer): time_per_site = time_spent / float(sites_processed) sites_left = len(site_addresses) - sites_processed time_left = time_per_site * sites_left - log.info("Checking sites: DONE: %d sites in %.2fs (%.2fs per site); LEFT: %d sites in %.2fs", + log.info("%s: DONE: %d sites in %.2fs (%.2fs per site); SKIPPED: %d sites; LEFT: %d sites in %.2fs", + task_description, sites_processed, time_spent, time_per_site, + sites_skipped, sites_left, time_left ) - if (self.update_start_time != start_time) and self.update_pool.full(): - # Another updateSites() is running, throttling now... - time.sleep(5) - else: - time.sleep(1) - - log.info("Checking sites: finished in %.2fs" % (time.time() - start_time)) + log.info("%s: finished in %.2fs", task_description, time.time() - start_time) def sitesMaintenanceThread(self, mode="full"): startup = True @@ -402,7 +441,7 @@ class FileServer(ConnectionServer): startup = False def keepAliveThread(self): - # This thread is mostly useless on a loaded system, since it never does + # This thread is mostly useless on a system under load, since it never does # any works, if we have active traffic. # # We should initiate some network activity to detect the Internet outage @@ -466,7 +505,13 @@ class FileServer(ConnectionServer): log.info("IP change detected from %s to %s" % (last_my_ips, my_ips)) if is_time_changed or is_ip_changed: - self.updateSites(force_port_check=True) + invalid_interval=( + last_time - self.internet_outage_threshold - random.randint(60 * 5, 60 * 10), + time.time() + ) + self.invalidateUpdateTime(invalid_interval) + self.recheck_port = True + gevent.spawn(self.updateSites) last_time = time.time() last_my_ips = my_ips @@ -494,7 +539,9 @@ class FileServer(ConnectionServer): DebugReloader.watcher.addCallback(self.reload) if check_sites: # Open port, Update sites, Check files integrity - gevent.spawn(self.updateSites) + gevent.spawn(self.updateSites, check_files=True) + + gevent.spawn(self.updateSites) thread_reaload_tracker_files = gevent.spawn(self.reloadTrackerFilesThread) thread_sites_maintenance_full = gevent.spawn(self.sitesMaintenanceThread, mode="full") diff --git a/src/Site/Site.py b/src/Site/Site.py index 6d72e119..14077aae 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -81,8 +81,6 @@ class Site(object): scaler=self.getActivityRating) ] - self.delayed_startup_announce = False - self.content = None # Load content.json self.peers = {} # Key: ip:port, Value: Peer.Peer self.peers_recent = collections.deque(maxlen=150) @@ -93,6 +91,7 @@ class Site(object): self.content_updated = None # Content.js update time self.last_check_files_time = 0 self.last_online_update = 0 + self.startup_announce_done = 0 self.notifications = [] # Pending notifications displayed once on page load [error|ok|info, message, timeout] self.page_requested = False # Page viewed in browser self.websockets = [] # Active site websocket connections @@ -525,6 +524,28 @@ class Site(object): time.sleep(0.1) return queried + def invalidateUpdateTime(self, invalid_interval): + a, b = invalid_interval + if b is None: + b = time.time() + if a is None: + a = b + if a <= self.last_online_update and self.last_online_update <= b: + self.last_online_update = 0 + self.log.info("Update time invalidated") + + def isUpdateTimeValid(self): + if not self.last_online_update: + return False + expirationThreshold = 60 * 60 * 6 + return self.last_online_update > time.time() - expirationThreshold + + def refreshUpdateTime(self, valid=True): + if valid: + self.last_online_update = time.time() + else: + self.last_online_update = 0 + # Update content.json from peers and download changed files # Return: None @util.Noparallel() @@ -564,45 +585,56 @@ class Site(object): else: self.content_updated = time.time() + self.sendMyHashfield() + self.updateHashfield() + + self.refreshUpdateTime(valid=self.connection_server.isInternetOnline()) + self.updateWebsocket(updated=True) - def considerUpdate(self): + @util.Noparallel(queue=True, ignore_args=True) + def considerUpdate(self, check_files=False, dry_run=False): if not self.isServing(): - return + return False online = self.connection_server.isInternetOnline() - if online and time.time() - self.last_online_update < 60 * 10: - with gevent.Timeout(10, exception=False): - self.announcer.announcePex() - return + run_update = False + msg = None + + if not online: + run_update = True + msg = "network connection seems broken, trying to update the site to check if the network is up" + elif check_files: + run_update = True + msg = "checking site's files..." + elif not self.isUpdateTimeValid(): + run_update = True + msg = "update time is not invalid, updating now..." + + if not run_update: + return False + + if dry_run: + return True + + self.log.debug(msg) # TODO: there should be a configuration options controlling: # * whether to check files on the program startup # * whether to check files during the run time and how often - check_files = self.last_check_files_time == 0 - + check_files = check_files and (self.last_check_files_time == 0) self.last_check_files_time = time.time() - # quick start, avoiding redundant announces - if len(self.peers) >= 50: - if len(self.getConnectedPeers()) > 4: - pass # Don't run announce() at all - else: - self.setDelayedStartupAnnounce() - else: - self.announce(mode="startup") - - online = online and self.connection_server.isInternetOnline() + if len(self.peers) < 50: + self.announce(mode="update") self.update(check_files=check_files) - self.sendMyHashfield() - self.updateHashfield() online = online and self.connection_server.isInternetOnline() + self.refreshUpdateTime(valid=online) - if online: - self.last_online_update = time.time() + return True # Update site by redownload all content.json def redownloadContents(self): @@ -932,16 +964,6 @@ class Site(object): peer.found(source) return peer - def setDelayedStartupAnnounce(self): - self.delayed_startup_announce = True - - def applyDelayedStartupAnnounce(self): - if self.delayed_startup_announce: - self.delayed_startup_announce = False - self.announce(mode="startup") - return True - return False - def announce(self, *args, **kwargs): if self.isServing(): self.announcer.announce(*args, **kwargs) @@ -1243,8 +1265,6 @@ class Site(object): if not self.isServing(): return False - self.applyDelayedStartupAnnounce() - if not self.peers: return False @@ -1259,8 +1279,6 @@ class Site(object): self.announcer.announcePex() self.update() - self.sendMyHashfield(3) - self.updateHashfield(3) return True @@ -1270,10 +1288,11 @@ class Site(object): self.log.debug("periodicMaintenanceHandler_announce: startup=%s, force=%s" % (startup, force)) - if self.applyDelayedStartupAnnounce(): - return True + if startup and len(self.peers) < 10: + self.announce(mode="startup") + else: + self.announce(mode="update", pex=False) - self.announce(mode="update", pex=False) return True # Send hashfield to peers From ba16fdcae9473025ce5bf930ed8733ee476a6b3e Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Mon, 2 Nov 2020 09:04:38 +0700 Subject: [PATCH 043/191] Fix a typo --- src/Site/Site.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 14077aae..e808b706 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -610,7 +610,7 @@ class Site(object): msg = "checking site's files..." elif not self.isUpdateTimeValid(): run_update = True - msg = "update time is not invalid, updating now..." + msg = "update time is not valid, updating now..." if not run_update: return False From c84b413f58bfc810f1e56b7e6414367f81efadf6 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Tue, 3 Nov 2020 21:21:33 +0700 Subject: [PATCH 044/191] Refactor ConnectionServer, FileServer; fix bugs introduced in previous commits --- src/Connection/ConnectionServer.py | 89 +++++++++++++++-- src/File/FileServer.py | 148 ++++++++++++++++++----------- src/Site/Site.py | 66 ++++++++----- src/Site/SiteAnnouncer.py | 6 ++ 4 files changed, 222 insertions(+), 87 deletions(-) diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index 3ec0932d..b66d1739 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -8,6 +8,7 @@ import gevent import msgpack from gevent.server import StreamServer from gevent.pool import Pool +import gevent.event import util from util import helper @@ -35,6 +36,8 @@ class ConnectionServer(object): self.port_opened = {} self.peer_blacklist = SiteManager.peer_blacklist + self.managed_pools = {} + self.tor_manager = TorManager(self.ip, self.port) self.connections = [] # Connections self.whitelist = config.ip_local # No flood protection on this ips @@ -53,8 +56,12 @@ class ConnectionServer(object): self.stream_server_proxy = None self.running = False self.stopping = False + self.stopping_event = gevent.event.Event() self.thread_checker = None + self.thread_pool = Pool(None) + self.managed_pools["thread"] = self.thread_pool + self.stat_recv = defaultdict(lambda: defaultdict(int)) self.stat_sent = defaultdict(lambda: defaultdict(int)) self.bytes_recv = 0 @@ -70,6 +77,7 @@ class ConnectionServer(object): self.timecorrection = 0.0 self.pool = Pool(500) # do not accept more than 500 connections + self.managed_pools["incoming"] = self.pool # Bittorrent style peerid self.peer_id = "-UT3530-%s" % CryptHash.random(12, "base64") @@ -90,7 +98,7 @@ class ConnectionServer(object): return False self.running = True if check_connections: - self.thread_checker = gevent.spawn(self.checkConnections) + self.thread_checker = self.spawn(self.checkConnections) CryptConnection.manager.loadCerts() if config.tor != "disable": self.tor_manager.start() @@ -114,7 +122,7 @@ class ConnectionServer(object): return None if self.stream_server_proxy: - gevent.spawn(self.listenProxy) + self.spawn(self.listenProxy) try: self.stream_server.serve_forever() except Exception as err: @@ -126,18 +134,65 @@ class ConnectionServer(object): self.log.debug("Stopping %s" % self.stream_server) self.stopping = True self.running = False + self.stopping_event.set() + self.onStop() + + def onStop(self): + prev_sizes = {} + for i in range(60): + sizes = {} + total_size = 0 + + for name, pool in self.managed_pools.items(): + pool.join(timeout=1) + size = len(pool) + sizes[name] = size + total_size += size + + if total_size == 0: + break + + if prev_sizes != sizes: + s = "" + for name, size in sizes.items(): + s += "%s pool: %s, " % (name, size) + s += "total: %s" % total_size + + self.log.info("Waiting for tasks in managed pools to stop: %s", s) + + prev_sizes = sizes + + for name, pool in self.managed_pools.items(): + size = len(pool) + if size: + self.log.info("Killing %s tasks in %s pool", size, name) + pool.kill() + if self.thread_checker: gevent.kill(self.thread_checker) + self.thread_checker = None if self.stream_server: self.stream_server.stop() + # Sleeps the specified amount of time or until ConnectionServer is stopped + def sleep(self, t): + if t: + self.stopping_event.wait(timeout=t) + else: + time.sleep(t) + + # Spawns a thread that will be waited for on server being stooped (and killed after a timeout) + def spawn(self, *args, **kwargs): + thread = self.thread_pool.spawn(*args, **kwargs) + return thread + def closeConnections(self): self.log.debug("Closing all connection: %s" % len(self.connections)) for connection in self.connections[:]: connection.close("Close all connections") def handleIncomingConnection(self, sock, addr): - if config.offline: + if self.allowsAcceptingConnections(): sock.close() return False @@ -155,7 +210,7 @@ class ConnectionServer(object): self.ip_incoming[ip] += 1 if self.ip_incoming[ip] > 6: # Allow 6 in 1 minute from same ip self.log.debug("Connection flood detected from %s" % ip) - time.sleep(30) + self.sleep(30) sock.close() return False else: @@ -207,7 +262,7 @@ class ConnectionServer(object): return connection # No connection found - if create and not config.offline: # Allow to create new connection if not found + if create and self.allowsCreatingConnections(): if port == 0: raise Exception("This peer is not connectable") @@ -233,7 +288,7 @@ class ConnectionServer(object): raise err if len(self.connections) > config.global_connected_limit: - gevent.spawn(self.checkMaxConnections) + self.spawn(self.checkMaxConnections) return connection else: @@ -256,7 +311,7 @@ class ConnectionServer(object): def checkConnections(self): run_i = 0 - time.sleep(15) + self.sleep(15) while self.running: run_i += 1 self.ip_incoming = {} # Reset connected ips counter @@ -321,7 +376,7 @@ class ConnectionServer(object): if time.time() - s > 0.01: self.log.debug("Connection cleanup in %.3fs" % (time.time() - s)) - time.sleep(15) + self.sleep(15) self.log.debug("Checkconnections ended") @util.Noparallel(blocking=False) @@ -385,10 +440,10 @@ class ConnectionServer(object): if self.has_internet: self.internet_online_since = time.time() - gevent.spawn(self.onInternetOnline) + self.spawn(self.onInternetOnline) else: self.internet_offline_since = time.time() - gevent.spawn(self.onInternetOffline) + self.spawn(self.onInternetOffline) def isInternetOnline(self): return self.has_internet @@ -400,6 +455,20 @@ class ConnectionServer(object): self.had_external_incoming = False self.log.info("Internet offline") + def allowsCreatingConnections(self): + if config.offline: + return False + if self.stopping: + return False + return True + + def allowsAcceptingConnections(self): + if config.offline: + return False + if self.stopping: + return False + return True + def getTimecorrection(self): corrections = sorted([ connection.handshake.get("time") - connection.handshake_time + connection.last_ping_delay diff --git a/src/File/FileServer.py b/src/File/FileServer.py index 66c8c135..b228680a 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -64,6 +64,8 @@ class FileServer(ConnectionServer): ConnectionServer.__init__(self, ip, port, self.handleRequest) log.debug("Supported IP types: %s" % self.supported_ip_types) + self.managed_pools["update"] = self.pool + if ip_type == "dual" and ip == "::": # Also bind to ipv4 addres in dual mode try: @@ -76,18 +78,28 @@ class FileServer(ConnectionServer): self.port_opened = {} - self.sites = self.site_manager.sites self.last_request = time.time() self.files_parsing = {} self.ui_server = None + def getSites(self): + sites = self.site_manager.list() + # We need to keep self.sites for the backward compatibility with plugins. + # Never. Ever. Use it. + # TODO: fix plugins + self.sites = sites + return sites + + def getSite(self, address): + return self.getSites().get(address, None) + def getSiteAddresses(self): # Avoid saving the site list on the stack, since a site may be deleted # from the original list while iterating. # Use the list of addresses instead. return [ site.address for site in - sorted(list(self.sites.values()), key=lambda site: site.settings.get("modified", 0), reverse=True) + sorted(list(self.getSites().values()), key=lambda site: site.settings.get("modified", 0), reverse=True) ] def getRandomPort(self, ip, port_range_from, port_range_to): @@ -110,7 +122,7 @@ class FileServer(ConnectionServer): log.info("Found unused random port: %s" % port) return port else: - time.sleep(0.1) + self.sleep(0.1) return False def isIpv6Supported(self): @@ -157,7 +169,7 @@ class FileServer(ConnectionServer): req = FileRequest(self, connection) req.route(message["cmd"], message.get("req_id"), message.get("params")) if not connection.is_private_ip: - self.setInternetStatus(self, True) + self.setInternetStatus(True) def onInternetOnline(self): log.info("Internet online") @@ -167,7 +179,7 @@ class FileServer(ConnectionServer): ) self.invalidateUpdateTime(invalid_interval) self.recheck_port = True - gevent.spawn(self.updateSites) + self.spawn(self.updateSites) # Reload the FileRequest class to prevent restarts in debug mode def reload(self): @@ -201,7 +213,7 @@ class FileServer(ConnectionServer): self.ui_server.updateWebsocket() if "ipv6" in self.supported_ip_types: - res_ipv6_thread = gevent.spawn(self.portchecker.portCheck, self.port, "ipv6") + res_ipv6_thread = self.spawn(self.portchecker.portCheck, self.port, "ipv6") else: res_ipv6_thread = None @@ -251,7 +263,7 @@ class FileServer(ConnectionServer): if not self.recheck_port: return - if not self.port_opened or self.recheck_port: + if not self.port_opened: self.portCheck() if not self.port_opened["ipv4"]: self.tor_manager.startOnions() @@ -259,21 +271,28 @@ class FileServer(ConnectionServer): # Returns False if Internet is immediately available # Returns True if we've spent some time waiting for Internet + # Returns None if FileServer is stopping or the Offline mode is enabled def waitForInternetOnline(self): + if config.offline or self.stopping: + return None + if self.isInternetOnline(): return False while not self.isInternetOnline(): - time.sleep(15) + self.sleep(30) + if config.offline or self.stopping: + return None if self.isInternetOnline(): break - if not self.update_pool.full(): - self.update_pool.spawn(self.updateRandomSite) + if len(self.update_pool) == 0: + thread = self.update_pool.spawn(self.updateRandomSite) + thread.join() self.recheckPort() return True - def updateRandomSite(self, site_addresses=None): + def updateRandomSite(self, site_addresses=None, force=False): if not site_addresses: site_addresses = self.getSiteAddresses() @@ -282,22 +301,19 @@ class FileServer(ConnectionServer): return address = site_addresses[0] - site = self.sites.get(address, None) + site = self.getSite(address) - if not site or not site.isServing(): + if not site: return log.debug("Checking randomly chosen site: %s", site.address_short) - self.updateSite(site) + self.updateSite(site, force=force) - def updateSite(self, site, check_files=False, dry_run=False): - if not site or not site.isServing(): + def updateSite(self, site, check_files=False, force=False, dry_run=False): + if not site: return False - return site.considerUpdate(check_files=check_files, dry_run=dry_run) - - def getSite(self, address): - return self.sites.get(address, None) + return site.considerUpdate(check_files=check_files, force=force, dry_run=dry_run) def invalidateUpdateTime(self, invalid_interval): for address in self.getSiteAddresses(): @@ -313,8 +329,8 @@ class FileServer(ConnectionServer): log.info("%s: started", task_description) # Don't wait port opening on first startup. Do the instant check now. - if len(self.sites) <= 2: - for address, site in list(self.sites.items()): + if len(self.getSites()) <= 2: + for address, site in list(self.getSites().items()): self.updateSite(site, check_files=check_files) all_site_addresses = self.getSiteAddresses() @@ -334,9 +350,12 @@ class FileServer(ConnectionServer): # Check sites integrity for site_address in site_addresses: if check_files: - time.sleep(10) + self.sleep(10) else: - time.sleep(1) + self.sleep(1) + + if self.stopping: + break site = self.getSite(site_address) if not self.updateSite(site, check_files=check_files, dry_run=True): @@ -345,10 +364,9 @@ class FileServer(ConnectionServer): sites_processed += 1 - while 1: + while self.running: self.waitForInternetOnline() - thread = self.update_pool.spawn(self.updateSite, - site, check_files=check_files) + thread = self.update_pool.spawn(self.updateSite, site, check_files=check_files) if check_files: # Limit the concurency # ZeroNet may be laggy when running from HDD. @@ -383,8 +401,11 @@ class FileServer(ConnectionServer): long_timeout = min_long_timeout short_cycle_time_limit = 60 * 2 - while 1: - time.sleep(long_timeout) + while self.running: + self.sleep(long_timeout) + + if self.stopping: + break start_time = time.time() @@ -400,8 +421,11 @@ class FileServer(ConnectionServer): sites_processed = 0 for site_address in site_addresses: - site = self.sites.get(site_address, None) - if (not site) or (not site.isServing()): + if self.stopping: + break + + site = self.getSite(site_address) + if not site: continue log.debug("Running maintenance for site: %s", site.address_short) @@ -410,7 +434,7 @@ class FileServer(ConnectionServer): site = None if done: sites_processed += 1 - time.sleep(short_timeout) + self.sleep(short_timeout) # If we host hundreds of sites, the full maintenance cycle may take very # long time, especially on startup ( > 1 hour). @@ -454,19 +478,24 @@ class FileServer(ConnectionServer): # are interested in connecting to them), we initiate some traffic by # performing the update for a random site. It's way better than just # silly pinging a random peer for no profit. - while 1: - threshold = self.internet_outage_threshold / 2.0 - time.sleep(threshold / 2.0) + while self.running: self.waitForInternetOnline() + + threshold = self.internet_outage_threshold / 2.0 + + self.sleep(threshold / 2.0) + if self.stopping: + break + last_activity_time = max( self.last_successful_internet_activity_time, self.last_outgoing_internet_activity_time) now = time.time() - if not len(self.sites): + if not len(self.getSites()): continue if last_activity_time > now - threshold: continue - if self.update_pool.full(): + if len(self.update_pool) == 0: continue log.info("No network activity for %.2fs. Running an update for a random site.", @@ -481,16 +510,18 @@ class FileServer(ConnectionServer): # We should check if the files have actually changed, # and do it more often. interval = 60 * 10 - while 1: - time.sleep(interval) + while self.running: + self.sleep(interval) + if self.stopping: + break config.loadTrackersFile() # Detects if computer back from wakeup def wakeupWatcher(self): last_time = time.time() last_my_ips = socket.gethostbyname_ex('')[2] - while 1: - time.sleep(30) + while self.running: + self.sleep(30) is_time_changed = time.time() - max(self.last_request, last_time) > 60 * 3 if is_time_changed: # If taken more than 3 minute then the computer was in sleep mode @@ -511,17 +542,28 @@ class FileServer(ConnectionServer): ) self.invalidateUpdateTime(invalid_interval) self.recheck_port = True - gevent.spawn(self.updateSites) + self.spawn(self.updateSites) last_time = time.time() last_my_ips = my_ips # Bind and start serving sites - def start(self, check_sites=True): + # If passive_mode is False, FileServer starts the full-featured file serving: + # * Checks for updates at startup. + # * Checks site's integrity. + # * Runs periodic update checks. + # * Watches for internet being up or down and for computer to wake up and runs update checks. + # If passive_mode is True, all the mentioned activity is disabled. + def start(self, passive_mode=False, check_sites=None, check_connections=True): + + # Backward compatibility for a misnamed argument: + if check_sites is not None: + passive_mode = not check_sites + if self.stopping: return False - ConnectionServer.start(self) + ConnectionServer.start(self, check_connections=check_connections) try: self.stream_server.start() @@ -532,22 +574,22 @@ class FileServer(ConnectionServer): sys.modules["main"].ui_server.stop() return False - self.sites = self.site_manager.list() if config.debug: # Auto reload FileRequest on change from Debug import DebugReloader DebugReloader.watcher.addCallback(self.reload) - if check_sites: # Open port, Update sites, Check files integrity - gevent.spawn(self.updateSites, check_files=True) + if not passive_mode: + self.spawn(self.updateSites) + thread_reaload_tracker_files = self.spawn(self.reloadTrackerFilesThread) + thread_sites_maintenance_full = self.spawn(self.sitesMaintenanceThread, mode="full") + thread_sites_maintenance_short = self.spawn(self.sitesMaintenanceThread, mode="short") + thread_keep_alive = self.spawn(self.keepAliveThread) + thread_wakeup_watcher = self.spawn(self.wakeupWatcher) - gevent.spawn(self.updateSites) + self.sleep(0.1) + self.spawn(self.updateSites, check_files=True) - thread_reaload_tracker_files = gevent.spawn(self.reloadTrackerFilesThread) - thread_sites_maintenance_full = gevent.spawn(self.sitesMaintenanceThread, mode="full") - thread_sites_maintenance_short = gevent.spawn(self.sitesMaintenanceThread, mode="short") - thread_keep_alive = gevent.spawn(self.keepAliveThread) - thread_wakeup_watcher = gevent.spawn(self.wakeupWatcher) ConnectionServer.listen(self) diff --git a/src/Site/Site.py b/src/Site/Site.py index e808b706..1581a106 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -172,6 +172,8 @@ class Site(object): def isServing(self): if config.offline: return False + elif self.connection_server.stopping: + return False else: return self.settings["serving"] @@ -532,7 +534,7 @@ class Site(object): a = b if a <= self.last_online_update and self.last_online_update <= b: self.last_online_update = 0 - self.log.info("Update time invalidated") + self.log.debug("Update time invalidated") def isUpdateTimeValid(self): if not self.last_online_update: @@ -593,33 +595,12 @@ class Site(object): self.updateWebsocket(updated=True) @util.Noparallel(queue=True, ignore_args=True) - def considerUpdate(self, check_files=False, dry_run=False): - if not self.isServing(): + def _considerUpdate_realJob(self, check_files=False, force=False): + if not self._considerUpdate_check(check_files=check_files, force=force, log_reason=True): return False online = self.connection_server.isInternetOnline() - run_update = False - msg = None - - if not online: - run_update = True - msg = "network connection seems broken, trying to update the site to check if the network is up" - elif check_files: - run_update = True - msg = "checking site's files..." - elif not self.isUpdateTimeValid(): - run_update = True - msg = "update time is not valid, updating now..." - - if not run_update: - return False - - if dry_run: - return True - - self.log.debug(msg) - # TODO: there should be a configuration options controlling: # * whether to check files on the program startup # * whether to check files during the run time and how often @@ -628,6 +609,7 @@ class Site(object): if len(self.peers) < 50: self.announce(mode="update") + online = online and self.connection_server.isInternetOnline() self.update(check_files=check_files) @@ -636,6 +618,39 @@ class Site(object): return True + def _considerUpdate_check(self, check_files=False, force=False, log_reason=False): + if not self.isServing(): + return False + + online = self.connection_server.isInternetOnline() + + run_update = False + msg = None + + if force: + run_update = True + msg = "forcing site update" + elif not online: + run_update = True + msg = "network connection seems broken, trying to update a site to check if the network is up" + elif check_files: + run_update = True + msg = "checking site's files..." + elif not self.isUpdateTimeValid(): + run_update = True + msg = "update time is not valid, updating now..." + + if run_update and log_reason: + self.log.debug(msg) + + return run_update + + def considerUpdate(self, check_files=False, force=False, dry_run=False): + run_update = self._considerUpdate_check(check_files=check_files, force=force) + if run_update and not dry_run: + run_update = self._considerUpdate_realJob(check_files=check_files, force=force) + return run_update + # Update site by redownload all content.json def redownloadContents(self): # Download all content.json again @@ -1085,6 +1100,9 @@ class Site(object): # Keep connections def needConnections(self, num=None, check_site_on_reconnect=False, pex=True): + if not self.connection_server.allowsCreatingConnections(): + return + if num is None: num = self.getPreferableActiveConnectionCount() diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index 5a97807e..6a510583 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -127,6 +127,9 @@ class SiteAnnouncer(object): @util.Noparallel() def announce(self, force=False, mode="start", pex=True): + if not self.site.isServing(): + return + if time.time() - self.time_last_announce < 30 and not force: return # No reannouncing within 30 secs @@ -300,6 +303,9 @@ class SiteAnnouncer(object): @util.Noparallel(blocking=False) def announcePex(self, query_num=2, need_num=10): + if not self.site.isServing(): + return + self.updateWebsocket(pex="announcing") peers = self.site.getConnectedPeers() From 325f071329eaf7abe97e6fe4d79f6cbeb8163562 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Tue, 3 Nov 2020 23:25:29 +0700 Subject: [PATCH 045/191] Fixes and refactoring in Connection.py, Peer.py --- src/Connection/Connection.py | 86 ++++++++++++++++++++++-------------- src/Peer/Peer.py | 17 ++++--- 2 files changed, 64 insertions(+), 39 deletions(-) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 27ae3734..4a7c6ab9 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -118,6 +118,9 @@ class Connection(object): # Open connection to peer and wait for handshake def connect(self): + if not self.event_connected or self.event_connected.ready(): + self.event_connected = gevent.event.AsyncResult() + self.type = "out" if self.ip_type == "onion": if not self.server.tor_manager or not self.server.tor_manager.enabled: @@ -148,37 +151,56 @@ class Connection(object): self.sock.connect(sock_address) - # Implicit SSL - should_encrypt = not self.ip_type == "onion" and self.ip not in self.server.broken_ssl_ips and self.ip not in config.ip_local - if self.cert_pin: - self.sock = CryptConnection.manager.wrapSocket(self.sock, "tls-rsa", cert_pin=self.cert_pin) - self.sock.do_handshake() - self.crypt = "tls-rsa" - self.sock_wrapped = True - elif should_encrypt and "tls-rsa" in CryptConnection.manager.crypt_supported: + if self.shouldEncrypt(): try: - self.sock = CryptConnection.manager.wrapSocket(self.sock, "tls-rsa") - self.sock.do_handshake() - self.crypt = "tls-rsa" - self.sock_wrapped = True + self.wrapSocket() except Exception as err: - if not config.force_encryption: - self.log("Crypt connection error, adding %s:%s as broken ssl. %s" % (self.ip, self.port, Debug.formatException(err))) - self.server.broken_ssl_ips[self.ip] = True - self.sock.close() - self.crypt = None - self.sock = self.createSocket() - self.sock.settimeout(30) - self.sock.connect(sock_address) + if self.sock: + self.sock.close() + self.sock = None + if self.mustEncrypt(): + raise + self.log("Crypt connection error, adding %s:%s as broken ssl. %s" % (self.ip, self.port, Debug.formatException(err))) + self.server.broken_ssl_ips[self.ip] = True + return self.connect() # Detect protocol - self.send({"cmd": "handshake", "req_id": 0, "params": self.getHandshakeInfo()}) event_connected = self.event_connected + self.send({"cmd": "handshake", "req_id": 0, "params": self.getHandshakeInfo()}) gevent.spawn(self.messageLoop) connect_res = event_connected.get() # Wait for handshake - self.sock.settimeout(timeout_before) + if self.sock: + self.sock.settimeout(timeout_before) return connect_res + def mustEncrypt(self): + if self.cert_pin: + return True + if (not self.ip_type == "onion") and config.force_encryption: + return True + return False + + def shouldEncrypt(self): + if self.mustEncrypt(): + return True + return ( + (not self.ip_type == "onion") + and + (self.ip not in self.server.broken_ssl_ips) + and + (self.ip not in config.ip_local) + and + ("tls-rsa" in CryptConnection.manager.crypt_supported) + ) + + def wrapSocket(self, crypt="tls-rsa", do_handshake=True): + server = (self.type == "in") + sock = CryptConnection.manager.wrapSocket(self.sock, crypt, server=server, cert_pin=self.cert_pin) + sock.do_handshake() + self.crypt = crypt + self.sock_wrapped = True + self.sock = sock + # Handle incoming connection def handleIncomingConnection(self, sock): self.log("Incoming connection...") @@ -192,9 +214,7 @@ class Connection(object): first_byte = sock.recv(1, gevent.socket.MSG_PEEK) if first_byte == b"\x16": self.log("Crypt in connection using implicit SSL") - self.sock = CryptConnection.manager.wrapSocket(self.sock, "tls-rsa", True) - self.sock_wrapped = True - self.crypt = "tls-rsa" + self.wrapSocket(do_handshake=False) except Exception as err: self.log("Socket peek error: %s" % Debug.formatException(err)) self.messageLoop() @@ -435,7 +455,6 @@ class Connection(object): self.updateName() self.event_connected.set(True) # Mark handshake as done - self.event_connected = None self.handshake_time = time.time() # Handle incoming message @@ -459,12 +478,10 @@ class Connection(object): self.last_ping_delay = ping # Server switched to crypt, lets do it also if not crypted already if message.get("crypt") and not self.sock_wrapped: - self.crypt = message["crypt"] + crypt = message["crypt"] server = (self.type == "in") - self.log("Crypt out connection using: %s (server side: %s, ping: %.3fs)..." % (self.crypt, server, ping)) - self.sock = CryptConnection.manager.wrapSocket(self.sock, self.crypt, server, cert_pin=self.cert_pin) - self.sock.do_handshake() - self.sock_wrapped = True + self.log("Crypt out connection using: %s (server side: %s, ping: %.3fs)..." % (crypt, server, ping)) + self.wrapSocket(crypt) if not self.sock_wrapped and self.cert_pin: self.close("Crypt connection error: Socket not encrypted, but certificate pin present") @@ -492,8 +509,7 @@ class Connection(object): server = (self.type == "in") self.log("Crypt in connection using: %s (server side: %s)..." % (self.crypt, server)) try: - self.sock = CryptConnection.manager.wrapSocket(self.sock, self.crypt, server, cert_pin=self.cert_pin) - self.sock_wrapped = True + self.wrapSocket(self.crypt) except Exception as err: if not config.force_encryption: self.log("Crypt connection error, adding %s:%s as broken ssl. %s" % (self.ip, self.port, Debug.formatException(err))) @@ -640,6 +656,10 @@ class Connection(object): self.sock = None self.unpacker = None self.event_connected = None + self.crypt = None + self.sock_wrapped = False + + return True def updateOnlineStatus(self, outgoing_activity=False, successful_activity=False): self.server.updateOnlineStatus(self, diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index 0a518fdc..2e809b26 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -33,6 +33,7 @@ class Peer(object): self.key = "%s:%s" % (ip, port) self.log_level = logging.DEBUG + self.connection_error_log_level = logging.DEBUG self.connection = None self.connection_server = connection_server @@ -61,8 +62,10 @@ class Peer(object): else: return getattr(self, key) - def log(self, text): - if self.log_level <= logging.DEBUG: + def log(self, text, log_level = None): + if log_level is None: + log_level = self.log_level + if log_level <= logging.DEBUG: if not config.verbose: return # Only log if we are in debug mode @@ -73,7 +76,7 @@ class Peer(object): else: logger = logging.getLogger() - logger.log(self.log_level, "%s:%s %s" % (self.ip, self.port, text)) + logger.log(log_level, "%s:%s %s" % (self.ip, self.port, text)) # Site marks its Peers protected, if it has not enough peers connected. # This is to be used to prevent disconnecting from peers when doing @@ -124,12 +127,14 @@ class Peer(object): import main connection_server = main.file_server self.connection = connection_server.getConnection(self.ip, self.port, site=self.site, is_tracker_connection=self.is_tracker_connection) - self.reputation += 1 - self.connection.sites += 1 + if self.connection and self.connection.connected: + self.reputation += 1 + self.connection.sites += 1 except Exception as err: self.onConnectionError("Getting connection error") self.log("Getting connection error: %s (connection_error: %s, hash_failed: %s)" % - (Debug.formatException(err), self.connection_error, self.hash_failed)) + (Debug.formatException(err), self.connection_error, self.hash_failed), + log_level=self.connection_error_log_level) self.connection = None return self.connection From 90d01e6004544871e87abf5abbcd27c40bb44e2c Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 4 Nov 2020 09:25:44 +0700 Subject: [PATCH 046/191] Fix a tor issue introduced in the latest changes --- src/Connection/ConnectionServer.py | 1 + src/File/FileServer.py | 9 +++------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index b66d1739..20bd165a 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -102,6 +102,7 @@ class ConnectionServer(object): CryptConnection.manager.loadCerts() if config.tor != "disable": self.tor_manager.start() + self.tor_manager.startOnions() if not self.port: self.log.info("No port found, not binding") return False diff --git a/src/File/FileServer.py b/src/File/FileServer.py index b228680a..eea34ff0 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -260,13 +260,8 @@ class FileServer(ConnectionServer): @util.Noparallel(queue=True) def recheckPort(self): - if not self.recheck_port: - return - - if not self.port_opened: + if self.recheck_port: self.portCheck() - if not self.port_opened["ipv4"]: - self.tor_manager.startOnions() self.recheck_port = False # Returns False if Internet is immediately available @@ -322,6 +317,8 @@ class FileServer(ConnectionServer): site.invalidateUpdateTime(invalid_interval) def updateSites(self, check_files=False): + self.recheckPort() + task_nr = self.update_sites_task_next_nr self.update_sites_task_next_nr += 1 From 27ce79f04432c732d0ae9903103d75a486b804f7 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 4 Nov 2020 09:56:29 +0700 Subject: [PATCH 047/191] Fix a typo in FileServer --- src/File/FileServer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index eea34ff0..33dc7ff7 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -492,7 +492,7 @@ class FileServer(ConnectionServer): continue if last_activity_time > now - threshold: continue - if len(self.update_pool) == 0: + if len(self.update_pool) != 0: continue log.info("No network activity for %.2fs. Running an update for a random site.", From 6c8b059f57b6f7787e505b14fafd9ec78aa81caa Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 4 Nov 2020 16:05:01 +0700 Subject: [PATCH 048/191] FileServer: small fixes --- src/File/FileServer.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index 33dc7ff7..c2a7d7d0 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -498,7 +498,7 @@ class FileServer(ConnectionServer): log.info("No network activity for %.2fs. Running an update for a random site.", now - last_activity_time ) - self.update_pool.spawn(self.updateRandomSite) + self.update_pool.spawn(self.updateRandomSite, force=True) # Periodic reloading of tracker files def reloadTrackerFilesThread(self): @@ -576,6 +576,10 @@ class FileServer(ConnectionServer): from Debug import DebugReloader DebugReloader.watcher.addCallback(self.reload) + # XXX: for initializing self.sites + # Remove this line when self.sites gets completely unused + self.getSites() + if not passive_mode: self.spawn(self.updateSites) thread_reaload_tracker_files = self.spawn(self.reloadTrackerFilesThread) From 7354d712e0b31e1eb6b497b4857fd17674926211 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 4 Nov 2020 16:08:01 +0700 Subject: [PATCH 049/191] Be more persistent in delivering site updates. --- src/File/FileRequest.py | 2 +- src/Site/Site.py | 125 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 120 insertions(+), 7 deletions(-) diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index 65c335a9..f7249d81 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -165,7 +165,7 @@ class FileRequest(object): peer = site.addPeer(self.connection.ip, self.connection.port, return_peer=True, source="update") # Add or get peer # On complete publish to other peers diffs = params.get("diffs", {}) - site.onComplete.once(lambda: site.publish(inner_path=inner_path, diffs=diffs, limit=3), "publish_%s" % inner_path) + site.onComplete.once(lambda: site.publish(inner_path=inner_path, diffs=diffs), "publish_%s" % inner_path) # Load new content file and download changed files in new thread def downloader(): diff --git a/src/Site/Site.py b/src/Site/Site.py index 1581a106..d79d16ec 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -11,6 +11,7 @@ import base64 import gevent import gevent.pool +import gevent.lock import util from Config import config @@ -61,6 +62,90 @@ class ScaledTimeoutHandler: else: return None +class BackgroundPublisher: + def __init__(self, site, published=[], limit=5, inner_path="content.json", diffs={}): + self.site = site + self.threads = gevent.pool.Pool(None) + self.inner_path = inner_path + self.stages = [ + { + "interval": ScaledTimeoutHandler(60, 60), + "max_tries": 2, + "tries": 0, + "limit": 0, + "done": False + }, + { + "interval": ScaledTimeoutHandler(60 * 10, 60 * 10), + "max_tries": 5, + "tries": 0, + "limit": 0, + "done": False + } + ] + self.reinit(published=published, limit=limit, diffs=diffs) + + def reinit(self, published=[], limit=5, diffs={}): + self.threads.kill() + self.published = published + self.diffs = diffs + + i = 0 + for stage in self.stages: + stage["nr"] = i + stage["limit"] = limit * (2 + i) + stage["tries"] = False + stage["done"] = False + stage["thread"] = None + if i > 0: + stage["interval"].done() + i += 1 + + def isStageComplete(self, stage): + if not stage["done"]: + stage["done"] = len(self.published) >= stage["limit"] + if not stage["done"]: + stage["done"] = stage["tries"] >= stage["max_tries"] + return stage["done"] + + def isComplete(self): + for stage in self.stages: + if not self.isStageComplete(stage): + return False + return True + + def process(self): + for stage in self.stages: + if not self.isStageComplete(stage): + self.processStage(stage) + break + return self.isComplete() + + def processStage(self, stage): + if not stage["interval"].isExpired(0): + return + + if len(self.site.peers) < stage["limit"]: + self.site.announce(mode="more") + + if not stage["thread"]: + peers = list(self.site.peers.values()) + random.shuffle(peers) + stage["thread"] = self.threads.spawn(self.site.publisher, + self.inner_path, peers, self.published, stage["limit"], diffs=self.diffs, max_retries=1) + + stage["tries"] += 1 + stage["interval"].done() + + self.site.log.info("Background publisher: Stage #%s: %s published to %s/%s peers", + stage["nr"], self.inner_path, len(self.published), stage["limit"]) + + def finalize(self): + self.threads.kill() + self.site.log.info("Background publisher: Published %s to %s peers", self.inner_path, len(self.published)) + + + @PluginManager.acceptPlugins class Site(object): @@ -81,6 +166,9 @@ class Site(object): scaler=self.getActivityRating) ] + self.background_publishers = {} + self.background_publishers_lock = gevent.lock.RLock() + self.content = None # Load content.json self.peers = {} # Key: ip:port, Value: Peer.Peer self.peers_recent = collections.deque(maxlen=150) @@ -328,6 +416,11 @@ class Site(object): inner_path, time.time() - s, len(self.worker_manager.tasks) )) + + # If no file tasks have been started, worker_manager.checkComplete() + # never called. So call it explicitly. + self.greenlet_manager.spawn(self.worker_manager.checkComplete) + return True # Return bad files with less than 3 retry @@ -662,7 +755,7 @@ class Site(object): gevent.joinall(content_threads) # Publish worker - def publisher(self, inner_path, peers, published, limit, diffs={}, event_done=None, cb_progress=None): + def publisher(self, inner_path, peers, published, limit, diffs={}, event_done=None, cb_progress=None, max_retries=2): file_size = self.storage.getSize(inner_path) content_json_modified = self.content_manager.contents[inner_path]["modified"] body = self.storage.read(inner_path) @@ -687,7 +780,7 @@ class Site(object): timeout = 10 + int(file_size / 1024) result = {"exception": "Timeout"} - for retry in range(2): + for retry in range(max_retries): try: with gevent.Timeout(timeout, False): result = peer.publish(self.address, inner_path, body, content_json_modified, diffs) @@ -708,6 +801,25 @@ class Site(object): self.log.info("[FAILED] %s: %s" % (peer.key, result)) time.sleep(0.01) + def addBackgroundPublisher(self, published=[], limit=5, inner_path="content.json", diffs={}): + with self.background_publishers_lock: + if self.background_publishers.get(inner_path, None): + background_publisher = self.background_publishers[inner_path] + background_publisher.reinit(published=published, limit=limit, diffs=diffs) + else: + background_publisher = BackgroundPublisher(self, published=published, limit=limit, inner_path=inner_path, diffs=diffs) + self.background_publishers[inner_path] = background_publisher + + gevent.spawn(background_publisher.process) + + def processBackgroundPublishers(self): + with self.background_publishers_lock: + for inner_path, background_publisher in list(self.background_publishers.items()): + background_publisher.process() + if background_publisher.isComplete(): + background_publisher.finalize() + del self.background_publishers[inner_path] + # Update content.json on peers @util.Noparallel() def publish(self, limit="default", inner_path="content.json", diffs={}, cb_progress=None): @@ -752,12 +864,11 @@ class Site(object): # Publish more peers in the backgroup self.log.info( - "Published %s to %s peers, publishing to %s more peers in the background" % - (inner_path, len(published), limit) + "Published %s to %s peers, publishing to more peers in the background" % + (inner_path, len(published)) ) - for thread in range(2): - gevent.spawn(self.publisher, inner_path, peers, published, limit=limit * 2, diffs=diffs) + self.addBackgroundPublisher(published=published, limit=limit, inner_path=inner_path, diffs=diffs) # Send my hashfield to every connected peer if changed gevent.spawn(self.sendMyHashfield, 100) @@ -1296,6 +1407,8 @@ class Site(object): with gevent.Timeout(10, exception=False): self.announcer.announcePex() + self.processBackgroundPublishers() + self.update() return True From 5d5b3684cc3e2875673a6307451388a2e19f0079 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 4 Nov 2020 18:22:14 +0700 Subject: [PATCH 050/191] ContentManager: split verifyFile() into 2 functions and always log the verify error at INFO level --- src/Content/ContentManager.py | 191 +++++++++++++++++----------------- 1 file changed, 98 insertions(+), 93 deletions(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 27da402b..5fe80952 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -925,102 +925,107 @@ class ContentManager(object): return True # All good + def verifyContentJson(self, inner_path, file, ignore_same=True): + from Crypt import CryptBitcoin + + if type(file) is dict: + new_content = file + else: + try: + if sys.version_info.major == 3 and sys.version_info.minor < 6: + new_content = json.loads(file.read().decode("utf8")) + else: + new_content = json.load(file) + except Exception as err: + raise VerifyError("Invalid json file: %s" % err) + if inner_path in self.contents: + old_content = self.contents.get(inner_path, {"modified": 0}) + # Checks if its newer the ours + if old_content["modified"] == new_content["modified"] and ignore_same: # Ignore, have the same content.json + return None + elif old_content["modified"] > new_content["modified"]: # We have newer + raise VerifyError( + "We have newer (Our: %s, Sent: %s)" % + (old_content["modified"], new_content["modified"]) + ) + if new_content["modified"] > time.time() + 60 * 60 * 24: # Content modified in the far future (allow 1 day+) + raise VerifyError("Modify timestamp is in the far future!") + if self.isArchived(inner_path, new_content["modified"]): + if inner_path in self.site.bad_files: + del self.site.bad_files[inner_path] + raise VerifyError("This file is archived!") + # Check sign + sign = new_content.get("sign") + signs = new_content.get("signs", {}) + if "sign" in new_content: + del(new_content["sign"]) # The file signed without the sign + if "signs" in new_content: + del(new_content["signs"]) # The file signed without the signs + + sign_content = json.dumps(new_content, sort_keys=True) # Dump the json to string to remove whitepsace + + # Fix float representation error on Android + modified = new_content["modified"] + if config.fix_float_decimals and type(modified) is float and not str(modified).endswith(".0"): + modified_fixed = "{:.6f}".format(modified).strip("0.") + sign_content = sign_content.replace( + '"modified": %s' % repr(modified), + '"modified": %s' % modified_fixed + ) + + if signs: # New style signing + valid_signers = self.getValidSigners(inner_path, new_content) + signs_required = self.getSignsRequired(inner_path, new_content) + + if inner_path == "content.json" and len(valid_signers) > 1: # Check signers_sign on root content.json + signers_data = "%s:%s" % (signs_required, ",".join(valid_signers)) + if not CryptBitcoin.verify(signers_data, self.site.address, new_content["signers_sign"]): + raise VerifyError("Invalid signers_sign!") + + if inner_path != "content.json" and not self.verifyCert(inner_path, new_content): # Check if cert valid + raise VerifyError("Invalid cert!") + + valid_signs = 0 + for address in valid_signers: + if address in signs: + valid_signs += CryptBitcoin.verify(sign_content, address, signs[address]) + if valid_signs >= signs_required: + break # Break if we has enough signs + if valid_signs < signs_required: + raise VerifyError("Valid signs: %s/%s" % (valid_signs, signs_required)) + else: + return self.verifyContent(inner_path, new_content) + else: # Old style signing + raise VerifyError("Invalid old-style sign") + + def verifyOrdinaryFile(self, inner_path, file, ignore_same=True): + file_info = self.getFileInfo(inner_path) + if file_info: + if CryptHash.sha512sum(file) != file_info.get("sha512", ""): + raise VerifyError("Invalid hash") + + if file_info.get("size", 0) != file.tell(): + raise VerifyError( + "File size does not match %s <> %s" % + (inner_path, file.tell(), file_info.get("size", 0)) + ) + + return True + + else: # File not in content.json + raise VerifyError("File not in content.json") + # Verify file validity # Return: None = Same as before, False = Invalid, True = Valid def verifyFile(self, inner_path, file, ignore_same=True): - if inner_path.endswith("content.json"): # content.json: Check using sign - from Crypt import CryptBitcoin - try: - if type(file) is dict: - new_content = file - else: - try: - if sys.version_info.major == 3 and sys.version_info.minor < 6: - new_content = json.loads(file.read().decode("utf8")) - else: - new_content = json.load(file) - except Exception as err: - raise VerifyError("Invalid json file: %s" % err) - if inner_path in self.contents: - old_content = self.contents.get(inner_path, {"modified": 0}) - # Checks if its newer the ours - if old_content["modified"] == new_content["modified"] and ignore_same: # Ignore, have the same content.json - return None - elif old_content["modified"] > new_content["modified"]: # We have newer - raise VerifyError( - "We have newer (Our: %s, Sent: %s)" % - (old_content["modified"], new_content["modified"]) - ) - if new_content["modified"] > time.time() + 60 * 60 * 24: # Content modified in the far future (allow 1 day+) - raise VerifyError("Modify timestamp is in the far future!") - if self.isArchived(inner_path, new_content["modified"]): - if inner_path in self.site.bad_files: - del self.site.bad_files[inner_path] - raise VerifyError("This file is archived!") - # Check sign - sign = new_content.get("sign") - signs = new_content.get("signs", {}) - if "sign" in new_content: - del(new_content["sign"]) # The file signed without the sign - if "signs" in new_content: - del(new_content["signs"]) # The file signed without the signs - - sign_content = json.dumps(new_content, sort_keys=True) # Dump the json to string to remove whitepsace - - # Fix float representation error on Android - modified = new_content["modified"] - if config.fix_float_decimals and type(modified) is float and not str(modified).endswith(".0"): - modified_fixed = "{:.6f}".format(modified).strip("0.") - sign_content = sign_content.replace( - '"modified": %s' % repr(modified), - '"modified": %s' % modified_fixed - ) - - if signs: # New style signing - valid_signers = self.getValidSigners(inner_path, new_content) - signs_required = self.getSignsRequired(inner_path, new_content) - - if inner_path == "content.json" and len(valid_signers) > 1: # Check signers_sign on root content.json - signers_data = "%s:%s" % (signs_required, ",".join(valid_signers)) - if not CryptBitcoin.verify(signers_data, self.site.address, new_content["signers_sign"]): - raise VerifyError("Invalid signers_sign!") - - if inner_path != "content.json" and not self.verifyCert(inner_path, new_content): # Check if cert valid - raise VerifyError("Invalid cert!") - - valid_signs = 0 - for address in valid_signers: - if address in signs: - valid_signs += CryptBitcoin.verify(sign_content, address, signs[address]) - if valid_signs >= signs_required: - break # Break if we has enough signs - if valid_signs < signs_required: - raise VerifyError("Valid signs: %s/%s" % (valid_signs, signs_required)) - else: - return self.verifyContent(inner_path, new_content) - else: # Old style signing - raise VerifyError("Invalid old-style sign") - - except Exception as err: - self.log.warning("%s: verify sign error: %s" % (inner_path, Debug.formatException(err))) - raise err - - else: # Check using sha512 hash - file_info = self.getFileInfo(inner_path) - if file_info: - if CryptHash.sha512sum(file) != file_info.get("sha512", ""): - raise VerifyError("Invalid hash") - - if file_info.get("size", 0) != file.tell(): - raise VerifyError( - "File size does not match %s <> %s" % - (inner_path, file.tell(), file_info.get("size", 0)) - ) - - return True - - else: # File not in content.json - raise VerifyError("File not in content.json") + try: + if inner_path.endswith("content.json"): + return self.verifyContentJson(inner_path, file, ignore_same) + else: + return self.verifyOrdinaryFile(inner_path, file, ignore_same) + except Exception as err: + self.log.info("%s: verify error: %s" % (inner_path, Debug.formatException(err))) + raise err def optionalDelete(self, inner_path): self.site.storage.delete(inner_path) From 570f854485aa8e7ad7e9fc3045b4226c95aeffcc Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 4 Nov 2020 21:57:54 +0700 Subject: [PATCH 051/191] Fix a typo in Site.py --- src/Site/Site.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index d79d16ec..5e15172b 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -94,7 +94,7 @@ class BackgroundPublisher: for stage in self.stages: stage["nr"] = i stage["limit"] = limit * (2 + i) - stage["tries"] = False + stage["tries"] = 0 stage["done"] = False stage["thread"] = None if i > 0: From 697b12d808d42f93865c754ed0bea57aa99b12ec Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 4 Nov 2020 22:13:21 +0700 Subject: [PATCH 052/191] Fix naming. verifyContentJson() should be verifyContentFile() --- src/Content/ContentManager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 5fe80952..2283c91b 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -925,7 +925,7 @@ class ContentManager(object): return True # All good - def verifyContentJson(self, inner_path, file, ignore_same=True): + def verifyContentFile(self, inner_path, file, ignore_same=True): from Crypt import CryptBitcoin if type(file) is dict: @@ -1020,7 +1020,7 @@ class ContentManager(object): def verifyFile(self, inner_path, file, ignore_same=True): try: if inner_path.endswith("content.json"): - return self.verifyContentJson(inner_path, file, ignore_same) + return self.verifyContentFile(inner_path, file, ignore_same) else: return self.verifyOrdinaryFile(inner_path, file, ignore_same) except Exception as err: From 4e27e300e3e449714bd861967b1d30cb96f0d3cd Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Tue, 23 Mar 2021 16:01:36 +0700 Subject: [PATCH 053/191] Delete plugin/AnnounceShare/ --- plugins/AnnounceShare/AnnounceSharePlugin.py | 190 ------------------ .../AnnounceShare/Test/TestAnnounceShare.py | 24 --- plugins/AnnounceShare/Test/conftest.py | 3 - plugins/AnnounceShare/Test/pytest.ini | 5 - plugins/AnnounceShare/__init__.py | 1 - plugins/AnnounceShare/plugin_info.json | 5 - 6 files changed, 228 deletions(-) delete mode 100644 plugins/AnnounceShare/AnnounceSharePlugin.py delete mode 100644 plugins/AnnounceShare/Test/TestAnnounceShare.py delete mode 100644 plugins/AnnounceShare/Test/conftest.py delete mode 100644 plugins/AnnounceShare/Test/pytest.ini delete mode 100644 plugins/AnnounceShare/__init__.py delete mode 100644 plugins/AnnounceShare/plugin_info.json diff --git a/plugins/AnnounceShare/AnnounceSharePlugin.py b/plugins/AnnounceShare/AnnounceSharePlugin.py deleted file mode 100644 index 057ce55a..00000000 --- a/plugins/AnnounceShare/AnnounceSharePlugin.py +++ /dev/null @@ -1,190 +0,0 @@ -import time -import os -import logging -import json -import atexit - -import gevent - -from Config import config -from Plugin import PluginManager -from util import helper - - -class TrackerStorage(object): - def __init__(self): - self.log = logging.getLogger("TrackerStorage") - self.file_path = "%s/trackers.json" % config.data_dir - self.load() - self.time_discover = 0.0 - atexit.register(self.save) - - def getDefaultFile(self): - return {"shared": {}} - - def onTrackerFound(self, tracker_address, type="shared", my=False): - if not tracker_address.startswith("zero://"): - return False - - trackers = self.getTrackers() - added = False - if tracker_address not in trackers: - trackers[tracker_address] = { - "time_added": time.time(), - "time_success": 0, - "latency": 99.0, - "num_error": 0, - "my": False - } - self.log.debug("New tracker found: %s" % tracker_address) - added = True - - trackers[tracker_address]["time_found"] = time.time() - trackers[tracker_address]["my"] = my - return added - - def onTrackerSuccess(self, tracker_address, latency): - trackers = self.getTrackers() - if tracker_address not in trackers: - return False - - trackers[tracker_address]["latency"] = latency - trackers[tracker_address]["time_success"] = time.time() - trackers[tracker_address]["num_error"] = 0 - - def onTrackerError(self, tracker_address): - trackers = self.getTrackers() - if tracker_address not in trackers: - return False - - trackers[tracker_address]["time_error"] = time.time() - trackers[tracker_address]["num_error"] += 1 - - if len(self.getWorkingTrackers()) >= config.working_shared_trackers_limit: - error_limit = 5 - else: - error_limit = 30 - error_limit - - if trackers[tracker_address]["num_error"] > error_limit and trackers[tracker_address]["time_success"] < time.time() - 60 * 60: - self.log.debug("Tracker %s looks down, removing." % tracker_address) - del trackers[tracker_address] - - def getTrackers(self, type="shared"): - return self.file_content.setdefault(type, {}) - - def getWorkingTrackers(self, type="shared"): - trackers = { - key: tracker for key, tracker in self.getTrackers(type).items() - if tracker["time_success"] > time.time() - 60 * 60 - } - return trackers - - def getFileContent(self): - if not os.path.isfile(self.file_path): - open(self.file_path, "w").write("{}") - return self.getDefaultFile() - try: - return json.load(open(self.file_path)) - except Exception as err: - self.log.error("Error loading trackers list: %s" % err) - return self.getDefaultFile() - - def load(self): - self.file_content = self.getFileContent() - - trackers = self.getTrackers() - self.log.debug("Loaded %s shared trackers" % len(trackers)) - for address, tracker in list(trackers.items()): - tracker["num_error"] = 0 - if not address.startswith("zero://"): - del trackers[address] - - def save(self): - s = time.time() - helper.atomicWrite(self.file_path, json.dumps(self.file_content, indent=2, sort_keys=True).encode("utf8")) - self.log.debug("Saved in %.3fs" % (time.time() - s)) - - def discoverTrackers(self, peers): - if len(self.getWorkingTrackers()) > config.working_shared_trackers_limit: - return False - s = time.time() - num_success = 0 - for peer in peers: - if peer.connection and peer.connection.handshake.get("rev", 0) < 3560: - continue # Not supported - - res = peer.request("getTrackers") - if not res or "error" in res: - continue - - num_success += 1 - for tracker_address in res["trackers"]: - if type(tracker_address) is bytes: # Backward compatibilitys - tracker_address = tracker_address.decode("utf8") - added = self.onTrackerFound(tracker_address) - if added: # Only add one tracker from one source - break - - if not num_success and len(peers) < 20: - self.time_discover = 0.0 - - if num_success: - self.save() - - self.log.debug("Trackers discovered from %s/%s peers in %.3fs" % (num_success, len(peers), time.time() - s)) - - -if "tracker_storage" not in locals(): - tracker_storage = TrackerStorage() - - -@PluginManager.registerTo("SiteAnnouncer") -class SiteAnnouncerPlugin(object): - def getTrackers(self): - if tracker_storage.time_discover < time.time() - 5 * 60: - tracker_storage.time_discover = time.time() - gevent.spawn(tracker_storage.discoverTrackers, self.site.getConnectedPeers()) - trackers = super(SiteAnnouncerPlugin, self).getTrackers() - shared_trackers = list(tracker_storage.getTrackers("shared").keys()) - if shared_trackers: - return trackers + shared_trackers - else: - return trackers - - def announceTracker(self, tracker, *args, **kwargs): - res = super(SiteAnnouncerPlugin, self).announceTracker(tracker, *args, **kwargs) - if res: - latency = res - tracker_storage.onTrackerSuccess(tracker, latency) - elif res is False: - tracker_storage.onTrackerError(tracker) - - return res - - -@PluginManager.registerTo("FileRequest") -class FileRequestPlugin(object): - def actionGetTrackers(self, params): - shared_trackers = list(tracker_storage.getWorkingTrackers("shared").keys()) - self.response({"trackers": shared_trackers}) - - -@PluginManager.registerTo("FileServer") -class FileServerPlugin(object): - def portCheck(self, *args, **kwargs): - res = super(FileServerPlugin, self).portCheck(*args, **kwargs) - if res and not config.tor == "always" and "Bootstrapper" in PluginManager.plugin_manager.plugin_names: - for ip in self.ip_external_list: - my_tracker_address = "zero://%s:%s" % (ip, config.fileserver_port) - tracker_storage.onTrackerFound(my_tracker_address, my=True) - return res - - -@PluginManager.registerTo("ConfigPlugin") -class ConfigPlugin(object): - def createArguments(self): - group = self.parser.add_argument_group("AnnounceShare plugin") - group.add_argument('--working_shared_trackers_limit', help='Stop discovering new shared trackers after this number of shared trackers reached', default=5, type=int, metavar='limit') - - return super(ConfigPlugin, self).createArguments() diff --git a/plugins/AnnounceShare/Test/TestAnnounceShare.py b/plugins/AnnounceShare/Test/TestAnnounceShare.py deleted file mode 100644 index 7178eac8..00000000 --- a/plugins/AnnounceShare/Test/TestAnnounceShare.py +++ /dev/null @@ -1,24 +0,0 @@ -import pytest - -from AnnounceShare import AnnounceSharePlugin -from Peer import Peer -from Config import config - - -@pytest.mark.usefixtures("resetSettings") -@pytest.mark.usefixtures("resetTempSettings") -class TestAnnounceShare: - def testAnnounceList(self, file_server): - open("%s/trackers.json" % config.data_dir, "w").write("{}") - tracker_storage = AnnounceSharePlugin.tracker_storage - tracker_storage.load() - peer = Peer(file_server.ip, 1544, connection_server=file_server) - assert peer.request("getTrackers")["trackers"] == [] - - tracker_storage.onTrackerFound("zero://%s:15441" % file_server.ip) - assert peer.request("getTrackers")["trackers"] == [] - - # It needs to have at least one successfull announce to be shared to other peers - tracker_storage.onTrackerSuccess("zero://%s:15441" % file_server.ip, 1.0) - assert peer.request("getTrackers")["trackers"] == ["zero://%s:15441" % file_server.ip] - diff --git a/plugins/AnnounceShare/Test/conftest.py b/plugins/AnnounceShare/Test/conftest.py deleted file mode 100644 index 5abd4dd6..00000000 --- a/plugins/AnnounceShare/Test/conftest.py +++ /dev/null @@ -1,3 +0,0 @@ -from src.Test.conftest import * - -from Config import config diff --git a/plugins/AnnounceShare/Test/pytest.ini b/plugins/AnnounceShare/Test/pytest.ini deleted file mode 100644 index d09210d1..00000000 --- a/plugins/AnnounceShare/Test/pytest.ini +++ /dev/null @@ -1,5 +0,0 @@ -[pytest] -python_files = Test*.py -addopts = -rsxX -v --durations=6 -markers = - webtest: mark a test as a webtest. \ No newline at end of file diff --git a/plugins/AnnounceShare/__init__.py b/plugins/AnnounceShare/__init__.py deleted file mode 100644 index dc1e40bd..00000000 --- a/plugins/AnnounceShare/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import AnnounceSharePlugin diff --git a/plugins/AnnounceShare/plugin_info.json b/plugins/AnnounceShare/plugin_info.json deleted file mode 100644 index 0ad07e71..00000000 --- a/plugins/AnnounceShare/plugin_info.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "AnnounceShare", - "description": "Share possible trackers between clients.", - "default": "enabled" -} \ No newline at end of file From 3677684971580aab1ddd7d0507d5f9937a965b7d Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Tue, 23 Mar 2021 19:02:43 +0700 Subject: [PATCH 054/191] Add ZNE-ChangeLog/ChangeLog-0.8.0.md --- ZNE-ChangeLog/ChangeLog-0.8.0.md | 60 ++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 ZNE-ChangeLog/ChangeLog-0.8.0.md diff --git a/ZNE-ChangeLog/ChangeLog-0.8.0.md b/ZNE-ChangeLog/ChangeLog-0.8.0.md new file mode 100644 index 00000000..9925dbdc --- /dev/null +++ b/ZNE-ChangeLog/ChangeLog-0.8.0.md @@ -0,0 +1,60 @@ + +## Core + +**Network:** + +* Reworked the algorithm of checking zite updates on startup / after the network outages / periodically. ZeroNet tries not to spam too many update queries at once in order to prevent network overload. (Which especially the issue when running over Tor.) At the same time, it tries to keep balance between frequent checks for frequently updating zites and ensuring that all the zites are checked in some reasonable time interval. Tests show that the full check cycle for a peer that hosts 800+ zites and is connected over Tor can take up to several hours. We cannot significantly reduce this time, since the Tor throughput is the bottleneck. Running more checks at once just results in more connections to fail. The other bottleneck is the HDD throughput. Increasing the parallelization doesn't help in this case as well. So the implemented solution **decreases** the concurency. +* Improved the Internet outage detection and the recovery procedures after the Internet be back. ZeroNet "steps back" and schedules rechecking zites that were checked shortly before the Internet connection get lost. The network outage detection normally has some lag, so the recently checked zites are better to checked again. +* When the network is down, reduce the frequency of connection attempts to prevent overloading Tor with hanged connections. +* The connection handling code had several bugs that were hidden by silently ignored exceptions. These were fixed, but some new ones may be introduced. +* For each zite the activity rate is calculated based on the last modification time. The milestone values are 1 hour, 5 hours, 24 hours, 3 days and 7 days. The activity rate is used to scale frequency of various maintenance routines, including update checks, reannounces, dead connections check etc. +* The activity rate is also used to calculate the minimum preferred number of active connections per each zite. +* The reannounce frequency is adjusted dynamically based on: + * Activity. More activity ==> frequent announces. + * Peer count. More peers ==> rare announces. + * Tracker count. More trackers ==> frequent announces to iterate over more trackers. +* For owned zites, the activity rate doesn't drop below 0.6 to force more frequent checks. This, however, can be used to investigate which peer belongs to the zire owner. A new commnd line option `--expose_no_ownership` is introduced to disable that behavior. +* `ZeroNet` now tries harder in delivering updates to more peers in the background. + +**Other:** + +* Implemented the log level overriding for separate modules for easier debugging. + +## Plugins + +### TrackerShare + +The `AnnounceShare` plugin renamed to `TrackerShare` and redesigned. The new name is more consistent and better reflects the purpose of the plugin: sharing the list of known working trackers. Other `Announce`-like names are clearly related to zite announcing facilities: `AnnounceBitTorrent` (announce to a BitTorrent tracker), `AnnounceLocal` (announce in a Local Area Network), `AnnounceZero` (announce to a Zero tracker). + +Changes in the plugin: + +* The default total tracker limit increased up to 20 from 5. This reduces the probability of accidental splitting the network into segments that operate with disjoint sets of trackers. +* The plugin now shares any types of trackers, working both on IP and Onion networks, not limiting solely to Zero-based IP trackers. +* The plugin now takes into account not only the total number of known working trackers, but also does it on per protocol basis. The defaults are to keep 10 trackers for Zero protocol and 5 trackers per each other protocol. The available protocols are detected automatically. (For example, UDP is considered disabled, when working in the Tor-always mode.) The following protocols are recognized: `zero://` (Zero Tracker), `udp://` (UDP-based bitTorrent tracker), `http://` or `https://` (HTTP or HTTPS-based bitTorrent tracker; considered the same protocol). In case of new protocols implemented by third-party plugins, trackers are grouped automatically by the protocol prefix. +* Reworked the interaction of this plugin and the Zero tracker (`Bootstrapper`) plugin. Previously: `AnnounceShare` checks if `Bootstrapper` is running and adds its addresses to the list. Now: the tracker explicitly asks `TrackerShare` to add its addresses. +* The plugin allows adjustings the behaviour per each tracker entry with the following boolean fields: + * `my` — "My" trackers get never deleted on announce errors, but aren't saved between restarts. Designed to be used by tracker implementation plugins. `TrackerShare` acts more persistently in recommending "my" trackers to other peers. + * `persistent` — "Persistent" trackers get never deleted, when unresponsive. + * `private` — "Private" trackers are never exposed to other peer in response of the getTrackers command. + +### TrackerZero + +`TrackerZero` is an attempt of implementing the better version of the `Bootstrapper` plugin to make setting up and launching a new tracker possible in a couple of mouse clicks. Work in progress, so no detailed change log yet. The `Bootstrapper` plugin itself is kept untouched. + +The plugin has the following self-explanatory options: + +``` +enable +enable_only_in_tor_always_mode +listen_on_public_ips +listen_on_temporary_onion_address +listen_on_persistent_onion_address +``` + +Running over TOR doesn't seem to be stable so far. Any investigations and bug reports are welcome. + +### TrackerList + +`TrackerZero` is a new plugin for fetching tracker lists. By default is list is fetched from [https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_ip.txt](https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_ip.txt). + +TODO: add support of fetching from ZeroNet URLs From 6c8849139fa0f975b5fa5291cdbd9148eda91a3f Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Tue, 23 Mar 2021 19:05:51 +0700 Subject: [PATCH 055/191] ZNE-ChangeLog/ChangeLog-0.8.0.md: typos --- ZNE-ChangeLog/ChangeLog-0.8.0.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ZNE-ChangeLog/ChangeLog-0.8.0.md b/ZNE-ChangeLog/ChangeLog-0.8.0.md index 9925dbdc..59f0af55 100644 --- a/ZNE-ChangeLog/ChangeLog-0.8.0.md +++ b/ZNE-ChangeLog/ChangeLog-0.8.0.md @@ -6,8 +6,8 @@ * Reworked the algorithm of checking zite updates on startup / after the network outages / periodically. ZeroNet tries not to spam too many update queries at once in order to prevent network overload. (Which especially the issue when running over Tor.) At the same time, it tries to keep balance between frequent checks for frequently updating zites and ensuring that all the zites are checked in some reasonable time interval. Tests show that the full check cycle for a peer that hosts 800+ zites and is connected over Tor can take up to several hours. We cannot significantly reduce this time, since the Tor throughput is the bottleneck. Running more checks at once just results in more connections to fail. The other bottleneck is the HDD throughput. Increasing the parallelization doesn't help in this case as well. So the implemented solution **decreases** the concurency. * Improved the Internet outage detection and the recovery procedures after the Internet be back. ZeroNet "steps back" and schedules rechecking zites that were checked shortly before the Internet connection get lost. The network outage detection normally has some lag, so the recently checked zites are better to checked again. * When the network is down, reduce the frequency of connection attempts to prevent overloading Tor with hanged connections. -* The connection handling code had several bugs that were hidden by silently ignored exceptions. These were fixed, but some new ones may be introduced. -* For each zite the activity rate is calculated based on the last modification time. The milestone values are 1 hour, 5 hours, 24 hours, 3 days and 7 days. The activity rate is used to scale frequency of various maintenance routines, including update checks, reannounces, dead connections check etc. +* The connection handling code had several bugs that were hidden by silently ignored exceptions. These were fixed, but some new ones might be introduced. +* For each zite the activity rate is calculated based on the last modification time. The milestone values are 1 hour, 5 hours, 24 hours, 3 days and 7 days. The activity rate is used to scale frequency of various maintenance routines, including update checks, reannounces, dead connection checks etc. * The activity rate is also used to calculate the minimum preferred number of active connections per each zite. * The reannounce frequency is adjusted dynamically based on: * Activity. More activity ==> frequent announces. From 1144964062e9494139dc93526dd2de943fe13cc7 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Tue, 23 Mar 2021 23:12:54 +0700 Subject: [PATCH 056/191] Make the site block check usable from plugins and core modules Fixes https://github.com/HelloZeroNet/ZeroNet/issues/1888 --- ZNE-ChangeLog/ChangeLog-0.8.0.md | 1 + plugins/ContentFilter/ContentFilterPlugin.py | 8 ++++++++ src/Site/SiteManager.py | 4 ++++ 3 files changed, 13 insertions(+) diff --git a/ZNE-ChangeLog/ChangeLog-0.8.0.md b/ZNE-ChangeLog/ChangeLog-0.8.0.md index 59f0af55..f87f1cb9 100644 --- a/ZNE-ChangeLog/ChangeLog-0.8.0.md +++ b/ZNE-ChangeLog/ChangeLog-0.8.0.md @@ -19,6 +19,7 @@ **Other:** * Implemented the log level overriding for separate modules for easier debugging. +* Make the site block check implemented in `ContentFilter` usable from plugins and core modules via `SiteManager.isAddressBlocked()`. ## Plugins diff --git a/plugins/ContentFilter/ContentFilterPlugin.py b/plugins/ContentFilter/ContentFilterPlugin.py index f2f84b49..6bd8c7f7 100644 --- a/plugins/ContentFilter/ContentFilterPlugin.py +++ b/plugins/ContentFilter/ContentFilterPlugin.py @@ -24,6 +24,14 @@ class SiteManagerPlugin(object): super(SiteManagerPlugin, self).load(*args, **kwargs) filter_storage = ContentFilterStorage(site_manager=self) + def isAddressBlocked(self, address): + # FIXME: code duplication of isSiteblocked(address) or isSiteblocked(address_hashed) + # in several places here and below + address_hashed = filter_storage.getSiteAddressHashed(address) + if filter_storage.isSiteblocked(address) or filter_storage.isSiteblocked(address_hashed): + return True + return super(SiteManagerPlugin, self).isAddressBlocked(address) + def add(self, address, *args, **kwargs): should_ignore_block = kwargs.get("ignore_block") or kwargs.get("settings") if should_ignore_block: diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index 684d69fc..1b065e93 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -155,6 +155,10 @@ class SiteManager(object): def resolveDomainCached(self, domain): return self.resolveDomain(domain) + # Checks if the address is blocked. To be implemented in content filter plugins. + def isAddressBlocked(self, address): + return False + # Return: Site object or None if not found def get(self, address): if self.isDomainCached(address): From d0069471b8bcf0a61b6c5a9849621f255580065a Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 24 Mar 2021 08:04:23 +0700 Subject: [PATCH 057/191] Don't raise VerifyError with misleading message "Invalid old-style sign" when the file has no sign at all. --- src/Content/ContentManager.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 2283c91b..c5ce1d61 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -995,8 +995,10 @@ class ContentManager(object): raise VerifyError("Valid signs: %s/%s" % (valid_signs, signs_required)) else: return self.verifyContent(inner_path, new_content) - else: # Old style signing + elif sign: # Old style signing raise VerifyError("Invalid old-style sign") + else: + raise VerifyError("Not signed") def verifyOrdinaryFile(self, inner_path, file, ignore_same=True): file_info = self.getFileInfo(inner_path) From 0151546329ffa8ada1f4c09efab921560153780d Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 24 Mar 2021 09:51:07 +0700 Subject: [PATCH 058/191] ContentManager.py: move duplicated code to new method serializeForSigning() --- src/Content/ContentManager.py | 42 +++++++++++++++++------------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index c5ce1d61..7d1263ef 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -651,6 +651,25 @@ class ContentManager(object): ) return files_node, files_optional_node + def serializeForSigning(self, content): + if "sign" in content: + del(content["sign"]) # The file signed without the sign + if "signs" in content: + del(content["signs"]) # The file signed without the signs + + sign_content = json.dumps(content, sort_keys=True) # Dump the json to string to remove whitespaces + + # Fix float representation error on Android + modified = content["modified"] + if config.fix_float_decimals and type(modified) is float and not str(modified).endswith(".0"): + modified_fixed = "{:.6f}".format(modified).strip("0.") + sign_content = sign_content.replace( + '"modified": %s' % repr(modified), + '"modified": %s' % modified_fixed + ) + + return sign_content + # Create and sign a content.json # Return: The new content if filewrite = False def sign(self, inner_path="content.json", privatekey=None, filewrite=True, update_changed_files=False, extend=None, remove_missing_optional=False): @@ -756,12 +775,7 @@ class ContentManager(object): self.log.info("Signing %s..." % inner_path) - if "signs" in new_content: - del(new_content["signs"]) # Delete old signs - if "sign" in new_content: - del(new_content["sign"]) # Delete old sign (backward compatibility) - - sign_content = json.dumps(new_content, sort_keys=True) + sign_content = self.serializeForSigning(new_content) sign = CryptBitcoin.sign(sign_content, privatekey) # new_content["signs"] = content.get("signs", {}) # TODO: Multisig if sign: # If signing is successful (not an old address) @@ -957,21 +971,7 @@ class ContentManager(object): # Check sign sign = new_content.get("sign") signs = new_content.get("signs", {}) - if "sign" in new_content: - del(new_content["sign"]) # The file signed without the sign - if "signs" in new_content: - del(new_content["signs"]) # The file signed without the signs - - sign_content = json.dumps(new_content, sort_keys=True) # Dump the json to string to remove whitepsace - - # Fix float representation error on Android - modified = new_content["modified"] - if config.fix_float_decimals and type(modified) is float and not str(modified).endswith(".0"): - modified_fixed = "{:.6f}".format(modified).strip("0.") - sign_content = sign_content.replace( - '"modified": %s' % repr(modified), - '"modified": %s' % modified_fixed - ) + sign_content = self.serializeForSigning(new_content) if signs: # New style signing valid_signers = self.getValidSigners(inner_path, new_content) From 986dedfa7f4ea8e0c1cc6c4bbfe3fccdf15640c6 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Thu, 25 Mar 2021 12:41:53 +0700 Subject: [PATCH 059/191] Trying to fix incomplete updates Partially fixes https://github.com/HelloZeroNet/ZeroNet/issues/2476 Approx. every 10th update check is now performed with `since = 0` --- src/Site/Site.py | 111 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 78 insertions(+), 33 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 5e15172b..0a8be32d 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -169,6 +169,12 @@ class Site(object): self.background_publishers = {} self.background_publishers_lock = gevent.lock.RLock() + # FZS = forced zero "since" + self.fzs_range = 20 + self.fzs_interval = 30 * 60 + self.fzs_count = random.randint(0, self.fzs_range / 4) + self.fzs_timestamp = 0 + self.content = None # Load content.json self.peers = {} # Key: ip:port, Value: Peer.Peer self.peers_recent = collections.deque(maxlen=150) @@ -530,14 +536,18 @@ class Site(object): self.log.debug("Ended downloadFile pool len: %s, skipped: %s" % (len(inner_paths), num_skipped)) # Update worker, try to find client that supports listModifications command - def updater(self, peers_try, queried, since): + def updater(self, peers_try, queried, need_queries, since): threads = [] while 1: - if not peers_try or len(queried) >= 3: # Stop after 3 successful query + if not peers_try or len(queried) >= need_queries: # Stop after 3 successful query break peer = peers_try.pop(0) + + if peer in queried: + continue + if config.verbose: - self.log.debug("CheckModifications: Try to get updates from: %s Left: %s" % (peer, peers_try)) + self.log.debug("CheckModifications: Trying to get updates from: %s Left: %s" % (peer, peers_try)) res = None with gevent.Timeout(20, exception=False): @@ -548,6 +558,8 @@ class Site(object): queried.append(peer) modified_contents = [] + send_back = [] + send_back_limit = 5 my_modified = self.content_manager.listModified(since) num_old_files = 0 for inner_path, modified in res["modified_files"].items(): # Check if the peer has newer files than we @@ -558,26 +570,56 @@ class Site(object): # We dont have this file or we have older modified_contents.append(inner_path) self.bad_files[inner_path] = self.bad_files.get(inner_path, 0) + 1 - if has_older and num_old_files < 5: - num_old_files += 1 - self.log.debug("CheckModifications: %s client has older version of %s, publishing there (%s/5)..." % (peer, inner_path, num_old_files)) - gevent.spawn(self.publisher, inner_path, [peer], [], 1) + if has_older: + send_back.append(inner_path) + if modified_contents: - self.log.debug("CheckModifications: %s new modified file from %s" % (len(modified_contents), peer)) + self.log.info("CheckModifications: %s new modified files from %s" % (len(modified_contents), peer)) modified_contents.sort(key=lambda inner_path: 0 - res["modified_files"][inner_path]) # Download newest first + for inner_path in modified_contents: + self.log.info("CheckModifications: %s: %s > %s" % ( + inner_path, res["modified_files"][inner_path], my_modified.get(inner_path, 0) + )) t = gevent.spawn(self.pooledDownloadContent, modified_contents, only_if_bad=True) threads.append(t) - if config.verbose: - self.log.debug("CheckModifications: Waiting for %s pooledDownloadContent" % len(threads)) + + if send_back: + self.log.info("CheckModifications: %s has older versions of %s files" % (peer, len(send_back))) + if len(send_back) > send_back_limit: + self.log.info("CheckModifications: choosing %s files to publish back" % (send_back_limit)) + random.shuffle(send_back) + send_back = send_back[0:send_back_limit] + for inner_path in send_back: + self.log.info("CheckModifications: %s: %s < %s" % ( + inner_path, res["modified_files"][inner_path], my_modified.get(inner_path, 0) + )) + gevent.spawn(self.publisher, inner_path, [peer], [], 1) + + self.log.debug("CheckModifications: Waiting for %s pooledDownloadContent" % len(threads)) gevent.joinall(threads) + # We need, with some rate, to perform the full check of modifications, + # "since the beginning of time", instead of the partial one. + def getForcedZeroSince(self): + now = time.time() + if self.fzs_timestamp + self.fzs_interval > now: + return False + self.fzs_count -= 1 + if self.fzs_count < 1: + self.fzs_count = random.randint(0, self.fzs_range) + self.fzs_timestamp = now + return True + return False + # Check modified content.json files from peers and add modified files to bad_files # Return: Successfully queried peers [Peer, Peer...] def checkModifications(self, since=None): s = time.time() peers_try = [] # Try these peers queried = [] # Successfully queried from these peers - limit = 5 + peer_limit = 10 + updater_limit = 3 + need_queries = 3 # Wait for peers if not self.peers: @@ -588,34 +630,37 @@ class Site(object): if self.peers: break - peers_try = self.getConnectedPeers() - peers_connected_num = len(peers_try) - if peers_connected_num < limit * 2: # Add more, non-connected peers if necessary - peers_try += self.getRecentPeers(limit * 5) + if since is None: + if self.getForcedZeroSince(): + since = 0 + else: + margin = 60 * 60 * 24 + since = self.settings.get("modified", margin) - margin - if since is None: # No since defined, download from last modification time-1day - since = self.settings.get("modified", 60 * 60 * 24) - 60 * 60 * 24 + if since == 0: + peer_limit *= 4 + need_queries *= 4 - if config.verbose: - self.log.debug( - "CheckModifications: Try to get listModifications from peers: %s, connected: %s, since: %s" % - (peers_try, peers_connected_num, since) - ) + peers_try = self.getConnectedPeers() + self.getConnectablePeers(peer_limit) + + self.log.debug( + "CheckModifications: Trying to get listModifications from %s peers, %s connected, since: %s" % + (len(peers_try), len(self.getConnectedPeers()), since) + ) updaters = [] - for i in range(3): - updaters.append(gevent.spawn(self.updater, peers_try, queried, since)) + for i in range(updater_limit): + updaters.append(gevent.spawn(self.updater, peers_try, queried, need_queries, since)) - gevent.joinall(updaters, timeout=10) # Wait 10 sec to workers done query modifications + for r in range(10): + gevent.joinall(updaters, timeout=5+r) + if len(queried) >= need_queries or len(peers_try) == 0: + break + self.log.debug("CheckModifications: Waiting... (%s) succesfully queried: %s, left: %s" % + (r + 1, len(queried), len(peers_try))) - if not queried: # Start another 3 thread if first 3 is stuck - peers_try[0:0] = [peer for peer in self.getConnectedPeers() if peer.connection.connected] # Add connected peers - for _ in range(10): - gevent.joinall(updaters, timeout=10) # Wait another 10 sec if none of updaters finished - if queried: - break - - self.log.debug("CheckModifications: Queried listModifications from: %s in %.3fs since %s" % (queried, time.time() - s, since)) + self.log.debug("CheckModifications: Queried listModifications from %s peers in %.3fs since %s" % ( + len(queried), time.time() - s, since)) time.sleep(0.1) return queried From 8474abc9673f5e9c8dac850f67e71a91d85d737e Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 31 Mar 2021 09:39:22 +0700 Subject: [PATCH 060/191] Fix building the docker image --- Dockerfile | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7839cfa0..1274c7fc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,33 +1,42 @@ FROM alpine:3.11 -#Base settings +# Base settings ENV HOME /root +# Install packages + +RUN apk --update --no-cache --no-progress add \ + python3 python3-dev \ + libffi-dev musl-dev \ + gcc g++ make \ + automake autoconf libtool \ + openssl \ + tor + COPY requirements.txt /root/requirements.txt -#Install ZeroNet -RUN apk --update --no-cache --no-progress add python3 python3-dev gcc libffi-dev musl-dev make tor openssl \ - && pip3 install -r /root/requirements.txt \ +RUN pip3 install -r /root/requirements.txt \ && apk del python3-dev gcc libffi-dev musl-dev make \ && echo "ControlPort 9051" >> /etc/tor/torrc \ && echo "CookieAuthentication 1" >> /etc/tor/torrc - + RUN python3 -V \ && python3 -m pip list \ && tor --version \ && openssl version -#Add Zeronet source +# Add Zeronet source + COPY . /root VOLUME /root/data -#Control if Tor proxy is started +# Control if Tor proxy is started ENV ENABLE_TOR false WORKDIR /root -#Set upstart command +# Set upstart command CMD (! ${ENABLE_TOR} || tor&) && python3 zeronet.py --ui_ip 0.0.0.0 --fileserver_port 26552 -#Expose ports +# Expose ports EXPOSE 43110 26552 From adaeedf4d8dfbd2e946ca1a8f336b4d2904860d6 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 31 Mar 2021 10:51:17 +0700 Subject: [PATCH 061/191] Dockerfile: move to alpine:3.13 in order to use newer tor version --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1274c7fc..bc834293 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.11 +FROM alpine:3.13 # Base settings ENV HOME /root @@ -6,7 +6,7 @@ ENV HOME /root # Install packages RUN apk --update --no-cache --no-progress add \ - python3 python3-dev \ + python3 python3-dev py3-pip \ libffi-dev musl-dev \ gcc g++ make \ automake autoconf libtool \ From 5d6fe6a63185392e464018cef5a94e82fca0beea Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 31 Mar 2021 11:16:06 +0700 Subject: [PATCH 062/191] Fix a bug introduced in c84b413f58bfc810f1e56b7e6414367f81efadf6 --- src/Connection/ConnectionServer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index 20bd165a..ad834c54 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -193,7 +193,7 @@ class ConnectionServer(object): connection.close("Close all connections") def handleIncomingConnection(self, sock, addr): - if self.allowsAcceptingConnections(): + if not self.allowsAcceptingConnections(): sock.close() return False From c772592c4af2da32fdacb81106c24d6e46806095 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 31 Mar 2021 11:20:37 +0700 Subject: [PATCH 063/191] New default ZeroHello address --- README-ru.md | 2 +- README-zh-cn.md | 4 ++-- README.md | 4 ++-- src/Config.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README-ru.md b/README-ru.md index 75abbfab..1b1c660b 100644 --- a/README-ru.md +++ b/README-ru.md @@ -34,7 +34,7 @@ * После запуска `zeronet.py` вы сможете посетить зайты (zeronet сайты) используя адрес `http://127.0.0.1:43110/{zeronet_address}` -(например. `http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D`). +(например. `http://127.0.0.1:43110/1HeLLoPVbqF3UEj8aWXErwTxrwkyjwGtZN`). * Когда вы посещаете новый сайт zeronet, он пытается найти пиров с помощью BitTorrent чтобы загрузить файлы сайтов (html, css, js ...) из них. * Каждый посещенный зайт также обслуживается вами. (Т.е хранится у вас на компьютере) diff --git a/README-zh-cn.md b/README-zh-cn.md index fabdb0e5..a008e154 100644 --- a/README-zh-cn.md +++ b/README-zh-cn.md @@ -33,7 +33,7 @@ * 在运行 `zeronet.py` 后,您将可以通过 `http://127.0.0.1:43110/{zeronet_address}`(例如: - `http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D`)访问 zeronet 中的站点 + `http://127.0.0.1:43110/1HeLLoPVbqF3UEj8aWXErwTxrwkyjwGtZN`)访问 zeronet 中的站点 * 在您浏览 zeronet 站点时,客户端会尝试通过 BitTorrent 网络来寻找可用的节点,从而下载需要的文件(html,css,js...) * 您将会储存每一个浏览过的站点 * 每个站点都包含一个名为 `content.json` 的文件,它储存了其他所有文件的 sha512 散列值以及一个通过站点私钥生成的签名 @@ -99,7 +99,7 @@ ## 如何创建一个 ZeroNet 站点? - * 点击 [ZeroHello](http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D) 站点的 **⋮** > **「新建空站点」** 菜单项 + * 点击 [ZeroHello](http://127.0.0.1:43110/1HeLLoPVbqF3UEj8aWXErwTxrwkyjwGtZN) 站点的 **⋮** > **「新建空站点」** 菜单项 * 您将被**重定向**到一个全新的站点,该站点只能由您修改 * 您可以在 **data/[您的站点地址]** 目录中找到并修改网站的内容 * 修改后打开您的网站,将右上角的「0」按钮拖到左侧,然后点击底部的**签名**并**发布**按钮 diff --git a/README.md b/README.md index 5b02a2e9..1b08c868 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Decentralized websites using Bitcoin crypto and the BitTorrent network - https:/ * After starting `zeronet.py` you will be able to visit zeronet sites using `http://127.0.0.1:43110/{zeronet_address}` (eg. - `http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D`). + `http://127.0.0.1:43110/1HeLLoPVbqF3UEj8aWXErwTxrwkyjwGtZN`). * When you visit a new zeronet site, it tries to find peers using the BitTorrent network so it can download the site files (html, css, js...) from them. * Each visited site is also served by you. @@ -114,7 +114,7 @@ There is an official image, built from source at: https://hub.docker.com/r/nofis ## How can I create a ZeroNet site? - * Click on **⋮** > **"Create new, empty site"** menu item on the site [ZeroHello](http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D). + * Click on **⋮** > **"Create new, empty site"** menu item on the site [ZeroHello](http://127.0.0.1:43110/1HeLLoPVbqF3UEj8aWXErwTxrwkyjwGtZN). * You will be **redirected** to a completely new site that is only modifiable by you! * You can find and modify your site's content in **data/[yoursiteaddress]** directory * After the modifications open your site, drag the topright "0" button to left, then press **sign** and **publish** buttons on the bottom diff --git a/src/Config.py b/src/Config.py index a3d66395..fcba371e 100644 --- a/src/Config.py +++ b/src/Config.py @@ -245,7 +245,7 @@ class Config(object): self.parser.add_argument('--open_browser', help='Open homepage in web browser automatically', nargs='?', const="default_browser", metavar='browser_name') - self.parser.add_argument('--homepage', help='Web interface Homepage', default='1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D', + self.parser.add_argument('--homepage', help='Web interface Homepage', default='1HeLLoPVbqF3UEj8aWXErwTxrwkyjwGtZN', metavar='address') self.parser.add_argument('--updatesite', help='Source code update site', default='1uPDaT3uSyWAPdCv1WkMb5hBQjWSNNACf', metavar='address') From be65ff2c40724d7936333eac12d6964ae683d35b Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 31 Mar 2021 11:27:19 +0700 Subject: [PATCH 064/191] Make more efforts of looking for peers when publishing a site --- src/Site/Site.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 0a8be32d..1d396b08 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -865,29 +865,46 @@ class Site(object): background_publisher.finalize() del self.background_publishers[inner_path] + def waitForPeers(self, num_peers, num_connected_peers, time_limit): + start_time = time.time() + for _ in range(time_limit): + if len(self.peers) >= num_peers and len(self.getConnectedPeers()) >= num_connected_peers: + return True + self.announce(mode="more", force=True) + for wait in range(10): + self.needConnections(num=num_connected_peers) + time.sleep(2) + if len(self.peers) >= num_peers and len(self.getConnectedPeers()) >= num_connected_peers: + return True + if time.time() - start_time > time_limit: + return True + + return False + # Update content.json on peers @util.Noparallel() def publish(self, limit="default", inner_path="content.json", diffs={}, cb_progress=None): published = [] # Successfully published (Peer) publishers = [] # Publisher threads - if not self.peers: - self.announce(mode="more") - if limit == "default": limit = 5 threads = limit + self.waitForPeers(limit, limit / 2, 10) + self.waitForPeers(1, 1, 60) + peers = self.getConnectedPeers() num_connected_peers = len(peers) random.shuffle(peers) - peers = sorted(peers, key=lambda peer: peer.connection.handshake.get("rev", 0) < config.rev - 100) # Prefer newer clients + # Prefer newer clients + peers = sorted(peers, key=lambda peer: peer.connection.handshake.get("rev", 0) < config.rev - 100) - if len(peers) < limit * 2 and len(self.peers) > len(peers): # Add more, non-connected peers if necessary + # Add more, non-connected peers if necessary + if len(peers) < limit * 2 and len(self.peers) > len(peers): peers += self.getRecentPeers(limit * 2) - - peers = set(peers) + peers = set(peers) self.log.info("Publishing %s to %s/%s peers (connected: %s) diffs: %s (%.2fk)..." % ( inner_path, limit, len(self.peers), num_connected_peers, list(diffs.keys()), float(len(str(diffs))) / 1024 From abbd2c51f0c568f63e2fd8a4a87ac744d1e4d913 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 31 Mar 2021 12:55:45 +0700 Subject: [PATCH 065/191] Update ChangeLog-0.8.0.md --- ZNE-ChangeLog/ChangeLog-0.8.0.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/ZNE-ChangeLog/ChangeLog-0.8.0.md b/ZNE-ChangeLog/ChangeLog-0.8.0.md index f87f1cb9..f2d10b4a 100644 --- a/ZNE-ChangeLog/ChangeLog-0.8.0.md +++ b/ZNE-ChangeLog/ChangeLog-0.8.0.md @@ -14,13 +14,25 @@ * Peer count. More peers ==> rare announces. * Tracker count. More trackers ==> frequent announces to iterate over more trackers. * For owned zites, the activity rate doesn't drop below 0.6 to force more frequent checks. This, however, can be used to investigate which peer belongs to the zire owner. A new commnd line option `--expose_no_ownership` is introduced to disable that behavior. -* `ZeroNet` now tries harder in delivering updates to more peers in the background. +* When checking for updates, ZeroNet normally asks other peers for new data since the previous update. This however can result in losing some updates in specific conditions. To overcome this, ZeroNet now asks for the full site listing on every Nth update check. +* When asking a peer for updates, ZeroNet may see that the other peer has an older version of a file. In this case, ZeroNet sends back the notification of the new version available. The logic in 0.8.0 is generally the same, but some randomization is added which may help in distributing the "update waves" among peers. +* ZeroNet now tries harder in delivering updates to more peers in the background. +* ZeroNet also make more efforts of searching the peers before publishing updates. **Other:** * Implemented the log level overriding for separate modules for easier debugging. * Make the site block check implemented in `ContentFilter` usable from plugins and core modules via `SiteManager.isAddressBlocked()`. +## Docker Image + +* The base image upgraded from `alpine:3.11` to `alpine:3.13`. +* Tor upgraded to 0.4.4.8. + +## ZeroHello + +The default ZeroHello address changed from [1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D/](http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D/) to [1HeLLoPVbqF3UEj8aWXErwTxrwkyjwGtZN/](http://127.0.0.1:43110/1HeLLoPVbqF3UEj8aWXErwTxrwkyjwGtZN/). This is the first step in migrating away from the nofish's infrastructure which development seems to be stalled. + ## Plugins ### TrackerShare From b9ec7124f95a1c820f5698a355a42abf91a33e97 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 20 Oct 2021 14:02:00 +0700 Subject: [PATCH 066/191] minor changes for debugging --- plugins/AnnounceZero/AnnounceZeroPlugin.py | 2 ++ src/Site/Site.py | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins/AnnounceZero/AnnounceZeroPlugin.py b/plugins/AnnounceZero/AnnounceZeroPlugin.py index 623cd4b5..76ff357d 100644 --- a/plugins/AnnounceZero/AnnounceZeroPlugin.py +++ b/plugins/AnnounceZero/AnnounceZeroPlugin.py @@ -1,3 +1,4 @@ +import logging import time import itertools @@ -94,6 +95,7 @@ class SiteAnnouncerPlugin(object): tracker_ip, tracker_port = tracker_address.rsplit(":", 1) tracker_peer = Peer(str(tracker_ip), int(tracker_port), connection_server=self.site.connection_server) tracker_peer.is_tracker_connection = True + #tracker_peer.log_level = logging.INFO connection_pool[tracker_address] = tracker_peer res = tracker_peer.request("announce", request) diff --git a/src/Site/Site.py b/src/Site/Site.py index 1d396b08..010d493b 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -1357,8 +1357,13 @@ class Site(object): else: peers = list(self.peers.values()) + self.log.info("%s" % peers) + self.log.info("%s" % need_more) + + peers = peers[0:need_more * 50] + found_more = sorted( - peers[0:need_more * 50], + peers, key=lambda peer: peer.reputation, reverse=True )[0:need_more * 2] From 1c73d1a0952d6d7af8e3b563f9307fe25af6317c Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 20 Oct 2021 14:26:07 +0700 Subject: [PATCH 067/191] Merge TORv3 patch From: http://127.0.0.1:43110/19HKdTAeBh5nRiKn791czY7TwRB1QNrf1Q/?:users/1HvNGwHKqhj3ZMEM53tz6jbdqe4LRpanEu:zn:dc17f896-bf3f-4962-bdd4-0a470040c9c5 Related issues: https://github.com/HelloZeroNet/ZeroNet/issues/2351 https://github.com/HelloZeroNet/ZeroNet/issues/1292 --- plugins/AnnounceZero/AnnounceZeroPlugin.py | 1 + src/Crypt/CryptEd25519.py | 340 +++++++++++++++++++++ src/Crypt/CryptRsa.py | 27 ++ src/Tor/TorManager.py | 5 +- 4 files changed, 371 insertions(+), 2 deletions(-) create mode 100644 src/Crypt/CryptEd25519.py diff --git a/plugins/AnnounceZero/AnnounceZeroPlugin.py b/plugins/AnnounceZero/AnnounceZeroPlugin.py index 76ff357d..e71e221e 100644 --- a/plugins/AnnounceZero/AnnounceZeroPlugin.py +++ b/plugins/AnnounceZero/AnnounceZeroPlugin.py @@ -5,6 +5,7 @@ import itertools from Plugin import PluginManager from util import helper from Crypt import CryptRsa +from Crypt import CryptEd25519 allow_reload = False # No source reload supported in this plugin time_full_announced = {} # Tracker address: Last announced all site to tracker diff --git a/src/Crypt/CryptEd25519.py b/src/Crypt/CryptEd25519.py new file mode 100644 index 00000000..fc05a932 --- /dev/null +++ b/src/Crypt/CryptEd25519.py @@ -0,0 +1,340 @@ +## ZeroNet onion V3 support +## The following copied code is copied from stem.util.ed25519 official Tor Project python3 lib +## url : https://gitweb.torproject.org/stem.git/tree/stem/util/ed25519.py +## the ##modified tag means that the function has been modified respect to the one used by stem lib +## the ##custom tag means that the function has been added by me and it's not present on the stem ed25519.py file +## every comment i make begins with ## +## +# The following is copied from... +# +# https://github.com/pyca/ed25519 +# +# This is under the CC0 license. For more information please see... +# +# https://github.com/pyca/cryptography/issues/5068 + +# ed25519.py - Optimized version of the reference implementation of Ed25519 +# +# Written in 2011? by Daniel J. Bernstein +# 2013 by Donald Stufft +# 2013 by Alex Gaynor +# 2013 by Greg Price +# +# To the extent possible under law, the author(s) have dedicated all copyright +# and related and neighboring rights to this software to the public domain +# worldwide. This software is distributed without any warranty. +# +# You should have received a copy of the CC0 Public Domain Dedication along +# with this software. If not, see +# . + +""" +NB: This code is not safe for use with secret keys or secret data. +The only safe use of this code is for verifying signatures on public messages. + +Functions for computing the public key of a secret key and for signing +a message are included, namely publickey_unsafe and signature_unsafe, +for testing purposes only. + +The root of the problem is that Python's long-integer arithmetic is +not designed for use in cryptography. Specifically, it may take more +or less time to execute an operation depending on the values of the +inputs, and its memory access patterns may also depend on the inputs. +This opens it to timing and cache side-channel attacks which can +disclose data to an attacker. We rely on Python's long-integer +arithmetic, so we cannot handle secrets without risking their disclosure. +""" + +import hashlib +import operator +import sys +import base64 + + +__version__ = "1.0.dev0" + + +# Useful for very coarse version differentiation. +PY3 = sys.version_info[0] == 3 + +if PY3: + indexbytes = operator.getitem + intlist2bytes = bytes + int2byte = operator.methodcaller("to_bytes", 1, "big") +else: + int2byte = chr + range = xrange + + def indexbytes(buf, i): + return ord(buf[i]) + + def intlist2bytes(l): + return b"".join(chr(c) for c in l) + + +b = 256 +q = 2 ** 255 - 19 +l = 2 ** 252 + 27742317777372353535851937790883648493 + + +def H(m): + return hashlib.sha512(m).digest() + + +def pow2(x, p): + """== pow(x, 2**p, q)""" + while p > 0: + x = x * x % q + p -= 1 + return x + + +def inv(z): + """$= z^{-1} \mod q$, for z != 0""" + # Adapted from curve25519_athlon.c in djb's Curve25519. + z2 = z * z % q # 2 + z9 = pow2(z2, 2) * z % q # 9 + z11 = z9 * z2 % q # 11 + z2_5_0 = (z11 * z11) % q * z9 % q # 31 == 2^5 - 2^0 + z2_10_0 = pow2(z2_5_0, 5) * z2_5_0 % q # 2^10 - 2^0 + z2_20_0 = pow2(z2_10_0, 10) * z2_10_0 % q # ... + z2_40_0 = pow2(z2_20_0, 20) * z2_20_0 % q + z2_50_0 = pow2(z2_40_0, 10) * z2_10_0 % q + z2_100_0 = pow2(z2_50_0, 50) * z2_50_0 % q + z2_200_0 = pow2(z2_100_0, 100) * z2_100_0 % q + z2_250_0 = pow2(z2_200_0, 50) * z2_50_0 % q # 2^250 - 2^0 + return pow2(z2_250_0, 5) * z11 % q # 2^255 - 2^5 + 11 = q - 2 + + +d = -121665 * inv(121666) % q +I = pow(2, (q - 1) // 4, q) + + +def xrecover(y): + xx = (y * y - 1) * inv(d * y * y + 1) + x = pow(xx, (q + 3) // 8, q) + + if (x * x - xx) % q != 0: + x = (x * I) % q + + if x % 2 != 0: + x = q-x + + return x + + +By = 4 * inv(5) +Bx = xrecover(By) +B = (Bx % q, By % q, 1, (Bx * By) % q) +ident = (0, 1, 1, 0) + + +def edwards_add(P, Q): + # This is formula sequence 'addition-add-2008-hwcd-3' from + # http://www.hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html + (x1, y1, z1, t1) = P + (x2, y2, z2, t2) = Q + + a = (y1-x1)*(y2-x2) % q + b = (y1+x1)*(y2+x2) % q + c = t1*2*d*t2 % q + dd = z1*2*z2 % q + e = b - a + f = dd - c + g = dd + c + h = b + a + x3 = e*f + y3 = g*h + t3 = e*h + z3 = f*g + + return (x3 % q, y3 % q, z3 % q, t3 % q) + + +def edwards_double(P): + # This is formula sequence 'dbl-2008-hwcd' from + # http://www.hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html + (x1, y1, z1, t1) = P + + a = x1*x1 % q + b = y1*y1 % q + c = 2*z1*z1 % q + # dd = -a + e = ((x1+y1)*(x1+y1) - a - b) % q + g = -a + b # dd + b + f = g - c + h = -a - b # dd - b + x3 = e*f + y3 = g*h + t3 = e*h + z3 = f*g + + return (x3 % q, y3 % q, z3 % q, t3 % q) + + +def scalarmult(P, e): + if e == 0: + return ident + Q = scalarmult(P, e // 2) + Q = edwards_double(Q) + if e & 1: + Q = edwards_add(Q, P) + return Q + + +# Bpow[i] == scalarmult(B, 2**i) +Bpow = [] + + +def make_Bpow(): + P = B + for i in range(253): + Bpow.append(P) + P = edwards_double(P) +make_Bpow() + + +def scalarmult_B(e): + """ + Implements scalarmult(B, e) more efficiently. + """ + # scalarmult(B, l) is the identity + e = e % l + P = ident + for i in range(253): + if e & 1: + P = edwards_add(P, Bpow[i]) + e = e // 2 + assert e == 0, e + return P + + +def encodeint(y): + bits = [(y >> i) & 1 for i in range(b)] + return b''.join([ + int2byte(sum([bits[i * 8 + j] << j for j in range(8)])) + for i in range(b//8) + ]) + + +def encodepoint(P): + (x, y, z, t) = P + zi = inv(z) + x = (x * zi) % q + y = (y * zi) % q + bits = [(y >> i) & 1 for i in range(b - 1)] + [x & 1] + return b''.join([ + int2byte(sum([bits[i * 8 + j] << j for j in range(8)])) + for i in range(b // 8) + ]) + + +def bit(h, i): + return (indexbytes(h, i // 8) >> (i % 8)) & 1 + +##modified +def publickey_unsafe(sk): + """ + Not safe to use with secret keys or secret data. + + See module docstring. This function should be used for testing only. + """ + ##h = H(sk) + h = sk + a = 2 ** (b - 2) + sum(2 ** i * bit(h, i) for i in range(3, b - 2)) + A = scalarmult_B(a) + return encodepoint(A) + +##custom +## from stem.util.str_tools._to_unicode_impl +## from https://gitweb.torproject.org/stem.git/tree/stem/util/str_tools.py#n80 +def to_unicode_impl(msg): + if msg is not None and not isinstance(msg, str): + return msg.decode('utf-8', 'replace') + else: + return msg + +##custom +## rewritten stem.descriptor.hidden_service.address_from_identity_key +## from https://gitweb.torproject.org/stem.git/tree/stem/descriptor/hidden_service.py#n1088 +def publickey_to_onionaddress(key): + CHECKSUM_CONSTANT = b'.onion checksum' + ## version = stem.client.datatype.Size.CHAR.pack(3) + version = b'\x03' + checksum = hashlib.sha3_256(CHECKSUM_CONSTANT + key + version).digest()[:2] + onion_address = base64.b32encode(key + checksum + version) + return to_unicode_impl(onion_address + b'.onion').lower() + + +def Hint(m): + h = H(m) + return sum(2 ** i * bit(h, i) for i in range(2 * b)) + +##modified +def signature_unsafe(m, sk, pk): + """ + Not safe to use with secret keys or secret data. + + See module docstring. This function should be used for testing only. + """ + ##h = H(sk) + h = sk + a = 2 ** (b - 2) + sum(2 ** i * bit(h, i) for i in range(3, b - 2)) + r = Hint( + intlist2bytes([indexbytes(h, j) for j in range(b // 8, b // 4)]) + m + ) + R = scalarmult_B(r) + S = (r + Hint(encodepoint(R) + pk + m) * a) % l + return encodepoint(R) + encodeint(S) + + +def isoncurve(P): + (x, y, z, t) = P + return (z % q != 0 and + x*y % q == z*t % q and + (y*y - x*x - z*z - d*t*t) % q == 0) + + +def decodeint(s): + return sum(2 ** i * bit(s, i) for i in range(0, b)) + + +def decodepoint(s): + y = sum(2 ** i * bit(s, i) for i in range(0, b - 1)) + x = xrecover(y) + if x & 1 != bit(s, b-1): + x = q - x + P = (x, y, 1, (x*y) % q) + if not isoncurve(P): + raise ValueError("decoding point that is not on curve") + return P + + +class SignatureMismatch(Exception): + pass + + +def checkvalid(s, m, pk): + """ + Not safe to use when any argument is secret. + + See module docstring. This function should be used only for + verifying public signatures of public messages. + """ + if len(s) != b // 4: + raise ValueError("signature length is wrong") + + if len(pk) != b // 8: + raise ValueError("public-key length is wrong") + + R = decodepoint(s[:b // 8]) + A = decodepoint(pk) + S = decodeint(s[b // 8:b // 4]) + h = Hint(encodepoint(R) + pk + m) + + (x1, y1, z1, t1) = P = scalarmult_B(S) + (x2, y2, z2, t2) = Q = edwards_add(R, scalarmult(A, h)) + + if (not isoncurve(P) or not isoncurve(Q) or + (x1*z2 - x2*z1) % q != 0 or (y1*z2 - y2*z1) % q != 0): + raise SignatureMismatch("signature does not pass verification") diff --git a/src/Crypt/CryptRsa.py b/src/Crypt/CryptRsa.py index 494c4d24..02df8f41 100644 --- a/src/Crypt/CryptRsa.py +++ b/src/Crypt/CryptRsa.py @@ -4,6 +4,13 @@ import hashlib def sign(data, privatekey): import rsa from rsa import pkcs1 + from Crypt import CryptEd25519 + ## v3 = 88 + if len(privatekey) == 88: + prv_key = base64.b64decode(privatekey) + pub_key = CryptEd25519.publickey_unsafe(prv_key) + sign = CryptEd25519.signature_unsafe(data, prv_key, pub_key) + return sign if "BEGIN RSA PRIVATE KEY" not in privatekey: privatekey = "-----BEGIN RSA PRIVATE KEY-----\n%s\n-----END RSA PRIVATE KEY-----" % privatekey @@ -15,6 +22,16 @@ def sign(data, privatekey): def verify(data, publickey, sign): import rsa from rsa import pkcs1 + from Crypt import CryptEd25519 + + if len(publickey) == 32: + try: + valid = CryptEd25519.checkvalid(sign, data, publickey) + valid = 'SHA-256' + except Exception as err: + print(err) + valid = False + return valid pub = rsa.PublicKey.load_pkcs1(publickey, format="DER") try: @@ -24,9 +41,15 @@ def verify(data, publickey, sign): return valid def privatekeyToPublickey(privatekey): + from Crypt import CryptEd25519 import rsa from rsa import pkcs1 + if len(privatekey) == 88: + prv_key = base64.b64decode(privatekey) + pub_key = CryptEd25519.publickey_unsafe(prv_key) + return pub_key + if "BEGIN RSA PRIVATE KEY" not in privatekey: privatekey = "-----BEGIN RSA PRIVATE KEY-----\n%s\n-----END RSA PRIVATE KEY-----" % privatekey @@ -35,4 +58,8 @@ def privatekeyToPublickey(privatekey): return pub.save_pkcs1("DER") def publickeyToOnion(publickey): + from Crypt import CryptEd25519 + if len(publickey) == 32: + addr = CryptEd25519.publickey_to_onionaddress(publickey)[:-6] + return addr return base64.b32encode(hashlib.sha1(publickey).digest()[:10]).lower().decode("ascii") diff --git a/src/Tor/TorManager.py b/src/Tor/TorManager.py index 7e5c8bb0..8de33b4b 100644 --- a/src/Tor/TorManager.py +++ b/src/Tor/TorManager.py @@ -13,6 +13,7 @@ import gevent from Config import config from Crypt import CryptRsa +from Crypt import CryptEd25519 from Site import SiteManager import socks from gevent.lock import RLock @@ -214,8 +215,8 @@ class TorManager(object): return False def makeOnionAndKey(self): - res = self.request("ADD_ONION NEW:RSA1024 port=%s" % self.fileserver_port) - match = re.search("ServiceID=([A-Za-z0-9]+).*PrivateKey=RSA1024:(.*?)[\r\n]", res, re.DOTALL) + res = self.request("ADD_ONION NEW:ED25519-V3 port=%s" % self.fileserver_port) + match = re.search("ServiceID=([A-Za-z0-9]+).*PrivateKey=ED25519-V3:(.*?)[\r\n]", res, re.DOTALL) if match: onion_address, onion_privatekey = match.groups() return (onion_address, onion_privatekey) From 72e5d3df640a08edd2913d7f9423ea9f57613171 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 20 Oct 2021 14:28:22 +0700 Subject: [PATCH 068/191] Fix typo in "All server stopped" --- src/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index 209bb9d2..ec04c9e3 100644 --- a/src/main.py +++ b/src/main.py @@ -155,7 +155,7 @@ class Actions(object): logging.info("Starting servers....") gevent.joinall([gevent.spawn(ui_server.start), gevent.spawn(file_server.start)]) - logging.info("All server stopped") + logging.info("All servers stopped") # Site commands From 164f5199a9e4f107b0fbed1f843cb908949755ca Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 20 Oct 2021 16:28:28 +0700 Subject: [PATCH 069/191] Add default onion v3 tracker addresses --- src/Config.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Config.py b/src/Config.py index fcba371e..67f65f7b 100644 --- a/src/Config.py +++ b/src/Config.py @@ -80,6 +80,17 @@ class Config(object): # Create command line arguments def createArguments(self): trackers = [ + # by zeroseed at http://127.0.0.1:43110/19HKdTAeBh5nRiKn791czY7TwRB1QNrf1Q/?:users/1HvNGwHKqhj3ZMEM53tz6jbdqe4LRpanEu:zn:dc17f896-bf3f-4962-bdd4-0a470040c9c5 + "zero://k5w77dozo3hy5zualyhni6vrh73iwfkaofa64abbilwyhhd3wgenbjqd.onion:15441", + "zero://2kcb2fqesyaevc4lntogupa4mkdssth2ypfwczd2ov5a3zo6ytwwbayd.onion:15441", + "zero://my562dxpjropcd5hy3nd5pemsc4aavbiptci5amwxzbelmzgkkuxpvid.onion:15441", + "zero://pn4q2zzt2pw4nk7yidxvsxmydko7dfibuzxdswi6gu6ninjpofvqs2id.onion:15441", + "zero://6i54dd5th73oelv636ivix6sjnwfgk2qsltnyvswagwphub375t3xcad.onion:15441", + "zero://tl74auz4tyqv4bieeclmyoe4uwtoc2dj7fdqv4nc4gl5j2bwg2r26bqd.onion:15441", + "zero://wlxav3szbrdhest4j7dib2vgbrd7uj7u7rnuzg22cxbih7yxyg2hsmid.onion:15441", + "zero://zy7wttvjtsijt5uwmlar4yguvjc2gppzbdj4v6bujng6xwjmkdg7uvqd.onion:15441", + + # ZeroNet 0.7.2 defaults: "zero://boot3rdez4rzn36x.onion:15441", "zero://zero.booth.moe#f36ca555bee6ba216b14d10f38c16f7769ff064e0e37d887603548cc2e64191d:443", # US/NY "udp://tracker.coppersurfer.tk:6969", # DE From d5652eaa511e576c02dba2097c3e6506bae66f36 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 20 Oct 2021 16:42:21 +0700 Subject: [PATCH 070/191] Update ChangeLog-0.8.0.md --- ZNE-ChangeLog/ChangeLog-0.8.0.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ZNE-ChangeLog/ChangeLog-0.8.0.md b/ZNE-ChangeLog/ChangeLog-0.8.0.md index f2d10b4a..17266268 100644 --- a/ZNE-ChangeLog/ChangeLog-0.8.0.md +++ b/ZNE-ChangeLog/ChangeLog-0.8.0.md @@ -3,6 +3,8 @@ **Network:** +* Added support of Onion v3 addresses. Thanks to @anonymoose and @zeroseed. +* Added a few Onion v3 tracker addresses. * Reworked the algorithm of checking zite updates on startup / after the network outages / periodically. ZeroNet tries not to spam too many update queries at once in order to prevent network overload. (Which especially the issue when running over Tor.) At the same time, it tries to keep balance between frequent checks for frequently updating zites and ensuring that all the zites are checked in some reasonable time interval. Tests show that the full check cycle for a peer that hosts 800+ zites and is connected over Tor can take up to several hours. We cannot significantly reduce this time, since the Tor throughput is the bottleneck. Running more checks at once just results in more connections to fail. The other bottleneck is the HDD throughput. Increasing the parallelization doesn't help in this case as well. So the implemented solution **decreases** the concurency. * Improved the Internet outage detection and the recovery procedures after the Internet be back. ZeroNet "steps back" and schedules rechecking zites that were checked shortly before the Internet connection get lost. The network outage detection normally has some lag, so the recently checked zites are better to checked again. * When the network is down, reduce the frequency of connection attempts to prevent overloading Tor with hanged connections. From d2b65c550c405237087b09ba2f84477fbac3f042 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 20 Oct 2021 16:43:13 +0700 Subject: [PATCH 071/191] Minor fix in ChangeLog-0.8.0.md --- ZNE-ChangeLog/ChangeLog-0.8.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ZNE-ChangeLog/ChangeLog-0.8.0.md b/ZNE-ChangeLog/ChangeLog-0.8.0.md index 17266268..f662d353 100644 --- a/ZNE-ChangeLog/ChangeLog-0.8.0.md +++ b/ZNE-ChangeLog/ChangeLog-0.8.0.md @@ -7,7 +7,7 @@ * Added a few Onion v3 tracker addresses. * Reworked the algorithm of checking zite updates on startup / after the network outages / periodically. ZeroNet tries not to spam too many update queries at once in order to prevent network overload. (Which especially the issue when running over Tor.) At the same time, it tries to keep balance between frequent checks for frequently updating zites and ensuring that all the zites are checked in some reasonable time interval. Tests show that the full check cycle for a peer that hosts 800+ zites and is connected over Tor can take up to several hours. We cannot significantly reduce this time, since the Tor throughput is the bottleneck. Running more checks at once just results in more connections to fail. The other bottleneck is the HDD throughput. Increasing the parallelization doesn't help in this case as well. So the implemented solution **decreases** the concurency. * Improved the Internet outage detection and the recovery procedures after the Internet be back. ZeroNet "steps back" and schedules rechecking zites that were checked shortly before the Internet connection get lost. The network outage detection normally has some lag, so the recently checked zites are better to checked again. -* When the network is down, reduce the frequency of connection attempts to prevent overloading Tor with hanged connections. +* When the network is down, reduce the frequency of connection attempts to prevent overloading Tor with hanged connections. * The connection handling code had several bugs that were hidden by silently ignored exceptions. These were fixed, but some new ones might be introduced. * For each zite the activity rate is calculated based on the last modification time. The milestone values are 1 hour, 5 hours, 24 hours, 3 days and 7 days. The activity rate is used to scale frequency of various maintenance routines, including update checks, reannounces, dead connection checks etc. * The activity rate is also used to calculate the minimum preferred number of active connections per each zite. From 769a2c08ddb86066097c21f8ce53bf57b007a6cf Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 20 Oct 2021 17:25:47 +0700 Subject: [PATCH 072/191] Fix possible infinite growing of the `SafeRe` regexp cache by limiting the cache size --- ZNE-ChangeLog/ChangeLog-0.8.0.md | 1 + src/util/SafeRe.py | 42 +++++++++++++++++++++++++++----- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/ZNE-ChangeLog/ChangeLog-0.8.0.md b/ZNE-ChangeLog/ChangeLog-0.8.0.md index f662d353..9990c683 100644 --- a/ZNE-ChangeLog/ChangeLog-0.8.0.md +++ b/ZNE-ChangeLog/ChangeLog-0.8.0.md @@ -25,6 +25,7 @@ * Implemented the log level overriding for separate modules for easier debugging. * Make the site block check implemented in `ContentFilter` usable from plugins and core modules via `SiteManager.isAddressBlocked()`. +* Fixed possible infinite growing of the `SafeRe` regexp cache by limiting the cache size. ## Docker Image diff --git a/src/util/SafeRe.py b/src/util/SafeRe.py index 6018e2d3..2d519a79 100644 --- a/src/util/SafeRe.py +++ b/src/util/SafeRe.py @@ -1,10 +1,16 @@ import re +import logging + +log = logging.getLogger("SafeRe") + class UnsafePatternError(Exception): pass +max_cache_size = 1000 cached_patterns = {} +old_cached_patterns = {} def isSafePattern(pattern): @@ -22,11 +28,35 @@ def isSafePattern(pattern): return True -def match(pattern, *args, **kwargs): +def compilePattern(pattern): + global cached_patterns + global old_cached_patterns + cached_pattern = cached_patterns.get(pattern) if cached_pattern: - return cached_pattern.match(*args, **kwargs) - else: - if isSafePattern(pattern): - cached_patterns[pattern] = re.compile(pattern) - return cached_patterns[pattern].match(*args, **kwargs) + return cached_pattern + + cached_pattern = old_cached_patterns.get(pattern) + if cached_pattern: + del old_cached_patterns[pattern] + cached_patterns[pattern] = cached_pattern + return cached_pattern + + if isSafePattern(pattern): + cached_pattern = re.compile(pattern) + cached_patterns[pattern] = cached_pattern + log.debug("Compiled new pattern: %s" % pattern) + log.debug("Cache size: %d + %d" % (len(cached_patterns), len(old_cached_patterns))) + + if len(cached_patterns) > max_cache_size: + old_cached_patterns = cached_patterns + cached_patterns = {} + log.debug("Size limit reached. Rotating cache.") + log.debug("Cache size: %d + %d" % (len(cached_patterns), len(old_cached_patterns))) + + return cached_pattern + + +def match(pattern, *args, **kwargs): + cached_pattern = compilePattern(pattern) + return cached_pattern.match(*args, **kwargs) From 488fd4045ee79489d21fbb31bf5ad12645ebb017 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 20 Oct 2021 17:28:03 +0700 Subject: [PATCH 073/191] Update ChangeLog-0.8.0.md --- ZNE-ChangeLog/ChangeLog-0.8.0.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ZNE-ChangeLog/ChangeLog-0.8.0.md b/ZNE-ChangeLog/ChangeLog-0.8.0.md index 9990c683..2b532d62 100644 --- a/ZNE-ChangeLog/ChangeLog-0.8.0.md +++ b/ZNE-ChangeLog/ChangeLog-0.8.0.md @@ -36,6 +36,9 @@ The default ZeroHello address changed from [1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D/](http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D/) to [1HeLLoPVbqF3UEj8aWXErwTxrwkyjwGtZN/](http://127.0.0.1:43110/1HeLLoPVbqF3UEj8aWXErwTxrwkyjwGtZN/). This is the first step in migrating away from the nofish's infrastructure which development seems to be stalled. +* Make #Dashboard .tracker-status wider to fit onion v3 addresses. +* Added link to /Stats to the menu. + ## Plugins ### TrackerShare From 23ef37374b6b641a2ea342a93cb1572811428eb5 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 20 Oct 2021 17:39:39 +0700 Subject: [PATCH 074/191] Various fixes in ChangeLog-0.8.0.md --- ZNE-ChangeLog/ChangeLog-0.8.0.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/ZNE-ChangeLog/ChangeLog-0.8.0.md b/ZNE-ChangeLog/ChangeLog-0.8.0.md index 2b532d62..a2bba51b 100644 --- a/ZNE-ChangeLog/ChangeLog-0.8.0.md +++ b/ZNE-ChangeLog/ChangeLog-0.8.0.md @@ -5,8 +5,8 @@ * Added support of Onion v3 addresses. Thanks to @anonymoose and @zeroseed. * Added a few Onion v3 tracker addresses. -* Reworked the algorithm of checking zite updates on startup / after the network outages / periodically. ZeroNet tries not to spam too many update queries at once in order to prevent network overload. (Which especially the issue when running over Tor.) At the same time, it tries to keep balance between frequent checks for frequently updating zites and ensuring that all the zites are checked in some reasonable time interval. Tests show that the full check cycle for a peer that hosts 800+ zites and is connected over Tor can take up to several hours. We cannot significantly reduce this time, since the Tor throughput is the bottleneck. Running more checks at once just results in more connections to fail. The other bottleneck is the HDD throughput. Increasing the parallelization doesn't help in this case as well. So the implemented solution **decreases** the concurency. -* Improved the Internet outage detection and the recovery procedures after the Internet be back. ZeroNet "steps back" and schedules rechecking zites that were checked shortly before the Internet connection get lost. The network outage detection normally has some lag, so the recently checked zites are better to checked again. +* Reworked the algorithm of checking zite updates on startup / after the network outages / periodically. ZeroNet tries not to spam too many update queries at once in order to prevent network overload. (Which especially the issue when running over Tor.) At the same time, it tries to keep balance between frequent checks for frequently updating zites and ensuring that all the zites are checked in some reasonable time interval. Tests show that the full check cycle for a peer that hosts 800+ zites and is connected over Tor can take up to several hours. We cannot significantly reduce this time, since the Tor throughput is the bottleneck. Running more checks at once just results in more connections to fail. The other bottleneck is the HDD throughput. Increasing the parallelization doesn't help in this case as well. So the implemented solution do actually **decreases** the parallelization. +* Improved the Internet outage detection and the recovery procedures after the Internet be back. ZeroNet "steps back" and schedules rechecking zites that were checked shortly before the Internet connection get lost. The network outage detection normally has some lag, so the recently checked zites are better to be checked again. * When the network is down, reduce the frequency of connection attempts to prevent overloading Tor with hanged connections. * The connection handling code had several bugs that were hidden by silently ignored exceptions. These were fixed, but some new ones might be introduced. * For each zite the activity rate is calculated based on the last modification time. The milestone values are 1 hour, 5 hours, 24 hours, 3 days and 7 days. The activity rate is used to scale frequency of various maintenance routines, including update checks, reannounces, dead connection checks etc. @@ -15,11 +15,11 @@ * Activity. More activity ==> frequent announces. * Peer count. More peers ==> rare announces. * Tracker count. More trackers ==> frequent announces to iterate over more trackers. -* For owned zites, the activity rate doesn't drop below 0.6 to force more frequent checks. This, however, can be used to investigate which peer belongs to the zire owner. A new commnd line option `--expose_no_ownership` is introduced to disable that behavior. +* For owned zites, the activity rate doesn't drop below 0.6 to force more frequent checks. This, however, can be used to investigate which peer belongs to the zite owner. A new commnd line option `--expose_no_ownership` is introduced to disable that behavior. * When checking for updates, ZeroNet normally asks other peers for new data since the previous update. This however can result in losing some updates in specific conditions. To overcome this, ZeroNet now asks for the full site listing on every Nth update check. * When asking a peer for updates, ZeroNet may see that the other peer has an older version of a file. In this case, ZeroNet sends back the notification of the new version available. The logic in 0.8.0 is generally the same, but some randomization is added which may help in distributing the "update waves" among peers. * ZeroNet now tries harder in delivering updates to more peers in the background. -* ZeroNet also make more efforts of searching the peers before publishing updates. +* ZeroNet also makes more efforts of searching the peers before publishing updates. **Other:** @@ -49,12 +49,12 @@ Changes in the plugin: * The default total tracker limit increased up to 20 from 5. This reduces the probability of accidental splitting the network into segments that operate with disjoint sets of trackers. * The plugin now shares any types of trackers, working both on IP and Onion networks, not limiting solely to Zero-based IP trackers. -* The plugin now takes into account not only the total number of known working trackers, but also does it on per protocol basis. The defaults are to keep 10 trackers for Zero protocol and 5 trackers per each other protocol. The available protocols are detected automatically. (For example, UDP is considered disabled, when working in the Tor-always mode.) The following protocols are recognized: `zero://` (Zero Tracker), `udp://` (UDP-based bitTorrent tracker), `http://` or `https://` (HTTP or HTTPS-based bitTorrent tracker; considered the same protocol). In case of new protocols implemented by third-party plugins, trackers are grouped automatically by the protocol prefix. -* Reworked the interaction of this plugin and the Zero tracker (`Bootstrapper`) plugin. Previously: `AnnounceShare` checks if `Bootstrapper` is running and adds its addresses to the list. Now: the tracker explicitly asks `TrackerShare` to add its addresses. -* The plugin allows adjustings the behaviour per each tracker entry with the following boolean fields: +* The plugin now takes into account not only the total number of known working trackers, but also does it on per protocol basis. The defaults are to keep 10 trackers for Zero protocol and 5 trackers per each other protocol. The available protocols are detected automatically. (For example, UDP is considered disabled, when working in the Tor-always mode.) The following protocols are recognized: `zero://` (Zero Tracker), `udp://` (UDP-based BitTorrent tracker), `http://` or `https://` (HTTP or HTTPS-based BitTorrent tracker; considered the same protocol). In case of new protocols implemented by third-party plugins, trackers are grouped automatically by the protocol prefix. +* Reworked the interaction of this plugin and the Zero tracker (`Bootstrapper`) plugin. Previously: `AnnounceShare` checks if `Bootstrapper` is running and adds its addresses to the list. Now: `Bootstrapper` explicitly asks `TrackerShare` to add its addresses. +* The plugin allows adjusting the behaviour per each tracker entry with the following boolean fields: * `my` — "My" trackers get never deleted on announce errors, but aren't saved between restarts. Designed to be used by tracker implementation plugins. `TrackerShare` acts more persistently in recommending "my" trackers to other peers. * `persistent` — "Persistent" trackers get never deleted, when unresponsive. - * `private` — "Private" trackers are never exposed to other peer in response of the getTrackers command. + * `private` — "Private" trackers are never exposed to other peers in response of the getTrackers command. ### TrackerZero @@ -74,6 +74,6 @@ Running over TOR doesn't seem to be stable so far. Any investigations and bug re ### TrackerList -`TrackerZero` is a new plugin for fetching tracker lists. By default is list is fetched from [https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_ip.txt](https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_ip.txt). +`TrackerList` is a new plugin for fetching tracker lists. By default is list is fetched from [https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_ip.txt](https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_ip.txt). TODO: add support of fetching from ZeroNet URLs From ba6295f793ea6dc1831eb72b0e75469d5e9ccd21 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 20 Oct 2021 18:28:03 +0700 Subject: [PATCH 075/191] Add tests directly to SafeRe.py --- src/util/SafeRe.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/util/SafeRe.py b/src/util/SafeRe.py index 2d519a79..c9e14c0d 100644 --- a/src/util/SafeRe.py +++ b/src/util/SafeRe.py @@ -60,3 +60,34 @@ def compilePattern(pattern): def match(pattern, *args, **kwargs): cached_pattern = compilePattern(pattern) return cached_pattern.match(*args, **kwargs) + +################################################################################ + +# TESTS + +def testSafePattern(pattern): + try: + return isSafePattern(pattern) + except UnsafePatternError as err: + return False + + +# Some real examples to make sure it works as expected +assert testSafePattern('(data/mp4/.*|updater/.*)') +assert testSafePattern('((js|css)/(?!all.(js|css)))|.git') + + +# Unsafe cases: + +# ((?!json).)*$ not allowed, because of ) before the * character. Possible fix: .*(?!json)$ +assert not testSafePattern('((?!json).)*$') +assert testSafePattern('.*(?!json)$') + +# (.*.epub|.*.jpg|.*.jpeg|.*.png|data/.*.gif|.*.avi|.*.ogg|.*.webm|.*.mp4|.*.mp3|.*.mkv|.*.eot) not allowed, because it has 12 .* repetition patterns. Possible fix: .*(epub|jpg|jpeg|png|data/gif|avi|ogg|webm|mp4|mp3|mkv|eot) +assert not testSafePattern('(.*.epub|.*.jpg|.*.jpeg|.*.png|data/.*.gif|.*.avi|.*.ogg|.*.webm|.*.mp4|.*.mp3|.*.mkv|.*.eot)') +assert testSafePattern('.*(epub|jpg|jpeg|png|data/gif|avi|ogg|webm|mp4|mp3|mkv|eot)') + +# FIXME: https://github.com/HelloZeroNet/ZeroNet/issues/2757 +#assert not testSafePattern('a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') + +################################################################################ From 2a25d61b968a21aa98c6db2ca9d64f1bbdc54773 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 20 Oct 2021 19:01:55 +0700 Subject: [PATCH 076/191] Fix https://github.com/HelloZeroNet/ZeroNet/issues/2757 --- src/util/SafeRe.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/util/SafeRe.py b/src/util/SafeRe.py index c9e14c0d..8c394a84 100644 --- a/src/util/SafeRe.py +++ b/src/util/SafeRe.py @@ -21,9 +21,10 @@ def isSafePattern(pattern): if unsafe_pattern_match: raise UnsafePatternError("Potentially unsafe part of the pattern: %s in %s" % (unsafe_pattern_match.group(0), pattern)) - repetitions = re.findall(r"\.[\*\{\+]", pattern) - if len(repetitions) >= 10: - raise UnsafePatternError("More than 10 repetitions of %s in %s" % (repetitions[0], pattern)) + repetitions1 = re.findall(r"\.[\*\{\+]", pattern) + repetitions2 = re.findall(r"[^(][?]", pattern) + if len(repetitions1) + len(repetitions2) >= 10: + raise UnsafePatternError("More than 10 repetitions in %s" % pattern) return True @@ -87,7 +88,11 @@ assert testSafePattern('.*(?!json)$') assert not testSafePattern('(.*.epub|.*.jpg|.*.jpeg|.*.png|data/.*.gif|.*.avi|.*.ogg|.*.webm|.*.mp4|.*.mp3|.*.mkv|.*.eot)') assert testSafePattern('.*(epub|jpg|jpeg|png|data/gif|avi|ogg|webm|mp4|mp3|mkv|eot)') -# FIXME: https://github.com/HelloZeroNet/ZeroNet/issues/2757 -#assert not testSafePattern('a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') +# https://github.com/HelloZeroNet/ZeroNet/issues/2757 +assert not testSafePattern('a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') +assert not testSafePattern('a?a?a?a?a?a?a?x.{0,1}x.{0,1}x.{0,1}') +assert testSafePattern('a?a?a?a?a?a?a?x.{0,1}x.{0,1}') +assert not testSafePattern('a?a?a?a?a?a?a?x.*x.*x.*') +assert testSafePattern('a?a?a?a?a?a?a?x.*x.*') ################################################################################ From 46bea950024aa606a9e8c5f63ebadc2361533f09 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 20 Oct 2021 20:43:52 +0700 Subject: [PATCH 077/191] Small cleanup in TrackerSharePlugin.py --- plugins/TrackerShare/TrackerSharePlugin.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/plugins/TrackerShare/TrackerSharePlugin.py b/plugins/TrackerShare/TrackerSharePlugin.py index e6b4454b..99ebf692 100644 --- a/plugins/TrackerShare/TrackerSharePlugin.py +++ b/plugins/TrackerShare/TrackerSharePlugin.py @@ -443,6 +443,16 @@ class TrackerStorage(object): self.log.info("Discovered %s new trackers from %s/%s peers in %.3fs" % (num_trackers_discovered, num_success, len(peers), time.time() - s)) + def checkDiscoveringTrackers(self, peers): + if not peers or len(peers) < 1: + return + + now = time.time() + if self.time_discover + self.tracker_discover_time_interval >= now: + return + + self.time_discover = now + gevent.spawn(self.discoverTrackers, peers) if "tracker_storage" not in locals(): tracker_storage = TrackerStorage() @@ -452,9 +462,7 @@ if "tracker_storage" not in locals(): class SiteAnnouncerPlugin(object): def getTrackers(self): tracker_storage.setSiteAnnouncer(self) - if tracker_storage.time_discover < time.time() - tracker_storage.tracker_discover_time_interval: - tracker_storage.time_discover = time.time() - gevent.spawn(tracker_storage.discoverTrackers, self.site.getConnectedPeers()) + tracker_storage.checkDiscoveringTrackers(self.site.getConnectedPeers()) trackers = super(SiteAnnouncerPlugin, self).getTrackers() shared_trackers = list(tracker_storage.getTrackers().keys()) if shared_trackers: From 7e438a90e15cb9e034355e22a942c7055a7febcc Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 20 Oct 2021 21:10:04 +0700 Subject: [PATCH 078/191] Don't spam in log.info() from getRecentPeers() --- src/Site/Site.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 010d493b..536f3faa 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -1357,8 +1357,8 @@ class Site(object): else: peers = list(self.peers.values()) - self.log.info("%s" % peers) - self.log.info("%s" % need_more) + self.log.debug("getRecentPeers: peers = %s" % peers) + self.log.debug("getRecentPeers: need_more = %s" % need_more) peers = peers[0:need_more * 50] From 75bba6ca1a6ead87b1b4e766741cfc53dbfa276d Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 20 Oct 2021 22:40:34 +0700 Subject: [PATCH 079/191] Be more verbose about starting/stopping FileServer threads --- src/File/FileServer.py | 38 +++++++++++++++++++++++++++++--------- src/Ui/UiServer.py | 2 +- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index c2a7d7d0..73c86bd4 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -322,7 +322,7 @@ class FileServer(ConnectionServer): task_nr = self.update_sites_task_next_nr self.update_sites_task_next_nr += 1 - task_description = "updateSites: #%d, check_files=%s" % (task_nr, check_files) + task_description = "updateSites [#%d, check_files=%s]" % (task_nr, check_files) log.info("%s: started", task_description) # Don't wait port opening on first startup. Do the instant check now. @@ -363,6 +363,9 @@ class FileServer(ConnectionServer): while self.running: self.waitForInternetOnline() + if self.stopping: + break + thread = self.update_pool.spawn(self.updateSite, site, check_files=check_files) if check_files: # Limit the concurency @@ -371,6 +374,9 @@ class FileServer(ConnectionServer): if not self.waitForInternetOnline(): break + if self.stopping: + break + if time.time() - progress_print_time > 60: progress_print_time = time.time() time_spent = time.time() - start_time @@ -387,9 +393,14 @@ class FileServer(ConnectionServer): time_left ) - log.info("%s: finished in %.2fs", task_description, time.time() - start_time) + if self.stopping: + log.info("%s: stopped", task_description) + else: + log.info("%s: finished in %.2fs", task_description, time.time() - start_time) def sitesMaintenanceThread(self, mode="full"): + log.info("sitesMaintenanceThread(%s) started" % mode) + startup = True short_timeout = 2 @@ -460,6 +471,7 @@ class FileServer(ConnectionServer): site_addresses = None startup = False + log.info("sitesMaintenanceThread(%s) stopped" % mode) def keepAliveThread(self): # This thread is mostly useless on a system under load, since it never does @@ -475,6 +487,7 @@ class FileServer(ConnectionServer): # are interested in connecting to them), we initiate some traffic by # performing the update for a random site. It's way better than just # silly pinging a random peer for no profit. + log.info("keepAliveThread started") while self.running: self.waitForInternetOnline() @@ -499,6 +512,7 @@ class FileServer(ConnectionServer): now - last_activity_time ) self.update_pool.spawn(self.updateRandomSite, force=True) + log.info("keepAliveThread stopped") # Periodic reloading of tracker files def reloadTrackerFilesThread(self): @@ -506,19 +520,24 @@ class FileServer(ConnectionServer): # This should probably be more sophisticated. # We should check if the files have actually changed, # and do it more often. + log.info("reloadTrackerFilesThread started") interval = 60 * 10 while self.running: self.sleep(interval) if self.stopping: break config.loadTrackersFile() + log.info("reloadTrackerFilesThread stopped") # Detects if computer back from wakeup - def wakeupWatcher(self): + def wakeupWatcherThread(self): + log.info("wakeupWatcherThread started") last_time = time.time() last_my_ips = socket.gethostbyname_ex('')[2] while self.running: self.sleep(30) + if self.stopping: + break is_time_changed = time.time() - max(self.last_request, last_time) > 60 * 3 if is_time_changed: # If taken more than 3 minute then the computer was in sleep mode @@ -543,6 +562,7 @@ class FileServer(ConnectionServer): last_time = time.time() last_my_ips = my_ips + log.info("wakeupWatcherThread stopped") # Bind and start serving sites # If passive_mode is False, FileServer starts the full-featured file serving: @@ -581,20 +601,20 @@ class FileServer(ConnectionServer): self.getSites() if not passive_mode: - self.spawn(self.updateSites) - thread_reaload_tracker_files = self.spawn(self.reloadTrackerFilesThread) + thread_keep_alive = self.spawn(self.keepAliveThread) + thread_wakeup_watcher = self.spawn(self.wakeupWatcherThread) + thread_reload_tracker_files = self.spawn(self.reloadTrackerFilesThread) thread_sites_maintenance_full = self.spawn(self.sitesMaintenanceThread, mode="full") thread_sites_maintenance_short = self.spawn(self.sitesMaintenanceThread, mode="short") - thread_keep_alive = self.spawn(self.keepAliveThread) - thread_wakeup_watcher = self.spawn(self.wakeupWatcher) + self.sleep(0.1) + self.spawn(self.updateSites) self.sleep(0.1) self.spawn(self.updateSites, check_files=True) - ConnectionServer.listen(self) - log.debug("Stopped.") + log.info("Stopped.") def stop(self): if self.running and self.portchecker.upnp_port_opened: diff --git a/src/Ui/UiServer.py b/src/Ui/UiServer.py index 9d93ccfd..6cd0545c 100644 --- a/src/Ui/UiServer.py +++ b/src/Ui/UiServer.py @@ -166,7 +166,7 @@ class UiServer: self.log.error("Web interface bind error, must be running already, exiting.... %s" % err) import main main.file_server.stop() - self.log.debug("Stopped.") + self.log.info("Stopped.") def stop(self): self.log.debug("Stopping...") From 5ec970adb89e0f0084b07763c171e9130804d40e Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Thu, 21 Oct 2021 02:24:16 +0700 Subject: [PATCH 080/191] Add option verify_files to Site.update() to allow the real content verification check, not just the simple file size-based one Add more informative updateWebsocket() notification in Site.update() and SiteStorage.verifyFiles() --- src/Site/Site.py | 19 +++++++++++++++---- src/Site/SiteStorage.py | 39 +++++++++++++++++++++++++++++++++++---- src/Ui/UiWebsocket.py | 4 ++-- 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 536f3faa..9e333751 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -689,18 +689,28 @@ class Site(object): # Update content.json from peers and download changed files # Return: None @util.Noparallel() - def update(self, announce=False, check_files=False, since=None): + def update(self, announce=False, check_files=False, verify_files=False, since=None): self.content_manager.loadContent("content.json", load_includes=False) # Reload content.json self.content_updated = None # Reset content updated time - if check_files: + if verify_files: + check_files = True + + self.updateWebsocket(updating=True) + if verify_files: + self.updateWebsocket(verifying=True) + elif check_files: + self.updateWebsocket(checking=True) + + if verify_files: + self.storage.updateBadFiles(quick_check=False) + elif check_files: self.storage.updateBadFiles(quick_check=True) # Quick check and mark bad files based on file size if not self.isServing(): + self.updateWebsocket(updated=True) return False - self.updateWebsocket(updating=True) - # Remove files that no longer in content.json self.checkBadFiles() @@ -1573,6 +1583,7 @@ class Site(object): param = None for ws in self.websockets: ws.event("siteChanged", self, param) + time.sleep(0.001) def messageWebsocket(self, message, type="info", progress=None): for ws in self.websockets: diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index c12a80b0..4e532788 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -24,6 +24,25 @@ thread_pool_fs_read = ThreadPool.ThreadPool(config.threads_fs_read, name="FS rea thread_pool_fs_write = ThreadPool.ThreadPool(config.threads_fs_write, name="FS write") thread_pool_fs_batch = ThreadPool.ThreadPool(1, name="FS batch") +class VerifyFiles_Notificator(object): + def __init__(self, site, quick_check): + self.site = site + self.quick_check = quick_check + self.scanned_files = 0 + self.websocket_update_interval = 0.25 + self.websocket_update_time = time.time() + + def inc(self): + self.scanned_files += 1 + if self.websocket_update_time + self.websocket_update_interval < time.time(): + self.send() + + def send(self): + self.websocket_update_time = time.time() + if self.quick_check: + self.site.updateWebsocket(checking=self.scanned_files) + else: + self.site.updateWebsocket(verifying=self.scanned_files) @PluginManager.acceptPlugins class SiteStorage(object): @@ -427,11 +446,14 @@ class SiteStorage(object): i = 0 self.log.debug("Verifing files...") + notificator = VerifyFiles_Notificator(self.site, quick_check) + if not self.site.content_manager.contents.get("content.json"): # No content.json, download it first self.log.debug("VerifyFile content.json not exists") self.site.needFile("content.json", update=True) # Force update to fix corrupt file self.site.content_manager.loadContent() # Reload content.json - for content_inner_path, content in list(self.site.content_manager.contents.items()): + for content_inner_path, content in self.site.content_manager.contents.iteritems(): + notificator.inc() back["num_content"] += 1 i += 1 if i % 50 == 0: @@ -442,6 +464,7 @@ class SiteStorage(object): bad_files.append(content_inner_path) for file_relative_path in list(content.get("files", {}).keys()): + notificator.inc() back["num_file"] += 1 file_inner_path = helper.getDirname(content_inner_path) + file_relative_path # Relative to site dir file_inner_path = file_inner_path.strip("/") # Strip leading / @@ -452,14 +475,19 @@ class SiteStorage(object): bad_files.append(file_inner_path) continue + err = None + if quick_check: - ok = os.path.getsize(file_path) == content["files"][file_relative_path]["size"] + file_size = os.path.getsize(file_path) + expected_size = content["files"][file_relative_path]["size"] + ok = file_size == expected_size if not ok: - err = "Invalid size" + err = "Invalid size: %s - actual, %s - expected" % (file_size, expected_size) else: try: ok = self.site.content_manager.verifyFile(file_inner_path, open(file_path, "rb")) - except Exception as err: + except Exception as err2: + err = err2 ok = False if not ok: @@ -472,6 +500,7 @@ class SiteStorage(object): optional_added = 0 optional_removed = 0 for file_relative_path in list(content.get("files_optional", {}).keys()): + notificator.inc() back["num_optional"] += 1 file_node = content["files_optional"][file_relative_path] file_inner_path = helper.getDirname(content_inner_path) + file_relative_path # Relative to site dir @@ -516,6 +545,8 @@ class SiteStorage(object): (content_inner_path, len(content["files"]), quick_check, optional_added, optional_removed) ) + notificator.send() + self.site.content_manager.contents.db.processDelayed() time.sleep(0.001) # Context switch to avoid gevent hangs return back diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 88e395d6..85fa904d 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -912,9 +912,9 @@ class UiWebsocket(object): self.response(to, "ok") # Update site content.json - def actionSiteUpdate(self, to, address, check_files=False, since=None, announce=False): + def actionSiteUpdate(self, to, address, check_files=False, verify_files=False, since=None, announce=False): def updateThread(): - site.update(announce=announce, check_files=check_files, since=since) + site.update(announce=announce, check_files=check_files, verify_files=verify_files, since=since) self.response(to, "Updated") site = self.server.sites.get(address) From 5744e40505c5ab6daad597da5aadaf85dedb04c6 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Thu, 21 Oct 2021 13:19:10 +0700 Subject: [PATCH 081/191] Redesign the scheduling of site checking and verification Save `check_files_timestamp` and `verify_files_timestamp` in sites.json and run checks only when the time interval is expired. --- src/File/FileServer.py | 101 ++++++++++++++++++++++++++++---------- src/Site/Site.py | 109 +++++++++++++++++++---------------------- 2 files changed, 124 insertions(+), 86 deletions(-) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index 73c86bd4..a0f16b97 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -303,12 +303,12 @@ class FileServer(ConnectionServer): log.debug("Checking randomly chosen site: %s", site.address_short) - self.updateSite(site, force=force) + self.updateSite(site) - def updateSite(self, site, check_files=False, force=False, dry_run=False): + def updateSite(self, site, check_files=False, verify_files=False): if not site: return False - return site.considerUpdate(check_files=check_files, force=force, dry_run=dry_run) + return site.update2(check_files=check_files, verify_files=verify_files) def invalidateUpdateTime(self, invalid_interval): for address in self.getSiteAddresses(): @@ -316,24 +316,30 @@ class FileServer(ConnectionServer): if site: site.invalidateUpdateTime(invalid_interval) - def updateSites(self, check_files=False): - self.recheckPort() + def isSiteUpdateTimeValid(self, site_address): + site = self.getSite(site_address) + if not site: + return False + return site.isUpdateTimeValid() + def updateSites(self): task_nr = self.update_sites_task_next_nr self.update_sites_task_next_nr += 1 - task_description = "updateSites [#%d, check_files=%s]" % (task_nr, check_files) + task_description = "updateSites [#%d]" % task_nr log.info("%s: started", task_description) # Don't wait port opening on first startup. Do the instant check now. if len(self.getSites()) <= 2: for address, site in list(self.getSites().items()): - self.updateSite(site, check_files=check_files) + self.updateSite(site, check_files=True) + + self.recheckPort() all_site_addresses = self.getSiteAddresses() site_addresses = [ address for address in all_site_addresses - if self.updateSite(self.getSite(address), check_files=check_files, dry_run=True) + if not self.isSiteUpdateTimeValid(address) ] log.info("%s: chosen %d sites (of %d)", task_description, len(site_addresses), len(all_site_addresses)) @@ -346,33 +352,21 @@ class FileServer(ConnectionServer): # Check sites integrity for site_address in site_addresses: - if check_files: - self.sleep(10) - else: - self.sleep(1) + site = None + self.sleep(1) + self.waitForInternetOnline() if self.stopping: break site = self.getSite(site_address) - if not self.updateSite(site, check_files=check_files, dry_run=True): + if not site or site.isUpdateTimeValid(): sites_skipped += 1 continue sites_processed += 1 - while self.running: - self.waitForInternetOnline() - if self.stopping: - break - - thread = self.update_pool.spawn(self.updateSite, site, check_files=check_files) - if check_files: - # Limit the concurency - # ZeroNet may be laggy when running from HDD. - thread.join(timeout=60) - if not self.waitForInternetOnline(): - break + thread = self.update_pool.spawn(self.updateSite, site) if self.stopping: break @@ -398,6 +392,60 @@ class FileServer(ConnectionServer): else: log.info("%s: finished in %.2fs", task_description, time.time() - start_time) + def peekSiteForVerification(self): + check_files_interval = 60 * 60 * 24 + verify_files_interval = 60 * 60 * 24 * 10 + site_addresses = self.getSiteAddresses() + random.shuffle(site_addresses) + for site_address in site_addresses: + site = self.getSite(site_address) + if not site: + continue + mode = site.isFileVerificationExpired(check_files_interval, verify_files_interval) + if mode: + return (site_address, mode) + return (None, None) + + + def sitesVerificationThread(self): + log.info("sitesVerificationThread started") + short_timeout = 10 + long_timeout = 60 + + self.sleep(long_timeout) + + while self.running: + site = None + self.sleep(short_timeout) + + if self.stopping: + break + + site_address, mode = self.peekSiteForVerification() + if not site_address: + self.sleep(long_timeout) + continue + + site = self.getSite(site_address) + if not site: + continue + + if mode == "verify": + check_files = False + verify_files = True + elif mode == "check": + check_files = True + verify_files = False + else: + continue + + log.info("running <%s> for %s" % (mode, site.address_short)) + + thread = self.update_pool.spawn(self.updateSite, site, + check_files=check_files, verify_files=verify_files) + + log.info("sitesVerificationThread stopped") + def sitesMaintenanceThread(self, mode="full"): log.info("sitesMaintenanceThread(%s) started" % mode) @@ -603,14 +651,13 @@ class FileServer(ConnectionServer): if not passive_mode: thread_keep_alive = self.spawn(self.keepAliveThread) thread_wakeup_watcher = self.spawn(self.wakeupWatcherThread) + thread_sites_verification = self.spawn(self.sitesVerificationThread) thread_reload_tracker_files = self.spawn(self.reloadTrackerFilesThread) thread_sites_maintenance_full = self.spawn(self.sitesMaintenanceThread, mode="full") thread_sites_maintenance_short = self.spawn(self.sitesMaintenanceThread, mode="short") self.sleep(0.1) self.spawn(self.updateSites) - self.sleep(0.1) - self.spawn(self.updateSites, check_files=True) ConnectionServer.listen(self) diff --git a/src/Site/Site.py b/src/Site/Site.py index 9e333751..aa2bc66b 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -183,7 +183,6 @@ class Site(object): self.worker_manager = WorkerManager(self) # Handle site download from other peers self.bad_files = {} # SHA check failed files, need to redownload {"inner.content": 1} (key: file, value: failed accept) self.content_updated = None # Content.js update time - self.last_check_files_time = 0 self.last_online_update = 0 self.startup_announce_done = 0 self.notifications = [] # Pending notifications displayed once on page load [error|ok|info, message, timeout] @@ -227,6 +226,10 @@ class Site(object): settings = json.load(open("%s/sites.json" % config.data_dir)).get(self.address) if settings: self.settings = settings + if "check_files_timestamp" not in settings: + settings["check_files_timestamp"] = 0 + if "verify_files_timestamp" not in settings: + settings["verify_files_timestamp"] = 0 if "cache" not in settings: settings["cache"] = {} if "size_files_optional" not in settings: @@ -242,8 +245,17 @@ class Site(object): self.bad_files[inner_path] = min(self.bad_files[inner_path], 20) else: self.settings = { - "own": False, "serving": True, "permissions": [], "cache": {"bad_files": {}}, "size_files_optional": 0, - "added": int(time.time()), "downloaded": None, "optional_downloaded": 0, "size_optional": 0 + "check_files_timestamp": 0, + "verify_files_timestamp": 0, + "own": False, + "serving": True, + "permissions": [], + "cache": {"bad_files": {}}, + "size_files_optional": 0, + "added": int(time.time()), + "downloaded": None, + "optional_downloaded": 0, + "size_optional": 0 } # Default if config.download_optional == "auto": self.settings["autodownloadoptional"] = True @@ -281,6 +293,29 @@ class Site(object): def getSizeLimit(self): return self.settings.get("size_limit", int(config.size_limit)) + def isFileVerificationExpired(self, check_files_interval, verify_files_interval): + now = time.time() + check_files_timestamp = self.settings.get("check_files_timestamp", 0) + verify_files_timestamp = self.settings.get("verify_files_timestamp", 0) + + if check_files_interval is None: + check_files_expiration = now + 1 + else: + check_files_expiration = check_files_timestamp + check_files_interval + + if verify_files_interval is None: + verify_files_expiration = now + 1 + else: + verify_files_expiration = verify_files_timestamp + verify_files_interval + + if verify_files_expiration < now: + return "verify" + + if check_files_expiration < now: + return "check" + + return False + # Next size limit based on current size def getNextSizeLimit(self): size_limits = [10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000] @@ -690,9 +725,10 @@ class Site(object): # Return: None @util.Noparallel() def update(self, announce=False, check_files=False, verify_files=False, since=None): + online = self.connection_server.isInternetOnline() + self.content_manager.loadContent("content.json", load_includes=False) # Reload content.json self.content_updated = None # Reset content updated time - if verify_files: check_files = True @@ -704,8 +740,11 @@ class Site(object): if verify_files: self.storage.updateBadFiles(quick_check=False) + self.settings["check_files_timestamp"] = time.time() + self.settings["verify_files_timestamp"] = time.time() elif check_files: self.storage.updateBadFiles(quick_check=True) # Quick check and mark bad files based on file size + self.settings["check_files_timestamp"] = time.time() if not self.isServing(): self.updateWebsocket(updated=True) @@ -738,66 +777,18 @@ class Site(object): self.sendMyHashfield() self.updateHashfield() - self.refreshUpdateTime(valid=self.connection_server.isInternetOnline()) - - self.updateWebsocket(updated=True) - - @util.Noparallel(queue=True, ignore_args=True) - def _considerUpdate_realJob(self, check_files=False, force=False): - if not self._considerUpdate_check(check_files=check_files, force=force, log_reason=True): - return False - - online = self.connection_server.isInternetOnline() - - # TODO: there should be a configuration options controlling: - # * whether to check files on the program startup - # * whether to check files during the run time and how often - check_files = check_files and (self.last_check_files_time == 0) - self.last_check_files_time = time.time() - - if len(self.peers) < 50: - self.announce(mode="update") - online = online and self.connection_server.isInternetOnline() - - self.update(check_files=check_files) - online = online and self.connection_server.isInternetOnline() self.refreshUpdateTime(valid=online) - return True + self.updateWebsocket(updated=True) - def _considerUpdate_check(self, check_files=False, force=False, log_reason=False): - if not self.isServing(): - return False + # To be called from FileServer + @util.Noparallel(queue=True, ignore_args=True) + def update2(self, check_files=False, verify_files=False): + if len(self.peers) < 50: + self.announce(mode="update") - online = self.connection_server.isInternetOnline() - - run_update = False - msg = None - - if force: - run_update = True - msg = "forcing site update" - elif not online: - run_update = True - msg = "network connection seems broken, trying to update a site to check if the network is up" - elif check_files: - run_update = True - msg = "checking site's files..." - elif not self.isUpdateTimeValid(): - run_update = True - msg = "update time is not valid, updating now..." - - if run_update and log_reason: - self.log.debug(msg) - - return run_update - - def considerUpdate(self, check_files=False, force=False, dry_run=False): - run_update = self._considerUpdate_check(check_files=check_files, force=force) - if run_update and not dry_run: - run_update = self._considerUpdate_realJob(check_files=check_files, force=force) - return run_update + self.update(check_files=check_files, verify_files=verify_files) # Update site by redownload all content.json def redownloadContents(self): From b194eb0f33cd9e1f0c47a125daf6eee4f776c13c Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Thu, 21 Oct 2021 14:37:53 +0700 Subject: [PATCH 082/191] Rename param to reflect its meaning: check_site_on_reconnect -> update_site_on_reconnect --- src/Site/Site.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index aa2bc66b..d1751be3 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -1254,7 +1254,7 @@ class Site(object): return connected - def bringConnections(self, need=1, check_site_on_reconnect=False, pex=True, try_harder=False): + def bringConnections(self, need=1, update_site_on_reconnect=False, pex=True, try_harder=False): connected = len(self.getConnectedPeers()) connected_before = connected @@ -1264,16 +1264,16 @@ class Site(object): connected += self.tryConnectingToMorePeers(more=(need-connected), pex=pex, try_harder=try_harder) self.log.debug( "Connected before: %s, after: %s. Check site: %s." % - (connected_before, connected, check_site_on_reconnect) + (connected_before, connected, update_site_on_reconnect) ) - if check_site_on_reconnect and connected_before == 0 and connected > 0 and self.connection_server.has_internet: + if update_site_on_reconnect and connected_before == 0 and connected > 0 and self.connection_server.has_internet: self.greenlet_manager.spawn(self.update, check_files=False) return connected # Keep connections - def needConnections(self, num=None, check_site_on_reconnect=False, pex=True): + def needConnections(self, num=None, update_site_on_reconnect=False, pex=True): if not self.connection_server.allowsCreatingConnections(): return @@ -1284,14 +1284,14 @@ class Site(object): connected = self.bringConnections( need=need, - check_site_on_reconnect=check_site_on_reconnect, + update_site_on_reconnect=update_site_on_reconnect, pex=pex, try_harder=False) if connected < need: self.greenlet_manager.spawnLater(1.0, self.bringConnections, need=need, - check_site_on_reconnect=check_site_on_reconnect, + update_site_on_reconnect=update_site_on_reconnect, pex=pex, try_harder=True) @@ -1470,7 +1470,7 @@ class Site(object): if not startup: self.cleanupPeers() - self.needConnections(check_site_on_reconnect=True) + self.needConnections(update_site_on_reconnect=True) with gevent.Timeout(10, exception=False): self.announcer.announcePex() From cd3262a2a793046c76878e6b089d116ce6be48af Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Thu, 21 Oct 2021 14:50:54 +0700 Subject: [PATCH 083/191] code style fix in Peer.py --- src/Peer/Peer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index 2e809b26..6e0d57af 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -82,7 +82,7 @@ class Peer(object): # This is to be used to prevent disconnecting from peers when doing # a periodic cleanup. def markProtected(self, interval=60*20): - self.protected = time.time() + interval + self.protected = max(self.protected, time.time() + interval) def isProtected(self): if self.protected > 0: From ddc4861223f0308917b4cd64e41480a55859f349 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Thu, 21 Oct 2021 16:18:40 +0700 Subject: [PATCH 084/191] Site.py: code cleanup --- src/Site/Site.py | 44 ++++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index d1751be3..ba3c9812 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -882,6 +882,28 @@ class Site(object): return False + def getPeersForForegroundPublishing(self, limit): + # Wait for some peers to appear + self.waitForPeers(limit, limit / 2, 10) # some of them... + self.waitForPeers(1, 1, 60) # or at least one... + + peers = self.getConnectedPeers() + random.shuffle(peers) + + # Prefer newer clients. + # Trying to deliver foreground updates to the latest version clients, + # expecting that they have better networking facilities. + # Note: background updates SHOULD NOT discriminate peers by their rev number, + # otherwise it can cause troubles in delivering updates to older versions. + peers = sorted(peers, key=lambda peer: peer.connection.handshake.get("rev", 0) < config.rev - 100) + + # Add more, non-connected peers if necessary + if len(peers) < limit * 2 and len(self.peers) > len(peers): + peers += self.getRecentPeers(limit * 2) + peers = set(peers) + + return peers + # Update content.json on peers @util.Noparallel() def publish(self, limit="default", inner_path="content.json", diffs={}, cb_progress=None): @@ -892,26 +914,16 @@ class Site(object): limit = 5 threads = limit - self.waitForPeers(limit, limit / 2, 10) - self.waitForPeers(1, 1, 60) - - peers = self.getConnectedPeers() - num_connected_peers = len(peers) - - random.shuffle(peers) - # Prefer newer clients - peers = sorted(peers, key=lambda peer: peer.connection.handshake.get("rev", 0) < config.rev - 100) - - # Add more, non-connected peers if necessary - if len(peers) < limit * 2 and len(self.peers) > len(peers): - peers += self.getRecentPeers(limit * 2) - peers = set(peers) + peers = self.getPeersForForegroundPublishing(limit) self.log.info("Publishing %s to %s/%s peers (connected: %s) diffs: %s (%.2fk)..." % ( - inner_path, limit, len(self.peers), num_connected_peers, list(diffs.keys()), float(len(str(diffs))) / 1024 + inner_path, + limit, len(self.peers), len(self.getConnectedPeers()), + list(diffs.keys()), float(len(str(diffs))) / 1024 )) if not peers: + self.addBackgroundPublisher(published=published, limit=limit, inner_path=inner_path, diffs=diffs) return 0 # No peers found event_done = gevent.event.AsyncResult() @@ -925,7 +937,7 @@ class Site(object): if len(published) == 0: gevent.joinall(publishers) # No successful publish, wait for all publisher - # Publish more peers in the backgroup + # Publish to more peers in the background self.log.info( "Published %s to %s peers, publishing to more peers in the background" % (inner_path, len(published)) From c36cba79806726c014eb904dbe7b1554ace6f5e7 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Thu, 21 Oct 2021 18:45:08 +0700 Subject: [PATCH 085/191] Implement new websocket command serverSetPassiveMode --- src/Connection/ConnectionServer.py | 14 ++--- src/File/FileServer.py | 97 ++++++++++++++++++++++-------- src/Ui/UiWebsocket.py | 13 ++++ 3 files changed, 90 insertions(+), 34 deletions(-) diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index ad834c54..bf95a21a 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -142,28 +142,24 @@ class ConnectionServer(object): prev_sizes = {} for i in range(60): sizes = {} - total_size = 0 - for name, pool in self.managed_pools.items(): + for name, pool in list(self.managed_pools.items()): pool.join(timeout=1) size = len(pool) - sizes[name] = size - total_size += size + if size: + sizes[name] = size - if total_size == 0: + if len(sizes) == 0: break if prev_sizes != sizes: s = "" for name, size in sizes.items(): s += "%s pool: %s, " % (name, size) - s += "total: %s" % total_size - self.log.info("Waiting for tasks in managed pools to stop: %s", s) - prev_sizes = sizes - for name, pool in self.managed_pools.items(): + for name, pool in list(self.managed_pools.items()): size = len(pool) if size: self.log.info("Killing %s tasks in %s pool", size, name) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index a0f16b97..5991adf0 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -35,10 +35,15 @@ class FileServer(ConnectionServer): self.recheck_port = True + self.active_mode_thread_pool = gevent.pool.Pool(None) + self.update_pool = gevent.pool.Pool(5) self.update_start_time = 0 self.update_sites_task_next_nr = 1 + self.passive_mode = None + self.active_mode_threads = {} + self.supported_ip_types = ["ipv4"] # Outgoing ip_type support if helper.getIpType(ip) == "ipv6" or self.isIpv6Supported(): self.supported_ip_types.append("ipv6") @@ -64,7 +69,8 @@ class FileServer(ConnectionServer): ConnectionServer.__init__(self, ip, port, self.handleRequest) log.debug("Supported IP types: %s" % self.supported_ip_types) - self.managed_pools["update"] = self.pool + self.managed_pools["active_mode_thread"] = self.active_mode_thread_pool + self.managed_pools["update"] = self.update_pool if ip_type == "dual" and ip == "::": # Also bind to ipv4 addres in dual mode @@ -356,7 +362,7 @@ class FileServer(ConnectionServer): self.sleep(1) self.waitForInternetOnline() - if self.stopping: + if not self.inActiveMode(): break site = self.getSite(site_address) @@ -368,7 +374,7 @@ class FileServer(ConnectionServer): thread = self.update_pool.spawn(self.updateSite, site) - if self.stopping: + if not self.inActiveMode(): break if time.time() - progress_print_time > 60: @@ -387,7 +393,7 @@ class FileServer(ConnectionServer): time_left ) - if self.stopping: + if not self.inActiveMode(): log.info("%s: stopped", task_description) else: log.info("%s: finished in %.2fs", task_description, time.time() - start_time) @@ -414,11 +420,11 @@ class FileServer(ConnectionServer): self.sleep(long_timeout) - while self.running: + while self.inActiveMode(): site = None self.sleep(short_timeout) - if self.stopping: + if not self.inActiveMode(): break site_address, mode = self.peekSiteForVerification() @@ -457,10 +463,10 @@ class FileServer(ConnectionServer): long_timeout = min_long_timeout short_cycle_time_limit = 60 * 2 - while self.running: + while self.inActiveMode(): self.sleep(long_timeout) - if self.stopping: + if not self.inActiveMode(): break start_time = time.time() @@ -477,7 +483,7 @@ class FileServer(ConnectionServer): sites_processed = 0 for site_address in site_addresses: - if self.stopping: + if not self.inActiveMode(): break site = self.getSite(site_address) @@ -536,13 +542,13 @@ class FileServer(ConnectionServer): # performing the update for a random site. It's way better than just # silly pinging a random peer for no profit. log.info("keepAliveThread started") - while self.running: + while self.inActiveMode(): self.waitForInternetOnline() threshold = self.internet_outage_threshold / 2.0 self.sleep(threshold / 2.0) - if self.stopping: + if not self.inActiveMode(): break last_activity_time = max( @@ -570,9 +576,9 @@ class FileServer(ConnectionServer): # and do it more often. log.info("reloadTrackerFilesThread started") interval = 60 * 10 - while self.running: + while self.inActiveMode(): self.sleep(interval) - if self.stopping: + if not self.inActiveMode(): break config.loadTrackersFile() log.info("reloadTrackerFilesThread stopped") @@ -582,9 +588,9 @@ class FileServer(ConnectionServer): log.info("wakeupWatcherThread started") last_time = time.time() last_my_ips = socket.gethostbyname_ex('')[2] - while self.running: + while self.inActiveMode(): self.sleep(30) - if self.stopping: + if not self.inActiveMode(): break is_time_changed = time.time() - max(self.last_request, last_time) > 60 * 3 if is_time_changed: @@ -612,6 +618,53 @@ class FileServer(ConnectionServer): last_my_ips = my_ips log.info("wakeupWatcherThread stopped") + def killActiveModeThreads(self): + for key, thread in list(self.active_mode_threads.items()): + if thread: + if not thread.ready(): + self.log.info("killing %s" % key) + gevent.kill(thread) + del self.active_mode_threads[key] + + def setPassiveMode(self, passive_mode): + if passive_mode: + self.leaveActiveMode(); + else: + self.enterActiveMode(); + + def leaveActiveMode(self): + if self.passive_mode: + return + log.info("passive mode is ON"); + self.passive_mode = True + + def enterActiveMode(self): + if not self.passive_mode and self.passive_mode is not None: + return + log.info("passive mode is OFF"); + self.passive_mode = False + self.killActiveModeThreads() + x = self.active_mode_threads + p = self.active_mode_thread_pool + x["thread_keep_alive"] = p.spawn(self.keepAliveThread) + x["thread_wakeup_watcher"] = p.spawn(self.wakeupWatcherThread) + x["thread_sites_verification"] = p.spawn(self.sitesVerificationThread) + x["thread_reload_tracker_files"] = p.spawn(self.reloadTrackerFilesThread) + x["thread_sites_maintenance_full"] = p.spawn(self.sitesMaintenanceThread, mode="full") + x["thread_sites_maintenance_short"] = p.spawn(self.sitesMaintenanceThread, mode="short") + x["thread_initial_site_updater"] = p.spawn(self.updateSites) + + # Returns True, if an active mode thread should keep going, + # i.e active mode is enabled and the server not going to shutdown + def inActiveMode(self): + if self.passive_mode: + return False + if not self.running: + return False + if self.stopping: + return False + return True + # Bind and start serving sites # If passive_mode is False, FileServer starts the full-featured file serving: # * Checks for updates at startup. @@ -648,16 +701,7 @@ class FileServer(ConnectionServer): # Remove this line when self.sites gets completely unused self.getSites() - if not passive_mode: - thread_keep_alive = self.spawn(self.keepAliveThread) - thread_wakeup_watcher = self.spawn(self.wakeupWatcherThread) - thread_sites_verification = self.spawn(self.sitesVerificationThread) - thread_reload_tracker_files = self.spawn(self.reloadTrackerFilesThread) - thread_sites_maintenance_full = self.spawn(self.sitesMaintenanceThread, mode="full") - thread_sites_maintenance_short = self.spawn(self.sitesMaintenanceThread, mode="short") - - self.sleep(0.1) - self.spawn(self.updateSites) + self.setPassiveMode(passive_mode) ConnectionServer.listen(self) @@ -672,4 +716,7 @@ class FileServer(ConnectionServer): except Exception as err: log.info("Failed at attempt to use upnp to close port: %s" % err) + self.leaveActiveMode(); + gevent.joinall(self.active_mode_threads.values(), timeout=15) + return ConnectionServer.stop(self) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 85fa904d..77e5b12f 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -318,6 +318,7 @@ class UiWebsocket(object): back["updatesite"] = config.updatesite back["dist_type"] = config.dist_type back["lib_verify_best"] = CryptBitcoin.lib_verify_best + back["passive_mode"] = file_server.passive_mode return back def formatAnnouncerInfo(self, site): @@ -1164,6 +1165,18 @@ class UiWebsocket(object): file_server.portCheck() self.response(to, file_server.port_opened) + @flag.admin + @flag.no_multiuser + def actionServerSetPassiveMode(self, to, passive_mode=False): + import main + file_server = main.file_server + file_server.setPassiveMode(passive_mode) + if passive_mode: + self.cmd("notification", ["info", _["Passive mode enabled"], 5000]) + else: + self.cmd("notification", ["info", _["Passive mode disabled"], 5000]) + self.server.updateWebsocket() + @flag.admin @flag.no_multiuser def actionServerShutdown(self, to, restart=False): From 77d2d6937616a65b329bf99ed4a0d12a18ed8e44 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Fri, 22 Oct 2021 00:27:47 +0700 Subject: [PATCH 086/191] Replace `file_server.sites` with `file_server.getSites()` in ChartCollector.py --- plugins/Chart/ChartCollector.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/Chart/ChartCollector.py b/plugins/Chart/ChartCollector.py index 215c603c..ceb16350 100644 --- a/plugins/Chart/ChartCollector.py +++ b/plugins/Chart/ChartCollector.py @@ -27,7 +27,7 @@ class ChartCollector(object): collectors = {} import main file_server = main.file_server - sites = file_server.sites + sites = file_server.getSites() if not sites: return collectors content_db = list(sites.values())[0].content_manager.contents.db @@ -102,7 +102,7 @@ class ChartCollector(object): def getUniquePeers(self): import main - sites = main.file_server.sites + sites = main.file_server.getSites() return set(itertools.chain.from_iterable( [site.peers.keys() for site in sites.values()] )) @@ -171,7 +171,7 @@ class ChartCollector(object): collectors = self.getCollectors() site_collectors = self.getSiteCollectors() import main - sites = main.file_server.sites + sites = main.file_server.getSites() i = 0 while 1: self.collectGlobal(collectors, self.last_values) From e3daa09316bc099e7d7db646f98c1c6a125aaf9e Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Fri, 22 Oct 2021 00:30:42 +0700 Subject: [PATCH 087/191] Improve the file server shutdown logic and display the shutdown progress bar in the UI --- plugins/AnnounceLocal/AnnounceLocalPlugin.py | 4 +- src/Connection/ConnectionServer.py | 55 +++++++--- src/Content/ContentManager.py | 2 +- src/Debug/DebugHook.py | 34 ++++-- src/File/FileServer.py | 9 +- src/Site/Site.py | 107 ++++++++++++------- src/Site/SiteStorage.py | 6 +- src/Ui/UiWebsocket.py | 2 +- src/util/GreenletManager.py | 26 ++++- 9 files changed, 170 insertions(+), 75 deletions(-) diff --git a/plugins/AnnounceLocal/AnnounceLocalPlugin.py b/plugins/AnnounceLocal/AnnounceLocalPlugin.py index b9225966..01202774 100644 --- a/plugins/AnnounceLocal/AnnounceLocalPlugin.py +++ b/plugins/AnnounceLocal/AnnounceLocalPlugin.py @@ -131,10 +131,10 @@ class FileServerPlugin(object): gevent.spawn(self.local_announcer.start) return super(FileServerPlugin, self).start(*args, **kwargs) - def stop(self): + def stop(self, ui_websocket=None): if self.local_announcer: self.local_announcer.stop() - res = super(FileServerPlugin, self).stop() + res = super(FileServerPlugin, self).stop(ui_websocket=ui_websocket) return res diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index bf95a21a..f4358965 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -131,33 +131,52 @@ class ConnectionServer(object): return False self.log.debug("Stopped.") - def stop(self): + def stop(self, ui_websocket=None): self.log.debug("Stopping %s" % self.stream_server) self.stopping = True self.running = False self.stopping_event.set() - self.onStop() + self.onStop(ui_websocket=ui_websocket) - def onStop(self): - prev_sizes = {} - for i in range(60): + def onStop(self, ui_websocket=None): + timeout = 30 + start_time = time.time() + join_quantum = 0.1 + prev_msg = None + while True: + if time.time() >= start_time + timeout: + break + + total_size = 0 sizes = {} - + timestep = 0 for name, pool in list(self.managed_pools.items()): - pool.join(timeout=1) + timestep += join_quantum + pool.join(timeout=join_quantum) size = len(pool) if size: sizes[name] = size + total_size += size if len(sizes) == 0: break - if prev_sizes != sizes: - s = "" - for name, size in sizes.items(): - s += "%s pool: %s, " % (name, size) - self.log.info("Waiting for tasks in managed pools to stop: %s", s) - prev_sizes = sizes + if timestep < 1: + time.sleep(1 - timestep) + + # format message + s = "" + for name, size in sizes.items(): + s += "%s pool: %s, " % (name, size) + msg = "Waiting for tasks in managed pools to stop: %s" % s + # Prevent flooding to log + if msg != prev_msg: + prev_msg = msg + self.log.info("%s", msg) + + percent = 100 * (time.time() - start_time) / timeout + msg = "File Server: waiting for %s tasks to stop" % total_size + self.sendShutdownProgress(ui_websocket, msg, percent) for name, pool in list(self.managed_pools.items()): size = len(pool) @@ -165,12 +184,20 @@ class ConnectionServer(object): self.log.info("Killing %s tasks in %s pool", size, name) pool.kill() + self.sendShutdownProgress(ui_websocket, "File Server stopped. Now to exit.", 100) + if self.thread_checker: gevent.kill(self.thread_checker) self.thread_checker = None if self.stream_server: self.stream_server.stop() + def sendShutdownProgress(self, ui_websocket, message, progress): + if not ui_websocket: + return + ui_websocket.cmd("progress", ["shutdown", message, progress]) + time.sleep(0.01) + # Sleeps the specified amount of time or until ConnectionServer is stopped def sleep(self, t): if t: @@ -178,7 +205,7 @@ class ConnectionServer(object): else: time.sleep(t) - # Spawns a thread that will be waited for on server being stooped (and killed after a timeout) + # Spawns a thread that will be waited for on server being stopped (and killed after a timeout) def spawn(self, *args, **kwargs): thread = self.thread_pool.spawn(*args, **kwargs) return thread diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 7d1263ef..c6a64750 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -239,7 +239,7 @@ class ContentManager(object): if num_removed_bad_files > 0: self.site.worker_manager.removeSolvedFileTasks(mark_as_good=False) - gevent.spawn(self.site.update, since=0) + self.site.spawn(self.site.update, since=0) self.log.debug("Archived removed contents: %s, removed bad files: %s" % (num_removed_contents, num_removed_bad_files)) diff --git a/src/Debug/DebugHook.py b/src/Debug/DebugHook.py index d100a3b8..4a5bfd75 100644 --- a/src/Debug/DebugHook.py +++ b/src/Debug/DebugHook.py @@ -11,20 +11,34 @@ from . import Debug last_error = None -def shutdown(reason="Unknown"): - logging.info("Shutting down (reason: %s)..." % reason) +thread_shutdown = None + +def shutdownThread(): import main - if "file_server" in dir(main): - try: - gevent.spawn(main.file_server.stop) - if "ui_server" in dir(main): - gevent.spawn(main.ui_server.stop) - except Exception as err: - print("Proper shutdown error: %s" % err) - sys.exit(0) + try: + if "file_server" in dir(main): + thread = gevent.spawn(main.file_server.stop) + thread.join(timeout=60) + if "ui_server" in dir(main): + thread = gevent.spawn(main.ui_server.stop) + thread.join(timeout=10) + except Exception as err: + print("Error in shutdown thread: %s" % err) + sys.exit(0) else: sys.exit(0) + +def shutdown(reason="Unknown"): + global thread_shutdown + logging.info("Shutting down (reason: %s)..." % reason) + try: + if not thread_shutdown: + thread_shutdown = gevent.spawn(shutdownThread) + except Exception as err: + print("Proper shutdown error: %s" % err) + sys.exit(0) + # Store last error, ignore notify, allow manual error logging def handleError(*args, **kwargs): global last_error diff --git a/src/File/FileServer.py b/src/File/FileServer.py index 5991adf0..6eb1ec5b 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -36,6 +36,7 @@ class FileServer(ConnectionServer): self.recheck_port = True self.active_mode_thread_pool = gevent.pool.Pool(None) + self.site_pool = gevent.pool.Pool(None) self.update_pool = gevent.pool.Pool(5) self.update_start_time = 0 @@ -71,6 +72,7 @@ class FileServer(ConnectionServer): self.managed_pools["active_mode_thread"] = self.active_mode_thread_pool self.managed_pools["update"] = self.update_pool + self.managed_pools["site"] = self.site_pool if ip_type == "dual" and ip == "::": # Also bind to ipv4 addres in dual mode @@ -707,7 +709,7 @@ class FileServer(ConnectionServer): log.info("Stopped.") - def stop(self): + def stop(self, ui_websocket=None): if self.running and self.portchecker.upnp_port_opened: log.debug('Closing port %d' % self.port) try: @@ -716,7 +718,4 @@ class FileServer(ConnectionServer): except Exception as err: log.info("Failed at attempt to use upnp to close port: %s" % err) - self.leaveActiveMode(); - gevent.joinall(self.active_mode_threads.values(), timeout=15) - - return ConnectionServer.stop(self) + return ConnectionServer.stop(self, ui_websocket=ui_websocket) diff --git a/src/Site/Site.py b/src/Site/Site.py index ba3c9812..5b228ff9 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -175,25 +175,8 @@ class Site(object): self.fzs_count = random.randint(0, self.fzs_range / 4) self.fzs_timestamp = 0 - self.content = None # Load content.json - self.peers = {} # Key: ip:port, Value: Peer.Peer - self.peers_recent = collections.deque(maxlen=150) - self.peer_blacklist = SiteManager.peer_blacklist # Ignore this peers (eg. myself) - self.greenlet_manager = GreenletManager.GreenletManager() # Running greenlets - self.worker_manager = WorkerManager(self) # Handle site download from other peers - self.bad_files = {} # SHA check failed files, need to redownload {"inner.content": 1} (key: file, value: failed accept) - self.content_updated = None # Content.js update time - self.last_online_update = 0 - self.startup_announce_done = 0 - self.notifications = [] # Pending notifications displayed once on page load [error|ok|info, message, timeout] - self.page_requested = False # Page viewed in browser - self.websockets = [] # Active site websocket connections - + ############################################## self.connection_server = None - self.loadSettings(settings) # Load settings from sites.json - self.storage = SiteStorage(self, allow_create=allow_create) # Save and load site files - self.content_manager = ContentManager(self) - self.content_manager.loadContents() # Load content.json files if "main" in sys.modules: # import main has side-effects, breaks tests import main if "file_server" in dir(main): # Use global file server by default if possible @@ -203,6 +186,26 @@ class Site(object): self.connection_server = main.file_server else: self.connection_server = FileServer() + ############################################## + + self.content = None # Load content.json + self.peers = {} # Key: ip:port, Value: Peer.Peer + self.peers_recent = collections.deque(maxlen=150) + self.peer_blacklist = SiteManager.peer_blacklist # Ignore this peers (eg. myself) + self.greenlet_manager = GreenletManager.GreenletManager(self.connection_server.site_pool) # Running greenlets + self.worker_manager = WorkerManager(self) # Handle site download from other peers + self.bad_files = {} # SHA check failed files, need to redownload {"inner.content": 1} (key: file, value: failed accept) + self.content_updated = None # Content.js update time + self.last_online_update = 0 + self.startup_announce_done = 0 + self.notifications = [] # Pending notifications displayed once on page load [error|ok|info, message, timeout] + self.page_requested = False # Page viewed in browser + self.websockets = [] # Active site websocket connections + + self.loadSettings(settings) # Load settings from sites.json + self.storage = SiteStorage(self, allow_create=allow_create) # Save and load site files + self.content_manager = ContentManager(self) + self.content_manager.loadContents() # Load content.json files self.announcer = SiteAnnouncer(self) # Announce and get peer list from other nodes @@ -275,14 +278,32 @@ class Site(object): SiteManager.site_manager.load(False) SiteManager.site_manager.saveDelayed() + # Returns True if any site-related activity should be interrupted + # due to connection server being stooped or site being deleted + def isStopping(self): + return self.connection_server.stopping or self.settings.get("deleting", False) + + # Returns False if any network activity for the site should not happen def isServing(self): if config.offline: return False - elif self.connection_server.stopping: + elif self.isStopping(): return False else: return self.settings["serving"] + # Spawns a thread that will be waited for on server being stopped (and killed after a timeout). + # Short cut to self.greenlet_manager.spawn() + def spawn(self, *args, **kwargs): + thread = self.greenlet_manager.spawn(*args, **kwargs) + return thread + + # Spawns a thread that will be waited for on server being stopped (and killed after a timeout). + # Short cut to self.greenlet_manager.spawnLater() + def spawnLater(self, *args, **kwargs): + thread = self.greenlet_manager.spawnLater(*args, **kwargs) + return thread + def getSettingsCache(self): back = {} back["bad_files"] = self.bad_files @@ -418,7 +439,7 @@ class Site(object): # Optionals files if inner_path == "content.json": - gevent.spawn(self.updateHashfield) + self.spawn(self.updateHashfield) for file_relative_path in list(self.content_manager.contents[inner_path].get("files_optional", {}).keys()): file_inner_path = content_inner_dir + file_relative_path @@ -437,7 +458,7 @@ class Site(object): include_threads = [] for file_relative_path in list(self.content_manager.contents[inner_path].get("includes", {}).keys()): file_inner_path = content_inner_dir + file_relative_path - include_thread = gevent.spawn(self.downloadContent, file_inner_path, download_files=download_files, peer=peer) + include_thread = self.spawn(self.downloadContent, file_inner_path, download_files=download_files, peer=peer) include_threads.append(include_thread) if config.verbose: @@ -517,9 +538,9 @@ class Site(object): ) if self.isAddedRecently(): - gevent.spawn(self.announce, mode="start", force=True) + self.spawn(self.announce, mode="start", force=True) else: - gevent.spawn(self.announce, mode="update") + self.spawn(self.announce, mode="update") if check_size: # Check the size first valid = self.downloadContent("content.json", download_files=False) # Just download content.json files @@ -615,7 +636,7 @@ class Site(object): self.log.info("CheckModifications: %s: %s > %s" % ( inner_path, res["modified_files"][inner_path], my_modified.get(inner_path, 0) )) - t = gevent.spawn(self.pooledDownloadContent, modified_contents, only_if_bad=True) + t = self.spawn(self.pooledDownloadContent, modified_contents, only_if_bad=True) threads.append(t) if send_back: @@ -628,7 +649,7 @@ class Site(object): self.log.info("CheckModifications: %s: %s < %s" % ( inner_path, res["modified_files"][inner_path], my_modified.get(inner_path, 0) )) - gevent.spawn(self.publisher, inner_path, [peer], [], 1) + self.spawn(self.publisher, inner_path, [peer], [], 1) self.log.debug("CheckModifications: Waiting for %s pooledDownloadContent" % len(threads)) gevent.joinall(threads) @@ -685,7 +706,7 @@ class Site(object): updaters = [] for i in range(updater_limit): - updaters.append(gevent.spawn(self.updater, peers_try, queried, need_queries, since)) + updaters.append(self.spawn(self.updater, peers_try, queried, need_queries, since)) for r in range(10): gevent.joinall(updaters, timeout=5+r) @@ -738,13 +759,17 @@ class Site(object): elif check_files: self.updateWebsocket(checking=True) - if verify_files: - self.storage.updateBadFiles(quick_check=False) - self.settings["check_files_timestamp"] = time.time() - self.settings["verify_files_timestamp"] = time.time() - elif check_files: - self.storage.updateBadFiles(quick_check=True) # Quick check and mark bad files based on file size - self.settings["check_files_timestamp"] = time.time() + if check_files: + if verify_files: + self.storage.updateBadFiles(quick_check=False) # Full-featured checksum verification + else: + self.storage.updateBadFiles(quick_check=True) # Quick check and mark bad files based on file size + # Don't update the timestamps in case of the application being shut down, + # so we can make another try next time. + if not self.isStopping(): + self.settings["check_files_timestamp"] = time.time() + if verify_files: + self.settings["verify_files_timestamp"] = time.time() if not self.isServing(): self.updateWebsocket(updated=True) @@ -766,7 +791,7 @@ class Site(object): if self.bad_files: self.log.debug("Bad files: %s" % self.bad_files) - gevent.spawn(self.retryBadFiles, force=True) + self.spawn(self.retryBadFiles, force=True) if len(queried) == 0: # Failed to query modifications @@ -856,7 +881,7 @@ class Site(object): background_publisher = BackgroundPublisher(self, published=published, limit=limit, inner_path=inner_path, diffs=diffs) self.background_publishers[inner_path] = background_publisher - gevent.spawn(background_publisher.process) + self.spawn(background_publisher.process) def processBackgroundPublishers(self): with self.background_publishers_lock: @@ -928,7 +953,7 @@ class Site(object): event_done = gevent.event.AsyncResult() for i in range(min(len(peers), limit, threads)): - publisher = gevent.spawn(self.publisher, inner_path, peers, published, limit, diffs, event_done, cb_progress) + publisher = self.spawn(self.publisher, inner_path, peers, published, limit, diffs, event_done, cb_progress) publishers.append(publisher) event_done.get() # Wait for done @@ -946,7 +971,7 @@ class Site(object): self.addBackgroundPublisher(published=published, limit=limit, inner_path=inner_path, diffs=diffs) # Send my hashfield to every connected peer if changed - gevent.spawn(self.sendMyHashfield, 100) + self.spawn(self.sendMyHashfield, 100) return len(published) @@ -1109,7 +1134,7 @@ class Site(object): if not self.content_manager.contents.get("content.json"): # No content.json, download it first! self.log.debug("Need content.json first (inner_path: %s, priority: %s)" % (inner_path, priority)) if priority > 0: - gevent.spawn(self.announce) + self.spawn(self.announce) if inner_path != "content.json": # Prevent double download task = self.worker_manager.addTask("content.json", peer) task["evt"].get() @@ -1508,6 +1533,9 @@ class Site(object): # Send hashfield to peers def sendMyHashfield(self, limit=5): + if not self.isServing(): + return False + if not self.content_manager.hashfield: # No optional files return False @@ -1525,6 +1553,9 @@ class Site(object): # Update hashfield def updateHashfield(self, limit=5): + if not self.isServing(): + return False + # Return if no optional files if not self.content_manager.hashfield and not self.content_manager.has_optional_files: return False diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index 4e532788..b89aedbf 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -375,7 +375,7 @@ class SiteStorage(object): # Reopen DB to check changes if self.has_db: self.closeDb("New dbschema") - gevent.spawn(self.getDb) + self.site.spawn(self.getDb) elif not config.disable_db and should_load_to_db and self.has_db: # Load json file to db if config.verbose: self.log.debug("Loading json file to db: %s (file: %s)" % (inner_path, file)) @@ -458,6 +458,10 @@ class SiteStorage(object): i += 1 if i % 50 == 0: time.sleep(0.001) # Context switch to avoid gevent hangs + + if self.site.isStopping(): + break + if not os.path.isfile(self.getPath(content_inner_path)): # Missing content.json file back["num_content_missing"] += 1 self.log.debug("[MISSING] %s" % content_inner_path) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 77e5b12f..7e68eb34 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -1187,7 +1187,7 @@ class UiWebsocket(object): return False if restart: main.restart_after_shutdown = True - main.file_server.stop() + main.file_server.stop(ui_websocket=self) main.ui_server.stop() if restart: diff --git a/src/util/GreenletManager.py b/src/util/GreenletManager.py index e024233d..d711d09a 100644 --- a/src/util/GreenletManager.py +++ b/src/util/GreenletManager.py @@ -3,17 +3,37 @@ from Debug import Debug class GreenletManager: - def __init__(self): + # pool is either gevent.pool.Pool or GreenletManager. + # if pool is None, new gevent.pool.Pool() is created. + def __init__(self, pool=None): self.greenlets = set() + if not pool: + pool = gevent.pool.Pool(None) + self.pool = pool + + def _spawn_later(self, seconds, *args, **kwargs): + # If pool is another GreenletManager, delegate to it. + if hasattr(self.pool, 'spawnLater'): + return self.pool.spawnLater(seconds, *args, **kwargs) + + # There's gevent.spawn_later(), but there isn't gevent.pool.Pool.spawn_later(). + # Doing manually. + greenlet = self.pool.greenlet_class(*args, **kwargs) + self.pool.add(greenlet) + greenlet.start_later(seconds) + return greenlet + + def _spawn(self, *args, **kwargs): + return self.pool.spawn(*args, **kwargs) def spawnLater(self, *args, **kwargs): - greenlet = gevent.spawn_later(*args, **kwargs) + greenlet = self._spawn_later(*args, **kwargs) greenlet.link(lambda greenlet: self.greenlets.remove(greenlet)) self.greenlets.add(greenlet) return greenlet def spawn(self, *args, **kwargs): - greenlet = gevent.spawn(*args, **kwargs) + greenlet = self._spawn(*args, **kwargs) greenlet.link(lambda greenlet: self.greenlets.remove(greenlet)) self.greenlets.add(greenlet) return greenlet From 19b840defde0585a50cbfb08895d06f7c660825c Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Fri, 22 Oct 2021 02:59:28 +0700 Subject: [PATCH 088/191] Implement new websocket command serverSetOfflineMode --- src/Connection/ConnectionServer.py | 16 +++++- src/File/FileServer.py | 87 ++++++++++++++++++------------ src/Site/Site.py | 3 +- src/Ui/UiWebsocket.py | 26 ++++++--- 4 files changed, 89 insertions(+), 43 deletions(-) diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index f4358965..e7951321 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -479,15 +479,27 @@ class ConnectionServer(object): self.had_external_incoming = False self.log.info("Internet offline") + def setOfflineMode(self, offline_mode): + if config.offline == offline_mode: + return + config.offline = offline_mode # Yep, awkward + if offline_mode: + self.log.info("offline mode is ON") + else: + self.log.info("offline mode is OFF") + + def isOfflineMode(self): + return config.offline + def allowsCreatingConnections(self): - if config.offline: + if self.isOfflineMode(): return False if self.stopping: return False return True def allowsAcceptingConnections(self): - if config.offline: + if self.isOfflineMode(): return False if self.stopping: return False diff --git a/src/File/FileServer.py b/src/File/FileServer.py index 6eb1ec5b..e432e4a7 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -43,8 +43,10 @@ class FileServer(ConnectionServer): self.update_sites_task_next_nr = 1 self.passive_mode = None + self.active_mode = None self.active_mode_threads = {} + self.supported_ip_types = ["ipv4"] # Outgoing ip_type support if helper.getIpType(ip) == "ipv6" or self.isIpv6Supported(): self.supported_ip_types.append("ipv6") @@ -196,7 +198,7 @@ class FileServer(ConnectionServer): FileRequest = imp.load_source("FileRequest", "src/File/FileRequest.py").FileRequest def portCheck(self): - if config.offline: + if self.isOfflineMode(): log.info("Offline mode: port check disabled") res = {"ipv4": None, "ipv6": None} self.port_opened = res @@ -276,7 +278,7 @@ class FileServer(ConnectionServer): # Returns True if we've spent some time waiting for Internet # Returns None if FileServer is stopping or the Offline mode is enabled def waitForInternetOnline(self): - if config.offline or self.stopping: + if self.isOfflineMode() or self.stopping: return None if self.isInternetOnline(): @@ -284,7 +286,7 @@ class FileServer(ConnectionServer): while not self.isInternetOnline(): self.sleep(30) - if config.offline or self.stopping: + if self.isOfflineMode() or self.stopping: return None if self.isInternetOnline(): break @@ -364,7 +366,7 @@ class FileServer(ConnectionServer): self.sleep(1) self.waitForInternetOnline() - if not self.inActiveMode(): + if not self.isActiveMode(): break site = self.getSite(site_address) @@ -376,7 +378,7 @@ class FileServer(ConnectionServer): thread = self.update_pool.spawn(self.updateSite, site) - if not self.inActiveMode(): + if not self.isActiveMode(): break if time.time() - progress_print_time > 60: @@ -395,7 +397,7 @@ class FileServer(ConnectionServer): time_left ) - if not self.inActiveMode(): + if not self.isActiveMode(): log.info("%s: stopped", task_description) else: log.info("%s: finished in %.2fs", task_description, time.time() - start_time) @@ -422,11 +424,11 @@ class FileServer(ConnectionServer): self.sleep(long_timeout) - while self.inActiveMode(): + while self.isActiveMode(): site = None self.sleep(short_timeout) - if not self.inActiveMode(): + if not self.isActiveMode(): break site_address, mode = self.peekSiteForVerification() @@ -465,10 +467,10 @@ class FileServer(ConnectionServer): long_timeout = min_long_timeout short_cycle_time_limit = 60 * 2 - while self.inActiveMode(): + while self.isActiveMode(): self.sleep(long_timeout) - if not self.inActiveMode(): + if not self.isActiveMode(): break start_time = time.time() @@ -485,7 +487,7 @@ class FileServer(ConnectionServer): sites_processed = 0 for site_address in site_addresses: - if not self.inActiveMode(): + if not self.isActiveMode(): break site = self.getSite(site_address) @@ -544,13 +546,13 @@ class FileServer(ConnectionServer): # performing the update for a random site. It's way better than just # silly pinging a random peer for no profit. log.info("keepAliveThread started") - while self.inActiveMode(): + while self.isActiveMode(): self.waitForInternetOnline() threshold = self.internet_outage_threshold / 2.0 self.sleep(threshold / 2.0) - if not self.inActiveMode(): + if not self.isActiveMode(): break last_activity_time = max( @@ -578,9 +580,9 @@ class FileServer(ConnectionServer): # and do it more often. log.info("reloadTrackerFilesThread started") interval = 60 * 10 - while self.inActiveMode(): + while self.isActiveMode(): self.sleep(interval) - if not self.inActiveMode(): + if not self.isActiveMode(): break config.loadTrackersFile() log.info("reloadTrackerFilesThread stopped") @@ -590,9 +592,9 @@ class FileServer(ConnectionServer): log.info("wakeupWatcherThread started") last_time = time.time() last_my_ips = socket.gethostbyname_ex('')[2] - while self.inActiveMode(): + while self.isActiveMode(): self.sleep(30) - if not self.inActiveMode(): + if not self.isActiveMode(): break is_time_changed = time.time() - max(self.last_request, last_time) > 60 * 3 if is_time_changed: @@ -620,31 +622,47 @@ class FileServer(ConnectionServer): last_my_ips = my_ips log.info("wakeupWatcherThread stopped") + def setOfflineMode(self, offline_mode): + ConnectionServer.setOfflineMode(self, offline_mode) + self.setupActiveMode() + + def setPassiveMode(self, passive_mode): + if self.passive_mode == passive_mode: + return + self.passive_mode = passive_mode + if self.passive_mode: + log.info("passive mode is ON"); + else: + log.info("passive mode is OFF"); + self.setupActiveMode() + + def isPassiveMode(self): + return self.passive_mode + + def setupActiveMode(self): + active_mode = (not self.passive_mode) and (not self.isOfflineMode()) + if self.active_mode == active_mode: + return + self.active_mode = active_mode + if self.active_mode: + log.info("active mode is ON"); + self.enterActiveMode(); + else: + log.info("active mode is OFF"); + self.leaveActiveMode(); + def killActiveModeThreads(self): for key, thread in list(self.active_mode_threads.items()): if thread: if not thread.ready(): - self.log.info("killing %s" % key) + log.info("killing %s" % key) gevent.kill(thread) del self.active_mode_threads[key] - def setPassiveMode(self, passive_mode): - if passive_mode: - self.leaveActiveMode(); - else: - self.enterActiveMode(); - def leaveActiveMode(self): - if self.passive_mode: - return - log.info("passive mode is ON"); - self.passive_mode = True + pass def enterActiveMode(self): - if not self.passive_mode and self.passive_mode is not None: - return - log.info("passive mode is OFF"); - self.passive_mode = False self.killActiveModeThreads() x = self.active_mode_threads p = self.active_mode_thread_pool @@ -658,8 +676,9 @@ class FileServer(ConnectionServer): # Returns True, if an active mode thread should keep going, # i.e active mode is enabled and the server not going to shutdown - def inActiveMode(self): - if self.passive_mode: + def isActiveMode(self): + self.setupActiveMode() + if not self.active_mode: return False if not self.running: return False diff --git a/src/Site/Site.py b/src/Site/Site.py index 5b228ff9..31acea03 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -285,7 +285,7 @@ class Site(object): # Returns False if any network activity for the site should not happen def isServing(self): - if config.offline: + if self.connection_server.isOfflineMode(): return False elif self.isStopping(): return False @@ -1375,6 +1375,7 @@ class Site(object): # Return: Recently found peers def getRecentPeers(self, need_num): + need_num = int(need_num) found = list(set(self.peers_recent)) self.log.debug( "Recent peers %s of %s (need: %s)" % diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 7e68eb34..80d53b45 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -1170,12 +1170,26 @@ class UiWebsocket(object): def actionServerSetPassiveMode(self, to, passive_mode=False): import main file_server = main.file_server - file_server.setPassiveMode(passive_mode) - if passive_mode: - self.cmd("notification", ["info", _["Passive mode enabled"], 5000]) - else: - self.cmd("notification", ["info", _["Passive mode disabled"], 5000]) - self.server.updateWebsocket() + if file_server.isPassiveMode() != passive_mode: + file_server.setPassiveMode(passive_mode) + if file_server.isPassiveMode(): + self.cmd("notification", ["info", _["Passive mode enabled"], 5000]) + else: + self.cmd("notification", ["info", _["Passive mode disabled"], 5000]) + self.server.updateWebsocket() + + @flag.admin + @flag.no_multiuser + def actionServerSetOfflineMode(self, to, offline_mode=False): + import main + file_server = main.file_server + if file_server.isOfflineMode() != offline_mode: + file_server.setOfflineMode(offline_mode) + if file_server.isOfflineMode(): + self.cmd("notification", ["info", _["Offline mode enabled"], 5000]) + else: + self.cmd("notification", ["info", _["Offline mode disabled"], 5000]) + self.server.updateWebsocket() @flag.admin @flag.no_multiuser From 1ef129bdf9193cbbc9aa1d0f595345e6453eff52 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Fri, 22 Oct 2021 14:38:05 +0700 Subject: [PATCH 089/191] Fix "changed size during iteration" in verifyFiles() --- src/Site/SiteStorage.py | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index b89aedbf..97f720dc 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -439,6 +439,8 @@ class SiteStorage(object): return inner_path # Verify all files sha512sum using content.json + # The result may not be accurate if self.site.isStopping(). + # verifyFiles() return immediately in that case. def verifyFiles(self, quick_check=False, add_optional=False, add_changed=True): bad_files = [] back = defaultdict(int) @@ -446,14 +448,44 @@ class SiteStorage(object): i = 0 self.log.debug("Verifing files...") - notificator = VerifyFiles_Notificator(self.site, quick_check) - if not self.site.content_manager.contents.get("content.json"): # No content.json, download it first self.log.debug("VerifyFile content.json not exists") self.site.needFile("content.json", update=True) # Force update to fix corrupt file self.site.content_manager.loadContent() # Reload content.json - for content_inner_path, content in self.site.content_manager.contents.iteritems(): - notificator.inc() + + # Trying to read self.site.content_manager.contents without being stuck + # on reading the long file list and also without getting + # "RuntimeError: dictionary changed size during iteration" + # We can't use just list(iteritems()) since it loads all the contents files + # at once and gets unresponsive. + contents = {} + notificator = None + tries = 0 + max_tries = 40 + stop = False + while not stop: + try: + contents = {} + notificator = VerifyFiles_Notificator(self.site, quick_check) + for content_inner_path, content in self.site.content_manager.contents.iteritems(): + notificator.inc() + contents[content_inner_path] = content + if self.site.isStopping(): + stop = True + break + stop = True + except RuntimeError as err: + if "changed size during iteration" in str(err): + tries += 1 + if tries >= max_tries: + self.log.info("contents.json file list changed during iteration. %s tries done. Giving up.", tries) + stop = True + self.log.info("contents.json file list changed during iteration. Trying again... (%s)", tries) + time.sleep(2 * tries) + else: + stop = True + + for content_inner_path, content in contents.items(): back["num_content"] += 1 i += 1 if i % 50 == 0: From 1b68182a76e7ab9fbbb8ed82a266d9b55dfab391 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Fri, 22 Oct 2021 17:18:24 +0700 Subject: [PATCH 090/191] FileServer: don't schedule multiple updates for the same site in parallel --- src/File/FileServer.py | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index e432e4a7..ab669090 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -313,13 +313,34 @@ class FileServer(ConnectionServer): log.debug("Checking randomly chosen site: %s", site.address_short) - self.updateSite(site) + self.spawnUpdateSite(site).join() def updateSite(self, site, check_files=False, verify_files=False): if not site: return False return site.update2(check_files=check_files, verify_files=verify_files) + def spawnUpdateSite(self, site, check_files=False, verify_files=False): + thread = self.update_pool.spawn(self.updateSite, site, + check_files=check_files, verify_files=verify_files) + thread.site_address = site.address + return thread + + def siteIsInUpdatePool(self, site_address): + while True: + restart = False + for thread in list(iter(self.update_pool)): + if not thread.site_address: + # Possible race condition in assigning thread.site_address in spawnUpdateSite() + # Trying again. + self.sleep(0.1) + restart = True + break + if thread.site_address == site_address: + return True + if not restart: + return False + def invalidateUpdateTime(self, invalid_interval): for address in self.getSiteAddresses(): site = self.getSite(address) @@ -370,13 +391,13 @@ class FileServer(ConnectionServer): break site = self.getSite(site_address) - if not site or site.isUpdateTimeValid(): + if not site or site.isUpdateTimeValid() or self.siteIsInUpdatePool(site_address): sites_skipped += 1 continue sites_processed += 1 - thread = self.update_pool.spawn(self.updateSite, site) + thread = self.spawnUpdateSite(site) if not self.isActiveMode(): break @@ -436,6 +457,12 @@ class FileServer(ConnectionServer): self.sleep(long_timeout) continue + while self.siteIsInUpdatePool(site_address) and self.isActiveMode(): + self.sleep(1) + + if not self.isActiveMode(): + break + site = self.getSite(site_address) if not site: continue @@ -451,7 +478,7 @@ class FileServer(ConnectionServer): log.info("running <%s> for %s" % (mode, site.address_short)) - thread = self.update_pool.spawn(self.updateSite, site, + thread = self.spawnUpdateSite(site, check_files=check_files, verify_files=verify_files) log.info("sitesVerificationThread stopped") From fe24e17baa24dd364943702e8b4f701b4407f39c Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Sat, 23 Oct 2021 21:40:41 +0700 Subject: [PATCH 091/191] Fix the prev commit --- src/File/FileServer.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index ab669090..16afab24 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -291,7 +291,7 @@ class FileServer(ConnectionServer): if self.isInternetOnline(): break if len(self.update_pool) == 0: - thread = self.update_pool.spawn(self.updateRandomSite) + thread = self.thread_pool.spawn(self.updateRandomSite) thread.join() self.recheckPort() @@ -330,13 +330,14 @@ class FileServer(ConnectionServer): while True: restart = False for thread in list(iter(self.update_pool)): - if not thread.site_address: + thread_site_address = getattr(thread, 'site_address', None) + if not thread_site_address: # Possible race condition in assigning thread.site_address in spawnUpdateSite() # Trying again. self.sleep(0.1) restart = True break - if thread.site_address == site_address: + if thread_site_address == site_address: return True if not restart: return False @@ -397,6 +398,7 @@ class FileServer(ConnectionServer): sites_processed += 1 + log.info("running for %s" % site.address_short) thread = self.spawnUpdateSite(site) if not self.isActiveMode(): From e612f9363118d5e53e8597a369195f100a49b9b9 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Sat, 23 Oct 2021 21:41:14 +0700 Subject: [PATCH 092/191] Spawn message loops for outgoing connections in a sepatare pool managed by ConnectionServer --- src/Connection/Connection.py | 2 +- src/Connection/ConnectionServer.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 4a7c6ab9..398a4cc3 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -167,7 +167,7 @@ class Connection(object): # Detect protocol event_connected = self.event_connected self.send({"cmd": "handshake", "req_id": 0, "params": self.getHandshakeInfo()}) - gevent.spawn(self.messageLoop) + self.server.outgoing_pool.spawn(self.messageLoop) connect_res = event_connected.get() # Wait for handshake if self.sock: self.sock.settimeout(timeout_before) diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index e7951321..03a93daa 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -79,6 +79,9 @@ class ConnectionServer(object): self.pool = Pool(500) # do not accept more than 500 connections self.managed_pools["incoming"] = self.pool + self.outgoing_pool = Pool(None) + self.managed_pools["outgoing"] = self.outgoing_pool + # Bittorrent style peerid self.peer_id = "-UT3530-%s" % CryptHash.random(12, "base64") From b4f94e50221a5cb0612d1a63fb3c576284f5637b Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Sat, 23 Oct 2021 21:44:41 +0700 Subject: [PATCH 093/191] Make use of waitForPeers() when running FileServer-driven update() --- src/Site/Site.py | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 31acea03..881f7d7c 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -812,6 +812,9 @@ class Site(object): def update2(self, check_files=False, verify_files=False): if len(self.peers) < 50: self.announce(mode="update") + self.waitForPeers(5, 5, 30); + self.waitForPeers(2, 2, 30); + self.waitForPeers(1, 1, 60); self.update(check_files=check_files, verify_files=verify_files) @@ -891,21 +894,36 @@ class Site(object): background_publisher.finalize() del self.background_publishers[inner_path] - def waitForPeers(self, num_peers, num_connected_peers, time_limit): + def waitForPeers_realJob(self, need_nr_peers, need_nr_connected_peers, time_limit): start_time = time.time() for _ in range(time_limit): - if len(self.peers) >= num_peers and len(self.getConnectedPeers()) >= num_connected_peers: - return True + nr_connected_peers = len(self.getConnectedPeers()) + nr_peers = len(self.peers) + if nr_peers >= need_nr_peers and nr_connected_peers >= need_nr_connected_peers: + return nr_connected_peers + self.updateWebsocket(connecting_to_peers=nr_connected_peers) self.announce(mode="more", force=True) + if not self.isServing(): + return nr_connected_peers for wait in range(10): - self.needConnections(num=num_connected_peers) + self.needConnections(num=need_nr_connected_peers) time.sleep(2) - if len(self.peers) >= num_peers and len(self.getConnectedPeers()) >= num_connected_peers: - return True + nr_connected_peers = len(self.getConnectedPeers()) + nr_peers = len(self.peers) + self.updateWebsocket(connecting_to_peers=nr_connected_peers) + if not self.isServing(): + return nr_connected_peers + if nr_peers >= need_nr_peers and nr_connected_peers >= need_nr_connected_peers: + return nr_connected_peers if time.time() - start_time > time_limit: - return True + return nr_connected_peers - return False + return nr_connected_peers + + def waitForPeers(self, need_nr_peers, need_nr_connected_peers, time_limit): + nr_connected_peers = self.waitForPeers_realJob(need_nr_peers, need_nr_connected_peers, time_limit) + self.updateWebsocket(connected_to_peers=nr_connected_peers) + return nr_connected_peers def getPeersForForegroundPublishing(self, limit): # Wait for some peers to appear From b512c54f75b4eb9eef9479a582e637be54bdd898 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Sun, 24 Oct 2021 23:53:08 +0700 Subject: [PATCH 094/191] Implement new logic for waiting for connected peers when updating or publishing a site --- plugins/TrackerShare/TrackerSharePlugin.py | 2 +- src/File/FileServer.py | 30 ++- src/Peer/Peer.py | 10 +- src/Site/Site.py | 204 +++++++++---------- src/Site/SiteHelpers.py | 220 +++++++++++++++++++++ 5 files changed, 331 insertions(+), 135 deletions(-) create mode 100644 src/Site/SiteHelpers.py diff --git a/plugins/TrackerShare/TrackerSharePlugin.py b/plugins/TrackerShare/TrackerSharePlugin.py index 99ebf692..4fe888fd 100644 --- a/plugins/TrackerShare/TrackerSharePlugin.py +++ b/plugins/TrackerShare/TrackerSharePlugin.py @@ -462,7 +462,7 @@ if "tracker_storage" not in locals(): class SiteAnnouncerPlugin(object): def getTrackers(self): tracker_storage.setSiteAnnouncer(self) - tracker_storage.checkDiscoveringTrackers(self.site.getConnectedPeers()) + tracker_storage.checkDiscoveringTrackers(self.site.getConnectedPeers(onlyFullyConnected=True)) trackers = super(SiteAnnouncerPlugin, self).getTrackers() shared_trackers = list(tracker_storage.getTrackers().keys()) if shared_trackers: diff --git a/src/File/FileServer.py b/src/File/FileServer.py index 16afab24..5487d0e9 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -3,6 +3,7 @@ import time import random import socket import sys +import weakref import gevent import gevent.pool @@ -42,6 +43,8 @@ class FileServer(ConnectionServer): self.update_start_time = 0 self.update_sites_task_next_nr = 1 + self.update_threads = weakref.WeakValueDictionary() + self.passive_mode = None self.active_mode = None self.active_mode_threads = {} @@ -317,30 +320,23 @@ class FileServer(ConnectionServer): def updateSite(self, site, check_files=False, verify_files=False): if not site: - return False - return site.update2(check_files=check_files, verify_files=verify_files) + return + site.update2(check_files=check_files, verify_files=verify_files) def spawnUpdateSite(self, site, check_files=False, verify_files=False): thread = self.update_pool.spawn(self.updateSite, site, check_files=check_files, verify_files=verify_files) - thread.site_address = site.address + self.update_threads[site.address] = thread return thread + def lookupInUpdatePool(self, site_address): + thread = self.update_threads.get(site_address, None) + if not thread or thread.ready(): + return None + return thread + def siteIsInUpdatePool(self, site_address): - while True: - restart = False - for thread in list(iter(self.update_pool)): - thread_site_address = getattr(thread, 'site_address', None) - if not thread_site_address: - # Possible race condition in assigning thread.site_address in spawnUpdateSite() - # Trying again. - self.sleep(0.1) - restart = True - break - if thread_site_address == site_address: - return True - if not restart: - return False + return self.lookupInUpdatePool(site_address) is not None def invalidateUpdateTime(self, invalid_interval): for address in self.getSiteAddresses(): diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index 6e0d57af..3637c822 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -54,6 +54,8 @@ class Peer(object): self.download_bytes = 0 # Bytes downloaded self.download_time = 0 # Time spent to download + self.protectedRequests = ["getFile", "streamFile", "update", "listModified"] + def __getattr__(self, key): if key == "hashfield": self.has_hashfield = True @@ -78,10 +80,8 @@ class Peer(object): logger.log(log_level, "%s:%s %s" % (self.ip, self.port, text)) - # Site marks its Peers protected, if it has not enough peers connected. - # This is to be used to prevent disconnecting from peers when doing - # a periodic cleanup. - def markProtected(self, interval=60*20): + # Protect connection from being closed by site.cleanupPeers() + def markProtected(self, interval=60*2): self.protected = max(self.protected, time.time() + interval) def isProtected(self): @@ -195,6 +195,8 @@ class Peer(object): for retry in range(1, 4): # Retry 3 times try: + if cmd in self.protectedRequests: + self.markProtected() if not self.connection: raise Exception("No connection found") res = self.connection.request(cmd, params, stream_to) diff --git a/src/Site/Site.py b/src/Site/Site.py index 881f7d7c..5929617c 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -28,6 +28,7 @@ from Plugin import PluginManager from File import FileServer from .SiteAnnouncer import SiteAnnouncer from . import SiteManager +from . import SiteHelpers class ScaledTimeoutHandler: def __init__(self, val_min, val_max, handler=None, scaler=None): @@ -145,7 +146,6 @@ class BackgroundPublisher: self.site.log.info("Background publisher: Published %s to %s peers", self.inner_path, len(self.published)) - @PluginManager.acceptPlugins class Site(object): @@ -209,6 +209,10 @@ class Site(object): self.announcer = SiteAnnouncer(self) # Announce and get peer list from other nodes + self.peer_connector = SiteHelpers.PeerConnector(self) # Connect more peers in background by request + self.persistent_peer_req = None # The persistent peer requirement, managed by maintenance handler + + if not self.settings.get("wrapper_key"): # To auth websocket permissions self.settings["wrapper_key"] = CryptHash.random() self.log.debug("New wrapper key: %s" % self.settings["wrapper_key"]) @@ -753,7 +757,6 @@ class Site(object): if verify_files: check_files = True - self.updateWebsocket(updating=True) if verify_files: self.updateWebsocket(verifying=True) elif check_files: @@ -771,16 +774,32 @@ class Site(object): if verify_files: self.settings["verify_files_timestamp"] = time.time() + if verify_files: + self.updateWebsocket(verified=True) + elif check_files: + self.updateWebsocket(checked=True) + if not self.isServing(): - self.updateWebsocket(updated=True) return False + if announce: + self.updateWebsocket(updating=True) + self.announce(mode="update", force=True) + + reqs = [ + self.peer_connector.newReq(4, 4, 30), + self.peer_connector.newReq(2, 2, 60), + self.peer_connector.newReq(1, 1, 120) + ] + nr_connected_peers = self.waitForPeers(reqs); + if nr_connected_peers < 1: + return + + self.updateWebsocket(updating=True) + # Remove files that no longer in content.json self.checkBadFiles() - if announce: - self.announce(mode="update", force=True) - # Full update, we can reset bad files if check_files and since == 0: self.bad_files = {} @@ -810,12 +829,6 @@ class Site(object): # To be called from FileServer @util.Noparallel(queue=True, ignore_args=True) def update2(self, check_files=False, verify_files=False): - if len(self.peers) < 50: - self.announce(mode="update") - self.waitForPeers(5, 5, 30); - self.waitForPeers(2, 2, 30); - self.waitForPeers(1, 1, 60); - self.update(check_files=check_files, verify_files=verify_files) # Update site by redownload all content.json @@ -894,41 +907,13 @@ class Site(object): background_publisher.finalize() del self.background_publishers[inner_path] - def waitForPeers_realJob(self, need_nr_peers, need_nr_connected_peers, time_limit): - start_time = time.time() - for _ in range(time_limit): - nr_connected_peers = len(self.getConnectedPeers()) - nr_peers = len(self.peers) - if nr_peers >= need_nr_peers and nr_connected_peers >= need_nr_connected_peers: - return nr_connected_peers - self.updateWebsocket(connecting_to_peers=nr_connected_peers) - self.announce(mode="more", force=True) - if not self.isServing(): - return nr_connected_peers - for wait in range(10): - self.needConnections(num=need_nr_connected_peers) - time.sleep(2) - nr_connected_peers = len(self.getConnectedPeers()) - nr_peers = len(self.peers) - self.updateWebsocket(connecting_to_peers=nr_connected_peers) - if not self.isServing(): - return nr_connected_peers - if nr_peers >= need_nr_peers and nr_connected_peers >= need_nr_connected_peers: - return nr_connected_peers - if time.time() - start_time > time_limit: - return nr_connected_peers - - return nr_connected_peers - - def waitForPeers(self, need_nr_peers, need_nr_connected_peers, time_limit): - nr_connected_peers = self.waitForPeers_realJob(need_nr_peers, need_nr_connected_peers, time_limit) - self.updateWebsocket(connected_to_peers=nr_connected_peers) - return nr_connected_peers - def getPeersForForegroundPublishing(self, limit): # Wait for some peers to appear - self.waitForPeers(limit, limit / 2, 10) # some of them... - self.waitForPeers(1, 1, 60) # or at least one... + reqs = [ + self.peer_connector.newReq(limit, limit / 2, 10), # some of them... + self.peer_connector.newReq(1, 1, 60) # or at least one... + ] + self.waitForPeers(reqs) peers = self.getConnectedPeers() random.shuffle(peers) @@ -1206,6 +1191,10 @@ class Site(object): peer = Peer(ip, port, self) self.peers[key] = peer peer.found(source) + + self.peer_connector.processReqs() + self.peer_connector.addPeer(peer) + return peer def announce(self, *args, **kwargs): @@ -1288,76 +1277,54 @@ class Site(object): limit = min(limit, config.connected_limit) return limit - def tryConnectingToMorePeers(self, more=1, pex=True, try_harder=False): - max_peers = more * 2 + 10 - if try_harder: - max_peers += 10000 + ############################################################################ - connected = 0 - for peer in self.getRecentPeers(max_peers): - if not peer.isConnected(): - if pex: - peer.pex() - else: - peer.ping(timeout=2.0, tryes=1) + # Returns the maximum value of current reqs for connections + def waitingForConnections(self): + self.peer_connector.processReqs() + return self.peer_connector.need_nr_connected_peers - if peer.isConnected(): - connected += 1 - - if connected >= more: - break - - return connected - - def bringConnections(self, need=1, update_site_on_reconnect=False, pex=True, try_harder=False): - connected = len(self.getConnectedPeers()) - connected_before = connected - - self.log.debug("Need connections: %s, Current: %s, Total: %s" % (need, connected, len(self.peers))) - - if connected < need: - connected += self.tryConnectingToMorePeers(more=(need-connected), pex=pex, try_harder=try_harder) - self.log.debug( - "Connected before: %s, after: %s. Check site: %s." % - (connected_before, connected, update_site_on_reconnect) - ) - - if update_site_on_reconnect and connected_before == 0 and connected > 0 and self.connection_server.has_internet: - self.greenlet_manager.spawn(self.update, check_files=False) - - return connected - - # Keep connections - def needConnections(self, num=None, update_site_on_reconnect=False, pex=True): + def needConnections(self, num=None, update_site_on_reconnect=False): if not self.connection_server.allowsCreatingConnections(): return if num is None: num = self.getPreferableActiveConnectionCount() + num = min(len(self.peers), num) - need = min(len(self.peers), num) + req = self.peer_connector.newReq(0, num) + return req - connected = self.bringConnections( - need=need, - update_site_on_reconnect=update_site_on_reconnect, - pex=pex, - try_harder=False) + # Wait for peers to ne known and/or connected and send updates to the UI + def waitForPeers(self, reqs): + if not reqs: + return 0 + i = 0 + nr_connected_peers = -1 + while self.isServing(): + ready_reqs = list(filter(lambda req: req.ready(), reqs)) + if len(ready_reqs) == len(reqs): + if nr_connected_peers < 0: + nr_connected_peers = ready_reqs[0].nr_connected_peers + break + waiting_reqs = list(filter(lambda req: not req.ready(), reqs)) + if not waiting_reqs: + break + waiting_req = waiting_reqs[0] + #self.log.debug("waiting_req: %s %s %s", waiting_req.need_nr_connected_peers, waiting_req.nr_connected_peers, waiting_req.expiration_interval) + waiting_req.waitHeartbeat(timeout=1.0) + if i > 0 and nr_connected_peers != waiting_req.nr_connected_peers: + nr_connected_peers = waiting_req.nr_connected_peers + self.updateWebsocket(connecting_to_peers=nr_connected_peers) + i += 1 + self.updateWebsocket(connected_to_peers=max(nr_connected_peers, 0)) + if i > 1: + # If we waited some time, pause now for displaying connected_to_peers message in the UI. + # This sleep is solely needed for site status updates on ZeroHello to be more cool-looking. + gevent.sleep(1) + return nr_connected_peers - if connected < need: - self.greenlet_manager.spawnLater(1.0, self.bringConnections, - need=need, - update_site_on_reconnect=update_site_on_reconnect, - pex=pex, - try_harder=True) - - if connected < num: - self.markConnectedPeersProtected() - - return connected - - def markConnectedPeersProtected(self): - for peer in self.getConnectedPeers(): - peer.markProtected() + ############################################################################ # Return: Probably peers verified to be connectable recently def getConnectablePeers(self, need_num=5, ignore=[], allow_private=True): @@ -1429,15 +1396,26 @@ class Site(object): return found[0:need_num] - def getConnectedPeers(self): + # Returns the list of connected peers + # By default the result may contain peers chosen optimistically: + # If the connection is being established and 20 seconds have not yet passed + # since the connection start time, those peers are included in the result. + # Set onlyFullyConnected=True for restricting only by fully connected peers. + def getConnectedPeers(self, onlyFullyConnected=False): back = [] if not self.connection_server: return [] tor_manager = self.connection_server.tor_manager for connection in self.connection_server.connections: + if len(back) >= len(self.peers): # short cut for breaking early; no peers to check left + break + if not connection.connected and time.time() - connection.start_time > 20: # Still not connected after 20s continue + if not connection.connected and onlyFullyConnected: # Only fully connected peers + continue + peer = self.peers.get("%s:%s" % (connection.ip, connection.port)) if peer: if connection.ip.endswith(".onion") and connection.target_onion and tor_manager.start_onions: @@ -1479,8 +1457,8 @@ class Site(object): def cleanupPeers(self): self.removeDeadPeers() - limit = self.getActiveConnectionCountLimit() - connected_peers = [peer for peer in self.getConnectedPeers() if peer.isConnected()] # Only fully connected peers + limit = max(self.getActiveConnectionCountLimit(), self.waitingForConnections()) + connected_peers = self.getConnectedPeers(onlyFullyConnected=True) need_to_close = len(connected_peers) - limit if need_to_close > 0: @@ -1526,10 +1504,10 @@ class Site(object): if not startup: self.cleanupPeers() - self.needConnections(update_site_on_reconnect=True) + self.persistent_peer_req = self.needConnections(update_site_on_reconnect=True) + self.persistent_peer_req.result_connected.wait(timeout=2.0) - with gevent.Timeout(10, exception=False): - self.announcer.announcePex() + #self.announcer.announcePex() self.processBackgroundPublishers() @@ -1559,7 +1537,7 @@ class Site(object): return False sent = 0 - connected_peers = self.getConnectedPeers() + connected_peers = self.getConnectedPeers(onlyFullyConnected=True) for peer in connected_peers: if peer.sendMyHashfield(): sent += 1 @@ -1581,7 +1559,7 @@ class Site(object): s = time.time() queried = 0 - connected_peers = self.getConnectedPeers() + connected_peers = self.getConnectedPeers(onlyFullyConnected=True) for peer in connected_peers: if peer.time_hashfield: continue diff --git a/src/Site/SiteHelpers.py b/src/Site/SiteHelpers.py new file mode 100644 index 00000000..65f0530b --- /dev/null +++ b/src/Site/SiteHelpers.py @@ -0,0 +1,220 @@ +import time +import weakref +import gevent + +class ConnectRequirement(object): + next_id = 1 + def __init__(self, need_nr_peers, need_nr_connected_peers, expiration_interval=None): + self.need_nr_peers = need_nr_peers # how many total peers we need + self.need_nr_connected_peers = need_nr_connected_peers # how many connected peers we need + self.result = gevent.event.AsyncResult() # resolves on need_nr_peers condition + self.result_connected = gevent.event.AsyncResult() # resolves on need_nr_connected_peers condition + + self.expiration_interval = expiration_interval + self.expired = False + if expiration_interval: + self.expire_at = time.time() + expiration_interval + else: + self.expire_at = None + + self.nr_peers = -1 # updated PeerConnector() + self.nr_connected_peers = -1 # updated PeerConnector() + + self.heartbeat = gevent.event.AsyncResult() + + self.id = type(self).next_id + type(self).next_id += 1 + + def fulfilled(self): + return self.result.ready() and self.result_connected.ready() + + def ready(self): + return self.expired or self.fulfilled() + + # Heartbeat send when any of the following happens: + # * self.result is set + # * self.result_connected is set + # * self.nr_peers changed + # * self.nr_peers_connected changed + # * self.expired is set + def waitHeartbeat(self, timeout=None): + if self.heartbeat.ready(): + self.heartbeat = gevent.event.AsyncResult() + return self.heartbeat.wait(timeout=timeout) + + def sendHeartbeat(self): + self.heartbeat.set_result() + if self.heartbeat.ready(): + self.heartbeat = gevent.event.AsyncResult() + +class PeerConnector(object): + + def __init__(self, site): + self.site = site + + self.peer_reqs = weakref.WeakValueDictionary() # How many connected peers we need. + # Separate entry for each requirement. + # Objects of type ConnectRequirement. + self.peer_connector_controller = None # Thread doing the orchestration in background. + self.peer_connector_workers = dict() # Threads trying to connect to individual peers. + self.peer_connector_worker_limit = 5 # Max nr of workers. + self.peer_connector_announcer = None # Thread doing announces in background. + + # Max effective values. Set by processReqs(). + self.need_nr_peers = 0 + self.need_nr_connected_peers = 0 + self.nr_peers = 0 # set by processReqs() + self.nr_connected_peers = 0 # set by processReqs2() + + self.peers = list() + + def addReq(self, req): + self.peer_reqs[req.id] = req + self.processReqs() + + def newReq(self, need_nr_peers, need_nr_connected_peers, expiration_interval=None): + req = ConnectRequirement(need_nr_peers, need_nr_connected_peers, expiration_interval=expiration_interval) + self.addReq(req) + return req + + def processReqs(self, nr_connected_peers=None): + nr_peers = len(self.site.peers) + self.nr_peers = nr_peers + + need_nr_peers = 0 + need_nr_connected_peers = 0 + + items = list(self.peer_reqs.items()) + for key, req in items: + send_heartbeat = False + + if req.expire_at and req.expire_at < time.time(): + req.expired = True + self.peer_reqs.pop(key, None) + send_heartbeat = True + elif req.result.ready() and req.result_connected.ready(): + pass + else: + if nr_connected_peers is not None: + if req.need_nr_peers <= nr_peers and req.need_nr_connected_peers <= nr_connected_peers: + req.result.set_result(nr_peers) + req.result_connected.set_result(nr_connected_peers) + send_heartbeat = True + if req.nr_peers != nr_peers or req.nr_connected_peers != nr_connected_peers: + req.nr_peers = nr_peers + req.nr_connected_peers = nr_connected_peers + send_heartbeat = True + + if not (req.result.ready() and req.result_connected.ready()): + need_nr_peers = max(need_nr_peers, req.need_nr_peers) + need_nr_connected_peers = max(need_nr_connected_peers, req.need_nr_connected_peers) + + if send_heartbeat: + req.sendHeartbeat() + + self.need_nr_peers = need_nr_peers + self.need_nr_connected_peers = need_nr_connected_peers + + if nr_connected_peers is None: + nr_connected_peers = 0 + if need_nr_peers > nr_peers: + self.spawnPeerConnectorAnnouncer(); + if need_nr_connected_peers > nr_connected_peers: + self.spawnPeerConnectorController(); + + def processReqs2(self): + self.nr_connected_peers = len(self.site.getConnectedPeers(onlyFullyConnected=True)) + self.processReqs(nr_connected_peers=self.nr_connected_peers) + + # For adding new peers when ConnectorController is working. + # While it is iterating over a cached list of peers, there can be a significant lag + # for a newly discovered peer to get in sight of the controller. + # Suppose most previously known peers are dead and we've just get a few + # new peers from a tracker. + # So we mix the new peer to the cached list. + # When ConnectorController is stopped (self.peers is empty), we just do nothing here. + def addPeer(self, peer): + if not self.peers: + return + if peer not in self.peers: + self.peers.append(peer) + + def keepGoing(self): + return self.site.isServing() and self.site.connection_server.allowsCreatingConnections() + + def peerConnectorWorker(self, peer): + if not peer.isConnected(): + peer.connect() + if peer.isConnected(): + self.processReqs2() + + def peerConnectorController(self): + self.peers = list() + addendum = 20 + while self.keepGoing(): + + if len(self.site.peers) < 1: + # No peers and no way to manage this from this method. + # Just give up. + break + + self.processReqs2() + + if self.need_nr_connected_peers <= self.nr_connected_peers: + # Ok, nobody waits for connected peers. + # Done. + break + + if len(self.peers) < 1: + # refill the peer list + self.peers = self.site.getRecentPeers(self.need_nr_connected_peers * 2 + addendum) + addendum = addendum * 2 + 50 + if len(self.peers) <= self.nr_connected_peers: + # looks like all known peers are connected + # start announcePex() in background and give up + self.site.announcer.announcePex() + break + + # try connecting to peers + while self.keepGoing() and len(self.peer_connector_workers) < self.peer_connector_worker_limit: + if len(self.peers) < 1: + break + + peer = self.peers.pop(0) + + if peer.isConnected(): + continue + + thread = self.peer_connector_workers.get(peer, None) + if thread: + continue + + thread = self.site.spawn(self.peerConnectorWorker, peer) + self.peer_connector_workers[peer] = thread + thread.link(lambda thread, peer=peer: self.peer_connector_workers.pop(peer, None)) + + # wait for more room in self.peer_connector_workers + while self.keepGoing() and len(self.peer_connector_workers) >= self.peer_connector_worker_limit: + gevent.sleep(2) + + self.peers = list() + self.peer_connector_controller = None + + def peerConnectorAnnouncer(self): + while self.keepGoing(): + if self.need_nr_peers <= self.nr_peers: + break + self.site.announce(mode="more") + self.processReqs2() + if self.need_nr_peers <= self.nr_peers: + break + gevent.sleep(10) + self.peer_connector_announcer = None + + def spawnPeerConnectorController(self): + if self.peer_connector_controller is None or self.peer_connector_controller.ready(): + self.peer_connector_controller = self.site.spawn(self.peerConnectorController) + + def spawnPeerConnectorAnnouncer(self): + if self.peer_connector_announcer is None or self.peer_connector_announcer.ready(): + self.peer_connector_announcer = self.site.spawn(self.peerConnectorAnnouncer) From 1fd1f47a9410331ea80aed37cde9d3f4743a8e7c Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Mon, 25 Oct 2021 12:53:37 +0700 Subject: [PATCH 095/191] Fix detection of the broken Internet connection on the app start up --- src/Connection/Connection.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 398a4cc3..504daf58 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -118,6 +118,8 @@ class Connection(object): # Open connection to peer and wait for handshake def connect(self): + self.updateOnlineStatus(outgoing_activity=True) + if not self.event_connected or self.event_connected.ready(): self.event_connected = gevent.event.AsyncResult() From ce971ab738e8ee938aeab9b0e927872b41a8df3d Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Mon, 25 Oct 2021 13:18:43 +0700 Subject: [PATCH 096/191] Don't increment `bad_file` failed tries counter on rediscovering the same file on update() Do increment it only on actual fileFailed() event. --- src/Site/Site.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 5929617c..cb6630a5 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -629,7 +629,7 @@ class Site(object): if has_newer: # We dont have this file or we have older modified_contents.append(inner_path) - self.bad_files[inner_path] = self.bad_files.get(inner_path, 0) + 1 + self.bad_files[inner_path] = self.bad_files.get(inner_path, 1) if has_older: send_back.append(inner_path) @@ -1163,7 +1163,7 @@ class Site(object): self.log.debug("%s: Download not allowed" % inner_path) return False - self.bad_files[inner_path] = self.bad_files.get(inner_path, 0) + 1 # Mark as bad file + self.bad_files[inner_path] = self.bad_files.get(inner_path, 1) # Mark as bad file task = self.worker_manager.addTask(inner_path, peer, priority=priority, file_info=file_info) if blocking: From 8f908c961db56a463708903d86c6c5afb620c82f Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Mon, 25 Oct 2021 16:12:00 +0700 Subject: [PATCH 097/191] Fine-tuning PeerConnector --- src/Site/SiteHelpers.py | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/src/Site/SiteHelpers.py b/src/Site/SiteHelpers.py index 65f0530b..c3be446c 100644 --- a/src/Site/SiteHelpers.py +++ b/src/Site/SiteHelpers.py @@ -66,6 +66,7 @@ class PeerConnector(object): self.nr_peers = 0 # set by processReqs() self.nr_connected_peers = 0 # set by processReqs2() + # Connector Controller state self.peers = list() def addReq(self, req): @@ -139,6 +140,9 @@ class PeerConnector(object): if peer not in self.peers: self.peers.append(peer) + def sleep(self, t): + self.site.connection_server.sleep(t) + def keepGoing(self): return self.site.isServing() and self.site.connection_server.allowsCreatingConnections() @@ -153,10 +157,14 @@ class PeerConnector(object): addendum = 20 while self.keepGoing(): - if len(self.site.peers) < 1: - # No peers and no way to manage this from this method. - # Just give up. - break + no_peers_loop = 0 + while len(self.site.peers) < 1: + # No peers at all. + # Waiting for the announcer to discover some peers. + self.sleep(10 + no_peers_loop) + no_peers_loop += 1 + if not self.keepGoing() or no_peers_loop > 60: + break self.processReqs2() @@ -165,15 +173,19 @@ class PeerConnector(object): # Done. break + if len(self.site.peers) < 1: + break + if len(self.peers) < 1: # refill the peer list - self.peers = self.site.getRecentPeers(self.need_nr_connected_peers * 2 + addendum) - addendum = addendum * 2 + 50 + self.peers = self.site.getRecentPeers(self.need_nr_connected_peers * 2 + self.nr_connected_peers + addendum) + addendum = min(addendum * 2 + 50, 10000) if len(self.peers) <= self.nr_connected_peers: - # looks like all known peers are connected - # start announcePex() in background and give up + # Looks like all known peers are connected. + # Waiting for the announcer to discover some peers. self.site.announcer.announcePex() - break + self.sleep(10) + continue # try connecting to peers while self.keepGoing() and len(self.peer_connector_workers) < self.peer_connector_worker_limit: @@ -195,7 +207,10 @@ class PeerConnector(object): # wait for more room in self.peer_connector_workers while self.keepGoing() and len(self.peer_connector_workers) >= self.peer_connector_worker_limit: - gevent.sleep(2) + self.sleep(2) + + if not self.site.connection_server.isInternetOnline(): + self.sleep(20) self.peers = list() self.peer_connector_controller = None @@ -208,7 +223,9 @@ class PeerConnector(object): self.processReqs2() if self.need_nr_peers <= self.nr_peers: break - gevent.sleep(10) + self.sleep(10) + if not self.site.connection_server.isInternetOnline(): + self.sleep(20) self.peer_connector_announcer = None def spawnPeerConnectorController(self): From 1a8d30146ecabae3e0e23174a9918498ffd4c19d Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Mon, 25 Oct 2021 16:18:35 +0700 Subject: [PATCH 098/191] Fix typos in comments --- src/Site/Site.py | 10 +++++----- src/Site/SiteHelpers.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index cb6630a5..456d02f1 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -283,7 +283,7 @@ class Site(object): SiteManager.site_manager.saveDelayed() # Returns True if any site-related activity should be interrupted - # due to connection server being stooped or site being deleted + # due to connection server being stopped or site being deleted def isStopping(self): return self.connection_server.stopping or self.settings.get("deleting", False) @@ -353,7 +353,7 @@ class Site(object): def isAddedRecently(self): return time.time() - self.settings.get("added", 0) < 60 * 60 * 24 - # Download all file from content.json + # Download all files from content.json def downloadContent(self, inner_path, download_files=True, peer=None, check_modifications=False, diffs={}): s = time.time() if config.verbose: @@ -1597,10 +1597,10 @@ class Site(object): # Add event listeners def addEventListeners(self): - self.onFileStart = util.Event() # If WorkerManager added new task + self.onFileStart = util.Event() # If WorkerManager added new task self.onFileDone = util.Event() # If WorkerManager successfully downloaded a file self.onFileFail = util.Event() # If WorkerManager failed to download a file - self.onComplete = util.Event() # All file finished + self.onComplete = util.Event() # All files finished self.onFileStart.append(lambda inner_path: self.fileStarted()) # No parameters to make Noparallel batching working self.onFileDone.append(lambda inner_path: self.fileDone(inner_path)) @@ -1629,7 +1629,7 @@ class Site(object): time.sleep(0.001) # Wait for other files adds self.updateWebsocket(file_started=True) - # File downloaded successful + # File downloaded successfully def fileDone(self, inner_path): # File downloaded, remove it from bad files if inner_path in self.bad_files: diff --git a/src/Site/SiteHelpers.py b/src/Site/SiteHelpers.py index c3be446c..5abcd46e 100644 --- a/src/Site/SiteHelpers.py +++ b/src/Site/SiteHelpers.py @@ -31,7 +31,7 @@ class ConnectRequirement(object): def ready(self): return self.expired or self.fulfilled() - # Heartbeat send when any of the following happens: + # Heartbeat sent when any of the following happens: # * self.result is set # * self.result_connected is set # * self.nr_peers changed From dff52d691a51b2c87d71464829a5dc3658b7c267 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Mon, 25 Oct 2021 17:09:16 +0700 Subject: [PATCH 099/191] Small improvements in FileServer --- src/File/FileServer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index 5487d0e9..9ef3ae9c 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -280,6 +280,7 @@ class FileServer(ConnectionServer): # Returns False if Internet is immediately available # Returns True if we've spent some time waiting for Internet # Returns None if FileServer is stopping or the Offline mode is enabled + @util.Noparallel() def waitForInternetOnline(self): if self.isOfflineMode() or self.stopping: return None @@ -294,6 +295,7 @@ class FileServer(ConnectionServer): if self.isInternetOnline(): break if len(self.update_pool) == 0: + log.info("Internet connection seems to be broken. Running an update for a random site to check if we are able to connect to any peer.") thread = self.thread_pool.spawn(self.updateRandomSite) thread.join() @@ -314,7 +316,7 @@ class FileServer(ConnectionServer): if not site: return - log.debug("Checking randomly chosen site: %s", site.address_short) + log.info("Randomly chosen site: %s", site.address_short) self.spawnUpdateSite(site).join() @@ -446,6 +448,7 @@ class FileServer(ConnectionServer): while self.isActiveMode(): site = None self.sleep(short_timeout) + self.waitForInternetOnline() if not self.isActiveMode(): break From 93a95f511a21f06aa80bb05e3a45011bb758e283 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Mon, 25 Oct 2021 17:59:35 +0700 Subject: [PATCH 100/191] Limit the pex request frequency, interval is 120 secs for each peer --- src/Peer/Peer.py | 9 +++++- src/Site/SiteAnnouncer.py | 64 ++++++++++++++++++++++----------------- src/Site/SiteHelpers.py | 2 +- 3 files changed, 45 insertions(+), 30 deletions(-) diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index 3637c822..72e9c47f 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -44,6 +44,7 @@ class Peer(object): self.time_response = None # Time of last successful response from peer self.time_added = time.time() self.last_ping = None # Last response time for ping + self.last_pex = 0 # Last query/response time for pex self.is_tracker_connection = False # Tracker connection instead of normal peer self.reputation = 0 # More likely to connect if larger self.last_content_json_update = 0.0 # Modify date of last received content.json @@ -305,10 +306,15 @@ class Peer(object): return response_time # Request peer exchange from peer - def pex(self, site=None, need_num=5): + def pex(self, site=None, need_num=5, request_interval=60*2): if not site: site = self.site # If no site defined request peers for this site + if self.last_pex + request_interval >= time.time(): + return False + + self.last_pex = time.time() + # give back 5 connectible peers packed_peers = helper.packPeers(self.site.getConnectablePeers(5, allow_private=False)) request = {"site": site.address, "peers": packed_peers["ipv4"], "need": need_num} @@ -317,6 +323,7 @@ class Peer(object): if packed_peers["ipv6"]: request["peers_ipv6"] = packed_peers["ipv6"] res = self.request("pex", request) + self.last_pex = time.time() if not res or "error" in res: return False added = 0 diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index 6a510583..31004d02 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -302,39 +302,47 @@ class SiteAnnouncer(object): self.updateWebsocket(trackers="announced") @util.Noparallel(blocking=False) - def announcePex(self, query_num=2, need_num=10): - if not self.site.isServing(): - return + def announcePex(self, query_num=2, need_num=10, establish_connections=True): + peers = [] + try: + peer_count = 20 + query_num * 2 - self.updateWebsocket(pex="announcing") + # Wait for some peers to connect + for _ in range(5): + if not self.site.isServing(): + return + peers = self.site.getConnectedPeers(onlyFullyConnected=True) + if len(peers) > 0: + break + time.sleep(2) - peers = self.site.getConnectedPeers() - if len(peers) == 0: # Wait 3s for connections - time.sleep(3) - peers = self.site.getConnectedPeers() + if len(peers) < peer_count and establish_connections: + # Small number of connected peers for this site, connect to any + peers = list(self.site.getRecentPeers(peer_count)) - if len(peers) == 0: # Small number of connected peers for this site, connect to any - peers = list(self.site.getRecentPeers(20)) - need_num = 10 + if len(peers) > 0: + self.updateWebsocket(pex="announcing") - random.shuffle(peers) - done = 0 - total_added = 0 - for peer in peers: - num_added = peer.pex(need_num=need_num) - if num_added is not False: - done += 1 - total_added += num_added - if num_added: - self.site.worker_manager.onPeers() - self.site.updateWebsocket(peers_added=num_added) - else: + random.shuffle(peers) + done = 0 + total_added = 0 + for peer in peers: + if not establish_connections and not peer.isConnected(): + continue + num_added = peer.pex(need_num=need_num) + if num_added is not False: + done += 1 + total_added += num_added + if num_added: + self.site.worker_manager.onPeers() + self.site.updateWebsocket(peers_added=num_added) + if done == query_num: + break time.sleep(0.1) - if done == query_num: - break - self.log.debug("Pex result: from %s peers got %s new peers." % (done, total_added)) - - self.updateWebsocket(pex="announced") + self.log.debug("Pex result: from %s peers got %s new peers." % (done, total_added)) + finally: + if len(peers) > 0: + self.updateWebsocket(pex="announced") def updateWebsocket(self, **kwargs): if kwargs: diff --git a/src/Site/SiteHelpers.py b/src/Site/SiteHelpers.py index 5abcd46e..c095ad66 100644 --- a/src/Site/SiteHelpers.py +++ b/src/Site/SiteHelpers.py @@ -183,7 +183,7 @@ class PeerConnector(object): if len(self.peers) <= self.nr_connected_peers: # Looks like all known peers are connected. # Waiting for the announcer to discover some peers. - self.site.announcer.announcePex() + self.site.announcer.announcePex(establish_connections=False) self.sleep(10) continue From 645f3ba34ab37b508d4da7d0d222d963d7908a3c Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Tue, 26 Oct 2021 17:38:40 +0700 Subject: [PATCH 101/191] Reorganization of Peer class and peer-related Site's methods --- plugins/TrackerShare/TrackerSharePlugin.py | 2 +- src/Connection/Connection.py | 9 +- src/Connection/ConnectionServer.py | 14 +++ src/Peer/Peer.py | 66 +++++++++--- src/Site/Site.py | 118 ++++++++++----------- src/Site/SiteAnnouncer.py | 2 +- src/Site/SiteHelpers.py | 22 +++- 7 files changed, 150 insertions(+), 83 deletions(-) diff --git a/plugins/TrackerShare/TrackerSharePlugin.py b/plugins/TrackerShare/TrackerSharePlugin.py index 4fe888fd..42c79422 100644 --- a/plugins/TrackerShare/TrackerSharePlugin.py +++ b/plugins/TrackerShare/TrackerSharePlugin.py @@ -462,7 +462,7 @@ if "tracker_storage" not in locals(): class SiteAnnouncerPlugin(object): def getTrackers(self): tracker_storage.setSiteAnnouncer(self) - tracker_storage.checkDiscoveringTrackers(self.site.getConnectedPeers(onlyFullyConnected=True)) + tracker_storage.checkDiscoveringTrackers(self.site.getConnectedPeers(only_fully_connected=True)) trackers = super(SiteAnnouncerPlugin, self).getTrackers() shared_trackers = list(tracker_storage.getTrackers().keys()) if shared_trackers: diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 504daf58..40519b7f 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -124,12 +124,13 @@ class Connection(object): self.event_connected = gevent.event.AsyncResult() self.type = "out" + + unreachability = self.server.getIpUnreachability(self.ip) + if unreachability: + raise Exception(unreachability) + if self.ip_type == "onion": - if not self.server.tor_manager or not self.server.tor_manager.enabled: - raise Exception("Can't connect to onion addresses, no Tor controller present") self.sock = self.server.tor_manager.createSocket(self.ip, self.port) - elif config.tor == "always" and helper.isPrivateIp(self.ip) and self.ip not in config.ip_local: - raise Exception("Can't connect to local IPs in Tor: always mode") elif config.trackers_proxy != "disable" and config.tor != "always" and self.is_tracker_connection: if config.trackers_proxy == "tor": self.sock = self.server.tor_manager.createSocket(self.ip, self.port) diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index 03a93daa..16834ff5 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -519,3 +519,17 @@ class ConnectionServer(object): mid = int(len(corrections) / 2 - 1) median = (corrections[mid - 1] + corrections[mid] + corrections[mid + 1]) / 3 return median + + # Checks if a network address can be reachable in the current configuration + # and returs a string describing why it cannot. + # If the network address can be reachable, returns False. + def getIpUnreachability(self, ip): + ip_type = helper.getIpType(ip) + if ip_type == 'onion' and not self.tor_manager.enabled: + return "Can't connect to onion addresses, no Tor controller present" + if config.tor == "always" and helper.isPrivateIp(ip) and ip not in config.ip_local: + return "Can't connect to local IPs in Tor: always mode" + return False + + def isIpReachable(self, ip): + return self.getIpUnreachability(ip) == False diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index 72e9c47f..28a7220b 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -32,6 +32,10 @@ class Peer(object): self.site = site self.key = "%s:%s" % (ip, port) + self.ip_type = helper.getIpType(ip) + + self.removed = False + self.log_level = logging.DEBUG self.connection_error_log_level = logging.DEBUG @@ -41,7 +45,7 @@ class Peer(object): self.time_hashfield = None # Last time peer's hashfiled downloaded self.time_my_hashfield_sent = None # Last time my hashfield sent to peer self.time_found = time.time() # Time of last found in the torrent tracker - self.time_response = None # Time of last successful response from peer + self.time_response = 0 # Time of last successful response from peer self.time_added = time.time() self.last_ping = None # Last response time for ping self.last_pex = 0 # Last query/response time for pex @@ -49,6 +53,7 @@ class Peer(object): self.reputation = 0 # More likely to connect if larger self.last_content_json_update = 0.0 # Modify date of last received content.json self.protected = 0 + self.reachable = False self.connection_error = 0 # Series of connection error self.hash_failed = 0 # Number of bad files from peer @@ -57,6 +62,8 @@ class Peer(object): self.protectedRequests = ["getFile", "streamFile", "update", "listModified"] + self.updateReachable() + def __getattr__(self, key): if key == "hashfield": self.has_hashfield = True @@ -97,7 +104,36 @@ class Peer(object): return self.connection and self.connection.connected def isTtlExpired(self, ttl): - return (time.time() - self.time_found) > ttl + last_activity = max(self.time_found, self.time_response) + return (time.time() - last_activity) > ttl + + def isReachable(self): + return self.reachable + + def updateReachable(self): + connection_server = self.getConnectionServer() + if not self.port: + self.reachable = False + else: + self.reachable = connection_server.isIpReachable(self.ip) + + # Peer proved to to be connectable recently + def isConnectable(self): + if self.connection_error >= 1: # The last connection attempt failed + return False + if time.time() - self.time_response > 60 * 60 * 2: # Last successful response more than 2 hours ago + return False + return self.isReachable() + + def getConnectionServer(self): + if self.connection_server: + connection_server = self.connection_server + elif self.site: + connection_server = self.site.connection_server + else: + import main + connection_server = main.file_server + return connection_server # Connect to host def connect(self, connection=None): @@ -120,13 +156,7 @@ class Peer(object): self.connection = None try: - if self.connection_server: - connection_server = self.connection_server - elif self.site: - connection_server = self.site.connection_server - else: - import main - connection_server = main.file_server + connection_server = self.getConnectionServer() self.connection = connection_server.getConnection(self.ip, self.port, site=self.site, is_tracker_connection=self.is_tracker_connection) if self.connection and self.connection.connected: self.reputation += 1 @@ -183,6 +213,7 @@ class Peer(object): if source in ("tracker", "local"): self.site.peers_recent.appendleft(self) self.time_found = time.time() + self.updateReachable() # Send a command to peer and return response value def request(self, cmd, params={}, stream_to=None): @@ -355,6 +386,8 @@ class Peer(object): # List modified files since the date # Return: {inner_path: modification date,...} def listModified(self, since): + if self.removed: + return False return self.request("listModified", {"since": since, "site": self.site.address}) def updateHashfield(self, force=False): @@ -430,12 +463,11 @@ class Peer(object): # Stop and remove from site def remove(self, reason="Removing"): - self.log("Removing peer...Connection error: %s, Hash failed: %s" % (self.connection_error, self.hash_failed)) - if self.site and self.key in self.site.peers: - del(self.site.peers[self.key]) - - if self.site and self in self.site.peers_recent: - self.site.peers_recent.remove(self) + self.removed = True + self.log("Removing peer with reason: <%s>. Connection error: %s, Hash failed: %s" % (reason, self.connection_error, self.hash_failed)) + if self.site: + self.site.deregisterPeer(self) + self.site = None self.disconnect(reason) @@ -443,6 +475,8 @@ class Peer(object): # On connection error def onConnectionError(self, reason="Unknown"): + if not self.getConnectionServer().isInternetOnline(): + return self.connection_error += 1 if self.site and len(self.site.peers) > 200: limit = 3 @@ -450,7 +484,7 @@ class Peer(object): limit = 6 self.reputation -= 1 if self.connection_error >= limit: # Dead peer - self.remove("Peer connection: %s" % reason) + self.remove("Connection error limit reached: %s. Provided message: %s" % (limit, reason)) # Done working with peer def onWorkerDone(self): diff --git a/src/Site/Site.py b/src/Site/Site.py index 456d02f1..d4f2ad62 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -913,7 +913,7 @@ class Site(object): self.peer_connector.newReq(limit, limit / 2, 10), # some of them... self.peer_connector.newReq(1, 1, 60) # or at least one... ] - self.waitForPeers(reqs) + self.waitForPeers(reqs, update_websocket=False) peers = self.getConnectedPeers() random.shuffle(peers) @@ -1197,6 +1197,15 @@ class Site(object): return peer + # Called from peer.remove to erase links to peer + def deregisterPeer(self, peer): + self.peers.pop(peer.key, None) + try: + self.peers_recent.remove(peer) + except: + pass + self.peer_connector.deregisterPeer(peer) + def announce(self, *args, **kwargs): if self.isServing(): self.announcer.announce(*args, **kwargs) @@ -1295,8 +1304,9 @@ class Site(object): req = self.peer_connector.newReq(0, num) return req - # Wait for peers to ne known and/or connected and send updates to the UI - def waitForPeers(self, reqs): + # Wait for peers to be discovered and/or connected according to reqs + # and send updates to the UI + def waitForPeers(self, reqs, update_websocket=True): if not reqs: return 0 i = 0 @@ -1315,84 +1325,72 @@ class Site(object): waiting_req.waitHeartbeat(timeout=1.0) if i > 0 and nr_connected_peers != waiting_req.nr_connected_peers: nr_connected_peers = waiting_req.nr_connected_peers - self.updateWebsocket(connecting_to_peers=nr_connected_peers) + if update_websocket: + self.updateWebsocket(connecting_to_peers=nr_connected_peers) i += 1 - self.updateWebsocket(connected_to_peers=max(nr_connected_peers, 0)) - if i > 1: - # If we waited some time, pause now for displaying connected_to_peers message in the UI. - # This sleep is solely needed for site status updates on ZeroHello to be more cool-looking. - gevent.sleep(1) + if update_websocket: + self.updateWebsocket(connected_to_peers=max(nr_connected_peers, 0)) + if i > 1: + # If we waited some time, pause now for displaying connected_to_peers message in the UI. + # This sleep is solely needed for site status updates on ZeroHello to be more cool-looking. + gevent.sleep(1) return nr_connected_peers ############################################################################ - # Return: Probably peers verified to be connectable recently + # Return: Peers verified to be connectable recently, or if not enough, other peers as well def getConnectablePeers(self, need_num=5, ignore=[], allow_private=True): peers = list(self.peers.values()) - found = [] + random.shuffle(peers) + connectable_peers = [] + reachable_peers = [] for peer in peers: - if peer.key.endswith(":0"): - continue # Not connectable - if not peer.connection: - continue # No connection - if peer.ip.endswith(".onion") and not self.connection_server.tor_manager.enabled: - continue # Onion not supported if peer.key in ignore: - continue # The requester has this peer - if time.time() - peer.connection.last_recv_time > 60 * 60 * 2: # Last message more than 2 hours ago - peer.connection = None # Cleanup: Dead connection continue if not allow_private and helper.isPrivateIp(peer.ip): continue - found.append(peer) - if len(found) >= need_num: + if peer.isConnectable(): + connectable_peers.append(peer) + elif peer.isReachable(): + reachable_peers.append(peer) + if len(connectable_peers) >= need_num: break # Found requested number of peers - if len(found) < need_num: # Return not that good peers - found += [ - peer for peer in peers - if not peer.key.endswith(":0") and - peer.key not in ignore and - (allow_private or not helper.isPrivateIp(peer.ip)) - ][0:need_num - len(found)] + if len(connectable_peers) < need_num: # Return not that good peers + connectable_peers += reachable_peers[0:need_num - len(connectable_peers)] - return found + return connectable_peers # Return: Recently found peers + def getReachablePeers(self): + return [peer for peer in self.peers.values() if peer.isReachable()] + + # Return: Recently found peers, sorted by reputation. + # If there not enough recently found peers, adds other known peers with highest reputation def getRecentPeers(self, need_num): need_num = int(need_num) - found = list(set(self.peers_recent)) + found = set(self.peers_recent) self.log.debug( "Recent peers %s of %s (need: %s)" % (len(found), len(self.peers), need_num) ) - if len(found) >= need_num or len(found) >= len(self.peers): - return sorted( - found, + if len(found) < need_num and len(found) < len(self.peers): + # Add random peers + peers = self.getReachablePeers() + peers = sorted( + list(peers), key=lambda peer: peer.reputation, reverse=True - )[0:need_num] + ) + while len(found) < need_num and len(peers) > 0: + found.add(peers.pop()) - # Add random peers - need_more = need_num - len(found) - if not self.connection_server.tor_manager.enabled: - peers = [peer for peer in self.peers.values() if not peer.ip.endswith(".onion")] - else: - peers = list(self.peers.values()) - - self.log.debug("getRecentPeers: peers = %s" % peers) - self.log.debug("getRecentPeers: need_more = %s" % need_more) - - peers = peers[0:need_more * 50] - - found_more = sorted( - peers, + return sorted( + list(found), key=lambda peer: peer.reputation, reverse=True - )[0:need_more * 2] - - found += found_more + )[0:need_num] return found[0:need_num] @@ -1400,8 +1398,8 @@ class Site(object): # By default the result may contain peers chosen optimistically: # If the connection is being established and 20 seconds have not yet passed # since the connection start time, those peers are included in the result. - # Set onlyFullyConnected=True for restricting only by fully connected peers. - def getConnectedPeers(self, onlyFullyConnected=False): + # Set only_fully_connected=True for restricting only by fully connected peers. + def getConnectedPeers(self, only_fully_connected=False): back = [] if not self.connection_server: return [] @@ -1413,7 +1411,7 @@ class Site(object): if not connection.connected and time.time() - connection.start_time > 20: # Still not connected after 20s continue - if not connection.connected and onlyFullyConnected: # Only fully connected peers + if not connection.connected and only_fully_connected: # Only fully connected peers continue peer = self.peers.get("%s:%s" % (connection.ip, connection.port)) @@ -1434,7 +1432,9 @@ class Site(object): return removed = 0 - if len(peers) > 1000: + if len(peers) > 10000: + ttl = 60 * 2 + elif len(peers) > 1000: ttl = 60 * 60 * 1 elif len(peers) > 100: ttl = 60 * 60 * 4 @@ -1458,7 +1458,7 @@ class Site(object): self.removeDeadPeers() limit = max(self.getActiveConnectionCountLimit(), self.waitingForConnections()) - connected_peers = self.getConnectedPeers(onlyFullyConnected=True) + connected_peers = self.getConnectedPeers(only_fully_connected=True) need_to_close = len(connected_peers) - limit if need_to_close > 0: @@ -1537,7 +1537,7 @@ class Site(object): return False sent = 0 - connected_peers = self.getConnectedPeers(onlyFullyConnected=True) + connected_peers = self.getConnectedPeers(only_fully_connected=True) for peer in connected_peers: if peer.sendMyHashfield(): sent += 1 @@ -1559,7 +1559,7 @@ class Site(object): s = time.time() queried = 0 - connected_peers = self.getConnectedPeers(onlyFullyConnected=True) + connected_peers = self.getConnectedPeers(only_fully_connected=True) for peer in connected_peers: if peer.time_hashfield: continue diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index 31004d02..1cb0a445 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -311,7 +311,7 @@ class SiteAnnouncer(object): for _ in range(5): if not self.site.isServing(): return - peers = self.site.getConnectedPeers(onlyFullyConnected=True) + peers = self.site.getConnectedPeers(only_fully_connected=True) if len(peers) > 0: break time.sleep(2) diff --git a/src/Site/SiteHelpers.py b/src/Site/SiteHelpers.py index c095ad66..53105f65 100644 --- a/src/Site/SiteHelpers.py +++ b/src/Site/SiteHelpers.py @@ -124,7 +124,7 @@ class PeerConnector(object): self.spawnPeerConnectorController(); def processReqs2(self): - self.nr_connected_peers = len(self.site.getConnectedPeers(onlyFullyConnected=True)) + self.nr_connected_peers = len(self.site.getConnectedPeers(only_fully_connected=True)) self.processReqs(nr_connected_peers=self.nr_connected_peers) # For adding new peers when ConnectorController is working. @@ -140,6 +140,12 @@ class PeerConnector(object): if peer not in self.peers: self.peers.append(peer) + def deregisterPeer(self, peer): + try: + self.peers.remove(peer) + except: + pass + def sleep(self, t): self.site.connection_server.sleep(t) @@ -187,6 +193,8 @@ class PeerConnector(object): self.sleep(10) continue + added = 0 + # try connecting to peers while self.keepGoing() and len(self.peer_connector_workers) < self.peer_connector_worker_limit: if len(self.peers) < 1: @@ -204,13 +212,23 @@ class PeerConnector(object): thread = self.site.spawn(self.peerConnectorWorker, peer) self.peer_connector_workers[peer] = thread thread.link(lambda thread, peer=peer: self.peer_connector_workers.pop(peer, None)) + added += 1 + + if not self.keepGoing(): + break + + if not added: + # Looks like all known peers are either connected or being connected, + # so we weren't able to start connecting any peer in this iteration. + # Waiting for the announcer to discover some peers. + self.sleep(20) # wait for more room in self.peer_connector_workers while self.keepGoing() and len(self.peer_connector_workers) >= self.peer_connector_worker_limit: self.sleep(2) if not self.site.connection_server.isInternetOnline(): - self.sleep(20) + self.sleep(30) self.peers = list() self.peer_connector_controller = None From f484c0a1b8ffc8af6ae7e15a9a3375d1f532f9a8 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Tue, 26 Oct 2021 17:40:03 +0700 Subject: [PATCH 102/191] fine-tuning FileServer --- src/File/FileServer.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index 9ef3ae9c..f9f31163 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -21,6 +21,12 @@ from Debug import Debug log = logging.getLogger("FileServer") +class FakeThread(object): + def __init__(self): + pass + def ready(self): + return False + @PluginManager.acceptPlugins class FileServer(ConnectionServer): @@ -39,7 +45,7 @@ class FileServer(ConnectionServer): self.active_mode_thread_pool = gevent.pool.Pool(None) self.site_pool = gevent.pool.Pool(None) - self.update_pool = gevent.pool.Pool(5) + self.update_pool = gevent.pool.Pool(10) self.update_start_time = 0 self.update_sites_task_next_nr = 1 @@ -323,9 +329,18 @@ class FileServer(ConnectionServer): def updateSite(self, site, check_files=False, verify_files=False): if not site: return + if verify_files: + mode = 'verify' + elif check_files: + mode = 'check' + else: + mode = 'update' + log.info("running <%s> for %s" % (mode, site.address_short)) site.update2(check_files=check_files, verify_files=verify_files) def spawnUpdateSite(self, site, check_files=False, verify_files=False): + fake_thread = FakeThread() + self.update_threads[site.address] = fake_thread thread = self.update_pool.spawn(self.updateSite, site, check_files=check_files, verify_files=verify_files) self.update_threads[site.address] = thread @@ -395,8 +410,6 @@ class FileServer(ConnectionServer): continue sites_processed += 1 - - log.info("running for %s" % site.address_short) thread = self.spawnUpdateSite(site) if not self.isActiveMode(): @@ -440,8 +453,8 @@ class FileServer(ConnectionServer): def sitesVerificationThread(self): log.info("sitesVerificationThread started") - short_timeout = 10 - long_timeout = 60 + short_timeout = 20 + long_timeout = 120 self.sleep(long_timeout) @@ -477,8 +490,6 @@ class FileServer(ConnectionServer): else: continue - log.info("running <%s> for %s" % (mode, site.address_short)) - thread = self.spawnUpdateSite(site, check_files=check_files, verify_files=verify_files) From 32eb47c482f17455518c909975d86d8f4d14dae6 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Tue, 26 Oct 2021 22:32:28 +0700 Subject: [PATCH 103/191] a small fix in Peer --- src/Peer/Peer.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index 28a7220b..aad25110 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -217,6 +217,9 @@ class Peer(object): # Send a command to peer and return response value def request(self, cmd, params={}, stream_to=None): + if self.removed: + return False + if not self.connection or self.connection.closed: self.connect() if not self.connection: @@ -261,6 +264,9 @@ class Peer(object): # Get a file content from peer def getFile(self, site, inner_path, file_size=None, pos_from=0, pos_to=None, streaming=False): + if self.removed: + return False + if file_size and file_size > 5 * 1024 * 1024: max_read_size = 1024 * 1024 else: @@ -315,6 +321,9 @@ class Peer(object): # Send a ping request def ping(self, timeout=10.0, tryes=3): + if self.removed: + return False + response_time = None for retry in range(1, tryes): # Retry 3 times s = time.time() @@ -338,6 +347,9 @@ class Peer(object): # Request peer exchange from peer def pex(self, site=None, need_num=5, request_interval=60*2): + if self.removed: + return False + if not site: site = self.site # If no site defined request peers for this site @@ -391,6 +403,9 @@ class Peer(object): return self.request("listModified", {"since": since, "site": self.site.address}) def updateHashfield(self, force=False): + if self.removed: + return False + # Don't update hashfield again in 5 min if self.time_hashfield and time.time() - self.time_hashfield < 5 * 60 and not force: return False @@ -406,6 +421,9 @@ class Peer(object): # Find peers for hashids # Return: {hash1: ["ip:port", "ip:port",...],...} def findHashIds(self, hash_ids): + if self.removed: + return False + res = self.request("findHashIds", {"site": self.site.address, "hash_ids": hash_ids}) if not res or "error" in res or type(res) is not dict: return False @@ -449,6 +467,9 @@ class Peer(object): return True def publish(self, address, inner_path, body, modified, diffs=[]): + if self.removed: + return False + if len(body) > 10 * 1024 and self.connection and self.connection.handshake.get("rev", 0) >= 4095: # To save bw we don't push big content.json to peers body = b"" @@ -467,7 +488,9 @@ class Peer(object): self.log("Removing peer with reason: <%s>. Connection error: %s, Hash failed: %s" % (reason, self.connection_error, self.hash_failed)) if self.site: self.site.deregisterPeer(self) - self.site = None + # No way: self.site = None + # We don't assign None to self.site here because it leads to random exceptions in various threads, + # that hold references to the peer and still believe it belongs to the site. self.disconnect(reason) From ef69dcd331b4123c3bec267b23c26b0b4807afb1 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 27 Oct 2021 01:06:12 +0700 Subject: [PATCH 104/191] Implement Send Back LRU cache to reduce useless network transfers --- src/Config.py | 3 +++ src/Site/Site.py | 28 +++++++++++++++++++++++----- src/Site/SiteManager.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/src/Config.py b/src/Config.py index 67f65f7b..26fbafb2 100644 --- a/src/Config.py +++ b/src/Config.py @@ -268,6 +268,9 @@ class Config(object): self.parser.add_argument('--global_connected_limit', help='Max connections', default=512, type=int, metavar='global_connected_limit') self.parser.add_argument('--workers', help='Download workers per site', default=5, type=int, metavar='workers') + self.parser.add_argument('--send_back_lru_size', help='Size of the send back LRU cache', default=5000, type=int, metavar='send_back_lru_size') + self.parser.add_argument('--send_back_limit', help='Send no more than so many files at once back to peer, when we discovered that the peer held older file versions', default=3, type=int, metavar='send_back_limit') + self.parser.add_argument('--expose_no_ownership', help='By default, ZeroNet tries checking updates for own sites more frequently. This can be used by a third party for revealing the network addresses of a site owner. If this option is enabled, ZeroNet performs the checks in the same way for any sites.', type='bool', choices=[True, False], default=False) self.parser.add_argument('--fileserver_ip', help='FileServer bind address', default="*", metavar='ip') diff --git a/src/Site/Site.py b/src/Site/Site.py index d4f2ad62..73bb01dc 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -212,7 +212,6 @@ class Site(object): self.peer_connector = SiteHelpers.PeerConnector(self) # Connect more peers in background by request self.persistent_peer_req = None # The persistent peer requirement, managed by maintenance handler - if not self.settings.get("wrapper_key"): # To auth websocket permissions self.settings["wrapper_key"] = CryptHash.random() self.log.debug("New wrapper key: %s" % self.settings["wrapper_key"]) @@ -308,6 +307,12 @@ class Site(object): thread = self.greenlet_manager.spawnLater(*args, **kwargs) return thread + def checkSendBackLRU(self, peer, inner_path, remote_modified): + return SiteManager.site_manager.checkSendBackLRU(self, peer, inner_path, remote_modified) + + def addToSendBackLRU(self, peer, inner_path, modified): + return SiteManager.site_manager.addToSendBackLRU(self, peer, inner_path, modified) + def getSettingsCache(self): back = {} back["bad_files"] = self.bad_files @@ -619,7 +624,8 @@ class Site(object): queried.append(peer) modified_contents = [] send_back = [] - send_back_limit = 5 + send_back_limit = config.send_back_limit + send_back_skipped = 0 my_modified = self.content_manager.listModified(since) num_old_files = 0 for inner_path, modified in res["modified_files"].items(): # Check if the peer has newer files than we @@ -631,7 +637,10 @@ class Site(object): modified_contents.append(inner_path) self.bad_files[inner_path] = self.bad_files.get(inner_path, 1) if has_older: - send_back.append(inner_path) + if self.checkSendBackLRU(peer, inner_path, modified): + send_back_skipped += 1 + else: + send_back.append(inner_path) if modified_contents: self.log.info("CheckModifications: %s new modified files from %s" % (len(modified_contents), peer)) @@ -653,7 +662,10 @@ class Site(object): self.log.info("CheckModifications: %s: %s < %s" % ( inner_path, res["modified_files"][inner_path], my_modified.get(inner_path, 0) )) - self.spawn(self.publisher, inner_path, [peer], [], 1) + self.spawn(self.publisher, inner_path, [peer], [], 1, save_to_send_back_lru=True) + + if send_back_skipped: + self.log.info("CheckModifications: %s has older versions of %s files, skipped according to send back LRU" % (peer, send_back_skipped)) self.log.debug("CheckModifications: Waiting for %s pooledDownloadContent" % len(threads)) gevent.joinall(threads) @@ -842,7 +854,7 @@ class Site(object): gevent.joinall(content_threads) # Publish worker - def publisher(self, inner_path, peers, published, limit, diffs={}, event_done=None, cb_progress=None, max_retries=2): + def publisher(self, inner_path, peers, published, limit, diffs={}, event_done=None, cb_progress=None, max_retries=2, save_to_send_back_lru=False): file_size = self.storage.getSize(inner_path) content_json_modified = self.content_manager.contents[inner_path]["modified"] body = self.storage.read(inner_path) @@ -877,6 +889,12 @@ class Site(object): self.log.error("Publish error: %s" % Debug.formatException(err)) result = {"exception": Debug.formatException(err)} + # We add to the send back lru not only on success, but also on errors. + # Some peers returns None. (Why?) + # Anyway, we tried our best in delivering possibly lost updates. + if save_to_send_back_lru: + self.addToSendBackLRU(peer, inner_path, content_json_modified) + if result and "ok" in result: published.append(peer) if cb_progress and len(published) <= limit: diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index 1b065e93..035e9279 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -4,6 +4,7 @@ import re import os import time import atexit +import collections import gevent @@ -27,6 +28,21 @@ class SiteManager(object): gevent.spawn(self.saveTimer) atexit.register(lambda: self.save(recalculate_size=True)) + # ZeroNet has a bug of desyncing between: + # * time sent in a response of listModified + # and + # * time checked on receiving a file. + # This leads to the following scenario: + # * Request listModified. + # * Detect that the remote peer missing an update + # * Send a newer version of the file back to the peer. + # * The peer responses "ok: File not changed" + # ..... + # * Request listModified the next time and do all the same again. + # So we keep the list of sent back entries to prevent sending multiple useless updates: + # "{site.address} - {peer.key} - {inner_path}" -> mtime + self.send_back_lru = collections.OrderedDict() + # Load all sites from data/sites.json @util.Noparallel() def load(self, cleanup=True, startup=False): @@ -220,6 +236,23 @@ class SiteManager(object): self.load(startup=True) return self.sites + # Return False if we never sent to + # or if the file that was sent was older than + # so that send back logic is suppressed for . + # True if can be sent back to . + def checkSendBackLRU(self, site, peer, inner_path, remote_modified): + key = site.address + ' - ' + peer.key + ' - ' + inner_path + sent_modified = self.send_back_lru.get(key, 0) + return remote_modified < sent_modified + + def addToSendBackLRU(self, site, peer, inner_path, modified): + key = site.address + ' - ' + peer.key + ' - ' + inner_path + if self.send_back_lru.get(key, None) is None: + self.send_back_lru[key] = modified + while len(self.send_back_lru) > config.send_back_lru_size: + self.send_back_lru.popitem(last=False) + else: + self.send_back_lru.move_to_end(key, last=True) site_manager = SiteManager() # Singletone From 77e0bb3650ca3e79c0ae14e8de3840e7accbbaaa Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 27 Oct 2021 18:54:58 +0700 Subject: [PATCH 105/191] PeerDb plugin: save and restore fields time_response and connection_error --- plugins/PeerDb/PeerDbPlugin.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/plugins/PeerDb/PeerDbPlugin.py b/plugins/PeerDb/PeerDbPlugin.py index a66b81cf..aea11fbb 100644 --- a/plugins/PeerDb/PeerDbPlugin.py +++ b/plugins/PeerDb/PeerDbPlugin.py @@ -24,12 +24,14 @@ class ContentDbPlugin(object): ["hashfield", "BLOB"], ["reputation", "INTEGER NOT NULL"], ["time_added", "INTEGER NOT NULL"], - ["time_found", "INTEGER NOT NULL"] + ["time_found", "INTEGER NOT NULL"], + ["time_response", "INTEGER NOT NULL"], + ["connection_error", "INTEGER NOT NULL"] ], "indexes": [ "CREATE UNIQUE INDEX peer_key ON peer (site_id, address, port)" ], - "schema_changed": 2 + "schema_changed": 3 } return schema @@ -49,9 +51,15 @@ class ContentDbPlugin(object): num_hashfield += 1 peer.time_added = row["time_added"] peer.time_found = row["time_found"] - peer.reputation = row["reputation"] + peer.time_found = row["time_found"] + peer.time_response = row["time_response"] + peer.connection_error = row["connection_error"] if row["address"].endswith(".onion"): - peer.reputation = peer.reputation / 2 - 1 # Onion peers less likely working + # Onion peers less likely working + if peer.reputation > 0: + peer.reputation = peer.reputation / 2 + else: + peer.reputation -= 1 num += 1 if num_hashfield: site.content_manager.has_optional_files = True @@ -65,7 +73,7 @@ class ContentDbPlugin(object): hashfield = sqlite3.Binary(peer.hashfield.tobytes()) else: hashfield = "" - yield (site_id, address, port, hashfield, peer.reputation, int(peer.time_added), int(peer.time_found)) + yield (site_id, address, port, hashfield, peer.reputation, int(peer.time_added), int(peer.time_found), int(peer.time_response), int(peer.connection_error)) def savePeers(self, site, spawn=False): if spawn: @@ -80,7 +88,7 @@ class ContentDbPlugin(object): try: cur.execute("DELETE FROM peer WHERE site_id = :site_id", {"site_id": site_id}) cur.executemany( - "INSERT INTO peer (site_id, address, port, hashfield, reputation, time_added, time_found) VALUES (?, ?, ?, ?, ?, ?, ?)", + "INSERT INTO peer (site_id, address, port, hashfield, reputation, time_added, time_found, time_response, connection_error) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", self.iteratePeers(site) ) except Exception as err: From 168c436b735b1d0aa724907c7fc5fdbf0ed28413 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 27 Oct 2021 20:57:44 +0700 Subject: [PATCH 106/191] Add new configuration variables and temporarily disable Site.persistent_peer_req New configuration options: site_announce_interval_min site_announce_interval_max site_peer_check_interval_min site_peer_check_interval_max site_update_check_interval_min site_update_check_interval_max site_connectable_peer_count_max site_connectable_peer_count_min Site.persistent_peer_req is temporarily disabled since it makes excessive pressure on the network when working over TOR and needs some reworking. --- src/Config.py | 16 +++++++-- src/Site/Site.py | 78 ++++++++++++++++++++++++++++++++++++----- src/Site/SiteHelpers.py | 1 + 3 files changed, 85 insertions(+), 10 deletions(-) diff --git a/src/Config.py b/src/Config.py index 26fbafb2..15a0c87f 100644 --- a/src/Config.py +++ b/src/Config.py @@ -264,10 +264,22 @@ class Config(object): self.parser.add_argument('--size_limit', help='Default site size limit in MB', default=10, type=int, metavar='limit') self.parser.add_argument('--file_size_limit', help='Maximum per file size limit in MB', default=10, type=int, metavar='limit') - self.parser.add_argument('--connected_limit', help='Max connected peer per site', default=10, type=int, metavar='connected_limit') - self.parser.add_argument('--global_connected_limit', help='Max connections', default=512, type=int, metavar='global_connected_limit') + self.parser.add_argument('--connected_limit', help='Max number of connected peers per site. Soft limit.', default=10, type=int, metavar='connected_limit') + self.parser.add_argument('--global_connected_limit', help='Max number of connections. Soft limit.', default=512, type=int, metavar='global_connected_limit') self.parser.add_argument('--workers', help='Download workers per site', default=5, type=int, metavar='workers') + self.parser.add_argument('--site_announce_interval_min', help='Site announce interval for the most active sites, in minutes.', default=4, type=int, metavar='site_announce_interval_min') + self.parser.add_argument('--site_announce_interval_max', help='Site announce interval for inactive sites, in minutes.', default=30, type=int, metavar='site_announce_interval_max') + + self.parser.add_argument('--site_peer_check_interval_min', help='Connectable peers check interval for the most active sites, in minutes.', default=5, type=int, metavar='site_peer_check_interval_min') + self.parser.add_argument('--site_peer_check_interval_max', help='Connectable peers check interval for inactive sites, in minutes.', default=20, type=int, metavar='site_peer_check_interval_max') + + self.parser.add_argument('--site_update_check_interval_min', help='Site update check interval for the most active sites, in minutes.', default=5, type=int, metavar='site_update_check_interval_min') + self.parser.add_argument('--site_update_check_interval_max', help='Site update check interval for inactive sites, in minutes.', default=45, type=int, metavar='site_update_check_interval_max') + + self.parser.add_argument('--site_connectable_peer_count_max', help='Search for as many connectable peers for the most active sites', default=10, type=int, metavar='site_connectable_peer_count_max') + self.parser.add_argument('--site_connectable_peer_count_min', help='Search for as many connectable peers for inactive sites', default=2, type=int, metavar='site_connectable_peer_count_min') + self.parser.add_argument('--send_back_lru_size', help='Size of the send back LRU cache', default=5000, type=int, metavar='send_back_lru_size') self.parser.add_argument('--send_back_limit', help='Send no more than so many files at once back to peer, when we discovered that the peer held older file versions', default=3, type=int, metavar='send_back_limit') diff --git a/src/Site/Site.py b/src/Site/Site.py index 73bb01dc..0cea4733 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -30,6 +30,9 @@ from .SiteAnnouncer import SiteAnnouncer from . import SiteManager from . import SiteHelpers +def lerp(val_min, val_max, scale): + return scale * (val_max - val_min) + val_min + class ScaledTimeoutHandler: def __init__(self, val_min, val_max, handler=None, scaler=None): self.val_min = val_min @@ -40,7 +43,7 @@ class ScaledTimeoutHandler: self.log = logging.getLogger("ScaledTimeoutHandler") def isExpired(self, scale): - interval = scale * (self.val_max - self.val_min) + self.val_min + interval = lerp(self.val_min, self.val_max, scale) expired_at = self.timestamp + interval now = time.time() expired = (now > expired_at) @@ -158,10 +161,19 @@ class Site(object): self.addEventListeners() self.periodic_maintenance_handlers = [ - ScaledTimeoutHandler(60 * 30, 60 * 2, + ScaledTimeoutHandler( + config.site_announce_interval_max * 60, + config.site_announce_interval_min * 60, handler=self.periodicMaintenanceHandler_announce, scaler=self.getAnnounceRating), - ScaledTimeoutHandler(60 * 30, 60 * 5, + ScaledTimeoutHandler( + config.site_peer_check_interval_max * 60, + config.site_peer_check_interval_min * 60, + handler=self.periodicMaintenanceHandler_peer_check, + scaler=self.getAnnounceRating), + ScaledTimeoutHandler( + config.site_update_check_interval_max * 60, + config.site_update_check_interval_min * 60, handler=self.periodicMaintenanceHandler_general, scaler=self.getActivityRating) ] @@ -1279,6 +1291,16 @@ class Site(object): v = [activity_rating, peer_rating, tracker_rating] return sum(v) / float(len(v)) + def getPreferableConnectablePeerCount(self): + if not self.isServing(): + return 0 + + count = lerp( + config.site_connectable_peer_count_min, + config.site_connectable_peer_count_max, + self.getActivityRating(force_safe=True)) + return count + # The engine tries to maintain the number of active connections: # >= getPreferableActiveConnectionCount() # and @@ -1496,6 +1518,33 @@ class Site(object): self.log.debug("Connected: %s, Need to close: %s, Closed: %s" % ( len(connected_peers), need_to_close, closed)) + def lookForConnectablePeers(self): + num_tries = 2 + need_connectable_peers = self.getPreferableConnectablePeerCount() + + connectable_peers = 0 + reachable_peers = [] + + for peer in list(self.peers.values()): + if peer.isConnected() or peer.isConnectable(): + connectable_peers += 1 + elif peer.isReachable(): + reachable_peers.append(peer) + if connectable_peers >= need_connectable_peers: + return True + + random.shuffle(reachable_peers) + + for peer in reachable_peers: + if peer.isConnected() or peer.isConnectable() or peer.removed: + continue + peer.ping() + if peer.isConnected(): + peer.pex() + num_tries -= 1 + if num_tries < 1: + break + @util.Noparallel(queue=True) def runPeriodicMaintenance(self, startup=False, force=False): if not self.isServing(): @@ -1519,11 +1568,8 @@ class Site(object): self.log.debug("periodicMaintenanceHandler_general: startup=%s, force=%s" % (startup, force)) - if not startup: - self.cleanupPeers() - - self.persistent_peer_req = self.needConnections(update_site_on_reconnect=True) - self.persistent_peer_req.result_connected.wait(timeout=2.0) + #self.persistent_peer_req = self.needConnections(update_site_on_reconnect=True) + #self.persistent_peer_req.result_connected.wait(timeout=2.0) #self.announcer.announcePex() @@ -1533,6 +1579,22 @@ class Site(object): return True + def periodicMaintenanceHandler_peer_check(self, startup=False, force=False): + if not self.isServing(): + return False + + if not self.peers: + return False + + self.log.debug("periodicMaintenanceHandler_peer_check: startup=%s, force=%s" % (startup, force)) + + if not startup: + self.cleanupPeers() + + self.lookForConnectablePeers() + + return True + def periodicMaintenanceHandler_announce(self, startup=False, force=False): if not self.isServing(): return False diff --git a/src/Site/SiteHelpers.py b/src/Site/SiteHelpers.py index 53105f65..90a298cf 100644 --- a/src/Site/SiteHelpers.py +++ b/src/Site/SiteHelpers.py @@ -156,6 +156,7 @@ class PeerConnector(object): if not peer.isConnected(): peer.connect() if peer.isConnected(): + peer.ping() self.processReqs2() def peerConnectorController(self): From d32d9f781b81555d64e4b29fdafd497fbf2e31c9 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 3 Nov 2021 11:48:02 +0700 Subject: [PATCH 107/191] Move getIpType() from helper to ConnectionServer --- .../AnnounceBitTorrentPlugin.py | 4 +- plugins/TrackerZero/TrackerZeroPlugin.py | 3 +- .../BootstrapperPlugin.py | 2 +- .../Test/TestBootstrapper.py | 6 +- src/Connection/Connection.py | 6 +- src/Connection/ConnectionServer.py | 36 ++++++++++- src/File/FileRequest.py | 4 +- src/File/FileServer.py | 8 +-- src/Peer/Peer.py | 63 ++++++++++++------- src/Site/SiteAnnouncer.py | 20 +++--- src/Site/SiteManager.py | 1 + src/util/helper.py | 3 +- 12 files changed, 109 insertions(+), 47 deletions(-) diff --git a/plugins/AnnounceBitTorrent/AnnounceBitTorrentPlugin.py b/plugins/AnnounceBitTorrent/AnnounceBitTorrentPlugin.py index fab7bb1f..734070dd 100644 --- a/plugins/AnnounceBitTorrent/AnnounceBitTorrentPlugin.py +++ b/plugins/AnnounceBitTorrent/AnnounceBitTorrentPlugin.py @@ -52,7 +52,7 @@ class SiteAnnouncerPlugin(object): ip, port = tracker_address.split("/")[0].split(":") tracker = UdpTrackerClient(ip, int(port)) - if helper.getIpType(ip) in self.getOpenedServiceTypes(): + if self.connection_server.getIpType(ip) in self.getOpenedServiceTypes(): tracker.peer_port = self.fileserver_port else: tracker.peer_port = 0 @@ -101,7 +101,7 @@ class SiteAnnouncerPlugin(object): def announceTrackerHttp(self, tracker_address, mode="start", num_want=10, protocol="http"): tracker_ip, tracker_port = tracker_address.rsplit(":", 1) - if helper.getIpType(tracker_ip) in self.getOpenedServiceTypes(): + if self.connection_server.getIpType(tracker_ip) in self.getOpenedServiceTypes(): port = self.fileserver_port else: port = 1 diff --git a/plugins/TrackerZero/TrackerZeroPlugin.py b/plugins/TrackerZero/TrackerZeroPlugin.py index e90f085d..a59bc309 100644 --- a/plugins/TrackerZero/TrackerZeroPlugin.py +++ b/plugins/TrackerZero/TrackerZeroPlugin.py @@ -122,7 +122,8 @@ class TrackerZero(object): time_onion_check = time.time() - s - ip_type = helper.getIpType(file_request.connection.ip) + connection_server = file_request.server + ip_type = connection_server.getIpType(file_request.connection.ip) if ip_type == "onion" or file_request.connection.ip in config.ip_local: is_port_open = False diff --git a/plugins/disabled-Bootstrapper/BootstrapperPlugin.py b/plugins/disabled-Bootstrapper/BootstrapperPlugin.py index 59e7af7b..5ddc36b6 100644 --- a/plugins/disabled-Bootstrapper/BootstrapperPlugin.py +++ b/plugins/disabled-Bootstrapper/BootstrapperPlugin.py @@ -49,7 +49,7 @@ class FileRequestPlugin(object): time_onion_check = time.time() - s - ip_type = helper.getIpType(self.connection.ip) + ip_type = self.server.getIpType(self.connection.ip) if ip_type == "onion" or self.connection.ip in config.ip_local: is_port_open = False diff --git a/plugins/disabled-Bootstrapper/Test/TestBootstrapper.py b/plugins/disabled-Bootstrapper/Test/TestBootstrapper.py index 69bdc54c..198cd022 100644 --- a/plugins/disabled-Bootstrapper/Test/TestBootstrapper.py +++ b/plugins/disabled-Bootstrapper/Test/TestBootstrapper.py @@ -28,7 +28,7 @@ def bootstrapper_db(request): @pytest.mark.usefixtures("resetSettings") class TestBootstrapper: def testHashCache(self, file_server, bootstrapper_db): - ip_type = helper.getIpType(file_server.ip) + ip_type = file_server.getIpType(file_server.ip) peer = Peer(file_server.ip, 1544, connection_server=file_server) hash1 = hashlib.sha256(b"site1").digest() hash2 = hashlib.sha256(b"site2").digest() @@ -50,7 +50,7 @@ class TestBootstrapper: def testBootstrapperDb(self, file_server, bootstrapper_db): - ip_type = helper.getIpType(file_server.ip) + ip_type = file_server.getIpType(file_server.ip) peer = Peer(file_server.ip, 1544, connection_server=file_server) hash1 = hashlib.sha256(b"site1").digest() hash2 = hashlib.sha256(b"site2").digest() @@ -111,7 +111,7 @@ class TestBootstrapper: def testPassive(self, file_server, bootstrapper_db): peer = Peer(file_server.ip, 1544, connection_server=file_server) - ip_type = helper.getIpType(file_server.ip) + ip_type = file_server.getIpType(file_server.ip) hash1 = hashlib.sha256(b"hash1").digest() bootstrapper_db.peerAnnounce(ip_type, address=None, port=15441, hashes=[hash1]) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 40519b7f..c147ee35 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -23,6 +23,7 @@ class Connection(object): ) def __init__(self, server, ip, port, sock=None, target_onion=None, is_tracker_connection=False): + self.server = server self.sock = sock self.cert_pin = None if "#" in ip: @@ -42,7 +43,6 @@ class Connection(object): self.is_private_ip = False self.is_tracker_connection = is_tracker_connection - self.server = server self.unpacker = None # Stream incoming socket messages here self.unpacker_bytes = 0 # How many bytes the unpacker received self.req_id = 0 # Last request id @@ -81,11 +81,11 @@ class Connection(object): def setIp(self, ip): self.ip = ip - self.ip_type = helper.getIpType(ip) + self.ip_type = self.server.getIpType(ip) self.updateName() def createSocket(self): - if helper.getIpType(self.ip) == "ipv6" and not hasattr(socket, "socket_noproxy"): + if self.server.getIpType(self.ip) == "ipv6" and not hasattr(socket, "socket_noproxy"): # Create IPv6 connection as IPv4 when using proxy return socket.socket(socket.AF_INET6, socket.SOCK_STREAM) else: diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index 16834ff5..9f24e377 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -1,4 +1,5 @@ import logging +import re import time import sys import socket @@ -253,7 +254,7 @@ class ConnectionServer(object): pass def getConnection(self, ip=None, port=None, peer_id=None, create=True, site=None, is_tracker_connection=False): - ip_type = helper.getIpType(ip) + ip_type = self.getIpType(ip) has_per_site_onion = (ip.endswith(".onion") or self.port_opened.get(ip_type, None) == False) and self.tor_manager.start_onions and site if has_per_site_onion: # Site-unique connection for Tor if ip.endswith(".onion"): @@ -520,16 +521,47 @@ class ConnectionServer(object): median = (corrections[mid - 1] + corrections[mid] + corrections[mid + 1]) / 3 return median + + ############################################################################ + + # Methods for handling network address types + # (ipv4, ipv6, onion etc... more to be implemented by plugins) + # + # All the functions handling network address types have "Ip" in the name. + # So it was in the initial codebase, and I keep the naming, since I couldn't + # think of a better option. + # "IP" is short and quite clear and lets you understand that a variable + # contains a peer address or other transport-level address and not + # an address of ZeroNet site. + # + + # Returns type of the given network address. + # Since: 0.8.0 + # Replaces helper.getIpType() in order to be extensible by plugins. + def getIpType(self, ip): + if ip.endswith(".onion"): + return "onion" + elif ":" in ip: + return "ipv6" + elif re.match(r"[0-9\.]+$", ip): + return "ipv4" + else: + return "unknown" + # Checks if a network address can be reachable in the current configuration # and returs a string describing why it cannot. # If the network address can be reachable, returns False. + # Since: 0.8.0 def getIpUnreachability(self, ip): - ip_type = helper.getIpType(ip) + ip_type = self.getIpType(ip) if ip_type == 'onion' and not self.tor_manager.enabled: return "Can't connect to onion addresses, no Tor controller present" if config.tor == "always" and helper.isPrivateIp(ip) and ip not in config.ip_local: return "Can't connect to local IPs in Tor: always mode" return False + # Returns True if ConnctionServer has means for establishing outgoing + # connections to the given address. + # Since: 0.8.0 def isIpReachable(self, ip): return self.getIpUnreachability(ip) == False diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index f7249d81..8a16e591 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -376,7 +376,7 @@ class FileRequest(object): for hash_id, peers in found.items(): for peer in peers: - ip_type = helper.getIpType(peer.ip) + ip_type = self.server.getIpType(peer.ip) if len(back[ip_type][hash_id]) < 20: back[ip_type][hash_id].append(peer.packMyAddress()) return back @@ -430,7 +430,7 @@ class FileRequest(object): # Check requested port of the other peer def actionCheckport(self, params): - if helper.getIpType(self.connection.ip) == "ipv6": + if self.server.getIpType(self.connection.ip) == "ipv6": sock_address = (self.connection.ip, params["port"], 0, 0) else: sock_address = (self.connection.ip, params["port"]) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index f9f31163..ac4b8c55 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -57,7 +57,7 @@ class FileServer(ConnectionServer): self.supported_ip_types = ["ipv4"] # Outgoing ip_type support - if helper.getIpType(ip) == "ipv6" or self.isIpv6Supported(): + if self.getIpType(ip) == "ipv6" or self.isIpv6Supported(): self.supported_ip_types.append("ipv6") if ip_type == "ipv6" or (ip_type == "dual" and "ipv6" in self.supported_ip_types): @@ -217,7 +217,7 @@ class FileServer(ConnectionServer): for ip_external in config.ip_external: SiteManager.peer_blacklist.append((ip_external, self.port)) # Add myself to peer blacklist - ip_external_types = set([helper.getIpType(ip) for ip in config.ip_external]) + ip_external_types = set([self.getIpType(ip) for ip in config.ip_external]) res = { "ipv4": "ipv4" in ip_external_types, "ipv6": "ipv6" in ip_external_types @@ -245,7 +245,7 @@ class FileServer(ConnectionServer): res_ipv6 = {"ip": None, "opened": None} else: res_ipv6 = res_ipv6_thread.get() - if res_ipv6["opened"] and not helper.getIpType(res_ipv6["ip"]) == "ipv6": + if res_ipv6["opened"] and not self.getIpType(res_ipv6["ip"]) == "ipv6": log.info("Invalid IPv6 address from port check: %s" % res_ipv6["ip"]) res_ipv6["opened"] = False @@ -266,7 +266,7 @@ class FileServer(ConnectionServer): for ip in interface_ips: if not helper.isPrivateIp(ip) and ip not in self.ip_external_list: self.ip_external_list.append(ip) - res[helper.getIpType(ip)] = True # We have opened port if we have external ip + res[self.getIpType(ip)] = True # We have opened port if we have external ip SiteManager.peer_blacklist.append((ip, self.port)) log.debug("External ip found on interfaces: %s" % ip) diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index aad25110..6e4863e0 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -32,7 +32,7 @@ class Peer(object): self.site = site self.key = "%s:%s" % (ip, port) - self.ip_type = helper.getIpType(ip) + self.ip_type = None self.removed = False @@ -53,7 +53,7 @@ class Peer(object): self.reputation = 0 # More likely to connect if larger self.last_content_json_update = 0.0 # Modify date of last received content.json self.protected = 0 - self.reachable = False + self.reachable = None self.connection_error = 0 # Series of connection error self.hash_failed = 0 # Number of bad files from peer @@ -62,15 +62,14 @@ class Peer(object): self.protectedRequests = ["getFile", "streamFile", "update", "listModified"] - self.updateReachable() - def __getattr__(self, key): if key == "hashfield": self.has_hashfield = True self.hashfield = PeerHashfield() return self.hashfield else: - return getattr(self, key) + # Raise appropriately formatted attribute error + return object.__getattribute__(self, key) def log(self, text, log_level = None): if log_level is None: @@ -98,26 +97,18 @@ class Peer(object): self.protected = 0 return self.protected > 0 + def isTtlExpired(self, ttl): + last_activity = max(self.time_found, self.time_response) + return (time.time() - last_activity) > ttl + + # Since 0.8.0 def isConnected(self): if self.connection and not self.connection.connected: self.connection = None return self.connection and self.connection.connected - def isTtlExpired(self, ttl): - last_activity = max(self.time_found, self.time_response) - return (time.time() - last_activity) > ttl - - def isReachable(self): - return self.reachable - - def updateReachable(self): - connection_server = self.getConnectionServer() - if not self.port: - self.reachable = False - else: - self.reachable = connection_server.isIpReachable(self.ip) - # Peer proved to to be connectable recently + # Since 0.8.0 def isConnectable(self): if self.connection_error >= 1: # The last connection attempt failed return False @@ -125,6 +116,36 @@ class Peer(object): return False return self.isReachable() + # Since 0.8.0 + def isReachable(self): + if self.reachable is None: + self.updateCachedState() + return self.reachable + + # Since 0.8.0 + def getIpType(self): + if not self.ip_type: + self.updateCachedState() + return self.ip_type + + # We cache some ConnectionServer-related state for better performance. + # This kind of state currently doesn't change during a program session, + # and it's safe to read and cache it just once. But future versions + # may bring more pieces of dynamic configuration. So we update the state + # on each peer.found(). + def updateCachedState(self): + connection_server = self.getConnectionServer() + if not self.port or self.port == 1: # Port 1 considered as "no open port" + self.reachable = False + else: + self.reachable = connection_server.isIpReachable(self.ip) + self.ip_type = connection_server.getIpType(self.ip) + + + # FIXME: + # This should probably be changed. + # When creating a peer object, the caller must provide either `connection_server`, + # or `site`, so Peer object is able to use `site.connection_server`. def getConnectionServer(self): if self.connection_server: connection_server = self.connection_server @@ -179,7 +200,7 @@ class Peer(object): if self.connection and self.connection.connected: # We have connection to peer return self.connection else: # Try to find from other sites connections - self.connection = self.site.connection_server.getConnection(self.ip, self.port, create=False, site=self.site) + self.connection = self.getConnectionServer().getConnection(self.ip, self.port, create=False, site=self.site) if self.connection: self.connection.sites += 1 return self.connection @@ -213,7 +234,7 @@ class Peer(object): if source in ("tracker", "local"): self.site.peers_recent.appendleft(self) self.time_found = time.time() - self.updateReachable() + self.updateCachedState() # Send a command to peer and return response value def request(self, cmd, params={}, stream_to=None): diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index 1cb0a445..1baf39af 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -35,19 +35,25 @@ class SiteAnnouncer(object): self.time_last_announce = 0 self.supported_tracker_count = 0 + # Returns connection_server rela + # Since 0.8.0 + @property + def connection_server(self): + return self.site.connection_server + def getTrackers(self): return config.trackers def getSupportedTrackers(self): trackers = self.getTrackers() - if not self.site.connection_server.tor_manager.enabled: + if not self.connection_server.tor_manager.enabled: trackers = [tracker for tracker in trackers if ".onion" not in tracker] trackers = [tracker for tracker in trackers if self.getAddressParts(tracker)] # Remove trackers with unknown address - if "ipv6" not in self.site.connection_server.supported_ip_types: - trackers = [tracker for tracker in trackers if helper.getIpType(self.getAddressParts(tracker)["ip"]) != "ipv6"] + if "ipv6" not in self.connection_server.supported_ip_types: + trackers = [tracker for tracker in trackers if self.connection_server.getIpType(self.getAddressParts(tracker)["ip"]) != "ipv6"] return trackers @@ -118,10 +124,10 @@ class SiteAnnouncer(object): back = [] # Type of addresses they can reach me if config.trackers_proxy == "disable" and config.tor != "always": - for ip_type, opened in list(self.site.connection_server.port_opened.items()): + for ip_type, opened in list(self.connection_server.port_opened.items()): if opened: back.append(ip_type) - if self.site.connection_server.tor_manager.start_onions: + if self.connection_server.tor_manager.start_onions: back.append("onion") return back @@ -204,11 +210,11 @@ class SiteAnnouncer(object): self.stats[tracker]["time_status"] = time.time() self.stats[tracker]["last_error"] = str(error) self.stats[tracker]["time_last_error"] = time.time() - if self.site.connection_server.has_internet: + if self.connection_server.has_internet: self.stats[tracker]["num_error"] += 1 self.stats[tracker]["num_request"] += 1 global_stats[tracker]["num_request"] += 1 - if self.site.connection_server.has_internet: + if self.connection_server.has_internet: global_stats[tracker]["num_error"] += 1 self.updateWebsocket(tracker="error") return False diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index 035e9279..8175a1f5 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -172,6 +172,7 @@ class SiteManager(object): return self.resolveDomain(domain) # Checks if the address is blocked. To be implemented in content filter plugins. + # Since 0.8.0 def isAddressBlocked(self, address): return False diff --git a/src/util/helper.py b/src/util/helper.py index 61455b08..f44bcfce 100644 --- a/src/util/helper.py +++ b/src/util/helper.py @@ -290,7 +290,8 @@ local_ip_pattern = re.compile(r"^127\.|192\.168\.|10\.|172\.1[6-9]\.|172\.2[0-9] def isPrivateIp(ip): return local_ip_pattern.match(ip) - +# XXX: Deprecated. Use ConnectionServer.getIpType() instead. +# To be removed in 0.9.0 def getIpType(ip): if ip.endswith(".onion"): return "onion" From 18630435051d492b7a953c461392cab0dd7576ef Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 3 Nov 2021 12:59:49 +0700 Subject: [PATCH 108/191] Update ChangeLog-0.8.0.md --- ZNE-ChangeLog/ChangeLog-0.8.0.md | 34 +++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/ZNE-ChangeLog/ChangeLog-0.8.0.md b/ZNE-ChangeLog/ChangeLog-0.8.0.md index a2bba51b..568b015d 100644 --- a/ZNE-ChangeLog/ChangeLog-0.8.0.md +++ b/ZNE-ChangeLog/ChangeLog-0.8.0.md @@ -1,14 +1,17 @@ ## Core -**Network:** +**Support for Onion v3:** -* Added support of Onion v3 addresses. Thanks to @anonymoose and @zeroseed. -* Added a few Onion v3 tracker addresses. +* Add initial support of Onion v3 addresses. Thanks to @anonymoose and @zeroseed. +* Add a few Onion v3 tracker addresses. + +**Network scalabily and performance issuses:** + +* The connection handling code had several bugs that were hidden by silently ignored exceptions. These were fixed, but some new ones might be introduced. * Reworked the algorithm of checking zite updates on startup / after the network outages / periodically. ZeroNet tries not to spam too many update queries at once in order to prevent network overload. (Which especially the issue when running over Tor.) At the same time, it tries to keep balance between frequent checks for frequently updating zites and ensuring that all the zites are checked in some reasonable time interval. Tests show that the full check cycle for a peer that hosts 800+ zites and is connected over Tor can take up to several hours. We cannot significantly reduce this time, since the Tor throughput is the bottleneck. Running more checks at once just results in more connections to fail. The other bottleneck is the HDD throughput. Increasing the parallelization doesn't help in this case as well. So the implemented solution do actually **decreases** the parallelization. * Improved the Internet outage detection and the recovery procedures after the Internet be back. ZeroNet "steps back" and schedules rechecking zites that were checked shortly before the Internet connection get lost. The network outage detection normally has some lag, so the recently checked zites are better to be checked again. * When the network is down, reduce the frequency of connection attempts to prevent overloading Tor with hanged connections. -* The connection handling code had several bugs that were hidden by silently ignored exceptions. These were fixed, but some new ones might be introduced. * For each zite the activity rate is calculated based on the last modification time. The milestone values are 1 hour, 5 hours, 24 hours, 3 days and 7 days. The activity rate is used to scale frequency of various maintenance routines, including update checks, reannounces, dead connection checks etc. * The activity rate is also used to calculate the minimum preferred number of active connections per each zite. * The reannounce frequency is adjusted dynamically based on: @@ -17,15 +20,28 @@ * Tracker count. More trackers ==> frequent announces to iterate over more trackers. * For owned zites, the activity rate doesn't drop below 0.6 to force more frequent checks. This, however, can be used to investigate which peer belongs to the zite owner. A new commnd line option `--expose_no_ownership` is introduced to disable that behavior. * When checking for updates, ZeroNet normally asks other peers for new data since the previous update. This however can result in losing some updates in specific conditions. To overcome this, ZeroNet now asks for the full site listing on every Nth update check. -* When asking a peer for updates, ZeroNet may see that the other peer has an older version of a file. In this case, ZeroNet sends back the notification of the new version available. The logic in 0.8.0 is generally the same, but some randomization is added which may help in distributing the "update waves" among peers. +* When asking a peer for updates, ZeroNet may detect that the other peer has outdated versions of some files. In this case, ZeroNet sends back the notification of the new versions available. This behaviour was improved in the following ways: + * Prior to 0.8.0 ZeroNet always chose the first 5 files in the list. If peer rejects updating those files for some reason, this strategy led to pushing the same 5 files over and over again on very update check. Now files are selected randomly. This should also improve spreading of "update waves" among peers. + * The engine now keeps track of which files were already sent to a specific peer and doesn't send those again as long as the file timestamp hasn't changed. Now we do not try to resend files if a peer refuses to update them. The watchlist is currently limited to 5000 items. + * The previous two changes should make the send back mechanism more efficient. So number of files to send at once reduced from 5 to 3 to reduce the network load. * ZeroNet now tries harder in delivering updates to more peers in the background. * ZeroNet also makes more efforts of searching the peers before publishing updates. -**Other:** +**Other scalabily and performance issuses:** -* Implemented the log level overriding for separate modules for easier debugging. -* Make the site block check implemented in `ContentFilter` usable from plugins and core modules via `SiteManager.isAddressBlocked()`. -* Fixed possible infinite growing of the `SafeRe` regexp cache by limiting the cache size. +* Fix possible infinite growing of the `SafeRe` regexp cache by limiting the cache size. + +**Changes in API and architecture:** + +* Add method `SiteManager.isAddressBlocked()` available for overriding by content filter plugins in order for other plugins be able explicitly check site block status. +* Hard-coded logic related to the network operation modes (TOR mode, proxies, network address types etc) in many places replaced with appropriate method calls and lots of new methods introduced. These changes are made in order to make it someday possible implementing support for different types of overlay networks (first of all, we mean I2P) in plugins without hardcoding more stuff to the core. A lot of refactoring is yet to come. Changes include but are not limited to: + * `helper.getIpType()` moved to `ConnectionServer.getIpType()`. + * New methods `ConnectionServer.getIpUnreachability()`, `ConnectionServer.isIpReachable()`. + * New methods `Peer.isConnected()`, `Peer.isConnectable()`, `Peer.isReachable()`, `Peer.getIpType()`. + +**Other changes:** + +* Implement the log level overriding for separate modules for easier debugging. ## Docker Image From e000eae04640698d870723e4ad45ef73f643c7dd Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 3 Nov 2021 16:06:06 +0700 Subject: [PATCH 109/191] Add new configuration option: simultaneous_connection_throttle_threshold --- src/Config.py | 2 ++ src/Connection/Connection.py | 14 +++++++++++++- src/Connection/ConnectionServer.py | 17 +++++++++++++++++ src/File/FileServer.py | 13 +++++++++++++ 4 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 15a0c87f..a9e05a6f 100644 --- a/src/Config.py +++ b/src/Config.py @@ -285,6 +285,8 @@ class Config(object): self.parser.add_argument('--expose_no_ownership', help='By default, ZeroNet tries checking updates for own sites more frequently. This can be used by a third party for revealing the network addresses of a site owner. If this option is enabled, ZeroNet performs the checks in the same way for any sites.', type='bool', choices=[True, False], default=False) + self.parser.add_argument('--simultaneous_connection_throttle_threshold', help='Throttle opening new connections when the number of outgoing connections in not fully established state exceeds the threshold.', default=15, type=int, metavar='simultaneous_connection_throttle_threshold') + self.parser.add_argument('--fileserver_ip', help='FileServer bind address', default="*", metavar='ip') self.parser.add_argument('--fileserver_port', help='FileServer bind port (0: randomize)', default=0, type=int, metavar='port') self.parser.add_argument('--fileserver_port_range', help='FileServer randomization range', default="10000-40000", metavar='port') diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index c147ee35..44665b2f 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -17,7 +17,7 @@ from util import helper class Connection(object): __slots__ = ( "sock", "sock_wrapped", "ip", "port", "cert_pin", "target_onion", "id", "protocol", "type", "server", "unpacker", "unpacker_bytes", "req_id", "ip_type", - "handshake", "crypt", "connected", "event_connected", "closed", "start_time", "handshake_time", "last_recv_time", "is_private_ip", "is_tracker_connection", + "handshake", "crypt", "connected", "connecting", "event_connected", "closed", "start_time", "handshake_time", "last_recv_time", "is_private_ip", "is_tracker_connection", "last_message_time", "last_send_time", "last_sent_time", "incomplete_buff_recv", "bytes_recv", "bytes_sent", "cpu_time", "send_lock", "last_ping_delay", "last_req_time", "last_cmd_sent", "last_cmd_recv", "bad_actions", "sites", "name", "waiting_requests", "waiting_streams" ) @@ -50,6 +50,7 @@ class Connection(object): self.crypt = None # Connection encryption method self.sock_wrapped = False # Socket wrapped to encryption + self.connecting = False self.connected = False self.event_connected = gevent.event.AsyncResult() # Solves on handshake received self.closed = False @@ -118,6 +119,15 @@ class Connection(object): # Open connection to peer and wait for handshake def connect(self): + self.connecting = True + try: + return self._connect() + except Exception as err: + self.connecting = False + self.connected = False + raise + + def _connect(self): self.updateOnlineStatus(outgoing_activity=True) if not self.event_connected or self.event_connected.ready(): @@ -236,6 +246,7 @@ class Connection(object): self.protocol = "v2" self.updateName() self.connected = True + self.connecting = False buff_len = 0 req_len = 0 self.unpacker_bytes = 0 @@ -634,6 +645,7 @@ class Connection(object): return False # Already closed self.closed = True self.connected = False + self.connecting = False if self.event_connected: self.event_connected.set(False) diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index 9f24e377..9c16b774 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -298,6 +298,7 @@ class ConnectionServer(object): raise Exception("This peer is blacklisted") try: + #self.log.info("Connection to: %s:%s", ip, port) if has_per_site_onion: # Lock connection to site connection = Connection(self, ip, port, target_onion=site_onion, is_tracker_connection=is_tracker_connection) else: @@ -312,6 +313,7 @@ class ConnectionServer(object): raise Exception("Connection event return error") except Exception as err: + #self.log.info("Connection error (%s, %s): %s", ip, port, Debug.formatException(err)) connection.close("%s Connect error: %s" % (ip, Debug.formatException(err))) raise err @@ -429,6 +431,21 @@ class ConnectionServer(object): )) return num_closed + # Returns True if we should slow down opening new connections as at the moment + # there are too many connections being established and not connected completely + # (not entered the message loop yet). + def shouldThrottleNewConnections(self): + threshold = config.simultaneous_connection_throttle_threshold + if len(self.connections) <= threshold: + return False + nr_connections_being_established = 0 + for connection in self.connections[:]: # Make a copy + if connection.connecting and not connection.connected and connection.type == "out": + nr_connections_being_established += 1 + if nr_connections_being_established > threshold: + return True + return False + # Internet outage detection def updateOnlineStatus(self, connection, outgoing_activity=False, successful_activity=False): diff --git a/src/File/FileServer.py b/src/File/FileServer.py index ac4b8c55..66cefd39 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -401,6 +401,9 @@ class FileServer(ConnectionServer): self.sleep(1) self.waitForInternetOnline() + while self.isActiveMode() and self.shouldThrottleNewConnections(): + self.sleep(1) + if not self.isActiveMode(): break @@ -463,6 +466,9 @@ class FileServer(ConnectionServer): self.sleep(short_timeout) self.waitForInternetOnline() + while self.isActiveMode() and self.shouldThrottleNewConnections(): + self.sleep(1) + if not self.isActiveMode(): break @@ -509,6 +515,9 @@ class FileServer(ConnectionServer): while self.isActiveMode(): self.sleep(long_timeout) + while self.isActiveMode() and self.shouldThrottleNewConnections(): + self.sleep(1) + if not self.isActiveMode(): break @@ -591,6 +600,10 @@ class FileServer(ConnectionServer): threshold = self.internet_outage_threshold / 2.0 self.sleep(threshold / 2.0) + + while self.isActiveMode() and self.shouldThrottleNewConnections(): + self.sleep(1) + if not self.isActiveMode(): break From b6b23d0e8e163978b34d22974f1cfc5c9d098393 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Wed, 3 Nov 2021 17:15:43 +0700 Subject: [PATCH 110/191] In Site.updater(), properly detect the case when peer has no file at all, not just an older version. --- ZNE-ChangeLog/ChangeLog-0.8.0.md | 3 +- src/Site/Site.py | 47 ++++++++++++++++++++++---------- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/ZNE-ChangeLog/ChangeLog-0.8.0.md b/ZNE-ChangeLog/ChangeLog-0.8.0.md index 568b015d..b51effef 100644 --- a/ZNE-ChangeLog/ChangeLog-0.8.0.md +++ b/ZNE-ChangeLog/ChangeLog-0.8.0.md @@ -23,7 +23,8 @@ * When asking a peer for updates, ZeroNet may detect that the other peer has outdated versions of some files. In this case, ZeroNet sends back the notification of the new versions available. This behaviour was improved in the following ways: * Prior to 0.8.0 ZeroNet always chose the first 5 files in the list. If peer rejects updating those files for some reason, this strategy led to pushing the same 5 files over and over again on very update check. Now files are selected randomly. This should also improve spreading of "update waves" among peers. * The engine now keeps track of which files were already sent to a specific peer and doesn't send those again as long as the file timestamp hasn't changed. Now we do not try to resend files if a peer refuses to update them. The watchlist is currently limited to 5000 items. - * The previous two changes should make the send back mechanism more efficient. So number of files to send at once reduced from 5 to 3 to reduce the network load. + * Prior to 0.8.0 ZeroNet failed to detect the case when remote peer misses a file at all, not just has an older version. Now it is handled properly and those files also can be send back. + * The previous changes should make the send back mechanism more efficient. So number of files to send at once reduced from 5 to 3 to reduce the network load. * ZeroNet now tries harder in delivering updates to more peers in the background. * ZeroNet also makes more efforts of searching the peers before publishing updates. diff --git a/src/Site/Site.py b/src/Site/Site.py index 0cea4733..46e19169 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -6,6 +6,7 @@ import time import random import sys import hashlib +import itertools import collections import base64 @@ -634,32 +635,50 @@ class Site(object): continue # Failed query queried.append(peer) + + modified_contents = [] send_back = [] send_back_limit = config.send_back_limit send_back_skipped = 0 - my_modified = self.content_manager.listModified(since) - num_old_files = 0 - for inner_path, modified in res["modified_files"].items(): # Check if the peer has newer files than we - has_newer = int(modified) > my_modified.get(inner_path, 0) - has_older = int(modified) < my_modified.get(inner_path, 0) - if inner_path not in self.bad_files and not self.content_manager.isArchived(inner_path, modified): - if has_newer: - # We dont have this file or we have older + peer_modified_files = res["modified_files"] + my_modified_files = self.content_manager.listModified(since) + + inner_paths = itertools.chain(peer_modified_files.keys(), my_modified_files.keys()) + seen_inner_paths = {} + for inner_path in inner_paths: # Check if the peer has newer files than we have + if seen_inner_paths.get(inner_path, False): + continue + seen_inner_paths[inner_path] = True + + peer_modified = int(peer_modified_files.get(inner_path, 0)) + my_modified = int(my_modified_files.get(inner_path, 0)) + + diff = peer_modified - my_modified + if diff == 0: + continue + has_newer = diff > 0 + has_older = diff < 0 + + if inner_path not in self.bad_files and not self.content_manager.isArchived(inner_path, peer_modified): + if has_newer: # We don't have this file or we have older version modified_contents.append(inner_path) self.bad_files[inner_path] = self.bad_files.get(inner_path, 1) - if has_older: - if self.checkSendBackLRU(peer, inner_path, modified): + if has_older: # The remote peer doesn't have this file or it has older version + if self.checkSendBackLRU(peer, inner_path, peer_modified): send_back_skipped += 1 else: send_back.append(inner_path) + inner_paths = None + seen_inner_paths = None + if modified_contents: self.log.info("CheckModifications: %s new modified files from %s" % (len(modified_contents), peer)) - modified_contents.sort(key=lambda inner_path: 0 - res["modified_files"][inner_path]) # Download newest first + modified_contents.sort(key=lambda inner_path: 0 - peer_modified_files[inner_path]) # Download newest first for inner_path in modified_contents: self.log.info("CheckModifications: %s: %s > %s" % ( - inner_path, res["modified_files"][inner_path], my_modified.get(inner_path, 0) + inner_path, peer_modified_files.get(inner_path, 0), my_modified_files.get(inner_path, 0) )) t = self.spawn(self.pooledDownloadContent, modified_contents, only_if_bad=True) threads.append(t) @@ -667,12 +686,12 @@ class Site(object): if send_back: self.log.info("CheckModifications: %s has older versions of %s files" % (peer, len(send_back))) if len(send_back) > send_back_limit: - self.log.info("CheckModifications: choosing %s files to publish back" % (send_back_limit)) + self.log.info("CheckModifications: choosing %s random files to publish back" % (send_back_limit)) random.shuffle(send_back) send_back = send_back[0:send_back_limit] for inner_path in send_back: self.log.info("CheckModifications: %s: %s < %s" % ( - inner_path, res["modified_files"][inner_path], my_modified.get(inner_path, 0) + inner_path, peer_modified_files.get(inner_path, 0), my_modified_files.get(inner_path, 0) )) self.spawn(self.publisher, inner_path, [peer], [], 1, save_to_send_back_lru=True) From b7a3aa37e1399a589609b82dd1d8642436638de5 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Fri, 5 Nov 2021 23:38:38 +0700 Subject: [PATCH 111/191] Sidebar Plugin: display more detailed statistics about peers --- plugins/Sidebar/SidebarPlugin.py | 151 ++++++++++++++++++++++++------- src/Connection/Connection.py | 3 +- 2 files changed, 121 insertions(+), 33 deletions(-) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index 4ecca75a..f5b40e2d 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -88,31 +88,84 @@ class UiRequestPlugin(object): @PluginManager.registerTo("UiWebsocket") class UiWebsocketPlugin(object): def sidebarRenderPeerStats(self, body, site): - connected = len([peer for peer in list(site.peers.values()) if peer.connection and peer.connection.connected]) - connectable = len([peer_id for peer_id in list(site.peers.keys()) if not peer_id.endswith(":0")]) - onion = len([peer_id for peer_id in list(site.peers.keys()) if ".onion" in peer_id]) - local = len([peer for peer in list(site.peers.values()) if helper.isPrivateIp(peer.ip)]) + # Peers by status peers_total = len(site.peers) + peers_reachable = 0 + peers_connectable = 0 + peers_connected = 0 + peers_failed = 0 + # Peers by type + peers_by_type = {} - # Add myself - if site.isServing(): - peers_total += 1 - if any(site.connection_server.port_opened.values()): - connectable += 1 - if site.connection_server.tor_manager.start_onions: - onion += 1 + type_proper_names = { + 'ipv4': 'IPv4', + 'ipv6': 'IPv6', + 'onion': 'Onion', + 'unknown': 'Unknown' + } + + type_defs = { + 'local-ipv4': { + 'order' : -21, + 'color' : 'yellow' + }, + 'local-ipv6': { + 'order' : -20, + 'color' : 'orange' + }, + 'ipv4': { + 'order' : -11, + 'color' : 'blue' + }, + 'ipv6': { + 'order' : -10, + 'color' : 'darkblue' + }, + 'unknown': { + 'order' : 10, + 'color' : 'red' + }, + } + + for peer in list(site.peers.values()): + # Peers by status + if peer.isConnected(): + peers_connected += 1 + elif peer.isConnectable(): + peers_connectable += 1 + elif peer.isReachable() and not peer.connection_error: + peers_reachable += 1 + elif peer.isReachable() and peer.connection_error: + peers_failed += 1 + # Peers by type + peer_type = peer.getIpType() + peer_readable_type = type_proper_names.get(peer_type, peer_type) + if helper.isPrivateIp(peer.ip): + peer_type = 'local-' + peer.getIpType() + peer_readable_type = 'Local ' + peer_readable_type + peers_by_type[peer_type] = peers_by_type.get(peer_type, { + 'type': peer_type, + 'readable_type': _[peer_readable_type], + 'order': type_defs.get(peer_type, {}).get('order', 0), + 'color': type_defs.get(peer_type, {}).get('color', 'purple'), + 'count': 0 + }) + peers_by_type[peer_type]['count'] += 1 + + ######################################################################## if peers_total: - percent_connected = float(connected) / peers_total - percent_connectable = float(connectable) / peers_total - percent_onion = float(onion) / peers_total + percent_connected = float(peers_connected) / peers_total + percent_connectable = float(peers_connectable) / peers_total + percent_reachable = float(peers_reachable) / peers_total + percent_failed = float(peers_failed) / peers_total + percent_other = min(0.0, 1.0 - percent_connected - percent_connectable - percent_reachable - percent_failed) else: - percent_connectable = percent_connected = percent_onion = 0 - - if local: - local_html = _("
  • {_[Local]}:{local}
  • ") - else: - local_html = "" + percent_connected = 0 + percent_reachable = 0 + percent_connectable = 0 + percent_failed = 0 + percent_other = 0 peer_ips = [peer.key for peer in site.getConnectablePeers(20, allow_private=False)] peer_ips.sort(key=lambda peer_ip: ".onion:" in peer_ip) @@ -127,21 +180,55 @@ class UiWebsocketPlugin(object): {_[Peers]} {_[Copy to clipboard]} -
      -
    • -
    • -
    • +
      • +
      • +
      • +
      • +
        -
      • {_[Connected]}:{connected}
      • -
      • {_[Connectable]}:{connectable}
      • -
      • {_[Onion]}:{onion}
      • - {local_html} -
      • {_[Total]}:{peers_total}
      • +
      • {_[Connected]}:{peers_connected}
      • +
      • {_[Connectable]}:{peers_connectable}
      • +
      • {_[Reachable]}:{peers_reachable}
      • +
      • {_[Failed]}:{peers_failed}
      • +
      • {_[Total peers]}:{peers_total}
      - - """.replace("{local_html}", local_html))) + """)) + + ######################################################################## + + peers_by_type = sorted( + peers_by_type.values(), + key=lambda t: t['order'], + ) + + peers_by_type_graph = '' + peers_by_type_legend = '' + + if peers_total: + for t in peers_by_type: + peers_by_type_graph += """ +
    • + """ % (float(t['count']) * 100.0 / peers_total, t['type'], t["color"], t['readable_type']) + peers_by_type_legend += """ +
    • %s:%s
    • + """ % (t["color"], t['readable_type'], t['count']) + + if peers_by_type_legend != '': + body.append(_(""" +
    • + +
        + %s +
      +
        + %s +
      +
    • + """ % (peers_by_type_graph, peers_by_type_legend))) def sidebarRenderTransferStats(self, body, site): recv = float(site.settings.get("bytes_recv", 0)) / 1024 / 1024 @@ -682,7 +769,7 @@ class UiWebsocketPlugin(object): # Create position array lat, lon = loc["lat"], loc["lon"] latlon = "%s,%s" % (lat, lon) - if latlon in placed and helper.getIpType(peer.ip) == "ipv4": # Dont place more than 1 bar to same place, fake repos using ip address last two part + if latlon in placed and peer.getIpType() == "ipv4": # Dont place more than 1 bar to same place, fake repos using ip address last two part lat += float(128 - int(peer.ip.split(".")[-2])) / 50 lon += float(128 - int(peer.ip.split(".")[-1])) / 50 latlon = "%s,%s" % (lat, lon) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 44665b2f..ad1312f2 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -621,7 +621,8 @@ class Connection(object): self.waiting_requests[self.req_id] = {"evt": event, "cmd": cmd} if stream_to: self.waiting_streams[self.req_id] = stream_to - self.send(data) # Send request + if not self.send(data): # Send request + return False res = event.get() # Wait until event solves return res From f7372fc393021eddcc81dec22b1c604e73f2b34d Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Sat, 6 Nov 2021 13:16:43 +0700 Subject: [PATCH 112/191] Remove __slots__ from Peer, the slot system doesn't work well with classes extended by plugins anyway --- src/Peer/Peer.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index 6e4863e0..43c2932f 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -20,12 +20,6 @@ if config.use_tempfiles: # Communicate remote peers @PluginManager.acceptPlugins class Peer(object): - __slots__ = ( - "ip", "port", "site", "key", "connection", "connection_server", "time_found", "time_response", "time_hashfield", - "time_added", "has_hashfield", "is_tracker_connection", "time_my_hashfield_sent", "last_ping", "reputation", - "last_content_json_update", "hashfield", "connection_error", "hash_failed", "download_bytes", "download_time" - ) - def __init__(self, ip, port, site=None, connection_server=None): self.ip = ip self.port = port From 348a4b08650a255a64359b07e525e27aaba8e077 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Sat, 6 Nov 2021 20:33:19 +0700 Subject: [PATCH 113/191] Add support for building ubuntu:20.04-based docker images + some automation --- Dockerfile | 43 +--------------- build-docker-images.sh | 11 +++++ dockerfiles/Dockerfile.alpine | 1 + dockerfiles/Dockerfile.alpine3.13 | 44 +++++++++++++++++ dockerfiles/Dockerfile.ubuntu | 1 + dockerfiles/Dockerfile.ubuntu20.04 | 44 +++++++++++++++++ dockerfiles/gen-dockerfiles.sh | 34 +++++++++++++ .../zeronet-Dockerfile | 28 ++++++----- install-dep-packages.sh | 49 +++++++++++++++++++ 9 files changed, 200 insertions(+), 55 deletions(-) mode change 100644 => 120000 Dockerfile create mode 100755 build-docker-images.sh create mode 120000 dockerfiles/Dockerfile.alpine create mode 100644 dockerfiles/Dockerfile.alpine3.13 create mode 120000 dockerfiles/Dockerfile.ubuntu create mode 100644 dockerfiles/Dockerfile.ubuntu20.04 create mode 100755 dockerfiles/gen-dockerfiles.sh rename Dockerfile.arm64v8 => dockerfiles/zeronet-Dockerfile (55%) create mode 100755 install-dep-packages.sh diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index bc834293..00000000 --- a/Dockerfile +++ /dev/null @@ -1,42 +0,0 @@ -FROM alpine:3.13 - -# Base settings -ENV HOME /root - -# Install packages - -RUN apk --update --no-cache --no-progress add \ - python3 python3-dev py3-pip \ - libffi-dev musl-dev \ - gcc g++ make \ - automake autoconf libtool \ - openssl \ - tor - -COPY requirements.txt /root/requirements.txt - -RUN pip3 install -r /root/requirements.txt \ - && apk del python3-dev gcc libffi-dev musl-dev make \ - && echo "ControlPort 9051" >> /etc/tor/torrc \ - && echo "CookieAuthentication 1" >> /etc/tor/torrc - -RUN python3 -V \ - && python3 -m pip list \ - && tor --version \ - && openssl version - -# Add Zeronet source - -COPY . /root -VOLUME /root/data - -# Control if Tor proxy is started -ENV ENABLE_TOR false - -WORKDIR /root - -# Set upstart command -CMD (! ${ENABLE_TOR} || tor&) && python3 zeronet.py --ui_ip 0.0.0.0 --fileserver_port 26552 - -# Expose ports -EXPOSE 43110 26552 diff --git a/Dockerfile b/Dockerfile new file mode 120000 index 00000000..ae9f3313 --- /dev/null +++ b/Dockerfile @@ -0,0 +1 @@ +dockerfiles/Dockerfile.alpine \ No newline at end of file diff --git a/build-docker-images.sh b/build-docker-images.sh new file mode 100755 index 00000000..88dec2fb --- /dev/null +++ b/build-docker-images.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -e + +prefix="${1:-local/}" + +for dokerfile in dockerfiles/Dockerfile.* ; do + suffix="`echo "$dokerfile" | sed 's/.*\/Dockerfile\.//'`" + image_name="${prefix}zeronet:$suffix" + echo "DOCKER BUILD $image_name" + docker build -f "$dokerfile" -t "$image_name" . +done diff --git a/dockerfiles/Dockerfile.alpine b/dockerfiles/Dockerfile.alpine new file mode 120000 index 00000000..0f848cc8 --- /dev/null +++ b/dockerfiles/Dockerfile.alpine @@ -0,0 +1 @@ +Dockerfile.alpine3.13 \ No newline at end of file diff --git a/dockerfiles/Dockerfile.alpine3.13 b/dockerfiles/Dockerfile.alpine3.13 new file mode 100644 index 00000000..79f15b9b --- /dev/null +++ b/dockerfiles/Dockerfile.alpine3.13 @@ -0,0 +1,44 @@ +# THIS FILE IS AUTOGENERATED BY gen-dockerfiles.sh. +# SEE zeronet-Dockerfile FOR THE SOURCE FILE. + +FROM alpine:3.13 + +# Base settings +ENV HOME /root + +# Install packages + +# Install packages + +COPY install-dep-packages.sh /root/install-dep-packages.sh + +RUN /root/install-dep-packages.sh install + +COPY requirements.txt /root/requirements.txt + +RUN pip3 install -r /root/requirements.txt \ + && /root/install-dep-packages.sh remove-makedeps \ + && echo "ControlPort 9051" >> /etc/tor/torrc \ + && echo "CookieAuthentication 1" >> /etc/tor/torrc + +RUN python3 -V \ + && python3 -m pip list \ + && tor --version \ + && openssl version + +# Add Zeronet source + +COPY . /root +VOLUME /root/data + +# Control if Tor proxy is started +ENV ENABLE_TOR false + +WORKDIR /root + +# Set upstart command +CMD (! ${ENABLE_TOR} || tor&) && python3 zeronet.py --ui_ip 0.0.0.0 --fileserver_port 26552 + +# Expose ports +EXPOSE 43110 26552 + diff --git a/dockerfiles/Dockerfile.ubuntu b/dockerfiles/Dockerfile.ubuntu new file mode 120000 index 00000000..29adf7ef --- /dev/null +++ b/dockerfiles/Dockerfile.ubuntu @@ -0,0 +1 @@ +Dockerfile.ubuntu20.04 \ No newline at end of file diff --git a/dockerfiles/Dockerfile.ubuntu20.04 b/dockerfiles/Dockerfile.ubuntu20.04 new file mode 100644 index 00000000..bc32cf86 --- /dev/null +++ b/dockerfiles/Dockerfile.ubuntu20.04 @@ -0,0 +1,44 @@ +# THIS FILE IS AUTOGENERATED BY gen-dockerfiles.sh. +# SEE zeronet-Dockerfile FOR THE SOURCE FILE. + +FROM ubuntu:20.04 + +# Base settings +ENV HOME /root + +# Install packages + +# Install packages + +COPY install-dep-packages.sh /root/install-dep-packages.sh + +RUN /root/install-dep-packages.sh install + +COPY requirements.txt /root/requirements.txt + +RUN pip3 install -r /root/requirements.txt \ + && /root/install-dep-packages.sh remove-makedeps \ + && echo "ControlPort 9051" >> /etc/tor/torrc \ + && echo "CookieAuthentication 1" >> /etc/tor/torrc + +RUN python3 -V \ + && python3 -m pip list \ + && tor --version \ + && openssl version + +# Add Zeronet source + +COPY . /root +VOLUME /root/data + +# Control if Tor proxy is started +ENV ENABLE_TOR false + +WORKDIR /root + +# Set upstart command +CMD (! ${ENABLE_TOR} || tor&) && python3 zeronet.py --ui_ip 0.0.0.0 --fileserver_port 26552 + +# Expose ports +EXPOSE 43110 26552 + diff --git a/dockerfiles/gen-dockerfiles.sh b/dockerfiles/gen-dockerfiles.sh new file mode 100755 index 00000000..75a6edf6 --- /dev/null +++ b/dockerfiles/gen-dockerfiles.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +set -e + +die() { + echo "$@" > /dev/stderr + exit 1 +} + +for os in alpine:3.13 ubuntu:20.04 ; do + prefix="`echo "$os" | sed -e 's/://'`" + short_prefix="`echo "$os" | sed -e 's/:.*//'`" + + zeronet="zeronet-Dockerfile" + + dockerfile="Dockerfile.$prefix" + dockerfile_short="Dockerfile.$short_prefix" + + echo "GEN $dockerfile" + + if ! test -f "$zeronet" ; then + die "No such file: $zeronet" + fi + + echo "\ +# THIS FILE IS AUTOGENERATED BY gen-dockerfiles.sh. +# SEE $zeronet FOR THE SOURCE FILE. + +FROM $os + +`cat "$zeronet"` +" > "$dockerfile.tmp" && mv "$dockerfile.tmp" "$dockerfile" && ln -s -f "$dockerfile" "$dockerfile_short" +done + diff --git a/Dockerfile.arm64v8 b/dockerfiles/zeronet-Dockerfile similarity index 55% rename from Dockerfile.arm64v8 rename to dockerfiles/zeronet-Dockerfile index d27b7620..92c67c84 100644 --- a/Dockerfile.arm64v8 +++ b/dockerfiles/zeronet-Dockerfile @@ -1,34 +1,36 @@ -FROM alpine:3.12 - -#Base settings +# Base settings ENV HOME /root +# Install packages + +COPY install-dep-packages.sh /root/install-dep-packages.sh + +RUN /root/install-dep-packages.sh install + COPY requirements.txt /root/requirements.txt -#Install ZeroNet -RUN apk --update --no-cache --no-progress add python3 python3-dev gcc libffi-dev musl-dev make tor openssl \ - && pip3 install -r /root/requirements.txt \ - && apk del python3-dev gcc libffi-dev musl-dev make \ +RUN pip3 install -r /root/requirements.txt \ + && /root/install-dep-packages.sh remove-makedeps \ && echo "ControlPort 9051" >> /etc/tor/torrc \ && echo "CookieAuthentication 1" >> /etc/tor/torrc - + RUN python3 -V \ && python3 -m pip list \ && tor --version \ && openssl version -#Add Zeronet source +# Add Zeronet source + COPY . /root VOLUME /root/data -#Control if Tor proxy is started +# Control if Tor proxy is started ENV ENABLE_TOR false WORKDIR /root -#Set upstart command +# Set upstart command CMD (! ${ENABLE_TOR} || tor&) && python3 zeronet.py --ui_ip 0.0.0.0 --fileserver_port 26552 -#Expose ports +# Expose ports EXPOSE 43110 26552 - diff --git a/install-dep-packages.sh b/install-dep-packages.sh new file mode 100755 index 00000000..655a33aa --- /dev/null +++ b/install-dep-packages.sh @@ -0,0 +1,49 @@ +#!/bin/sh +set -e + +do_alpine() { + local deps="python3 py3-pip openssl tor" + local makedeps="python3-dev gcc g++ libffi-dev musl-dev make automake autoconf libtool" + + case "$1" in + install) + apk --update --no-cache --no-progress add $deps $makedeps + ;; + remove-makedeps) + apk del $makedeps + ;; + esac +} + +do_ubuntu() { + local deps="python3 python3-pip openssl tor" + local makedeps="python3-dev gcc g++ libffi-dev make automake autoconf libtool" + + case "$1" in + install) + apt-get update && \ + apt-get install --no-install-recommends -y $deps $makedeps && \ + rm -rf /var/lib/apt/lists/* + ;; + remove-makedeps) + apt-get remove -y $makedeps + ;; + esac +} + +if test -f /etc/os-release ; then + . /etc/os-release +elif test -f /usr/lib/os-release ; then + . /usr/lib/os-release +else + echo "No such file: /etc/os-release" > /dev/stderr + exit 1 +fi + +case "$ID" in + ubuntu) do_ubuntu "$@" ;; + alpine) do_alpine "$@" ;; + *) + echo "Unsupported OS ID: $ID" > /dev/stderr + exit 1 +esac From 5c8bbe5801ea499e59d89274686de2662a6d2560 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Sat, 6 Nov 2021 22:00:46 +0700 Subject: [PATCH 114/191] build-docker-images.sh: push to Docker Hub as well --- build-docker-images.sh | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/build-docker-images.sh b/build-docker-images.sh index 88dec2fb..8eff34f4 100755 --- a/build-docker-images.sh +++ b/build-docker-images.sh @@ -1,11 +1,32 @@ #!/bin/sh set -e +arg_push= + +case "$1" in + --push) arg_push=y ; shift ;; +esac + +default_suffix=alpine prefix="${1:-local/}" for dokerfile in dockerfiles/Dockerfile.* ; do suffix="`echo "$dokerfile" | sed 's/.*\/Dockerfile\.//'`" image_name="${prefix}zeronet:$suffix" + + latest="" + t_latest="" + if [ "$suffix" = "$default_suffix" ] ; then + latest="${prefix}zeronet:latest" + t_latest="-t ${latest}" + fi + echo "DOCKER BUILD $image_name" - docker build -f "$dokerfile" -t "$image_name" . + docker build -f "$dokerfile" -t "$image_name" $t_latest . + if [ -n "$arg_push" ] ; then + docker push "$image_name" + if [ -n "$latest" ] ; then + docker push "$latest" + fi + fi done From 0bbf19aab98ccd847024ad0f60fe283596f14713 Mon Sep 17 00:00:00 2001 From: canewsin Date: Sat, 11 Dec 2021 16:26:27 +0530 Subject: [PATCH 115/191] Update Donation Link --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index db8c40a5..8c9f6621 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -custom: https://zeronet.io/docs/help_zeronet/donate/ +custom: https://zerolink.ml/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/help_zeronet/donate/ From 0e480045634044abb995674df50e619753c88bd0 Mon Sep 17 00:00:00 2001 From: canewsin Date: Sat, 11 Dec 2021 16:35:02 +0530 Subject: [PATCH 116/191] v 0.7.6-internal - Added `access_key` variable in Config, this used to access restrited plugins when multiuser plugin is enabled. When MultiUserPlugin is enabled we cannot access some pages like /Stats, this key will remove such restriction with access key. - Added `last_connection_id_current_version` to ConnectionServer, helpful to estimate no of connection from current client version. - Added current version: connections to /Stats page. see the previous point. --- plugins/Stats/StatsPlugin.py | 17 ++++++++++++----- src/Config.py | 8 ++++---- src/Connection/ConnectionServer.py | 10 +++++++++- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py index 32fd06c5..cdd09d8f 100644 --- a/plugins/Stats/StatsPlugin.py +++ b/plugins/Stats/StatsPlugin.py @@ -73,14 +73,15 @@ class UiRequestPlugin(object): import main # Connections - yield "Connections (%s, total made: %s, in: %s, out: %s):
      " % ( - len(main.file_server.connections), main.file_server.last_connection_id, + yield "Connections (%s, current version: %s, total made: %s, in: %s, out: %s):
      " % ( + len(main.file_server.connections), main.file_server.last_connection_id_current_version, main.file_server.last_connection_id, main.file_server.num_incoming, main.file_server.num_outgoing ) yield "" yield "" yield "" - for connection in main.file_server.connections: + connections = sorted(main.file_server.connections, key=lambda connection: connection.handshake.get("rev", 0), reverse=True) + for connection in connections: if "cipher" in dir(connection.sock): cipher = connection.sock.cipher()[0] tls_version = connection.sock.version() @@ -364,8 +365,14 @@ class UiRequestPlugin(object): self.sendHeader() if "Multiuser" in PluginManager.plugin_manager.plugin_names and not config.multiuser_local: - yield "This function is disabled on this proxy" - return + if 'access_key' not in self.get.keys(): + yield "This function is disabled on this proxy" + return + else: + access_key = self.get["access_key"] + if access_key != config.access_key: + yield "This function is disabled on this proxy" + return s = time.time() diff --git a/src/Config.py b/src/Config.py index 9bfcf96d..7555f65d 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ import time class Config(object): def __init__(self, argv): - self.version = "0.7.5" + self.version = "0.7.6-internal" self.rev = 4560 self.argv = argv self.action = None @@ -79,6 +79,8 @@ class Config(object): # Create command line arguments def createArguments(self): + from Crypt import CryptHash + access_key_default = CryptHash.random(24, "base64") # Used to allow restrited plugins when multiuser plugin is enabled trackers = [ "zero://boot3rdez4rzn36x.onion:15441", "http://open.acgnxtracker.com:80/announce", # DE @@ -97,9 +99,6 @@ class Config(object): "udp://tracker.0x.tf:6969/announce", "udp://tracker.zerobytes.xyz:1337/announce", "udp://vibe.sleepyinternetfun.xyz:1738/announce", - "udp://tracker.bitsearch.to:1337/announce", - "udp://jeremylee.sh:6969/announce", - "udp://tracker.pomf.se:80/announce", "udp://www.torrent.eu.org:451/announce", "zero://k5w77dozo3hy5zualyhni6vrh73iwfkaofa64abbilwyhhd3wgenbjqd.onion:15441", "zero://2kcb2fqesyaevc4lntogupa4mkdssth2ypfwczd2ov5a3zo6ytwwbayd.onion:15441", @@ -271,6 +270,7 @@ class Config(object): metavar='address') self.parser.add_argument('--updatesite', help='Source code update site', default='1Update8crprmciJHwp2WXqkx2c4iYp18', metavar='address') + self.parser.add_argument('--access_key', help='Plugin access key default: Random key generated at startup', default=access_key_default, metavar='key') self.parser.add_argument('--dist_type', help='Type of installed distribution', default='source') self.parser.add_argument('--size_limit', help='Default site size limit in MB', default=10, type=int, metavar='limit') diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index 8d377aca..090d96a6 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -30,7 +30,8 @@ class ConnectionServer(object): port = 15441 self.ip = ip self.port = port - self.last_connection_id = 1 # Connection id incrementer + self.last_connection_id = 0 # Connection id incrementer + self.last_connection_id_current_version = 0 # Connection id incrementer for current client version self.log = logging.getLogger("ConnServer") self.port_opened = {} self.peer_blacklist = SiteManager.peer_blacklist @@ -155,6 +156,9 @@ class ConnectionServer(object): connection = Connection(self, ip, port, sock) self.connections.append(connection) + rev = connection.handshake.get("rev", 0) + if rev > 0 and rev == config.rev: + self.last_connection_id_current_version += 1 if ip not in config.ip_local: self.ips[ip] = connection connection.handleIncomingConnection(sock) @@ -219,6 +223,10 @@ class ConnectionServer(object): if not succ: connection.close("Connection event return error") raise Exception("Connection event return error") + else: + rev = connection.handshake.get("rev", 0) + if rev > 0 and rev == config.rev: + self.last_connection_id_current_version += 1 except Exception as err: connection.close("%s Connect error: %s" % (ip, Debug.formatException(err))) From 44483582470f06fe0ff1ed17f01b5d47f590e1ba Mon Sep 17 00:00:00 2001 From: canewsin Date: Thu, 16 Dec 2021 19:35:01 +0530 Subject: [PATCH 117/191] 0.7.6-internal 2 Moved Plugins to Seperate Repo --- .gitmodules | 3 + plugins | 1 + .../AnnounceBitTorrentPlugin.py | 148 - plugins/AnnounceBitTorrent/__init__.py | 1 - plugins/AnnounceBitTorrent/plugin_info.json | 5 - plugins/AnnounceLocal/AnnounceLocalPlugin.py | 147 - plugins/AnnounceLocal/BroadcastServer.py | 139 - plugins/AnnounceLocal/Test/TestAnnounce.py | 113 - plugins/AnnounceLocal/Test/conftest.py | 4 - plugins/AnnounceLocal/Test/pytest.ini | 5 - plugins/AnnounceLocal/__init__.py | 1 - plugins/AnnounceLocal/plugin_info.json | 5 - plugins/AnnounceShare/AnnounceSharePlugin.py | 190 - .../AnnounceShare/Test/TestAnnounceShare.py | 24 - plugins/AnnounceShare/Test/conftest.py | 3 - plugins/AnnounceShare/Test/pytest.ini | 5 - plugins/AnnounceShare/__init__.py | 1 - plugins/AnnounceShare/plugin_info.json | 5 - plugins/AnnounceZero/AnnounceZeroPlugin.py | 141 - plugins/AnnounceZero/__init__.py | 1 - plugins/AnnounceZero/plugin_info.json | 5 - plugins/Benchmark/BenchmarkDb.py | 143 - plugins/Benchmark/BenchmarkPack.py | 183 - plugins/Benchmark/BenchmarkPlugin.py | 428 - plugins/Benchmark/__init__.py | 3 - plugins/Benchmark/media/benchmark.html | 123 - plugins/Benchmark/plugin_info.json | 5 - plugins/Bigfile/BigfilePiecefield.py | 170 - plugins/Bigfile/BigfilePlugin.py | 843 - plugins/Bigfile/Test/TestBigfile.py | 574 - plugins/Bigfile/Test/conftest.py | 1 - plugins/Bigfile/Test/pytest.ini | 5 - plugins/Bigfile/__init__.py | 2 - plugins/Chart/ChartCollector.py | 181 - plugins/Chart/ChartDb.py | 133 - plugins/Chart/ChartPlugin.py | 57 - plugins/Chart/__init__.py | 1 - plugins/Chart/plugin_info.json | 5 - plugins/ContentFilter/ContentFilterPlugin.py | 262 - plugins/ContentFilter/ContentFilterStorage.py | 164 - .../ContentFilter/Test/TestContentFilter.py | 82 - plugins/ContentFilter/Test/conftest.py | 1 - plugins/ContentFilter/Test/pytest.ini | 5 - plugins/ContentFilter/__init__.py | 1 - plugins/ContentFilter/languages/hu.json | 6 - plugins/ContentFilter/languages/it.json | 6 - plugins/ContentFilter/languages/jp.json | 6 - plugins/ContentFilter/languages/pt-br.json | 6 - plugins/ContentFilter/languages/zh-tw.json | 6 - plugins/ContentFilter/languages/zh.json | 6 - plugins/ContentFilter/media/blocklisted.html | 89 - plugins/ContentFilter/media/js/ZeroFrame.js | 119 - plugins/ContentFilter/plugin_info.json | 5 - plugins/Cors/CorsPlugin.py | 139 - plugins/Cors/__init__.py | 1 - plugins/Cors/plugin_info.json | 5 - plugins/CryptMessage/CryptMessage.py | 58 - plugins/CryptMessage/CryptMessagePlugin.py | 225 - plugins/CryptMessage/Test/TestCrypt.py | 136 - plugins/CryptMessage/Test/conftest.py | 1 - plugins/CryptMessage/Test/pytest.ini | 5 - plugins/CryptMessage/__init__.py | 1 - plugins/CryptMessage/plugin_info.json | 5 - plugins/FilePack/FilePackPlugin.py | 193 - plugins/FilePack/__init__.py | 1 - plugins/FilePack/plugin_info.json | 5 - plugins/MergerSite/MergerSitePlugin.py | 399 - plugins/MergerSite/__init__.py | 1 - plugins/MergerSite/languages/es.json | 5 - plugins/MergerSite/languages/fr.json | 5 - plugins/MergerSite/languages/hu.json | 5 - plugins/MergerSite/languages/it.json | 5 - plugins/MergerSite/languages/jp.json | 5 - plugins/MergerSite/languages/pt-br.json | 5 - plugins/MergerSite/languages/tr.json | 5 - plugins/MergerSite/languages/zh-tw.json | 5 - plugins/MergerSite/languages/zh.json | 5 - plugins/Newsfeed/NewsfeedPlugin.py | 187 - plugins/Newsfeed/__init__.py | 1 - plugins/OptionalManager/ContentDbPlugin.py | 414 - .../OptionalManager/OptionalManagerPlugin.py | 253 - .../Test/TestOptionalManager.py | 158 - plugins/OptionalManager/Test/conftest.py | 1 - plugins/OptionalManager/Test/pytest.ini | 5 - plugins/OptionalManager/UiWebsocketPlugin.py | 396 - plugins/OptionalManager/__init__.py | 2 - plugins/OptionalManager/languages/es.json | 7 - plugins/OptionalManager/languages/fr.json | 7 - plugins/OptionalManager/languages/hu.json | 7 - plugins/OptionalManager/languages/jp.json | 7 - plugins/OptionalManager/languages/pt-br.json | 7 - plugins/OptionalManager/languages/zh-tw.json | 7 - plugins/OptionalManager/languages/zh.json | 7 - plugins/PeerDb/PeerDbPlugin.py | 100 - plugins/PeerDb/__init__.py | 2 - plugins/PeerDb/plugin_info.json | 5 - plugins/Sidebar/ConsolePlugin.py | 101 - plugins/Sidebar/SidebarPlugin.py | 805 - plugins/Sidebar/ZipStream.py | 59 - plugins/Sidebar/__init__.py | 2 - plugins/Sidebar/languages/da.json | 81 - plugins/Sidebar/languages/de.json | 81 - plugins/Sidebar/languages/es.json | 79 - plugins/Sidebar/languages/fr.json | 82 - plugins/Sidebar/languages/hu.json | 82 - plugins/Sidebar/languages/it.json | 81 - plugins/Sidebar/languages/jp.json | 104 - plugins/Sidebar/languages/pl.json | 82 - plugins/Sidebar/languages/pt-br.json | 97 - plugins/Sidebar/languages/ru.json | 82 - plugins/Sidebar/languages/tr.json | 82 - plugins/Sidebar/languages/zh-tw.json | 83 - plugins/Sidebar/languages/zh.json | 101 - plugins/Sidebar/media/Class.coffee | 23 - plugins/Sidebar/media/Console.coffee | 201 - plugins/Sidebar/media/Console.css | 31 - plugins/Sidebar/media/Menu.coffee | 49 - plugins/Sidebar/media/Menu.css | 19 - plugins/Sidebar/media/Prototypes.coffee | 9 - plugins/Sidebar/media/RateLimit.coffee | 14 - plugins/Sidebar/media/Scrollable.js | 91 - plugins/Sidebar/media/Scrollbable.css | 44 - plugins/Sidebar/media/Sidebar.coffee | 644 - plugins/Sidebar/media/Sidebar.css | 169 - plugins/Sidebar/media/all.css | 281 - plugins/Sidebar/media/all.js | 1770 -- plugins/Sidebar/media/morphdom.js | 340 - plugins/Sidebar/media_globe/Detector.js | 60 - plugins/Sidebar/media_globe/Tween.js | 12 - plugins/Sidebar/media_globe/all.js | 1345 -- plugins/Sidebar/media_globe/globe.js | 436 - plugins/Sidebar/media_globe/three.min.js | 814 - plugins/Sidebar/media_globe/world.jpg | Bin 94795 -> 0 bytes plugins/Sidebar/plugin_info.json | 5 - plugins/Stats/StatsPlugin.py | 634 - plugins/Stats/__init__.py | 1 - plugins/Stats/plugin_info.json | 5 - plugins/TranslateSite/TranslateSitePlugin.py | 80 - plugins/TranslateSite/__init__.py | 1 - plugins/TranslateSite/plugin_info.json | 5 - plugins/Trayicon/TrayiconPlugin.py | 171 - plugins/Trayicon/__init__.py | 4 - plugins/Trayicon/languages/es.json | 14 - plugins/Trayicon/languages/fr.json | 14 - plugins/Trayicon/languages/hu.json | 14 - plugins/Trayicon/languages/it.json | 14 - plugins/Trayicon/languages/jp.json | 14 - plugins/Trayicon/languages/pl.json | 14 - plugins/Trayicon/languages/pt-br.json | 14 - plugins/Trayicon/languages/tr.json | 14 - plugins/Trayicon/languages/zh-tw.json | 14 - plugins/Trayicon/languages/zh.json | 14 - plugins/Trayicon/lib/__init__.py | 0 plugins/Trayicon/lib/notificationicon.py | 730 - plugins/Trayicon/lib/winfolders.py | 54 - plugins/Trayicon/plugin_info.json | 5 - plugins/Trayicon/trayicon.ico | Bin 1150 -> 0 bytes plugins/UiConfig/UiConfigPlugin.py | 72 - plugins/UiConfig/__init__.py | 1 - plugins/UiConfig/languages/hu.json | 33 - plugins/UiConfig/languages/jp.json | 62 - plugins/UiConfig/languages/pl.json | 62 - plugins/UiConfig/languages/pt-br.json | 56 - plugins/UiConfig/languages/zh.json | 62 - plugins/UiConfig/media/config.html | 20 - plugins/UiConfig/media/css/Config.css | 68 - plugins/UiConfig/media/css/all.css | 124 - plugins/UiConfig/media/css/button.css | 12 - plugins/UiConfig/media/css/fonts.css | 30 - plugins/UiConfig/media/img/loading.gif | Bin 723 -> 0 bytes .../UiConfig/media/js/ConfigStorage.coffee | 222 - plugins/UiConfig/media/js/ConfigView.coffee | 124 - plugins/UiConfig/media/js/UiConfig.coffee | 129 - plugins/UiConfig/media/js/all.js | 2066 -- plugins/UiConfig/media/js/lib/Class.coffee | 23 - plugins/UiConfig/media/js/lib/Promise.coffee | 74 - .../UiConfig/media/js/lib/Prototypes.coffee | 8 - plugins/UiConfig/media/js/lib/maquette.js | 770 - .../UiConfig/media/js/utils/Animation.coffee | 138 - plugins/UiConfig/media/js/utils/Dollar.coffee | 3 - .../UiConfig/media/js/utils/ZeroFrame.coffee | 85 - plugins/UiConfig/plugin_info.json | 5 - plugins/UiFileManager/UiFileManagerPlugin.py | 90 - plugins/UiFileManager/__init__.py | 1 - plugins/UiFileManager/languages/hu.json | 20 - plugins/UiFileManager/languages/jp.json | 20 - .../UiFileManager/media/codemirror/LICENSE | 21 - .../UiFileManager/media/codemirror/all.css | 678 - plugins/UiFileManager/media/codemirror/all.js | 19964 ---------------- .../media/codemirror/base/codemirror.css | 349 - .../media/codemirror/base/codemirror.js | 9778 -------- .../codemirror/extension/dialog/dialog.css | 32 - .../codemirror/extension/dialog/dialog.js | 163 - .../extension/edit/closebrackets.js | 191 - .../codemirror/extension/edit/closetag.js | 184 - .../codemirror/extension/edit/continuelist.js | 101 - .../extension/edit/matchbrackets.js | 158 - .../codemirror/extension/edit/matchtags.js | 66 - .../extension/edit/trailingspace.js | 27 - .../codemirror/extension/fold/brace-fold.js | 105 - .../codemirror/extension/fold/comment-fold.js | 59 - .../codemirror/extension/fold/foldcode.js | 157 - .../codemirror/extension/fold/foldgutter.css | 20 - .../codemirror/extension/fold/foldgutter.js | 163 - .../codemirror/extension/fold/indent-fold.js | 48 - .../extension/fold/markdown-fold.js | 49 - .../codemirror/extension/fold/xml-fold.js | 184 - .../codemirror/extension/hint/anyword-hint.js | 41 - .../codemirror/extension/hint/html-hint.js | 350 - .../codemirror/extension/hint/show-hint.css | 36 - .../codemirror/extension/hint/show-hint.js | 479 - .../codemirror/extension/hint/sql-hint.js | 304 - .../codemirror/extension/hint/xml-hint.js | 123 - .../codemirror/extension/lint/json-lint.js | 40 - .../codemirror/extension/lint/jsonlint.js | 1 - .../media/codemirror/extension/lint/lint.css | 73 - .../media/codemirror/extension/lint/lint.js | 255 - .../codemirror/extension/mdn-like-custom.css | 44 - .../extension/scroll/annotatescrollbar.js | 122 - .../extension/scroll/scrollpastend.js | 48 - .../extension/scroll/simplescrollbars.css | 66 - .../extension/scroll/simplescrollbars.js | 152 - .../extension/search/jump-to-line.js | 50 - .../extension/search/match-highlighter.js | 167 - .../extension/search/matchesonscrollbar.css | 8 - .../extension/search/matchesonscrollbar.js | 97 - .../codemirror/extension/search/search.js | 260 - .../extension/search/searchcursor.js | 296 - .../extension/selection/active-line.js | 72 - .../extension/selection/mark-selection.js | 119 - .../extension/selection/selection-pointer.js | 98 - .../media/codemirror/extension/simple.js | 216 - .../media/codemirror/extension/sublime.js | 714 - .../media/codemirror/mode/coffeescript.js | 359 - .../media/codemirror/mode/css.js | 860 - .../UiFileManager/media/codemirror/mode/go.js | 187 - .../media/codemirror/mode/htmlembedded.js | 37 - .../media/codemirror/mode/htmlmixed.js | 152 - .../media/codemirror/mode/javascript.js | 934 - .../media/codemirror/mode/markdown.js | 886 - .../media/codemirror/mode/python.js | 399 - .../media/codemirror/mode/rust.js | 72 - .../media/codemirror/mode/xml.js | 413 - plugins/UiFileManager/media/css/Menu.css | 33 - plugins/UiFileManager/media/css/Selectbar.css | 17 - .../UiFileManager/media/css/UiFileManager.css | 148 - plugins/UiFileManager/media/css/all.css | 211 - plugins/UiFileManager/media/img/loading.gif | Bin 723 -> 0 bytes plugins/UiFileManager/media/js/Config.coffee | 15 - .../UiFileManager/media/js/FileEditor.coffee | 179 - .../media/js/FileItemList.coffee | 194 - .../UiFileManager/media/js/FileList.coffee | 268 - .../media/js/UiFileManager.coffee | 79 - plugins/UiFileManager/media/js/all.js | 3042 --- .../media/js/lib/Animation.coffee | 138 - .../UiFileManager/media/js/lib/Class.coffee | 23 - .../UiFileManager/media/js/lib/Dollar.coffee | 3 - .../media/js/lib/ItemList.coffee | 26 - .../UiFileManager/media/js/lib/Menu.coffee | 110 - .../UiFileManager/media/js/lib/Promise.coffee | 74 - .../media/js/lib/Prototypes.coffee | 9 - .../media/js/lib/RateLimitCb.coffee | 62 - .../UiFileManager/media/js/lib/Text.coffee | 147 - .../UiFileManager/media/js/lib/Time.coffee | 59 - .../media/js/lib/ZeroFrame.coffee | 85 - .../UiFileManager/media/js/lib/maquette.js | 770 - plugins/UiFileManager/media/list.html | 18 - .../UiPluginManager/UiPluginManagerPlugin.py | 221 - plugins/UiPluginManager/__init__.py | 1 - .../media/css/PluginManager.css | 75 - plugins/UiPluginManager/media/css/all.css | 129 - plugins/UiPluginManager/media/css/button.css | 12 - plugins/UiPluginManager/media/css/fonts.css | 30 - plugins/UiPluginManager/media/img/loading.gif | Bin 723 -> 0 bytes .../media/js/PluginList.coffee | 132 - .../media/js/UiPluginManager.coffee | 71 - plugins/UiPluginManager/media/js/all.js | 1606 -- .../UiPluginManager/media/js/lib/Class.coffee | 23 - .../media/js/lib/Promise.coffee | 74 - .../media/js/lib/Prototypes.coffee | 8 - .../UiPluginManager/media/js/lib/maquette.js | 770 - .../media/js/utils/Animation.coffee | 138 - .../media/js/utils/Dollar.coffee | 3 - .../media/js/utils/ZeroFrame.coffee | 85 - .../UiPluginManager/media/plugin_manager.html | 19 - plugins/Zeroname/README.md | 55 - plugins/Zeroname/SiteManagerPlugin.py | 69 - plugins/Zeroname/__init__.py | 1 - plugins/Zeroname/updater/zeroname_updater.py | 249 - plugins/__init__.py | 0 .../disabled-Bootstrapper/BootstrapperDb.py | 156 - .../BootstrapperPlugin.py | 156 - .../Test/TestBootstrapper.py | 246 - .../disabled-Bootstrapper/Test/conftest.py | 1 - plugins/disabled-Bootstrapper/Test/pytest.ini | 6 - plugins/disabled-Bootstrapper/__init__.py | 1 - .../disabled-Bootstrapper/plugin_info.json | 5 - .../disabled-Dnschain/SiteManagerPlugin.py | 153 - plugins/disabled-Dnschain/UiRequestPlugin.py | 34 - plugins/disabled-Dnschain/__init__.py | 3 - .../DonationMessagePlugin.py | 22 - plugins/disabled-DonationMessage/__init__.py | 1 - plugins/disabled-Multiuser/MultiuserPlugin.py | 275 - .../disabled-Multiuser/Test/TestMultiuser.py | 14 - plugins/disabled-Multiuser/Test/conftest.py | 1 - plugins/disabled-Multiuser/Test/pytest.ini | 5 - plugins/disabled-Multiuser/UserPlugin.py | 35 - plugins/disabled-Multiuser/__init__.py | 1 - plugins/disabled-Multiuser/plugin_info.json | 5 - plugins/disabled-StemPort/StemPortPlugin.py | 135 - plugins/disabled-StemPort/__init__.py | 10 - .../disabled-UiPassword/UiPasswordPlugin.py | 183 - plugins/disabled-UiPassword/__init__.py | 1 - plugins/disabled-UiPassword/login.html | 116 - plugins/disabled-UiPassword/plugin_info.json | 5 - .../SiteManagerPlugin.py | 180 - .../disabled-ZeronameLocal/UiRequestPlugin.py | 39 - plugins/disabled-ZeronameLocal/__init__.py | 2 - src/Config.py | 2 +- 319 files changed, 5 insertions(+), 74308 deletions(-) create mode 100644 .gitmodules create mode 160000 plugins delete mode 100644 plugins/AnnounceBitTorrent/AnnounceBitTorrentPlugin.py delete mode 100644 plugins/AnnounceBitTorrent/__init__.py delete mode 100644 plugins/AnnounceBitTorrent/plugin_info.json delete mode 100644 plugins/AnnounceLocal/AnnounceLocalPlugin.py delete mode 100644 plugins/AnnounceLocal/BroadcastServer.py delete mode 100644 plugins/AnnounceLocal/Test/TestAnnounce.py delete mode 100644 plugins/AnnounceLocal/Test/conftest.py delete mode 100644 plugins/AnnounceLocal/Test/pytest.ini delete mode 100644 plugins/AnnounceLocal/__init__.py delete mode 100644 plugins/AnnounceLocal/plugin_info.json delete mode 100644 plugins/AnnounceShare/AnnounceSharePlugin.py delete mode 100644 plugins/AnnounceShare/Test/TestAnnounceShare.py delete mode 100644 plugins/AnnounceShare/Test/conftest.py delete mode 100644 plugins/AnnounceShare/Test/pytest.ini delete mode 100644 plugins/AnnounceShare/__init__.py delete mode 100644 plugins/AnnounceShare/plugin_info.json delete mode 100644 plugins/AnnounceZero/AnnounceZeroPlugin.py delete mode 100644 plugins/AnnounceZero/__init__.py delete mode 100644 plugins/AnnounceZero/plugin_info.json delete mode 100644 plugins/Benchmark/BenchmarkDb.py delete mode 100644 plugins/Benchmark/BenchmarkPack.py delete mode 100644 plugins/Benchmark/BenchmarkPlugin.py delete mode 100644 plugins/Benchmark/__init__.py delete mode 100644 plugins/Benchmark/media/benchmark.html delete mode 100644 plugins/Benchmark/plugin_info.json delete mode 100644 plugins/Bigfile/BigfilePiecefield.py delete mode 100644 plugins/Bigfile/BigfilePlugin.py delete mode 100644 plugins/Bigfile/Test/TestBigfile.py delete mode 100644 plugins/Bigfile/Test/conftest.py delete mode 100644 plugins/Bigfile/Test/pytest.ini delete mode 100644 plugins/Bigfile/__init__.py delete mode 100644 plugins/Chart/ChartCollector.py delete mode 100644 plugins/Chart/ChartDb.py delete mode 100644 plugins/Chart/ChartPlugin.py delete mode 100644 plugins/Chart/__init__.py delete mode 100644 plugins/Chart/plugin_info.json delete mode 100644 plugins/ContentFilter/ContentFilterPlugin.py delete mode 100644 plugins/ContentFilter/ContentFilterStorage.py delete mode 100644 plugins/ContentFilter/Test/TestContentFilter.py delete mode 100644 plugins/ContentFilter/Test/conftest.py delete mode 100644 plugins/ContentFilter/Test/pytest.ini delete mode 100644 plugins/ContentFilter/__init__.py delete mode 100644 plugins/ContentFilter/languages/hu.json delete mode 100644 plugins/ContentFilter/languages/it.json delete mode 100644 plugins/ContentFilter/languages/jp.json delete mode 100644 plugins/ContentFilter/languages/pt-br.json delete mode 100644 plugins/ContentFilter/languages/zh-tw.json delete mode 100644 plugins/ContentFilter/languages/zh.json delete mode 100644 plugins/ContentFilter/media/blocklisted.html delete mode 100644 plugins/ContentFilter/media/js/ZeroFrame.js delete mode 100644 plugins/ContentFilter/plugin_info.json delete mode 100644 plugins/Cors/CorsPlugin.py delete mode 100644 plugins/Cors/__init__.py delete mode 100644 plugins/Cors/plugin_info.json delete mode 100644 plugins/CryptMessage/CryptMessage.py delete mode 100644 plugins/CryptMessage/CryptMessagePlugin.py delete mode 100644 plugins/CryptMessage/Test/TestCrypt.py delete mode 100644 plugins/CryptMessage/Test/conftest.py delete mode 100644 plugins/CryptMessage/Test/pytest.ini delete mode 100644 plugins/CryptMessage/__init__.py delete mode 100644 plugins/CryptMessage/plugin_info.json delete mode 100644 plugins/FilePack/FilePackPlugin.py delete mode 100644 plugins/FilePack/__init__.py delete mode 100644 plugins/FilePack/plugin_info.json delete mode 100644 plugins/MergerSite/MergerSitePlugin.py delete mode 100644 plugins/MergerSite/__init__.py delete mode 100644 plugins/MergerSite/languages/es.json delete mode 100644 plugins/MergerSite/languages/fr.json delete mode 100644 plugins/MergerSite/languages/hu.json delete mode 100644 plugins/MergerSite/languages/it.json delete mode 100644 plugins/MergerSite/languages/jp.json delete mode 100644 plugins/MergerSite/languages/pt-br.json delete mode 100644 plugins/MergerSite/languages/tr.json delete mode 100644 plugins/MergerSite/languages/zh-tw.json delete mode 100644 plugins/MergerSite/languages/zh.json delete mode 100644 plugins/Newsfeed/NewsfeedPlugin.py delete mode 100644 plugins/Newsfeed/__init__.py delete mode 100644 plugins/OptionalManager/ContentDbPlugin.py delete mode 100644 plugins/OptionalManager/OptionalManagerPlugin.py delete mode 100644 plugins/OptionalManager/Test/TestOptionalManager.py delete mode 100644 plugins/OptionalManager/Test/conftest.py delete mode 100644 plugins/OptionalManager/Test/pytest.ini delete mode 100644 plugins/OptionalManager/UiWebsocketPlugin.py delete mode 100644 plugins/OptionalManager/__init__.py delete mode 100644 plugins/OptionalManager/languages/es.json delete mode 100644 plugins/OptionalManager/languages/fr.json delete mode 100644 plugins/OptionalManager/languages/hu.json delete mode 100644 plugins/OptionalManager/languages/jp.json delete mode 100644 plugins/OptionalManager/languages/pt-br.json delete mode 100644 plugins/OptionalManager/languages/zh-tw.json delete mode 100644 plugins/OptionalManager/languages/zh.json delete mode 100644 plugins/PeerDb/PeerDbPlugin.py delete mode 100644 plugins/PeerDb/__init__.py delete mode 100644 plugins/PeerDb/plugin_info.json delete mode 100644 plugins/Sidebar/ConsolePlugin.py delete mode 100644 plugins/Sidebar/SidebarPlugin.py delete mode 100644 plugins/Sidebar/ZipStream.py delete mode 100644 plugins/Sidebar/__init__.py delete mode 100644 plugins/Sidebar/languages/da.json delete mode 100644 plugins/Sidebar/languages/de.json delete mode 100644 plugins/Sidebar/languages/es.json delete mode 100644 plugins/Sidebar/languages/fr.json delete mode 100644 plugins/Sidebar/languages/hu.json delete mode 100644 plugins/Sidebar/languages/it.json delete mode 100644 plugins/Sidebar/languages/jp.json delete mode 100644 plugins/Sidebar/languages/pl.json delete mode 100644 plugins/Sidebar/languages/pt-br.json delete mode 100644 plugins/Sidebar/languages/ru.json delete mode 100644 plugins/Sidebar/languages/tr.json delete mode 100644 plugins/Sidebar/languages/zh-tw.json delete mode 100644 plugins/Sidebar/languages/zh.json delete mode 100644 plugins/Sidebar/media/Class.coffee delete mode 100644 plugins/Sidebar/media/Console.coffee delete mode 100644 plugins/Sidebar/media/Console.css delete mode 100644 plugins/Sidebar/media/Menu.coffee delete mode 100644 plugins/Sidebar/media/Menu.css delete mode 100644 plugins/Sidebar/media/Prototypes.coffee delete mode 100644 plugins/Sidebar/media/RateLimit.coffee delete mode 100644 plugins/Sidebar/media/Scrollable.js delete mode 100644 plugins/Sidebar/media/Scrollbable.css delete mode 100644 plugins/Sidebar/media/Sidebar.coffee delete mode 100644 plugins/Sidebar/media/Sidebar.css delete mode 100644 plugins/Sidebar/media/all.css delete mode 100644 plugins/Sidebar/media/all.js delete mode 100644 plugins/Sidebar/media/morphdom.js delete mode 100644 plugins/Sidebar/media_globe/Detector.js delete mode 100644 plugins/Sidebar/media_globe/Tween.js delete mode 100644 plugins/Sidebar/media_globe/all.js delete mode 100644 plugins/Sidebar/media_globe/globe.js delete mode 100644 plugins/Sidebar/media_globe/three.min.js delete mode 100644 plugins/Sidebar/media_globe/world.jpg delete mode 100644 plugins/Sidebar/plugin_info.json delete mode 100644 plugins/Stats/StatsPlugin.py delete mode 100644 plugins/Stats/__init__.py delete mode 100644 plugins/Stats/plugin_info.json delete mode 100644 plugins/TranslateSite/TranslateSitePlugin.py delete mode 100644 plugins/TranslateSite/__init__.py delete mode 100644 plugins/TranslateSite/plugin_info.json delete mode 100644 plugins/Trayicon/TrayiconPlugin.py delete mode 100644 plugins/Trayicon/__init__.py delete mode 100644 plugins/Trayicon/languages/es.json delete mode 100644 plugins/Trayicon/languages/fr.json delete mode 100644 plugins/Trayicon/languages/hu.json delete mode 100644 plugins/Trayicon/languages/it.json delete mode 100644 plugins/Trayicon/languages/jp.json delete mode 100644 plugins/Trayicon/languages/pl.json delete mode 100644 plugins/Trayicon/languages/pt-br.json delete mode 100644 plugins/Trayicon/languages/tr.json delete mode 100644 plugins/Trayicon/languages/zh-tw.json delete mode 100644 plugins/Trayicon/languages/zh.json delete mode 100644 plugins/Trayicon/lib/__init__.py delete mode 100644 plugins/Trayicon/lib/notificationicon.py delete mode 100644 plugins/Trayicon/lib/winfolders.py delete mode 100644 plugins/Trayicon/plugin_info.json delete mode 100644 plugins/Trayicon/trayicon.ico delete mode 100644 plugins/UiConfig/UiConfigPlugin.py delete mode 100644 plugins/UiConfig/__init__.py delete mode 100644 plugins/UiConfig/languages/hu.json delete mode 100644 plugins/UiConfig/languages/jp.json delete mode 100644 plugins/UiConfig/languages/pl.json delete mode 100644 plugins/UiConfig/languages/pt-br.json delete mode 100644 plugins/UiConfig/languages/zh.json delete mode 100644 plugins/UiConfig/media/config.html delete mode 100644 plugins/UiConfig/media/css/Config.css delete mode 100644 plugins/UiConfig/media/css/all.css delete mode 100644 plugins/UiConfig/media/css/button.css delete mode 100644 plugins/UiConfig/media/css/fonts.css delete mode 100644 plugins/UiConfig/media/img/loading.gif delete mode 100644 plugins/UiConfig/media/js/ConfigStorage.coffee delete mode 100644 plugins/UiConfig/media/js/ConfigView.coffee delete mode 100644 plugins/UiConfig/media/js/UiConfig.coffee delete mode 100644 plugins/UiConfig/media/js/all.js delete mode 100644 plugins/UiConfig/media/js/lib/Class.coffee delete mode 100644 plugins/UiConfig/media/js/lib/Promise.coffee delete mode 100644 plugins/UiConfig/media/js/lib/Prototypes.coffee delete mode 100644 plugins/UiConfig/media/js/lib/maquette.js delete mode 100644 plugins/UiConfig/media/js/utils/Animation.coffee delete mode 100644 plugins/UiConfig/media/js/utils/Dollar.coffee delete mode 100644 plugins/UiConfig/media/js/utils/ZeroFrame.coffee delete mode 100644 plugins/UiConfig/plugin_info.json delete mode 100644 plugins/UiFileManager/UiFileManagerPlugin.py delete mode 100644 plugins/UiFileManager/__init__.py delete mode 100644 plugins/UiFileManager/languages/hu.json delete mode 100644 plugins/UiFileManager/languages/jp.json delete mode 100644 plugins/UiFileManager/media/codemirror/LICENSE delete mode 100644 plugins/UiFileManager/media/codemirror/all.css delete mode 100644 plugins/UiFileManager/media/codemirror/all.js delete mode 100644 plugins/UiFileManager/media/codemirror/base/codemirror.css delete mode 100644 plugins/UiFileManager/media/codemirror/base/codemirror.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/dialog/dialog.css delete mode 100644 plugins/UiFileManager/media/codemirror/extension/dialog/dialog.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/edit/closebrackets.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/edit/closetag.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/edit/continuelist.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/edit/matchbrackets.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/edit/matchtags.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/edit/trailingspace.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/fold/brace-fold.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/fold/comment-fold.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/fold/foldcode.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/fold/foldgutter.css delete mode 100644 plugins/UiFileManager/media/codemirror/extension/fold/foldgutter.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/fold/indent-fold.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/fold/markdown-fold.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/fold/xml-fold.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/hint/anyword-hint.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/hint/html-hint.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/hint/show-hint.css delete mode 100644 plugins/UiFileManager/media/codemirror/extension/hint/show-hint.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/hint/sql-hint.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/hint/xml-hint.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/lint/json-lint.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/lint/jsonlint.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/lint/lint.css delete mode 100644 plugins/UiFileManager/media/codemirror/extension/lint/lint.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/mdn-like-custom.css delete mode 100644 plugins/UiFileManager/media/codemirror/extension/scroll/annotatescrollbar.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/scroll/scrollpastend.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/scroll/simplescrollbars.css delete mode 100644 plugins/UiFileManager/media/codemirror/extension/scroll/simplescrollbars.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/search/jump-to-line.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/search/match-highlighter.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/search/matchesonscrollbar.css delete mode 100644 plugins/UiFileManager/media/codemirror/extension/search/matchesonscrollbar.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/search/search.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/search/searchcursor.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/selection/active-line.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/selection/mark-selection.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/selection/selection-pointer.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/simple.js delete mode 100644 plugins/UiFileManager/media/codemirror/extension/sublime.js delete mode 100644 plugins/UiFileManager/media/codemirror/mode/coffeescript.js delete mode 100644 plugins/UiFileManager/media/codemirror/mode/css.js delete mode 100644 plugins/UiFileManager/media/codemirror/mode/go.js delete mode 100644 plugins/UiFileManager/media/codemirror/mode/htmlembedded.js delete mode 100644 plugins/UiFileManager/media/codemirror/mode/htmlmixed.js delete mode 100644 plugins/UiFileManager/media/codemirror/mode/javascript.js delete mode 100644 plugins/UiFileManager/media/codemirror/mode/markdown.js delete mode 100644 plugins/UiFileManager/media/codemirror/mode/python.js delete mode 100644 plugins/UiFileManager/media/codemirror/mode/rust.js delete mode 100644 plugins/UiFileManager/media/codemirror/mode/xml.js delete mode 100644 plugins/UiFileManager/media/css/Menu.css delete mode 100644 plugins/UiFileManager/media/css/Selectbar.css delete mode 100644 plugins/UiFileManager/media/css/UiFileManager.css delete mode 100644 plugins/UiFileManager/media/css/all.css delete mode 100644 plugins/UiFileManager/media/img/loading.gif delete mode 100644 plugins/UiFileManager/media/js/Config.coffee delete mode 100644 plugins/UiFileManager/media/js/FileEditor.coffee delete mode 100644 plugins/UiFileManager/media/js/FileItemList.coffee delete mode 100644 plugins/UiFileManager/media/js/FileList.coffee delete mode 100644 plugins/UiFileManager/media/js/UiFileManager.coffee delete mode 100644 plugins/UiFileManager/media/js/all.js delete mode 100644 plugins/UiFileManager/media/js/lib/Animation.coffee delete mode 100644 plugins/UiFileManager/media/js/lib/Class.coffee delete mode 100644 plugins/UiFileManager/media/js/lib/Dollar.coffee delete mode 100644 plugins/UiFileManager/media/js/lib/ItemList.coffee delete mode 100644 plugins/UiFileManager/media/js/lib/Menu.coffee delete mode 100644 plugins/UiFileManager/media/js/lib/Promise.coffee delete mode 100644 plugins/UiFileManager/media/js/lib/Prototypes.coffee delete mode 100644 plugins/UiFileManager/media/js/lib/RateLimitCb.coffee delete mode 100644 plugins/UiFileManager/media/js/lib/Text.coffee delete mode 100644 plugins/UiFileManager/media/js/lib/Time.coffee delete mode 100644 plugins/UiFileManager/media/js/lib/ZeroFrame.coffee delete mode 100644 plugins/UiFileManager/media/js/lib/maquette.js delete mode 100644 plugins/UiFileManager/media/list.html delete mode 100644 plugins/UiPluginManager/UiPluginManagerPlugin.py delete mode 100644 plugins/UiPluginManager/__init__.py delete mode 100644 plugins/UiPluginManager/media/css/PluginManager.css delete mode 100644 plugins/UiPluginManager/media/css/all.css delete mode 100644 plugins/UiPluginManager/media/css/button.css delete mode 100644 plugins/UiPluginManager/media/css/fonts.css delete mode 100644 plugins/UiPluginManager/media/img/loading.gif delete mode 100644 plugins/UiPluginManager/media/js/PluginList.coffee delete mode 100644 plugins/UiPluginManager/media/js/UiPluginManager.coffee delete mode 100644 plugins/UiPluginManager/media/js/all.js delete mode 100644 plugins/UiPluginManager/media/js/lib/Class.coffee delete mode 100644 plugins/UiPluginManager/media/js/lib/Promise.coffee delete mode 100644 plugins/UiPluginManager/media/js/lib/Prototypes.coffee delete mode 100644 plugins/UiPluginManager/media/js/lib/maquette.js delete mode 100644 plugins/UiPluginManager/media/js/utils/Animation.coffee delete mode 100644 plugins/UiPluginManager/media/js/utils/Dollar.coffee delete mode 100644 plugins/UiPluginManager/media/js/utils/ZeroFrame.coffee delete mode 100644 plugins/UiPluginManager/media/plugin_manager.html delete mode 100644 plugins/Zeroname/README.md delete mode 100644 plugins/Zeroname/SiteManagerPlugin.py delete mode 100644 plugins/Zeroname/__init__.py delete mode 100644 plugins/Zeroname/updater/zeroname_updater.py delete mode 100644 plugins/__init__.py delete mode 100644 plugins/disabled-Bootstrapper/BootstrapperDb.py delete mode 100644 plugins/disabled-Bootstrapper/BootstrapperPlugin.py delete mode 100644 plugins/disabled-Bootstrapper/Test/TestBootstrapper.py delete mode 100644 plugins/disabled-Bootstrapper/Test/conftest.py delete mode 100644 plugins/disabled-Bootstrapper/Test/pytest.ini delete mode 100644 plugins/disabled-Bootstrapper/__init__.py delete mode 100644 plugins/disabled-Bootstrapper/plugin_info.json delete mode 100644 plugins/disabled-Dnschain/SiteManagerPlugin.py delete mode 100644 plugins/disabled-Dnschain/UiRequestPlugin.py delete mode 100644 plugins/disabled-Dnschain/__init__.py delete mode 100644 plugins/disabled-DonationMessage/DonationMessagePlugin.py delete mode 100644 plugins/disabled-DonationMessage/__init__.py delete mode 100644 plugins/disabled-Multiuser/MultiuserPlugin.py delete mode 100644 plugins/disabled-Multiuser/Test/TestMultiuser.py delete mode 100644 plugins/disabled-Multiuser/Test/conftest.py delete mode 100644 plugins/disabled-Multiuser/Test/pytest.ini delete mode 100644 plugins/disabled-Multiuser/UserPlugin.py delete mode 100644 plugins/disabled-Multiuser/__init__.py delete mode 100644 plugins/disabled-Multiuser/plugin_info.json delete mode 100644 plugins/disabled-StemPort/StemPortPlugin.py delete mode 100644 plugins/disabled-StemPort/__init__.py delete mode 100644 plugins/disabled-UiPassword/UiPasswordPlugin.py delete mode 100644 plugins/disabled-UiPassword/__init__.py delete mode 100644 plugins/disabled-UiPassword/login.html delete mode 100644 plugins/disabled-UiPassword/plugin_info.json delete mode 100644 plugins/disabled-ZeronameLocal/SiteManagerPlugin.py delete mode 100644 plugins/disabled-ZeronameLocal/UiRequestPlugin.py delete mode 100644 plugins/disabled-ZeronameLocal/__init__.py diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..2c602a5a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "plugins"] + path = plugins + url = https://github.com/ZeroNetX/ZeroNet-Plugins.git diff --git a/plugins b/plugins new file mode 160000 index 00000000..5c22d549 --- /dev/null +++ b/plugins @@ -0,0 +1 @@ +Subproject commit 5c22d54984363d425d9f7b6aabbf1a871e7e60a1 diff --git a/plugins/AnnounceBitTorrent/AnnounceBitTorrentPlugin.py b/plugins/AnnounceBitTorrent/AnnounceBitTorrentPlugin.py deleted file mode 100644 index fab7bb1f..00000000 --- a/plugins/AnnounceBitTorrent/AnnounceBitTorrentPlugin.py +++ /dev/null @@ -1,148 +0,0 @@ -import time -import urllib.request -import struct -import socket - -import lib.bencode_open as bencode_open -from lib.subtl.subtl import UdpTrackerClient -import socks -import sockshandler -import gevent - -from Plugin import PluginManager -from Config import config -from Debug import Debug -from util import helper - - -# We can only import plugin host clases after the plugins are loaded -@PluginManager.afterLoad -def importHostClasses(): - global Peer, AnnounceError - from Peer import Peer - from Site.SiteAnnouncer import AnnounceError - - -@PluginManager.registerTo("SiteAnnouncer") -class SiteAnnouncerPlugin(object): - def getSupportedTrackers(self): - trackers = super(SiteAnnouncerPlugin, self).getSupportedTrackers() - if config.disable_udp or config.trackers_proxy != "disable": - trackers = [tracker for tracker in trackers if not tracker.startswith("udp://")] - - return trackers - - def getTrackerHandler(self, protocol): - if protocol == "udp": - handler = self.announceTrackerUdp - elif protocol == "http": - handler = self.announceTrackerHttp - elif protocol == "https": - handler = self.announceTrackerHttps - else: - handler = super(SiteAnnouncerPlugin, self).getTrackerHandler(protocol) - return handler - - def announceTrackerUdp(self, tracker_address, mode="start", num_want=10): - s = time.time() - if config.disable_udp: - raise AnnounceError("Udp disabled by config") - if config.trackers_proxy != "disable": - raise AnnounceError("Udp trackers not available with proxies") - - ip, port = tracker_address.split("/")[0].split(":") - tracker = UdpTrackerClient(ip, int(port)) - if helper.getIpType(ip) in self.getOpenedServiceTypes(): - tracker.peer_port = self.fileserver_port - else: - tracker.peer_port = 0 - tracker.connect() - if not tracker.poll_once(): - raise AnnounceError("Could not connect") - tracker.announce(info_hash=self.site.address_sha1, num_want=num_want, left=431102370) - back = tracker.poll_once() - if not back: - raise AnnounceError("No response after %.0fs" % (time.time() - s)) - elif type(back) is dict and "response" in back: - peers = back["response"]["peers"] - else: - raise AnnounceError("Invalid response: %r" % back) - - return peers - - def httpRequest(self, url): - headers = { - 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', - 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.3', - 'Accept-Encoding': 'none', - 'Accept-Language': 'en-US,en;q=0.8', - 'Connection': 'keep-alive' - } - - req = urllib.request.Request(url, headers=headers) - - if config.trackers_proxy == "tor": - tor_manager = self.site.connection_server.tor_manager - handler = sockshandler.SocksiPyHandler(socks.SOCKS5, tor_manager.proxy_ip, tor_manager.proxy_port) - opener = urllib.request.build_opener(handler) - return opener.open(req, timeout=50) - elif config.trackers_proxy == "disable": - return urllib.request.urlopen(req, timeout=25) - else: - proxy_ip, proxy_port = config.trackers_proxy.split(":") - handler = sockshandler.SocksiPyHandler(socks.SOCKS5, proxy_ip, int(proxy_port)) - opener = urllib.request.build_opener(handler) - return opener.open(req, timeout=50) - - def announceTrackerHttps(self, *args, **kwargs): - kwargs["protocol"] = "https" - return self.announceTrackerHttp(*args, **kwargs) - - def announceTrackerHttp(self, tracker_address, mode="start", num_want=10, protocol="http"): - tracker_ip, tracker_port = tracker_address.rsplit(":", 1) - if helper.getIpType(tracker_ip) in self.getOpenedServiceTypes(): - port = self.fileserver_port - else: - port = 1 - params = { - 'info_hash': self.site.address_sha1, - 'peer_id': self.peer_id, 'port': port, - 'uploaded': 0, 'downloaded': 0, 'left': 431102370, 'compact': 1, 'numwant': num_want, - 'event': 'started' - } - - url = protocol + "://" + tracker_address + "?" + urllib.parse.urlencode(params) - - s = time.time() - response = None - # Load url - if config.tor == "always" or config.trackers_proxy != "disable": - timeout = 60 - else: - timeout = 30 - - with gevent.Timeout(timeout, False): # Make sure of timeout - req = self.httpRequest(url) - response = req.read() - req.close() - req = None - - if not response: - raise AnnounceError("No response after %.0fs" % (time.time() - s)) - - # Decode peers - try: - peer_data = bencode_open.loads(response)[b"peers"] - response = None - peer_count = int(len(peer_data) / 6) - peers = [] - for peer_offset in range(peer_count): - off = 6 * peer_offset - peer = peer_data[off:off + 6] - addr, port = struct.unpack('!LH', peer) - peers.append({"addr": socket.inet_ntoa(struct.pack('!L', addr)), "port": port}) - except Exception as err: - raise AnnounceError("Invalid response: %r (%s)" % (response, Debug.formatException(err))) - - return peers diff --git a/plugins/AnnounceBitTorrent/__init__.py b/plugins/AnnounceBitTorrent/__init__.py deleted file mode 100644 index c7422855..00000000 --- a/plugins/AnnounceBitTorrent/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import AnnounceBitTorrentPlugin \ No newline at end of file diff --git a/plugins/AnnounceBitTorrent/plugin_info.json b/plugins/AnnounceBitTorrent/plugin_info.json deleted file mode 100644 index 824749ee..00000000 --- a/plugins/AnnounceBitTorrent/plugin_info.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "AnnounceBitTorrent", - "description": "Discover new peers using BitTorrent trackers.", - "default": "enabled" -} \ No newline at end of file diff --git a/plugins/AnnounceLocal/AnnounceLocalPlugin.py b/plugins/AnnounceLocal/AnnounceLocalPlugin.py deleted file mode 100644 index b9225966..00000000 --- a/plugins/AnnounceLocal/AnnounceLocalPlugin.py +++ /dev/null @@ -1,147 +0,0 @@ -import time - -import gevent - -from Plugin import PluginManager -from Config import config -from . import BroadcastServer - - -@PluginManager.registerTo("SiteAnnouncer") -class SiteAnnouncerPlugin(object): - def announce(self, force=False, *args, **kwargs): - local_announcer = self.site.connection_server.local_announcer - - thread = None - if local_announcer and (force or time.time() - local_announcer.last_discover > 5 * 60): - thread = gevent.spawn(local_announcer.discover, force=force) - back = super(SiteAnnouncerPlugin, self).announce(force=force, *args, **kwargs) - - if thread: - thread.join() - - return back - - -class LocalAnnouncer(BroadcastServer.BroadcastServer): - def __init__(self, server, listen_port): - super(LocalAnnouncer, self).__init__("zeronet", listen_port=listen_port) - self.server = server - - self.sender_info["peer_id"] = self.server.peer_id - self.sender_info["port"] = self.server.port - self.sender_info["broadcast_port"] = listen_port - self.sender_info["rev"] = config.rev - - self.known_peers = {} - self.last_discover = 0 - - def discover(self, force=False): - self.log.debug("Sending discover request (force: %s)" % force) - self.last_discover = time.time() - if force: # Probably new site added, clean cache - self.known_peers = {} - - for peer_id, known_peer in list(self.known_peers.items()): - if time.time() - known_peer["found"] > 20 * 60: - del(self.known_peers[peer_id]) - self.log.debug("Timeout, removing from known_peers: %s" % peer_id) - self.broadcast({"cmd": "discoverRequest", "params": {}}, port=self.listen_port) - - def actionDiscoverRequest(self, sender, params): - back = { - "cmd": "discoverResponse", - "params": { - "sites_changed": self.server.site_manager.sites_changed - } - } - - if sender["peer_id"] not in self.known_peers: - self.known_peers[sender["peer_id"]] = {"added": time.time(), "sites_changed": 0, "updated": 0, "found": time.time()} - self.log.debug("Got discover request from unknown peer %s (%s), time to refresh known peers" % (sender["ip"], sender["peer_id"])) - gevent.spawn_later(1.0, self.discover) # Let the response arrive first to the requester - - return back - - def actionDiscoverResponse(self, sender, params): - if sender["peer_id"] in self.known_peers: - self.known_peers[sender["peer_id"]]["found"] = time.time() - if params["sites_changed"] != self.known_peers.get(sender["peer_id"], {}).get("sites_changed"): - # Peer's site list changed, request the list of new sites - return {"cmd": "siteListRequest"} - else: - # Peer's site list is the same - for site in self.server.sites.values(): - peer = site.peers.get("%s:%s" % (sender["ip"], sender["port"])) - if peer: - peer.found("local") - - def actionSiteListRequest(self, sender, params): - back = [] - sites = list(self.server.sites.values()) - - # Split adresses to group of 100 to avoid UDP size limit - site_groups = [sites[i:i + 100] for i in range(0, len(sites), 100)] - for site_group in site_groups: - res = {} - res["sites_changed"] = self.server.site_manager.sites_changed - res["sites"] = [site.address_hash for site in site_group] - back.append({"cmd": "siteListResponse", "params": res}) - return back - - def actionSiteListResponse(self, sender, params): - s = time.time() - peer_sites = set(params["sites"]) - num_found = 0 - added_sites = [] - for site in self.server.sites.values(): - if site.address_hash in peer_sites: - added = site.addPeer(sender["ip"], sender["port"], source="local") - num_found += 1 - if added: - site.worker_manager.onPeers() - site.updateWebsocket(peers_added=1) - added_sites.append(site) - - # Save sites changed value to avoid unnecessary site list download - if sender["peer_id"] not in self.known_peers: - self.known_peers[sender["peer_id"]] = {"added": time.time()} - - self.known_peers[sender["peer_id"]]["sites_changed"] = params["sites_changed"] - self.known_peers[sender["peer_id"]]["updated"] = time.time() - self.known_peers[sender["peer_id"]]["found"] = time.time() - - self.log.debug( - "Tracker result: Discover from %s response parsed in %.3fs, found: %s added: %s of %s" % - (sender["ip"], time.time() - s, num_found, added_sites, len(peer_sites)) - ) - - -@PluginManager.registerTo("FileServer") -class FileServerPlugin(object): - def __init__(self, *args, **kwargs): - super(FileServerPlugin, self).__init__(*args, **kwargs) - if config.broadcast_port and config.tor != "always" and not config.disable_udp: - self.local_announcer = LocalAnnouncer(self, config.broadcast_port) - else: - self.local_announcer = None - - def start(self, *args, **kwargs): - if self.local_announcer: - gevent.spawn(self.local_announcer.start) - return super(FileServerPlugin, self).start(*args, **kwargs) - - def stop(self): - if self.local_announcer: - self.local_announcer.stop() - res = super(FileServerPlugin, self).stop() - return res - - -@PluginManager.registerTo("ConfigPlugin") -class ConfigPlugin(object): - def createArguments(self): - group = self.parser.add_argument_group("AnnounceLocal plugin") - group.add_argument('--broadcast_port', help='UDP broadcasting port for local peer discovery', default=1544, type=int, metavar='port') - - return super(ConfigPlugin, self).createArguments() diff --git a/plugins/AnnounceLocal/BroadcastServer.py b/plugins/AnnounceLocal/BroadcastServer.py deleted file mode 100644 index 74678896..00000000 --- a/plugins/AnnounceLocal/BroadcastServer.py +++ /dev/null @@ -1,139 +0,0 @@ -import socket -import logging -import time -from contextlib import closing - -from Debug import Debug -from util import UpnpPunch -from util import Msgpack - - -class BroadcastServer(object): - def __init__(self, service_name, listen_port=1544, listen_ip=''): - self.log = logging.getLogger("BroadcastServer") - self.listen_port = listen_port - self.listen_ip = listen_ip - - self.running = False - self.sock = None - self.sender_info = {"service": service_name} - - def createBroadcastSocket(self): - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - if hasattr(socket, 'SO_REUSEPORT'): - try: - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) - except Exception as err: - self.log.warning("Error setting SO_REUSEPORT: %s" % err) - - binded = False - for retry in range(3): - try: - sock.bind((self.listen_ip, self.listen_port)) - binded = True - break - except Exception as err: - self.log.error( - "Socket bind to %s:%s error: %s, retry #%s" % - (self.listen_ip, self.listen_port, Debug.formatException(err), retry) - ) - time.sleep(retry) - - if binded: - return sock - else: - return False - - def start(self): # Listens for discover requests - self.sock = self.createBroadcastSocket() - if not self.sock: - self.log.error("Unable to listen on port %s" % self.listen_port) - return - - self.log.debug("Started on port %s" % self.listen_port) - - self.running = True - - while self.running: - try: - data, addr = self.sock.recvfrom(8192) - except Exception as err: - if self.running: - self.log.error("Listener receive error: %s" % err) - continue - - if not self.running: - break - - try: - message = Msgpack.unpack(data) - response_addr, message = self.handleMessage(addr, message) - if message: - self.send(response_addr, message) - except Exception as err: - self.log.error("Handlemessage error: %s" % Debug.formatException(err)) - self.log.debug("Stopped listening on port %s" % self.listen_port) - - def stop(self): - self.log.debug("Stopping, socket: %s" % self.sock) - self.running = False - if self.sock: - self.sock.close() - - def send(self, addr, message): - if type(message) is not list: - message = [message] - - for message_part in message: - message_part["sender"] = self.sender_info - - self.log.debug("Send to %s: %s" % (addr, message_part["cmd"])) - with closing(socket.socket(socket.AF_INET, socket.SOCK_DGRAM)) as sock: - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.sendto(Msgpack.pack(message_part), addr) - - def getMyIps(self): - return UpnpPunch._get_local_ips() - - def broadcast(self, message, port=None): - if not port: - port = self.listen_port - - my_ips = self.getMyIps() - addr = ("255.255.255.255", port) - - message["sender"] = self.sender_info - self.log.debug("Broadcast using ips %s on port %s: %s" % (my_ips, port, message["cmd"])) - - for my_ip in my_ips: - try: - with closing(socket.socket(socket.AF_INET, socket.SOCK_DGRAM)) as sock: - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) - sock.bind((my_ip, 0)) - sock.sendto(Msgpack.pack(message), addr) - except Exception as err: - self.log.warning("Error sending broadcast using ip %s: %s" % (my_ip, err)) - - def handleMessage(self, addr, message): - self.log.debug("Got from %s: %s" % (addr, message["cmd"])) - cmd = message["cmd"] - params = message.get("params", {}) - sender = message["sender"] - sender["ip"] = addr[0] - - func_name = "action" + cmd[0].upper() + cmd[1:] - func = getattr(self, func_name, None) - - if sender["service"] != "zeronet" or sender["peer_id"] == self.sender_info["peer_id"]: - # Skip messages not for us or sent by us - message = None - elif func: - message = func(sender, params) - else: - self.log.debug("Unknown cmd: %s" % cmd) - message = None - - return (sender["ip"], sender["broadcast_port"]), message diff --git a/plugins/AnnounceLocal/Test/TestAnnounce.py b/plugins/AnnounceLocal/Test/TestAnnounce.py deleted file mode 100644 index 4def02ed..00000000 --- a/plugins/AnnounceLocal/Test/TestAnnounce.py +++ /dev/null @@ -1,113 +0,0 @@ -import time -import copy - -import gevent -import pytest -import mock - -from AnnounceLocal import AnnounceLocalPlugin -from File import FileServer -from Test import Spy - -@pytest.fixture -def announcer(file_server, site): - file_server.sites[site.address] = site - announcer = AnnounceLocalPlugin.LocalAnnouncer(file_server, listen_port=1100) - file_server.local_announcer = announcer - announcer.listen_port = 1100 - announcer.sender_info["broadcast_port"] = 1100 - announcer.getMyIps = mock.MagicMock(return_value=["127.0.0.1"]) - announcer.discover = mock.MagicMock(return_value=False) # Don't send discover requests automatically - gevent.spawn(announcer.start) - time.sleep(0.5) - - assert file_server.local_announcer.running - return file_server.local_announcer - -@pytest.fixture -def announcer_remote(request, site_temp): - file_server_remote = FileServer("127.0.0.1", 1545) - file_server_remote.sites[site_temp.address] = site_temp - announcer = AnnounceLocalPlugin.LocalAnnouncer(file_server_remote, listen_port=1101) - file_server_remote.local_announcer = announcer - announcer.listen_port = 1101 - announcer.sender_info["broadcast_port"] = 1101 - announcer.getMyIps = mock.MagicMock(return_value=["127.0.0.1"]) - announcer.discover = mock.MagicMock(return_value=False) # Don't send discover requests automatically - gevent.spawn(announcer.start) - time.sleep(0.5) - - assert file_server_remote.local_announcer.running - - def cleanup(): - file_server_remote.stop() - request.addfinalizer(cleanup) - - - return file_server_remote.local_announcer - -@pytest.mark.usefixtures("resetSettings") -@pytest.mark.usefixtures("resetTempSettings") -class TestAnnounce: - def testSenderInfo(self, announcer): - sender_info = announcer.sender_info - assert sender_info["port"] > 0 - assert len(sender_info["peer_id"]) == 20 - assert sender_info["rev"] > 0 - - def testIgnoreSelfMessages(self, announcer): - # No response to messages that has same peer_id as server - assert not announcer.handleMessage(("0.0.0.0", 123), {"cmd": "discoverRequest", "sender": announcer.sender_info, "params": {}})[1] - - # Response to messages with different peer id - sender_info = copy.copy(announcer.sender_info) - sender_info["peer_id"] += "-" - addr, res = announcer.handleMessage(("0.0.0.0", 123), {"cmd": "discoverRequest", "sender": sender_info, "params": {}}) - assert res["params"]["sites_changed"] > 0 - - def testDiscoverRequest(self, announcer, announcer_remote): - assert len(announcer_remote.known_peers) == 0 - with Spy.Spy(announcer_remote, "handleMessage") as responses: - announcer_remote.broadcast({"cmd": "discoverRequest", "params": {}}, port=announcer.listen_port) - time.sleep(0.1) - - response_cmds = [response[1]["cmd"] for response in responses] - assert response_cmds == ["discoverResponse", "siteListResponse"] - assert len(responses[-1][1]["params"]["sites"]) == 1 - - # It should only request siteList if sites_changed value is different from last response - with Spy.Spy(announcer_remote, "handleMessage") as responses: - announcer_remote.broadcast({"cmd": "discoverRequest", "params": {}}, port=announcer.listen_port) - time.sleep(0.1) - - response_cmds = [response[1]["cmd"] for response in responses] - assert response_cmds == ["discoverResponse"] - - def testPeerDiscover(self, announcer, announcer_remote, site): - assert announcer.server.peer_id != announcer_remote.server.peer_id - assert len(list(announcer.server.sites.values())[0].peers) == 0 - announcer.broadcast({"cmd": "discoverRequest"}, port=announcer_remote.listen_port) - time.sleep(0.1) - assert len(list(announcer.server.sites.values())[0].peers) == 1 - - def testRecentPeerList(self, announcer, announcer_remote, site): - assert len(site.peers_recent) == 0 - assert len(site.peers) == 0 - with Spy.Spy(announcer, "handleMessage") as responses: - announcer.broadcast({"cmd": "discoverRequest", "params": {}}, port=announcer_remote.listen_port) - time.sleep(0.1) - assert [response[1]["cmd"] for response in responses] == ["discoverResponse", "siteListResponse"] - assert len(site.peers_recent) == 1 - assert len(site.peers) == 1 - - # It should update peer without siteListResponse - last_time_found = list(site.peers.values())[0].time_found - site.peers_recent.clear() - with Spy.Spy(announcer, "handleMessage") as responses: - announcer.broadcast({"cmd": "discoverRequest", "params": {}}, port=announcer_remote.listen_port) - time.sleep(0.1) - assert [response[1]["cmd"] for response in responses] == ["discoverResponse"] - assert len(site.peers_recent) == 1 - assert list(site.peers.values())[0].time_found > last_time_found - - diff --git a/plugins/AnnounceLocal/Test/conftest.py b/plugins/AnnounceLocal/Test/conftest.py deleted file mode 100644 index a88c642c..00000000 --- a/plugins/AnnounceLocal/Test/conftest.py +++ /dev/null @@ -1,4 +0,0 @@ -from src.Test.conftest import * - -from Config import config -config.broadcast_port = 0 diff --git a/plugins/AnnounceLocal/Test/pytest.ini b/plugins/AnnounceLocal/Test/pytest.ini deleted file mode 100644 index d09210d1..00000000 --- a/plugins/AnnounceLocal/Test/pytest.ini +++ /dev/null @@ -1,5 +0,0 @@ -[pytest] -python_files = Test*.py -addopts = -rsxX -v --durations=6 -markers = - webtest: mark a test as a webtest. \ No newline at end of file diff --git a/plugins/AnnounceLocal/__init__.py b/plugins/AnnounceLocal/__init__.py deleted file mode 100644 index 5b80abd2..00000000 --- a/plugins/AnnounceLocal/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import AnnounceLocalPlugin \ No newline at end of file diff --git a/plugins/AnnounceLocal/plugin_info.json b/plugins/AnnounceLocal/plugin_info.json deleted file mode 100644 index 2908cbf1..00000000 --- a/plugins/AnnounceLocal/plugin_info.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "AnnounceLocal", - "description": "Discover LAN clients using UDP broadcasting.", - "default": "enabled" -} \ No newline at end of file diff --git a/plugins/AnnounceShare/AnnounceSharePlugin.py b/plugins/AnnounceShare/AnnounceSharePlugin.py deleted file mode 100644 index 057ce55a..00000000 --- a/plugins/AnnounceShare/AnnounceSharePlugin.py +++ /dev/null @@ -1,190 +0,0 @@ -import time -import os -import logging -import json -import atexit - -import gevent - -from Config import config -from Plugin import PluginManager -from util import helper - - -class TrackerStorage(object): - def __init__(self): - self.log = logging.getLogger("TrackerStorage") - self.file_path = "%s/trackers.json" % config.data_dir - self.load() - self.time_discover = 0.0 - atexit.register(self.save) - - def getDefaultFile(self): - return {"shared": {}} - - def onTrackerFound(self, tracker_address, type="shared", my=False): - if not tracker_address.startswith("zero://"): - return False - - trackers = self.getTrackers() - added = False - if tracker_address not in trackers: - trackers[tracker_address] = { - "time_added": time.time(), - "time_success": 0, - "latency": 99.0, - "num_error": 0, - "my": False - } - self.log.debug("New tracker found: %s" % tracker_address) - added = True - - trackers[tracker_address]["time_found"] = time.time() - trackers[tracker_address]["my"] = my - return added - - def onTrackerSuccess(self, tracker_address, latency): - trackers = self.getTrackers() - if tracker_address not in trackers: - return False - - trackers[tracker_address]["latency"] = latency - trackers[tracker_address]["time_success"] = time.time() - trackers[tracker_address]["num_error"] = 0 - - def onTrackerError(self, tracker_address): - trackers = self.getTrackers() - if tracker_address not in trackers: - return False - - trackers[tracker_address]["time_error"] = time.time() - trackers[tracker_address]["num_error"] += 1 - - if len(self.getWorkingTrackers()) >= config.working_shared_trackers_limit: - error_limit = 5 - else: - error_limit = 30 - error_limit - - if trackers[tracker_address]["num_error"] > error_limit and trackers[tracker_address]["time_success"] < time.time() - 60 * 60: - self.log.debug("Tracker %s looks down, removing." % tracker_address) - del trackers[tracker_address] - - def getTrackers(self, type="shared"): - return self.file_content.setdefault(type, {}) - - def getWorkingTrackers(self, type="shared"): - trackers = { - key: tracker for key, tracker in self.getTrackers(type).items() - if tracker["time_success"] > time.time() - 60 * 60 - } - return trackers - - def getFileContent(self): - if not os.path.isfile(self.file_path): - open(self.file_path, "w").write("{}") - return self.getDefaultFile() - try: - return json.load(open(self.file_path)) - except Exception as err: - self.log.error("Error loading trackers list: %s" % err) - return self.getDefaultFile() - - def load(self): - self.file_content = self.getFileContent() - - trackers = self.getTrackers() - self.log.debug("Loaded %s shared trackers" % len(trackers)) - for address, tracker in list(trackers.items()): - tracker["num_error"] = 0 - if not address.startswith("zero://"): - del trackers[address] - - def save(self): - s = time.time() - helper.atomicWrite(self.file_path, json.dumps(self.file_content, indent=2, sort_keys=True).encode("utf8")) - self.log.debug("Saved in %.3fs" % (time.time() - s)) - - def discoverTrackers(self, peers): - if len(self.getWorkingTrackers()) > config.working_shared_trackers_limit: - return False - s = time.time() - num_success = 0 - for peer in peers: - if peer.connection and peer.connection.handshake.get("rev", 0) < 3560: - continue # Not supported - - res = peer.request("getTrackers") - if not res or "error" in res: - continue - - num_success += 1 - for tracker_address in res["trackers"]: - if type(tracker_address) is bytes: # Backward compatibilitys - tracker_address = tracker_address.decode("utf8") - added = self.onTrackerFound(tracker_address) - if added: # Only add one tracker from one source - break - - if not num_success and len(peers) < 20: - self.time_discover = 0.0 - - if num_success: - self.save() - - self.log.debug("Trackers discovered from %s/%s peers in %.3fs" % (num_success, len(peers), time.time() - s)) - - -if "tracker_storage" not in locals(): - tracker_storage = TrackerStorage() - - -@PluginManager.registerTo("SiteAnnouncer") -class SiteAnnouncerPlugin(object): - def getTrackers(self): - if tracker_storage.time_discover < time.time() - 5 * 60: - tracker_storage.time_discover = time.time() - gevent.spawn(tracker_storage.discoverTrackers, self.site.getConnectedPeers()) - trackers = super(SiteAnnouncerPlugin, self).getTrackers() - shared_trackers = list(tracker_storage.getTrackers("shared").keys()) - if shared_trackers: - return trackers + shared_trackers - else: - return trackers - - def announceTracker(self, tracker, *args, **kwargs): - res = super(SiteAnnouncerPlugin, self).announceTracker(tracker, *args, **kwargs) - if res: - latency = res - tracker_storage.onTrackerSuccess(tracker, latency) - elif res is False: - tracker_storage.onTrackerError(tracker) - - return res - - -@PluginManager.registerTo("FileRequest") -class FileRequestPlugin(object): - def actionGetTrackers(self, params): - shared_trackers = list(tracker_storage.getWorkingTrackers("shared").keys()) - self.response({"trackers": shared_trackers}) - - -@PluginManager.registerTo("FileServer") -class FileServerPlugin(object): - def portCheck(self, *args, **kwargs): - res = super(FileServerPlugin, self).portCheck(*args, **kwargs) - if res and not config.tor == "always" and "Bootstrapper" in PluginManager.plugin_manager.plugin_names: - for ip in self.ip_external_list: - my_tracker_address = "zero://%s:%s" % (ip, config.fileserver_port) - tracker_storage.onTrackerFound(my_tracker_address, my=True) - return res - - -@PluginManager.registerTo("ConfigPlugin") -class ConfigPlugin(object): - def createArguments(self): - group = self.parser.add_argument_group("AnnounceShare plugin") - group.add_argument('--working_shared_trackers_limit', help='Stop discovering new shared trackers after this number of shared trackers reached', default=5, type=int, metavar='limit') - - return super(ConfigPlugin, self).createArguments() diff --git a/plugins/AnnounceShare/Test/TestAnnounceShare.py b/plugins/AnnounceShare/Test/TestAnnounceShare.py deleted file mode 100644 index 7178eac8..00000000 --- a/plugins/AnnounceShare/Test/TestAnnounceShare.py +++ /dev/null @@ -1,24 +0,0 @@ -import pytest - -from AnnounceShare import AnnounceSharePlugin -from Peer import Peer -from Config import config - - -@pytest.mark.usefixtures("resetSettings") -@pytest.mark.usefixtures("resetTempSettings") -class TestAnnounceShare: - def testAnnounceList(self, file_server): - open("%s/trackers.json" % config.data_dir, "w").write("{}") - tracker_storage = AnnounceSharePlugin.tracker_storage - tracker_storage.load() - peer = Peer(file_server.ip, 1544, connection_server=file_server) - assert peer.request("getTrackers")["trackers"] == [] - - tracker_storage.onTrackerFound("zero://%s:15441" % file_server.ip) - assert peer.request("getTrackers")["trackers"] == [] - - # It needs to have at least one successfull announce to be shared to other peers - tracker_storage.onTrackerSuccess("zero://%s:15441" % file_server.ip, 1.0) - assert peer.request("getTrackers")["trackers"] == ["zero://%s:15441" % file_server.ip] - diff --git a/plugins/AnnounceShare/Test/conftest.py b/plugins/AnnounceShare/Test/conftest.py deleted file mode 100644 index 5abd4dd6..00000000 --- a/plugins/AnnounceShare/Test/conftest.py +++ /dev/null @@ -1,3 +0,0 @@ -from src.Test.conftest import * - -from Config import config diff --git a/plugins/AnnounceShare/Test/pytest.ini b/plugins/AnnounceShare/Test/pytest.ini deleted file mode 100644 index d09210d1..00000000 --- a/plugins/AnnounceShare/Test/pytest.ini +++ /dev/null @@ -1,5 +0,0 @@ -[pytest] -python_files = Test*.py -addopts = -rsxX -v --durations=6 -markers = - webtest: mark a test as a webtest. \ No newline at end of file diff --git a/plugins/AnnounceShare/__init__.py b/plugins/AnnounceShare/__init__.py deleted file mode 100644 index dc1e40bd..00000000 --- a/plugins/AnnounceShare/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import AnnounceSharePlugin diff --git a/plugins/AnnounceShare/plugin_info.json b/plugins/AnnounceShare/plugin_info.json deleted file mode 100644 index 0ad07e71..00000000 --- a/plugins/AnnounceShare/plugin_info.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "AnnounceShare", - "description": "Share possible trackers between clients.", - "default": "enabled" -} \ No newline at end of file diff --git a/plugins/AnnounceZero/AnnounceZeroPlugin.py b/plugins/AnnounceZero/AnnounceZeroPlugin.py deleted file mode 100644 index 42196831..00000000 --- a/plugins/AnnounceZero/AnnounceZeroPlugin.py +++ /dev/null @@ -1,141 +0,0 @@ -import time -import itertools - -from Plugin import PluginManager -from util import helper -from Crypt import CryptEd25519 -from Crypt import CryptRsa - -allow_reload = False # No source reload supported in this plugin -time_full_announced = {} # Tracker address: Last announced all site to tracker -connection_pool = {} # Tracker address: Peer object - - -# We can only import plugin host clases after the plugins are loaded -@PluginManager.afterLoad -def importHostClasses(): - global Peer, AnnounceError - from Peer import Peer - from Site.SiteAnnouncer import AnnounceError - - -# Process result got back from tracker -def processPeerRes(tracker_address, site, peers): - added = 0 - - # Onion - found_onion = 0 - for packed_address in peers["onion"]: - found_onion += 1 - peer_onion, peer_port = helper.unpackOnionAddress(packed_address) - if site.addPeer(peer_onion, peer_port, source="tracker"): - added += 1 - - # Ip4 - found_ipv4 = 0 - peers_normal = itertools.chain(peers.get("ip4", []), peers.get("ipv4", []), peers.get("ipv6", [])) - for packed_address in peers_normal: - found_ipv4 += 1 - peer_ip, peer_port = helper.unpackAddress(packed_address) - if site.addPeer(peer_ip, peer_port, source="tracker"): - added += 1 - - if added: - site.worker_manager.onPeers() - site.updateWebsocket(peers_added=added) - return added - - -@PluginManager.registerTo("SiteAnnouncer") -class SiteAnnouncerPlugin(object): - def getTrackerHandler(self, protocol): - if protocol == "zero": - return self.announceTrackerZero - else: - return super(SiteAnnouncerPlugin, self).getTrackerHandler(protocol) - - def announceTrackerZero(self, tracker_address, mode="start", num_want=10): - global time_full_announced - s = time.time() - - need_types = ["ip4"] # ip4 for backward compatibility reasons - need_types += self.site.connection_server.supported_ip_types - if self.site.connection_server.tor_manager.enabled: - need_types.append("onion") - - if mode == "start" or mode == "more": # Single: Announce only this site - sites = [self.site] - full_announce = False - else: # Multi: Announce all currently serving site - full_announce = True - if time.time() - time_full_announced.get(tracker_address, 0) < 60 * 15: # No reannounce all sites within short time - return None - time_full_announced[tracker_address] = time.time() - from Site import SiteManager - sites = [site for site in SiteManager.site_manager.sites.values() if site.isServing()] - - # Create request - add_types = self.getOpenedServiceTypes() - request = { - "hashes": [], "onions": [], "port": self.fileserver_port, "need_types": need_types, "need_num": 20, "add": add_types - } - for site in sites: - if "onion" in add_types: - onion = self.site.connection_server.tor_manager.getOnion(site.address) - request["onions"].append(onion) - request["hashes"].append(site.address_hash) - - # Tracker can remove sites that we don't announce - if full_announce: - request["delete"] = True - - # Sent request to tracker - tracker_peer = connection_pool.get(tracker_address) # Re-use tracker connection if possible - if not tracker_peer: - tracker_ip, tracker_port = tracker_address.rsplit(":", 1) - tracker_peer = Peer(str(tracker_ip), int(tracker_port), connection_server=self.site.connection_server) - tracker_peer.is_tracker_connection = True - connection_pool[tracker_address] = tracker_peer - - res = tracker_peer.request("announce", request) - - if not res or "peers" not in res: - if full_announce: - time_full_announced[tracker_address] = 0 - raise AnnounceError("Invalid response: %s" % res) - - # Add peers from response to site - site_index = 0 - peers_added = 0 - for site_res in res["peers"]: - site = sites[site_index] - peers_added += processPeerRes(tracker_address, site, site_res) - site_index += 1 - - # Check if we need to sign prove the onion addresses - if "onion_sign_this" in res: - self.site.log.debug("Signing %s for %s to add %s onions" % (res["onion_sign_this"], tracker_address, len(sites))) - request["onion_signs"] = {} - request["onion_sign_this"] = res["onion_sign_this"] - request["need_num"] = 0 - for site in sites: - onion = self.site.connection_server.tor_manager.getOnion(site.address) - publickey = self.site.connection_server.tor_manager.getPublickey(onion) - if publickey not in request["onion_signs"]: - sign = CryptRsa.sign(res["onion_sign_this"].encode("utf8"), self.site.connection_server.tor_manager.getPrivatekey(onion)) - request["onion_signs"][publickey] = sign - res = tracker_peer.request("announce", request) - if not res or "onion_sign_this" in res: - if full_announce: - time_full_announced[tracker_address] = 0 - raise AnnounceError("Announce onion address to failed: %s" % res) - - if full_announce: - tracker_peer.remove() # Close connection, we don't need it in next 5 minute - - self.site.log.debug( - "Tracker announce result: zero://%s (sites: %s, new peers: %s, add: %s, mode: %s) in %.3fs" % - (tracker_address, site_index, peers_added, add_types, mode, time.time() - s) - ) - - return True diff --git a/plugins/AnnounceZero/__init__.py b/plugins/AnnounceZero/__init__.py deleted file mode 100644 index 8aec5ddb..00000000 --- a/plugins/AnnounceZero/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import AnnounceZeroPlugin \ No newline at end of file diff --git a/plugins/AnnounceZero/plugin_info.json b/plugins/AnnounceZero/plugin_info.json deleted file mode 100644 index 50e7cf7f..00000000 --- a/plugins/AnnounceZero/plugin_info.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "AnnounceZero", - "description": "Announce using ZeroNet protocol.", - "default": "enabled" -} \ No newline at end of file diff --git a/plugins/Benchmark/BenchmarkDb.py b/plugins/Benchmark/BenchmarkDb.py deleted file mode 100644 index a767a3f4..00000000 --- a/plugins/Benchmark/BenchmarkDb.py +++ /dev/null @@ -1,143 +0,0 @@ -import os -import json -import contextlib -import time - -from Plugin import PluginManager -from Config import config - - -@PluginManager.registerTo("Actions") -class ActionsPlugin: - def getBenchmarkTests(self, online=False): - tests = super().getBenchmarkTests(online) - tests.extend([ - {"func": self.testDbConnect, "num": 10, "time_standard": 0.27}, - {"func": self.testDbInsert, "num": 10, "time_standard": 0.91}, - {"func": self.testDbInsertMultiuser, "num": 1, "time_standard": 0.57}, - {"func": self.testDbQueryIndexed, "num": 1000, "time_standard": 0.84}, - {"func": self.testDbQueryNotIndexed, "num": 1000, "time_standard": 1.30} - ]) - return tests - - - @contextlib.contextmanager - def getTestDb(self): - from Db import Db - path = "%s/benchmark.db" % config.data_dir - if os.path.isfile(path): - os.unlink(path) - schema = { - "db_name": "TestDb", - "db_file": path, - "maps": { - ".*": { - "to_table": { - "test": "test" - } - } - }, - "tables": { - "test": { - "cols": [ - ["test_id", "INTEGER"], - ["title", "TEXT"], - ["json_id", "INTEGER REFERENCES json (json_id)"] - ], - "indexes": ["CREATE UNIQUE INDEX test_key ON test(test_id, json_id)"], - "schema_changed": 1426195822 - } - } - } - - db = Db.Db(schema, path) - - yield db - - db.close() - if os.path.isfile(path): - os.unlink(path) - - def testDbConnect(self, num_run=1): - import sqlite3 - for i in range(num_run): - with self.getTestDb() as db: - db.checkTables() - yield "." - yield "(SQLite version: %s, API: %s)" % (sqlite3.sqlite_version, sqlite3.version) - - def testDbInsert(self, num_run=1): - yield "x 1000 lines " - for u in range(num_run): - with self.getTestDb() as db: - db.checkTables() - data = {"test": []} - for i in range(1000): # 1000 line of data - data["test"].append({"test_id": i, "title": "Testdata for %s message %s" % (u, i)}) - json.dump(data, open("%s/test_%s.json" % (config.data_dir, u), "w")) - db.updateJson("%s/test_%s.json" % (config.data_dir, u)) - os.unlink("%s/test_%s.json" % (config.data_dir, u)) - assert db.execute("SELECT COUNT(*) FROM test").fetchone()[0] == 1000 - yield "." - - def fillTestDb(self, db): - db.checkTables() - cur = db.getCursor() - cur.logging = False - for u in range(100, 200): # 100 user - data = {"test": []} - for i in range(100): # 1000 line of data - data["test"].append({"test_id": i, "title": "Testdata for %s message %s" % (u, i)}) - json.dump(data, open("%s/test_%s.json" % (config.data_dir, u), "w")) - db.updateJson("%s/test_%s.json" % (config.data_dir, u), cur=cur) - os.unlink("%s/test_%s.json" % (config.data_dir, u)) - if u % 10 == 0: - yield "." - - def testDbInsertMultiuser(self, num_run=1): - yield "x 100 users x 100 lines " - for u in range(num_run): - with self.getTestDb() as db: - for progress in self.fillTestDb(db): - yield progress - num_rows = db.execute("SELECT COUNT(*) FROM test").fetchone()[0] - assert num_rows == 10000, "%s != 10000" % num_rows - - def testDbQueryIndexed(self, num_run=1): - s = time.time() - with self.getTestDb() as db: - for progress in self.fillTestDb(db): - pass - yield " (Db warmup done in %.3fs) " % (time.time() - s) - found_total = 0 - for i in range(num_run): # 1000x by test_id - found = 0 - res = db.execute("SELECT * FROM test WHERE test_id = %s" % (i % 100)) - for row in res: - found_total += 1 - found += 1 - del(res) - yield "." - assert found == 100, "%s != 100 (i: %s)" % (found, i) - yield "Found: %s" % found_total - - def testDbQueryNotIndexed(self, num_run=1): - s = time.time() - with self.getTestDb() as db: - for progress in self.fillTestDb(db): - pass - yield " (Db warmup done in %.3fs) " % (time.time() - s) - found_total = 0 - for i in range(num_run): # 1000x by test_id - found = 0 - res = db.execute("SELECT * FROM test WHERE json_id = %s" % i) - for row in res: - found_total += 1 - found += 1 - yield "." - del(res) - if i == 0 or i > 100: - assert found == 0, "%s != 0 (i: %s)" % (found, i) - else: - assert found == 100, "%s != 100 (i: %s)" % (found, i) - yield "Found: %s" % found_total diff --git a/plugins/Benchmark/BenchmarkPack.py b/plugins/Benchmark/BenchmarkPack.py deleted file mode 100644 index 6b92e43a..00000000 --- a/plugins/Benchmark/BenchmarkPack.py +++ /dev/null @@ -1,183 +0,0 @@ -import os -import io -from collections import OrderedDict - -from Plugin import PluginManager -from Config import config -from util import Msgpack - - -@PluginManager.registerTo("Actions") -class ActionsPlugin: - def createZipFile(self, path): - import zipfile - test_data = b"Test" * 1024 - file_name = b"\xc3\x81rv\xc3\xadzt\xc5\xb1r\xc5\x91%s.txt".decode("utf8") - with zipfile.ZipFile(path, 'w') as archive: - for y in range(100): - zip_info = zipfile.ZipInfo(file_name % y, (1980, 1, 1, 0, 0, 0)) - zip_info.compress_type = zipfile.ZIP_DEFLATED - zip_info.create_system = 3 - zip_info.flag_bits = 0 - zip_info.external_attr = 25165824 - archive.writestr(zip_info, test_data) - - def testPackZip(self, num_run=1): - """ - Test zip file creating - """ - yield "x 100 x 5KB " - from Crypt import CryptHash - zip_path = '%s/test.zip' % config.data_dir - for i in range(num_run): - self.createZipFile(zip_path) - yield "." - - archive_size = os.path.getsize(zip_path) / 1024 - yield "(Generated file size: %.2fkB)" % archive_size - - hash = CryptHash.sha512sum(open(zip_path, "rb")) - valid = "cb32fb43783a1c06a2170a6bc5bb228a032b67ff7a1fd7a5efb9b467b400f553" - assert hash == valid, "Invalid hash: %s != %s
      " % (hash, valid) - os.unlink(zip_path) - - def testUnpackZip(self, num_run=1): - """ - Test zip file reading - """ - yield "x 100 x 5KB " - import zipfile - zip_path = '%s/test.zip' % config.data_dir - test_data = b"Test" * 1024 - file_name = b"\xc3\x81rv\xc3\xadzt\xc5\xb1r\xc5\x91".decode("utf8") - - self.createZipFile(zip_path) - for i in range(num_run): - with zipfile.ZipFile(zip_path) as archive: - for f in archive.filelist: - assert f.filename.startswith(file_name), "Invalid filename: %s != %s" % (f.filename, file_name) - data = archive.open(f.filename).read() - assert archive.open(f.filename).read() == test_data, "Invalid data: %s..." % data[0:30] - yield "." - - os.unlink(zip_path) - - def createArchiveFile(self, path, archive_type="gz"): - import tarfile - import gzip - - # Monkey patch _init_write_gz to use fixed date in order to keep the hash independent from datetime - def nodate_write_gzip_header(self): - self._write_mtime = 0 - original_write_gzip_header(self) - - test_data_io = io.BytesIO(b"Test" * 1024) - file_name = b"\xc3\x81rv\xc3\xadzt\xc5\xb1r\xc5\x91%s.txt".decode("utf8") - - original_write_gzip_header = gzip.GzipFile._write_gzip_header - gzip.GzipFile._write_gzip_header = nodate_write_gzip_header - with tarfile.open(path, 'w:%s' % archive_type) as archive: - for y in range(100): - test_data_io.seek(0) - tar_info = tarfile.TarInfo(file_name % y) - tar_info.size = 4 * 1024 - archive.addfile(tar_info, test_data_io) - - def testPackArchive(self, num_run=1, archive_type="gz"): - """ - Test creating tar archive files - """ - yield "x 100 x 5KB " - from Crypt import CryptHash - - hash_valid_db = { - "gz": "92caec5121a31709cbbc8c11b0939758e670b055bbbe84f9beb3e781dfde710f", - "bz2": "b613f41e6ee947c8b9b589d3e8fa66f3e28f63be23f4faf015e2f01b5c0b032d", - "xz": "ae43892581d770959c8d993daffab25fd74490b7cf9fafc7aaee746f69895bcb", - } - archive_path = '%s/test.tar.%s' % (config.data_dir, archive_type) - for i in range(num_run): - self.createArchiveFile(archive_path, archive_type=archive_type) - yield "." - - archive_size = os.path.getsize(archive_path) / 1024 - yield "(Generated file size: %.2fkB)" % archive_size - - hash = CryptHash.sha512sum(open("%s/test.tar.%s" % (config.data_dir, archive_type), "rb")) - valid = hash_valid_db[archive_type] - assert hash == valid, "Invalid hash: %s != %s
      " % (hash, valid) - - if os.path.isfile(archive_path): - os.unlink(archive_path) - - def testUnpackArchive(self, num_run=1, archive_type="gz"): - """ - Test reading tar archive files - """ - yield "x 100 x 5KB " - import tarfile - - test_data = b"Test" * 1024 - file_name = b"\xc3\x81rv\xc3\xadzt\xc5\xb1r\xc5\x91%s.txt".decode("utf8") - archive_path = '%s/test.tar.%s' % (config.data_dir, archive_type) - self.createArchiveFile(archive_path, archive_type=archive_type) - for i in range(num_run): - with tarfile.open(archive_path, 'r:%s' % archive_type) as archive: - for y in range(100): - assert archive.extractfile(file_name % y).read() == test_data - yield "." - if os.path.isfile(archive_path): - os.unlink(archive_path) - - def testPackMsgpack(self, num_run=1): - """ - Test msgpack encoding - """ - yield "x 100 x 5KB " - binary = b'fqv\xf0\x1a"e\x10,\xbe\x9cT\x9e(\xa5]u\x072C\x8c\x15\xa2\xa8\x93Sw)\x19\x02\xdd\t\xfb\xf67\x88\xd9\xee\x86\xa1\xe4\xb6,\xc6\x14\xbb\xd7$z\x1d\xb2\xda\x85\xf5\xa0\x97^\x01*\xaf\xd3\xb0!\xb7\x9d\xea\x89\xbbh8\xa1"\xa7]e(@\xa2\xa5g\xb7[\xae\x8eE\xc2\x9fL\xb6s\x19\x19\r\xc8\x04S\xd0N\xe4]?/\x01\xea\xf6\xec\xd1\xb3\xc2\x91\x86\xd7\xf4K\xdf\xc2lV\xf4\xe8\x80\xfc\x8ep\xbb\x82\xb3\x86\x98F\x1c\xecS\xc8\x15\xcf\xdc\xf1\xed\xfc\xd8\x18r\xf9\x80\x0f\xfa\x8cO\x97(\x0b]\xf1\xdd\r\xe7\xbf\xed\x06\xbd\x1b?\xc5\xa0\xd7a\x82\xf3\xa8\xe6@\xf3\ri\xa1\xb10\xf6\xd4W\xbc\x86\x1a\xbb\xfd\x94!bS\xdb\xaeM\x92\x00#\x0b\xf7\xad\xe9\xc2\x8e\x86\xbfi![%\xd31]\xc6\xfc2\xc9\xda\xc6v\x82P\xcc\xa9\xea\xb9\xff\xf6\xc8\x17iD\xcf\xf3\xeeI\x04\xe9\xa1\x19\xbb\x01\x92\xf5nn4K\xf8\xbb\xc6\x17e>\xa7 \xbbv' - data = OrderedDict( - sorted({"int": 1024 * 1024 * 1024, "float": 12345.67890, "text": "hello" * 1024, "binary": binary}.items()) - ) - data_packed_valid = b'\x84\xa6binary\xc5\x01\x00fqv\xf0\x1a"e\x10,\xbe\x9cT\x9e(\xa5]u\x072C\x8c\x15\xa2\xa8\x93Sw)\x19\x02\xdd\t\xfb\xf67\x88\xd9\xee\x86\xa1\xe4\xb6,\xc6\x14\xbb\xd7$z\x1d\xb2\xda\x85\xf5\xa0\x97^\x01*\xaf\xd3\xb0!\xb7\x9d\xea\x89\xbbh8\xa1"\xa7]e(@\xa2\xa5g\xb7[\xae\x8eE\xc2\x9fL\xb6s\x19\x19\r\xc8\x04S\xd0N\xe4]?/\x01\xea\xf6\xec\xd1\xb3\xc2\x91\x86\xd7\xf4K\xdf\xc2lV\xf4\xe8\x80\xfc\x8ep\xbb\x82\xb3\x86\x98F\x1c\xecS\xc8\x15\xcf\xdc\xf1\xed\xfc\xd8\x18r\xf9\x80\x0f\xfa\x8cO\x97(\x0b]\xf1\xdd\r\xe7\xbf\xed\x06\xbd\x1b?\xc5\xa0\xd7a\x82\xf3\xa8\xe6@\xf3\ri\xa1\xb10\xf6\xd4W\xbc\x86\x1a\xbb\xfd\x94!bS\xdb\xaeM\x92\x00#\x0b\xf7\xad\xe9\xc2\x8e\x86\xbfi![%\xd31]\xc6\xfc2\xc9\xda\xc6v\x82P\xcc\xa9\xea\xb9\xff\xf6\xc8\x17iD\xcf\xf3\xeeI\x04\xe9\xa1\x19\xbb\x01\x92\xf5nn4K\xf8\xbb\xc6\x17e>\xa7 \xbbv\xa5float\xcb@\xc8\x1c\xd6\xe61\xf8\xa1\xa3int\xce@\x00\x00\x00\xa4text\xda\x14\x00' - data_packed_valid += b'hello' * 1024 - for y in range(num_run): - for i in range(100): - data_packed = Msgpack.pack(data) - yield "." - assert data_packed == data_packed_valid, "%s
      !=
      %s" % (repr(data_packed), repr(data_packed_valid)) - - def testUnpackMsgpack(self, num_run=1): - """ - Test msgpack decoding - """ - yield "x 5KB " - binary = b'fqv\xf0\x1a"e\x10,\xbe\x9cT\x9e(\xa5]u\x072C\x8c\x15\xa2\xa8\x93Sw)\x19\x02\xdd\t\xfb\xf67\x88\xd9\xee\x86\xa1\xe4\xb6,\xc6\x14\xbb\xd7$z\x1d\xb2\xda\x85\xf5\xa0\x97^\x01*\xaf\xd3\xb0!\xb7\x9d\xea\x89\xbbh8\xa1"\xa7]e(@\xa2\xa5g\xb7[\xae\x8eE\xc2\x9fL\xb6s\x19\x19\r\xc8\x04S\xd0N\xe4]?/\x01\xea\xf6\xec\xd1\xb3\xc2\x91\x86\xd7\xf4K\xdf\xc2lV\xf4\xe8\x80\xfc\x8ep\xbb\x82\xb3\x86\x98F\x1c\xecS\xc8\x15\xcf\xdc\xf1\xed\xfc\xd8\x18r\xf9\x80\x0f\xfa\x8cO\x97(\x0b]\xf1\xdd\r\xe7\xbf\xed\x06\xbd\x1b?\xc5\xa0\xd7a\x82\xf3\xa8\xe6@\xf3\ri\xa1\xb10\xf6\xd4W\xbc\x86\x1a\xbb\xfd\x94!bS\xdb\xaeM\x92\x00#\x0b\xf7\xad\xe9\xc2\x8e\x86\xbfi![%\xd31]\xc6\xfc2\xc9\xda\xc6v\x82P\xcc\xa9\xea\xb9\xff\xf6\xc8\x17iD\xcf\xf3\xeeI\x04\xe9\xa1\x19\xbb\x01\x92\xf5nn4K\xf8\xbb\xc6\x17e>\xa7 \xbbv' - data = OrderedDict( - sorted({"int": 1024 * 1024 * 1024, "float": 12345.67890, "text": "hello" * 1024, "binary": binary}.items()) - ) - data_packed = b'\x84\xa6binary\xc5\x01\x00fqv\xf0\x1a"e\x10,\xbe\x9cT\x9e(\xa5]u\x072C\x8c\x15\xa2\xa8\x93Sw)\x19\x02\xdd\t\xfb\xf67\x88\xd9\xee\x86\xa1\xe4\xb6,\xc6\x14\xbb\xd7$z\x1d\xb2\xda\x85\xf5\xa0\x97^\x01*\xaf\xd3\xb0!\xb7\x9d\xea\x89\xbbh8\xa1"\xa7]e(@\xa2\xa5g\xb7[\xae\x8eE\xc2\x9fL\xb6s\x19\x19\r\xc8\x04S\xd0N\xe4]?/\x01\xea\xf6\xec\xd1\xb3\xc2\x91\x86\xd7\xf4K\xdf\xc2lV\xf4\xe8\x80\xfc\x8ep\xbb\x82\xb3\x86\x98F\x1c\xecS\xc8\x15\xcf\xdc\xf1\xed\xfc\xd8\x18r\xf9\x80\x0f\xfa\x8cO\x97(\x0b]\xf1\xdd\r\xe7\xbf\xed\x06\xbd\x1b?\xc5\xa0\xd7a\x82\xf3\xa8\xe6@\xf3\ri\xa1\xb10\xf6\xd4W\xbc\x86\x1a\xbb\xfd\x94!bS\xdb\xaeM\x92\x00#\x0b\xf7\xad\xe9\xc2\x8e\x86\xbfi![%\xd31]\xc6\xfc2\xc9\xda\xc6v\x82P\xcc\xa9\xea\xb9\xff\xf6\xc8\x17iD\xcf\xf3\xeeI\x04\xe9\xa1\x19\xbb\x01\x92\xf5nn4K\xf8\xbb\xc6\x17e>\xa7 \xbbv\xa5float\xcb@\xc8\x1c\xd6\xe61\xf8\xa1\xa3int\xce@\x00\x00\x00\xa4text\xda\x14\x00' - data_packed += b'hello' * 1024 - for y in range(num_run): - data_unpacked = Msgpack.unpack(data_packed, decode=False) - yield "." - assert data_unpacked == data, "%s
      !=
      %s" % (data_unpacked, data) - - def testUnpackMsgpackStreaming(self, num_run=1, fallback=False): - """ - Test streaming msgpack decoding - """ - yield "x 1000 x 5KB " - binary = b'fqv\xf0\x1a"e\x10,\xbe\x9cT\x9e(\xa5]u\x072C\x8c\x15\xa2\xa8\x93Sw)\x19\x02\xdd\t\xfb\xf67\x88\xd9\xee\x86\xa1\xe4\xb6,\xc6\x14\xbb\xd7$z\x1d\xb2\xda\x85\xf5\xa0\x97^\x01*\xaf\xd3\xb0!\xb7\x9d\xea\x89\xbbh8\xa1"\xa7]e(@\xa2\xa5g\xb7[\xae\x8eE\xc2\x9fL\xb6s\x19\x19\r\xc8\x04S\xd0N\xe4]?/\x01\xea\xf6\xec\xd1\xb3\xc2\x91\x86\xd7\xf4K\xdf\xc2lV\xf4\xe8\x80\xfc\x8ep\xbb\x82\xb3\x86\x98F\x1c\xecS\xc8\x15\xcf\xdc\xf1\xed\xfc\xd8\x18r\xf9\x80\x0f\xfa\x8cO\x97(\x0b]\xf1\xdd\r\xe7\xbf\xed\x06\xbd\x1b?\xc5\xa0\xd7a\x82\xf3\xa8\xe6@\xf3\ri\xa1\xb10\xf6\xd4W\xbc\x86\x1a\xbb\xfd\x94!bS\xdb\xaeM\x92\x00#\x0b\xf7\xad\xe9\xc2\x8e\x86\xbfi![%\xd31]\xc6\xfc2\xc9\xda\xc6v\x82P\xcc\xa9\xea\xb9\xff\xf6\xc8\x17iD\xcf\xf3\xeeI\x04\xe9\xa1\x19\xbb\x01\x92\xf5nn4K\xf8\xbb\xc6\x17e>\xa7 \xbbv' - data = OrderedDict( - sorted({"int": 1024 * 1024 * 1024, "float": 12345.67890, "text": "hello" * 1024, "binary": binary}.items()) - ) - data_packed = b'\x84\xa6binary\xc5\x01\x00fqv\xf0\x1a"e\x10,\xbe\x9cT\x9e(\xa5]u\x072C\x8c\x15\xa2\xa8\x93Sw)\x19\x02\xdd\t\xfb\xf67\x88\xd9\xee\x86\xa1\xe4\xb6,\xc6\x14\xbb\xd7$z\x1d\xb2\xda\x85\xf5\xa0\x97^\x01*\xaf\xd3\xb0!\xb7\x9d\xea\x89\xbbh8\xa1"\xa7]e(@\xa2\xa5g\xb7[\xae\x8eE\xc2\x9fL\xb6s\x19\x19\r\xc8\x04S\xd0N\xe4]?/\x01\xea\xf6\xec\xd1\xb3\xc2\x91\x86\xd7\xf4K\xdf\xc2lV\xf4\xe8\x80\xfc\x8ep\xbb\x82\xb3\x86\x98F\x1c\xecS\xc8\x15\xcf\xdc\xf1\xed\xfc\xd8\x18r\xf9\x80\x0f\xfa\x8cO\x97(\x0b]\xf1\xdd\r\xe7\xbf\xed\x06\xbd\x1b?\xc5\xa0\xd7a\x82\xf3\xa8\xe6@\xf3\ri\xa1\xb10\xf6\xd4W\xbc\x86\x1a\xbb\xfd\x94!bS\xdb\xaeM\x92\x00#\x0b\xf7\xad\xe9\xc2\x8e\x86\xbfi![%\xd31]\xc6\xfc2\xc9\xda\xc6v\x82P\xcc\xa9\xea\xb9\xff\xf6\xc8\x17iD\xcf\xf3\xeeI\x04\xe9\xa1\x19\xbb\x01\x92\xf5nn4K\xf8\xbb\xc6\x17e>\xa7 \xbbv\xa5float\xcb@\xc8\x1c\xd6\xe61\xf8\xa1\xa3int\xce@\x00\x00\x00\xa4text\xda\x14\x00' - data_packed += b'hello' * 1024 - for i in range(num_run): - unpacker = Msgpack.getUnpacker(decode=False, fallback=fallback) - for y in range(1000): - unpacker.feed(data_packed) - for data_unpacked in unpacker: - pass - yield "." - assert data == data_unpacked, "%s != %s" % (data_unpacked, data) diff --git a/plugins/Benchmark/BenchmarkPlugin.py b/plugins/Benchmark/BenchmarkPlugin.py deleted file mode 100644 index fd6cacf3..00000000 --- a/plugins/Benchmark/BenchmarkPlugin.py +++ /dev/null @@ -1,428 +0,0 @@ -import os -import time -import io -import math -import hashlib -import re -import sys - -from Config import config -from Crypt import CryptHash -from Plugin import PluginManager -from Debug import Debug -from util import helper - -plugin_dir = os.path.dirname(__file__) - -benchmark_key = None - - -@PluginManager.registerTo("UiRequest") -class UiRequestPlugin(object): - @helper.encodeResponse - def actionBenchmark(self): - global benchmark_key - script_nonce = self.getScriptNonce() - if not benchmark_key: - benchmark_key = CryptHash.random(encoding="base64") - self.sendHeader(script_nonce=script_nonce) - - if "Multiuser" in PluginManager.plugin_manager.plugin_names and not config.multiuser_local: - yield "This function is disabled on this proxy" - return - - data = self.render( - plugin_dir + "/media/benchmark.html", - script_nonce=script_nonce, - benchmark_key=benchmark_key, - filter=re.sub("[^A-Za-z0-9]", "", self.get.get("filter", "")) - ) - yield data - - @helper.encodeResponse - def actionBenchmarkResult(self): - global benchmark_key - if self.get.get("benchmark_key", "") != benchmark_key: - return self.error403("Invalid benchmark key") - - self.sendHeader(content_type="text/plain", noscript=True) - - if "Multiuser" in PluginManager.plugin_manager.plugin_names and not config.multiuser_local: - yield "This function is disabled on this proxy" - return - - yield " " * 1024 # Head (required for streaming) - - import main - s = time.time() - - for part in main.actions.testBenchmark(filter=self.get.get("filter", "")): - yield part - - yield "\n - Total time: %.3fs" % (time.time() - s) - - -@PluginManager.registerTo("Actions") -class ActionsPlugin: - def getMultiplerTitle(self, multipler): - if multipler < 0.3: - multipler_title = "Sloooow" - elif multipler < 0.6: - multipler_title = "Ehh" - elif multipler < 0.8: - multipler_title = "Goodish" - elif multipler < 1.2: - multipler_title = "OK" - elif multipler < 1.7: - multipler_title = "Fine" - elif multipler < 2.5: - multipler_title = "Fast" - elif multipler < 3.5: - multipler_title = "WOW" - else: - multipler_title = "Insane!!" - return multipler_title - - def formatResult(self, taken, standard): - if not standard: - return " Done in %.3fs" % taken - - if taken > 0: - multipler = standard / taken - else: - multipler = 99 - multipler_title = self.getMultiplerTitle(multipler) - - return " Done in %.3fs = %s (%.2fx)" % (taken, multipler_title, multipler) - - def getBenchmarkTests(self, online=False): - if hasattr(super(), "getBenchmarkTests"): - tests = super().getBenchmarkTests(online) - else: - tests = [] - - tests.extend([ - {"func": self.testHdPrivatekey, "num": 50, "time_standard": 0.57}, - {"func": self.testSign, "num": 20, "time_standard": 0.46}, - {"func": self.testVerify, "kwargs": {"lib_verify": "sslcrypto_fallback"}, "num": 20, "time_standard": 0.38}, - {"func": self.testVerify, "kwargs": {"lib_verify": "sslcrypto"}, "num": 200, "time_standard": 0.30}, - {"func": self.testVerify, "kwargs": {"lib_verify": "libsecp256k1"}, "num": 200, "time_standard": 0.10}, - - {"func": self.testPackMsgpack, "num": 100, "time_standard": 0.35}, - {"func": self.testUnpackMsgpackStreaming, "kwargs": {"fallback": False}, "num": 100, "time_standard": 0.35}, - {"func": self.testUnpackMsgpackStreaming, "kwargs": {"fallback": True}, "num": 10, "time_standard": 0.5}, - - {"func": self.testPackZip, "num": 5, "time_standard": 0.065}, - {"func": self.testPackArchive, "kwargs": {"archive_type": "gz"}, "num": 5, "time_standard": 0.08}, - {"func": self.testPackArchive, "kwargs": {"archive_type": "bz2"}, "num": 5, "time_standard": 0.68}, - {"func": self.testPackArchive, "kwargs": {"archive_type": "xz"}, "num": 5, "time_standard": 0.47}, - {"func": self.testUnpackZip, "num": 20, "time_standard": 0.25}, - {"func": self.testUnpackArchive, "kwargs": {"archive_type": "gz"}, "num": 20, "time_standard": 0.28}, - {"func": self.testUnpackArchive, "kwargs": {"archive_type": "bz2"}, "num": 20, "time_standard": 0.83}, - {"func": self.testUnpackArchive, "kwargs": {"archive_type": "xz"}, "num": 20, "time_standard": 0.38}, - - {"func": self.testCryptHash, "kwargs": {"hash_type": "sha256"}, "num": 10, "time_standard": 0.50}, - {"func": self.testCryptHash, "kwargs": {"hash_type": "sha512"}, "num": 10, "time_standard": 0.33}, - {"func": self.testCryptHashlib, "kwargs": {"hash_type": "sha3_256"}, "num": 10, "time_standard": 0.33}, - {"func": self.testCryptHashlib, "kwargs": {"hash_type": "sha3_512"}, "num": 10, "time_standard": 0.65}, - - {"func": self.testRandom, "num": 100, "time_standard": 0.08}, - ]) - - if online: - tests += [ - {"func": self.testHttps, "num": 1, "time_standard": 2.1} - ] - return tests - - def testBenchmark(self, num_multipler=1, online=False, num_run=None, filter=None): - """ - Run benchmark on client functions - """ - tests = self.getBenchmarkTests(online=online) - - if filter: - tests = [test for test in tests[:] if filter.lower() in test["func"].__name__.lower()] - - yield "\n" - res = {} - res_time_taken = {} - multiplers = [] - for test in tests: - s = time.time() - if num_run: - num_run_test = num_run - else: - num_run_test = math.ceil(test["num"] * num_multipler) - func = test["func"] - func_name = func.__name__ - kwargs = test.get("kwargs", {}) - key = "%s %s" % (func_name, kwargs) - if kwargs: - yield "* Running %s (%s) x %s " % (func_name, kwargs, num_run_test) - else: - yield "* Running %s x %s " % (func_name, num_run_test) - i = 0 - try: - for progress in func(num_run_test, **kwargs): - i += 1 - if num_run_test > 10: - should_print = i % (num_run_test / 10) == 0 or progress != "." - else: - should_print = True - - if should_print: - if num_run_test == 1 and progress == ".": - progress = "..." - yield progress - time_taken = time.time() - s - if num_run: - time_standard = 0 - else: - time_standard = test["time_standard"] * num_multipler - yield self.formatResult(time_taken, time_standard) - yield "\n" - res[key] = "ok" - res_time_taken[key] = time_taken - multiplers.append(time_standard / max(time_taken, 0.001)) - except Exception as err: - res[key] = err - yield "Failed!\n! Error: %s\n\n" % Debug.formatException(err) - - yield "\n== Result ==\n" - - # Check verification speed - if "testVerify {'lib_verify': 'sslcrypto'}" in res_time_taken: - speed_order = ["sslcrypto_fallback", "sslcrypto", "libsecp256k1"] - time_taken = {} - for lib_verify in speed_order: - time_taken[lib_verify] = res_time_taken["testVerify {'lib_verify': '%s'}" % lib_verify] - - time_taken["sslcrypto_fallback"] *= 10 # fallback benchmark only run 20 times instead of 200 - speedup_sslcrypto = time_taken["sslcrypto_fallback"] / time_taken["sslcrypto"] - speedup_libsecp256k1 = time_taken["sslcrypto_fallback"] / time_taken["libsecp256k1"] - - yield "\n* Verification speedup:\n" - yield " - OpenSSL: %.1fx (reference: 7.0x)\n" % speedup_sslcrypto - yield " - libsecp256k1: %.1fx (reference: 23.8x)\n" % speedup_libsecp256k1 - - if speedup_sslcrypto < 2: - res["Verification speed"] = "error: OpenSSL speedup low: %.1fx" % speedup_sslcrypto - - if speedup_libsecp256k1 < speedup_sslcrypto: - res["Verification speed"] = "error: libsecp256k1 speedup low: %.1fx" % speedup_libsecp256k1 - - if not res: - yield "! No tests found" - if config.action == "test": - sys.exit(1) - else: - num_failed = len([res_key for res_key, res_val in res.items() if res_val != "ok"]) - num_success = len([res_key for res_key, res_val in res.items() if res_val == "ok"]) - yield "\n* Tests:\n" - yield " - Total: %s tests\n" % len(res) - yield " - Success: %s tests\n" % num_success - yield " - Failed: %s tests\n" % num_failed - if any(multiplers): - multipler_avg = sum(multiplers) / len(multiplers) - multipler_title = self.getMultiplerTitle(multipler_avg) - yield " - Average speed factor: %.2fx (%s)\n" % (multipler_avg, multipler_title) - - # Display errors - for res_key, res_val in res.items(): - if res_val != "ok": - yield " ! %s %s\n" % (res_key, res_val) - - if num_failed != 0 and config.action == "test": - sys.exit(1) - - def testHttps(self, num_run=1): - """ - Test https connection with valid and invalid certs - """ - import urllib.request - import urllib.error - - body = urllib.request.urlopen("https://google.com").read() - assert len(body) > 100 - yield "." - - badssl_urls = [ - "https://expired.badssl.com/", - "https://wrong.host.badssl.com/", - "https://self-signed.badssl.com/", - "https://untrusted-root.badssl.com/" - ] - for badssl_url in badssl_urls: - try: - body = urllib.request.urlopen(badssl_url).read() - https_err = None - except urllib.error.URLError as err: - https_err = err - assert https_err - yield "." - - def testCryptHash(self, num_run=1, hash_type="sha256"): - """ - Test hashing functions - """ - yield "(5MB) " - - from Crypt import CryptHash - - hash_types = { - "sha256": {"func": CryptHash.sha256sum, "hash_valid": "8cd629d9d6aff6590da8b80782a5046d2673d5917b99d5603c3dcb4005c45ffa"}, - "sha512": {"func": CryptHash.sha512sum, "hash_valid": "9ca7e855d430964d5b55b114e95c6bbb114a6d478f6485df93044d87b108904d"} - } - hash_func = hash_types[hash_type]["func"] - hash_valid = hash_types[hash_type]["hash_valid"] - - data = io.BytesIO(b"Hello" * 1024 * 1024) # 5MB - for i in range(num_run): - data.seek(0) - hash = hash_func(data) - yield "." - assert hash == hash_valid, "%s != %s" % (hash, hash_valid) - - def testCryptHashlib(self, num_run=1, hash_type="sha3_256"): - """ - Test SHA3 hashing functions - """ - yield "x 5MB " - - hash_types = { - "sha3_256": {"func": hashlib.sha3_256, "hash_valid": "c8aeb3ef9fe5d6404871c0d2a4410a4d4e23268e06735648c9596f436c495f7e"}, - "sha3_512": {"func": hashlib.sha3_512, "hash_valid": "b75dba9472d8af3cc945ce49073f3f8214d7ac12086c0453fb08944823dee1ae83b3ffbc87a53a57cc454521d6a26fe73ff0f3be38dddf3f7de5d7692ebc7f95"}, - } - - hash_func = hash_types[hash_type]["func"] - hash_valid = hash_types[hash_type]["hash_valid"] - - data = io.BytesIO(b"Hello" * 1024 * 1024) # 5MB - for i in range(num_run): - data.seek(0) - h = hash_func() - while 1: - buff = data.read(1024 * 64) - if not buff: - break - h.update(buff) - hash = h.hexdigest() - yield "." - assert hash == hash_valid, "%s != %s" % (hash, hash_valid) - - def testRandom(self, num_run=1): - """ - Test generating random data - """ - yield "x 1000 x 256 bytes " - for i in range(num_run): - data_last = None - for y in range(1000): - data = os.urandom(256) - assert data != data_last - assert len(data) == 256 - data_last = data - yield "." - - def testHdPrivatekey(self, num_run=2): - """ - Test generating deterministic private keys from a master seed - """ - from Crypt import CryptBitcoin - seed = "e180efa477c63b0f2757eac7b1cce781877177fe0966be62754ffd4c8592ce38" - privatekeys = [] - for i in range(num_run): - privatekeys.append(CryptBitcoin.hdPrivatekey(seed, i * 10)) - yield "." - valid = "5JSbeF5PevdrsYjunqpg7kAGbnCVYa1T4APSL3QRu8EoAmXRc7Y" - assert privatekeys[0] == valid, "%s != %s" % (privatekeys[0], valid) - if len(privatekeys) > 1: - assert privatekeys[0] != privatekeys[-1] - - def testSign(self, num_run=1): - """ - Test signing data using a private key - """ - from Crypt import CryptBitcoin - data = "Hello" * 1024 - privatekey = "5JsunC55XGVqFQj5kPGK4MWgTL26jKbnPhjnmchSNPo75XXCwtk" - for i in range(num_run): - yield "." - sign = CryptBitcoin.sign(data, privatekey) - valid = "G1GXaDauZ8vX/N9Jn+MRiGm9h+I94zUhDnNYFaqMGuOiBHB+kp4cRPZOL7l1yqK5BHa6J+W97bMjvTXtxzljp6w=" - assert sign == valid, "%s != %s" % (sign, valid) - - def testVerify(self, num_run=1, lib_verify="sslcrypto"): - """ - Test verification of generated signatures - """ - from Crypt import CryptBitcoin - CryptBitcoin.loadLib(lib_verify, silent=True) - - - data = "Hello" * 1024 - privatekey = "5JsunC55XGVqFQj5kPGK4MWgTL26jKbnPhjnmchSNPo75XXCwtk" - address = CryptBitcoin.privatekeyToAddress(privatekey) - sign = "G1GXaDauZ8vX/N9Jn+MRiGm9h+I94zUhDnNYFaqMGuOiBHB+kp4cRPZOL7l1yqK5BHa6J+W97bMjvTXtxzljp6w=" - - for i in range(num_run): - ok = CryptBitcoin.verify(data, address, sign, lib_verify=lib_verify) - yield "." - assert ok, "does not verify from %s" % address - - if lib_verify == "sslcrypto": - yield("(%s)" % CryptBitcoin.sslcrypto.ecc.get_backend()) - - def testPortCheckers(self): - """ - Test all active open port checker - """ - from Peer import PeerPortchecker - for ip_type, func_names in PeerPortchecker.PeerPortchecker.checker_functions.items(): - yield "\n- %s:" % ip_type - for func_name in func_names: - yield "\n - Tracker %s: " % func_name - try: - for res in self.testPortChecker(func_name): - yield res - except Exception as err: - yield Debug.formatException(err) - - def testPortChecker(self, func_name): - """ - Test single open port checker - """ - from Peer import PeerPortchecker - peer_portchecker = PeerPortchecker.PeerPortchecker(None) - announce_func = getattr(peer_portchecker, func_name) - res = announce_func(3894) - yield res - - def testAll(self): - """ - Run all tests to check system compatibility with ZeroNet functions - """ - for progress in self.testBenchmark(online=not config.offline, num_run=1): - yield progress - - -@PluginManager.registerTo("ConfigPlugin") -class ConfigPlugin(object): - def createArguments(self): - back = super(ConfigPlugin, self).createArguments() - if self.getCmdlineValue("test") == "benchmark": - self.test_parser.add_argument( - '--num_multipler', help='Benchmark run time multipler', - default=1.0, type=float, metavar='num' - ) - self.test_parser.add_argument( - '--filter', help='Filter running benchmark', - default=None, metavar='test name' - ) - elif self.getCmdlineValue("test") == "portChecker": - self.test_parser.add_argument( - '--func_name', help='Name of open port checker function', - default=None, metavar='func_name' - ) - return back diff --git a/plugins/Benchmark/__init__.py b/plugins/Benchmark/__init__.py deleted file mode 100644 index 76a5ae9c..00000000 --- a/plugins/Benchmark/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from . import BenchmarkPlugin -from . import BenchmarkDb -from . import BenchmarkPack diff --git a/plugins/Benchmark/media/benchmark.html b/plugins/Benchmark/media/benchmark.html deleted file mode 100644 index 73571367..00000000 --- a/plugins/Benchmark/media/benchmark.html +++ /dev/null @@ -1,123 +0,0 @@ - - - - - -

      Benchmark

      -
      -
      - Start benchmark - (It will take around 20 sec) -
      -
      - - - - \ No newline at end of file diff --git a/plugins/Benchmark/plugin_info.json b/plugins/Benchmark/plugin_info.json deleted file mode 100644 index f3f57417..00000000 --- a/plugins/Benchmark/plugin_info.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Benchmark", - "description": "Test and benchmark database and cryptographic functions related to ZeroNet.", - "default": "enabled" -} \ No newline at end of file diff --git a/plugins/Bigfile/BigfilePiecefield.py b/plugins/Bigfile/BigfilePiecefield.py deleted file mode 100644 index 9a6f370b..00000000 --- a/plugins/Bigfile/BigfilePiecefield.py +++ /dev/null @@ -1,170 +0,0 @@ -import array - - -def packPiecefield(data): - if not isinstance(data, bytes) and not isinstance(data, bytearray): - raise Exception("Invalid data type: %s" % type(data)) - - res = [] - if not data: - return array.array("H", b"") - - if data[0] == b"\x00": - res.append(0) - find = b"\x01" - else: - find = b"\x00" - last_pos = 0 - pos = 0 - while 1: - pos = data.find(find, pos) - if find == b"\x00": - find = b"\x01" - else: - find = b"\x00" - if pos == -1: - res.append(len(data) - last_pos) - break - res.append(pos - last_pos) - last_pos = pos - return array.array("H", res) - - -def unpackPiecefield(data): - if not data: - return b"" - - res = [] - char = b"\x01" - for times in data: - if times > 10000: - return b"" - res.append(char * times) - if char == b"\x01": - char = b"\x00" - else: - char = b"\x01" - return b"".join(res) - - -def spliceBit(data, idx, bit): - if bit != b"\x00" and bit != b"\x01": - raise Exception("Invalid bit: %s" % bit) - - if len(data) < idx: - data = data.ljust(idx + 1, b"\x00") - return data[:idx] + bit + data[idx+ 1:] - -class Piecefield(object): - def tostring(self): - return "".join(["1" if b else "0" for b in self.tobytes()]) - - -class BigfilePiecefield(Piecefield): - __slots__ = ["data"] - - def __init__(self): - self.data = b"" - - def frombytes(self, s): - if not isinstance(s, bytes) and not isinstance(s, bytearray): - raise Exception("Invalid type: %s" % type(s)) - self.data = s - - def tobytes(self): - return self.data - - def pack(self): - return packPiecefield(self.data).tobytes() - - def unpack(self, s): - self.data = unpackPiecefield(array.array("H", s)) - - def __getitem__(self, key): - try: - return self.data[key] - except IndexError: - return False - - def __setitem__(self, key, value): - self.data = spliceBit(self.data, key, value) - -class BigfilePiecefieldPacked(Piecefield): - __slots__ = ["data"] - - def __init__(self): - self.data = b"" - - def frombytes(self, data): - if not isinstance(data, bytes) and not isinstance(data, bytearray): - raise Exception("Invalid type: %s" % type(data)) - self.data = packPiecefield(data).tobytes() - - def tobytes(self): - return unpackPiecefield(array.array("H", self.data)) - - def pack(self): - return array.array("H", self.data).tobytes() - - def unpack(self, data): - self.data = data - - def __getitem__(self, key): - try: - return self.tobytes()[key] - except IndexError: - return False - - def __setitem__(self, key, value): - data = spliceBit(self.tobytes(), key, value) - self.frombytes(data) - - -if __name__ == "__main__": - import os - import psutil - import time - testdata = b"\x01" * 100 + b"\x00" * 900 + b"\x01" * 4000 + b"\x00" * 4999 + b"\x01" - meminfo = psutil.Process(os.getpid()).memory_info - - for storage in [BigfilePiecefieldPacked, BigfilePiecefield]: - print("-- Testing storage: %s --" % storage) - m = meminfo()[0] - s = time.time() - piecefields = {} - for i in range(10000): - piecefield = storage() - piecefield.frombytes(testdata[:i] + b"\x00" + testdata[i + 1:]) - piecefields[i] = piecefield - - print("Create x10000: +%sKB in %.3fs (len: %s)" % ((meminfo()[0] - m) / 1024, time.time() - s, len(piecefields[0].data))) - - m = meminfo()[0] - s = time.time() - for piecefield in list(piecefields.values()): - val = piecefield[1000] - - print("Query one x10000: +%sKB in %.3fs" % ((meminfo()[0] - m) / 1024, time.time() - s)) - - m = meminfo()[0] - s = time.time() - for piecefield in list(piecefields.values()): - piecefield[1000] = b"\x01" - - print("Change one x10000: +%sKB in %.3fs" % ((meminfo()[0] - m) / 1024, time.time() - s)) - - m = meminfo()[0] - s = time.time() - for piecefield in list(piecefields.values()): - packed = piecefield.pack() - - print("Pack x10000: +%sKB in %.3fs (len: %s)" % ((meminfo()[0] - m) / 1024, time.time() - s, len(packed))) - - m = meminfo()[0] - s = time.time() - for piecefield in list(piecefields.values()): - piecefield.unpack(packed) - - print("Unpack x10000: +%sKB in %.3fs (len: %s)" % ((meminfo()[0] - m) / 1024, time.time() - s, len(piecefields[0].data))) - - piecefields = {} diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py deleted file mode 100644 index 78a27b05..00000000 --- a/plugins/Bigfile/BigfilePlugin.py +++ /dev/null @@ -1,843 +0,0 @@ -import time -import os -import subprocess -import shutil -import collections -import math -import warnings -import base64 -import binascii -import json - -import gevent -import gevent.lock - -from Plugin import PluginManager -from Debug import Debug -from Crypt import CryptHash -with warnings.catch_warnings(): - warnings.filterwarnings("ignore") # Ignore missing sha3 warning - import merkletools - -from util import helper -from util import Msgpack -from util.Flag import flag -import util -from .BigfilePiecefield import BigfilePiecefield, BigfilePiecefieldPacked - - -# We can only import plugin host clases after the plugins are loaded -@PluginManager.afterLoad -def importPluginnedClasses(): - global VerifyError, config - from Content.ContentManager import VerifyError - from Config import config - - -if "upload_nonces" not in locals(): - upload_nonces = {} - - -@PluginManager.registerTo("UiRequest") -class UiRequestPlugin(object): - def isCorsAllowed(self, path): - if path == "/ZeroNet-Internal/BigfileUpload": - return True - else: - return super(UiRequestPlugin, self).isCorsAllowed(path) - - @helper.encodeResponse - def actionBigfileUpload(self): - nonce = self.get.get("upload_nonce") - if nonce not in upload_nonces: - return self.error403("Upload nonce error.") - - upload_info = upload_nonces[nonce] - del upload_nonces[nonce] - - self.sendHeader(200, "text/html", noscript=True, extra_headers={ - "Access-Control-Allow-Origin": "null", - "Access-Control-Allow-Credentials": "true" - }) - - self.readMultipartHeaders(self.env['wsgi.input']) # Skip http headers - result = self.handleBigfileUpload(upload_info, self.env['wsgi.input'].read) - return json.dumps(result) - - def actionBigfileUploadWebsocket(self): - ws = self.env.get("wsgi.websocket") - - if not ws: - self.start_response("400 Bad Request", []) - return [b"Not a websocket request!"] - - nonce = self.get.get("upload_nonce") - if nonce not in upload_nonces: - return self.error403("Upload nonce error.") - - upload_info = upload_nonces[nonce] - del upload_nonces[nonce] - - ws.send("poll") - - buffer = b"" - def read(size): - nonlocal buffer - while len(buffer) < size: - buffer += ws.receive() - ws.send("poll") - part, buffer = buffer[:size], buffer[size:] - return part - - result = self.handleBigfileUpload(upload_info, read) - ws.send(json.dumps(result)) - - def handleBigfileUpload(self, upload_info, read): - site = upload_info["site"] - inner_path = upload_info["inner_path"] - - with site.storage.open(inner_path, "wb", create_dirs=True) as out_file: - merkle_root, piece_size, piecemap_info = site.content_manager.hashBigfile( - read, upload_info["size"], upload_info["piece_size"], out_file - ) - - if len(piecemap_info["sha512_pieces"]) == 1: # Small file, don't split - hash = binascii.hexlify(piecemap_info["sha512_pieces"][0]) - hash_id = site.content_manager.hashfield.getHashId(hash) - site.content_manager.optionalDownloaded(inner_path, hash_id, upload_info["size"], own=True) - - else: # Big file - file_name = helper.getFilename(inner_path) - site.storage.open(upload_info["piecemap"], "wb").write(Msgpack.pack({file_name: piecemap_info})) - - # Find piecemap and file relative path to content.json - file_info = site.content_manager.getFileInfo(inner_path, new_file=True) - content_inner_path_dir = helper.getDirname(file_info["content_inner_path"]) - piecemap_relative_path = upload_info["piecemap"][len(content_inner_path_dir):] - file_relative_path = inner_path[len(content_inner_path_dir):] - - # Add file to content.json - if site.storage.isFile(file_info["content_inner_path"]): - content = site.storage.loadJson(file_info["content_inner_path"]) - else: - content = {} - if "files_optional" not in content: - content["files_optional"] = {} - - content["files_optional"][file_relative_path] = { - "sha512": merkle_root, - "size": upload_info["size"], - "piecemap": piecemap_relative_path, - "piece_size": piece_size - } - - merkle_root_hash_id = site.content_manager.hashfield.getHashId(merkle_root) - site.content_manager.optionalDownloaded(inner_path, merkle_root_hash_id, upload_info["size"], own=True) - site.storage.writeJson(file_info["content_inner_path"], content) - - site.content_manager.contents.loadItem(file_info["content_inner_path"]) # reload cache - - return { - "merkle_root": merkle_root, - "piece_num": len(piecemap_info["sha512_pieces"]), - "piece_size": piece_size, - "inner_path": inner_path - } - - def readMultipartHeaders(self, wsgi_input): - found = False - for i in range(100): - line = wsgi_input.readline() - if line == b"\r\n": - found = True - break - if not found: - raise Exception("No multipart header found") - return i - - def actionFile(self, file_path, *args, **kwargs): - if kwargs.get("file_size", 0) > 1024 * 1024 and kwargs.get("path_parts"): # Only check files larger than 1MB - path_parts = kwargs["path_parts"] - site = self.server.site_manager.get(path_parts["address"]) - big_file = site.storage.openBigfile(path_parts["inner_path"], prebuffer=2 * 1024 * 1024) - if big_file: - kwargs["file_obj"] = big_file - kwargs["file_size"] = big_file.size - - return super(UiRequestPlugin, self).actionFile(file_path, *args, **kwargs) - - -@PluginManager.registerTo("UiWebsocket") -class UiWebsocketPlugin(object): - def actionBigfileUploadInit(self, to, inner_path, size, protocol="xhr"): - valid_signers = self.site.content_manager.getValidSigners(inner_path) - auth_address = self.user.getAuthAddress(self.site.address) - if not self.site.settings["own"] and auth_address not in valid_signers: - self.log.error("FileWrite forbidden %s not in valid_signers %s" % (auth_address, valid_signers)) - return self.response(to, {"error": "Forbidden, you can only modify your own files"}) - - nonce = CryptHash.random() - piece_size = 1024 * 1024 - inner_path = self.site.content_manager.sanitizePath(inner_path) - file_info = self.site.content_manager.getFileInfo(inner_path, new_file=True) - - content_inner_path_dir = helper.getDirname(file_info["content_inner_path"]) - file_relative_path = inner_path[len(content_inner_path_dir):] - - upload_nonces[nonce] = { - "added": time.time(), - "site": self.site, - "inner_path": inner_path, - "websocket_client": self, - "size": size, - "piece_size": piece_size, - "piecemap": inner_path + ".piecemap.msgpack" - } - - if protocol == "xhr": - return { - "url": "/ZeroNet-Internal/BigfileUpload?upload_nonce=" + nonce, - "piece_size": piece_size, - "inner_path": inner_path, - "file_relative_path": file_relative_path - } - elif protocol == "websocket": - server_url = self.request.getWsServerUrl() - if server_url: - proto, host = server_url.split("://") - origin = proto.replace("http", "ws") + "://" + host - else: - origin = "{origin}" - return { - "url": origin + "/ZeroNet-Internal/BigfileUploadWebsocket?upload_nonce=" + nonce, - "piece_size": piece_size, - "inner_path": inner_path, - "file_relative_path": file_relative_path - } - else: - return {"error": "Unknown protocol"} - - @flag.no_multiuser - def actionSiteSetAutodownloadBigfileLimit(self, to, limit): - permissions = self.getPermissions(to) - if "ADMIN" not in permissions: - return self.response(to, "You don't have permission to run this command") - - self.site.settings["autodownload_bigfile_size_limit"] = int(limit) - self.response(to, "ok") - - def actionFileDelete(self, to, inner_path): - piecemap_inner_path = inner_path + ".piecemap.msgpack" - if self.hasFilePermission(inner_path) and self.site.storage.isFile(piecemap_inner_path): - # Also delete .piecemap.msgpack file if exists - self.log.debug("Deleting piecemap: %s" % piecemap_inner_path) - file_info = self.site.content_manager.getFileInfo(piecemap_inner_path) - if file_info: - content_json = self.site.storage.loadJson(file_info["content_inner_path"]) - relative_path = file_info["relative_path"] - if relative_path in content_json.get("files_optional", {}): - del content_json["files_optional"][relative_path] - self.site.storage.writeJson(file_info["content_inner_path"], content_json) - self.site.content_manager.loadContent(file_info["content_inner_path"], add_bad_files=False, force=True) - try: - self.site.storage.delete(piecemap_inner_path) - except Exception as err: - self.log.error("File %s delete error: %s" % (piecemap_inner_path, err)) - - return super(UiWebsocketPlugin, self).actionFileDelete(to, inner_path) - - -@PluginManager.registerTo("ContentManager") -class ContentManagerPlugin(object): - def getFileInfo(self, inner_path, *args, **kwargs): - if "|" not in inner_path: - return super(ContentManagerPlugin, self).getFileInfo(inner_path, *args, **kwargs) - - inner_path, file_range = inner_path.split("|") - pos_from, pos_to = map(int, file_range.split("-")) - file_info = super(ContentManagerPlugin, self).getFileInfo(inner_path, *args, **kwargs) - return file_info - - def readFile(self, read_func, size, buff_size=1024 * 64): - part_num = 0 - recv_left = size - - while 1: - part_num += 1 - read_size = min(buff_size, recv_left) - part = read_func(read_size) - - if not part: - break - yield part - - if part_num % 100 == 0: # Avoid blocking ZeroNet execution during upload - time.sleep(0.001) - - recv_left -= read_size - if recv_left <= 0: - break - - def hashBigfile(self, read_func, size, piece_size=1024 * 1024, file_out=None): - self.site.settings["has_bigfile"] = True - - recv = 0 - try: - piece_hash = CryptHash.sha512t() - piece_hashes = [] - piece_recv = 0 - - mt = merkletools.MerkleTools() - mt.hash_function = CryptHash.sha512t - - part = "" - for part in self.readFile(read_func, size): - if file_out: - file_out.write(part) - - recv += len(part) - piece_recv += len(part) - piece_hash.update(part) - if piece_recv >= piece_size: - piece_digest = piece_hash.digest() - piece_hashes.append(piece_digest) - mt.leaves.append(piece_digest) - piece_hash = CryptHash.sha512t() - piece_recv = 0 - - if len(piece_hashes) % 100 == 0 or recv == size: - self.log.info("- [HASHING:%.0f%%] Pieces: %s, %.1fMB/%.1fMB" % ( - float(recv) / size * 100, len(piece_hashes), recv / 1024 / 1024, size / 1024 / 1024 - )) - part = "" - if len(part) > 0: - piece_digest = piece_hash.digest() - piece_hashes.append(piece_digest) - mt.leaves.append(piece_digest) - except Exception as err: - raise err - finally: - if file_out: - file_out.close() - - mt.make_tree() - merkle_root = mt.get_merkle_root() - if type(merkle_root) is bytes: # Python <3.5 - merkle_root = merkle_root.decode() - return merkle_root, piece_size, { - "sha512_pieces": piece_hashes - } - - def hashFile(self, dir_inner_path, file_relative_path, optional=False): - inner_path = dir_inner_path + file_relative_path - - file_size = self.site.storage.getSize(inner_path) - # Only care about optional files >1MB - if not optional or file_size < 1 * 1024 * 1024: - return super(ContentManagerPlugin, self).hashFile(dir_inner_path, file_relative_path, optional) - - back = {} - content = self.contents.get(dir_inner_path + "content.json") - - hash = None - piecemap_relative_path = None - piece_size = None - - # Don't re-hash if it's already in content.json - if content and file_relative_path in content.get("files_optional", {}): - file_node = content["files_optional"][file_relative_path] - if file_node["size"] == file_size: - self.log.info("- [SAME SIZE] %s" % file_relative_path) - hash = file_node.get("sha512") - piecemap_relative_path = file_node.get("piecemap") - piece_size = file_node.get("piece_size") - - if not hash or not piecemap_relative_path: # Not in content.json yet - if file_size < 5 * 1024 * 1024: # Don't create piecemap automatically for files smaller than 5MB - return super(ContentManagerPlugin, self).hashFile(dir_inner_path, file_relative_path, optional) - - self.log.info("- [HASHING] %s" % file_relative_path) - merkle_root, piece_size, piecemap_info = self.hashBigfile(self.site.storage.open(inner_path, "rb").read, file_size) - if not hash: - hash = merkle_root - - if not piecemap_relative_path: - file_name = helper.getFilename(file_relative_path) - piecemap_relative_path = file_relative_path + ".piecemap.msgpack" - piecemap_inner_path = inner_path + ".piecemap.msgpack" - - self.site.storage.open(piecemap_inner_path, "wb").write(Msgpack.pack({file_name: piecemap_info})) - - back.update(super(ContentManagerPlugin, self).hashFile(dir_inner_path, piecemap_relative_path, optional=True)) - - piece_num = int(math.ceil(float(file_size) / piece_size)) - - # Add the merkle root to hashfield - hash_id = self.site.content_manager.hashfield.getHashId(hash) - self.optionalDownloaded(inner_path, hash_id, file_size, own=True) - self.site.storage.piecefields[hash].frombytes(b"\x01" * piece_num) - - back[file_relative_path] = {"sha512": hash, "size": file_size, "piecemap": piecemap_relative_path, "piece_size": piece_size} - return back - - def getPiecemap(self, inner_path): - file_info = self.site.content_manager.getFileInfo(inner_path) - piecemap_inner_path = helper.getDirname(file_info["content_inner_path"]) + file_info["piecemap"] - self.site.needFile(piecemap_inner_path, priority=20) - piecemap = Msgpack.unpack(self.site.storage.open(piecemap_inner_path, "rb").read())[helper.getFilename(inner_path)] - piecemap["piece_size"] = file_info["piece_size"] - return piecemap - - def verifyPiece(self, inner_path, pos, piece): - try: - piecemap = self.getPiecemap(inner_path) - except Exception as err: - raise VerifyError("Unable to download piecemap: %s" % Debug.formatException(err)) - - piece_i = int(pos / piecemap["piece_size"]) - if CryptHash.sha512sum(piece, format="digest") != piecemap["sha512_pieces"][piece_i]: - raise VerifyError("Invalid hash") - return True - - def verifyFile(self, inner_path, file, ignore_same=True): - if "|" not in inner_path: - return super(ContentManagerPlugin, self).verifyFile(inner_path, file, ignore_same) - - inner_path, file_range = inner_path.split("|") - pos_from, pos_to = map(int, file_range.split("-")) - - return self.verifyPiece(inner_path, pos_from, file) - - def optionalDownloaded(self, inner_path, hash_id, size=None, own=False): - if "|" in inner_path: - inner_path, file_range = inner_path.split("|") - pos_from, pos_to = map(int, file_range.split("-")) - file_info = self.getFileInfo(inner_path) - - # Mark piece downloaded - piece_i = int(pos_from / file_info["piece_size"]) - self.site.storage.piecefields[file_info["sha512"]][piece_i] = b"\x01" - - # Only add to site size on first request - if hash_id in self.hashfield: - size = 0 - elif size > 1024 * 1024: - file_info = self.getFileInfo(inner_path) - if file_info and "sha512" in file_info: # We already have the file, but not in piecefield - sha512 = file_info["sha512"] - if sha512 not in self.site.storage.piecefields: - self.site.storage.checkBigfile(inner_path) - - return super(ContentManagerPlugin, self).optionalDownloaded(inner_path, hash_id, size, own) - - def optionalRemoved(self, inner_path, hash_id, size=None): - if size and size > 1024 * 1024: - file_info = self.getFileInfo(inner_path) - sha512 = file_info["sha512"] - if sha512 in self.site.storage.piecefields: - del self.site.storage.piecefields[sha512] - - # Also remove other pieces of the file from download queue - for key in list(self.site.bad_files.keys()): - if key.startswith(inner_path + "|"): - del self.site.bad_files[key] - self.site.worker_manager.removeSolvedFileTasks() - return super(ContentManagerPlugin, self).optionalRemoved(inner_path, hash_id, size) - - -@PluginManager.registerTo("SiteStorage") -class SiteStoragePlugin(object): - def __init__(self, *args, **kwargs): - super(SiteStoragePlugin, self).__init__(*args, **kwargs) - self.piecefields = collections.defaultdict(BigfilePiecefield) - if "piecefields" in self.site.settings.get("cache", {}): - for sha512, piecefield_packed in self.site.settings["cache"].get("piecefields").items(): - if piecefield_packed: - self.piecefields[sha512].unpack(base64.b64decode(piecefield_packed)) - self.site.settings["cache"]["piecefields"] = {} - - def createSparseFile(self, inner_path, size, sha512=None): - file_path = self.getPath(inner_path) - - self.ensureDir(os.path.dirname(inner_path)) - - f = open(file_path, 'wb') - f.truncate(min(1024 * 1024 * 5, size)) # Only pre-allocate up to 5MB - f.close() - if os.name == "nt": - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - subprocess.call(["fsutil", "sparse", "setflag", file_path], close_fds=True, startupinfo=startupinfo) - - if sha512 and sha512 in self.piecefields: - self.log.debug("%s: File not exists, but has piecefield. Deleting piecefield." % inner_path) - del self.piecefields[sha512] - - def write(self, inner_path, content): - if "|" not in inner_path: - return super(SiteStoragePlugin, self).write(inner_path, content) - - # Write to specific position by passing |{pos} after the filename - inner_path, file_range = inner_path.split("|") - pos_from, pos_to = map(int, file_range.split("-")) - file_path = self.getPath(inner_path) - - # Create dir if not exist - self.ensureDir(os.path.dirname(inner_path)) - - if not os.path.isfile(file_path): - file_info = self.site.content_manager.getFileInfo(inner_path) - self.createSparseFile(inner_path, file_info["size"]) - - # Write file - with open(file_path, "rb+") as file: - file.seek(pos_from) - if hasattr(content, 'read'): # File-like object - shutil.copyfileobj(content, file) # Write buff to disk - else: # Simple string - file.write(content) - del content - self.onUpdated(inner_path) - - def checkBigfile(self, inner_path): - file_info = self.site.content_manager.getFileInfo(inner_path) - if not file_info or (file_info and "piecemap" not in file_info): # It's not a big file - return False - - self.site.settings["has_bigfile"] = True - file_path = self.getPath(inner_path) - sha512 = file_info["sha512"] - piece_num = int(math.ceil(float(file_info["size"]) / file_info["piece_size"])) - if os.path.isfile(file_path): - if sha512 not in self.piecefields: - if open(file_path, "rb").read(128) == b"\0" * 128: - piece_data = b"\x00" - else: - piece_data = b"\x01" - self.log.debug("%s: File exists, but not in piecefield. Filling piecefiled with %s * %s." % (inner_path, piece_num, piece_data)) - self.piecefields[sha512].frombytes(piece_data * piece_num) - else: - self.log.debug("Creating bigfile: %s" % inner_path) - self.createSparseFile(inner_path, file_info["size"], sha512) - self.piecefields[sha512].frombytes(b"\x00" * piece_num) - self.log.debug("Created bigfile: %s" % inner_path) - return True - - def openBigfile(self, inner_path, prebuffer=0): - if not self.checkBigfile(inner_path): - return False - self.site.needFile(inner_path, blocking=False) # Download piecemap - return BigFile(self.site, inner_path, prebuffer=prebuffer) - - -class BigFile(object): - def __init__(self, site, inner_path, prebuffer=0): - self.site = site - self.inner_path = inner_path - file_path = site.storage.getPath(inner_path) - file_info = self.site.content_manager.getFileInfo(inner_path) - self.piece_size = file_info["piece_size"] - self.sha512 = file_info["sha512"] - self.size = file_info["size"] - self.prebuffer = prebuffer - self.read_bytes = 0 - - self.piecefield = self.site.storage.piecefields[self.sha512] - self.f = open(file_path, "rb+") - self.read_lock = gevent.lock.Semaphore() - - def read(self, buff=64 * 1024): - with self.read_lock: - pos = self.f.tell() - read_until = min(self.size, pos + buff) - requests = [] - # Request all required blocks - while 1: - piece_i = int(pos / self.piece_size) - if piece_i * self.piece_size >= read_until: - break - pos_from = piece_i * self.piece_size - pos_to = pos_from + self.piece_size - if not self.piecefield[piece_i]: - requests.append(self.site.needFile("%s|%s-%s" % (self.inner_path, pos_from, pos_to), blocking=False, update=True, priority=10)) - pos += self.piece_size - - if not all(requests): - return None - - # Request prebuffer - if self.prebuffer: - prebuffer_until = min(self.size, read_until + self.prebuffer) - priority = 3 - while 1: - piece_i = int(pos / self.piece_size) - if piece_i * self.piece_size >= prebuffer_until: - break - pos_from = piece_i * self.piece_size - pos_to = pos_from + self.piece_size - if not self.piecefield[piece_i]: - self.site.needFile("%s|%s-%s" % (self.inner_path, pos_from, pos_to), blocking=False, update=True, priority=max(0, priority)) - priority -= 1 - pos += self.piece_size - - gevent.joinall(requests) - self.read_bytes += buff - - # Increase buffer for long reads - if self.read_bytes > 7 * 1024 * 1024 and self.prebuffer < 5 * 1024 * 1024: - self.site.log.debug("%s: Increasing bigfile buffer size to 5MB..." % self.inner_path) - self.prebuffer = 5 * 1024 * 1024 - - return self.f.read(buff) - - def seek(self, pos, whence=0): - with self.read_lock: - if whence == 2: # Relative from file end - pos = self.size + pos # Use the real size instead of size on the disk - whence = 0 - return self.f.seek(pos, whence) - - def seekable(self): - return self.f.seekable() - - def tell(self): - return self.f.tell() - - def close(self): - self.f.close() - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.close() - - -@PluginManager.registerTo("WorkerManager") -class WorkerManagerPlugin(object): - def addTask(self, inner_path, *args, **kwargs): - file_info = kwargs.get("file_info") - if file_info and "piecemap" in file_info: # Bigfile - self.site.settings["has_bigfile"] = True - - piecemap_inner_path = helper.getDirname(file_info["content_inner_path"]) + file_info["piecemap"] - piecemap_task = None - if not self.site.storage.isFile(piecemap_inner_path): - # Start download piecemap - piecemap_task = super(WorkerManagerPlugin, self).addTask(piecemap_inner_path, priority=30) - autodownload_bigfile_size_limit = self.site.settings.get("autodownload_bigfile_size_limit", config.autodownload_bigfile_size_limit) - if "|" not in inner_path and self.site.isDownloadable(inner_path) and file_info["size"] / 1024 / 1024 <= autodownload_bigfile_size_limit: - gevent.spawn_later(0.1, self.site.needFile, inner_path + "|all") # Download all pieces - - if "|" in inner_path: - # Start download piece - task = super(WorkerManagerPlugin, self).addTask(inner_path, *args, **kwargs) - - inner_path, file_range = inner_path.split("|") - pos_from, pos_to = map(int, file_range.split("-")) - task["piece_i"] = int(pos_from / file_info["piece_size"]) - task["sha512"] = file_info["sha512"] - else: - if inner_path in self.site.bad_files: - del self.site.bad_files[inner_path] - if piecemap_task: - task = piecemap_task - else: - fake_evt = gevent.event.AsyncResult() # Don't download anything if no range specified - fake_evt.set(True) - task = {"evt": fake_evt} - - if not self.site.storage.isFile(inner_path): - self.site.storage.createSparseFile(inner_path, file_info["size"], file_info["sha512"]) - piece_num = int(math.ceil(float(file_info["size"]) / file_info["piece_size"])) - self.site.storage.piecefields[file_info["sha512"]].frombytes(b"\x00" * piece_num) - else: - task = super(WorkerManagerPlugin, self).addTask(inner_path, *args, **kwargs) - return task - - def taskAddPeer(self, task, peer): - if "piece_i" in task: - if not peer.piecefields[task["sha512"]][task["piece_i"]]: - if task["sha512"] not in peer.piecefields: - gevent.spawn(peer.updatePiecefields, force=True) - elif not task["peers"]: - gevent.spawn(peer.updatePiecefields) - - return False # Deny to add peers to task if file not in piecefield - return super(WorkerManagerPlugin, self).taskAddPeer(task, peer) - - -@PluginManager.registerTo("FileRequest") -class FileRequestPlugin(object): - def isReadable(self, site, inner_path, file, pos): - # Peek into file - if file.read(10) == b"\0" * 10: - # Looks empty, but makes sures we don't have that piece - file_info = site.content_manager.getFileInfo(inner_path) - if "piece_size" in file_info: - piece_i = int(pos / file_info["piece_size"]) - if not site.storage.piecefields[file_info["sha512"]][piece_i]: - return False - # Seek back to position we want to read - file.seek(pos) - return super(FileRequestPlugin, self).isReadable(site, inner_path, file, pos) - - def actionGetPiecefields(self, params): - site = self.sites.get(params["site"]) - if not site or not site.isServing(): # Site unknown or not serving - self.response({"error": "Unknown site"}) - return False - - # Add peer to site if not added before - peer = site.addPeer(self.connection.ip, self.connection.port, return_peer=True) - if not peer.connection: # Just added - peer.connect(self.connection) # Assign current connection to peer - - piecefields_packed = {sha512: piecefield.pack() for sha512, piecefield in site.storage.piecefields.items()} - self.response({"piecefields_packed": piecefields_packed}) - - def actionSetPiecefields(self, params): - site = self.sites.get(params["site"]) - if not site or not site.isServing(): # Site unknown or not serving - self.response({"error": "Unknown site"}) - self.connection.badAction(5) - return False - - # Add or get peer - peer = site.addPeer(self.connection.ip, self.connection.port, return_peer=True, connection=self.connection) - if not peer.connection: - peer.connect(self.connection) - - peer.piecefields = collections.defaultdict(BigfilePiecefieldPacked) - for sha512, piecefield_packed in params["piecefields_packed"].items(): - peer.piecefields[sha512].unpack(piecefield_packed) - site.settings["has_bigfile"] = True - - self.response({"ok": "Updated"}) - - -@PluginManager.registerTo("Peer") -class PeerPlugin(object): - def __getattr__(self, key): - if key == "piecefields": - self.piecefields = collections.defaultdict(BigfilePiecefieldPacked) - return self.piecefields - elif key == "time_piecefields_updated": - self.time_piecefields_updated = None - return self.time_piecefields_updated - else: - return super(PeerPlugin, self).__getattr__(key) - - @util.Noparallel(ignore_args=True) - def updatePiecefields(self, force=False): - if self.connection and self.connection.handshake.get("rev", 0) < 2190: - return False # Not supported - - # Don't update piecefield again in 1 min - if self.time_piecefields_updated and time.time() - self.time_piecefields_updated < 60 and not force: - return False - - self.time_piecefields_updated = time.time() - res = self.request("getPiecefields", {"site": self.site.address}) - if not res or "error" in res: - return False - - self.piecefields = collections.defaultdict(BigfilePiecefieldPacked) - try: - for sha512, piecefield_packed in res["piecefields_packed"].items(): - self.piecefields[sha512].unpack(piecefield_packed) - except Exception as err: - self.log("Invalid updatePiecefields response: %s" % Debug.formatException(err)) - - return self.piecefields - - def sendMyHashfield(self, *args, **kwargs): - return super(PeerPlugin, self).sendMyHashfield(*args, **kwargs) - - def updateHashfield(self, *args, **kwargs): - if self.site.settings.get("has_bigfile"): - thread = gevent.spawn(self.updatePiecefields, *args, **kwargs) - back = super(PeerPlugin, self).updateHashfield(*args, **kwargs) - thread.join() - return back - else: - return super(PeerPlugin, self).updateHashfield(*args, **kwargs) - - def getFile(self, site, inner_path, *args, **kwargs): - if "|" in inner_path: - inner_path, file_range = inner_path.split("|") - pos_from, pos_to = map(int, file_range.split("-")) - kwargs["pos_from"] = pos_from - kwargs["pos_to"] = pos_to - return super(PeerPlugin, self).getFile(site, inner_path, *args, **kwargs) - - -@PluginManager.registerTo("Site") -class SitePlugin(object): - def isFileDownloadAllowed(self, inner_path, file_info): - if "piecemap" in file_info: - file_size_mb = file_info["size"] / 1024 / 1024 - if config.bigfile_size_limit and file_size_mb > config.bigfile_size_limit: - self.log.debug( - "Bigfile size %s too large: %sMB > %sMB, skipping..." % - (inner_path, file_size_mb, config.bigfile_size_limit) - ) - return False - - file_info = file_info.copy() - file_info["size"] = file_info["piece_size"] - return super(SitePlugin, self).isFileDownloadAllowed(inner_path, file_info) - - def getSettingsCache(self): - back = super(SitePlugin, self).getSettingsCache() - if self.storage.piecefields: - back["piecefields"] = {sha512: base64.b64encode(piecefield.pack()).decode("utf8") for sha512, piecefield in self.storage.piecefields.items()} - return back - - def needFile(self, inner_path, *args, **kwargs): - if inner_path.endswith("|all"): - @util.Pooled(20) - def pooledNeedBigfile(inner_path, *args, **kwargs): - if inner_path not in self.bad_files: - self.log.debug("Cancelled piece, skipping %s" % inner_path) - return False - return self.needFile(inner_path, *args, **kwargs) - - inner_path = inner_path.replace("|all", "") - file_info = self.needFileInfo(inner_path) - - # Use default function to download non-optional file - if "piece_size" not in file_info: - return super(SitePlugin, self).needFile(inner_path, *args, **kwargs) - - file_size = file_info["size"] - piece_size = file_info["piece_size"] - - piece_num = int(math.ceil(float(file_size) / piece_size)) - - file_threads = [] - - piecefield = self.storage.piecefields.get(file_info["sha512"]) - - for piece_i in range(piece_num): - piece_from = piece_i * piece_size - piece_to = min(file_size, piece_from + piece_size) - if not piecefield or not piecefield[piece_i]: - inner_path_piece = "%s|%s-%s" % (inner_path, piece_from, piece_to) - self.bad_files[inner_path_piece] = self.bad_files.get(inner_path_piece, 1) - res = pooledNeedBigfile(inner_path_piece, blocking=False) - if res is not True and res is not False: - file_threads.append(res) - gevent.joinall(file_threads) - else: - return super(SitePlugin, self).needFile(inner_path, *args, **kwargs) - - -@PluginManager.registerTo("ConfigPlugin") -class ConfigPlugin(object): - def createArguments(self): - group = self.parser.add_argument_group("Bigfile plugin") - group.add_argument('--autodownload_bigfile_size_limit', help='Also download bigfiles smaller than this limit if help distribute option is checked', default=10, metavar="MB", type=int) - group.add_argument('--bigfile_size_limit', help='Maximum size of downloaded big files', default=False, metavar="MB", type=int) - - return super(ConfigPlugin, self).createArguments() diff --git a/plugins/Bigfile/Test/TestBigfile.py b/plugins/Bigfile/Test/TestBigfile.py deleted file mode 100644 index 402646a6..00000000 --- a/plugins/Bigfile/Test/TestBigfile.py +++ /dev/null @@ -1,574 +0,0 @@ -import time -import io -import binascii - -import pytest -import mock - -from Connection import ConnectionServer -from Content.ContentManager import VerifyError -from File import FileServer -from File import FileRequest -from Worker import WorkerManager -from Peer import Peer -from Bigfile import BigfilePiecefield, BigfilePiecefieldPacked -from Test import Spy -from util import Msgpack - - -@pytest.mark.usefixtures("resetSettings") -@pytest.mark.usefixtures("resetTempSettings") -class TestBigfile: - privatekey = "5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv" - piece_size = 1024 * 1024 - - def createBigfile(self, site, inner_path="data/optional.any.iso", pieces=10): - f = site.storage.open(inner_path, "w") - for i in range(pieces * 100): - f.write(("Test%s" % i).ljust(10, "-") * 1000) - f.close() - assert site.content_manager.sign("content.json", self.privatekey) - return inner_path - - def testPiecemapCreate(self, site): - inner_path = self.createBigfile(site) - content = site.storage.loadJson("content.json") - assert "data/optional.any.iso" in content["files_optional"] - file_node = content["files_optional"][inner_path] - assert file_node["size"] == 10 * 1000 * 1000 - assert file_node["sha512"] == "47a72cde3be80b4a829e7674f72b7c6878cf6a70b0c58c6aa6c17d7e9948daf6" - assert file_node["piecemap"] == inner_path + ".piecemap.msgpack" - - piecemap = Msgpack.unpack(site.storage.open(file_node["piecemap"], "rb").read())["optional.any.iso"] - assert len(piecemap["sha512_pieces"]) == 10 - assert piecemap["sha512_pieces"][0] != piecemap["sha512_pieces"][1] - assert binascii.hexlify(piecemap["sha512_pieces"][0]) == b"a73abad9992b3d0b672d0c2a292046695d31bebdcb1e150c8410bbe7c972eff3" - - def testVerifyPiece(self, site): - inner_path = self.createBigfile(site) - - # Verify all 10 piece - f = site.storage.open(inner_path, "rb") - for i in range(10): - piece = io.BytesIO(f.read(1024 * 1024)) - piece.seek(0) - site.content_manager.verifyPiece(inner_path, i * 1024 * 1024, piece) - f.close() - - # Try to verify piece 0 with piece 1 hash - with pytest.raises(VerifyError) as err: - i = 1 - f = site.storage.open(inner_path, "rb") - piece = io.BytesIO(f.read(1024 * 1024)) - f.close() - site.content_manager.verifyPiece(inner_path, i * 1024 * 1024, piece) - assert "Invalid hash" in str(err.value) - - def testSparseFile(self, site): - inner_path = "sparsefile" - - # Create a 100MB sparse file - site.storage.createSparseFile(inner_path, 100 * 1024 * 1024) - - # Write to file beginning - s = time.time() - f = site.storage.write("%s|%s-%s" % (inner_path, 0, 1024 * 1024), b"hellostart" * 1024) - time_write_start = time.time() - s - - # Write to file end - s = time.time() - f = site.storage.write("%s|%s-%s" % (inner_path, 99 * 1024 * 1024, 99 * 1024 * 1024 + 1024 * 1024), b"helloend" * 1024) - time_write_end = time.time() - s - - # Verify writes - f = site.storage.open(inner_path) - assert f.read(10) == b"hellostart" - f.seek(99 * 1024 * 1024) - assert f.read(8) == b"helloend" - f.close() - - site.storage.delete(inner_path) - - # Writing to end shold not take much longer, than writing to start - assert time_write_end <= max(0.1, time_write_start * 1.1) - - def testRangedFileRequest(self, file_server, site, site_temp): - inner_path = self.createBigfile(site) - - file_server.sites[site.address] = site - client = FileServer(file_server.ip, 1545) - client.sites[site_temp.address] = site_temp - site_temp.connection_server = client - connection = client.getConnection(file_server.ip, 1544) - - # Add file_server as peer to client - peer_file_server = site_temp.addPeer(file_server.ip, 1544) - - buff = peer_file_server.getFile(site_temp.address, "%s|%s-%s" % (inner_path, 5 * 1024 * 1024, 6 * 1024 * 1024)) - - assert len(buff.getvalue()) == 1 * 1024 * 1024 # Correct block size - assert buff.getvalue().startswith(b"Test524") # Correct data - buff.seek(0) - assert site.content_manager.verifyPiece(inner_path, 5 * 1024 * 1024, buff) # Correct hash - - connection.close() - client.stop() - - def testRangedFileDownload(self, file_server, site, site_temp): - inner_path = self.createBigfile(site) - - # Init source server - site.connection_server = file_server - file_server.sites[site.address] = site - - # Make sure the file and the piecemap in the optional hashfield - file_info = site.content_manager.getFileInfo(inner_path) - assert site.content_manager.hashfield.hasHash(file_info["sha512"]) - - piecemap_hash = site.content_manager.getFileInfo(file_info["piecemap"])["sha512"] - assert site.content_manager.hashfield.hasHash(piecemap_hash) - - # Init client server - client = ConnectionServer(file_server.ip, 1545) - site_temp.connection_server = client - peer_client = site_temp.addPeer(file_server.ip, 1544) - - # Download site - site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10) - - bad_files = site_temp.storage.verifyFiles(quick_check=True)["bad_files"] - assert not bad_files - - # client_piecefield = peer_client.piecefields[file_info["sha512"]].tostring() - # assert client_piecefield == "1" * 10 - - # Download 5. and 10. block - - site_temp.needFile("%s|%s-%s" % (inner_path, 5 * 1024 * 1024, 6 * 1024 * 1024)) - site_temp.needFile("%s|%s-%s" % (inner_path, 9 * 1024 * 1024, 10 * 1024 * 1024)) - - # Verify 0. block not downloaded - f = site_temp.storage.open(inner_path) - assert f.read(10) == b"\0" * 10 - # Verify 5. and 10. block downloaded - f.seek(5 * 1024 * 1024) - assert f.read(7) == b"Test524" - f.seek(9 * 1024 * 1024) - assert f.read(7) == b"943---T" - - # Verify hashfield - assert set(site_temp.content_manager.hashfield) == set([18343, 43727]) # 18343: data/optional.any.iso, 43727: data/optional.any.iso.hashmap.msgpack - - def testOpenBigfile(self, file_server, site, site_temp): - inner_path = self.createBigfile(site) - - # Init source server - site.connection_server = file_server - file_server.sites[site.address] = site - - # Init client server - client = ConnectionServer(file_server.ip, 1545) - site_temp.connection_server = client - site_temp.addPeer(file_server.ip, 1544) - - # Download site - site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10) - - # Open virtual file - assert not site_temp.storage.isFile(inner_path) - - with site_temp.storage.openBigfile(inner_path) as f: - with Spy.Spy(FileRequest, "route") as requests: - f.seek(5 * 1024 * 1024) - assert f.read(7) == b"Test524" - - f.seek(9 * 1024 * 1024) - assert f.read(7) == b"943---T" - - assert len(requests) == 4 # 1x peicemap + 1x getpiecefield + 2x for pieces - - assert set(site_temp.content_manager.hashfield) == set([18343, 43727]) - - assert site_temp.storage.piecefields[f.sha512].tostring() == "0000010001" - assert f.sha512 in site_temp.getSettingsCache()["piecefields"] - - # Test requesting already downloaded - with Spy.Spy(FileRequest, "route") as requests: - f.seek(5 * 1024 * 1024) - assert f.read(7) == b"Test524" - - assert len(requests) == 0 - - # Test requesting multi-block overflow reads - with Spy.Spy(FileRequest, "route") as requests: - f.seek(5 * 1024 * 1024) # We already have this block - data = f.read(1024 * 1024 * 3) # Our read overflow to 6. and 7. block - assert data.startswith(b"Test524") - assert data.endswith(b"Test838-") - assert b"\0" not in data # No null bytes allowed - - assert len(requests) == 2 # Two block download - - # Test out of range request - f.seek(5 * 1024 * 1024) - data = f.read(1024 * 1024 * 30) - assert len(data) == 10 * 1000 * 1000 - (5 * 1024 * 1024) - - f.seek(30 * 1024 * 1024) - data = f.read(1024 * 1024 * 30) - assert len(data) == 0 - - @pytest.mark.parametrize("piecefield_obj", [BigfilePiecefield, BigfilePiecefieldPacked]) - def testPiecefield(self, piecefield_obj, site): - testdatas = [ - b"\x01" * 100 + b"\x00" * 900 + b"\x01" * 4000 + b"\x00" * 4999 + b"\x01", - b"\x00\x01\x00\x01\x00\x01" * 10 + b"\x00\x01" * 90 + b"\x01\x00" * 400 + b"\x00" * 4999, - b"\x01" * 10000, - b"\x00" * 10000 - ] - for testdata in testdatas: - piecefield = piecefield_obj() - - piecefield.frombytes(testdata) - assert piecefield.tobytes() == testdata - assert piecefield[0] == testdata[0] - assert piecefield[100] == testdata[100] - assert piecefield[1000] == testdata[1000] - assert piecefield[len(testdata) - 1] == testdata[len(testdata) - 1] - - packed = piecefield.pack() - piecefield_new = piecefield_obj() - piecefield_new.unpack(packed) - assert piecefield.tobytes() == piecefield_new.tobytes() - assert piecefield_new.tobytes() == testdata - - def testFileGet(self, file_server, site, site_temp): - inner_path = self.createBigfile(site) - - # Init source server - site.connection_server = file_server - file_server.sites[site.address] = site - - # Init client server - site_temp.connection_server = FileServer(file_server.ip, 1545) - site_temp.connection_server.sites[site_temp.address] = site_temp - site_temp.addPeer(file_server.ip, 1544) - - # Download site - site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10) - - # Download second block - with site_temp.storage.openBigfile(inner_path) as f: - f.seek(1024 * 1024) - assert f.read(1024)[0:1] != b"\0" - - # Make sure first block not download - with site_temp.storage.open(inner_path) as f: - assert f.read(1024)[0:1] == b"\0" - - peer2 = site.addPeer(file_server.ip, 1545, return_peer=True) - - # Should drop error on first block request - assert not peer2.getFile(site.address, "%s|0-%s" % (inner_path, 1024 * 1024 * 1)) - - # Should not drop error for second block request - assert peer2.getFile(site.address, "%s|%s-%s" % (inner_path, 1024 * 1024 * 1, 1024 * 1024 * 2)) - - def benchmarkPeerMemory(self, site, file_server): - # Init source server - site.connection_server = file_server - file_server.sites[site.address] = site - - import psutil, os - meminfo = psutil.Process(os.getpid()).memory_info - - mem_s = meminfo()[0] - s = time.time() - for i in range(25000): - site.addPeer(file_server.ip, i) - print("%.3fs MEM: + %sKB" % (time.time() - s, (meminfo()[0] - mem_s) / 1024)) # 0.082s MEM: + 6800KB - print(list(site.peers.values())[0].piecefields) - - def testUpdatePiecefield(self, file_server, site, site_temp): - inner_path = self.createBigfile(site) - - server1 = file_server - server1.sites[site.address] = site - server2 = FileServer(file_server.ip, 1545) - server2.sites[site_temp.address] = site_temp - site_temp.connection_server = server2 - - # Add file_server as peer to client - server2_peer1 = site_temp.addPeer(file_server.ip, 1544) - - # Testing piecefield sync - assert len(server2_peer1.piecefields) == 0 - assert server2_peer1.updatePiecefields() # Query piecefields from peer - assert len(server2_peer1.piecefields) > 0 - - def testWorkerManagerPiecefieldDeny(self, file_server, site, site_temp): - inner_path = self.createBigfile(site) - - server1 = file_server - server1.sites[site.address] = site - server2 = FileServer(file_server.ip, 1545) - server2.sites[site_temp.address] = site_temp - site_temp.connection_server = server2 - - # Add file_server as peer to client - server2_peer1 = site_temp.addPeer(file_server.ip, 1544) # Working - - site_temp.downloadContent("content.json", download_files=False) - site_temp.needFile("data/optional.any.iso.piecemap.msgpack") - - # Add fake peers with optional files downloaded - for i in range(5): - fake_peer = site_temp.addPeer("127.0.1.%s" % i, 1544) - fake_peer.hashfield = site.content_manager.hashfield - fake_peer.has_hashfield = True - - with Spy.Spy(WorkerManager, "addWorker") as requests: - site_temp.needFile("%s|%s-%s" % (inner_path, 5 * 1024 * 1024, 6 * 1024 * 1024)) - site_temp.needFile("%s|%s-%s" % (inner_path, 6 * 1024 * 1024, 7 * 1024 * 1024)) - - # It should only request parts from peer1 as the other peers does not have the requested parts in piecefields - assert len([request[1] for request in requests if request[1] != server2_peer1]) == 0 - - def testWorkerManagerPiecefieldDownload(self, file_server, site, site_temp): - inner_path = self.createBigfile(site) - - server1 = file_server - server1.sites[site.address] = site - server2 = FileServer(file_server.ip, 1545) - server2.sites[site_temp.address] = site_temp - site_temp.connection_server = server2 - sha512 = site.content_manager.getFileInfo(inner_path)["sha512"] - - # Create 10 fake peer for each piece - for i in range(10): - peer = Peer(file_server.ip, 1544, site_temp, server2) - peer.piecefields[sha512][i] = b"\x01" - peer.updateHashfield = mock.MagicMock(return_value=False) - peer.updatePiecefields = mock.MagicMock(return_value=False) - peer.findHashIds = mock.MagicMock(return_value={"nope": []}) - peer.hashfield = site.content_manager.hashfield - peer.has_hashfield = True - peer.key = "Peer:%s" % i - site_temp.peers["Peer:%s" % i] = peer - - site_temp.downloadContent("content.json", download_files=False) - site_temp.needFile("data/optional.any.iso.piecemap.msgpack") - - with Spy.Spy(Peer, "getFile") as requests: - for i in range(10): - site_temp.needFile("%s|%s-%s" % (inner_path, i * 1024 * 1024, (i + 1) * 1024 * 1024)) - - assert len(requests) == 10 - for i in range(10): - assert requests[i][0] == site_temp.peers["Peer:%s" % i] # Every part should be requested from piece owner peer - - def testDownloadStats(self, file_server, site, site_temp): - inner_path = self.createBigfile(site) - - # Init source server - site.connection_server = file_server - file_server.sites[site.address] = site - - # Init client server - client = ConnectionServer(file_server.ip, 1545) - site_temp.connection_server = client - site_temp.addPeer(file_server.ip, 1544) - - # Download site - site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10) - - # Open virtual file - assert not site_temp.storage.isFile(inner_path) - - # Check size before downloads - assert site_temp.settings["size"] < 10 * 1024 * 1024 - assert site_temp.settings["optional_downloaded"] == 0 - size_piecemap = site_temp.content_manager.getFileInfo(inner_path + ".piecemap.msgpack")["size"] - size_bigfile = site_temp.content_manager.getFileInfo(inner_path)["size"] - - with site_temp.storage.openBigfile(inner_path) as f: - assert b"\0" not in f.read(1024) - assert site_temp.settings["optional_downloaded"] == size_piecemap + size_bigfile - - with site_temp.storage.openBigfile(inner_path) as f: - # Don't count twice - assert b"\0" not in f.read(1024) - assert site_temp.settings["optional_downloaded"] == size_piecemap + size_bigfile - - # Add second block - assert b"\0" not in f.read(1024 * 1024) - assert site_temp.settings["optional_downloaded"] == size_piecemap + size_bigfile - - def testPrebuffer(self, file_server, site, site_temp): - inner_path = self.createBigfile(site) - - # Init source server - site.connection_server = file_server - file_server.sites[site.address] = site - - # Init client server - client = ConnectionServer(file_server.ip, 1545) - site_temp.connection_server = client - site_temp.addPeer(file_server.ip, 1544) - - # Download site - site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10) - - # Open virtual file - assert not site_temp.storage.isFile(inner_path) - - with site_temp.storage.openBigfile(inner_path, prebuffer=1024 * 1024 * 2) as f: - with Spy.Spy(FileRequest, "route") as requests: - f.seek(5 * 1024 * 1024) - assert f.read(7) == b"Test524" - # assert len(requests) == 3 # 1x piecemap + 1x getpiecefield + 1x for pieces - assert len([task for task in site_temp.worker_manager.tasks if task["inner_path"].startswith(inner_path)]) == 2 - - time.sleep(0.5) # Wait prebuffer download - - sha512 = site.content_manager.getFileInfo(inner_path)["sha512"] - assert site_temp.storage.piecefields[sha512].tostring() == "0000011100" - - # No prebuffer beyond end of the file - f.seek(9 * 1024 * 1024) - assert b"\0" not in f.read(7) - - assert len([task for task in site_temp.worker_manager.tasks if task["inner_path"].startswith(inner_path)]) == 0 - - def testDownloadAllPieces(self, file_server, site, site_temp): - inner_path = self.createBigfile(site) - - # Init source server - site.connection_server = file_server - file_server.sites[site.address] = site - - # Init client server - client = ConnectionServer(file_server.ip, 1545) - site_temp.connection_server = client - site_temp.addPeer(file_server.ip, 1544) - - # Download site - site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10) - - # Open virtual file - assert not site_temp.storage.isFile(inner_path) - - with Spy.Spy(FileRequest, "route") as requests: - site_temp.needFile("%s|all" % inner_path) - - assert len(requests) == 12 # piecemap.msgpack, getPiecefields, 10 x piece - - # Don't re-download already got pieces - with Spy.Spy(FileRequest, "route") as requests: - site_temp.needFile("%s|all" % inner_path) - - assert len(requests) == 0 - - def testFileSize(self, file_server, site, site_temp): - inner_path = self.createBigfile(site) - - # Init source server - site.connection_server = file_server - file_server.sites[site.address] = site - - # Init client server - client = ConnectionServer(file_server.ip, 1545) - site_temp.connection_server = client - site_temp.addPeer(file_server.ip, 1544) - - # Download site - site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10) - - # Open virtual file - assert not site_temp.storage.isFile(inner_path) - - # Download first block - site_temp.needFile("%s|%s-%s" % (inner_path, 0 * 1024 * 1024, 1 * 1024 * 1024)) - assert site_temp.storage.getSize(inner_path) < 1000 * 1000 * 10 # Size on the disk should be smaller than the real size - - site_temp.needFile("%s|%s-%s" % (inner_path, 9 * 1024 * 1024, 10 * 1024 * 1024)) - assert site_temp.storage.getSize(inner_path) == site.storage.getSize(inner_path) - - def testFileRename(self, file_server, site, site_temp): - inner_path = self.createBigfile(site) - - # Init source server - site.connection_server = file_server - file_server.sites[site.address] = site - - # Init client server - site_temp.connection_server = FileServer(file_server.ip, 1545) - site_temp.connection_server.sites[site_temp.address] = site_temp - site_temp.addPeer(file_server.ip, 1544) - - # Download site - site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10) - - with Spy.Spy(FileRequest, "route") as requests: - site_temp.needFile("%s|%s-%s" % (inner_path, 0, 1 * self.piece_size)) - - assert len([req for req in requests if req[1] == "streamFile"]) == 2 # 1 piece + piecemap - - # Rename the file - inner_path_new = inner_path.replace(".iso", "-new.iso") - site.storage.rename(inner_path, inner_path_new) - site.storage.delete("data/optional.any.iso.piecemap.msgpack") - assert site.content_manager.sign("content.json", self.privatekey, remove_missing_optional=True) - - files_optional = site.content_manager.contents["content.json"]["files_optional"].keys() - - assert "data/optional.any-new.iso.piecemap.msgpack" in files_optional - assert "data/optional.any.iso.piecemap.msgpack" not in files_optional - assert "data/optional.any.iso" not in files_optional - - with Spy.Spy(FileRequest, "route") as requests: - site.publish() - time.sleep(0.1) - site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10) # Wait for download - - assert len([req[1] for req in requests if req[1] == "streamFile"]) == 0 - - with site_temp.storage.openBigfile(inner_path_new, prebuffer=0) as f: - f.read(1024) - - # First piece already downloaded - assert [req for req in requests if req[1] == "streamFile"] == [] - - # Second piece needs to be downloaded + changed piecemap - f.seek(self.piece_size) - f.read(1024) - assert [req[3]["inner_path"] for req in requests if req[1] == "streamFile"] == [inner_path_new + ".piecemap.msgpack", inner_path_new] - - @pytest.mark.parametrize("size", [1024 * 3, 1024 * 1024 * 3, 1024 * 1024 * 30]) - def testNullFileRead(self, file_server, site, site_temp, size): - inner_path = "data/optional.iso" - - f = site.storage.open(inner_path, "w") - f.write("\0" * size) - f.close() - assert site.content_manager.sign("content.json", self.privatekey) - - # Init source server - site.connection_server = file_server - file_server.sites[site.address] = site - - # Init client server - site_temp.connection_server = FileServer(file_server.ip, 1545) - site_temp.connection_server.sites[site_temp.address] = site_temp - site_temp.addPeer(file_server.ip, 1544) - - # Download site - site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10) - - if "piecemap" in site.content_manager.getFileInfo(inner_path): # Bigfile - site_temp.needFile(inner_path + "|all") - else: - site_temp.needFile(inner_path) - - - assert site_temp.storage.getSize(inner_path) == size diff --git a/plugins/Bigfile/Test/conftest.py b/plugins/Bigfile/Test/conftest.py deleted file mode 100644 index 634e66e2..00000000 --- a/plugins/Bigfile/Test/conftest.py +++ /dev/null @@ -1 +0,0 @@ -from src.Test.conftest import * diff --git a/plugins/Bigfile/Test/pytest.ini b/plugins/Bigfile/Test/pytest.ini deleted file mode 100644 index d09210d1..00000000 --- a/plugins/Bigfile/Test/pytest.ini +++ /dev/null @@ -1,5 +0,0 @@ -[pytest] -python_files = Test*.py -addopts = -rsxX -v --durations=6 -markers = - webtest: mark a test as a webtest. \ No newline at end of file diff --git a/plugins/Bigfile/__init__.py b/plugins/Bigfile/__init__.py deleted file mode 100644 index cf2dcb49..00000000 --- a/plugins/Bigfile/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from . import BigfilePlugin -from .BigfilePiecefield import BigfilePiecefield, BigfilePiecefieldPacked \ No newline at end of file diff --git a/plugins/Chart/ChartCollector.py b/plugins/Chart/ChartCollector.py deleted file mode 100644 index 215c603c..00000000 --- a/plugins/Chart/ChartCollector.py +++ /dev/null @@ -1,181 +0,0 @@ -import time -import sys -import collections -import itertools -import logging - -import gevent -from util import helper -from Config import config - - -class ChartCollector(object): - def __init__(self, db): - self.db = db - if config.action == "main": - gevent.spawn_later(60 * 3, self.collector) - self.log = logging.getLogger("ChartCollector") - self.last_values = collections.defaultdict(dict) - - def setInitialLastValues(self, sites): - # Recover last value of site bytes/sent - for site in sites: - self.last_values["site:" + site.address]["site_bytes_recv"] = site.settings.get("bytes_recv", 0) - self.last_values["site:" + site.address]["site_bytes_sent"] = site.settings.get("bytes_sent", 0) - - def getCollectors(self): - collectors = {} - import main - file_server = main.file_server - sites = file_server.sites - if not sites: - return collectors - content_db = list(sites.values())[0].content_manager.contents.db - - # Connection stats - collectors["connection"] = lambda: len(file_server.connections) - collectors["connection_in"] = ( - lambda: len([1 for connection in file_server.connections if connection.type == "in"]) - ) - collectors["connection_onion"] = ( - lambda: len([1 for connection in file_server.connections if connection.ip.endswith(".onion")]) - ) - collectors["connection_ping_avg"] = ( - lambda: round(1000 * helper.avg( - [connection.last_ping_delay for connection in file_server.connections if connection.last_ping_delay] - )) - ) - collectors["connection_ping_min"] = ( - lambda: round(1000 * min( - [connection.last_ping_delay for connection in file_server.connections if connection.last_ping_delay] - )) - ) - collectors["connection_rev_avg"] = ( - lambda: helper.avg( - [connection.handshake["rev"] for connection in file_server.connections if connection.handshake] - ) - ) - - # Request stats - collectors["file_bytes_recv|change"] = lambda: file_server.bytes_recv - collectors["file_bytes_sent|change"] = lambda: file_server.bytes_sent - collectors["request_num_recv|change"] = lambda: file_server.num_recv - collectors["request_num_sent|change"] = lambda: file_server.num_sent - - # Limit - collectors["optional_limit"] = lambda: content_db.getOptionalLimitBytes() - collectors["optional_used"] = lambda: content_db.getOptionalUsedBytes() - collectors["optional_downloaded"] = lambda: sum([site.settings.get("optional_downloaded", 0) for site in sites.values()]) - - # Peers - collectors["peer"] = lambda peers: len(peers) - collectors["peer_onion"] = lambda peers: len([True for peer in peers if ".onion" in peer]) - - # Size - collectors["size"] = lambda: sum([site.settings.get("size", 0) for site in sites.values()]) - collectors["size_optional"] = lambda: sum([site.settings.get("size_optional", 0) for site in sites.values()]) - collectors["content"] = lambda: sum([len(site.content_manager.contents) for site in sites.values()]) - - return collectors - - def getSiteCollectors(self): - site_collectors = {} - - # Size - site_collectors["site_size"] = lambda site: site.settings.get("size", 0) - site_collectors["site_size_optional"] = lambda site: site.settings.get("size_optional", 0) - site_collectors["site_optional_downloaded"] = lambda site: site.settings.get("optional_downloaded", 0) - site_collectors["site_content"] = lambda site: len(site.content_manager.contents) - - # Data transfer - site_collectors["site_bytes_recv|change"] = lambda site: site.settings.get("bytes_recv", 0) - site_collectors["site_bytes_sent|change"] = lambda site: site.settings.get("bytes_sent", 0) - - # Peers - site_collectors["site_peer"] = lambda site: len(site.peers) - site_collectors["site_peer_onion"] = lambda site: len( - [True for peer in site.peers.values() if peer.ip.endswith(".onion")] - ) - site_collectors["site_peer_connected"] = lambda site: len([True for peer in site.peers.values() if peer.connection]) - - return site_collectors - - def getUniquePeers(self): - import main - sites = main.file_server.sites - return set(itertools.chain.from_iterable( - [site.peers.keys() for site in sites.values()] - )) - - def collectDatas(self, collectors, last_values, site=None): - if site is None: - peers = self.getUniquePeers() - datas = {} - for key, collector in collectors.items(): - try: - if site: - value = collector(site) - elif key.startswith("peer"): - value = collector(peers) - else: - value = collector() - except ValueError: - value = None - except Exception as err: - self.log.info("Collector %s error: %s" % (key, err)) - value = None - - if "|change" in key: # Store changes relative to last value - key = key.replace("|change", "") - last_value = last_values.get(key, 0) - last_values[key] = value - value = value - last_value - - if value is None: - datas[key] = None - else: - datas[key] = round(value, 3) - return datas - - def collectGlobal(self, collectors, last_values): - now = int(time.time()) - s = time.time() - datas = self.collectDatas(collectors, last_values["global"]) - values = [] - for key, value in datas.items(): - values.append((self.db.getTypeId(key), value, now)) - self.log.debug("Global collectors done in %.3fs" % (time.time() - s)) - - s = time.time() - cur = self.db.getCursor() - cur.executemany("INSERT INTO data (type_id, value, date_added) VALUES (?, ?, ?)", values) - self.log.debug("Global collectors inserted in %.3fs" % (time.time() - s)) - - def collectSites(self, sites, collectors, last_values): - now = int(time.time()) - s = time.time() - values = [] - for address, site in list(sites.items()): - site_datas = self.collectDatas(collectors, last_values["site:%s" % address], site) - for key, value in site_datas.items(): - values.append((self.db.getTypeId(key), self.db.getSiteId(address), value, now)) - time.sleep(0.001) - self.log.debug("Site collections done in %.3fs" % (time.time() - s)) - - s = time.time() - cur = self.db.getCursor() - cur.executemany("INSERT INTO data (type_id, site_id, value, date_added) VALUES (?, ?, ?, ?)", values) - self.log.debug("Site collectors inserted in %.3fs" % (time.time() - s)) - - def collector(self): - collectors = self.getCollectors() - site_collectors = self.getSiteCollectors() - import main - sites = main.file_server.sites - i = 0 - while 1: - self.collectGlobal(collectors, self.last_values) - if i % 12 == 0: # Only collect sites data every hour - self.collectSites(sites, site_collectors, self.last_values) - time.sleep(60 * 5) - i += 1 diff --git a/plugins/Chart/ChartDb.py b/plugins/Chart/ChartDb.py deleted file mode 100644 index 66a22082..00000000 --- a/plugins/Chart/ChartDb.py +++ /dev/null @@ -1,133 +0,0 @@ -from Config import config -from Db.Db import Db -import time - - -class ChartDb(Db): - def __init__(self): - self.version = 2 - super(ChartDb, self).__init__(self.getSchema(), "%s/chart.db" % config.data_dir) - self.foreign_keys = True - self.checkTables() - self.sites = self.loadSites() - self.types = self.loadTypes() - - def getSchema(self): - schema = {} - schema["db_name"] = "Chart" - schema["tables"] = {} - schema["tables"]["data"] = { - "cols": [ - ["data_id", "INTEGER PRIMARY KEY ASC AUTOINCREMENT NOT NULL UNIQUE"], - ["type_id", "INTEGER NOT NULL"], - ["site_id", "INTEGER"], - ["value", "INTEGER"], - ["date_added", "DATETIME DEFAULT (CURRENT_TIMESTAMP)"] - ], - "indexes": [ - "CREATE INDEX site_id ON data (site_id)", - "CREATE INDEX date_added ON data (date_added)" - ], - "schema_changed": 2 - } - schema["tables"]["type"] = { - "cols": [ - ["type_id", "INTEGER PRIMARY KEY NOT NULL UNIQUE"], - ["name", "TEXT"] - ], - "schema_changed": 1 - } - schema["tables"]["site"] = { - "cols": [ - ["site_id", "INTEGER PRIMARY KEY NOT NULL UNIQUE"], - ["address", "TEXT"] - ], - "schema_changed": 1 - } - return schema - - def getTypeId(self, name): - if name not in self.types: - res = self.execute("INSERT INTO type ?", {"name": name}) - self.types[name] = res.lastrowid - - return self.types[name] - - def getSiteId(self, address): - if address not in self.sites: - res = self.execute("INSERT INTO site ?", {"address": address}) - self.sites[address] = res.lastrowid - - return self.sites[address] - - def loadSites(self): - sites = {} - for row in self.execute("SELECT * FROM site"): - sites[row["address"]] = row["site_id"] - return sites - - def loadTypes(self): - types = {} - for row in self.execute("SELECT * FROM type"): - types[row["name"]] = row["type_id"] - return types - - def deleteSite(self, address): - if address in self.sites: - site_id = self.sites[address] - del self.sites[address] - self.execute("DELETE FROM site WHERE ?", {"site_id": site_id}) - self.execute("DELETE FROM data WHERE ?", {"site_id": site_id}) - - def archive(self): - week_back = 1 - while 1: - s = time.time() - date_added_from = time.time() - 60 * 60 * 24 * 7 * (week_back + 1) - date_added_to = date_added_from + 60 * 60 * 24 * 7 - res = self.execute(""" - SELECT - MAX(date_added) AS date_added, - SUM(value) AS value, - GROUP_CONCAT(data_id) AS data_ids, - type_id, - site_id, - COUNT(*) AS num - FROM data - WHERE - site_id IS NULL AND - date_added > :date_added_from AND - date_added < :date_added_to - GROUP BY strftime('%Y-%m-%d %H', date_added, 'unixepoch', 'localtime'), type_id - """, {"date_added_from": date_added_from, "date_added_to": date_added_to}) - - num_archived = 0 - cur = self.getCursor() - for row in res: - if row["num"] == 1: - continue - cur.execute("INSERT INTO data ?", { - "type_id": row["type_id"], - "site_id": row["site_id"], - "value": row["value"], - "date_added": row["date_added"] - }) - cur.execute("DELETE FROM data WHERE data_id IN (%s)" % row["data_ids"]) - num_archived += row["num"] - self.log.debug("Archived %s data from %s weeks ago in %.3fs" % (num_archived, week_back, time.time() - s)) - week_back += 1 - time.sleep(0.1) - if num_archived == 0: - break - # Only keep 6 month of global stats - self.execute( - "DELETE FROM data WHERE site_id IS NULL AND date_added < :date_added_limit", - {"date_added_limit": time.time() - 60 * 60 * 24 * 30 * 6 } - ) - # Only keep 1 month of site stats - self.execute( - "DELETE FROM data WHERE site_id IS NOT NULL AND date_added < :date_added_limit", - {"date_added_limit": time.time() - 60 * 60 * 24 * 30 } - ) - if week_back > 1: - self.execute("VACUUM") diff --git a/plugins/Chart/ChartPlugin.py b/plugins/Chart/ChartPlugin.py deleted file mode 100644 index 80a4d976..00000000 --- a/plugins/Chart/ChartPlugin.py +++ /dev/null @@ -1,57 +0,0 @@ -import time -import itertools - -import gevent - -from Config import config -from util import helper -from util.Flag import flag -from Plugin import PluginManager -from .ChartDb import ChartDb -from .ChartCollector import ChartCollector - -if "db" not in locals().keys(): # Share on reloads - db = ChartDb() - gevent.spawn_later(10 * 60, db.archive) - helper.timer(60 * 60 * 6, db.archive) - collector = ChartCollector(db) - -@PluginManager.registerTo("SiteManager") -class SiteManagerPlugin(object): - def load(self, *args, **kwargs): - back = super(SiteManagerPlugin, self).load(*args, **kwargs) - collector.setInitialLastValues(self.sites.values()) - return back - - def delete(self, address, *args, **kwargs): - db.deleteSite(address) - return super(SiteManagerPlugin, self).delete(address, *args, **kwargs) - -@PluginManager.registerTo("UiWebsocket") -class UiWebsocketPlugin(object): - @flag.admin - def actionChartDbQuery(self, to, query, params=None): - if config.debug or config.verbose: - s = time.time() - rows = [] - try: - if not query.strip().upper().startswith("SELECT"): - raise Exception("Only SELECT query supported") - res = db.execute(query, params) - except Exception as err: # Response the error to client - self.log.error("ChartDbQuery error: %s" % err) - return {"error": str(err)} - # Convert result to dict - for row in res: - rows.append(dict(row)) - if config.verbose and time.time() - s > 0.1: # Log slow query - self.log.debug("Slow query: %s (%.3fs)" % (query, time.time() - s)) - return rows - - @flag.admin - def actionChartGetPeerLocations(self, to): - peers = {} - for site in self.server.sites.values(): - peers.update(site.peers) - peer_locations = self.getPeerLocations(peers) - return peer_locations diff --git a/plugins/Chart/__init__.py b/plugins/Chart/__init__.py deleted file mode 100644 index 2c284609..00000000 --- a/plugins/Chart/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import ChartPlugin \ No newline at end of file diff --git a/plugins/Chart/plugin_info.json b/plugins/Chart/plugin_info.json deleted file mode 100644 index 3bdaea8a..00000000 --- a/plugins/Chart/plugin_info.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Chart", - "description": "Collect and provide stats of client information.", - "default": "enabled" -} \ No newline at end of file diff --git a/plugins/ContentFilter/ContentFilterPlugin.py b/plugins/ContentFilter/ContentFilterPlugin.py deleted file mode 100644 index f2f84b49..00000000 --- a/plugins/ContentFilter/ContentFilterPlugin.py +++ /dev/null @@ -1,262 +0,0 @@ -import time -import re -import html -import os - -from Plugin import PluginManager -from Translate import Translate -from Config import config -from util.Flag import flag - -from .ContentFilterStorage import ContentFilterStorage - - -plugin_dir = os.path.dirname(__file__) - -if "_" not in locals(): - _ = Translate(plugin_dir + "/languages/") - - -@PluginManager.registerTo("SiteManager") -class SiteManagerPlugin(object): - def load(self, *args, **kwargs): - global filter_storage - super(SiteManagerPlugin, self).load(*args, **kwargs) - filter_storage = ContentFilterStorage(site_manager=self) - - def add(self, address, *args, **kwargs): - should_ignore_block = kwargs.get("ignore_block") or kwargs.get("settings") - if should_ignore_block: - block_details = None - elif filter_storage.isSiteblocked(address): - block_details = filter_storage.getSiteblockDetails(address) - else: - address_hashed = filter_storage.getSiteAddressHashed(address) - if filter_storage.isSiteblocked(address_hashed): - block_details = filter_storage.getSiteblockDetails(address_hashed) - else: - block_details = None - - if block_details: - raise Exception("Site blocked: %s" % html.escape(block_details.get("reason", "unknown reason"))) - else: - return super(SiteManagerPlugin, self).add(address, *args, **kwargs) - - -@PluginManager.registerTo("UiWebsocket") -class UiWebsocketPlugin(object): - # Mute - def cbMuteAdd(self, to, auth_address, cert_user_id, reason): - filter_storage.file_content["mutes"][auth_address] = { - "cert_user_id": cert_user_id, "reason": reason, "source": self.site.address, "date_added": time.time() - } - filter_storage.save() - filter_storage.changeDbs(auth_address, "remove") - self.response(to, "ok") - - @flag.no_multiuser - def actionMuteAdd(self, to, auth_address, cert_user_id, reason): - if "ADMIN" in self.getPermissions(to): - self.cbMuteAdd(to, auth_address, cert_user_id, reason) - else: - self.cmd( - "confirm", - [_["Hide all content from %s?"] % html.escape(cert_user_id), _["Mute"]], - lambda res: self.cbMuteAdd(to, auth_address, cert_user_id, reason) - ) - - @flag.no_multiuser - def cbMuteRemove(self, to, auth_address): - del filter_storage.file_content["mutes"][auth_address] - filter_storage.save() - filter_storage.changeDbs(auth_address, "load") - self.response(to, "ok") - - @flag.no_multiuser - def actionMuteRemove(self, to, auth_address): - if "ADMIN" in self.getPermissions(to): - self.cbMuteRemove(to, auth_address) - else: - cert_user_id = html.escape(filter_storage.file_content["mutes"][auth_address]["cert_user_id"]) - self.cmd( - "confirm", - [_["Unmute %s?"] % cert_user_id, _["Unmute"]], - lambda res: self.cbMuteRemove(to, auth_address) - ) - - @flag.admin - def actionMuteList(self, to): - self.response(to, filter_storage.file_content["mutes"]) - - # Siteblock - @flag.no_multiuser - @flag.admin - def actionSiteblockIgnoreAddSite(self, to, site_address): - if site_address in filter_storage.site_manager.sites: - return {"error": "Site already added"} - else: - if filter_storage.site_manager.need(site_address, ignore_block=True): - return "ok" - else: - return {"error": "Invalid address"} - - @flag.no_multiuser - @flag.admin - def actionSiteblockAdd(self, to, site_address, reason=None): - filter_storage.file_content["siteblocks"][site_address] = {"date_added": time.time(), "reason": reason} - filter_storage.save() - self.response(to, "ok") - - @flag.no_multiuser - @flag.admin - def actionSiteblockRemove(self, to, site_address): - del filter_storage.file_content["siteblocks"][site_address] - filter_storage.save() - self.response(to, "ok") - - @flag.admin - def actionSiteblockList(self, to): - self.response(to, filter_storage.file_content["siteblocks"]) - - @flag.admin - def actionSiteblockGet(self, to, site_address): - if filter_storage.isSiteblocked(site_address): - res = filter_storage.getSiteblockDetails(site_address) - else: - site_address_hashed = filter_storage.getSiteAddressHashed(site_address) - if filter_storage.isSiteblocked(site_address_hashed): - res = filter_storage.getSiteblockDetails(site_address_hashed) - else: - res = {"error": "Site block not found"} - self.response(to, res) - - # Include - @flag.no_multiuser - def actionFilterIncludeAdd(self, to, inner_path, description=None, address=None): - if address: - if "ADMIN" not in self.getPermissions(to): - return self.response(to, {"error": "Forbidden: Only ADMIN sites can manage different site include"}) - site = self.server.sites[address] - else: - address = self.site.address - site = self.site - - if "ADMIN" in self.getPermissions(to): - self.cbFilterIncludeAdd(to, True, address, inner_path, description) - else: - content = site.storage.loadJson(inner_path) - title = _["New shared global content filter: %s (%s sites, %s users)"] % ( - html.escape(inner_path), len(content.get("siteblocks", {})), len(content.get("mutes", {})) - ) - - self.cmd( - "confirm", - [title, "Add"], - lambda res: self.cbFilterIncludeAdd(to, res, address, inner_path, description) - ) - - def cbFilterIncludeAdd(self, to, res, address, inner_path, description): - if not res: - self.response(to, res) - return False - - filter_storage.includeAdd(address, inner_path, description) - self.response(to, "ok") - - @flag.no_multiuser - def actionFilterIncludeRemove(self, to, inner_path, address=None): - if address: - if "ADMIN" not in self.getPermissions(to): - return self.response(to, {"error": "Forbidden: Only ADMIN sites can manage different site include"}) - else: - address = self.site.address - - key = "%s/%s" % (address, inner_path) - if key not in filter_storage.file_content["includes"]: - self.response(to, {"error": "Include not found"}) - filter_storage.includeRemove(address, inner_path) - self.response(to, "ok") - - def actionFilterIncludeList(self, to, all_sites=False, filters=False): - if all_sites and "ADMIN" not in self.getPermissions(to): - return self.response(to, {"error": "Forbidden: Only ADMIN sites can list all sites includes"}) - - back = [] - includes = filter_storage.file_content.get("includes", {}).values() - for include in includes: - if not all_sites and include["address"] != self.site.address: - continue - if filters: - include = dict(include) # Don't modify original file_content - include_site = filter_storage.site_manager.get(include["address"]) - if not include_site: - continue - content = include_site.storage.loadJson(include["inner_path"]) - include["mutes"] = content.get("mutes", {}) - include["siteblocks"] = content.get("siteblocks", {}) - back.append(include) - self.response(to, back) - - -@PluginManager.registerTo("SiteStorage") -class SiteStoragePlugin(object): - def updateDbFile(self, inner_path, file=None, cur=None): - if file is not False: # File deletion always allowed - # Find for bitcoin addresses in file path - matches = re.findall("/(1[A-Za-z0-9]{26,35})/", inner_path) - # Check if any of the adresses are in the mute list - for auth_address in matches: - if filter_storage.isMuted(auth_address): - self.log.debug("Mute match: %s, ignoring %s" % (auth_address, inner_path)) - return False - - return super(SiteStoragePlugin, self).updateDbFile(inner_path, file=file, cur=cur) - - def onUpdated(self, inner_path, file=None): - file_path = "%s/%s" % (self.site.address, inner_path) - if file_path in filter_storage.file_content["includes"]: - self.log.debug("Filter file updated: %s" % inner_path) - filter_storage.includeUpdateAll() - return super(SiteStoragePlugin, self).onUpdated(inner_path, file=file) - - -@PluginManager.registerTo("UiRequest") -class UiRequestPlugin(object): - def actionWrapper(self, path, extra_headers=None): - match = re.match(r"/(?P
      [A-Za-z0-9\._-]+)(?P/.*|$)", path) - if not match: - return False - address = match.group("address") - - if self.server.site_manager.get(address): # Site already exists - return super(UiRequestPlugin, self).actionWrapper(path, extra_headers) - - if self.isDomain(address): - address = self.resolveDomain(address) - - if address: - address_hashed = filter_storage.getSiteAddressHashed(address) - else: - address_hashed = None - - if filter_storage.isSiteblocked(address) or filter_storage.isSiteblocked(address_hashed): - site = self.server.site_manager.get(config.homepage) - if not extra_headers: - extra_headers = {} - - script_nonce = self.getScriptNonce() - - self.sendHeader(extra_headers=extra_headers, script_nonce=script_nonce) - return iter([super(UiRequestPlugin, self).renderWrapper( - site, path, "uimedia/plugins/contentfilter/blocklisted.html?address=" + address, - "Blacklisted site", extra_headers, show_loadingscreen=False, script_nonce=script_nonce - )]) - else: - return super(UiRequestPlugin, self).actionWrapper(path, extra_headers) - - def actionUiMedia(self, path, *args, **kwargs): - if path.startswith("/uimedia/plugins/contentfilter/"): - file_path = path.replace("/uimedia/plugins/contentfilter/", plugin_dir + "/media/") - return self.actionFile(file_path) - else: - return super(UiRequestPlugin, self).actionUiMedia(path) diff --git a/plugins/ContentFilter/ContentFilterStorage.py b/plugins/ContentFilter/ContentFilterStorage.py deleted file mode 100644 index 289ec2a9..00000000 --- a/plugins/ContentFilter/ContentFilterStorage.py +++ /dev/null @@ -1,164 +0,0 @@ -import os -import json -import logging -import collections -import time -import hashlib - -from Debug import Debug -from Plugin import PluginManager -from Config import config -from util import helper - - -class ContentFilterStorage(object): - def __init__(self, site_manager): - self.log = logging.getLogger("ContentFilterStorage") - self.file_path = "%s/filters.json" % config.data_dir - self.site_manager = site_manager - self.file_content = self.load() - - # Set default values for filters.json - if not self.file_content: - self.file_content = {} - - # Site blacklist renamed to site blocks - if "site_blacklist" in self.file_content: - self.file_content["siteblocks"] = self.file_content["site_blacklist"] - del self.file_content["site_blacklist"] - - for key in ["mutes", "siteblocks", "includes"]: - if key not in self.file_content: - self.file_content[key] = {} - - self.include_filters = collections.defaultdict(set) # Merged list of mutes and blacklists from all include - self.includeUpdateAll(update_site_dbs=False) - - def load(self): - # Rename previously used mutes.json -> filters.json - if os.path.isfile("%s/mutes.json" % config.data_dir): - self.log.info("Renaming mutes.json to filters.json...") - os.rename("%s/mutes.json" % config.data_dir, self.file_path) - if os.path.isfile(self.file_path): - try: - return json.load(open(self.file_path)) - except Exception as err: - self.log.error("Error loading filters.json: %s" % err) - return None - else: - return None - - def includeUpdateAll(self, update_site_dbs=True): - s = time.time() - new_include_filters = collections.defaultdict(set) - - # Load all include files data into a merged set - for include_path in self.file_content["includes"]: - address, inner_path = include_path.split("/", 1) - try: - content = self.site_manager.get(address).storage.loadJson(inner_path) - except Exception as err: - self.log.warning( - "Error loading include %s: %s" % - (include_path, Debug.formatException(err)) - ) - continue - - for key, val in content.items(): - if type(val) is not dict: - continue - - new_include_filters[key].update(val.keys()) - - mutes_added = new_include_filters["mutes"].difference(self.include_filters["mutes"]) - mutes_removed = self.include_filters["mutes"].difference(new_include_filters["mutes"]) - - self.include_filters = new_include_filters - - if update_site_dbs: - for auth_address in mutes_added: - self.changeDbs(auth_address, "remove") - - for auth_address in mutes_removed: - if not self.isMuted(auth_address): - self.changeDbs(auth_address, "load") - - num_mutes = len(self.include_filters["mutes"]) - num_siteblocks = len(self.include_filters["siteblocks"]) - self.log.debug( - "Loaded %s mutes, %s blocked sites from %s includes in %.3fs" % - (num_mutes, num_siteblocks, len(self.file_content["includes"]), time.time() - s) - ) - - def includeAdd(self, address, inner_path, description=None): - self.file_content["includes"]["%s/%s" % (address, inner_path)] = { - "date_added": time.time(), - "address": address, - "description": description, - "inner_path": inner_path - } - self.includeUpdateAll() - self.save() - - def includeRemove(self, address, inner_path): - del self.file_content["includes"]["%s/%s" % (address, inner_path)] - self.includeUpdateAll() - self.save() - - def save(self): - s = time.time() - helper.atomicWrite(self.file_path, json.dumps(self.file_content, indent=2, sort_keys=True).encode("utf8")) - self.log.debug("Saved in %.3fs" % (time.time() - s)) - - def isMuted(self, auth_address): - if auth_address in self.file_content["mutes"] or auth_address in self.include_filters["mutes"]: - return True - else: - return False - - def getSiteAddressHashed(self, address): - return "0x" + hashlib.sha256(address.encode("ascii")).hexdigest() - - def isSiteblocked(self, address): - if address in self.file_content["siteblocks"] or address in self.include_filters["siteblocks"]: - return True - return False - - def getSiteblockDetails(self, address): - details = self.file_content["siteblocks"].get(address) - if not details: - address_sha256 = self.getSiteAddressHashed(address) - details = self.file_content["siteblocks"].get(address_sha256) - - if not details: - includes = self.file_content.get("includes", {}).values() - for include in includes: - include_site = self.site_manager.get(include["address"]) - if not include_site: - continue - content = include_site.storage.loadJson(include["inner_path"]) - details = content.get("siteblocks", {}).get(address) - if details: - details["include"] = include - break - - return details - - # Search and remove or readd files of an user - def changeDbs(self, auth_address, action): - self.log.debug("Mute action %s on user %s" % (action, auth_address)) - res = list(self.site_manager.list().values())[0].content_manager.contents.db.execute( - "SELECT * FROM content LEFT JOIN site USING (site_id) WHERE inner_path LIKE :inner_path", - {"inner_path": "%%/%s/%%" % auth_address} - ) - for row in res: - site = self.site_manager.sites.get(row["address"]) - if not site: - continue - dir_inner_path = helper.getDirname(row["inner_path"]) - for file_name in site.storage.walk(dir_inner_path): - if action == "remove": - site.storage.onUpdated(dir_inner_path + file_name, False) - else: - site.storage.onUpdated(dir_inner_path + file_name) - site.onFileDone(dir_inner_path + file_name) diff --git a/plugins/ContentFilter/Test/TestContentFilter.py b/plugins/ContentFilter/Test/TestContentFilter.py deleted file mode 100644 index e1b37b16..00000000 --- a/plugins/ContentFilter/Test/TestContentFilter.py +++ /dev/null @@ -1,82 +0,0 @@ -import pytest -from ContentFilter import ContentFilterPlugin -from Site import SiteManager - - -@pytest.fixture -def filter_storage(): - ContentFilterPlugin.filter_storage = ContentFilterPlugin.ContentFilterStorage(SiteManager.site_manager) - return ContentFilterPlugin.filter_storage - - -@pytest.mark.usefixtures("resetSettings") -@pytest.mark.usefixtures("resetTempSettings") -class TestContentFilter: - def createInclude(self, site): - site.storage.writeJson("filters.json", { - "mutes": {"1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C": {}}, - "siteblocks": {site.address: {}} - }) - - def testIncludeLoad(self, site, filter_storage): - self.createInclude(site) - filter_storage.file_content["includes"]["%s/%s" % (site.address, "filters.json")] = { - "date_added": 1528295893, - } - - assert not filter_storage.include_filters["mutes"] - assert not filter_storage.isMuted("1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C") - assert not filter_storage.isSiteblocked(site.address) - filter_storage.includeUpdateAll(update_site_dbs=False) - assert len(filter_storage.include_filters["mutes"]) == 1 - assert filter_storage.isMuted("1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C") - assert filter_storage.isSiteblocked(site.address) - - def testIncludeAdd(self, site, filter_storage): - self.createInclude(site) - query_num_json = "SELECT COUNT(*) AS num FROM json WHERE directory = 'users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C'" - assert not filter_storage.isSiteblocked(site.address) - assert not filter_storage.isMuted("1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C") - assert site.storage.query(query_num_json).fetchone()["num"] == 2 - - # Add include - filter_storage.includeAdd(site.address, "filters.json") - - assert filter_storage.isSiteblocked(site.address) - assert filter_storage.isMuted("1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C") - assert site.storage.query(query_num_json).fetchone()["num"] == 0 - - # Remove include - filter_storage.includeRemove(site.address, "filters.json") - - assert not filter_storage.isSiteblocked(site.address) - assert not filter_storage.isMuted("1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C") - assert site.storage.query(query_num_json).fetchone()["num"] == 2 - - def testIncludeChange(self, site, filter_storage): - self.createInclude(site) - filter_storage.includeAdd(site.address, "filters.json") - assert filter_storage.isSiteblocked(site.address) - assert filter_storage.isMuted("1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C") - - # Add new blocked site - assert not filter_storage.isSiteblocked("1Hello") - - filter_content = site.storage.loadJson("filters.json") - filter_content["siteblocks"]["1Hello"] = {} - site.storage.writeJson("filters.json", filter_content) - - assert filter_storage.isSiteblocked("1Hello") - - # Add new muted user - query_num_json = "SELECT COUNT(*) AS num FROM json WHERE directory = 'users/1C5sgvWaSgfaTpV5kjBCnCiKtENNMYo69q'" - assert not filter_storage.isMuted("1C5sgvWaSgfaTpV5kjBCnCiKtENNMYo69q") - assert site.storage.query(query_num_json).fetchone()["num"] == 2 - - filter_content["mutes"]["1C5sgvWaSgfaTpV5kjBCnCiKtENNMYo69q"] = {} - site.storage.writeJson("filters.json", filter_content) - - assert filter_storage.isMuted("1C5sgvWaSgfaTpV5kjBCnCiKtENNMYo69q") - assert site.storage.query(query_num_json).fetchone()["num"] == 0 - - diff --git a/plugins/ContentFilter/Test/conftest.py b/plugins/ContentFilter/Test/conftest.py deleted file mode 100644 index 634e66e2..00000000 --- a/plugins/ContentFilter/Test/conftest.py +++ /dev/null @@ -1 +0,0 @@ -from src.Test.conftest import * diff --git a/plugins/ContentFilter/Test/pytest.ini b/plugins/ContentFilter/Test/pytest.ini deleted file mode 100644 index d09210d1..00000000 --- a/plugins/ContentFilter/Test/pytest.ini +++ /dev/null @@ -1,5 +0,0 @@ -[pytest] -python_files = Test*.py -addopts = -rsxX -v --durations=6 -markers = - webtest: mark a test as a webtest. \ No newline at end of file diff --git a/plugins/ContentFilter/__init__.py b/plugins/ContentFilter/__init__.py deleted file mode 100644 index 2cbca8ee..00000000 --- a/plugins/ContentFilter/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import ContentFilterPlugin diff --git a/plugins/ContentFilter/languages/hu.json b/plugins/ContentFilter/languages/hu.json deleted file mode 100644 index 9b57e697..00000000 --- a/plugins/ContentFilter/languages/hu.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "Hide all content from %s?": "%s tartalmaniak elrejtése?", - "Mute": "Elnémítás", - "Unmute %s?": "%s tartalmaniak megjelenítése?", - "Unmute": "Némítás visszavonása" -} diff --git a/plugins/ContentFilter/languages/it.json b/plugins/ContentFilter/languages/it.json deleted file mode 100644 index 9a2c6761..00000000 --- a/plugins/ContentFilter/languages/it.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "Hide all content from %s?": "%s Vuoi nascondere i contenuti di questo utente ?", - "Mute": "Attiva Silenzia", - "Unmute %s?": "%s Vuoi mostrare i contenuti di questo utente ?", - "Unmute": "Disattiva Silenzia" -} diff --git a/plugins/ContentFilter/languages/jp.json b/plugins/ContentFilter/languages/jp.json deleted file mode 100644 index ef586a1a..00000000 --- a/plugins/ContentFilter/languages/jp.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "Hide all content from %s?": "%s のコンテンツをすべて隠しますか?", - "Mute": "ミュート", - "Unmute %s?": "%s のミュートを解除しますか?", - "Unmute": "ミュート解除" -} diff --git a/plugins/ContentFilter/languages/pt-br.json b/plugins/ContentFilter/languages/pt-br.json deleted file mode 100644 index 3c6bfbdc..00000000 --- a/plugins/ContentFilter/languages/pt-br.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "Hide all content from %s?": "%s Ocultar todo o conteúdo de ?", - "Mute": "Ativar o Silêncio", - "Unmute %s?": "%s Você quer mostrar o conteúdo deste usuário ?", - "Unmute": "Desligar o silêncio" -} diff --git a/plugins/ContentFilter/languages/zh-tw.json b/plugins/ContentFilter/languages/zh-tw.json deleted file mode 100644 index 0995f3a0..00000000 --- a/plugins/ContentFilter/languages/zh-tw.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "Hide all content from %s?": "屏蔽 %s 的所有內容?", - "Mute": "屏蔽", - "Unmute %s?": "對 %s 解除屏蔽?", - "Unmute": "解除屏蔽" -} diff --git a/plugins/ContentFilter/languages/zh.json b/plugins/ContentFilter/languages/zh.json deleted file mode 100644 index bf63f107..00000000 --- a/plugins/ContentFilter/languages/zh.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "Hide all content from %s?": "屏蔽 %s 的所有内容?", - "Mute": "屏蔽", - "Unmute %s?": "对 %s 解除屏蔽?", - "Unmute": "解除屏蔽" -} diff --git a/plugins/ContentFilter/media/blocklisted.html b/plugins/ContentFilter/media/blocklisted.html deleted file mode 100644 index c9d201a9..00000000 --- a/plugins/ContentFilter/media/blocklisted.html +++ /dev/null @@ -1,89 +0,0 @@ - - - - - -
      -

      Site blocked

      -

      This site is on your blocklist:

      -
      -
      Too much image
      -
      on 2015-01-25 12:32:11
      -
      - -
      - - - - - - diff --git a/plugins/ContentFilter/media/js/ZeroFrame.js b/plugins/ContentFilter/media/js/ZeroFrame.js deleted file mode 100644 index d6facdbf..00000000 --- a/plugins/ContentFilter/media/js/ZeroFrame.js +++ /dev/null @@ -1,119 +0,0 @@ -// Version 1.0.0 - Initial release -// Version 1.1.0 (2017-08-02) - Added cmdp function that returns promise instead of using callback -// Version 1.2.0 (2017-08-02) - Added Ajax monkey patch to emulate XMLHttpRequest over ZeroFrame API - -const CMD_INNER_READY = 'innerReady' -const CMD_RESPONSE = 'response' -const CMD_WRAPPER_READY = 'wrapperReady' -const CMD_PING = 'ping' -const CMD_PONG = 'pong' -const CMD_WRAPPER_OPENED_WEBSOCKET = 'wrapperOpenedWebsocket' -const CMD_WRAPPER_CLOSE_WEBSOCKET = 'wrapperClosedWebsocket' - -class ZeroFrame { - constructor(url) { - this.url = url - this.waiting_cb = {} - this.wrapper_nonce = document.location.href.replace(/.*wrapper_nonce=([A-Za-z0-9]+).*/, "$1") - this.connect() - this.next_message_id = 1 - this.init() - } - - init() { - return this - } - - connect() { - this.target = window.parent - window.addEventListener('message', e => this.onMessage(e), false) - this.cmd(CMD_INNER_READY) - } - - onMessage(e) { - let message = e.data - let cmd = message.cmd - if (cmd === CMD_RESPONSE) { - if (this.waiting_cb[message.to] !== undefined) { - this.waiting_cb[message.to](message.result) - } - else { - this.log("Websocket callback not found:", message) - } - } else if (cmd === CMD_WRAPPER_READY) { - this.cmd(CMD_INNER_READY) - } else if (cmd === CMD_PING) { - this.response(message.id, CMD_PONG) - } else if (cmd === CMD_WRAPPER_OPENED_WEBSOCKET) { - this.onOpenWebsocket() - } else if (cmd === CMD_WRAPPER_CLOSE_WEBSOCKET) { - this.onCloseWebsocket() - } else { - this.onRequest(cmd, message) - } - } - - onRequest(cmd, message) { - this.log("Unknown request", message) - } - - response(to, result) { - this.send({ - cmd: CMD_RESPONSE, - to: to, - result: result - }) - } - - cmd(cmd, params={}, cb=null) { - this.send({ - cmd: cmd, - params: params - }, cb) - } - - cmdp(cmd, params={}) { - return new Promise((resolve, reject) => { - this.cmd(cmd, params, (res) => { - if (res && res.error) { - reject(res.error) - } else { - resolve(res) - } - }) - }) - } - - send(message, cb=null) { - message.wrapper_nonce = this.wrapper_nonce - message.id = this.next_message_id - this.next_message_id++ - this.target.postMessage(message, '*') - if (cb) { - this.waiting_cb[message.id] = cb - } - } - - log(...args) { - console.log.apply(console, ['[ZeroFrame]'].concat(args)) - } - - onOpenWebsocket() { - this.log('Websocket open') - } - - onCloseWebsocket() { - this.log('Websocket close') - } - - monkeyPatchAjax() { - var page = this - XMLHttpRequest.prototype.realOpen = XMLHttpRequest.prototype.open - this.cmd("wrapperGetAjaxKey", [], (res) => { this.ajax_key = res }) - var newOpen = function (method, url, async) { - url += "?ajax_key=" + page.ajax_key - return this.realOpen(method, url, async) - } - XMLHttpRequest.prototype.open = newOpen - } -} diff --git a/plugins/ContentFilter/plugin_info.json b/plugins/ContentFilter/plugin_info.json deleted file mode 100644 index f63bc984..00000000 --- a/plugins/ContentFilter/plugin_info.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "ContentFilter", - "description": "Manage site and user block list.", - "default": "enabled" -} \ No newline at end of file diff --git a/plugins/Cors/CorsPlugin.py b/plugins/Cors/CorsPlugin.py deleted file mode 100644 index c9437538..00000000 --- a/plugins/Cors/CorsPlugin.py +++ /dev/null @@ -1,139 +0,0 @@ -import re -import html -import copy -import os -import gevent - -from Plugin import PluginManager -from Translate import Translate - - -plugin_dir = os.path.dirname(__file__) - -if "_" not in locals(): - _ = Translate(plugin_dir + "/languages/") - - -def getCorsPath(site, inner_path): - match = re.match("^cors-([A-Za-z0-9]{26,35})/(.*)", inner_path) - if not match: - raise Exception("Invalid cors path: %s" % inner_path) - cors_address = match.group(1) - cors_inner_path = match.group(2) - - if not "Cors:%s" % cors_address in site.settings["permissions"]: - raise Exception("This site has no permission to access site %s" % cors_address) - - return cors_address, cors_inner_path - - -@PluginManager.registerTo("UiWebsocket") -class UiWebsocketPlugin(object): - def hasSitePermission(self, address, cmd=None): - if super(UiWebsocketPlugin, self).hasSitePermission(address, cmd=cmd): - return True - - allowed_commands = [ - "fileGet", "fileList", "dirList", "fileRules", "optionalFileInfo", - "fileQuery", "dbQuery", "userGetSettings", "siteInfo" - ] - if not "Cors:%s" % address in self.site.settings["permissions"] or cmd not in allowed_commands: - return False - else: - return True - - # Add cors support for file commands - def corsFuncWrapper(self, func_name, to, inner_path, *args, **kwargs): - if inner_path.startswith("cors-"): - cors_address, cors_inner_path = getCorsPath(self.site, inner_path) - - req_self = copy.copy(self) - req_self.site = self.server.sites.get(cors_address) # Change the site to the merged one - if not req_self.site: - return {"error": "No site found"} - - func = getattr(super(UiWebsocketPlugin, req_self), func_name) - back = func(to, cors_inner_path, *args, **kwargs) - return back - else: - func = getattr(super(UiWebsocketPlugin, self), func_name) - return func(to, inner_path, *args, **kwargs) - - def actionFileGet(self, to, inner_path, *args, **kwargs): - return self.corsFuncWrapper("actionFileGet", to, inner_path, *args, **kwargs) - - def actionFileList(self, to, inner_path, *args, **kwargs): - return self.corsFuncWrapper("actionFileList", to, inner_path, *args, **kwargs) - - def actionDirList(self, to, inner_path, *args, **kwargs): - return self.corsFuncWrapper("actionDirList", to, inner_path, *args, **kwargs) - - def actionFileRules(self, to, inner_path, *args, **kwargs): - return self.corsFuncWrapper("actionFileRules", to, inner_path, *args, **kwargs) - - def actionOptionalFileInfo(self, to, inner_path, *args, **kwargs): - return self.corsFuncWrapper("actionOptionalFileInfo", to, inner_path, *args, **kwargs) - - def actionCorsPermission(self, to, address): - if isinstance(address, list): - addresses = address - else: - addresses = [address] - - button_title = _["Grant"] - site_names = [] - site_addresses = [] - for address in addresses: - site = self.server.sites.get(address) - if site: - site_name = site.content_manager.contents.get("content.json", {}).get("title", address) - else: - site_name = address - # If at least one site is not downloaded yet, show "Grant & Add" instead - button_title = _["Grant & Add"] - - if not (site and "Cors:" + address in self.permissions): - # No site or no permission - site_names.append(site_name) - site_addresses.append(address) - - if len(site_names) == 0: - return "ignored" - - self.cmd( - "confirm", - [_["This site requests read permission to: %s"] % ", ".join(map(html.escape, site_names)), button_title], - lambda res: self.cbCorsPermission(to, site_addresses) - ) - - def cbCorsPermission(self, to, addresses): - # Add permissions - for address in addresses: - permission = "Cors:" + address - if permission not in self.site.settings["permissions"]: - self.site.settings["permissions"].append(permission) - - self.site.saveSettings() - self.site.updateWebsocket(permission_added=permission) - - self.response(to, "ok") - - for address in addresses: - site = self.server.sites.get(address) - if not site: - gevent.spawn(self.server.site_manager.need, address) - - -@PluginManager.registerTo("UiRequest") -class UiRequestPlugin(object): - # Allow to load cross origin files using /cors-address/file.jpg - def parsePath(self, path): - path_parts = super(UiRequestPlugin, self).parsePath(path) - if "cors-" not in path: # Optimization - return path_parts - site = self.server.sites[path_parts["address"]] - try: - path_parts["address"], path_parts["inner_path"] = getCorsPath(site, path_parts["inner_path"]) - except Exception: - return None - return path_parts diff --git a/plugins/Cors/__init__.py b/plugins/Cors/__init__.py deleted file mode 100644 index bcaa502b..00000000 --- a/plugins/Cors/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import CorsPlugin \ No newline at end of file diff --git a/plugins/Cors/plugin_info.json b/plugins/Cors/plugin_info.json deleted file mode 100644 index f8af18fa..00000000 --- a/plugins/Cors/plugin_info.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Cors", - "description": "Cross site resource read.", - "default": "enabled" -} \ No newline at end of file diff --git a/plugins/CryptMessage/CryptMessage.py b/plugins/CryptMessage/CryptMessage.py deleted file mode 100644 index 8349809c..00000000 --- a/plugins/CryptMessage/CryptMessage.py +++ /dev/null @@ -1,58 +0,0 @@ -import hashlib -import base64 -import struct -from lib import sslcrypto -from Crypt import Crypt - - -curve = sslcrypto.ecc.get_curve("secp256k1") - - -def eciesEncrypt(data, pubkey, ciphername="aes-256-cbc"): - ciphertext, key_e = curve.encrypt( - data, - base64.b64decode(pubkey), - algo=ciphername, - derivation="sha512", - return_aes_key=True - ) - return key_e, ciphertext - - -@Crypt.thread_pool_crypt.wrap -def eciesDecryptMulti(encrypted_datas, privatekey): - texts = [] # Decoded texts - for encrypted_data in encrypted_datas: - try: - text = eciesDecrypt(encrypted_data, privatekey).decode("utf8") - texts.append(text) - except Exception: - texts.append(None) - return texts - - -def eciesDecrypt(ciphertext, privatekey): - return curve.decrypt(base64.b64decode(ciphertext), curve.wif_to_private(privatekey.encode()), derivation="sha512") - - -def decodePubkey(pubkey): - i = 0 - curve = struct.unpack('!H', pubkey[i:i + 2])[0] - i += 2 - tmplen = struct.unpack('!H', pubkey[i:i + 2])[0] - i += 2 - pubkey_x = pubkey[i:i + tmplen] - i += tmplen - tmplen = struct.unpack('!H', pubkey[i:i + 2])[0] - i += 2 - pubkey_y = pubkey[i:i + tmplen] - i += tmplen - return curve, pubkey_x, pubkey_y, i - - -def split(encrypted): - iv = encrypted[0:16] - curve, pubkey_x, pubkey_y, i = decodePubkey(encrypted[16:]) - ciphertext = encrypted[16 + i:-32] - - return iv, ciphertext diff --git a/plugins/CryptMessage/CryptMessagePlugin.py b/plugins/CryptMessage/CryptMessagePlugin.py deleted file mode 100644 index 7c24f730..00000000 --- a/plugins/CryptMessage/CryptMessagePlugin.py +++ /dev/null @@ -1,225 +0,0 @@ -import base64 -import os - -import gevent - -from Plugin import PluginManager -from Crypt import CryptBitcoin, CryptHash -from Config import config -import sslcrypto - -from . import CryptMessage - -curve = sslcrypto.ecc.get_curve("secp256k1") - - -@PluginManager.registerTo("UiWebsocket") -class UiWebsocketPlugin(object): - # - Actions - - - # Returns user's public key unique to site - # Return: Public key - def actionUserPublickey(self, to, index=0): - self.response(to, self.user.getEncryptPublickey(self.site.address, index)) - - # Encrypt a text using the publickey or user's sites unique publickey - # Return: Encrypted text using base64 encoding - def actionEciesEncrypt(self, to, text, publickey=0, return_aes_key=False): - if type(publickey) is int: # Encrypt using user's publickey - publickey = self.user.getEncryptPublickey(self.site.address, publickey) - aes_key, encrypted = CryptMessage.eciesEncrypt(text.encode("utf8"), publickey) - if return_aes_key: - self.response(to, [base64.b64encode(encrypted).decode("utf8"), base64.b64encode(aes_key).decode("utf8")]) - else: - self.response(to, base64.b64encode(encrypted).decode("utf8")) - - # Decrypt a text using privatekey or the user's site unique private key - # Return: Decrypted text or list of decrypted texts - def actionEciesDecrypt(self, to, param, privatekey=0): - if type(privatekey) is int: # Decrypt using user's privatekey - privatekey = self.user.getEncryptPrivatekey(self.site.address, privatekey) - - if type(param) == list: - encrypted_texts = param - else: - encrypted_texts = [param] - - texts = CryptMessage.eciesDecryptMulti(encrypted_texts, privatekey) - - if type(param) == list: - self.response(to, texts) - else: - self.response(to, texts[0]) - - # Encrypt a text using AES - # Return: Iv, AES key, Encrypted text - def actionAesEncrypt(self, to, text, key=None): - if key: - key = base64.b64decode(key) - else: - key = sslcrypto.aes.new_key() - - if text: - encrypted, iv = sslcrypto.aes.encrypt(text.encode("utf8"), key) - else: - encrypted, iv = b"", b"" - - res = [base64.b64encode(item).decode("utf8") for item in [key, iv, encrypted]] - self.response(to, res) - - # Decrypt a text using AES - # Return: Decrypted text - def actionAesDecrypt(self, to, *args): - if len(args) == 3: # Single decrypt - encrypted_texts = [(args[0], args[1])] - keys = [args[2]] - else: # Batch decrypt - encrypted_texts, keys = args - - texts = [] # Decoded texts - for iv, encrypted_text in encrypted_texts: - encrypted_text = base64.b64decode(encrypted_text) - iv = base64.b64decode(iv) - text = None - for key in keys: - try: - decrypted = sslcrypto.aes.decrypt(encrypted_text, iv, base64.b64decode(key)) - if decrypted and decrypted.decode("utf8"): # Valid text decoded - text = decrypted.decode("utf8") - except Exception as err: - pass - texts.append(text) - - if len(args) == 3: - self.response(to, texts[0]) - else: - self.response(to, texts) - - # Sign data using ECDSA - # Return: Signature - def actionEcdsaSign(self, to, data, privatekey=None): - if privatekey is None: # Sign using user's privatekey - privatekey = self.user.getAuthPrivatekey(self.site.address) - - self.response(to, CryptBitcoin.sign(data, privatekey)) - - # Verify data using ECDSA (address is either a address or array of addresses) - # Return: bool - def actionEcdsaVerify(self, to, data, address, signature): - self.response(to, CryptBitcoin.verify(data, address, signature)) - - # Gets the publickey of a given privatekey - def actionEccPrivToPub(self, to, privatekey): - self.response(to, curve.private_to_public(curve.wif_to_private(privatekey.encode()))) - - # Gets the address of a given publickey - def actionEccPubToAddr(self, to, publickey): - self.response(to, curve.public_to_address(bytes.fromhex(publickey))) - - -@PluginManager.registerTo("User") -class UserPlugin(object): - def getEncryptPrivatekey(self, address, param_index=0): - if param_index < 0 or param_index > 1000: - raise Exception("Param_index out of range") - - site_data = self.getSiteData(address) - - if site_data.get("cert"): # Different privatekey for different cert provider - index = param_index + self.getAddressAuthIndex(site_data["cert"]) - else: - index = param_index - - if "encrypt_privatekey_%s" % index not in site_data: - address_index = self.getAddressAuthIndex(address) - crypt_index = address_index + 1000 + index - site_data["encrypt_privatekey_%s" % index] = CryptBitcoin.hdPrivatekey(self.master_seed, crypt_index) - self.log.debug("New encrypt privatekey generated for %s:%s" % (address, index)) - return site_data["encrypt_privatekey_%s" % index] - - def getEncryptPublickey(self, address, param_index=0): - if param_index < 0 or param_index > 1000: - raise Exception("Param_index out of range") - - site_data = self.getSiteData(address) - - if site_data.get("cert"): # Different privatekey for different cert provider - index = param_index + self.getAddressAuthIndex(site_data["cert"]) - else: - index = param_index - - if "encrypt_publickey_%s" % index not in site_data: - privatekey = self.getEncryptPrivatekey(address, param_index).encode() - publickey = curve.private_to_public(curve.wif_to_private(privatekey) + b"\x01") - site_data["encrypt_publickey_%s" % index] = base64.b64encode(publickey).decode("utf8") - return site_data["encrypt_publickey_%s" % index] - - -@PluginManager.registerTo("Actions") -class ActionsPlugin: - publickey = "A3HatibU4S6eZfIQhVs2u7GLN5G9wXa9WwlkyYIfwYaj" - privatekey = "5JBiKFYBm94EUdbxtnuLi6cvNcPzcKymCUHBDf2B6aq19vvG3rL" - utf8_text = '\xc1rv\xedzt\xfbr\xf5t\xfck\xf6rf\xfar\xf3g\xe9p' - - def getBenchmarkTests(self, online=False): - if hasattr(super(), "getBenchmarkTests"): - tests = super().getBenchmarkTests(online) - else: - tests = [] - - aes_key, encrypted = CryptMessage.eciesEncrypt(self.utf8_text.encode("utf8"), self.publickey) # Warm-up - tests.extend([ - {"func": self.testCryptEciesEncrypt, "kwargs": {}, "num": 100, "time_standard": 1.2}, - {"func": self.testCryptEciesDecrypt, "kwargs": {}, "num": 500, "time_standard": 1.3}, - {"func": self.testCryptEciesDecryptMulti, "kwargs": {}, "num": 5, "time_standard": 0.68}, - {"func": self.testCryptAesEncrypt, "kwargs": {}, "num": 10000, "time_standard": 0.27}, - {"func": self.testCryptAesDecrypt, "kwargs": {}, "num": 10000, "time_standard": 0.25} - ]) - return tests - - def testCryptEciesEncrypt(self, num_run=1): - for i in range(num_run): - aes_key, encrypted = CryptMessage.eciesEncrypt(self.utf8_text.encode("utf8"), self.publickey) - assert len(aes_key) == 32 - yield "." - - def testCryptEciesDecrypt(self, num_run=1): - aes_key, encrypted = CryptMessage.eciesEncrypt(self.utf8_text.encode("utf8"), self.publickey) - for i in range(num_run): - assert len(aes_key) == 32 - decrypted = CryptMessage.eciesDecrypt(base64.b64encode(encrypted), self.privatekey) - assert decrypted == self.utf8_text.encode("utf8"), "%s != %s" % (decrypted, self.utf8_text.encode("utf8")) - yield "." - - def testCryptEciesDecryptMulti(self, num_run=1): - yield "x 100 (%s threads) " % config.threads_crypt - aes_key, encrypted = CryptMessage.eciesEncrypt(self.utf8_text.encode("utf8"), self.publickey) - - threads = [] - for i in range(num_run): - assert len(aes_key) == 32 - threads.append(gevent.spawn( - CryptMessage.eciesDecryptMulti, [base64.b64encode(encrypted)] * 100, self.privatekey - )) - - for thread in threads: - res = thread.get() - assert res[0] == self.utf8_text, "%s != %s" % (res[0], self.utf8_text) - assert res[0] == res[-1], "%s != %s" % (res[0], res[-1]) - yield "." - gevent.joinall(threads) - - def testCryptAesEncrypt(self, num_run=1): - for i in range(num_run): - key = os.urandom(32) - encrypted = sslcrypto.aes.encrypt(self.utf8_text.encode("utf8"), key) - yield "." - - def testCryptAesDecrypt(self, num_run=1): - key = os.urandom(32) - encrypted_text, iv = sslcrypto.aes.encrypt(self.utf8_text.encode("utf8"), key) - - for i in range(num_run): - decrypted = sslcrypto.aes.decrypt(encrypted_text, iv, key).decode("utf8") - assert decrypted == self.utf8_text - yield "." diff --git a/plugins/CryptMessage/Test/TestCrypt.py b/plugins/CryptMessage/Test/TestCrypt.py deleted file mode 100644 index 25a077d8..00000000 --- a/plugins/CryptMessage/Test/TestCrypt.py +++ /dev/null @@ -1,136 +0,0 @@ -import pytest -import base64 -from CryptMessage import CryptMessage - - -@pytest.mark.usefixtures("resetSettings") -class TestCrypt: - publickey = "A3HatibU4S6eZfIQhVs2u7GLN5G9wXa9WwlkyYIfwYaj" - privatekey = "5JBiKFYBm94EUdbxtnuLi6cvNcPzcKymCUHBDf2B6aq19vvG3rL" - utf8_text = '\xc1rv\xedzt\xfbr\xf5t\xfck\xf6rf\xfar\xf3g\xe9' - ecies_encrypted_text = "R5J1RFIDOzE5bnWopvccmALKACCk/CRcd/KSE9OgExJKASyMbZ57JVSUenL2TpABMmcT+wAgr2UrOqClxpOWvIUwvwwupXnMbRTzthhIJJrTRW3sCJVaYlGEMn9DAcvbflgEkQX/MVVdLV3tWKySs1Vk8sJC/y+4pGYCrZz7vwDNEEERaqU=" - - @pytest.mark.parametrize("text", [b"hello", '\xc1rv\xedzt\xfbr\xf5t\xfck\xf6rf\xfar\xf3g\xe9'.encode("utf8")]) - @pytest.mark.parametrize("text_repeat", [1, 10, 128, 1024]) - def testEncryptEcies(self, text, text_repeat): - text_repeated = text * text_repeat - aes_key, encrypted = CryptMessage.eciesEncrypt(text_repeated, self.publickey) - assert len(aes_key) == 32 - # assert len(encrypted) == 134 + int(len(text) / 16) * 16 # Not always true - - assert CryptMessage.eciesDecrypt(base64.b64encode(encrypted), self.privatekey) == text_repeated - - def testDecryptEcies(self, user): - assert CryptMessage.eciesDecrypt(self.ecies_encrypted_text, self.privatekey) == b"hello" - - def testPublickey(self, ui_websocket): - pub = ui_websocket.testAction("UserPublickey", 0) - assert len(pub) == 44 # Compressed, b64 encoded publickey - - # Different pubkey for specificed index - assert ui_websocket.testAction("UserPublickey", 1) != ui_websocket.testAction("UserPublickey", 0) - - # Same publickey for same index - assert ui_websocket.testAction("UserPublickey", 2) == ui_websocket.testAction("UserPublickey", 2) - - # Different publickey for different cert - site_data = ui_websocket.user.getSiteData(ui_websocket.site.address) - site_data["cert"] = None - pub1 = ui_websocket.testAction("UserPublickey", 0) - - site_data = ui_websocket.user.getSiteData(ui_websocket.site.address) - site_data["cert"] = "zeroid.bit" - pub2 = ui_websocket.testAction("UserPublickey", 0) - assert pub1 != pub2 - - def testEcies(self, ui_websocket): - pub = ui_websocket.testAction("UserPublickey") - - encrypted = ui_websocket.testAction("EciesEncrypt", "hello", pub) - assert len(encrypted) == 180 - - # Don't allow decrypt using other privatekey index - decrypted = ui_websocket.testAction("EciesDecrypt", encrypted, 123) - assert decrypted != "hello" - - # Decrypt using correct privatekey - decrypted = ui_websocket.testAction("EciesDecrypt", encrypted) - assert decrypted == "hello" - - # Decrypt incorrect text - decrypted = ui_websocket.testAction("EciesDecrypt", "baad") - assert decrypted is None - - # Decrypt batch - decrypted = ui_websocket.testAction("EciesDecrypt", [encrypted, "baad", encrypted]) - assert decrypted == ["hello", None, "hello"] - - def testEciesUtf8(self, ui_websocket): - # Utf8 test - ui_websocket.actionEciesEncrypt(0, self.utf8_text) - encrypted = ui_websocket.ws.getResult() - - ui_websocket.actionEciesDecrypt(0, encrypted) - assert ui_websocket.ws.getResult() == self.utf8_text - - def testEciesAes(self, ui_websocket): - ui_websocket.actionEciesEncrypt(0, "hello", return_aes_key=True) - ecies_encrypted, aes_key = ui_websocket.ws.getResult() - - # Decrypt using Ecies - ui_websocket.actionEciesDecrypt(0, ecies_encrypted) - assert ui_websocket.ws.getResult() == "hello" - - # Decrypt using AES - aes_iv, aes_encrypted = CryptMessage.split(base64.b64decode(ecies_encrypted)) - - ui_websocket.actionAesDecrypt(0, base64.b64encode(aes_iv), base64.b64encode(aes_encrypted), aes_key) - assert ui_websocket.ws.getResult() == "hello" - - def testEciesAesLongpubkey(self, ui_websocket): - privatekey = "5HwVS1bTFnveNk9EeGaRenWS1QFzLFb5kuncNbiY3RiHZrVR6ok" - - ecies_encrypted, aes_key = ["lWiXfEikIjw1ac3J/RaY/gLKACALRUfksc9rXYRFyKDSaxhwcSFBYCgAdIyYlY294g/6VgAf/68PYBVMD3xKH1n7Zbo+ge8b4i/XTKmCZRJvy0eutMKWckYCMVcxgIYNa/ZL1BY1kvvH7omgzg1wBraoLfdbNmVtQgdAZ9XS8PwRy6OB2Q==", "Rvlf7zsMuBFHZIGHcbT1rb4If+YTmsWDv6kGwcvSeMM="] - - # Decrypt using Ecies - ui_websocket.actionEciesDecrypt(0, ecies_encrypted, privatekey) - assert ui_websocket.ws.getResult() == "hello" - - # Decrypt using AES - aes_iv, aes_encrypted = CryptMessage.split(base64.b64decode(ecies_encrypted)) - - ui_websocket.actionAesDecrypt(0, base64.b64encode(aes_iv), base64.b64encode(aes_encrypted), aes_key) - assert ui_websocket.ws.getResult() == "hello" - - def testAes(self, ui_websocket): - ui_websocket.actionAesEncrypt(0, "hello") - key, iv, encrypted = ui_websocket.ws.getResult() - - assert len(key) == 44 - assert len(iv) == 24 - assert len(encrypted) == 24 - - # Single decrypt - ui_websocket.actionAesDecrypt(0, iv, encrypted, key) - assert ui_websocket.ws.getResult() == "hello" - - # Batch decrypt - ui_websocket.actionAesEncrypt(0, "hello") - key2, iv2, encrypted2 = ui_websocket.ws.getResult() - - assert [key, iv, encrypted] != [key2, iv2, encrypted2] - - # 2 correct key - ui_websocket.actionAesDecrypt(0, [[iv, encrypted], [iv, encrypted], [iv, "baad"], [iv2, encrypted2]], [key]) - assert ui_websocket.ws.getResult() == ["hello", "hello", None, None] - - # 3 key - ui_websocket.actionAesDecrypt(0, [[iv, encrypted], [iv, encrypted], [iv, "baad"], [iv2, encrypted2]], [key, key2]) - assert ui_websocket.ws.getResult() == ["hello", "hello", None, "hello"] - - def testAesUtf8(self, ui_websocket): - ui_websocket.actionAesEncrypt(0, self.utf8_text) - key, iv, encrypted = ui_websocket.ws.getResult() - - ui_websocket.actionAesDecrypt(0, iv, encrypted, key) - assert ui_websocket.ws.getResult() == self.utf8_text diff --git a/plugins/CryptMessage/Test/conftest.py b/plugins/CryptMessage/Test/conftest.py deleted file mode 100644 index 8c1df5b2..00000000 --- a/plugins/CryptMessage/Test/conftest.py +++ /dev/null @@ -1 +0,0 @@ -from src.Test.conftest import * \ No newline at end of file diff --git a/plugins/CryptMessage/Test/pytest.ini b/plugins/CryptMessage/Test/pytest.ini deleted file mode 100644 index d09210d1..00000000 --- a/plugins/CryptMessage/Test/pytest.ini +++ /dev/null @@ -1,5 +0,0 @@ -[pytest] -python_files = Test*.py -addopts = -rsxX -v --durations=6 -markers = - webtest: mark a test as a webtest. \ No newline at end of file diff --git a/plugins/CryptMessage/__init__.py b/plugins/CryptMessage/__init__.py deleted file mode 100644 index 6aeb4e52..00000000 --- a/plugins/CryptMessage/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import CryptMessagePlugin \ No newline at end of file diff --git a/plugins/CryptMessage/plugin_info.json b/plugins/CryptMessage/plugin_info.json deleted file mode 100644 index 96dfdd89..00000000 --- a/plugins/CryptMessage/plugin_info.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "CryptMessage", - "description": "Cryptographic functions of ECIES and AES data encryption/decryption.", - "default": "enabled" -} \ No newline at end of file diff --git a/plugins/FilePack/FilePackPlugin.py b/plugins/FilePack/FilePackPlugin.py deleted file mode 100644 index a095c6d4..00000000 --- a/plugins/FilePack/FilePackPlugin.py +++ /dev/null @@ -1,193 +0,0 @@ -import os -import re - -import gevent - -from Plugin import PluginManager -from Config import config -from Debug import Debug - -# Keep archive open for faster reponse times for large sites -archive_cache = {} - - -def closeArchive(archive_path): - if archive_path in archive_cache: - del archive_cache[archive_path] - - -def openArchive(archive_path, file_obj=None): - if archive_path not in archive_cache: - if archive_path.endswith("tar.gz"): - import tarfile - archive_cache[archive_path] = tarfile.open(archive_path, fileobj=file_obj, mode="r:gz") - else: - import zipfile - archive_cache[archive_path] = zipfile.ZipFile(file_obj or archive_path) - gevent.spawn_later(5, lambda: closeArchive(archive_path)) # Close after 5 sec - - archive = archive_cache[archive_path] - return archive - - -def openArchiveFile(archive_path, path_within, file_obj=None): - archive = openArchive(archive_path, file_obj=file_obj) - if archive_path.endswith(".zip"): - return archive.open(path_within) - else: - return archive.extractfile(path_within) - - -@PluginManager.registerTo("UiRequest") -class UiRequestPlugin(object): - def actionSiteMedia(self, path, **kwargs): - if ".zip/" in path or ".tar.gz/" in path: - file_obj = None - path_parts = self.parsePath(path) - file_path = "%s/%s/%s" % (config.data_dir, path_parts["address"], path_parts["inner_path"]) - match = re.match("^(.*\.(?:tar.gz|zip))/(.*)", file_path) - archive_path, path_within = match.groups() - if archive_path not in archive_cache: - site = self.server.site_manager.get(path_parts["address"]) - if not site: - return self.actionSiteAddPrompt(path) - archive_inner_path = site.storage.getInnerPath(archive_path) - if not os.path.isfile(archive_path): - # Wait until file downloads - result = site.needFile(archive_inner_path, priority=10) - # Send virutal file path download finished event to remove loading screen - site.updateWebsocket(file_done=archive_inner_path) - if not result: - return self.error404(archive_inner_path) - file_obj = site.storage.openBigfile(archive_inner_path) - if file_obj == False: - file_obj = None - - header_allow_ajax = False - if self.get.get("ajax_key"): - requester_site = self.server.site_manager.get(path_parts["request_address"]) - if self.get["ajax_key"] == requester_site.settings["ajax_key"]: - header_allow_ajax = True - else: - return self.error403("Invalid ajax_key") - - try: - file = openArchiveFile(archive_path, path_within, file_obj=file_obj) - content_type = self.getContentType(file_path) - self.sendHeader(200, content_type=content_type, noscript=kwargs.get("header_noscript", False), allow_ajax=header_allow_ajax) - return self.streamFile(file) - except Exception as err: - self.log.debug("Error opening archive file: %s" % Debug.formatException(err)) - return self.error404(path) - - return super(UiRequestPlugin, self).actionSiteMedia(path, **kwargs) - - def streamFile(self, file): - for i in range(100): # Read max 6MB - try: - block = file.read(60 * 1024) - if block: - yield block - else: - raise StopIteration - except StopIteration: - file.close() - break - - -@PluginManager.registerTo("SiteStorage") -class SiteStoragePlugin(object): - def isFile(self, inner_path): - if ".zip/" in inner_path or ".tar.gz/" in inner_path: - match = re.match("^(.*\.(?:tar.gz|zip))/(.*)", inner_path) - archive_inner_path, path_within = match.groups() - return super(SiteStoragePlugin, self).isFile(archive_inner_path) - else: - return super(SiteStoragePlugin, self).isFile(inner_path) - - def openArchive(self, inner_path): - archive_path = self.getPath(inner_path) - file_obj = None - if archive_path not in archive_cache: - if not os.path.isfile(archive_path): - result = self.site.needFile(inner_path, priority=10) - self.site.updateWebsocket(file_done=inner_path) - if not result: - raise Exception("Unable to download file") - file_obj = self.site.storage.openBigfile(inner_path) - if file_obj == False: - file_obj = None - - try: - archive = openArchive(archive_path, file_obj=file_obj) - except Exception as err: - raise Exception("Unable to download file: %s" % Debug.formatException(err)) - - return archive - - def walk(self, inner_path, *args, **kwags): - if ".zip" in inner_path or ".tar.gz" in inner_path: - match = re.match("^(.*\.(?:tar.gz|zip))(.*)", inner_path) - archive_inner_path, path_within = match.groups() - archive = self.openArchive(archive_inner_path) - path_within = path_within.lstrip("/") - - if archive_inner_path.endswith(".zip"): - namelist = [name for name in archive.namelist() if not name.endswith("/")] - else: - namelist = [item.name for item in archive.getmembers() if not item.isdir()] - - namelist_relative = [] - for name in namelist: - if not name.startswith(path_within): - continue - name_relative = name.replace(path_within, "", 1).rstrip("/") - namelist_relative.append(name_relative) - - return namelist_relative - - else: - return super(SiteStoragePlugin, self).walk(inner_path, *args, **kwags) - - def list(self, inner_path, *args, **kwags): - if ".zip" in inner_path or ".tar.gz" in inner_path: - match = re.match("^(.*\.(?:tar.gz|zip))(.*)", inner_path) - archive_inner_path, path_within = match.groups() - archive = self.openArchive(archive_inner_path) - path_within = path_within.lstrip("/") - - if archive_inner_path.endswith(".zip"): - namelist = [name for name in archive.namelist()] - else: - namelist = [item.name for item in archive.getmembers()] - - namelist_relative = [] - for name in namelist: - if not name.startswith(path_within): - continue - name_relative = name.replace(path_within, "", 1).rstrip("/") - - if "/" in name_relative: # File is in sub-directory - continue - - namelist_relative.append(name_relative) - return namelist_relative - - else: - return super(SiteStoragePlugin, self).list(inner_path, *args, **kwags) - - def read(self, inner_path, mode="rb", **kwargs): - if ".zip/" in inner_path or ".tar.gz/" in inner_path: - match = re.match("^(.*\.(?:tar.gz|zip))(.*)", inner_path) - archive_inner_path, path_within = match.groups() - archive = self.openArchive(archive_inner_path) - path_within = path_within.lstrip("/") - - if archive_inner_path.endswith(".zip"): - return archive.open(path_within).read() - else: - return archive.extractfile(path_within).read() - - else: - return super(SiteStoragePlugin, self).read(inner_path, mode, **kwargs) - diff --git a/plugins/FilePack/__init__.py b/plugins/FilePack/__init__.py deleted file mode 100644 index 660a0920..00000000 --- a/plugins/FilePack/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import FilePackPlugin \ No newline at end of file diff --git a/plugins/FilePack/plugin_info.json b/plugins/FilePack/plugin_info.json deleted file mode 100644 index 42112f95..00000000 --- a/plugins/FilePack/plugin_info.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "FilePack", - "description": "Transparent web access for Zip and Tar.gz files.", - "default": "enabled" -} \ No newline at end of file diff --git a/plugins/MergerSite/MergerSitePlugin.py b/plugins/MergerSite/MergerSitePlugin.py deleted file mode 100644 index 2dccc6de..00000000 --- a/plugins/MergerSite/MergerSitePlugin.py +++ /dev/null @@ -1,399 +0,0 @@ -import re -import time -import copy -import os - -from Plugin import PluginManager -from Translate import Translate -from util import RateLimit -from util import helper -from util.Flag import flag -from Debug import Debug -try: - import OptionalManager.UiWebsocketPlugin # To make optioanlFileInfo merger sites compatible -except Exception: - pass - -if "merger_db" not in locals().keys(): # To keep merger_sites between module reloads - merger_db = {} # Sites that allowed to list other sites {address: [type1, type2...]} - merged_db = {} # Sites that allowed to be merged to other sites {address: type, ...} - merged_to_merger = {} # {address: [site1, site2, ...]} cache - site_manager = None # Site manager for merger sites - - -plugin_dir = os.path.dirname(__file__) - -if "_" not in locals(): - _ = Translate(plugin_dir + "/languages/") - - -# Check if the site has permission to this merger site -def checkMergerPath(address, inner_path): - merged_match = re.match("^merged-(.*?)/([A-Za-z0-9]{26,35})/", inner_path) - if merged_match: - merger_type = merged_match.group(1) - # Check if merged site is allowed to include other sites - if merger_type in merger_db.get(address, []): - # Check if included site allows to include - merged_address = merged_match.group(2) - if merged_db.get(merged_address) == merger_type: - inner_path = re.sub("^merged-(.*?)/([A-Za-z0-9]{26,35})/", "", inner_path) - return merged_address, inner_path - else: - raise Exception( - "Merger site (%s) does not have permission for merged site: %s (%s)" % - (merger_type, merged_address, merged_db.get(merged_address)) - ) - else: - raise Exception("No merger (%s) permission to load:
      %s (%s not in %s)" % ( - address, inner_path, merger_type, merger_db.get(address, [])) - ) - else: - raise Exception("Invalid merger path: %s" % inner_path) - - -@PluginManager.registerTo("UiWebsocket") -class UiWebsocketPlugin(object): - # Download new site - def actionMergerSiteAdd(self, to, addresses): - if type(addresses) != list: - # Single site add - addresses = [addresses] - # Check if the site has merger permission - merger_types = merger_db.get(self.site.address) - if not merger_types: - return self.response(to, {"error": "Not a merger site"}) - - if RateLimit.isAllowed(self.site.address + "-MergerSiteAdd", 10) and len(addresses) == 1: - # Without confirmation if only one site address and not called in last 10 sec - self.cbMergerSiteAdd(to, addresses) - else: - self.cmd( - "confirm", - [_["Add %s new site?"] % len(addresses), "Add"], - lambda res: self.cbMergerSiteAdd(to, addresses) - ) - self.response(to, "ok") - - # Callback of adding new site confirmation - def cbMergerSiteAdd(self, to, addresses): - added = 0 - for address in addresses: - try: - site_manager.need(address) - added += 1 - except Exception as err: - self.cmd("notification", ["error", _["Adding %s failed: %s"] % (address, err)]) - if added: - self.cmd("notification", ["done", _["Added %s new site"] % added, 5000]) - RateLimit.called(self.site.address + "-MergerSiteAdd") - site_manager.updateMergerSites() - - # Delete a merged site - @flag.no_multiuser - def actionMergerSiteDelete(self, to, address): - site = self.server.sites.get(address) - if not site: - return self.response(to, {"error": "No site found: %s" % address}) - - merger_types = merger_db.get(self.site.address) - if not merger_types: - return self.response(to, {"error": "Not a merger site"}) - if merged_db.get(address) not in merger_types: - return self.response(to, {"error": "Merged type (%s) not in %s" % (merged_db.get(address), merger_types)}) - - self.cmd("notification", ["done", _["Site deleted: %s"] % address, 5000]) - self.response(to, "ok") - - # Lists merged sites - def actionMergerSiteList(self, to, query_site_info=False): - merger_types = merger_db.get(self.site.address) - ret = {} - if not merger_types: - return self.response(to, {"error": "Not a merger site"}) - for address, merged_type in merged_db.items(): - if merged_type not in merger_types: - continue # Site not for us - if query_site_info: - site = self.server.sites.get(address) - ret[address] = self.formatSiteInfo(site, create_user=False) - else: - ret[address] = merged_type - self.response(to, ret) - - def hasSitePermission(self, address, *args, **kwargs): - if super(UiWebsocketPlugin, self).hasSitePermission(address, *args, **kwargs): - return True - else: - if self.site.address in [merger_site.address for merger_site in merged_to_merger.get(address, [])]: - return True - else: - return False - - # Add support merger sites for file commands - def mergerFuncWrapper(self, func_name, to, inner_path, *args, **kwargs): - if inner_path.startswith("merged-"): - merged_address, merged_inner_path = checkMergerPath(self.site.address, inner_path) - - # Set the same cert for merged site - merger_cert = self.user.getSiteData(self.site.address).get("cert") - if merger_cert and self.user.getSiteData(merged_address).get("cert") != merger_cert: - self.user.setCert(merged_address, merger_cert) - - req_self = copy.copy(self) - req_self.site = self.server.sites.get(merged_address) # Change the site to the merged one - - func = getattr(super(UiWebsocketPlugin, req_self), func_name) - return func(to, merged_inner_path, *args, **kwargs) - else: - func = getattr(super(UiWebsocketPlugin, self), func_name) - return func(to, inner_path, *args, **kwargs) - - def actionFileList(self, to, inner_path, *args, **kwargs): - return self.mergerFuncWrapper("actionFileList", to, inner_path, *args, **kwargs) - - def actionDirList(self, to, inner_path, *args, **kwargs): - return self.mergerFuncWrapper("actionDirList", to, inner_path, *args, **kwargs) - - def actionFileGet(self, to, inner_path, *args, **kwargs): - return self.mergerFuncWrapper("actionFileGet", to, inner_path, *args, **kwargs) - - def actionFileWrite(self, to, inner_path, *args, **kwargs): - return self.mergerFuncWrapper("actionFileWrite", to, inner_path, *args, **kwargs) - - def actionFileDelete(self, to, inner_path, *args, **kwargs): - return self.mergerFuncWrapper("actionFileDelete", to, inner_path, *args, **kwargs) - - def actionFileRules(self, to, inner_path, *args, **kwargs): - return self.mergerFuncWrapper("actionFileRules", to, inner_path, *args, **kwargs) - - def actionFileNeed(self, to, inner_path, *args, **kwargs): - return self.mergerFuncWrapper("actionFileNeed", to, inner_path, *args, **kwargs) - - def actionOptionalFileInfo(self, to, inner_path, *args, **kwargs): - return self.mergerFuncWrapper("actionOptionalFileInfo", to, inner_path, *args, **kwargs) - - def actionOptionalFileDelete(self, to, inner_path, *args, **kwargs): - return self.mergerFuncWrapper("actionOptionalFileDelete", to, inner_path, *args, **kwargs) - - def actionBigfileUploadInit(self, to, inner_path, *args, **kwargs): - back = self.mergerFuncWrapper("actionBigfileUploadInit", to, inner_path, *args, **kwargs) - if inner_path.startswith("merged-"): - merged_address, merged_inner_path = checkMergerPath(self.site.address, inner_path) - back["inner_path"] = "merged-%s/%s/%s" % (merged_db[merged_address], merged_address, back["inner_path"]) - return back - - # Add support merger sites for file commands with privatekey parameter - def mergerFuncWrapperWithPrivatekey(self, func_name, to, privatekey, inner_path, *args, **kwargs): - func = getattr(super(UiWebsocketPlugin, self), func_name) - if inner_path.startswith("merged-"): - merged_address, merged_inner_path = checkMergerPath(self.site.address, inner_path) - merged_site = self.server.sites.get(merged_address) - - # Set the same cert for merged site - merger_cert = self.user.getSiteData(self.site.address).get("cert") - if merger_cert: - self.user.setCert(merged_address, merger_cert) - - site_before = self.site # Save to be able to change it back after we ran the command - self.site = merged_site # Change the site to the merged one - try: - back = func(to, privatekey, merged_inner_path, *args, **kwargs) - finally: - self.site = site_before # Change back to original site - return back - else: - return func(to, privatekey, inner_path, *args, **kwargs) - - def actionSiteSign(self, to, privatekey=None, inner_path="content.json", *args, **kwargs): - return self.mergerFuncWrapperWithPrivatekey("actionSiteSign", to, privatekey, inner_path, *args, **kwargs) - - def actionSitePublish(self, to, privatekey=None, inner_path="content.json", *args, **kwargs): - return self.mergerFuncWrapperWithPrivatekey("actionSitePublish", to, privatekey, inner_path, *args, **kwargs) - - def actionPermissionAdd(self, to, permission): - super(UiWebsocketPlugin, self).actionPermissionAdd(to, permission) - if permission.startswith("Merger"): - self.site.storage.rebuildDb() - - def actionPermissionDetails(self, to, permission): - if not permission.startswith("Merger"): - return super(UiWebsocketPlugin, self).actionPermissionDetails(to, permission) - - merger_type = permission.replace("Merger:", "") - if not re.match("^[A-Za-z0-9-]+$", merger_type): - raise Exception("Invalid merger_type: %s" % merger_type) - merged_sites = [] - for address, merged_type in merged_db.items(): - if merged_type != merger_type: - continue - site = self.server.sites.get(address) - try: - merged_sites.append(site.content_manager.contents.get("content.json").get("title", address)) - except Exception: - merged_sites.append(address) - - details = _["Read and write permissions to sites with merged type of %s "] % merger_type - details += _["(%s sites)"] % len(merged_sites) - details += "
      %s
      " % ", ".join(merged_sites) - self.response(to, details) - - -@PluginManager.registerTo("UiRequest") -class UiRequestPlugin(object): - # Allow to load merged site files using /merged-ZeroMe/address/file.jpg - def parsePath(self, path): - path_parts = super(UiRequestPlugin, self).parsePath(path) - if "merged-" not in path: # Optimization - return path_parts - path_parts["address"], path_parts["inner_path"] = checkMergerPath(path_parts["address"], path_parts["inner_path"]) - return path_parts - - -@PluginManager.registerTo("SiteStorage") -class SiteStoragePlugin(object): - # Also rebuild from merged sites - def getDbFiles(self): - merger_types = merger_db.get(self.site.address) - - # First return the site's own db files - for item in super(SiteStoragePlugin, self).getDbFiles(): - yield item - - # Not a merger site, that's all - if not merger_types: - return - - merged_sites = [ - site_manager.sites[address] - for address, merged_type in merged_db.items() - if merged_type in merger_types - ] - found = 0 - for merged_site in merged_sites: - self.log.debug("Loading merged site: %s" % merged_site) - merged_type = merged_db[merged_site.address] - for content_inner_path, content in merged_site.content_manager.contents.items(): - # content.json file itself - if merged_site.storage.isFile(content_inner_path): # Missing content.json file - merged_inner_path = "merged-%s/%s/%s" % (merged_type, merged_site.address, content_inner_path) - yield merged_inner_path, merged_site.storage.getPath(content_inner_path) - else: - merged_site.log.error("[MISSING] %s" % content_inner_path) - # Data files in content.json - content_inner_path_dir = helper.getDirname(content_inner_path) # Content.json dir relative to site - for file_relative_path in list(content.get("files", {}).keys()) + list(content.get("files_optional", {}).keys()): - if not file_relative_path.endswith(".json"): - continue # We only interesed in json files - file_inner_path = content_inner_path_dir + file_relative_path # File Relative to site dir - file_inner_path = file_inner_path.strip("/") # Strip leading / - if merged_site.storage.isFile(file_inner_path): - merged_inner_path = "merged-%s/%s/%s" % (merged_type, merged_site.address, file_inner_path) - yield merged_inner_path, merged_site.storage.getPath(file_inner_path) - else: - merged_site.log.error("[MISSING] %s" % file_inner_path) - found += 1 - if found % 100 == 0: - time.sleep(0.001) # Context switch to avoid UI block - - # Also notice merger sites on a merged site file change - def onUpdated(self, inner_path, file=None): - super(SiteStoragePlugin, self).onUpdated(inner_path, file) - - merged_type = merged_db.get(self.site.address) - - for merger_site in merged_to_merger.get(self.site.address, []): - if merger_site.address == self.site.address: # Avoid infinite loop - continue - virtual_path = "merged-%s/%s/%s" % (merged_type, self.site.address, inner_path) - if inner_path.endswith(".json"): - if file is not None: - merger_site.storage.onUpdated(virtual_path, file=file) - else: - merger_site.storage.onUpdated(virtual_path, file=self.open(inner_path)) - else: - merger_site.storage.onUpdated(virtual_path) - - -@PluginManager.registerTo("Site") -class SitePlugin(object): - def fileDone(self, inner_path): - super(SitePlugin, self).fileDone(inner_path) - - for merger_site in merged_to_merger.get(self.address, []): - if merger_site.address == self.address: - continue - for ws in merger_site.websockets: - ws.event("siteChanged", self, {"event": ["file_done", inner_path]}) - - def fileFailed(self, inner_path): - super(SitePlugin, self).fileFailed(inner_path) - - for merger_site in merged_to_merger.get(self.address, []): - if merger_site.address == self.address: - continue - for ws in merger_site.websockets: - ws.event("siteChanged", self, {"event": ["file_failed", inner_path]}) - - -@PluginManager.registerTo("SiteManager") -class SiteManagerPlugin(object): - # Update merger site for site types - def updateMergerSites(self): - global merger_db, merged_db, merged_to_merger, site_manager - s = time.time() - merger_db_new = {} - merged_db_new = {} - merged_to_merger_new = {} - site_manager = self - if not self.sites: - return - for site in self.sites.values(): - # Update merged sites - try: - merged_type = site.content_manager.contents.get("content.json", {}).get("merged_type") - except Exception as err: - self.log.error("Error loading site %s: %s" % (site.address, Debug.formatException(err))) - continue - if merged_type: - merged_db_new[site.address] = merged_type - - # Update merger sites - for permission in site.settings["permissions"]: - if not permission.startswith("Merger:"): - continue - if merged_type: - self.log.error( - "Removing permission %s from %s: Merger and merged at the same time." % - (permission, site.address) - ) - site.settings["permissions"].remove(permission) - continue - merger_type = permission.replace("Merger:", "") - if site.address not in merger_db_new: - merger_db_new[site.address] = [] - merger_db_new[site.address].append(merger_type) - site_manager.sites[site.address] = site - - # Update merged to merger - if merged_type: - for merger_site in self.sites.values(): - if "Merger:" + merged_type in merger_site.settings["permissions"]: - if site.address not in merged_to_merger_new: - merged_to_merger_new[site.address] = [] - merged_to_merger_new[site.address].append(merger_site) - - # Update globals - merger_db = merger_db_new - merged_db = merged_db_new - merged_to_merger = merged_to_merger_new - - self.log.debug("Updated merger sites in %.3fs" % (time.time() - s)) - - def load(self, *args, **kwags): - super(SiteManagerPlugin, self).load(*args, **kwags) - self.updateMergerSites() - - def saveDelayed(self, *args, **kwags): - super(SiteManagerPlugin, self).saveDelayed(*args, **kwags) - self.updateMergerSites() diff --git a/plugins/MergerSite/__init__.py b/plugins/MergerSite/__init__.py deleted file mode 100644 index 2cf54611..00000000 --- a/plugins/MergerSite/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import MergerSitePlugin \ No newline at end of file diff --git a/plugins/MergerSite/languages/es.json b/plugins/MergerSite/languages/es.json deleted file mode 100644 index d554c3a9..00000000 --- a/plugins/MergerSite/languages/es.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "Add %s new site?": "¿Agregar %s nuevo sitio?", - "Added %s new site": "Sitio %s agregado", - "Site deleted: %s": "Sitio removido: %s" -} diff --git a/plugins/MergerSite/languages/fr.json b/plugins/MergerSite/languages/fr.json deleted file mode 100644 index 9d59fde9..00000000 --- a/plugins/MergerSite/languages/fr.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "Add %s new site?": "Ajouter le site %s ?", - "Added %s new site": "Site %s ajouté", - "Site deleted: %s": "Site %s supprimé" -} diff --git a/plugins/MergerSite/languages/hu.json b/plugins/MergerSite/languages/hu.json deleted file mode 100644 index 8e377aaa..00000000 --- a/plugins/MergerSite/languages/hu.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "Add %s new site?": "Új oldal hozzáadása: %s?", - "Added %s new site": "Új oldal hozzáadva: %s", - "Site deleted: %s": "Oldal törölve: %s" -} diff --git a/plugins/MergerSite/languages/it.json b/plugins/MergerSite/languages/it.json deleted file mode 100644 index d56c9817..00000000 --- a/plugins/MergerSite/languages/it.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "Add %s new site?": "Aggiungere %s nuovo sito ?", - "Added %s new site": "Sito %s aggiunto", - "Site deleted: %s": "Sito %s eliminato" -} diff --git a/plugins/MergerSite/languages/jp.json b/plugins/MergerSite/languages/jp.json deleted file mode 100644 index 7216f268..00000000 --- a/plugins/MergerSite/languages/jp.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "Add %s new site?": "サイト: %s を追加しますか?", - "Added %s new site": "サイト: %s を追加しました", - "Site deleted: %s": "サイト: %s を削除しました" -} diff --git a/plugins/MergerSite/languages/pt-br.json b/plugins/MergerSite/languages/pt-br.json deleted file mode 100644 index cdc298cb..00000000 --- a/plugins/MergerSite/languages/pt-br.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "Add %s new site?": "Adicionar %s novo site?", - "Added %s new site": "Site %s adicionado", - "Site deleted: %s": "Site removido: %s" -} diff --git a/plugins/MergerSite/languages/tr.json b/plugins/MergerSite/languages/tr.json deleted file mode 100644 index 5afb3942..00000000 --- a/plugins/MergerSite/languages/tr.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "Add %s new site?": "%s sitesi eklensin mi?", - "Added %s new site": "%s sitesi eklendi", - "Site deleted: %s": "%s sitesi silindi" -} diff --git a/plugins/MergerSite/languages/zh-tw.json b/plugins/MergerSite/languages/zh-tw.json deleted file mode 100644 index a0684e63..00000000 --- a/plugins/MergerSite/languages/zh-tw.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "Add %s new site?": "添加新網站: %s?", - "Added %s new site": "已添加到新網站:%s", - "Site deleted: %s": "網站已刪除:%s" -} diff --git a/plugins/MergerSite/languages/zh.json b/plugins/MergerSite/languages/zh.json deleted file mode 100644 index 127044e6..00000000 --- a/plugins/MergerSite/languages/zh.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "Add %s new site?": "添加新站点: %s?", - "Added %s new site": "已添加到新站点:%s", - "Site deleted: %s": "站点已删除:%s" -} diff --git a/plugins/Newsfeed/NewsfeedPlugin.py b/plugins/Newsfeed/NewsfeedPlugin.py deleted file mode 100644 index 3eb14d6c..00000000 --- a/plugins/Newsfeed/NewsfeedPlugin.py +++ /dev/null @@ -1,187 +0,0 @@ -import time -import re - -from Plugin import PluginManager -from Db.DbQuery import DbQuery -from Debug import Debug -from util import helper -from util.Flag import flag - - -@PluginManager.registerTo("UiWebsocket") -class UiWebsocketPlugin(object): - def formatSiteInfo(self, site, create_user=True): - site_info = super(UiWebsocketPlugin, self).formatSiteInfo(site, create_user=create_user) - feed_following = self.user.sites.get(site.address, {}).get("follow", None) - if feed_following == None: - site_info["feed_follow_num"] = None - else: - site_info["feed_follow_num"] = len(feed_following) - return site_info - - def actionFeedFollow(self, to, feeds): - self.user.setFeedFollow(self.site.address, feeds) - self.user.save() - self.response(to, "ok") - - def actionFeedListFollow(self, to): - feeds = self.user.sites.get(self.site.address, {}).get("follow", {}) - self.response(to, feeds) - - @flag.admin - def actionFeedQuery(self, to, limit=10, day_limit=3): - from Site import SiteManager - rows = [] - stats = [] - - total_s = time.time() - num_sites = 0 - - for address, site_data in list(self.user.sites.items()): - feeds = site_data.get("follow") - if not feeds: - continue - if type(feeds) is not dict: - self.log.debug("Invalid feed for site %s" % address) - continue - num_sites += 1 - for name, query_set in feeds.items(): - site = SiteManager.site_manager.get(address) - if not site or not site.storage.has_db: - continue - - s = time.time() - try: - query_raw, params = query_set - query_parts = re.split(r"UNION(?:\s+ALL|)", query_raw) - for i, query_part in enumerate(query_parts): - db_query = DbQuery(query_part) - if day_limit: - where = " WHERE %s > strftime('%%s', 'now', '-%s day')" % (db_query.fields.get("date_added", "date_added"), day_limit) - if "WHERE" in query_part: - query_part = re.sub("WHERE (.*?)(?=$| GROUP BY)", where+" AND (\\1)", query_part) - else: - query_part += where - query_parts[i] = query_part - query = " UNION ".join(query_parts) - - if ":params" in query: - query_params = map(helper.sqlquote, params) - query = query.replace(":params", ",".join(query_params)) - - res = site.storage.query(query + " ORDER BY date_added DESC LIMIT %s" % limit) - - except Exception as err: # Log error - self.log.error("%s feed query %s error: %s" % (address, name, Debug.formatException(err))) - stats.append({"site": site.address, "feed_name": name, "error": str(err)}) - continue - - for row in res: - row = dict(row) - if not isinstance(row["date_added"], (int, float, complex)): - self.log.debug("Invalid date_added from site %s: %r" % (address, row["date_added"])) - continue - if row["date_added"] > 1000000000000: # Formatted as millseconds - row["date_added"] = row["date_added"] / 1000 - if "date_added" not in row or row["date_added"] > time.time() + 120: - self.log.debug("Newsfeed item from the future from from site %s" % address) - continue # Feed item is in the future, skip it - row["site"] = address - row["feed_name"] = name - rows.append(row) - stats.append({"site": site.address, "feed_name": name, "taken": round(time.time() - s, 3)}) - time.sleep(0.001) - return self.response(to, {"rows": rows, "stats": stats, "num": len(rows), "sites": num_sites, "taken": round(time.time() - total_s, 3)}) - - def parseSearch(self, search): - parts = re.split("(site|type):", search) - if len(parts) > 1: # Found filter - search_text = parts[0] - parts = [part.strip() for part in parts] - filters = dict(zip(parts[1::2], parts[2::2])) - else: - search_text = search - filters = {} - return [search_text, filters] - - def actionFeedSearch(self, to, search, limit=30, day_limit=30): - if "ADMIN" not in self.site.settings["permissions"]: - return self.response(to, "FeedSearch not allowed") - - from Site import SiteManager - rows = [] - stats = [] - num_sites = 0 - total_s = time.time() - - search_text, filters = self.parseSearch(search) - - for address, site in SiteManager.site_manager.list().items(): - if not site.storage.has_db: - continue - - if "site" in filters: - if filters["site"].lower() not in [site.address, site.content_manager.contents["content.json"].get("title").lower()]: - continue - - if site.storage.db: # Database loaded - feeds = site.storage.db.schema.get("feeds") - else: - try: - feeds = site.storage.loadJson("dbschema.json").get("feeds") - except: - continue - - if not feeds: - continue - - num_sites += 1 - - for name, query in feeds.items(): - s = time.time() - try: - db_query = DbQuery(query) - - params = [] - # Filters - if search_text: - db_query.wheres.append("(%s LIKE ? OR %s LIKE ?)" % (db_query.fields["body"], db_query.fields["title"])) - search_like = "%" + search_text.replace(" ", "%") + "%" - params.append(search_like) - params.append(search_like) - if filters.get("type") and filters["type"] not in query: - continue - - if day_limit: - db_query.wheres.append( - "%s > strftime('%%s', 'now', '-%s day')" % (db_query.fields.get("date_added", "date_added"), day_limit) - ) - - # Order - db_query.parts["ORDER BY"] = "date_added DESC" - db_query.parts["LIMIT"] = str(limit) - - res = site.storage.query(str(db_query), params) - except Exception as err: - self.log.error("%s feed query %s error: %s" % (address, name, Debug.formatException(err))) - stats.append({"site": site.address, "feed_name": name, "error": str(err), "query": query}) - continue - for row in res: - row = dict(row) - if not row["date_added"] or row["date_added"] > time.time() + 120: - continue # Feed item is in the future, skip it - row["site"] = address - row["feed_name"] = name - rows.append(row) - stats.append({"site": site.address, "feed_name": name, "taken": round(time.time() - s, 3)}) - return self.response(to, {"rows": rows, "num": len(rows), "sites": num_sites, "taken": round(time.time() - total_s, 3), "stats": stats}) - - -@PluginManager.registerTo("User") -class UserPlugin(object): - # Set queries that user follows - def setFeedFollow(self, address, feeds): - site_data = self.getSiteData(address) - site_data["follow"] = feeds - self.save() - return site_data diff --git a/plugins/Newsfeed/__init__.py b/plugins/Newsfeed/__init__.py deleted file mode 100644 index 6e624df6..00000000 --- a/plugins/Newsfeed/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import NewsfeedPlugin \ No newline at end of file diff --git a/plugins/OptionalManager/ContentDbPlugin.py b/plugins/OptionalManager/ContentDbPlugin.py deleted file mode 100644 index f0f8a877..00000000 --- a/plugins/OptionalManager/ContentDbPlugin.py +++ /dev/null @@ -1,414 +0,0 @@ -import time -import collections -import itertools -import re - -import gevent - -from util import helper -from Plugin import PluginManager -from Config import config -from Debug import Debug - -if "content_db" not in locals().keys(): # To keep between module reloads - content_db = None - - -@PluginManager.registerTo("ContentDb") -class ContentDbPlugin(object): - def __init__(self, *args, **kwargs): - global content_db - content_db = self - self.filled = {} # Site addresses that already filled from content.json - self.need_filling = False # file_optional table just created, fill data from content.json files - self.time_peer_numbers_updated = 0 - self.my_optional_files = {} # Last 50 site_address/inner_path called by fileWrite (auto-pinning these files) - self.optional_files = collections.defaultdict(dict) - self.optional_files_loaded = False - self.timer_check_optional = helper.timer(60 * 5, self.checkOptionalLimit) - super(ContentDbPlugin, self).__init__(*args, **kwargs) - - def getSchema(self): - schema = super(ContentDbPlugin, self).getSchema() - - # Need file_optional table - schema["tables"]["file_optional"] = { - "cols": [ - ["file_id", "INTEGER PRIMARY KEY UNIQUE NOT NULL"], - ["site_id", "INTEGER REFERENCES site (site_id) ON DELETE CASCADE"], - ["inner_path", "TEXT"], - ["hash_id", "INTEGER"], - ["size", "INTEGER"], - ["peer", "INTEGER DEFAULT 0"], - ["uploaded", "INTEGER DEFAULT 0"], - ["is_downloaded", "INTEGER DEFAULT 0"], - ["is_pinned", "INTEGER DEFAULT 0"], - ["time_added", "INTEGER DEFAULT 0"], - ["time_downloaded", "INTEGER DEFAULT 0"], - ["time_accessed", "INTEGER DEFAULT 0"] - ], - "indexes": [ - "CREATE UNIQUE INDEX file_optional_key ON file_optional (site_id, inner_path)", - "CREATE INDEX is_downloaded ON file_optional (is_downloaded)" - ], - "schema_changed": 11 - } - - return schema - - def initSite(self, site): - super(ContentDbPlugin, self).initSite(site) - if self.need_filling: - self.fillTableFileOptional(site) - - def checkTables(self): - changed_tables = super(ContentDbPlugin, self).checkTables() - if "file_optional" in changed_tables: - self.need_filling = True - return changed_tables - - # Load optional files ending - def loadFilesOptional(self): - s = time.time() - num = 0 - total = 0 - total_downloaded = 0 - res = content_db.execute("SELECT site_id, inner_path, size, is_downloaded FROM file_optional") - site_sizes = collections.defaultdict(lambda: collections.defaultdict(int)) - for row in res: - self.optional_files[row["site_id"]][row["inner_path"][-8:]] = 1 - num += 1 - - # Update site size stats - site_sizes[row["site_id"]]["size_optional"] += row["size"] - if row["is_downloaded"]: - site_sizes[row["site_id"]]["optional_downloaded"] += row["size"] - - # Site site size stats to sites.json settings - site_ids_reverse = {val: key for key, val in self.site_ids.items()} - for site_id, stats in site_sizes.items(): - site_address = site_ids_reverse.get(site_id) - if not site_address or site_address not in self.sites: - self.log.error("Not found site_id: %s" % site_id) - continue - site = self.sites[site_address] - site.settings["size_optional"] = stats["size_optional"] - site.settings["optional_downloaded"] = stats["optional_downloaded"] - total += stats["size_optional"] - total_downloaded += stats["optional_downloaded"] - - self.log.info( - "Loaded %s optional files: %.2fMB, downloaded: %.2fMB in %.3fs" % - (num, float(total) / 1024 / 1024, float(total_downloaded) / 1024 / 1024, time.time() - s) - ) - - if self.need_filling and self.getOptionalLimitBytes() >= 0 and self.getOptionalLimitBytes() < total_downloaded: - limit_bytes = self.getOptionalLimitBytes() - limit_new = round((float(total_downloaded) / 1024 / 1024 / 1024) * 1.1, 2) # Current limit + 10% - self.log.info( - "First startup after update and limit is smaller than downloaded files size (%.2fGB), increasing it from %.2fGB to %.2fGB" % - (float(total_downloaded) / 1024 / 1024 / 1024, float(limit_bytes) / 1024 / 1024 / 1024, limit_new) - ) - config.saveValue("optional_limit", limit_new) - config.optional_limit = str(limit_new) - - # Predicts if the file is optional - def isOptionalFile(self, site_id, inner_path): - return self.optional_files[site_id].get(inner_path[-8:]) - - # Fill file_optional table with optional files found in sites - def fillTableFileOptional(self, site): - s = time.time() - site_id = self.site_ids.get(site.address) - if not site_id: - return False - cur = self.getCursor() - res = cur.execute("SELECT * FROM content WHERE size_files_optional > 0 AND site_id = %s" % site_id) - num = 0 - for row in res.fetchall(): - content = site.content_manager.contents[row["inner_path"]] - try: - num += self.setContentFilesOptional(site, row["inner_path"], content, cur=cur) - except Exception as err: - self.log.error("Error loading %s into file_optional: %s" % (row["inner_path"], err)) - cur.close() - - # Set my files to pinned - from User import UserManager - user = UserManager.user_manager.get() - if not user: - user = UserManager.user_manager.create() - auth_address = user.getAuthAddress(site.address) - res = self.execute( - "UPDATE file_optional SET is_pinned = 1 WHERE site_id = :site_id AND inner_path LIKE :inner_path", - {"site_id": site_id, "inner_path": "%%/%s/%%" % auth_address} - ) - - self.log.debug( - "Filled file_optional table for %s in %.3fs (loaded: %s, is_pinned: %s)" % - (site.address, time.time() - s, num, res.rowcount) - ) - self.filled[site.address] = True - - def setContentFilesOptional(self, site, content_inner_path, content, cur=None): - if not cur: - cur = self - - num = 0 - site_id = self.site_ids[site.address] - content_inner_dir = helper.getDirname(content_inner_path) - for relative_inner_path, file in content.get("files_optional", {}).items(): - file_inner_path = content_inner_dir + relative_inner_path - hash_id = int(file["sha512"][0:4], 16) - if hash_id in site.content_manager.hashfield: - is_downloaded = 1 - else: - is_downloaded = 0 - if site.address + "/" + content_inner_dir in self.my_optional_files: - is_pinned = 1 - else: - is_pinned = 0 - cur.insertOrUpdate("file_optional", { - "hash_id": hash_id, - "size": int(file["size"]) - }, { - "site_id": site_id, - "inner_path": file_inner_path - }, oninsert={ - "time_added": int(time.time()), - "time_downloaded": int(time.time()) if is_downloaded else 0, - "is_downloaded": is_downloaded, - "peer": is_downloaded, - "is_pinned": is_pinned - }) - self.optional_files[site_id][file_inner_path[-8:]] = 1 - num += 1 - - return num - - def setContent(self, site, inner_path, content, size=0): - super(ContentDbPlugin, self).setContent(site, inner_path, content, size=size) - old_content = site.content_manager.contents.get(inner_path, {}) - if (not self.need_filling or self.filled.get(site.address)) and ("files_optional" in content or "files_optional" in old_content): - self.setContentFilesOptional(site, inner_path, content) - # Check deleted files - if old_content: - old_files = old_content.get("files_optional", {}).keys() - new_files = content.get("files_optional", {}).keys() - content_inner_dir = helper.getDirname(inner_path) - deleted = [content_inner_dir + key for key in old_files if key not in new_files] - if deleted: - site_id = self.site_ids[site.address] - self.execute("DELETE FROM file_optional WHERE ?", {"site_id": site_id, "inner_path": deleted}) - - def deleteContent(self, site, inner_path): - content = site.content_manager.contents.get(inner_path) - if content and "files_optional" in content: - site_id = self.site_ids[site.address] - content_inner_dir = helper.getDirname(inner_path) - optional_inner_paths = [ - content_inner_dir + relative_inner_path - for relative_inner_path in content.get("files_optional", {}).keys() - ] - self.execute("DELETE FROM file_optional WHERE ?", {"site_id": site_id, "inner_path": optional_inner_paths}) - super(ContentDbPlugin, self).deleteContent(site, inner_path) - - def updatePeerNumbers(self): - s = time.time() - num_file = 0 - num_updated = 0 - num_site = 0 - for site in list(self.sites.values()): - if not site.content_manager.has_optional_files: - continue - if not site.isServing(): - continue - has_updated_hashfield = next(( - peer - for peer in site.peers.values() - if peer.has_hashfield and peer.hashfield.time_changed > self.time_peer_numbers_updated - ), None) - - if not has_updated_hashfield and site.content_manager.hashfield.time_changed < self.time_peer_numbers_updated: - continue - - hashfield_peers = itertools.chain.from_iterable( - peer.hashfield.storage - for peer in site.peers.values() - if peer.has_hashfield - ) - peer_nums = collections.Counter( - itertools.chain( - hashfield_peers, - site.content_manager.hashfield - ) - ) - - site_id = self.site_ids[site.address] - if not site_id: - continue - - res = self.execute("SELECT file_id, hash_id, peer FROM file_optional WHERE ?", {"site_id": site_id}) - updates = {} - for row in res: - peer_num = peer_nums.get(row["hash_id"], 0) - if peer_num != row["peer"]: - updates[row["file_id"]] = peer_num - - for file_id, peer_num in updates.items(): - self.execute("UPDATE file_optional SET peer = ? WHERE file_id = ?", (peer_num, file_id)) - - num_updated += len(updates) - num_file += len(peer_nums) - num_site += 1 - - self.time_peer_numbers_updated = time.time() - self.log.debug("%s/%s peer number for %s site updated in %.3fs" % (num_updated, num_file, num_site, time.time() - s)) - - def queryDeletableFiles(self): - # First return the files with atleast 10 seeder and not accessed in last week - query = """ - SELECT * FROM file_optional - WHERE peer > 10 AND %s - ORDER BY time_accessed < %s DESC, uploaded / size - """ % (self.getOptionalUsedWhere(), int(time.time() - 60 * 60 * 7)) - limit_start = 0 - while 1: - num = 0 - res = self.execute("%s LIMIT %s, 50" % (query, limit_start)) - for row in res: - yield row - num += 1 - if num < 50: - break - limit_start += 50 - - self.log.debug("queryDeletableFiles returning less-seeded files") - - # Then return files less seeder but still not accessed in last week - query = """ - SELECT * FROM file_optional - WHERE peer <= 10 AND %s - ORDER BY peer DESC, time_accessed < %s DESC, uploaded / size - """ % (self.getOptionalUsedWhere(), int(time.time() - 60 * 60 * 7)) - limit_start = 0 - while 1: - num = 0 - res = self.execute("%s LIMIT %s, 50" % (query, limit_start)) - for row in res: - yield row - num += 1 - if num < 50: - break - limit_start += 50 - - self.log.debug("queryDeletableFiles returning everyting") - - # At the end return all files - query = """ - SELECT * FROM file_optional - WHERE peer <= 10 AND %s - ORDER BY peer DESC, time_accessed, uploaded / size - """ % self.getOptionalUsedWhere() - limit_start = 0 - while 1: - num = 0 - res = self.execute("%s LIMIT %s, 50" % (query, limit_start)) - for row in res: - yield row - num += 1 - if num < 50: - break - limit_start += 50 - - def getOptionalLimitBytes(self): - if config.optional_limit.endswith("%"): - limit_percent = float(re.sub("[^0-9.]", "", config.optional_limit)) - limit_bytes = helper.getFreeSpace() * (limit_percent / 100) - else: - limit_bytes = float(re.sub("[^0-9.]", "", config.optional_limit)) * 1024 * 1024 * 1024 - return limit_bytes - - def getOptionalUsedWhere(self): - maxsize = config.optional_limit_exclude_minsize * 1024 * 1024 - query = "is_downloaded = 1 AND is_pinned = 0 AND size < %s" % maxsize - - # Don't delete optional files from owned sites - my_site_ids = [] - for address, site in self.sites.items(): - if site.settings["own"]: - my_site_ids.append(str(self.site_ids[address])) - - if my_site_ids: - query += " AND site_id NOT IN (%s)" % ", ".join(my_site_ids) - return query - - def getOptionalUsedBytes(self): - size = self.execute("SELECT SUM(size) FROM file_optional WHERE %s" % self.getOptionalUsedWhere()).fetchone()[0] - if not size: - size = 0 - return size - - def getOptionalNeedDelete(self, size): - if config.optional_limit.endswith("%"): - limit_percent = float(re.sub("[^0-9.]", "", config.optional_limit)) - need_delete = size - ((helper.getFreeSpace() + size) * (limit_percent / 100)) - else: - need_delete = size - self.getOptionalLimitBytes() - return need_delete - - def checkOptionalLimit(self, limit=None): - if not limit: - limit = self.getOptionalLimitBytes() - - if limit < 0: - self.log.debug("Invalid limit for optional files: %s" % limit) - return False - - size = self.getOptionalUsedBytes() - - need_delete = self.getOptionalNeedDelete(size) - - self.log.debug( - "Optional size: %.1fMB/%.1fMB, Need delete: %.1fMB" % - (float(size) / 1024 / 1024, float(limit) / 1024 / 1024, float(need_delete) / 1024 / 1024) - ) - if need_delete <= 0: - return False - - self.updatePeerNumbers() - - site_ids_reverse = {val: key for key, val in self.site_ids.items()} - deleted_file_ids = [] - for row in self.queryDeletableFiles(): - site_address = site_ids_reverse.get(row["site_id"]) - site = self.sites.get(site_address) - if not site: - self.log.error("No site found for id: %s" % row["site_id"]) - continue - site.log.debug("Deleting %s %.3f MB left" % (row["inner_path"], float(need_delete) / 1024 / 1024)) - deleted_file_ids.append(row["file_id"]) - try: - site.content_manager.optionalRemoved(row["inner_path"], row["hash_id"], row["size"]) - site.storage.delete(row["inner_path"]) - need_delete -= row["size"] - except Exception as err: - site.log.error("Error deleting %s: %s" % (row["inner_path"], err)) - - if need_delete <= 0: - break - - cur = self.getCursor() - for file_id in deleted_file_ids: - cur.execute("UPDATE file_optional SET is_downloaded = 0, is_pinned = 0, peer = peer - 1 WHERE ?", {"file_id": file_id}) - cur.close() - - -@PluginManager.registerTo("SiteManager") -class SiteManagerPlugin(object): - def load(self, *args, **kwargs): - back = super(SiteManagerPlugin, self).load(*args, **kwargs) - if self.sites and not content_db.optional_files_loaded and content_db.conn: - content_db.optional_files_loaded = True - content_db.loadFilesOptional() - return back \ No newline at end of file diff --git a/plugins/OptionalManager/OptionalManagerPlugin.py b/plugins/OptionalManager/OptionalManagerPlugin.py deleted file mode 100644 index f01fab65..00000000 --- a/plugins/OptionalManager/OptionalManagerPlugin.py +++ /dev/null @@ -1,253 +0,0 @@ -import time -import re -import collections - -import gevent - -from util import helper -from Plugin import PluginManager -from . import ContentDbPlugin - - -# We can only import plugin host clases after the plugins are loaded -@PluginManager.afterLoad -def importPluginnedClasses(): - global config - from Config import config - - -def processAccessLog(): - global access_log - if access_log: - content_db = ContentDbPlugin.content_db - if not content_db.conn: - return False - - s = time.time() - access_log_prev = access_log - access_log = collections.defaultdict(dict) - now = int(time.time()) - num = 0 - for site_id in access_log_prev: - content_db.execute( - "UPDATE file_optional SET time_accessed = %s WHERE ?" % now, - {"site_id": site_id, "inner_path": list(access_log_prev[site_id].keys())} - ) - num += len(access_log_prev[site_id]) - - content_db.log.debug("Inserted %s web request stat in %.3fs" % (num, time.time() - s)) - - -def processRequestLog(): - global request_log - if request_log: - content_db = ContentDbPlugin.content_db - if not content_db.conn: - return False - - s = time.time() - request_log_prev = request_log - request_log = collections.defaultdict(lambda: collections.defaultdict(int)) # {site_id: {inner_path1: 1, inner_path2: 1...}} - num = 0 - for site_id in request_log_prev: - for inner_path, uploaded in request_log_prev[site_id].items(): - content_db.execute( - "UPDATE file_optional SET uploaded = uploaded + %s WHERE ?" % uploaded, - {"site_id": site_id, "inner_path": inner_path} - ) - num += 1 - content_db.log.debug("Inserted %s file request stat in %.3fs" % (num, time.time() - s)) - - -if "access_log" not in locals().keys(): # To keep between module reloads - access_log = collections.defaultdict(dict) # {site_id: {inner_path1: 1, inner_path2: 1...}} - request_log = collections.defaultdict(lambda: collections.defaultdict(int)) # {site_id: {inner_path1: 1, inner_path2: 1...}} - helper.timer(61, processAccessLog) - helper.timer(60, processRequestLog) - - -@PluginManager.registerTo("ContentManager") -class ContentManagerPlugin(object): - def __init__(self, *args, **kwargs): - self.cache_is_pinned = {} - super(ContentManagerPlugin, self).__init__(*args, **kwargs) - - def optionalDownloaded(self, inner_path, hash_id, size=None, own=False): - if "|" in inner_path: # Big file piece - file_inner_path, file_range = inner_path.split("|") - else: - file_inner_path = inner_path - - self.contents.db.executeDelayed( - "UPDATE file_optional SET time_downloaded = :now, is_downloaded = 1, peer = peer + 1 WHERE site_id = :site_id AND inner_path = :inner_path AND is_downloaded = 0", - {"now": int(time.time()), "site_id": self.contents.db.site_ids[self.site.address], "inner_path": file_inner_path} - ) - - return super(ContentManagerPlugin, self).optionalDownloaded(inner_path, hash_id, size, own) - - def optionalRemoved(self, inner_path, hash_id, size=None): - res = self.contents.db.execute( - "UPDATE file_optional SET is_downloaded = 0, is_pinned = 0, peer = peer - 1 WHERE site_id = :site_id AND inner_path = :inner_path AND is_downloaded = 1", - {"site_id": self.contents.db.site_ids[self.site.address], "inner_path": inner_path} - ) - - if res.rowcount > 0: - back = super(ContentManagerPlugin, self).optionalRemoved(inner_path, hash_id, size) - # Re-add to hashfield if we have other file with the same hash_id - if self.isDownloaded(hash_id=hash_id, force_check_db=True): - self.hashfield.appendHashId(hash_id) - else: - back = False - self.cache_is_pinned = {} - return back - - def optionalRenamed(self, inner_path_old, inner_path_new): - back = super(ContentManagerPlugin, self).optionalRenamed(inner_path_old, inner_path_new) - self.cache_is_pinned = {} - self.contents.db.execute( - "UPDATE file_optional SET inner_path = :inner_path_new WHERE site_id = :site_id AND inner_path = :inner_path_old", - {"site_id": self.contents.db.site_ids[self.site.address], "inner_path_old": inner_path_old, "inner_path_new": inner_path_new} - ) - return back - - def isDownloaded(self, inner_path=None, hash_id=None, force_check_db=False): - if hash_id and not force_check_db and hash_id not in self.hashfield: - return False - - if inner_path: - res = self.contents.db.execute( - "SELECT is_downloaded FROM file_optional WHERE site_id = :site_id AND inner_path = :inner_path LIMIT 1", - {"site_id": self.contents.db.site_ids[self.site.address], "inner_path": inner_path} - ) - else: - res = self.contents.db.execute( - "SELECT is_downloaded FROM file_optional WHERE site_id = :site_id AND hash_id = :hash_id AND is_downloaded = 1 LIMIT 1", - {"site_id": self.contents.db.site_ids[self.site.address], "hash_id": hash_id} - ) - row = res.fetchone() - if row and row["is_downloaded"]: - return True - else: - return False - - def isPinned(self, inner_path): - if inner_path in self.cache_is_pinned: - self.site.log.debug("Cached is pinned: %s" % inner_path) - return self.cache_is_pinned[inner_path] - - res = self.contents.db.execute( - "SELECT is_pinned FROM file_optional WHERE site_id = :site_id AND inner_path = :inner_path LIMIT 1", - {"site_id": self.contents.db.site_ids[self.site.address], "inner_path": inner_path} - ) - row = res.fetchone() - - if row and row[0]: - is_pinned = True - else: - is_pinned = False - - self.cache_is_pinned[inner_path] = is_pinned - self.site.log.debug("Cache set is pinned: %s %s" % (inner_path, is_pinned)) - - return is_pinned - - def setPin(self, inner_path, is_pinned): - content_db = self.contents.db - site_id = content_db.site_ids[self.site.address] - content_db.execute("UPDATE file_optional SET is_pinned = %d WHERE ?" % is_pinned, {"site_id": site_id, "inner_path": inner_path}) - self.cache_is_pinned = {} - - def optionalDelete(self, inner_path): - if self.isPinned(inner_path): - self.site.log.debug("Skip deleting pinned optional file: %s" % inner_path) - return False - else: - return super(ContentManagerPlugin, self).optionalDelete(inner_path) - - -@PluginManager.registerTo("WorkerManager") -class WorkerManagerPlugin(object): - def doneTask(self, task): - super(WorkerManagerPlugin, self).doneTask(task) - - if task["optional_hash_id"] and not self.tasks: # Execute delayed queries immedietly after tasks finished - ContentDbPlugin.content_db.processDelayed() - - -@PluginManager.registerTo("UiRequest") -class UiRequestPlugin(object): - def parsePath(self, path): - global access_log - path_parts = super(UiRequestPlugin, self).parsePath(path) - if path_parts: - site_id = ContentDbPlugin.content_db.site_ids.get(path_parts["request_address"]) - if site_id: - if ContentDbPlugin.content_db.isOptionalFile(site_id, path_parts["inner_path"]): - access_log[site_id][path_parts["inner_path"]] = 1 - return path_parts - - -@PluginManager.registerTo("FileRequest") -class FileRequestPlugin(object): - def actionGetFile(self, params): - stats = super(FileRequestPlugin, self).actionGetFile(params) - self.recordFileRequest(params["site"], params["inner_path"], stats) - return stats - - def actionStreamFile(self, params): - stats = super(FileRequestPlugin, self).actionStreamFile(params) - self.recordFileRequest(params["site"], params["inner_path"], stats) - return stats - - def recordFileRequest(self, site_address, inner_path, stats): - if not stats: - # Only track the last request of files - return False - site_id = ContentDbPlugin.content_db.site_ids[site_address] - if site_id and ContentDbPlugin.content_db.isOptionalFile(site_id, inner_path): - request_log[site_id][inner_path] += stats["bytes_sent"] - - -@PluginManager.registerTo("Site") -class SitePlugin(object): - def isDownloadable(self, inner_path): - is_downloadable = super(SitePlugin, self).isDownloadable(inner_path) - if is_downloadable: - return is_downloadable - - for path in self.settings.get("optional_help", {}).keys(): - if inner_path.startswith(path): - return True - - return False - - def fileForgot(self, inner_path): - if "|" in inner_path and self.content_manager.isPinned(re.sub(r"\|.*", "", inner_path)): - self.log.debug("File %s is pinned, no fileForgot" % inner_path) - return False - else: - return super(SitePlugin, self).fileForgot(inner_path) - - def fileDone(self, inner_path): - if "|" in inner_path and self.bad_files.get(inner_path, 0) > 5: # Idle optional file done - inner_path_file = re.sub(r"\|.*", "", inner_path) - num_changed = 0 - for key, val in self.bad_files.items(): - if key.startswith(inner_path_file) and val > 1: - self.bad_files[key] = 1 - num_changed += 1 - self.log.debug("Idle optional file piece done, changed retry number of %s pieces." % num_changed) - if num_changed: - gevent.spawn(self.retryBadFiles) - - return super(SitePlugin, self).fileDone(inner_path) - - -@PluginManager.registerTo("ConfigPlugin") -class ConfigPlugin(object): - def createArguments(self): - group = self.parser.add_argument_group("OptionalManager plugin") - group.add_argument('--optional_limit', help='Limit total size of optional files', default="10%", metavar="GB or free space %") - group.add_argument('--optional_limit_exclude_minsize', help='Exclude files larger than this limit from optional size limit calculation', default=20, metavar="MB", type=int) - - return super(ConfigPlugin, self).createArguments() diff --git a/plugins/OptionalManager/Test/TestOptionalManager.py b/plugins/OptionalManager/Test/TestOptionalManager.py deleted file mode 100644 index 4bd44695..00000000 --- a/plugins/OptionalManager/Test/TestOptionalManager.py +++ /dev/null @@ -1,158 +0,0 @@ -import copy - -import pytest - - -@pytest.mark.usefixtures("resetSettings") -class TestOptionalManager: - def testDbFill(self, site): - contents = site.content_manager.contents - assert len(site.content_manager.hashfield) > 0 - assert contents.db.execute("SELECT COUNT(*) FROM file_optional WHERE is_downloaded = 1").fetchone()[0] == len(site.content_manager.hashfield) - - def testSetContent(self, site): - contents = site.content_manager.contents - - # Add new file - new_content = copy.deepcopy(contents["content.json"]) - new_content["files_optional"]["testfile"] = { - "size": 1234, - "sha512": "aaaabbbbcccc" - } - num_optional_files_before = contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0] - contents["content.json"] = new_content - assert contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0] > num_optional_files_before - - # Remove file - new_content = copy.deepcopy(contents["content.json"]) - del new_content["files_optional"]["testfile"] - num_optional_files_before = contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0] - contents["content.json"] = new_content - assert contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0] < num_optional_files_before - - def testDeleteContent(self, site): - contents = site.content_manager.contents - num_optional_files_before = contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0] - del contents["content.json"] - assert contents.db.execute("SELECT COUNT(*) FROM file_optional").fetchone()[0] < num_optional_files_before - - def testVerifyFiles(self, site): - contents = site.content_manager.contents - - # Add new file - new_content = copy.deepcopy(contents["content.json"]) - new_content["files_optional"]["testfile"] = { - "size": 1234, - "sha512": "aaaabbbbcccc" - } - contents["content.json"] = new_content - file_row = contents.db.execute("SELECT * FROM file_optional WHERE inner_path = 'testfile'").fetchone() - assert not file_row["is_downloaded"] - - # Write file from outside of ZeroNet - site.storage.open("testfile", "wb").write(b"A" * 1234) # For quick check hash does not matter only file size - - hashfield_len_before = len(site.content_manager.hashfield) - site.storage.verifyFiles(quick_check=True) - assert len(site.content_manager.hashfield) == hashfield_len_before + 1 - - file_row = contents.db.execute("SELECT * FROM file_optional WHERE inner_path = 'testfile'").fetchone() - assert file_row["is_downloaded"] - - # Delete file outside of ZeroNet - site.storage.delete("testfile") - site.storage.verifyFiles(quick_check=True) - file_row = contents.db.execute("SELECT * FROM file_optional WHERE inner_path = 'testfile'").fetchone() - assert not file_row["is_downloaded"] - - def testVerifyFilesSameHashId(self, site): - contents = site.content_manager.contents - - new_content = copy.deepcopy(contents["content.json"]) - - # Add two files with same hashid (first 4 character) - new_content["files_optional"]["testfile1"] = { - "size": 1234, - "sha512": "aaaabbbbcccc" - } - new_content["files_optional"]["testfile2"] = { - "size": 2345, - "sha512": "aaaabbbbdddd" - } - contents["content.json"] = new_content - - assert site.content_manager.hashfield.getHashId("aaaabbbbcccc") == site.content_manager.hashfield.getHashId("aaaabbbbdddd") - - # Write files from outside of ZeroNet (For quick check hash does not matter only file size) - site.storage.open("testfile1", "wb").write(b"A" * 1234) - site.storage.open("testfile2", "wb").write(b"B" * 2345) - - site.storage.verifyFiles(quick_check=True) - - # Make sure that both is downloaded - assert site.content_manager.isDownloaded("testfile1") - assert site.content_manager.isDownloaded("testfile2") - assert site.content_manager.hashfield.getHashId("aaaabbbbcccc") in site.content_manager.hashfield - - # Delete one of the files - site.storage.delete("testfile1") - site.storage.verifyFiles(quick_check=True) - assert not site.content_manager.isDownloaded("testfile1") - assert site.content_manager.isDownloaded("testfile2") - assert site.content_manager.hashfield.getHashId("aaaabbbbdddd") in site.content_manager.hashfield - - def testIsPinned(self, site): - assert not site.content_manager.isPinned("data/img/zerotalk-upvote.png") - site.content_manager.setPin("data/img/zerotalk-upvote.png", True) - assert site.content_manager.isPinned("data/img/zerotalk-upvote.png") - - assert len(site.content_manager.cache_is_pinned) == 1 - site.content_manager.cache_is_pinned = {} - assert site.content_manager.isPinned("data/img/zerotalk-upvote.png") - - def testBigfilePieceReset(self, site): - site.bad_files = { - "data/fake_bigfile.mp4|0-1024": 10, - "data/fake_bigfile.mp4|1024-2048": 10, - "data/fake_bigfile.mp4|2048-3064": 10 - } - site.onFileDone("data/fake_bigfile.mp4|0-1024") - assert site.bad_files["data/fake_bigfile.mp4|1024-2048"] == 1 - assert site.bad_files["data/fake_bigfile.mp4|2048-3064"] == 1 - - def testOptionalDelete(self, site): - contents = site.content_manager.contents - - site.content_manager.setPin("data/img/zerotalk-upvote.png", True) - site.content_manager.setPin("data/img/zeroid.png", False) - new_content = copy.deepcopy(contents["content.json"]) - del new_content["files_optional"]["data/img/zerotalk-upvote.png"] - del new_content["files_optional"]["data/img/zeroid.png"] - - assert site.storage.isFile("data/img/zerotalk-upvote.png") - assert site.storage.isFile("data/img/zeroid.png") - - site.storage.writeJson("content.json", new_content) - site.content_manager.loadContent("content.json", force=True) - - assert not site.storage.isFile("data/img/zeroid.png") - assert site.storage.isFile("data/img/zerotalk-upvote.png") - - def testOptionalRename(self, site): - contents = site.content_manager.contents - - site.content_manager.setPin("data/img/zerotalk-upvote.png", True) - new_content = copy.deepcopy(contents["content.json"]) - new_content["files_optional"]["data/img/zerotalk-upvote-new.png"] = new_content["files_optional"]["data/img/zerotalk-upvote.png"] - del new_content["files_optional"]["data/img/zerotalk-upvote.png"] - - assert site.storage.isFile("data/img/zerotalk-upvote.png") - assert site.content_manager.isPinned("data/img/zerotalk-upvote.png") - - site.storage.writeJson("content.json", new_content) - site.content_manager.loadContent("content.json", force=True) - - assert not site.storage.isFile("data/img/zerotalk-upvote.png") - assert not site.content_manager.isPinned("data/img/zerotalk-upvote.png") - assert site.content_manager.isPinned("data/img/zerotalk-upvote-new.png") - assert site.storage.isFile("data/img/zerotalk-upvote-new.png") diff --git a/plugins/OptionalManager/Test/conftest.py b/plugins/OptionalManager/Test/conftest.py deleted file mode 100644 index 8c1df5b2..00000000 --- a/plugins/OptionalManager/Test/conftest.py +++ /dev/null @@ -1 +0,0 @@ -from src.Test.conftest import * \ No newline at end of file diff --git a/plugins/OptionalManager/Test/pytest.ini b/plugins/OptionalManager/Test/pytest.ini deleted file mode 100644 index d09210d1..00000000 --- a/plugins/OptionalManager/Test/pytest.ini +++ /dev/null @@ -1,5 +0,0 @@ -[pytest] -python_files = Test*.py -addopts = -rsxX -v --durations=6 -markers = - webtest: mark a test as a webtest. \ No newline at end of file diff --git a/plugins/OptionalManager/UiWebsocketPlugin.py b/plugins/OptionalManager/UiWebsocketPlugin.py deleted file mode 100644 index 0acc53cf..00000000 --- a/plugins/OptionalManager/UiWebsocketPlugin.py +++ /dev/null @@ -1,396 +0,0 @@ -import re -import time -import html -import os - -import gevent - -from Plugin import PluginManager -from Config import config -from util import helper -from util.Flag import flag -from Translate import Translate - - -plugin_dir = os.path.dirname(__file__) - -if "_" not in locals(): - _ = Translate(plugin_dir + "/languages/") - -bigfile_sha512_cache = {} - - -@PluginManager.registerTo("UiWebsocket") -class UiWebsocketPlugin(object): - def __init__(self, *args, **kwargs): - self.time_peer_numbers_updated = 0 - super(UiWebsocketPlugin, self).__init__(*args, **kwargs) - - def actionSiteSign(self, to, privatekey=None, inner_path="content.json", *args, **kwargs): - # Add file to content.db and set it as pinned - content_db = self.site.content_manager.contents.db - content_inner_dir = helper.getDirname(inner_path) - content_db.my_optional_files[self.site.address + "/" + content_inner_dir] = time.time() - if len(content_db.my_optional_files) > 50: # Keep only last 50 - oldest_key = min( - iter(content_db.my_optional_files.keys()), - key=(lambda key: content_db.my_optional_files[key]) - ) - del content_db.my_optional_files[oldest_key] - - return super(UiWebsocketPlugin, self).actionSiteSign(to, privatekey, inner_path, *args, **kwargs) - - def updatePeerNumbers(self): - self.site.updateHashfield() - content_db = self.site.content_manager.contents.db - content_db.updatePeerNumbers() - self.site.updateWebsocket(peernumber_updated=True) - - def addBigfileInfo(self, row): - global bigfile_sha512_cache - - content_db = self.site.content_manager.contents.db - site = content_db.sites[row["address"]] - if not site.settings.get("has_bigfile"): - return False - - file_key = row["address"] + "/" + row["inner_path"] - sha512 = bigfile_sha512_cache.get(file_key) - file_info = None - if not sha512: - file_info = site.content_manager.getFileInfo(row["inner_path"]) - if not file_info or not file_info.get("piece_size"): - return False - sha512 = file_info["sha512"] - bigfile_sha512_cache[file_key] = sha512 - - if sha512 in site.storage.piecefields: - piecefield = site.storage.piecefields[sha512].tobytes() - else: - piecefield = None - - if piecefield: - row["pieces"] = len(piecefield) - row["pieces_downloaded"] = piecefield.count(b"\x01") - row["downloaded_percent"] = 100 * row["pieces_downloaded"] / row["pieces"] - if row["pieces_downloaded"]: - if row["pieces"] == row["pieces_downloaded"]: - row["bytes_downloaded"] = row["size"] - else: - if not file_info: - file_info = site.content_manager.getFileInfo(row["inner_path"]) - row["bytes_downloaded"] = row["pieces_downloaded"] * file_info.get("piece_size", 0) - else: - row["bytes_downloaded"] = 0 - - row["is_downloading"] = bool(next((inner_path for inner_path in site.bad_files if inner_path.startswith(row["inner_path"])), False)) - - # Add leech / seed stats - row["peer_seed"] = 0 - row["peer_leech"] = 0 - for peer in site.peers.values(): - if not peer.time_piecefields_updated or sha512 not in peer.piecefields: - continue - peer_piecefield = peer.piecefields[sha512].tobytes() - if not peer_piecefield: - continue - if peer_piecefield == b"\x01" * len(peer_piecefield): - row["peer_seed"] += 1 - else: - row["peer_leech"] += 1 - - # Add myself - if piecefield: - if row["pieces_downloaded"] == row["pieces"]: - row["peer_seed"] += 1 - else: - row["peer_leech"] += 1 - - return True - - # Optional file functions - - def actionOptionalFileList(self, to, address=None, orderby="time_downloaded DESC", limit=10, filter="downloaded", filter_inner_path=None): - if not address: - address = self.site.address - - # Update peer numbers if necessary - content_db = self.site.content_manager.contents.db - if time.time() - content_db.time_peer_numbers_updated > 60 * 1 and time.time() - self.time_peer_numbers_updated > 60 * 5: - # Start in new thread to avoid blocking - self.time_peer_numbers_updated = time.time() - gevent.spawn(self.updatePeerNumbers) - - if address == "all" and "ADMIN" not in self.permissions: - return self.response(to, {"error": "Forbidden"}) - - if not self.hasSitePermission(address): - return self.response(to, {"error": "Forbidden"}) - - if not all([re.match("^[a-z_*/+-]+( DESC| ASC|)$", part.strip()) for part in orderby.split(",")]): - return self.response(to, "Invalid order_by") - - if type(limit) != int: - return self.response(to, "Invalid limit") - - back = [] - content_db = self.site.content_manager.contents.db - - wheres = {} - wheres_raw = [] - if "bigfile" in filter: - wheres["size >"] = 1024 * 1024 * 1 - if "downloaded" in filter: - wheres_raw.append("(is_downloaded = 1 OR is_pinned = 1)") - if "pinned" in filter: - wheres["is_pinned"] = 1 - if filter_inner_path: - wheres["inner_path__like"] = filter_inner_path - - if address == "all": - join = "LEFT JOIN site USING (site_id)" - else: - wheres["site_id"] = content_db.site_ids[address] - join = "" - - if wheres_raw: - query_wheres_raw = "AND" + " AND ".join(wheres_raw) - else: - query_wheres_raw = "" - - query = "SELECT * FROM file_optional %s WHERE ? %s ORDER BY %s LIMIT %s" % (join, query_wheres_raw, orderby, limit) - - for row in content_db.execute(query, wheres): - row = dict(row) - if address != "all": - row["address"] = address - - if row["size"] > 1024 * 1024: - has_bigfile_info = self.addBigfileInfo(row) - else: - has_bigfile_info = False - - if not has_bigfile_info and "bigfile" in filter: - continue - - if not has_bigfile_info: - if row["is_downloaded"]: - row["bytes_downloaded"] = row["size"] - row["downloaded_percent"] = 100 - else: - row["bytes_downloaded"] = 0 - row["downloaded_percent"] = 0 - - back.append(row) - self.response(to, back) - - def actionOptionalFileInfo(self, to, inner_path): - content_db = self.site.content_manager.contents.db - site_id = content_db.site_ids[self.site.address] - - # Update peer numbers if necessary - if time.time() - content_db.time_peer_numbers_updated > 60 * 1 and time.time() - self.time_peer_numbers_updated > 60 * 5: - # Start in new thread to avoid blocking - self.time_peer_numbers_updated = time.time() - gevent.spawn(self.updatePeerNumbers) - - query = "SELECT * FROM file_optional WHERE site_id = :site_id AND inner_path = :inner_path LIMIT 1" - res = content_db.execute(query, {"site_id": site_id, "inner_path": inner_path}) - row = next(res, None) - if row: - row = dict(row) - if row["size"] > 1024 * 1024: - row["address"] = self.site.address - self.addBigfileInfo(row) - self.response(to, row) - else: - self.response(to, None) - - def setPin(self, inner_path, is_pinned, address=None): - if not address: - address = self.site.address - - if not self.hasSitePermission(address): - return {"error": "Forbidden"} - - site = self.server.sites[address] - site.content_manager.setPin(inner_path, is_pinned) - - return "ok" - - @flag.no_multiuser - def actionOptionalFilePin(self, to, inner_path, address=None): - if type(inner_path) is not list: - inner_path = [inner_path] - back = self.setPin(inner_path, 1, address) - num_file = len(inner_path) - if back == "ok": - if num_file == 1: - self.cmd("notification", ["done", _["Pinned %s"] % html.escape(helper.getFilename(inner_path[0])), 5000]) - else: - self.cmd("notification", ["done", _["Pinned %s files"] % num_file, 5000]) - self.response(to, back) - - @flag.no_multiuser - def actionOptionalFileUnpin(self, to, inner_path, address=None): - if type(inner_path) is not list: - inner_path = [inner_path] - back = self.setPin(inner_path, 0, address) - num_file = len(inner_path) - if back == "ok": - if num_file == 1: - self.cmd("notification", ["done", _["Removed pin from %s"] % html.escape(helper.getFilename(inner_path[0])), 5000]) - else: - self.cmd("notification", ["done", _["Removed pin from %s files"] % num_file, 5000]) - self.response(to, back) - - @flag.no_multiuser - def actionOptionalFileDelete(self, to, inner_path, address=None): - if not address: - address = self.site.address - - if not self.hasSitePermission(address): - return self.response(to, {"error": "Forbidden"}) - - site = self.server.sites[address] - - content_db = site.content_manager.contents.db - site_id = content_db.site_ids[site.address] - - res = content_db.execute("SELECT * FROM file_optional WHERE ? LIMIT 1", {"site_id": site_id, "inner_path": inner_path, "is_downloaded": 1}) - row = next(res, None) - - if not row: - return self.response(to, {"error": "Not found in content.db"}) - - removed = site.content_manager.optionalRemoved(inner_path, row["hash_id"], row["size"]) - # if not removed: - # return self.response(to, {"error": "Not found in hash_id: %s" % row["hash_id"]}) - - content_db.execute("UPDATE file_optional SET is_downloaded = 0, is_pinned = 0, peer = peer - 1 WHERE ?", {"site_id": site_id, "inner_path": inner_path}) - - try: - site.storage.delete(inner_path) - except Exception as err: - return self.response(to, {"error": "File delete error: %s" % err}) - site.updateWebsocket(file_delete=inner_path) - - if inner_path in site.content_manager.cache_is_pinned: - site.content_manager.cache_is_pinned = {} - - self.response(to, "ok") - - # Limit functions - - @flag.admin - def actionOptionalLimitStats(self, to): - back = {} - back["limit"] = config.optional_limit - back["used"] = self.site.content_manager.contents.db.getOptionalUsedBytes() - back["free"] = helper.getFreeSpace() - - self.response(to, back) - - @flag.no_multiuser - @flag.admin - def actionOptionalLimitSet(self, to, limit): - config.optional_limit = re.sub(r"\.0+$", "", limit) # Remove unnecessary digits from end - config.saveValue("optional_limit", limit) - self.response(to, "ok") - - # Distribute help functions - - def actionOptionalHelpList(self, to, address=None): - if not address: - address = self.site.address - - if not self.hasSitePermission(address): - return self.response(to, {"error": "Forbidden"}) - - site = self.server.sites[address] - - self.response(to, site.settings.get("optional_help", {})) - - @flag.no_multiuser - def actionOptionalHelp(self, to, directory, title, address=None): - if not address: - address = self.site.address - - if not self.hasSitePermission(address): - return self.response(to, {"error": "Forbidden"}) - - site = self.server.sites[address] - content_db = site.content_manager.contents.db - site_id = content_db.site_ids[address] - - if "optional_help" not in site.settings: - site.settings["optional_help"] = {} - - stats = content_db.execute( - "SELECT COUNT(*) AS num, SUM(size) AS size FROM file_optional WHERE site_id = :site_id AND inner_path LIKE :inner_path", - {"site_id": site_id, "inner_path": directory + "%"} - ).fetchone() - stats = dict(stats) - - if not stats["size"]: - stats["size"] = 0 - if not stats["num"]: - stats["num"] = 0 - - self.cmd("notification", [ - "done", - _["You started to help distribute %s.
      Directory: %s"] % - (html.escape(title), html.escape(directory)), - 10000 - ]) - - site.settings["optional_help"][directory] = title - - self.response(to, dict(stats)) - - @flag.no_multiuser - def actionOptionalHelpRemove(self, to, directory, address=None): - if not address: - address = self.site.address - - if not self.hasSitePermission(address): - return self.response(to, {"error": "Forbidden"}) - - site = self.server.sites[address] - - try: - del site.settings["optional_help"][directory] - self.response(to, "ok") - except Exception: - self.response(to, {"error": "Not found"}) - - def cbOptionalHelpAll(self, to, site, value): - site.settings["autodownloadoptional"] = value - self.response(to, value) - - @flag.no_multiuser - def actionOptionalHelpAll(self, to, value, address=None): - if not address: - address = self.site.address - - if not self.hasSitePermission(address): - return self.response(to, {"error": "Forbidden"}) - - site = self.server.sites[address] - - if value: - if "ADMIN" in self.site.settings["permissions"]: - self.cbOptionalHelpAll(to, site, True) - else: - site_title = site.content_manager.contents["content.json"].get("title", address) - self.cmd( - "confirm", - [ - _["Help distribute all new optional files on site %s"] % html.escape(site_title), - _["Yes, I want to help!"] - ], - lambda res: self.cbOptionalHelpAll(to, site, True) - ) - else: - site.settings["autodownloadoptional"] = False - self.response(to, False) diff --git a/plugins/OptionalManager/__init__.py b/plugins/OptionalManager/__init__.py deleted file mode 100644 index 77b8c348..00000000 --- a/plugins/OptionalManager/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from . import OptionalManagerPlugin -from . import UiWebsocketPlugin diff --git a/plugins/OptionalManager/languages/es.json b/plugins/OptionalManager/languages/es.json deleted file mode 100644 index 32ae46ae..00000000 --- a/plugins/OptionalManager/languages/es.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "Pinned %s files": "Archivos %s fijados", - "Removed pin from %s files": "Archivos %s que no estan fijados", - "You started to help distribute %s.
      Directory: %s": "Tu empezaste a ayudar a distribuir %s.
      Directorio: %s", - "Help distribute all new optional files on site %s": "Ayude a distribuir todos los archivos opcionales en el sitio %s", - "Yes, I want to help!": "¡Si, yo quiero ayudar!" -} diff --git a/plugins/OptionalManager/languages/fr.json b/plugins/OptionalManager/languages/fr.json deleted file mode 100644 index 47a563dc..00000000 --- a/plugins/OptionalManager/languages/fr.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "Pinned %s files": "Fichiers %s épinglés", - "Removed pin from %s files": "Fichiers %s ne sont plus épinglés", - "You started to help distribute %s.
      Directory: %s": "Vous avez commencé à aider à distribuer %s.
      Dossier : %s", - "Help distribute all new optional files on site %s": "Aider à distribuer tous les fichiers optionnels du site %s", - "Yes, I want to help!": "Oui, je veux aider !" -} diff --git a/plugins/OptionalManager/languages/hu.json b/plugins/OptionalManager/languages/hu.json deleted file mode 100644 index 7a23b86c..00000000 --- a/plugins/OptionalManager/languages/hu.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "Pinned %s files": "%s fájl rögzítve", - "Removed pin from %s files": "%s fájl rögzítés eltávolítva", - "You started to help distribute %s.
      Directory: %s": "Új segítség a terjesztésben: %s.
      Könyvtár: %s", - "Help distribute all new optional files on site %s": "Segítség az összes új opcionális fájl terjesztésében az %s oldalon", - "Yes, I want to help!": "Igen, segíteni akarok!" -} diff --git a/plugins/OptionalManager/languages/jp.json b/plugins/OptionalManager/languages/jp.json deleted file mode 100644 index af6dc79e..00000000 --- a/plugins/OptionalManager/languages/jp.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "Pinned %s files": "%s 件のファイルを固定", - "Removed pin from %s files": "%s 件のファイルの固定を解除", - "You started to help distribute %s.
      Directory: %s": "あなたはサイト: %s の配布の援助を開始しました。
      ディレクトリ: %s", - "Help distribute all new optional files on site %s": "サイト: %s のすべての新しいオプションファイルの配布を援助しますか?", - "Yes, I want to help!": "はい、やります!" -} diff --git a/plugins/OptionalManager/languages/pt-br.json b/plugins/OptionalManager/languages/pt-br.json deleted file mode 100644 index 21d90cc0..00000000 --- a/plugins/OptionalManager/languages/pt-br.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "Pinned %s files": "Arquivos %s fixados", - "Removed pin from %s files": "Arquivos %s não estão fixados", - "You started to help distribute %s.
      Directory: %s": "Você começou a ajudar a distribuir %s.
      Pasta: %s", - "Help distribute all new optional files on site %s": "Ajude a distribuir todos os novos arquivos opcionais no site %s", - "Yes, I want to help!": "Sim, eu quero ajudar!" -} diff --git a/plugins/OptionalManager/languages/zh-tw.json b/plugins/OptionalManager/languages/zh-tw.json deleted file mode 100644 index dfa9eaf3..00000000 --- a/plugins/OptionalManager/languages/zh-tw.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "Pinned %s files": "已固定 %s 個檔", - "Removed pin from %s files": "已解除固定 %s 個檔", - "You started to help distribute %s.
      Directory: %s": "你已經開始幫助分發 %s
      目錄:%s", - "Help distribute all new optional files on site %s": "你想要幫助分發 %s 網站的所有檔嗎?", - "Yes, I want to help!": "是,我想要幫助!" -} diff --git a/plugins/OptionalManager/languages/zh.json b/plugins/OptionalManager/languages/zh.json deleted file mode 100644 index ae18118e..00000000 --- a/plugins/OptionalManager/languages/zh.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "Pinned %s files": "已固定 %s 个文件", - "Removed pin from %s files": "已解除固定 %s 个文件", - "You started to help distribute %s.
      Directory: %s": "您已经开始帮助分发 %s
      目录:%s", - "Help distribute all new optional files on site %s": "您想要帮助分发 %s 站点的所有文件吗?", - "Yes, I want to help!": "是,我想要帮助!" -} diff --git a/plugins/PeerDb/PeerDbPlugin.py b/plugins/PeerDb/PeerDbPlugin.py deleted file mode 100644 index a66b81cf..00000000 --- a/plugins/PeerDb/PeerDbPlugin.py +++ /dev/null @@ -1,100 +0,0 @@ -import time -import sqlite3 -import random -import atexit - -import gevent -from Plugin import PluginManager - - -@PluginManager.registerTo("ContentDb") -class ContentDbPlugin(object): - def __init__(self, *args, **kwargs): - atexit.register(self.saveAllPeers) - super(ContentDbPlugin, self).__init__(*args, **kwargs) - - def getSchema(self): - schema = super(ContentDbPlugin, self).getSchema() - - schema["tables"]["peer"] = { - "cols": [ - ["site_id", "INTEGER REFERENCES site (site_id) ON DELETE CASCADE"], - ["address", "TEXT NOT NULL"], - ["port", "INTEGER NOT NULL"], - ["hashfield", "BLOB"], - ["reputation", "INTEGER NOT NULL"], - ["time_added", "INTEGER NOT NULL"], - ["time_found", "INTEGER NOT NULL"] - ], - "indexes": [ - "CREATE UNIQUE INDEX peer_key ON peer (site_id, address, port)" - ], - "schema_changed": 2 - } - - return schema - - def loadPeers(self, site): - s = time.time() - site_id = self.site_ids.get(site.address) - res = self.execute("SELECT * FROM peer WHERE site_id = :site_id", {"site_id": site_id}) - num = 0 - num_hashfield = 0 - for row in res: - peer = site.addPeer(str(row["address"]), row["port"]) - if not peer: # Already exist - continue - if row["hashfield"]: - peer.hashfield.replaceFromBytes(row["hashfield"]) - num_hashfield += 1 - peer.time_added = row["time_added"] - peer.time_found = row["time_found"] - peer.reputation = row["reputation"] - if row["address"].endswith(".onion"): - peer.reputation = peer.reputation / 2 - 1 # Onion peers less likely working - num += 1 - if num_hashfield: - site.content_manager.has_optional_files = True - site.log.debug("%s peers (%s with hashfield) loaded in %.3fs" % (num, num_hashfield, time.time() - s)) - - def iteratePeers(self, site): - site_id = self.site_ids.get(site.address) - for key, peer in list(site.peers.items()): - address, port = key.rsplit(":", 1) - if peer.has_hashfield: - hashfield = sqlite3.Binary(peer.hashfield.tobytes()) - else: - hashfield = "" - yield (site_id, address, port, hashfield, peer.reputation, int(peer.time_added), int(peer.time_found)) - - def savePeers(self, site, spawn=False): - if spawn: - # Save peers every hour (+random some secs to not update very site at same time) - site.greenlet_manager.spawnLater(60 * 60 + random.randint(0, 60), self.savePeers, site, spawn=True) - if not site.peers: - site.log.debug("Peers not saved: No peers found") - return - s = time.time() - site_id = self.site_ids.get(site.address) - cur = self.getCursor() - try: - cur.execute("DELETE FROM peer WHERE site_id = :site_id", {"site_id": site_id}) - cur.executemany( - "INSERT INTO peer (site_id, address, port, hashfield, reputation, time_added, time_found) VALUES (?, ?, ?, ?, ?, ?, ?)", - self.iteratePeers(site) - ) - except Exception as err: - site.log.error("Save peer error: %s" % err) - site.log.debug("Peers saved in %.3fs" % (time.time() - s)) - - def initSite(self, site): - super(ContentDbPlugin, self).initSite(site) - site.greenlet_manager.spawnLater(0.5, self.loadPeers, site) - site.greenlet_manager.spawnLater(60*60, self.savePeers, site, spawn=True) - - def saveAllPeers(self): - for site in list(self.sites.values()): - try: - self.savePeers(site) - except Exception as err: - site.log.error("Save peer error: %s" % err) diff --git a/plugins/PeerDb/__init__.py b/plugins/PeerDb/__init__.py deleted file mode 100644 index bc8c93b9..00000000 --- a/plugins/PeerDb/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from . import PeerDbPlugin - diff --git a/plugins/PeerDb/plugin_info.json b/plugins/PeerDb/plugin_info.json deleted file mode 100644 index b13915ff..00000000 --- a/plugins/PeerDb/plugin_info.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "PeerDb", - "description": "Save/restore peer list on client restart.", - "default": "enabled" -} \ No newline at end of file diff --git a/plugins/Sidebar/ConsolePlugin.py b/plugins/Sidebar/ConsolePlugin.py deleted file mode 100644 index 15f6a1ba..00000000 --- a/plugins/Sidebar/ConsolePlugin.py +++ /dev/null @@ -1,101 +0,0 @@ -import re -import logging - -from Plugin import PluginManager -from Config import config -from Debug import Debug -from util import SafeRe -from util.Flag import flag - - -class WsLogStreamer(logging.StreamHandler): - def __init__(self, stream_id, ui_websocket, filter): - self.stream_id = stream_id - self.ui_websocket = ui_websocket - - if filter: - if not SafeRe.isSafePattern(filter): - raise Exception("Not a safe prex pattern") - self.filter_re = re.compile(".*" + filter) - else: - self.filter_re = None - return super(WsLogStreamer, self).__init__() - - def emit(self, record): - if self.ui_websocket.ws.closed: - self.stop() - return - line = self.format(record) - if self.filter_re and not self.filter_re.match(line): - return False - - self.ui_websocket.cmd("logLineAdd", {"stream_id": self.stream_id, "lines": [line]}) - - def stop(self): - logging.getLogger('').removeHandler(self) - - -@PluginManager.registerTo("UiWebsocket") -class UiWebsocketPlugin(object): - def __init__(self, *args, **kwargs): - self.log_streamers = {} - return super(UiWebsocketPlugin, self).__init__(*args, **kwargs) - - @flag.no_multiuser - @flag.admin - def actionConsoleLogRead(self, to, filter=None, read_size=32 * 1024, limit=500): - log_file_path = "%s/debug.log" % config.log_dir - log_file = open(log_file_path, encoding="utf-8") - log_file.seek(0, 2) - end_pos = log_file.tell() - log_file.seek(max(0, end_pos - read_size)) - if log_file.tell() != 0: - log_file.readline() # Partial line junk - - pos_start = log_file.tell() - lines = [] - if filter: - assert SafeRe.isSafePattern(filter) - filter_re = re.compile(".*" + filter) - - last_match = False - for line in log_file: - if not line.startswith("[") and last_match: # Multi-line log entry - lines.append(line.replace(" ", " ")) - continue - - if filter and not filter_re.match(line): - last_match = False - continue - last_match = True - lines.append(line) - - num_found = len(lines) - lines = lines[-limit:] - - return {"lines": lines, "pos_end": log_file.tell(), "pos_start": pos_start, "num_found": num_found} - - def addLogStreamer(self, stream_id, filter=None): - logger = WsLogStreamer(stream_id, self, filter) - logger.setFormatter(logging.Formatter('[%(asctime)s] %(levelname)-8s %(name)s %(message)s')) - logger.setLevel(logging.getLevelName("DEBUG")) - - logging.getLogger('').addHandler(logger) - return logger - - @flag.no_multiuser - @flag.admin - def actionConsoleLogStream(self, to, filter=None): - stream_id = to - self.log_streamers[stream_id] = self.addLogStreamer(stream_id, filter) - self.response(to, {"stream_id": stream_id}) - - @flag.no_multiuser - @flag.admin - def actionConsoleLogStreamRemove(self, to, stream_id): - try: - self.log_streamers[stream_id].stop() - del self.log_streamers[stream_id] - return "ok" - except Exception as err: - return {"error": Debug.formatException(err)} diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py deleted file mode 100644 index 4ecca75a..00000000 --- a/plugins/Sidebar/SidebarPlugin.py +++ /dev/null @@ -1,805 +0,0 @@ -import re -import os -import html -import sys -import math -import time -import json -import io -import urllib -import urllib.parse - -import gevent - -import util -from Config import config -from Plugin import PluginManager -from Debug import Debug -from Translate import Translate -from util import helper -from util.Flag import flag -from .ZipStream import ZipStream - -plugin_dir = os.path.dirname(__file__) -media_dir = plugin_dir + "/media" - -loc_cache = {} -if "_" not in locals(): - _ = Translate(plugin_dir + "/languages/") - - -@PluginManager.registerTo("UiRequest") -class UiRequestPlugin(object): - # Inject our resources to end of original file streams - def actionUiMedia(self, path): - if path == "/uimedia/all.js" or path == "/uimedia/all.css": - # First yield the original file and header - body_generator = super(UiRequestPlugin, self).actionUiMedia(path) - for part in body_generator: - yield part - - # Append our media file to the end - ext = re.match(".*(js|css)$", path).group(1) - plugin_media_file = "%s/all.%s" % (media_dir, ext) - if config.debug: - # If debugging merge *.css to all.css and *.js to all.js - from Debug import DebugMedia - DebugMedia.merge(plugin_media_file) - if ext == "js": - yield _.translateData(open(plugin_media_file).read()).encode("utf8") - else: - for part in self.actionFile(plugin_media_file, send_header=False): - yield part - elif path.startswith("/uimedia/globe/"): # Serve WebGL globe files - file_name = re.match(".*/(.*)", path).group(1) - plugin_media_file = "%s_globe/%s" % (media_dir, file_name) - if config.debug and path.endswith("all.js"): - # If debugging merge *.css to all.css and *.js to all.js - from Debug import DebugMedia - DebugMedia.merge(plugin_media_file) - for part in self.actionFile(plugin_media_file): - yield part - else: - for part in super(UiRequestPlugin, self).actionUiMedia(path): - yield part - - def actionZip(self): - address = self.get["address"] - site = self.server.site_manager.get(address) - if not site: - return self.error404("Site not found") - - title = site.content_manager.contents.get("content.json", {}).get("title", "") - filename = "%s-backup-%s.zip" % (title, time.strftime("%Y-%m-%d_%H_%M")) - filename_quoted = urllib.parse.quote(filename) - self.sendHeader(content_type="application/zip", extra_headers={'Content-Disposition': 'attachment; filename="%s"' % filename_quoted}) - - return self.streamZip(site.storage.getPath(".")) - - def streamZip(self, dir_path): - zs = ZipStream(dir_path) - while 1: - data = zs.read() - if not data: - break - yield data - - -@PluginManager.registerTo("UiWebsocket") -class UiWebsocketPlugin(object): - def sidebarRenderPeerStats(self, body, site): - connected = len([peer for peer in list(site.peers.values()) if peer.connection and peer.connection.connected]) - connectable = len([peer_id for peer_id in list(site.peers.keys()) if not peer_id.endswith(":0")]) - onion = len([peer_id for peer_id in list(site.peers.keys()) if ".onion" in peer_id]) - local = len([peer for peer in list(site.peers.values()) if helper.isPrivateIp(peer.ip)]) - peers_total = len(site.peers) - - # Add myself - if site.isServing(): - peers_total += 1 - if any(site.connection_server.port_opened.values()): - connectable += 1 - if site.connection_server.tor_manager.start_onions: - onion += 1 - - if peers_total: - percent_connected = float(connected) / peers_total - percent_connectable = float(connectable) / peers_total - percent_onion = float(onion) / peers_total - else: - percent_connectable = percent_connected = percent_onion = 0 - - if local: - local_html = _("
    • {_[Local]}:{local}
    • ") - else: - local_html = "" - - peer_ips = [peer.key for peer in site.getConnectablePeers(20, allow_private=False)] - peer_ips.sort(key=lambda peer_ip: ".onion:" in peer_ip) - copy_link = "http://127.0.0.1:43110/%s/?zeronet_peers=%s" % ( - site.content_manager.contents.get("content.json", {}).get("domain", site.address), - ",".join(peer_ips) - ) - - body.append(_(""" -
    • - -
        -
      • -
      • -
      • -
      • -
      -
        -
      • {_[Connected]}:{connected}
      • -
      • {_[Connectable]}:{connectable}
      • -
      • {_[Onion]}:{onion}
      • - {local_html} -
      • {_[Total]}:{peers_total}
      • -
      -
    • - """.replace("{local_html}", local_html))) - - def sidebarRenderTransferStats(self, body, site): - recv = float(site.settings.get("bytes_recv", 0)) / 1024 / 1024 - sent = float(site.settings.get("bytes_sent", 0)) / 1024 / 1024 - transfer_total = recv + sent - if transfer_total: - percent_recv = recv / transfer_total - percent_sent = sent / transfer_total - else: - percent_recv = 0.5 - percent_sent = 0.5 - - body.append(_(""" -
    • - -
        -
      • -
      • -
      -
        -
      • {_[Received]}:{recv:.2f}MB
      • -
      • {_[Sent]}:{sent:.2f}MB
      • -
      -
    • - """)) - - def sidebarRenderFileStats(self, body, site): - body.append(_(""" -
    • - -
        - """)) - - extensions = ( - ("html", "yellow"), - ("css", "orange"), - ("js", "purple"), - ("Image", "green"), - ("json", "darkblue"), - ("User data", "blue"), - ("Other", "white"), - ("Total", "black") - ) - # Collect stats - size_filetypes = {} - size_total = 0 - contents = site.content_manager.listContents() # Without user files - for inner_path in contents: - content = site.content_manager.contents[inner_path] - if "files" not in content or content["files"] is None: - continue - for file_name, file_details in list(content["files"].items()): - size_total += file_details["size"] - ext = file_name.split(".")[-1] - size_filetypes[ext] = size_filetypes.get(ext, 0) + file_details["size"] - - # Get user file sizes - size_user_content = site.content_manager.contents.execute( - "SELECT SUM(size) + SUM(size_files) AS size FROM content WHERE ?", - {"not__inner_path": contents} - ).fetchone()["size"] - if not size_user_content: - size_user_content = 0 - size_filetypes["User data"] = size_user_content - size_total += size_user_content - - # The missing difference is content.json sizes - if "json" in size_filetypes: - size_filetypes["json"] += max(0, site.settings["size"] - size_total) - size_total = size_other = site.settings["size"] - - # Bar - for extension, color in extensions: - if extension == "Total": - continue - if extension == "Other": - size = max(0, size_other) - elif extension == "Image": - size = size_filetypes.get("jpg", 0) + size_filetypes.get("png", 0) + size_filetypes.get("gif", 0) - size_other -= size - else: - size = size_filetypes.get(extension, 0) - size_other -= size - if size_total == 0: - percent = 0 - else: - percent = 100 * (float(size) / size_total) - percent = math.floor(percent * 100) / 100 # Floor to 2 digits - body.append( - """
      • """ % - (percent, _[extension], color, _[extension]) - ) - - # Legend - body.append("
        ") - for extension, color in extensions: - if extension == "Other": - size = max(0, size_other) - elif extension == "Image": - size = size_filetypes.get("jpg", 0) + size_filetypes.get("png", 0) + size_filetypes.get("gif", 0) - elif extension == "Total": - size = size_total - else: - size = size_filetypes.get(extension, 0) - - if extension == "js": - title = "javascript" - else: - title = extension - - if size > 1024 * 1024 * 10: # Format as mB is more than 10mB - size_formatted = "%.0fMB" % (size / 1024 / 1024) - else: - size_formatted = "%.0fkB" % (size / 1024) - - body.append("
      • %s:%s
      • " % (color, _[title], size_formatted)) - - body.append("
    • ") - - def sidebarRenderSizeLimit(self, body, site): - free_space = helper.getFreeSpace() / 1024 / 1024 - size = float(site.settings["size"]) / 1024 / 1024 - size_limit = site.getSizeLimit() - percent_used = size / size_limit - - body.append(_(""" -
    • - - MB - {_[Set]} -
    • - """)) - - def sidebarRenderOptionalFileStats(self, body, site): - size_total = float(site.settings["size_optional"]) - size_downloaded = float(site.settings["optional_downloaded"]) - - if not size_total: - return False - - percent_downloaded = size_downloaded / size_total - - size_formatted_total = size_total / 1024 / 1024 - size_formatted_downloaded = size_downloaded / 1024 / 1024 - - body.append(_(""" -
    • - -
        -
      • -
      • -
      -
        -
      • {_[Downloaded]}:{size_formatted_downloaded:.2f}MB
      • -
      • {_[Total]}:{size_formatted_total:.2f}MB
      • -
      -
    • - """)) - - return True - - def sidebarRenderOptionalFileSettings(self, body, site): - if self.site.settings.get("autodownloadoptional"): - checked = "checked='checked'" - else: - checked = "" - - body.append(_(""" -
    • - -
      - """)) - - if hasattr(config, "autodownload_bigfile_size_limit"): - autodownload_bigfile_size_limit = int(site.settings.get("autodownload_bigfile_size_limit", config.autodownload_bigfile_size_limit)) - body.append(_(""" -
      - - MB - {_[Set]} - {_[Download previous files]} -
      - """)) - body.append("
    • ") - - def sidebarRenderBadFiles(self, body, site): - body.append(_(""" -
    • - -
        - """)) - - i = 0 - for bad_file, tries in site.bad_files.items(): - i += 1 - body.append(_("""
      • {bad_filename}
      • """, { - "bad_file_path": bad_file, - "bad_filename": helper.getFilename(bad_file), - "tries": _.pluralize(tries, "{} try", "{} tries") - })) - if i > 30: - break - - if len(site.bad_files) > 30: - num_bad_files = len(site.bad_files) - 30 - body.append(_("""
      • {_[+ {num_bad_files} more]}
      • """, nested=True)) - - body.append(""" -
      -
    • - """) - - def sidebarRenderDbOptions(self, body, site): - if site.storage.db: - inner_path = site.storage.getInnerPath(site.storage.db.db_path) - size = float(site.storage.getSize(inner_path)) / 1024 - feeds = len(site.storage.db.schema.get("feeds", {})) - else: - inner_path = _["No database found"] - size = 0.0 - feeds = 0 - - body.append(_(""" -
    • - - -
    • - """, nested=True)) - - def sidebarRenderIdentity(self, body, site): - auth_address = self.user.getAuthAddress(self.site.address, create=False) - rules = self.site.content_manager.getRules("data/users/%s/content.json" % auth_address) - if rules and rules.get("max_size"): - quota = rules["max_size"] / 1024 - try: - content = site.content_manager.contents["data/users/%s/content.json" % auth_address] - used = len(json.dumps(content)) + sum([file["size"] for file in list(content["files"].values())]) - except: - used = 0 - used = used / 1024 - else: - quota = used = 0 - - body.append(_(""" -
    • - -
      - {auth_address} - {_[Change]} -
      -
    • - """)) - - def sidebarRenderControls(self, body, site): - auth_address = self.user.getAuthAddress(self.site.address, create=False) - if self.site.settings["serving"]: - class_pause = "" - class_resume = "hidden" - else: - class_pause = "hidden" - class_resume = "" - - body.append(_(""" -
    • - - {_[Update]} - {_[Pause]} - {_[Resume]} - {_[Delete]} -
    • - """)) - - donate_key = site.content_manager.contents.get("content.json", {}).get("donate", True) - site_address = self.site.address - body.append(_(""" -
    • -
      -
      - {site_address} - """)) - if donate_key == False or donate_key == "": - pass - elif (type(donate_key) == str or type(donate_key) == str) and len(donate_key) > 0: - body.append(_(""" -
      -
    • -
    • -
      -
      - {donate_key} - """)) - else: - body.append(_(""" - {_[Donate]} - """)) - body.append(_(""" -
      -
    • - """)) - - def sidebarRenderOwnedCheckbox(self, body, site): - if self.site.settings["own"]: - checked = "checked='checked'" - else: - checked = "" - - body.append(_(""" -

      {_[This is my site]}

      -
      - """)) - - def sidebarRenderOwnSettings(self, body, site): - title = site.content_manager.contents.get("content.json", {}).get("title", "") - description = site.content_manager.contents.get("content.json", {}).get("description", "") - - body.append(_(""" -
    • - - -
    • - -
    • - - -
    • - -
    • - {_[Save site settings]} -
    • - """)) - - def sidebarRenderContents(self, body, site): - has_privatekey = bool(self.user.getSiteData(site.address, create=False).get("privatekey")) - if has_privatekey: - tag_privatekey = _("{_[Private key saved.]} {_[Forget]}") - else: - tag_privatekey = _("{_[Add saved private key]}") - - body.append(_(""" -
    • - - """.replace("{tag_privatekey}", tag_privatekey))) - - # Choose content you want to sign - body.append(_(""" - - """)) - - contents = ["content.json"] - contents += list(site.content_manager.contents.get("content.json", {}).get("includes", {}).keys()) - body.append(_("
      {_[Choose]}: ")) - for content in contents: - body.append(_("{content} ")) - body.append("
      ") - body.append("
    • ") - - @flag.admin - def actionSidebarGetHtmlTag(self, to): - site = self.site - - body = [] - - body.append("
      ") - body.append("×") - body.append("

      %s

      " % html.escape(site.content_manager.contents.get("content.json", {}).get("title", ""), True)) - - body.append("
      ") - - body.append("
        ") - - self.sidebarRenderPeerStats(body, site) - self.sidebarRenderTransferStats(body, site) - self.sidebarRenderFileStats(body, site) - self.sidebarRenderSizeLimit(body, site) - has_optional = self.sidebarRenderOptionalFileStats(body, site) - if has_optional: - self.sidebarRenderOptionalFileSettings(body, site) - self.sidebarRenderDbOptions(body, site) - self.sidebarRenderIdentity(body, site) - self.sidebarRenderControls(body, site) - if site.bad_files: - self.sidebarRenderBadFiles(body, site) - - self.sidebarRenderOwnedCheckbox(body, site) - body.append("
        ") - self.sidebarRenderOwnSettings(body, site) - self.sidebarRenderContents(body, site) - body.append("
        ") - body.append("
      ") - body.append("
      ") - - body.append("") - - self.response(to, "".join(body)) - - def downloadGeoLiteDb(self, db_path): - import gzip - import shutil - from util import helper - - if config.offline: - return False - - self.log.info("Downloading GeoLite2 City database...") - self.cmd("progress", ["geolite-info", _["Downloading GeoLite2 City database (one time only, ~20MB)..."], 0]) - db_urls = [ - "https://raw.githubusercontent.com/aemr3/GeoLite2-Database/master/GeoLite2-City.mmdb.gz", - "https://raw.githubusercontent.com/texnikru/GeoLite2-Database/master/GeoLite2-City.mmdb.gz" - ] - for db_url in db_urls: - downloadl_err = None - try: - # Download - response = helper.httpRequest(db_url) - data_size = response.getheader('content-length') - data_recv = 0 - data = io.BytesIO() - while True: - buff = response.read(1024 * 512) - if not buff: - break - data.write(buff) - data_recv += 1024 * 512 - if data_size: - progress = int(float(data_recv) / int(data_size) * 100) - self.cmd("progress", ["geolite-info", _["Downloading GeoLite2 City database (one time only, ~20MB)..."], progress]) - self.log.info("GeoLite2 City database downloaded (%s bytes), unpacking..." % data.tell()) - data.seek(0) - - # Unpack - with gzip.GzipFile(fileobj=data) as gzip_file: - shutil.copyfileobj(gzip_file, open(db_path, "wb")) - - self.cmd("progress", ["geolite-info", _["GeoLite2 City database downloaded!"], 100]) - time.sleep(2) # Wait for notify animation - self.log.info("GeoLite2 City database is ready at: %s" % db_path) - return True - except Exception as err: - download_err = err - self.log.error("Error downloading %s: %s" % (db_url, err)) - pass - self.cmd("progress", [ - "geolite-info", - _["GeoLite2 City database download error: {}!
      Please download manually and unpack to data dir:
      {}"].format(download_err, db_urls[0]), - -100 - ]) - - def getLoc(self, geodb, ip): - global loc_cache - - if ip in loc_cache: - return loc_cache[ip] - else: - try: - loc_data = geodb.get(ip) - except: - loc_data = None - - if not loc_data or "location" not in loc_data: - loc_cache[ip] = None - return None - - loc = { - "lat": loc_data["location"]["latitude"], - "lon": loc_data["location"]["longitude"], - } - if "city" in loc_data: - loc["city"] = loc_data["city"]["names"]["en"] - - if "country" in loc_data: - loc["country"] = loc_data["country"]["names"]["en"] - - loc_cache[ip] = loc - return loc - - @util.Noparallel() - def getGeoipDb(self): - db_name = 'GeoLite2-City.mmdb' - - sys_db_paths = [] - if sys.platform == "linux": - sys_db_paths += ['/usr/share/GeoIP/' + db_name] - - data_dir_db_path = os.path.join(config.data_dir, db_name) - - db_paths = sys_db_paths + [data_dir_db_path] - - for path in db_paths: - if os.path.isfile(path) and os.path.getsize(path) > 0: - return path - - self.log.info("GeoIP database not found at [%s]. Downloading to: %s", - " ".join(db_paths), data_dir_db_path) - if self.downloadGeoLiteDb(data_dir_db_path): - return data_dir_db_path - return None - - def getPeerLocations(self, peers): - import maxminddb - - db_path = self.getGeoipDb() - if not db_path: - self.log.debug("Not showing peer locations: no GeoIP database") - return False - - geodb = maxminddb.open_database(db_path) - - peers = list(peers.values()) - # Place bars - peer_locations = [] - placed = {} # Already placed bars here - for peer in peers: - # Height of bar - if peer.connection and peer.connection.last_ping_delay: - ping = round(peer.connection.last_ping_delay * 1000) - else: - ping = None - loc = self.getLoc(geodb, peer.ip) - - if not loc: - continue - # Create position array - lat, lon = loc["lat"], loc["lon"] - latlon = "%s,%s" % (lat, lon) - if latlon in placed and helper.getIpType(peer.ip) == "ipv4": # Dont place more than 1 bar to same place, fake repos using ip address last two part - lat += float(128 - int(peer.ip.split(".")[-2])) / 50 - lon += float(128 - int(peer.ip.split(".")[-1])) / 50 - latlon = "%s,%s" % (lat, lon) - placed[latlon] = True - peer_location = {} - peer_location.update(loc) - peer_location["lat"] = lat - peer_location["lon"] = lon - peer_location["ping"] = ping - - peer_locations.append(peer_location) - - # Append myself - for ip in self.site.connection_server.ip_external_list: - my_loc = self.getLoc(geodb, ip) - if my_loc: - my_loc["ping"] = 0 - peer_locations.append(my_loc) - - return peer_locations - - @flag.admin - @flag.async_run - def actionSidebarGetPeers(self, to): - try: - peer_locations = self.getPeerLocations(self.site.peers) - globe_data = [] - ping_times = [ - peer_location["ping"] - for peer_location in peer_locations - if peer_location["ping"] - ] - if ping_times: - ping_avg = sum(ping_times) / float(len(ping_times)) - else: - ping_avg = 0 - - for peer_location in peer_locations: - if peer_location["ping"] == 0: # Me - height = -0.135 - elif peer_location["ping"]: - height = min(0.20, math.log(1 + peer_location["ping"] / ping_avg, 300)) - else: - height = -0.03 - - globe_data += [peer_location["lat"], peer_location["lon"], height] - - self.response(to, globe_data) - except Exception as err: - self.log.debug("sidebarGetPeers error: %s" % Debug.formatException(err)) - self.response(to, {"error": str(err)}) - - @flag.admin - @flag.no_multiuser - def actionSiteSetOwned(self, to, owned): - if self.site.address == config.updatesite: - return {"error": "You can't change the ownership of the updater site"} - - self.site.settings["own"] = bool(owned) - self.site.updateWebsocket(owned=owned) - return "ok" - - @flag.admin - @flag.no_multiuser - def actionSiteRecoverPrivatekey(self, to): - from Crypt import CryptBitcoin - - site_data = self.user.sites[self.site.address] - if site_data.get("privatekey"): - return {"error": "This site already has saved privated key"} - - address_index = self.site.content_manager.contents.get("content.json", {}).get("address_index") - if not address_index: - return {"error": "No address_index in content.json"} - - privatekey = CryptBitcoin.hdPrivatekey(self.user.master_seed, address_index) - privatekey_address = CryptBitcoin.privatekeyToAddress(privatekey) - - if privatekey_address == self.site.address: - site_data["privatekey"] = privatekey - self.user.save() - self.site.updateWebsocket(recover_privatekey=True) - return "ok" - else: - return {"error": "Unable to deliver private key for this site from current user's master_seed"} - - @flag.admin - @flag.no_multiuser - def actionUserSetSitePrivatekey(self, to, privatekey): - site_data = self.user.sites[self.site.address] - site_data["privatekey"] = privatekey - self.site.updateWebsocket(set_privatekey=bool(privatekey)) - self.user.save() - - return "ok" - - @flag.admin - @flag.no_multiuser - def actionSiteSetAutodownloadoptional(self, to, owned): - self.site.settings["autodownloadoptional"] = bool(owned) - self.site.worker_manager.removeSolvedFileTasks() - - @flag.no_multiuser - @flag.admin - def actionDbReload(self, to): - self.site.storage.closeDb() - self.site.storage.getDb() - - return self.response(to, "ok") - - @flag.no_multiuser - @flag.admin - def actionDbRebuild(self, to): - try: - self.site.storage.rebuildDb() - except Exception as err: - return self.response(to, {"error": str(err)}) - - - return self.response(to, "ok") diff --git a/plugins/Sidebar/ZipStream.py b/plugins/Sidebar/ZipStream.py deleted file mode 100644 index b6e05b21..00000000 --- a/plugins/Sidebar/ZipStream.py +++ /dev/null @@ -1,59 +0,0 @@ -import io -import os -import zipfile - -class ZipStream(object): - def __init__(self, dir_path): - self.dir_path = dir_path - self.pos = 0 - self.buff_pos = 0 - self.zf = zipfile.ZipFile(self, 'w', zipfile.ZIP_DEFLATED, allowZip64=True) - self.buff = io.BytesIO() - self.file_list = self.getFileList() - - def getFileList(self): - for root, dirs, files in os.walk(self.dir_path): - for file in files: - file_path = root + "/" + file - relative_path = os.path.join(os.path.relpath(root, self.dir_path), file) - yield file_path, relative_path - self.zf.close() - - def read(self, size=60 * 1024): - for file_path, relative_path in self.file_list: - self.zf.write(file_path, relative_path) - if self.buff.tell() >= size: - break - self.buff.seek(0) - back = self.buff.read() - self.buff.truncate(0) - self.buff.seek(0) - self.buff_pos += len(back) - return back - - def write(self, data): - self.pos += len(data) - self.buff.write(data) - - def tell(self): - return self.pos - - def seek(self, pos, whence=0): - if pos >= self.buff_pos: - self.buff.seek(pos - self.buff_pos, whence) - self.pos = pos - - def flush(self): - pass - - -if __name__ == "__main__": - zs = ZipStream(".") - out = open("out.zip", "wb") - while 1: - data = zs.read() - print("Write %s" % len(data)) - if not data: - break - out.write(data) - out.close() diff --git a/plugins/Sidebar/__init__.py b/plugins/Sidebar/__init__.py deleted file mode 100644 index be7f14e1..00000000 --- a/plugins/Sidebar/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from . import SidebarPlugin -from . import ConsolePlugin \ No newline at end of file diff --git a/plugins/Sidebar/languages/da.json b/plugins/Sidebar/languages/da.json deleted file mode 100644 index a421292c..00000000 --- a/plugins/Sidebar/languages/da.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "Peers": "Klienter", - "Connected": "Forbundet", - "Connectable": "Mulige", - "Connectable peers": "Mulige klienter", - - "Data transfer": "Data overførsel", - "Received": "Modtaget", - "Received bytes": "Bytes modtaget", - "Sent": "Sendt", - "Sent bytes": "Bytes sendt", - - "Files": "Filer", - "Total": "I alt", - "Image": "Image", - "Other": "Andet", - "User data": "Bruger data", - - "Size limit": "Side max størrelse", - "limit used": "brugt", - "free space": "fri", - "Set": "Opdater", - - "Optional files": "Valgfri filer", - "Downloaded": "Downloadet", - "Download and help distribute all files": "Download og hjælp med at dele filer", - "Total size": "Størrelse i alt", - "Downloaded files": "Filer downloadet", - - "Database": "Database", - "search feeds": "søgninger", - "{feeds} query": "{feeds} søgninger", - "Reload": "Genindlæs", - "Rebuild": "Genopbyg", - "No database found": "Ingen database fundet", - - "Identity address": "Autorisations ID", - "Change": "Skift", - - "Update": "Opdater", - "Pause": "Pause", - "Resume": "Aktiv", - "Delete": "Slet", - "Are you sure?": "Er du sikker?", - - "Site address": "Side addresse", - "Donate": "Doner penge", - - "Missing files": "Manglende filer", - "{} try": "{} forsøg", - "{} tries": "{} forsøg", - "+ {num_bad_files} more": "+ {num_bad_files} mere", - - "This is my site": "Dette er min side", - "Site title": "Side navn", - "Site description": "Side beskrivelse", - "Save site settings": "Gem side opsætning", - - "Content publishing": "Indhold offentliggøres", - "Choose": "Vælg", - "Sign": "Signer", - "Publish": "Offentliggør", - - "This function is disabled on this proxy": "Denne funktion er slået fra på denne ZeroNet proxyEz a funkció ki van kapcsolva ezen a proxy-n", - "GeoLite2 City database download error: {}!
      Please download manually and unpack to data dir:
      {}": "GeoLite2 City database kunne ikke downloades: {}!
      Download venligst databasen manuelt og udpak i data folder:
      {}", - "Downloading GeoLite2 City database (one time only, ~20MB)...": "GeoLite2 város adatbázis letöltése (csak egyszer kell, kb 20MB)...", - "GeoLite2 City database downloaded!": "GeoLite2 City database downloadet!", - - "Are you sure?": "Er du sikker?", - "Site storage limit modified!": "Side max størrelse ændret!", - "Database schema reloaded!": "Database definition genindlæst!", - "Database rebuilding....": "Genopbygger database...", - "Database rebuilt!": "Database genopbygget!", - "Site updated!": "Side opdateret!", - "Delete this site": "Slet denne side", - "File write error: ": "Fejl ved skrivning af fil: ", - "Site settings saved!": "Side opsætning gemt!", - "Enter your private key:": "Indtast din private nøgle:", - " Signed!": " Signeret!", - "WebGL not supported": "WebGL er ikke supporteret" -} \ No newline at end of file diff --git a/plugins/Sidebar/languages/de.json b/plugins/Sidebar/languages/de.json deleted file mode 100644 index 2f5feacd..00000000 --- a/plugins/Sidebar/languages/de.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "Peers": "Peers", - "Connected": "Verbunden", - "Connectable": "Verbindbar", - "Connectable peers": "Verbindbare Peers", - - "Data transfer": "Datei Transfer", - "Received": "Empfangen", - "Received bytes": "Empfangene Bytes", - "Sent": "Gesendet", - "Sent bytes": "Gesendete Bytes", - - "Files": "Dateien", - "Total": "Gesamt", - "Image": "Bilder", - "Other": "Sonstiges", - "User data": "Nutzer Daten", - - "Size limit": "Speicher Limit", - "limit used": "Limit benutzt", - "free space": "freier Speicher", - "Set": "Setzten", - - "Optional files": "Optionale Dateien", - "Downloaded": "Heruntergeladen", - "Download and help distribute all files": "Herunterladen und helfen alle Dateien zu verteilen", - "Total size": "Gesamte Größe", - "Downloaded files": "Heruntergeladene Dateien", - - "Database": "Datenbank", - "search feeds": "Feeds durchsuchen", - "{feeds} query": "{feeds} Abfrage", - "Reload": "Neu laden", - "Rebuild": "Neu bauen", - "No database found": "Keine Datenbank gefunden", - - "Identity address": "Identitäts Adresse", - "Change": "Ändern", - - "Update": "Aktualisieren", - "Pause": "Pausieren", - "Resume": "Fortsetzen", - "Delete": "Löschen", - "Are you sure?": "Bist du sicher?", - - "Site address": "Seiten Adresse", - "Donate": "Spenden", - - "Missing files": "Fehlende Dateien", - "{} try": "{} versuch", - "{} tries": "{} versuche", - "+ {num_bad_files} more": "+ {num_bad_files} mehr", - - "This is my site": "Das ist meine Seite", - "Site title": "Seiten Titel", - "Site description": "Seiten Beschreibung", - "Save site settings": "Einstellungen der Seite speichern", - - "Content publishing": "Inhaltsveröffentlichung", - "Choose": "Wähle", - "Sign": "Signieren", - "Publish": "Veröffentlichen", - - "This function is disabled on this proxy": "Diese Funktion ist auf dieser Proxy deaktiviert", - "GeoLite2 City database download error: {}!
      Please download manually and unpack to data dir:
      {}": "GeoLite2 City Datenbank Download Fehler: {}!
      Bitte manuell herunterladen und die Datei in das Datei Verzeichnis extrahieren:
      {}", - "Downloading GeoLite2 City database (one time only, ~20MB)...": "Herunterladen der GeoLite2 City Datenbank (einmalig, ~20MB)...", - "GeoLite2 City database downloaded!": "GeoLite2 City Datenbank heruntergeladen!", - - "Are you sure?": "Bist du sicher?", - "Site storage limit modified!": "Speicher Limit der Seite modifiziert!", - "Database schema reloaded!": "Datebank Schema neu geladen!", - "Database rebuilding....": "Datenbank neu bauen...", - "Database rebuilt!": "Datenbank neu gebaut!", - "Site updated!": "Seite aktualisiert!", - "Delete this site": "Diese Seite löschen", - "File write error: ": "Datei schreib fehler:", - "Site settings saved!": "Seiten Einstellungen gespeichert!", - "Enter your private key:": "Gib deinen privaten Schlüssel ein:", - " Signed!": " Signiert!", - "WebGL not supported": "WebGL nicht unterstützt" -} diff --git a/plugins/Sidebar/languages/es.json b/plugins/Sidebar/languages/es.json deleted file mode 100644 index b9e98c46..00000000 --- a/plugins/Sidebar/languages/es.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "Peers": "Pares", - "Connected": "Conectados", - "Connectable": "Conectables", - "Connectable peers": "Pares conectables", - - "Data transfer": "Transferencia de datos", - "Received": "Recibidos", - "Received bytes": "Bytes recibidos", - "Sent": "Enviados", - "Sent bytes": "Bytes envidados", - - "Files": "Ficheros", - "Total": "Total", - "Image": "Imagen", - "Other": "Otro", - "User data": "Datos del usuario", - - "Size limit": "Límite de tamaño", - "limit used": "Límite utilizado", - "free space": "Espacio libre", - "Set": "Establecer", - - "Optional files": "Ficheros opcionales", - "Downloaded": "Descargado", - "Download and help distribute all files": "Descargar y ayudar a distribuir todos los ficheros", - "Total size": "Tamaño total", - "Downloaded files": "Ficheros descargados", - - "Database": "Base de datos", - "search feeds": "Fuentes de búsqueda", - "{feeds} query": "{feeds} consulta", - "Reload": "Recargar", - "Rebuild": "Reconstruir", - "No database found": "No se ha encontrado la base de datos", - - "Identity address": "Dirección de la identidad", - "Change": "Cambiar", - - "Update": "Actualizar", - "Pause": "Pausar", - "Resume": "Reanudar", - "Delete": "Borrar", - - "Site address": "Dirección del sitio", - "Donate": "Donar", - - "Missing files": "Ficheros perdidos", - "{} try": "{} intento", - "{} tries": "{} intentos", - "+ {num_bad_files} more": "+ {num_bad_files} más", - - "This is my site": "Este es mi sitio", - "Site title": "Título del sitio", - "Site description": "Descripción del sitio", - "Save site settings": "Guardar la configuración del sitio", - - "Content publishing": "Publicación del contenido", - "Choose": "Elegir", - "Sign": "Firmar", - "Publish": "Publicar", - "This function is disabled on this proxy": "Esta función está deshabilitada en este proxy", - "GeoLite2 City database download error: {}!
      Please download manually and unpack to data dir:
      {}": "¡Error de la base de datos GeoLite2: {}!
      Por favor, descárgalo manualmente y descomprime al directorio de datos:
      {}", - "Downloading GeoLite2 City database (one time only, ~20MB)...": "Descargando la base de datos de GeoLite2 (una única vez, ~20MB)...", - "GeoLite2 City database downloaded!": "¡Base de datos de GeoLite2 descargada!", - - "Are you sure?": "¿Estás seguro?", - "Site storage limit modified!": "¡Límite de almacenamiento del sitio modificado!", - "Database schema reloaded!": "¡Esquema de la base de datos recargado!", - "Database rebuilding....": "Reconstruyendo la base de datos...", - "Database rebuilt!": "¡Base de datos reconstruida!", - "Site updated!": "¡Sitio actualizado!", - "Delete this site": "Borrar este sitio", - "File write error: ": "Error de escritura de fichero:", - "Site settings saved!": "¡Configuración del sitio guardada!", - "Enter your private key:": "Introduce tu clave privada:", - " Signed!": " ¡firmado!", - "WebGL not supported": "WebGL no está soportado" -} diff --git a/plugins/Sidebar/languages/fr.json b/plugins/Sidebar/languages/fr.json deleted file mode 100644 index 5c4b3ac7..00000000 --- a/plugins/Sidebar/languages/fr.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "Peers": "Pairs", - "Connected": "Connectés", - "Connectable": "Accessibles", - "Connectable peers": "Pairs accessibles", - - "Data transfer": "Données transférées", - "Received": "Reçues", - "Received bytes": "Bytes reçus", - "Sent": "Envoyées", - "Sent bytes": "Bytes envoyés", - - "Files": "Fichiers", - "Total": "Total", - "Image": "Image", - "Other": "Autre", - "User data": "Utilisateurs", - - "Size limit": "Taille maximale", - "limit used": "utlisé", - "free space": "libre", - "Set": "Modifier", - - "Optional files": "Fichiers optionnels", - "Downloaded": "Téléchargé", - "Download and help distribute all files": "Télécharger et distribuer tous les fichiers", - "Total size": "Taille totale", - "Downloaded files": "Fichiers téléchargés", - - "Database": "Base de données", - "search feeds": "recherche", - "{feeds} query": "{feeds} requête", - "Reload": "Recharger", - "Rebuild": "Reconstruire", - "No database found": "Aucune base de données trouvée", - - "Identity address": "Adresse d'identité", - "Change": "Modifier", - - "Site control": "Opérations", - "Update": "Mettre à jour", - "Pause": "Suspendre", - "Resume": "Reprendre", - "Delete": "Supprimer", - "Are you sure?": "Êtes-vous certain?", - - "Site address": "Adresse du site", - "Donate": "Faire un don", - - "Missing files": "Fichiers manquants", - "{} try": "{} essai", - "{} tries": "{} essais", - "+ {num_bad_files} more": "+ {num_bad_files} manquants", - - "This is my site": "Ce site m'appartient", - "Site title": "Nom du site", - "Site description": "Description du site", - "Save site settings": "Enregistrer les paramètres", - - "Content publishing": "Publication du contenu", - "Choose": "Sélectionner", - "Sign": "Signer", - "Publish": "Publier", - - "This function is disabled on this proxy": "Cette fonction est désactivé sur ce proxy", - "GeoLite2 City database download error: {}!
      Please download manually and unpack to data dir:
      {}": "Erreur au téléchargement de la base de données GeoLite2: {}!
      Téléchargez et décompressez dans le dossier data:
      {}", - "Downloading GeoLite2 City database (one time only, ~20MB)...": "Téléchargement de la base de données GeoLite2 (une seule fois, ~20MB)...", - "GeoLite2 City database downloaded!": "Base de données GeoLite2 téléchargée!", - - "Are you sure?": "Êtes-vous certain?", - "Site storage limit modified!": "Taille maximale modifiée!", - "Database schema reloaded!": "Base de données rechargée!", - "Database rebuilding....": "Reconstruction de la base de données...", - "Database rebuilt!": "Base de données reconstruite!", - "Site updated!": "Site mis à jour!", - "Delete this site": "Supprimer ce site", - "File write error: ": "Erreur à l'écriture du fichier: ", - "Site settings saved!": "Paramètres du site enregistrés!", - "Enter your private key:": "Entrez votre clé privée:", - " Signed!": " Signé!", - "WebGL not supported": "WebGL n'est pas supporté" -} diff --git a/plugins/Sidebar/languages/hu.json b/plugins/Sidebar/languages/hu.json deleted file mode 100644 index 21216825..00000000 --- a/plugins/Sidebar/languages/hu.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "Peers": "Csatlakozási pontok", - "Connected": "Csaltakozva", - "Connectable": "Csatlakozható", - "Connectable peers": "Csatlakozható peer-ek", - - "Data transfer": "Adatátvitel", - "Received": "Fogadott", - "Received bytes": "Fogadott byte-ok", - "Sent": "Küldött", - "Sent bytes": "Küldött byte-ok", - - "Files": "Fájlok", - "Total": "Összesen", - "Image": "Kép", - "Other": "Egyéb", - "User data": "Felh. adat", - - "Size limit": "Méret korlát", - "limit used": "felhasznált", - "free space": "szabad hely", - "Set": "Beállít", - - "Optional files": "Opcionális fájlok", - "Downloaded": "Letöltött", - "Download and help distribute all files": "Minden opcionális fájl letöltése", - "Total size": "Teljes méret", - "Downloaded files": "Letöltve", - - "Database": "Adatbázis", - "search feeds": "Keresés források", - "{feeds} query": "{feeds} lekérdezés", - "Reload": "Újratöltés", - "Rebuild": "Újraépítés", - "No database found": "Adatbázis nem található", - - "Identity address": "Azonosító cím", - "Change": "Módosít", - - "Site control": "Oldal műveletek", - "Update": "Frissít", - "Pause": "Szünteltet", - "Resume": "Folytat", - "Delete": "Töröl", - "Are you sure?": "Biztos vagy benne?", - - "Site address": "Oldal címe", - "Donate": "Támogatás", - - "Missing files": "Hiányzó fájlok", - "{} try": "{} próbálkozás", - "{} tries": "{} próbálkozás", - "+ {num_bad_files} more": "+ még {num_bad_files} darab", - - "This is my site": "Ez az én oldalam", - "Site title": "Oldal neve", - "Site description": "Oldal leírása", - "Save site settings": "Oldal beállítások mentése", - - "Content publishing": "Tartalom publikálás", - "Choose": "Válassz", - "Sign": "Aláírás", - "Publish": "Publikálás", - - "This function is disabled on this proxy": "Ez a funkció ki van kapcsolva ezen a proxy-n", - "GeoLite2 City database download error: {}!
      Please download manually and unpack to data dir:
      {}": "GeoLite2 város adatbázis letöltési hiba: {}!
      A térképhez töltsd le és csomagold ki a data könyvtárba:
      {}", - "Downloading GeoLite2 City database (one time only, ~20MB)...": "GeoLite2 város adatbázis letöltése (csak egyszer kell, kb 20MB)...", - "GeoLite2 City database downloaded!": "GeoLite2 város adatbázis letöltve!", - - "Are you sure?": "Biztos vagy benne?", - "Site storage limit modified!": "Az oldalt méret korlát módosítva!", - "Database schema reloaded!": "Adatbázis séma újratöltve!", - "Database rebuilding....": "Adatbázis újraépítés...", - "Database rebuilt!": "Adatbázis újraépítve!", - "Site updated!": "Az oldal frissítve!", - "Delete this site": "Az oldal törlése", - "File write error: ": "Fájl írási hiba: ", - "Site settings saved!": "Az oldal beállításai elmentve!", - "Enter your private key:": "Add meg a privát kulcsod:", - " Signed!": " Aláírva!", - "WebGL not supported": "WebGL nem támogatott" -} diff --git a/plugins/Sidebar/languages/it.json b/plugins/Sidebar/languages/it.json deleted file mode 100644 index 6aa0969a..00000000 --- a/plugins/Sidebar/languages/it.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "Peers": "Peer", - "Connected": "Connessi", - "Connectable": "Collegabili", - "Connectable peers": "Peer collegabili", - - "Data transfer": "Trasferimento dati", - "Received": "Ricevuti", - "Received bytes": "Byte ricevuti", - "Sent": "Inviati", - "Sent bytes": "Byte inviati", - - "Files": "File", - "Total": "Totale", - "Image": "Imagine", - "Other": "Altro", - "User data": "Dati utente", - - "Size limit": "Limite dimensione", - "limit used": "limite usato", - "free space": "spazio libero", - "Set": "Imposta", - - "Optional files": "File facoltativi", - "Downloaded": "Scaricati", - "Download and help distribute all files": "Scarica e aiuta a distribuire tutti i file", - "Total size": "Dimensione totale", - "Downloaded files": "File scaricati", - - "Database": "Database", - "search feeds": "ricerca di feed", - "{feeds} query": "{feeds} interrogazione", - "Reload": "Ricaricare", - "Rebuild": "Ricostruire", - "No database found": "Nessun database trovato", - - "Identity address": "Indirizzo di identità", - "Change": "Cambia", - - "Update": "Aggiorna", - "Pause": "Sospendi", - "Resume": "Riprendi", - "Delete": "Cancella", - "Are you sure?": "Sei sicuro?", - - "Site address": "Indirizzo sito", - "Donate": "Dona", - - "Missing files": "File mancanti", - "{} try": "{} tenta", - "{} tries": "{} prova", - "+ {num_bad_files} more": "+ {num_bad_files} altri", - - "This is my site": "Questo è il mio sito", - "Site title": "Titolo sito", - "Site description": "Descrizione sito", - "Save site settings": "Salva impostazioni sito", - - "Content publishing": "Pubblicazione contenuto", - "Choose": "Scegli", - "Sign": "Firma", - "Publish": "Pubblica", - - "This function is disabled on this proxy": "Questa funzione è disabilitata su questo proxy", - "GeoLite2 City database download error: {}!
      Please download manually and unpack to data dir:
      {}": "Errore scaricamento database GeoLite2 City: {}!
      Si prega di scaricarlo manualmente e spacchetarlo nella cartella dir:
      {}", - "Downloading GeoLite2 City database (one time only, ~20MB)...": "Scaricamento database GeoLite2 City (solo una volta, ~20MB)...", - "GeoLite2 City database downloaded!": "Database GeoLite2 City scaricato!", - - "Are you sure?": "Sei sicuro?", - "Site storage limit modified!": "Limite di archiviazione del sito modificato!", - "Database schema reloaded!": "Schema database ricaricato!", - "Database rebuilding....": "Ricostruzione database...", - "Database rebuilt!": "Database ricostruito!", - "Site updated!": "Sito aggiornato!", - "Delete this site": "Cancella questo sito", - "File write error: ": "Errore scrittura file:", - "Site settings saved!": "Impostazioni sito salvate!", - "Enter your private key:": "Inserisci la tua chiave privata:", - " Signed!": " Firmato!", - "WebGL not supported": "WebGL non supportato" -} diff --git a/plugins/Sidebar/languages/jp.json b/plugins/Sidebar/languages/jp.json deleted file mode 100644 index 38bbd420..00000000 --- a/plugins/Sidebar/languages/jp.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "Copy to clipboard": "クリップボードにコピー", - "Peers": "ピア", - "Connected": "接続済み", - "Connectable": "利用可能", - "Connectable peers": "ピアに接続可能", - "Onion": "Onion", - "Local": "ローカル", - - "Data transfer": "データ転送", - "Received": "受信", - "Received bytes": "受信バイト数", - "Sent": "送信", - "Sent bytes": "送信バイト数", - - "Files": "ファイル", - "Browse files": "ファイルを見る", - "Save as .zip": "ZIP形式で保存", - "Total": "合計", - "Image": "画像", - "Other": "その他", - "User data": "ユーザーデータ", - - "Size limit": "サイズ制限", - "limit used": "使用上限", - "free space": "フリースペース", - "Set": "セット", - - "Optional files": "オプション ファイル", - "Downloaded": "ダウンロード済み", - "Help distribute added optional files": "オプションファイルの配布を支援する", - "Auto download big file size limit": "大きなファイルの自動ダウンロードのサイズ制限", - "Download previous files": "以前のファイルのダウンロード", - "Optional files download started": "オプションファイルのダウンロードを開始", - "Optional files downloaded": "オプションファイルのダウンロードが完了しました", - "Download and help distribute all files": "ダウンロードしてすべてのファイルの配布を支援する", - "Total size": "合計サイズ", - "Downloaded files": "ダウンロードされたファイル", - - "Database": "データベース", - "search feeds": "フィードを検索する", - "{feeds} query": "{feeds} お問い合わせ", - "Reload": "再読込", - "Rebuild": "再ビルド", - "No database found": "データベースが見つかりません", - - "Identity address": "あなたの識別アドレス", - "Change": "編集", - - "Site control": "サイト管理", - "Update": "更新", - "Pause": "一時停止", - "Resume": "再開", - "Delete": "削除", - "Are you sure?": "本当によろしいですか?", - - "Site address": "サイトアドレス", - "Donate": "寄付する", - - "Missing files": "ファイルがありません", - "{} try": "{} 試す", - "{} tries": "{} 試行", - "+ {num_bad_files} more": "+ {num_bad_files} more", - - "This is my site": "これは私のサイトです", - "Site title": "サイトタイトル", - "Site description": "サイトの説明", - "Save site settings": "サイトの設定を保存する", - "Open site directory": "サイトのディレクトリを開く", - - "Content publishing": "コンテンツを公開する", - "Add saved private key": "秘密鍵の追加と保存", - "Save": "保存", - "Private key saved.": "秘密鍵が保存されています", - "Private key saved for site signing": "サイトに署名するための秘密鍵を保存", - "Forgot": "わすれる", - "Saved private key removed": "保存された秘密鍵を削除しました", - "Choose": "選択", - "Sign": "署名", - "Publish": "公開する", - "Sign and publish": "署名して公開", - - "This function is disabled on this proxy": "この機能はこのプロキシで無効になっています", - "GeoLite2 City database download error: {}!
      Please download manually and unpack to data dir:
      {}": "GeoLite2 Cityデータベースのダウンロードエラー: {}!
      手動でダウンロードして、フォルダに解凍してください。:
      {}", - "Downloading GeoLite2 City database (one time only, ~20MB)...": "GeoLite2 Cityデータベースの読み込み (これは一度だけ行われます, ~20MB)...", - "GeoLite2 City database downloaded!": "GeoLite2 Cityデータベースがダウンロードされました!", - - "Are you sure?": "本当によろしいですか?", - "Site storage limit modified!": "サイトの保存容量の制限が変更されました!", - "Database schema reloaded!": "データベーススキーマがリロードされました!", - "Database rebuilding....": "データベースの再構築中....", - "Database rebuilt!": "データベースが再構築されました!", - "Site updated!": "サイトが更新されました!", - "Delete this site": "このサイトを削除する", - "Blacklist": "NG", - "Blacklist this site": "NGリストに入れる", - "Reason": "理由", - "Delete and Blacklist": "削除してNG", - "File write error: ": "ファイル書き込みエラー:", - "Site settings saved!": "サイト設定が保存されました!", - "Enter your private key:": "秘密鍵を入力してください:", - " Signed!": " 署名しました!", - "WebGL not supported": "WebGLはサポートされていません" -} diff --git a/plugins/Sidebar/languages/pl.json b/plugins/Sidebar/languages/pl.json deleted file mode 100644 index 93268507..00000000 --- a/plugins/Sidebar/languages/pl.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "Peers": "Użytkownicy równorzędni", - "Connected": "Połączony", - "Connectable": "Możliwy do podłączenia", - "Connectable peers": "Połączeni użytkownicy równorzędni", - - "Data transfer": "Transfer danych", - "Received": "Odebrane", - "Received bytes": "Odebrany bajty", - "Sent": "Wysłane", - "Sent bytes": "Wysłane bajty", - - "Files": "Pliki", - "Total": "Sumarycznie", - "Image": "Obraz", - "Other": "Inne", - "User data": "Dane użytkownika", - - "Size limit": "Rozmiar limitu", - "limit used": "zużyty limit", - "free space": "wolna przestrzeń", - "Set": "Ustaw", - - "Optional files": "Pliki opcjonalne", - "Downloaded": "Ściągnięte", - "Download and help distribute all files": "Ściągnij i pomóż rozpowszechniać wszystkie pliki", - "Total size": "Rozmiar sumaryczny", - "Downloaded files": "Ściągnięte pliki", - - "Database": "Baza danych", - "search feeds": "przeszukaj zasoby", - "{feeds} query": "{feeds} pytanie", - "Reload": "Odśwież", - "Rebuild": "Odbuduj", - "No database found": "Nie odnaleziono bazy danych", - - "Identity address": "Adres identyfikacyjny", - "Change": "Zmień", - - "Site control": "Kontrola strony", - "Update": "Zaktualizuj", - "Pause": "Wstrzymaj", - "Resume": "Wznów", - "Delete": "Skasuj", - "Are you sure?": "Jesteś pewien?", - - "Site address": "Adres strony", - "Donate": "Wspomóż", - - "Missing files": "Brakujące pliki", - "{} try": "{} próba", - "{} tries": "{} próby", - "+ {num_bad_files} more": "+ {num_bad_files} więcej", - - "This is my site": "To moja strona", - "Site title": "Tytuł strony", - "Site description": "Opis strony", - "Save site settings": "Zapisz ustawienia strony", - - "Content publishing": "Publikowanie treści", - "Choose": "Wybierz", - "Sign": "Podpisz", - "Publish": "Opublikuj", - - "This function is disabled on this proxy": "Ta funkcja jest zablokowana w tym proxy", - "GeoLite2 City database download error: {}!
      Please download manually and unpack to data dir:
      {}": "Błąd ściągania bazy danych GeoLite2 City: {}!
      Proszę ściągnąć ją recznie i wypakować do katalogu danych:
      {}", - "Downloading GeoLite2 City database (one time only, ~20MB)...": "Ściąganie bazy danych GeoLite2 City (tylko jednorazowo, ok. 20MB)...", - "GeoLite2 City database downloaded!": "Baza danych GeoLite2 City ściagnięta!", - - "Are you sure?": "Jesteś pewien?", - "Site storage limit modified!": "Limit pamięci strony zmodyfikowany!", - "Database schema reloaded!": "Schemat bazy danych załadowany ponownie!", - "Database rebuilding....": "Przebudowywanie bazy danych...", - "Database rebuilt!": "Baza danych przebudowana!", - "Site updated!": "Strona zaktualizowana!", - "Delete this site": "Usuń tę stronę", - "File write error: ": "Błąd zapisu pliku: ", - "Site settings saved!": "Ustawienia strony zapisane!", - "Enter your private key:": "Wpisz swój prywatny klucz:", - " Signed!": " Podpisane!", - "WebGL not supported": "WebGL nie jest obsługiwany" -} diff --git a/plugins/Sidebar/languages/pt-br.json b/plugins/Sidebar/languages/pt-br.json deleted file mode 100644 index d5659171..00000000 --- a/plugins/Sidebar/languages/pt-br.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "Copy to clipboard": "Copiar para área de transferência (clipboard)", - "Peers": "Peers", - "Connected": "Ligados", - "Connectable": "Disponíveis", - "Onion": "Onion", - "Local": "Locais", - "Connectable peers": "Peers disponíveis", - - "Data transfer": "Transferência de dados", - "Received": "Recebidos", - "Received bytes": "Bytes recebidos", - "Sent": "Enviados", - "Sent bytes": "Bytes enviados", - - "Files": "Arquivos", - "Save as .zip": "Salvar como .zip", - "Total": "Total", - "Image": "Imagem", - "Other": "Outros", - "User data": "Dados do usuário", - - "Size limit": "Limite de tamanho", - "limit used": "limite utilizado", - "free space": "espaço livre", - "Set": "Definir", - - "Optional files": "Arquivos opcionais", - "Downloaded": "Baixados", - "Download and help distribute all files": "Baixar e ajudar a distribuir todos os arquivos", - "Total size": "Tamanho total", - "Downloaded files": "Arquivos baixados", - - "Database": "Banco de dados", - "search feeds": "pesquisar feeds", - "{feeds} query": "consulta de {feeds}", - "Reload": "Recarregar", - "Rebuild": "Reconstruir", - "No database found": "Base de dados não encontrada", - - "Identity address": "Endereço de identidade", - "Change": "Alterar", - - "Site control": "Controle do site", - "Update": "Atualizar", - "Pause": "Suspender", - "Resume": "Continuar", - "Delete": "Remover", - "Are you sure?": "Tem certeza?", - - "Site address": "Endereço do site", - "Donate": "Doar", - - "Needs to be updated": "Necessitam ser atualizados", - "{} try": "{} tentativa", - "{} tries": "{} tentativas", - "+ {num_bad_files} more": "+ {num_bad_files} adicionais", - - "This is my site": "Este é o meu site", - "Site title": "Título do site", - "Site description": "Descrição do site", - "Save site settings": "Salvar definições do site", - "Open site directory": "Abrir diretório do site", - - "Content publishing": "Publicação do conteúdo", - "Choose": "Escolher", - "Sign": "Assinar", - "Publish": "Publicar", - "Sign and publish": "Assinar e publicar", - "add saved private key": "adicionar privatekey (chave privada) para salvar", - "Private key saved for site signing": "Privatekey foi salva para assinar o site", - "Private key saved.": "Privatekey salva.", - "forgot": "esquecer", - "Saved private key removed": "Privatekey salva foi removida", - "This function is disabled on this proxy": "Esta função encontra-se desativada neste proxy", - "GeoLite2 City database download error: {}!
      Please download manually and unpack to data dir:
      {}": "Erro ao baixar a base de dados GeoLite2 City: {}!
      Por favor baixe manualmente e descompacte os dados para a seguinte pasta:
      {}", - "Downloading GeoLite2 City database (one time only, ~20MB)...": "Baixando a base de dados GeoLite2 City (uma única vez, ~20MB)...", - "GeoLite2 City database downloaded!": "A base de dados GeoLite2 City foi baixada!", - - "Are you sure?": "Tem certeza?", - "Site storage limit modified!": "O limite de armazenamento do site foi modificado!", - "Database schema reloaded!": "O esquema da base de dados foi atualizado!", - "Database rebuilding....": "Reconstruindo base de dados...", - "Database rebuilt!": "Base de dados reconstruída!", - "Site updated!": "Site atualizado!", - "Delete this site": "Remover este site", - "Blacklist": "Blacklist", - "Blacklist this site": "Blacklistar este site", - "Reason": "Motivo", - "Delete and Blacklist": "Deletar e blacklistar", - "File write error: ": "Erro de escrita de arquivo: ", - "Site settings saved!": "Definições do site salvas!", - "Enter your private key:": "Digite sua chave privada:", - " Signed!": " Assinado!", - "WebGL not supported": "WebGL não é suportado", - "Save as .zip": "Salvar como .zip" -} diff --git a/plugins/Sidebar/languages/ru.json b/plugins/Sidebar/languages/ru.json deleted file mode 100644 index f2eeca04..00000000 --- a/plugins/Sidebar/languages/ru.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "Peers": "Пиры", - "Connected": "Подключенные", - "Connectable": "Доступные", - "Connectable peers": "Пиры доступны для подключения", - - "Data transfer": "Передача данных", - "Received": "Получено", - "Received bytes": "Получено байн", - "Sent": "Отправлено", - "Sent bytes": "Отправлено байт", - - "Files": "Файлы", - "Total": "Всего", - "Image": "Изображений", - "Other": "Другое", - "User data": "Ваш контент", - - "Size limit": "Ограничение по размеру", - "limit used": "Использовано", - "free space": "Доступно", - "Set": "Установить", - - "Optional files": "Опциональные файлы", - "Downloaded": "Загружено", - "Download and help distribute all files": "Загрузить опциональные файлы для помощи сайту", - "Total size": "Объём", - "Downloaded files": "Загруженные файлы", - - "Database": "База данных", - "search feeds": "поиск подписок", - "{feeds} query": "{feeds} запрос", - "Reload": "Перезагрузить", - "Rebuild": "Перестроить", - "No database found": "База данных не найдена", - - "Identity address": "Уникальный адрес", - "Change": "Изменить", - - "Site control": "Управление сайтом", - "Update": "Обновить", - "Pause": "Пауза", - "Resume": "Продолжить", - "Delete": "Удалить", - "Are you sure?": "Вы уверены?", - - "Site address": "Адрес сайта", - "Donate": "Пожертвовать", - - "Missing files": "Отсутствующие файлы", - "{} try": "{} попробовать", - "{} tries": "{} попыток", - "+ {num_bad_files} more": "+ {num_bad_files} ещё", - - "This is my site": "Это мой сайт", - "Site title": "Название сайта", - "Site description": "Описание сайта", - "Save site settings": "Сохранить настройки сайта", - - "Content publishing": "Публикация контента", - "Choose": "Выбрать", - "Sign": "Подписать", - "Publish": "Опубликовать", - - "This function is disabled on this proxy": "Эта функция отключена на этом прокси", - "GeoLite2 City database download error: {}!
      Please download manually and unpack to data dir:
      {}": "Ошибка загрузки базы городов GeoLite2: {}!
      Пожалуйста, загрузите её вручную и распакуйте в папку:
      {}", - "Downloading GeoLite2 City database (one time only, ~20MB)...": "Загрузка базы городов GeoLite2 (это делается только 1 раз, ~20MB)...", - "GeoLite2 City database downloaded!": "База GeoLite2 успешно загружена!", - - "Are you sure?": "Вы уверены?", - "Site storage limit modified!": "Лимит хранилища для сайта изменен!", - "Database schema reloaded!": "Схема базы данных перезагружена!", - "Database rebuilding....": "Перестройка базы данных...", - "Database rebuilt!": "База данных перестроена!", - "Site updated!": "Сайт обновлён!", - "Delete this site": "Удалить этот сайт", - "File write error: ": "Ошибка записи файла:", - "Site settings saved!": "Настройки сайта сохранены!", - "Enter your private key:": "Введите свой приватный ключ:", - " Signed!": " Подписано!", - "WebGL not supported": "WebGL не поддерживается" -} diff --git a/plugins/Sidebar/languages/tr.json b/plugins/Sidebar/languages/tr.json deleted file mode 100644 index 88fcd6e0..00000000 --- a/plugins/Sidebar/languages/tr.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "Peers": "Eşler", - "Connected": "Bağlı", - "Connectable": "Erişilebilir", - "Connectable peers": "Bağlanılabilir eşler", - - "Data transfer": "Veri aktarımı", - "Received": "Alınan", - "Received bytes": "Bayt alındı", - "Sent": "Gönderilen", - "Sent bytes": "Bayt gönderildi", - - "Files": "Dosyalar", - "Total": "Toplam", - "Image": "Resim", - "Other": "Diğer", - "User data": "Kullanıcı verisi", - - "Size limit": "Boyut sınırı", - "limit used": "kullanılan", - "free space": "boş", - "Set": "Ayarla", - - "Optional files": "İsteğe bağlı dosyalar", - "Downloaded": "İndirilen", - "Download and help distribute all files": "Tüm dosyaları indir ve yayılmalarına yardım et", - "Total size": "Toplam boyut", - "Downloaded files": "İndirilen dosyalar", - - "Database": "Veritabanı", - "search feeds": "kaynak ara", - "{feeds} query": "{feeds} sorgu", - "Reload": "Yenile", - "Rebuild": "Yapılandır", - "No database found": "Veritabanı yok", - - "Identity address": "Kimlik adresi", - "Change": "Değiştir", - - "Site control": "Site kontrolü", - "Update": "Güncelle", - "Pause": "Duraklat", - "Resume": "Sürdür", - "Delete": "Sil", - "Are you sure?": "Emin misin?", - - "Site address": "Site adresi", - "Donate": "Bağış yap", - - "Missing files": "Eksik dosyalar", - "{} try": "{} deneme", - "{} tries": "{} deneme", - "+ {num_bad_files} more": "+ {num_bad_files} tane daha", - - "This is my site": "Bu benim sitem", - "Site title": "Site başlığı", - "Site description": "Site açıklaması", - "Save site settings": "Site ayarlarını kaydet", - - "Content publishing": "İçerik yayımlanıyor", - "Choose": "Seç", - "Sign": "İmzala", - "Publish": "Yayımla", - - "This function is disabled on this proxy": "Bu özellik bu vekilde kullanılamaz", - "GeoLite2 City database download error: {}!
      Please download manually and unpack to data dir:
      {}": "GeoLite2 Şehir veritabanı indirme hatası: {}!
      Lütfen kendiniz indirip aşağıdaki konuma açınınız:
      {}", - "Downloading GeoLite2 City database (one time only, ~20MB)...": "GeoLite2 Şehir veritabanı indiriliyor (sadece bir kere, ~20MB)...", - "GeoLite2 City database downloaded!": "GeoLite2 Şehir veritabanı indirildi!", - - "Are you sure?": "Emin misiniz?", - "Site storage limit modified!": "Site saklama sınırı değiştirildi!", - "Database schema reloaded!": "Veritabanı şeması yeniden yüklendi!", - "Database rebuilding....": "Veritabanı yeniden inşa ediliyor...", - "Database rebuilt!": "Veritabanı yeniden inşa edildi!", - "Site updated!": "Site güncellendi!", - "Delete this site": "Bu siteyi sil", - "File write error: ": "Dosya yazma hatası: ", - "Site settings saved!": "Site ayarları kaydedildi!", - "Enter your private key:": "Özel anahtarınızı giriniz:", - " Signed!": " İmzala!", - "WebGL not supported": "WebGL desteklenmiyor" -} diff --git a/plugins/Sidebar/languages/zh-tw.json b/plugins/Sidebar/languages/zh-tw.json deleted file mode 100644 index 9d4ea1be..00000000 --- a/plugins/Sidebar/languages/zh-tw.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "Peers": "節點數", - "Connected": "已連線", - "Connectable": "可連線", - "Connectable peers": "可連線節點", - - "Data transfer": "數據傳輸", - "Received": "已接收", - "Received bytes": "已接收位元組", - "Sent": "已傳送", - "Sent bytes": "已傳送位元組", - - "Files": "檔案", - "Total": "共計", - "Image": "圖片", - "Other": "其他", - "User data": "使用者數據", - - "Size limit": "大小限制", - "limit used": "已使用", - "free space": "可用空間", - "Set": "偏好設定", - - "Optional files": "可選檔案", - "Downloaded": "已下載", - "Download and help distribute all files": "下載並幫助分發所有檔案", - "Total size": "總大小", - "Downloaded files": "下載的檔案", - - "Database": "資料庫", - "search feeds": "搜尋供稿", - "{feeds} query": "{feeds} 查詢 ", - "Reload": "重新整理", - "Rebuild": "重建", - "No database found": "未找到資料庫", - - "Identity address": "身分位址", - "Change": "變更", - - "Site control": "網站控制", - "Update": "更新", - "Pause": "暫停", - "Resume": "恢復", - "Delete": "刪除", - "Are you sure?": "你確定?", - - "Site address": "網站位址", - "Donate": "捐贈", - - "Missing files": "缺少的檔案", - "{} try": "{} 嘗試", - "{} tries": "{} 已嘗試", - "+ {num_bad_files} more": "+ {num_bad_files} 更多", - - "This is my site": "這是我的網站", - "Site title": "網站標題", - "Site description": "網站描述", - "Save site settings": "存儲網站設定", - "Open site directory": "打開所在資料夾", - - "Content publishing": "內容發布", - "Choose": "選擇", - "Sign": "簽署", - "Publish": "發布", - "Sign and publish": "簽名並發布", - "This function is disabled on this proxy": "此代理上禁用此功能", - "GeoLite2 City database download error: {}!
      Please download manually and unpack to data dir:
      {}": "GeoLite2 地理位置資料庫下載錯誤:{}!
      請手動下載並解壓到數據目錄:
      {}", - "Downloading GeoLite2 City database (one time only, ~20MB)...": "正在下載 GeoLite2 地理位置資料庫 (僅一次,約 20MB )...", - "GeoLite2 City database downloaded!": "GeoLite2 地理位置資料庫已下載!", - - "Are you sure?": "你確定?", - "Site storage limit modified!": "網站存儲限制已變更!", - "Database schema reloaded!": "資料庫架構重新加載!", - "Database rebuilding....": "資料庫重建中...", - "Database rebuilt!": "資料庫已重建!", - "Site updated!": "網站已更新!", - "Delete this site": "刪除此網站", - "File write error: ": "檔案寫入錯誤:", - "Site settings saved!": "網站設置已保存!", - "Enter your private key:": "輸入您的私鑰:", - " Signed!": " 已簽署!", - "WebGL not supported": "不支援 WebGL" -} diff --git a/plugins/Sidebar/languages/zh.json b/plugins/Sidebar/languages/zh.json deleted file mode 100644 index 639ac7f6..00000000 --- a/plugins/Sidebar/languages/zh.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "Copy to clipboard": "复制到剪切板", - "Peers": "节点数", - "Connected": "已连接", - "Connectable": "可连接", - "Onion": "洋葱点", - "Local": "局域网", - "Connectable peers": "可连接节点", - - "Data transfer": "数据传输", - "Received": "已接收", - "Received bytes": "已接收字节", - "Sent": "已发送", - "Sent bytes": "已发送字节", - - "Files": "文件", - "Save as .zip": "打包成zip文件", - "Total": "总计", - "Image": "图像", - "Other": "其他", - "User data": "用户数据", - - "Size limit": "大小限制", - "limit used": "限额", - "free space": "剩余空间", - "Set": "设置", - - "Optional files": "可选文件", - "Downloaded": "已下载", - "Help distribute added optional files": "帮助分发新的可选文件", - "Auto download big file size limit": "自动下载大文件大小限制", - "Download previous files": "下载之前的文件", - "Optional files download started": "可选文件下载启动", - "Optional files downloaded": "可选文件下载完成", - "Total size": "总大小", - "Downloaded files": "已下载文件", - - "Database": "数据库", - "search feeds": "搜索数据源", - "{feeds} query": "{feeds} 请求", - "Reload": "重载", - "Rebuild": "重建", - "No database found": "没有找到数据库", - - "Identity address": "身份地址", - "Change": "更改", - - "Site control": "站点控制", - "Update": "更新", - "Pause": "暂停", - "Resume": "恢复", - "Delete": "删除", - "Are you sure?": "您确定吗?", - - "Site address": "站点地址", - "Donate": "捐赠", - - "Needs to be updated": "需要更新", - "{} try": "{} 尝试", - "{} tries": "{} 已尝试", - "+ {num_bad_files} more": "+ {num_bad_files} 更多", - - "This is my site": "这是我的站点", - "Site title": "站点标题", - "Site description": "站点描述", - "Save site settings": "保存站点设置", - "Open site directory": "打开所在文件夹", - - "Content publishing": "内容发布", - "Add saved private key": "添加并保存私钥", - "Save": "保存", - "Private key saved.": "私钥已保存", - "Private key saved for site signing": "已保存用于站点签名的私钥", - "Forgot": "删除私钥", - "Saved private key removed": "保存的私钥已删除", - "Choose": "选择", - "Sign": "签名", - "Publish": "发布", - "Sign and publish": "签名并发布", - "This function is disabled on this proxy": "此功能在代理上被禁用", - "GeoLite2 City database download error: {}!
      Please download manually and unpack to data dir:
      {}": "GeoLite2 地理位置数据库下载错误:{}!
      请手动下载并解压在数据目录:
      {}", - "Downloading GeoLite2 City database (one time only, ~20MB)...": "正在下载 GeoLite2 地理位置数据库 (仅需一次,约 20MB )...", - "GeoLite2 City database downloaded!": "GeoLite2 地理位置数据库已下载!", - - "Are you sure?": "您确定吗?", - "Site storage limit modified!": "站点存储限制已更改!", - "Database schema reloaded!": "数据库模式已重新加载!", - "Database rebuilding....": "数据库重建中...", - "Database rebuilt!": "数据库已重建!", - "Site updated!": "站点已更新!", - "Delete this site": "删除此站点", - "Blacklist": "黑名单", - "Blacklist this site": "拉黑此站点", - "Reason": "原因", - "Delete and Blacklist": "删除并拉黑", - "File write error: ": "文件写入错误:", - "Site settings saved!": "站点设置已保存!", - "Enter your private key:": "输入您的私钥:", - " Signed!": " 已签名!", - "WebGL not supported": "不支持 WebGL" -} diff --git a/plugins/Sidebar/media/Class.coffee b/plugins/Sidebar/media/Class.coffee deleted file mode 100644 index d62ab25c..00000000 --- a/plugins/Sidebar/media/Class.coffee +++ /dev/null @@ -1,23 +0,0 @@ -class Class - trace: true - - log: (args...) -> - return unless @trace - return if typeof console is 'undefined' - args.unshift("[#{@.constructor.name}]") - console.log(args...) - @ - - logStart: (name, args...) -> - return unless @trace - @logtimers or= {} - @logtimers[name] = +(new Date) - @log "#{name}", args..., "(started)" if args.length > 0 - @ - - logEnd: (name, args...) -> - ms = +(new Date)-@logtimers[name] - @log "#{name}", args..., "(Done in #{ms}ms)" - @ - -window.Class = Class \ No newline at end of file diff --git a/plugins/Sidebar/media/Console.coffee b/plugins/Sidebar/media/Console.coffee deleted file mode 100644 index d5a83346..00000000 --- a/plugins/Sidebar/media/Console.coffee +++ /dev/null @@ -1,201 +0,0 @@ -class Console extends Class - constructor: (@sidebar) -> - @tag = null - @opened = false - @filter = null - @tab_types = [ - {title: "All", filter: ""}, - {title: "Info", filter: "INFO"}, - {title: "Warning", filter: "WARNING"}, - {title: "Error", filter: "ERROR"} - ] - @read_size = 32 * 1024 - @tab_active = "" - #@filter = @sidebar.wrapper.site_info.address_short - handleMessageWebsocket_original = @sidebar.wrapper.handleMessageWebsocket - @sidebar.wrapper.handleMessageWebsocket = (message) => - if message.cmd == "logLineAdd" and message.params.stream_id == @stream_id - @addLines(message.params.lines) - else - handleMessageWebsocket_original(message) - - $(window).on "hashchange", => - if window.top.location.hash.startsWith("#ZeroNet:Console") - @open() - - if window.top.location.hash.startsWith("#ZeroNet:Console") - setTimeout (=> @open()), 10 - - createHtmltag: -> - if not @container - @container = $(""" -
      -
      -
      -
      -
      Loading...
      -
      -
      -
      - -
      -
      -
      - - """) - @text = @container.find(".console-text") - @text_elem = @text[0] - @tabs = @container.find(".console-tabs") - - @text.on "mousewheel", (e) => # Stop animation on manual scrolling - if e.originalEvent.deltaY < 0 - @text.stop() - RateLimit 300, @checkTextIsBottom - - @text.is_bottom = true - - @container.appendTo(document.body) - @tag = @container.find(".console") - for tab_type in @tab_types - tab = $("", {href: "#", "data-filter": tab_type.filter, "data-title": tab_type.title}).text(tab_type.title) - if tab_type.filter == @tab_active - tab.addClass("active") - tab.on("click", @handleTabClick) - if window.top.location.hash.endsWith(tab_type.title) - @log "Triggering click on", tab - tab.trigger("click") - @tabs.append(tab) - - @container.on "mousedown touchend touchcancel", (e) => - if e.target != e.currentTarget - return true - @log "closing" - if $(document.body).hasClass("body-console") - @close() - return true - - @loadConsoleText() - - checkTextIsBottom: => - @text.is_bottom = Math.round(@text_elem.scrollTop + @text_elem.clientHeight) >= @text_elem.scrollHeight - 15 - - toColor: (text, saturation=60, lightness=70) -> - hash = 0 - for i in [0..text.length-1] - hash += text.charCodeAt(i)*i - hash = hash % 1777 - return "hsl(" + (hash % 360) + ",#{saturation}%,#{lightness}%)"; - - formatLine: (line) => - match = line.match(/(\[.*?\])[ ]+(.*?)[ ]+(.*?)[ ]+(.*)/) - if not match - return line.replace(/\/g, ">") - - [line, added, level, module, text] = line.match(/(\[.*?\])[ ]+(.*?)[ ]+(.*?)[ ]+(.*)/) - added = "#{added}" - level = "#{level}" - module = "#{module}" - - text = text.replace(/(Site:[A-Za-z0-9\.]+)/g, "$1") - text = text.replace(/\/g, ">") - #text = text.replace(/( [0-9\.]+(|s|ms))/g, "$1") - return "#{added} #{level} #{module} #{text}" - - - addLines: (lines, animate=true) => - html_lines = [] - @logStart "formatting" - for line in lines - html_lines.push @formatLine(line) - @logEnd "formatting" - @logStart "adding" - @text.append(html_lines.join("
      ") + "
      ") - @logEnd "adding" - if @text.is_bottom and animate - @text.stop().animate({scrollTop: @text_elem.scrollHeight - @text_elem.clientHeight + 1}, 600, 'easeInOutCubic') - - - loadConsoleText: => - @sidebar.wrapper.ws.cmd "consoleLogRead", {filter: @filter, read_size: @read_size}, (res) => - @text.html("") - pos_diff = res["pos_end"] - res["pos_start"] - size_read = Math.round(pos_diff/1024) - size_total = Math.round(res['pos_end']/1024) - @text.append("

      ") - @text.append("Displaying #{res.lines.length} of #{res.num_found} lines found in the last #{size_read}kB of the log file. (#{size_total}kB total)
      ") - @addLines res.lines, false - @text_elem.scrollTop = @text_elem.scrollHeight - if @stream_id - @sidebar.wrapper.ws.cmd "consoleLogStreamRemove", {stream_id: @stream_id} - @sidebar.wrapper.ws.cmd "consoleLogStream", {filter: @filter}, (res) => - @stream_id = res.stream_id - - close: => - window.top.location.hash = "" - @sidebar.move_lock = "y" - @sidebar.startDrag() - @sidebar.stopDrag() - - open: => - @sidebar.startDrag() - @sidebar.moved("y") - @sidebar.fixbutton_targety = @sidebar.page_height - @sidebar.fixbutton_inity - 50 - @sidebar.stopDrag() - - onOpened: => - @sidebar.onClosed() - @log "onOpened" - - onClosed: => - $(document.body).removeClass("body-console") - if @stream_id - @sidebar.wrapper.ws.cmd "consoleLogStreamRemove", {stream_id: @stream_id} - - cleanup: => - if @container - @container.remove() - @container = null - - stopDragY: => - # Animate sidebar and iframe - if @sidebar.fixbutton_targety == @sidebar.fixbutton_inity - # Closed - targety = 0 - @opened = false - else - # Opened - targety = @sidebar.fixbutton_targety - @sidebar.fixbutton_inity - @onOpened() - @opened = true - - # Revent sidebar transitions - if @tag - @tag.css("transition", "0.5s ease-out") - @tag.css("transform", "translateY(#{targety}px)").one transitionEnd, => - @tag.css("transition", "") - if not @opened - @cleanup() - # Revert body transformations - @log "stopDragY", "opened:", @opened, targety - if not @opened - @onClosed() - - changeFilter: (filter) => - @filter = filter - if @filter == "" - @read_size = 32 * 1024 - else - @read_size = 5 * 1024 * 1024 - @loadConsoleText() - - handleTabClick: (e) => - elem = $(e.currentTarget) - @tab_active = elem.data("filter") - $("a", @tabs).removeClass("active") - elem.addClass("active") - @changeFilter(@tab_active) - window.top.location.hash = "#ZeroNet:Console:" + elem.data("title") - return false - -window.Console = Console diff --git a/plugins/Sidebar/media/Console.css b/plugins/Sidebar/media/Console.css deleted file mode 100644 index 127d15bf..00000000 --- a/plugins/Sidebar/media/Console.css +++ /dev/null @@ -1,31 +0,0 @@ -.console-container { width: 100%; z-index: 998; position: absolute; top: -100vh; padding-bottom: 100%; } -.console-container .console { background-color: #212121; height: 100vh; transform: translateY(0px); padding-top: 80px; box-sizing: border-box; } - -.console-top { color: white; font-family: Consolas, monospace; font-size: 11px; line-height: 20px; height: 100%; box-sizing: border-box; letter-spacing: 0.5px;} -.console-text { overflow-y: scroll; height: calc(100% - 10px); color: #DDD; padding: 5px; margin-top: -36px; overflow-wrap: break-word; } -.console-tabs { - background-color: #41193fad; position: relative; margin-right: 17px; /*backdrop-filter: blur(2px);*/ - box-shadow: -30px 0px 45px #7d2463; background: linear-gradient(-75deg, #591a48ed, #70305e66); border-bottom: 1px solid #792e6473; -} -.console-tabs a { - margin-right: 5px; padding: 5px 15px; text-decoration: none; color: #AAA; - font-size: 11px; font-family: "Consolas"; text-transform: uppercase; border: 1px solid #666; - border-bottom: 0px; display: inline-block; margin: 5px; margin-bottom: 0px; background-color: rgba(0,0,0,0.5); -} -.console-tabs a:hover { color: #FFF } -.console-tabs a.active { background-color: #46223c; color: #FFF } -.console-middle {height: 0px; top: 50%; position: absolute; width: 100%; left: 50%; display: none; } - -.console .mynode { - border: 0.5px solid #aaa; width: 50px; height: 50px; transform: rotateZ(45deg); margin-top: -25px; margin-left: -25px; - opacity: 1; display: inline-block; background-color: #EEE; z-index: 9; position: absolute; outline: 5px solid #EEE; -} -.console .peers { width: 0px; height: 0px; position: absolute; left: -20px; top: -20px; text-align: center; } -.console .peer { left: 0px; top: 0px; position: absolute; } -.console .peer .icon { width: 20px; height: 20px; padding: 10px; display: inline-block; text-decoration: none; left: 200px; position: absolute; color: #666; } -.console .peer .icon:before { content: "\25BC"; position: absolute; margin-top: 3px; margin-left: -1px; opacity: 0; transition: all 0.3s } -.console .peer .icon:hover:before { opacity: 1; transition: none } -.console .peer .line { - width: 187px; border-top: 1px solid #CCC; position: absolute; top: 20px; left: 20px; - transform: rotateZ(334deg); transform-origin: bottom left; -} diff --git a/plugins/Sidebar/media/Menu.coffee b/plugins/Sidebar/media/Menu.coffee deleted file mode 100644 index 3e19fd9f..00000000 --- a/plugins/Sidebar/media/Menu.coffee +++ /dev/null @@ -1,49 +0,0 @@ -class Menu - constructor: (@button) -> - @elem = $(".menu.template").clone().removeClass("template") - @elem.appendTo("body") - @items = [] - - show: -> - if window.visible_menu and window.visible_menu.button[0] == @button[0] # Same menu visible then hide it - window.visible_menu.hide() - @hide() - else - button_pos = @button.offset() - left = button_pos.left - @elem.css({"top": button_pos.top+@button.outerHeight(), "left": left}) - @button.addClass("menu-active") - @elem.addClass("visible") - if @elem.position().left + @elem.width() + 20 > window.innerWidth - @elem.css("left", window.innerWidth - @elem.width() - 20) - if window.visible_menu then window.visible_menu.hide() - window.visible_menu = @ - - - hide: -> - @elem.removeClass("visible") - @button.removeClass("menu-active") - window.visible_menu = null - - - addItem: (title, cb) -> - item = $(".menu-item.template", @elem).clone().removeClass("template") - item.html(title) - item.on "click", => - if not cb(item) - @hide() - return false - item.appendTo(@elem) - @items.push item - return item - - - log: (args...) -> - console.log "[Menu]", args... - -window.Menu = Menu - -# Hide menu on outside click -$("body").on "click", (e) -> - if window.visible_menu and e.target != window.visible_menu.button[0] and $(e.target).parent()[0] != window.visible_menu.elem[0] - window.visible_menu.hide() diff --git a/plugins/Sidebar/media/Menu.css b/plugins/Sidebar/media/Menu.css deleted file mode 100644 index e2afa16e..00000000 --- a/plugins/Sidebar/media/Menu.css +++ /dev/null @@ -1,19 +0,0 @@ -.menu { - background-color: white; padding: 10px 0px; position: absolute; top: 0px; left: 0px; max-height: 0px; overflow: hidden; transform: translate(0px, -30px); pointer-events: none; - box-shadow: 0px 2px 8px rgba(0,0,0,0.3); border-radius: 2px; opacity: 0; transition: opacity 0.2s ease-out, transform 1s ease-out, max-height 0.2s ease-in-out; -} -.menu.visible { opacity: 1; max-height: 350px; transform: translate(0px, 0px); transition: opacity 0.1s ease-out, transform 0.3s ease-out, max-height 0.3s ease-in-out; pointer-events: all } - -.menu-item { display: block; text-decoration: none; color: black; padding: 6px 24px; transition: all 0.2s; border-bottom: none; font-weight: normal; padding-left: 30px; } -.menu-item-separator { margin-top: 5px; border-top: 1px solid #eee } - -.menu-item:hover { background-color: #F6F6F6; transition: none; color: inherit; border: none } -.menu-item:active, .menu-item:focus { background-color: #AF3BFF; color: white; transition: none } -.menu-item.selected:before { - content: "L"; display: inline-block; transform: rotateZ(45deg) scaleX(-1); - font-weight: bold; position: absolute; margin-left: -17px; font-size: 12px; margin-top: 2px; -} - -@media only screen and (max-width: 800px) { -.menu, .menu.visible { position: absolute; left: unset !important; right: 20px; } -} \ No newline at end of file diff --git a/plugins/Sidebar/media/Prototypes.coffee b/plugins/Sidebar/media/Prototypes.coffee deleted file mode 100644 index a9edd255..00000000 --- a/plugins/Sidebar/media/Prototypes.coffee +++ /dev/null @@ -1,9 +0,0 @@ -String::startsWith = (s) -> @[...s.length] is s -String::endsWith = (s) -> s is '' or @[-s.length..] is s -String::capitalize = -> if @.length then @[0].toUpperCase() + @.slice(1) else "" -String::repeat = (count) -> new Array( count + 1 ).join(@) - -window.isEmpty = (obj) -> - for key of obj - return false - return true diff --git a/plugins/Sidebar/media/RateLimit.coffee b/plugins/Sidebar/media/RateLimit.coffee deleted file mode 100644 index 17c67433..00000000 --- a/plugins/Sidebar/media/RateLimit.coffee +++ /dev/null @@ -1,14 +0,0 @@ -limits = {} -call_after_interval = {} -window.RateLimit = (interval, fn) -> - if not limits[fn] - call_after_interval[fn] = false - fn() # First call is not delayed - limits[fn] = setTimeout (-> - if call_after_interval[fn] - fn() - delete limits[fn] - delete call_after_interval[fn] - ), interval - else # Called within iterval, delay the call - call_after_interval[fn] = true diff --git a/plugins/Sidebar/media/Scrollable.js b/plugins/Sidebar/media/Scrollable.js deleted file mode 100644 index 689a5719..00000000 --- a/plugins/Sidebar/media/Scrollable.js +++ /dev/null @@ -1,91 +0,0 @@ -/* via http://jsfiddle.net/elGrecode/00dgurnn/ */ - -window.initScrollable = function () { - - var scrollContainer = document.querySelector('.scrollable'), - scrollContentWrapper = document.querySelector('.scrollable .content-wrapper'), - scrollContent = document.querySelector('.scrollable .content'), - contentPosition = 0, - scrollerBeingDragged = false, - scroller, - topPosition, - scrollerHeight; - - function calculateScrollerHeight() { - // *Calculation of how tall scroller should be - var visibleRatio = scrollContainer.offsetHeight / scrollContentWrapper.scrollHeight; - if (visibleRatio == 1) - scroller.style.display = "none"; - else - scroller.style.display = "block"; - return visibleRatio * scrollContainer.offsetHeight; - } - - function moveScroller(evt) { - // Move Scroll bar to top offset - var scrollPercentage = evt.target.scrollTop / scrollContentWrapper.scrollHeight; - topPosition = scrollPercentage * (scrollContainer.offsetHeight - 5); // 5px arbitrary offset so scroll bar doesn't move too far beyond content wrapper bounding box - scroller.style.top = topPosition + 'px'; - } - - function startDrag(evt) { - normalizedPosition = evt.pageY; - contentPosition = scrollContentWrapper.scrollTop; - scrollerBeingDragged = true; - window.addEventListener('mousemove', scrollBarScroll); - return false; - } - - function stopDrag(evt) { - scrollerBeingDragged = false; - window.removeEventListener('mousemove', scrollBarScroll); - } - - function scrollBarScroll(evt) { - if (scrollerBeingDragged === true) { - evt.preventDefault(); - var mouseDifferential = evt.pageY - normalizedPosition; - var scrollEquivalent = mouseDifferential * (scrollContentWrapper.scrollHeight / scrollContainer.offsetHeight); - scrollContentWrapper.scrollTop = contentPosition + scrollEquivalent; - } - } - - function updateHeight() { - scrollerHeight = calculateScrollerHeight() - 10; - scroller.style.height = scrollerHeight + 'px'; - } - - function createScroller() { - // *Creates scroller element and appends to '.scrollable' div - // create scroller element - scroller = document.createElement("div"); - scroller.className = 'scroller'; - - // determine how big scroller should be based on content - scrollerHeight = calculateScrollerHeight() - 10; - - if (scrollerHeight / scrollContainer.offsetHeight < 1) { - // *If there is a need to have scroll bar based on content size - scroller.style.height = scrollerHeight + 'px'; - - // append scroller to scrollContainer div - scrollContainer.appendChild(scroller); - - // show scroll path divot - scrollContainer.className += ' showScroll'; - - // attach related draggable listeners - scroller.addEventListener('mousedown', startDrag); - window.addEventListener('mouseup', stopDrag); - } - - } - - createScroller(); - - - // *** Listeners *** - scrollContentWrapper.addEventListener('scroll', moveScroller); - - return updateHeight; -}; \ No newline at end of file diff --git a/plugins/Sidebar/media/Scrollbable.css b/plugins/Sidebar/media/Scrollbable.css deleted file mode 100644 index 6e3e0b6a..00000000 --- a/plugins/Sidebar/media/Scrollbable.css +++ /dev/null @@ -1,44 +0,0 @@ -.scrollable { - overflow: hidden; -} - -.scrollable.showScroll::after { - position: absolute; - content: ''; - top: 5%; - right: 7px; - height: 90%; - width: 3px; - background: rgba(224, 224, 255, .3); -} - -.scrollable .content-wrapper { - width: 100%; - height: 100%; - padding-right: 50%; - overflow-y: scroll; -} -.scroller { - margin-top: 5px; - z-index: 5; - cursor: pointer; - position: absolute; - width: 7px; - border-radius: 5px; - background: #3A3A3A; - top: 0px; - left: 395px; - -webkit-transition: top .08s; - -moz-transition: top .08s; - -ms-transition: top .08s; - -o-transition: top .08s; - transition: top .08s; -} -.scroller { - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee deleted file mode 100644 index 57d36eac..00000000 --- a/plugins/Sidebar/media/Sidebar.coffee +++ /dev/null @@ -1,644 +0,0 @@ -class Sidebar extends Class - constructor: (@wrapper) -> - @tag = null - @container = null - @opened = false - @width = 410 - @console = new Console(@) - @fixbutton = $(".fixbutton") - @fixbutton_addx = 0 - @fixbutton_addy = 0 - @fixbutton_initx = 0 - @fixbutton_inity = 15 - @fixbutton_targetx = 0 - @move_lock = null - @page_width = $(window).width() - @page_height = $(window).height() - @frame = $("#inner-iframe") - @initFixbutton() - @dragStarted = 0 - @globe = null - @preload_html = null - - @original_set_site_info = @wrapper.setSiteInfo # We going to override this, save the original - - # Start in opened state for debugging - if window.top.location.hash == "#ZeroNet:OpenSidebar" - @startDrag() - @moved("x") - @fixbutton_targetx = @fixbutton_initx - @width - @stopDrag() - - - initFixbutton: -> - - # Detect dragging - @fixbutton.on "mousedown touchstart", (e) => - if e.button > 0 # Right or middle click - return - e.preventDefault() - - # Disable previous listeners - @fixbutton.off "click touchend touchcancel" - - # Make sure its not a click - @dragStarted = (+ new Date) - - # Fullscreen drag bg to capture mouse events over iframe - $(".drag-bg").remove() - $("
      ").appendTo(document.body) - - $("body").one "mousemove touchmove", (e) => - mousex = e.pageX - mousey = e.pageY - if not mousex - mousex = e.originalEvent.touches[0].pageX - mousey = e.originalEvent.touches[0].pageY - - @fixbutton_addx = @fixbutton.offset().left - mousex - @fixbutton_addy = @fixbutton.offset().top - mousey - @startDrag() - @fixbutton.parent().on "click touchend touchcancel", (e) => - if (+ new Date) - @dragStarted < 100 - window.top.location = @fixbutton.find(".fixbutton-bg").attr("href") - @stopDrag() - @resized() - $(window).on "resize", @resized - - resized: => - @page_width = $(window).width() - @page_height = $(window).height() - @fixbutton_initx = @page_width - 75 # Initial x position - if @opened - @fixbutton.css - left: @fixbutton_initx - @width - else - @fixbutton.css - left: @fixbutton_initx - - # Start dragging the fixbutton - startDrag: -> - #@move_lock = "x" # Temporary until internals not finished - @log "startDrag", @fixbutton_initx, @fixbutton_inity - @fixbutton_targetx = @fixbutton_initx # Fallback x position - @fixbutton_targety = @fixbutton_inity # Fallback y position - - @fixbutton.addClass("dragging") - - # IE position wrap fix - if navigator.userAgent.indexOf('MSIE') != -1 or navigator.appVersion.indexOf('Trident/') > 0 - @fixbutton.css("pointer-events", "none") - - # Don't go to homepage - @fixbutton.one "click", (e) => - @stopDrag() - @fixbutton.removeClass("dragging") - moved_x = Math.abs(@fixbutton.offset().left - @fixbutton_initx) - moved_y = Math.abs(@fixbutton.offset().top - @fixbutton_inity) - if moved_x > 5 or moved_y > 10 - # If moved more than some pixel the button then don't go to homepage - e.preventDefault() - - # Animate drag - @fixbutton.parents().on "mousemove touchmove", @animDrag - @fixbutton.parents().on "mousemove touchmove" ,@waitMove - - # Stop dragging listener - @fixbutton.parents().one "mouseup touchend touchcancel", (e) => - e.preventDefault() - @stopDrag() - - - # Wait for moving the fixbutton - waitMove: (e) => - document.body.style.perspective = "1000px" - document.body.style.height = "100%" - document.body.style.willChange = "perspective" - document.documentElement.style.height = "100%" - #$(document.body).css("backface-visibility", "hidden").css("perspective", "1000px").css("height", "900px") - # $("iframe").css("backface-visibility", "hidden") - - moved_x = Math.abs(parseInt(@fixbutton[0].style.left) - @fixbutton_targetx) - moved_y = Math.abs(parseInt(@fixbutton[0].style.top) - @fixbutton_targety) - if moved_x > 5 and (+ new Date) - @dragStarted + moved_x > 50 - @moved("x") - @fixbutton.stop().animate {"top": @fixbutton_inity}, 1000 - @fixbutton.parents().off "mousemove touchmove" ,@waitMove - - else if moved_y > 5 and (+ new Date) - @dragStarted + moved_y > 50 - @moved("y") - @fixbutton.parents().off "mousemove touchmove" ,@waitMove - - moved: (direction) -> - @log "Moved", direction - @move_lock = direction - if direction == "y" - $(document.body).addClass("body-console") - return @console.createHtmltag() - @createHtmltag() - $(document.body).addClass("body-sidebar") - @container.on "mousedown touchend touchcancel", (e) => - if e.target != e.currentTarget - return true - @log "closing" - if $(document.body).hasClass("body-sidebar") - @close() - return true - - $(window).off "resize" - $(window).on "resize", => - $(document.body).css "height", $(window).height() - @scrollable() - @resized() - - # Override setsiteinfo to catch changes - @wrapper.setSiteInfo = (site_info) => - @setSiteInfo(site_info) - @original_set_site_info.apply(@wrapper, arguments) - - # Preload world.jpg - img = new Image(); - img.src = "/uimedia/globe/world.jpg"; - - setSiteInfo: (site_info) -> - RateLimit 1500, => - @updateHtmlTag() - RateLimit 30000, => - @displayGlobe() - - # Create the sidebar html tag - createHtmltag: -> - @when_loaded = $.Deferred() - if not @container - @container = $(""" - - """) - @container.appendTo(document.body) - @tag = @container.find(".sidebar") - @updateHtmlTag() - @scrollable = window.initScrollable() - - - updateHtmlTag: -> - if @preload_html - @setHtmlTag(@preload_html) - @preload_html = null - else - @wrapper.ws.cmd "sidebarGetHtmlTag", {}, @setHtmlTag - - setHtmlTag: (res) => - if @tag.find(".content").children().length == 0 # First update - @log "Creating content" - @container.addClass("loaded") - morphdom(@tag.find(".content")[0], '
      '+res+'
      ') - # @scrollable() - @when_loaded.resolve() - - else # Not first update, patch the html to keep unchanged dom elements - morphdom @tag.find(".content")[0], '
      '+res+'
      ', { - onBeforeMorphEl: (from_el, to_el) -> # Ignore globe loaded state - if from_el.className == "globe" or from_el.className.indexOf("noupdate") >= 0 - return false - else - return true - } - - # Save and forget privatekey for site signing - @tag.find("#privatekey-add").off("click, touchend").on "click touchend", (e) => - @wrapper.displayPrompt "Enter your private key:", "password", "Save", "", (privatekey) => - @wrapper.ws.cmd "userSetSitePrivatekey", [privatekey], (res) => - @wrapper.notifications.add "privatekey", "done", "Private key saved for site signing", 5000 - return false - - @tag.find("#privatekey-forget").off("click, touchend").on "click touchend", (e) => - @wrapper.displayConfirm "Remove saved private key for this site?", "Forget", (res) => - if not res - return false - @wrapper.ws.cmd "userSetSitePrivatekey", [""], (res) => - @wrapper.notifications.add "privatekey", "done", "Saved private key removed", 5000 - return false - - # Use requested address for browse files urls - @tag.find("#browse-files").attr("href", document.location.pathname.replace(/(\/.*?(\/|$)).*$/, "/list$1")) - - - - animDrag: (e) => - mousex = e.pageX - mousey = e.pageY - if not mousex and e.originalEvent.touches - mousex = e.originalEvent.touches[0].pageX - mousey = e.originalEvent.touches[0].pageY - - overdrag = @fixbutton_initx - @width - mousex - if overdrag > 0 # Overdragged - overdrag_percent = 1 + overdrag/300 - mousex = (mousex + (@fixbutton_initx-@width)*overdrag_percent)/(1+overdrag_percent) - targetx = @fixbutton_initx - mousex - @fixbutton_addx - targety = @fixbutton_inity - mousey - @fixbutton_addy - - if @move_lock == "x" - targety = @fixbutton_inity - else if @move_lock == "y" - targetx = @fixbutton_initx - - if not @move_lock or @move_lock == "x" - @fixbutton[0].style.left = (mousex + @fixbutton_addx) + "px" - if @tag - @tag[0].style.transform = "translateX(#{0 - targetx}px)" - - if not @move_lock or @move_lock == "y" - @fixbutton[0].style.top = (mousey + @fixbutton_addy) + "px" - if @console.tag - @console.tag[0].style.transform = "translateY(#{0 - targety}px)" - - #if @move_lock == "x" - # @fixbutton[0].style.left = "#{@fixbutton_targetx} px" - #@fixbutton[0].style.top = "#{@fixbutton_inity}px" - #if @move_lock == "y" - # @fixbutton[0].style.top = "#{@fixbutton_targety} px" - - # Check if opened - if (not @opened and targetx > @width/3) or (@opened and targetx > @width*0.9) - @fixbutton_targetx = @fixbutton_initx - @width # Make it opened - else - @fixbutton_targetx = @fixbutton_initx - - if (not @console.opened and 0 - targety > @page_height/10) or (@console.opened and 0 - targety > @page_height*0.8) - @fixbutton_targety = @page_height - @fixbutton_inity - 50 - else - @fixbutton_targety = @fixbutton_inity - - - # Stop dragging the fixbutton - stopDrag: -> - @fixbutton.parents().off "mousemove touchmove" - @fixbutton.off "mousemove touchmove" - @fixbutton.css("pointer-events", "") - $(".drag-bg").remove() - if not @fixbutton.hasClass("dragging") - return - @fixbutton.removeClass("dragging") - - # Move back to initial position - if @fixbutton_targetx != @fixbutton.offset().left or @fixbutton_targety != @fixbutton.offset().top - # Animate fixbutton - if @move_lock == "y" - top = @fixbutton_targety - left = @fixbutton_initx - if @move_lock == "x" - top = @fixbutton_inity - left = @fixbutton_targetx - @fixbutton.stop().animate {"left": left, "top": top}, 500, "easeOutBack", => - # Switch back to auto align - if @fixbutton_targetx == @fixbutton_initx # Closed - @fixbutton.css("left", "auto") - else # Opened - @fixbutton.css("left", left) - - $(".fixbutton-bg").trigger "mouseout" # Switch fixbutton back to normal status - - @stopDragX() - @console.stopDragY() - @move_lock = null - - stopDragX: -> - # Animate sidebar and iframe - if @fixbutton_targetx == @fixbutton_initx or @move_lock == "y" - # Closed - targetx = 0 - @opened = false - else - # Opened - targetx = @width - if @opened - @onOpened() - else - @when_loaded.done => - @onOpened() - @opened = true - - # Revent sidebar transitions - if @tag - @tag.css("transition", "0.4s ease-out") - @tag.css("transform", "translateX(-#{targetx}px)").one transitionEnd, => - @tag.css("transition", "") - if not @opened - @container.remove() - @container = null - if @tag - @tag.remove() - @tag = null - - # Revert body transformations - @log "stopdrag", "opened:", @opened - if not @opened - @onClosed() - - sign: (inner_path, privatekey) -> - @wrapper.displayProgress("sign", "Signing: #{inner_path}...", 0) - @wrapper.ws.cmd "siteSign", {privatekey: privatekey, inner_path: inner_path, update_changed_files: true}, (res) => - if res == "ok" - @wrapper.displayProgress("sign", "#{inner_path} signed!", 100) - else - @wrapper.displayProgress("sign", "Error signing #{inner_path}", -1) - - publish: (inner_path, privatekey) -> - @wrapper.ws.cmd "sitePublish", {privatekey: privatekey, inner_path: inner_path, sign: true, update_changed_files: true}, (res) => - if res == "ok" - @wrapper.notifications.add "sign", "done", "#{inner_path} Signed and published!", 5000 - - handleSiteDeleteClick: -> - if @wrapper.site_info.privatekey - question = "Are you sure?
      This site has a saved private key" - options = ["Forget private key and delete site"] - else - question = "Are you sure?" - options = ["Delete this site", "Blacklist"] - @wrapper.displayConfirm question, options, (confirmed) => - if confirmed == 1 - @tag.find("#button-delete").addClass("loading") - @wrapper.ws.cmd "siteDelete", @wrapper.site_info.address, -> - document.location = $(".fixbutton-bg").attr("href") - else if confirmed == 2 - @wrapper.displayPrompt "Blacklist this site", "text", "Delete and Blacklist", "Reason", (reason) => - @tag.find("#button-delete").addClass("loading") - @wrapper.ws.cmd "siteblockAdd", [@wrapper.site_info.address, reason] - @wrapper.ws.cmd "siteDelete", @wrapper.site_info.address, -> - document.location = $(".fixbutton-bg").attr("href") - - onOpened: -> - @log "Opened" - @scrollable() - - # Re-calculate height when site admin opened or closed - @tag.find("#checkbox-owned, #checkbox-autodownloadoptional").off("click touchend").on "click touchend", => - setTimeout (=> - @scrollable() - ), 300 - - # Site limit button - @tag.find("#button-sitelimit").off("click touchend").on "click touchend", => - @wrapper.ws.cmd "siteSetLimit", $("#input-sitelimit").val(), (res) => - if res == "ok" - @wrapper.notifications.add "done-sitelimit", "done", "Site storage limit modified!", 5000 - @updateHtmlTag() - return false - - # Site autodownload limit button - @tag.find("#button-autodownload_bigfile_size_limit").off("click touchend").on "click touchend", => - @wrapper.ws.cmd "siteSetAutodownloadBigfileLimit", $("#input-autodownload_bigfile_size_limit").val(), (res) => - if res == "ok" - @wrapper.notifications.add "done-bigfilelimit", "done", "Site bigfile auto download limit modified!", 5000 - @updateHtmlTag() - return false - - # Site start download optional files - @tag.find("#button-autodownload_previous").off("click touchend").on "click touchend", => - @wrapper.ws.cmd "siteUpdate", {"address": @wrapper.site_info.address, "check_files": true}, => - @wrapper.notifications.add "done-download_optional", "done", "Optional files downloaded", 5000 - - @wrapper.notifications.add "start-download_optional", "info", "Optional files download started", 5000 - return false - - # Database reload - @tag.find("#button-dbreload").off("click touchend").on "click touchend", => - @wrapper.ws.cmd "dbReload", [], => - @wrapper.notifications.add "done-dbreload", "done", "Database schema reloaded!", 5000 - @updateHtmlTag() - return false - - # Database rebuild - @tag.find("#button-dbrebuild").off("click touchend").on "click touchend", => - @wrapper.notifications.add "done-dbrebuild", "info", "Database rebuilding...." - @wrapper.ws.cmd "dbRebuild", [], => - @wrapper.notifications.add "done-dbrebuild", "done", "Database rebuilt!", 5000 - @updateHtmlTag() - return false - - # Update site - @tag.find("#button-update").off("click touchend").on "click touchend", => - @tag.find("#button-update").addClass("loading") - @wrapper.ws.cmd "siteUpdate", @wrapper.site_info.address, => - @wrapper.notifications.add "done-updated", "done", "Site updated!", 5000 - @tag.find("#button-update").removeClass("loading") - return false - - # Pause site - @tag.find("#button-pause").off("click touchend").on "click touchend", => - @tag.find("#button-pause").addClass("hidden") - @wrapper.ws.cmd "sitePause", @wrapper.site_info.address - return false - - # Resume site - @tag.find("#button-resume").off("click touchend").on "click touchend", => - @tag.find("#button-resume").addClass("hidden") - @wrapper.ws.cmd "siteResume", @wrapper.site_info.address - return false - - # Delete site - @tag.find("#button-delete").off("click touchend").on "click touchend", => - @handleSiteDeleteClick() - return false - - # Owned checkbox - @tag.find("#checkbox-owned").off("click touchend").on "click touchend", => - owned = @tag.find("#checkbox-owned").is(":checked") - @wrapper.ws.cmd "siteSetOwned", [owned], (res_set_owned) => - @log "Owned", owned - if owned - @wrapper.ws.cmd "siteRecoverPrivatekey", [], (res_recover) => - if res_recover == "ok" - @wrapper.notifications.add("recover", "done", "Private key recovered from master seed", 5000) - else - @log "Unable to recover private key: #{res_recover.error}" - - - # Owned auto download checkbox - @tag.find("#checkbox-autodownloadoptional").off("click touchend").on "click touchend", => - @wrapper.ws.cmd "siteSetAutodownloadoptional", [@tag.find("#checkbox-autodownloadoptional").is(":checked")] - - # Change identity button - @tag.find("#button-identity").off("click touchend").on "click touchend", => - @wrapper.ws.cmd "certSelect" - return false - - # Save settings - @tag.find("#button-settings").off("click touchend").on "click touchend", => - @wrapper.ws.cmd "fileGet", "content.json", (res) => - data = JSON.parse(res) - data["title"] = $("#settings-title").val() - data["description"] = $("#settings-description").val() - json_raw = unescape(encodeURIComponent(JSON.stringify(data, undefined, '\t'))) - @wrapper.ws.cmd "fileWrite", ["content.json", btoa(json_raw), true], (res) => - if res != "ok" # fileWrite failed - @wrapper.notifications.add "file-write", "error", "File write error: #{res}" - else - @wrapper.notifications.add "file-write", "done", "Site settings saved!", 5000 - if @wrapper.site_info.privatekey - @wrapper.ws.cmd "siteSign", {privatekey: "stored", inner_path: "content.json", update_changed_files: true} - @updateHtmlTag() - return false - - - # Open site directory - @tag.find("#link-directory").off("click touchend").on "click touchend", => - @wrapper.ws.cmd "serverShowdirectory", ["site", @wrapper.site_info.address] - return false - - # Copy site with peers - @tag.find("#link-copypeers").off("click touchend").on "click touchend", (e) => - copy_text = e.currentTarget.href - handler = (e) => - e.clipboardData.setData('text/plain', copy_text) - e.preventDefault() - @wrapper.notifications.add "copy", "done", "Site address with peers copied to your clipboard", 5000 - document.removeEventListener('copy', handler, true) - - document.addEventListener('copy', handler, true) - document.execCommand('copy') - return false - - # Sign and publish content.json - $(document).on "click touchend", => - @tag?.find("#button-sign-publish-menu").removeClass("visible") - @tag?.find(".contents + .flex").removeClass("sign-publish-flex") - - @tag.find(".contents-content").off("click touchend").on "click touchend", (e) => - $("#input-contents").val(e.currentTarget.innerText); - return false; - - menu = new Menu(@tag.find("#menu-sign-publish")) - menu.elem.css("margin-top", "-130px") # Open upwards - menu.addItem "Sign", => - inner_path = @tag.find("#input-contents").val() - - @wrapper.ws.cmd "fileRules", {inner_path: inner_path}, (rules) => - if @wrapper.site_info.auth_address in rules.signers - # ZeroID or other ID provider - @sign(inner_path) - else if @wrapper.site_info.privatekey - # Privatekey stored in users.json - @sign(inner_path, "stored") - else - # Ask the user for privatekey - @wrapper.displayPrompt "Enter your private key:", "password", "Sign", "", (privatekey) => # Prompt the private key - @sign(inner_path, privatekey) - - @tag.find(".contents + .flex").removeClass "active" - menu.hide() - - menu.addItem "Publish", => - inner_path = @tag.find("#input-contents").val() - @wrapper.ws.cmd "sitePublish", {"inner_path": inner_path, "sign": false} - - @tag.find(".contents + .flex").removeClass "active" - menu.hide() - - @tag.find("#menu-sign-publish").off("click touchend").on "click touchend", => - if window.visible_menu == menu - @tag.find(".contents + .flex").removeClass "active" - menu.hide() - else - @tag.find(".contents + .flex").addClass "active" - @tag.find(".content-wrapper").prop "scrollTop", 10000 - menu.show() - return false - - $("body").on "click", => - if @tag - @tag.find(".contents + .flex").removeClass "active" - - @tag.find("#button-sign-publish").off("click touchend").on "click touchend", => - inner_path = @tag.find("#input-contents").val() - - @wrapper.ws.cmd "fileRules", {inner_path: inner_path}, (rules) => - if @wrapper.site_info.auth_address in rules.signers - # ZeroID or other ID provider - @publish(inner_path, null) - else if @wrapper.site_info.privatekey - # Privatekey stored in users.json - @publish(inner_path, "stored") - else - # Ask the user for privatekey - @wrapper.displayPrompt "Enter your private key:", "password", "Sign", "", (privatekey) => # Prompt the private key - @publish(inner_path, privatekey) - return false - - # Close - @tag.find(".close").off("click touchend").on "click touchend", (e) => - @close() - return false - - @loadGlobe() - - close: -> - @move_lock = "x" - @startDrag() - @stopDrag() - - - onClosed: -> - $(window).off "resize" - $(window).on "resize", @resized - $(document.body).css("transition", "0.6s ease-in-out").removeClass("body-sidebar").on transitionEnd, (e) => - if e.target == document.body and not $(document.body).hasClass("body-sidebar") and not $(document.body).hasClass("body-console") - $(document.body).css("height", "auto").css("perspective", "").css("will-change", "").css("transition", "").off transitionEnd - @unloadGlobe() - - # We dont need site info anymore - @wrapper.setSiteInfo = @original_set_site_info - - - loadGlobe: => - if @tag.find(".globe").hasClass("loading") - setTimeout (=> - if typeof(DAT) == "undefined" # Globe script not loaded, do it first - script_tag = $(" - - diff --git a/plugins/UiConfig/media/css/Config.css b/plugins/UiConfig/media/css/Config.css deleted file mode 100644 index 2211758e..00000000 --- a/plugins/UiConfig/media/css/Config.css +++ /dev/null @@ -1,68 +0,0 @@ -body { background-color: #EDF2F5; font-family: Roboto, 'Segoe UI', Arial, 'Helvetica Neue'; margin: 0px; padding: 0px; backface-visibility: hidden; } -h1, h2, h3, h4 { font-family: 'Roboto', Arial, sans-serif; font-weight: 200; font-size: 30px; margin: 0px; padding: 0px } -h1 { background: linear-gradient(33deg,#af3bff,#0d99c9); color: white; padding: 16px 30px; } -h2 { margin-top: 10px; } -h3 { font-weight: normal } -a { color: #9760F9 } -a:hover { text-decoration: none } - -.link { background-color: transparent; outline: 5px solid transparent; transition: all 0.3s } -.link:active { background-color: #EFEFEF; outline: 5px solid #EFEFEF; transition: none } - -.content { max-width: 800px; margin: auto; background-color: white; padding: 60px 20px; box-sizing: border-box; padding-bottom: 150px; } -.section { margin: 0px 10%; } -.config-items { font-size: 19px; margin-top: 25px; margin-bottom: 75px; } -.config-item { transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); position: relative; padding-bottom: 20px; padding-top: 10px; } -.config-item.hidden { opacity: 0; height: 0px; padding: 0px; } -.config-item .title { display: inline-block; line-height: 36px; } -.config-item .title h3 { font-size: 20px; font-weight: lighter; margin-right: 100px; } -.config-item .description { font-size: 14px; color: #666; line-height: 24px; } -.config-item .value { display: inline-block; white-space: nowrap; } -.config-item .value-right { right: 0px; position: absolute; } -.config-item .value-fullwidth { width: 100% } -.config-item .marker { - font-weight: bold; text-decoration: none; font-size: 25px; position: absolute; padding: 2px 15px; line-height: 32px; - opacity: 0; pointer-events: none; transition: all 0.6s; transform: scale(2); color: #9760F9; -} -.config-item .marker.visible { opacity: 1; pointer-events: all; transform: scale(1); } -.config-item .marker.changed { color: #2ecc71; } -.config-item .marker.pending { color: #ffa200; } - - -.input-text, .input-select { padding: 8px 18px; border: 1px solid #CCC; border-radius: 3px; font-size: 17px; box-sizing: border-box; } -.input-text:focus, .input-select:focus { border: 1px solid #3396ff; outline: none; } -.input-textarea { overflow-x: auto; overflow-y: hidden; white-space: pre; line-height: 22px; } - -.input-select { width: initial; font-size: 14px; padding-right: 10px; padding-left: 10px; } - -.value-right .input-text { text-align: right; width: 100px; } -.value-fullwidth .input-text { width: 100%; font-size: 14px; font-family: 'Segoe UI', Arial, 'Helvetica Neue'; } -.value-fullwidth { margin-top: 10px; } - -/* Checkbox */ -.checkbox-skin { background-color: #CCC; width: 50px; height: 24px; border-radius: 15px; transition: all 0.3s ease-in-out; display: inline-block; } -.checkbox-skin:before { - content: ""; position: relative; width: 20px; background-color: white; height: 20px; display: block; border-radius: 100%; margin-top: 2px; margin-left: 2px; - transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); -} -.checkbox { font-size: 14px; font-weight: normal; display: inline-block; cursor: pointer; margin-top: 5px; } -.checkbox .title { display: inline; line-height: 30px; vertical-align: 4px; margin-left: 11px } -.checkbox.checked .checkbox-skin:before { margin-left: 27px; } -.checkbox.checked .checkbox-skin { background-color: #2ECC71 } - -/* Bottom */ - -.bottom { - width: 100%; text-align: center; background-color: #ffffffde; padding: 25px; bottom: -120px; -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px); - transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); position: fixed; backface-visibility: hidden; box-sizing: border-box; -} -.bottom-content { max-width: 750px; width: 100%; margin: 0px auto; } -.bottom .button { float: right; } -.bottom.visible { bottom: 0px; box-shadow: 0px 0px 35px #dcdcdc; } -.bottom .title { padding: 10px 10px; color: #363636; float: left; text-transform: uppercase; letter-spacing: 1px; } -.bottom .title:before { content: "•"; display: inline-block; color: #2ecc71; font-size: 31px; vertical-align: -7px; margin-right: 8px; line-height: 25px; } -.bottom-restart .title:before { color: #ffa200; } - -.animate { transition: all 0.3s ease-out !important; } -.animate-back { transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; } -.animate-inout { transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; } diff --git a/plugins/UiConfig/media/css/all.css b/plugins/UiConfig/media/css/all.css deleted file mode 100644 index 2b2991d0..00000000 --- a/plugins/UiConfig/media/css/all.css +++ /dev/null @@ -1,124 +0,0 @@ - -/* ---- Config.css ---- */ - - -body { background-color: #EDF2F5; font-family: Roboto, 'Segoe UI', Arial, 'Helvetica Neue'; margin: 0px; padding: 0px; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden ; } -h1, h2, h3, h4 { font-family: 'Roboto', Arial, sans-serif; font-weight: 200; font-size: 30px; margin: 0px; padding: 0px } -h1 { background: -webkit-linear-gradient(33deg,#af3bff,#0d99c9);background: -moz-linear-gradient(33deg,#af3bff,#0d99c9);background: -o-linear-gradient(33deg,#af3bff,#0d99c9);background: -ms-linear-gradient(33deg,#af3bff,#0d99c9);background: linear-gradient(33deg,#af3bff,#0d99c9); color: white; padding: 16px 30px; } -h2 { margin-top: 10px; } -h3 { font-weight: normal } -a { color: #9760F9 } -a:hover { text-decoration: none } - -.link { background-color: transparent; outline: 5px solid transparent; -webkit-transition: all 0.3s ; -moz-transition: all 0.3s ; -o-transition: all 0.3s ; -ms-transition: all 0.3s ; transition: all 0.3s } -.link:active { background-color: #EFEFEF; outline: 5px solid #EFEFEF; -webkit-transition: none ; -moz-transition: none ; -o-transition: none ; -ms-transition: none ; transition: none } - -.content { max-width: 800px; margin: auto; background-color: white; padding: 60px 20px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; padding-bottom: 150px; } -.section { margin: 0px 10%; } -.config-items { font-size: 19px; margin-top: 25px; margin-bottom: 75px; } -.config-item { -webkit-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -moz-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -o-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -ms-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1) ; position: relative; padding-bottom: 20px; padding-top: 10px; } -.config-item.hidden { opacity: 0; height: 0px; padding: 0px; } -.config-item .title { display: inline-block; line-height: 36px; } -.config-item .title h3 { font-size: 20px; font-weight: lighter; margin-right: 100px; } -.config-item .description { font-size: 14px; color: #666; line-height: 24px; } -.config-item .value { display: inline-block; white-space: nowrap; } -.config-item .value-right { right: 0px; position: absolute; } -.config-item .value-fullwidth { width: 100% } -.config-item .marker { - font-weight: bold; text-decoration: none; font-size: 25px; position: absolute; padding: 2px 15px; line-height: 32px; - opacity: 0; pointer-events: none; -webkit-transition: all 0.6s; -moz-transition: all 0.6s; -o-transition: all 0.6s; -ms-transition: all 0.6s; transition: all 0.6s ; -webkit-transform: scale(2); -moz-transform: scale(2); -o-transform: scale(2); -ms-transform: scale(2); transform: scale(2) ; color: #9760F9; -} -.config-item .marker.visible { opacity: 1; pointer-events: all; -webkit-transform: scale(1); -moz-transform: scale(1); -o-transform: scale(1); -ms-transform: scale(1); transform: scale(1) ; } -.config-item .marker.changed { color: #2ecc71; } -.config-item .marker.pending { color: #ffa200; } - - -.input-text, .input-select { padding: 8px 18px; border: 1px solid #CCC; -webkit-border-radius: 3px; -moz-border-radius: 3px; -o-border-radius: 3px; -ms-border-radius: 3px; border-radius: 3px ; font-size: 17px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; } -.input-text:focus, .input-select:focus { border: 1px solid #3396ff; outline: none; } -.input-textarea { overflow-x: auto; overflow-y: hidden; white-space: pre; line-height: 22px; } - -.input-select { width: initial; font-size: 14px; padding-right: 10px; padding-left: 10px; } - -.value-right .input-text { text-align: right; width: 100px; } -.value-fullwidth .input-text { width: 100%; font-size: 14px; font-family: 'Segoe UI', Arial, 'Helvetica Neue'; } -.value-fullwidth { margin-top: 10px; } - -/* Checkbox */ -.checkbox-skin { background-color: #CCC; width: 50px; height: 24px; -webkit-border-radius: 15px; -moz-border-radius: 15px; -o-border-radius: 15px; -ms-border-radius: 15px; border-radius: 15px ; -webkit-transition: all 0.3s ease-in-out; -moz-transition: all 0.3s ease-in-out; -o-transition: all 0.3s ease-in-out; -ms-transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out ; display: inline-block; } -.checkbox-skin:before { - content: ""; position: relative; width: 20px; background-color: white; height: 20px; display: block; -webkit-border-radius: 100%; -moz-border-radius: 100%; -o-border-radius: 100%; -ms-border-radius: 100%; border-radius: 100% ; margin-top: 2px; margin-left: 2px; - -webkit-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); -moz-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); -o-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); -ms-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86) ; -} -.checkbox { font-size: 14px; font-weight: normal; display: inline-block; cursor: pointer; margin-top: 5px; } -.checkbox .title { display: inline; line-height: 30px; vertical-align: 4px; margin-left: 11px } -.checkbox.checked .checkbox-skin:before { margin-left: 27px; } -.checkbox.checked .checkbox-skin { background-color: #2ECC71 } - -/* Bottom */ - -.bottom { - width: 100%; text-align: center; background-color: #ffffffde; padding: 25px; bottom: -120px; -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px); - -webkit-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -moz-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -o-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -ms-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1) ; position: fixed; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden ; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; -} -.bottom-content { max-width: 750px; width: 100%; margin: 0px auto; } -.bottom .button { float: right; } -.bottom.visible { bottom: 0px; -webkit-box-shadow: 0px 0px 35px #dcdcdc; -moz-box-shadow: 0px 0px 35px #dcdcdc; -o-box-shadow: 0px 0px 35px #dcdcdc; -ms-box-shadow: 0px 0px 35px #dcdcdc; box-shadow: 0px 0px 35px #dcdcdc ; } -.bottom .title { padding: 10px 10px; color: #363636; float: left; text-transform: uppercase; letter-spacing: 1px; } -.bottom .title:before { content: "•"; display: inline-block; color: #2ecc71; font-size: 31px; vertical-align: -7px; margin-right: 8px; line-height: 25px; } -.bottom-restart .title:before { color: #ffa200; } - -.animate { -webkit-transition: all 0.3s ease-out !important; -moz-transition: all 0.3s ease-out !important; -o-transition: all 0.3s ease-out !important; -ms-transition: all 0.3s ease-out !important; transition: all 0.3s ease-out !important ; } -.animate-back { -webkit-transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; -moz-transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; -o-transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; -ms-transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important ; } -.animate-inout { -webkit-transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; -moz-transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; -o-transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; -ms-transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important ; } - - -/* ---- button.css ---- */ - - -/* Button */ -.button { - background-color: #FFDC00; color: black; padding: 10px 20px; display: inline-block; background-position: left center; - -webkit-border-radius: 2px; -moz-border-radius: 2px; -o-border-radius: 2px; -ms-border-radius: 2px; border-radius: 2px ; border-bottom: 2px solid #E8BE29; -webkit-transition: all 0.5s ease-out; -moz-transition: all 0.5s ease-out; -o-transition: all 0.5s ease-out; -ms-transition: all 0.5s ease-out; transition: all 0.5s ease-out ; text-decoration: none; -} -.button:hover { border-color: white; border-bottom: 2px solid #BD960C; -webkit-transition: none ; -moz-transition: none ; -o-transition: none ; -ms-transition: none ; transition: none ; background-color: #FDEB07 } -.button:active { position: relative; top: 1px } -.button.loading { - color: rgba(0,0,0,0); background: #999 url(../img/loading.gif) no-repeat center center; - -webkit-transition: all 0.5s ease-out ; -moz-transition: all 0.5s ease-out ; -o-transition: all 0.5s ease-out ; -ms-transition: all 0.5s ease-out ; transition: all 0.5s ease-out ; pointer-events: none; border-bottom: 2px solid #666 -} -.button.disabled { color: #DDD; background-color: #999; pointer-events: none; border-bottom: 2px solid #666 } - - -/* ---- fonts.css ---- */ - - -/* Base64 encoder: http://www.motobit.com/util/base64-decoder-encoder.asp */ -/* Generated by Font Squirrel (http://www.fontsquirrel.com) on January 21, 2015 */ - - -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 400; - src: - local('Roboto'), - url(data:application/x-font-woff;charset=utf-8;base64,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) format('woff'); -} - -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: bold; - src: - local('Roboto Medium'), - url(data:application/x-font-woff;charset=utf-8;base64,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) format('woff'), -} - -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 200; - src: - local('Roboto Light'), - url(data:application/x-font-woff;charset=utf-8;base64,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) format('woff'); -} \ No newline at end of file diff --git a/plugins/UiConfig/media/css/button.css b/plugins/UiConfig/media/css/button.css deleted file mode 100644 index f69021bf..00000000 --- a/plugins/UiConfig/media/css/button.css +++ /dev/null @@ -1,12 +0,0 @@ -/* Button */ -.button { - background-color: #FFDC00; color: black; padding: 10px 20px; display: inline-block; background-position: left center; - border-radius: 2px; border-bottom: 2px solid #E8BE29; transition: all 0.5s ease-out; text-decoration: none; -} -.button:hover { border-color: white; border-bottom: 2px solid #BD960C; transition: none ; background-color: #FDEB07 } -.button:active { position: relative; top: 1px } -.button.loading { - color: rgba(0,0,0,0); background: #999 url(../img/loading.gif) no-repeat center center; - transition: all 0.5s ease-out ; pointer-events: none; border-bottom: 2px solid #666 -} -.button.disabled { color: #DDD; background-color: #999; pointer-events: none; border-bottom: 2px solid #666 } diff --git a/plugins/UiConfig/media/css/fonts.css b/plugins/UiConfig/media/css/fonts.css deleted file mode 100644 index f5576c5a..00000000 --- a/plugins/UiConfig/media/css/fonts.css +++ /dev/null @@ -1,30 +0,0 @@ -/* Base64 encoder: http://www.motobit.com/util/base64-decoder-encoder.asp */ -/* Generated by Font Squirrel (http://www.fontsquirrel.com) on January 21, 2015 */ - - -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 400; - src: - local('Roboto'), - url(data:application/x-font-woff;charset=utf-8;base64,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) format('woff'); -} - -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: bold; - src: - local('Roboto Medium'), - url(data:application/x-font-woff;charset=utf-8;base64,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) format('woff'), -} - -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 200; - src: - local('Roboto Light'), - url(data:application/x-font-woff;charset=utf-8;base64,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) format('woff'); -} \ No newline at end of file diff --git a/plugins/UiConfig/media/img/loading.gif b/plugins/UiConfig/media/img/loading.gif deleted file mode 100644 index 27d0aa8108b0800f9cddcf613f787347d9981e05..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 723 zcmZ?wbhEHb6ky0iXa9k4z>53@HBR_Hzvhc6JPKHPSO+W(0~W{*!Vp zN=+!e)X>LZSp~!n_rkGVK%h z9k_L9<(o^(d!N7A`+9eTzQ!EZMr*-N2_|eB&45;SC+a-zP~lXP;z?eTv`FKm^!Y8l zuZ^S*OlLmOv^VZNyYQx zE1Q1d^QD!~t!MErXFkzlm$bqCmuUZ)iN%&IQkAQ(b??%e8>EQMBqK<8T-y}!%q4L0 z4v$MoL7}cEx5PfOihDclHe=f1_`ny+jJ+qGonTF#=e6?cS1GK1Glv+XQW)E^VpGzx z%$u!=(=#3~+Lk*jmQUf$-=^(}f)AMWru(Y&&oE(%*JUs>JH24vgCGuUPSS^%^#tgi z6`S6zDw0tR+QR$5bp7w`G6mDQzjYm%RoE)?D^8cegv~i}{SvGJM6wyypd - @items = [] - @createSections() - @setValues(@config) - - setValues: (values) -> - for section in @items - for item in section.items - if not values[item.key] - continue - item.value = @formatValue(values[item.key].value) - item.default = @formatValue(values[item.key].default) - item.pending = values[item.key].pending - values[item.key].item = item - - formatValue: (value) -> - if not value - return false - else if typeof(value) == "object" - return value.join("\n") - else if typeof(value) == "number" - return value.toString() - else - return value - - deformatValue: (value, type) -> - if type == "object" and typeof(value) == "string" - if not value.length - return value = null - else - return value.split("\n") - if type == "boolean" and not value - return false - else if type == "number" - if typeof(value) == "number" - return value.toString() - else if not value - return "0" - else - return value - else - return value - - createSections: -> - # Web Interface - section = @createSection("Web Interface") - - section.items.push - key: "open_browser" - title: "Open web browser on ZeroNet startup" - type: "checkbox" - - # Network - section = @createSection("Network") - section.items.push - key: "offline" - title: "Offline mode" - type: "checkbox" - description: "Disable network communication." - - section.items.push - key: "fileserver_ip_type" - title: "File server network" - type: "select" - options: [ - {title: "IPv4", value: "ipv4"} - {title: "IPv6", value: "ipv6"} - {title: "Dual (IPv4 & IPv6)", value: "dual"} - ] - description: "Accept incoming peers using IPv4 or IPv6 address. (default: dual)" - - section.items.push - key: "fileserver_port" - title: "File server port" - type: "text" - valid_pattern: /[0-9]*/ - description: "Other peers will use this port to reach your served sites. (default: randomize)" - - section.items.push - key: "ip_external" - title: "File server external ip" - type: "textarea" - placeholder: "Detect automatically" - description: "Your file server is accessible on these ips. (default: detect automatically)" - - section.items.push - title: "Tor" - key: "tor" - type: "select" - options: [ - {title: "Disable", value: "disable"} - {title: "Enable", value: "enable"} - {title: "Always", value: "always"} - ] - description: [ - "Disable: Don't connect to peers on Tor network", h("br"), - "Enable: Only use Tor for Tor network peers", h("br"), - "Always: Use Tor for every connections to hide your IP address (slower)" - ] - - section.items.push - title: "Use Tor bridges" - key: "tor_use_bridges" - type: "checkbox" - description: "Use obfuscated bridge relays to avoid network level Tor block (even slower)" - isHidden: -> - return not Page.server_info.tor_has_meek_bridges - - section.items.push - title: "Trackers" - key: "trackers" - type: "textarea" - description: "Discover new peers using these adresses" - - section.items.push - title: "Trackers files" - key: "trackers_file" - type: "textarea" - description: "Load additional list of torrent trackers dynamically, from a file" - placeholder: "Eg.: {data_dir}/trackers.json" - value_pos: "fullwidth" - - section.items.push - title: "Proxy for tracker connections" - key: "trackers_proxy" - type: "select" - options: [ - {title: "Custom", value: ""} - {title: "Tor", value: "tor"} - {title: "Disable", value: "disable"} - ] - isHidden: -> - Page.values["tor"] == "always" - - section.items.push - title: "Custom socks proxy address for trackers" - key: "trackers_proxy" - type: "text" - placeholder: "Eg.: 127.0.0.1:1080" - value_pos: "fullwidth" - valid_pattern: /.+:[0-9]+/ - isHidden: => - Page.values["trackers_proxy"] in ["tor", "disable"] - - # Performance - section = @createSection("Performance") - - section.items.push - key: "log_level" - title: "Level of logging to file" - type: "select" - options: [ - {title: "Everything", value: "DEBUG"} - {title: "Only important messages", value: "INFO"} - {title: "Only errors", value: "ERROR"} - ] - - section.items.push - key: "threads_fs_read" - title: "Threads for async file system reads" - type: "select" - options: [ - {title: "Sync read", value: 0} - {title: "1 thread", value: 1} - {title: "2 threads", value: 2} - {title: "3 threads", value: 3} - {title: "4 threads", value: 4} - {title: "5 threads", value: 5} - {title: "10 threads", value: 10} - ] - - section.items.push - key: "threads_fs_write" - title: "Threads for async file system writes" - type: "select" - options: [ - {title: "Sync write", value: 0} - {title: "1 thread", value: 1} - {title: "2 threads", value: 2} - {title: "3 threads", value: 3} - {title: "4 threads", value: 4} - {title: "5 threads", value: 5} - {title: "10 threads", value: 10} - ] - - section.items.push - key: "threads_crypt" - title: "Threads for cryptographic functions" - type: "select" - options: [ - {title: "Sync execution", value: 0} - {title: "1 thread", value: 1} - {title: "2 threads", value: 2} - {title: "3 threads", value: 3} - {title: "4 threads", value: 4} - {title: "5 threads", value: 5} - {title: "10 threads", value: 10} - ] - - section.items.push - key: "threads_db" - title: "Threads for database operations" - type: "select" - options: [ - {title: "Sync execution", value: 0} - {title: "1 thread", value: 1} - {title: "2 threads", value: 2} - {title: "3 threads", value: 3} - {title: "4 threads", value: 4} - {title: "5 threads", value: 5} - {title: "10 threads", value: 10} - ] - - createSection: (title) => - section = {} - section.title = title - section.items = [] - @items.push(section) - return section - -window.ConfigStorage = ConfigStorage diff --git a/plugins/UiConfig/media/js/ConfigView.coffee b/plugins/UiConfig/media/js/ConfigView.coffee deleted file mode 100644 index 64b86e5e..00000000 --- a/plugins/UiConfig/media/js/ConfigView.coffee +++ /dev/null @@ -1,124 +0,0 @@ -class ConfigView extends Class - constructor: () -> - @ - - render: -> - @config_storage.items.map @renderSection - - renderSection: (section) => - h("div.section", {key: section.title}, [ - h("h2", section.title), - h("div.config-items", section.items.map @renderSectionItem) - ]) - - handleResetClick: (e) => - node = e.currentTarget - config_key = node.attributes.config_key.value - default_value = node.attributes.default_value?.value - Page.cmd "wrapperConfirm", ["Reset #{config_key} value?", "Reset to default"], (res) => - if (res) - @values[config_key] = default_value - Page.projector.scheduleRender() - - renderSectionItem: (item) => - value_pos = item.value_pos - - if item.type == "textarea" - value_pos ?= "fullwidth" - else - value_pos ?= "right" - - value_changed = @config_storage.formatValue(@values[item.key]) != item.value - value_default = @config_storage.formatValue(@values[item.key]) == item.default - - if item.key in ["open_browser", "fileserver_port"] # Value default for some settings makes no sense - value_default = true - - marker_title = "Changed from default value: #{item.default} -> #{@values[item.key]}" - if item.pending - marker_title += " (change pending until client restart)" - - if item.isHidden?() - return null - - h("div.config-item", {key: item.title, enterAnimation: Animation.slideDown, exitAnimation: Animation.slideUpInout}, [ - h("div.title", [ - h("h3", item.title), - h("div.description", item.description) - ]) - h("div.value.value-#{value_pos}", - if item.type == "select" - @renderValueSelect(item) - else if item.type == "checkbox" - @renderValueCheckbox(item) - else if item.type == "textarea" - @renderValueTextarea(item) - else - @renderValueText(item) - h("a.marker", { - href: "#Reset", title: marker_title, - onclick: @handleResetClick, config_key: item.key, default_value: item.default, - classes: {default: value_default, changed: value_changed, visible: not value_default or value_changed or item.pending, pending: item.pending} - }, "\u2022") - ) - ]) - - # Values - handleInputChange: (e) => - node = e.target - config_key = node.attributes.config_key.value - @values[config_key] = node.value - Page.projector.scheduleRender() - - handleCheckboxChange: (e) => - node = e.currentTarget - config_key = node.attributes.config_key.value - value = not node.classList.contains("checked") - @values[config_key] = value - Page.projector.scheduleRender() - - renderValueText: (item) => - value = @values[item.key] - if not value - value = "" - h("input.input-#{item.type}", {type: item.type, config_key: item.key, value: value, placeholder: item.placeholder, oninput: @handleInputChange}) - - autosizeTextarea: (e) => - if e.currentTarget - # @handleInputChange(e) - node = e.currentTarget - else - node = e - height_before = node.style.height - if height_before - node.style.height = "0px" - h = node.offsetHeight - scrollh = node.scrollHeight + 20 - if scrollh > h - node.style.height = scrollh + "px" - else - node.style.height = height_before - - renderValueTextarea: (item) => - value = @values[item.key] - if not value - value = "" - h("textarea.input-#{item.type}.input-text",{ - type: item.type, config_key: item.key, oninput: @handleInputChange, afterCreate: @autosizeTextarea, - updateAnimation: @autosizeTextarea, value: value, placeholder: item.placeholder - }) - - renderValueCheckbox: (item) => - if @values[item.key] and @values[item.key] != "False" - checked = true - else - checked = false - h("div.checkbox", {onclick: @handleCheckboxChange, config_key: item.key, classes: {checked: checked}}, h("div.checkbox-skin")) - - renderValueSelect: (item) => - h("select.input-select", {config_key: item.key, oninput: @handleInputChange}, - item.options.map (option) => - h("option", {selected: option.value.toString() == @values[item.key], value: option.value}, option.title) - ) - -window.ConfigView = ConfigView \ No newline at end of file diff --git a/plugins/UiConfig/media/js/UiConfig.coffee b/plugins/UiConfig/media/js/UiConfig.coffee deleted file mode 100644 index bc1ac697..00000000 --- a/plugins/UiConfig/media/js/UiConfig.coffee +++ /dev/null @@ -1,129 +0,0 @@ -window.h = maquette.h - -class UiConfig extends ZeroFrame - init: -> - @save_visible = true - @config = null # Setting currently set on the server - @values = null # Entered values on the page - @config_view = new ConfigView() - window.onbeforeunload = => - if @getValuesChanged().length > 0 - return true - else - return null - - onOpenWebsocket: => - @cmd("wrapperSetTitle", "Config - ZeroNet") - @cmd "serverInfo", {}, (server_info) => - @server_info = server_info - @restart_loading = false - @updateConfig() - - updateConfig: (cb) => - @cmd "configList", [], (res) => - @config = res - @values = {} - @config_storage = new ConfigStorage(@config) - @config_view.values = @values - @config_view.config_storage = @config_storage - for key, item of res - value = item.value - @values[key] = @config_storage.formatValue(value) - @projector.scheduleRender() - cb?() - - createProjector: => - @projector = maquette.createProjector() - @projector.replace($("#content"), @render) - @projector.replace($("#bottom-save"), @renderBottomSave) - @projector.replace($("#bottom-restart"), @renderBottomRestart) - - getValuesChanged: => - values_changed = [] - for key, value of @values - if @config_storage.formatValue(value) != @config_storage.formatValue(@config[key]?.value) - values_changed.push({key: key, value: value}) - return values_changed - - getValuesPending: => - values_pending = [] - for key, item of @config - if item.pending - values_pending.push(key) - return values_pending - - saveValues: (cb) => - changed_values = @getValuesChanged() - for item, i in changed_values - last = i == changed_values.length - 1 - value = @config_storage.deformatValue(item.value, typeof(@config[item.key].default)) - default_value = @config_storage.deformatValue(@config[item.key].default, typeof(@config[item.key].default)) - value_same_as_default = JSON.stringify(default_value) == JSON.stringify(value) - - if @config[item.key].item.valid_pattern and not @config[item.key].item.isHidden?() - match = value.match(@config[item.key].item.valid_pattern) - if not match or match[0] != value - message = "Invalid value of #{@config[item.key].item.title}: #{value} (does not matches #{@config[item.key].item.valid_pattern})" - Page.cmd("wrapperNotification", ["error", message]) - cb(false) - break - - if value_same_as_default - value = null - - @saveValue(item.key, value, if last then cb else null) - - saveValue: (key, value, cb) => - if key == "open_browser" - if value - value = "default_browser" - else - value = "False" - - Page.cmd "configSet", [key, value], (res) => - if res != "ok" - Page.cmd "wrapperNotification", ["error", res.error] - cb?(true) - - render: => - if not @config - return h("div.content") - - h("div.content", [ - @config_view.render() - ]) - - handleSaveClick: => - @save_loading = true - @logStart "Save" - @saveValues (success) => - @save_loading = false - @logEnd "Save" - if success - @updateConfig() - Page.projector.scheduleRender() - return false - - renderBottomSave: => - values_changed = @getValuesChanged() - h("div.bottom.bottom-save", {classes: {visible: values_changed.length}}, h("div.bottom-content", [ - h("div.title", "#{values_changed.length} configuration item value changed"), - h("a.button.button-submit.button-save", {href: "#Save", classes: {loading: @save_loading}, onclick: @handleSaveClick}, "Save settings") - ])) - - handleRestartClick: => - @restart_loading = true - Page.cmd("serverShutdown", {restart: true}) - Page.projector.scheduleRender() - return false - - renderBottomRestart: => - values_pending = @getValuesPending() - values_changed = @getValuesChanged() - h("div.bottom.bottom-restart", {classes: {visible: values_pending.length and not values_changed.length}}, h("div.bottom-content", [ - h("div.title", "Some changed settings requires restart"), - h("a.button.button-submit.button-restart", {href: "#Restart", classes: {loading: @restart_loading}, onclick: @handleRestartClick}, "Restart ZeroNet client") - ])) - -window.Page = new UiConfig() -window.Page.createProjector() diff --git a/plugins/UiConfig/media/js/all.js b/plugins/UiConfig/media/js/all.js deleted file mode 100644 index cb3b553f..00000000 --- a/plugins/UiConfig/media/js/all.js +++ /dev/null @@ -1,2066 +0,0 @@ - -/* ---- lib/Class.coffee ---- */ - - -(function() { - var Class, - slice = [].slice; - - Class = (function() { - function Class() {} - - Class.prototype.trace = true; - - Class.prototype.log = function() { - var args; - args = 1 <= arguments.length ? slice.call(arguments, 0) : []; - if (!this.trace) { - return; - } - if (typeof console === 'undefined') { - return; - } - args.unshift("[" + this.constructor.name + "]"); - console.log.apply(console, args); - return this; - }; - - Class.prototype.logStart = function() { - var args, name; - name = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; - if (!this.trace) { - return; - } - this.logtimers || (this.logtimers = {}); - this.logtimers[name] = +(new Date); - if (args.length > 0) { - this.log.apply(this, ["" + name].concat(slice.call(args), ["(started)"])); - } - return this; - }; - - Class.prototype.logEnd = function() { - var args, ms, name; - name = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; - ms = +(new Date) - this.logtimers[name]; - this.log.apply(this, ["" + name].concat(slice.call(args), ["(Done in " + ms + "ms)"])); - return this; - }; - - return Class; - - })(); - - window.Class = Class; - -}).call(this); - -/* ---- lib/Promise.coffee ---- */ - - -(function() { - var Promise, - slice = [].slice; - - Promise = (function() { - Promise.when = function() { - var args, fn, i, len, num_uncompleted, promise, task, task_id, tasks; - tasks = 1 <= arguments.length ? slice.call(arguments, 0) : []; - num_uncompleted = tasks.length; - args = new Array(num_uncompleted); - promise = new Promise(); - fn = function(task_id) { - return task.then(function() { - args[task_id] = Array.prototype.slice.call(arguments); - num_uncompleted--; - if (num_uncompleted === 0) { - return promise.complete.apply(promise, args); - } - }); - }; - for (task_id = i = 0, len = tasks.length; i < len; task_id = ++i) { - task = tasks[task_id]; - fn(task_id); - } - return promise; - }; - - function Promise() { - this.resolved = false; - this.end_promise = null; - this.result = null; - this.callbacks = []; - } - - Promise.prototype.resolve = function() { - var back, callback, i, len, ref; - if (this.resolved) { - return false; - } - this.resolved = true; - this.data = arguments; - if (!arguments.length) { - this.data = [true]; - } - this.result = this.data[0]; - ref = this.callbacks; - for (i = 0, len = ref.length; i < len; i++) { - callback = ref[i]; - back = callback.apply(callback, this.data); - } - if (this.end_promise) { - return this.end_promise.resolve(back); - } - }; - - Promise.prototype.fail = function() { - return this.resolve(false); - }; - - Promise.prototype.then = function(callback) { - if (this.resolved === true) { - callback.apply(callback, this.data); - return; - } - this.callbacks.push(callback); - return this.end_promise = new Promise(); - }; - - return Promise; - - })(); - - window.Promise = Promise; - - - /* - s = Date.now() - log = (text) -> - console.log Date.now()-s, Array.prototype.slice.call(arguments).join(", ") - - log "Started" - - cmd = (query) -> - p = new Promise() - setTimeout ( -> - p.resolve query+" Result" - ), 100 - return p - - back = cmd("SELECT * FROM message").then (res) -> - log res - return "Return from query" - .then (res) -> - log "Back then", res - - log "Query started", back - */ - -}).call(this); - -/* ---- lib/Prototypes.coffee ---- */ - - -(function() { - String.prototype.startsWith = function(s) { - return this.slice(0, s.length) === s; - }; - - String.prototype.endsWith = function(s) { - return s === '' || this.slice(-s.length) === s; - }; - - String.prototype.repeat = function(count) { - return new Array(count + 1).join(this); - }; - - window.isEmpty = function(obj) { - var key; - for (key in obj) { - return false; - } - return true; - }; - -}).call(this); - -/* ---- lib/maquette.js ---- */ - - -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(['exports'], factory); - } else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') { - // CommonJS - factory(exports); - } else { - // Browser globals - factory(root.maquette = {}); - } -}(this, function (exports) { - 'use strict'; - ; - ; - ; - ; - var NAMESPACE_W3 = 'http://www.w3.org/'; - var NAMESPACE_SVG = NAMESPACE_W3 + '2000/svg'; - var NAMESPACE_XLINK = NAMESPACE_W3 + '1999/xlink'; - // Utilities - var emptyArray = []; - var extend = function (base, overrides) { - var result = {}; - Object.keys(base).forEach(function (key) { - result[key] = base[key]; - }); - if (overrides) { - Object.keys(overrides).forEach(function (key) { - result[key] = overrides[key]; - }); - } - return result; - }; - // Hyperscript helper functions - var same = function (vnode1, vnode2) { - if (vnode1.vnodeSelector !== vnode2.vnodeSelector) { - return false; - } - if (vnode1.properties && vnode2.properties) { - if (vnode1.properties.key !== vnode2.properties.key) { - return false; - } - return vnode1.properties.bind === vnode2.properties.bind; - } - return !vnode1.properties && !vnode2.properties; - }; - var toTextVNode = function (data) { - return { - vnodeSelector: '', - properties: undefined, - children: undefined, - text: data.toString(), - domNode: null - }; - }; - var appendChildren = function (parentSelector, insertions, main) { - for (var i = 0; i < insertions.length; i++) { - var item = insertions[i]; - if (Array.isArray(item)) { - appendChildren(parentSelector, item, main); - } else { - if (item !== null && item !== undefined) { - if (!item.hasOwnProperty('vnodeSelector')) { - item = toTextVNode(item); - } - main.push(item); - } - } - } - }; - // Render helper functions - var missingTransition = function () { - throw new Error('Provide a transitions object to the projectionOptions to do animations'); - }; - var DEFAULT_PROJECTION_OPTIONS = { - namespace: undefined, - eventHandlerInterceptor: undefined, - styleApplyer: function (domNode, styleName, value) { - // Provides a hook to add vendor prefixes for browsers that still need it. - domNode.style[styleName] = value; - }, - transitions: { - enter: missingTransition, - exit: missingTransition - } - }; - var applyDefaultProjectionOptions = function (projectorOptions) { - return extend(DEFAULT_PROJECTION_OPTIONS, projectorOptions); - }; - var checkStyleValue = function (styleValue) { - if (typeof styleValue !== 'string') { - throw new Error('Style values must be strings'); - } - }; - var setProperties = function (domNode, properties, projectionOptions) { - if (!properties) { - return; - } - var eventHandlerInterceptor = projectionOptions.eventHandlerInterceptor; - var propNames = Object.keys(properties); - var propCount = propNames.length; - for (var i = 0; i < propCount; i++) { - var propName = propNames[i]; - /* tslint:disable:no-var-keyword: edge case */ - var propValue = properties[propName]; - /* tslint:enable:no-var-keyword */ - if (propName === 'className') { - throw new Error('Property "className" is not supported, use "class".'); - } else if (propName === 'class') { - if (domNode.className) { - // May happen if classes is specified before class - domNode.className += ' ' + propValue; - } else { - domNode.className = propValue; - } - } else if (propName === 'classes') { - // object with string keys and boolean values - var classNames = Object.keys(propValue); - var classNameCount = classNames.length; - for (var j = 0; j < classNameCount; j++) { - var className = classNames[j]; - if (propValue[className]) { - domNode.classList.add(className); - } - } - } else if (propName === 'styles') { - // object with string keys and string (!) values - var styleNames = Object.keys(propValue); - var styleCount = styleNames.length; - for (var j = 0; j < styleCount; j++) { - var styleName = styleNames[j]; - var styleValue = propValue[styleName]; - if (styleValue) { - checkStyleValue(styleValue); - projectionOptions.styleApplyer(domNode, styleName, styleValue); - } - } - } else if (propName === 'key') { - continue; - } else if (propValue === null || propValue === undefined) { - continue; - } else { - var type = typeof propValue; - if (type === 'function') { - if (propName.lastIndexOf('on', 0) === 0) { - if (eventHandlerInterceptor) { - propValue = eventHandlerInterceptor(propName, propValue, domNode, properties); // intercept eventhandlers - } - if (propName === 'oninput') { - (function () { - // record the evt.target.value, because IE and Edge sometimes do a requestAnimationFrame between changing value and running oninput - var oldPropValue = propValue; - propValue = function (evt) { - evt.target['oninput-value'] = evt.target.value; - // may be HTMLTextAreaElement as well - oldPropValue.apply(this, [evt]); - }; - }()); - } - domNode[propName] = propValue; - } - } else if (type === 'string' && propName !== 'value' && propName !== 'innerHTML') { - if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') { - domNode.setAttributeNS(NAMESPACE_XLINK, propName, propValue); - } else { - domNode.setAttribute(propName, propValue); - } - } else { - domNode[propName] = propValue; - } - } - } - }; - var updateProperties = function (domNode, previousProperties, properties, projectionOptions) { - if (!properties) { - return; - } - var propertiesUpdated = false; - var propNames = Object.keys(properties); - var propCount = propNames.length; - for (var i = 0; i < propCount; i++) { - var propName = propNames[i]; - // assuming that properties will be nullified instead of missing is by design - var propValue = properties[propName]; - var previousValue = previousProperties[propName]; - if (propName === 'class') { - if (previousValue !== propValue) { - throw new Error('"class" property may not be updated. Use the "classes" property for conditional css classes.'); - } - } else if (propName === 'classes') { - var classList = domNode.classList; - var classNames = Object.keys(propValue); - var classNameCount = classNames.length; - for (var j = 0; j < classNameCount; j++) { - var className = classNames[j]; - var on = !!propValue[className]; - var previousOn = !!previousValue[className]; - if (on === previousOn) { - continue; - } - propertiesUpdated = true; - if (on) { - classList.add(className); - } else { - classList.remove(className); - } - } - } else if (propName === 'styles') { - var styleNames = Object.keys(propValue); - var styleCount = styleNames.length; - for (var j = 0; j < styleCount; j++) { - var styleName = styleNames[j]; - var newStyleValue = propValue[styleName]; - var oldStyleValue = previousValue[styleName]; - if (newStyleValue === oldStyleValue) { - continue; - } - propertiesUpdated = true; - if (newStyleValue) { - checkStyleValue(newStyleValue); - projectionOptions.styleApplyer(domNode, styleName, newStyleValue); - } else { - projectionOptions.styleApplyer(domNode, styleName, ''); - } - } - } else { - if (!propValue && typeof previousValue === 'string') { - propValue = ''; - } - if (propName === 'value') { - if (domNode[propName] !== propValue && domNode['oninput-value'] !== propValue) { - domNode[propName] = propValue; - // Reset the value, even if the virtual DOM did not change - domNode['oninput-value'] = undefined; - } - // else do not update the domNode, otherwise the cursor position would be changed - if (propValue !== previousValue) { - propertiesUpdated = true; - } - } else if (propValue !== previousValue) { - var type = typeof propValue; - if (type === 'function') { - throw new Error('Functions may not be updated on subsequent renders (property: ' + propName + '). Hint: declare event handler functions outside the render() function.'); - } - if (type === 'string' && propName !== 'innerHTML') { - if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') { - domNode.setAttributeNS(NAMESPACE_XLINK, propName, propValue); - } else { - domNode.setAttribute(propName, propValue); - } - } else { - if (domNode[propName] !== propValue) { - domNode[propName] = propValue; - } - } - propertiesUpdated = true; - } - } - } - return propertiesUpdated; - }; - var findIndexOfChild = function (children, sameAs, start) { - if (sameAs.vnodeSelector !== '') { - // Never scan for text-nodes - for (var i = start; i < children.length; i++) { - if (same(children[i], sameAs)) { - return i; - } - } - } - return -1; - }; - var nodeAdded = function (vNode, transitions) { - if (vNode.properties) { - var enterAnimation = vNode.properties.enterAnimation; - if (enterAnimation) { - if (typeof enterAnimation === 'function') { - enterAnimation(vNode.domNode, vNode.properties); - } else { - transitions.enter(vNode.domNode, vNode.properties, enterAnimation); - } - } - } - }; - var nodeToRemove = function (vNode, transitions) { - var domNode = vNode.domNode; - if (vNode.properties) { - var exitAnimation = vNode.properties.exitAnimation; - if (exitAnimation) { - domNode.style.pointerEvents = 'none'; - var removeDomNode = function () { - if (domNode.parentNode) { - domNode.parentNode.removeChild(domNode); - } - }; - if (typeof exitAnimation === 'function') { - exitAnimation(domNode, removeDomNode, vNode.properties); - return; - } else { - transitions.exit(vNode.domNode, vNode.properties, exitAnimation, removeDomNode); - return; - } - } - } - if (domNode.parentNode) { - domNode.parentNode.removeChild(domNode); - } - }; - var checkDistinguishable = function (childNodes, indexToCheck, parentVNode, operation) { - var childNode = childNodes[indexToCheck]; - if (childNode.vnodeSelector === '') { - return; // Text nodes need not be distinguishable - } - var properties = childNode.properties; - var key = properties ? properties.key === undefined ? properties.bind : properties.key : undefined; - if (!key) { - for (var i = 0; i < childNodes.length; i++) { - if (i !== indexToCheck) { - var node = childNodes[i]; - if (same(node, childNode)) { - if (operation === 'added') { - throw new Error(parentVNode.vnodeSelector + ' had a ' + childNode.vnodeSelector + ' child ' + 'added, but there is now more than one. You must add unique key properties to make them distinguishable.'); - } else { - throw new Error(parentVNode.vnodeSelector + ' had a ' + childNode.vnodeSelector + ' child ' + 'removed, but there were more than one. You must add unique key properties to make them distinguishable.'); - } - } - } - } - } - }; - var createDom; - var updateDom; - var updateChildren = function (vnode, domNode, oldChildren, newChildren, projectionOptions) { - if (oldChildren === newChildren) { - return false; - } - oldChildren = oldChildren || emptyArray; - newChildren = newChildren || emptyArray; - var oldChildrenLength = oldChildren.length; - var newChildrenLength = newChildren.length; - var transitions = projectionOptions.transitions; - var oldIndex = 0; - var newIndex = 0; - var i; - var textUpdated = false; - while (newIndex < newChildrenLength) { - var oldChild = oldIndex < oldChildrenLength ? oldChildren[oldIndex] : undefined; - var newChild = newChildren[newIndex]; - if (oldChild !== undefined && same(oldChild, newChild)) { - textUpdated = updateDom(oldChild, newChild, projectionOptions) || textUpdated; - oldIndex++; - } else { - var findOldIndex = findIndexOfChild(oldChildren, newChild, oldIndex + 1); - if (findOldIndex >= 0) { - // Remove preceding missing children - for (i = oldIndex; i < findOldIndex; i++) { - nodeToRemove(oldChildren[i], transitions); - checkDistinguishable(oldChildren, i, vnode, 'removed'); - } - textUpdated = updateDom(oldChildren[findOldIndex], newChild, projectionOptions) || textUpdated; - oldIndex = findOldIndex + 1; - } else { - // New child - createDom(newChild, domNode, oldIndex < oldChildrenLength ? oldChildren[oldIndex].domNode : undefined, projectionOptions); - nodeAdded(newChild, transitions); - checkDistinguishable(newChildren, newIndex, vnode, 'added'); - } - } - newIndex++; - } - if (oldChildrenLength > oldIndex) { - // Remove child fragments - for (i = oldIndex; i < oldChildrenLength; i++) { - nodeToRemove(oldChildren[i], transitions); - checkDistinguishable(oldChildren, i, vnode, 'removed'); - } - } - return textUpdated; - }; - var addChildren = function (domNode, children, projectionOptions) { - if (!children) { - return; - } - for (var i = 0; i < children.length; i++) { - createDom(children[i], domNode, undefined, projectionOptions); - } - }; - var initPropertiesAndChildren = function (domNode, vnode, projectionOptions) { - addChildren(domNode, vnode.children, projectionOptions); - // children before properties, needed for value property of . - if (vnode.text) { - domNode.textContent = vnode.text; - } - setProperties(domNode, vnode.properties, projectionOptions); - if (vnode.properties && vnode.properties.afterCreate) { - vnode.properties.afterCreate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children); - } - }; - createDom = function (vnode, parentNode, insertBefore, projectionOptions) { - var domNode, i, c, start = 0, type, found; - var vnodeSelector = vnode.vnodeSelector; - if (vnodeSelector === '') { - domNode = vnode.domNode = document.createTextNode(vnode.text); - if (insertBefore !== undefined) { - parentNode.insertBefore(domNode, insertBefore); - } else { - parentNode.appendChild(domNode); - } - } else { - for (i = 0; i <= vnodeSelector.length; ++i) { - c = vnodeSelector.charAt(i); - if (i === vnodeSelector.length || c === '.' || c === '#') { - type = vnodeSelector.charAt(start - 1); - found = vnodeSelector.slice(start, i); - if (type === '.') { - domNode.classList.add(found); - } else if (type === '#') { - domNode.id = found; - } else { - if (found === 'svg') { - projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG }); - } - if (projectionOptions.namespace !== undefined) { - domNode = vnode.domNode = document.createElementNS(projectionOptions.namespace, found); - } else { - domNode = vnode.domNode = document.createElement(found); - } - if (insertBefore !== undefined) { - parentNode.insertBefore(domNode, insertBefore); - } else { - parentNode.appendChild(domNode); - } - } - start = i + 1; - } - } - initPropertiesAndChildren(domNode, vnode, projectionOptions); - } - }; - updateDom = function (previous, vnode, projectionOptions) { - var domNode = previous.domNode; - var textUpdated = false; - if (previous === vnode) { - return false; // By contract, VNode objects may not be modified anymore after passing them to maquette - } - var updated = false; - if (vnode.vnodeSelector === '') { - if (vnode.text !== previous.text) { - var newVNode = document.createTextNode(vnode.text); - domNode.parentNode.replaceChild(newVNode, domNode); - vnode.domNode = newVNode; - textUpdated = true; - return textUpdated; - } - } else { - if (vnode.vnodeSelector.lastIndexOf('svg', 0) === 0) { - projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG }); - } - if (previous.text !== vnode.text) { - updated = true; - if (vnode.text === undefined) { - domNode.removeChild(domNode.firstChild); // the only textnode presumably - } else { - domNode.textContent = vnode.text; - } - } - updated = updateChildren(vnode, domNode, previous.children, vnode.children, projectionOptions) || updated; - updated = updateProperties(domNode, previous.properties, vnode.properties, projectionOptions) || updated; - if (vnode.properties && vnode.properties.afterUpdate) { - vnode.properties.afterUpdate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children); - } - } - if (updated && vnode.properties && vnode.properties.updateAnimation) { - vnode.properties.updateAnimation(domNode, vnode.properties, previous.properties); - } - vnode.domNode = previous.domNode; - return textUpdated; - }; - var createProjection = function (vnode, projectionOptions) { - return { - update: function (updatedVnode) { - if (vnode.vnodeSelector !== updatedVnode.vnodeSelector) { - throw new Error('The selector for the root VNode may not be changed. (consider using dom.merge and add one extra level to the virtual DOM)'); - } - updateDom(vnode, updatedVnode, projectionOptions); - vnode = updatedVnode; - }, - domNode: vnode.domNode - }; - }; - ; - // The other two parameters are not added here, because the Typescript compiler creates surrogate code for desctructuring 'children'. - exports.h = function (selector) { - var properties = arguments[1]; - if (typeof selector !== 'string') { - throw new Error(); - } - var childIndex = 1; - if (properties && !properties.hasOwnProperty('vnodeSelector') && !Array.isArray(properties) && typeof properties === 'object') { - childIndex = 2; - } else { - // Optional properties argument was omitted - properties = undefined; - } - var text = undefined; - var children = undefined; - var argsLength = arguments.length; - // Recognize a common special case where there is only a single text node - if (argsLength === childIndex + 1) { - var onlyChild = arguments[childIndex]; - if (typeof onlyChild === 'string') { - text = onlyChild; - } else if (onlyChild !== undefined && onlyChild.length === 1 && typeof onlyChild[0] === 'string') { - text = onlyChild[0]; - } - } - if (text === undefined) { - children = []; - for (; childIndex < arguments.length; childIndex++) { - var child = arguments[childIndex]; - if (child === null || child === undefined) { - continue; - } else if (Array.isArray(child)) { - appendChildren(selector, child, children); - } else if (child.hasOwnProperty('vnodeSelector')) { - children.push(child); - } else { - children.push(toTextVNode(child)); - } - } - } - return { - vnodeSelector: selector, - properties: properties, - children: children, - text: text === '' ? undefined : text, - domNode: null - }; - }; - /** - * Contains simple low-level utility functions to manipulate the real DOM. - */ - exports.dom = { - /** - * Creates a real DOM tree from `vnode`. The [[Projection]] object returned will contain the resulting DOM Node in - * its [[Projection.domNode|domNode]] property. - * This is a low-level method. Users wil typically use a [[Projector]] instead. - * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]] - * objects may only be rendered once. - * @param projectionOptions - Options to be used to create and update the projection. - * @returns The [[Projection]] which also contains the DOM Node that was created. - */ - create: function (vnode, projectionOptions) { - projectionOptions = applyDefaultProjectionOptions(projectionOptions); - createDom(vnode, document.createElement('div'), undefined, projectionOptions); - return createProjection(vnode, projectionOptions); - }, - /** - * Appends a new childnode to the DOM which is generated from a [[VNode]]. - * This is a low-level method. Users wil typically use a [[Projector]] instead. - * @param parentNode - The parent node for the new childNode. - * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]] - * objects may only be rendered once. - * @param projectionOptions - Options to be used to create and update the [[Projection]]. - * @returns The [[Projection]] that was created. - */ - append: function (parentNode, vnode, projectionOptions) { - projectionOptions = applyDefaultProjectionOptions(projectionOptions); - createDom(vnode, parentNode, undefined, projectionOptions); - return createProjection(vnode, projectionOptions); - }, - /** - * Inserts a new DOM node which is generated from a [[VNode]]. - * This is a low-level method. Users wil typically use a [[Projector]] instead. - * @param beforeNode - The node that the DOM Node is inserted before. - * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. - * NOTE: [[VNode]] objects may only be rendered once. - * @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]]. - * @returns The [[Projection]] that was created. - */ - insertBefore: function (beforeNode, vnode, projectionOptions) { - projectionOptions = applyDefaultProjectionOptions(projectionOptions); - createDom(vnode, beforeNode.parentNode, beforeNode, projectionOptions); - return createProjection(vnode, projectionOptions); - }, - /** - * Merges a new DOM node which is generated from a [[VNode]] with an existing DOM Node. - * This means that the virtual DOM and the real DOM will have one overlapping element. - * Therefore the selector for the root [[VNode]] will be ignored, but its properties and children will be applied to the Element provided. - * This is a low-level method. Users wil typically use a [[Projector]] instead. - * @param domNode - The existing element to adopt as the root of the new virtual DOM. Existing attributes and childnodes are preserved. - * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]] objects - * may only be rendered once. - * @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]]. - * @returns The [[Projection]] that was created. - */ - merge: function (element, vnode, projectionOptions) { - projectionOptions = applyDefaultProjectionOptions(projectionOptions); - vnode.domNode = element; - initPropertiesAndChildren(element, vnode, projectionOptions); - return createProjection(vnode, projectionOptions); - } - }; - /** - * Creates a [[CalculationCache]] object, useful for caching [[VNode]] trees. - * In practice, caching of [[VNode]] trees is not needed, because achieving 60 frames per second is almost never a problem. - * For more information, see [[CalculationCache]]. - * - * @param The type of the value that is cached. - */ - exports.createCache = function () { - var cachedInputs = undefined; - var cachedOutcome = undefined; - var result = { - invalidate: function () { - cachedOutcome = undefined; - cachedInputs = undefined; - }, - result: function (inputs, calculation) { - if (cachedInputs) { - for (var i = 0; i < inputs.length; i++) { - if (cachedInputs[i] !== inputs[i]) { - cachedOutcome = undefined; - } - } - } - if (!cachedOutcome) { - cachedOutcome = calculation(); - cachedInputs = inputs; - } - return cachedOutcome; - } - }; - return result; - }; - /** - * Creates a {@link Mapping} instance that keeps an array of result objects synchronized with an array of source objects. - * See {@link http://maquettejs.org/docs/arrays.html|Working with arrays}. - * - * @param The type of source items. A database-record for instance. - * @param The type of target items. A [[Component]] for instance. - * @param getSourceKey `function(source)` that must return a key to identify each source object. The result must either be a string or a number. - * @param createResult `function(source, index)` that must create a new result object from a given source. This function is identical - * to the `callback` argument in `Array.map(callback)`. - * @param updateResult `function(source, target, index)` that updates a result to an updated source. - */ - exports.createMapping = function (getSourceKey, createResult, updateResult) { - var keys = []; - var results = []; - return { - results: results, - map: function (newSources) { - var newKeys = newSources.map(getSourceKey); - var oldTargets = results.slice(); - var oldIndex = 0; - for (var i = 0; i < newSources.length; i++) { - var source = newSources[i]; - var sourceKey = newKeys[i]; - if (sourceKey === keys[oldIndex]) { - results[i] = oldTargets[oldIndex]; - updateResult(source, oldTargets[oldIndex], i); - oldIndex++; - } else { - var found = false; - for (var j = 1; j < keys.length; j++) { - var searchIndex = (oldIndex + j) % keys.length; - if (keys[searchIndex] === sourceKey) { - results[i] = oldTargets[searchIndex]; - updateResult(newSources[i], oldTargets[searchIndex], i); - oldIndex = searchIndex + 1; - found = true; - break; - } - } - if (!found) { - results[i] = createResult(source, i); - } - } - } - results.length = newSources.length; - keys = newKeys; - } - }; - }; - /** - * Creates a [[Projector]] instance using the provided projectionOptions. - * - * For more information, see [[Projector]]. - * - * @param projectionOptions Options that influence how the DOM is rendered and updated. - */ - exports.createProjector = function (projectorOptions) { - var projector; - var projectionOptions = applyDefaultProjectionOptions(projectorOptions); - projectionOptions.eventHandlerInterceptor = function (propertyName, eventHandler, domNode, properties) { - return function () { - // intercept function calls (event handlers) to do a render afterwards. - projector.scheduleRender(); - return eventHandler.apply(properties.bind || this, arguments); - }; - }; - var renderCompleted = true; - var scheduled; - var stopped = false; - var projections = []; - var renderFunctions = []; - // matches the projections array - var doRender = function () { - scheduled = undefined; - if (!renderCompleted) { - return; // The last render threw an error, it should be logged in the browser console. - } - renderCompleted = false; - for (var i = 0; i < projections.length; i++) { - var updatedVnode = renderFunctions[i](); - projections[i].update(updatedVnode); - } - renderCompleted = true; - }; - projector = { - scheduleRender: function () { - if (!scheduled && !stopped) { - scheduled = requestAnimationFrame(doRender); - } - }, - stop: function () { - if (scheduled) { - cancelAnimationFrame(scheduled); - scheduled = undefined; - } - stopped = true; - }, - resume: function () { - stopped = false; - renderCompleted = true; - projector.scheduleRender(); - }, - append: function (parentNode, renderMaquetteFunction) { - projections.push(exports.dom.append(parentNode, renderMaquetteFunction(), projectionOptions)); - renderFunctions.push(renderMaquetteFunction); - }, - insertBefore: function (beforeNode, renderMaquetteFunction) { - projections.push(exports.dom.insertBefore(beforeNode, renderMaquetteFunction(), projectionOptions)); - renderFunctions.push(renderMaquetteFunction); - }, - merge: function (domNode, renderMaquetteFunction) { - projections.push(exports.dom.merge(domNode, renderMaquetteFunction(), projectionOptions)); - renderFunctions.push(renderMaquetteFunction); - }, - replace: function (domNode, renderMaquetteFunction) { - var vnode = renderMaquetteFunction(); - createDom(vnode, domNode.parentNode, domNode, projectionOptions); - domNode.parentNode.removeChild(domNode); - projections.push(createProjection(vnode, projectionOptions)); - renderFunctions.push(renderMaquetteFunction); - }, - detach: function (renderMaquetteFunction) { - for (var i = 0; i < renderFunctions.length; i++) { - if (renderFunctions[i] === renderMaquetteFunction) { - renderFunctions.splice(i, 1); - return projections.splice(i, 1)[0]; - } - } - throw new Error('renderMaquetteFunction was not found'); - } - }; - return projector; - }; -})); diff --git a/plugins/UiConfig/media/js/utils/Animation.coffee b/plugins/UiConfig/media/js/utils/Animation.coffee deleted file mode 100644 index 271b88c1..00000000 --- a/plugins/UiConfig/media/js/utils/Animation.coffee +++ /dev/null @@ -1,138 +0,0 @@ -class Animation - slideDown: (elem, props) -> - if elem.offsetTop > 2000 - return - - h = elem.offsetHeight - cstyle = window.getComputedStyle(elem) - margin_top = cstyle.marginTop - margin_bottom = cstyle.marginBottom - padding_top = cstyle.paddingTop - padding_bottom = cstyle.paddingBottom - transition = cstyle.transition - - elem.style.boxSizing = "border-box" - elem.style.overflow = "hidden" - elem.style.transform = "scale(0.6)" - elem.style.opacity = "0" - elem.style.height = "0px" - elem.style.marginTop = "0px" - elem.style.marginBottom = "0px" - elem.style.paddingTop = "0px" - elem.style.paddingBottom = "0px" - elem.style.transition = "none" - - setTimeout (-> - elem.className += " animate-inout" - elem.style.height = h+"px" - elem.style.transform = "scale(1)" - elem.style.opacity = "1" - elem.style.marginTop = margin_top - elem.style.marginBottom = margin_bottom - elem.style.paddingTop = padding_top - elem.style.paddingBottom = padding_bottom - ), 1 - - elem.addEventListener "transitionend", -> - elem.classList.remove("animate-inout") - elem.style.transition = elem.style.transform = elem.style.opacity = elem.style.height = null - elem.style.boxSizing = elem.style.marginTop = elem.style.marginBottom = null - elem.style.paddingTop = elem.style.paddingBottom = elem.style.overflow = null - elem.removeEventListener "transitionend", arguments.callee, false - - - slideUp: (elem, remove_func, props) -> - if elem.offsetTop > 1000 - return remove_func() - - elem.className += " animate-back" - elem.style.boxSizing = "border-box" - elem.style.height = elem.offsetHeight+"px" - elem.style.overflow = "hidden" - elem.style.transform = "scale(1)" - elem.style.opacity = "1" - elem.style.pointerEvents = "none" - setTimeout (-> - elem.style.height = "0px" - elem.style.marginTop = "0px" - elem.style.marginBottom = "0px" - elem.style.paddingTop = "0px" - elem.style.paddingBottom = "0px" - elem.style.transform = "scale(0.8)" - elem.style.borderTopWidth = "0px" - elem.style.borderBottomWidth = "0px" - elem.style.opacity = "0" - ), 1 - elem.addEventListener "transitionend", (e) -> - if e.propertyName == "opacity" or e.elapsedTime >= 0.6 - elem.removeEventListener "transitionend", arguments.callee, false - remove_func() - - - slideUpInout: (elem, remove_func, props) -> - elem.className += " animate-inout" - elem.style.boxSizing = "border-box" - elem.style.height = elem.offsetHeight+"px" - elem.style.overflow = "hidden" - elem.style.transform = "scale(1)" - elem.style.opacity = "1" - elem.style.pointerEvents = "none" - setTimeout (-> - elem.style.height = "0px" - elem.style.marginTop = "0px" - elem.style.marginBottom = "0px" - elem.style.paddingTop = "0px" - elem.style.paddingBottom = "0px" - elem.style.transform = "scale(0.8)" - elem.style.borderTopWidth = "0px" - elem.style.borderBottomWidth = "0px" - elem.style.opacity = "0" - ), 1 - elem.addEventListener "transitionend", (e) -> - if e.propertyName == "opacity" or e.elapsedTime >= 0.6 - elem.removeEventListener "transitionend", arguments.callee, false - remove_func() - - - showRight: (elem, props) -> - elem.className += " animate" - elem.style.opacity = 0 - elem.style.transform = "TranslateX(-20px) Scale(1.01)" - setTimeout (-> - elem.style.opacity = 1 - elem.style.transform = "TranslateX(0px) Scale(1)" - ), 1 - elem.addEventListener "transitionend", -> - elem.classList.remove("animate") - elem.style.transform = elem.style.opacity = null - - - show: (elem, props) -> - delay = arguments[arguments.length-2]?.delay*1000 or 1 - elem.style.opacity = 0 - setTimeout (-> - elem.className += " animate" - ), 1 - setTimeout (-> - elem.style.opacity = 1 - ), delay - elem.addEventListener "transitionend", -> - elem.classList.remove("animate") - elem.style.opacity = null - elem.removeEventListener "transitionend", arguments.callee, false - - hide: (elem, remove_func, props) -> - delay = arguments[arguments.length-2]?.delay*1000 or 1 - elem.className += " animate" - setTimeout (-> - elem.style.opacity = 0 - ), delay - elem.addEventListener "transitionend", (e) -> - if e.propertyName == "opacity" - remove_func() - - addVisibleClass: (elem, props) -> - setTimeout -> - elem.classList.add("visible") - -window.Animation = new Animation() \ No newline at end of file diff --git a/plugins/UiConfig/media/js/utils/Dollar.coffee b/plugins/UiConfig/media/js/utils/Dollar.coffee deleted file mode 100644 index 7f19f551..00000000 --- a/plugins/UiConfig/media/js/utils/Dollar.coffee +++ /dev/null @@ -1,3 +0,0 @@ -window.$ = (selector) -> - if selector.startsWith("#") - return document.getElementById(selector.replace("#", "")) diff --git a/plugins/UiConfig/media/js/utils/ZeroFrame.coffee b/plugins/UiConfig/media/js/utils/ZeroFrame.coffee deleted file mode 100644 index 11512d16..00000000 --- a/plugins/UiConfig/media/js/utils/ZeroFrame.coffee +++ /dev/null @@ -1,85 +0,0 @@ -class ZeroFrame extends Class - constructor: (url) -> - @url = url - @waiting_cb = {} - @wrapper_nonce = document.location.href.replace(/.*wrapper_nonce=([A-Za-z0-9]+).*/, "$1") - @connect() - @next_message_id = 1 - @history_state = {} - @init() - - - init: -> - @ - - - connect: -> - @target = window.parent - window.addEventListener("message", @onMessage, false) - @cmd("innerReady") - - # Save scrollTop - window.addEventListener "beforeunload", (e) => - @log "save scrollTop", window.pageYOffset - @history_state["scrollTop"] = window.pageYOffset - @cmd "wrapperReplaceState", [@history_state, null] - - # Restore scrollTop - @cmd "wrapperGetState", [], (state) => - @history_state = state if state? - @log "restore scrollTop", state, window.pageYOffset - if window.pageYOffset == 0 and state - window.scroll(window.pageXOffset, state.scrollTop) - - - onMessage: (e) => - message = e.data - cmd = message.cmd - if cmd == "response" - if @waiting_cb[message.to]? - @waiting_cb[message.to](message.result) - else - @log "Websocket callback not found:", message - else if cmd == "wrapperReady" # Wrapper inited later - @cmd("innerReady") - else if cmd == "ping" - @response message.id, "pong" - else if cmd == "wrapperOpenedWebsocket" - @onOpenWebsocket() - else if cmd == "wrapperClosedWebsocket" - @onCloseWebsocket() - else - @onRequest cmd, message.params - - - onRequest: (cmd, message) => - @log "Unknown request", message - - - response: (to, result) -> - @send {"cmd": "response", "to": to, "result": result} - - - cmd: (cmd, params={}, cb=null) -> - @send {"cmd": cmd, "params": params}, cb - - - send: (message, cb=null) -> - message.wrapper_nonce = @wrapper_nonce - message.id = @next_message_id - @next_message_id += 1 - @target.postMessage(message, "*") - if cb - @waiting_cb[message.id] = cb - - - onOpenWebsocket: => - @log "Websocket open" - - - onCloseWebsocket: => - @log "Websocket close" - - - -window.ZeroFrame = ZeroFrame diff --git a/plugins/UiConfig/plugin_info.json b/plugins/UiConfig/plugin_info.json deleted file mode 100644 index 01e9dd31..00000000 --- a/plugins/UiConfig/plugin_info.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "UiConfig", - "description": "Change client settings using the web interface.", - "default": "enabled" -} \ No newline at end of file diff --git a/plugins/UiFileManager/UiFileManagerPlugin.py b/plugins/UiFileManager/UiFileManagerPlugin.py deleted file mode 100644 index 040e1d22..00000000 --- a/plugins/UiFileManager/UiFileManagerPlugin.py +++ /dev/null @@ -1,90 +0,0 @@ -import io -import os -import re -import urllib - -from Plugin import PluginManager -from Config import config -from Translate import Translate - -plugin_dir = os.path.dirname(__file__) - -if "_" not in locals(): - _ = Translate(plugin_dir + "/languages/") - - -@PluginManager.registerTo("UiRequest") -class UiFileManagerPlugin(object): - def actionWrapper(self, path, extra_headers=None): - match = re.match("/list/(.*?)(/.*|)$", path) - if not match: - return super().actionWrapper(path, extra_headers) - - if not extra_headers: - extra_headers = {} - - request_address, inner_path = match.groups() - - script_nonce = self.getScriptNonce() - - self.sendHeader(extra_headers=extra_headers, script_nonce=script_nonce) - - site = self.server.site_manager.need(request_address) - - if not site: - return super().actionWrapper(path, extra_headers) - - request_params = urllib.parse.urlencode( - {"address": site.address, "site": request_address, "inner_path": inner_path.strip("/")} - ) - - is_content_loaded = "content.json" in site.content_manager.contents - - return iter([super().renderWrapper( - site, path, "uimedia/plugins/uifilemanager/list.html?%s" % request_params, - "List", extra_headers, show_loadingscreen=not is_content_loaded, script_nonce=script_nonce - )]) - - def actionUiMedia(self, path, *args, **kwargs): - if path.startswith("/uimedia/plugins/uifilemanager/"): - file_path = path.replace("/uimedia/plugins/uifilemanager/", plugin_dir + "/media/") - if config.debug and (file_path.endswith("all.js") or file_path.endswith("all.css")): - # If debugging merge *.css to all.css and *.js to all.js - from Debug import DebugMedia - DebugMedia.merge(file_path) - - if file_path.endswith("js"): - data = _.translateData(open(file_path).read(), mode="js").encode("utf8") - elif file_path.endswith("html"): - if self.get.get("address"): - site = self.server.site_manager.need(self.get.get("address")) - if "content.json" not in site.content_manager.contents: - site.needFile("content.json") - data = _.translateData(open(file_path).read(), mode="html").encode("utf8") - else: - data = open(file_path, "rb").read() - - return self.actionFile(file_path, file_obj=io.BytesIO(data), file_size=len(data)) - else: - return super().actionUiMedia(path) - - def error404(self, path=""): - if not path.endswith("index.html") and not path.endswith("/"): - return super().error404(path) - - path_parts = self.parsePath(path) - if not path_parts: - return super().error404(path) - - site = self.server.site_manager.get(path_parts["request_address"]) - - if not site or not site.content_manager.contents.get("content.json"): - return super().error404(path) - - if path_parts["inner_path"] in site.content_manager.contents.get("content.json").get("files", {}): - return super().error404(path) - - self.sendHeader(200) - path_redirect = "/list" + re.sub("^/media/", "/", path) - self.log.debug("Index.html not found: %s, redirecting to: %s" % (path, path_redirect)) - return self.formatRedirect(path_redirect) diff --git a/plugins/UiFileManager/__init__.py b/plugins/UiFileManager/__init__.py deleted file mode 100644 index b6c2bd1a..00000000 --- a/plugins/UiFileManager/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import UiFileManagerPlugin diff --git a/plugins/UiFileManager/languages/hu.json b/plugins/UiFileManager/languages/hu.json deleted file mode 100644 index 5a915f9f..00000000 --- a/plugins/UiFileManager/languages/hu.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "New file name:": "Új fájl neve:", - "Delete": "Törlés", - "Cancel": "Mégse", - "Selected:": "Köjelölt:", - "Delete and remove optional:": "Törlés és opcionális fájl eltávolítása", - " files": " fájl", - " (modified)": " (módostott)", - " (new)": " (új)", - " (optional)": " (opcionális)", - " (ignored from content.json)": " (content.json-ból kihagyott)", - "Total: ": "Összesen: ", - " dir, ": " könyvtár, ", - " file in ": " fájl, ", - "+ New": "+ Új", - "Edit": "Módosít", - "View": "Megnyit", - "Save": "Mentés", - "Save: done!": "Mentés: Kész!" -} \ No newline at end of file diff --git a/plugins/UiFileManager/languages/jp.json b/plugins/UiFileManager/languages/jp.json deleted file mode 100644 index 6d874a61..00000000 --- a/plugins/UiFileManager/languages/jp.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "New file name:": "新しいファイルの名前:", - "Delete": "削除", - "Cancel": "キャンセル", - "Selected:": "選択済み: ", - "Delete and remove optional:": "オプションを削除", - " files": " ファイル", - " (modified)": " (編集済み)", - " (new)": " (新しい)", - " (optional)": " (オプション)", - " (ignored from content.json)": " (content.jsonから無視されます)", - "Total: ": "合計: ", - " dir, ": " のディレクトリ, ", - " file in ": " のファイル, ", - "+ New": "+ 新規作成", - "Edit": "編集", - "View": "閲覧", - "Save": "保存", - "Save: done!": "保存完了!" -} diff --git a/plugins/UiFileManager/media/codemirror/LICENSE b/plugins/UiFileManager/media/codemirror/LICENSE deleted file mode 100644 index ff7db4b9..00000000 --- a/plugins/UiFileManager/media/codemirror/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (C) 2017 by Marijn Haverbeke and others - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/plugins/UiFileManager/media/codemirror/all.css b/plugins/UiFileManager/media/codemirror/all.css deleted file mode 100644 index 86904409..00000000 --- a/plugins/UiFileManager/media/codemirror/all.css +++ /dev/null @@ -1,678 +0,0 @@ - -/* ---- base/codemirror.css ---- */ - - -/* BASICS */ - -.CodeMirror { - /* Set height, width, borders, and global font properties here */ - font-family: monospace; - height: 300px; - color: black; - direction: ltr; -} - -/* PADDING */ - -.CodeMirror-lines { - padding: 4px 0; /* Vertical padding around content */ -} -.CodeMirror pre.CodeMirror-line, -.CodeMirror pre.CodeMirror-line-like { - padding: 0 4px; /* Horizontal padding of content */ -} - -.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { - background-color: white; /* The little square between H and V scrollbars */ -} - -/* GUTTER */ - -.CodeMirror-gutters { - border-right: 1px solid #ddd; - background-color: #f7f7f7; - white-space: nowrap; -} -.CodeMirror-linenumbers {} -.CodeMirror-linenumber { - padding: 0 3px 0 5px; - min-width: 20px; - text-align: right; - color: #999; - white-space: nowrap; -} - -.CodeMirror-guttermarker { color: black; } -.CodeMirror-guttermarker-subtle { color: #999; } - -/* CURSOR */ - -.CodeMirror-cursor { - border-left: 1px solid black; - border-right: none; - width: 0; -} -/* Shown when moving in bi-directional text */ -.CodeMirror div.CodeMirror-secondarycursor { - border-left: 1px solid silver; -} -.cm-fat-cursor .CodeMirror-cursor { - width: auto; - border: 0 !important; - background: #7e7; -} -.cm-fat-cursor div.CodeMirror-cursors { - z-index: 1; -} -.cm-fat-cursor-mark { - background-color: rgba(20, 255, 20, 0.5); - -webkit-animation: blink 1.06s steps(1) infinite; - -moz-animation: blink 1.06s steps(1) infinite; - -webkit-animation: blink 1.06s steps(1) infinite; -moz-animation: blink 1.06s steps(1) infinite; -o-animation: blink 1.06s steps(1) infinite; -ms-animation: blink 1.06s steps(1) infinite; animation: blink 1.06s steps(1) infinite ; -} -.cm-animate-fat-cursor { - width: auto; - border: 0; - -webkit-animation: blink 1.06s steps(1) infinite; - -moz-animation: blink 1.06s steps(1) infinite; - -webkit-animation: blink 1.06s steps(1) infinite; -moz-animation: blink 1.06s steps(1) infinite; -o-animation: blink 1.06s steps(1) infinite; -ms-animation: blink 1.06s steps(1) infinite; animation: blink 1.06s steps(1) infinite ; - background-color: #7e7; -} -@-moz-keyframes blink { - 0% {} - 50% { background-color: transparent; } - 100% {} -} -@-webkit-keyframes blink { - 0% {} - 50% { background-color: transparent; } - 100% {} -} -@keyframes blink { - 0% {} - 50% { background-color: transparent; } - 100% {} -} -@-webkit-keyframes blink { - 0% {} - 50% { background-color: transparent; } - 100% {} -} -@-moz-keyframes blink { - 0% {} - 50% { background-color: transparent; } - 100% {} -} - - -/* Can style cursor different in overwrite (non-insert) mode */ -.CodeMirror-overwrite .CodeMirror-cursor {} - -.cm-tab { display: inline-block; text-decoration: inherit; } - -.CodeMirror-rulers { - position: absolute; - left: 0; right: 0; top: -50px; bottom: 0; - overflow: hidden; -} -.CodeMirror-ruler { - border-left: 1px solid #ccc; - top: 0; bottom: 0; - position: absolute; -} - -/* DEFAULT THEME */ - -.cm-s-default .cm-header {color: blue;} -.cm-s-default .cm-quote {color: #090;} -.cm-negative {color: #d44;} -.cm-positive {color: #292;} -.cm-header, .cm-strong {font-weight: bold;} -.cm-em {font-style: italic;} -.cm-link {text-decoration: underline;} -.cm-strikethrough {text-decoration: line-through;} - -.cm-s-default .cm-keyword {color: #708;} -.cm-s-default .cm-atom {color: #219;} -.cm-s-default .cm-number {color: #164;} -.cm-s-default .cm-def {color: #00f;} -.cm-s-default .cm-variable, -.cm-s-default .cm-punctuation, -.cm-s-default .cm-property, -.cm-s-default .cm-operator {} -.cm-s-default .cm-variable-2 {color: #05a;} -.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;} -.cm-s-default .cm-comment {color: #a50;} -.cm-s-default .cm-string {color: #a11;} -.cm-s-default .cm-string-2 {color: #f50;} -.cm-s-default .cm-meta {color: #555;} -.cm-s-default .cm-qualifier {color: #555;} -.cm-s-default .cm-builtin {color: #30a;} -.cm-s-default .cm-bracket {color: #997;} -.cm-s-default .cm-tag {color: #170;} -.cm-s-default .cm-attribute {color: #00c;} -.cm-s-default .cm-hr {color: #999;} -.cm-s-default .cm-link {color: #00c;} - -.cm-s-default .cm-error {color: #f00;} -.cm-invalidchar {color: #f00;} - -.CodeMirror-composing { border-bottom: 2px solid; } - -/* Default styles for common addons */ - -div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;} -div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;} -.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } -.CodeMirror-activeline-background {background: #e8f2ff;} - -/* STOP */ - -/* The rest of this file contains styles related to the mechanics of - the editor. You probably shouldn't touch them. */ - -.CodeMirror { - position: relative; - overflow: hidden; - background: white; -} - -.CodeMirror-scroll { - overflow: scroll !important; /* Things will break if this is overridden */ - /* 50px is the magic margin used to hide the element's real scrollbars */ - /* See overflow: hidden in .CodeMirror */ - margin-bottom: -50px; margin-right: -50px; - padding-bottom: 50px; - height: 100%; - outline: none; /* Prevent dragging from highlighting the element */ - position: relative; -} -.CodeMirror-sizer { - position: relative; - border-right: 50px solid transparent; -} - -/* The fake, visible scrollbars. Used to force redraw during scrolling - before actual scrolling happens, thus preventing shaking and - flickering artifacts. */ -.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { - position: absolute; - z-index: 6; - display: none; -} -.CodeMirror-vscrollbar { - right: 0; top: 0; - overflow-x: hidden; - overflow-y: scroll; -} -.CodeMirror-hscrollbar { - bottom: 0; left: 0; - overflow-y: hidden; - overflow-x: scroll; -} -.CodeMirror-scrollbar-filler { - right: 0; bottom: 0; -} -.CodeMirror-gutter-filler { - left: 0; bottom: 0; -} - -.CodeMirror-gutters { - position: absolute; left: 0; top: 0; - min-height: 100%; - z-index: 3; -} -.CodeMirror-gutter { - white-space: normal; - height: 100%; - display: inline-block; - vertical-align: top; - margin-bottom: -50px; -} -.CodeMirror-gutter-wrapper { - position: absolute; - z-index: 4; - background: none !important; - border: none !important; -} -.CodeMirror-gutter-background { - position: absolute; - top: 0; bottom: 0; - z-index: 4; -} -.CodeMirror-gutter-elt { - position: absolute; - cursor: default; - z-index: 4; -} -.CodeMirror-gutter-wrapper ::selection { background-color: transparent } -.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent } - -.CodeMirror-lines { - cursor: text; - min-height: 1px; /* prevents collapsing before first draw */ -} -.CodeMirror pre.CodeMirror-line, -.CodeMirror pre.CodeMirror-line-like { - /* Reset some styles that the rest of the page might have set */ - -moz-border-radius: 0; -webkit-border-radius: 0; -webkit-border-radius: 0; -moz-border-radius: 0; -o-border-radius: 0; -ms-border-radius: 0; border-radius: 0 ; - border-width: 0; - background: transparent; - font-family: inherit; - font-size: inherit; - margin: 0; - white-space: pre; - word-wrap: normal; - line-height: inherit; - color: inherit; - z-index: 2; - position: relative; - overflow: visible; - -webkit-tap-highlight-color: transparent; - -webkit-font-variant-ligatures: contextual; - font-variant-ligatures: contextual; -} -.CodeMirror-wrap pre.CodeMirror-line, -.CodeMirror-wrap pre.CodeMirror-line-like { - word-wrap: break-word; - white-space: pre-wrap; - word-break: normal; -} - -.CodeMirror-linebackground { - position: absolute; - left: 0; right: 0; top: 0; bottom: 0; - z-index: 0; -} - -.CodeMirror-linewidget { - position: relative; - z-index: 2; - padding: 0.1px; /* Force widget margins to stay inside of the container */ -} - -.CodeMirror-widget {} - -.CodeMirror-rtl pre { direction: rtl; } - -.CodeMirror-code { - outline: none; -} - -/* Force content-box sizing for the elements where we expect it */ -.CodeMirror-scroll, -.CodeMirror-sizer, -.CodeMirror-gutter, -.CodeMirror-gutters, -.CodeMirror-linenumber { - -moz-box-sizing: content-box; - -webkit-box-sizing: content-box; -moz-box-sizing: content-box; -o-box-sizing: content-box; -ms-box-sizing: content-box; box-sizing: content-box ; -} - -.CodeMirror-measure { - position: absolute; - width: 100%; - height: 0; - overflow: hidden; - visibility: hidden; -} - -.CodeMirror-cursor { - position: absolute; - pointer-events: none; -} -.CodeMirror-measure pre { position: static; } - -div.CodeMirror-cursors { - visibility: hidden; - position: relative; - z-index: 3; -} -div.CodeMirror-dragcursors { - visibility: visible; -} - -.CodeMirror-focused div.CodeMirror-cursors { - visibility: visible; -} - -.CodeMirror-selected { background: #d9d9d9; } -.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } -.CodeMirror-crosshair { cursor: crosshair; } -.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } -.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } - -.cm-searching { - background-color: #ffa; - background-color: rgba(255, 255, 0, .4); -} - -/* Used to force a border model for a node */ -.cm-force-border { padding-right: .1px; } - -@media print { - /* Hide the cursor when printing */ - .CodeMirror div.CodeMirror-cursors { - visibility: hidden; - } -} - -/* See issue #2901 */ -.cm-tab-wrap-hack:after { content: ''; } - -/* Help users use markselection to safely style text background */ -span.CodeMirror-selectedtext { background: none; } - - -/* ---- extension/mdn-like-custom.css ---- */ - - -/* - MDN-LIKE Theme - Mozilla - Ported to CodeMirror by Peter Kroon - Report bugs/issues here: https://github.com/codemirror/CodeMirror/issues - GitHub: @peterkroon - - The mdn-like theme is inspired on the displayed code examples at: https://developer.mozilla.org/en-US/docs/Web/CSS/animation - -*/ -.cm-s-mdn-like.CodeMirror { color: #666; background-color: #fff; } -.cm-s-mdn-like div.CodeMirror-selected { background: #fefdb5; } -.cm-s-mdn-like .CodeMirror-line::selection, .cm-s-mdn-like .CodeMirror-line > span::selection, .cm-s-mdn-like .CodeMirror-line > span > span::selection { background: #fefdb5; } -.cm-s-mdn-like .CodeMirror-line::-moz-selection, .cm-s-mdn-like .CodeMirror-line > span::-moz-selection, .cm-s-mdn-like .CodeMirror-line > span > span::-moz-selection { background: #fefdb5; } - -.cm-s-mdn-like .CodeMirror-gutters { background: #f8f8f8; color: #333; } -.cm-s-mdn-like .CodeMirror-linenumber { color: #aaa; padding-left: 8px; } -.cm-s-mdn-like .CodeMirror-cursor { border-left: 2px solid #222; } - -.cm-s-mdn-like .cm-keyword { color: #6262FF; } -.cm-s-mdn-like .cm-atom { color: #F90; } -.cm-s-mdn-like .cm-number { color: #ca7841; } -.cm-s-mdn-like .cm-def { color: #8DA6CE; } -.cm-s-mdn-like span.cm-variable-2, .cm-s-mdn-like span.cm-tag { color: #690; } -.cm-s-mdn-like span.cm-variable-3, .cm-s-mdn-like span.cm-def, .cm-s-mdn-like span.cm-type { color: #07a; } - -.cm-s-mdn-like .cm-variable { color: #07a; } -.cm-s-mdn-like .cm-property { color: #905; } -.cm-s-mdn-like .cm-qualifier { color: #690; } - -.cm-s-mdn-like .cm-operator { color: #cda869; } -.cm-s-mdn-like .cm-comment { color:#777; font-weight:normal; } -.cm-s-mdn-like .cm-string { color:#07a; } -.cm-s-mdn-like .cm-string-2 { color:#bd6b18; } /*?*/ -.cm-s-mdn-like .cm-meta { color: #000; } /*?*/ -.cm-s-mdn-like .cm-builtin { color: #9B7536; } /*?*/ -.cm-s-mdn-like .cm-tag { color: #997643; } -.cm-s-mdn-like .cm-attribute { color: #d6bb6d; } /*?*/ -.cm-s-mdn-like .cm-header { color: #FF6400; } -.cm-s-mdn-like .cm-hr { color: #AEAEAE; } -.cm-s-mdn-like .cm-link { color:#ad9361; font-style:italic; text-decoration:none; } -.cm-s-mdn-like .cm-error { border-bottom: 1px solid red; } - -div.cm-s-mdn-like .CodeMirror-activeline-background { background: #efefff; } -div.cm-s-mdn-like span.CodeMirror-matchingbracket { outline:1px solid grey; color: inherit; } - - -/* ---- extension/dialog/dialog.css ---- */ - - -.CodeMirror-dialog { - position: absolute; - left: 0; right: 0; - background: inherit; - z-index: 15; - padding: .1em .8em; - overflow: hidden; - color: inherit; -} - -.CodeMirror-dialog-top { - border-bottom: 1px solid #eee; - top: 0; -} - -.CodeMirror-dialog-bottom { - border-top: 1px solid #eee; - bottom: 0; -} - -.CodeMirror-dialog input { - border: none; - outline: none; - background: transparent; - width: 20em; - color: inherit; - font-family: monospace; -} - -.CodeMirror-dialog button { - font-size: 70%; -} - - -/* ---- extension/fold/foldgutter.css ---- */ - - -.CodeMirror-foldmarker { - color: blue; - text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px; - font-family: arial; - line-height: .3; - cursor: pointer; -} -.CodeMirror-foldgutter { - width: .7em; -} -.CodeMirror-foldgutter-open, -.CodeMirror-foldgutter-folded { - cursor: pointer; -} -.CodeMirror-foldgutter-open:after { - content: "\25BE"; -} -.CodeMirror-foldgutter-folded:after { - content: "\25B8"; -} - - -/* ---- extension/hint/show-hint.css ---- */ - - -.CodeMirror-hints { - position: absolute; - z-index: 10; - overflow: hidden; - list-style: none; - - margin: 0; - padding: 2px; - - -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2); - -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2); - -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2); -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2); -o-box-shadow: 2px 3px 5px rgba(0,0,0,.2); -ms-box-shadow: 2px 3px 5px rgba(0,0,0,.2); box-shadow: 2px 3px 5px rgba(0,0,0,.2) ; - -webkit-border-radius: 3px; -moz-border-radius: 3px; -o-border-radius: 3px; -ms-border-radius: 3px; border-radius: 3px ; - border: 1px solid silver; - - background: white; - font-size: 90%; - font-family: monospace; - - max-height: 20em; - overflow-y: auto; -} - -.CodeMirror-hint { - margin: 0; - padding: 0 4px; - -webkit-border-radius: 2px; -moz-border-radius: 2px; -o-border-radius: 2px; -ms-border-radius: 2px; border-radius: 2px ; - white-space: pre; - color: black; - cursor: pointer; -} - -li.CodeMirror-hint-active { - background: #08f; - color: white; -} - - -/* ---- extension/lint/lint.css ---- */ - - -/* The lint marker gutter */ -.CodeMirror-lint-markers { - width: 16px; -} - -.CodeMirror-lint-tooltip { - background-color: #ffd; - border: 1px solid black; - -webkit-border-radius: 4px 4px 4px 4px; -moz-border-radius: 4px 4px 4px 4px; -o-border-radius: 4px 4px 4px 4px; -ms-border-radius: 4px 4px 4px 4px; border-radius: 4px 4px 4px 4px ; - color: black; - font-family: monospace; - font-size: 10pt; - overflow: hidden; - padding: 2px 5px; - position: fixed; - white-space: pre; - white-space: pre-wrap; - z-index: 100; - max-width: 600px; - opacity: 0; - -webkit-transition: opacity .4s; -moz-transition: opacity .4s; -o-transition: opacity .4s; -ms-transition: opacity .4s; transition: opacity .4s ; - -moz-transition: opacity .4s; - -webkit-transition: opacity .4s; - -o-transition: opacity .4s; - -ms-transition: opacity .4s; -} - -.CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning { - background-position: left bottom; - background-repeat: repeat-x; -} - -.CodeMirror-lint-mark-error { - background-image: - url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==") - ; -} - -.CodeMirror-lint-mark-warning { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII="); -} - -.CodeMirror-lint-marker-error, .CodeMirror-lint-marker-warning { - background-position: center center; - background-repeat: no-repeat; - cursor: pointer; - display: inline-block; - height: 16px; - width: 16px; - vertical-align: middle; - position: relative; -} - -.CodeMirror-lint-message-error, .CodeMirror-lint-message-warning { - padding-left: 18px; - background-position: top left; - background-repeat: no-repeat; -} - -.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII="); -} - -.CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII="); -} - -.CodeMirror-lint-marker-multiple { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC"); - background-repeat: no-repeat; - background-position: right bottom; - width: 100%; height: 100%; -} - - -/* ---- extension/scroll/simplescrollbars.css ---- */ - - -.CodeMirror-simplescroll-horizontal div, .CodeMirror-simplescroll-vertical div { - position: absolute; - background: #ccc; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; - border: 1px solid #bbb; - -webkit-border-radius: 2px; -moz-border-radius: 2px; -o-border-radius: 2px; -ms-border-radius: 2px; border-radius: 2px ; -} - -.CodeMirror-simplescroll-horizontal, .CodeMirror-simplescroll-vertical { - position: absolute; - z-index: 6; - background: #eee; -} - -.CodeMirror-simplescroll-horizontal { - bottom: 0; left: 0; - height: 8px; -} -.CodeMirror-simplescroll-horizontal div { - bottom: 0; - height: 100%; -} - -.CodeMirror-simplescroll-vertical { - right: 0; top: 0; - width: 8px; -} -.CodeMirror-simplescroll-vertical div { - right: 0; - width: 100%; -} - - -.CodeMirror-overlayscroll .CodeMirror-scrollbar-filler, .CodeMirror-overlayscroll .CodeMirror-gutter-filler { - display: none; -} - -.CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div { - position: absolute; - background: #bcd; - -webkit-border-radius: 3px; -moz-border-radius: 3px; -o-border-radius: 3px; -ms-border-radius: 3px; border-radius: 3px ; -} - -.CodeMirror-overlayscroll-horizontal, .CodeMirror-overlayscroll-vertical { - position: absolute; - z-index: 6; -} - -.CodeMirror-overlayscroll-horizontal { - bottom: 0; left: 0; - height: 6px; -} -.CodeMirror-overlayscroll-horizontal div { - bottom: 0; - height: 100%; -} - -.CodeMirror-overlayscroll-vertical { - right: 0; top: 0; - width: 6px; -} -.CodeMirror-overlayscroll-vertical div { - right: 0; - width: 100%; -} - - -/* ---- extension/search/matchesonscrollbar.css ---- */ - - -.CodeMirror-search-match { - background: gold; - border-top: 1px solid orange; - border-bottom: 1px solid orange; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; - opacity: .5; -} diff --git a/plugins/UiFileManager/media/codemirror/all.js b/plugins/UiFileManager/media/codemirror/all.js deleted file mode 100644 index ef2a423a..00000000 --- a/plugins/UiFileManager/media/codemirror/all.js +++ /dev/null @@ -1,19964 +0,0 @@ - -/* ---- base/codemirror.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -// This is CodeMirror (https://codemirror.net), a code editor -// implemented in JavaScript on top of the browser's DOM. -// -// You can find some technical background for some of the code below -// at http://marijnhaverbeke.nl/blog/#cm-internals . - -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - (global = global || self, global.CodeMirror = factory()); -}(this, (function () { 'use strict'; - - // Kludges for bugs and behavior differences that can't be feature - // detected are enabled based on userAgent etc sniffing. - var userAgent = navigator.userAgent; - var platform = navigator.platform; - - var gecko = /gecko\/\d/i.test(userAgent); - var ie_upto10 = /MSIE \d/.test(userAgent); - var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(userAgent); - var edge = /Edge\/(\d+)/.exec(userAgent); - var ie = ie_upto10 || ie_11up || edge; - var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : +(edge || ie_11up)[1]); - var webkit = !edge && /WebKit\//.test(userAgent); - var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(userAgent); - var chrome = !edge && /Chrome\//.test(userAgent); - var presto = /Opera\//.test(userAgent); - var safari = /Apple Computer/.test(navigator.vendor); - var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(userAgent); - var phantom = /PhantomJS/.test(userAgent); - - var ios = !edge && /AppleWebKit/.test(userAgent) && /Mobile\/\w+/.test(userAgent); - var android = /Android/.test(userAgent); - // This is woefully incomplete. Suggestions for alternative methods welcome. - var mobile = ios || android || /webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(userAgent); - var mac = ios || /Mac/.test(platform); - var chromeOS = /\bCrOS\b/.test(userAgent); - var windows = /win/i.test(platform); - - var presto_version = presto && userAgent.match(/Version\/(\d*\.\d*)/); - if (presto_version) { presto_version = Number(presto_version[1]); } - if (presto_version && presto_version >= 15) { presto = false; webkit = true; } - // Some browsers use the wrong event properties to signal cmd/ctrl on OS X - var flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11)); - var captureRightClick = gecko || (ie && ie_version >= 9); - - function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*") } - - var rmClass = function(node, cls) { - var current = node.className; - var match = classTest(cls).exec(current); - if (match) { - var after = current.slice(match.index + match[0].length); - node.className = current.slice(0, match.index) + (after ? match[1] + after : ""); - } - }; - - function removeChildren(e) { - for (var count = e.childNodes.length; count > 0; --count) - { e.removeChild(e.firstChild); } - return e - } - - function removeChildrenAndAdd(parent, e) { - return removeChildren(parent).appendChild(e) - } - - function elt(tag, content, className, style) { - var e = document.createElement(tag); - if (className) { e.className = className; } - if (style) { e.style.cssText = style; } - if (typeof content == "string") { e.appendChild(document.createTextNode(content)); } - else if (content) { for (var i = 0; i < content.length; ++i) { e.appendChild(content[i]); } } - return e - } - // wrapper for elt, which removes the elt from the accessibility tree - function eltP(tag, content, className, style) { - var e = elt(tag, content, className, style); - e.setAttribute("role", "presentation"); - return e - } - - var range; - if (document.createRange) { range = function(node, start, end, endNode) { - var r = document.createRange(); - r.setEnd(endNode || node, end); - r.setStart(node, start); - return r - }; } - else { range = function(node, start, end) { - var r = document.body.createTextRange(); - try { r.moveToElementText(node.parentNode); } - catch(e) { return r } - r.collapse(true); - r.moveEnd("character", end); - r.moveStart("character", start); - return r - }; } - - function contains(parent, child) { - if (child.nodeType == 3) // Android browser always returns false when child is a textnode - { child = child.parentNode; } - if (parent.contains) - { return parent.contains(child) } - do { - if (child.nodeType == 11) { child = child.host; } - if (child == parent) { return true } - } while (child = child.parentNode) - } - - function activeElt() { - // IE and Edge may throw an "Unspecified Error" when accessing document.activeElement. - // IE < 10 will throw when accessed while the page is loading or in an iframe. - // IE > 9 and Edge will throw when accessed in an iframe if document.body is unavailable. - var activeElement; - try { - activeElement = document.activeElement; - } catch(e) { - activeElement = document.body || null; - } - while (activeElement && activeElement.shadowRoot && activeElement.shadowRoot.activeElement) - { activeElement = activeElement.shadowRoot.activeElement; } - return activeElement - } - - function addClass(node, cls) { - var current = node.className; - if (!classTest(cls).test(current)) { node.className += (current ? " " : "") + cls; } - } - function joinClasses(a, b) { - var as = a.split(" "); - for (var i = 0; i < as.length; i++) - { if (as[i] && !classTest(as[i]).test(b)) { b += " " + as[i]; } } - return b - } - - var selectInput = function(node) { node.select(); }; - if (ios) // Mobile Safari apparently has a bug where select() is broken. - { selectInput = function(node) { node.selectionStart = 0; node.selectionEnd = node.value.length; }; } - else if (ie) // Suppress mysterious IE10 errors - { selectInput = function(node) { try { node.select(); } catch(_e) {} }; } - - function bind(f) { - var args = Array.prototype.slice.call(arguments, 1); - return function(){return f.apply(null, args)} - } - - function copyObj(obj, target, overwrite) { - if (!target) { target = {}; } - for (var prop in obj) - { if (obj.hasOwnProperty(prop) && (overwrite !== false || !target.hasOwnProperty(prop))) - { target[prop] = obj[prop]; } } - return target - } - - // Counts the column offset in a string, taking tabs into account. - // Used mostly to find indentation. - function countColumn(string, end, tabSize, startIndex, startValue) { - if (end == null) { - end = string.search(/[^\s\u00a0]/); - if (end == -1) { end = string.length; } - } - for (var i = startIndex || 0, n = startValue || 0;;) { - var nextTab = string.indexOf("\t", i); - if (nextTab < 0 || nextTab >= end) - { return n + (end - i) } - n += nextTab - i; - n += tabSize - (n % tabSize); - i = nextTab + 1; - } - } - - var Delayed = function() { - this.id = null; - this.f = null; - this.time = 0; - this.handler = bind(this.onTimeout, this); - }; - Delayed.prototype.onTimeout = function (self) { - self.id = 0; - if (self.time <= +new Date) { - self.f(); - } else { - setTimeout(self.handler, self.time - +new Date); - } - }; - Delayed.prototype.set = function (ms, f) { - this.f = f; - var time = +new Date + ms; - if (!this.id || time < this.time) { - clearTimeout(this.id); - this.id = setTimeout(this.handler, ms); - this.time = time; - } - }; - - function indexOf(array, elt) { - for (var i = 0; i < array.length; ++i) - { if (array[i] == elt) { return i } } - return -1 - } - - // Number of pixels added to scroller and sizer to hide scrollbar - var scrollerGap = 50; - - // Returned or thrown by various protocols to signal 'I'm not - // handling this'. - var Pass = {toString: function(){return "CodeMirror.Pass"}}; - - // Reused option objects for setSelection & friends - var sel_dontScroll = {scroll: false}, sel_mouse = {origin: "*mouse"}, sel_move = {origin: "+move"}; - - // The inverse of countColumn -- find the offset that corresponds to - // a particular column. - function findColumn(string, goal, tabSize) { - for (var pos = 0, col = 0;;) { - var nextTab = string.indexOf("\t", pos); - if (nextTab == -1) { nextTab = string.length; } - var skipped = nextTab - pos; - if (nextTab == string.length || col + skipped >= goal) - { return pos + Math.min(skipped, goal - col) } - col += nextTab - pos; - col += tabSize - (col % tabSize); - pos = nextTab + 1; - if (col >= goal) { return pos } - } - } - - var spaceStrs = [""]; - function spaceStr(n) { - while (spaceStrs.length <= n) - { spaceStrs.push(lst(spaceStrs) + " "); } - return spaceStrs[n] - } - - function lst(arr) { return arr[arr.length-1] } - - function map(array, f) { - var out = []; - for (var i = 0; i < array.length; i++) { out[i] = f(array[i], i); } - return out - } - - function insertSorted(array, value, score) { - var pos = 0, priority = score(value); - while (pos < array.length && score(array[pos]) <= priority) { pos++; } - array.splice(pos, 0, value); - } - - function nothing() {} - - function createObj(base, props) { - var inst; - if (Object.create) { - inst = Object.create(base); - } else { - nothing.prototype = base; - inst = new nothing(); - } - if (props) { copyObj(props, inst); } - return inst - } - - var nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/; - function isWordCharBasic(ch) { - return /\w/.test(ch) || ch > "\x80" && - (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch)) - } - function isWordChar(ch, helper) { - if (!helper) { return isWordCharBasic(ch) } - if (helper.source.indexOf("\\w") > -1 && isWordCharBasic(ch)) { return true } - return helper.test(ch) - } - - function isEmpty(obj) { - for (var n in obj) { if (obj.hasOwnProperty(n) && obj[n]) { return false } } - return true - } - - // Extending unicode characters. A series of a non-extending char + - // any number of extending chars is treated as a single unit as far - // as editing and measuring is concerned. This is not fully correct, - // since some scripts/fonts/browsers also treat other configurations - // of code points as a group. - var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/; - function isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch) } - - // Returns a number from the range [`0`; `str.length`] unless `pos` is outside that range. - function skipExtendingChars(str, pos, dir) { - while ((dir < 0 ? pos > 0 : pos < str.length) && isExtendingChar(str.charAt(pos))) { pos += dir; } - return pos - } - - // Returns the value from the range [`from`; `to`] that satisfies - // `pred` and is closest to `from`. Assumes that at least `to` - // satisfies `pred`. Supports `from` being greater than `to`. - function findFirst(pred, from, to) { - // At any point we are certain `to` satisfies `pred`, don't know - // whether `from` does. - var dir = from > to ? -1 : 1; - for (;;) { - if (from == to) { return from } - var midF = (from + to) / 2, mid = dir < 0 ? Math.ceil(midF) : Math.floor(midF); - if (mid == from) { return pred(mid) ? from : to } - if (pred(mid)) { to = mid; } - else { from = mid + dir; } - } - } - - // BIDI HELPERS - - function iterateBidiSections(order, from, to, f) { - if (!order) { return f(from, to, "ltr", 0) } - var found = false; - for (var i = 0; i < order.length; ++i) { - var part = order[i]; - if (part.from < to && part.to > from || from == to && part.to == from) { - f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr", i); - found = true; - } - } - if (!found) { f(from, to, "ltr"); } - } - - var bidiOther = null; - function getBidiPartAt(order, ch, sticky) { - var found; - bidiOther = null; - for (var i = 0; i < order.length; ++i) { - var cur = order[i]; - if (cur.from < ch && cur.to > ch) { return i } - if (cur.to == ch) { - if (cur.from != cur.to && sticky == "before") { found = i; } - else { bidiOther = i; } - } - if (cur.from == ch) { - if (cur.from != cur.to && sticky != "before") { found = i; } - else { bidiOther = i; } - } - } - return found != null ? found : bidiOther - } - - // Bidirectional ordering algorithm - // See http://unicode.org/reports/tr9/tr9-13.html for the algorithm - // that this (partially) implements. - - // One-char codes used for character types: - // L (L): Left-to-Right - // R (R): Right-to-Left - // r (AL): Right-to-Left Arabic - // 1 (EN): European Number - // + (ES): European Number Separator - // % (ET): European Number Terminator - // n (AN): Arabic Number - // , (CS): Common Number Separator - // m (NSM): Non-Spacing Mark - // b (BN): Boundary Neutral - // s (B): Paragraph Separator - // t (S): Segment Separator - // w (WS): Whitespace - // N (ON): Other Neutrals - - // Returns null if characters are ordered as they appear - // (left-to-right), or an array of sections ({from, to, level} - // objects) in the order in which they occur visually. - var bidiOrdering = (function() { - // Character types for codepoints 0 to 0xff - var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN"; - // Character types for codepoints 0x600 to 0x6f9 - var arabicTypes = "nnnnnnNNr%%r,rNNmmmmmmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmmmnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmnNmmmmmmrrmmNmmmmrr1111111111"; - function charType(code) { - if (code <= 0xf7) { return lowTypes.charAt(code) } - else if (0x590 <= code && code <= 0x5f4) { return "R" } - else if (0x600 <= code && code <= 0x6f9) { return arabicTypes.charAt(code - 0x600) } - else if (0x6ee <= code && code <= 0x8ac) { return "r" } - else if (0x2000 <= code && code <= 0x200b) { return "w" } - else if (code == 0x200c) { return "b" } - else { return "L" } - } - - var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/; - var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/; - - function BidiSpan(level, from, to) { - this.level = level; - this.from = from; this.to = to; - } - - return function(str, direction) { - var outerType = direction == "ltr" ? "L" : "R"; - - if (str.length == 0 || direction == "ltr" && !bidiRE.test(str)) { return false } - var len = str.length, types = []; - for (var i = 0; i < len; ++i) - { types.push(charType(str.charCodeAt(i))); } - - // W1. Examine each non-spacing mark (NSM) in the level run, and - // change the type of the NSM to the type of the previous - // character. If the NSM is at the start of the level run, it will - // get the type of sor. - for (var i$1 = 0, prev = outerType; i$1 < len; ++i$1) { - var type = types[i$1]; - if (type == "m") { types[i$1] = prev; } - else { prev = type; } - } - - // W2. Search backwards from each instance of a European number - // until the first strong type (R, L, AL, or sor) is found. If an - // AL is found, change the type of the European number to Arabic - // number. - // W3. Change all ALs to R. - for (var i$2 = 0, cur = outerType; i$2 < len; ++i$2) { - var type$1 = types[i$2]; - if (type$1 == "1" && cur == "r") { types[i$2] = "n"; } - else if (isStrong.test(type$1)) { cur = type$1; if (type$1 == "r") { types[i$2] = "R"; } } - } - - // W4. A single European separator between two European numbers - // changes to a European number. A single common separator between - // two numbers of the same type changes to that type. - for (var i$3 = 1, prev$1 = types[0]; i$3 < len - 1; ++i$3) { - var type$2 = types[i$3]; - if (type$2 == "+" && prev$1 == "1" && types[i$3+1] == "1") { types[i$3] = "1"; } - else if (type$2 == "," && prev$1 == types[i$3+1] && - (prev$1 == "1" || prev$1 == "n")) { types[i$3] = prev$1; } - prev$1 = type$2; - } - - // W5. A sequence of European terminators adjacent to European - // numbers changes to all European numbers. - // W6. Otherwise, separators and terminators change to Other - // Neutral. - for (var i$4 = 0; i$4 < len; ++i$4) { - var type$3 = types[i$4]; - if (type$3 == ",") { types[i$4] = "N"; } - else if (type$3 == "%") { - var end = (void 0); - for (end = i$4 + 1; end < len && types[end] == "%"; ++end) {} - var replace = (i$4 && types[i$4-1] == "!") || (end < len && types[end] == "1") ? "1" : "N"; - for (var j = i$4; j < end; ++j) { types[j] = replace; } - i$4 = end - 1; - } - } - - // W7. Search backwards from each instance of a European number - // until the first strong type (R, L, or sor) is found. If an L is - // found, then change the type of the European number to L. - for (var i$5 = 0, cur$1 = outerType; i$5 < len; ++i$5) { - var type$4 = types[i$5]; - if (cur$1 == "L" && type$4 == "1") { types[i$5] = "L"; } - else if (isStrong.test(type$4)) { cur$1 = type$4; } - } - - // N1. A sequence of neutrals takes the direction of the - // surrounding strong text if the text on both sides has the same - // direction. European and Arabic numbers act as if they were R in - // terms of their influence on neutrals. Start-of-level-run (sor) - // and end-of-level-run (eor) are used at level run boundaries. - // N2. Any remaining neutrals take the embedding direction. - for (var i$6 = 0; i$6 < len; ++i$6) { - if (isNeutral.test(types[i$6])) { - var end$1 = (void 0); - for (end$1 = i$6 + 1; end$1 < len && isNeutral.test(types[end$1]); ++end$1) {} - var before = (i$6 ? types[i$6-1] : outerType) == "L"; - var after = (end$1 < len ? types[end$1] : outerType) == "L"; - var replace$1 = before == after ? (before ? "L" : "R") : outerType; - for (var j$1 = i$6; j$1 < end$1; ++j$1) { types[j$1] = replace$1; } - i$6 = end$1 - 1; - } - } - - // Here we depart from the documented algorithm, in order to avoid - // building up an actual levels array. Since there are only three - // levels (0, 1, 2) in an implementation that doesn't take - // explicit embedding into account, we can build up the order on - // the fly, without following the level-based algorithm. - var order = [], m; - for (var i$7 = 0; i$7 < len;) { - if (countsAsLeft.test(types[i$7])) { - var start = i$7; - for (++i$7; i$7 < len && countsAsLeft.test(types[i$7]); ++i$7) {} - order.push(new BidiSpan(0, start, i$7)); - } else { - var pos = i$7, at = order.length, isRTL = direction == "rtl" ? 1 : 0; - for (++i$7; i$7 < len && types[i$7] != "L"; ++i$7) {} - for (var j$2 = pos; j$2 < i$7;) { - if (countsAsNum.test(types[j$2])) { - if (pos < j$2) { order.splice(at, 0, new BidiSpan(1, pos, j$2)); at += isRTL; } - var nstart = j$2; - for (++j$2; j$2 < i$7 && countsAsNum.test(types[j$2]); ++j$2) {} - order.splice(at, 0, new BidiSpan(2, nstart, j$2)); - at += isRTL; - pos = j$2; - } else { ++j$2; } - } - if (pos < i$7) { order.splice(at, 0, new BidiSpan(1, pos, i$7)); } - } - } - if (direction == "ltr") { - if (order[0].level == 1 && (m = str.match(/^\s+/))) { - order[0].from = m[0].length; - order.unshift(new BidiSpan(0, 0, m[0].length)); - } - if (lst(order).level == 1 && (m = str.match(/\s+$/))) { - lst(order).to -= m[0].length; - order.push(new BidiSpan(0, len - m[0].length, len)); - } - } - - return direction == "rtl" ? order.reverse() : order - } - })(); - - // Get the bidi ordering for the given line (and cache it). Returns - // false for lines that are fully left-to-right, and an array of - // BidiSpan objects otherwise. - function getOrder(line, direction) { - var order = line.order; - if (order == null) { order = line.order = bidiOrdering(line.text, direction); } - return order - } - - // EVENT HANDLING - - // Lightweight event framework. on/off also work on DOM nodes, - // registering native DOM handlers. - - var noHandlers = []; - - var on = function(emitter, type, f) { - if (emitter.addEventListener) { - emitter.addEventListener(type, f, false); - } else if (emitter.attachEvent) { - emitter.attachEvent("on" + type, f); - } else { - var map = emitter._handlers || (emitter._handlers = {}); - map[type] = (map[type] || noHandlers).concat(f); - } - }; - - function getHandlers(emitter, type) { - return emitter._handlers && emitter._handlers[type] || noHandlers - } - - function off(emitter, type, f) { - if (emitter.removeEventListener) { - emitter.removeEventListener(type, f, false); - } else if (emitter.detachEvent) { - emitter.detachEvent("on" + type, f); - } else { - var map = emitter._handlers, arr = map && map[type]; - if (arr) { - var index = indexOf(arr, f); - if (index > -1) - { map[type] = arr.slice(0, index).concat(arr.slice(index + 1)); } - } - } - } - - function signal(emitter, type /*, values...*/) { - var handlers = getHandlers(emitter, type); - if (!handlers.length) { return } - var args = Array.prototype.slice.call(arguments, 2); - for (var i = 0; i < handlers.length; ++i) { handlers[i].apply(null, args); } - } - - // The DOM events that CodeMirror handles can be overridden by - // registering a (non-DOM) handler on the editor for the event name, - // and preventDefault-ing the event in that handler. - function signalDOMEvent(cm, e, override) { - if (typeof e == "string") - { e = {type: e, preventDefault: function() { this.defaultPrevented = true; }}; } - signal(cm, override || e.type, cm, e); - return e_defaultPrevented(e) || e.codemirrorIgnore - } - - function signalCursorActivity(cm) { - var arr = cm._handlers && cm._handlers.cursorActivity; - if (!arr) { return } - var set = cm.curOp.cursorActivityHandlers || (cm.curOp.cursorActivityHandlers = []); - for (var i = 0; i < arr.length; ++i) { if (indexOf(set, arr[i]) == -1) - { set.push(arr[i]); } } - } - - function hasHandler(emitter, type) { - return getHandlers(emitter, type).length > 0 - } - - // Add on and off methods to a constructor's prototype, to make - // registering events on such objects more convenient. - function eventMixin(ctor) { - ctor.prototype.on = function(type, f) {on(this, type, f);}; - ctor.prototype.off = function(type, f) {off(this, type, f);}; - } - - // Due to the fact that we still support jurassic IE versions, some - // compatibility wrappers are needed. - - function e_preventDefault(e) { - if (e.preventDefault) { e.preventDefault(); } - else { e.returnValue = false; } - } - function e_stopPropagation(e) { - if (e.stopPropagation) { e.stopPropagation(); } - else { e.cancelBubble = true; } - } - function e_defaultPrevented(e) { - return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false - } - function e_stop(e) {e_preventDefault(e); e_stopPropagation(e);} - - function e_target(e) {return e.target || e.srcElement} - function e_button(e) { - var b = e.which; - if (b == null) { - if (e.button & 1) { b = 1; } - else if (e.button & 2) { b = 3; } - else if (e.button & 4) { b = 2; } - } - if (mac && e.ctrlKey && b == 1) { b = 3; } - return b - } - - // Detect drag-and-drop - var dragAndDrop = function() { - // There is *some* kind of drag-and-drop support in IE6-8, but I - // couldn't get it to work yet. - if (ie && ie_version < 9) { return false } - var div = elt('div'); - return "draggable" in div || "dragDrop" in div - }(); - - var zwspSupported; - function zeroWidthElement(measure) { - if (zwspSupported == null) { - var test = elt("span", "\u200b"); - removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")])); - if (measure.firstChild.offsetHeight != 0) - { zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !(ie && ie_version < 8); } - } - var node = zwspSupported ? elt("span", "\u200b") : - elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px"); - node.setAttribute("cm-text", ""); - return node - } - - // Feature-detect IE's crummy client rect reporting for bidi text - var badBidiRects; - function hasBadBidiRects(measure) { - if (badBidiRects != null) { return badBidiRects } - var txt = removeChildrenAndAdd(measure, document.createTextNode("A\u062eA")); - var r0 = range(txt, 0, 1).getBoundingClientRect(); - var r1 = range(txt, 1, 2).getBoundingClientRect(); - removeChildren(measure); - if (!r0 || r0.left == r0.right) { return false } // Safari returns null in some cases (#2780) - return badBidiRects = (r1.right - r0.right < 3) - } - - // See if "".split is the broken IE version, if so, provide an - // alternative way to split lines. - var splitLinesAuto = "\n\nb".split(/\n/).length != 3 ? function (string) { - var pos = 0, result = [], l = string.length; - while (pos <= l) { - var nl = string.indexOf("\n", pos); - if (nl == -1) { nl = string.length; } - var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl); - var rt = line.indexOf("\r"); - if (rt != -1) { - result.push(line.slice(0, rt)); - pos += rt + 1; - } else { - result.push(line); - pos = nl + 1; - } - } - return result - } : function (string) { return string.split(/\r\n?|\n/); }; - - var hasSelection = window.getSelection ? function (te) { - try { return te.selectionStart != te.selectionEnd } - catch(e) { return false } - } : function (te) { - var range; - try {range = te.ownerDocument.selection.createRange();} - catch(e) {} - if (!range || range.parentElement() != te) { return false } - return range.compareEndPoints("StartToEnd", range) != 0 - }; - - var hasCopyEvent = (function () { - var e = elt("div"); - if ("oncopy" in e) { return true } - e.setAttribute("oncopy", "return;"); - return typeof e.oncopy == "function" - })(); - - var badZoomedRects = null; - function hasBadZoomedRects(measure) { - if (badZoomedRects != null) { return badZoomedRects } - var node = removeChildrenAndAdd(measure, elt("span", "x")); - var normal = node.getBoundingClientRect(); - var fromRange = range(node, 0, 1).getBoundingClientRect(); - return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1 - } - - // Known modes, by name and by MIME - var modes = {}, mimeModes = {}; - - // Extra arguments are stored as the mode's dependencies, which is - // used by (legacy) mechanisms like loadmode.js to automatically - // load a mode. (Preferred mechanism is the require/define calls.) - function defineMode(name, mode) { - if (arguments.length > 2) - { mode.dependencies = Array.prototype.slice.call(arguments, 2); } - modes[name] = mode; - } - - function defineMIME(mime, spec) { - mimeModes[mime] = spec; - } - - // Given a MIME type, a {name, ...options} config object, or a name - // string, return a mode config object. - function resolveMode(spec) { - if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) { - spec = mimeModes[spec]; - } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) { - var found = mimeModes[spec.name]; - if (typeof found == "string") { found = {name: found}; } - spec = createObj(found, spec); - spec.name = found.name; - } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) { - return resolveMode("application/xml") - } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+json$/.test(spec)) { - return resolveMode("application/json") - } - if (typeof spec == "string") { return {name: spec} } - else { return spec || {name: "null"} } - } - - // Given a mode spec (anything that resolveMode accepts), find and - // initialize an actual mode object. - function getMode(options, spec) { - spec = resolveMode(spec); - var mfactory = modes[spec.name]; - if (!mfactory) { return getMode(options, "text/plain") } - var modeObj = mfactory(options, spec); - if (modeExtensions.hasOwnProperty(spec.name)) { - var exts = modeExtensions[spec.name]; - for (var prop in exts) { - if (!exts.hasOwnProperty(prop)) { continue } - if (modeObj.hasOwnProperty(prop)) { modeObj["_" + prop] = modeObj[prop]; } - modeObj[prop] = exts[prop]; - } - } - modeObj.name = spec.name; - if (spec.helperType) { modeObj.helperType = spec.helperType; } - if (spec.modeProps) { for (var prop$1 in spec.modeProps) - { modeObj[prop$1] = spec.modeProps[prop$1]; } } - - return modeObj - } - - // This can be used to attach properties to mode objects from - // outside the actual mode definition. - var modeExtensions = {}; - function extendMode(mode, properties) { - var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {}); - copyObj(properties, exts); - } - - function copyState(mode, state) { - if (state === true) { return state } - if (mode.copyState) { return mode.copyState(state) } - var nstate = {}; - for (var n in state) { - var val = state[n]; - if (val instanceof Array) { val = val.concat([]); } - nstate[n] = val; - } - return nstate - } - - // Given a mode and a state (for that mode), find the inner mode and - // state at the position that the state refers to. - function innerMode(mode, state) { - var info; - while (mode.innerMode) { - info = mode.innerMode(state); - if (!info || info.mode == mode) { break } - state = info.state; - mode = info.mode; - } - return info || {mode: mode, state: state} - } - - function startState(mode, a1, a2) { - return mode.startState ? mode.startState(a1, a2) : true - } - - // STRING STREAM - - // Fed to the mode parsers, provides helper functions to make - // parsers more succinct. - - var StringStream = function(string, tabSize, lineOracle) { - this.pos = this.start = 0; - this.string = string; - this.tabSize = tabSize || 8; - this.lastColumnPos = this.lastColumnValue = 0; - this.lineStart = 0; - this.lineOracle = lineOracle; - }; - - StringStream.prototype.eol = function () {return this.pos >= this.string.length}; - StringStream.prototype.sol = function () {return this.pos == this.lineStart}; - StringStream.prototype.peek = function () {return this.string.charAt(this.pos) || undefined}; - StringStream.prototype.next = function () { - if (this.pos < this.string.length) - { return this.string.charAt(this.pos++) } - }; - StringStream.prototype.eat = function (match) { - var ch = this.string.charAt(this.pos); - var ok; - if (typeof match == "string") { ok = ch == match; } - else { ok = ch && (match.test ? match.test(ch) : match(ch)); } - if (ok) {++this.pos; return ch} - }; - StringStream.prototype.eatWhile = function (match) { - var start = this.pos; - while (this.eat(match)){} - return this.pos > start - }; - StringStream.prototype.eatSpace = function () { - var start = this.pos; - while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) { ++this.pos; } - return this.pos > start - }; - StringStream.prototype.skipToEnd = function () {this.pos = this.string.length;}; - StringStream.prototype.skipTo = function (ch) { - var found = this.string.indexOf(ch, this.pos); - if (found > -1) {this.pos = found; return true} - }; - StringStream.prototype.backUp = function (n) {this.pos -= n;}; - StringStream.prototype.column = function () { - if (this.lastColumnPos < this.start) { - this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue); - this.lastColumnPos = this.start; - } - return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0) - }; - StringStream.prototype.indentation = function () { - return countColumn(this.string, null, this.tabSize) - - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0) - }; - StringStream.prototype.match = function (pattern, consume, caseInsensitive) { - if (typeof pattern == "string") { - var cased = function (str) { return caseInsensitive ? str.toLowerCase() : str; }; - var substr = this.string.substr(this.pos, pattern.length); - if (cased(substr) == cased(pattern)) { - if (consume !== false) { this.pos += pattern.length; } - return true - } - } else { - var match = this.string.slice(this.pos).match(pattern); - if (match && match.index > 0) { return null } - if (match && consume !== false) { this.pos += match[0].length; } - return match - } - }; - StringStream.prototype.current = function (){return this.string.slice(this.start, this.pos)}; - StringStream.prototype.hideFirstChars = function (n, inner) { - this.lineStart += n; - try { return inner() } - finally { this.lineStart -= n; } - }; - StringStream.prototype.lookAhead = function (n) { - var oracle = this.lineOracle; - return oracle && oracle.lookAhead(n) - }; - StringStream.prototype.baseToken = function () { - var oracle = this.lineOracle; - return oracle && oracle.baseToken(this.pos) - }; - - // Find the line object corresponding to the given line number. - function getLine(doc, n) { - n -= doc.first; - if (n < 0 || n >= doc.size) { throw new Error("There is no line " + (n + doc.first) + " in the document.") } - var chunk = doc; - while (!chunk.lines) { - for (var i = 0;; ++i) { - var child = chunk.children[i], sz = child.chunkSize(); - if (n < sz) { chunk = child; break } - n -= sz; - } - } - return chunk.lines[n] - } - - // Get the part of a document between two positions, as an array of - // strings. - function getBetween(doc, start, end) { - var out = [], n = start.line; - doc.iter(start.line, end.line + 1, function (line) { - var text = line.text; - if (n == end.line) { text = text.slice(0, end.ch); } - if (n == start.line) { text = text.slice(start.ch); } - out.push(text); - ++n; - }); - return out - } - // Get the lines between from and to, as array of strings. - function getLines(doc, from, to) { - var out = []; - doc.iter(from, to, function (line) { out.push(line.text); }); // iter aborts when callback returns truthy value - return out - } - - // Update the height of a line, propagating the height change - // upwards to parent nodes. - function updateLineHeight(line, height) { - var diff = height - line.height; - if (diff) { for (var n = line; n; n = n.parent) { n.height += diff; } } - } - - // Given a line object, find its line number by walking up through - // its parent links. - function lineNo(line) { - if (line.parent == null) { return null } - var cur = line.parent, no = indexOf(cur.lines, line); - for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) { - for (var i = 0;; ++i) { - if (chunk.children[i] == cur) { break } - no += chunk.children[i].chunkSize(); - } - } - return no + cur.first - } - - // Find the line at the given vertical position, using the height - // information in the document tree. - function lineAtHeight(chunk, h) { - var n = chunk.first; - outer: do { - for (var i$1 = 0; i$1 < chunk.children.length; ++i$1) { - var child = chunk.children[i$1], ch = child.height; - if (h < ch) { chunk = child; continue outer } - h -= ch; - n += child.chunkSize(); - } - return n - } while (!chunk.lines) - var i = 0; - for (; i < chunk.lines.length; ++i) { - var line = chunk.lines[i], lh = line.height; - if (h < lh) { break } - h -= lh; - } - return n + i - } - - function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size} - - function lineNumberFor(options, i) { - return String(options.lineNumberFormatter(i + options.firstLineNumber)) - } - - // A Pos instance represents a position within the text. - function Pos(line, ch, sticky) { - if ( sticky === void 0 ) sticky = null; - - if (!(this instanceof Pos)) { return new Pos(line, ch, sticky) } - this.line = line; - this.ch = ch; - this.sticky = sticky; - } - - // Compare two positions, return 0 if they are the same, a negative - // number when a is less, and a positive number otherwise. - function cmp(a, b) { return a.line - b.line || a.ch - b.ch } - - function equalCursorPos(a, b) { return a.sticky == b.sticky && cmp(a, b) == 0 } - - function copyPos(x) {return Pos(x.line, x.ch)} - function maxPos(a, b) { return cmp(a, b) < 0 ? b : a } - function minPos(a, b) { return cmp(a, b) < 0 ? a : b } - - // Most of the external API clips given positions to make sure they - // actually exist within the document. - function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1))} - function clipPos(doc, pos) { - if (pos.line < doc.first) { return Pos(doc.first, 0) } - var last = doc.first + doc.size - 1; - if (pos.line > last) { return Pos(last, getLine(doc, last).text.length) } - return clipToLen(pos, getLine(doc, pos.line).text.length) - } - function clipToLen(pos, linelen) { - var ch = pos.ch; - if (ch == null || ch > linelen) { return Pos(pos.line, linelen) } - else if (ch < 0) { return Pos(pos.line, 0) } - else { return pos } - } - function clipPosArray(doc, array) { - var out = []; - for (var i = 0; i < array.length; i++) { out[i] = clipPos(doc, array[i]); } - return out - } - - var SavedContext = function(state, lookAhead) { - this.state = state; - this.lookAhead = lookAhead; - }; - - var Context = function(doc, state, line, lookAhead) { - this.state = state; - this.doc = doc; - this.line = line; - this.maxLookAhead = lookAhead || 0; - this.baseTokens = null; - this.baseTokenPos = 1; - }; - - Context.prototype.lookAhead = function (n) { - var line = this.doc.getLine(this.line + n); - if (line != null && n > this.maxLookAhead) { this.maxLookAhead = n; } - return line - }; - - Context.prototype.baseToken = function (n) { - if (!this.baseTokens) { return null } - while (this.baseTokens[this.baseTokenPos] <= n) - { this.baseTokenPos += 2; } - var type = this.baseTokens[this.baseTokenPos + 1]; - return {type: type && type.replace(/( |^)overlay .*/, ""), - size: this.baseTokens[this.baseTokenPos] - n} - }; - - Context.prototype.nextLine = function () { - this.line++; - if (this.maxLookAhead > 0) { this.maxLookAhead--; } - }; - - Context.fromSaved = function (doc, saved, line) { - if (saved instanceof SavedContext) - { return new Context(doc, copyState(doc.mode, saved.state), line, saved.lookAhead) } - else - { return new Context(doc, copyState(doc.mode, saved), line) } - }; - - Context.prototype.save = function (copy) { - var state = copy !== false ? copyState(this.doc.mode, this.state) : this.state; - return this.maxLookAhead > 0 ? new SavedContext(state, this.maxLookAhead) : state - }; - - - // Compute a style array (an array starting with a mode generation - // -- for invalidation -- followed by pairs of end positions and - // style strings), which is used to highlight the tokens on the - // line. - function highlightLine(cm, line, context, forceToEnd) { - // A styles array always starts with a number identifying the - // mode/overlays that it is based on (for easy invalidation). - var st = [cm.state.modeGen], lineClasses = {}; - // Compute the base array of styles - runMode(cm, line.text, cm.doc.mode, context, function (end, style) { return st.push(end, style); }, - lineClasses, forceToEnd); - var state = context.state; - - // Run overlays, adjust style array. - var loop = function ( o ) { - context.baseTokens = st; - var overlay = cm.state.overlays[o], i = 1, at = 0; - context.state = true; - runMode(cm, line.text, overlay.mode, context, function (end, style) { - var start = i; - // Ensure there's a token end at the current position, and that i points at it - while (at < end) { - var i_end = st[i]; - if (i_end > end) - { st.splice(i, 1, end, st[i+1], i_end); } - i += 2; - at = Math.min(end, i_end); - } - if (!style) { return } - if (overlay.opaque) { - st.splice(start, i - start, end, "overlay " + style); - i = start + 2; - } else { - for (; start < i; start += 2) { - var cur = st[start+1]; - st[start+1] = (cur ? cur + " " : "") + "overlay " + style; - } - } - }, lineClasses); - context.state = state; - context.baseTokens = null; - context.baseTokenPos = 1; - }; - - for (var o = 0; o < cm.state.overlays.length; ++o) loop( o ); - - return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null} - } - - function getLineStyles(cm, line, updateFrontier) { - if (!line.styles || line.styles[0] != cm.state.modeGen) { - var context = getContextBefore(cm, lineNo(line)); - var resetState = line.text.length > cm.options.maxHighlightLength && copyState(cm.doc.mode, context.state); - var result = highlightLine(cm, line, context); - if (resetState) { context.state = resetState; } - line.stateAfter = context.save(!resetState); - line.styles = result.styles; - if (result.classes) { line.styleClasses = result.classes; } - else if (line.styleClasses) { line.styleClasses = null; } - if (updateFrontier === cm.doc.highlightFrontier) - { cm.doc.modeFrontier = Math.max(cm.doc.modeFrontier, ++cm.doc.highlightFrontier); } - } - return line.styles - } - - function getContextBefore(cm, n, precise) { - var doc = cm.doc, display = cm.display; - if (!doc.mode.startState) { return new Context(doc, true, n) } - var start = findStartLine(cm, n, precise); - var saved = start > doc.first && getLine(doc, start - 1).stateAfter; - var context = saved ? Context.fromSaved(doc, saved, start) : new Context(doc, startState(doc.mode), start); - - doc.iter(start, n, function (line) { - processLine(cm, line.text, context); - var pos = context.line; - line.stateAfter = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo ? context.save() : null; - context.nextLine(); - }); - if (precise) { doc.modeFrontier = context.line; } - return context - } - - // Lightweight form of highlight -- proceed over this line and - // update state, but don't save a style array. Used for lines that - // aren't currently visible. - function processLine(cm, text, context, startAt) { - var mode = cm.doc.mode; - var stream = new StringStream(text, cm.options.tabSize, context); - stream.start = stream.pos = startAt || 0; - if (text == "") { callBlankLine(mode, context.state); } - while (!stream.eol()) { - readToken(mode, stream, context.state); - stream.start = stream.pos; - } - } - - function callBlankLine(mode, state) { - if (mode.blankLine) { return mode.blankLine(state) } - if (!mode.innerMode) { return } - var inner = innerMode(mode, state); - if (inner.mode.blankLine) { return inner.mode.blankLine(inner.state) } - } - - function readToken(mode, stream, state, inner) { - for (var i = 0; i < 10; i++) { - if (inner) { inner[0] = innerMode(mode, state).mode; } - var style = mode.token(stream, state); - if (stream.pos > stream.start) { return style } - } - throw new Error("Mode " + mode.name + " failed to advance stream.") - } - - var Token = function(stream, type, state) { - this.start = stream.start; this.end = stream.pos; - this.string = stream.current(); - this.type = type || null; - this.state = state; - }; - - // Utility for getTokenAt and getLineTokens - function takeToken(cm, pos, precise, asArray) { - var doc = cm.doc, mode = doc.mode, style; - pos = clipPos(doc, pos); - var line = getLine(doc, pos.line), context = getContextBefore(cm, pos.line, precise); - var stream = new StringStream(line.text, cm.options.tabSize, context), tokens; - if (asArray) { tokens = []; } - while ((asArray || stream.pos < pos.ch) && !stream.eol()) { - stream.start = stream.pos; - style = readToken(mode, stream, context.state); - if (asArray) { tokens.push(new Token(stream, style, copyState(doc.mode, context.state))); } - } - return asArray ? tokens : new Token(stream, style, context.state) - } - - function extractLineClasses(type, output) { - if (type) { for (;;) { - var lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/); - if (!lineClass) { break } - type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length); - var prop = lineClass[1] ? "bgClass" : "textClass"; - if (output[prop] == null) - { output[prop] = lineClass[2]; } - else if (!(new RegExp("(?:^|\\s)" + lineClass[2] + "(?:$|\\s)")).test(output[prop])) - { output[prop] += " " + lineClass[2]; } - } } - return type - } - - // Run the given mode's parser over a line, calling f for each token. - function runMode(cm, text, mode, context, f, lineClasses, forceToEnd) { - var flattenSpans = mode.flattenSpans; - if (flattenSpans == null) { flattenSpans = cm.options.flattenSpans; } - var curStart = 0, curStyle = null; - var stream = new StringStream(text, cm.options.tabSize, context), style; - var inner = cm.options.addModeClass && [null]; - if (text == "") { extractLineClasses(callBlankLine(mode, context.state), lineClasses); } - while (!stream.eol()) { - if (stream.pos > cm.options.maxHighlightLength) { - flattenSpans = false; - if (forceToEnd) { processLine(cm, text, context, stream.pos); } - stream.pos = text.length; - style = null; - } else { - style = extractLineClasses(readToken(mode, stream, context.state, inner), lineClasses); - } - if (inner) { - var mName = inner[0].name; - if (mName) { style = "m-" + (style ? mName + " " + style : mName); } - } - if (!flattenSpans || curStyle != style) { - while (curStart < stream.start) { - curStart = Math.min(stream.start, curStart + 5000); - f(curStart, curStyle); - } - curStyle = style; - } - stream.start = stream.pos; - } - while (curStart < stream.pos) { - // Webkit seems to refuse to render text nodes longer than 57444 - // characters, and returns inaccurate measurements in nodes - // starting around 5000 chars. - var pos = Math.min(stream.pos, curStart + 5000); - f(pos, curStyle); - curStart = pos; - } - } - - // Finds the line to start with when starting a parse. Tries to - // find a line with a stateAfter, so that it can start with a - // valid state. If that fails, it returns the line with the - // smallest indentation, which tends to need the least context to - // parse correctly. - function findStartLine(cm, n, precise) { - var minindent, minline, doc = cm.doc; - var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100); - for (var search = n; search > lim; --search) { - if (search <= doc.first) { return doc.first } - var line = getLine(doc, search - 1), after = line.stateAfter; - if (after && (!precise || search + (after instanceof SavedContext ? after.lookAhead : 0) <= doc.modeFrontier)) - { return search } - var indented = countColumn(line.text, null, cm.options.tabSize); - if (minline == null || minindent > indented) { - minline = search - 1; - minindent = indented; - } - } - return minline - } - - function retreatFrontier(doc, n) { - doc.modeFrontier = Math.min(doc.modeFrontier, n); - if (doc.highlightFrontier < n - 10) { return } - var start = doc.first; - for (var line = n - 1; line > start; line--) { - var saved = getLine(doc, line).stateAfter; - // change is on 3 - // state on line 1 looked ahead 2 -- so saw 3 - // test 1 + 2 < 3 should cover this - if (saved && (!(saved instanceof SavedContext) || line + saved.lookAhead < n)) { - start = line + 1; - break - } - } - doc.highlightFrontier = Math.min(doc.highlightFrontier, start); - } - - // Optimize some code when these features are not used. - var sawReadOnlySpans = false, sawCollapsedSpans = false; - - function seeReadOnlySpans() { - sawReadOnlySpans = true; - } - - function seeCollapsedSpans() { - sawCollapsedSpans = true; - } - - // TEXTMARKER SPANS - - function MarkedSpan(marker, from, to) { - this.marker = marker; - this.from = from; this.to = to; - } - - // Search an array of spans for a span matching the given marker. - function getMarkedSpanFor(spans, marker) { - if (spans) { for (var i = 0; i < spans.length; ++i) { - var span = spans[i]; - if (span.marker == marker) { return span } - } } - } - // Remove a span from an array, returning undefined if no spans are - // left (we don't store arrays for lines without spans). - function removeMarkedSpan(spans, span) { - var r; - for (var i = 0; i < spans.length; ++i) - { if (spans[i] != span) { (r || (r = [])).push(spans[i]); } } - return r - } - // Add a span to a line. - function addMarkedSpan(line, span) { - line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span]; - span.marker.attachLine(line); - } - - // Used for the algorithm that adjusts markers for a change in the - // document. These functions cut an array of spans at a given - // character position, returning an array of remaining chunks (or - // undefined if nothing remains). - function markedSpansBefore(old, startCh, isInsert) { - var nw; - if (old) { for (var i = 0; i < old.length; ++i) { - var span = old[i], marker = span.marker; - var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh); - if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) { - var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh) - ;(nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to)); - } - } } - return nw - } - function markedSpansAfter(old, endCh, isInsert) { - var nw; - if (old) { for (var i = 0; i < old.length; ++i) { - var span = old[i], marker = span.marker; - var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh); - if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) { - var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh) - ;(nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh, - span.to == null ? null : span.to - endCh)); - } - } } - return nw - } - - // Given a change object, compute the new set of marker spans that - // cover the line in which the change took place. Removes spans - // entirely within the change, reconnects spans belonging to the - // same marker that appear on both sides of the change, and cuts off - // spans partially within the change. Returns an array of span - // arrays with one element for each line in (after) the change. - function stretchSpansOverChange(doc, change) { - if (change.full) { return null } - var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans; - var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans; - if (!oldFirst && !oldLast) { return null } - - var startCh = change.from.ch, endCh = change.to.ch, isInsert = cmp(change.from, change.to) == 0; - // Get the spans that 'stick out' on both sides - var first = markedSpansBefore(oldFirst, startCh, isInsert); - var last = markedSpansAfter(oldLast, endCh, isInsert); - - // Next, merge those two ends - var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0); - if (first) { - // Fix up .to properties of first - for (var i = 0; i < first.length; ++i) { - var span = first[i]; - if (span.to == null) { - var found = getMarkedSpanFor(last, span.marker); - if (!found) { span.to = startCh; } - else if (sameLine) { span.to = found.to == null ? null : found.to + offset; } - } - } - } - if (last) { - // Fix up .from in last (or move them into first in case of sameLine) - for (var i$1 = 0; i$1 < last.length; ++i$1) { - var span$1 = last[i$1]; - if (span$1.to != null) { span$1.to += offset; } - if (span$1.from == null) { - var found$1 = getMarkedSpanFor(first, span$1.marker); - if (!found$1) { - span$1.from = offset; - if (sameLine) { (first || (first = [])).push(span$1); } - } - } else { - span$1.from += offset; - if (sameLine) { (first || (first = [])).push(span$1); } - } - } - } - // Make sure we didn't create any zero-length spans - if (first) { first = clearEmptySpans(first); } - if (last && last != first) { last = clearEmptySpans(last); } - - var newMarkers = [first]; - if (!sameLine) { - // Fill gap with whole-line-spans - var gap = change.text.length - 2, gapMarkers; - if (gap > 0 && first) - { for (var i$2 = 0; i$2 < first.length; ++i$2) - { if (first[i$2].to == null) - { (gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i$2].marker, null, null)); } } } - for (var i$3 = 0; i$3 < gap; ++i$3) - { newMarkers.push(gapMarkers); } - newMarkers.push(last); - } - return newMarkers - } - - // Remove spans that are empty and don't have a clearWhenEmpty - // option of false. - function clearEmptySpans(spans) { - for (var i = 0; i < spans.length; ++i) { - var span = spans[i]; - if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false) - { spans.splice(i--, 1); } - } - if (!spans.length) { return null } - return spans - } - - // Used to 'clip' out readOnly ranges when making a change. - function removeReadOnlyRanges(doc, from, to) { - var markers = null; - doc.iter(from.line, to.line + 1, function (line) { - if (line.markedSpans) { for (var i = 0; i < line.markedSpans.length; ++i) { - var mark = line.markedSpans[i].marker; - if (mark.readOnly && (!markers || indexOf(markers, mark) == -1)) - { (markers || (markers = [])).push(mark); } - } } - }); - if (!markers) { return null } - var parts = [{from: from, to: to}]; - for (var i = 0; i < markers.length; ++i) { - var mk = markers[i], m = mk.find(0); - for (var j = 0; j < parts.length; ++j) { - var p = parts[j]; - if (cmp(p.to, m.from) < 0 || cmp(p.from, m.to) > 0) { continue } - var newParts = [j, 1], dfrom = cmp(p.from, m.from), dto = cmp(p.to, m.to); - if (dfrom < 0 || !mk.inclusiveLeft && !dfrom) - { newParts.push({from: p.from, to: m.from}); } - if (dto > 0 || !mk.inclusiveRight && !dto) - { newParts.push({from: m.to, to: p.to}); } - parts.splice.apply(parts, newParts); - j += newParts.length - 3; - } - } - return parts - } - - // Connect or disconnect spans from a line. - function detachMarkedSpans(line) { - var spans = line.markedSpans; - if (!spans) { return } - for (var i = 0; i < spans.length; ++i) - { spans[i].marker.detachLine(line); } - line.markedSpans = null; - } - function attachMarkedSpans(line, spans) { - if (!spans) { return } - for (var i = 0; i < spans.length; ++i) - { spans[i].marker.attachLine(line); } - line.markedSpans = spans; - } - - // Helpers used when computing which overlapping collapsed span - // counts as the larger one. - function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0 } - function extraRight(marker) { return marker.inclusiveRight ? 1 : 0 } - - // Returns a number indicating which of two overlapping collapsed - // spans is larger (and thus includes the other). Falls back to - // comparing ids when the spans cover exactly the same range. - function compareCollapsedMarkers(a, b) { - var lenDiff = a.lines.length - b.lines.length; - if (lenDiff != 0) { return lenDiff } - var aPos = a.find(), bPos = b.find(); - var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b); - if (fromCmp) { return -fromCmp } - var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b); - if (toCmp) { return toCmp } - return b.id - a.id - } - - // Find out whether a line ends or starts in a collapsed span. If - // so, return the marker for that span. - function collapsedSpanAtSide(line, start) { - var sps = sawCollapsedSpans && line.markedSpans, found; - if (sps) { for (var sp = (void 0), i = 0; i < sps.length; ++i) { - sp = sps[i]; - if (sp.marker.collapsed && (start ? sp.from : sp.to) == null && - (!found || compareCollapsedMarkers(found, sp.marker) < 0)) - { found = sp.marker; } - } } - return found - } - function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true) } - function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false) } - - function collapsedSpanAround(line, ch) { - var sps = sawCollapsedSpans && line.markedSpans, found; - if (sps) { for (var i = 0; i < sps.length; ++i) { - var sp = sps[i]; - if (sp.marker.collapsed && (sp.from == null || sp.from < ch) && (sp.to == null || sp.to > ch) && - (!found || compareCollapsedMarkers(found, sp.marker) < 0)) { found = sp.marker; } - } } - return found - } - - // Test whether there exists a collapsed span that partially - // overlaps (covers the start or end, but not both) of a new span. - // Such overlap is not allowed. - function conflictingCollapsedRange(doc, lineNo, from, to, marker) { - var line = getLine(doc, lineNo); - var sps = sawCollapsedSpans && line.markedSpans; - if (sps) { for (var i = 0; i < sps.length; ++i) { - var sp = sps[i]; - if (!sp.marker.collapsed) { continue } - var found = sp.marker.find(0); - var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker); - var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker); - if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) { continue } - if (fromCmp <= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.to, from) >= 0 : cmp(found.to, from) > 0) || - fromCmp >= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.from, to) <= 0 : cmp(found.from, to) < 0)) - { return true } - } } - } - - // A visual line is a line as drawn on the screen. Folding, for - // example, can cause multiple logical lines to appear on the same - // visual line. This finds the start of the visual line that the - // given line is part of (usually that is the line itself). - function visualLine(line) { - var merged; - while (merged = collapsedSpanAtStart(line)) - { line = merged.find(-1, true).line; } - return line - } - - function visualLineEnd(line) { - var merged; - while (merged = collapsedSpanAtEnd(line)) - { line = merged.find(1, true).line; } - return line - } - - // Returns an array of logical lines that continue the visual line - // started by the argument, or undefined if there are no such lines. - function visualLineContinued(line) { - var merged, lines; - while (merged = collapsedSpanAtEnd(line)) { - line = merged.find(1, true).line - ;(lines || (lines = [])).push(line); - } - return lines - } - - // Get the line number of the start of the visual line that the - // given line number is part of. - function visualLineNo(doc, lineN) { - var line = getLine(doc, lineN), vis = visualLine(line); - if (line == vis) { return lineN } - return lineNo(vis) - } - - // Get the line number of the start of the next visual line after - // the given line. - function visualLineEndNo(doc, lineN) { - if (lineN > doc.lastLine()) { return lineN } - var line = getLine(doc, lineN), merged; - if (!lineIsHidden(doc, line)) { return lineN } - while (merged = collapsedSpanAtEnd(line)) - { line = merged.find(1, true).line; } - return lineNo(line) + 1 - } - - // Compute whether a line is hidden. Lines count as hidden when they - // are part of a visual line that starts with another line, or when - // they are entirely covered by collapsed, non-widget span. - function lineIsHidden(doc, line) { - var sps = sawCollapsedSpans && line.markedSpans; - if (sps) { for (var sp = (void 0), i = 0; i < sps.length; ++i) { - sp = sps[i]; - if (!sp.marker.collapsed) { continue } - if (sp.from == null) { return true } - if (sp.marker.widgetNode) { continue } - if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp)) - { return true } - } } - } - function lineIsHiddenInner(doc, line, span) { - if (span.to == null) { - var end = span.marker.find(1, true); - return lineIsHiddenInner(doc, end.line, getMarkedSpanFor(end.line.markedSpans, span.marker)) - } - if (span.marker.inclusiveRight && span.to == line.text.length) - { return true } - for (var sp = (void 0), i = 0; i < line.markedSpans.length; ++i) { - sp = line.markedSpans[i]; - if (sp.marker.collapsed && !sp.marker.widgetNode && sp.from == span.to && - (sp.to == null || sp.to != span.from) && - (sp.marker.inclusiveLeft || span.marker.inclusiveRight) && - lineIsHiddenInner(doc, line, sp)) { return true } - } - } - - // Find the height above the given line. - function heightAtLine(lineObj) { - lineObj = visualLine(lineObj); - - var h = 0, chunk = lineObj.parent; - for (var i = 0; i < chunk.lines.length; ++i) { - var line = chunk.lines[i]; - if (line == lineObj) { break } - else { h += line.height; } - } - for (var p = chunk.parent; p; chunk = p, p = chunk.parent) { - for (var i$1 = 0; i$1 < p.children.length; ++i$1) { - var cur = p.children[i$1]; - if (cur == chunk) { break } - else { h += cur.height; } - } - } - return h - } - - // Compute the character length of a line, taking into account - // collapsed ranges (see markText) that might hide parts, and join - // other lines onto it. - function lineLength(line) { - if (line.height == 0) { return 0 } - var len = line.text.length, merged, cur = line; - while (merged = collapsedSpanAtStart(cur)) { - var found = merged.find(0, true); - cur = found.from.line; - len += found.from.ch - found.to.ch; - } - cur = line; - while (merged = collapsedSpanAtEnd(cur)) { - var found$1 = merged.find(0, true); - len -= cur.text.length - found$1.from.ch; - cur = found$1.to.line; - len += cur.text.length - found$1.to.ch; - } - return len - } - - // Find the longest line in the document. - function findMaxLine(cm) { - var d = cm.display, doc = cm.doc; - d.maxLine = getLine(doc, doc.first); - d.maxLineLength = lineLength(d.maxLine); - d.maxLineChanged = true; - doc.iter(function (line) { - var len = lineLength(line); - if (len > d.maxLineLength) { - d.maxLineLength = len; - d.maxLine = line; - } - }); - } - - // LINE DATA STRUCTURE - - // Line objects. These hold state related to a line, including - // highlighting info (the styles array). - var Line = function(text, markedSpans, estimateHeight) { - this.text = text; - attachMarkedSpans(this, markedSpans); - this.height = estimateHeight ? estimateHeight(this) : 1; - }; - - Line.prototype.lineNo = function () { return lineNo(this) }; - eventMixin(Line); - - // Change the content (text, markers) of a line. Automatically - // invalidates cached information and tries to re-estimate the - // line's height. - function updateLine(line, text, markedSpans, estimateHeight) { - line.text = text; - if (line.stateAfter) { line.stateAfter = null; } - if (line.styles) { line.styles = null; } - if (line.order != null) { line.order = null; } - detachMarkedSpans(line); - attachMarkedSpans(line, markedSpans); - var estHeight = estimateHeight ? estimateHeight(line) : 1; - if (estHeight != line.height) { updateLineHeight(line, estHeight); } - } - - // Detach a line from the document tree and its markers. - function cleanUpLine(line) { - line.parent = null; - detachMarkedSpans(line); - } - - // Convert a style as returned by a mode (either null, or a string - // containing one or more styles) to a CSS style. This is cached, - // and also looks for line-wide styles. - var styleToClassCache = {}, styleToClassCacheWithMode = {}; - function interpretTokenStyle(style, options) { - if (!style || /^\s*$/.test(style)) { return null } - var cache = options.addModeClass ? styleToClassCacheWithMode : styleToClassCache; - return cache[style] || - (cache[style] = style.replace(/\S+/g, "cm-$&")) - } - - // Render the DOM representation of the text of a line. Also builds - // up a 'line map', which points at the DOM nodes that represent - // specific stretches of text, and is used by the measuring code. - // The returned object contains the DOM node, this map, and - // information about line-wide styles that were set by the mode. - function buildLineContent(cm, lineView) { - // The padding-right forces the element to have a 'border', which - // is needed on Webkit to be able to get line-level bounding - // rectangles for it (in measureChar). - var content = eltP("span", null, null, webkit ? "padding-right: .1px" : null); - var builder = {pre: eltP("pre", [content], "CodeMirror-line"), content: content, - col: 0, pos: 0, cm: cm, - trailingSpace: false, - splitSpaces: cm.getOption("lineWrapping")}; - lineView.measure = {}; - - // Iterate over the logical lines that make up this visual line. - for (var i = 0; i <= (lineView.rest ? lineView.rest.length : 0); i++) { - var line = i ? lineView.rest[i - 1] : lineView.line, order = (void 0); - builder.pos = 0; - builder.addToken = buildToken; - // Optionally wire in some hacks into the token-rendering - // algorithm, to deal with browser quirks. - if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line, cm.doc.direction))) - { builder.addToken = buildTokenBadBidi(builder.addToken, order); } - builder.map = []; - var allowFrontierUpdate = lineView != cm.display.externalMeasured && lineNo(line); - insertLineContent(line, builder, getLineStyles(cm, line, allowFrontierUpdate)); - if (line.styleClasses) { - if (line.styleClasses.bgClass) - { builder.bgClass = joinClasses(line.styleClasses.bgClass, builder.bgClass || ""); } - if (line.styleClasses.textClass) - { builder.textClass = joinClasses(line.styleClasses.textClass, builder.textClass || ""); } - } - - // Ensure at least a single node is present, for measuring. - if (builder.map.length == 0) - { builder.map.push(0, 0, builder.content.appendChild(zeroWidthElement(cm.display.measure))); } - - // Store the map and a cache object for the current logical line - if (i == 0) { - lineView.measure.map = builder.map; - lineView.measure.cache = {}; - } else { - (lineView.measure.maps || (lineView.measure.maps = [])).push(builder.map) - ;(lineView.measure.caches || (lineView.measure.caches = [])).push({}); - } - } - - // See issue #2901 - if (webkit) { - var last = builder.content.lastChild; - if (/\bcm-tab\b/.test(last.className) || (last.querySelector && last.querySelector(".cm-tab"))) - { builder.content.className = "cm-tab-wrap-hack"; } - } - - signal(cm, "renderLine", cm, lineView.line, builder.pre); - if (builder.pre.className) - { builder.textClass = joinClasses(builder.pre.className, builder.textClass || ""); } - - return builder - } - - function defaultSpecialCharPlaceholder(ch) { - var token = elt("span", "\u2022", "cm-invalidchar"); - token.title = "\\u" + ch.charCodeAt(0).toString(16); - token.setAttribute("aria-label", token.title); - return token - } - - // Build up the DOM representation for a single token, and add it to - // the line map. Takes care to render special characters separately. - function buildToken(builder, text, style, startStyle, endStyle, css, attributes) { - if (!text) { return } - var displayText = builder.splitSpaces ? splitSpaces(text, builder.trailingSpace) : text; - var special = builder.cm.state.specialChars, mustWrap = false; - var content; - if (!special.test(text)) { - builder.col += text.length; - content = document.createTextNode(displayText); - builder.map.push(builder.pos, builder.pos + text.length, content); - if (ie && ie_version < 9) { mustWrap = true; } - builder.pos += text.length; - } else { - content = document.createDocumentFragment(); - var pos = 0; - while (true) { - special.lastIndex = pos; - var m = special.exec(text); - var skipped = m ? m.index - pos : text.length - pos; - if (skipped) { - var txt = document.createTextNode(displayText.slice(pos, pos + skipped)); - if (ie && ie_version < 9) { content.appendChild(elt("span", [txt])); } - else { content.appendChild(txt); } - builder.map.push(builder.pos, builder.pos + skipped, txt); - builder.col += skipped; - builder.pos += skipped; - } - if (!m) { break } - pos += skipped + 1; - var txt$1 = (void 0); - if (m[0] == "\t") { - var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize; - txt$1 = content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab")); - txt$1.setAttribute("role", "presentation"); - txt$1.setAttribute("cm-text", "\t"); - builder.col += tabWidth; - } else if (m[0] == "\r" || m[0] == "\n") { - txt$1 = content.appendChild(elt("span", m[0] == "\r" ? "\u240d" : "\u2424", "cm-invalidchar")); - txt$1.setAttribute("cm-text", m[0]); - builder.col += 1; - } else { - txt$1 = builder.cm.options.specialCharPlaceholder(m[0]); - txt$1.setAttribute("cm-text", m[0]); - if (ie && ie_version < 9) { content.appendChild(elt("span", [txt$1])); } - else { content.appendChild(txt$1); } - builder.col += 1; - } - builder.map.push(builder.pos, builder.pos + 1, txt$1); - builder.pos++; - } - } - builder.trailingSpace = displayText.charCodeAt(text.length - 1) == 32; - if (style || startStyle || endStyle || mustWrap || css) { - var fullStyle = style || ""; - if (startStyle) { fullStyle += startStyle; } - if (endStyle) { fullStyle += endStyle; } - var token = elt("span", [content], fullStyle, css); - if (attributes) { - for (var attr in attributes) { if (attributes.hasOwnProperty(attr) && attr != "style" && attr != "class") - { token.setAttribute(attr, attributes[attr]); } } - } - return builder.content.appendChild(token) - } - builder.content.appendChild(content); - } - - // Change some spaces to NBSP to prevent the browser from collapsing - // trailing spaces at the end of a line when rendering text (issue #1362). - function splitSpaces(text, trailingBefore) { - if (text.length > 1 && !/ /.test(text)) { return text } - var spaceBefore = trailingBefore, result = ""; - for (var i = 0; i < text.length; i++) { - var ch = text.charAt(i); - if (ch == " " && spaceBefore && (i == text.length - 1 || text.charCodeAt(i + 1) == 32)) - { ch = "\u00a0"; } - result += ch; - spaceBefore = ch == " "; - } - return result - } - - // Work around nonsense dimensions being reported for stretches of - // right-to-left text. - function buildTokenBadBidi(inner, order) { - return function (builder, text, style, startStyle, endStyle, css, attributes) { - style = style ? style + " cm-force-border" : "cm-force-border"; - var start = builder.pos, end = start + text.length; - for (;;) { - // Find the part that overlaps with the start of this text - var part = (void 0); - for (var i = 0; i < order.length; i++) { - part = order[i]; - if (part.to > start && part.from <= start) { break } - } - if (part.to >= end) { return inner(builder, text, style, startStyle, endStyle, css, attributes) } - inner(builder, text.slice(0, part.to - start), style, startStyle, null, css, attributes); - startStyle = null; - text = text.slice(part.to - start); - start = part.to; - } - } - } - - function buildCollapsedSpan(builder, size, marker, ignoreWidget) { - var widget = !ignoreWidget && marker.widgetNode; - if (widget) { builder.map.push(builder.pos, builder.pos + size, widget); } - if (!ignoreWidget && builder.cm.display.input.needsContentAttribute) { - if (!widget) - { widget = builder.content.appendChild(document.createElement("span")); } - widget.setAttribute("cm-marker", marker.id); - } - if (widget) { - builder.cm.display.input.setUneditable(widget); - builder.content.appendChild(widget); - } - builder.pos += size; - builder.trailingSpace = false; - } - - // Outputs a number of spans to make up a line, taking highlighting - // and marked text into account. - function insertLineContent(line, builder, styles) { - var spans = line.markedSpans, allText = line.text, at = 0; - if (!spans) { - for (var i$1 = 1; i$1 < styles.length; i$1+=2) - { builder.addToken(builder, allText.slice(at, at = styles[i$1]), interpretTokenStyle(styles[i$1+1], builder.cm.options)); } - return - } - - var len = allText.length, pos = 0, i = 1, text = "", style, css; - var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, collapsed, attributes; - for (;;) { - if (nextChange == pos) { // Update current marker set - spanStyle = spanEndStyle = spanStartStyle = css = ""; - attributes = null; - collapsed = null; nextChange = Infinity; - var foundBookmarks = [], endStyles = (void 0); - for (var j = 0; j < spans.length; ++j) { - var sp = spans[j], m = sp.marker; - if (m.type == "bookmark" && sp.from == pos && m.widgetNode) { - foundBookmarks.push(m); - } else if (sp.from <= pos && (sp.to == null || sp.to > pos || m.collapsed && sp.to == pos && sp.from == pos)) { - if (sp.to != null && sp.to != pos && nextChange > sp.to) { - nextChange = sp.to; - spanEndStyle = ""; - } - if (m.className) { spanStyle += " " + m.className; } - if (m.css) { css = (css ? css + ";" : "") + m.css; } - if (m.startStyle && sp.from == pos) { spanStartStyle += " " + m.startStyle; } - if (m.endStyle && sp.to == nextChange) { (endStyles || (endStyles = [])).push(m.endStyle, sp.to); } - // support for the old title property - // https://github.com/codemirror/CodeMirror/pull/5673 - if (m.title) { (attributes || (attributes = {})).title = m.title; } - if (m.attributes) { - for (var attr in m.attributes) - { (attributes || (attributes = {}))[attr] = m.attributes[attr]; } - } - if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0)) - { collapsed = sp; } - } else if (sp.from > pos && nextChange > sp.from) { - nextChange = sp.from; - } - } - if (endStyles) { for (var j$1 = 0; j$1 < endStyles.length; j$1 += 2) - { if (endStyles[j$1 + 1] == nextChange) { spanEndStyle += " " + endStyles[j$1]; } } } - - if (!collapsed || collapsed.from == pos) { for (var j$2 = 0; j$2 < foundBookmarks.length; ++j$2) - { buildCollapsedSpan(builder, 0, foundBookmarks[j$2]); } } - if (collapsed && (collapsed.from || 0) == pos) { - buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos, - collapsed.marker, collapsed.from == null); - if (collapsed.to == null) { return } - if (collapsed.to == pos) { collapsed = false; } - } - } - if (pos >= len) { break } - - var upto = Math.min(len, nextChange); - while (true) { - if (text) { - var end = pos + text.length; - if (!collapsed) { - var tokenText = end > upto ? text.slice(0, upto - pos) : text; - builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle, - spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", css, attributes); - } - if (end >= upto) {text = text.slice(upto - pos); pos = upto; break} - pos = end; - spanStartStyle = ""; - } - text = allText.slice(at, at = styles[i++]); - style = interpretTokenStyle(styles[i++], builder.cm.options); - } - } - } - - - // These objects are used to represent the visible (currently drawn) - // part of the document. A LineView may correspond to multiple - // logical lines, if those are connected by collapsed ranges. - function LineView(doc, line, lineN) { - // The starting line - this.line = line; - // Continuing lines, if any - this.rest = visualLineContinued(line); - // Number of logical lines in this visual line - this.size = this.rest ? lineNo(lst(this.rest)) - lineN + 1 : 1; - this.node = this.text = null; - this.hidden = lineIsHidden(doc, line); - } - - // Create a range of LineView objects for the given lines. - function buildViewArray(cm, from, to) { - var array = [], nextPos; - for (var pos = from; pos < to; pos = nextPos) { - var view = new LineView(cm.doc, getLine(cm.doc, pos), pos); - nextPos = pos + view.size; - array.push(view); - } - return array - } - - var operationGroup = null; - - function pushOperation(op) { - if (operationGroup) { - operationGroup.ops.push(op); - } else { - op.ownsGroup = operationGroup = { - ops: [op], - delayedCallbacks: [] - }; - } - } - - function fireCallbacksForOps(group) { - // Calls delayed callbacks and cursorActivity handlers until no - // new ones appear - var callbacks = group.delayedCallbacks, i = 0; - do { - for (; i < callbacks.length; i++) - { callbacks[i].call(null); } - for (var j = 0; j < group.ops.length; j++) { - var op = group.ops[j]; - if (op.cursorActivityHandlers) - { while (op.cursorActivityCalled < op.cursorActivityHandlers.length) - { op.cursorActivityHandlers[op.cursorActivityCalled++].call(null, op.cm); } } - } - } while (i < callbacks.length) - } - - function finishOperation(op, endCb) { - var group = op.ownsGroup; - if (!group) { return } - - try { fireCallbacksForOps(group); } - finally { - operationGroup = null; - endCb(group); - } - } - - var orphanDelayedCallbacks = null; - - // Often, we want to signal events at a point where we are in the - // middle of some work, but don't want the handler to start calling - // other methods on the editor, which might be in an inconsistent - // state or simply not expect any other events to happen. - // signalLater looks whether there are any handlers, and schedules - // them to be executed when the last operation ends, or, if no - // operation is active, when a timeout fires. - function signalLater(emitter, type /*, values...*/) { - var arr = getHandlers(emitter, type); - if (!arr.length) { return } - var args = Array.prototype.slice.call(arguments, 2), list; - if (operationGroup) { - list = operationGroup.delayedCallbacks; - } else if (orphanDelayedCallbacks) { - list = orphanDelayedCallbacks; - } else { - list = orphanDelayedCallbacks = []; - setTimeout(fireOrphanDelayed, 0); - } - var loop = function ( i ) { - list.push(function () { return arr[i].apply(null, args); }); - }; - - for (var i = 0; i < arr.length; ++i) - loop( i ); - } - - function fireOrphanDelayed() { - var delayed = orphanDelayedCallbacks; - orphanDelayedCallbacks = null; - for (var i = 0; i < delayed.length; ++i) { delayed[i](); } - } - - // When an aspect of a line changes, a string is added to - // lineView.changes. This updates the relevant part of the line's - // DOM structure. - function updateLineForChanges(cm, lineView, lineN, dims) { - for (var j = 0; j < lineView.changes.length; j++) { - var type = lineView.changes[j]; - if (type == "text") { updateLineText(cm, lineView); } - else if (type == "gutter") { updateLineGutter(cm, lineView, lineN, dims); } - else if (type == "class") { updateLineClasses(cm, lineView); } - else if (type == "widget") { updateLineWidgets(cm, lineView, dims); } - } - lineView.changes = null; - } - - // Lines with gutter elements, widgets or a background class need to - // be wrapped, and have the extra elements added to the wrapper div - function ensureLineWrapped(lineView) { - if (lineView.node == lineView.text) { - lineView.node = elt("div", null, null, "position: relative"); - if (lineView.text.parentNode) - { lineView.text.parentNode.replaceChild(lineView.node, lineView.text); } - lineView.node.appendChild(lineView.text); - if (ie && ie_version < 8) { lineView.node.style.zIndex = 2; } - } - return lineView.node - } - - function updateLineBackground(cm, lineView) { - var cls = lineView.bgClass ? lineView.bgClass + " " + (lineView.line.bgClass || "") : lineView.line.bgClass; - if (cls) { cls += " CodeMirror-linebackground"; } - if (lineView.background) { - if (cls) { lineView.background.className = cls; } - else { lineView.background.parentNode.removeChild(lineView.background); lineView.background = null; } - } else if (cls) { - var wrap = ensureLineWrapped(lineView); - lineView.background = wrap.insertBefore(elt("div", null, cls), wrap.firstChild); - cm.display.input.setUneditable(lineView.background); - } - } - - // Wrapper around buildLineContent which will reuse the structure - // in display.externalMeasured when possible. - function getLineContent(cm, lineView) { - var ext = cm.display.externalMeasured; - if (ext && ext.line == lineView.line) { - cm.display.externalMeasured = null; - lineView.measure = ext.measure; - return ext.built - } - return buildLineContent(cm, lineView) - } - - // Redraw the line's text. Interacts with the background and text - // classes because the mode may output tokens that influence these - // classes. - function updateLineText(cm, lineView) { - var cls = lineView.text.className; - var built = getLineContent(cm, lineView); - if (lineView.text == lineView.node) { lineView.node = built.pre; } - lineView.text.parentNode.replaceChild(built.pre, lineView.text); - lineView.text = built.pre; - if (built.bgClass != lineView.bgClass || built.textClass != lineView.textClass) { - lineView.bgClass = built.bgClass; - lineView.textClass = built.textClass; - updateLineClasses(cm, lineView); - } else if (cls) { - lineView.text.className = cls; - } - } - - function updateLineClasses(cm, lineView) { - updateLineBackground(cm, lineView); - if (lineView.line.wrapClass) - { ensureLineWrapped(lineView).className = lineView.line.wrapClass; } - else if (lineView.node != lineView.text) - { lineView.node.className = ""; } - var textClass = lineView.textClass ? lineView.textClass + " " + (lineView.line.textClass || "") : lineView.line.textClass; - lineView.text.className = textClass || ""; - } - - function updateLineGutter(cm, lineView, lineN, dims) { - if (lineView.gutter) { - lineView.node.removeChild(lineView.gutter); - lineView.gutter = null; - } - if (lineView.gutterBackground) { - lineView.node.removeChild(lineView.gutterBackground); - lineView.gutterBackground = null; - } - if (lineView.line.gutterClass) { - var wrap = ensureLineWrapped(lineView); - lineView.gutterBackground = elt("div", null, "CodeMirror-gutter-background " + lineView.line.gutterClass, - ("left: " + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px; width: " + (dims.gutterTotalWidth) + "px")); - cm.display.input.setUneditable(lineView.gutterBackground); - wrap.insertBefore(lineView.gutterBackground, lineView.text); - } - var markers = lineView.line.gutterMarkers; - if (cm.options.lineNumbers || markers) { - var wrap$1 = ensureLineWrapped(lineView); - var gutterWrap = lineView.gutter = elt("div", null, "CodeMirror-gutter-wrapper", ("left: " + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px")); - cm.display.input.setUneditable(gutterWrap); - wrap$1.insertBefore(gutterWrap, lineView.text); - if (lineView.line.gutterClass) - { gutterWrap.className += " " + lineView.line.gutterClass; } - if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"])) - { lineView.lineNumber = gutterWrap.appendChild( - elt("div", lineNumberFor(cm.options, lineN), - "CodeMirror-linenumber CodeMirror-gutter-elt", - ("left: " + (dims.gutterLeft["CodeMirror-linenumbers"]) + "px; width: " + (cm.display.lineNumInnerWidth) + "px"))); } - if (markers) { for (var k = 0; k < cm.display.gutterSpecs.length; ++k) { - var id = cm.display.gutterSpecs[k].className, found = markers.hasOwnProperty(id) && markers[id]; - if (found) - { gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", - ("left: " + (dims.gutterLeft[id]) + "px; width: " + (dims.gutterWidth[id]) + "px"))); } - } } - } - } - - function updateLineWidgets(cm, lineView, dims) { - if (lineView.alignable) { lineView.alignable = null; } - var isWidget = classTest("CodeMirror-linewidget"); - for (var node = lineView.node.firstChild, next = (void 0); node; node = next) { - next = node.nextSibling; - if (isWidget.test(node.className)) { lineView.node.removeChild(node); } - } - insertLineWidgets(cm, lineView, dims); - } - - // Build a line's DOM representation from scratch - function buildLineElement(cm, lineView, lineN, dims) { - var built = getLineContent(cm, lineView); - lineView.text = lineView.node = built.pre; - if (built.bgClass) { lineView.bgClass = built.bgClass; } - if (built.textClass) { lineView.textClass = built.textClass; } - - updateLineClasses(cm, lineView); - updateLineGutter(cm, lineView, lineN, dims); - insertLineWidgets(cm, lineView, dims); - return lineView.node - } - - // A lineView may contain multiple logical lines (when merged by - // collapsed spans). The widgets for all of them need to be drawn. - function insertLineWidgets(cm, lineView, dims) { - insertLineWidgetsFor(cm, lineView.line, lineView, dims, true); - if (lineView.rest) { for (var i = 0; i < lineView.rest.length; i++) - { insertLineWidgetsFor(cm, lineView.rest[i], lineView, dims, false); } } - } - - function insertLineWidgetsFor(cm, line, lineView, dims, allowAbove) { - if (!line.widgets) { return } - var wrap = ensureLineWrapped(lineView); - for (var i = 0, ws = line.widgets; i < ws.length; ++i) { - var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget" + (widget.className ? " " + widget.className : "")); - if (!widget.handleMouseEvents) { node.setAttribute("cm-ignore-events", "true"); } - positionLineWidget(widget, node, lineView, dims); - cm.display.input.setUneditable(node); - if (allowAbove && widget.above) - { wrap.insertBefore(node, lineView.gutter || lineView.text); } - else - { wrap.appendChild(node); } - signalLater(widget, "redraw"); - } - } - - function positionLineWidget(widget, node, lineView, dims) { - if (widget.noHScroll) { - (lineView.alignable || (lineView.alignable = [])).push(node); - var width = dims.wrapperWidth; - node.style.left = dims.fixedPos + "px"; - if (!widget.coverGutter) { - width -= dims.gutterTotalWidth; - node.style.paddingLeft = dims.gutterTotalWidth + "px"; - } - node.style.width = width + "px"; - } - if (widget.coverGutter) { - node.style.zIndex = 5; - node.style.position = "relative"; - if (!widget.noHScroll) { node.style.marginLeft = -dims.gutterTotalWidth + "px"; } - } - } - - function widgetHeight(widget) { - if (widget.height != null) { return widget.height } - var cm = widget.doc.cm; - if (!cm) { return 0 } - if (!contains(document.body, widget.node)) { - var parentStyle = "position: relative;"; - if (widget.coverGutter) - { parentStyle += "margin-left: -" + cm.display.gutters.offsetWidth + "px;"; } - if (widget.noHScroll) - { parentStyle += "width: " + cm.display.wrapper.clientWidth + "px;"; } - removeChildrenAndAdd(cm.display.measure, elt("div", [widget.node], null, parentStyle)); - } - return widget.height = widget.node.parentNode.offsetHeight - } - - // Return true when the given mouse event happened in a widget - function eventInWidget(display, e) { - for (var n = e_target(e); n != display.wrapper; n = n.parentNode) { - if (!n || (n.nodeType == 1 && n.getAttribute("cm-ignore-events") == "true") || - (n.parentNode == display.sizer && n != display.mover)) - { return true } - } - } - - // POSITION MEASUREMENT - - function paddingTop(display) {return display.lineSpace.offsetTop} - function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight} - function paddingH(display) { - if (display.cachedPaddingH) { return display.cachedPaddingH } - var e = removeChildrenAndAdd(display.measure, elt("pre", "x", "CodeMirror-line-like")); - var style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle; - var data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)}; - if (!isNaN(data.left) && !isNaN(data.right)) { display.cachedPaddingH = data; } - return data - } - - function scrollGap(cm) { return scrollerGap - cm.display.nativeBarWidth } - function displayWidth(cm) { - return cm.display.scroller.clientWidth - scrollGap(cm) - cm.display.barWidth - } - function displayHeight(cm) { - return cm.display.scroller.clientHeight - scrollGap(cm) - cm.display.barHeight - } - - // Ensure the lineView.wrapping.heights array is populated. This is - // an array of bottom offsets for the lines that make up a drawn - // line. When lineWrapping is on, there might be more than one - // height. - function ensureLineHeights(cm, lineView, rect) { - var wrapping = cm.options.lineWrapping; - var curWidth = wrapping && displayWidth(cm); - if (!lineView.measure.heights || wrapping && lineView.measure.width != curWidth) { - var heights = lineView.measure.heights = []; - if (wrapping) { - lineView.measure.width = curWidth; - var rects = lineView.text.firstChild.getClientRects(); - for (var i = 0; i < rects.length - 1; i++) { - var cur = rects[i], next = rects[i + 1]; - if (Math.abs(cur.bottom - next.bottom) > 2) - { heights.push((cur.bottom + next.top) / 2 - rect.top); } - } - } - heights.push(rect.bottom - rect.top); - } - } - - // Find a line map (mapping character offsets to text nodes) and a - // measurement cache for the given line number. (A line view might - // contain multiple lines when collapsed ranges are present.) - function mapFromLineView(lineView, line, lineN) { - if (lineView.line == line) - { return {map: lineView.measure.map, cache: lineView.measure.cache} } - for (var i = 0; i < lineView.rest.length; i++) - { if (lineView.rest[i] == line) - { return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]} } } - for (var i$1 = 0; i$1 < lineView.rest.length; i$1++) - { if (lineNo(lineView.rest[i$1]) > lineN) - { return {map: lineView.measure.maps[i$1], cache: lineView.measure.caches[i$1], before: true} } } - } - - // Render a line into the hidden node display.externalMeasured. Used - // when measurement is needed for a line that's not in the viewport. - function updateExternalMeasurement(cm, line) { - line = visualLine(line); - var lineN = lineNo(line); - var view = cm.display.externalMeasured = new LineView(cm.doc, line, lineN); - view.lineN = lineN; - var built = view.built = buildLineContent(cm, view); - view.text = built.pre; - removeChildrenAndAdd(cm.display.lineMeasure, built.pre); - return view - } - - // Get a {top, bottom, left, right} box (in line-local coordinates) - // for a given character. - function measureChar(cm, line, ch, bias) { - return measureCharPrepared(cm, prepareMeasureForLine(cm, line), ch, bias) - } - - // Find a line view that corresponds to the given line number. - function findViewForLine(cm, lineN) { - if (lineN >= cm.display.viewFrom && lineN < cm.display.viewTo) - { return cm.display.view[findViewIndex(cm, lineN)] } - var ext = cm.display.externalMeasured; - if (ext && lineN >= ext.lineN && lineN < ext.lineN + ext.size) - { return ext } - } - - // Measurement can be split in two steps, the set-up work that - // applies to the whole line, and the measurement of the actual - // character. Functions like coordsChar, that need to do a lot of - // measurements in a row, can thus ensure that the set-up work is - // only done once. - function prepareMeasureForLine(cm, line) { - var lineN = lineNo(line); - var view = findViewForLine(cm, lineN); - if (view && !view.text) { - view = null; - } else if (view && view.changes) { - updateLineForChanges(cm, view, lineN, getDimensions(cm)); - cm.curOp.forceUpdate = true; - } - if (!view) - { view = updateExternalMeasurement(cm, line); } - - var info = mapFromLineView(view, line, lineN); - return { - line: line, view: view, rect: null, - map: info.map, cache: info.cache, before: info.before, - hasHeights: false - } - } - - // Given a prepared measurement object, measures the position of an - // actual character (or fetches it from the cache). - function measureCharPrepared(cm, prepared, ch, bias, varHeight) { - if (prepared.before) { ch = -1; } - var key = ch + (bias || ""), found; - if (prepared.cache.hasOwnProperty(key)) { - found = prepared.cache[key]; - } else { - if (!prepared.rect) - { prepared.rect = prepared.view.text.getBoundingClientRect(); } - if (!prepared.hasHeights) { - ensureLineHeights(cm, prepared.view, prepared.rect); - prepared.hasHeights = true; - } - found = measureCharInner(cm, prepared, ch, bias); - if (!found.bogus) { prepared.cache[key] = found; } - } - return {left: found.left, right: found.right, - top: varHeight ? found.rtop : found.top, - bottom: varHeight ? found.rbottom : found.bottom} - } - - var nullRect = {left: 0, right: 0, top: 0, bottom: 0}; - - function nodeAndOffsetInLineMap(map, ch, bias) { - var node, start, end, collapse, mStart, mEnd; - // First, search the line map for the text node corresponding to, - // or closest to, the target character. - for (var i = 0; i < map.length; i += 3) { - mStart = map[i]; - mEnd = map[i + 1]; - if (ch < mStart) { - start = 0; end = 1; - collapse = "left"; - } else if (ch < mEnd) { - start = ch - mStart; - end = start + 1; - } else if (i == map.length - 3 || ch == mEnd && map[i + 3] > ch) { - end = mEnd - mStart; - start = end - 1; - if (ch >= mEnd) { collapse = "right"; } - } - if (start != null) { - node = map[i + 2]; - if (mStart == mEnd && bias == (node.insertLeft ? "left" : "right")) - { collapse = bias; } - if (bias == "left" && start == 0) - { while (i && map[i - 2] == map[i - 3] && map[i - 1].insertLeft) { - node = map[(i -= 3) + 2]; - collapse = "left"; - } } - if (bias == "right" && start == mEnd - mStart) - { while (i < map.length - 3 && map[i + 3] == map[i + 4] && !map[i + 5].insertLeft) { - node = map[(i += 3) + 2]; - collapse = "right"; - } } - break - } - } - return {node: node, start: start, end: end, collapse: collapse, coverStart: mStart, coverEnd: mEnd} - } - - function getUsefulRect(rects, bias) { - var rect = nullRect; - if (bias == "left") { for (var i = 0; i < rects.length; i++) { - if ((rect = rects[i]).left != rect.right) { break } - } } else { for (var i$1 = rects.length - 1; i$1 >= 0; i$1--) { - if ((rect = rects[i$1]).left != rect.right) { break } - } } - return rect - } - - function measureCharInner(cm, prepared, ch, bias) { - var place = nodeAndOffsetInLineMap(prepared.map, ch, bias); - var node = place.node, start = place.start, end = place.end, collapse = place.collapse; - - var rect; - if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates. - for (var i$1 = 0; i$1 < 4; i$1++) { // Retry a maximum of 4 times when nonsense rectangles are returned - while (start && isExtendingChar(prepared.line.text.charAt(place.coverStart + start))) { --start; } - while (place.coverStart + end < place.coverEnd && isExtendingChar(prepared.line.text.charAt(place.coverStart + end))) { ++end; } - if (ie && ie_version < 9 && start == 0 && end == place.coverEnd - place.coverStart) - { rect = node.parentNode.getBoundingClientRect(); } - else - { rect = getUsefulRect(range(node, start, end).getClientRects(), bias); } - if (rect.left || rect.right || start == 0) { break } - end = start; - start = start - 1; - collapse = "right"; - } - if (ie && ie_version < 11) { rect = maybeUpdateRectForZooming(cm.display.measure, rect); } - } else { // If it is a widget, simply get the box for the whole widget. - if (start > 0) { collapse = bias = "right"; } - var rects; - if (cm.options.lineWrapping && (rects = node.getClientRects()).length > 1) - { rect = rects[bias == "right" ? rects.length - 1 : 0]; } - else - { rect = node.getBoundingClientRect(); } - } - if (ie && ie_version < 9 && !start && (!rect || !rect.left && !rect.right)) { - var rSpan = node.parentNode.getClientRects()[0]; - if (rSpan) - { rect = {left: rSpan.left, right: rSpan.left + charWidth(cm.display), top: rSpan.top, bottom: rSpan.bottom}; } - else - { rect = nullRect; } - } - - var rtop = rect.top - prepared.rect.top, rbot = rect.bottom - prepared.rect.top; - var mid = (rtop + rbot) / 2; - var heights = prepared.view.measure.heights; - var i = 0; - for (; i < heights.length - 1; i++) - { if (mid < heights[i]) { break } } - var top = i ? heights[i - 1] : 0, bot = heights[i]; - var result = {left: (collapse == "right" ? rect.right : rect.left) - prepared.rect.left, - right: (collapse == "left" ? rect.left : rect.right) - prepared.rect.left, - top: top, bottom: bot}; - if (!rect.left && !rect.right) { result.bogus = true; } - if (!cm.options.singleCursorHeightPerLine) { result.rtop = rtop; result.rbottom = rbot; } - - return result - } - - // Work around problem with bounding client rects on ranges being - // returned incorrectly when zoomed on IE10 and below. - function maybeUpdateRectForZooming(measure, rect) { - if (!window.screen || screen.logicalXDPI == null || - screen.logicalXDPI == screen.deviceXDPI || !hasBadZoomedRects(measure)) - { return rect } - var scaleX = screen.logicalXDPI / screen.deviceXDPI; - var scaleY = screen.logicalYDPI / screen.deviceYDPI; - return {left: rect.left * scaleX, right: rect.right * scaleX, - top: rect.top * scaleY, bottom: rect.bottom * scaleY} - } - - function clearLineMeasurementCacheFor(lineView) { - if (lineView.measure) { - lineView.measure.cache = {}; - lineView.measure.heights = null; - if (lineView.rest) { for (var i = 0; i < lineView.rest.length; i++) - { lineView.measure.caches[i] = {}; } } - } - } - - function clearLineMeasurementCache(cm) { - cm.display.externalMeasure = null; - removeChildren(cm.display.lineMeasure); - for (var i = 0; i < cm.display.view.length; i++) - { clearLineMeasurementCacheFor(cm.display.view[i]); } - } - - function clearCaches(cm) { - clearLineMeasurementCache(cm); - cm.display.cachedCharWidth = cm.display.cachedTextHeight = cm.display.cachedPaddingH = null; - if (!cm.options.lineWrapping) { cm.display.maxLineChanged = true; } - cm.display.lineNumChars = null; - } - - function pageScrollX() { - // Work around https://bugs.chromium.org/p/chromium/issues/detail?id=489206 - // which causes page_Offset and bounding client rects to use - // different reference viewports and invalidate our calculations. - if (chrome && android) { return -(document.body.getBoundingClientRect().left - parseInt(getComputedStyle(document.body).marginLeft)) } - return window.pageXOffset || (document.documentElement || document.body).scrollLeft - } - function pageScrollY() { - if (chrome && android) { return -(document.body.getBoundingClientRect().top - parseInt(getComputedStyle(document.body).marginTop)) } - return window.pageYOffset || (document.documentElement || document.body).scrollTop - } - - function widgetTopHeight(lineObj) { - var height = 0; - if (lineObj.widgets) { for (var i = 0; i < lineObj.widgets.length; ++i) { if (lineObj.widgets[i].above) - { height += widgetHeight(lineObj.widgets[i]); } } } - return height - } - - // Converts a {top, bottom, left, right} box from line-local - // coordinates into another coordinate system. Context may be one of - // "line", "div" (display.lineDiv), "local"./null (editor), "window", - // or "page". - function intoCoordSystem(cm, lineObj, rect, context, includeWidgets) { - if (!includeWidgets) { - var height = widgetTopHeight(lineObj); - rect.top += height; rect.bottom += height; - } - if (context == "line") { return rect } - if (!context) { context = "local"; } - var yOff = heightAtLine(lineObj); - if (context == "local") { yOff += paddingTop(cm.display); } - else { yOff -= cm.display.viewOffset; } - if (context == "page" || context == "window") { - var lOff = cm.display.lineSpace.getBoundingClientRect(); - yOff += lOff.top + (context == "window" ? 0 : pageScrollY()); - var xOff = lOff.left + (context == "window" ? 0 : pageScrollX()); - rect.left += xOff; rect.right += xOff; - } - rect.top += yOff; rect.bottom += yOff; - return rect - } - - // Coverts a box from "div" coords to another coordinate system. - // Context may be "window", "page", "div", or "local"./null. - function fromCoordSystem(cm, coords, context) { - if (context == "div") { return coords } - var left = coords.left, top = coords.top; - // First move into "page" coordinate system - if (context == "page") { - left -= pageScrollX(); - top -= pageScrollY(); - } else if (context == "local" || !context) { - var localBox = cm.display.sizer.getBoundingClientRect(); - left += localBox.left; - top += localBox.top; - } - - var lineSpaceBox = cm.display.lineSpace.getBoundingClientRect(); - return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top} - } - - function charCoords(cm, pos, context, lineObj, bias) { - if (!lineObj) { lineObj = getLine(cm.doc, pos.line); } - return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, bias), context) - } - - // Returns a box for a given cursor position, which may have an - // 'other' property containing the position of the secondary cursor - // on a bidi boundary. - // A cursor Pos(line, char, "before") is on the same visual line as `char - 1` - // and after `char - 1` in writing order of `char - 1` - // A cursor Pos(line, char, "after") is on the same visual line as `char` - // and before `char` in writing order of `char` - // Examples (upper-case letters are RTL, lower-case are LTR): - // Pos(0, 1, ...) - // before after - // ab a|b a|b - // aB a|B aB| - // Ab |Ab A|b - // AB B|A B|A - // Every position after the last character on a line is considered to stick - // to the last character on the line. - function cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeight) { - lineObj = lineObj || getLine(cm.doc, pos.line); - if (!preparedMeasure) { preparedMeasure = prepareMeasureForLine(cm, lineObj); } - function get(ch, right) { - var m = measureCharPrepared(cm, preparedMeasure, ch, right ? "right" : "left", varHeight); - if (right) { m.left = m.right; } else { m.right = m.left; } - return intoCoordSystem(cm, lineObj, m, context) - } - var order = getOrder(lineObj, cm.doc.direction), ch = pos.ch, sticky = pos.sticky; - if (ch >= lineObj.text.length) { - ch = lineObj.text.length; - sticky = "before"; - } else if (ch <= 0) { - ch = 0; - sticky = "after"; - } - if (!order) { return get(sticky == "before" ? ch - 1 : ch, sticky == "before") } - - function getBidi(ch, partPos, invert) { - var part = order[partPos], right = part.level == 1; - return get(invert ? ch - 1 : ch, right != invert) - } - var partPos = getBidiPartAt(order, ch, sticky); - var other = bidiOther; - var val = getBidi(ch, partPos, sticky == "before"); - if (other != null) { val.other = getBidi(ch, other, sticky != "before"); } - return val - } - - // Used to cheaply estimate the coordinates for a position. Used for - // intermediate scroll updates. - function estimateCoords(cm, pos) { - var left = 0; - pos = clipPos(cm.doc, pos); - if (!cm.options.lineWrapping) { left = charWidth(cm.display) * pos.ch; } - var lineObj = getLine(cm.doc, pos.line); - var top = heightAtLine(lineObj) + paddingTop(cm.display); - return {left: left, right: left, top: top, bottom: top + lineObj.height} - } - - // Positions returned by coordsChar contain some extra information. - // xRel is the relative x position of the input coordinates compared - // to the found position (so xRel > 0 means the coordinates are to - // the right of the character position, for example). When outside - // is true, that means the coordinates lie outside the line's - // vertical range. - function PosWithInfo(line, ch, sticky, outside, xRel) { - var pos = Pos(line, ch, sticky); - pos.xRel = xRel; - if (outside) { pos.outside = outside; } - return pos - } - - // Compute the character position closest to the given coordinates. - // Input must be lineSpace-local ("div" coordinate system). - function coordsChar(cm, x, y) { - var doc = cm.doc; - y += cm.display.viewOffset; - if (y < 0) { return PosWithInfo(doc.first, 0, null, -1, -1) } - var lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1; - if (lineN > last) - { return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, null, 1, 1) } - if (x < 0) { x = 0; } - - var lineObj = getLine(doc, lineN); - for (;;) { - var found = coordsCharInner(cm, lineObj, lineN, x, y); - var collapsed = collapsedSpanAround(lineObj, found.ch + (found.xRel > 0 || found.outside > 0 ? 1 : 0)); - if (!collapsed) { return found } - var rangeEnd = collapsed.find(1); - if (rangeEnd.line == lineN) { return rangeEnd } - lineObj = getLine(doc, lineN = rangeEnd.line); - } - } - - function wrappedLineExtent(cm, lineObj, preparedMeasure, y) { - y -= widgetTopHeight(lineObj); - var end = lineObj.text.length; - var begin = findFirst(function (ch) { return measureCharPrepared(cm, preparedMeasure, ch - 1).bottom <= y; }, end, 0); - end = findFirst(function (ch) { return measureCharPrepared(cm, preparedMeasure, ch).top > y; }, begin, end); - return {begin: begin, end: end} - } - - function wrappedLineExtentChar(cm, lineObj, preparedMeasure, target) { - if (!preparedMeasure) { preparedMeasure = prepareMeasureForLine(cm, lineObj); } - var targetTop = intoCoordSystem(cm, lineObj, measureCharPrepared(cm, preparedMeasure, target), "line").top; - return wrappedLineExtent(cm, lineObj, preparedMeasure, targetTop) - } - - // Returns true if the given side of a box is after the given - // coordinates, in top-to-bottom, left-to-right order. - function boxIsAfter(box, x, y, left) { - return box.bottom <= y ? false : box.top > y ? true : (left ? box.left : box.right) > x - } - - function coordsCharInner(cm, lineObj, lineNo, x, y) { - // Move y into line-local coordinate space - y -= heightAtLine(lineObj); - var preparedMeasure = prepareMeasureForLine(cm, lineObj); - // When directly calling `measureCharPrepared`, we have to adjust - // for the widgets at this line. - var widgetHeight = widgetTopHeight(lineObj); - var begin = 0, end = lineObj.text.length, ltr = true; - - var order = getOrder(lineObj, cm.doc.direction); - // If the line isn't plain left-to-right text, first figure out - // which bidi section the coordinates fall into. - if (order) { - var part = (cm.options.lineWrapping ? coordsBidiPartWrapped : coordsBidiPart) - (cm, lineObj, lineNo, preparedMeasure, order, x, y); - ltr = part.level != 1; - // The awkward -1 offsets are needed because findFirst (called - // on these below) will treat its first bound as inclusive, - // second as exclusive, but we want to actually address the - // characters in the part's range - begin = ltr ? part.from : part.to - 1; - end = ltr ? part.to : part.from - 1; - } - - // A binary search to find the first character whose bounding box - // starts after the coordinates. If we run across any whose box wrap - // the coordinates, store that. - var chAround = null, boxAround = null; - var ch = findFirst(function (ch) { - var box = measureCharPrepared(cm, preparedMeasure, ch); - box.top += widgetHeight; box.bottom += widgetHeight; - if (!boxIsAfter(box, x, y, false)) { return false } - if (box.top <= y && box.left <= x) { - chAround = ch; - boxAround = box; - } - return true - }, begin, end); - - var baseX, sticky, outside = false; - // If a box around the coordinates was found, use that - if (boxAround) { - // Distinguish coordinates nearer to the left or right side of the box - var atLeft = x - boxAround.left < boxAround.right - x, atStart = atLeft == ltr; - ch = chAround + (atStart ? 0 : 1); - sticky = atStart ? "after" : "before"; - baseX = atLeft ? boxAround.left : boxAround.right; - } else { - // (Adjust for extended bound, if necessary.) - if (!ltr && (ch == end || ch == begin)) { ch++; } - // To determine which side to associate with, get the box to the - // left of the character and compare it's vertical position to the - // coordinates - sticky = ch == 0 ? "after" : ch == lineObj.text.length ? "before" : - (measureCharPrepared(cm, preparedMeasure, ch - (ltr ? 1 : 0)).bottom + widgetHeight <= y) == ltr ? - "after" : "before"; - // Now get accurate coordinates for this place, in order to get a - // base X position - var coords = cursorCoords(cm, Pos(lineNo, ch, sticky), "line", lineObj, preparedMeasure); - baseX = coords.left; - outside = y < coords.top ? -1 : y >= coords.bottom ? 1 : 0; - } - - ch = skipExtendingChars(lineObj.text, ch, 1); - return PosWithInfo(lineNo, ch, sticky, outside, x - baseX) - } - - function coordsBidiPart(cm, lineObj, lineNo, preparedMeasure, order, x, y) { - // Bidi parts are sorted left-to-right, and in a non-line-wrapping - // situation, we can take this ordering to correspond to the visual - // ordering. This finds the first part whose end is after the given - // coordinates. - var index = findFirst(function (i) { - var part = order[i], ltr = part.level != 1; - return boxIsAfter(cursorCoords(cm, Pos(lineNo, ltr ? part.to : part.from, ltr ? "before" : "after"), - "line", lineObj, preparedMeasure), x, y, true) - }, 0, order.length - 1); - var part = order[index]; - // If this isn't the first part, the part's start is also after - // the coordinates, and the coordinates aren't on the same line as - // that start, move one part back. - if (index > 0) { - var ltr = part.level != 1; - var start = cursorCoords(cm, Pos(lineNo, ltr ? part.from : part.to, ltr ? "after" : "before"), - "line", lineObj, preparedMeasure); - if (boxIsAfter(start, x, y, true) && start.top > y) - { part = order[index - 1]; } - } - return part - } - - function coordsBidiPartWrapped(cm, lineObj, _lineNo, preparedMeasure, order, x, y) { - // In a wrapped line, rtl text on wrapping boundaries can do things - // that don't correspond to the ordering in our `order` array at - // all, so a binary search doesn't work, and we want to return a - // part that only spans one line so that the binary search in - // coordsCharInner is safe. As such, we first find the extent of the - // wrapped line, and then do a flat search in which we discard any - // spans that aren't on the line. - var ref = wrappedLineExtent(cm, lineObj, preparedMeasure, y); - var begin = ref.begin; - var end = ref.end; - if (/\s/.test(lineObj.text.charAt(end - 1))) { end--; } - var part = null, closestDist = null; - for (var i = 0; i < order.length; i++) { - var p = order[i]; - if (p.from >= end || p.to <= begin) { continue } - var ltr = p.level != 1; - var endX = measureCharPrepared(cm, preparedMeasure, ltr ? Math.min(end, p.to) - 1 : Math.max(begin, p.from)).right; - // Weigh against spans ending before this, so that they are only - // picked if nothing ends after - var dist = endX < x ? x - endX + 1e9 : endX - x; - if (!part || closestDist > dist) { - part = p; - closestDist = dist; - } - } - if (!part) { part = order[order.length - 1]; } - // Clip the part to the wrapped line. - if (part.from < begin) { part = {from: begin, to: part.to, level: part.level}; } - if (part.to > end) { part = {from: part.from, to: end, level: part.level}; } - return part - } - - var measureText; - // Compute the default text height. - function textHeight(display) { - if (display.cachedTextHeight != null) { return display.cachedTextHeight } - if (measureText == null) { - measureText = elt("pre", null, "CodeMirror-line-like"); - // Measure a bunch of lines, for browsers that compute - // fractional heights. - for (var i = 0; i < 49; ++i) { - measureText.appendChild(document.createTextNode("x")); - measureText.appendChild(elt("br")); - } - measureText.appendChild(document.createTextNode("x")); - } - removeChildrenAndAdd(display.measure, measureText); - var height = measureText.offsetHeight / 50; - if (height > 3) { display.cachedTextHeight = height; } - removeChildren(display.measure); - return height || 1 - } - - // Compute the default character width. - function charWidth(display) { - if (display.cachedCharWidth != null) { return display.cachedCharWidth } - var anchor = elt("span", "xxxxxxxxxx"); - var pre = elt("pre", [anchor], "CodeMirror-line-like"); - removeChildrenAndAdd(display.measure, pre); - var rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10; - if (width > 2) { display.cachedCharWidth = width; } - return width || 10 - } - - // Do a bulk-read of the DOM positions and sizes needed to draw the - // view, so that we don't interleave reading and writing to the DOM. - function getDimensions(cm) { - var d = cm.display, left = {}, width = {}; - var gutterLeft = d.gutters.clientLeft; - for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) { - var id = cm.display.gutterSpecs[i].className; - left[id] = n.offsetLeft + n.clientLeft + gutterLeft; - width[id] = n.clientWidth; - } - return {fixedPos: compensateForHScroll(d), - gutterTotalWidth: d.gutters.offsetWidth, - gutterLeft: left, - gutterWidth: width, - wrapperWidth: d.wrapper.clientWidth} - } - - // Computes display.scroller.scrollLeft + display.gutters.offsetWidth, - // but using getBoundingClientRect to get a sub-pixel-accurate - // result. - function compensateForHScroll(display) { - return display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left - } - - // Returns a function that estimates the height of a line, to use as - // first approximation until the line becomes visible (and is thus - // properly measurable). - function estimateHeight(cm) { - var th = textHeight(cm.display), wrapping = cm.options.lineWrapping; - var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3); - return function (line) { - if (lineIsHidden(cm.doc, line)) { return 0 } - - var widgetsHeight = 0; - if (line.widgets) { for (var i = 0; i < line.widgets.length; i++) { - if (line.widgets[i].height) { widgetsHeight += line.widgets[i].height; } - } } - - if (wrapping) - { return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th } - else - { return widgetsHeight + th } - } - } - - function estimateLineHeights(cm) { - var doc = cm.doc, est = estimateHeight(cm); - doc.iter(function (line) { - var estHeight = est(line); - if (estHeight != line.height) { updateLineHeight(line, estHeight); } - }); - } - - // Given a mouse event, find the corresponding position. If liberal - // is false, it checks whether a gutter or scrollbar was clicked, - // and returns null if it was. forRect is used by rectangular - // selections, and tries to estimate a character position even for - // coordinates beyond the right of the text. - function posFromMouse(cm, e, liberal, forRect) { - var display = cm.display; - if (!liberal && e_target(e).getAttribute("cm-not-content") == "true") { return null } - - var x, y, space = display.lineSpace.getBoundingClientRect(); - // Fails unpredictably on IE[67] when mouse is dragged around quickly. - try { x = e.clientX - space.left; y = e.clientY - space.top; } - catch (e$1) { return null } - var coords = coordsChar(cm, x, y), line; - if (forRect && coords.xRel > 0 && (line = getLine(cm.doc, coords.line).text).length == coords.ch) { - var colDiff = countColumn(line, line.length, cm.options.tabSize) - line.length; - coords = Pos(coords.line, Math.max(0, Math.round((x - paddingH(cm.display).left) / charWidth(cm.display)) - colDiff)); - } - return coords - } - - // Find the view element corresponding to a given line. Return null - // when the line isn't visible. - function findViewIndex(cm, n) { - if (n >= cm.display.viewTo) { return null } - n -= cm.display.viewFrom; - if (n < 0) { return null } - var view = cm.display.view; - for (var i = 0; i < view.length; i++) { - n -= view[i].size; - if (n < 0) { return i } - } - } - - // Updates the display.view data structure for a given change to the - // document. From and to are in pre-change coordinates. Lendiff is - // the amount of lines added or subtracted by the change. This is - // used for changes that span multiple lines, or change the way - // lines are divided into visual lines. regLineChange (below) - // registers single-line changes. - function regChange(cm, from, to, lendiff) { - if (from == null) { from = cm.doc.first; } - if (to == null) { to = cm.doc.first + cm.doc.size; } - if (!lendiff) { lendiff = 0; } - - var display = cm.display; - if (lendiff && to < display.viewTo && - (display.updateLineNumbers == null || display.updateLineNumbers > from)) - { display.updateLineNumbers = from; } - - cm.curOp.viewChanged = true; - - if (from >= display.viewTo) { // Change after - if (sawCollapsedSpans && visualLineNo(cm.doc, from) < display.viewTo) - { resetView(cm); } - } else if (to <= display.viewFrom) { // Change before - if (sawCollapsedSpans && visualLineEndNo(cm.doc, to + lendiff) > display.viewFrom) { - resetView(cm); - } else { - display.viewFrom += lendiff; - display.viewTo += lendiff; - } - } else if (from <= display.viewFrom && to >= display.viewTo) { // Full overlap - resetView(cm); - } else if (from <= display.viewFrom) { // Top overlap - var cut = viewCuttingPoint(cm, to, to + lendiff, 1); - if (cut) { - display.view = display.view.slice(cut.index); - display.viewFrom = cut.lineN; - display.viewTo += lendiff; - } else { - resetView(cm); - } - } else if (to >= display.viewTo) { // Bottom overlap - var cut$1 = viewCuttingPoint(cm, from, from, -1); - if (cut$1) { - display.view = display.view.slice(0, cut$1.index); - display.viewTo = cut$1.lineN; - } else { - resetView(cm); - } - } else { // Gap in the middle - var cutTop = viewCuttingPoint(cm, from, from, -1); - var cutBot = viewCuttingPoint(cm, to, to + lendiff, 1); - if (cutTop && cutBot) { - display.view = display.view.slice(0, cutTop.index) - .concat(buildViewArray(cm, cutTop.lineN, cutBot.lineN)) - .concat(display.view.slice(cutBot.index)); - display.viewTo += lendiff; - } else { - resetView(cm); - } - } - - var ext = display.externalMeasured; - if (ext) { - if (to < ext.lineN) - { ext.lineN += lendiff; } - else if (from < ext.lineN + ext.size) - { display.externalMeasured = null; } - } - } - - // Register a change to a single line. Type must be one of "text", - // "gutter", "class", "widget" - function regLineChange(cm, line, type) { - cm.curOp.viewChanged = true; - var display = cm.display, ext = cm.display.externalMeasured; - if (ext && line >= ext.lineN && line < ext.lineN + ext.size) - { display.externalMeasured = null; } - - if (line < display.viewFrom || line >= display.viewTo) { return } - var lineView = display.view[findViewIndex(cm, line)]; - if (lineView.node == null) { return } - var arr = lineView.changes || (lineView.changes = []); - if (indexOf(arr, type) == -1) { arr.push(type); } - } - - // Clear the view. - function resetView(cm) { - cm.display.viewFrom = cm.display.viewTo = cm.doc.first; - cm.display.view = []; - cm.display.viewOffset = 0; - } - - function viewCuttingPoint(cm, oldN, newN, dir) { - var index = findViewIndex(cm, oldN), diff, view = cm.display.view; - if (!sawCollapsedSpans || newN == cm.doc.first + cm.doc.size) - { return {index: index, lineN: newN} } - var n = cm.display.viewFrom; - for (var i = 0; i < index; i++) - { n += view[i].size; } - if (n != oldN) { - if (dir > 0) { - if (index == view.length - 1) { return null } - diff = (n + view[index].size) - oldN; - index++; - } else { - diff = n - oldN; - } - oldN += diff; newN += diff; - } - while (visualLineNo(cm.doc, newN) != newN) { - if (index == (dir < 0 ? 0 : view.length - 1)) { return null } - newN += dir * view[index - (dir < 0 ? 1 : 0)].size; - index += dir; - } - return {index: index, lineN: newN} - } - - // Force the view to cover a given range, adding empty view element - // or clipping off existing ones as needed. - function adjustView(cm, from, to) { - var display = cm.display, view = display.view; - if (view.length == 0 || from >= display.viewTo || to <= display.viewFrom) { - display.view = buildViewArray(cm, from, to); - display.viewFrom = from; - } else { - if (display.viewFrom > from) - { display.view = buildViewArray(cm, from, display.viewFrom).concat(display.view); } - else if (display.viewFrom < from) - { display.view = display.view.slice(findViewIndex(cm, from)); } - display.viewFrom = from; - if (display.viewTo < to) - { display.view = display.view.concat(buildViewArray(cm, display.viewTo, to)); } - else if (display.viewTo > to) - { display.view = display.view.slice(0, findViewIndex(cm, to)); } - } - display.viewTo = to; - } - - // Count the number of lines in the view whose DOM representation is - // out of date (or nonexistent). - function countDirtyView(cm) { - var view = cm.display.view, dirty = 0; - for (var i = 0; i < view.length; i++) { - var lineView = view[i]; - if (!lineView.hidden && (!lineView.node || lineView.changes)) { ++dirty; } - } - return dirty - } - - function updateSelection(cm) { - cm.display.input.showSelection(cm.display.input.prepareSelection()); - } - - function prepareSelection(cm, primary) { - if ( primary === void 0 ) primary = true; - - var doc = cm.doc, result = {}; - var curFragment = result.cursors = document.createDocumentFragment(); - var selFragment = result.selection = document.createDocumentFragment(); - - for (var i = 0; i < doc.sel.ranges.length; i++) { - if (!primary && i == doc.sel.primIndex) { continue } - var range = doc.sel.ranges[i]; - if (range.from().line >= cm.display.viewTo || range.to().line < cm.display.viewFrom) { continue } - var collapsed = range.empty(); - if (collapsed || cm.options.showCursorWhenSelecting) - { drawSelectionCursor(cm, range.head, curFragment); } - if (!collapsed) - { drawSelectionRange(cm, range, selFragment); } - } - return result - } - - // Draws a cursor for the given range - function drawSelectionCursor(cm, head, output) { - var pos = cursorCoords(cm, head, "div", null, null, !cm.options.singleCursorHeightPerLine); - - var cursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor")); - cursor.style.left = pos.left + "px"; - cursor.style.top = pos.top + "px"; - cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px"; - - if (pos.other) { - // Secondary cursor, shown when on a 'jump' in bi-directional text - var otherCursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor")); - otherCursor.style.display = ""; - otherCursor.style.left = pos.other.left + "px"; - otherCursor.style.top = pos.other.top + "px"; - otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px"; - } - } - - function cmpCoords(a, b) { return a.top - b.top || a.left - b.left } - - // Draws the given range as a highlighted selection - function drawSelectionRange(cm, range, output) { - var display = cm.display, doc = cm.doc; - var fragment = document.createDocumentFragment(); - var padding = paddingH(cm.display), leftSide = padding.left; - var rightSide = Math.max(display.sizerWidth, displayWidth(cm) - display.sizer.offsetLeft) - padding.right; - var docLTR = doc.direction == "ltr"; - - function add(left, top, width, bottom) { - if (top < 0) { top = 0; } - top = Math.round(top); - bottom = Math.round(bottom); - fragment.appendChild(elt("div", null, "CodeMirror-selected", ("position: absolute; left: " + left + "px;\n top: " + top + "px; width: " + (width == null ? rightSide - left : width) + "px;\n height: " + (bottom - top) + "px"))); - } - - function drawForLine(line, fromArg, toArg) { - var lineObj = getLine(doc, line); - var lineLen = lineObj.text.length; - var start, end; - function coords(ch, bias) { - return charCoords(cm, Pos(line, ch), "div", lineObj, bias) - } - - function wrapX(pos, dir, side) { - var extent = wrappedLineExtentChar(cm, lineObj, null, pos); - var prop = (dir == "ltr") == (side == "after") ? "left" : "right"; - var ch = side == "after" ? extent.begin : extent.end - (/\s/.test(lineObj.text.charAt(extent.end - 1)) ? 2 : 1); - return coords(ch, prop)[prop] - } - - var order = getOrder(lineObj, doc.direction); - iterateBidiSections(order, fromArg || 0, toArg == null ? lineLen : toArg, function (from, to, dir, i) { - var ltr = dir == "ltr"; - var fromPos = coords(from, ltr ? "left" : "right"); - var toPos = coords(to - 1, ltr ? "right" : "left"); - - var openStart = fromArg == null && from == 0, openEnd = toArg == null && to == lineLen; - var first = i == 0, last = !order || i == order.length - 1; - if (toPos.top - fromPos.top <= 3) { // Single line - var openLeft = (docLTR ? openStart : openEnd) && first; - var openRight = (docLTR ? openEnd : openStart) && last; - var left = openLeft ? leftSide : (ltr ? fromPos : toPos).left; - var right = openRight ? rightSide : (ltr ? toPos : fromPos).right; - add(left, fromPos.top, right - left, fromPos.bottom); - } else { // Multiple lines - var topLeft, topRight, botLeft, botRight; - if (ltr) { - topLeft = docLTR && openStart && first ? leftSide : fromPos.left; - topRight = docLTR ? rightSide : wrapX(from, dir, "before"); - botLeft = docLTR ? leftSide : wrapX(to, dir, "after"); - botRight = docLTR && openEnd && last ? rightSide : toPos.right; - } else { - topLeft = !docLTR ? leftSide : wrapX(from, dir, "before"); - topRight = !docLTR && openStart && first ? rightSide : fromPos.right; - botLeft = !docLTR && openEnd && last ? leftSide : toPos.left; - botRight = !docLTR ? rightSide : wrapX(to, dir, "after"); - } - add(topLeft, fromPos.top, topRight - topLeft, fromPos.bottom); - if (fromPos.bottom < toPos.top) { add(leftSide, fromPos.bottom, null, toPos.top); } - add(botLeft, toPos.top, botRight - botLeft, toPos.bottom); - } - - if (!start || cmpCoords(fromPos, start) < 0) { start = fromPos; } - if (cmpCoords(toPos, start) < 0) { start = toPos; } - if (!end || cmpCoords(fromPos, end) < 0) { end = fromPos; } - if (cmpCoords(toPos, end) < 0) { end = toPos; } - }); - return {start: start, end: end} - } - - var sFrom = range.from(), sTo = range.to(); - if (sFrom.line == sTo.line) { - drawForLine(sFrom.line, sFrom.ch, sTo.ch); - } else { - var fromLine = getLine(doc, sFrom.line), toLine = getLine(doc, sTo.line); - var singleVLine = visualLine(fromLine) == visualLine(toLine); - var leftEnd = drawForLine(sFrom.line, sFrom.ch, singleVLine ? fromLine.text.length + 1 : null).end; - var rightStart = drawForLine(sTo.line, singleVLine ? 0 : null, sTo.ch).start; - if (singleVLine) { - if (leftEnd.top < rightStart.top - 2) { - add(leftEnd.right, leftEnd.top, null, leftEnd.bottom); - add(leftSide, rightStart.top, rightStart.left, rightStart.bottom); - } else { - add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom); - } - } - if (leftEnd.bottom < rightStart.top) - { add(leftSide, leftEnd.bottom, null, rightStart.top); } - } - - output.appendChild(fragment); - } - - // Cursor-blinking - function restartBlink(cm) { - if (!cm.state.focused) { return } - var display = cm.display; - clearInterval(display.blinker); - var on = true; - display.cursorDiv.style.visibility = ""; - if (cm.options.cursorBlinkRate > 0) - { display.blinker = setInterval(function () { return display.cursorDiv.style.visibility = (on = !on) ? "" : "hidden"; }, - cm.options.cursorBlinkRate); } - else if (cm.options.cursorBlinkRate < 0) - { display.cursorDiv.style.visibility = "hidden"; } - } - - function ensureFocus(cm) { - if (!cm.state.focused) { cm.display.input.focus(); onFocus(cm); } - } - - function delayBlurEvent(cm) { - cm.state.delayingBlurEvent = true; - setTimeout(function () { if (cm.state.delayingBlurEvent) { - cm.state.delayingBlurEvent = false; - onBlur(cm); - } }, 100); - } - - function onFocus(cm, e) { - if (cm.state.delayingBlurEvent) { cm.state.delayingBlurEvent = false; } - - if (cm.options.readOnly == "nocursor") { return } - if (!cm.state.focused) { - signal(cm, "focus", cm, e); - cm.state.focused = true; - addClass(cm.display.wrapper, "CodeMirror-focused"); - // This test prevents this from firing when a context - // menu is closed (since the input reset would kill the - // select-all detection hack) - if (!cm.curOp && cm.display.selForContextMenu != cm.doc.sel) { - cm.display.input.reset(); - if (webkit) { setTimeout(function () { return cm.display.input.reset(true); }, 20); } // Issue #1730 - } - cm.display.input.receivedFocus(); - } - restartBlink(cm); - } - function onBlur(cm, e) { - if (cm.state.delayingBlurEvent) { return } - - if (cm.state.focused) { - signal(cm, "blur", cm, e); - cm.state.focused = false; - rmClass(cm.display.wrapper, "CodeMirror-focused"); - } - clearInterval(cm.display.blinker); - setTimeout(function () { if (!cm.state.focused) { cm.display.shift = false; } }, 150); - } - - // Read the actual heights of the rendered lines, and update their - // stored heights to match. - function updateHeightsInViewport(cm) { - var display = cm.display; - var prevBottom = display.lineDiv.offsetTop; - for (var i = 0; i < display.view.length; i++) { - var cur = display.view[i], wrapping = cm.options.lineWrapping; - var height = (void 0), width = 0; - if (cur.hidden) { continue } - if (ie && ie_version < 8) { - var bot = cur.node.offsetTop + cur.node.offsetHeight; - height = bot - prevBottom; - prevBottom = bot; - } else { - var box = cur.node.getBoundingClientRect(); - height = box.bottom - box.top; - // Check that lines don't extend past the right of the current - // editor width - if (!wrapping && cur.text.firstChild) - { width = cur.text.firstChild.getBoundingClientRect().right - box.left - 1; } - } - var diff = cur.line.height - height; - if (diff > .005 || diff < -.005) { - updateLineHeight(cur.line, height); - updateWidgetHeight(cur.line); - if (cur.rest) { for (var j = 0; j < cur.rest.length; j++) - { updateWidgetHeight(cur.rest[j]); } } - } - if (width > cm.display.sizerWidth) { - var chWidth = Math.ceil(width / charWidth(cm.display)); - if (chWidth > cm.display.maxLineLength) { - cm.display.maxLineLength = chWidth; - cm.display.maxLine = cur.line; - cm.display.maxLineChanged = true; - } - } - } - } - - // Read and store the height of line widgets associated with the - // given line. - function updateWidgetHeight(line) { - if (line.widgets) { for (var i = 0; i < line.widgets.length; ++i) { - var w = line.widgets[i], parent = w.node.parentNode; - if (parent) { w.height = parent.offsetHeight; } - } } - } - - // Compute the lines that are visible in a given viewport (defaults - // the the current scroll position). viewport may contain top, - // height, and ensure (see op.scrollToPos) properties. - function visibleLines(display, doc, viewport) { - var top = viewport && viewport.top != null ? Math.max(0, viewport.top) : display.scroller.scrollTop; - top = Math.floor(top - paddingTop(display)); - var bottom = viewport && viewport.bottom != null ? viewport.bottom : top + display.wrapper.clientHeight; - - var from = lineAtHeight(doc, top), to = lineAtHeight(doc, bottom); - // Ensure is a {from: {line, ch}, to: {line, ch}} object, and - // forces those lines into the viewport (if possible). - if (viewport && viewport.ensure) { - var ensureFrom = viewport.ensure.from.line, ensureTo = viewport.ensure.to.line; - if (ensureFrom < from) { - from = ensureFrom; - to = lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight); - } else if (Math.min(ensureTo, doc.lastLine()) >= to) { - from = lineAtHeight(doc, heightAtLine(getLine(doc, ensureTo)) - display.wrapper.clientHeight); - to = ensureTo; - } - } - return {from: from, to: Math.max(to, from + 1)} - } - - // SCROLLING THINGS INTO VIEW - - // If an editor sits on the top or bottom of the window, partially - // scrolled out of view, this ensures that the cursor is visible. - function maybeScrollWindow(cm, rect) { - if (signalDOMEvent(cm, "scrollCursorIntoView")) { return } - - var display = cm.display, box = display.sizer.getBoundingClientRect(), doScroll = null; - if (rect.top + box.top < 0) { doScroll = true; } - else if (rect.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) { doScroll = false; } - if (doScroll != null && !phantom) { - var scrollNode = elt("div", "\u200b", null, ("position: absolute;\n top: " + (rect.top - display.viewOffset - paddingTop(cm.display)) + "px;\n height: " + (rect.bottom - rect.top + scrollGap(cm) + display.barHeight) + "px;\n left: " + (rect.left) + "px; width: " + (Math.max(2, rect.right - rect.left)) + "px;")); - cm.display.lineSpace.appendChild(scrollNode); - scrollNode.scrollIntoView(doScroll); - cm.display.lineSpace.removeChild(scrollNode); - } - } - - // Scroll a given position into view (immediately), verifying that - // it actually became visible (as line heights are accurately - // measured, the position of something may 'drift' during drawing). - function scrollPosIntoView(cm, pos, end, margin) { - if (margin == null) { margin = 0; } - var rect; - if (!cm.options.lineWrapping && pos == end) { - // Set pos and end to the cursor positions around the character pos sticks to - // If pos.sticky == "before", that is around pos.ch - 1, otherwise around pos.ch - // If pos == Pos(_, 0, "before"), pos and end are unchanged - pos = pos.ch ? Pos(pos.line, pos.sticky == "before" ? pos.ch - 1 : pos.ch, "after") : pos; - end = pos.sticky == "before" ? Pos(pos.line, pos.ch + 1, "before") : pos; - } - for (var limit = 0; limit < 5; limit++) { - var changed = false; - var coords = cursorCoords(cm, pos); - var endCoords = !end || end == pos ? coords : cursorCoords(cm, end); - rect = {left: Math.min(coords.left, endCoords.left), - top: Math.min(coords.top, endCoords.top) - margin, - right: Math.max(coords.left, endCoords.left), - bottom: Math.max(coords.bottom, endCoords.bottom) + margin}; - var scrollPos = calculateScrollPos(cm, rect); - var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft; - if (scrollPos.scrollTop != null) { - updateScrollTop(cm, scrollPos.scrollTop); - if (Math.abs(cm.doc.scrollTop - startTop) > 1) { changed = true; } - } - if (scrollPos.scrollLeft != null) { - setScrollLeft(cm, scrollPos.scrollLeft); - if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) { changed = true; } - } - if (!changed) { break } - } - return rect - } - - // Scroll a given set of coordinates into view (immediately). - function scrollIntoView(cm, rect) { - var scrollPos = calculateScrollPos(cm, rect); - if (scrollPos.scrollTop != null) { updateScrollTop(cm, scrollPos.scrollTop); } - if (scrollPos.scrollLeft != null) { setScrollLeft(cm, scrollPos.scrollLeft); } - } - - // Calculate a new scroll position needed to scroll the given - // rectangle into view. Returns an object with scrollTop and - // scrollLeft properties. When these are undefined, the - // vertical/horizontal position does not need to be adjusted. - function calculateScrollPos(cm, rect) { - var display = cm.display, snapMargin = textHeight(cm.display); - if (rect.top < 0) { rect.top = 0; } - var screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop; - var screen = displayHeight(cm), result = {}; - if (rect.bottom - rect.top > screen) { rect.bottom = rect.top + screen; } - var docBottom = cm.doc.height + paddingVert(display); - var atTop = rect.top < snapMargin, atBottom = rect.bottom > docBottom - snapMargin; - if (rect.top < screentop) { - result.scrollTop = atTop ? 0 : rect.top; - } else if (rect.bottom > screentop + screen) { - var newTop = Math.min(rect.top, (atBottom ? docBottom : rect.bottom) - screen); - if (newTop != screentop) { result.scrollTop = newTop; } - } - - var screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft; - var screenw = displayWidth(cm) - (cm.options.fixedGutter ? display.gutters.offsetWidth : 0); - var tooWide = rect.right - rect.left > screenw; - if (tooWide) { rect.right = rect.left + screenw; } - if (rect.left < 10) - { result.scrollLeft = 0; } - else if (rect.left < screenleft) - { result.scrollLeft = Math.max(0, rect.left - (tooWide ? 0 : 10)); } - else if (rect.right > screenw + screenleft - 3) - { result.scrollLeft = rect.right + (tooWide ? 0 : 10) - screenw; } - return result - } - - // Store a relative adjustment to the scroll position in the current - // operation (to be applied when the operation finishes). - function addToScrollTop(cm, top) { - if (top == null) { return } - resolveScrollToPos(cm); - cm.curOp.scrollTop = (cm.curOp.scrollTop == null ? cm.doc.scrollTop : cm.curOp.scrollTop) + top; - } - - // Make sure that at the end of the operation the current cursor is - // shown. - function ensureCursorVisible(cm) { - resolveScrollToPos(cm); - var cur = cm.getCursor(); - cm.curOp.scrollToPos = {from: cur, to: cur, margin: cm.options.cursorScrollMargin}; - } - - function scrollToCoords(cm, x, y) { - if (x != null || y != null) { resolveScrollToPos(cm); } - if (x != null) { cm.curOp.scrollLeft = x; } - if (y != null) { cm.curOp.scrollTop = y; } - } - - function scrollToRange(cm, range) { - resolveScrollToPos(cm); - cm.curOp.scrollToPos = range; - } - - // When an operation has its scrollToPos property set, and another - // scroll action is applied before the end of the operation, this - // 'simulates' scrolling that position into view in a cheap way, so - // that the effect of intermediate scroll commands is not ignored. - function resolveScrollToPos(cm) { - var range = cm.curOp.scrollToPos; - if (range) { - cm.curOp.scrollToPos = null; - var from = estimateCoords(cm, range.from), to = estimateCoords(cm, range.to); - scrollToCoordsRange(cm, from, to, range.margin); - } - } - - function scrollToCoordsRange(cm, from, to, margin) { - var sPos = calculateScrollPos(cm, { - left: Math.min(from.left, to.left), - top: Math.min(from.top, to.top) - margin, - right: Math.max(from.right, to.right), - bottom: Math.max(from.bottom, to.bottom) + margin - }); - scrollToCoords(cm, sPos.scrollLeft, sPos.scrollTop); - } - - // Sync the scrollable area and scrollbars, ensure the viewport - // covers the visible area. - function updateScrollTop(cm, val) { - if (Math.abs(cm.doc.scrollTop - val) < 2) { return } - if (!gecko) { updateDisplaySimple(cm, {top: val}); } - setScrollTop(cm, val, true); - if (gecko) { updateDisplaySimple(cm); } - startWorker(cm, 100); - } - - function setScrollTop(cm, val, forceScroll) { - val = Math.max(0, Math.min(cm.display.scroller.scrollHeight - cm.display.scroller.clientHeight, val)); - if (cm.display.scroller.scrollTop == val && !forceScroll) { return } - cm.doc.scrollTop = val; - cm.display.scrollbars.setScrollTop(val); - if (cm.display.scroller.scrollTop != val) { cm.display.scroller.scrollTop = val; } - } - - // Sync scroller and scrollbar, ensure the gutter elements are - // aligned. - function setScrollLeft(cm, val, isScroller, forceScroll) { - val = Math.max(0, Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth)); - if ((isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) && !forceScroll) { return } - cm.doc.scrollLeft = val; - alignHorizontally(cm); - if (cm.display.scroller.scrollLeft != val) { cm.display.scroller.scrollLeft = val; } - cm.display.scrollbars.setScrollLeft(val); - } - - // SCROLLBARS - - // Prepare DOM reads needed to update the scrollbars. Done in one - // shot to minimize update/measure roundtrips. - function measureForScrollbars(cm) { - var d = cm.display, gutterW = d.gutters.offsetWidth; - var docH = Math.round(cm.doc.height + paddingVert(cm.display)); - return { - clientHeight: d.scroller.clientHeight, - viewHeight: d.wrapper.clientHeight, - scrollWidth: d.scroller.scrollWidth, clientWidth: d.scroller.clientWidth, - viewWidth: d.wrapper.clientWidth, - barLeft: cm.options.fixedGutter ? gutterW : 0, - docHeight: docH, - scrollHeight: docH + scrollGap(cm) + d.barHeight, - nativeBarWidth: d.nativeBarWidth, - gutterWidth: gutterW - } - } - - var NativeScrollbars = function(place, scroll, cm) { - this.cm = cm; - var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar"); - var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar"); - vert.tabIndex = horiz.tabIndex = -1; - place(vert); place(horiz); - - on(vert, "scroll", function () { - if (vert.clientHeight) { scroll(vert.scrollTop, "vertical"); } - }); - on(horiz, "scroll", function () { - if (horiz.clientWidth) { scroll(horiz.scrollLeft, "horizontal"); } - }); - - this.checkedZeroWidth = false; - // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8). - if (ie && ie_version < 8) { this.horiz.style.minHeight = this.vert.style.minWidth = "18px"; } - }; - - NativeScrollbars.prototype.update = function (measure) { - var needsH = measure.scrollWidth > measure.clientWidth + 1; - var needsV = measure.scrollHeight > measure.clientHeight + 1; - var sWidth = measure.nativeBarWidth; - - if (needsV) { - this.vert.style.display = "block"; - this.vert.style.bottom = needsH ? sWidth + "px" : "0"; - var totalHeight = measure.viewHeight - (needsH ? sWidth : 0); - // A bug in IE8 can cause this value to be negative, so guard it. - this.vert.firstChild.style.height = - Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px"; - } else { - this.vert.style.display = ""; - this.vert.firstChild.style.height = "0"; - } - - if (needsH) { - this.horiz.style.display = "block"; - this.horiz.style.right = needsV ? sWidth + "px" : "0"; - this.horiz.style.left = measure.barLeft + "px"; - var totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0); - this.horiz.firstChild.style.width = - Math.max(0, measure.scrollWidth - measure.clientWidth + totalWidth) + "px"; - } else { - this.horiz.style.display = ""; - this.horiz.firstChild.style.width = "0"; - } - - if (!this.checkedZeroWidth && measure.clientHeight > 0) { - if (sWidth == 0) { this.zeroWidthHack(); } - this.checkedZeroWidth = true; - } - - return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0} - }; - - NativeScrollbars.prototype.setScrollLeft = function (pos) { - if (this.horiz.scrollLeft != pos) { this.horiz.scrollLeft = pos; } - if (this.disableHoriz) { this.enableZeroWidthBar(this.horiz, this.disableHoriz, "horiz"); } - }; - - NativeScrollbars.prototype.setScrollTop = function (pos) { - if (this.vert.scrollTop != pos) { this.vert.scrollTop = pos; } - if (this.disableVert) { this.enableZeroWidthBar(this.vert, this.disableVert, "vert"); } - }; - - NativeScrollbars.prototype.zeroWidthHack = function () { - var w = mac && !mac_geMountainLion ? "12px" : "18px"; - this.horiz.style.height = this.vert.style.width = w; - this.horiz.style.pointerEvents = this.vert.style.pointerEvents = "none"; - this.disableHoriz = new Delayed; - this.disableVert = new Delayed; - }; - - NativeScrollbars.prototype.enableZeroWidthBar = function (bar, delay, type) { - bar.style.pointerEvents = "auto"; - function maybeDisable() { - // To find out whether the scrollbar is still visible, we - // check whether the element under the pixel in the bottom - // right corner of the scrollbar box is the scrollbar box - // itself (when the bar is still visible) or its filler child - // (when the bar is hidden). If it is still visible, we keep - // it enabled, if it's hidden, we disable pointer events. - var box = bar.getBoundingClientRect(); - var elt = type == "vert" ? document.elementFromPoint(box.right - 1, (box.top + box.bottom) / 2) - : document.elementFromPoint((box.right + box.left) / 2, box.bottom - 1); - if (elt != bar) { bar.style.pointerEvents = "none"; } - else { delay.set(1000, maybeDisable); } - } - delay.set(1000, maybeDisable); - }; - - NativeScrollbars.prototype.clear = function () { - var parent = this.horiz.parentNode; - parent.removeChild(this.horiz); - parent.removeChild(this.vert); - }; - - var NullScrollbars = function () {}; - - NullScrollbars.prototype.update = function () { return {bottom: 0, right: 0} }; - NullScrollbars.prototype.setScrollLeft = function () {}; - NullScrollbars.prototype.setScrollTop = function () {}; - NullScrollbars.prototype.clear = function () {}; - - function updateScrollbars(cm, measure) { - if (!measure) { measure = measureForScrollbars(cm); } - var startWidth = cm.display.barWidth, startHeight = cm.display.barHeight; - updateScrollbarsInner(cm, measure); - for (var i = 0; i < 4 && startWidth != cm.display.barWidth || startHeight != cm.display.barHeight; i++) { - if (startWidth != cm.display.barWidth && cm.options.lineWrapping) - { updateHeightsInViewport(cm); } - updateScrollbarsInner(cm, measureForScrollbars(cm)); - startWidth = cm.display.barWidth; startHeight = cm.display.barHeight; - } - } - - // Re-synchronize the fake scrollbars with the actual size of the - // content. - function updateScrollbarsInner(cm, measure) { - var d = cm.display; - var sizes = d.scrollbars.update(measure); - - d.sizer.style.paddingRight = (d.barWidth = sizes.right) + "px"; - d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + "px"; - d.heightForcer.style.borderBottom = sizes.bottom + "px solid transparent"; - - if (sizes.right && sizes.bottom) { - d.scrollbarFiller.style.display = "block"; - d.scrollbarFiller.style.height = sizes.bottom + "px"; - d.scrollbarFiller.style.width = sizes.right + "px"; - } else { d.scrollbarFiller.style.display = ""; } - if (sizes.bottom && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) { - d.gutterFiller.style.display = "block"; - d.gutterFiller.style.height = sizes.bottom + "px"; - d.gutterFiller.style.width = measure.gutterWidth + "px"; - } else { d.gutterFiller.style.display = ""; } - } - - var scrollbarModel = {"native": NativeScrollbars, "null": NullScrollbars}; - - function initScrollbars(cm) { - if (cm.display.scrollbars) { - cm.display.scrollbars.clear(); - if (cm.display.scrollbars.addClass) - { rmClass(cm.display.wrapper, cm.display.scrollbars.addClass); } - } - - cm.display.scrollbars = new scrollbarModel[cm.options.scrollbarStyle](function (node) { - cm.display.wrapper.insertBefore(node, cm.display.scrollbarFiller); - // Prevent clicks in the scrollbars from killing focus - on(node, "mousedown", function () { - if (cm.state.focused) { setTimeout(function () { return cm.display.input.focus(); }, 0); } - }); - node.setAttribute("cm-not-content", "true"); - }, function (pos, axis) { - if (axis == "horizontal") { setScrollLeft(cm, pos); } - else { updateScrollTop(cm, pos); } - }, cm); - if (cm.display.scrollbars.addClass) - { addClass(cm.display.wrapper, cm.display.scrollbars.addClass); } - } - - // Operations are used to wrap a series of changes to the editor - // state in such a way that each change won't have to update the - // cursor and display (which would be awkward, slow, and - // error-prone). Instead, display updates are batched and then all - // combined and executed at once. - - var nextOpId = 0; - // Start a new operation. - function startOperation(cm) { - cm.curOp = { - cm: cm, - viewChanged: false, // Flag that indicates that lines might need to be redrawn - startHeight: cm.doc.height, // Used to detect need to update scrollbar - forceUpdate: false, // Used to force a redraw - updateInput: 0, // Whether to reset the input textarea - typing: false, // Whether this reset should be careful to leave existing text (for compositing) - changeObjs: null, // Accumulated changes, for firing change events - cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on - cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already - selectionChanged: false, // Whether the selection needs to be redrawn - updateMaxLine: false, // Set when the widest line needs to be determined anew - scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet - scrollToPos: null, // Used to scroll to a specific position - focus: false, - id: ++nextOpId // Unique ID - }; - pushOperation(cm.curOp); - } - - // Finish an operation, updating the display and signalling delayed events - function endOperation(cm) { - var op = cm.curOp; - if (op) { finishOperation(op, function (group) { - for (var i = 0; i < group.ops.length; i++) - { group.ops[i].cm.curOp = null; } - endOperations(group); - }); } - } - - // The DOM updates done when an operation finishes are batched so - // that the minimum number of relayouts are required. - function endOperations(group) { - var ops = group.ops; - for (var i = 0; i < ops.length; i++) // Read DOM - { endOperation_R1(ops[i]); } - for (var i$1 = 0; i$1 < ops.length; i$1++) // Write DOM (maybe) - { endOperation_W1(ops[i$1]); } - for (var i$2 = 0; i$2 < ops.length; i$2++) // Read DOM - { endOperation_R2(ops[i$2]); } - for (var i$3 = 0; i$3 < ops.length; i$3++) // Write DOM (maybe) - { endOperation_W2(ops[i$3]); } - for (var i$4 = 0; i$4 < ops.length; i$4++) // Read DOM - { endOperation_finish(ops[i$4]); } - } - - function endOperation_R1(op) { - var cm = op.cm, display = cm.display; - maybeClipScrollbars(cm); - if (op.updateMaxLine) { findMaxLine(cm); } - - op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null || - op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom || - op.scrollToPos.to.line >= display.viewTo) || - display.maxLineChanged && cm.options.lineWrapping; - op.update = op.mustUpdate && - new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate); - } - - function endOperation_W1(op) { - op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update); - } - - function endOperation_R2(op) { - var cm = op.cm, display = cm.display; - if (op.updatedDisplay) { updateHeightsInViewport(cm); } - - op.barMeasure = measureForScrollbars(cm); - - // If the max line changed since it was last measured, measure it, - // and ensure the document's width matches it. - // updateDisplay_W2 will use these properties to do the actual resizing - if (display.maxLineChanged && !cm.options.lineWrapping) { - op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3; - cm.display.sizerWidth = op.adjustWidthTo; - op.barMeasure.scrollWidth = - Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth); - op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm)); - } - - if (op.updatedDisplay || op.selectionChanged) - { op.preparedSelection = display.input.prepareSelection(); } - } - - function endOperation_W2(op) { - var cm = op.cm; - - if (op.adjustWidthTo != null) { - cm.display.sizer.style.minWidth = op.adjustWidthTo + "px"; - if (op.maxScrollLeft < cm.doc.scrollLeft) - { setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true); } - cm.display.maxLineChanged = false; - } - - var takeFocus = op.focus && op.focus == activeElt(); - if (op.preparedSelection) - { cm.display.input.showSelection(op.preparedSelection, takeFocus); } - if (op.updatedDisplay || op.startHeight != cm.doc.height) - { updateScrollbars(cm, op.barMeasure); } - if (op.updatedDisplay) - { setDocumentHeight(cm, op.barMeasure); } - - if (op.selectionChanged) { restartBlink(cm); } - - if (cm.state.focused && op.updateInput) - { cm.display.input.reset(op.typing); } - if (takeFocus) { ensureFocus(op.cm); } - } - - function endOperation_finish(op) { - var cm = op.cm, display = cm.display, doc = cm.doc; - - if (op.updatedDisplay) { postUpdateDisplay(cm, op.update); } - - // Abort mouse wheel delta measurement, when scrolling explicitly - if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos)) - { display.wheelStartX = display.wheelStartY = null; } - - // Propagate the scroll position to the actual DOM scroller - if (op.scrollTop != null) { setScrollTop(cm, op.scrollTop, op.forceScroll); } - - if (op.scrollLeft != null) { setScrollLeft(cm, op.scrollLeft, true, true); } - // If we need to scroll a specific position into view, do so. - if (op.scrollToPos) { - var rect = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from), - clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin); - maybeScrollWindow(cm, rect); - } - - // Fire events for markers that are hidden/unidden by editing or - // undoing - var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers; - if (hidden) { for (var i = 0; i < hidden.length; ++i) - { if (!hidden[i].lines.length) { signal(hidden[i], "hide"); } } } - if (unhidden) { for (var i$1 = 0; i$1 < unhidden.length; ++i$1) - { if (unhidden[i$1].lines.length) { signal(unhidden[i$1], "unhide"); } } } - - if (display.wrapper.offsetHeight) - { doc.scrollTop = cm.display.scroller.scrollTop; } - - // Fire change events, and delayed event handlers - if (op.changeObjs) - { signal(cm, "changes", cm, op.changeObjs); } - if (op.update) - { op.update.finish(); } - } - - // Run the given function in an operation - function runInOp(cm, f) { - if (cm.curOp) { return f() } - startOperation(cm); - try { return f() } - finally { endOperation(cm); } - } - // Wraps a function in an operation. Returns the wrapped function. - function operation(cm, f) { - return function() { - if (cm.curOp) { return f.apply(cm, arguments) } - startOperation(cm); - try { return f.apply(cm, arguments) } - finally { endOperation(cm); } - } - } - // Used to add methods to editor and doc instances, wrapping them in - // operations. - function methodOp(f) { - return function() { - if (this.curOp) { return f.apply(this, arguments) } - startOperation(this); - try { return f.apply(this, arguments) } - finally { endOperation(this); } - } - } - function docMethodOp(f) { - return function() { - var cm = this.cm; - if (!cm || cm.curOp) { return f.apply(this, arguments) } - startOperation(cm); - try { return f.apply(this, arguments) } - finally { endOperation(cm); } - } - } - - // HIGHLIGHT WORKER - - function startWorker(cm, time) { - if (cm.doc.highlightFrontier < cm.display.viewTo) - { cm.state.highlight.set(time, bind(highlightWorker, cm)); } - } - - function highlightWorker(cm) { - var doc = cm.doc; - if (doc.highlightFrontier >= cm.display.viewTo) { return } - var end = +new Date + cm.options.workTime; - var context = getContextBefore(cm, doc.highlightFrontier); - var changedLines = []; - - doc.iter(context.line, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function (line) { - if (context.line >= cm.display.viewFrom) { // Visible - var oldStyles = line.styles; - var resetState = line.text.length > cm.options.maxHighlightLength ? copyState(doc.mode, context.state) : null; - var highlighted = highlightLine(cm, line, context, true); - if (resetState) { context.state = resetState; } - line.styles = highlighted.styles; - var oldCls = line.styleClasses, newCls = highlighted.classes; - if (newCls) { line.styleClasses = newCls; } - else if (oldCls) { line.styleClasses = null; } - var ischange = !oldStyles || oldStyles.length != line.styles.length || - oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass); - for (var i = 0; !ischange && i < oldStyles.length; ++i) { ischange = oldStyles[i] != line.styles[i]; } - if (ischange) { changedLines.push(context.line); } - line.stateAfter = context.save(); - context.nextLine(); - } else { - if (line.text.length <= cm.options.maxHighlightLength) - { processLine(cm, line.text, context); } - line.stateAfter = context.line % 5 == 0 ? context.save() : null; - context.nextLine(); - } - if (+new Date > end) { - startWorker(cm, cm.options.workDelay); - return true - } - }); - doc.highlightFrontier = context.line; - doc.modeFrontier = Math.max(doc.modeFrontier, context.line); - if (changedLines.length) { runInOp(cm, function () { - for (var i = 0; i < changedLines.length; i++) - { regLineChange(cm, changedLines[i], "text"); } - }); } - } - - // DISPLAY DRAWING - - var DisplayUpdate = function(cm, viewport, force) { - var display = cm.display; - - this.viewport = viewport; - // Store some values that we'll need later (but don't want to force a relayout for) - this.visible = visibleLines(display, cm.doc, viewport); - this.editorIsHidden = !display.wrapper.offsetWidth; - this.wrapperHeight = display.wrapper.clientHeight; - this.wrapperWidth = display.wrapper.clientWidth; - this.oldDisplayWidth = displayWidth(cm); - this.force = force; - this.dims = getDimensions(cm); - this.events = []; - }; - - DisplayUpdate.prototype.signal = function (emitter, type) { - if (hasHandler(emitter, type)) - { this.events.push(arguments); } - }; - DisplayUpdate.prototype.finish = function () { - for (var i = 0; i < this.events.length; i++) - { signal.apply(null, this.events[i]); } - }; - - function maybeClipScrollbars(cm) { - var display = cm.display; - if (!display.scrollbarsClipped && display.scroller.offsetWidth) { - display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth; - display.heightForcer.style.height = scrollGap(cm) + "px"; - display.sizer.style.marginBottom = -display.nativeBarWidth + "px"; - display.sizer.style.borderRightWidth = scrollGap(cm) + "px"; - display.scrollbarsClipped = true; - } - } - - function selectionSnapshot(cm) { - if (cm.hasFocus()) { return null } - var active = activeElt(); - if (!active || !contains(cm.display.lineDiv, active)) { return null } - var result = {activeElt: active}; - if (window.getSelection) { - var sel = window.getSelection(); - if (sel.anchorNode && sel.extend && contains(cm.display.lineDiv, sel.anchorNode)) { - result.anchorNode = sel.anchorNode; - result.anchorOffset = sel.anchorOffset; - result.focusNode = sel.focusNode; - result.focusOffset = sel.focusOffset; - } - } - return result - } - - function restoreSelection(snapshot) { - if (!snapshot || !snapshot.activeElt || snapshot.activeElt == activeElt()) { return } - snapshot.activeElt.focus(); - if (!/^(INPUT|TEXTAREA)$/.test(snapshot.activeElt.nodeName) && - snapshot.anchorNode && contains(document.body, snapshot.anchorNode) && contains(document.body, snapshot.focusNode)) { - var sel = window.getSelection(), range = document.createRange(); - range.setEnd(snapshot.anchorNode, snapshot.anchorOffset); - range.collapse(false); - sel.removeAllRanges(); - sel.addRange(range); - sel.extend(snapshot.focusNode, snapshot.focusOffset); - } - } - - // Does the actual updating of the line display. Bails out - // (returning false) when there is nothing to be done and forced is - // false. - function updateDisplayIfNeeded(cm, update) { - var display = cm.display, doc = cm.doc; - - if (update.editorIsHidden) { - resetView(cm); - return false - } - - // Bail out if the visible area is already rendered and nothing changed. - if (!update.force && - update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo && - (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) && - display.renderedView == display.view && countDirtyView(cm) == 0) - { return false } - - if (maybeUpdateLineNumberWidth(cm)) { - resetView(cm); - update.dims = getDimensions(cm); - } - - // Compute a suitable new viewport (from & to) - var end = doc.first + doc.size; - var from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first); - var to = Math.min(end, update.visible.to + cm.options.viewportMargin); - if (display.viewFrom < from && from - display.viewFrom < 20) { from = Math.max(doc.first, display.viewFrom); } - if (display.viewTo > to && display.viewTo - to < 20) { to = Math.min(end, display.viewTo); } - if (sawCollapsedSpans) { - from = visualLineNo(cm.doc, from); - to = visualLineEndNo(cm.doc, to); - } - - var different = from != display.viewFrom || to != display.viewTo || - display.lastWrapHeight != update.wrapperHeight || display.lastWrapWidth != update.wrapperWidth; - adjustView(cm, from, to); - - display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom)); - // Position the mover div to align with the current scroll position - cm.display.mover.style.top = display.viewOffset + "px"; - - var toUpdate = countDirtyView(cm); - if (!different && toUpdate == 0 && !update.force && display.renderedView == display.view && - (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo)) - { return false } - - // For big changes, we hide the enclosing element during the - // update, since that speeds up the operations on most browsers. - var selSnapshot = selectionSnapshot(cm); - if (toUpdate > 4) { display.lineDiv.style.display = "none"; } - patchDisplay(cm, display.updateLineNumbers, update.dims); - if (toUpdate > 4) { display.lineDiv.style.display = ""; } - display.renderedView = display.view; - // There might have been a widget with a focused element that got - // hidden or updated, if so re-focus it. - restoreSelection(selSnapshot); - - // Prevent selection and cursors from interfering with the scroll - // width and height. - removeChildren(display.cursorDiv); - removeChildren(display.selectionDiv); - display.gutters.style.height = display.sizer.style.minHeight = 0; - - if (different) { - display.lastWrapHeight = update.wrapperHeight; - display.lastWrapWidth = update.wrapperWidth; - startWorker(cm, 400); - } - - display.updateLineNumbers = null; - - return true - } - - function postUpdateDisplay(cm, update) { - var viewport = update.viewport; - - for (var first = true;; first = false) { - if (!first || !cm.options.lineWrapping || update.oldDisplayWidth == displayWidth(cm)) { - // Clip forced viewport to actual scrollable area. - if (viewport && viewport.top != null) - { viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - displayHeight(cm), viewport.top)}; } - // Updated line heights might result in the drawn area not - // actually covering the viewport. Keep looping until it does. - update.visible = visibleLines(cm.display, cm.doc, viewport); - if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo) - { break } - } else if (first) { - update.visible = visibleLines(cm.display, cm.doc, viewport); - } - if (!updateDisplayIfNeeded(cm, update)) { break } - updateHeightsInViewport(cm); - var barMeasure = measureForScrollbars(cm); - updateSelection(cm); - updateScrollbars(cm, barMeasure); - setDocumentHeight(cm, barMeasure); - update.force = false; - } - - update.signal(cm, "update", cm); - if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) { - update.signal(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo); - cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo; - } - } - - function updateDisplaySimple(cm, viewport) { - var update = new DisplayUpdate(cm, viewport); - if (updateDisplayIfNeeded(cm, update)) { - updateHeightsInViewport(cm); - postUpdateDisplay(cm, update); - var barMeasure = measureForScrollbars(cm); - updateSelection(cm); - updateScrollbars(cm, barMeasure); - setDocumentHeight(cm, barMeasure); - update.finish(); - } - } - - // Sync the actual display DOM structure with display.view, removing - // nodes for lines that are no longer in view, and creating the ones - // that are not there yet, and updating the ones that are out of - // date. - function patchDisplay(cm, updateNumbersFrom, dims) { - var display = cm.display, lineNumbers = cm.options.lineNumbers; - var container = display.lineDiv, cur = container.firstChild; - - function rm(node) { - var next = node.nextSibling; - // Works around a throw-scroll bug in OS X Webkit - if (webkit && mac && cm.display.currentWheelTarget == node) - { node.style.display = "none"; } - else - { node.parentNode.removeChild(node); } - return next - } - - var view = display.view, lineN = display.viewFrom; - // Loop over the elements in the view, syncing cur (the DOM nodes - // in display.lineDiv) with the view as we go. - for (var i = 0; i < view.length; i++) { - var lineView = view[i]; - if (lineView.hidden) ; else if (!lineView.node || lineView.node.parentNode != container) { // Not drawn yet - var node = buildLineElement(cm, lineView, lineN, dims); - container.insertBefore(node, cur); - } else { // Already drawn - while (cur != lineView.node) { cur = rm(cur); } - var updateNumber = lineNumbers && updateNumbersFrom != null && - updateNumbersFrom <= lineN && lineView.lineNumber; - if (lineView.changes) { - if (indexOf(lineView.changes, "gutter") > -1) { updateNumber = false; } - updateLineForChanges(cm, lineView, lineN, dims); - } - if (updateNumber) { - removeChildren(lineView.lineNumber); - lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN))); - } - cur = lineView.node.nextSibling; - } - lineN += lineView.size; - } - while (cur) { cur = rm(cur); } - } - - function updateGutterSpace(display) { - var width = display.gutters.offsetWidth; - display.sizer.style.marginLeft = width + "px"; - } - - function setDocumentHeight(cm, measure) { - cm.display.sizer.style.minHeight = measure.docHeight + "px"; - cm.display.heightForcer.style.top = measure.docHeight + "px"; - cm.display.gutters.style.height = (measure.docHeight + cm.display.barHeight + scrollGap(cm)) + "px"; - } - - // Re-align line numbers and gutter marks to compensate for - // horizontal scrolling. - function alignHorizontally(cm) { - var display = cm.display, view = display.view; - if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) { return } - var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft; - var gutterW = display.gutters.offsetWidth, left = comp + "px"; - for (var i = 0; i < view.length; i++) { if (!view[i].hidden) { - if (cm.options.fixedGutter) { - if (view[i].gutter) - { view[i].gutter.style.left = left; } - if (view[i].gutterBackground) - { view[i].gutterBackground.style.left = left; } - } - var align = view[i].alignable; - if (align) { for (var j = 0; j < align.length; j++) - { align[j].style.left = left; } } - } } - if (cm.options.fixedGutter) - { display.gutters.style.left = (comp + gutterW) + "px"; } - } - - // Used to ensure that the line number gutter is still the right - // size for the current document size. Returns true when an update - // is needed. - function maybeUpdateLineNumberWidth(cm) { - if (!cm.options.lineNumbers) { return false } - var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display; - if (last.length != display.lineNumChars) { - var test = display.measure.appendChild(elt("div", [elt("div", last)], - "CodeMirror-linenumber CodeMirror-gutter-elt")); - var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW; - display.lineGutter.style.width = ""; - display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding) + 1; - display.lineNumWidth = display.lineNumInnerWidth + padding; - display.lineNumChars = display.lineNumInnerWidth ? last.length : -1; - display.lineGutter.style.width = display.lineNumWidth + "px"; - updateGutterSpace(cm.display); - return true - } - return false - } - - function getGutters(gutters, lineNumbers) { - var result = [], sawLineNumbers = false; - for (var i = 0; i < gutters.length; i++) { - var name = gutters[i], style = null; - if (typeof name != "string") { style = name.style; name = name.className; } - if (name == "CodeMirror-linenumbers") { - if (!lineNumbers) { continue } - else { sawLineNumbers = true; } - } - result.push({className: name, style: style}); - } - if (lineNumbers && !sawLineNumbers) { result.push({className: "CodeMirror-linenumbers", style: null}); } - return result - } - - // Rebuild the gutter elements, ensure the margin to the left of the - // code matches their width. - function renderGutters(display) { - var gutters = display.gutters, specs = display.gutterSpecs; - removeChildren(gutters); - display.lineGutter = null; - for (var i = 0; i < specs.length; ++i) { - var ref = specs[i]; - var className = ref.className; - var style = ref.style; - var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + className)); - if (style) { gElt.style.cssText = style; } - if (className == "CodeMirror-linenumbers") { - display.lineGutter = gElt; - gElt.style.width = (display.lineNumWidth || 1) + "px"; - } - } - gutters.style.display = specs.length ? "" : "none"; - updateGutterSpace(display); - } - - function updateGutters(cm) { - renderGutters(cm.display); - regChange(cm); - alignHorizontally(cm); - } - - // The display handles the DOM integration, both for input reading - // and content drawing. It holds references to DOM nodes and - // display-related state. - - function Display(place, doc, input, options) { - var d = this; - this.input = input; - - // Covers bottom-right square when both scrollbars are present. - d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler"); - d.scrollbarFiller.setAttribute("cm-not-content", "true"); - // Covers bottom of gutter when coverGutterNextToScrollbar is on - // and h scrollbar is present. - d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler"); - d.gutterFiller.setAttribute("cm-not-content", "true"); - // Will contain the actual code, positioned to cover the viewport. - d.lineDiv = eltP("div", null, "CodeMirror-code"); - // Elements are added to these to represent selection and cursors. - d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1"); - d.cursorDiv = elt("div", null, "CodeMirror-cursors"); - // A visibility: hidden element used to find the size of things. - d.measure = elt("div", null, "CodeMirror-measure"); - // When lines outside of the viewport are measured, they are drawn in this. - d.lineMeasure = elt("div", null, "CodeMirror-measure"); - // Wraps everything that needs to exist inside the vertically-padded coordinate system - d.lineSpace = eltP("div", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv], - null, "position: relative; outline: none"); - var lines = eltP("div", [d.lineSpace], "CodeMirror-lines"); - // Moved around its parent to cover visible view. - d.mover = elt("div", [lines], null, "position: relative"); - // Set to the height of the document, allowing scrolling. - d.sizer = elt("div", [d.mover], "CodeMirror-sizer"); - d.sizerWidth = null; - // Behavior of elts with overflow: auto and padding is - // inconsistent across browsers. This is used to ensure the - // scrollable area is big enough. - d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerGap + "px; width: 1px;"); - // Will contain the gutters, if any. - d.gutters = elt("div", null, "CodeMirror-gutters"); - d.lineGutter = null; - // Actual scrollable element. - d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll"); - d.scroller.setAttribute("tabIndex", "-1"); - // The element in which the editor lives. - d.wrapper = elt("div", [d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror"); - - // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported) - if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; } - if (!webkit && !(gecko && mobile)) { d.scroller.draggable = true; } - - if (place) { - if (place.appendChild) { place.appendChild(d.wrapper); } - else { place(d.wrapper); } - } - - // Current rendered range (may be bigger than the view window). - d.viewFrom = d.viewTo = doc.first; - d.reportedViewFrom = d.reportedViewTo = doc.first; - // Information about the rendered lines. - d.view = []; - d.renderedView = null; - // Holds info about a single rendered line when it was rendered - // for measurement, while not in view. - d.externalMeasured = null; - // Empty space (in pixels) above the view - d.viewOffset = 0; - d.lastWrapHeight = d.lastWrapWidth = 0; - d.updateLineNumbers = null; - - d.nativeBarWidth = d.barHeight = d.barWidth = 0; - d.scrollbarsClipped = false; - - // Used to only resize the line number gutter when necessary (when - // the amount of lines crosses a boundary that makes its width change) - d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null; - // Set to true when a non-horizontal-scrolling line widget is - // added. As an optimization, line widget aligning is skipped when - // this is false. - d.alignWidgets = false; - - d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; - - // Tracks the maximum line length so that the horizontal scrollbar - // can be kept static when scrolling. - d.maxLine = null; - d.maxLineLength = 0; - d.maxLineChanged = false; - - // Used for measuring wheel scrolling granularity - d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null; - - // True when shift is held down. - d.shift = false; - - // Used to track whether anything happened since the context menu - // was opened. - d.selForContextMenu = null; - - d.activeTouch = null; - - d.gutterSpecs = getGutters(options.gutters, options.lineNumbers); - renderGutters(d); - - input.init(d); - } - - // Since the delta values reported on mouse wheel events are - // unstandardized between browsers and even browser versions, and - // generally horribly unpredictable, this code starts by measuring - // the scroll effect that the first few mouse wheel events have, - // and, from that, detects the way it can convert deltas to pixel - // offsets afterwards. - // - // The reason we want to know the amount a wheel event will scroll - // is that it gives us a chance to update the display before the - // actual scrolling happens, reducing flickering. - - var wheelSamples = 0, wheelPixelsPerUnit = null; - // Fill in a browser-detected starting value on browsers where we - // know one. These don't have to be accurate -- the result of them - // being wrong would just be a slight flicker on the first wheel - // scroll (if it is large enough). - if (ie) { wheelPixelsPerUnit = -.53; } - else if (gecko) { wheelPixelsPerUnit = 15; } - else if (chrome) { wheelPixelsPerUnit = -.7; } - else if (safari) { wheelPixelsPerUnit = -1/3; } - - function wheelEventDelta(e) { - var dx = e.wheelDeltaX, dy = e.wheelDeltaY; - if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) { dx = e.detail; } - if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) { dy = e.detail; } - else if (dy == null) { dy = e.wheelDelta; } - return {x: dx, y: dy} - } - function wheelEventPixels(e) { - var delta = wheelEventDelta(e); - delta.x *= wheelPixelsPerUnit; - delta.y *= wheelPixelsPerUnit; - return delta - } - - function onScrollWheel(cm, e) { - var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y; - - var display = cm.display, scroll = display.scroller; - // Quit if there's nothing to scroll here - var canScrollX = scroll.scrollWidth > scroll.clientWidth; - var canScrollY = scroll.scrollHeight > scroll.clientHeight; - if (!(dx && canScrollX || dy && canScrollY)) { return } - - // Webkit browsers on OS X abort momentum scrolls when the target - // of the scroll event is removed from the scrollable element. - // This hack (see related code in patchDisplay) makes sure the - // element is kept around. - if (dy && mac && webkit) { - outer: for (var cur = e.target, view = display.view; cur != scroll; cur = cur.parentNode) { - for (var i = 0; i < view.length; i++) { - if (view[i].node == cur) { - cm.display.currentWheelTarget = cur; - break outer - } - } - } - } - - // On some browsers, horizontal scrolling will cause redraws to - // happen before the gutter has been realigned, causing it to - // wriggle around in a most unseemly way. When we have an - // estimated pixels/delta value, we just handle horizontal - // scrolling entirely here. It'll be slightly off from native, but - // better than glitching out. - if (dx && !gecko && !presto && wheelPixelsPerUnit != null) { - if (dy && canScrollY) - { updateScrollTop(cm, Math.max(0, scroll.scrollTop + dy * wheelPixelsPerUnit)); } - setScrollLeft(cm, Math.max(0, scroll.scrollLeft + dx * wheelPixelsPerUnit)); - // Only prevent default scrolling if vertical scrolling is - // actually possible. Otherwise, it causes vertical scroll - // jitter on OSX trackpads when deltaX is small and deltaY - // is large (issue #3579) - if (!dy || (dy && canScrollY)) - { e_preventDefault(e); } - display.wheelStartX = null; // Abort measurement, if in progress - return - } - - // 'Project' the visible viewport to cover the area that is being - // scrolled into view (if we know enough to estimate it). - if (dy && wheelPixelsPerUnit != null) { - var pixels = dy * wheelPixelsPerUnit; - var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight; - if (pixels < 0) { top = Math.max(0, top + pixels - 50); } - else { bot = Math.min(cm.doc.height, bot + pixels + 50); } - updateDisplaySimple(cm, {top: top, bottom: bot}); - } - - if (wheelSamples < 20) { - if (display.wheelStartX == null) { - display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop; - display.wheelDX = dx; display.wheelDY = dy; - setTimeout(function () { - if (display.wheelStartX == null) { return } - var movedX = scroll.scrollLeft - display.wheelStartX; - var movedY = scroll.scrollTop - display.wheelStartY; - var sample = (movedY && display.wheelDY && movedY / display.wheelDY) || - (movedX && display.wheelDX && movedX / display.wheelDX); - display.wheelStartX = display.wheelStartY = null; - if (!sample) { return } - wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1); - ++wheelSamples; - }, 200); - } else { - display.wheelDX += dx; display.wheelDY += dy; - } - } - } - - // Selection objects are immutable. A new one is created every time - // the selection changes. A selection is one or more non-overlapping - // (and non-touching) ranges, sorted, and an integer that indicates - // which one is the primary selection (the one that's scrolled into - // view, that getCursor returns, etc). - var Selection = function(ranges, primIndex) { - this.ranges = ranges; - this.primIndex = primIndex; - }; - - Selection.prototype.primary = function () { return this.ranges[this.primIndex] }; - - Selection.prototype.equals = function (other) { - if (other == this) { return true } - if (other.primIndex != this.primIndex || other.ranges.length != this.ranges.length) { return false } - for (var i = 0; i < this.ranges.length; i++) { - var here = this.ranges[i], there = other.ranges[i]; - if (!equalCursorPos(here.anchor, there.anchor) || !equalCursorPos(here.head, there.head)) { return false } - } - return true - }; - - Selection.prototype.deepCopy = function () { - var out = []; - for (var i = 0; i < this.ranges.length; i++) - { out[i] = new Range(copyPos(this.ranges[i].anchor), copyPos(this.ranges[i].head)); } - return new Selection(out, this.primIndex) - }; - - Selection.prototype.somethingSelected = function () { - for (var i = 0; i < this.ranges.length; i++) - { if (!this.ranges[i].empty()) { return true } } - return false - }; - - Selection.prototype.contains = function (pos, end) { - if (!end) { end = pos; } - for (var i = 0; i < this.ranges.length; i++) { - var range = this.ranges[i]; - if (cmp(end, range.from()) >= 0 && cmp(pos, range.to()) <= 0) - { return i } - } - return -1 - }; - - var Range = function(anchor, head) { - this.anchor = anchor; this.head = head; - }; - - Range.prototype.from = function () { return minPos(this.anchor, this.head) }; - Range.prototype.to = function () { return maxPos(this.anchor, this.head) }; - Range.prototype.empty = function () { return this.head.line == this.anchor.line && this.head.ch == this.anchor.ch }; - - // Take an unsorted, potentially overlapping set of ranges, and - // build a selection out of it. 'Consumes' ranges array (modifying - // it). - function normalizeSelection(cm, ranges, primIndex) { - var mayTouch = cm && cm.options.selectionsMayTouch; - var prim = ranges[primIndex]; - ranges.sort(function (a, b) { return cmp(a.from(), b.from()); }); - primIndex = indexOf(ranges, prim); - for (var i = 1; i < ranges.length; i++) { - var cur = ranges[i], prev = ranges[i - 1]; - var diff = cmp(prev.to(), cur.from()); - if (mayTouch && !cur.empty() ? diff > 0 : diff >= 0) { - var from = minPos(prev.from(), cur.from()), to = maxPos(prev.to(), cur.to()); - var inv = prev.empty() ? cur.from() == cur.head : prev.from() == prev.head; - if (i <= primIndex) { --primIndex; } - ranges.splice(--i, 2, new Range(inv ? to : from, inv ? from : to)); - } - } - return new Selection(ranges, primIndex) - } - - function simpleSelection(anchor, head) { - return new Selection([new Range(anchor, head || anchor)], 0) - } - - // Compute the position of the end of a change (its 'to' property - // refers to the pre-change end). - function changeEnd(change) { - if (!change.text) { return change.to } - return Pos(change.from.line + change.text.length - 1, - lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0)) - } - - // Adjust a position to refer to the post-change position of the - // same text, or the end of the change if the change covers it. - function adjustForChange(pos, change) { - if (cmp(pos, change.from) < 0) { return pos } - if (cmp(pos, change.to) <= 0) { return changeEnd(change) } - - var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch; - if (pos.line == change.to.line) { ch += changeEnd(change).ch - change.to.ch; } - return Pos(line, ch) - } - - function computeSelAfterChange(doc, change) { - var out = []; - for (var i = 0; i < doc.sel.ranges.length; i++) { - var range = doc.sel.ranges[i]; - out.push(new Range(adjustForChange(range.anchor, change), - adjustForChange(range.head, change))); - } - return normalizeSelection(doc.cm, out, doc.sel.primIndex) - } - - function offsetPos(pos, old, nw) { - if (pos.line == old.line) - { return Pos(nw.line, pos.ch - old.ch + nw.ch) } - else - { return Pos(nw.line + (pos.line - old.line), pos.ch) } - } - - // Used by replaceSelections to allow moving the selection to the - // start or around the replaced test. Hint may be "start" or "around". - function computeReplacedSel(doc, changes, hint) { - var out = []; - var oldPrev = Pos(doc.first, 0), newPrev = oldPrev; - for (var i = 0; i < changes.length; i++) { - var change = changes[i]; - var from = offsetPos(change.from, oldPrev, newPrev); - var to = offsetPos(changeEnd(change), oldPrev, newPrev); - oldPrev = change.to; - newPrev = to; - if (hint == "around") { - var range = doc.sel.ranges[i], inv = cmp(range.head, range.anchor) < 0; - out[i] = new Range(inv ? to : from, inv ? from : to); - } else { - out[i] = new Range(from, from); - } - } - return new Selection(out, doc.sel.primIndex) - } - - // Used to get the editor into a consistent state again when options change. - - function loadMode(cm) { - cm.doc.mode = getMode(cm.options, cm.doc.modeOption); - resetModeState(cm); - } - - function resetModeState(cm) { - cm.doc.iter(function (line) { - if (line.stateAfter) { line.stateAfter = null; } - if (line.styles) { line.styles = null; } - }); - cm.doc.modeFrontier = cm.doc.highlightFrontier = cm.doc.first; - startWorker(cm, 100); - cm.state.modeGen++; - if (cm.curOp) { regChange(cm); } - } - - // DOCUMENT DATA STRUCTURE - - // By default, updates that start and end at the beginning of a line - // are treated specially, in order to make the association of line - // widgets and marker elements with the text behave more intuitive. - function isWholeLineUpdate(doc, change) { - return change.from.ch == 0 && change.to.ch == 0 && lst(change.text) == "" && - (!doc.cm || doc.cm.options.wholeLineUpdateBefore) - } - - // Perform a change on the document data structure. - function updateDoc(doc, change, markedSpans, estimateHeight) { - function spansFor(n) {return markedSpans ? markedSpans[n] : null} - function update(line, text, spans) { - updateLine(line, text, spans, estimateHeight); - signalLater(line, "change", line, change); - } - function linesFor(start, end) { - var result = []; - for (var i = start; i < end; ++i) - { result.push(new Line(text[i], spansFor(i), estimateHeight)); } - return result - } - - var from = change.from, to = change.to, text = change.text; - var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line); - var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line; - - // Adjust the line structure - if (change.full) { - doc.insert(0, linesFor(0, text.length)); - doc.remove(text.length, doc.size - text.length); - } else if (isWholeLineUpdate(doc, change)) { - // This is a whole-line replace. Treated specially to make - // sure line objects move the way they are supposed to. - var added = linesFor(0, text.length - 1); - update(lastLine, lastLine.text, lastSpans); - if (nlines) { doc.remove(from.line, nlines); } - if (added.length) { doc.insert(from.line, added); } - } else if (firstLine == lastLine) { - if (text.length == 1) { - update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans); - } else { - var added$1 = linesFor(1, text.length - 1); - added$1.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight)); - update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); - doc.insert(from.line + 1, added$1); - } - } else if (text.length == 1) { - update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0)); - doc.remove(from.line + 1, nlines); - } else { - update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); - update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans); - var added$2 = linesFor(1, text.length - 1); - if (nlines > 1) { doc.remove(from.line + 1, nlines - 1); } - doc.insert(from.line + 1, added$2); - } - - signalLater(doc, "change", doc, change); - } - - // Call f for all linked documents. - function linkedDocs(doc, f, sharedHistOnly) { - function propagate(doc, skip, sharedHist) { - if (doc.linked) { for (var i = 0; i < doc.linked.length; ++i) { - var rel = doc.linked[i]; - if (rel.doc == skip) { continue } - var shared = sharedHist && rel.sharedHist; - if (sharedHistOnly && !shared) { continue } - f(rel.doc, shared); - propagate(rel.doc, doc, shared); - } } - } - propagate(doc, null, true); - } - - // Attach a document to an editor. - function attachDoc(cm, doc) { - if (doc.cm) { throw new Error("This document is already in use.") } - cm.doc = doc; - doc.cm = cm; - estimateLineHeights(cm); - loadMode(cm); - setDirectionClass(cm); - if (!cm.options.lineWrapping) { findMaxLine(cm); } - cm.options.mode = doc.modeOption; - regChange(cm); - } - - function setDirectionClass(cm) { - (cm.doc.direction == "rtl" ? addClass : rmClass)(cm.display.lineDiv, "CodeMirror-rtl"); - } - - function directionChanged(cm) { - runInOp(cm, function () { - setDirectionClass(cm); - regChange(cm); - }); - } - - function History(startGen) { - // Arrays of change events and selections. Doing something adds an - // event to done and clears undo. Undoing moves events from done - // to undone, redoing moves them in the other direction. - this.done = []; this.undone = []; - this.undoDepth = Infinity; - // Used to track when changes can be merged into a single undo - // event - this.lastModTime = this.lastSelTime = 0; - this.lastOp = this.lastSelOp = null; - this.lastOrigin = this.lastSelOrigin = null; - // Used by the isClean() method - this.generation = this.maxGeneration = startGen || 1; - } - - // Create a history change event from an updateDoc-style change - // object. - function historyChangeFromChange(doc, change) { - var histChange = {from: copyPos(change.from), to: changeEnd(change), text: getBetween(doc, change.from, change.to)}; - attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); - linkedDocs(doc, function (doc) { return attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); }, true); - return histChange - } - - // Pop all selection events off the end of a history array. Stop at - // a change event. - function clearSelectionEvents(array) { - while (array.length) { - var last = lst(array); - if (last.ranges) { array.pop(); } - else { break } - } - } - - // Find the top change event in the history. Pop off selection - // events that are in the way. - function lastChangeEvent(hist, force) { - if (force) { - clearSelectionEvents(hist.done); - return lst(hist.done) - } else if (hist.done.length && !lst(hist.done).ranges) { - return lst(hist.done) - } else if (hist.done.length > 1 && !hist.done[hist.done.length - 2].ranges) { - hist.done.pop(); - return lst(hist.done) - } - } - - // Register a change in the history. Merges changes that are within - // a single operation, or are close together with an origin that - // allows merging (starting with "+") into a single event. - function addChangeToHistory(doc, change, selAfter, opId) { - var hist = doc.history; - hist.undone.length = 0; - var time = +new Date, cur; - var last; - - if ((hist.lastOp == opId || - hist.lastOrigin == change.origin && change.origin && - ((change.origin.charAt(0) == "+" && hist.lastModTime > time - (doc.cm ? doc.cm.options.historyEventDelay : 500)) || - change.origin.charAt(0) == "*")) && - (cur = lastChangeEvent(hist, hist.lastOp == opId))) { - // Merge this change into the last event - last = lst(cur.changes); - if (cmp(change.from, change.to) == 0 && cmp(change.from, last.to) == 0) { - // Optimized case for simple insertion -- don't want to add - // new changesets for every character typed - last.to = changeEnd(change); - } else { - // Add new sub-event - cur.changes.push(historyChangeFromChange(doc, change)); - } - } else { - // Can not be merged, start a new event. - var before = lst(hist.done); - if (!before || !before.ranges) - { pushSelectionToHistory(doc.sel, hist.done); } - cur = {changes: [historyChangeFromChange(doc, change)], - generation: hist.generation}; - hist.done.push(cur); - while (hist.done.length > hist.undoDepth) { - hist.done.shift(); - if (!hist.done[0].ranges) { hist.done.shift(); } - } - } - hist.done.push(selAfter); - hist.generation = ++hist.maxGeneration; - hist.lastModTime = hist.lastSelTime = time; - hist.lastOp = hist.lastSelOp = opId; - hist.lastOrigin = hist.lastSelOrigin = change.origin; - - if (!last) { signal(doc, "historyAdded"); } - } - - function selectionEventCanBeMerged(doc, origin, prev, sel) { - var ch = origin.charAt(0); - return ch == "*" || - ch == "+" && - prev.ranges.length == sel.ranges.length && - prev.somethingSelected() == sel.somethingSelected() && - new Date - doc.history.lastSelTime <= (doc.cm ? doc.cm.options.historyEventDelay : 500) - } - - // Called whenever the selection changes, sets the new selection as - // the pending selection in the history, and pushes the old pending - // selection into the 'done' array when it was significantly - // different (in number of selected ranges, emptiness, or time). - function addSelectionToHistory(doc, sel, opId, options) { - var hist = doc.history, origin = options && options.origin; - - // A new event is started when the previous origin does not match - // the current, or the origins don't allow matching. Origins - // starting with * are always merged, those starting with + are - // merged when similar and close together in time. - if (opId == hist.lastSelOp || - (origin && hist.lastSelOrigin == origin && - (hist.lastModTime == hist.lastSelTime && hist.lastOrigin == origin || - selectionEventCanBeMerged(doc, origin, lst(hist.done), sel)))) - { hist.done[hist.done.length - 1] = sel; } - else - { pushSelectionToHistory(sel, hist.done); } - - hist.lastSelTime = +new Date; - hist.lastSelOrigin = origin; - hist.lastSelOp = opId; - if (options && options.clearRedo !== false) - { clearSelectionEvents(hist.undone); } - } - - function pushSelectionToHistory(sel, dest) { - var top = lst(dest); - if (!(top && top.ranges && top.equals(sel))) - { dest.push(sel); } - } - - // Used to store marked span information in the history. - function attachLocalSpans(doc, change, from, to) { - var existing = change["spans_" + doc.id], n = 0; - doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function (line) { - if (line.markedSpans) - { (existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans; } - ++n; - }); - } - - // When un/re-doing restores text containing marked spans, those - // that have been explicitly cleared should not be restored. - function removeClearedSpans(spans) { - if (!spans) { return null } - var out; - for (var i = 0; i < spans.length; ++i) { - if (spans[i].marker.explicitlyCleared) { if (!out) { out = spans.slice(0, i); } } - else if (out) { out.push(spans[i]); } - } - return !out ? spans : out.length ? out : null - } - - // Retrieve and filter the old marked spans stored in a change event. - function getOldSpans(doc, change) { - var found = change["spans_" + doc.id]; - if (!found) { return null } - var nw = []; - for (var i = 0; i < change.text.length; ++i) - { nw.push(removeClearedSpans(found[i])); } - return nw - } - - // Used for un/re-doing changes from the history. Combines the - // result of computing the existing spans with the set of spans that - // existed in the history (so that deleting around a span and then - // undoing brings back the span). - function mergeOldSpans(doc, change) { - var old = getOldSpans(doc, change); - var stretched = stretchSpansOverChange(doc, change); - if (!old) { return stretched } - if (!stretched) { return old } - - for (var i = 0; i < old.length; ++i) { - var oldCur = old[i], stretchCur = stretched[i]; - if (oldCur && stretchCur) { - spans: for (var j = 0; j < stretchCur.length; ++j) { - var span = stretchCur[j]; - for (var k = 0; k < oldCur.length; ++k) - { if (oldCur[k].marker == span.marker) { continue spans } } - oldCur.push(span); - } - } else if (stretchCur) { - old[i] = stretchCur; - } - } - return old - } - - // Used both to provide a JSON-safe object in .getHistory, and, when - // detaching a document, to split the history in two - function copyHistoryArray(events, newGroup, instantiateSel) { - var copy = []; - for (var i = 0; i < events.length; ++i) { - var event = events[i]; - if (event.ranges) { - copy.push(instantiateSel ? Selection.prototype.deepCopy.call(event) : event); - continue - } - var changes = event.changes, newChanges = []; - copy.push({changes: newChanges}); - for (var j = 0; j < changes.length; ++j) { - var change = changes[j], m = (void 0); - newChanges.push({from: change.from, to: change.to, text: change.text}); - if (newGroup) { for (var prop in change) { if (m = prop.match(/^spans_(\d+)$/)) { - if (indexOf(newGroup, Number(m[1])) > -1) { - lst(newChanges)[prop] = change[prop]; - delete change[prop]; - } - } } } - } - } - return copy - } - - // The 'scroll' parameter given to many of these indicated whether - // the new cursor position should be scrolled into view after - // modifying the selection. - - // If shift is held or the extend flag is set, extends a range to - // include a given position (and optionally a second position). - // Otherwise, simply returns the range between the given positions. - // Used for cursor motion and such. - function extendRange(range, head, other, extend) { - if (extend) { - var anchor = range.anchor; - if (other) { - var posBefore = cmp(head, anchor) < 0; - if (posBefore != (cmp(other, anchor) < 0)) { - anchor = head; - head = other; - } else if (posBefore != (cmp(head, other) < 0)) { - head = other; - } - } - return new Range(anchor, head) - } else { - return new Range(other || head, head) - } - } - - // Extend the primary selection range, discard the rest. - function extendSelection(doc, head, other, options, extend) { - if (extend == null) { extend = doc.cm && (doc.cm.display.shift || doc.extend); } - setSelection(doc, new Selection([extendRange(doc.sel.primary(), head, other, extend)], 0), options); - } - - // Extend all selections (pos is an array of selections with length - // equal the number of selections) - function extendSelections(doc, heads, options) { - var out = []; - var extend = doc.cm && (doc.cm.display.shift || doc.extend); - for (var i = 0; i < doc.sel.ranges.length; i++) - { out[i] = extendRange(doc.sel.ranges[i], heads[i], null, extend); } - var newSel = normalizeSelection(doc.cm, out, doc.sel.primIndex); - setSelection(doc, newSel, options); - } - - // Updates a single range in the selection. - function replaceOneSelection(doc, i, range, options) { - var ranges = doc.sel.ranges.slice(0); - ranges[i] = range; - setSelection(doc, normalizeSelection(doc.cm, ranges, doc.sel.primIndex), options); - } - - // Reset the selection to a single range. - function setSimpleSelection(doc, anchor, head, options) { - setSelection(doc, simpleSelection(anchor, head), options); - } - - // Give beforeSelectionChange handlers a change to influence a - // selection update. - function filterSelectionChange(doc, sel, options) { - var obj = { - ranges: sel.ranges, - update: function(ranges) { - this.ranges = []; - for (var i = 0; i < ranges.length; i++) - { this.ranges[i] = new Range(clipPos(doc, ranges[i].anchor), - clipPos(doc, ranges[i].head)); } - }, - origin: options && options.origin - }; - signal(doc, "beforeSelectionChange", doc, obj); - if (doc.cm) { signal(doc.cm, "beforeSelectionChange", doc.cm, obj); } - if (obj.ranges != sel.ranges) { return normalizeSelection(doc.cm, obj.ranges, obj.ranges.length - 1) } - else { return sel } - } - - function setSelectionReplaceHistory(doc, sel, options) { - var done = doc.history.done, last = lst(done); - if (last && last.ranges) { - done[done.length - 1] = sel; - setSelectionNoUndo(doc, sel, options); - } else { - setSelection(doc, sel, options); - } - } - - // Set a new selection. - function setSelection(doc, sel, options) { - setSelectionNoUndo(doc, sel, options); - addSelectionToHistory(doc, doc.sel, doc.cm ? doc.cm.curOp.id : NaN, options); - } - - function setSelectionNoUndo(doc, sel, options) { - if (hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) - { sel = filterSelectionChange(doc, sel, options); } - - var bias = options && options.bias || - (cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1); - setSelectionInner(doc, skipAtomicInSelection(doc, sel, bias, true)); - - if (!(options && options.scroll === false) && doc.cm) - { ensureCursorVisible(doc.cm); } - } - - function setSelectionInner(doc, sel) { - if (sel.equals(doc.sel)) { return } - - doc.sel = sel; - - if (doc.cm) { - doc.cm.curOp.updateInput = 1; - doc.cm.curOp.selectionChanged = true; - signalCursorActivity(doc.cm); - } - signalLater(doc, "cursorActivity", doc); - } - - // Verify that the selection does not partially select any atomic - // marked ranges. - function reCheckSelection(doc) { - setSelectionInner(doc, skipAtomicInSelection(doc, doc.sel, null, false)); - } - - // Return a selection that does not partially select any atomic - // ranges. - function skipAtomicInSelection(doc, sel, bias, mayClear) { - var out; - for (var i = 0; i < sel.ranges.length; i++) { - var range = sel.ranges[i]; - var old = sel.ranges.length == doc.sel.ranges.length && doc.sel.ranges[i]; - var newAnchor = skipAtomic(doc, range.anchor, old && old.anchor, bias, mayClear); - var newHead = skipAtomic(doc, range.head, old && old.head, bias, mayClear); - if (out || newAnchor != range.anchor || newHead != range.head) { - if (!out) { out = sel.ranges.slice(0, i); } - out[i] = new Range(newAnchor, newHead); - } - } - return out ? normalizeSelection(doc.cm, out, sel.primIndex) : sel - } - - function skipAtomicInner(doc, pos, oldPos, dir, mayClear) { - var line = getLine(doc, pos.line); - if (line.markedSpans) { for (var i = 0; i < line.markedSpans.length; ++i) { - var sp = line.markedSpans[i], m = sp.marker; - - // Determine if we should prevent the cursor being placed to the left/right of an atomic marker - // Historically this was determined using the inclusiveLeft/Right option, but the new way to control it - // is with selectLeft/Right - var preventCursorLeft = ("selectLeft" in m) ? !m.selectLeft : m.inclusiveLeft; - var preventCursorRight = ("selectRight" in m) ? !m.selectRight : m.inclusiveRight; - - if ((sp.from == null || (preventCursorLeft ? sp.from <= pos.ch : sp.from < pos.ch)) && - (sp.to == null || (preventCursorRight ? sp.to >= pos.ch : sp.to > pos.ch))) { - if (mayClear) { - signal(m, "beforeCursorEnter"); - if (m.explicitlyCleared) { - if (!line.markedSpans) { break } - else {--i; continue} - } - } - if (!m.atomic) { continue } - - if (oldPos) { - var near = m.find(dir < 0 ? 1 : -1), diff = (void 0); - if (dir < 0 ? preventCursorRight : preventCursorLeft) - { near = movePos(doc, near, -dir, near && near.line == pos.line ? line : null); } - if (near && near.line == pos.line && (diff = cmp(near, oldPos)) && (dir < 0 ? diff < 0 : diff > 0)) - { return skipAtomicInner(doc, near, pos, dir, mayClear) } - } - - var far = m.find(dir < 0 ? -1 : 1); - if (dir < 0 ? preventCursorLeft : preventCursorRight) - { far = movePos(doc, far, dir, far.line == pos.line ? line : null); } - return far ? skipAtomicInner(doc, far, pos, dir, mayClear) : null - } - } } - return pos - } - - // Ensure a given position is not inside an atomic range. - function skipAtomic(doc, pos, oldPos, bias, mayClear) { - var dir = bias || 1; - var found = skipAtomicInner(doc, pos, oldPos, dir, mayClear) || - (!mayClear && skipAtomicInner(doc, pos, oldPos, dir, true)) || - skipAtomicInner(doc, pos, oldPos, -dir, mayClear) || - (!mayClear && skipAtomicInner(doc, pos, oldPos, -dir, true)); - if (!found) { - doc.cantEdit = true; - return Pos(doc.first, 0) - } - return found - } - - function movePos(doc, pos, dir, line) { - if (dir < 0 && pos.ch == 0) { - if (pos.line > doc.first) { return clipPos(doc, Pos(pos.line - 1)) } - else { return null } - } else if (dir > 0 && pos.ch == (line || getLine(doc, pos.line)).text.length) { - if (pos.line < doc.first + doc.size - 1) { return Pos(pos.line + 1, 0) } - else { return null } - } else { - return new Pos(pos.line, pos.ch + dir) - } - } - - function selectAll(cm) { - cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()), sel_dontScroll); - } - - // UPDATING - - // Allow "beforeChange" event handlers to influence a change - function filterChange(doc, change, update) { - var obj = { - canceled: false, - from: change.from, - to: change.to, - text: change.text, - origin: change.origin, - cancel: function () { return obj.canceled = true; } - }; - if (update) { obj.update = function (from, to, text, origin) { - if (from) { obj.from = clipPos(doc, from); } - if (to) { obj.to = clipPos(doc, to); } - if (text) { obj.text = text; } - if (origin !== undefined) { obj.origin = origin; } - }; } - signal(doc, "beforeChange", doc, obj); - if (doc.cm) { signal(doc.cm, "beforeChange", doc.cm, obj); } - - if (obj.canceled) { - if (doc.cm) { doc.cm.curOp.updateInput = 2; } - return null - } - return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin} - } - - // Apply a change to a document, and add it to the document's - // history, and propagating it to all linked documents. - function makeChange(doc, change, ignoreReadOnly) { - if (doc.cm) { - if (!doc.cm.curOp) { return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly) } - if (doc.cm.state.suppressEdits) { return } - } - - if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) { - change = filterChange(doc, change, true); - if (!change) { return } - } - - // Possibly split or suppress the update based on the presence - // of read-only spans in its range. - var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to); - if (split) { - for (var i = split.length - 1; i >= 0; --i) - { makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text, origin: change.origin}); } - } else { - makeChangeInner(doc, change); - } - } - - function makeChangeInner(doc, change) { - if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) { return } - var selAfter = computeSelAfterChange(doc, change); - addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN); - - makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change)); - var rebased = []; - - linkedDocs(doc, function (doc, sharedHist) { - if (!sharedHist && indexOf(rebased, doc.history) == -1) { - rebaseHist(doc.history, change); - rebased.push(doc.history); - } - makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change)); - }); - } - - // Revert a change stored in a document's history. - function makeChangeFromHistory(doc, type, allowSelectionOnly) { - var suppress = doc.cm && doc.cm.state.suppressEdits; - if (suppress && !allowSelectionOnly) { return } - - var hist = doc.history, event, selAfter = doc.sel; - var source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done; - - // Verify that there is a useable event (so that ctrl-z won't - // needlessly clear selection events) - var i = 0; - for (; i < source.length; i++) { - event = source[i]; - if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges) - { break } - } - if (i == source.length) { return } - hist.lastOrigin = hist.lastSelOrigin = null; - - for (;;) { - event = source.pop(); - if (event.ranges) { - pushSelectionToHistory(event, dest); - if (allowSelectionOnly && !event.equals(doc.sel)) { - setSelection(doc, event, {clearRedo: false}); - return - } - selAfter = event; - } else if (suppress) { - source.push(event); - return - } else { break } - } - - // Build up a reverse change object to add to the opposite history - // stack (redo when undoing, and vice versa). - var antiChanges = []; - pushSelectionToHistory(selAfter, dest); - dest.push({changes: antiChanges, generation: hist.generation}); - hist.generation = event.generation || ++hist.maxGeneration; - - var filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange"); - - var loop = function ( i ) { - var change = event.changes[i]; - change.origin = type; - if (filter && !filterChange(doc, change, false)) { - source.length = 0; - return {} - } - - antiChanges.push(historyChangeFromChange(doc, change)); - - var after = i ? computeSelAfterChange(doc, change) : lst(source); - makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change)); - if (!i && doc.cm) { doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)}); } - var rebased = []; - - // Propagate to the linked documents - linkedDocs(doc, function (doc, sharedHist) { - if (!sharedHist && indexOf(rebased, doc.history) == -1) { - rebaseHist(doc.history, change); - rebased.push(doc.history); - } - makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change)); - }); - }; - - for (var i$1 = event.changes.length - 1; i$1 >= 0; --i$1) { - var returned = loop( i$1 ); - - if ( returned ) return returned.v; - } - } - - // Sub-views need their line numbers shifted when text is added - // above or below them in the parent document. - function shiftDoc(doc, distance) { - if (distance == 0) { return } - doc.first += distance; - doc.sel = new Selection(map(doc.sel.ranges, function (range) { return new Range( - Pos(range.anchor.line + distance, range.anchor.ch), - Pos(range.head.line + distance, range.head.ch) - ); }), doc.sel.primIndex); - if (doc.cm) { - regChange(doc.cm, doc.first, doc.first - distance, distance); - for (var d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++) - { regLineChange(doc.cm, l, "gutter"); } - } - } - - // More lower-level change function, handling only a single document - // (not linked ones). - function makeChangeSingleDoc(doc, change, selAfter, spans) { - if (doc.cm && !doc.cm.curOp) - { return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans) } - - if (change.to.line < doc.first) { - shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line)); - return - } - if (change.from.line > doc.lastLine()) { return } - - // Clip the change to the size of this doc - if (change.from.line < doc.first) { - var shift = change.text.length - 1 - (doc.first - change.from.line); - shiftDoc(doc, shift); - change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch), - text: [lst(change.text)], origin: change.origin}; - } - var last = doc.lastLine(); - if (change.to.line > last) { - change = {from: change.from, to: Pos(last, getLine(doc, last).text.length), - text: [change.text[0]], origin: change.origin}; - } - - change.removed = getBetween(doc, change.from, change.to); - - if (!selAfter) { selAfter = computeSelAfterChange(doc, change); } - if (doc.cm) { makeChangeSingleDocInEditor(doc.cm, change, spans); } - else { updateDoc(doc, change, spans); } - setSelectionNoUndo(doc, selAfter, sel_dontScroll); - - if (doc.cantEdit && skipAtomic(doc, Pos(doc.firstLine(), 0))) - { doc.cantEdit = false; } - } - - // Handle the interaction of a change to a document with the editor - // that this document is part of. - function makeChangeSingleDocInEditor(cm, change, spans) { - var doc = cm.doc, display = cm.display, from = change.from, to = change.to; - - var recomputeMaxLength = false, checkWidthStart = from.line; - if (!cm.options.lineWrapping) { - checkWidthStart = lineNo(visualLine(getLine(doc, from.line))); - doc.iter(checkWidthStart, to.line + 1, function (line) { - if (line == display.maxLine) { - recomputeMaxLength = true; - return true - } - }); - } - - if (doc.sel.contains(change.from, change.to) > -1) - { signalCursorActivity(cm); } - - updateDoc(doc, change, spans, estimateHeight(cm)); - - if (!cm.options.lineWrapping) { - doc.iter(checkWidthStart, from.line + change.text.length, function (line) { - var len = lineLength(line); - if (len > display.maxLineLength) { - display.maxLine = line; - display.maxLineLength = len; - display.maxLineChanged = true; - recomputeMaxLength = false; - } - }); - if (recomputeMaxLength) { cm.curOp.updateMaxLine = true; } - } - - retreatFrontier(doc, from.line); - startWorker(cm, 400); - - var lendiff = change.text.length - (to.line - from.line) - 1; - // Remember that these lines changed, for updating the display - if (change.full) - { regChange(cm); } - else if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change)) - { regLineChange(cm, from.line, "text"); } - else - { regChange(cm, from.line, to.line + 1, lendiff); } - - var changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change"); - if (changeHandler || changesHandler) { - var obj = { - from: from, to: to, - text: change.text, - removed: change.removed, - origin: change.origin - }; - if (changeHandler) { signalLater(cm, "change", cm, obj); } - if (changesHandler) { (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj); } - } - cm.display.selForContextMenu = null; - } - - function replaceRange(doc, code, from, to, origin) { - var assign; - - if (!to) { to = from; } - if (cmp(to, from) < 0) { (assign = [to, from], from = assign[0], to = assign[1]); } - if (typeof code == "string") { code = doc.splitLines(code); } - makeChange(doc, {from: from, to: to, text: code, origin: origin}); - } - - // Rebasing/resetting history to deal with externally-sourced changes - - function rebaseHistSelSingle(pos, from, to, diff) { - if (to < pos.line) { - pos.line += diff; - } else if (from < pos.line) { - pos.line = from; - pos.ch = 0; - } - } - - // Tries to rebase an array of history events given a change in the - // document. If the change touches the same lines as the event, the - // event, and everything 'behind' it, is discarded. If the change is - // before the event, the event's positions are updated. Uses a - // copy-on-write scheme for the positions, to avoid having to - // reallocate them all on every rebase, but also avoid problems with - // shared position objects being unsafely updated. - function rebaseHistArray(array, from, to, diff) { - for (var i = 0; i < array.length; ++i) { - var sub = array[i], ok = true; - if (sub.ranges) { - if (!sub.copied) { sub = array[i] = sub.deepCopy(); sub.copied = true; } - for (var j = 0; j < sub.ranges.length; j++) { - rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff); - rebaseHistSelSingle(sub.ranges[j].head, from, to, diff); - } - continue - } - for (var j$1 = 0; j$1 < sub.changes.length; ++j$1) { - var cur = sub.changes[j$1]; - if (to < cur.from.line) { - cur.from = Pos(cur.from.line + diff, cur.from.ch); - cur.to = Pos(cur.to.line + diff, cur.to.ch); - } else if (from <= cur.to.line) { - ok = false; - break - } - } - if (!ok) { - array.splice(0, i + 1); - i = 0; - } - } - } - - function rebaseHist(hist, change) { - var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1; - rebaseHistArray(hist.done, from, to, diff); - rebaseHistArray(hist.undone, from, to, diff); - } - - // Utility for applying a change to a line by handle or number, - // returning the number and optionally registering the line as - // changed. - function changeLine(doc, handle, changeType, op) { - var no = handle, line = handle; - if (typeof handle == "number") { line = getLine(doc, clipLine(doc, handle)); } - else { no = lineNo(handle); } - if (no == null) { return null } - if (op(line, no) && doc.cm) { regLineChange(doc.cm, no, changeType); } - return line - } - - // The document is represented as a BTree consisting of leaves, with - // chunk of lines in them, and branches, with up to ten leaves or - // other branch nodes below them. The top node is always a branch - // node, and is the document object itself (meaning it has - // additional methods and properties). - // - // All nodes have parent links. The tree is used both to go from - // line numbers to line objects, and to go from objects to numbers. - // It also indexes by height, and is used to convert between height - // and line object, and to find the total height of the document. - // - // See also http://marijnhaverbeke.nl/blog/codemirror-line-tree.html - - function LeafChunk(lines) { - this.lines = lines; - this.parent = null; - var height = 0; - for (var i = 0; i < lines.length; ++i) { - lines[i].parent = this; - height += lines[i].height; - } - this.height = height; - } - - LeafChunk.prototype = { - chunkSize: function() { return this.lines.length }, - - // Remove the n lines at offset 'at'. - removeInner: function(at, n) { - for (var i = at, e = at + n; i < e; ++i) { - var line = this.lines[i]; - this.height -= line.height; - cleanUpLine(line); - signalLater(line, "delete"); - } - this.lines.splice(at, n); - }, - - // Helper used to collapse a small branch into a single leaf. - collapse: function(lines) { - lines.push.apply(lines, this.lines); - }, - - // Insert the given array of lines at offset 'at', count them as - // having the given height. - insertInner: function(at, lines, height) { - this.height += height; - this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at)); - for (var i = 0; i < lines.length; ++i) { lines[i].parent = this; } - }, - - // Used to iterate over a part of the tree. - iterN: function(at, n, op) { - for (var e = at + n; at < e; ++at) - { if (op(this.lines[at])) { return true } } - } - }; - - function BranchChunk(children) { - this.children = children; - var size = 0, height = 0; - for (var i = 0; i < children.length; ++i) { - var ch = children[i]; - size += ch.chunkSize(); height += ch.height; - ch.parent = this; - } - this.size = size; - this.height = height; - this.parent = null; - } - - BranchChunk.prototype = { - chunkSize: function() { return this.size }, - - removeInner: function(at, n) { - this.size -= n; - for (var i = 0; i < this.children.length; ++i) { - var child = this.children[i], sz = child.chunkSize(); - if (at < sz) { - var rm = Math.min(n, sz - at), oldHeight = child.height; - child.removeInner(at, rm); - this.height -= oldHeight - child.height; - if (sz == rm) { this.children.splice(i--, 1); child.parent = null; } - if ((n -= rm) == 0) { break } - at = 0; - } else { at -= sz; } - } - // If the result is smaller than 25 lines, ensure that it is a - // single leaf node. - if (this.size - n < 25 && - (this.children.length > 1 || !(this.children[0] instanceof LeafChunk))) { - var lines = []; - this.collapse(lines); - this.children = [new LeafChunk(lines)]; - this.children[0].parent = this; - } - }, - - collapse: function(lines) { - for (var i = 0; i < this.children.length; ++i) { this.children[i].collapse(lines); } - }, - - insertInner: function(at, lines, height) { - this.size += lines.length; - this.height += height; - for (var i = 0; i < this.children.length; ++i) { - var child = this.children[i], sz = child.chunkSize(); - if (at <= sz) { - child.insertInner(at, lines, height); - if (child.lines && child.lines.length > 50) { - // To avoid memory thrashing when child.lines is huge (e.g. first view of a large file), it's never spliced. - // Instead, small slices are taken. They're taken in order because sequential memory accesses are fastest. - var remaining = child.lines.length % 25 + 25; - for (var pos = remaining; pos < child.lines.length;) { - var leaf = new LeafChunk(child.lines.slice(pos, pos += 25)); - child.height -= leaf.height; - this.children.splice(++i, 0, leaf); - leaf.parent = this; - } - child.lines = child.lines.slice(0, remaining); - this.maybeSpill(); - } - break - } - at -= sz; - } - }, - - // When a node has grown, check whether it should be split. - maybeSpill: function() { - if (this.children.length <= 10) { return } - var me = this; - do { - var spilled = me.children.splice(me.children.length - 5, 5); - var sibling = new BranchChunk(spilled); - if (!me.parent) { // Become the parent node - var copy = new BranchChunk(me.children); - copy.parent = me; - me.children = [copy, sibling]; - me = copy; - } else { - me.size -= sibling.size; - me.height -= sibling.height; - var myIndex = indexOf(me.parent.children, me); - me.parent.children.splice(myIndex + 1, 0, sibling); - } - sibling.parent = me.parent; - } while (me.children.length > 10) - me.parent.maybeSpill(); - }, - - iterN: function(at, n, op) { - for (var i = 0; i < this.children.length; ++i) { - var child = this.children[i], sz = child.chunkSize(); - if (at < sz) { - var used = Math.min(n, sz - at); - if (child.iterN(at, used, op)) { return true } - if ((n -= used) == 0) { break } - at = 0; - } else { at -= sz; } - } - } - }; - - // Line widgets are block elements displayed above or below a line. - - var LineWidget = function(doc, node, options) { - if (options) { for (var opt in options) { if (options.hasOwnProperty(opt)) - { this[opt] = options[opt]; } } } - this.doc = doc; - this.node = node; - }; - - LineWidget.prototype.clear = function () { - var cm = this.doc.cm, ws = this.line.widgets, line = this.line, no = lineNo(line); - if (no == null || !ws) { return } - for (var i = 0; i < ws.length; ++i) { if (ws[i] == this) { ws.splice(i--, 1); } } - if (!ws.length) { line.widgets = null; } - var height = widgetHeight(this); - updateLineHeight(line, Math.max(0, line.height - height)); - if (cm) { - runInOp(cm, function () { - adjustScrollWhenAboveVisible(cm, line, -height); - regLineChange(cm, no, "widget"); - }); - signalLater(cm, "lineWidgetCleared", cm, this, no); - } - }; - - LineWidget.prototype.changed = function () { - var this$1 = this; - - var oldH = this.height, cm = this.doc.cm, line = this.line; - this.height = null; - var diff = widgetHeight(this) - oldH; - if (!diff) { return } - if (!lineIsHidden(this.doc, line)) { updateLineHeight(line, line.height + diff); } - if (cm) { - runInOp(cm, function () { - cm.curOp.forceUpdate = true; - adjustScrollWhenAboveVisible(cm, line, diff); - signalLater(cm, "lineWidgetChanged", cm, this$1, lineNo(line)); - }); - } - }; - eventMixin(LineWidget); - - function adjustScrollWhenAboveVisible(cm, line, diff) { - if (heightAtLine(line) < ((cm.curOp && cm.curOp.scrollTop) || cm.doc.scrollTop)) - { addToScrollTop(cm, diff); } - } - - function addLineWidget(doc, handle, node, options) { - var widget = new LineWidget(doc, node, options); - var cm = doc.cm; - if (cm && widget.noHScroll) { cm.display.alignWidgets = true; } - changeLine(doc, handle, "widget", function (line) { - var widgets = line.widgets || (line.widgets = []); - if (widget.insertAt == null) { widgets.push(widget); } - else { widgets.splice(Math.min(widgets.length - 1, Math.max(0, widget.insertAt)), 0, widget); } - widget.line = line; - if (cm && !lineIsHidden(doc, line)) { - var aboveVisible = heightAtLine(line) < doc.scrollTop; - updateLineHeight(line, line.height + widgetHeight(widget)); - if (aboveVisible) { addToScrollTop(cm, widget.height); } - cm.curOp.forceUpdate = true; - } - return true - }); - if (cm) { signalLater(cm, "lineWidgetAdded", cm, widget, typeof handle == "number" ? handle : lineNo(handle)); } - return widget - } - - // TEXTMARKERS - - // Created with markText and setBookmark methods. A TextMarker is a - // handle that can be used to clear or find a marked position in the - // document. Line objects hold arrays (markedSpans) containing - // {from, to, marker} object pointing to such marker objects, and - // indicating that such a marker is present on that line. Multiple - // lines may point to the same marker when it spans across lines. - // The spans will have null for their from/to properties when the - // marker continues beyond the start/end of the line. Markers have - // links back to the lines they currently touch. - - // Collapsed markers have unique ids, in order to be able to order - // them, which is needed for uniquely determining an outer marker - // when they overlap (they may nest, but not partially overlap). - var nextMarkerId = 0; - - var TextMarker = function(doc, type) { - this.lines = []; - this.type = type; - this.doc = doc; - this.id = ++nextMarkerId; - }; - - // Clear the marker. - TextMarker.prototype.clear = function () { - if (this.explicitlyCleared) { return } - var cm = this.doc.cm, withOp = cm && !cm.curOp; - if (withOp) { startOperation(cm); } - if (hasHandler(this, "clear")) { - var found = this.find(); - if (found) { signalLater(this, "clear", found.from, found.to); } - } - var min = null, max = null; - for (var i = 0; i < this.lines.length; ++i) { - var line = this.lines[i]; - var span = getMarkedSpanFor(line.markedSpans, this); - if (cm && !this.collapsed) { regLineChange(cm, lineNo(line), "text"); } - else if (cm) { - if (span.to != null) { max = lineNo(line); } - if (span.from != null) { min = lineNo(line); } - } - line.markedSpans = removeMarkedSpan(line.markedSpans, span); - if (span.from == null && this.collapsed && !lineIsHidden(this.doc, line) && cm) - { updateLineHeight(line, textHeight(cm.display)); } - } - if (cm && this.collapsed && !cm.options.lineWrapping) { for (var i$1 = 0; i$1 < this.lines.length; ++i$1) { - var visual = visualLine(this.lines[i$1]), len = lineLength(visual); - if (len > cm.display.maxLineLength) { - cm.display.maxLine = visual; - cm.display.maxLineLength = len; - cm.display.maxLineChanged = true; - } - } } - - if (min != null && cm && this.collapsed) { regChange(cm, min, max + 1); } - this.lines.length = 0; - this.explicitlyCleared = true; - if (this.atomic && this.doc.cantEdit) { - this.doc.cantEdit = false; - if (cm) { reCheckSelection(cm.doc); } - } - if (cm) { signalLater(cm, "markerCleared", cm, this, min, max); } - if (withOp) { endOperation(cm); } - if (this.parent) { this.parent.clear(); } - }; - - // Find the position of the marker in the document. Returns a {from, - // to} object by default. Side can be passed to get a specific side - // -- 0 (both), -1 (left), or 1 (right). When lineObj is true, the - // Pos objects returned contain a line object, rather than a line - // number (used to prevent looking up the same line twice). - TextMarker.prototype.find = function (side, lineObj) { - if (side == null && this.type == "bookmark") { side = 1; } - var from, to; - for (var i = 0; i < this.lines.length; ++i) { - var line = this.lines[i]; - var span = getMarkedSpanFor(line.markedSpans, this); - if (span.from != null) { - from = Pos(lineObj ? line : lineNo(line), span.from); - if (side == -1) { return from } - } - if (span.to != null) { - to = Pos(lineObj ? line : lineNo(line), span.to); - if (side == 1) { return to } - } - } - return from && {from: from, to: to} - }; - - // Signals that the marker's widget changed, and surrounding layout - // should be recomputed. - TextMarker.prototype.changed = function () { - var this$1 = this; - - var pos = this.find(-1, true), widget = this, cm = this.doc.cm; - if (!pos || !cm) { return } - runInOp(cm, function () { - var line = pos.line, lineN = lineNo(pos.line); - var view = findViewForLine(cm, lineN); - if (view) { - clearLineMeasurementCacheFor(view); - cm.curOp.selectionChanged = cm.curOp.forceUpdate = true; - } - cm.curOp.updateMaxLine = true; - if (!lineIsHidden(widget.doc, line) && widget.height != null) { - var oldHeight = widget.height; - widget.height = null; - var dHeight = widgetHeight(widget) - oldHeight; - if (dHeight) - { updateLineHeight(line, line.height + dHeight); } - } - signalLater(cm, "markerChanged", cm, this$1); - }); - }; - - TextMarker.prototype.attachLine = function (line) { - if (!this.lines.length && this.doc.cm) { - var op = this.doc.cm.curOp; - if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1) - { (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this); } - } - this.lines.push(line); - }; - - TextMarker.prototype.detachLine = function (line) { - this.lines.splice(indexOf(this.lines, line), 1); - if (!this.lines.length && this.doc.cm) { - var op = this.doc.cm.curOp - ;(op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this); - } - }; - eventMixin(TextMarker); - - // Create a marker, wire it up to the right lines, and - function markText(doc, from, to, options, type) { - // Shared markers (across linked documents) are handled separately - // (markTextShared will call out to this again, once per - // document). - if (options && options.shared) { return markTextShared(doc, from, to, options, type) } - // Ensure we are in an operation. - if (doc.cm && !doc.cm.curOp) { return operation(doc.cm, markText)(doc, from, to, options, type) } - - var marker = new TextMarker(doc, type), diff = cmp(from, to); - if (options) { copyObj(options, marker, false); } - // Don't connect empty markers unless clearWhenEmpty is false - if (diff > 0 || diff == 0 && marker.clearWhenEmpty !== false) - { return marker } - if (marker.replacedWith) { - // Showing up as a widget implies collapsed (widget replaces text) - marker.collapsed = true; - marker.widgetNode = eltP("span", [marker.replacedWith], "CodeMirror-widget"); - if (!options.handleMouseEvents) { marker.widgetNode.setAttribute("cm-ignore-events", "true"); } - if (options.insertLeft) { marker.widgetNode.insertLeft = true; } - } - if (marker.collapsed) { - if (conflictingCollapsedRange(doc, from.line, from, to, marker) || - from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker)) - { throw new Error("Inserting collapsed marker partially overlapping an existing one") } - seeCollapsedSpans(); - } - - if (marker.addToHistory) - { addChangeToHistory(doc, {from: from, to: to, origin: "markText"}, doc.sel, NaN); } - - var curLine = from.line, cm = doc.cm, updateMaxLine; - doc.iter(curLine, to.line + 1, function (line) { - if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(line) == cm.display.maxLine) - { updateMaxLine = true; } - if (marker.collapsed && curLine != from.line) { updateLineHeight(line, 0); } - addMarkedSpan(line, new MarkedSpan(marker, - curLine == from.line ? from.ch : null, - curLine == to.line ? to.ch : null)); - ++curLine; - }); - // lineIsHidden depends on the presence of the spans, so needs a second pass - if (marker.collapsed) { doc.iter(from.line, to.line + 1, function (line) { - if (lineIsHidden(doc, line)) { updateLineHeight(line, 0); } - }); } - - if (marker.clearOnEnter) { on(marker, "beforeCursorEnter", function () { return marker.clear(); }); } - - if (marker.readOnly) { - seeReadOnlySpans(); - if (doc.history.done.length || doc.history.undone.length) - { doc.clearHistory(); } - } - if (marker.collapsed) { - marker.id = ++nextMarkerId; - marker.atomic = true; - } - if (cm) { - // Sync editor state - if (updateMaxLine) { cm.curOp.updateMaxLine = true; } - if (marker.collapsed) - { regChange(cm, from.line, to.line + 1); } - else if (marker.className || marker.startStyle || marker.endStyle || marker.css || - marker.attributes || marker.title) - { for (var i = from.line; i <= to.line; i++) { regLineChange(cm, i, "text"); } } - if (marker.atomic) { reCheckSelection(cm.doc); } - signalLater(cm, "markerAdded", cm, marker); - } - return marker - } - - // SHARED TEXTMARKERS - - // A shared marker spans multiple linked documents. It is - // implemented as a meta-marker-object controlling multiple normal - // markers. - var SharedTextMarker = function(markers, primary) { - this.markers = markers; - this.primary = primary; - for (var i = 0; i < markers.length; ++i) - { markers[i].parent = this; } - }; - - SharedTextMarker.prototype.clear = function () { - if (this.explicitlyCleared) { return } - this.explicitlyCleared = true; - for (var i = 0; i < this.markers.length; ++i) - { this.markers[i].clear(); } - signalLater(this, "clear"); - }; - - SharedTextMarker.prototype.find = function (side, lineObj) { - return this.primary.find(side, lineObj) - }; - eventMixin(SharedTextMarker); - - function markTextShared(doc, from, to, options, type) { - options = copyObj(options); - options.shared = false; - var markers = [markText(doc, from, to, options, type)], primary = markers[0]; - var widget = options.widgetNode; - linkedDocs(doc, function (doc) { - if (widget) { options.widgetNode = widget.cloneNode(true); } - markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type)); - for (var i = 0; i < doc.linked.length; ++i) - { if (doc.linked[i].isParent) { return } } - primary = lst(markers); - }); - return new SharedTextMarker(markers, primary) - } - - function findSharedMarkers(doc) { - return doc.findMarks(Pos(doc.first, 0), doc.clipPos(Pos(doc.lastLine())), function (m) { return m.parent; }) - } - - function copySharedMarkers(doc, markers) { - for (var i = 0; i < markers.length; i++) { - var marker = markers[i], pos = marker.find(); - var mFrom = doc.clipPos(pos.from), mTo = doc.clipPos(pos.to); - if (cmp(mFrom, mTo)) { - var subMark = markText(doc, mFrom, mTo, marker.primary, marker.primary.type); - marker.markers.push(subMark); - subMark.parent = marker; - } - } - } - - function detachSharedMarkers(markers) { - var loop = function ( i ) { - var marker = markers[i], linked = [marker.primary.doc]; - linkedDocs(marker.primary.doc, function (d) { return linked.push(d); }); - for (var j = 0; j < marker.markers.length; j++) { - var subMarker = marker.markers[j]; - if (indexOf(linked, subMarker.doc) == -1) { - subMarker.parent = null; - marker.markers.splice(j--, 1); - } - } - }; - - for (var i = 0; i < markers.length; i++) loop( i ); - } - - var nextDocId = 0; - var Doc = function(text, mode, firstLine, lineSep, direction) { - if (!(this instanceof Doc)) { return new Doc(text, mode, firstLine, lineSep, direction) } - if (firstLine == null) { firstLine = 0; } - - BranchChunk.call(this, [new LeafChunk([new Line("", null)])]); - this.first = firstLine; - this.scrollTop = this.scrollLeft = 0; - this.cantEdit = false; - this.cleanGeneration = 1; - this.modeFrontier = this.highlightFrontier = firstLine; - var start = Pos(firstLine, 0); - this.sel = simpleSelection(start); - this.history = new History(null); - this.id = ++nextDocId; - this.modeOption = mode; - this.lineSep = lineSep; - this.direction = (direction == "rtl") ? "rtl" : "ltr"; - this.extend = false; - - if (typeof text == "string") { text = this.splitLines(text); } - updateDoc(this, {from: start, to: start, text: text}); - setSelection(this, simpleSelection(start), sel_dontScroll); - }; - - Doc.prototype = createObj(BranchChunk.prototype, { - constructor: Doc, - // Iterate over the document. Supports two forms -- with only one - // argument, it calls that for each line in the document. With - // three, it iterates over the range given by the first two (with - // the second being non-inclusive). - iter: function(from, to, op) { - if (op) { this.iterN(from - this.first, to - from, op); } - else { this.iterN(this.first, this.first + this.size, from); } - }, - - // Non-public interface for adding and removing lines. - insert: function(at, lines) { - var height = 0; - for (var i = 0; i < lines.length; ++i) { height += lines[i].height; } - this.insertInner(at - this.first, lines, height); - }, - remove: function(at, n) { this.removeInner(at - this.first, n); }, - - // From here, the methods are part of the public interface. Most - // are also available from CodeMirror (editor) instances. - - getValue: function(lineSep) { - var lines = getLines(this, this.first, this.first + this.size); - if (lineSep === false) { return lines } - return lines.join(lineSep || this.lineSeparator()) - }, - setValue: docMethodOp(function(code) { - var top = Pos(this.first, 0), last = this.first + this.size - 1; - makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length), - text: this.splitLines(code), origin: "setValue", full: true}, true); - if (this.cm) { scrollToCoords(this.cm, 0, 0); } - setSelection(this, simpleSelection(top), sel_dontScroll); - }), - replaceRange: function(code, from, to, origin) { - from = clipPos(this, from); - to = to ? clipPos(this, to) : from; - replaceRange(this, code, from, to, origin); - }, - getRange: function(from, to, lineSep) { - var lines = getBetween(this, clipPos(this, from), clipPos(this, to)); - if (lineSep === false) { return lines } - return lines.join(lineSep || this.lineSeparator()) - }, - - getLine: function(line) {var l = this.getLineHandle(line); return l && l.text}, - - getLineHandle: function(line) {if (isLine(this, line)) { return getLine(this, line) }}, - getLineNumber: function(line) {return lineNo(line)}, - - getLineHandleVisualStart: function(line) { - if (typeof line == "number") { line = getLine(this, line); } - return visualLine(line) - }, - - lineCount: function() {return this.size}, - firstLine: function() {return this.first}, - lastLine: function() {return this.first + this.size - 1}, - - clipPos: function(pos) {return clipPos(this, pos)}, - - getCursor: function(start) { - var range = this.sel.primary(), pos; - if (start == null || start == "head") { pos = range.head; } - else if (start == "anchor") { pos = range.anchor; } - else if (start == "end" || start == "to" || start === false) { pos = range.to(); } - else { pos = range.from(); } - return pos - }, - listSelections: function() { return this.sel.ranges }, - somethingSelected: function() {return this.sel.somethingSelected()}, - - setCursor: docMethodOp(function(line, ch, options) { - setSimpleSelection(this, clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line), null, options); - }), - setSelection: docMethodOp(function(anchor, head, options) { - setSimpleSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), options); - }), - extendSelection: docMethodOp(function(head, other, options) { - extendSelection(this, clipPos(this, head), other && clipPos(this, other), options); - }), - extendSelections: docMethodOp(function(heads, options) { - extendSelections(this, clipPosArray(this, heads), options); - }), - extendSelectionsBy: docMethodOp(function(f, options) { - var heads = map(this.sel.ranges, f); - extendSelections(this, clipPosArray(this, heads), options); - }), - setSelections: docMethodOp(function(ranges, primary, options) { - if (!ranges.length) { return } - var out = []; - for (var i = 0; i < ranges.length; i++) - { out[i] = new Range(clipPos(this, ranges[i].anchor), - clipPos(this, ranges[i].head)); } - if (primary == null) { primary = Math.min(ranges.length - 1, this.sel.primIndex); } - setSelection(this, normalizeSelection(this.cm, out, primary), options); - }), - addSelection: docMethodOp(function(anchor, head, options) { - var ranges = this.sel.ranges.slice(0); - ranges.push(new Range(clipPos(this, anchor), clipPos(this, head || anchor))); - setSelection(this, normalizeSelection(this.cm, ranges, ranges.length - 1), options); - }), - - getSelection: function(lineSep) { - var ranges = this.sel.ranges, lines; - for (var i = 0; i < ranges.length; i++) { - var sel = getBetween(this, ranges[i].from(), ranges[i].to()); - lines = lines ? lines.concat(sel) : sel; - } - if (lineSep === false) { return lines } - else { return lines.join(lineSep || this.lineSeparator()) } - }, - getSelections: function(lineSep) { - var parts = [], ranges = this.sel.ranges; - for (var i = 0; i < ranges.length; i++) { - var sel = getBetween(this, ranges[i].from(), ranges[i].to()); - if (lineSep !== false) { sel = sel.join(lineSep || this.lineSeparator()); } - parts[i] = sel; - } - return parts - }, - replaceSelection: function(code, collapse, origin) { - var dup = []; - for (var i = 0; i < this.sel.ranges.length; i++) - { dup[i] = code; } - this.replaceSelections(dup, collapse, origin || "+input"); - }, - replaceSelections: docMethodOp(function(code, collapse, origin) { - var changes = [], sel = this.sel; - for (var i = 0; i < sel.ranges.length; i++) { - var range = sel.ranges[i]; - changes[i] = {from: range.from(), to: range.to(), text: this.splitLines(code[i]), origin: origin}; - } - var newSel = collapse && collapse != "end" && computeReplacedSel(this, changes, collapse); - for (var i$1 = changes.length - 1; i$1 >= 0; i$1--) - { makeChange(this, changes[i$1]); } - if (newSel) { setSelectionReplaceHistory(this, newSel); } - else if (this.cm) { ensureCursorVisible(this.cm); } - }), - undo: docMethodOp(function() {makeChangeFromHistory(this, "undo");}), - redo: docMethodOp(function() {makeChangeFromHistory(this, "redo");}), - undoSelection: docMethodOp(function() {makeChangeFromHistory(this, "undo", true);}), - redoSelection: docMethodOp(function() {makeChangeFromHistory(this, "redo", true);}), - - setExtending: function(val) {this.extend = val;}, - getExtending: function() {return this.extend}, - - historySize: function() { - var hist = this.history, done = 0, undone = 0; - for (var i = 0; i < hist.done.length; i++) { if (!hist.done[i].ranges) { ++done; } } - for (var i$1 = 0; i$1 < hist.undone.length; i$1++) { if (!hist.undone[i$1].ranges) { ++undone; } } - return {undo: done, redo: undone} - }, - clearHistory: function() { - var this$1 = this; - - this.history = new History(this.history.maxGeneration); - linkedDocs(this, function (doc) { return doc.history = this$1.history; }, true); - }, - - markClean: function() { - this.cleanGeneration = this.changeGeneration(true); - }, - changeGeneration: function(forceSplit) { - if (forceSplit) - { this.history.lastOp = this.history.lastSelOp = this.history.lastOrigin = null; } - return this.history.generation - }, - isClean: function (gen) { - return this.history.generation == (gen || this.cleanGeneration) - }, - - getHistory: function() { - return {done: copyHistoryArray(this.history.done), - undone: copyHistoryArray(this.history.undone)} - }, - setHistory: function(histData) { - var hist = this.history = new History(this.history.maxGeneration); - hist.done = copyHistoryArray(histData.done.slice(0), null, true); - hist.undone = copyHistoryArray(histData.undone.slice(0), null, true); - }, - - setGutterMarker: docMethodOp(function(line, gutterID, value) { - return changeLine(this, line, "gutter", function (line) { - var markers = line.gutterMarkers || (line.gutterMarkers = {}); - markers[gutterID] = value; - if (!value && isEmpty(markers)) { line.gutterMarkers = null; } - return true - }) - }), - - clearGutter: docMethodOp(function(gutterID) { - var this$1 = this; - - this.iter(function (line) { - if (line.gutterMarkers && line.gutterMarkers[gutterID]) { - changeLine(this$1, line, "gutter", function () { - line.gutterMarkers[gutterID] = null; - if (isEmpty(line.gutterMarkers)) { line.gutterMarkers = null; } - return true - }); - } - }); - }), - - lineInfo: function(line) { - var n; - if (typeof line == "number") { - if (!isLine(this, line)) { return null } - n = line; - line = getLine(this, line); - if (!line) { return null } - } else { - n = lineNo(line); - if (n == null) { return null } - } - return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers, - textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass, - widgets: line.widgets} - }, - - addLineClass: docMethodOp(function(handle, where, cls) { - return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function (line) { - var prop = where == "text" ? "textClass" - : where == "background" ? "bgClass" - : where == "gutter" ? "gutterClass" : "wrapClass"; - if (!line[prop]) { line[prop] = cls; } - else if (classTest(cls).test(line[prop])) { return false } - else { line[prop] += " " + cls; } - return true - }) - }), - removeLineClass: docMethodOp(function(handle, where, cls) { - return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function (line) { - var prop = where == "text" ? "textClass" - : where == "background" ? "bgClass" - : where == "gutter" ? "gutterClass" : "wrapClass"; - var cur = line[prop]; - if (!cur) { return false } - else if (cls == null) { line[prop] = null; } - else { - var found = cur.match(classTest(cls)); - if (!found) { return false } - var end = found.index + found[0].length; - line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null; - } - return true - }) - }), - - addLineWidget: docMethodOp(function(handle, node, options) { - return addLineWidget(this, handle, node, options) - }), - removeLineWidget: function(widget) { widget.clear(); }, - - markText: function(from, to, options) { - return markText(this, clipPos(this, from), clipPos(this, to), options, options && options.type || "range") - }, - setBookmark: function(pos, options) { - var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options), - insertLeft: options && options.insertLeft, - clearWhenEmpty: false, shared: options && options.shared, - handleMouseEvents: options && options.handleMouseEvents}; - pos = clipPos(this, pos); - return markText(this, pos, pos, realOpts, "bookmark") - }, - findMarksAt: function(pos) { - pos = clipPos(this, pos); - var markers = [], spans = getLine(this, pos.line).markedSpans; - if (spans) { for (var i = 0; i < spans.length; ++i) { - var span = spans[i]; - if ((span.from == null || span.from <= pos.ch) && - (span.to == null || span.to >= pos.ch)) - { markers.push(span.marker.parent || span.marker); } - } } - return markers - }, - findMarks: function(from, to, filter) { - from = clipPos(this, from); to = clipPos(this, to); - var found = [], lineNo = from.line; - this.iter(from.line, to.line + 1, function (line) { - var spans = line.markedSpans; - if (spans) { for (var i = 0; i < spans.length; i++) { - var span = spans[i]; - if (!(span.to != null && lineNo == from.line && from.ch >= span.to || - span.from == null && lineNo != from.line || - span.from != null && lineNo == to.line && span.from >= to.ch) && - (!filter || filter(span.marker))) - { found.push(span.marker.parent || span.marker); } - } } - ++lineNo; - }); - return found - }, - getAllMarks: function() { - var markers = []; - this.iter(function (line) { - var sps = line.markedSpans; - if (sps) { for (var i = 0; i < sps.length; ++i) - { if (sps[i].from != null) { markers.push(sps[i].marker); } } } - }); - return markers - }, - - posFromIndex: function(off) { - var ch, lineNo = this.first, sepSize = this.lineSeparator().length; - this.iter(function (line) { - var sz = line.text.length + sepSize; - if (sz > off) { ch = off; return true } - off -= sz; - ++lineNo; - }); - return clipPos(this, Pos(lineNo, ch)) - }, - indexFromPos: function (coords) { - coords = clipPos(this, coords); - var index = coords.ch; - if (coords.line < this.first || coords.ch < 0) { return 0 } - var sepSize = this.lineSeparator().length; - this.iter(this.first, coords.line, function (line) { // iter aborts when callback returns a truthy value - index += line.text.length + sepSize; - }); - return index - }, - - copy: function(copyHistory) { - var doc = new Doc(getLines(this, this.first, this.first + this.size), - this.modeOption, this.first, this.lineSep, this.direction); - doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft; - doc.sel = this.sel; - doc.extend = false; - if (copyHistory) { - doc.history.undoDepth = this.history.undoDepth; - doc.setHistory(this.getHistory()); - } - return doc - }, - - linkedDoc: function(options) { - if (!options) { options = {}; } - var from = this.first, to = this.first + this.size; - if (options.from != null && options.from > from) { from = options.from; } - if (options.to != null && options.to < to) { to = options.to; } - var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from, this.lineSep, this.direction); - if (options.sharedHist) { copy.history = this.history - ; }(this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist}); - copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}]; - copySharedMarkers(copy, findSharedMarkers(this)); - return copy - }, - unlinkDoc: function(other) { - if (other instanceof CodeMirror) { other = other.doc; } - if (this.linked) { for (var i = 0; i < this.linked.length; ++i) { - var link = this.linked[i]; - if (link.doc != other) { continue } - this.linked.splice(i, 1); - other.unlinkDoc(this); - detachSharedMarkers(findSharedMarkers(this)); - break - } } - // If the histories were shared, split them again - if (other.history == this.history) { - var splitIds = [other.id]; - linkedDocs(other, function (doc) { return splitIds.push(doc.id); }, true); - other.history = new History(null); - other.history.done = copyHistoryArray(this.history.done, splitIds); - other.history.undone = copyHistoryArray(this.history.undone, splitIds); - } - }, - iterLinkedDocs: function(f) {linkedDocs(this, f);}, - - getMode: function() {return this.mode}, - getEditor: function() {return this.cm}, - - splitLines: function(str) { - if (this.lineSep) { return str.split(this.lineSep) } - return splitLinesAuto(str) - }, - lineSeparator: function() { return this.lineSep || "\n" }, - - setDirection: docMethodOp(function (dir) { - if (dir != "rtl") { dir = "ltr"; } - if (dir == this.direction) { return } - this.direction = dir; - this.iter(function (line) { return line.order = null; }); - if (this.cm) { directionChanged(this.cm); } - }) - }); - - // Public alias. - Doc.prototype.eachLine = Doc.prototype.iter; - - // Kludge to work around strange IE behavior where it'll sometimes - // re-fire a series of drag-related events right after the drop (#1551) - var lastDrop = 0; - - function onDrop(e) { - var cm = this; - clearDragCursor(cm); - if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) - { return } - e_preventDefault(e); - if (ie) { lastDrop = +new Date; } - var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files; - if (!pos || cm.isReadOnly()) { return } - // Might be a file drop, in which case we simply extract the text - // and insert it. - if (files && files.length && window.FileReader && window.File) { - var n = files.length, text = Array(n), read = 0; - var markAsReadAndPasteIfAllFilesAreRead = function () { - if (++read == n) { - operation(cm, function () { - pos = clipPos(cm.doc, pos); - var change = {from: pos, to: pos, - text: cm.doc.splitLines( - text.filter(function (t) { return t != null; }).join(cm.doc.lineSeparator())), - origin: "paste"}; - makeChange(cm.doc, change); - setSelectionReplaceHistory(cm.doc, simpleSelection(clipPos(cm.doc, pos), clipPos(cm.doc, changeEnd(change)))); - })(); - } - }; - var readTextFromFile = function (file, i) { - if (cm.options.allowDropFileTypes && - indexOf(cm.options.allowDropFileTypes, file.type) == -1) { - markAsReadAndPasteIfAllFilesAreRead(); - return - } - var reader = new FileReader; - reader.onerror = function () { return markAsReadAndPasteIfAllFilesAreRead(); }; - reader.onload = function () { - var content = reader.result; - if (/[\x00-\x08\x0e-\x1f]{2}/.test(content)) { - markAsReadAndPasteIfAllFilesAreRead(); - return - } - text[i] = content; - markAsReadAndPasteIfAllFilesAreRead(); - }; - reader.readAsText(file); - }; - for (var i = 0; i < files.length; i++) { readTextFromFile(files[i], i); } - } else { // Normal drop - // Don't do a replace if the drop happened inside of the selected text. - if (cm.state.draggingText && cm.doc.sel.contains(pos) > -1) { - cm.state.draggingText(e); - // Ensure the editor is re-focused - setTimeout(function () { return cm.display.input.focus(); }, 20); - return - } - try { - var text$1 = e.dataTransfer.getData("Text"); - if (text$1) { - var selected; - if (cm.state.draggingText && !cm.state.draggingText.copy) - { selected = cm.listSelections(); } - setSelectionNoUndo(cm.doc, simpleSelection(pos, pos)); - if (selected) { for (var i$1 = 0; i$1 < selected.length; ++i$1) - { replaceRange(cm.doc, "", selected[i$1].anchor, selected[i$1].head, "drag"); } } - cm.replaceSelection(text$1, "around", "paste"); - cm.display.input.focus(); - } - } - catch(e$1){} - } - } - - function onDragStart(cm, e) { - if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return } - if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) { return } - - e.dataTransfer.setData("Text", cm.getSelection()); - e.dataTransfer.effectAllowed = "copyMove"; - - // Use dummy image instead of default browsers image. - // Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there. - if (e.dataTransfer.setDragImage && !safari) { - var img = elt("img", null, null, "position: fixed; left: 0; top: 0;"); - img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; - if (presto) { - img.width = img.height = 1; - cm.display.wrapper.appendChild(img); - // Force a relayout, or Opera won't use our image for some obscure reason - img._top = img.offsetTop; - } - e.dataTransfer.setDragImage(img, 0, 0); - if (presto) { img.parentNode.removeChild(img); } - } - } - - function onDragOver(cm, e) { - var pos = posFromMouse(cm, e); - if (!pos) { return } - var frag = document.createDocumentFragment(); - drawSelectionCursor(cm, pos, frag); - if (!cm.display.dragCursor) { - cm.display.dragCursor = elt("div", null, "CodeMirror-cursors CodeMirror-dragcursors"); - cm.display.lineSpace.insertBefore(cm.display.dragCursor, cm.display.cursorDiv); - } - removeChildrenAndAdd(cm.display.dragCursor, frag); - } - - function clearDragCursor(cm) { - if (cm.display.dragCursor) { - cm.display.lineSpace.removeChild(cm.display.dragCursor); - cm.display.dragCursor = null; - } - } - - // These must be handled carefully, because naively registering a - // handler for each editor will cause the editors to never be - // garbage collected. - - function forEachCodeMirror(f) { - if (!document.getElementsByClassName) { return } - var byClass = document.getElementsByClassName("CodeMirror"), editors = []; - for (var i = 0; i < byClass.length; i++) { - var cm = byClass[i].CodeMirror; - if (cm) { editors.push(cm); } - } - if (editors.length) { editors[0].operation(function () { - for (var i = 0; i < editors.length; i++) { f(editors[i]); } - }); } - } - - var globalsRegistered = false; - function ensureGlobalHandlers() { - if (globalsRegistered) { return } - registerGlobalHandlers(); - globalsRegistered = true; - } - function registerGlobalHandlers() { - // When the window resizes, we need to refresh active editors. - var resizeTimer; - on(window, "resize", function () { - if (resizeTimer == null) { resizeTimer = setTimeout(function () { - resizeTimer = null; - forEachCodeMirror(onResize); - }, 100); } - }); - // When the window loses focus, we want to show the editor as blurred - on(window, "blur", function () { return forEachCodeMirror(onBlur); }); - } - // Called when the window resizes - function onResize(cm) { - var d = cm.display; - // Might be a text scaling operation, clear size caches. - d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; - d.scrollbarsClipped = false; - cm.setSize(); - } - - var keyNames = { - 3: "Pause", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", - 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", - 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", - 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", - 106: "*", 107: "=", 109: "-", 110: ".", 111: "/", 145: "ScrollLock", - 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", - 221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete", - 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert" - }; - - // Number keys - for (var i = 0; i < 10; i++) { keyNames[i + 48] = keyNames[i + 96] = String(i); } - // Alphabetic keys - for (var i$1 = 65; i$1 <= 90; i$1++) { keyNames[i$1] = String.fromCharCode(i$1); } - // Function keys - for (var i$2 = 1; i$2 <= 12; i$2++) { keyNames[i$2 + 111] = keyNames[i$2 + 63235] = "F" + i$2; } - - var keyMap = {}; - - keyMap.basic = { - "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown", - "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown", - "Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore", - "Tab": "defaultTab", "Shift-Tab": "indentAuto", - "Enter": "newlineAndIndent", "Insert": "toggleOverwrite", - "Esc": "singleSelection" - }; - // Note that the save and find-related commands aren't defined by - // default. User code or addons can define them. Unknown commands - // are simply ignored. - keyMap.pcDefault = { - "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo", - "Ctrl-Home": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Up": "goLineUp", "Ctrl-Down": "goLineDown", - "Ctrl-Left": "goGroupLeft", "Ctrl-Right": "goGroupRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd", - "Ctrl-Backspace": "delGroupBefore", "Ctrl-Delete": "delGroupAfter", "Ctrl-S": "save", "Ctrl-F": "find", - "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll", - "Ctrl-[": "indentLess", "Ctrl-]": "indentMore", - "Ctrl-U": "undoSelection", "Shift-Ctrl-U": "redoSelection", "Alt-U": "redoSelection", - "fallthrough": "basic" - }; - // Very basic readline/emacs-style bindings, which are standard on Mac. - keyMap.emacsy = { - "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", - "Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", - "Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore", - "Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars", - "Ctrl-O": "openLine" - }; - keyMap.macDefault = { - "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo", - "Cmd-Home": "goDocStart", "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft", - "Alt-Right": "goGroupRight", "Cmd-Left": "goLineLeft", "Cmd-Right": "goLineRight", "Alt-Backspace": "delGroupBefore", - "Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find", - "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll", - "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight", - "Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", "Ctrl-Up": "goDocStart", "Ctrl-Down": "goDocEnd", - "fallthrough": ["basic", "emacsy"] - }; - keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; - - // KEYMAP DISPATCH - - function normalizeKeyName(name) { - var parts = name.split(/-(?!$)/); - name = parts[parts.length - 1]; - var alt, ctrl, shift, cmd; - for (var i = 0; i < parts.length - 1; i++) { - var mod = parts[i]; - if (/^(cmd|meta|m)$/i.test(mod)) { cmd = true; } - else if (/^a(lt)?$/i.test(mod)) { alt = true; } - else if (/^(c|ctrl|control)$/i.test(mod)) { ctrl = true; } - else if (/^s(hift)?$/i.test(mod)) { shift = true; } - else { throw new Error("Unrecognized modifier name: " + mod) } - } - if (alt) { name = "Alt-" + name; } - if (ctrl) { name = "Ctrl-" + name; } - if (cmd) { name = "Cmd-" + name; } - if (shift) { name = "Shift-" + name; } - return name - } - - // This is a kludge to keep keymaps mostly working as raw objects - // (backwards compatibility) while at the same time support features - // like normalization and multi-stroke key bindings. It compiles a - // new normalized keymap, and then updates the old object to reflect - // this. - function normalizeKeyMap(keymap) { - var copy = {}; - for (var keyname in keymap) { if (keymap.hasOwnProperty(keyname)) { - var value = keymap[keyname]; - if (/^(name|fallthrough|(de|at)tach)$/.test(keyname)) { continue } - if (value == "...") { delete keymap[keyname]; continue } - - var keys = map(keyname.split(" "), normalizeKeyName); - for (var i = 0; i < keys.length; i++) { - var val = (void 0), name = (void 0); - if (i == keys.length - 1) { - name = keys.join(" "); - val = value; - } else { - name = keys.slice(0, i + 1).join(" "); - val = "..."; - } - var prev = copy[name]; - if (!prev) { copy[name] = val; } - else if (prev != val) { throw new Error("Inconsistent bindings for " + name) } - } - delete keymap[keyname]; - } } - for (var prop in copy) { keymap[prop] = copy[prop]; } - return keymap - } - - function lookupKey(key, map, handle, context) { - map = getKeyMap(map); - var found = map.call ? map.call(key, context) : map[key]; - if (found === false) { return "nothing" } - if (found === "...") { return "multi" } - if (found != null && handle(found)) { return "handled" } - - if (map.fallthrough) { - if (Object.prototype.toString.call(map.fallthrough) != "[object Array]") - { return lookupKey(key, map.fallthrough, handle, context) } - for (var i = 0; i < map.fallthrough.length; i++) { - var result = lookupKey(key, map.fallthrough[i], handle, context); - if (result) { return result } - } - } - } - - // Modifier key presses don't count as 'real' key presses for the - // purpose of keymap fallthrough. - function isModifierKey(value) { - var name = typeof value == "string" ? value : keyNames[value.keyCode]; - return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod" - } - - function addModifierNames(name, event, noShift) { - var base = name; - if (event.altKey && base != "Alt") { name = "Alt-" + name; } - if ((flipCtrlCmd ? event.metaKey : event.ctrlKey) && base != "Ctrl") { name = "Ctrl-" + name; } - if ((flipCtrlCmd ? event.ctrlKey : event.metaKey) && base != "Cmd") { name = "Cmd-" + name; } - if (!noShift && event.shiftKey && base != "Shift") { name = "Shift-" + name; } - return name - } - - // Look up the name of a key as indicated by an event object. - function keyName(event, noShift) { - if (presto && event.keyCode == 34 && event["char"]) { return false } - var name = keyNames[event.keyCode]; - if (name == null || event.altGraphKey) { return false } - // Ctrl-ScrollLock has keyCode 3, same as Ctrl-Pause, - // so we'll use event.code when available (Chrome 48+, FF 38+, Safari 10.1+) - if (event.keyCode == 3 && event.code) { name = event.code; } - return addModifierNames(name, event, noShift) - } - - function getKeyMap(val) { - return typeof val == "string" ? keyMap[val] : val - } - - // Helper for deleting text near the selection(s), used to implement - // backspace, delete, and similar functionality. - function deleteNearSelection(cm, compute) { - var ranges = cm.doc.sel.ranges, kill = []; - // Build up a set of ranges to kill first, merging overlapping - // ranges. - for (var i = 0; i < ranges.length; i++) { - var toKill = compute(ranges[i]); - while (kill.length && cmp(toKill.from, lst(kill).to) <= 0) { - var replaced = kill.pop(); - if (cmp(replaced.from, toKill.from) < 0) { - toKill.from = replaced.from; - break - } - } - kill.push(toKill); - } - // Next, remove those actual ranges. - runInOp(cm, function () { - for (var i = kill.length - 1; i >= 0; i--) - { replaceRange(cm.doc, "", kill[i].from, kill[i].to, "+delete"); } - ensureCursorVisible(cm); - }); - } - - function moveCharLogically(line, ch, dir) { - var target = skipExtendingChars(line.text, ch + dir, dir); - return target < 0 || target > line.text.length ? null : target - } - - function moveLogically(line, start, dir) { - var ch = moveCharLogically(line, start.ch, dir); - return ch == null ? null : new Pos(start.line, ch, dir < 0 ? "after" : "before") - } - - function endOfLine(visually, cm, lineObj, lineNo, dir) { - if (visually) { - if (cm.doc.direction == "rtl") { dir = -dir; } - var order = getOrder(lineObj, cm.doc.direction); - if (order) { - var part = dir < 0 ? lst(order) : order[0]; - var moveInStorageOrder = (dir < 0) == (part.level == 1); - var sticky = moveInStorageOrder ? "after" : "before"; - var ch; - // With a wrapped rtl chunk (possibly spanning multiple bidi parts), - // it could be that the last bidi part is not on the last visual line, - // since visual lines contain content order-consecutive chunks. - // Thus, in rtl, we are looking for the first (content-order) character - // in the rtl chunk that is on the last line (that is, the same line - // as the last (content-order) character). - if (part.level > 0 || cm.doc.direction == "rtl") { - var prep = prepareMeasureForLine(cm, lineObj); - ch = dir < 0 ? lineObj.text.length - 1 : 0; - var targetTop = measureCharPrepared(cm, prep, ch).top; - ch = findFirst(function (ch) { return measureCharPrepared(cm, prep, ch).top == targetTop; }, (dir < 0) == (part.level == 1) ? part.from : part.to - 1, ch); - if (sticky == "before") { ch = moveCharLogically(lineObj, ch, 1); } - } else { ch = dir < 0 ? part.to : part.from; } - return new Pos(lineNo, ch, sticky) - } - } - return new Pos(lineNo, dir < 0 ? lineObj.text.length : 0, dir < 0 ? "before" : "after") - } - - function moveVisually(cm, line, start, dir) { - var bidi = getOrder(line, cm.doc.direction); - if (!bidi) { return moveLogically(line, start, dir) } - if (start.ch >= line.text.length) { - start.ch = line.text.length; - start.sticky = "before"; - } else if (start.ch <= 0) { - start.ch = 0; - start.sticky = "after"; - } - var partPos = getBidiPartAt(bidi, start.ch, start.sticky), part = bidi[partPos]; - if (cm.doc.direction == "ltr" && part.level % 2 == 0 && (dir > 0 ? part.to > start.ch : part.from < start.ch)) { - // Case 1: We move within an ltr part in an ltr editor. Even with wrapped lines, - // nothing interesting happens. - return moveLogically(line, start, dir) - } - - var mv = function (pos, dir) { return moveCharLogically(line, pos instanceof Pos ? pos.ch : pos, dir); }; - var prep; - var getWrappedLineExtent = function (ch) { - if (!cm.options.lineWrapping) { return {begin: 0, end: line.text.length} } - prep = prep || prepareMeasureForLine(cm, line); - return wrappedLineExtentChar(cm, line, prep, ch) - }; - var wrappedLineExtent = getWrappedLineExtent(start.sticky == "before" ? mv(start, -1) : start.ch); - - if (cm.doc.direction == "rtl" || part.level == 1) { - var moveInStorageOrder = (part.level == 1) == (dir < 0); - var ch = mv(start, moveInStorageOrder ? 1 : -1); - if (ch != null && (!moveInStorageOrder ? ch >= part.from && ch >= wrappedLineExtent.begin : ch <= part.to && ch <= wrappedLineExtent.end)) { - // Case 2: We move within an rtl part or in an rtl editor on the same visual line - var sticky = moveInStorageOrder ? "before" : "after"; - return new Pos(start.line, ch, sticky) - } - } - - // Case 3: Could not move within this bidi part in this visual line, so leave - // the current bidi part - - var searchInVisualLine = function (partPos, dir, wrappedLineExtent) { - var getRes = function (ch, moveInStorageOrder) { return moveInStorageOrder - ? new Pos(start.line, mv(ch, 1), "before") - : new Pos(start.line, ch, "after"); }; - - for (; partPos >= 0 && partPos < bidi.length; partPos += dir) { - var part = bidi[partPos]; - var moveInStorageOrder = (dir > 0) == (part.level != 1); - var ch = moveInStorageOrder ? wrappedLineExtent.begin : mv(wrappedLineExtent.end, -1); - if (part.from <= ch && ch < part.to) { return getRes(ch, moveInStorageOrder) } - ch = moveInStorageOrder ? part.from : mv(part.to, -1); - if (wrappedLineExtent.begin <= ch && ch < wrappedLineExtent.end) { return getRes(ch, moveInStorageOrder) } - } - }; - - // Case 3a: Look for other bidi parts on the same visual line - var res = searchInVisualLine(partPos + dir, dir, wrappedLineExtent); - if (res) { return res } - - // Case 3b: Look for other bidi parts on the next visual line - var nextCh = dir > 0 ? wrappedLineExtent.end : mv(wrappedLineExtent.begin, -1); - if (nextCh != null && !(dir > 0 && nextCh == line.text.length)) { - res = searchInVisualLine(dir > 0 ? 0 : bidi.length - 1, dir, getWrappedLineExtent(nextCh)); - if (res) { return res } - } - - // Case 4: Nowhere to move - return null - } - - // Commands are parameter-less actions that can be performed on an - // editor, mostly used for keybindings. - var commands = { - selectAll: selectAll, - singleSelection: function (cm) { return cm.setSelection(cm.getCursor("anchor"), cm.getCursor("head"), sel_dontScroll); }, - killLine: function (cm) { return deleteNearSelection(cm, function (range) { - if (range.empty()) { - var len = getLine(cm.doc, range.head.line).text.length; - if (range.head.ch == len && range.head.line < cm.lastLine()) - { return {from: range.head, to: Pos(range.head.line + 1, 0)} } - else - { return {from: range.head, to: Pos(range.head.line, len)} } - } else { - return {from: range.from(), to: range.to()} - } - }); }, - deleteLine: function (cm) { return deleteNearSelection(cm, function (range) { return ({ - from: Pos(range.from().line, 0), - to: clipPos(cm.doc, Pos(range.to().line + 1, 0)) - }); }); }, - delLineLeft: function (cm) { return deleteNearSelection(cm, function (range) { return ({ - from: Pos(range.from().line, 0), to: range.from() - }); }); }, - delWrappedLineLeft: function (cm) { return deleteNearSelection(cm, function (range) { - var top = cm.charCoords(range.head, "div").top + 5; - var leftPos = cm.coordsChar({left: 0, top: top}, "div"); - return {from: leftPos, to: range.from()} - }); }, - delWrappedLineRight: function (cm) { return deleteNearSelection(cm, function (range) { - var top = cm.charCoords(range.head, "div").top + 5; - var rightPos = cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"); - return {from: range.from(), to: rightPos } - }); }, - undo: function (cm) { return cm.undo(); }, - redo: function (cm) { return cm.redo(); }, - undoSelection: function (cm) { return cm.undoSelection(); }, - redoSelection: function (cm) { return cm.redoSelection(); }, - goDocStart: function (cm) { return cm.extendSelection(Pos(cm.firstLine(), 0)); }, - goDocEnd: function (cm) { return cm.extendSelection(Pos(cm.lastLine())); }, - goLineStart: function (cm) { return cm.extendSelectionsBy(function (range) { return lineStart(cm, range.head.line); }, - {origin: "+move", bias: 1} - ); }, - goLineStartSmart: function (cm) { return cm.extendSelectionsBy(function (range) { return lineStartSmart(cm, range.head); }, - {origin: "+move", bias: 1} - ); }, - goLineEnd: function (cm) { return cm.extendSelectionsBy(function (range) { return lineEnd(cm, range.head.line); }, - {origin: "+move", bias: -1} - ); }, - goLineRight: function (cm) { return cm.extendSelectionsBy(function (range) { - var top = cm.cursorCoords(range.head, "div").top + 5; - return cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div") - }, sel_move); }, - goLineLeft: function (cm) { return cm.extendSelectionsBy(function (range) { - var top = cm.cursorCoords(range.head, "div").top + 5; - return cm.coordsChar({left: 0, top: top}, "div") - }, sel_move); }, - goLineLeftSmart: function (cm) { return cm.extendSelectionsBy(function (range) { - var top = cm.cursorCoords(range.head, "div").top + 5; - var pos = cm.coordsChar({left: 0, top: top}, "div"); - if (pos.ch < cm.getLine(pos.line).search(/\S/)) { return lineStartSmart(cm, range.head) } - return pos - }, sel_move); }, - goLineUp: function (cm) { return cm.moveV(-1, "line"); }, - goLineDown: function (cm) { return cm.moveV(1, "line"); }, - goPageUp: function (cm) { return cm.moveV(-1, "page"); }, - goPageDown: function (cm) { return cm.moveV(1, "page"); }, - goCharLeft: function (cm) { return cm.moveH(-1, "char"); }, - goCharRight: function (cm) { return cm.moveH(1, "char"); }, - goColumnLeft: function (cm) { return cm.moveH(-1, "column"); }, - goColumnRight: function (cm) { return cm.moveH(1, "column"); }, - goWordLeft: function (cm) { return cm.moveH(-1, "word"); }, - goGroupRight: function (cm) { return cm.moveH(1, "group"); }, - goGroupLeft: function (cm) { return cm.moveH(-1, "group"); }, - goWordRight: function (cm) { return cm.moveH(1, "word"); }, - delCharBefore: function (cm) { return cm.deleteH(-1, "char"); }, - delCharAfter: function (cm) { return cm.deleteH(1, "char"); }, - delWordBefore: function (cm) { return cm.deleteH(-1, "word"); }, - delWordAfter: function (cm) { return cm.deleteH(1, "word"); }, - delGroupBefore: function (cm) { return cm.deleteH(-1, "group"); }, - delGroupAfter: function (cm) { return cm.deleteH(1, "group"); }, - indentAuto: function (cm) { return cm.indentSelection("smart"); }, - indentMore: function (cm) { return cm.indentSelection("add"); }, - indentLess: function (cm) { return cm.indentSelection("subtract"); }, - insertTab: function (cm) { return cm.replaceSelection("\t"); }, - insertSoftTab: function (cm) { - var spaces = [], ranges = cm.listSelections(), tabSize = cm.options.tabSize; - for (var i = 0; i < ranges.length; i++) { - var pos = ranges[i].from(); - var col = countColumn(cm.getLine(pos.line), pos.ch, tabSize); - spaces.push(spaceStr(tabSize - col % tabSize)); - } - cm.replaceSelections(spaces); - }, - defaultTab: function (cm) { - if (cm.somethingSelected()) { cm.indentSelection("add"); } - else { cm.execCommand("insertTab"); } - }, - // Swap the two chars left and right of each selection's head. - // Move cursor behind the two swapped characters afterwards. - // - // Doesn't consider line feeds a character. - // Doesn't scan more than one line above to find a character. - // Doesn't do anything on an empty line. - // Doesn't do anything with non-empty selections. - transposeChars: function (cm) { return runInOp(cm, function () { - var ranges = cm.listSelections(), newSel = []; - for (var i = 0; i < ranges.length; i++) { - if (!ranges[i].empty()) { continue } - var cur = ranges[i].head, line = getLine(cm.doc, cur.line).text; - if (line) { - if (cur.ch == line.length) { cur = new Pos(cur.line, cur.ch - 1); } - if (cur.ch > 0) { - cur = new Pos(cur.line, cur.ch + 1); - cm.replaceRange(line.charAt(cur.ch - 1) + line.charAt(cur.ch - 2), - Pos(cur.line, cur.ch - 2), cur, "+transpose"); - } else if (cur.line > cm.doc.first) { - var prev = getLine(cm.doc, cur.line - 1).text; - if (prev) { - cur = new Pos(cur.line, 1); - cm.replaceRange(line.charAt(0) + cm.doc.lineSeparator() + - prev.charAt(prev.length - 1), - Pos(cur.line - 1, prev.length - 1), cur, "+transpose"); - } - } - } - newSel.push(new Range(cur, cur)); - } - cm.setSelections(newSel); - }); }, - newlineAndIndent: function (cm) { return runInOp(cm, function () { - var sels = cm.listSelections(); - for (var i = sels.length - 1; i >= 0; i--) - { cm.replaceRange(cm.doc.lineSeparator(), sels[i].anchor, sels[i].head, "+input"); } - sels = cm.listSelections(); - for (var i$1 = 0; i$1 < sels.length; i$1++) - { cm.indentLine(sels[i$1].from().line, null, true); } - ensureCursorVisible(cm); - }); }, - openLine: function (cm) { return cm.replaceSelection("\n", "start"); }, - toggleOverwrite: function (cm) { return cm.toggleOverwrite(); } - }; - - - function lineStart(cm, lineN) { - var line = getLine(cm.doc, lineN); - var visual = visualLine(line); - if (visual != line) { lineN = lineNo(visual); } - return endOfLine(true, cm, visual, lineN, 1) - } - function lineEnd(cm, lineN) { - var line = getLine(cm.doc, lineN); - var visual = visualLineEnd(line); - if (visual != line) { lineN = lineNo(visual); } - return endOfLine(true, cm, line, lineN, -1) - } - function lineStartSmart(cm, pos) { - var start = lineStart(cm, pos.line); - var line = getLine(cm.doc, start.line); - var order = getOrder(line, cm.doc.direction); - if (!order || order[0].level == 0) { - var firstNonWS = Math.max(start.ch, line.text.search(/\S/)); - var inWS = pos.line == start.line && pos.ch <= firstNonWS && pos.ch; - return Pos(start.line, inWS ? 0 : firstNonWS, start.sticky) - } - return start - } - - // Run a handler that was bound to a key. - function doHandleBinding(cm, bound, dropShift) { - if (typeof bound == "string") { - bound = commands[bound]; - if (!bound) { return false } - } - // Ensure previous input has been read, so that the handler sees a - // consistent view of the document - cm.display.input.ensurePolled(); - var prevShift = cm.display.shift, done = false; - try { - if (cm.isReadOnly()) { cm.state.suppressEdits = true; } - if (dropShift) { cm.display.shift = false; } - done = bound(cm) != Pass; - } finally { - cm.display.shift = prevShift; - cm.state.suppressEdits = false; - } - return done - } - - function lookupKeyForEditor(cm, name, handle) { - for (var i = 0; i < cm.state.keyMaps.length; i++) { - var result = lookupKey(name, cm.state.keyMaps[i], handle, cm); - if (result) { return result } - } - return (cm.options.extraKeys && lookupKey(name, cm.options.extraKeys, handle, cm)) - || lookupKey(name, cm.options.keyMap, handle, cm) - } - - // Note that, despite the name, this function is also used to check - // for bound mouse clicks. - - var stopSeq = new Delayed; - - function dispatchKey(cm, name, e, handle) { - var seq = cm.state.keySeq; - if (seq) { - if (isModifierKey(name)) { return "handled" } - if (/\'$/.test(name)) - { cm.state.keySeq = null; } - else - { stopSeq.set(50, function () { - if (cm.state.keySeq == seq) { - cm.state.keySeq = null; - cm.display.input.reset(); - } - }); } - if (dispatchKeyInner(cm, seq + " " + name, e, handle)) { return true } - } - return dispatchKeyInner(cm, name, e, handle) - } - - function dispatchKeyInner(cm, name, e, handle) { - var result = lookupKeyForEditor(cm, name, handle); - - if (result == "multi") - { cm.state.keySeq = name; } - if (result == "handled") - { signalLater(cm, "keyHandled", cm, name, e); } - - if (result == "handled" || result == "multi") { - e_preventDefault(e); - restartBlink(cm); - } - - return !!result - } - - // Handle a key from the keydown event. - function handleKeyBinding(cm, e) { - var name = keyName(e, true); - if (!name) { return false } - - if (e.shiftKey && !cm.state.keySeq) { - // First try to resolve full name (including 'Shift-'). Failing - // that, see if there is a cursor-motion command (starting with - // 'go') bound to the keyname without 'Shift-'. - return dispatchKey(cm, "Shift-" + name, e, function (b) { return doHandleBinding(cm, b, true); }) - || dispatchKey(cm, name, e, function (b) { - if (typeof b == "string" ? /^go[A-Z]/.test(b) : b.motion) - { return doHandleBinding(cm, b) } - }) - } else { - return dispatchKey(cm, name, e, function (b) { return doHandleBinding(cm, b); }) - } - } - - // Handle a key from the keypress event - function handleCharBinding(cm, e, ch) { - return dispatchKey(cm, "'" + ch + "'", e, function (b) { return doHandleBinding(cm, b, true); }) - } - - var lastStoppedKey = null; - function onKeyDown(e) { - var cm = this; - if (e.target && e.target != cm.display.input.getField()) { return } - cm.curOp.focus = activeElt(); - if (signalDOMEvent(cm, e)) { return } - // IE does strange things with escape. - if (ie && ie_version < 11 && e.keyCode == 27) { e.returnValue = false; } - var code = e.keyCode; - cm.display.shift = code == 16 || e.shiftKey; - var handled = handleKeyBinding(cm, e); - if (presto) { - lastStoppedKey = handled ? code : null; - // Opera has no cut event... we try to at least catch the key combo - if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey)) - { cm.replaceSelection("", null, "cut"); } - } - if (gecko && !mac && !handled && code == 46 && e.shiftKey && !e.ctrlKey && document.execCommand) - { document.execCommand("cut"); } - - // Turn mouse into crosshair when Alt is held on Mac. - if (code == 18 && !/\bCodeMirror-crosshair\b/.test(cm.display.lineDiv.className)) - { showCrossHair(cm); } - } - - function showCrossHair(cm) { - var lineDiv = cm.display.lineDiv; - addClass(lineDiv, "CodeMirror-crosshair"); - - function up(e) { - if (e.keyCode == 18 || !e.altKey) { - rmClass(lineDiv, "CodeMirror-crosshair"); - off(document, "keyup", up); - off(document, "mouseover", up); - } - } - on(document, "keyup", up); - on(document, "mouseover", up); - } - - function onKeyUp(e) { - if (e.keyCode == 16) { this.doc.sel.shift = false; } - signalDOMEvent(this, e); - } - - function onKeyPress(e) { - var cm = this; - if (e.target && e.target != cm.display.input.getField()) { return } - if (eventInWidget(cm.display, e) || signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) { return } - var keyCode = e.keyCode, charCode = e.charCode; - if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return} - if ((presto && (!e.which || e.which < 10)) && handleKeyBinding(cm, e)) { return } - var ch = String.fromCharCode(charCode == null ? keyCode : charCode); - // Some browsers fire keypress events for backspace - if (ch == "\x08") { return } - if (handleCharBinding(cm, e, ch)) { return } - cm.display.input.onKeyPress(e); - } - - var DOUBLECLICK_DELAY = 400; - - var PastClick = function(time, pos, button) { - this.time = time; - this.pos = pos; - this.button = button; - }; - - PastClick.prototype.compare = function (time, pos, button) { - return this.time + DOUBLECLICK_DELAY > time && - cmp(pos, this.pos) == 0 && button == this.button - }; - - var lastClick, lastDoubleClick; - function clickRepeat(pos, button) { - var now = +new Date; - if (lastDoubleClick && lastDoubleClick.compare(now, pos, button)) { - lastClick = lastDoubleClick = null; - return "triple" - } else if (lastClick && lastClick.compare(now, pos, button)) { - lastDoubleClick = new PastClick(now, pos, button); - lastClick = null; - return "double" - } else { - lastClick = new PastClick(now, pos, button); - lastDoubleClick = null; - return "single" - } - } - - // A mouse down can be a single click, double click, triple click, - // start of selection drag, start of text drag, new cursor - // (ctrl-click), rectangle drag (alt-drag), or xwin - // middle-click-paste. Or it might be a click on something we should - // not interfere with, such as a scrollbar or widget. - function onMouseDown(e) { - var cm = this, display = cm.display; - if (signalDOMEvent(cm, e) || display.activeTouch && display.input.supportsTouch()) { return } - display.input.ensurePolled(); - display.shift = e.shiftKey; - - if (eventInWidget(display, e)) { - if (!webkit) { - // Briefly turn off draggability, to allow widgets to do - // normal dragging things. - display.scroller.draggable = false; - setTimeout(function () { return display.scroller.draggable = true; }, 100); - } - return - } - if (clickInGutter(cm, e)) { return } - var pos = posFromMouse(cm, e), button = e_button(e), repeat = pos ? clickRepeat(pos, button) : "single"; - window.focus(); - - // #3261: make sure, that we're not starting a second selection - if (button == 1 && cm.state.selectingText) - { cm.state.selectingText(e); } - - if (pos && handleMappedButton(cm, button, pos, repeat, e)) { return } - - if (button == 1) { - if (pos) { leftButtonDown(cm, pos, repeat, e); } - else if (e_target(e) == display.scroller) { e_preventDefault(e); } - } else if (button == 2) { - if (pos) { extendSelection(cm.doc, pos); } - setTimeout(function () { return display.input.focus(); }, 20); - } else if (button == 3) { - if (captureRightClick) { cm.display.input.onContextMenu(e); } - else { delayBlurEvent(cm); } - } - } - - function handleMappedButton(cm, button, pos, repeat, event) { - var name = "Click"; - if (repeat == "double") { name = "Double" + name; } - else if (repeat == "triple") { name = "Triple" + name; } - name = (button == 1 ? "Left" : button == 2 ? "Middle" : "Right") + name; - - return dispatchKey(cm, addModifierNames(name, event), event, function (bound) { - if (typeof bound == "string") { bound = commands[bound]; } - if (!bound) { return false } - var done = false; - try { - if (cm.isReadOnly()) { cm.state.suppressEdits = true; } - done = bound(cm, pos) != Pass; - } finally { - cm.state.suppressEdits = false; - } - return done - }) - } - - function configureMouse(cm, repeat, event) { - var option = cm.getOption("configureMouse"); - var value = option ? option(cm, repeat, event) : {}; - if (value.unit == null) { - var rect = chromeOS ? event.shiftKey && event.metaKey : event.altKey; - value.unit = rect ? "rectangle" : repeat == "single" ? "char" : repeat == "double" ? "word" : "line"; - } - if (value.extend == null || cm.doc.extend) { value.extend = cm.doc.extend || event.shiftKey; } - if (value.addNew == null) { value.addNew = mac ? event.metaKey : event.ctrlKey; } - if (value.moveOnDrag == null) { value.moveOnDrag = !(mac ? event.altKey : event.ctrlKey); } - return value - } - - function leftButtonDown(cm, pos, repeat, event) { - if (ie) { setTimeout(bind(ensureFocus, cm), 0); } - else { cm.curOp.focus = activeElt(); } - - var behavior = configureMouse(cm, repeat, event); - - var sel = cm.doc.sel, contained; - if (cm.options.dragDrop && dragAndDrop && !cm.isReadOnly() && - repeat == "single" && (contained = sel.contains(pos)) > -1 && - (cmp((contained = sel.ranges[contained]).from(), pos) < 0 || pos.xRel > 0) && - (cmp(contained.to(), pos) > 0 || pos.xRel < 0)) - { leftButtonStartDrag(cm, event, pos, behavior); } - else - { leftButtonSelect(cm, event, pos, behavior); } - } - - // Start a text drag. When it ends, see if any dragging actually - // happen, and treat as a click if it didn't. - function leftButtonStartDrag(cm, event, pos, behavior) { - var display = cm.display, moved = false; - var dragEnd = operation(cm, function (e) { - if (webkit) { display.scroller.draggable = false; } - cm.state.draggingText = false; - off(display.wrapper.ownerDocument, "mouseup", dragEnd); - off(display.wrapper.ownerDocument, "mousemove", mouseMove); - off(display.scroller, "dragstart", dragStart); - off(display.scroller, "drop", dragEnd); - if (!moved) { - e_preventDefault(e); - if (!behavior.addNew) - { extendSelection(cm.doc, pos, null, null, behavior.extend); } - // Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081) - if ((webkit && !safari) || ie && ie_version == 9) - { setTimeout(function () {display.wrapper.ownerDocument.body.focus({preventScroll: true}); display.input.focus();}, 20); } - else - { display.input.focus(); } - } - }); - var mouseMove = function(e2) { - moved = moved || Math.abs(event.clientX - e2.clientX) + Math.abs(event.clientY - e2.clientY) >= 10; - }; - var dragStart = function () { return moved = true; }; - // Let the drag handler handle this. - if (webkit) { display.scroller.draggable = true; } - cm.state.draggingText = dragEnd; - dragEnd.copy = !behavior.moveOnDrag; - // IE's approach to draggable - if (display.scroller.dragDrop) { display.scroller.dragDrop(); } - on(display.wrapper.ownerDocument, "mouseup", dragEnd); - on(display.wrapper.ownerDocument, "mousemove", mouseMove); - on(display.scroller, "dragstart", dragStart); - on(display.scroller, "drop", dragEnd); - - delayBlurEvent(cm); - setTimeout(function () { return display.input.focus(); }, 20); - } - - function rangeForUnit(cm, pos, unit) { - if (unit == "char") { return new Range(pos, pos) } - if (unit == "word") { return cm.findWordAt(pos) } - if (unit == "line") { return new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0))) } - var result = unit(cm, pos); - return new Range(result.from, result.to) - } - - // Normal selection, as opposed to text dragging. - function leftButtonSelect(cm, event, start, behavior) { - var display = cm.display, doc = cm.doc; - e_preventDefault(event); - - var ourRange, ourIndex, startSel = doc.sel, ranges = startSel.ranges; - if (behavior.addNew && !behavior.extend) { - ourIndex = doc.sel.contains(start); - if (ourIndex > -1) - { ourRange = ranges[ourIndex]; } - else - { ourRange = new Range(start, start); } - } else { - ourRange = doc.sel.primary(); - ourIndex = doc.sel.primIndex; - } - - if (behavior.unit == "rectangle") { - if (!behavior.addNew) { ourRange = new Range(start, start); } - start = posFromMouse(cm, event, true, true); - ourIndex = -1; - } else { - var range = rangeForUnit(cm, start, behavior.unit); - if (behavior.extend) - { ourRange = extendRange(ourRange, range.anchor, range.head, behavior.extend); } - else - { ourRange = range; } - } - - if (!behavior.addNew) { - ourIndex = 0; - setSelection(doc, new Selection([ourRange], 0), sel_mouse); - startSel = doc.sel; - } else if (ourIndex == -1) { - ourIndex = ranges.length; - setSelection(doc, normalizeSelection(cm, ranges.concat([ourRange]), ourIndex), - {scroll: false, origin: "*mouse"}); - } else if (ranges.length > 1 && ranges[ourIndex].empty() && behavior.unit == "char" && !behavior.extend) { - setSelection(doc, normalizeSelection(cm, ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0), - {scroll: false, origin: "*mouse"}); - startSel = doc.sel; - } else { - replaceOneSelection(doc, ourIndex, ourRange, sel_mouse); - } - - var lastPos = start; - function extendTo(pos) { - if (cmp(lastPos, pos) == 0) { return } - lastPos = pos; - - if (behavior.unit == "rectangle") { - var ranges = [], tabSize = cm.options.tabSize; - var startCol = countColumn(getLine(doc, start.line).text, start.ch, tabSize); - var posCol = countColumn(getLine(doc, pos.line).text, pos.ch, tabSize); - var left = Math.min(startCol, posCol), right = Math.max(startCol, posCol); - for (var line = Math.min(start.line, pos.line), end = Math.min(cm.lastLine(), Math.max(start.line, pos.line)); - line <= end; line++) { - var text = getLine(doc, line).text, leftPos = findColumn(text, left, tabSize); - if (left == right) - { ranges.push(new Range(Pos(line, leftPos), Pos(line, leftPos))); } - else if (text.length > leftPos) - { ranges.push(new Range(Pos(line, leftPos), Pos(line, findColumn(text, right, tabSize)))); } - } - if (!ranges.length) { ranges.push(new Range(start, start)); } - setSelection(doc, normalizeSelection(cm, startSel.ranges.slice(0, ourIndex).concat(ranges), ourIndex), - {origin: "*mouse", scroll: false}); - cm.scrollIntoView(pos); - } else { - var oldRange = ourRange; - var range = rangeForUnit(cm, pos, behavior.unit); - var anchor = oldRange.anchor, head; - if (cmp(range.anchor, anchor) > 0) { - head = range.head; - anchor = minPos(oldRange.from(), range.anchor); - } else { - head = range.anchor; - anchor = maxPos(oldRange.to(), range.head); - } - var ranges$1 = startSel.ranges.slice(0); - ranges$1[ourIndex] = bidiSimplify(cm, new Range(clipPos(doc, anchor), head)); - setSelection(doc, normalizeSelection(cm, ranges$1, ourIndex), sel_mouse); - } - } - - var editorSize = display.wrapper.getBoundingClientRect(); - // Used to ensure timeout re-tries don't fire when another extend - // happened in the meantime (clearTimeout isn't reliable -- at - // least on Chrome, the timeouts still happen even when cleared, - // if the clear happens after their scheduled firing time). - var counter = 0; - - function extend(e) { - var curCount = ++counter; - var cur = posFromMouse(cm, e, true, behavior.unit == "rectangle"); - if (!cur) { return } - if (cmp(cur, lastPos) != 0) { - cm.curOp.focus = activeElt(); - extendTo(cur); - var visible = visibleLines(display, doc); - if (cur.line >= visible.to || cur.line < visible.from) - { setTimeout(operation(cm, function () {if (counter == curCount) { extend(e); }}), 150); } - } else { - var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0; - if (outside) { setTimeout(operation(cm, function () { - if (counter != curCount) { return } - display.scroller.scrollTop += outside; - extend(e); - }), 50); } - } - } - - function done(e) { - cm.state.selectingText = false; - counter = Infinity; - // If e is null or undefined we interpret this as someone trying - // to explicitly cancel the selection rather than the user - // letting go of the mouse button. - if (e) { - e_preventDefault(e); - display.input.focus(); - } - off(display.wrapper.ownerDocument, "mousemove", move); - off(display.wrapper.ownerDocument, "mouseup", up); - doc.history.lastSelOrigin = null; - } - - var move = operation(cm, function (e) { - if (e.buttons === 0 || !e_button(e)) { done(e); } - else { extend(e); } - }); - var up = operation(cm, done); - cm.state.selectingText = up; - on(display.wrapper.ownerDocument, "mousemove", move); - on(display.wrapper.ownerDocument, "mouseup", up); - } - - // Used when mouse-selecting to adjust the anchor to the proper side - // of a bidi jump depending on the visual position of the head. - function bidiSimplify(cm, range) { - var anchor = range.anchor; - var head = range.head; - var anchorLine = getLine(cm.doc, anchor.line); - if (cmp(anchor, head) == 0 && anchor.sticky == head.sticky) { return range } - var order = getOrder(anchorLine); - if (!order) { return range } - var index = getBidiPartAt(order, anchor.ch, anchor.sticky), part = order[index]; - if (part.from != anchor.ch && part.to != anchor.ch) { return range } - var boundary = index + ((part.from == anchor.ch) == (part.level != 1) ? 0 : 1); - if (boundary == 0 || boundary == order.length) { return range } - - // Compute the relative visual position of the head compared to the - // anchor (<0 is to the left, >0 to the right) - var leftSide; - if (head.line != anchor.line) { - leftSide = (head.line - anchor.line) * (cm.doc.direction == "ltr" ? 1 : -1) > 0; - } else { - var headIndex = getBidiPartAt(order, head.ch, head.sticky); - var dir = headIndex - index || (head.ch - anchor.ch) * (part.level == 1 ? -1 : 1); - if (headIndex == boundary - 1 || headIndex == boundary) - { leftSide = dir < 0; } - else - { leftSide = dir > 0; } - } - - var usePart = order[boundary + (leftSide ? -1 : 0)]; - var from = leftSide == (usePart.level == 1); - var ch = from ? usePart.from : usePart.to, sticky = from ? "after" : "before"; - return anchor.ch == ch && anchor.sticky == sticky ? range : new Range(new Pos(anchor.line, ch, sticky), head) - } - - - // Determines whether an event happened in the gutter, and fires the - // handlers for the corresponding event. - function gutterEvent(cm, e, type, prevent) { - var mX, mY; - if (e.touches) { - mX = e.touches[0].clientX; - mY = e.touches[0].clientY; - } else { - try { mX = e.clientX; mY = e.clientY; } - catch(e$1) { return false } - } - if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) { return false } - if (prevent) { e_preventDefault(e); } - - var display = cm.display; - var lineBox = display.lineDiv.getBoundingClientRect(); - - if (mY > lineBox.bottom || !hasHandler(cm, type)) { return e_defaultPrevented(e) } - mY -= lineBox.top - display.viewOffset; - - for (var i = 0; i < cm.display.gutterSpecs.length; ++i) { - var g = display.gutters.childNodes[i]; - if (g && g.getBoundingClientRect().right >= mX) { - var line = lineAtHeight(cm.doc, mY); - var gutter = cm.display.gutterSpecs[i]; - signal(cm, type, cm, line, gutter.className, e); - return e_defaultPrevented(e) - } - } - } - - function clickInGutter(cm, e) { - return gutterEvent(cm, e, "gutterClick", true) - } - - // CONTEXT MENU HANDLING - - // To make the context menu work, we need to briefly unhide the - // textarea (making it as unobtrusive as possible) to let the - // right-click take effect on it. - function onContextMenu(cm, e) { - if (eventInWidget(cm.display, e) || contextMenuInGutter(cm, e)) { return } - if (signalDOMEvent(cm, e, "contextmenu")) { return } - if (!captureRightClick) { cm.display.input.onContextMenu(e); } - } - - function contextMenuInGutter(cm, e) { - if (!hasHandler(cm, "gutterContextMenu")) { return false } - return gutterEvent(cm, e, "gutterContextMenu", false) - } - - function themeChanged(cm) { - cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") + - cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-"); - clearCaches(cm); - } - - var Init = {toString: function(){return "CodeMirror.Init"}}; - - var defaults = {}; - var optionHandlers = {}; - - function defineOptions(CodeMirror) { - var optionHandlers = CodeMirror.optionHandlers; - - function option(name, deflt, handle, notOnInit) { - CodeMirror.defaults[name] = deflt; - if (handle) { optionHandlers[name] = - notOnInit ? function (cm, val, old) {if (old != Init) { handle(cm, val, old); }} : handle; } - } - - CodeMirror.defineOption = option; - - // Passed to option handlers when there is no old value. - CodeMirror.Init = Init; - - // These two are, on init, called from the constructor because they - // have to be initialized before the editor can start at all. - option("value", "", function (cm, val) { return cm.setValue(val); }, true); - option("mode", null, function (cm, val) { - cm.doc.modeOption = val; - loadMode(cm); - }, true); - - option("indentUnit", 2, loadMode, true); - option("indentWithTabs", false); - option("smartIndent", true); - option("tabSize", 4, function (cm) { - resetModeState(cm); - clearCaches(cm); - regChange(cm); - }, true); - - option("lineSeparator", null, function (cm, val) { - cm.doc.lineSep = val; - if (!val) { return } - var newBreaks = [], lineNo = cm.doc.first; - cm.doc.iter(function (line) { - for (var pos = 0;;) { - var found = line.text.indexOf(val, pos); - if (found == -1) { break } - pos = found + val.length; - newBreaks.push(Pos(lineNo, found)); - } - lineNo++; - }); - for (var i = newBreaks.length - 1; i >= 0; i--) - { replaceRange(cm.doc, val, newBreaks[i], Pos(newBreaks[i].line, newBreaks[i].ch + val.length)); } - }); - option("specialChars", /[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b-\u200c\u200e\u200f\u2028\u2029\ufeff\ufff9-\ufffc]/g, function (cm, val, old) { - cm.state.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g"); - if (old != Init) { cm.refresh(); } - }); - option("specialCharPlaceholder", defaultSpecialCharPlaceholder, function (cm) { return cm.refresh(); }, true); - option("electricChars", true); - option("inputStyle", mobile ? "contenteditable" : "textarea", function () { - throw new Error("inputStyle can not (yet) be changed in a running editor") // FIXME - }, true); - option("spellcheck", false, function (cm, val) { return cm.getInputField().spellcheck = val; }, true); - option("autocorrect", false, function (cm, val) { return cm.getInputField().autocorrect = val; }, true); - option("autocapitalize", false, function (cm, val) { return cm.getInputField().autocapitalize = val; }, true); - option("rtlMoveVisually", !windows); - option("wholeLineUpdateBefore", true); - - option("theme", "default", function (cm) { - themeChanged(cm); - updateGutters(cm); - }, true); - option("keyMap", "default", function (cm, val, old) { - var next = getKeyMap(val); - var prev = old != Init && getKeyMap(old); - if (prev && prev.detach) { prev.detach(cm, next); } - if (next.attach) { next.attach(cm, prev || null); } - }); - option("extraKeys", null); - option("configureMouse", null); - - option("lineWrapping", false, wrappingChanged, true); - option("gutters", [], function (cm, val) { - cm.display.gutterSpecs = getGutters(val, cm.options.lineNumbers); - updateGutters(cm); - }, true); - option("fixedGutter", true, function (cm, val) { - cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0"; - cm.refresh(); - }, true); - option("coverGutterNextToScrollbar", false, function (cm) { return updateScrollbars(cm); }, true); - option("scrollbarStyle", "native", function (cm) { - initScrollbars(cm); - updateScrollbars(cm); - cm.display.scrollbars.setScrollTop(cm.doc.scrollTop); - cm.display.scrollbars.setScrollLeft(cm.doc.scrollLeft); - }, true); - option("lineNumbers", false, function (cm, val) { - cm.display.gutterSpecs = getGutters(cm.options.gutters, val); - updateGutters(cm); - }, true); - option("firstLineNumber", 1, updateGutters, true); - option("lineNumberFormatter", function (integer) { return integer; }, updateGutters, true); - option("showCursorWhenSelecting", false, updateSelection, true); - - option("resetSelectionOnContextMenu", true); - option("lineWiseCopyCut", true); - option("pasteLinesPerSelection", true); - option("selectionsMayTouch", false); - - option("readOnly", false, function (cm, val) { - if (val == "nocursor") { - onBlur(cm); - cm.display.input.blur(); - } - cm.display.input.readOnlyChanged(val); - }); - - option("screenReaderLabel", null, function (cm, val) { - val = (val === '') ? null : val; - cm.display.input.screenReaderLabelChanged(val); - }); - - option("disableInput", false, function (cm, val) {if (!val) { cm.display.input.reset(); }}, true); - option("dragDrop", true, dragDropChanged); - option("allowDropFileTypes", null); - - option("cursorBlinkRate", 530); - option("cursorScrollMargin", 0); - option("cursorHeight", 1, updateSelection, true); - option("singleCursorHeightPerLine", true, updateSelection, true); - option("workTime", 100); - option("workDelay", 100); - option("flattenSpans", true, resetModeState, true); - option("addModeClass", false, resetModeState, true); - option("pollInterval", 100); - option("undoDepth", 200, function (cm, val) { return cm.doc.history.undoDepth = val; }); - option("historyEventDelay", 1250); - option("viewportMargin", 10, function (cm) { return cm.refresh(); }, true); - option("maxHighlightLength", 10000, resetModeState, true); - option("moveInputWithCursor", true, function (cm, val) { - if (!val) { cm.display.input.resetPosition(); } - }); - - option("tabindex", null, function (cm, val) { return cm.display.input.getField().tabIndex = val || ""; }); - option("autofocus", null); - option("direction", "ltr", function (cm, val) { return cm.doc.setDirection(val); }, true); - option("phrases", null); - } - - function dragDropChanged(cm, value, old) { - var wasOn = old && old != Init; - if (!value != !wasOn) { - var funcs = cm.display.dragFunctions; - var toggle = value ? on : off; - toggle(cm.display.scroller, "dragstart", funcs.start); - toggle(cm.display.scroller, "dragenter", funcs.enter); - toggle(cm.display.scroller, "dragover", funcs.over); - toggle(cm.display.scroller, "dragleave", funcs.leave); - toggle(cm.display.scroller, "drop", funcs.drop); - } - } - - function wrappingChanged(cm) { - if (cm.options.lineWrapping) { - addClass(cm.display.wrapper, "CodeMirror-wrap"); - cm.display.sizer.style.minWidth = ""; - cm.display.sizerWidth = null; - } else { - rmClass(cm.display.wrapper, "CodeMirror-wrap"); - findMaxLine(cm); - } - estimateLineHeights(cm); - regChange(cm); - clearCaches(cm); - setTimeout(function () { return updateScrollbars(cm); }, 100); - } - - // A CodeMirror instance represents an editor. This is the object - // that user code is usually dealing with. - - function CodeMirror(place, options) { - var this$1 = this; - - if (!(this instanceof CodeMirror)) { return new CodeMirror(place, options) } - - this.options = options = options ? copyObj(options) : {}; - // Determine effective options based on given values and defaults. - copyObj(defaults, options, false); - - var doc = options.value; - if (typeof doc == "string") { doc = new Doc(doc, options.mode, null, options.lineSeparator, options.direction); } - else if (options.mode) { doc.modeOption = options.mode; } - this.doc = doc; - - var input = new CodeMirror.inputStyles[options.inputStyle](this); - var display = this.display = new Display(place, doc, input, options); - display.wrapper.CodeMirror = this; - themeChanged(this); - if (options.lineWrapping) - { this.display.wrapper.className += " CodeMirror-wrap"; } - initScrollbars(this); - - this.state = { - keyMaps: [], // stores maps added by addKeyMap - overlays: [], // highlighting overlays, as added by addOverlay - modeGen: 0, // bumped when mode/overlay changes, used to invalidate highlighting info - overwrite: false, - delayingBlurEvent: false, - focused: false, - suppressEdits: false, // used to disable editing during key handlers when in readOnly mode - pasteIncoming: -1, cutIncoming: -1, // help recognize paste/cut edits in input.poll - selectingText: false, - draggingText: false, - highlight: new Delayed(), // stores highlight worker timeout - keySeq: null, // Unfinished key sequence - specialChars: null - }; - - if (options.autofocus && !mobile) { display.input.focus(); } - - // Override magic textarea content restore that IE sometimes does - // on our hidden textarea on reload - if (ie && ie_version < 11) { setTimeout(function () { return this$1.display.input.reset(true); }, 20); } - - registerEventHandlers(this); - ensureGlobalHandlers(); - - startOperation(this); - this.curOp.forceUpdate = true; - attachDoc(this, doc); - - if ((options.autofocus && !mobile) || this.hasFocus()) - { setTimeout(bind(onFocus, this), 20); } - else - { onBlur(this); } - - for (var opt in optionHandlers) { if (optionHandlers.hasOwnProperty(opt)) - { optionHandlers[opt](this, options[opt], Init); } } - maybeUpdateLineNumberWidth(this); - if (options.finishInit) { options.finishInit(this); } - for (var i = 0; i < initHooks.length; ++i) { initHooks[i](this); } - endOperation(this); - // Suppress optimizelegibility in Webkit, since it breaks text - // measuring on line wrapping boundaries. - if (webkit && options.lineWrapping && - getComputedStyle(display.lineDiv).textRendering == "optimizelegibility") - { display.lineDiv.style.textRendering = "auto"; } - } - - // The default configuration options. - CodeMirror.defaults = defaults; - // Functions to run when options are changed. - CodeMirror.optionHandlers = optionHandlers; - - // Attach the necessary event handlers when initializing the editor - function registerEventHandlers(cm) { - var d = cm.display; - on(d.scroller, "mousedown", operation(cm, onMouseDown)); - // Older IE's will not fire a second mousedown for a double click - if (ie && ie_version < 11) - { on(d.scroller, "dblclick", operation(cm, function (e) { - if (signalDOMEvent(cm, e)) { return } - var pos = posFromMouse(cm, e); - if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) { return } - e_preventDefault(e); - var word = cm.findWordAt(pos); - extendSelection(cm.doc, word.anchor, word.head); - })); } - else - { on(d.scroller, "dblclick", function (e) { return signalDOMEvent(cm, e) || e_preventDefault(e); }); } - // Some browsers fire contextmenu *after* opening the menu, at - // which point we can't mess with it anymore. Context menu is - // handled in onMouseDown for these browsers. - on(d.scroller, "contextmenu", function (e) { return onContextMenu(cm, e); }); - on(d.input.getField(), "contextmenu", function (e) { - if (!d.scroller.contains(e.target)) { onContextMenu(cm, e); } - }); - - // Used to suppress mouse event handling when a touch happens - var touchFinished, prevTouch = {end: 0}; - function finishTouch() { - if (d.activeTouch) { - touchFinished = setTimeout(function () { return d.activeTouch = null; }, 1000); - prevTouch = d.activeTouch; - prevTouch.end = +new Date; - } - } - function isMouseLikeTouchEvent(e) { - if (e.touches.length != 1) { return false } - var touch = e.touches[0]; - return touch.radiusX <= 1 && touch.radiusY <= 1 - } - function farAway(touch, other) { - if (other.left == null) { return true } - var dx = other.left - touch.left, dy = other.top - touch.top; - return dx * dx + dy * dy > 20 * 20 - } - on(d.scroller, "touchstart", function (e) { - if (!signalDOMEvent(cm, e) && !isMouseLikeTouchEvent(e) && !clickInGutter(cm, e)) { - d.input.ensurePolled(); - clearTimeout(touchFinished); - var now = +new Date; - d.activeTouch = {start: now, moved: false, - prev: now - prevTouch.end <= 300 ? prevTouch : null}; - if (e.touches.length == 1) { - d.activeTouch.left = e.touches[0].pageX; - d.activeTouch.top = e.touches[0].pageY; - } - } - }); - on(d.scroller, "touchmove", function () { - if (d.activeTouch) { d.activeTouch.moved = true; } - }); - on(d.scroller, "touchend", function (e) { - var touch = d.activeTouch; - if (touch && !eventInWidget(d, e) && touch.left != null && - !touch.moved && new Date - touch.start < 300) { - var pos = cm.coordsChar(d.activeTouch, "page"), range; - if (!touch.prev || farAway(touch, touch.prev)) // Single tap - { range = new Range(pos, pos); } - else if (!touch.prev.prev || farAway(touch, touch.prev.prev)) // Double tap - { range = cm.findWordAt(pos); } - else // Triple tap - { range = new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0))); } - cm.setSelection(range.anchor, range.head); - cm.focus(); - e_preventDefault(e); - } - finishTouch(); - }); - on(d.scroller, "touchcancel", finishTouch); - - // Sync scrolling between fake scrollbars and real scrollable - // area, ensure viewport is updated when scrolling. - on(d.scroller, "scroll", function () { - if (d.scroller.clientHeight) { - updateScrollTop(cm, d.scroller.scrollTop); - setScrollLeft(cm, d.scroller.scrollLeft, true); - signal(cm, "scroll", cm); - } - }); - - // Listen to wheel events in order to try and update the viewport on time. - on(d.scroller, "mousewheel", function (e) { return onScrollWheel(cm, e); }); - on(d.scroller, "DOMMouseScroll", function (e) { return onScrollWheel(cm, e); }); - - // Prevent wrapper from ever scrolling - on(d.wrapper, "scroll", function () { return d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; }); - - d.dragFunctions = { - enter: function (e) {if (!signalDOMEvent(cm, e)) { e_stop(e); }}, - over: function (e) {if (!signalDOMEvent(cm, e)) { onDragOver(cm, e); e_stop(e); }}, - start: function (e) { return onDragStart(cm, e); }, - drop: operation(cm, onDrop), - leave: function (e) {if (!signalDOMEvent(cm, e)) { clearDragCursor(cm); }} - }; - - var inp = d.input.getField(); - on(inp, "keyup", function (e) { return onKeyUp.call(cm, e); }); - on(inp, "keydown", operation(cm, onKeyDown)); - on(inp, "keypress", operation(cm, onKeyPress)); - on(inp, "focus", function (e) { return onFocus(cm, e); }); - on(inp, "blur", function (e) { return onBlur(cm, e); }); - } - - var initHooks = []; - CodeMirror.defineInitHook = function (f) { return initHooks.push(f); }; - - // Indent the given line. The how parameter can be "smart", - // "add"/null, "subtract", or "prev". When aggressive is false - // (typically set to true for forced single-line indents), empty - // lines are not indented, and places where the mode returns Pass - // are left alone. - function indentLine(cm, n, how, aggressive) { - var doc = cm.doc, state; - if (how == null) { how = "add"; } - if (how == "smart") { - // Fall back to "prev" when the mode doesn't have an indentation - // method. - if (!doc.mode.indent) { how = "prev"; } - else { state = getContextBefore(cm, n).state; } - } - - var tabSize = cm.options.tabSize; - var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize); - if (line.stateAfter) { line.stateAfter = null; } - var curSpaceString = line.text.match(/^\s*/)[0], indentation; - if (!aggressive && !/\S/.test(line.text)) { - indentation = 0; - how = "not"; - } else if (how == "smart") { - indentation = doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text); - if (indentation == Pass || indentation > 150) { - if (!aggressive) { return } - how = "prev"; - } - } - if (how == "prev") { - if (n > doc.first) { indentation = countColumn(getLine(doc, n-1).text, null, tabSize); } - else { indentation = 0; } - } else if (how == "add") { - indentation = curSpace + cm.options.indentUnit; - } else if (how == "subtract") { - indentation = curSpace - cm.options.indentUnit; - } else if (typeof how == "number") { - indentation = curSpace + how; - } - indentation = Math.max(0, indentation); - - var indentString = "", pos = 0; - if (cm.options.indentWithTabs) - { for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";} } - if (pos < indentation) { indentString += spaceStr(indentation - pos); } - - if (indentString != curSpaceString) { - replaceRange(doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input"); - line.stateAfter = null; - return true - } else { - // Ensure that, if the cursor was in the whitespace at the start - // of the line, it is moved to the end of that space. - for (var i$1 = 0; i$1 < doc.sel.ranges.length; i$1++) { - var range = doc.sel.ranges[i$1]; - if (range.head.line == n && range.head.ch < curSpaceString.length) { - var pos$1 = Pos(n, curSpaceString.length); - replaceOneSelection(doc, i$1, new Range(pos$1, pos$1)); - break - } - } - } - } - - // This will be set to a {lineWise: bool, text: [string]} object, so - // that, when pasting, we know what kind of selections the copied - // text was made out of. - var lastCopied = null; - - function setLastCopied(newLastCopied) { - lastCopied = newLastCopied; - } - - function applyTextInput(cm, inserted, deleted, sel, origin) { - var doc = cm.doc; - cm.display.shift = false; - if (!sel) { sel = doc.sel; } - - var recent = +new Date - 200; - var paste = origin == "paste" || cm.state.pasteIncoming > recent; - var textLines = splitLinesAuto(inserted), multiPaste = null; - // When pasting N lines into N selections, insert one line per selection - if (paste && sel.ranges.length > 1) { - if (lastCopied && lastCopied.text.join("\n") == inserted) { - if (sel.ranges.length % lastCopied.text.length == 0) { - multiPaste = []; - for (var i = 0; i < lastCopied.text.length; i++) - { multiPaste.push(doc.splitLines(lastCopied.text[i])); } - } - } else if (textLines.length == sel.ranges.length && cm.options.pasteLinesPerSelection) { - multiPaste = map(textLines, function (l) { return [l]; }); - } - } - - var updateInput = cm.curOp.updateInput; - // Normal behavior is to insert the new text into every selection - for (var i$1 = sel.ranges.length - 1; i$1 >= 0; i$1--) { - var range = sel.ranges[i$1]; - var from = range.from(), to = range.to(); - if (range.empty()) { - if (deleted && deleted > 0) // Handle deletion - { from = Pos(from.line, from.ch - deleted); } - else if (cm.state.overwrite && !paste) // Handle overwrite - { to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length)); } - else if (paste && lastCopied && lastCopied.lineWise && lastCopied.text.join("\n") == textLines.join("\n")) - { from = to = Pos(from.line, 0); } - } - var changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i$1 % multiPaste.length] : textLines, - origin: origin || (paste ? "paste" : cm.state.cutIncoming > recent ? "cut" : "+input")}; - makeChange(cm.doc, changeEvent); - signalLater(cm, "inputRead", cm, changeEvent); - } - if (inserted && !paste) - { triggerElectric(cm, inserted); } - - ensureCursorVisible(cm); - if (cm.curOp.updateInput < 2) { cm.curOp.updateInput = updateInput; } - cm.curOp.typing = true; - cm.state.pasteIncoming = cm.state.cutIncoming = -1; - } - - function handlePaste(e, cm) { - var pasted = e.clipboardData && e.clipboardData.getData("Text"); - if (pasted) { - e.preventDefault(); - if (!cm.isReadOnly() && !cm.options.disableInput) - { runInOp(cm, function () { return applyTextInput(cm, pasted, 0, null, "paste"); }); } - return true - } - } - - function triggerElectric(cm, inserted) { - // When an 'electric' character is inserted, immediately trigger a reindent - if (!cm.options.electricChars || !cm.options.smartIndent) { return } - var sel = cm.doc.sel; - - for (var i = sel.ranges.length - 1; i >= 0; i--) { - var range = sel.ranges[i]; - if (range.head.ch > 100 || (i && sel.ranges[i - 1].head.line == range.head.line)) { continue } - var mode = cm.getModeAt(range.head); - var indented = false; - if (mode.electricChars) { - for (var j = 0; j < mode.electricChars.length; j++) - { if (inserted.indexOf(mode.electricChars.charAt(j)) > -1) { - indented = indentLine(cm, range.head.line, "smart"); - break - } } - } else if (mode.electricInput) { - if (mode.electricInput.test(getLine(cm.doc, range.head.line).text.slice(0, range.head.ch))) - { indented = indentLine(cm, range.head.line, "smart"); } - } - if (indented) { signalLater(cm, "electricInput", cm, range.head.line); } - } - } - - function copyableRanges(cm) { - var text = [], ranges = []; - for (var i = 0; i < cm.doc.sel.ranges.length; i++) { - var line = cm.doc.sel.ranges[i].head.line; - var lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)}; - ranges.push(lineRange); - text.push(cm.getRange(lineRange.anchor, lineRange.head)); - } - return {text: text, ranges: ranges} - } - - function disableBrowserMagic(field, spellcheck, autocorrect, autocapitalize) { - field.setAttribute("autocorrect", autocorrect ? "" : "off"); - field.setAttribute("autocapitalize", autocapitalize ? "" : "off"); - field.setAttribute("spellcheck", !!spellcheck); - } - - function hiddenTextarea() { - var te = elt("textarea", null, null, "position: absolute; bottom: -1em; padding: 0; width: 1px; height: 1em; outline: none"); - var div = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); - // The textarea is kept positioned near the cursor to prevent the - // fact that it'll be scrolled into view on input from scrolling - // our fake cursor out of view. On webkit, when wrap=off, paste is - // very slow. So make the area wide instead. - if (webkit) { te.style.width = "1000px"; } - else { te.setAttribute("wrap", "off"); } - // If border: 0; -- iOS fails to open keyboard (issue #1287) - if (ios) { te.style.border = "1px solid black"; } - disableBrowserMagic(te); - return div - } - - // The publicly visible API. Note that methodOp(f) means - // 'wrap f in an operation, performed on its `this` parameter'. - - // This is not the complete set of editor methods. Most of the - // methods defined on the Doc type are also injected into - // CodeMirror.prototype, for backwards compatibility and - // convenience. - - function addEditorMethods(CodeMirror) { - var optionHandlers = CodeMirror.optionHandlers; - - var helpers = CodeMirror.helpers = {}; - - CodeMirror.prototype = { - constructor: CodeMirror, - focus: function(){window.focus(); this.display.input.focus();}, - - setOption: function(option, value) { - var options = this.options, old = options[option]; - if (options[option] == value && option != "mode") { return } - options[option] = value; - if (optionHandlers.hasOwnProperty(option)) - { operation(this, optionHandlers[option])(this, value, old); } - signal(this, "optionChange", this, option); - }, - - getOption: function(option) {return this.options[option]}, - getDoc: function() {return this.doc}, - - addKeyMap: function(map, bottom) { - this.state.keyMaps[bottom ? "push" : "unshift"](getKeyMap(map)); - }, - removeKeyMap: function(map) { - var maps = this.state.keyMaps; - for (var i = 0; i < maps.length; ++i) - { if (maps[i] == map || maps[i].name == map) { - maps.splice(i, 1); - return true - } } - }, - - addOverlay: methodOp(function(spec, options) { - var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec); - if (mode.startState) { throw new Error("Overlays may not be stateful.") } - insertSorted(this.state.overlays, - {mode: mode, modeSpec: spec, opaque: options && options.opaque, - priority: (options && options.priority) || 0}, - function (overlay) { return overlay.priority; }); - this.state.modeGen++; - regChange(this); - }), - removeOverlay: methodOp(function(spec) { - var overlays = this.state.overlays; - for (var i = 0; i < overlays.length; ++i) { - var cur = overlays[i].modeSpec; - if (cur == spec || typeof spec == "string" && cur.name == spec) { - overlays.splice(i, 1); - this.state.modeGen++; - regChange(this); - return - } - } - }), - - indentLine: methodOp(function(n, dir, aggressive) { - if (typeof dir != "string" && typeof dir != "number") { - if (dir == null) { dir = this.options.smartIndent ? "smart" : "prev"; } - else { dir = dir ? "add" : "subtract"; } - } - if (isLine(this.doc, n)) { indentLine(this, n, dir, aggressive); } - }), - indentSelection: methodOp(function(how) { - var ranges = this.doc.sel.ranges, end = -1; - for (var i = 0; i < ranges.length; i++) { - var range = ranges[i]; - if (!range.empty()) { - var from = range.from(), to = range.to(); - var start = Math.max(end, from.line); - end = Math.min(this.lastLine(), to.line - (to.ch ? 0 : 1)) + 1; - for (var j = start; j < end; ++j) - { indentLine(this, j, how); } - var newRanges = this.doc.sel.ranges; - if (from.ch == 0 && ranges.length == newRanges.length && newRanges[i].from().ch > 0) - { replaceOneSelection(this.doc, i, new Range(from, newRanges[i].to()), sel_dontScroll); } - } else if (range.head.line > end) { - indentLine(this, range.head.line, how, true); - end = range.head.line; - if (i == this.doc.sel.primIndex) { ensureCursorVisible(this); } - } - } - }), - - // Fetch the parser token for a given character. Useful for hacks - // that want to inspect the mode state (say, for completion). - getTokenAt: function(pos, precise) { - return takeToken(this, pos, precise) - }, - - getLineTokens: function(line, precise) { - return takeToken(this, Pos(line), precise, true) - }, - - getTokenTypeAt: function(pos) { - pos = clipPos(this.doc, pos); - var styles = getLineStyles(this, getLine(this.doc, pos.line)); - var before = 0, after = (styles.length - 1) / 2, ch = pos.ch; - var type; - if (ch == 0) { type = styles[2]; } - else { for (;;) { - var mid = (before + after) >> 1; - if ((mid ? styles[mid * 2 - 1] : 0) >= ch) { after = mid; } - else if (styles[mid * 2 + 1] < ch) { before = mid + 1; } - else { type = styles[mid * 2 + 2]; break } - } } - var cut = type ? type.indexOf("overlay ") : -1; - return cut < 0 ? type : cut == 0 ? null : type.slice(0, cut - 1) - }, - - getModeAt: function(pos) { - var mode = this.doc.mode; - if (!mode.innerMode) { return mode } - return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode - }, - - getHelper: function(pos, type) { - return this.getHelpers(pos, type)[0] - }, - - getHelpers: function(pos, type) { - var found = []; - if (!helpers.hasOwnProperty(type)) { return found } - var help = helpers[type], mode = this.getModeAt(pos); - if (typeof mode[type] == "string") { - if (help[mode[type]]) { found.push(help[mode[type]]); } - } else if (mode[type]) { - for (var i = 0; i < mode[type].length; i++) { - var val = help[mode[type][i]]; - if (val) { found.push(val); } - } - } else if (mode.helperType && help[mode.helperType]) { - found.push(help[mode.helperType]); - } else if (help[mode.name]) { - found.push(help[mode.name]); - } - for (var i$1 = 0; i$1 < help._global.length; i$1++) { - var cur = help._global[i$1]; - if (cur.pred(mode, this) && indexOf(found, cur.val) == -1) - { found.push(cur.val); } - } - return found - }, - - getStateAfter: function(line, precise) { - var doc = this.doc; - line = clipLine(doc, line == null ? doc.first + doc.size - 1: line); - return getContextBefore(this, line + 1, precise).state - }, - - cursorCoords: function(start, mode) { - var pos, range = this.doc.sel.primary(); - if (start == null) { pos = range.head; } - else if (typeof start == "object") { pos = clipPos(this.doc, start); } - else { pos = start ? range.from() : range.to(); } - return cursorCoords(this, pos, mode || "page") - }, - - charCoords: function(pos, mode) { - return charCoords(this, clipPos(this.doc, pos), mode || "page") - }, - - coordsChar: function(coords, mode) { - coords = fromCoordSystem(this, coords, mode || "page"); - return coordsChar(this, coords.left, coords.top) - }, - - lineAtHeight: function(height, mode) { - height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top; - return lineAtHeight(this.doc, height + this.display.viewOffset) - }, - heightAtLine: function(line, mode, includeWidgets) { - var end = false, lineObj; - if (typeof line == "number") { - var last = this.doc.first + this.doc.size - 1; - if (line < this.doc.first) { line = this.doc.first; } - else if (line > last) { line = last; end = true; } - lineObj = getLine(this.doc, line); - } else { - lineObj = line; - } - return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || "page", includeWidgets || end).top + - (end ? this.doc.height - heightAtLine(lineObj) : 0) - }, - - defaultTextHeight: function() { return textHeight(this.display) }, - defaultCharWidth: function() { return charWidth(this.display) }, - - getViewport: function() { return {from: this.display.viewFrom, to: this.display.viewTo}}, - - addWidget: function(pos, node, scroll, vert, horiz) { - var display = this.display; - pos = cursorCoords(this, clipPos(this.doc, pos)); - var top = pos.bottom, left = pos.left; - node.style.position = "absolute"; - node.setAttribute("cm-ignore-events", "true"); - this.display.input.setUneditable(node); - display.sizer.appendChild(node); - if (vert == "over") { - top = pos.top; - } else if (vert == "above" || vert == "near") { - var vspace = Math.max(display.wrapper.clientHeight, this.doc.height), - hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth); - // Default to positioning above (if specified and possible); otherwise default to positioning below - if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight) - { top = pos.top - node.offsetHeight; } - else if (pos.bottom + node.offsetHeight <= vspace) - { top = pos.bottom; } - if (left + node.offsetWidth > hspace) - { left = hspace - node.offsetWidth; } - } - node.style.top = top + "px"; - node.style.left = node.style.right = ""; - if (horiz == "right") { - left = display.sizer.clientWidth - node.offsetWidth; - node.style.right = "0px"; - } else { - if (horiz == "left") { left = 0; } - else if (horiz == "middle") { left = (display.sizer.clientWidth - node.offsetWidth) / 2; } - node.style.left = left + "px"; - } - if (scroll) - { scrollIntoView(this, {left: left, top: top, right: left + node.offsetWidth, bottom: top + node.offsetHeight}); } - }, - - triggerOnKeyDown: methodOp(onKeyDown), - triggerOnKeyPress: methodOp(onKeyPress), - triggerOnKeyUp: onKeyUp, - triggerOnMouseDown: methodOp(onMouseDown), - - execCommand: function(cmd) { - if (commands.hasOwnProperty(cmd)) - { return commands[cmd].call(null, this) } - }, - - triggerElectric: methodOp(function(text) { triggerElectric(this, text); }), - - findPosH: function(from, amount, unit, visually) { - var dir = 1; - if (amount < 0) { dir = -1; amount = -amount; } - var cur = clipPos(this.doc, from); - for (var i = 0; i < amount; ++i) { - cur = findPosH(this.doc, cur, dir, unit, visually); - if (cur.hitSide) { break } - } - return cur - }, - - moveH: methodOp(function(dir, unit) { - var this$1 = this; - - this.extendSelectionsBy(function (range) { - if (this$1.display.shift || this$1.doc.extend || range.empty()) - { return findPosH(this$1.doc, range.head, dir, unit, this$1.options.rtlMoveVisually) } - else - { return dir < 0 ? range.from() : range.to() } - }, sel_move); - }), - - deleteH: methodOp(function(dir, unit) { - var sel = this.doc.sel, doc = this.doc; - if (sel.somethingSelected()) - { doc.replaceSelection("", null, "+delete"); } - else - { deleteNearSelection(this, function (range) { - var other = findPosH(doc, range.head, dir, unit, false); - return dir < 0 ? {from: other, to: range.head} : {from: range.head, to: other} - }); } - }), - - findPosV: function(from, amount, unit, goalColumn) { - var dir = 1, x = goalColumn; - if (amount < 0) { dir = -1; amount = -amount; } - var cur = clipPos(this.doc, from); - for (var i = 0; i < amount; ++i) { - var coords = cursorCoords(this, cur, "div"); - if (x == null) { x = coords.left; } - else { coords.left = x; } - cur = findPosV(this, coords, dir, unit); - if (cur.hitSide) { break } - } - return cur - }, - - moveV: methodOp(function(dir, unit) { - var this$1 = this; - - var doc = this.doc, goals = []; - var collapse = !this.display.shift && !doc.extend && doc.sel.somethingSelected(); - doc.extendSelectionsBy(function (range) { - if (collapse) - { return dir < 0 ? range.from() : range.to() } - var headPos = cursorCoords(this$1, range.head, "div"); - if (range.goalColumn != null) { headPos.left = range.goalColumn; } - goals.push(headPos.left); - var pos = findPosV(this$1, headPos, dir, unit); - if (unit == "page" && range == doc.sel.primary()) - { addToScrollTop(this$1, charCoords(this$1, pos, "div").top - headPos.top); } - return pos - }, sel_move); - if (goals.length) { for (var i = 0; i < doc.sel.ranges.length; i++) - { doc.sel.ranges[i].goalColumn = goals[i]; } } - }), - - // Find the word at the given position (as returned by coordsChar). - findWordAt: function(pos) { - var doc = this.doc, line = getLine(doc, pos.line).text; - var start = pos.ch, end = pos.ch; - if (line) { - var helper = this.getHelper(pos, "wordChars"); - if ((pos.sticky == "before" || end == line.length) && start) { --start; } else { ++end; } - var startChar = line.charAt(start); - var check = isWordChar(startChar, helper) - ? function (ch) { return isWordChar(ch, helper); } - : /\s/.test(startChar) ? function (ch) { return /\s/.test(ch); } - : function (ch) { return (!/\s/.test(ch) && !isWordChar(ch)); }; - while (start > 0 && check(line.charAt(start - 1))) { --start; } - while (end < line.length && check(line.charAt(end))) { ++end; } - } - return new Range(Pos(pos.line, start), Pos(pos.line, end)) - }, - - toggleOverwrite: function(value) { - if (value != null && value == this.state.overwrite) { return } - if (this.state.overwrite = !this.state.overwrite) - { addClass(this.display.cursorDiv, "CodeMirror-overwrite"); } - else - { rmClass(this.display.cursorDiv, "CodeMirror-overwrite"); } - - signal(this, "overwriteToggle", this, this.state.overwrite); - }, - hasFocus: function() { return this.display.input.getField() == activeElt() }, - isReadOnly: function() { return !!(this.options.readOnly || this.doc.cantEdit) }, - - scrollTo: methodOp(function (x, y) { scrollToCoords(this, x, y); }), - getScrollInfo: function() { - var scroller = this.display.scroller; - return {left: scroller.scrollLeft, top: scroller.scrollTop, - height: scroller.scrollHeight - scrollGap(this) - this.display.barHeight, - width: scroller.scrollWidth - scrollGap(this) - this.display.barWidth, - clientHeight: displayHeight(this), clientWidth: displayWidth(this)} - }, - - scrollIntoView: methodOp(function(range, margin) { - if (range == null) { - range = {from: this.doc.sel.primary().head, to: null}; - if (margin == null) { margin = this.options.cursorScrollMargin; } - } else if (typeof range == "number") { - range = {from: Pos(range, 0), to: null}; - } else if (range.from == null) { - range = {from: range, to: null}; - } - if (!range.to) { range.to = range.from; } - range.margin = margin || 0; - - if (range.from.line != null) { - scrollToRange(this, range); - } else { - scrollToCoordsRange(this, range.from, range.to, range.margin); - } - }), - - setSize: methodOp(function(width, height) { - var this$1 = this; - - var interpret = function (val) { return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val; }; - if (width != null) { this.display.wrapper.style.width = interpret(width); } - if (height != null) { this.display.wrapper.style.height = interpret(height); } - if (this.options.lineWrapping) { clearLineMeasurementCache(this); } - var lineNo = this.display.viewFrom; - this.doc.iter(lineNo, this.display.viewTo, function (line) { - if (line.widgets) { for (var i = 0; i < line.widgets.length; i++) - { if (line.widgets[i].noHScroll) { regLineChange(this$1, lineNo, "widget"); break } } } - ++lineNo; - }); - this.curOp.forceUpdate = true; - signal(this, "refresh", this); - }), - - operation: function(f){return runInOp(this, f)}, - startOperation: function(){return startOperation(this)}, - endOperation: function(){return endOperation(this)}, - - refresh: methodOp(function() { - var oldHeight = this.display.cachedTextHeight; - regChange(this); - this.curOp.forceUpdate = true; - clearCaches(this); - scrollToCoords(this, this.doc.scrollLeft, this.doc.scrollTop); - updateGutterSpace(this.display); - if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5 || this.options.lineWrapping) - { estimateLineHeights(this); } - signal(this, "refresh", this); - }), - - swapDoc: methodOp(function(doc) { - var old = this.doc; - old.cm = null; - // Cancel the current text selection if any (#5821) - if (this.state.selectingText) { this.state.selectingText(); } - attachDoc(this, doc); - clearCaches(this); - this.display.input.reset(); - scrollToCoords(this, doc.scrollLeft, doc.scrollTop); - this.curOp.forceScroll = true; - signalLater(this, "swapDoc", this, old); - return old - }), - - phrase: function(phraseText) { - var phrases = this.options.phrases; - return phrases && Object.prototype.hasOwnProperty.call(phrases, phraseText) ? phrases[phraseText] : phraseText - }, - - getInputField: function(){return this.display.input.getField()}, - getWrapperElement: function(){return this.display.wrapper}, - getScrollerElement: function(){return this.display.scroller}, - getGutterElement: function(){return this.display.gutters} - }; - eventMixin(CodeMirror); - - CodeMirror.registerHelper = function(type, name, value) { - if (!helpers.hasOwnProperty(type)) { helpers[type] = CodeMirror[type] = {_global: []}; } - helpers[type][name] = value; - }; - CodeMirror.registerGlobalHelper = function(type, name, predicate, value) { - CodeMirror.registerHelper(type, name, value); - helpers[type]._global.push({pred: predicate, val: value}); - }; - } - - // Used for horizontal relative motion. Dir is -1 or 1 (left or - // right), unit can be "char", "column" (like char, but doesn't - // cross line boundaries), "word" (across next word), or "group" (to - // the start of next group of word or non-word-non-whitespace - // chars). The visually param controls whether, in right-to-left - // text, direction 1 means to move towards the next index in the - // string, or towards the character to the right of the current - // position. The resulting position will have a hitSide=true - // property if it reached the end of the document. - function findPosH(doc, pos, dir, unit, visually) { - var oldPos = pos; - var origDir = dir; - var lineObj = getLine(doc, pos.line); - var lineDir = visually && doc.direction == "rtl" ? -dir : dir; - function findNextLine() { - var l = pos.line + lineDir; - if (l < doc.first || l >= doc.first + doc.size) { return false } - pos = new Pos(l, pos.ch, pos.sticky); - return lineObj = getLine(doc, l) - } - function moveOnce(boundToLine) { - var next; - if (visually) { - next = moveVisually(doc.cm, lineObj, pos, dir); - } else { - next = moveLogically(lineObj, pos, dir); - } - if (next == null) { - if (!boundToLine && findNextLine()) - { pos = endOfLine(visually, doc.cm, lineObj, pos.line, lineDir); } - else - { return false } - } else { - pos = next; - } - return true - } - - if (unit == "char") { - moveOnce(); - } else if (unit == "column") { - moveOnce(true); - } else if (unit == "word" || unit == "group") { - var sawType = null, group = unit == "group"; - var helper = doc.cm && doc.cm.getHelper(pos, "wordChars"); - for (var first = true;; first = false) { - if (dir < 0 && !moveOnce(!first)) { break } - var cur = lineObj.text.charAt(pos.ch) || "\n"; - var type = isWordChar(cur, helper) ? "w" - : group && cur == "\n" ? "n" - : !group || /\s/.test(cur) ? null - : "p"; - if (group && !first && !type) { type = "s"; } - if (sawType && sawType != type) { - if (dir < 0) {dir = 1; moveOnce(); pos.sticky = "after";} - break - } - - if (type) { sawType = type; } - if (dir > 0 && !moveOnce(!first)) { break } - } - } - var result = skipAtomic(doc, pos, oldPos, origDir, true); - if (equalCursorPos(oldPos, result)) { result.hitSide = true; } - return result - } - - // For relative vertical movement. Dir may be -1 or 1. Unit can be - // "page" or "line". The resulting position will have a hitSide=true - // property if it reached the end of the document. - function findPosV(cm, pos, dir, unit) { - var doc = cm.doc, x = pos.left, y; - if (unit == "page") { - var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight); - var moveAmount = Math.max(pageSize - .5 * textHeight(cm.display), 3); - y = (dir > 0 ? pos.bottom : pos.top) + dir * moveAmount; - - } else if (unit == "line") { - y = dir > 0 ? pos.bottom + 3 : pos.top - 3; - } - var target; - for (;;) { - target = coordsChar(cm, x, y); - if (!target.outside) { break } - if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break } - y += dir * 5; - } - return target - } - - // CONTENTEDITABLE INPUT STYLE - - var ContentEditableInput = function(cm) { - this.cm = cm; - this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null; - this.polling = new Delayed(); - this.composing = null; - this.gracePeriod = false; - this.readDOMTimeout = null; - }; - - ContentEditableInput.prototype.init = function (display) { - var this$1 = this; - - var input = this, cm = input.cm; - var div = input.div = display.lineDiv; - disableBrowserMagic(div, cm.options.spellcheck, cm.options.autocorrect, cm.options.autocapitalize); - - function belongsToInput(e) { - for (var t = e.target; t; t = t.parentNode) { - if (t == div) { return true } - if (/\bCodeMirror-(?:line)?widget\b/.test(t.className)) { break } - } - return false - } - - on(div, "paste", function (e) { - if (!belongsToInput(e) || signalDOMEvent(cm, e) || handlePaste(e, cm)) { return } - // IE doesn't fire input events, so we schedule a read for the pasted content in this way - if (ie_version <= 11) { setTimeout(operation(cm, function () { return this$1.updateFromDOM(); }), 20); } - }); - - on(div, "compositionstart", function (e) { - this$1.composing = {data: e.data, done: false}; - }); - on(div, "compositionupdate", function (e) { - if (!this$1.composing) { this$1.composing = {data: e.data, done: false}; } - }); - on(div, "compositionend", function (e) { - if (this$1.composing) { - if (e.data != this$1.composing.data) { this$1.readFromDOMSoon(); } - this$1.composing.done = true; - } - }); - - on(div, "touchstart", function () { return input.forceCompositionEnd(); }); - - on(div, "input", function () { - if (!this$1.composing) { this$1.readFromDOMSoon(); } - }); - - function onCopyCut(e) { - if (!belongsToInput(e) || signalDOMEvent(cm, e)) { return } - if (cm.somethingSelected()) { - setLastCopied({lineWise: false, text: cm.getSelections()}); - if (e.type == "cut") { cm.replaceSelection("", null, "cut"); } - } else if (!cm.options.lineWiseCopyCut) { - return - } else { - var ranges = copyableRanges(cm); - setLastCopied({lineWise: true, text: ranges.text}); - if (e.type == "cut") { - cm.operation(function () { - cm.setSelections(ranges.ranges, 0, sel_dontScroll); - cm.replaceSelection("", null, "cut"); - }); - } - } - if (e.clipboardData) { - e.clipboardData.clearData(); - var content = lastCopied.text.join("\n"); - // iOS exposes the clipboard API, but seems to discard content inserted into it - e.clipboardData.setData("Text", content); - if (e.clipboardData.getData("Text") == content) { - e.preventDefault(); - return - } - } - // Old-fashioned briefly-focus-a-textarea hack - var kludge = hiddenTextarea(), te = kludge.firstChild; - cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild); - te.value = lastCopied.text.join("\n"); - var hadFocus = document.activeElement; - selectInput(te); - setTimeout(function () { - cm.display.lineSpace.removeChild(kludge); - hadFocus.focus(); - if (hadFocus == div) { input.showPrimarySelection(); } - }, 50); - } - on(div, "copy", onCopyCut); - on(div, "cut", onCopyCut); - }; - - ContentEditableInput.prototype.screenReaderLabelChanged = function (label) { - // Label for screenreaders, accessibility - if(label) { - this.div.setAttribute('aria-label', label); - } else { - this.div.removeAttribute('aria-label'); - } - }; - - ContentEditableInput.prototype.prepareSelection = function () { - var result = prepareSelection(this.cm, false); - result.focus = document.activeElement == this.div; - return result - }; - - ContentEditableInput.prototype.showSelection = function (info, takeFocus) { - if (!info || !this.cm.display.view.length) { return } - if (info.focus || takeFocus) { this.showPrimarySelection(); } - this.showMultipleSelections(info); - }; - - ContentEditableInput.prototype.getSelection = function () { - return this.cm.display.wrapper.ownerDocument.getSelection() - }; - - ContentEditableInput.prototype.showPrimarySelection = function () { - var sel = this.getSelection(), cm = this.cm, prim = cm.doc.sel.primary(); - var from = prim.from(), to = prim.to(); - - if (cm.display.viewTo == cm.display.viewFrom || from.line >= cm.display.viewTo || to.line < cm.display.viewFrom) { - sel.removeAllRanges(); - return - } - - var curAnchor = domToPos(cm, sel.anchorNode, sel.anchorOffset); - var curFocus = domToPos(cm, sel.focusNode, sel.focusOffset); - if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad && - cmp(minPos(curAnchor, curFocus), from) == 0 && - cmp(maxPos(curAnchor, curFocus), to) == 0) - { return } - - var view = cm.display.view; - var start = (from.line >= cm.display.viewFrom && posToDOM(cm, from)) || - {node: view[0].measure.map[2], offset: 0}; - var end = to.line < cm.display.viewTo && posToDOM(cm, to); - if (!end) { - var measure = view[view.length - 1].measure; - var map = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map; - end = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]}; - } - - if (!start || !end) { - sel.removeAllRanges(); - return - } - - var old = sel.rangeCount && sel.getRangeAt(0), rng; - try { rng = range(start.node, start.offset, end.offset, end.node); } - catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible - if (rng) { - if (!gecko && cm.state.focused) { - sel.collapse(start.node, start.offset); - if (!rng.collapsed) { - sel.removeAllRanges(); - sel.addRange(rng); - } - } else { - sel.removeAllRanges(); - sel.addRange(rng); - } - if (old && sel.anchorNode == null) { sel.addRange(old); } - else if (gecko) { this.startGracePeriod(); } - } - this.rememberSelection(); - }; - - ContentEditableInput.prototype.startGracePeriod = function () { - var this$1 = this; - - clearTimeout(this.gracePeriod); - this.gracePeriod = setTimeout(function () { - this$1.gracePeriod = false; - if (this$1.selectionChanged()) - { this$1.cm.operation(function () { return this$1.cm.curOp.selectionChanged = true; }); } - }, 20); - }; - - ContentEditableInput.prototype.showMultipleSelections = function (info) { - removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors); - removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection); - }; - - ContentEditableInput.prototype.rememberSelection = function () { - var sel = this.getSelection(); - this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset; - this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset; - }; - - ContentEditableInput.prototype.selectionInEditor = function () { - var sel = this.getSelection(); - if (!sel.rangeCount) { return false } - var node = sel.getRangeAt(0).commonAncestorContainer; - return contains(this.div, node) - }; - - ContentEditableInput.prototype.focus = function () { - if (this.cm.options.readOnly != "nocursor") { - if (!this.selectionInEditor() || document.activeElement != this.div) - { this.showSelection(this.prepareSelection(), true); } - this.div.focus(); - } - }; - ContentEditableInput.prototype.blur = function () { this.div.blur(); }; - ContentEditableInput.prototype.getField = function () { return this.div }; - - ContentEditableInput.prototype.supportsTouch = function () { return true }; - - ContentEditableInput.prototype.receivedFocus = function () { - var input = this; - if (this.selectionInEditor()) - { this.pollSelection(); } - else - { runInOp(this.cm, function () { return input.cm.curOp.selectionChanged = true; }); } - - function poll() { - if (input.cm.state.focused) { - input.pollSelection(); - input.polling.set(input.cm.options.pollInterval, poll); - } - } - this.polling.set(this.cm.options.pollInterval, poll); - }; - - ContentEditableInput.prototype.selectionChanged = function () { - var sel = this.getSelection(); - return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset || - sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset - }; - - ContentEditableInput.prototype.pollSelection = function () { - if (this.readDOMTimeout != null || this.gracePeriod || !this.selectionChanged()) { return } - var sel = this.getSelection(), cm = this.cm; - // On Android Chrome (version 56, at least), backspacing into an - // uneditable block element will put the cursor in that element, - // and then, because it's not editable, hide the virtual keyboard. - // Because Android doesn't allow us to actually detect backspace - // presses in a sane way, this code checks for when that happens - // and simulates a backspace press in this case. - if (android && chrome && this.cm.display.gutterSpecs.length && isInGutter(sel.anchorNode)) { - this.cm.triggerOnKeyDown({type: "keydown", keyCode: 8, preventDefault: Math.abs}); - this.blur(); - this.focus(); - return - } - if (this.composing) { return } - this.rememberSelection(); - var anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset); - var head = domToPos(cm, sel.focusNode, sel.focusOffset); - if (anchor && head) { runInOp(cm, function () { - setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll); - if (anchor.bad || head.bad) { cm.curOp.selectionChanged = true; } - }); } - }; - - ContentEditableInput.prototype.pollContent = function () { - if (this.readDOMTimeout != null) { - clearTimeout(this.readDOMTimeout); - this.readDOMTimeout = null; - } - - var cm = this.cm, display = cm.display, sel = cm.doc.sel.primary(); - var from = sel.from(), to = sel.to(); - if (from.ch == 0 && from.line > cm.firstLine()) - { from = Pos(from.line - 1, getLine(cm.doc, from.line - 1).length); } - if (to.ch == getLine(cm.doc, to.line).text.length && to.line < cm.lastLine()) - { to = Pos(to.line + 1, 0); } - if (from.line < display.viewFrom || to.line > display.viewTo - 1) { return false } - - var fromIndex, fromLine, fromNode; - if (from.line == display.viewFrom || (fromIndex = findViewIndex(cm, from.line)) == 0) { - fromLine = lineNo(display.view[0].line); - fromNode = display.view[0].node; - } else { - fromLine = lineNo(display.view[fromIndex].line); - fromNode = display.view[fromIndex - 1].node.nextSibling; - } - var toIndex = findViewIndex(cm, to.line); - var toLine, toNode; - if (toIndex == display.view.length - 1) { - toLine = display.viewTo - 1; - toNode = display.lineDiv.lastChild; - } else { - toLine = lineNo(display.view[toIndex + 1].line) - 1; - toNode = display.view[toIndex + 1].node.previousSibling; - } - - if (!fromNode) { return false } - var newText = cm.doc.splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine)); - var oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length)); - while (newText.length > 1 && oldText.length > 1) { - if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine--; } - else if (newText[0] == oldText[0]) { newText.shift(); oldText.shift(); fromLine++; } - else { break } - } - - var cutFront = 0, cutEnd = 0; - var newTop = newText[0], oldTop = oldText[0], maxCutFront = Math.min(newTop.length, oldTop.length); - while (cutFront < maxCutFront && newTop.charCodeAt(cutFront) == oldTop.charCodeAt(cutFront)) - { ++cutFront; } - var newBot = lst(newText), oldBot = lst(oldText); - var maxCutEnd = Math.min(newBot.length - (newText.length == 1 ? cutFront : 0), - oldBot.length - (oldText.length == 1 ? cutFront : 0)); - while (cutEnd < maxCutEnd && - newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) - { ++cutEnd; } - // Try to move start of change to start of selection if ambiguous - if (newText.length == 1 && oldText.length == 1 && fromLine == from.line) { - while (cutFront && cutFront > from.ch && - newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) { - cutFront--; - cutEnd++; - } - } - - newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd).replace(/^\u200b+/, ""); - newText[0] = newText[0].slice(cutFront).replace(/\u200b+$/, ""); - - var chFrom = Pos(fromLine, cutFront); - var chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0); - if (newText.length > 1 || newText[0] || cmp(chFrom, chTo)) { - replaceRange(cm.doc, newText, chFrom, chTo, "+input"); - return true - } - }; - - ContentEditableInput.prototype.ensurePolled = function () { - this.forceCompositionEnd(); - }; - ContentEditableInput.prototype.reset = function () { - this.forceCompositionEnd(); - }; - ContentEditableInput.prototype.forceCompositionEnd = function () { - if (!this.composing) { return } - clearTimeout(this.readDOMTimeout); - this.composing = null; - this.updateFromDOM(); - this.div.blur(); - this.div.focus(); - }; - ContentEditableInput.prototype.readFromDOMSoon = function () { - var this$1 = this; - - if (this.readDOMTimeout != null) { return } - this.readDOMTimeout = setTimeout(function () { - this$1.readDOMTimeout = null; - if (this$1.composing) { - if (this$1.composing.done) { this$1.composing = null; } - else { return } - } - this$1.updateFromDOM(); - }, 80); - }; - - ContentEditableInput.prototype.updateFromDOM = function () { - var this$1 = this; - - if (this.cm.isReadOnly() || !this.pollContent()) - { runInOp(this.cm, function () { return regChange(this$1.cm); }); } - }; - - ContentEditableInput.prototype.setUneditable = function (node) { - node.contentEditable = "false"; - }; - - ContentEditableInput.prototype.onKeyPress = function (e) { - if (e.charCode == 0 || this.composing) { return } - e.preventDefault(); - if (!this.cm.isReadOnly()) - { operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0); } - }; - - ContentEditableInput.prototype.readOnlyChanged = function (val) { - this.div.contentEditable = String(val != "nocursor"); - }; - - ContentEditableInput.prototype.onContextMenu = function () {}; - ContentEditableInput.prototype.resetPosition = function () {}; - - ContentEditableInput.prototype.needsContentAttribute = true; - - function posToDOM(cm, pos) { - var view = findViewForLine(cm, pos.line); - if (!view || view.hidden) { return null } - var line = getLine(cm.doc, pos.line); - var info = mapFromLineView(view, line, pos.line); - - var order = getOrder(line, cm.doc.direction), side = "left"; - if (order) { - var partPos = getBidiPartAt(order, pos.ch); - side = partPos % 2 ? "right" : "left"; - } - var result = nodeAndOffsetInLineMap(info.map, pos.ch, side); - result.offset = result.collapse == "right" ? result.end : result.start; - return result - } - - function isInGutter(node) { - for (var scan = node; scan; scan = scan.parentNode) - { if (/CodeMirror-gutter-wrapper/.test(scan.className)) { return true } } - return false - } - - function badPos(pos, bad) { if (bad) { pos.bad = true; } return pos } - - function domTextBetween(cm, from, to, fromLine, toLine) { - var text = "", closing = false, lineSep = cm.doc.lineSeparator(), extraLinebreak = false; - function recognizeMarker(id) { return function (marker) { return marker.id == id; } } - function close() { - if (closing) { - text += lineSep; - if (extraLinebreak) { text += lineSep; } - closing = extraLinebreak = false; - } - } - function addText(str) { - if (str) { - close(); - text += str; - } - } - function walk(node) { - if (node.nodeType == 1) { - var cmText = node.getAttribute("cm-text"); - if (cmText) { - addText(cmText); - return - } - var markerID = node.getAttribute("cm-marker"), range; - if (markerID) { - var found = cm.findMarks(Pos(fromLine, 0), Pos(toLine + 1, 0), recognizeMarker(+markerID)); - if (found.length && (range = found[0].find(0))) - { addText(getBetween(cm.doc, range.from, range.to).join(lineSep)); } - return - } - if (node.getAttribute("contenteditable") == "false") { return } - var isBlock = /^(pre|div|p|li|table|br)$/i.test(node.nodeName); - if (!/^br$/i.test(node.nodeName) && node.textContent.length == 0) { return } - - if (isBlock) { close(); } - for (var i = 0; i < node.childNodes.length; i++) - { walk(node.childNodes[i]); } - - if (/^(pre|p)$/i.test(node.nodeName)) { extraLinebreak = true; } - if (isBlock) { closing = true; } - } else if (node.nodeType == 3) { - addText(node.nodeValue.replace(/\u200b/g, "").replace(/\u00a0/g, " ")); - } - } - for (;;) { - walk(from); - if (from == to) { break } - from = from.nextSibling; - extraLinebreak = false; - } - return text - } - - function domToPos(cm, node, offset) { - var lineNode; - if (node == cm.display.lineDiv) { - lineNode = cm.display.lineDiv.childNodes[offset]; - if (!lineNode) { return badPos(cm.clipPos(Pos(cm.display.viewTo - 1)), true) } - node = null; offset = 0; - } else { - for (lineNode = node;; lineNode = lineNode.parentNode) { - if (!lineNode || lineNode == cm.display.lineDiv) { return null } - if (lineNode.parentNode && lineNode.parentNode == cm.display.lineDiv) { break } - } - } - for (var i = 0; i < cm.display.view.length; i++) { - var lineView = cm.display.view[i]; - if (lineView.node == lineNode) - { return locateNodeInLineView(lineView, node, offset) } - } - } - - function locateNodeInLineView(lineView, node, offset) { - var wrapper = lineView.text.firstChild, bad = false; - if (!node || !contains(wrapper, node)) { return badPos(Pos(lineNo(lineView.line), 0), true) } - if (node == wrapper) { - bad = true; - node = wrapper.childNodes[offset]; - offset = 0; - if (!node) { - var line = lineView.rest ? lst(lineView.rest) : lineView.line; - return badPos(Pos(lineNo(line), line.text.length), bad) - } - } - - var textNode = node.nodeType == 3 ? node : null, topNode = node; - if (!textNode && node.childNodes.length == 1 && node.firstChild.nodeType == 3) { - textNode = node.firstChild; - if (offset) { offset = textNode.nodeValue.length; } - } - while (topNode.parentNode != wrapper) { topNode = topNode.parentNode; } - var measure = lineView.measure, maps = measure.maps; - - function find(textNode, topNode, offset) { - for (var i = -1; i < (maps ? maps.length : 0); i++) { - var map = i < 0 ? measure.map : maps[i]; - for (var j = 0; j < map.length; j += 3) { - var curNode = map[j + 2]; - if (curNode == textNode || curNode == topNode) { - var line = lineNo(i < 0 ? lineView.line : lineView.rest[i]); - var ch = map[j] + offset; - if (offset < 0 || curNode != textNode) { ch = map[j + (offset ? 1 : 0)]; } - return Pos(line, ch) - } - } - } - } - var found = find(textNode, topNode, offset); - if (found) { return badPos(found, bad) } - - // FIXME this is all really shaky. might handle the few cases it needs to handle, but likely to cause problems - for (var after = topNode.nextSibling, dist = textNode ? textNode.nodeValue.length - offset : 0; after; after = after.nextSibling) { - found = find(after, after.firstChild, 0); - if (found) - { return badPos(Pos(found.line, found.ch - dist), bad) } - else - { dist += after.textContent.length; } - } - for (var before = topNode.previousSibling, dist$1 = offset; before; before = before.previousSibling) { - found = find(before, before.firstChild, -1); - if (found) - { return badPos(Pos(found.line, found.ch + dist$1), bad) } - else - { dist$1 += before.textContent.length; } - } - } - - // TEXTAREA INPUT STYLE - - var TextareaInput = function(cm) { - this.cm = cm; - // See input.poll and input.reset - this.prevInput = ""; - - // Flag that indicates whether we expect input to appear real soon - // now (after some event like 'keypress' or 'input') and are - // polling intensively. - this.pollingFast = false; - // Self-resetting timeout for the poller - this.polling = new Delayed(); - // Used to work around IE issue with selection being forgotten when focus moves away from textarea - this.hasSelection = false; - this.composing = null; - }; - - TextareaInput.prototype.init = function (display) { - var this$1 = this; - - var input = this, cm = this.cm; - this.createField(display); - var te = this.textarea; - - display.wrapper.insertBefore(this.wrapper, display.wrapper.firstChild); - - // Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore) - if (ios) { te.style.width = "0px"; } - - on(te, "input", function () { - if (ie && ie_version >= 9 && this$1.hasSelection) { this$1.hasSelection = null; } - input.poll(); - }); - - on(te, "paste", function (e) { - if (signalDOMEvent(cm, e) || handlePaste(e, cm)) { return } - - cm.state.pasteIncoming = +new Date; - input.fastPoll(); - }); - - function prepareCopyCut(e) { - if (signalDOMEvent(cm, e)) { return } - if (cm.somethingSelected()) { - setLastCopied({lineWise: false, text: cm.getSelections()}); - } else if (!cm.options.lineWiseCopyCut) { - return - } else { - var ranges = copyableRanges(cm); - setLastCopied({lineWise: true, text: ranges.text}); - if (e.type == "cut") { - cm.setSelections(ranges.ranges, null, sel_dontScroll); - } else { - input.prevInput = ""; - te.value = ranges.text.join("\n"); - selectInput(te); - } - } - if (e.type == "cut") { cm.state.cutIncoming = +new Date; } - } - on(te, "cut", prepareCopyCut); - on(te, "copy", prepareCopyCut); - - on(display.scroller, "paste", function (e) { - if (eventInWidget(display, e) || signalDOMEvent(cm, e)) { return } - if (!te.dispatchEvent) { - cm.state.pasteIncoming = +new Date; - input.focus(); - return - } - - // Pass the `paste` event to the textarea so it's handled by its event listener. - var event = new Event("paste"); - event.clipboardData = e.clipboardData; - te.dispatchEvent(event); - }); - - // Prevent normal selection in the editor (we handle our own) - on(display.lineSpace, "selectstart", function (e) { - if (!eventInWidget(display, e)) { e_preventDefault(e); } - }); - - on(te, "compositionstart", function () { - var start = cm.getCursor("from"); - if (input.composing) { input.composing.range.clear(); } - input.composing = { - start: start, - range: cm.markText(start, cm.getCursor("to"), {className: "CodeMirror-composing"}) - }; - }); - on(te, "compositionend", function () { - if (input.composing) { - input.poll(); - input.composing.range.clear(); - input.composing = null; - } - }); - }; - - TextareaInput.prototype.createField = function (_display) { - // Wraps and hides input textarea - this.wrapper = hiddenTextarea(); - // The semihidden textarea that is focused when the editor is - // focused, and receives input. - this.textarea = this.wrapper.firstChild; - }; - - TextareaInput.prototype.screenReaderLabelChanged = function (label) { - // Label for screenreaders, accessibility - if(label) { - this.textarea.setAttribute('aria-label', label); - } else { - this.textarea.removeAttribute('aria-label'); - } - }; - - TextareaInput.prototype.prepareSelection = function () { - // Redraw the selection and/or cursor - var cm = this.cm, display = cm.display, doc = cm.doc; - var result = prepareSelection(cm); - - // Move the hidden textarea near the cursor to prevent scrolling artifacts - if (cm.options.moveInputWithCursor) { - var headPos = cursorCoords(cm, doc.sel.primary().head, "div"); - var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect(); - result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10, - headPos.top + lineOff.top - wrapOff.top)); - result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10, - headPos.left + lineOff.left - wrapOff.left)); - } - - return result - }; - - TextareaInput.prototype.showSelection = function (drawn) { - var cm = this.cm, display = cm.display; - removeChildrenAndAdd(display.cursorDiv, drawn.cursors); - removeChildrenAndAdd(display.selectionDiv, drawn.selection); - if (drawn.teTop != null) { - this.wrapper.style.top = drawn.teTop + "px"; - this.wrapper.style.left = drawn.teLeft + "px"; - } - }; - - // Reset the input to correspond to the selection (or to be empty, - // when not typing and nothing is selected) - TextareaInput.prototype.reset = function (typing) { - if (this.contextMenuPending || this.composing) { return } - var cm = this.cm; - if (cm.somethingSelected()) { - this.prevInput = ""; - var content = cm.getSelection(); - this.textarea.value = content; - if (cm.state.focused) { selectInput(this.textarea); } - if (ie && ie_version >= 9) { this.hasSelection = content; } - } else if (!typing) { - this.prevInput = this.textarea.value = ""; - if (ie && ie_version >= 9) { this.hasSelection = null; } - } - }; - - TextareaInput.prototype.getField = function () { return this.textarea }; - - TextareaInput.prototype.supportsTouch = function () { return false }; - - TextareaInput.prototype.focus = function () { - if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) { - try { this.textarea.focus(); } - catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM - } - }; - - TextareaInput.prototype.blur = function () { this.textarea.blur(); }; - - TextareaInput.prototype.resetPosition = function () { - this.wrapper.style.top = this.wrapper.style.left = 0; - }; - - TextareaInput.prototype.receivedFocus = function () { this.slowPoll(); }; - - // Poll for input changes, using the normal rate of polling. This - // runs as long as the editor is focused. - TextareaInput.prototype.slowPoll = function () { - var this$1 = this; - - if (this.pollingFast) { return } - this.polling.set(this.cm.options.pollInterval, function () { - this$1.poll(); - if (this$1.cm.state.focused) { this$1.slowPoll(); } - }); - }; - - // When an event has just come in that is likely to add or change - // something in the input textarea, we poll faster, to ensure that - // the change appears on the screen quickly. - TextareaInput.prototype.fastPoll = function () { - var missed = false, input = this; - input.pollingFast = true; - function p() { - var changed = input.poll(); - if (!changed && !missed) {missed = true; input.polling.set(60, p);} - else {input.pollingFast = false; input.slowPoll();} - } - input.polling.set(20, p); - }; - - // Read input from the textarea, and update the document to match. - // When something is selected, it is present in the textarea, and - // selected (unless it is huge, in which case a placeholder is - // used). When nothing is selected, the cursor sits after previously - // seen text (can be empty), which is stored in prevInput (we must - // not reset the textarea when typing, because that breaks IME). - TextareaInput.prototype.poll = function () { - var this$1 = this; - - var cm = this.cm, input = this.textarea, prevInput = this.prevInput; - // Since this is called a *lot*, try to bail out as cheaply as - // possible when it is clear that nothing happened. hasSelection - // will be the case when there is a lot of text in the textarea, - // in which case reading its value would be expensive. - if (this.contextMenuPending || !cm.state.focused || - (hasSelection(input) && !prevInput && !this.composing) || - cm.isReadOnly() || cm.options.disableInput || cm.state.keySeq) - { return false } - - var text = input.value; - // If nothing changed, bail. - if (text == prevInput && !cm.somethingSelected()) { return false } - // Work around nonsensical selection resetting in IE9/10, and - // inexplicable appearance of private area unicode characters on - // some key combos in Mac (#2689). - if (ie && ie_version >= 9 && this.hasSelection === text || - mac && /[\uf700-\uf7ff]/.test(text)) { - cm.display.input.reset(); - return false - } - - if (cm.doc.sel == cm.display.selForContextMenu) { - var first = text.charCodeAt(0); - if (first == 0x200b && !prevInput) { prevInput = "\u200b"; } - if (first == 0x21da) { this.reset(); return this.cm.execCommand("undo") } - } - // Find the part of the input that is actually new - var same = 0, l = Math.min(prevInput.length, text.length); - while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) { ++same; } - - runInOp(cm, function () { - applyTextInput(cm, text.slice(same), prevInput.length - same, - null, this$1.composing ? "*compose" : null); - - // Don't leave long text in the textarea, since it makes further polling slow - if (text.length > 1000 || text.indexOf("\n") > -1) { input.value = this$1.prevInput = ""; } - else { this$1.prevInput = text; } - - if (this$1.composing) { - this$1.composing.range.clear(); - this$1.composing.range = cm.markText(this$1.composing.start, cm.getCursor("to"), - {className: "CodeMirror-composing"}); - } - }); - return true - }; - - TextareaInput.prototype.ensurePolled = function () { - if (this.pollingFast && this.poll()) { this.pollingFast = false; } - }; - - TextareaInput.prototype.onKeyPress = function () { - if (ie && ie_version >= 9) { this.hasSelection = null; } - this.fastPoll(); - }; - - TextareaInput.prototype.onContextMenu = function (e) { - var input = this, cm = input.cm, display = cm.display, te = input.textarea; - if (input.contextMenuPending) { input.contextMenuPending(); } - var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop; - if (!pos || presto) { return } // Opera is difficult. - - // Reset the current text selection only if the click is done outside of the selection - // and 'resetSelectionOnContextMenu' option is true. - var reset = cm.options.resetSelectionOnContextMenu; - if (reset && cm.doc.sel.contains(pos) == -1) - { operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll); } - - var oldCSS = te.style.cssText, oldWrapperCSS = input.wrapper.style.cssText; - var wrapperBox = input.wrapper.offsetParent.getBoundingClientRect(); - input.wrapper.style.cssText = "position: static"; - te.style.cssText = "position: absolute; width: 30px; height: 30px;\n top: " + (e.clientY - wrapperBox.top - 5) + "px; left: " + (e.clientX - wrapperBox.left - 5) + "px;\n z-index: 1000; background: " + (ie ? "rgba(255, 255, 255, .05)" : "transparent") + ";\n outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; - var oldScrollY; - if (webkit) { oldScrollY = window.scrollY; } // Work around Chrome issue (#2712) - display.input.focus(); - if (webkit) { window.scrollTo(null, oldScrollY); } - display.input.reset(); - // Adds "Select all" to context menu in FF - if (!cm.somethingSelected()) { te.value = input.prevInput = " "; } - input.contextMenuPending = rehide; - display.selForContextMenu = cm.doc.sel; - clearTimeout(display.detectingSelectAll); - - // Select-all will be greyed out if there's nothing to select, so - // this adds a zero-width space so that we can later check whether - // it got selected. - function prepareSelectAllHack() { - if (te.selectionStart != null) { - var selected = cm.somethingSelected(); - var extval = "\u200b" + (selected ? te.value : ""); - te.value = "\u21da"; // Used to catch context-menu undo - te.value = extval; - input.prevInput = selected ? "" : "\u200b"; - te.selectionStart = 1; te.selectionEnd = extval.length; - // Re-set this, in case some other handler touched the - // selection in the meantime. - display.selForContextMenu = cm.doc.sel; - } - } - function rehide() { - if (input.contextMenuPending != rehide) { return } - input.contextMenuPending = false; - input.wrapper.style.cssText = oldWrapperCSS; - te.style.cssText = oldCSS; - if (ie && ie_version < 9) { display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos); } - - // Try to detect the user choosing select-all - if (te.selectionStart != null) { - if (!ie || (ie && ie_version < 9)) { prepareSelectAllHack(); } - var i = 0, poll = function () { - if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 && - te.selectionEnd > 0 && input.prevInput == "\u200b") { - operation(cm, selectAll)(cm); - } else if (i++ < 10) { - display.detectingSelectAll = setTimeout(poll, 500); - } else { - display.selForContextMenu = null; - display.input.reset(); - } - }; - display.detectingSelectAll = setTimeout(poll, 200); - } - } - - if (ie && ie_version >= 9) { prepareSelectAllHack(); } - if (captureRightClick) { - e_stop(e); - var mouseup = function () { - off(window, "mouseup", mouseup); - setTimeout(rehide, 20); - }; - on(window, "mouseup", mouseup); - } else { - setTimeout(rehide, 50); - } - }; - - TextareaInput.prototype.readOnlyChanged = function (val) { - if (!val) { this.reset(); } - this.textarea.disabled = val == "nocursor"; - }; - - TextareaInput.prototype.setUneditable = function () {}; - - TextareaInput.prototype.needsContentAttribute = false; - - function fromTextArea(textarea, options) { - options = options ? copyObj(options) : {}; - options.value = textarea.value; - if (!options.tabindex && textarea.tabIndex) - { options.tabindex = textarea.tabIndex; } - if (!options.placeholder && textarea.placeholder) - { options.placeholder = textarea.placeholder; } - // Set autofocus to true if this textarea is focused, or if it has - // autofocus and no other element is focused. - if (options.autofocus == null) { - var hasFocus = activeElt(); - options.autofocus = hasFocus == textarea || - textarea.getAttribute("autofocus") != null && hasFocus == document.body; - } - - function save() {textarea.value = cm.getValue();} - - var realSubmit; - if (textarea.form) { - on(textarea.form, "submit", save); - // Deplorable hack to make the submit method do the right thing. - if (!options.leaveSubmitMethodAlone) { - var form = textarea.form; - realSubmit = form.submit; - try { - var wrappedSubmit = form.submit = function () { - save(); - form.submit = realSubmit; - form.submit(); - form.submit = wrappedSubmit; - }; - } catch(e) {} - } - } - - options.finishInit = function (cm) { - cm.save = save; - cm.getTextArea = function () { return textarea; }; - cm.toTextArea = function () { - cm.toTextArea = isNaN; // Prevent this from being ran twice - save(); - textarea.parentNode.removeChild(cm.getWrapperElement()); - textarea.style.display = ""; - if (textarea.form) { - off(textarea.form, "submit", save); - if (!options.leaveSubmitMethodAlone && typeof textarea.form.submit == "function") - { textarea.form.submit = realSubmit; } - } - }; - }; - - textarea.style.display = "none"; - var cm = CodeMirror(function (node) { return textarea.parentNode.insertBefore(node, textarea.nextSibling); }, - options); - return cm - } - - function addLegacyProps(CodeMirror) { - CodeMirror.off = off; - CodeMirror.on = on; - CodeMirror.wheelEventPixels = wheelEventPixels; - CodeMirror.Doc = Doc; - CodeMirror.splitLines = splitLinesAuto; - CodeMirror.countColumn = countColumn; - CodeMirror.findColumn = findColumn; - CodeMirror.isWordChar = isWordCharBasic; - CodeMirror.Pass = Pass; - CodeMirror.signal = signal; - CodeMirror.Line = Line; - CodeMirror.changeEnd = changeEnd; - CodeMirror.scrollbarModel = scrollbarModel; - CodeMirror.Pos = Pos; - CodeMirror.cmpPos = cmp; - CodeMirror.modes = modes; - CodeMirror.mimeModes = mimeModes; - CodeMirror.resolveMode = resolveMode; - CodeMirror.getMode = getMode; - CodeMirror.modeExtensions = modeExtensions; - CodeMirror.extendMode = extendMode; - CodeMirror.copyState = copyState; - CodeMirror.startState = startState; - CodeMirror.innerMode = innerMode; - CodeMirror.commands = commands; - CodeMirror.keyMap = keyMap; - CodeMirror.keyName = keyName; - CodeMirror.isModifierKey = isModifierKey; - CodeMirror.lookupKey = lookupKey; - CodeMirror.normalizeKeyMap = normalizeKeyMap; - CodeMirror.StringStream = StringStream; - CodeMirror.SharedTextMarker = SharedTextMarker; - CodeMirror.TextMarker = TextMarker; - CodeMirror.LineWidget = LineWidget; - CodeMirror.e_preventDefault = e_preventDefault; - CodeMirror.e_stopPropagation = e_stopPropagation; - CodeMirror.e_stop = e_stop; - CodeMirror.addClass = addClass; - CodeMirror.contains = contains; - CodeMirror.rmClass = rmClass; - CodeMirror.keyNames = keyNames; - } - - // EDITOR CONSTRUCTOR - - defineOptions(CodeMirror); - - addEditorMethods(CodeMirror); - - // Set up methods on CodeMirror's prototype to redirect to the editor's document. - var dontDelegate = "iter insert remove copy getEditor constructor".split(" "); - for (var prop in Doc.prototype) { if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0) - { CodeMirror.prototype[prop] = (function(method) { - return function() {return method.apply(this.doc, arguments)} - })(Doc.prototype[prop]); } } - - eventMixin(Doc); - CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput}; - - // Extra arguments are stored as the mode's dependencies, which is - // used by (legacy) mechanisms like loadmode.js to automatically - // load a mode. (Preferred mechanism is the require/define calls.) - CodeMirror.defineMode = function(name/*, mode, …*/) { - if (!CodeMirror.defaults.mode && name != "null") { CodeMirror.defaults.mode = name; } - defineMode.apply(this, arguments); - }; - - CodeMirror.defineMIME = defineMIME; - - // Minimal default mode. - CodeMirror.defineMode("null", function () { return ({token: function (stream) { return stream.skipToEnd(); }}); }); - CodeMirror.defineMIME("text/plain", "null"); - - // EXTENSIONS - - CodeMirror.defineExtension = function (name, func) { - CodeMirror.prototype[name] = func; - }; - CodeMirror.defineDocExtension = function (name, func) { - Doc.prototype[name] = func; - }; - - CodeMirror.fromTextArea = fromTextArea; - - addLegacyProps(CodeMirror); - - CodeMirror.version = "5.56.0"; - - return CodeMirror; - -}))); - - -/* ---- extension/simple.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - CodeMirror.defineSimpleMode = function(name, states) { - CodeMirror.defineMode(name, function(config) { - return CodeMirror.simpleMode(config, states); - }); - }; - - CodeMirror.simpleMode = function(config, states) { - ensureState(states, "start"); - var states_ = {}, meta = states.meta || {}, hasIndentation = false; - for (var state in states) if (state != meta && states.hasOwnProperty(state)) { - var list = states_[state] = [], orig = states[state]; - for (var i = 0; i < orig.length; i++) { - var data = orig[i]; - list.push(new Rule(data, states)); - if (data.indent || data.dedent) hasIndentation = true; - } - } - var mode = { - startState: function() { - return {state: "start", pending: null, - local: null, localState: null, - indent: hasIndentation ? [] : null}; - }, - copyState: function(state) { - var s = {state: state.state, pending: state.pending, - local: state.local, localState: null, - indent: state.indent && state.indent.slice(0)}; - if (state.localState) - s.localState = CodeMirror.copyState(state.local.mode, state.localState); - if (state.stack) - s.stack = state.stack.slice(0); - for (var pers = state.persistentStates; pers; pers = pers.next) - s.persistentStates = {mode: pers.mode, - spec: pers.spec, - state: pers.state == state.localState ? s.localState : CodeMirror.copyState(pers.mode, pers.state), - next: s.persistentStates}; - return s; - }, - token: tokenFunction(states_, config), - innerMode: function(state) { return state.local && {mode: state.local.mode, state: state.localState}; }, - indent: indentFunction(states_, meta) - }; - if (meta) for (var prop in meta) if (meta.hasOwnProperty(prop)) - mode[prop] = meta[prop]; - return mode; - }; - - function ensureState(states, name) { - if (!states.hasOwnProperty(name)) - throw new Error("Undefined state " + name + " in simple mode"); - } - - function toRegex(val, caret) { - if (!val) return /(?:)/; - var flags = ""; - if (val instanceof RegExp) { - if (val.ignoreCase) flags = "i"; - val = val.source; - } else { - val = String(val); - } - return new RegExp((caret === false ? "" : "^") + "(?:" + val + ")", flags); - } - - function asToken(val) { - if (!val) return null; - if (val.apply) return val - if (typeof val == "string") return val.replace(/\./g, " "); - var result = []; - for (var i = 0; i < val.length; i++) - result.push(val[i] && val[i].replace(/\./g, " ")); - return result; - } - - function Rule(data, states) { - if (data.next || data.push) ensureState(states, data.next || data.push); - this.regex = toRegex(data.regex); - this.token = asToken(data.token); - this.data = data; - } - - function tokenFunction(states, config) { - return function(stream, state) { - if (state.pending) { - var pend = state.pending.shift(); - if (state.pending.length == 0) state.pending = null; - stream.pos += pend.text.length; - return pend.token; - } - - if (state.local) { - if (state.local.end && stream.match(state.local.end)) { - var tok = state.local.endToken || null; - state.local = state.localState = null; - return tok; - } else { - var tok = state.local.mode.token(stream, state.localState), m; - if (state.local.endScan && (m = state.local.endScan.exec(stream.current()))) - stream.pos = stream.start + m.index; - return tok; - } - } - - var curState = states[state.state]; - for (var i = 0; i < curState.length; i++) { - var rule = curState[i]; - var matches = (!rule.data.sol || stream.sol()) && stream.match(rule.regex); - if (matches) { - if (rule.data.next) { - state.state = rule.data.next; - } else if (rule.data.push) { - (state.stack || (state.stack = [])).push(state.state); - state.state = rule.data.push; - } else if (rule.data.pop && state.stack && state.stack.length) { - state.state = state.stack.pop(); - } - - if (rule.data.mode) - enterLocalMode(config, state, rule.data.mode, rule.token); - if (rule.data.indent) - state.indent.push(stream.indentation() + config.indentUnit); - if (rule.data.dedent) - state.indent.pop(); - var token = rule.token - if (token && token.apply) token = token(matches) - if (matches.length > 2 && rule.token && typeof rule.token != "string") { - state.pending = []; - for (var j = 2; j < matches.length; j++) - if (matches[j]) - state.pending.push({text: matches[j], token: rule.token[j - 1]}); - stream.backUp(matches[0].length - (matches[1] ? matches[1].length : 0)); - return token[0]; - } else if (token && token.join) { - return token[0]; - } else { - return token; - } - } - } - stream.next(); - return null; - }; - } - - function cmp(a, b) { - if (a === b) return true; - if (!a || typeof a != "object" || !b || typeof b != "object") return false; - var props = 0; - for (var prop in a) if (a.hasOwnProperty(prop)) { - if (!b.hasOwnProperty(prop) || !cmp(a[prop], b[prop])) return false; - props++; - } - for (var prop in b) if (b.hasOwnProperty(prop)) props--; - return props == 0; - } - - function enterLocalMode(config, state, spec, token) { - var pers; - if (spec.persistent) for (var p = state.persistentStates; p && !pers; p = p.next) - if (spec.spec ? cmp(spec.spec, p.spec) : spec.mode == p.mode) pers = p; - var mode = pers ? pers.mode : spec.mode || CodeMirror.getMode(config, spec.spec); - var lState = pers ? pers.state : CodeMirror.startState(mode); - if (spec.persistent && !pers) - state.persistentStates = {mode: mode, spec: spec.spec, state: lState, next: state.persistentStates}; - - state.localState = lState; - state.local = {mode: mode, - end: spec.end && toRegex(spec.end), - endScan: spec.end && spec.forceEnd !== false && toRegex(spec.end, false), - endToken: token && token.join ? token[token.length - 1] : token}; - } - - function indexOf(val, arr) { - for (var i = 0; i < arr.length; i++) if (arr[i] === val) return true; - } - - function indentFunction(states, meta) { - return function(state, textAfter, line) { - if (state.local && state.local.mode.indent) - return state.local.mode.indent(state.localState, textAfter, line); - if (state.indent == null || state.local || meta.dontIndentStates && indexOf(state.state, meta.dontIndentStates) > -1) - return CodeMirror.Pass; - - var pos = state.indent.length - 1, rules = states[state.state]; - scan: for (;;) { - for (var i = 0; i < rules.length; i++) { - var rule = rules[i]; - if (rule.data.dedent && rule.data.dedentIfLineStart !== false) { - var m = rule.regex.exec(textAfter); - if (m && m[0]) { - pos--; - if (rule.next || rule.push) rules = states[rule.next || rule.push]; - textAfter = textAfter.slice(m[0].length); - continue scan; - } - } - } - break; - } - return pos < 0 ? 0 : state.indent[pos]; - }; - } -}); - - -/* ---- extension/sublime.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -// A rough approximation of Sublime Text's keybindings -// Depends on addon/search/searchcursor.js and optionally addon/dialog/dialogs.js - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../lib/codemirror"), require("../addon/search/searchcursor"), require("../addon/edit/matchbrackets")); - else if (typeof define == "function" && define.amd) // AMD - define(["../lib/codemirror", "../addon/search/searchcursor", "../addon/edit/matchbrackets"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - var cmds = CodeMirror.commands; - var Pos = CodeMirror.Pos; - - // This is not exactly Sublime's algorithm. I couldn't make heads or tails of that. - function findPosSubword(doc, start, dir) { - if (dir < 0 && start.ch == 0) return doc.clipPos(Pos(start.line - 1)); - var line = doc.getLine(start.line); - if (dir > 0 && start.ch >= line.length) return doc.clipPos(Pos(start.line + 1, 0)); - var state = "start", type, startPos = start.ch; - for (var pos = startPos, e = dir < 0 ? 0 : line.length, i = 0; pos != e; pos += dir, i++) { - var next = line.charAt(dir < 0 ? pos - 1 : pos); - var cat = next != "_" && CodeMirror.isWordChar(next) ? "w" : "o"; - if (cat == "w" && next.toUpperCase() == next) cat = "W"; - if (state == "start") { - if (cat != "o") { state = "in"; type = cat; } - else startPos = pos + dir - } else if (state == "in") { - if (type != cat) { - if (type == "w" && cat == "W" && dir < 0) pos--; - if (type == "W" && cat == "w" && dir > 0) { // From uppercase to lowercase - if (pos == startPos + 1) { type = "w"; continue; } - else pos--; - } - break; - } - } - } - return Pos(start.line, pos); - } - - function moveSubword(cm, dir) { - cm.extendSelectionsBy(function(range) { - if (cm.display.shift || cm.doc.extend || range.empty()) - return findPosSubword(cm.doc, range.head, dir); - else - return dir < 0 ? range.from() : range.to(); - }); - } - - cmds.goSubwordLeft = function(cm) { moveSubword(cm, -1); }; - cmds.goSubwordRight = function(cm) { moveSubword(cm, 1); }; - - cmds.scrollLineUp = function(cm) { - var info = cm.getScrollInfo(); - if (!cm.somethingSelected()) { - var visibleBottomLine = cm.lineAtHeight(info.top + info.clientHeight, "local"); - if (cm.getCursor().line >= visibleBottomLine) - cm.execCommand("goLineUp"); - } - cm.scrollTo(null, info.top - cm.defaultTextHeight()); - }; - cmds.scrollLineDown = function(cm) { - var info = cm.getScrollInfo(); - if (!cm.somethingSelected()) { - var visibleTopLine = cm.lineAtHeight(info.top, "local")+1; - if (cm.getCursor().line <= visibleTopLine) - cm.execCommand("goLineDown"); - } - cm.scrollTo(null, info.top + cm.defaultTextHeight()); - }; - - cmds.splitSelectionByLine = function(cm) { - var ranges = cm.listSelections(), lineRanges = []; - for (var i = 0; i < ranges.length; i++) { - var from = ranges[i].from(), to = ranges[i].to(); - for (var line = from.line; line <= to.line; ++line) - if (!(to.line > from.line && line == to.line && to.ch == 0)) - lineRanges.push({anchor: line == from.line ? from : Pos(line, 0), - head: line == to.line ? to : Pos(line)}); - } - cm.setSelections(lineRanges, 0); - }; - - cmds.singleSelectionTop = function(cm) { - var range = cm.listSelections()[0]; - cm.setSelection(range.anchor, range.head, {scroll: false}); - }; - - cmds.selectLine = function(cm) { - var ranges = cm.listSelections(), extended = []; - for (var i = 0; i < ranges.length; i++) { - var range = ranges[i]; - extended.push({anchor: Pos(range.from().line, 0), - head: Pos(range.to().line + 1, 0)}); - } - cm.setSelections(extended); - }; - - function insertLine(cm, above) { - if (cm.isReadOnly()) return CodeMirror.Pass - cm.operation(function() { - var len = cm.listSelections().length, newSelection = [], last = -1; - for (var i = 0; i < len; i++) { - var head = cm.listSelections()[i].head; - if (head.line <= last) continue; - var at = Pos(head.line + (above ? 0 : 1), 0); - cm.replaceRange("\n", at, null, "+insertLine"); - cm.indentLine(at.line, null, true); - newSelection.push({head: at, anchor: at}); - last = head.line + 1; - } - cm.setSelections(newSelection); - }); - cm.execCommand("indentAuto"); - } - - cmds.insertLineAfter = function(cm) { return insertLine(cm, false); }; - - cmds.insertLineBefore = function(cm) { return insertLine(cm, true); }; - - function wordAt(cm, pos) { - var start = pos.ch, end = start, line = cm.getLine(pos.line); - while (start && CodeMirror.isWordChar(line.charAt(start - 1))) --start; - while (end < line.length && CodeMirror.isWordChar(line.charAt(end))) ++end; - return {from: Pos(pos.line, start), to: Pos(pos.line, end), word: line.slice(start, end)}; - } - - cmds.selectNextOccurrence = function(cm) { - var from = cm.getCursor("from"), to = cm.getCursor("to"); - var fullWord = cm.state.sublimeFindFullWord == cm.doc.sel; - if (CodeMirror.cmpPos(from, to) == 0) { - var word = wordAt(cm, from); - if (!word.word) return; - cm.setSelection(word.from, word.to); - fullWord = true; - } else { - var text = cm.getRange(from, to); - var query = fullWord ? new RegExp("\\b" + text + "\\b") : text; - var cur = cm.getSearchCursor(query, to); - var found = cur.findNext(); - if (!found) { - cur = cm.getSearchCursor(query, Pos(cm.firstLine(), 0)); - found = cur.findNext(); - } - if (!found || isSelectedRange(cm.listSelections(), cur.from(), cur.to())) return - cm.addSelection(cur.from(), cur.to()); - } - if (fullWord) - cm.state.sublimeFindFullWord = cm.doc.sel; - }; - - cmds.skipAndSelectNextOccurrence = function(cm) { - var prevAnchor = cm.getCursor("anchor"), prevHead = cm.getCursor("head"); - cmds.selectNextOccurrence(cm); - if (CodeMirror.cmpPos(prevAnchor, prevHead) != 0) { - cm.doc.setSelections(cm.doc.listSelections() - .filter(function (sel) { - return sel.anchor != prevAnchor || sel.head != prevHead; - })); - } - } - - function addCursorToSelection(cm, dir) { - var ranges = cm.listSelections(), newRanges = []; - for (var i = 0; i < ranges.length; i++) { - var range = ranges[i]; - var newAnchor = cm.findPosV( - range.anchor, dir, "line", range.anchor.goalColumn); - var newHead = cm.findPosV( - range.head, dir, "line", range.head.goalColumn); - newAnchor.goalColumn = range.anchor.goalColumn != null ? - range.anchor.goalColumn : cm.cursorCoords(range.anchor, "div").left; - newHead.goalColumn = range.head.goalColumn != null ? - range.head.goalColumn : cm.cursorCoords(range.head, "div").left; - var newRange = {anchor: newAnchor, head: newHead}; - newRanges.push(range); - newRanges.push(newRange); - } - cm.setSelections(newRanges); - } - cmds.addCursorToPrevLine = function(cm) { addCursorToSelection(cm, -1); }; - cmds.addCursorToNextLine = function(cm) { addCursorToSelection(cm, 1); }; - - function isSelectedRange(ranges, from, to) { - for (var i = 0; i < ranges.length; i++) - if (CodeMirror.cmpPos(ranges[i].from(), from) == 0 && - CodeMirror.cmpPos(ranges[i].to(), to) == 0) return true - return false - } - - var mirror = "(){}[]"; - function selectBetweenBrackets(cm) { - var ranges = cm.listSelections(), newRanges = [] - for (var i = 0; i < ranges.length; i++) { - var range = ranges[i], pos = range.head, opening = cm.scanForBracket(pos, -1); - if (!opening) return false; - for (;;) { - var closing = cm.scanForBracket(pos, 1); - if (!closing) return false; - if (closing.ch == mirror.charAt(mirror.indexOf(opening.ch) + 1)) { - var startPos = Pos(opening.pos.line, opening.pos.ch + 1); - if (CodeMirror.cmpPos(startPos, range.from()) == 0 && - CodeMirror.cmpPos(closing.pos, range.to()) == 0) { - opening = cm.scanForBracket(opening.pos, -1); - if (!opening) return false; - } else { - newRanges.push({anchor: startPos, head: closing.pos}); - break; - } - } - pos = Pos(closing.pos.line, closing.pos.ch + 1); - } - } - cm.setSelections(newRanges); - return true; - } - - cmds.selectScope = function(cm) { - selectBetweenBrackets(cm) || cm.execCommand("selectAll"); - }; - cmds.selectBetweenBrackets = function(cm) { - if (!selectBetweenBrackets(cm)) return CodeMirror.Pass; - }; - - function puncType(type) { - return !type ? null : /\bpunctuation\b/.test(type) ? type : undefined - } - - cmds.goToBracket = function(cm) { - cm.extendSelectionsBy(function(range) { - var next = cm.scanForBracket(range.head, 1, puncType(cm.getTokenTypeAt(range.head))); - if (next && CodeMirror.cmpPos(next.pos, range.head) != 0) return next.pos; - var prev = cm.scanForBracket(range.head, -1, puncType(cm.getTokenTypeAt(Pos(range.head.line, range.head.ch + 1)))); - return prev && Pos(prev.pos.line, prev.pos.ch + 1) || range.head; - }); - }; - - cmds.swapLineUp = function(cm) { - if (cm.isReadOnly()) return CodeMirror.Pass - var ranges = cm.listSelections(), linesToMove = [], at = cm.firstLine() - 1, newSels = []; - for (var i = 0; i < ranges.length; i++) { - var range = ranges[i], from = range.from().line - 1, to = range.to().line; - newSels.push({anchor: Pos(range.anchor.line - 1, range.anchor.ch), - head: Pos(range.head.line - 1, range.head.ch)}); - if (range.to().ch == 0 && !range.empty()) --to; - if (from > at) linesToMove.push(from, to); - else if (linesToMove.length) linesToMove[linesToMove.length - 1] = to; - at = to; - } - cm.operation(function() { - for (var i = 0; i < linesToMove.length; i += 2) { - var from = linesToMove[i], to = linesToMove[i + 1]; - var line = cm.getLine(from); - cm.replaceRange("", Pos(from, 0), Pos(from + 1, 0), "+swapLine"); - if (to > cm.lastLine()) - cm.replaceRange("\n" + line, Pos(cm.lastLine()), null, "+swapLine"); - else - cm.replaceRange(line + "\n", Pos(to, 0), null, "+swapLine"); - } - cm.setSelections(newSels); - cm.scrollIntoView(); - }); - }; - - cmds.swapLineDown = function(cm) { - if (cm.isReadOnly()) return CodeMirror.Pass - var ranges = cm.listSelections(), linesToMove = [], at = cm.lastLine() + 1; - for (var i = ranges.length - 1; i >= 0; i--) { - var range = ranges[i], from = range.to().line + 1, to = range.from().line; - if (range.to().ch == 0 && !range.empty()) from--; - if (from < at) linesToMove.push(from, to); - else if (linesToMove.length) linesToMove[linesToMove.length - 1] = to; - at = to; - } - cm.operation(function() { - for (var i = linesToMove.length - 2; i >= 0; i -= 2) { - var from = linesToMove[i], to = linesToMove[i + 1]; - var line = cm.getLine(from); - if (from == cm.lastLine()) - cm.replaceRange("", Pos(from - 1), Pos(from), "+swapLine"); - else - cm.replaceRange("", Pos(from, 0), Pos(from + 1, 0), "+swapLine"); - cm.replaceRange(line + "\n", Pos(to, 0), null, "+swapLine"); - } - cm.scrollIntoView(); - }); - }; - - cmds.toggleCommentIndented = function(cm) { - cm.toggleComment({ indent: true }); - } - - cmds.joinLines = function(cm) { - var ranges = cm.listSelections(), joined = []; - for (var i = 0; i < ranges.length; i++) { - var range = ranges[i], from = range.from(); - var start = from.line, end = range.to().line; - while (i < ranges.length - 1 && ranges[i + 1].from().line == end) - end = ranges[++i].to().line; - joined.push({start: start, end: end, anchor: !range.empty() && from}); - } - cm.operation(function() { - var offset = 0, ranges = []; - for (var i = 0; i < joined.length; i++) { - var obj = joined[i]; - var anchor = obj.anchor && Pos(obj.anchor.line - offset, obj.anchor.ch), head; - for (var line = obj.start; line <= obj.end; line++) { - var actual = line - offset; - if (line == obj.end) head = Pos(actual, cm.getLine(actual).length + 1); - if (actual < cm.lastLine()) { - cm.replaceRange(" ", Pos(actual), Pos(actual + 1, /^\s*/.exec(cm.getLine(actual + 1))[0].length)); - ++offset; - } - } - ranges.push({anchor: anchor || head, head: head}); - } - cm.setSelections(ranges, 0); - }); - }; - - cmds.duplicateLine = function(cm) { - cm.operation(function() { - var rangeCount = cm.listSelections().length; - for (var i = 0; i < rangeCount; i++) { - var range = cm.listSelections()[i]; - if (range.empty()) - cm.replaceRange(cm.getLine(range.head.line) + "\n", Pos(range.head.line, 0)); - else - cm.replaceRange(cm.getRange(range.from(), range.to()), range.from()); - } - cm.scrollIntoView(); - }); - }; - - - function sortLines(cm, caseSensitive) { - if (cm.isReadOnly()) return CodeMirror.Pass - var ranges = cm.listSelections(), toSort = [], selected; - for (var i = 0; i < ranges.length; i++) { - var range = ranges[i]; - if (range.empty()) continue; - var from = range.from().line, to = range.to().line; - while (i < ranges.length - 1 && ranges[i + 1].from().line == to) - to = ranges[++i].to().line; - if (!ranges[i].to().ch) to--; - toSort.push(from, to); - } - if (toSort.length) selected = true; - else toSort.push(cm.firstLine(), cm.lastLine()); - - cm.operation(function() { - var ranges = []; - for (var i = 0; i < toSort.length; i += 2) { - var from = toSort[i], to = toSort[i + 1]; - var start = Pos(from, 0), end = Pos(to); - var lines = cm.getRange(start, end, false); - if (caseSensitive) - lines.sort(); - else - lines.sort(function(a, b) { - var au = a.toUpperCase(), bu = b.toUpperCase(); - if (au != bu) { a = au; b = bu; } - return a < b ? -1 : a == b ? 0 : 1; - }); - cm.replaceRange(lines, start, end); - if (selected) ranges.push({anchor: start, head: Pos(to + 1, 0)}); - } - if (selected) cm.setSelections(ranges, 0); - }); - } - - cmds.sortLines = function(cm) { sortLines(cm, true); }; - cmds.sortLinesInsensitive = function(cm) { sortLines(cm, false); }; - - cmds.nextBookmark = function(cm) { - var marks = cm.state.sublimeBookmarks; - if (marks) while (marks.length) { - var current = marks.shift(); - var found = current.find(); - if (found) { - marks.push(current); - return cm.setSelection(found.from, found.to); - } - } - }; - - cmds.prevBookmark = function(cm) { - var marks = cm.state.sublimeBookmarks; - if (marks) while (marks.length) { - marks.unshift(marks.pop()); - var found = marks[marks.length - 1].find(); - if (!found) - marks.pop(); - else - return cm.setSelection(found.from, found.to); - } - }; - - cmds.toggleBookmark = function(cm) { - var ranges = cm.listSelections(); - var marks = cm.state.sublimeBookmarks || (cm.state.sublimeBookmarks = []); - for (var i = 0; i < ranges.length; i++) { - var from = ranges[i].from(), to = ranges[i].to(); - var found = ranges[i].empty() ? cm.findMarksAt(from) : cm.findMarks(from, to); - for (var j = 0; j < found.length; j++) { - if (found[j].sublimeBookmark) { - found[j].clear(); - for (var k = 0; k < marks.length; k++) - if (marks[k] == found[j]) - marks.splice(k--, 1); - break; - } - } - if (j == found.length) - marks.push(cm.markText(from, to, {sublimeBookmark: true, clearWhenEmpty: false})); - } - }; - - cmds.clearBookmarks = function(cm) { - var marks = cm.state.sublimeBookmarks; - if (marks) for (var i = 0; i < marks.length; i++) marks[i].clear(); - marks.length = 0; - }; - - cmds.selectBookmarks = function(cm) { - var marks = cm.state.sublimeBookmarks, ranges = []; - if (marks) for (var i = 0; i < marks.length; i++) { - var found = marks[i].find(); - if (!found) - marks.splice(i--, 0); - else - ranges.push({anchor: found.from, head: found.to}); - } - if (ranges.length) - cm.setSelections(ranges, 0); - }; - - function modifyWordOrSelection(cm, mod) { - cm.operation(function() { - var ranges = cm.listSelections(), indices = [], replacements = []; - for (var i = 0; i < ranges.length; i++) { - var range = ranges[i]; - if (range.empty()) { indices.push(i); replacements.push(""); } - else replacements.push(mod(cm.getRange(range.from(), range.to()))); - } - cm.replaceSelections(replacements, "around", "case"); - for (var i = indices.length - 1, at; i >= 0; i--) { - var range = ranges[indices[i]]; - if (at && CodeMirror.cmpPos(range.head, at) > 0) continue; - var word = wordAt(cm, range.head); - at = word.from; - cm.replaceRange(mod(word.word), word.from, word.to); - } - }); - } - - cmds.smartBackspace = function(cm) { - if (cm.somethingSelected()) return CodeMirror.Pass; - - cm.operation(function() { - var cursors = cm.listSelections(); - var indentUnit = cm.getOption("indentUnit"); - - for (var i = cursors.length - 1; i >= 0; i--) { - var cursor = cursors[i].head; - var toStartOfLine = cm.getRange({line: cursor.line, ch: 0}, cursor); - var column = CodeMirror.countColumn(toStartOfLine, null, cm.getOption("tabSize")); - - // Delete by one character by default - var deletePos = cm.findPosH(cursor, -1, "char", false); - - if (toStartOfLine && !/\S/.test(toStartOfLine) && column % indentUnit == 0) { - var prevIndent = new Pos(cursor.line, - CodeMirror.findColumn(toStartOfLine, column - indentUnit, indentUnit)); - - // Smart delete only if we found a valid prevIndent location - if (prevIndent.ch != cursor.ch) deletePos = prevIndent; - } - - cm.replaceRange("", deletePos, cursor, "+delete"); - } - }); - }; - - cmds.delLineRight = function(cm) { - cm.operation(function() { - var ranges = cm.listSelections(); - for (var i = ranges.length - 1; i >= 0; i--) - cm.replaceRange("", ranges[i].anchor, Pos(ranges[i].to().line), "+delete"); - cm.scrollIntoView(); - }); - }; - - cmds.upcaseAtCursor = function(cm) { - modifyWordOrSelection(cm, function(str) { return str.toUpperCase(); }); - }; - cmds.downcaseAtCursor = function(cm) { - modifyWordOrSelection(cm, function(str) { return str.toLowerCase(); }); - }; - - cmds.setSublimeMark = function(cm) { - if (cm.state.sublimeMark) cm.state.sublimeMark.clear(); - cm.state.sublimeMark = cm.setBookmark(cm.getCursor()); - }; - cmds.selectToSublimeMark = function(cm) { - var found = cm.state.sublimeMark && cm.state.sublimeMark.find(); - if (found) cm.setSelection(cm.getCursor(), found); - }; - cmds.deleteToSublimeMark = function(cm) { - var found = cm.state.sublimeMark && cm.state.sublimeMark.find(); - if (found) { - var from = cm.getCursor(), to = found; - if (CodeMirror.cmpPos(from, to) > 0) { var tmp = to; to = from; from = tmp; } - cm.state.sublimeKilled = cm.getRange(from, to); - cm.replaceRange("", from, to); - } - }; - cmds.swapWithSublimeMark = function(cm) { - var found = cm.state.sublimeMark && cm.state.sublimeMark.find(); - if (found) { - cm.state.sublimeMark.clear(); - cm.state.sublimeMark = cm.setBookmark(cm.getCursor()); - cm.setCursor(found); - } - }; - cmds.sublimeYank = function(cm) { - if (cm.state.sublimeKilled != null) - cm.replaceSelection(cm.state.sublimeKilled, null, "paste"); - }; - - cmds.showInCenter = function(cm) { - var pos = cm.cursorCoords(null, "local"); - cm.scrollTo(null, (pos.top + pos.bottom) / 2 - cm.getScrollInfo().clientHeight / 2); - }; - - function getTarget(cm) { - var from = cm.getCursor("from"), to = cm.getCursor("to"); - if (CodeMirror.cmpPos(from, to) == 0) { - var word = wordAt(cm, from); - if (!word.word) return; - from = word.from; - to = word.to; - } - return {from: from, to: to, query: cm.getRange(from, to), word: word}; - } - - function findAndGoTo(cm, forward) { - var target = getTarget(cm); - if (!target) return; - var query = target.query; - var cur = cm.getSearchCursor(query, forward ? target.to : target.from); - - if (forward ? cur.findNext() : cur.findPrevious()) { - cm.setSelection(cur.from(), cur.to()); - } else { - cur = cm.getSearchCursor(query, forward ? Pos(cm.firstLine(), 0) - : cm.clipPos(Pos(cm.lastLine()))); - if (forward ? cur.findNext() : cur.findPrevious()) - cm.setSelection(cur.from(), cur.to()); - else if (target.word) - cm.setSelection(target.from, target.to); - } - }; - cmds.findUnder = function(cm) { findAndGoTo(cm, true); }; - cmds.findUnderPrevious = function(cm) { findAndGoTo(cm,false); }; - cmds.findAllUnder = function(cm) { - var target = getTarget(cm); - if (!target) return; - var cur = cm.getSearchCursor(target.query); - var matches = []; - var primaryIndex = -1; - while (cur.findNext()) { - matches.push({anchor: cur.from(), head: cur.to()}); - if (cur.from().line <= target.from.line && cur.from().ch <= target.from.ch) - primaryIndex++; - } - cm.setSelections(matches, primaryIndex); - }; - - - var keyMap = CodeMirror.keyMap; - keyMap.macSublime = { - "Cmd-Left": "goLineStartSmart", - "Shift-Tab": "indentLess", - "Shift-Ctrl-K": "deleteLine", - "Alt-Q": "wrapLines", - "Ctrl-Left": "goSubwordLeft", - "Ctrl-Right": "goSubwordRight", - "Ctrl-Alt-Up": "scrollLineUp", - "Ctrl-Alt-Down": "scrollLineDown", - "Cmd-L": "selectLine", - "Shift-Cmd-L": "splitSelectionByLine", - "Esc": "singleSelectionTop", - "Cmd-Enter": "insertLineAfter", - "Shift-Cmd-Enter": "insertLineBefore", - "Cmd-D": "selectNextOccurrence", - "Shift-Cmd-Space": "selectScope", - "Shift-Cmd-M": "selectBetweenBrackets", - "Cmd-M": "goToBracket", - "Cmd-Ctrl-Up": "swapLineUp", - "Cmd-Ctrl-Down": "swapLineDown", - "Cmd-/": "toggleCommentIndented", - "Cmd-J": "joinLines", - "Shift-Cmd-D": "duplicateLine", - "F5": "sortLines", - "Cmd-F5": "sortLinesInsensitive", - "F2": "nextBookmark", - "Shift-F2": "prevBookmark", - "Cmd-F2": "toggleBookmark", - "Shift-Cmd-F2": "clearBookmarks", - "Alt-F2": "selectBookmarks", - "Backspace": "smartBackspace", - "Cmd-K Cmd-D": "skipAndSelectNextOccurrence", - "Cmd-K Cmd-K": "delLineRight", - "Cmd-K Cmd-U": "upcaseAtCursor", - "Cmd-K Cmd-L": "downcaseAtCursor", - "Cmd-K Cmd-Space": "setSublimeMark", - "Cmd-K Cmd-A": "selectToSublimeMark", - "Cmd-K Cmd-W": "deleteToSublimeMark", - "Cmd-K Cmd-X": "swapWithSublimeMark", - "Cmd-K Cmd-Y": "sublimeYank", - "Cmd-K Cmd-C": "showInCenter", - "Cmd-K Cmd-G": "clearBookmarks", - "Cmd-K Cmd-Backspace": "delLineLeft", - "Cmd-K Cmd-1": "foldAll", - "Cmd-K Cmd-0": "unfoldAll", - "Cmd-K Cmd-J": "unfoldAll", - "Ctrl-Shift-Up": "addCursorToPrevLine", - "Ctrl-Shift-Down": "addCursorToNextLine", - "Cmd-F3": "findUnder", - "Shift-Cmd-F3": "findUnderPrevious", - "Alt-F3": "findAllUnder", - "Shift-Cmd-[": "fold", - "Shift-Cmd-]": "unfold", - "Cmd-I": "findIncremental", - "Shift-Cmd-I": "findIncrementalReverse", - "Cmd-H": "replace", - "F3": "findNext", - "Shift-F3": "findPrev", - "fallthrough": "macDefault" - }; - CodeMirror.normalizeKeyMap(keyMap.macSublime); - - keyMap.pcSublime = { - "Shift-Tab": "indentLess", - "Shift-Ctrl-K": "deleteLine", - "Alt-Q": "wrapLines", - "Ctrl-T": "transposeChars", - "Alt-Left": "goSubwordLeft", - "Alt-Right": "goSubwordRight", - "Ctrl-Up": "scrollLineUp", - "Ctrl-Down": "scrollLineDown", - "Ctrl-L": "selectLine", - "Shift-Ctrl-L": "splitSelectionByLine", - "Esc": "singleSelectionTop", - "Ctrl-Enter": "insertLineAfter", - "Shift-Ctrl-Enter": "insertLineBefore", - "Ctrl-D": "selectNextOccurrence", - "Shift-Ctrl-Space": "selectScope", - "Shift-Ctrl-M": "selectBetweenBrackets", - "Ctrl-M": "goToBracket", - "Shift-Ctrl-Up": "swapLineUp", - "Shift-Ctrl-Down": "swapLineDown", - "Ctrl-/": "toggleCommentIndented", - "Ctrl-J": "joinLines", - "Shift-Ctrl-D": "duplicateLine", - "F9": "sortLines", - "Ctrl-F9": "sortLinesInsensitive", - "F2": "nextBookmark", - "Shift-F2": "prevBookmark", - "Ctrl-F2": "toggleBookmark", - "Shift-Ctrl-F2": "clearBookmarks", - "Alt-F2": "selectBookmarks", - "Backspace": "smartBackspace", - "Ctrl-K Ctrl-D": "skipAndSelectNextOccurrence", - "Ctrl-K Ctrl-K": "delLineRight", - "Ctrl-K Ctrl-U": "upcaseAtCursor", - "Ctrl-K Ctrl-L": "downcaseAtCursor", - "Ctrl-K Ctrl-Space": "setSublimeMark", - "Ctrl-K Ctrl-A": "selectToSublimeMark", - "Ctrl-K Ctrl-W": "deleteToSublimeMark", - "Ctrl-K Ctrl-X": "swapWithSublimeMark", - "Ctrl-K Ctrl-Y": "sublimeYank", - "Ctrl-K Ctrl-C": "showInCenter", - "Ctrl-K Ctrl-G": "clearBookmarks", - "Ctrl-K Ctrl-Backspace": "delLineLeft", - "Ctrl-K Ctrl-1": "foldAll", - "Ctrl-K Ctrl-0": "unfoldAll", - "Ctrl-K Ctrl-J": "unfoldAll", - "Ctrl-Alt-Up": "addCursorToPrevLine", - "Ctrl-Alt-Down": "addCursorToNextLine", - "Ctrl-F3": "findUnder", - "Shift-Ctrl-F3": "findUnderPrevious", - "Alt-F3": "findAllUnder", - "Shift-Ctrl-[": "fold", - "Shift-Ctrl-]": "unfold", - "Ctrl-I": "findIncremental", - "Shift-Ctrl-I": "findIncrementalReverse", - "Ctrl-H": "replace", - "F3": "findNext", - "Shift-F3": "findPrev", - "fallthrough": "pcDefault" - }; - CodeMirror.normalizeKeyMap(keyMap.pcSublime); - - var mac = keyMap.default == keyMap.macDefault; - keyMap.sublime = mac ? keyMap.macSublime : keyMap.pcSublime; -}); - - -/* ---- extension/dialog/dialog.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -// Open simple dialogs on top of an editor. Relies on dialog.css. - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - function dialogDiv(cm, template, bottom) { - var wrap = cm.getWrapperElement(); - var dialog; - dialog = wrap.appendChild(document.createElement("div")); - if (bottom) - dialog.className = "CodeMirror-dialog CodeMirror-dialog-bottom"; - else - dialog.className = "CodeMirror-dialog CodeMirror-dialog-top"; - - if (typeof template == "string") { - dialog.innerHTML = template; - } else { // Assuming it's a detached DOM element. - dialog.appendChild(template); - } - CodeMirror.addClass(wrap, 'dialog-opened'); - return dialog; - } - - function closeNotification(cm, newVal) { - if (cm.state.currentNotificationClose) - cm.state.currentNotificationClose(); - cm.state.currentNotificationClose = newVal; - } - - CodeMirror.defineExtension("openDialog", function(template, callback, options) { - if (!options) options = {}; - - closeNotification(this, null); - - var dialog = dialogDiv(this, template, options.bottom); - var closed = false, me = this; - function close(newVal) { - if (typeof newVal == 'string') { - inp.value = newVal; - } else { - if (closed) return; - closed = true; - CodeMirror.rmClass(dialog.parentNode, 'dialog-opened'); - dialog.parentNode.removeChild(dialog); - me.focus(); - - if (options.onClose) options.onClose(dialog); - } - } - - var inp = dialog.getElementsByTagName("input")[0], button; - if (inp) { - inp.focus(); - - if (options.value) { - inp.value = options.value; - if (options.selectValueOnOpen !== false) { - inp.select(); - } - } - - if (options.onInput) - CodeMirror.on(inp, "input", function(e) { options.onInput(e, inp.value, close);}); - if (options.onKeyUp) - CodeMirror.on(inp, "keyup", function(e) {options.onKeyUp(e, inp.value, close);}); - - CodeMirror.on(inp, "keydown", function(e) { - if (options && options.onKeyDown && options.onKeyDown(e, inp.value, close)) { return; } - if (e.keyCode == 27 || (options.closeOnEnter !== false && e.keyCode == 13)) { - inp.blur(); - CodeMirror.e_stop(e); - close(); - } - if (e.keyCode == 13) callback(inp.value, e); - }); - - if (options.closeOnBlur !== false) CodeMirror.on(dialog, "focusout", function (evt) { - if (evt.relatedTarget !== null) close(); - }); - } else if (button = dialog.getElementsByTagName("button")[0]) { - CodeMirror.on(button, "click", function() { - close(); - me.focus(); - }); - - if (options.closeOnBlur !== false) CodeMirror.on(button, "blur", close); - - button.focus(); - } - return close; - }); - - CodeMirror.defineExtension("openConfirm", function(template, callbacks, options) { - closeNotification(this, null); - var dialog = dialogDiv(this, template, options && options.bottom); - var buttons = dialog.getElementsByTagName("button"); - var closed = false, me = this, blurring = 1; - function close() { - if (closed) return; - closed = true; - CodeMirror.rmClass(dialog.parentNode, 'dialog-opened'); - dialog.parentNode.removeChild(dialog); - me.focus(); - } - buttons[0].focus(); - for (var i = 0; i < buttons.length; ++i) { - var b = buttons[i]; - (function(callback) { - CodeMirror.on(b, "click", function(e) { - CodeMirror.e_preventDefault(e); - close(); - if (callback) callback(me); - }); - })(callbacks[i]); - CodeMirror.on(b, "blur", function() { - --blurring; - setTimeout(function() { if (blurring <= 0) close(); }, 200); - }); - CodeMirror.on(b, "focus", function() { ++blurring; }); - } - }); - - /* - * openNotification - * Opens a notification, that can be closed with an optional timer - * (default 5000ms timer) and always closes on click. - * - * If a notification is opened while another is opened, it will close the - * currently opened one and open the new one immediately. - */ - CodeMirror.defineExtension("openNotification", function(template, options) { - closeNotification(this, close); - var dialog = dialogDiv(this, template, options && options.bottom); - var closed = false, doneTimer; - var duration = options && typeof options.duration !== "undefined" ? options.duration : 5000; - - function close() { - if (closed) return; - closed = true; - clearTimeout(doneTimer); - CodeMirror.rmClass(dialog.parentNode, 'dialog-opened'); - dialog.parentNode.removeChild(dialog); - } - - CodeMirror.on(dialog, 'click', function(e) { - CodeMirror.e_preventDefault(e); - close(); - }); - - if (duration) - doneTimer = setTimeout(close, duration); - - return close; - }); -}); - - -/* ---- extension/edit/closebrackets.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - var defaults = { - pairs: "()[]{}''\"\"", - closeBefore: ")]}'\":;>", - triples: "", - explode: "[]{}" - }; - - var Pos = CodeMirror.Pos; - - CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) { - if (old && old != CodeMirror.Init) { - cm.removeKeyMap(keyMap); - cm.state.closeBrackets = null; - } - if (val) { - ensureBound(getOption(val, "pairs")) - cm.state.closeBrackets = val; - cm.addKeyMap(keyMap); - } - }); - - function getOption(conf, name) { - if (name == "pairs" && typeof conf == "string") return conf; - if (typeof conf == "object" && conf[name] != null) return conf[name]; - return defaults[name]; - } - - var keyMap = {Backspace: handleBackspace, Enter: handleEnter}; - function ensureBound(chars) { - for (var i = 0; i < chars.length; i++) { - var ch = chars.charAt(i), key = "'" + ch + "'" - if (!keyMap[key]) keyMap[key] = handler(ch) - } - } - ensureBound(defaults.pairs + "`") - - function handler(ch) { - return function(cm) { return handleChar(cm, ch); }; - } - - function getConfig(cm) { - var deflt = cm.state.closeBrackets; - if (!deflt || deflt.override) return deflt; - var mode = cm.getModeAt(cm.getCursor()); - return mode.closeBrackets || deflt; - } - - function handleBackspace(cm) { - var conf = getConfig(cm); - if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass; - - var pairs = getOption(conf, "pairs"); - var ranges = cm.listSelections(); - for (var i = 0; i < ranges.length; i++) { - if (!ranges[i].empty()) return CodeMirror.Pass; - var around = charsAround(cm, ranges[i].head); - if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass; - } - for (var i = ranges.length - 1; i >= 0; i--) { - var cur = ranges[i].head; - cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1), "+delete"); - } - } - - function handleEnter(cm) { - var conf = getConfig(cm); - var explode = conf && getOption(conf, "explode"); - if (!explode || cm.getOption("disableInput")) return CodeMirror.Pass; - - var ranges = cm.listSelections(); - for (var i = 0; i < ranges.length; i++) { - if (!ranges[i].empty()) return CodeMirror.Pass; - var around = charsAround(cm, ranges[i].head); - if (!around || explode.indexOf(around) % 2 != 0) return CodeMirror.Pass; - } - cm.operation(function() { - var linesep = cm.lineSeparator() || "\n"; - cm.replaceSelection(linesep + linesep, null); - cm.execCommand("goCharLeft"); - ranges = cm.listSelections(); - for (var i = 0; i < ranges.length; i++) { - var line = ranges[i].head.line; - cm.indentLine(line, null, true); - cm.indentLine(line + 1, null, true); - } - }); - } - - function contractSelection(sel) { - var inverted = CodeMirror.cmpPos(sel.anchor, sel.head) > 0; - return {anchor: new Pos(sel.anchor.line, sel.anchor.ch + (inverted ? -1 : 1)), - head: new Pos(sel.head.line, sel.head.ch + (inverted ? 1 : -1))}; - } - - function handleChar(cm, ch) { - var conf = getConfig(cm); - if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass; - - var pairs = getOption(conf, "pairs"); - var pos = pairs.indexOf(ch); - if (pos == -1) return CodeMirror.Pass; - - var closeBefore = getOption(conf,"closeBefore"); - - var triples = getOption(conf, "triples"); - - var identical = pairs.charAt(pos + 1) == ch; - var ranges = cm.listSelections(); - var opening = pos % 2 == 0; - - var type; - for (var i = 0; i < ranges.length; i++) { - var range = ranges[i], cur = range.head, curType; - var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1)); - if (opening && !range.empty()) { - curType = "surround"; - } else if ((identical || !opening) && next == ch) { - if (identical && stringStartsAfter(cm, cur)) - curType = "both"; - else if (triples.indexOf(ch) >= 0 && cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch) - curType = "skipThree"; - else - curType = "skip"; - } else if (identical && cur.ch > 1 && triples.indexOf(ch) >= 0 && - cm.getRange(Pos(cur.line, cur.ch - 2), cur) == ch + ch) { - if (cur.ch > 2 && /\bstring/.test(cm.getTokenTypeAt(Pos(cur.line, cur.ch - 2)))) return CodeMirror.Pass; - curType = "addFour"; - } else if (identical) { - var prev = cur.ch == 0 ? " " : cm.getRange(Pos(cur.line, cur.ch - 1), cur) - if (!CodeMirror.isWordChar(next) && prev != ch && !CodeMirror.isWordChar(prev)) curType = "both"; - else return CodeMirror.Pass; - } else if (opening && (next.length === 0 || /\s/.test(next) || closeBefore.indexOf(next) > -1)) { - curType = "both"; - } else { - return CodeMirror.Pass; - } - if (!type) type = curType; - else if (type != curType) return CodeMirror.Pass; - } - - var left = pos % 2 ? pairs.charAt(pos - 1) : ch; - var right = pos % 2 ? ch : pairs.charAt(pos + 1); - cm.operation(function() { - if (type == "skip") { - cm.execCommand("goCharRight"); - } else if (type == "skipThree") { - for (var i = 0; i < 3; i++) - cm.execCommand("goCharRight"); - } else if (type == "surround") { - var sels = cm.getSelections(); - for (var i = 0; i < sels.length; i++) - sels[i] = left + sels[i] + right; - cm.replaceSelections(sels, "around"); - sels = cm.listSelections().slice(); - for (var i = 0; i < sels.length; i++) - sels[i] = contractSelection(sels[i]); - cm.setSelections(sels); - } else if (type == "both") { - cm.replaceSelection(left + right, null); - cm.triggerElectric(left + right); - cm.execCommand("goCharLeft"); - } else if (type == "addFour") { - cm.replaceSelection(left + left + left + left, "before"); - cm.execCommand("goCharRight"); - } - }); - } - - function charsAround(cm, pos) { - var str = cm.getRange(Pos(pos.line, pos.ch - 1), - Pos(pos.line, pos.ch + 1)); - return str.length == 2 ? str : null; - } - - function stringStartsAfter(cm, pos) { - var token = cm.getTokenAt(Pos(pos.line, pos.ch + 1)) - return /\bstring/.test(token.type) && token.start == pos.ch && - (pos.ch == 0 || !/\bstring/.test(cm.getTokenTypeAt(pos))) - } -}); - - -/* ---- extension/edit/closetag.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -/** - * Tag-closer extension for CodeMirror. - * - * This extension adds an "autoCloseTags" option that can be set to - * either true to get the default behavior, or an object to further - * configure its behavior. - * - * These are supported options: - * - * `whenClosing` (default true) - * Whether to autoclose when the '/' of a closing tag is typed. - * `whenOpening` (default true) - * Whether to autoclose the tag when the final '>' of an opening - * tag is typed. - * `dontCloseTags` (default is empty tags for HTML, none for XML) - * An array of tag names that should not be autoclosed. - * `indentTags` (default is block tags for HTML, none for XML) - * An array of tag names that should, when opened, cause a - * blank line to be added inside the tag, and the blank line and - * closing line to be indented. - * `emptyTags` (default is none) - * An array of XML tag names that should be autoclosed with '/>'. - * - * See demos/closetag.html for a usage example. - */ - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("../fold/xml-fold")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "../fold/xml-fold"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - CodeMirror.defineOption("autoCloseTags", false, function(cm, val, old) { - if (old != CodeMirror.Init && old) - cm.removeKeyMap("autoCloseTags"); - if (!val) return; - var map = {name: "autoCloseTags"}; - if (typeof val != "object" || val.whenClosing !== false) - map["'/'"] = function(cm) { return autoCloseSlash(cm); }; - if (typeof val != "object" || val.whenOpening !== false) - map["'>'"] = function(cm) { return autoCloseGT(cm); }; - cm.addKeyMap(map); - }); - - var htmlDontClose = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", - "source", "track", "wbr"]; - var htmlIndent = ["applet", "blockquote", "body", "button", "div", "dl", "fieldset", "form", "frameset", "h1", "h2", "h3", "h4", - "h5", "h6", "head", "html", "iframe", "layer", "legend", "object", "ol", "p", "select", "table", "ul"]; - - function autoCloseGT(cm) { - if (cm.getOption("disableInput")) return CodeMirror.Pass; - var ranges = cm.listSelections(), replacements = []; - var opt = cm.getOption("autoCloseTags"); - for (var i = 0; i < ranges.length; i++) { - if (!ranges[i].empty()) return CodeMirror.Pass; - var pos = ranges[i].head, tok = cm.getTokenAt(pos); - var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state; - var tagInfo = inner.mode.xmlCurrentTag && inner.mode.xmlCurrentTag(state) - var tagName = tagInfo && tagInfo.name - if (!tagName) return CodeMirror.Pass - - var html = inner.mode.configuration == "html"; - var dontCloseTags = (typeof opt == "object" && opt.dontCloseTags) || (html && htmlDontClose); - var indentTags = (typeof opt == "object" && opt.indentTags) || (html && htmlIndent); - - if (tok.end > pos.ch) tagName = tagName.slice(0, tagName.length - tok.end + pos.ch); - var lowerTagName = tagName.toLowerCase(); - // Don't process the '>' at the end of an end-tag or self-closing tag - if (!tagName || - tok.type == "string" && (tok.end != pos.ch || !/[\"\']/.test(tok.string.charAt(tok.string.length - 1)) || tok.string.length == 1) || - tok.type == "tag" && tagInfo.close || - tok.string.indexOf("/") == (pos.ch - tok.start - 1) || // match something like - dontCloseTags && indexOf(dontCloseTags, lowerTagName) > -1 || - closingTagExists(cm, inner.mode.xmlCurrentContext && inner.mode.xmlCurrentContext(state) || [], tagName, pos, true)) - return CodeMirror.Pass; - - var emptyTags = typeof opt == "object" && opt.emptyTags; - if (emptyTags && indexOf(emptyTags, tagName) > -1) { - replacements[i] = { text: "/>", newPos: CodeMirror.Pos(pos.line, pos.ch + 2) }; - continue; - } - - var indent = indentTags && indexOf(indentTags, lowerTagName) > -1; - replacements[i] = {indent: indent, - text: ">" + (indent ? "\n\n" : "") + "", - newPos: indent ? CodeMirror.Pos(pos.line + 1, 0) : CodeMirror.Pos(pos.line, pos.ch + 1)}; - } - - var dontIndentOnAutoClose = (typeof opt == "object" && opt.dontIndentOnAutoClose); - for (var i = ranges.length - 1; i >= 0; i--) { - var info = replacements[i]; - cm.replaceRange(info.text, ranges[i].head, ranges[i].anchor, "+insert"); - var sel = cm.listSelections().slice(0); - sel[i] = {head: info.newPos, anchor: info.newPos}; - cm.setSelections(sel); - if (!dontIndentOnAutoClose && info.indent) { - cm.indentLine(info.newPos.line, null, true); - cm.indentLine(info.newPos.line + 1, null, true); - } - } - } - - function autoCloseCurrent(cm, typingSlash) { - var ranges = cm.listSelections(), replacements = []; - var head = typingSlash ? "/" : "") replacement += ">"; - replacements[i] = replacement; - } - cm.replaceSelections(replacements); - ranges = cm.listSelections(); - if (!dontIndentOnAutoClose) { - for (var i = 0; i < ranges.length; i++) - if (i == ranges.length - 1 || ranges[i].head.line < ranges[i + 1].head.line) - cm.indentLine(ranges[i].head.line); - } - } - - function autoCloseSlash(cm) { - if (cm.getOption("disableInput")) return CodeMirror.Pass; - return autoCloseCurrent(cm, true); - } - - CodeMirror.commands.closeTag = function(cm) { return autoCloseCurrent(cm); }; - - function indexOf(collection, elt) { - if (collection.indexOf) return collection.indexOf(elt); - for (var i = 0, e = collection.length; i < e; ++i) - if (collection[i] == elt) return i; - return -1; - } - - // If xml-fold is loaded, we use its functionality to try and verify - // whether a given tag is actually unclosed. - function closingTagExists(cm, context, tagName, pos, newTag) { - if (!CodeMirror.scanForClosingTag) return false; - var end = Math.min(cm.lastLine() + 1, pos.line + 500); - var nextClose = CodeMirror.scanForClosingTag(cm, pos, null, end); - if (!nextClose || nextClose.tag != tagName) return false; - // If the immediate wrapping context contains onCx instances of - // the same tag, a closing tag only exists if there are at least - // that many closing tags of that type following. - var onCx = newTag ? 1 : 0 - for (var i = context.length - 1; i >= 0; i--) { - if (context[i] == tagName) ++onCx - else break - } - pos = nextClose.to; - for (var i = 1; i < onCx; i++) { - var next = CodeMirror.scanForClosingTag(cm, pos, null, end); - if (!next || next.tag != tagName) return false; - pos = next.to; - } - return true; - } -}); - - -/* ---- extension/edit/continuelist.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - var listRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/, - emptyListRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/, - unorderedListRE = /[*+-]\s/; - - CodeMirror.commands.newlineAndIndentContinueMarkdownList = function(cm) { - if (cm.getOption("disableInput")) return CodeMirror.Pass; - var ranges = cm.listSelections(), replacements = []; - for (var i = 0; i < ranges.length; i++) { - var pos = ranges[i].head; - - // If we're not in Markdown mode, fall back to normal newlineAndIndent - var eolState = cm.getStateAfter(pos.line); - var inner = CodeMirror.innerMode(cm.getMode(), eolState); - if (inner.mode.name !== "markdown") { - cm.execCommand("newlineAndIndent"); - return; - } else { - eolState = inner.state; - } - - var inList = eolState.list !== false; - var inQuote = eolState.quote !== 0; - - var line = cm.getLine(pos.line), match = listRE.exec(line); - var cursorBeforeBullet = /^\s*$/.test(line.slice(0, pos.ch)); - if (!ranges[i].empty() || (!inList && !inQuote) || !match || cursorBeforeBullet) { - cm.execCommand("newlineAndIndent"); - return; - } - if (emptyListRE.test(line)) { - var endOfQuote = inQuote && />\s*$/.test(line) - var endOfList = !/>\s*$/.test(line) - if (endOfQuote || endOfList) cm.replaceRange("", { - line: pos.line, ch: 0 - }, { - line: pos.line, ch: pos.ch + 1 - }); - replacements[i] = "\n"; - } else { - var indent = match[1], after = match[5]; - var numbered = !(unorderedListRE.test(match[2]) || match[2].indexOf(">") >= 0); - var bullet = numbered ? (parseInt(match[3], 10) + 1) + match[4] : match[2].replace("x", " "); - replacements[i] = "\n" + indent + bullet + after; - - if (numbered) incrementRemainingMarkdownListNumbers(cm, pos); - } - } - - cm.replaceSelections(replacements); - }; - - // Auto-updating Markdown list numbers when a new item is added to the - // middle of a list - function incrementRemainingMarkdownListNumbers(cm, pos) { - var startLine = pos.line, lookAhead = 0, skipCount = 0; - var startItem = listRE.exec(cm.getLine(startLine)), startIndent = startItem[1]; - - do { - lookAhead += 1; - var nextLineNumber = startLine + lookAhead; - var nextLine = cm.getLine(nextLineNumber), nextItem = listRE.exec(nextLine); - - if (nextItem) { - var nextIndent = nextItem[1]; - var newNumber = (parseInt(startItem[3], 10) + lookAhead - skipCount); - var nextNumber = (parseInt(nextItem[3], 10)), itemNumber = nextNumber; - - if (startIndent === nextIndent && !isNaN(nextNumber)) { - if (newNumber === nextNumber) itemNumber = nextNumber + 1; - if (newNumber > nextNumber) itemNumber = newNumber + 1; - cm.replaceRange( - nextLine.replace(listRE, nextIndent + itemNumber + nextItem[4] + nextItem[5]), - { - line: nextLineNumber, ch: 0 - }, { - line: nextLineNumber, ch: nextLine.length - }); - } else { - if (startIndent.length > nextIndent.length) return; - // This doesn't run if the next line immediatley indents, as it is - // not clear of the users intention (new indented item or same level) - if ((startIndent.length < nextIndent.length) && (lookAhead === 1)) return; - skipCount += 1; - } - } - } while (nextItem); - } -}); - - -/* ---- extension/edit/matchbrackets.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - var ie_lt8 = /MSIE \d/.test(navigator.userAgent) && - (document.documentMode == null || document.documentMode < 8); - - var Pos = CodeMirror.Pos; - - var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<", "<": ">>", ">": "<<"}; - - function bracketRegex(config) { - return config && config.bracketRegex || /[(){}[\]]/ - } - - function findMatchingBracket(cm, where, config) { - var line = cm.getLineHandle(where.line), pos = where.ch - 1; - var afterCursor = config && config.afterCursor - if (afterCursor == null) - afterCursor = /(^| )cm-fat-cursor($| )/.test(cm.getWrapperElement().className) - var re = bracketRegex(config) - - // A cursor is defined as between two characters, but in in vim command mode - // (i.e. not insert mode), the cursor is visually represented as a - // highlighted box on top of the 2nd character. Otherwise, we allow matches - // from before or after the cursor. - var match = (!afterCursor && pos >= 0 && re.test(line.text.charAt(pos)) && matching[line.text.charAt(pos)]) || - re.test(line.text.charAt(pos + 1)) && matching[line.text.charAt(++pos)]; - if (!match) return null; - var dir = match.charAt(1) == ">" ? 1 : -1; - if (config && config.strict && (dir > 0) != (pos == where.ch)) return null; - var style = cm.getTokenTypeAt(Pos(where.line, pos + 1)); - - var found = scanForBracket(cm, Pos(where.line, pos + (dir > 0 ? 1 : 0)), dir, style || null, config); - if (found == null) return null; - return {from: Pos(where.line, pos), to: found && found.pos, - match: found && found.ch == match.charAt(0), forward: dir > 0}; - } - - // bracketRegex is used to specify which type of bracket to scan - // should be a regexp, e.g. /[[\]]/ - // - // Note: If "where" is on an open bracket, then this bracket is ignored. - // - // Returns false when no bracket was found, null when it reached - // maxScanLines and gave up - function scanForBracket(cm, where, dir, style, config) { - var maxScanLen = (config && config.maxScanLineLength) || 10000; - var maxScanLines = (config && config.maxScanLines) || 1000; - - var stack = []; - var re = bracketRegex(config) - var lineEnd = dir > 0 ? Math.min(where.line + maxScanLines, cm.lastLine() + 1) - : Math.max(cm.firstLine() - 1, where.line - maxScanLines); - for (var lineNo = where.line; lineNo != lineEnd; lineNo += dir) { - var line = cm.getLine(lineNo); - if (!line) continue; - var pos = dir > 0 ? 0 : line.length - 1, end = dir > 0 ? line.length : -1; - if (line.length > maxScanLen) continue; - if (lineNo == where.line) pos = where.ch - (dir < 0 ? 1 : 0); - for (; pos != end; pos += dir) { - var ch = line.charAt(pos); - if (re.test(ch) && (style === undefined || cm.getTokenTypeAt(Pos(lineNo, pos + 1)) == style)) { - var match = matching[ch]; - if (match && (match.charAt(1) == ">") == (dir > 0)) stack.push(ch); - else if (!stack.length) return {pos: Pos(lineNo, pos), ch: ch}; - else stack.pop(); - } - } - } - return lineNo - dir == (dir > 0 ? cm.lastLine() : cm.firstLine()) ? false : null; - } - - function matchBrackets(cm, autoclear, config) { - // Disable brace matching in long lines, since it'll cause hugely slow updates - var maxHighlightLen = cm.state.matchBrackets.maxHighlightLineLength || 1000; - var marks = [], ranges = cm.listSelections(); - for (var i = 0; i < ranges.length; i++) { - var match = ranges[i].empty() && findMatchingBracket(cm, ranges[i].head, config); - if (match && cm.getLine(match.from.line).length <= maxHighlightLen) { - var style = match.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket"; - marks.push(cm.markText(match.from, Pos(match.from.line, match.from.ch + 1), {className: style})); - if (match.to && cm.getLine(match.to.line).length <= maxHighlightLen) - marks.push(cm.markText(match.to, Pos(match.to.line, match.to.ch + 1), {className: style})); - } - } - - if (marks.length) { - // Kludge to work around the IE bug from issue #1193, where text - // input stops going to the textare whever this fires. - if (ie_lt8 && cm.state.focused) cm.focus(); - - var clear = function() { - cm.operation(function() { - for (var i = 0; i < marks.length; i++) marks[i].clear(); - }); - }; - if (autoclear) setTimeout(clear, 800); - else return clear; - } - } - - function doMatchBrackets(cm) { - cm.operation(function() { - if (cm.state.matchBrackets.currentlyHighlighted) { - cm.state.matchBrackets.currentlyHighlighted(); - cm.state.matchBrackets.currentlyHighlighted = null; - } - cm.state.matchBrackets.currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets); - }); - } - - CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) { - function clear(cm) { - if (cm.state.matchBrackets && cm.state.matchBrackets.currentlyHighlighted) { - cm.state.matchBrackets.currentlyHighlighted(); - cm.state.matchBrackets.currentlyHighlighted = null; - } - } - - if (old && old != CodeMirror.Init) { - cm.off("cursorActivity", doMatchBrackets); - cm.off("focus", doMatchBrackets) - cm.off("blur", clear) - clear(cm); - } - if (val) { - cm.state.matchBrackets = typeof val == "object" ? val : {}; - cm.on("cursorActivity", doMatchBrackets); - cm.on("focus", doMatchBrackets) - cm.on("blur", clear) - } - }); - - CodeMirror.defineExtension("matchBrackets", function() {matchBrackets(this, true);}); - CodeMirror.defineExtension("findMatchingBracket", function(pos, config, oldConfig){ - // Backwards-compatibility kludge - if (oldConfig || typeof config == "boolean") { - if (!oldConfig) { - config = config ? {strict: true} : null - } else { - oldConfig.strict = config - config = oldConfig - } - } - return findMatchingBracket(this, pos, config) - }); - CodeMirror.defineExtension("scanForBracket", function(pos, dir, style, config){ - return scanForBracket(this, pos, dir, style, config); - }); -}); - - -/* ---- extension/edit/matchtags.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("../fold/xml-fold")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "../fold/xml-fold"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - CodeMirror.defineOption("matchTags", false, function(cm, val, old) { - if (old && old != CodeMirror.Init) { - cm.off("cursorActivity", doMatchTags); - cm.off("viewportChange", maybeUpdateMatch); - clear(cm); - } - if (val) { - cm.state.matchBothTags = typeof val == "object" && val.bothTags; - cm.on("cursorActivity", doMatchTags); - cm.on("viewportChange", maybeUpdateMatch); - doMatchTags(cm); - } - }); - - function clear(cm) { - if (cm.state.tagHit) cm.state.tagHit.clear(); - if (cm.state.tagOther) cm.state.tagOther.clear(); - cm.state.tagHit = cm.state.tagOther = null; - } - - function doMatchTags(cm) { - cm.state.failedTagMatch = false; - cm.operation(function() { - clear(cm); - if (cm.somethingSelected()) return; - var cur = cm.getCursor(), range = cm.getViewport(); - range.from = Math.min(range.from, cur.line); range.to = Math.max(cur.line + 1, range.to); - var match = CodeMirror.findMatchingTag(cm, cur, range); - if (!match) return; - if (cm.state.matchBothTags) { - var hit = match.at == "open" ? match.open : match.close; - if (hit) cm.state.tagHit = cm.markText(hit.from, hit.to, {className: "CodeMirror-matchingtag"}); - } - var other = match.at == "close" ? match.open : match.close; - if (other) - cm.state.tagOther = cm.markText(other.from, other.to, {className: "CodeMirror-matchingtag"}); - else - cm.state.failedTagMatch = true; - }); - } - - function maybeUpdateMatch(cm) { - if (cm.state.failedTagMatch) doMatchTags(cm); - } - - CodeMirror.commands.toMatchingTag = function(cm) { - var found = CodeMirror.findMatchingTag(cm, cm.getCursor()); - if (found) { - var other = found.at == "close" ? found.open : found.close; - if (other) cm.extendSelection(other.to, other.from); - } - }; -}); - - -/* ---- extension/edit/trailingspace.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - CodeMirror.defineOption("showTrailingSpace", false, function(cm, val, prev) { - if (prev == CodeMirror.Init) prev = false; - if (prev && !val) - cm.removeOverlay("trailingspace"); - else if (!prev && val) - cm.addOverlay({ - token: function(stream) { - for (var l = stream.string.length, i = l; i && /\s/.test(stream.string.charAt(i - 1)); --i) {} - if (i > stream.pos) { stream.pos = i; return null; } - stream.pos = l; - return "trailingspace"; - }, - name: "trailingspace" - }); - }); -}); - - -/* ---- extension/fold/brace-fold.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -CodeMirror.registerHelper("fold", "brace", function(cm, start) { - var line = start.line, lineText = cm.getLine(line); - var tokenType; - - function findOpening(openCh) { - for (var at = start.ch, pass = 0;;) { - var found = at <= 0 ? -1 : lineText.lastIndexOf(openCh, at - 1); - if (found == -1) { - if (pass == 1) break; - pass = 1; - at = lineText.length; - continue; - } - if (pass == 1 && found < start.ch) break; - tokenType = cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1)); - if (!/^(comment|string)/.test(tokenType)) return found + 1; - at = found - 1; - } - } - - var startToken = "{", endToken = "}", startCh = findOpening("{"); - if (startCh == null) { - startToken = "[", endToken = "]"; - startCh = findOpening("["); - } - - if (startCh == null) return; - var count = 1, lastLine = cm.lastLine(), end, endCh; - outer: for (var i = line; i <= lastLine; ++i) { - var text = cm.getLine(i), pos = i == line ? startCh : 0; - for (;;) { - var nextOpen = text.indexOf(startToken, pos), nextClose = text.indexOf(endToken, pos); - if (nextOpen < 0) nextOpen = text.length; - if (nextClose < 0) nextClose = text.length; - pos = Math.min(nextOpen, nextClose); - if (pos == text.length) break; - if (cm.getTokenTypeAt(CodeMirror.Pos(i, pos + 1)) == tokenType) { - if (pos == nextOpen) ++count; - else if (!--count) { end = i; endCh = pos; break outer; } - } - ++pos; - } - } - if (end == null || line == end) return; - return {from: CodeMirror.Pos(line, startCh), - to: CodeMirror.Pos(end, endCh)}; -}); - -CodeMirror.registerHelper("fold", "import", function(cm, start) { - function hasImport(line) { - if (line < cm.firstLine() || line > cm.lastLine()) return null; - var start = cm.getTokenAt(CodeMirror.Pos(line, 1)); - if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1)); - if (start.type != "keyword" || start.string != "import") return null; - // Now find closing semicolon, return its position - for (var i = line, e = Math.min(cm.lastLine(), line + 10); i <= e; ++i) { - var text = cm.getLine(i), semi = text.indexOf(";"); - if (semi != -1) return {startCh: start.end, end: CodeMirror.Pos(i, semi)}; - } - } - - var startLine = start.line, has = hasImport(startLine), prev; - if (!has || hasImport(startLine - 1) || ((prev = hasImport(startLine - 2)) && prev.end.line == startLine - 1)) - return null; - for (var end = has.end;;) { - var next = hasImport(end.line + 1); - if (next == null) break; - end = next.end; - } - return {from: cm.clipPos(CodeMirror.Pos(startLine, has.startCh + 1)), to: end}; -}); - -CodeMirror.registerHelper("fold", "include", function(cm, start) { - function hasInclude(line) { - if (line < cm.firstLine() || line > cm.lastLine()) return null; - var start = cm.getTokenAt(CodeMirror.Pos(line, 1)); - if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1)); - if (start.type == "meta" && start.string.slice(0, 8) == "#include") return start.start + 8; - } - - var startLine = start.line, has = hasInclude(startLine); - if (has == null || hasInclude(startLine - 1) != null) return null; - for (var end = startLine;;) { - var next = hasInclude(end + 1); - if (next == null) break; - ++end; - } - return {from: CodeMirror.Pos(startLine, has + 1), - to: cm.clipPos(CodeMirror.Pos(end))}; -}); - -}); - - -/* ---- extension/fold/comment-fold.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -CodeMirror.registerGlobalHelper("fold", "comment", function(mode) { - return mode.blockCommentStart && mode.blockCommentEnd; -}, function(cm, start) { - var mode = cm.getModeAt(start), startToken = mode.blockCommentStart, endToken = mode.blockCommentEnd; - if (!startToken || !endToken) return; - var line = start.line, lineText = cm.getLine(line); - - var startCh; - for (var at = start.ch, pass = 0;;) { - var found = at <= 0 ? -1 : lineText.lastIndexOf(startToken, at - 1); - if (found == -1) { - if (pass == 1) return; - pass = 1; - at = lineText.length; - continue; - } - if (pass == 1 && found < start.ch) return; - if (/comment/.test(cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1))) && - (found == 0 || lineText.slice(found - endToken.length, found) == endToken || - !/comment/.test(cm.getTokenTypeAt(CodeMirror.Pos(line, found))))) { - startCh = found + startToken.length; - break; - } - at = found - 1; - } - - var depth = 1, lastLine = cm.lastLine(), end, endCh; - outer: for (var i = line; i <= lastLine; ++i) { - var text = cm.getLine(i), pos = i == line ? startCh : 0; - for (;;) { - var nextOpen = text.indexOf(startToken, pos), nextClose = text.indexOf(endToken, pos); - if (nextOpen < 0) nextOpen = text.length; - if (nextClose < 0) nextClose = text.length; - pos = Math.min(nextOpen, nextClose); - if (pos == text.length) break; - if (pos == nextOpen) ++depth; - else if (!--depth) { end = i; endCh = pos; break outer; } - ++pos; - } - } - if (end == null || line == end && endCh == startCh) return; - return {from: CodeMirror.Pos(line, startCh), - to: CodeMirror.Pos(end, endCh)}; -}); - -}); - - -/* ---- extension/fold/foldcode.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - function doFold(cm, pos, options, force) { - if (options && options.call) { - var finder = options; - options = null; - } else { - var finder = getOption(cm, options, "rangeFinder"); - } - if (typeof pos == "number") pos = CodeMirror.Pos(pos, 0); - var minSize = getOption(cm, options, "minFoldSize"); - - function getRange(allowFolded) { - var range = finder(cm, pos); - if (!range || range.to.line - range.from.line < minSize) return null; - var marks = cm.findMarksAt(range.from); - for (var i = 0; i < marks.length; ++i) { - if (marks[i].__isFold && force !== "fold") { - if (!allowFolded) return null; - range.cleared = true; - marks[i].clear(); - } - } - return range; - } - - var range = getRange(true); - if (getOption(cm, options, "scanUp")) while (!range && pos.line > cm.firstLine()) { - pos = CodeMirror.Pos(pos.line - 1, 0); - range = getRange(false); - } - if (!range || range.cleared || force === "unfold") return; - - var myWidget = makeWidget(cm, options, range); - CodeMirror.on(myWidget, "mousedown", function(e) { - myRange.clear(); - CodeMirror.e_preventDefault(e); - }); - var myRange = cm.markText(range.from, range.to, { - replacedWith: myWidget, - clearOnEnter: getOption(cm, options, "clearOnEnter"), - __isFold: true - }); - myRange.on("clear", function(from, to) { - CodeMirror.signal(cm, "unfold", cm, from, to); - }); - CodeMirror.signal(cm, "fold", cm, range.from, range.to); - } - - function makeWidget(cm, options, range) { - var widget = getOption(cm, options, "widget"); - - if (typeof widget == "function") { - widget = widget(range.from, range.to); - } - - if (typeof widget == "string") { - var text = document.createTextNode(widget); - widget = document.createElement("span"); - widget.appendChild(text); - widget.className = "CodeMirror-foldmarker"; - } else if (widget) { - widget = widget.cloneNode(true) - } - return widget; - } - - // Clumsy backwards-compatible interface - CodeMirror.newFoldFunction = function(rangeFinder, widget) { - return function(cm, pos) { doFold(cm, pos, {rangeFinder: rangeFinder, widget: widget}); }; - }; - - // New-style interface - CodeMirror.defineExtension("foldCode", function(pos, options, force) { - doFold(this, pos, options, force); - }); - - CodeMirror.defineExtension("isFolded", function(pos) { - var marks = this.findMarksAt(pos); - for (var i = 0; i < marks.length; ++i) - if (marks[i].__isFold) return true; - }); - - CodeMirror.commands.toggleFold = function(cm) { - cm.foldCode(cm.getCursor()); - }; - CodeMirror.commands.fold = function(cm) { - cm.foldCode(cm.getCursor(), null, "fold"); - }; - CodeMirror.commands.unfold = function(cm) { - cm.foldCode(cm.getCursor(), null, "unfold"); - }; - CodeMirror.commands.foldAll = function(cm) { - cm.operation(function() { - for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) - cm.foldCode(CodeMirror.Pos(i, 0), null, "fold"); - }); - }; - CodeMirror.commands.unfoldAll = function(cm) { - cm.operation(function() { - for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) - cm.foldCode(CodeMirror.Pos(i, 0), null, "unfold"); - }); - }; - - CodeMirror.registerHelper("fold", "combine", function() { - var funcs = Array.prototype.slice.call(arguments, 0); - return function(cm, start) { - for (var i = 0; i < funcs.length; ++i) { - var found = funcs[i](cm, start); - if (found) return found; - } - }; - }); - - CodeMirror.registerHelper("fold", "auto", function(cm, start) { - var helpers = cm.getHelpers(start, "fold"); - for (var i = 0; i < helpers.length; i++) { - var cur = helpers[i](cm, start); - if (cur) return cur; - } - }); - - var defaultOptions = { - rangeFinder: CodeMirror.fold.auto, - widget: "\u2194", - minFoldSize: 0, - scanUp: false, - clearOnEnter: true - }; - - CodeMirror.defineOption("foldOptions", null); - - function getOption(cm, options, name) { - if (options && options[name] !== undefined) - return options[name]; - var editorOptions = cm.options.foldOptions; - if (editorOptions && editorOptions[name] !== undefined) - return editorOptions[name]; - return defaultOptions[name]; - } - - CodeMirror.defineExtension("foldOption", function(options, name) { - return getOption(this, options, name); - }); -}); - - -/* ---- extension/fold/foldgutter.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("./foldcode")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "./foldcode"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - CodeMirror.defineOption("foldGutter", false, function(cm, val, old) { - if (old && old != CodeMirror.Init) { - cm.clearGutter(cm.state.foldGutter.options.gutter); - cm.state.foldGutter = null; - cm.off("gutterClick", onGutterClick); - cm.off("changes", onChange); - cm.off("viewportChange", onViewportChange); - cm.off("fold", onFold); - cm.off("unfold", onFold); - cm.off("swapDoc", onChange); - } - if (val) { - cm.state.foldGutter = new State(parseOptions(val)); - updateInViewport(cm); - cm.on("gutterClick", onGutterClick); - cm.on("changes", onChange); - cm.on("viewportChange", onViewportChange); - cm.on("fold", onFold); - cm.on("unfold", onFold); - cm.on("swapDoc", onChange); - } - }); - - var Pos = CodeMirror.Pos; - - function State(options) { - this.options = options; - this.from = this.to = 0; - } - - function parseOptions(opts) { - if (opts === true) opts = {}; - if (opts.gutter == null) opts.gutter = "CodeMirror-foldgutter"; - if (opts.indicatorOpen == null) opts.indicatorOpen = "CodeMirror-foldgutter-open"; - if (opts.indicatorFolded == null) opts.indicatorFolded = "CodeMirror-foldgutter-folded"; - return opts; - } - - function isFolded(cm, line) { - var marks = cm.findMarks(Pos(line, 0), Pos(line + 1, 0)); - for (var i = 0; i < marks.length; ++i) { - if (marks[i].__isFold) { - var fromPos = marks[i].find(-1); - if (fromPos && fromPos.line === line) - return marks[i]; - } - } - } - - function marker(spec) { - if (typeof spec == "string") { - var elt = document.createElement("div"); - elt.className = spec + " CodeMirror-guttermarker-subtle"; - return elt; - } else { - return spec.cloneNode(true); - } - } - - function updateFoldInfo(cm, from, to) { - var opts = cm.state.foldGutter.options, cur = from - 1; - var minSize = cm.foldOption(opts, "minFoldSize"); - var func = cm.foldOption(opts, "rangeFinder"); - // we can reuse the built-in indicator element if its className matches the new state - var clsFolded = typeof opts.indicatorFolded == "string" && classTest(opts.indicatorFolded); - var clsOpen = typeof opts.indicatorOpen == "string" && classTest(opts.indicatorOpen); - cm.eachLine(from, to, function(line) { - ++cur; - var mark = null; - var old = line.gutterMarkers; - if (old) old = old[opts.gutter]; - if (isFolded(cm, cur)) { - if (clsFolded && old && clsFolded.test(old.className)) return; - mark = marker(opts.indicatorFolded); - } else { - var pos = Pos(cur, 0); - var range = func && func(cm, pos); - if (range && range.to.line - range.from.line >= minSize) { - if (clsOpen && old && clsOpen.test(old.className)) return; - mark = marker(opts.indicatorOpen); - } - } - if (!mark && !old) return; - cm.setGutterMarker(line, opts.gutter, mark); - }); - } - - // copied from CodeMirror/src/util/dom.js - function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*") } - - function updateInViewport(cm) { - var vp = cm.getViewport(), state = cm.state.foldGutter; - if (!state) return; - cm.operation(function() { - updateFoldInfo(cm, vp.from, vp.to); - }); - state.from = vp.from; state.to = vp.to; - } - - function onGutterClick(cm, line, gutter) { - var state = cm.state.foldGutter; - if (!state) return; - var opts = state.options; - if (gutter != opts.gutter) return; - var folded = isFolded(cm, line); - if (folded) folded.clear(); - else cm.foldCode(Pos(line, 0), opts); - } - - function onChange(cm) { - var state = cm.state.foldGutter; - if (!state) return; - var opts = state.options; - state.from = state.to = 0; - clearTimeout(state.changeUpdate); - state.changeUpdate = setTimeout(function() { updateInViewport(cm); }, opts.foldOnChangeTimeSpan || 600); - } - - function onViewportChange(cm) { - var state = cm.state.foldGutter; - if (!state) return; - var opts = state.options; - clearTimeout(state.changeUpdate); - state.changeUpdate = setTimeout(function() { - var vp = cm.getViewport(); - if (state.from == state.to || vp.from - state.to > 20 || state.from - vp.to > 20) { - updateInViewport(cm); - } else { - cm.operation(function() { - if (vp.from < state.from) { - updateFoldInfo(cm, vp.from, state.from); - state.from = vp.from; - } - if (vp.to > state.to) { - updateFoldInfo(cm, state.to, vp.to); - state.to = vp.to; - } - }); - } - }, opts.updateViewportTimeSpan || 400); - } - - function onFold(cm, from) { - var state = cm.state.foldGutter; - if (!state) return; - var line = from.line; - if (line >= state.from && line < state.to) - updateFoldInfo(cm, line, line + 1); - } -}); - - -/* ---- extension/fold/indent-fold.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -function lineIndent(cm, lineNo) { - var text = cm.getLine(lineNo) - var spaceTo = text.search(/\S/) - if (spaceTo == -1 || /\bcomment\b/.test(cm.getTokenTypeAt(CodeMirror.Pos(lineNo, spaceTo + 1)))) - return -1 - return CodeMirror.countColumn(text, null, cm.getOption("tabSize")) -} - -CodeMirror.registerHelper("fold", "indent", function(cm, start) { - var myIndent = lineIndent(cm, start.line) - if (myIndent < 0) return - var lastLineInFold = null - - // Go through lines until we find a line that definitely doesn't belong in - // the block we're folding, or to the end. - for (var i = start.line + 1, end = cm.lastLine(); i <= end; ++i) { - var indent = lineIndent(cm, i) - if (indent == -1) { - } else if (indent > myIndent) { - // Lines with a greater indent are considered part of the block. - lastLineInFold = i; - } else { - // If this line has non-space, non-comment content, and is - // indented less or equal to the start line, it is the start of - // another block. - break; - } - } - if (lastLineInFold) return { - from: CodeMirror.Pos(start.line, cm.getLine(start.line).length), - to: CodeMirror.Pos(lastLineInFold, cm.getLine(lastLineInFold).length) - }; -}); - -}); - - -/* ---- extension/fold/markdown-fold.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -CodeMirror.registerHelper("fold", "markdown", function(cm, start) { - var maxDepth = 100; - - function isHeader(lineNo) { - var tokentype = cm.getTokenTypeAt(CodeMirror.Pos(lineNo, 0)); - return tokentype && /\bheader\b/.test(tokentype); - } - - function headerLevel(lineNo, line, nextLine) { - var match = line && line.match(/^#+/); - if (match && isHeader(lineNo)) return match[0].length; - match = nextLine && nextLine.match(/^[=\-]+\s*$/); - if (match && isHeader(lineNo + 1)) return nextLine[0] == "=" ? 1 : 2; - return maxDepth; - } - - var firstLine = cm.getLine(start.line), nextLine = cm.getLine(start.line + 1); - var level = headerLevel(start.line, firstLine, nextLine); - if (level === maxDepth) return undefined; - - var lastLineNo = cm.lastLine(); - var end = start.line, nextNextLine = cm.getLine(end + 2); - while (end < lastLineNo) { - if (headerLevel(end + 1, nextLine, nextNextLine) <= level) break; - ++end; - nextLine = nextNextLine; - nextNextLine = cm.getLine(end + 2); - } - - return { - from: CodeMirror.Pos(start.line, firstLine.length), - to: CodeMirror.Pos(end, cm.getLine(end).length) - }; -}); - -}); - - -/* ---- extension/fold/xml-fold.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - var Pos = CodeMirror.Pos; - function cmp(a, b) { return a.line - b.line || a.ch - b.ch; } - - var nameStartChar = "A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD"; - var nameChar = nameStartChar + "\-\:\.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040"; - var xmlTagStart = new RegExp("<(/?)([" + nameStartChar + "][" + nameChar + "]*)", "g"); - - function Iter(cm, line, ch, range) { - this.line = line; this.ch = ch; - this.cm = cm; this.text = cm.getLine(line); - this.min = range ? Math.max(range.from, cm.firstLine()) : cm.firstLine(); - this.max = range ? Math.min(range.to - 1, cm.lastLine()) : cm.lastLine(); - } - - function tagAt(iter, ch) { - var type = iter.cm.getTokenTypeAt(Pos(iter.line, ch)); - return type && /\btag\b/.test(type); - } - - function nextLine(iter) { - if (iter.line >= iter.max) return; - iter.ch = 0; - iter.text = iter.cm.getLine(++iter.line); - return true; - } - function prevLine(iter) { - if (iter.line <= iter.min) return; - iter.text = iter.cm.getLine(--iter.line); - iter.ch = iter.text.length; - return true; - } - - function toTagEnd(iter) { - for (;;) { - var gt = iter.text.indexOf(">", iter.ch); - if (gt == -1) { if (nextLine(iter)) continue; else return; } - if (!tagAt(iter, gt + 1)) { iter.ch = gt + 1; continue; } - var lastSlash = iter.text.lastIndexOf("/", gt); - var selfClose = lastSlash > -1 && !/\S/.test(iter.text.slice(lastSlash + 1, gt)); - iter.ch = gt + 1; - return selfClose ? "selfClose" : "regular"; - } - } - function toTagStart(iter) { - for (;;) { - var lt = iter.ch ? iter.text.lastIndexOf("<", iter.ch - 1) : -1; - if (lt == -1) { if (prevLine(iter)) continue; else return; } - if (!tagAt(iter, lt + 1)) { iter.ch = lt; continue; } - xmlTagStart.lastIndex = lt; - iter.ch = lt; - var match = xmlTagStart.exec(iter.text); - if (match && match.index == lt) return match; - } - } - - function toNextTag(iter) { - for (;;) { - xmlTagStart.lastIndex = iter.ch; - var found = xmlTagStart.exec(iter.text); - if (!found) { if (nextLine(iter)) continue; else return; } - if (!tagAt(iter, found.index + 1)) { iter.ch = found.index + 1; continue; } - iter.ch = found.index + found[0].length; - return found; - } - } - function toPrevTag(iter) { - for (;;) { - var gt = iter.ch ? iter.text.lastIndexOf(">", iter.ch - 1) : -1; - if (gt == -1) { if (prevLine(iter)) continue; else return; } - if (!tagAt(iter, gt + 1)) { iter.ch = gt; continue; } - var lastSlash = iter.text.lastIndexOf("/", gt); - var selfClose = lastSlash > -1 && !/\S/.test(iter.text.slice(lastSlash + 1, gt)); - iter.ch = gt + 1; - return selfClose ? "selfClose" : "regular"; - } - } - - function findMatchingClose(iter, tag) { - var stack = []; - for (;;) { - var next = toNextTag(iter), end, startLine = iter.line, startCh = iter.ch - (next ? next[0].length : 0); - if (!next || !(end = toTagEnd(iter))) return; - if (end == "selfClose") continue; - if (next[1]) { // closing tag - for (var i = stack.length - 1; i >= 0; --i) if (stack[i] == next[2]) { - stack.length = i; - break; - } - if (i < 0 && (!tag || tag == next[2])) return { - tag: next[2], - from: Pos(startLine, startCh), - to: Pos(iter.line, iter.ch) - }; - } else { // opening tag - stack.push(next[2]); - } - } - } - function findMatchingOpen(iter, tag) { - var stack = []; - for (;;) { - var prev = toPrevTag(iter); - if (!prev) return; - if (prev == "selfClose") { toTagStart(iter); continue; } - var endLine = iter.line, endCh = iter.ch; - var start = toTagStart(iter); - if (!start) return; - if (start[1]) { // closing tag - stack.push(start[2]); - } else { // opening tag - for (var i = stack.length - 1; i >= 0; --i) if (stack[i] == start[2]) { - stack.length = i; - break; - } - if (i < 0 && (!tag || tag == start[2])) return { - tag: start[2], - from: Pos(iter.line, iter.ch), - to: Pos(endLine, endCh) - }; - } - } - } - - CodeMirror.registerHelper("fold", "xml", function(cm, start) { - var iter = new Iter(cm, start.line, 0); - for (;;) { - var openTag = toNextTag(iter) - if (!openTag || iter.line != start.line) return - var end = toTagEnd(iter) - if (!end) return - if (!openTag[1] && end != "selfClose") { - var startPos = Pos(iter.line, iter.ch); - var endPos = findMatchingClose(iter, openTag[2]); - return endPos && cmp(endPos.from, startPos) > 0 ? {from: startPos, to: endPos.from} : null - } - } - }); - CodeMirror.findMatchingTag = function(cm, pos, range) { - var iter = new Iter(cm, pos.line, pos.ch, range); - if (iter.text.indexOf(">") == -1 && iter.text.indexOf("<") == -1) return; - var end = toTagEnd(iter), to = end && Pos(iter.line, iter.ch); - var start = end && toTagStart(iter); - if (!end || !start || cmp(iter, pos) > 0) return; - var here = {from: Pos(iter.line, iter.ch), to: to, tag: start[2]}; - if (end == "selfClose") return {open: here, close: null, at: "open"}; - - if (start[1]) { // closing tag - return {open: findMatchingOpen(iter, start[2]), close: here, at: "close"}; - } else { // opening tag - iter = new Iter(cm, to.line, to.ch, range); - return {open: here, close: findMatchingClose(iter, start[2]), at: "open"}; - } - }; - - CodeMirror.findEnclosingTag = function(cm, pos, range, tag) { - var iter = new Iter(cm, pos.line, pos.ch, range); - for (;;) { - var open = findMatchingOpen(iter, tag); - if (!open) break; - var forward = new Iter(cm, pos.line, pos.ch, range); - var close = findMatchingClose(forward, open.tag); - if (close) return {open: open, close: close}; - } - }; - - // Used by addon/edit/closetag.js - CodeMirror.scanForClosingTag = function(cm, pos, name, end) { - var iter = new Iter(cm, pos.line, pos.ch, end ? {from: 0, to: end} : null); - return findMatchingClose(iter, name); - }; -}); - - -/* ---- extension/hint/anyword-hint.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - var WORD = /[\w$]+/, RANGE = 500; - - CodeMirror.registerHelper("hint", "anyword", function(editor, options) { - var word = options && options.word || WORD; - var range = options && options.range || RANGE; - var cur = editor.getCursor(), curLine = editor.getLine(cur.line); - var end = cur.ch, start = end; - while (start && word.test(curLine.charAt(start - 1))) --start; - var curWord = start != end && curLine.slice(start, end); - - var list = options && options.list || [], seen = {}; - var re = new RegExp(word.source, "g"); - for (var dir = -1; dir <= 1; dir += 2) { - var line = cur.line, endLine = Math.min(Math.max(line + dir * range, editor.firstLine()), editor.lastLine()) + dir; - for (; line != endLine; line += dir) { - var text = editor.getLine(line), m; - while (m = re.exec(text)) { - if (line == cur.line && m[0] === curWord) continue; - if ((!curWord || m[0].lastIndexOf(curWord, 0) == 0) && !Object.prototype.hasOwnProperty.call(seen, m[0])) { - seen[m[0]] = true; - list.push(m[0]); - } - } - } - } - return {list: list, from: CodeMirror.Pos(cur.line, start), to: CodeMirror.Pos(cur.line, end)}; - }); -}); - - -/* ---- extension/hint/html-hint.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("./xml-hint")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "./xml-hint"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - var langs = "ab aa af ak sq am ar an hy as av ae ay az bm ba eu be bn bh bi bs br bg my ca ch ce ny zh cv kw co cr hr cs da dv nl dz en eo et ee fo fj fi fr ff gl ka de el gn gu ht ha he hz hi ho hu ia id ie ga ig ik io is it iu ja jv kl kn kr ks kk km ki rw ky kv kg ko ku kj la lb lg li ln lo lt lu lv gv mk mg ms ml mt mi mr mh mn na nv nb nd ne ng nn no ii nr oc oj cu om or os pa pi fa pl ps pt qu rm rn ro ru sa sc sd se sm sg sr gd sn si sk sl so st es su sw ss sv ta te tg th ti bo tk tl tn to tr ts tt tw ty ug uk ur uz ve vi vo wa cy wo fy xh yi yo za zu".split(" "); - var targets = ["_blank", "_self", "_top", "_parent"]; - var charsets = ["ascii", "utf-8", "utf-16", "latin1", "latin1"]; - var methods = ["get", "post", "put", "delete"]; - var encs = ["application/x-www-form-urlencoded", "multipart/form-data", "text/plain"]; - var media = ["all", "screen", "print", "embossed", "braille", "handheld", "print", "projection", "screen", "tty", "tv", "speech", - "3d-glasses", "resolution [>][<][=] [X]", "device-aspect-ratio: X/Y", "orientation:portrait", - "orientation:landscape", "device-height: [X]", "device-width: [X]"]; - var s = { attrs: {} }; // Simple tag, reused for a whole lot of tags - - var data = { - a: { - attrs: { - href: null, ping: null, type: null, - media: media, - target: targets, - hreflang: langs - } - }, - abbr: s, - acronym: s, - address: s, - applet: s, - area: { - attrs: { - alt: null, coords: null, href: null, target: null, ping: null, - media: media, hreflang: langs, type: null, - shape: ["default", "rect", "circle", "poly"] - } - }, - article: s, - aside: s, - audio: { - attrs: { - src: null, mediagroup: null, - crossorigin: ["anonymous", "use-credentials"], - preload: ["none", "metadata", "auto"], - autoplay: ["", "autoplay"], - loop: ["", "loop"], - controls: ["", "controls"] - } - }, - b: s, - base: { attrs: { href: null, target: targets } }, - basefont: s, - bdi: s, - bdo: s, - big: s, - blockquote: { attrs: { cite: null } }, - body: s, - br: s, - button: { - attrs: { - form: null, formaction: null, name: null, value: null, - autofocus: ["", "autofocus"], - disabled: ["", "autofocus"], - formenctype: encs, - formmethod: methods, - formnovalidate: ["", "novalidate"], - formtarget: targets, - type: ["submit", "reset", "button"] - } - }, - canvas: { attrs: { width: null, height: null } }, - caption: s, - center: s, - cite: s, - code: s, - col: { attrs: { span: null } }, - colgroup: { attrs: { span: null } }, - command: { - attrs: { - type: ["command", "checkbox", "radio"], - label: null, icon: null, radiogroup: null, command: null, title: null, - disabled: ["", "disabled"], - checked: ["", "checked"] - } - }, - data: { attrs: { value: null } }, - datagrid: { attrs: { disabled: ["", "disabled"], multiple: ["", "multiple"] } }, - datalist: { attrs: { data: null } }, - dd: s, - del: { attrs: { cite: null, datetime: null } }, - details: { attrs: { open: ["", "open"] } }, - dfn: s, - dir: s, - div: s, - dl: s, - dt: s, - em: s, - embed: { attrs: { src: null, type: null, width: null, height: null } }, - eventsource: { attrs: { src: null } }, - fieldset: { attrs: { disabled: ["", "disabled"], form: null, name: null } }, - figcaption: s, - figure: s, - font: s, - footer: s, - form: { - attrs: { - action: null, name: null, - "accept-charset": charsets, - autocomplete: ["on", "off"], - enctype: encs, - method: methods, - novalidate: ["", "novalidate"], - target: targets - } - }, - frame: s, - frameset: s, - h1: s, h2: s, h3: s, h4: s, h5: s, h6: s, - head: { - attrs: {}, - children: ["title", "base", "link", "style", "meta", "script", "noscript", "command"] - }, - header: s, - hgroup: s, - hr: s, - html: { - attrs: { manifest: null }, - children: ["head", "body"] - }, - i: s, - iframe: { - attrs: { - src: null, srcdoc: null, name: null, width: null, height: null, - sandbox: ["allow-top-navigation", "allow-same-origin", "allow-forms", "allow-scripts"], - seamless: ["", "seamless"] - } - }, - img: { - attrs: { - alt: null, src: null, ismap: null, usemap: null, width: null, height: null, - crossorigin: ["anonymous", "use-credentials"] - } - }, - input: { - attrs: { - alt: null, dirname: null, form: null, formaction: null, - height: null, list: null, max: null, maxlength: null, min: null, - name: null, pattern: null, placeholder: null, size: null, src: null, - step: null, value: null, width: null, - accept: ["audio/*", "video/*", "image/*"], - autocomplete: ["on", "off"], - autofocus: ["", "autofocus"], - checked: ["", "checked"], - disabled: ["", "disabled"], - formenctype: encs, - formmethod: methods, - formnovalidate: ["", "novalidate"], - formtarget: targets, - multiple: ["", "multiple"], - readonly: ["", "readonly"], - required: ["", "required"], - type: ["hidden", "text", "search", "tel", "url", "email", "password", "datetime", "date", "month", - "week", "time", "datetime-local", "number", "range", "color", "checkbox", "radio", - "file", "submit", "image", "reset", "button"] - } - }, - ins: { attrs: { cite: null, datetime: null } }, - kbd: s, - keygen: { - attrs: { - challenge: null, form: null, name: null, - autofocus: ["", "autofocus"], - disabled: ["", "disabled"], - keytype: ["RSA"] - } - }, - label: { attrs: { "for": null, form: null } }, - legend: s, - li: { attrs: { value: null } }, - link: { - attrs: { - href: null, type: null, - hreflang: langs, - media: media, - sizes: ["all", "16x16", "16x16 32x32", "16x16 32x32 64x64"] - } - }, - map: { attrs: { name: null } }, - mark: s, - menu: { attrs: { label: null, type: ["list", "context", "toolbar"] } }, - meta: { - attrs: { - content: null, - charset: charsets, - name: ["viewport", "application-name", "author", "description", "generator", "keywords"], - "http-equiv": ["content-language", "content-type", "default-style", "refresh"] - } - }, - meter: { attrs: { value: null, min: null, low: null, high: null, max: null, optimum: null } }, - nav: s, - noframes: s, - noscript: s, - object: { - attrs: { - data: null, type: null, name: null, usemap: null, form: null, width: null, height: null, - typemustmatch: ["", "typemustmatch"] - } - }, - ol: { attrs: { reversed: ["", "reversed"], start: null, type: ["1", "a", "A", "i", "I"] } }, - optgroup: { attrs: { disabled: ["", "disabled"], label: null } }, - option: { attrs: { disabled: ["", "disabled"], label: null, selected: ["", "selected"], value: null } }, - output: { attrs: { "for": null, form: null, name: null } }, - p: s, - param: { attrs: { name: null, value: null } }, - pre: s, - progress: { attrs: { value: null, max: null } }, - q: { attrs: { cite: null } }, - rp: s, - rt: s, - ruby: s, - s: s, - samp: s, - script: { - attrs: { - type: ["text/javascript"], - src: null, - async: ["", "async"], - defer: ["", "defer"], - charset: charsets - } - }, - section: s, - select: { - attrs: { - form: null, name: null, size: null, - autofocus: ["", "autofocus"], - disabled: ["", "disabled"], - multiple: ["", "multiple"] - } - }, - small: s, - source: { attrs: { src: null, type: null, media: null } }, - span: s, - strike: s, - strong: s, - style: { - attrs: { - type: ["text/css"], - media: media, - scoped: null - } - }, - sub: s, - summary: s, - sup: s, - table: s, - tbody: s, - td: { attrs: { colspan: null, rowspan: null, headers: null } }, - textarea: { - attrs: { - dirname: null, form: null, maxlength: null, name: null, placeholder: null, - rows: null, cols: null, - autofocus: ["", "autofocus"], - disabled: ["", "disabled"], - readonly: ["", "readonly"], - required: ["", "required"], - wrap: ["soft", "hard"] - } - }, - tfoot: s, - th: { attrs: { colspan: null, rowspan: null, headers: null, scope: ["row", "col", "rowgroup", "colgroup"] } }, - thead: s, - time: { attrs: { datetime: null } }, - title: s, - tr: s, - track: { - attrs: { - src: null, label: null, "default": null, - kind: ["subtitles", "captions", "descriptions", "chapters", "metadata"], - srclang: langs - } - }, - tt: s, - u: s, - ul: s, - "var": s, - video: { - attrs: { - src: null, poster: null, width: null, height: null, - crossorigin: ["anonymous", "use-credentials"], - preload: ["auto", "metadata", "none"], - autoplay: ["", "autoplay"], - mediagroup: ["movie"], - muted: ["", "muted"], - controls: ["", "controls"] - } - }, - wbr: s - }; - - var globalAttrs = { - accesskey: ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], - "class": null, - contenteditable: ["true", "false"], - contextmenu: null, - dir: ["ltr", "rtl", "auto"], - draggable: ["true", "false", "auto"], - dropzone: ["copy", "move", "link", "string:", "file:"], - hidden: ["hidden"], - id: null, - inert: ["inert"], - itemid: null, - itemprop: null, - itemref: null, - itemscope: ["itemscope"], - itemtype: null, - lang: ["en", "es"], - spellcheck: ["true", "false"], - autocorrect: ["true", "false"], - autocapitalize: ["true", "false"], - style: null, - tabindex: ["1", "2", "3", "4", "5", "6", "7", "8", "9"], - title: null, - translate: ["yes", "no"], - onclick: null, - rel: ["stylesheet", "alternate", "author", "bookmark", "help", "license", "next", "nofollow", "noreferrer", "prefetch", "prev", "search", "tag"] - }; - function populate(obj) { - for (var attr in globalAttrs) if (globalAttrs.hasOwnProperty(attr)) - obj.attrs[attr] = globalAttrs[attr]; - } - - populate(s); - for (var tag in data) if (data.hasOwnProperty(tag) && data[tag] != s) - populate(data[tag]); - - CodeMirror.htmlSchema = data; - function htmlHint(cm, options) { - var local = {schemaInfo: data}; - if (options) for (var opt in options) local[opt] = options[opt]; - return CodeMirror.hint.xml(cm, local); - } - CodeMirror.registerHelper("hint", "html", htmlHint); -}); - - -/* ---- extension/hint/show-hint.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - var HINT_ELEMENT_CLASS = "CodeMirror-hint"; - var ACTIVE_HINT_ELEMENT_CLASS = "CodeMirror-hint-active"; - - // This is the old interface, kept around for now to stay - // backwards-compatible. - CodeMirror.showHint = function(cm, getHints, options) { - if (!getHints) return cm.showHint(options); - if (options && options.async) getHints.async = true; - var newOpts = {hint: getHints}; - if (options) for (var prop in options) newOpts[prop] = options[prop]; - return cm.showHint(newOpts); - }; - - CodeMirror.defineExtension("showHint", function(options) { - options = parseOptions(this, this.getCursor("start"), options); - var selections = this.listSelections() - if (selections.length > 1) return; - // By default, don't allow completion when something is selected. - // A hint function can have a `supportsSelection` property to - // indicate that it can handle selections. - if (this.somethingSelected()) { - if (!options.hint.supportsSelection) return; - // Don't try with cross-line selections - for (var i = 0; i < selections.length; i++) - if (selections[i].head.line != selections[i].anchor.line) return; - } - - if (this.state.completionActive) this.state.completionActive.close(); - var completion = this.state.completionActive = new Completion(this, options); - if (!completion.options.hint) return; - - CodeMirror.signal(this, "startCompletion", this); - completion.update(true); - }); - - CodeMirror.defineExtension("closeHint", function() { - if (this.state.completionActive) this.state.completionActive.close() - }) - - function Completion(cm, options) { - this.cm = cm; - this.options = options; - this.widget = null; - this.debounce = 0; - this.tick = 0; - this.startPos = this.cm.getCursor("start"); - this.startLen = this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length; - - var self = this; - cm.on("cursorActivity", this.activityFunc = function() { self.cursorActivity(); }); - } - - var requestAnimationFrame = window.requestAnimationFrame || function(fn) { - return setTimeout(fn, 1000/60); - }; - var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout; - - Completion.prototype = { - close: function() { - if (!this.active()) return; - this.cm.state.completionActive = null; - this.tick = null; - this.cm.off("cursorActivity", this.activityFunc); - - if (this.widget && this.data) CodeMirror.signal(this.data, "close"); - if (this.widget) this.widget.close(); - CodeMirror.signal(this.cm, "endCompletion", this.cm); - }, - - active: function() { - return this.cm.state.completionActive == this; - }, - - pick: function(data, i) { - var completion = data.list[i], self = this; - this.cm.operation(function() { - if (completion.hint) - completion.hint(self.cm, data, completion); - else - self.cm.replaceRange(getText(completion), completion.from || data.from, - completion.to || data.to, "complete"); - CodeMirror.signal(data, "pick", completion); - self.cm.scrollIntoView(); - }) - this.close(); - }, - - cursorActivity: function() { - if (this.debounce) { - cancelAnimationFrame(this.debounce); - this.debounce = 0; - } - - var identStart = this.startPos; - if(this.data) { - identStart = this.data.from; - } - - var pos = this.cm.getCursor(), line = this.cm.getLine(pos.line); - if (pos.line != this.startPos.line || line.length - pos.ch != this.startLen - this.startPos.ch || - pos.ch < identStart.ch || this.cm.somethingSelected() || - (!pos.ch || this.options.closeCharacters.test(line.charAt(pos.ch - 1)))) { - this.close(); - } else { - var self = this; - this.debounce = requestAnimationFrame(function() {self.update();}); - if (this.widget) this.widget.disable(); - } - }, - - update: function(first) { - if (this.tick == null) return - var self = this, myTick = ++this.tick - fetchHints(this.options.hint, this.cm, this.options, function(data) { - if (self.tick == myTick) self.finishUpdate(data, first) - }) - }, - - finishUpdate: function(data, first) { - if (this.data) CodeMirror.signal(this.data, "update"); - - var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle); - if (this.widget) this.widget.close(); - - this.data = data; - - if (data && data.list.length) { - if (picked && data.list.length == 1) { - this.pick(data, 0); - } else { - this.widget = new Widget(this, data); - CodeMirror.signal(data, "shown"); - } - } - } - }; - - function parseOptions(cm, pos, options) { - var editor = cm.options.hintOptions; - var out = {}; - for (var prop in defaultOptions) out[prop] = defaultOptions[prop]; - if (editor) for (var prop in editor) - if (editor[prop] !== undefined) out[prop] = editor[prop]; - if (options) for (var prop in options) - if (options[prop] !== undefined) out[prop] = options[prop]; - if (out.hint.resolve) out.hint = out.hint.resolve(cm, pos) - return out; - } - - function getText(completion) { - if (typeof completion == "string") return completion; - else return completion.text; - } - - function buildKeyMap(completion, handle) { - var baseMap = { - Up: function() {handle.moveFocus(-1);}, - Down: function() {handle.moveFocus(1);}, - PageUp: function() {handle.moveFocus(-handle.menuSize() + 1, true);}, - PageDown: function() {handle.moveFocus(handle.menuSize() - 1, true);}, - Home: function() {handle.setFocus(0);}, - End: function() {handle.setFocus(handle.length - 1);}, - Enter: handle.pick, - Tab: handle.pick, - Esc: handle.close - }; - - var mac = /Mac/.test(navigator.platform); - - if (mac) { - baseMap["Ctrl-P"] = function() {handle.moveFocus(-1);}; - baseMap["Ctrl-N"] = function() {handle.moveFocus(1);}; - } - - var custom = completion.options.customKeys; - var ourMap = custom ? {} : baseMap; - function addBinding(key, val) { - var bound; - if (typeof val != "string") - bound = function(cm) { return val(cm, handle); }; - // This mechanism is deprecated - else if (baseMap.hasOwnProperty(val)) - bound = baseMap[val]; - else - bound = val; - ourMap[key] = bound; - } - if (custom) - for (var key in custom) if (custom.hasOwnProperty(key)) - addBinding(key, custom[key]); - var extra = completion.options.extraKeys; - if (extra) - for (var key in extra) if (extra.hasOwnProperty(key)) - addBinding(key, extra[key]); - return ourMap; - } - - function getHintElement(hintsElement, el) { - while (el && el != hintsElement) { - if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el; - el = el.parentNode; - } - } - - function Widget(completion, data) { - this.completion = completion; - this.data = data; - this.picked = false; - var widget = this, cm = completion.cm; - var ownerDocument = cm.getInputField().ownerDocument; - var parentWindow = ownerDocument.defaultView || ownerDocument.parentWindow; - - var hints = this.hints = ownerDocument.createElement("ul"); - var theme = completion.cm.options.theme; - hints.className = "CodeMirror-hints " + theme; - this.selectedHint = data.selectedHint || 0; - - var completions = data.list; - for (var i = 0; i < completions.length; ++i) { - var elt = hints.appendChild(ownerDocument.createElement("li")), cur = completions[i]; - var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS); - if (cur.className != null) className = cur.className + " " + className; - elt.className = className; - if (cur.render) cur.render(elt, data, cur); - else elt.appendChild(ownerDocument.createTextNode(cur.displayText || getText(cur))); - elt.hintId = i; - } - - var container = completion.options.container || ownerDocument.body; - var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null); - var left = pos.left, top = pos.bottom, below = true; - var offsetLeft = 0, offsetTop = 0; - if (container !== ownerDocument.body) { - // We offset the cursor position because left and top are relative to the offsetParent's top left corner. - var isContainerPositioned = ['absolute', 'relative', 'fixed'].indexOf(parentWindow.getComputedStyle(container).position) !== -1; - var offsetParent = isContainerPositioned ? container : container.offsetParent; - var offsetParentPosition = offsetParent.getBoundingClientRect(); - var bodyPosition = ownerDocument.body.getBoundingClientRect(); - offsetLeft = (offsetParentPosition.left - bodyPosition.left - offsetParent.scrollLeft); - offsetTop = (offsetParentPosition.top - bodyPosition.top - offsetParent.scrollTop); - } - hints.style.left = (left - offsetLeft) + "px"; - hints.style.top = (top - offsetTop) + "px"; - - // If we're at the edge of the screen, then we want the menu to appear on the left of the cursor. - var winW = parentWindow.innerWidth || Math.max(ownerDocument.body.offsetWidth, ownerDocument.documentElement.offsetWidth); - var winH = parentWindow.innerHeight || Math.max(ownerDocument.body.offsetHeight, ownerDocument.documentElement.offsetHeight); - container.appendChild(hints); - var box = hints.getBoundingClientRect(), overlapY = box.bottom - winH; - var scrolls = hints.scrollHeight > hints.clientHeight + 1 - var startScroll = cm.getScrollInfo(); - - if (overlapY > 0) { - var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top); - if (curTop - height > 0) { // Fits above cursor - hints.style.top = (top = pos.top - height - offsetTop) + "px"; - below = false; - } else if (height > winH) { - hints.style.height = (winH - 5) + "px"; - hints.style.top = (top = pos.bottom - box.top - offsetTop) + "px"; - var cursor = cm.getCursor(); - if (data.from.ch != cursor.ch) { - pos = cm.cursorCoords(cursor); - hints.style.left = (left = pos.left - offsetLeft) + "px"; - box = hints.getBoundingClientRect(); - } - } - } - var overlapX = box.right - winW; - if (overlapX > 0) { - if (box.right - box.left > winW) { - hints.style.width = (winW - 5) + "px"; - overlapX -= (box.right - box.left) - winW; - } - hints.style.left = (left = pos.left - overlapX - offsetLeft) + "px"; - } - if (scrolls) for (var node = hints.firstChild; node; node = node.nextSibling) - node.style.paddingRight = cm.display.nativeBarWidth + "px" - - cm.addKeyMap(this.keyMap = buildKeyMap(completion, { - moveFocus: function(n, avoidWrap) { widget.changeActive(widget.selectedHint + n, avoidWrap); }, - setFocus: function(n) { widget.changeActive(n); }, - menuSize: function() { return widget.screenAmount(); }, - length: completions.length, - close: function() { completion.close(); }, - pick: function() { widget.pick(); }, - data: data - })); - - if (completion.options.closeOnUnfocus) { - var closingOnBlur; - cm.on("blur", this.onBlur = function() { closingOnBlur = setTimeout(function() { completion.close(); }, 100); }); - cm.on("focus", this.onFocus = function() { clearTimeout(closingOnBlur); }); - } - - cm.on("scroll", this.onScroll = function() { - var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect(); - var newTop = top + startScroll.top - curScroll.top; - var point = newTop - (parentWindow.pageYOffset || (ownerDocument.documentElement || ownerDocument.body).scrollTop); - if (!below) point += hints.offsetHeight; - if (point <= editor.top || point >= editor.bottom) return completion.close(); - hints.style.top = newTop + "px"; - hints.style.left = (left + startScroll.left - curScroll.left) + "px"; - }); - - CodeMirror.on(hints, "dblclick", function(e) { - var t = getHintElement(hints, e.target || e.srcElement); - if (t && t.hintId != null) {widget.changeActive(t.hintId); widget.pick();} - }); - - CodeMirror.on(hints, "click", function(e) { - var t = getHintElement(hints, e.target || e.srcElement); - if (t && t.hintId != null) { - widget.changeActive(t.hintId); - if (completion.options.completeOnSingleClick) widget.pick(); - } - }); - - CodeMirror.on(hints, "mousedown", function() { - setTimeout(function(){cm.focus();}, 20); - }); - this.scrollToActive() - - CodeMirror.signal(data, "select", completions[this.selectedHint], hints.childNodes[this.selectedHint]); - return true; - } - - Widget.prototype = { - close: function() { - if (this.completion.widget != this) return; - this.completion.widget = null; - this.hints.parentNode.removeChild(this.hints); - this.completion.cm.removeKeyMap(this.keyMap); - - var cm = this.completion.cm; - if (this.completion.options.closeOnUnfocus) { - cm.off("blur", this.onBlur); - cm.off("focus", this.onFocus); - } - cm.off("scroll", this.onScroll); - }, - - disable: function() { - this.completion.cm.removeKeyMap(this.keyMap); - var widget = this; - this.keyMap = {Enter: function() { widget.picked = true; }}; - this.completion.cm.addKeyMap(this.keyMap); - }, - - pick: function() { - this.completion.pick(this.data, this.selectedHint); - }, - - changeActive: function(i, avoidWrap) { - if (i >= this.data.list.length) - i = avoidWrap ? this.data.list.length - 1 : 0; - else if (i < 0) - i = avoidWrap ? 0 : this.data.list.length - 1; - if (this.selectedHint == i) return; - var node = this.hints.childNodes[this.selectedHint]; - if (node) node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, ""); - node = this.hints.childNodes[this.selectedHint = i]; - node.className += " " + ACTIVE_HINT_ELEMENT_CLASS; - this.scrollToActive() - CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node); - }, - - scrollToActive: function() { - var margin = this.completion.options.scrollMargin || 0; - var node1 = this.hints.childNodes[Math.max(0, this.selectedHint - margin)]; - var node2 = this.hints.childNodes[Math.min(this.data.list.length - 1, this.selectedHint + margin)]; - var firstNode = this.hints.firstChild; - if (node1.offsetTop < this.hints.scrollTop) - this.hints.scrollTop = node1.offsetTop - firstNode.offsetTop; - else if (node2.offsetTop + node2.offsetHeight > this.hints.scrollTop + this.hints.clientHeight) - this.hints.scrollTop = node2.offsetTop + node2.offsetHeight - this.hints.clientHeight + firstNode.offsetTop; - }, - - screenAmount: function() { - return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1; - } - }; - - function applicableHelpers(cm, helpers) { - if (!cm.somethingSelected()) return helpers - var result = [] - for (var i = 0; i < helpers.length; i++) - if (helpers[i].supportsSelection) result.push(helpers[i]) - return result - } - - function fetchHints(hint, cm, options, callback) { - if (hint.async) { - hint(cm, callback, options) - } else { - var result = hint(cm, options) - if (result && result.then) result.then(callback) - else callback(result) - } - } - - function resolveAutoHints(cm, pos) { - var helpers = cm.getHelpers(pos, "hint"), words - if (helpers.length) { - var resolved = function(cm, callback, options) { - var app = applicableHelpers(cm, helpers); - function run(i) { - if (i == app.length) return callback(null) - fetchHints(app[i], cm, options, function(result) { - if (result && result.list.length > 0) callback(result) - else run(i + 1) - }) - } - run(0) - } - resolved.async = true - resolved.supportsSelection = true - return resolved - } else if (words = cm.getHelper(cm.getCursor(), "hintWords")) { - return function(cm) { return CodeMirror.hint.fromList(cm, {words: words}) } - } else if (CodeMirror.hint.anyword) { - return function(cm, options) { return CodeMirror.hint.anyword(cm, options) } - } else { - return function() {} - } - } - - CodeMirror.registerHelper("hint", "auto", { - resolve: resolveAutoHints - }); - - CodeMirror.registerHelper("hint", "fromList", function(cm, options) { - var cur = cm.getCursor(), token = cm.getTokenAt(cur) - var term, from = CodeMirror.Pos(cur.line, token.start), to = cur - if (token.start < cur.ch && /\w/.test(token.string.charAt(cur.ch - token.start - 1))) { - term = token.string.substr(0, cur.ch - token.start) - } else { - term = "" - from = cur - } - var found = []; - for (var i = 0; i < options.words.length; i++) { - var word = options.words[i]; - if (word.slice(0, term.length) == term) - found.push(word); - } - - if (found.length) return {list: found, from: from, to: to}; - }); - - CodeMirror.commands.autocomplete = CodeMirror.showHint; - - var defaultOptions = { - hint: CodeMirror.hint.auto, - completeSingle: true, - alignWithWord: true, - closeCharacters: /[\s()\[\]{};:>,]/, - closeOnUnfocus: true, - completeOnSingleClick: true, - container: null, - customKeys: null, - extraKeys: null - }; - - CodeMirror.defineOption("hintOptions", null); -}); - - -/* ---- extension/hint/sql-hint.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("../../mode/sql/sql")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "../../mode/sql/sql"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - var tables; - var defaultTable; - var keywords; - var identifierQuote; - var CONS = { - QUERY_DIV: ";", - ALIAS_KEYWORD: "AS" - }; - var Pos = CodeMirror.Pos, cmpPos = CodeMirror.cmpPos; - - function isArray(val) { return Object.prototype.toString.call(val) == "[object Array]" } - - function getKeywords(editor) { - var mode = editor.doc.modeOption; - if (mode === "sql") mode = "text/x-sql"; - return CodeMirror.resolveMode(mode).keywords; - } - - function getIdentifierQuote(editor) { - var mode = editor.doc.modeOption; - if (mode === "sql") mode = "text/x-sql"; - return CodeMirror.resolveMode(mode).identifierQuote || "`"; - } - - function getText(item) { - return typeof item == "string" ? item : item.text; - } - - function wrapTable(name, value) { - if (isArray(value)) value = {columns: value} - if (!value.text) value.text = name - return value - } - - function parseTables(input) { - var result = {} - if (isArray(input)) { - for (var i = input.length - 1; i >= 0; i--) { - var item = input[i] - result[getText(item).toUpperCase()] = wrapTable(getText(item), item) - } - } else if (input) { - for (var name in input) - result[name.toUpperCase()] = wrapTable(name, input[name]) - } - return result - } - - function getTable(name) { - return tables[name.toUpperCase()] - } - - function shallowClone(object) { - var result = {}; - for (var key in object) if (object.hasOwnProperty(key)) - result[key] = object[key]; - return result; - } - - function match(string, word) { - var len = string.length; - var sub = getText(word).substr(0, len); - return string.toUpperCase() === sub.toUpperCase(); - } - - function addMatches(result, search, wordlist, formatter) { - if (isArray(wordlist)) { - for (var i = 0; i < wordlist.length; i++) - if (match(search, wordlist[i])) result.push(formatter(wordlist[i])) - } else { - for (var word in wordlist) if (wordlist.hasOwnProperty(word)) { - var val = wordlist[word] - if (!val || val === true) - val = word - else - val = val.displayText ? {text: val.text, displayText: val.displayText} : val.text - if (match(search, val)) result.push(formatter(val)) - } - } - } - - function cleanName(name) { - // Get rid name from identifierQuote and preceding dot(.) - if (name.charAt(0) == ".") { - name = name.substr(1); - } - // replace doublicated identifierQuotes with single identifierQuotes - // and remove single identifierQuotes - var nameParts = name.split(identifierQuote+identifierQuote); - for (var i = 0; i < nameParts.length; i++) - nameParts[i] = nameParts[i].replace(new RegExp(identifierQuote,"g"), ""); - return nameParts.join(identifierQuote); - } - - function insertIdentifierQuotes(name) { - var nameParts = getText(name).split("."); - for (var i = 0; i < nameParts.length; i++) - nameParts[i] = identifierQuote + - // doublicate identifierQuotes - nameParts[i].replace(new RegExp(identifierQuote,"g"), identifierQuote+identifierQuote) + - identifierQuote; - var escaped = nameParts.join("."); - if (typeof name == "string") return escaped; - name = shallowClone(name); - name.text = escaped; - return name; - } - - function nameCompletion(cur, token, result, editor) { - // Try to complete table, column names and return start position of completion - var useIdentifierQuotes = false; - var nameParts = []; - var start = token.start; - var cont = true; - while (cont) { - cont = (token.string.charAt(0) == "."); - useIdentifierQuotes = useIdentifierQuotes || (token.string.charAt(0) == identifierQuote); - - start = token.start; - nameParts.unshift(cleanName(token.string)); - - token = editor.getTokenAt(Pos(cur.line, token.start)); - if (token.string == ".") { - cont = true; - token = editor.getTokenAt(Pos(cur.line, token.start)); - } - } - - // Try to complete table names - var string = nameParts.join("."); - addMatches(result, string, tables, function(w) { - return useIdentifierQuotes ? insertIdentifierQuotes(w) : w; - }); - - // Try to complete columns from defaultTable - addMatches(result, string, defaultTable, function(w) { - return useIdentifierQuotes ? insertIdentifierQuotes(w) : w; - }); - - // Try to complete columns - string = nameParts.pop(); - var table = nameParts.join("."); - - var alias = false; - var aliasTable = table; - // Check if table is available. If not, find table by Alias - if (!getTable(table)) { - var oldTable = table; - table = findTableByAlias(table, editor); - if (table !== oldTable) alias = true; - } - - var columns = getTable(table); - if (columns && columns.columns) - columns = columns.columns; - - if (columns) { - addMatches(result, string, columns, function(w) { - var tableInsert = table; - if (alias == true) tableInsert = aliasTable; - if (typeof w == "string") { - w = tableInsert + "." + w; - } else { - w = shallowClone(w); - w.text = tableInsert + "." + w.text; - } - return useIdentifierQuotes ? insertIdentifierQuotes(w) : w; - }); - } - - return start; - } - - function eachWord(lineText, f) { - var words = lineText.split(/\s+/) - for (var i = 0; i < words.length; i++) - if (words[i]) f(words[i].replace(/[,;]/g, '')) - } - - function findTableByAlias(alias, editor) { - var doc = editor.doc; - var fullQuery = doc.getValue(); - var aliasUpperCase = alias.toUpperCase(); - var previousWord = ""; - var table = ""; - var separator = []; - var validRange = { - start: Pos(0, 0), - end: Pos(editor.lastLine(), editor.getLineHandle(editor.lastLine()).length) - }; - - //add separator - var indexOfSeparator = fullQuery.indexOf(CONS.QUERY_DIV); - while(indexOfSeparator != -1) { - separator.push(doc.posFromIndex(indexOfSeparator)); - indexOfSeparator = fullQuery.indexOf(CONS.QUERY_DIV, indexOfSeparator+1); - } - separator.unshift(Pos(0, 0)); - separator.push(Pos(editor.lastLine(), editor.getLineHandle(editor.lastLine()).text.length)); - - //find valid range - var prevItem = null; - var current = editor.getCursor() - for (var i = 0; i < separator.length; i++) { - if ((prevItem == null || cmpPos(current, prevItem) > 0) && cmpPos(current, separator[i]) <= 0) { - validRange = {start: prevItem, end: separator[i]}; - break; - } - prevItem = separator[i]; - } - - if (validRange.start) { - var query = doc.getRange(validRange.start, validRange.end, false); - - for (var i = 0; i < query.length; i++) { - var lineText = query[i]; - eachWord(lineText, function(word) { - var wordUpperCase = word.toUpperCase(); - if (wordUpperCase === aliasUpperCase && getTable(previousWord)) - table = previousWord; - if (wordUpperCase !== CONS.ALIAS_KEYWORD) - previousWord = word; - }); - if (table) break; - } - } - return table; - } - - CodeMirror.registerHelper("hint", "sql", function(editor, options) { - tables = parseTables(options && options.tables) - var defaultTableName = options && options.defaultTable; - var disableKeywords = options && options.disableKeywords; - defaultTable = defaultTableName && getTable(defaultTableName); - keywords = getKeywords(editor); - identifierQuote = getIdentifierQuote(editor); - - if (defaultTableName && !defaultTable) - defaultTable = findTableByAlias(defaultTableName, editor); - - defaultTable = defaultTable || []; - - if (defaultTable.columns) - defaultTable = defaultTable.columns; - - var cur = editor.getCursor(); - var result = []; - var token = editor.getTokenAt(cur), start, end, search; - if (token.end > cur.ch) { - token.end = cur.ch; - token.string = token.string.slice(0, cur.ch - token.start); - } - - if (token.string.match(/^[.`"'\w@][\w$#]*$/g)) { - search = token.string; - start = token.start; - end = token.end; - } else { - start = end = cur.ch; - search = ""; - } - if (search.charAt(0) == "." || search.charAt(0) == identifierQuote) { - start = nameCompletion(cur, token, result, editor); - } else { - var objectOrClass = function(w, className) { - if (typeof w === "object") { - w.className = className; - } else { - w = { text: w, className: className }; - } - return w; - }; - addMatches(result, search, defaultTable, function(w) { - return objectOrClass(w, "CodeMirror-hint-table CodeMirror-hint-default-table"); - }); - addMatches( - result, - search, - tables, function(w) { - return objectOrClass(w, "CodeMirror-hint-table"); - } - ); - if (!disableKeywords) - addMatches(result, search, keywords, function(w) { - return objectOrClass(w.toUpperCase(), "CodeMirror-hint-keyword"); - }); - } - - return {list: result, from: Pos(cur.line, start), to: Pos(cur.line, end)}; - }); -}); - - -/* ---- extension/hint/xml-hint.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - var Pos = CodeMirror.Pos; - - function matches(hint, typed, matchInMiddle) { - if (matchInMiddle) return hint.indexOf(typed) >= 0; - else return hint.lastIndexOf(typed, 0) == 0; - } - - function getHints(cm, options) { - var tags = options && options.schemaInfo; - var quote = (options && options.quoteChar) || '"'; - var matchInMiddle = options && options.matchInMiddle; - if (!tags) return; - var cur = cm.getCursor(), token = cm.getTokenAt(cur); - if (token.end > cur.ch) { - token.end = cur.ch; - token.string = token.string.slice(0, cur.ch - token.start); - } - var inner = CodeMirror.innerMode(cm.getMode(), token.state); - if (!inner.mode.xmlCurrentTag) return - var result = [], replaceToken = false, prefix; - var tag = /\btag\b/.test(token.type) && !/>$/.test(token.string); - var tagName = tag && /^\w/.test(token.string), tagStart; - - if (tagName) { - var before = cm.getLine(cur.line).slice(Math.max(0, token.start - 2), token.start); - var tagType = /<\/$/.test(before) ? "close" : /<$/.test(before) ? "open" : null; - if (tagType) tagStart = token.start - (tagType == "close" ? 2 : 1); - } else if (tag && token.string == "<") { - tagType = "open"; - } else if (tag && token.string == ""); - } else { - // Attribute completion - var curTag = tagInfo && tags[tagInfo.name], attrs = curTag && curTag.attrs; - var globalAttrs = tags["!attrs"]; - if (!attrs && !globalAttrs) return; - if (!attrs) { - attrs = globalAttrs; - } else if (globalAttrs) { // Combine tag-local and global attributes - var set = {}; - for (var nm in globalAttrs) if (globalAttrs.hasOwnProperty(nm)) set[nm] = globalAttrs[nm]; - for (var nm in attrs) if (attrs.hasOwnProperty(nm)) set[nm] = attrs[nm]; - attrs = set; - } - if (token.type == "string" || token.string == "=") { // A value - var before = cm.getRange(Pos(cur.line, Math.max(0, cur.ch - 60)), - Pos(cur.line, token.type == "string" ? token.start : token.end)); - var atName = before.match(/([^\s\u00a0=<>\"\']+)=$/), atValues; - if (!atName || !attrs.hasOwnProperty(atName[1]) || !(atValues = attrs[atName[1]])) return; - if (typeof atValues == 'function') atValues = atValues.call(this, cm); // Functions can be used to supply values for autocomplete widget - if (token.type == "string") { - prefix = token.string; - var n = 0; - if (/['"]/.test(token.string.charAt(0))) { - quote = token.string.charAt(0); - prefix = token.string.slice(1); - n++; - } - var len = token.string.length; - if (/['"]/.test(token.string.charAt(len - 1))) { - quote = token.string.charAt(len - 1); - prefix = token.string.substr(n, len - 2); - } - if (n) { // an opening quote - var line = cm.getLine(cur.line); - if (line.length > token.end && line.charAt(token.end) == quote) token.end++; // include a closing quote - } - replaceToken = true; - } - for (var i = 0; i < atValues.length; ++i) if (!prefix || matches(atValues[i], prefix, matchInMiddle)) - result.push(quote + atValues[i] + quote); - } else { // An attribute name - if (token.type == "attribute") { - prefix = token.string; - replaceToken = true; - } - for (var attr in attrs) if (attrs.hasOwnProperty(attr) && (!prefix || matches(attr, prefix, matchInMiddle))) - result.push(attr); - } - } - return { - list: result, - from: replaceToken ? Pos(cur.line, tagStart == null ? token.start : tagStart) : cur, - to: replaceToken ? Pos(cur.line, token.end) : cur - }; - } - - CodeMirror.registerHelper("hint", "xml", getHints); -}); - - -/* ---- extension/lint/json-lint.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -// Depends on jsonlint.js from https://github.com/zaach/jsonlint - -// declare global: jsonlint - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -CodeMirror.registerHelper("lint", "json", function(text) { - var found = []; - if (!window.jsonlint) { - if (window.console) { - window.console.error("Error: window.jsonlint not defined, CodeMirror JSON linting cannot run."); - } - return found; - } - // for jsonlint's web dist jsonlint is exported as an object with a single property parser, of which parseError - // is a subproperty - var jsonlint = window.jsonlint.parser || window.jsonlint - jsonlint.parseError = function(str, hash) { - var loc = hash.loc; - found.push({from: CodeMirror.Pos(loc.first_line - 1, loc.first_column), - to: CodeMirror.Pos(loc.last_line - 1, loc.last_column), - message: str}); - }; - try { jsonlint.parse(text); } - catch(e) {} - return found; -}); - -}); - - -/* ---- extension/lint/jsonlint.js ---- */ - - -var jsonlint=function(){var a=!0,b=!1,c={},d=function(){var a={trace:function(){},yy:{},symbols_:{error:2,JSONString:3,STRING:4,JSONNumber:5,NUMBER:6,JSONNullLiteral:7,NULL:8,JSONBooleanLiteral:9,TRUE:10,FALSE:11,JSONText:12,JSONValue:13,EOF:14,JSONObject:15,JSONArray:16,"{":17,"}":18,JSONMemberList:19,JSONMember:20,":":21,",":22,"[":23,"]":24,JSONElementList:25,$accept:0,$end:1},terminals_:{2:"error",4:"STRING",6:"NUMBER",8:"NULL",10:"TRUE",11:"FALSE",14:"EOF",17:"{",18:"}",21:":",22:",",23:"[",24:"]"},productions_:[0,[3,1],[5,1],[7,1],[9,1],[9,1],[12,2],[13,1],[13,1],[13,1],[13,1],[13,1],[13,1],[15,2],[15,3],[20,3],[19,1],[19,3],[16,2],[16,3],[25,1],[25,3]],performAction:function(b,c,d,e,f,g,h){var i=g.length-1;switch(f){case 1:this.$=b.replace(/\\(\\|")/g,"$1").replace(/\\n/g,"\n").replace(/\\r/g,"\r").replace(/\\t/g," ").replace(/\\v/g," ").replace(/\\f/g,"\f").replace(/\\b/g,"\b");break;case 2:this.$=Number(b);break;case 3:this.$=null;break;case 4:this.$=!0;break;case 5:this.$=!1;break;case 6:return this.$=g[i-1];case 13:this.$={};break;case 14:this.$=g[i-1];break;case 15:this.$=[g[i-2],g[i]];break;case 16:this.$={},this.$[g[i][0]]=g[i][1];break;case 17:this.$=g[i-2],g[i-2][g[i][0]]=g[i][1];break;case 18:this.$=[];break;case 19:this.$=g[i-1];break;case 20:this.$=[g[i]];break;case 21:this.$=g[i-2],g[i-2].push(g[i])}},table:[{3:5,4:[1,12],5:6,6:[1,13],7:3,8:[1,9],9:4,10:[1,10],11:[1,11],12:1,13:2,15:7,16:8,17:[1,14],23:[1,15]},{1:[3]},{14:[1,16]},{14:[2,7],18:[2,7],22:[2,7],24:[2,7]},{14:[2,8],18:[2,8],22:[2,8],24:[2,8]},{14:[2,9],18:[2,9],22:[2,9],24:[2,9]},{14:[2,10],18:[2,10],22:[2,10],24:[2,10]},{14:[2,11],18:[2,11],22:[2,11],24:[2,11]},{14:[2,12],18:[2,12],22:[2,12],24:[2,12]},{14:[2,3],18:[2,3],22:[2,3],24:[2,3]},{14:[2,4],18:[2,4],22:[2,4],24:[2,4]},{14:[2,5],18:[2,5],22:[2,5],24:[2,5]},{14:[2,1],18:[2,1],21:[2,1],22:[2,1],24:[2,1]},{14:[2,2],18:[2,2],22:[2,2],24:[2,2]},{3:20,4:[1,12],18:[1,17],19:18,20:19},{3:5,4:[1,12],5:6,6:[1,13],7:3,8:[1,9],9:4,10:[1,10],11:[1,11],13:23,15:7,16:8,17:[1,14],23:[1,15],24:[1,21],25:22},{1:[2,6]},{14:[2,13],18:[2,13],22:[2,13],24:[2,13]},{18:[1,24],22:[1,25]},{18:[2,16],22:[2,16]},{21:[1,26]},{14:[2,18],18:[2,18],22:[2,18],24:[2,18]},{22:[1,28],24:[1,27]},{22:[2,20],24:[2,20]},{14:[2,14],18:[2,14],22:[2,14],24:[2,14]},{3:20,4:[1,12],20:29},{3:5,4:[1,12],5:6,6:[1,13],7:3,8:[1,9],9:4,10:[1,10],11:[1,11],13:30,15:7,16:8,17:[1,14],23:[1,15]},{14:[2,19],18:[2,19],22:[2,19],24:[2,19]},{3:5,4:[1,12],5:6,6:[1,13],7:3,8:[1,9],9:4,10:[1,10],11:[1,11],13:31,15:7,16:8,17:[1,14],23:[1,15]},{18:[2,17],22:[2,17]},{18:[2,15],22:[2,15]},{22:[2,21],24:[2,21]}],defaultActions:{16:[2,6]},parseError:function(b,c){throw new Error(b)},parse:function(b){function o(a){d.length=d.length-2*a,e.length=e.length-a,f.length=f.length-a}function p(){var a;return a=c.lexer.lex()||1,typeof a!="number"&&(a=c.symbols_[a]||a),a}var c=this,d=[0],e=[null],f=[],g=this.table,h="",i=0,j=0,k=0,l=2,m=1;this.lexer.setInput(b),this.lexer.yy=this.yy,this.yy.lexer=this.lexer,typeof this.lexer.yylloc=="undefined"&&(this.lexer.yylloc={});var n=this.lexer.yylloc;f.push(n),typeof this.yy.parseError=="function"&&(this.parseError=this.yy.parseError);var q,r,s,t,u,v,w={},x,y,z,A;for(;;){s=d[d.length-1],this.defaultActions[s]?t=this.defaultActions[s]:(q==null&&(q=p()),t=g[s]&&g[s][q]);if(typeof t=="undefined"||!t.length||!t[0]){if(!k){A=[];for(x in g[s])this.terminals_[x]&&x>2&&A.push("'"+this.terminals_[x]+"'");var B="";this.lexer.showPosition?B="Parse error on line "+(i+1)+":\n"+this.lexer.showPosition()+"\nExpecting "+A.join(", ")+", got '"+this.terminals_[q]+"'":B="Parse error on line "+(i+1)+": Unexpected "+(q==1?"end of input":"'"+(this.terminals_[q]||q)+"'"),this.parseError(B,{text:this.lexer.match,token:this.terminals_[q]||q,line:this.lexer.yylineno,loc:n,expected:A})}if(k==3){if(q==m)throw new Error(B||"Parsing halted.");j=this.lexer.yyleng,h=this.lexer.yytext,i=this.lexer.yylineno,n=this.lexer.yylloc,q=p()}for(;;){if(l.toString()in g[s])break;if(s==0)throw new Error(B||"Parsing halted.");o(1),s=d[d.length-1]}r=q,q=l,s=d[d.length-1],t=g[s]&&g[s][l],k=3}if(t[0]instanceof Array&&t.length>1)throw new Error("Parse Error: multiple actions possible at state: "+s+", token: "+q);switch(t[0]){case 1:d.push(q),e.push(this.lexer.yytext),f.push(this.lexer.yylloc),d.push(t[1]),q=null,r?(q=r,r=null):(j=this.lexer.yyleng,h=this.lexer.yytext,i=this.lexer.yylineno,n=this.lexer.yylloc,k>0&&k--);break;case 2:y=this.productions_[t[1]][1],w.$=e[e.length-y],w._$={first_line:f[f.length-(y||1)].first_line,last_line:f[f.length-1].last_line,first_column:f[f.length-(y||1)].first_column,last_column:f[f.length-1].last_column},v=this.performAction.call(w,h,j,i,this.yy,t[1],e,f);if(typeof v!="undefined")return v;y&&(d=d.slice(0,-1*y*2),e=e.slice(0,-1*y),f=f.slice(0,-1*y)),d.push(this.productions_[t[1]][0]),e.push(w.$),f.push(w._$),z=g[d[d.length-2]][d[d.length-1]],d.push(z);break;case 3:return!0}}return!0}},b=function(){var a={EOF:1,parseError:function(b,c){if(!this.yy.parseError)throw new Error(b);this.yy.parseError(b,c)},setInput:function(a){return this._input=a,this._more=this._less=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this},input:function(){var a=this._input[0];this.yytext+=a,this.yyleng++,this.match+=a,this.matched+=a;var b=a.match(/\n/);return b&&this.yylineno++,this._input=this._input.slice(1),a},unput:function(a){return this._input=a+this._input,this},more:function(){return this._more=!0,this},less:function(a){this._input=this.match.slice(a)+this._input},pastInput:function(){var a=this.matched.substr(0,this.matched.length-this.match.length);return(a.length>20?"...":"")+a.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var a=this.match;return a.length<20&&(a+=this._input.substr(0,20-a.length)),(a.substr(0,20)+(a.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var a=this.pastInput(),b=(new Array(a.length+1)).join("-");return a+this.upcomingInput()+"\n"+b+"^"},next:function(){if(this.done)return this.EOF;this._input||(this.done=!0);var a,b,c,d,e,f;this._more||(this.yytext="",this.match="");var g=this._currentRules();for(var h=0;hb[0].length)){b=c,d=h;if(!this.options.flex)break}}if(b){f=b[0].match(/\n.*/g),f&&(this.yylineno+=f.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:f?f[f.length-1].length-1:this.yylloc.last_column+b[0].length},this.yytext+=b[0],this.match+=b[0],this.yyleng=this.yytext.length,this._more=!1,this._input=this._input.slice(b[0].length),this.matched+=b[0],a=this.performAction.call(this,this.yy,this,g[d],this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1);if(a)return a;return}if(this._input==="")return this.EOF;this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){var b=this.next();return typeof b!="undefined"?b:this.lex()},begin:function(b){this.conditionStack.push(b)},popState:function(){return this.conditionStack.pop()},_currentRules:function(){return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules},topState:function(){return this.conditionStack[this.conditionStack.length-2]},pushState:function(b){this.begin(b)}};return a.options={},a.performAction=function(b,c,d,e){var f=e;switch(d){case 0:break;case 1:return 6;case 2:return c.yytext=c.yytext.substr(1,c.yyleng-2),4;case 3:return 17;case 4:return 18;case 5:return 23;case 6:return 24;case 7:return 22;case 8:return 21;case 9:return 10;case 10:return 11;case 11:return 8;case 12:return 14;case 13:return"INVALID"}},a.rules=[/^(?:\s+)/,/^(?:(-?([0-9]|[1-9][0-9]+))(\.[0-9]+)?([eE][-+]?[0-9]+)?\b)/,/^(?:"(?:\\[\\"bfnrt/]|\\u[a-fA-F0-9]{4}|[^\\\0-\x09\x0a-\x1f"])*")/,/^(?:\{)/,/^(?:\})/,/^(?:\[)/,/^(?:\])/,/^(?:,)/,/^(?::)/,/^(?:true\b)/,/^(?:false\b)/,/^(?:null\b)/,/^(?:$)/,/^(?:.)/],a.conditions={INITIAL:{rules:[0,1,2,3,4,5,6,7,8,9,10,11,12,13],inclusive:!0}},a}();return a.lexer=b,a}();return typeof a!="undefined"&&typeof c!="undefined"&&(c.parser=d,c.parse=function(){return d.parse.apply(d,arguments)},c.main=function(d){if(!d[1])throw new Error("Usage: "+d[0]+" FILE");if(typeof process!="undefined")var e=a("fs").readFileSync(a("path").join(process.cwd(),d[1]),"utf8");else var f=a("file").path(a("file").cwd()),e=f.join(d[1]).read({charset:"utf-8"});return c.parser.parse(e)},typeof b!="undefined"&&a.main===b&&c.main(typeof process!="undefined"?process.argv.slice(1):a("system").args)),c}(); - -/* ---- extension/lint/lint.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - var GUTTER_ID = "CodeMirror-lint-markers"; - - function showTooltip(cm, e, content) { - var tt = document.createElement("div"); - tt.className = "CodeMirror-lint-tooltip cm-s-" + cm.options.theme; - tt.appendChild(content.cloneNode(true)); - if (cm.state.lint.options.selfContain) - cm.getWrapperElement().appendChild(tt); - else - document.body.appendChild(tt); - - function position(e) { - if (!tt.parentNode) return CodeMirror.off(document, "mousemove", position); - tt.style.top = Math.max(0, e.clientY - tt.offsetHeight - 5) + "px"; - tt.style.left = (e.clientX + 5) + "px"; - } - CodeMirror.on(document, "mousemove", position); - position(e); - if (tt.style.opacity != null) tt.style.opacity = 1; - return tt; - } - function rm(elt) { - if (elt.parentNode) elt.parentNode.removeChild(elt); - } - function hideTooltip(tt) { - if (!tt.parentNode) return; - if (tt.style.opacity == null) rm(tt); - tt.style.opacity = 0; - setTimeout(function() { rm(tt); }, 600); - } - - function showTooltipFor(cm, e, content, node) { - var tooltip = showTooltip(cm, e, content); - function hide() { - CodeMirror.off(node, "mouseout", hide); - if (tooltip) { hideTooltip(tooltip); tooltip = null; } - } - var poll = setInterval(function() { - if (tooltip) for (var n = node;; n = n.parentNode) { - if (n && n.nodeType == 11) n = n.host; - if (n == document.body) return; - if (!n) { hide(); break; } - } - if (!tooltip) return clearInterval(poll); - }, 400); - CodeMirror.on(node, "mouseout", hide); - } - - function LintState(cm, options, hasGutter) { - this.marked = []; - this.options = options; - this.timeout = null; - this.hasGutter = hasGutter; - this.onMouseOver = function(e) { onMouseOver(cm, e); }; - this.waitingFor = 0 - } - - function parseOptions(_cm, options) { - if (options instanceof Function) return {getAnnotations: options}; - if (!options || options === true) options = {}; - return options; - } - - function clearMarks(cm) { - var state = cm.state.lint; - if (state.hasGutter) cm.clearGutter(GUTTER_ID); - for (var i = 0; i < state.marked.length; ++i) - state.marked[i].clear(); - state.marked.length = 0; - } - - function makeMarker(cm, labels, severity, multiple, tooltips) { - var marker = document.createElement("div"), inner = marker; - marker.className = "CodeMirror-lint-marker-" + severity; - if (multiple) { - inner = marker.appendChild(document.createElement("div")); - inner.className = "CodeMirror-lint-marker-multiple"; - } - - if (tooltips != false) CodeMirror.on(inner, "mouseover", function(e) { - showTooltipFor(cm, e, labels, inner); - }); - - return marker; - } - - function getMaxSeverity(a, b) { - if (a == "error") return a; - else return b; - } - - function groupByLine(annotations) { - var lines = []; - for (var i = 0; i < annotations.length; ++i) { - var ann = annotations[i], line = ann.from.line; - (lines[line] || (lines[line] = [])).push(ann); - } - return lines; - } - - function annotationTooltip(ann) { - var severity = ann.severity; - if (!severity) severity = "error"; - var tip = document.createElement("div"); - tip.className = "CodeMirror-lint-message-" + severity; - if (typeof ann.messageHTML != 'undefined') { - tip.innerHTML = ann.messageHTML; - } else { - tip.appendChild(document.createTextNode(ann.message)); - } - return tip; - } - - function lintAsync(cm, getAnnotations, passOptions) { - var state = cm.state.lint - var id = ++state.waitingFor - function abort() { - id = -1 - cm.off("change", abort) - } - cm.on("change", abort) - getAnnotations(cm.getValue(), function(annotations, arg2) { - cm.off("change", abort) - if (state.waitingFor != id) return - if (arg2 && annotations instanceof CodeMirror) annotations = arg2 - cm.operation(function() {updateLinting(cm, annotations)}) - }, passOptions, cm); - } - - function startLinting(cm) { - var state = cm.state.lint, options = state.options; - /* - * Passing rules in `options` property prevents JSHint (and other linters) from complaining - * about unrecognized rules like `onUpdateLinting`, `delay`, `lintOnChange`, etc. - */ - var passOptions = options.options || options; - var getAnnotations = options.getAnnotations || cm.getHelper(CodeMirror.Pos(0, 0), "lint"); - if (!getAnnotations) return; - if (options.async || getAnnotations.async) { - lintAsync(cm, getAnnotations, passOptions) - } else { - var annotations = getAnnotations(cm.getValue(), passOptions, cm); - if (!annotations) return; - if (annotations.then) annotations.then(function(issues) { - cm.operation(function() {updateLinting(cm, issues)}) - }); - else cm.operation(function() {updateLinting(cm, annotations)}) - } - } - - function updateLinting(cm, annotationsNotSorted) { - clearMarks(cm); - var state = cm.state.lint, options = state.options; - - var annotations = groupByLine(annotationsNotSorted); - - for (var line = 0; line < annotations.length; ++line) { - var anns = annotations[line]; - if (!anns) continue; - - var maxSeverity = null; - var tipLabel = state.hasGutter && document.createDocumentFragment(); - - for (var i = 0; i < anns.length; ++i) { - var ann = anns[i]; - var severity = ann.severity; - if (!severity) severity = "error"; - maxSeverity = getMaxSeverity(maxSeverity, severity); - - if (options.formatAnnotation) ann = options.formatAnnotation(ann); - if (state.hasGutter) tipLabel.appendChild(annotationTooltip(ann)); - - if (ann.to) state.marked.push(cm.markText(ann.from, ann.to, { - className: "CodeMirror-lint-mark-" + severity, - __annotation: ann - })); - } - - if (state.hasGutter) - cm.setGutterMarker(line, GUTTER_ID, makeMarker(cm, tipLabel, maxSeverity, anns.length > 1, - state.options.tooltips)); - } - if (options.onUpdateLinting) options.onUpdateLinting(annotationsNotSorted, annotations, cm); - } - - function onChange(cm) { - var state = cm.state.lint; - if (!state) return; - clearTimeout(state.timeout); - state.timeout = setTimeout(function(){startLinting(cm);}, state.options.delay || 500); - } - - function popupTooltips(cm, annotations, e) { - var target = e.target || e.srcElement; - var tooltip = document.createDocumentFragment(); - for (var i = 0; i < annotations.length; i++) { - var ann = annotations[i]; - tooltip.appendChild(annotationTooltip(ann)); - } - showTooltipFor(cm, e, tooltip, target); - } - - function onMouseOver(cm, e) { - var target = e.target || e.srcElement; - if (!/\bCodeMirror-lint-mark-/.test(target.className)) return; - var box = target.getBoundingClientRect(), x = (box.left + box.right) / 2, y = (box.top + box.bottom) / 2; - var spans = cm.findMarksAt(cm.coordsChar({left: x, top: y}, "client")); - - var annotations = []; - for (var i = 0; i < spans.length; ++i) { - var ann = spans[i].__annotation; - if (ann) annotations.push(ann); - } - if (annotations.length) popupTooltips(cm, annotations, e); - } - - CodeMirror.defineOption("lint", false, function(cm, val, old) { - if (old && old != CodeMirror.Init) { - clearMarks(cm); - if (cm.state.lint.options.lintOnChange !== false) - cm.off("change", onChange); - CodeMirror.off(cm.getWrapperElement(), "mouseover", cm.state.lint.onMouseOver); - clearTimeout(cm.state.lint.timeout); - delete cm.state.lint; - } - - if (val) { - var gutters = cm.getOption("gutters"), hasLintGutter = false; - for (var i = 0; i < gutters.length; ++i) if (gutters[i] == GUTTER_ID) hasLintGutter = true; - var state = cm.state.lint = new LintState(cm, parseOptions(cm, val), hasLintGutter); - if (state.options.lintOnChange !== false) - cm.on("change", onChange); - if (state.options.tooltips != false && state.options.tooltips != "gutter") - CodeMirror.on(cm.getWrapperElement(), "mouseover", state.onMouseOver); - - startLinting(cm); - } - }); - - CodeMirror.defineExtension("performLint", function() { - if (this.state.lint) startLinting(this); - }); -}); - - -/* ---- extension/scroll/annotatescrollbar.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - CodeMirror.defineExtension("annotateScrollbar", function(options) { - if (typeof options == "string") options = {className: options}; - return new Annotation(this, options); - }); - - CodeMirror.defineOption("scrollButtonHeight", 0); - - function Annotation(cm, options) { - this.cm = cm; - this.options = options; - this.buttonHeight = options.scrollButtonHeight || cm.getOption("scrollButtonHeight"); - this.annotations = []; - this.doRedraw = this.doUpdate = null; - this.div = cm.getWrapperElement().appendChild(document.createElement("div")); - this.div.style.cssText = "position: absolute; right: 0; top: 0; z-index: 7; pointer-events: none"; - this.computeScale(); - - function scheduleRedraw(delay) { - clearTimeout(self.doRedraw); - self.doRedraw = setTimeout(function() { self.redraw(); }, delay); - } - - var self = this; - cm.on("refresh", this.resizeHandler = function() { - clearTimeout(self.doUpdate); - self.doUpdate = setTimeout(function() { - if (self.computeScale()) scheduleRedraw(20); - }, 100); - }); - cm.on("markerAdded", this.resizeHandler); - cm.on("markerCleared", this.resizeHandler); - if (options.listenForChanges !== false) - cm.on("changes", this.changeHandler = function() { - scheduleRedraw(250); - }); - } - - Annotation.prototype.computeScale = function() { - var cm = this.cm; - var hScale = (cm.getWrapperElement().clientHeight - cm.display.barHeight - this.buttonHeight * 2) / - cm.getScrollerElement().scrollHeight - if (hScale != this.hScale) { - this.hScale = hScale; - return true; - } - }; - - Annotation.prototype.update = function(annotations) { - this.annotations = annotations; - this.redraw(); - }; - - Annotation.prototype.redraw = function(compute) { - if (compute !== false) this.computeScale(); - var cm = this.cm, hScale = this.hScale; - - var frag = document.createDocumentFragment(), anns = this.annotations; - - var wrapping = cm.getOption("lineWrapping"); - var singleLineH = wrapping && cm.defaultTextHeight() * 1.5; - var curLine = null, curLineObj = null; - function getY(pos, top) { - if (curLine != pos.line) { - curLine = pos.line; - curLineObj = cm.getLineHandle(curLine); - } - if ((curLineObj.widgets && curLineObj.widgets.length) || - (wrapping && curLineObj.height > singleLineH)) - return cm.charCoords(pos, "local")[top ? "top" : "bottom"]; - var topY = cm.heightAtLine(curLineObj, "local"); - return topY + (top ? 0 : curLineObj.height); - } - - var lastLine = cm.lastLine() - if (cm.display.barWidth) for (var i = 0, nextTop; i < anns.length; i++) { - var ann = anns[i]; - if (ann.to.line > lastLine) continue; - var top = nextTop || getY(ann.from, true) * hScale; - var bottom = getY(ann.to, false) * hScale; - while (i < anns.length - 1) { - if (anns[i + 1].to.line > lastLine) break; - nextTop = getY(anns[i + 1].from, true) * hScale; - if (nextTop > bottom + .9) break; - ann = anns[++i]; - bottom = getY(ann.to, false) * hScale; - } - if (bottom == top) continue; - var height = Math.max(bottom - top, 3); - - var elt = frag.appendChild(document.createElement("div")); - elt.style.cssText = "position: absolute; right: 0px; width: " + Math.max(cm.display.barWidth - 1, 2) + "px; top: " - + (top + this.buttonHeight) + "px; height: " + height + "px"; - elt.className = this.options.className; - if (ann.id) { - elt.setAttribute("annotation-id", ann.id); - } - } - this.div.textContent = ""; - this.div.appendChild(frag); - }; - - Annotation.prototype.clear = function() { - this.cm.off("refresh", this.resizeHandler); - this.cm.off("markerAdded", this.resizeHandler); - this.cm.off("markerCleared", this.resizeHandler); - if (this.changeHandler) this.cm.off("changes", this.changeHandler); - this.div.parentNode.removeChild(this.div); - }; -}); - - -/* ---- extension/scroll/scrollpastend.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - CodeMirror.defineOption("scrollPastEnd", false, function(cm, val, old) { - if (old && old != CodeMirror.Init) { - cm.off("change", onChange); - cm.off("refresh", updateBottomMargin); - cm.display.lineSpace.parentNode.style.paddingBottom = ""; - cm.state.scrollPastEndPadding = null; - } - if (val) { - cm.on("change", onChange); - cm.on("refresh", updateBottomMargin); - updateBottomMargin(cm); - } - }); - - function onChange(cm, change) { - if (CodeMirror.changeEnd(change).line == cm.lastLine()) - updateBottomMargin(cm); - } - - function updateBottomMargin(cm) { - var padding = ""; - if (cm.lineCount() > 1) { - var totalH = cm.display.scroller.clientHeight - 30, - lastLineH = cm.getLineHandle(cm.lastLine()).height; - padding = (totalH - lastLineH) + "px"; - } - if (cm.state.scrollPastEndPadding != padding) { - cm.state.scrollPastEndPadding = padding; - cm.display.lineSpace.parentNode.style.paddingBottom = padding; - cm.off("refresh", updateBottomMargin); - cm.setSize(); - cm.on("refresh", updateBottomMargin); - } - } -}); - - -/* ---- extension/scroll/simplescrollbars.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - function Bar(cls, orientation, scroll) { - this.orientation = orientation; - this.scroll = scroll; - this.screen = this.total = this.size = 1; - this.pos = 0; - - this.node = document.createElement("div"); - this.node.className = cls + "-" + orientation; - this.inner = this.node.appendChild(document.createElement("div")); - - var self = this; - CodeMirror.on(this.inner, "mousedown", function(e) { - if (e.which != 1) return; - CodeMirror.e_preventDefault(e); - var axis = self.orientation == "horizontal" ? "pageX" : "pageY"; - var start = e[axis], startpos = self.pos; - function done() { - CodeMirror.off(document, "mousemove", move); - CodeMirror.off(document, "mouseup", done); - } - function move(e) { - if (e.which != 1) return done(); - self.moveTo(startpos + (e[axis] - start) * (self.total / self.size)); - } - CodeMirror.on(document, "mousemove", move); - CodeMirror.on(document, "mouseup", done); - }); - - CodeMirror.on(this.node, "click", function(e) { - CodeMirror.e_preventDefault(e); - var innerBox = self.inner.getBoundingClientRect(), where; - if (self.orientation == "horizontal") - where = e.clientX < innerBox.left ? -1 : e.clientX > innerBox.right ? 1 : 0; - else - where = e.clientY < innerBox.top ? -1 : e.clientY > innerBox.bottom ? 1 : 0; - self.moveTo(self.pos + where * self.screen); - }); - - function onWheel(e) { - var moved = CodeMirror.wheelEventPixels(e)[self.orientation == "horizontal" ? "x" : "y"]; - var oldPos = self.pos; - self.moveTo(self.pos + moved); - if (self.pos != oldPos) CodeMirror.e_preventDefault(e); - } - CodeMirror.on(this.node, "mousewheel", onWheel); - CodeMirror.on(this.node, "DOMMouseScroll", onWheel); - } - - Bar.prototype.setPos = function(pos, force) { - if (pos < 0) pos = 0; - if (pos > this.total - this.screen) pos = this.total - this.screen; - if (!force && pos == this.pos) return false; - this.pos = pos; - this.inner.style[this.orientation == "horizontal" ? "left" : "top"] = - (pos * (this.size / this.total)) + "px"; - return true - }; - - Bar.prototype.moveTo = function(pos) { - if (this.setPos(pos)) this.scroll(pos, this.orientation); - } - - var minButtonSize = 10; - - Bar.prototype.update = function(scrollSize, clientSize, barSize) { - var sizeChanged = this.screen != clientSize || this.total != scrollSize || this.size != barSize - if (sizeChanged) { - this.screen = clientSize; - this.total = scrollSize; - this.size = barSize; - } - - var buttonSize = this.screen * (this.size / this.total); - if (buttonSize < minButtonSize) { - this.size -= minButtonSize - buttonSize; - buttonSize = minButtonSize; - } - this.inner.style[this.orientation == "horizontal" ? "width" : "height"] = - buttonSize + "px"; - this.setPos(this.pos, sizeChanged); - }; - - function SimpleScrollbars(cls, place, scroll) { - this.addClass = cls; - this.horiz = new Bar(cls, "horizontal", scroll); - place(this.horiz.node); - this.vert = new Bar(cls, "vertical", scroll); - place(this.vert.node); - this.width = null; - } - - SimpleScrollbars.prototype.update = function(measure) { - if (this.width == null) { - var style = window.getComputedStyle ? window.getComputedStyle(this.horiz.node) : this.horiz.node.currentStyle; - if (style) this.width = parseInt(style.height); - } - var width = this.width || 0; - - var needsH = measure.scrollWidth > measure.clientWidth + 1; - var needsV = measure.scrollHeight > measure.clientHeight + 1; - this.vert.node.style.display = needsV ? "block" : "none"; - this.horiz.node.style.display = needsH ? "block" : "none"; - - if (needsV) { - this.vert.update(measure.scrollHeight, measure.clientHeight, - measure.viewHeight - (needsH ? width : 0)); - this.vert.node.style.bottom = needsH ? width + "px" : "0"; - } - if (needsH) { - this.horiz.update(measure.scrollWidth, measure.clientWidth, - measure.viewWidth - (needsV ? width : 0) - measure.barLeft); - this.horiz.node.style.right = needsV ? width + "px" : "0"; - this.horiz.node.style.left = measure.barLeft + "px"; - } - - return {right: needsV ? width : 0, bottom: needsH ? width : 0}; - }; - - SimpleScrollbars.prototype.setScrollTop = function(pos) { - this.vert.setPos(pos); - }; - - SimpleScrollbars.prototype.setScrollLeft = function(pos) { - this.horiz.setPos(pos); - }; - - SimpleScrollbars.prototype.clear = function() { - var parent = this.horiz.node.parentNode; - parent.removeChild(this.horiz.node); - parent.removeChild(this.vert.node); - }; - - CodeMirror.scrollbarModel.simple = function(place, scroll) { - return new SimpleScrollbars("CodeMirror-simplescroll", place, scroll); - }; - CodeMirror.scrollbarModel.overlay = function(place, scroll) { - return new SimpleScrollbars("CodeMirror-overlayscroll", place, scroll); - }; -}); - - -/* ---- extension/search/jump-to-line.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -// Defines jumpToLine command. Uses dialog.js if present. - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("../dialog/dialog")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "../dialog/dialog"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - function dialog(cm, text, shortText, deflt, f) { - if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true}); - else f(prompt(shortText, deflt)); - } - - function getJumpDialog(cm) { - return cm.phrase("Jump to line:") + ' ' + cm.phrase("(Use line:column or scroll% syntax)") + ''; - } - - function interpretLine(cm, string) { - var num = Number(string) - if (/^[-+]/.test(string)) return cm.getCursor().line + num - else return num - 1 - } - - CodeMirror.commands.jumpToLine = function(cm) { - var cur = cm.getCursor(); - dialog(cm, getJumpDialog(cm), cm.phrase("Jump to line:"), (cur.line + 1) + ":" + cur.ch, function(posStr) { - if (!posStr) return; - - var match; - if (match = /^\s*([\+\-]?\d+)\s*\:\s*(\d+)\s*$/.exec(posStr)) { - cm.setCursor(interpretLine(cm, match[1]), Number(match[2])) - } else if (match = /^\s*([\+\-]?\d+(\.\d+)?)\%\s*/.exec(posStr)) { - var line = Math.round(cm.lineCount() * Number(match[1]) / 100); - if (/^[-+]/.test(match[1])) line = cur.line + line + 1; - cm.setCursor(line - 1, cur.ch); - } else if (match = /^\s*\:?\s*([\+\-]?\d+)\s*/.exec(posStr)) { - cm.setCursor(interpretLine(cm, match[1]), cur.ch); - } - }); - }; - - CodeMirror.keyMap["default"]["Alt-G"] = "jumpToLine"; -}); - - -/* ---- extension/search/match-highlighter.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -// Highlighting text that matches the selection -// -// Defines an option highlightSelectionMatches, which, when enabled, -// will style strings that match the selection throughout the -// document. -// -// The option can be set to true to simply enable it, or to a -// {minChars, style, wordsOnly, showToken, delay} object to explicitly -// configure it. minChars is the minimum amount of characters that should be -// selected for the behavior to occur, and style is the token style to -// apply to the matches. This will be prefixed by "cm-" to create an -// actual CSS class name. If wordsOnly is enabled, the matches will be -// highlighted only if the selected text is a word. showToken, when enabled, -// will cause the current token to be highlighted when nothing is selected. -// delay is used to specify how much time to wait, in milliseconds, before -// highlighting the matches. If annotateScrollbar is enabled, the occurences -// will be highlighted on the scrollbar via the matchesonscrollbar addon. - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("./matchesonscrollbar")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "./matchesonscrollbar"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - var defaults = { - style: "matchhighlight", - minChars: 2, - delay: 100, - wordsOnly: false, - annotateScrollbar: false, - showToken: false, - trim: true - } - - function State(options) { - this.options = {} - for (var name in defaults) - this.options[name] = (options && options.hasOwnProperty(name) ? options : defaults)[name] - this.overlay = this.timeout = null; - this.matchesonscroll = null; - this.active = false; - } - - CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) { - if (old && old != CodeMirror.Init) { - removeOverlay(cm); - clearTimeout(cm.state.matchHighlighter.timeout); - cm.state.matchHighlighter = null; - cm.off("cursorActivity", cursorActivity); - cm.off("focus", onFocus) - } - if (val) { - var state = cm.state.matchHighlighter = new State(val); - if (cm.hasFocus()) { - state.active = true - highlightMatches(cm) - } else { - cm.on("focus", onFocus) - } - cm.on("cursorActivity", cursorActivity); - } - }); - - function cursorActivity(cm) { - var state = cm.state.matchHighlighter; - if (state.active || cm.hasFocus()) scheduleHighlight(cm, state) - } - - function onFocus(cm) { - var state = cm.state.matchHighlighter - if (!state.active) { - state.active = true - scheduleHighlight(cm, state) - } - } - - function scheduleHighlight(cm, state) { - clearTimeout(state.timeout); - state.timeout = setTimeout(function() {highlightMatches(cm);}, state.options.delay); - } - - function addOverlay(cm, query, hasBoundary, style) { - var state = cm.state.matchHighlighter; - cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style)); - if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) { - var searchFor = hasBoundary ? new RegExp((/\w/.test(query.charAt(0)) ? "\\b" : "") + - query.replace(/[\\\[.+*?(){|^$]/g, "\\$&") + - (/\w/.test(query.charAt(query.length - 1)) ? "\\b" : "")) : query; - state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false, - {className: "CodeMirror-selection-highlight-scrollbar"}); - } - } - - function removeOverlay(cm) { - var state = cm.state.matchHighlighter; - if (state.overlay) { - cm.removeOverlay(state.overlay); - state.overlay = null; - if (state.matchesonscroll) { - state.matchesonscroll.clear(); - state.matchesonscroll = null; - } - } - } - - function highlightMatches(cm) { - cm.operation(function() { - var state = cm.state.matchHighlighter; - removeOverlay(cm); - if (!cm.somethingSelected() && state.options.showToken) { - var re = state.options.showToken === true ? /[\w$]/ : state.options.showToken; - var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start; - while (start && re.test(line.charAt(start - 1))) --start; - while (end < line.length && re.test(line.charAt(end))) ++end; - if (start < end) - addOverlay(cm, line.slice(start, end), re, state.options.style); - return; - } - var from = cm.getCursor("from"), to = cm.getCursor("to"); - if (from.line != to.line) return; - if (state.options.wordsOnly && !isWord(cm, from, to)) return; - var selection = cm.getRange(from, to) - if (state.options.trim) selection = selection.replace(/^\s+|\s+$/g, "") - if (selection.length >= state.options.minChars) - addOverlay(cm, selection, false, state.options.style); - }); - } - - function isWord(cm, from, to) { - var str = cm.getRange(from, to); - if (str.match(/^\w+$/) !== null) { - if (from.ch > 0) { - var pos = {line: from.line, ch: from.ch - 1}; - var chr = cm.getRange(pos, from); - if (chr.match(/\W/) === null) return false; - } - if (to.ch < cm.getLine(from.line).length) { - var pos = {line: to.line, ch: to.ch + 1}; - var chr = cm.getRange(to, pos); - if (chr.match(/\W/) === null) return false; - } - return true; - } else return false; - } - - function boundariesAround(stream, re) { - return (!stream.start || !re.test(stream.string.charAt(stream.start - 1))) && - (stream.pos == stream.string.length || !re.test(stream.string.charAt(stream.pos))); - } - - function makeOverlay(query, hasBoundary, style) { - return {token: function(stream) { - if (stream.match(query) && - (!hasBoundary || boundariesAround(stream, hasBoundary))) - return style; - stream.next(); - stream.skipTo(query.charAt(0)) || stream.skipToEnd(); - }}; - } -}); - - -/* ---- extension/search/matchesonscrollbar.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("./searchcursor"), require("../scroll/annotatescrollbar")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "./searchcursor", "../scroll/annotatescrollbar"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - CodeMirror.defineExtension("showMatchesOnScrollbar", function(query, caseFold, options) { - if (typeof options == "string") options = {className: options}; - if (!options) options = {}; - return new SearchAnnotation(this, query, caseFold, options); - }); - - function SearchAnnotation(cm, query, caseFold, options) { - this.cm = cm; - this.options = options; - var annotateOptions = {listenForChanges: false}; - for (var prop in options) annotateOptions[prop] = options[prop]; - if (!annotateOptions.className) annotateOptions.className = "CodeMirror-search-match"; - this.annotation = cm.annotateScrollbar(annotateOptions); - this.query = query; - this.caseFold = caseFold; - this.gap = {from: cm.firstLine(), to: cm.lastLine() + 1}; - this.matches = []; - this.update = null; - - this.findMatches(); - this.annotation.update(this.matches); - - var self = this; - cm.on("change", this.changeHandler = function(_cm, change) { self.onChange(change); }); - } - - var MAX_MATCHES = 1000; - - SearchAnnotation.prototype.findMatches = function() { - if (!this.gap) return; - for (var i = 0; i < this.matches.length; i++) { - var match = this.matches[i]; - if (match.from.line >= this.gap.to) break; - if (match.to.line >= this.gap.from) this.matches.splice(i--, 1); - } - var cursor = this.cm.getSearchCursor(this.query, CodeMirror.Pos(this.gap.from, 0), {caseFold: this.caseFold, multiline: this.options.multiline}); - var maxMatches = this.options && this.options.maxMatches || MAX_MATCHES; - while (cursor.findNext()) { - var match = {from: cursor.from(), to: cursor.to()}; - if (match.from.line >= this.gap.to) break; - this.matches.splice(i++, 0, match); - if (this.matches.length > maxMatches) break; - } - this.gap = null; - }; - - function offsetLine(line, changeStart, sizeChange) { - if (line <= changeStart) return line; - return Math.max(changeStart, line + sizeChange); - } - - SearchAnnotation.prototype.onChange = function(change) { - var startLine = change.from.line; - var endLine = CodeMirror.changeEnd(change).line; - var sizeChange = endLine - change.to.line; - if (this.gap) { - this.gap.from = Math.min(offsetLine(this.gap.from, startLine, sizeChange), change.from.line); - this.gap.to = Math.max(offsetLine(this.gap.to, startLine, sizeChange), change.from.line); - } else { - this.gap = {from: change.from.line, to: endLine + 1}; - } - - if (sizeChange) for (var i = 0; i < this.matches.length; i++) { - var match = this.matches[i]; - var newFrom = offsetLine(match.from.line, startLine, sizeChange); - if (newFrom != match.from.line) match.from = CodeMirror.Pos(newFrom, match.from.ch); - var newTo = offsetLine(match.to.line, startLine, sizeChange); - if (newTo != match.to.line) match.to = CodeMirror.Pos(newTo, match.to.ch); - } - clearTimeout(this.update); - var self = this; - this.update = setTimeout(function() { self.updateAfterChange(); }, 250); - }; - - SearchAnnotation.prototype.updateAfterChange = function() { - this.findMatches(); - this.annotation.update(this.matches); - }; - - SearchAnnotation.prototype.clear = function() { - this.cm.off("change", this.changeHandler); - this.annotation.clear(); - }; -}); - - -/* ---- extension/search/search.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -// Define search commands. Depends on dialog.js or another -// implementation of the openDialog method. - -// Replace works a little oddly -- it will do the replace on the next -// Ctrl-G (or whatever is bound to findNext) press. You prevent a -// replace by making sure the match is no longer selected when hitting -// Ctrl-G. - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("./searchcursor"), require("../dialog/dialog")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "./searchcursor", "../dialog/dialog"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - function searchOverlay(query, caseInsensitive) { - if (typeof query == "string") - query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), caseInsensitive ? "gi" : "g"); - else if (!query.global) - query = new RegExp(query.source, query.ignoreCase ? "gi" : "g"); - - return {token: function(stream) { - query.lastIndex = stream.pos; - var match = query.exec(stream.string); - if (match && match.index == stream.pos) { - stream.pos += match[0].length || 1; - return "searching"; - } else if (match) { - stream.pos = match.index; - } else { - stream.skipToEnd(); - } - }}; - } - - function SearchState() { - this.posFrom = this.posTo = this.lastQuery = this.query = null; - this.overlay = null; - } - - function getSearchState(cm) { - return cm.state.search || (cm.state.search = new SearchState()); - } - - function queryCaseInsensitive(query) { - return typeof query == "string" && query == query.toLowerCase(); - } - - function getSearchCursor(cm, query, pos) { - // Heuristic: if the query string is all lowercase, do a case insensitive search. - return cm.getSearchCursor(query, pos, {caseFold: queryCaseInsensitive(query), multiline: true}); - } - - function persistentDialog(cm, text, deflt, onEnter, onKeyDown) { - cm.openDialog(text, onEnter, { - value: deflt, - selectValueOnOpen: true, - closeOnEnter: false, - onClose: function() { clearSearch(cm); }, - onKeyDown: onKeyDown - }); - } - - function dialog(cm, text, shortText, deflt, f) { - if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true}); - else f(prompt(shortText, deflt)); - } - - function confirmDialog(cm, text, shortText, fs) { - if (cm.openConfirm) cm.openConfirm(text, fs); - else if (confirm(shortText)) fs[0](); - } - - function parseString(string) { - return string.replace(/\\([nrt\\])/g, function(match, ch) { - if (ch == "n") return "\n" - if (ch == "r") return "\r" - if (ch == "t") return "\t" - if (ch == "\\") return "\\" - return match - }) - } - - function parseQuery(query) { - var isRE = query.match(/^\/(.*)\/([a-z]*)$/); - if (isRE) { - try { query = new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i"); } - catch(e) {} // Not a regular expression after all, do a string search - } else { - query = parseString(query) - } - if (typeof query == "string" ? query == "" : query.test("")) - query = /x^/; - return query; - } - - function startSearch(cm, state, query) { - state.queryText = query; - state.query = parseQuery(query); - cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query)); - state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query)); - cm.addOverlay(state.overlay); - if (cm.showMatchesOnScrollbar) { - if (state.annotate) { state.annotate.clear(); state.annotate = null; } - state.annotate = cm.showMatchesOnScrollbar(state.query, queryCaseInsensitive(state.query)); - } - } - - function doSearch(cm, rev, persistent, immediate) { - var state = getSearchState(cm); - if (state.query) return findNext(cm, rev); - var q = cm.getSelection() || state.lastQuery; - if (q instanceof RegExp && q.source == "x^") q = null - if (persistent && cm.openDialog) { - var hiding = null - var searchNext = function(query, event) { - CodeMirror.e_stop(event); - if (!query) return; - if (query != state.queryText) { - startSearch(cm, state, query); - state.posFrom = state.posTo = cm.getCursor(); - } - if (hiding) hiding.style.opacity = 1 - findNext(cm, event.shiftKey, function(_, to) { - var dialog - if (to.line < 3 && document.querySelector && - (dialog = cm.display.wrapper.querySelector(".CodeMirror-dialog")) && - dialog.getBoundingClientRect().bottom - 4 > cm.cursorCoords(to, "window").top) - (hiding = dialog).style.opacity = .4 - }) - }; - persistentDialog(cm, getQueryDialog(cm), q, searchNext, function(event, query) { - var keyName = CodeMirror.keyName(event) - var extra = cm.getOption('extraKeys'), cmd = (extra && extra[keyName]) || CodeMirror.keyMap[cm.getOption("keyMap")][keyName] - if (cmd == "findNext" || cmd == "findPrev" || - cmd == "findPersistentNext" || cmd == "findPersistentPrev") { - CodeMirror.e_stop(event); - startSearch(cm, getSearchState(cm), query); - cm.execCommand(cmd); - } else if (cmd == "find" || cmd == "findPersistent") { - CodeMirror.e_stop(event); - searchNext(query, event); - } - }); - if (immediate && q) { - startSearch(cm, state, q); - findNext(cm, rev); - } - } else { - dialog(cm, getQueryDialog(cm), "Search for:", q, function(query) { - if (query && !state.query) cm.operation(function() { - startSearch(cm, state, query); - state.posFrom = state.posTo = cm.getCursor(); - findNext(cm, rev); - }); - }); - } - } - - function findNext(cm, rev, callback) {cm.operation(function() { - var state = getSearchState(cm); - var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo); - if (!cursor.find(rev)) { - cursor = getSearchCursor(cm, state.query, rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0)); - if (!cursor.find(rev)) return; - } - cm.setSelection(cursor.from(), cursor.to()); - cm.scrollIntoView({from: cursor.from(), to: cursor.to()}, 20); - state.posFrom = cursor.from(); state.posTo = cursor.to(); - if (callback) callback(cursor.from(), cursor.to()) - });} - - function clearSearch(cm) {cm.operation(function() { - var state = getSearchState(cm); - state.lastQuery = state.query; - if (!state.query) return; - state.query = state.queryText = null; - cm.removeOverlay(state.overlay); - if (state.annotate) { state.annotate.clear(); state.annotate = null; } - });} - - - function getQueryDialog(cm) { - return '' + cm.phrase("Search:") + ' ' + cm.phrase("(Use /re/ syntax for regexp search)") + ''; - } - function getReplaceQueryDialog(cm) { - return ' ' + cm.phrase("(Use /re/ syntax for regexp search)") + ''; - } - function getReplacementQueryDialog(cm) { - return '' + cm.phrase("With:") + ' '; - } - function getDoReplaceConfirm(cm) { - return '' + cm.phrase("Replace?") + ' '; - } - - function replaceAll(cm, query, text) { - cm.operation(function() { - for (var cursor = getSearchCursor(cm, query); cursor.findNext();) { - if (typeof query != "string") { - var match = cm.getRange(cursor.from(), cursor.to()).match(query); - cursor.replace(text.replace(/\$(\d)/g, function(_, i) {return match[i];})); - } else cursor.replace(text); - } - }); - } - - function replace(cm, all) { - if (cm.getOption("readOnly")) return; - var query = cm.getSelection() || getSearchState(cm).lastQuery; - var dialogText = '' + (all ? cm.phrase("Replace all:") : cm.phrase("Replace:")) + ''; - dialog(cm, dialogText + getReplaceQueryDialog(cm), dialogText, query, function(query) { - if (!query) return; - query = parseQuery(query); - dialog(cm, getReplacementQueryDialog(cm), cm.phrase("Replace with:"), "", function(text) { - text = parseString(text) - if (all) { - replaceAll(cm, query, text) - } else { - clearSearch(cm); - var cursor = getSearchCursor(cm, query, cm.getCursor("from")); - var advance = function() { - var start = cursor.from(), match; - if (!(match = cursor.findNext())) { - cursor = getSearchCursor(cm, query); - if (!(match = cursor.findNext()) || - (start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return; - } - cm.setSelection(cursor.from(), cursor.to()); - cm.scrollIntoView({from: cursor.from(), to: cursor.to()}); - confirmDialog(cm, getDoReplaceConfirm(cm), cm.phrase("Replace?"), - [function() {doReplace(match);}, advance, - function() {replaceAll(cm, query, text)}]); - }; - var doReplace = function(match) { - cursor.replace(typeof query == "string" ? text : - text.replace(/\$(\d)/g, function(_, i) {return match[i];})); - advance(); - }; - advance(); - } - }); - }); - } - - CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);}; - CodeMirror.commands.findPersistent = function(cm) {clearSearch(cm); doSearch(cm, false, true);}; - CodeMirror.commands.findPersistentNext = function(cm) {doSearch(cm, false, true, true);}; - CodeMirror.commands.findPersistentPrev = function(cm) {doSearch(cm, true, true, true);}; - CodeMirror.commands.findNext = doSearch; - CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);}; - CodeMirror.commands.clearSearch = clearSearch; - CodeMirror.commands.replace = replace; - CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);}; -}); - - -/* ---- extension/search/searchcursor.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")) - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod) - else // Plain browser env - mod(CodeMirror) -})(function(CodeMirror) { - "use strict" - var Pos = CodeMirror.Pos - - function regexpFlags(regexp) { - var flags = regexp.flags - return flags != null ? flags : (regexp.ignoreCase ? "i" : "") - + (regexp.global ? "g" : "") - + (regexp.multiline ? "m" : "") - } - - function ensureFlags(regexp, flags) { - var current = regexpFlags(regexp), target = current - for (var i = 0; i < flags.length; i++) if (target.indexOf(flags.charAt(i)) == -1) - target += flags.charAt(i) - return current == target ? regexp : new RegExp(regexp.source, target) - } - - function maybeMultiline(regexp) { - return /\\s|\\n|\n|\\W|\\D|\[\^/.test(regexp.source) - } - - function searchRegexpForward(doc, regexp, start) { - regexp = ensureFlags(regexp, "g") - for (var line = start.line, ch = start.ch, last = doc.lastLine(); line <= last; line++, ch = 0) { - regexp.lastIndex = ch - var string = doc.getLine(line), match = regexp.exec(string) - if (match) - return {from: Pos(line, match.index), - to: Pos(line, match.index + match[0].length), - match: match} - } - } - - function searchRegexpForwardMultiline(doc, regexp, start) { - if (!maybeMultiline(regexp)) return searchRegexpForward(doc, regexp, start) - - regexp = ensureFlags(regexp, "gm") - var string, chunk = 1 - for (var line = start.line, last = doc.lastLine(); line <= last;) { - // This grows the search buffer in exponentially-sized chunks - // between matches, so that nearby matches are fast and don't - // require concatenating the whole document (in case we're - // searching for something that has tons of matches), but at the - // same time, the amount of retries is limited. - for (var i = 0; i < chunk; i++) { - if (line > last) break - var curLine = doc.getLine(line++) - string = string == null ? curLine : string + "\n" + curLine - } - chunk = chunk * 2 - regexp.lastIndex = start.ch - var match = regexp.exec(string) - if (match) { - var before = string.slice(0, match.index).split("\n"), inside = match[0].split("\n") - var startLine = start.line + before.length - 1, startCh = before[before.length - 1].length - return {from: Pos(startLine, startCh), - to: Pos(startLine + inside.length - 1, - inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length), - match: match} - } - } - } - - function lastMatchIn(string, regexp, endMargin) { - var match, from = 0 - while (from <= string.length) { - regexp.lastIndex = from - var newMatch = regexp.exec(string) - if (!newMatch) break - var end = newMatch.index + newMatch[0].length - if (end > string.length - endMargin) break - if (!match || end > match.index + match[0].length) - match = newMatch - from = newMatch.index + 1 - } - return match - } - - function searchRegexpBackward(doc, regexp, start) { - regexp = ensureFlags(regexp, "g") - for (var line = start.line, ch = start.ch, first = doc.firstLine(); line >= first; line--, ch = -1) { - var string = doc.getLine(line) - var match = lastMatchIn(string, regexp, ch < 0 ? 0 : string.length - ch) - if (match) - return {from: Pos(line, match.index), - to: Pos(line, match.index + match[0].length), - match: match} - } - } - - function searchRegexpBackwardMultiline(doc, regexp, start) { - if (!maybeMultiline(regexp)) return searchRegexpBackward(doc, regexp, start) - regexp = ensureFlags(regexp, "gm") - var string, chunkSize = 1, endMargin = doc.getLine(start.line).length - start.ch - for (var line = start.line, first = doc.firstLine(); line >= first;) { - for (var i = 0; i < chunkSize && line >= first; i++) { - var curLine = doc.getLine(line--) - string = string == null ? curLine : curLine + "\n" + string - } - chunkSize *= 2 - - var match = lastMatchIn(string, regexp, endMargin) - if (match) { - var before = string.slice(0, match.index).split("\n"), inside = match[0].split("\n") - var startLine = line + before.length, startCh = before[before.length - 1].length - return {from: Pos(startLine, startCh), - to: Pos(startLine + inside.length - 1, - inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length), - match: match} - } - } - } - - var doFold, noFold - if (String.prototype.normalize) { - doFold = function(str) { return str.normalize("NFD").toLowerCase() } - noFold = function(str) { return str.normalize("NFD") } - } else { - doFold = function(str) { return str.toLowerCase() } - noFold = function(str) { return str } - } - - // Maps a position in a case-folded line back to a position in the original line - // (compensating for codepoints increasing in number during folding) - function adjustPos(orig, folded, pos, foldFunc) { - if (orig.length == folded.length) return pos - for (var min = 0, max = pos + Math.max(0, orig.length - folded.length);;) { - if (min == max) return min - var mid = (min + max) >> 1 - var len = foldFunc(orig.slice(0, mid)).length - if (len == pos) return mid - else if (len > pos) max = mid - else min = mid + 1 - } - } - - function searchStringForward(doc, query, start, caseFold) { - // Empty string would match anything and never progress, so we - // define it to match nothing instead. - if (!query.length) return null - var fold = caseFold ? doFold : noFold - var lines = fold(query).split(/\r|\n\r?/) - - search: for (var line = start.line, ch = start.ch, last = doc.lastLine() + 1 - lines.length; line <= last; line++, ch = 0) { - var orig = doc.getLine(line).slice(ch), string = fold(orig) - if (lines.length == 1) { - var found = string.indexOf(lines[0]) - if (found == -1) continue search - var start = adjustPos(orig, string, found, fold) + ch - return {from: Pos(line, adjustPos(orig, string, found, fold) + ch), - to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold) + ch)} - } else { - var cutFrom = string.length - lines[0].length - if (string.slice(cutFrom) != lines[0]) continue search - for (var i = 1; i < lines.length - 1; i++) - if (fold(doc.getLine(line + i)) != lines[i]) continue search - var end = doc.getLine(line + lines.length - 1), endString = fold(end), lastLine = lines[lines.length - 1] - if (endString.slice(0, lastLine.length) != lastLine) continue search - return {from: Pos(line, adjustPos(orig, string, cutFrom, fold) + ch), - to: Pos(line + lines.length - 1, adjustPos(end, endString, lastLine.length, fold))} - } - } - } - - function searchStringBackward(doc, query, start, caseFold) { - if (!query.length) return null - var fold = caseFold ? doFold : noFold - var lines = fold(query).split(/\r|\n\r?/) - - search: for (var line = start.line, ch = start.ch, first = doc.firstLine() - 1 + lines.length; line >= first; line--, ch = -1) { - var orig = doc.getLine(line) - if (ch > -1) orig = orig.slice(0, ch) - var string = fold(orig) - if (lines.length == 1) { - var found = string.lastIndexOf(lines[0]) - if (found == -1) continue search - return {from: Pos(line, adjustPos(orig, string, found, fold)), - to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold))} - } else { - var lastLine = lines[lines.length - 1] - if (string.slice(0, lastLine.length) != lastLine) continue search - for (var i = 1, start = line - lines.length + 1; i < lines.length - 1; i++) - if (fold(doc.getLine(start + i)) != lines[i]) continue search - var top = doc.getLine(line + 1 - lines.length), topString = fold(top) - if (topString.slice(topString.length - lines[0].length) != lines[0]) continue search - return {from: Pos(line + 1 - lines.length, adjustPos(top, topString, top.length - lines[0].length, fold)), - to: Pos(line, adjustPos(orig, string, lastLine.length, fold))} - } - } - } - - function SearchCursor(doc, query, pos, options) { - this.atOccurrence = false - this.doc = doc - pos = pos ? doc.clipPos(pos) : Pos(0, 0) - this.pos = {from: pos, to: pos} - - var caseFold - if (typeof options == "object") { - caseFold = options.caseFold - } else { // Backwards compat for when caseFold was the 4th argument - caseFold = options - options = null - } - - if (typeof query == "string") { - if (caseFold == null) caseFold = false - this.matches = function(reverse, pos) { - return (reverse ? searchStringBackward : searchStringForward)(doc, query, pos, caseFold) - } - } else { - query = ensureFlags(query, "gm") - if (!options || options.multiline !== false) - this.matches = function(reverse, pos) { - return (reverse ? searchRegexpBackwardMultiline : searchRegexpForwardMultiline)(doc, query, pos) - } - else - this.matches = function(reverse, pos) { - return (reverse ? searchRegexpBackward : searchRegexpForward)(doc, query, pos) - } - } - } - - SearchCursor.prototype = { - findNext: function() {return this.find(false)}, - findPrevious: function() {return this.find(true)}, - - find: function(reverse) { - var result = this.matches(reverse, this.doc.clipPos(reverse ? this.pos.from : this.pos.to)) - - // Implements weird auto-growing behavior on null-matches for - // backwards-compatibility with the vim code (unfortunately) - while (result && CodeMirror.cmpPos(result.from, result.to) == 0) { - if (reverse) { - if (result.from.ch) result.from = Pos(result.from.line, result.from.ch - 1) - else if (result.from.line == this.doc.firstLine()) result = null - else result = this.matches(reverse, this.doc.clipPos(Pos(result.from.line - 1))) - } else { - if (result.to.ch < this.doc.getLine(result.to.line).length) result.to = Pos(result.to.line, result.to.ch + 1) - else if (result.to.line == this.doc.lastLine()) result = null - else result = this.matches(reverse, Pos(result.to.line + 1, 0)) - } - } - - if (result) { - this.pos = result - this.atOccurrence = true - return this.pos.match || true - } else { - var end = Pos(reverse ? this.doc.firstLine() : this.doc.lastLine() + 1, 0) - this.pos = {from: end, to: end} - return this.atOccurrence = false - } - }, - - from: function() {if (this.atOccurrence) return this.pos.from}, - to: function() {if (this.atOccurrence) return this.pos.to}, - - replace: function(newText, origin) { - if (!this.atOccurrence) return - var lines = CodeMirror.splitLines(newText) - this.doc.replaceRange(lines, this.pos.from, this.pos.to, origin) - this.pos.to = Pos(this.pos.from.line + lines.length - 1, - lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0)) - } - } - - CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) { - return new SearchCursor(this.doc, query, pos, caseFold) - }) - CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) { - return new SearchCursor(this, query, pos, caseFold) - }) - - CodeMirror.defineExtension("selectMatches", function(query, caseFold) { - var ranges = [] - var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold) - while (cur.findNext()) { - if (CodeMirror.cmpPos(cur.to(), this.getCursor("to")) > 0) break - ranges.push({anchor: cur.from(), head: cur.to()}) - } - if (ranges.length) - this.setSelections(ranges, 0) - }) -}); - - -/* ---- extension/selection/active-line.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - var WRAP_CLASS = "CodeMirror-activeline"; - var BACK_CLASS = "CodeMirror-activeline-background"; - var GUTT_CLASS = "CodeMirror-activeline-gutter"; - - CodeMirror.defineOption("styleActiveLine", false, function(cm, val, old) { - var prev = old == CodeMirror.Init ? false : old; - if (val == prev) return - if (prev) { - cm.off("beforeSelectionChange", selectionChange); - clearActiveLines(cm); - delete cm.state.activeLines; - } - if (val) { - cm.state.activeLines = []; - updateActiveLines(cm, cm.listSelections()); - cm.on("beforeSelectionChange", selectionChange); - } - }); - - function clearActiveLines(cm) { - for (var i = 0; i < cm.state.activeLines.length; i++) { - cm.removeLineClass(cm.state.activeLines[i], "wrap", WRAP_CLASS); - cm.removeLineClass(cm.state.activeLines[i], "background", BACK_CLASS); - cm.removeLineClass(cm.state.activeLines[i], "gutter", GUTT_CLASS); - } - } - - function sameArray(a, b) { - if (a.length != b.length) return false; - for (var i = 0; i < a.length; i++) - if (a[i] != b[i]) return false; - return true; - } - - function updateActiveLines(cm, ranges) { - var active = []; - for (var i = 0; i < ranges.length; i++) { - var range = ranges[i]; - var option = cm.getOption("styleActiveLine"); - if (typeof option == "object" && option.nonEmpty ? range.anchor.line != range.head.line : !range.empty()) - continue - var line = cm.getLineHandleVisualStart(range.head.line); - if (active[active.length - 1] != line) active.push(line); - } - if (sameArray(cm.state.activeLines, active)) return; - cm.operation(function() { - clearActiveLines(cm); - for (var i = 0; i < active.length; i++) { - cm.addLineClass(active[i], "wrap", WRAP_CLASS); - cm.addLineClass(active[i], "background", BACK_CLASS); - cm.addLineClass(active[i], "gutter", GUTT_CLASS); - } - cm.state.activeLines = active; - }); - } - - function selectionChange(cm, sel) { - updateActiveLines(cm, sel.ranges); - } -}); - - -/* ---- extension/selection/mark-selection.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -// Because sometimes you need to mark the selected *text*. -// -// Adds an option 'styleSelectedText' which, when enabled, gives -// selected text the CSS class given as option value, or -// "CodeMirror-selectedtext" when the value is not a string. - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - CodeMirror.defineOption("styleSelectedText", false, function(cm, val, old) { - var prev = old && old != CodeMirror.Init; - if (val && !prev) { - cm.state.markedSelection = []; - cm.state.markedSelectionStyle = typeof val == "string" ? val : "CodeMirror-selectedtext"; - reset(cm); - cm.on("cursorActivity", onCursorActivity); - cm.on("change", onChange); - } else if (!val && prev) { - cm.off("cursorActivity", onCursorActivity); - cm.off("change", onChange); - clear(cm); - cm.state.markedSelection = cm.state.markedSelectionStyle = null; - } - }); - - function onCursorActivity(cm) { - if (cm.state.markedSelection) - cm.operation(function() { update(cm); }); - } - - function onChange(cm) { - if (cm.state.markedSelection && cm.state.markedSelection.length) - cm.operation(function() { clear(cm); }); - } - - var CHUNK_SIZE = 8; - var Pos = CodeMirror.Pos; - var cmp = CodeMirror.cmpPos; - - function coverRange(cm, from, to, addAt) { - if (cmp(from, to) == 0) return; - var array = cm.state.markedSelection; - var cls = cm.state.markedSelectionStyle; - for (var line = from.line;;) { - var start = line == from.line ? from : Pos(line, 0); - var endLine = line + CHUNK_SIZE, atEnd = endLine >= to.line; - var end = atEnd ? to : Pos(endLine, 0); - var mark = cm.markText(start, end, {className: cls}); - if (addAt == null) array.push(mark); - else array.splice(addAt++, 0, mark); - if (atEnd) break; - line = endLine; - } - } - - function clear(cm) { - var array = cm.state.markedSelection; - for (var i = 0; i < array.length; ++i) array[i].clear(); - array.length = 0; - } - - function reset(cm) { - clear(cm); - var ranges = cm.listSelections(); - for (var i = 0; i < ranges.length; i++) - coverRange(cm, ranges[i].from(), ranges[i].to()); - } - - function update(cm) { - if (!cm.somethingSelected()) return clear(cm); - if (cm.listSelections().length > 1) return reset(cm); - - var from = cm.getCursor("start"), to = cm.getCursor("end"); - - var array = cm.state.markedSelection; - if (!array.length) return coverRange(cm, from, to); - - var coverStart = array[0].find(), coverEnd = array[array.length - 1].find(); - if (!coverStart || !coverEnd || to.line - from.line <= CHUNK_SIZE || - cmp(from, coverEnd.to) >= 0 || cmp(to, coverStart.from) <= 0) - return reset(cm); - - while (cmp(from, coverStart.from) > 0) { - array.shift().clear(); - coverStart = array[0].find(); - } - if (cmp(from, coverStart.from) < 0) { - if (coverStart.to.line - from.line < CHUNK_SIZE) { - array.shift().clear(); - coverRange(cm, from, coverStart.to, 0); - } else { - coverRange(cm, from, coverStart.from, 0); - } - } - - while (cmp(to, coverEnd.to) < 0) { - array.pop().clear(); - coverEnd = array[array.length - 1].find(); - } - if (cmp(to, coverEnd.to) > 0) { - if (to.line - coverEnd.from.line < CHUNK_SIZE) { - array.pop().clear(); - coverRange(cm, coverEnd.from, to); - } else { - coverRange(cm, coverEnd.to, to); - } - } - } -}); - - -/* ---- extension/selection/selection-pointer.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - CodeMirror.defineOption("selectionPointer", false, function(cm, val) { - var data = cm.state.selectionPointer; - if (data) { - CodeMirror.off(cm.getWrapperElement(), "mousemove", data.mousemove); - CodeMirror.off(cm.getWrapperElement(), "mouseout", data.mouseout); - CodeMirror.off(window, "scroll", data.windowScroll); - cm.off("cursorActivity", reset); - cm.off("scroll", reset); - cm.state.selectionPointer = null; - cm.display.lineDiv.style.cursor = ""; - } - if (val) { - data = cm.state.selectionPointer = { - value: typeof val == "string" ? val : "default", - mousemove: function(event) { mousemove(cm, event); }, - mouseout: function(event) { mouseout(cm, event); }, - windowScroll: function() { reset(cm); }, - rects: null, - mouseX: null, mouseY: null, - willUpdate: false - }; - CodeMirror.on(cm.getWrapperElement(), "mousemove", data.mousemove); - CodeMirror.on(cm.getWrapperElement(), "mouseout", data.mouseout); - CodeMirror.on(window, "scroll", data.windowScroll); - cm.on("cursorActivity", reset); - cm.on("scroll", reset); - } - }); - - function mousemove(cm, event) { - var data = cm.state.selectionPointer; - if (event.buttons == null ? event.which : event.buttons) { - data.mouseX = data.mouseY = null; - } else { - data.mouseX = event.clientX; - data.mouseY = event.clientY; - } - scheduleUpdate(cm); - } - - function mouseout(cm, event) { - if (!cm.getWrapperElement().contains(event.relatedTarget)) { - var data = cm.state.selectionPointer; - data.mouseX = data.mouseY = null; - scheduleUpdate(cm); - } - } - - function reset(cm) { - cm.state.selectionPointer.rects = null; - scheduleUpdate(cm); - } - - function scheduleUpdate(cm) { - if (!cm.state.selectionPointer.willUpdate) { - cm.state.selectionPointer.willUpdate = true; - setTimeout(function() { - update(cm); - cm.state.selectionPointer.willUpdate = false; - }, 50); - } - } - - function update(cm) { - var data = cm.state.selectionPointer; - if (!data) return; - if (data.rects == null && data.mouseX != null) { - data.rects = []; - if (cm.somethingSelected()) { - for (var sel = cm.display.selectionDiv.firstChild; sel; sel = sel.nextSibling) - data.rects.push(sel.getBoundingClientRect()); - } - } - var inside = false; - if (data.mouseX != null) for (var i = 0; i < data.rects.length; i++) { - var rect = data.rects[i]; - if (rect.left <= data.mouseX && rect.right >= data.mouseX && - rect.top <= data.mouseY && rect.bottom >= data.mouseY) - inside = true; - } - var cursor = inside ? data.value : ""; - if (cm.display.lineDiv.style.cursor != cursor) - cm.display.lineDiv.style.cursor = cursor; - } -}); - - -/* ---- mode/coffeescript.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -/** - * Link to the project's GitHub page: - * https://github.com/pickhardt/coffeescript-codemirror-mode - */ -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -CodeMirror.defineMode("coffeescript", function(conf, parserConf) { - var ERRORCLASS = "error"; - - function wordRegexp(words) { - return new RegExp("^((" + words.join(")|(") + "))\\b"); - } - - var operators = /^(?:->|=>|\+[+=]?|-[\-=]?|\*[\*=]?|\/[\/=]?|[=!]=|<[><]?=?|>>?=?|%=?|&=?|\|=?|\^=?|\~|!|\?|(or|and|\|\||&&|\?)=)/; - var delimiters = /^(?:[()\[\]{},:`=;]|\.\.?\.?)/; - var identifiers = /^[_A-Za-z$][_A-Za-z$0-9]*/; - var atProp = /^@[_A-Za-z$][_A-Za-z$0-9]*/; - - var wordOperators = wordRegexp(["and", "or", "not", - "is", "isnt", "in", - "instanceof", "typeof"]); - var indentKeywords = ["for", "while", "loop", "if", "unless", "else", - "switch", "try", "catch", "finally", "class"]; - var commonKeywords = ["break", "by", "continue", "debugger", "delete", - "do", "in", "of", "new", "return", "then", - "this", "@", "throw", "when", "until", "extends"]; - - var keywords = wordRegexp(indentKeywords.concat(commonKeywords)); - - indentKeywords = wordRegexp(indentKeywords); - - - var stringPrefixes = /^('{3}|\"{3}|['\"])/; - var regexPrefixes = /^(\/{3}|\/)/; - var commonConstants = ["Infinity", "NaN", "undefined", "null", "true", "false", "on", "off", "yes", "no"]; - var constants = wordRegexp(commonConstants); - - // Tokenizers - function tokenBase(stream, state) { - // Handle scope changes - if (stream.sol()) { - if (state.scope.align === null) state.scope.align = false; - var scopeOffset = state.scope.offset; - if (stream.eatSpace()) { - var lineOffset = stream.indentation(); - if (lineOffset > scopeOffset && state.scope.type == "coffee") { - return "indent"; - } else if (lineOffset < scopeOffset) { - return "dedent"; - } - return null; - } else { - if (scopeOffset > 0) { - dedent(stream, state); - } - } - } - if (stream.eatSpace()) { - return null; - } - - var ch = stream.peek(); - - // Handle docco title comment (single line) - if (stream.match("####")) { - stream.skipToEnd(); - return "comment"; - } - - // Handle multi line comments - if (stream.match("###")) { - state.tokenize = longComment; - return state.tokenize(stream, state); - } - - // Single line comment - if (ch === "#") { - stream.skipToEnd(); - return "comment"; - } - - // Handle number literals - if (stream.match(/^-?[0-9\.]/, false)) { - var floatLiteral = false; - // Floats - if (stream.match(/^-?\d*\.\d+(e[\+\-]?\d+)?/i)) { - floatLiteral = true; - } - if (stream.match(/^-?\d+\.\d*/)) { - floatLiteral = true; - } - if (stream.match(/^-?\.\d+/)) { - floatLiteral = true; - } - - if (floatLiteral) { - // prevent from getting extra . on 1.. - if (stream.peek() == "."){ - stream.backUp(1); - } - return "number"; - } - // Integers - var intLiteral = false; - // Hex - if (stream.match(/^-?0x[0-9a-f]+/i)) { - intLiteral = true; - } - // Decimal - if (stream.match(/^-?[1-9]\d*(e[\+\-]?\d+)?/)) { - intLiteral = true; - } - // Zero by itself with no other piece of number. - if (stream.match(/^-?0(?![\dx])/i)) { - intLiteral = true; - } - if (intLiteral) { - return "number"; - } - } - - // Handle strings - if (stream.match(stringPrefixes)) { - state.tokenize = tokenFactory(stream.current(), false, "string"); - return state.tokenize(stream, state); - } - // Handle regex literals - if (stream.match(regexPrefixes)) { - if (stream.current() != "/" || stream.match(/^.*\//, false)) { // prevent highlight of division - state.tokenize = tokenFactory(stream.current(), true, "string-2"); - return state.tokenize(stream, state); - } else { - stream.backUp(1); - } - } - - - - // Handle operators and delimiters - if (stream.match(operators) || stream.match(wordOperators)) { - return "operator"; - } - if (stream.match(delimiters)) { - return "punctuation"; - } - - if (stream.match(constants)) { - return "atom"; - } - - if (stream.match(atProp) || state.prop && stream.match(identifiers)) { - return "property"; - } - - if (stream.match(keywords)) { - return "keyword"; - } - - if (stream.match(identifiers)) { - return "variable"; - } - - // Handle non-detected items - stream.next(); - return ERRORCLASS; - } - - function tokenFactory(delimiter, singleline, outclass) { - return function(stream, state) { - while (!stream.eol()) { - stream.eatWhile(/[^'"\/\\]/); - if (stream.eat("\\")) { - stream.next(); - if (singleline && stream.eol()) { - return outclass; - } - } else if (stream.match(delimiter)) { - state.tokenize = tokenBase; - return outclass; - } else { - stream.eat(/['"\/]/); - } - } - if (singleline) { - if (parserConf.singleLineStringErrors) { - outclass = ERRORCLASS; - } else { - state.tokenize = tokenBase; - } - } - return outclass; - }; - } - - function longComment(stream, state) { - while (!stream.eol()) { - stream.eatWhile(/[^#]/); - if (stream.match("###")) { - state.tokenize = tokenBase; - break; - } - stream.eatWhile("#"); - } - return "comment"; - } - - function indent(stream, state, type) { - type = type || "coffee"; - var offset = 0, align = false, alignOffset = null; - for (var scope = state.scope; scope; scope = scope.prev) { - if (scope.type === "coffee" || scope.type == "}") { - offset = scope.offset + conf.indentUnit; - break; - } - } - if (type !== "coffee") { - align = null; - alignOffset = stream.column() + stream.current().length; - } else if (state.scope.align) { - state.scope.align = false; - } - state.scope = { - offset: offset, - type: type, - prev: state.scope, - align: align, - alignOffset: alignOffset - }; - } - - function dedent(stream, state) { - if (!state.scope.prev) return; - if (state.scope.type === "coffee") { - var _indent = stream.indentation(); - var matched = false; - for (var scope = state.scope; scope; scope = scope.prev) { - if (_indent === scope.offset) { - matched = true; - break; - } - } - if (!matched) { - return true; - } - while (state.scope.prev && state.scope.offset !== _indent) { - state.scope = state.scope.prev; - } - return false; - } else { - state.scope = state.scope.prev; - return false; - } - } - - function tokenLexer(stream, state) { - var style = state.tokenize(stream, state); - var current = stream.current(); - - // Handle scope changes. - if (current === "return") { - state.dedent = true; - } - if (((current === "->" || current === "=>") && stream.eol()) - || style === "indent") { - indent(stream, state); - } - var delimiter_index = "[({".indexOf(current); - if (delimiter_index !== -1) { - indent(stream, state, "])}".slice(delimiter_index, delimiter_index+1)); - } - if (indentKeywords.exec(current)){ - indent(stream, state); - } - if (current == "then"){ - dedent(stream, state); - } - - - if (style === "dedent") { - if (dedent(stream, state)) { - return ERRORCLASS; - } - } - delimiter_index = "])}".indexOf(current); - if (delimiter_index !== -1) { - while (state.scope.type == "coffee" && state.scope.prev) - state.scope = state.scope.prev; - if (state.scope.type == current) - state.scope = state.scope.prev; - } - if (state.dedent && stream.eol()) { - if (state.scope.type == "coffee" && state.scope.prev) - state.scope = state.scope.prev; - state.dedent = false; - } - - return style; - } - - var external = { - startState: function(basecolumn) { - return { - tokenize: tokenBase, - scope: {offset:basecolumn || 0, type:"coffee", prev: null, align: false}, - prop: false, - dedent: 0 - }; - }, - - token: function(stream, state) { - var fillAlign = state.scope.align === null && state.scope; - if (fillAlign && stream.sol()) fillAlign.align = false; - - var style = tokenLexer(stream, state); - if (style && style != "comment") { - if (fillAlign) fillAlign.align = true; - state.prop = style == "punctuation" && stream.current() == "." - } - - return style; - }, - - indent: function(state, text) { - if (state.tokenize != tokenBase) return 0; - var scope = state.scope; - var closer = text && "])}".indexOf(text.charAt(0)) > -1; - if (closer) while (scope.type == "coffee" && scope.prev) scope = scope.prev; - var closes = closer && scope.type === text.charAt(0); - if (scope.align) - return scope.alignOffset - (closes ? 1 : 0); - else - return (closes ? scope.prev : scope).offset; - }, - - lineComment: "#", - fold: "indent" - }; - return external; -}); - -// IANA registered media type -// https://www.iana.org/assignments/media-types/ -CodeMirror.defineMIME("application/vnd.coffeescript", "coffeescript"); - -CodeMirror.defineMIME("text/x-coffeescript", "coffeescript"); -CodeMirror.defineMIME("text/coffeescript", "coffeescript"); - -}); - - -/* ---- mode/css.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -CodeMirror.defineMode("css", function(config, parserConfig) { - var inline = parserConfig.inline - if (!parserConfig.propertyKeywords) parserConfig = CodeMirror.resolveMode("text/css"); - - var indentUnit = config.indentUnit, - tokenHooks = parserConfig.tokenHooks, - documentTypes = parserConfig.documentTypes || {}, - mediaTypes = parserConfig.mediaTypes || {}, - mediaFeatures = parserConfig.mediaFeatures || {}, - mediaValueKeywords = parserConfig.mediaValueKeywords || {}, - propertyKeywords = parserConfig.propertyKeywords || {}, - nonStandardPropertyKeywords = parserConfig.nonStandardPropertyKeywords || {}, - fontProperties = parserConfig.fontProperties || {}, - counterDescriptors = parserConfig.counterDescriptors || {}, - colorKeywords = parserConfig.colorKeywords || {}, - valueKeywords = parserConfig.valueKeywords || {}, - allowNested = parserConfig.allowNested, - lineComment = parserConfig.lineComment, - supportsAtComponent = parserConfig.supportsAtComponent === true; - - var type, override; - function ret(style, tp) { type = tp; return style; } - - // Tokenizers - - function tokenBase(stream, state) { - var ch = stream.next(); - if (tokenHooks[ch]) { - var result = tokenHooks[ch](stream, state); - if (result !== false) return result; - } - if (ch == "@") { - stream.eatWhile(/[\w\\\-]/); - return ret("def", stream.current()); - } else if (ch == "=" || (ch == "~" || ch == "|") && stream.eat("=")) { - return ret(null, "compare"); - } else if (ch == "\"" || ch == "'") { - state.tokenize = tokenString(ch); - return state.tokenize(stream, state); - } else if (ch == "#") { - stream.eatWhile(/[\w\\\-]/); - return ret("atom", "hash"); - } else if (ch == "!") { - stream.match(/^\s*\w*/); - return ret("keyword", "important"); - } else if (/\d/.test(ch) || ch == "." && stream.eat(/\d/)) { - stream.eatWhile(/[\w.%]/); - return ret("number", "unit"); - } else if (ch === "-") { - if (/[\d.]/.test(stream.peek())) { - stream.eatWhile(/[\w.%]/); - return ret("number", "unit"); - } else if (stream.match(/^-[\w\\\-]*/)) { - stream.eatWhile(/[\w\\\-]/); - if (stream.match(/^\s*:/, false)) - return ret("variable-2", "variable-definition"); - return ret("variable-2", "variable"); - } else if (stream.match(/^\w+-/)) { - return ret("meta", "meta"); - } - } else if (/[,+>*\/]/.test(ch)) { - return ret(null, "select-op"); - } else if (ch == "." && stream.match(/^-?[_a-z][_a-z0-9-]*/i)) { - return ret("qualifier", "qualifier"); - } else if (/[:;{}\[\]\(\)]/.test(ch)) { - return ret(null, ch); - } else if (stream.match(/[\w-.]+(?=\()/)) { - if (/^(url(-prefix)?|domain|regexp)$/.test(stream.current().toLowerCase())) { - state.tokenize = tokenParenthesized; - } - return ret("variable callee", "variable"); - } else if (/[\w\\\-]/.test(ch)) { - stream.eatWhile(/[\w\\\-]/); - return ret("property", "word"); - } else { - return ret(null, null); - } - } - - function tokenString(quote) { - return function(stream, state) { - var escaped = false, ch; - while ((ch = stream.next()) != null) { - if (ch == quote && !escaped) { - if (quote == ")") stream.backUp(1); - break; - } - escaped = !escaped && ch == "\\"; - } - if (ch == quote || !escaped && quote != ")") state.tokenize = null; - return ret("string", "string"); - }; - } - - function tokenParenthesized(stream, state) { - stream.next(); // Must be '(' - if (!stream.match(/\s*[\"\')]/, false)) - state.tokenize = tokenString(")"); - else - state.tokenize = null; - return ret(null, "("); - } - - // Context management - - function Context(type, indent, prev) { - this.type = type; - this.indent = indent; - this.prev = prev; - } - - function pushContext(state, stream, type, indent) { - state.context = new Context(type, stream.indentation() + (indent === false ? 0 : indentUnit), state.context); - return type; - } - - function popContext(state) { - if (state.context.prev) - state.context = state.context.prev; - return state.context.type; - } - - function pass(type, stream, state) { - return states[state.context.type](type, stream, state); - } - function popAndPass(type, stream, state, n) { - for (var i = n || 1; i > 0; i--) - state.context = state.context.prev; - return pass(type, stream, state); - } - - // Parser - - function wordAsValue(stream) { - var word = stream.current().toLowerCase(); - if (valueKeywords.hasOwnProperty(word)) - override = "atom"; - else if (colorKeywords.hasOwnProperty(word)) - override = "keyword"; - else - override = "variable"; - } - - var states = {}; - - states.top = function(type, stream, state) { - if (type == "{") { - return pushContext(state, stream, "block"); - } else if (type == "}" && state.context.prev) { - return popContext(state); - } else if (supportsAtComponent && /@component/i.test(type)) { - return pushContext(state, stream, "atComponentBlock"); - } else if (/^@(-moz-)?document$/i.test(type)) { - return pushContext(state, stream, "documentTypes"); - } else if (/^@(media|supports|(-moz-)?document|import)$/i.test(type)) { - return pushContext(state, stream, "atBlock"); - } else if (/^@(font-face|counter-style)/i.test(type)) { - state.stateArg = type; - return "restricted_atBlock_before"; - } else if (/^@(-(moz|ms|o|webkit)-)?keyframes$/i.test(type)) { - return "keyframes"; - } else if (type && type.charAt(0) == "@") { - return pushContext(state, stream, "at"); - } else if (type == "hash") { - override = "builtin"; - } else if (type == "word") { - override = "tag"; - } else if (type == "variable-definition") { - return "maybeprop"; - } else if (type == "interpolation") { - return pushContext(state, stream, "interpolation"); - } else if (type == ":") { - return "pseudo"; - } else if (allowNested && type == "(") { - return pushContext(state, stream, "parens"); - } - return state.context.type; - }; - - states.block = function(type, stream, state) { - if (type == "word") { - var word = stream.current().toLowerCase(); - if (propertyKeywords.hasOwnProperty(word)) { - override = "property"; - return "maybeprop"; - } else if (nonStandardPropertyKeywords.hasOwnProperty(word)) { - override = "string-2"; - return "maybeprop"; - } else if (allowNested) { - override = stream.match(/^\s*:(?:\s|$)/, false) ? "property" : "tag"; - return "block"; - } else { - override += " error"; - return "maybeprop"; - } - } else if (type == "meta") { - return "block"; - } else if (!allowNested && (type == "hash" || type == "qualifier")) { - override = "error"; - return "block"; - } else { - return states.top(type, stream, state); - } - }; - - states.maybeprop = function(type, stream, state) { - if (type == ":") return pushContext(state, stream, "prop"); - return pass(type, stream, state); - }; - - states.prop = function(type, stream, state) { - if (type == ";") return popContext(state); - if (type == "{" && allowNested) return pushContext(state, stream, "propBlock"); - if (type == "}" || type == "{") return popAndPass(type, stream, state); - if (type == "(") return pushContext(state, stream, "parens"); - - if (type == "hash" && !/^#([0-9a-fA-f]{3,4}|[0-9a-fA-f]{6}|[0-9a-fA-f]{8})$/.test(stream.current())) { - override += " error"; - } else if (type == "word") { - wordAsValue(stream); - } else if (type == "interpolation") { - return pushContext(state, stream, "interpolation"); - } - return "prop"; - }; - - states.propBlock = function(type, _stream, state) { - if (type == "}") return popContext(state); - if (type == "word") { override = "property"; return "maybeprop"; } - return state.context.type; - }; - - states.parens = function(type, stream, state) { - if (type == "{" || type == "}") return popAndPass(type, stream, state); - if (type == ")") return popContext(state); - if (type == "(") return pushContext(state, stream, "parens"); - if (type == "interpolation") return pushContext(state, stream, "interpolation"); - if (type == "word") wordAsValue(stream); - return "parens"; - }; - - states.pseudo = function(type, stream, state) { - if (type == "meta") return "pseudo"; - - if (type == "word") { - override = "variable-3"; - return state.context.type; - } - return pass(type, stream, state); - }; - - states.documentTypes = function(type, stream, state) { - if (type == "word" && documentTypes.hasOwnProperty(stream.current())) { - override = "tag"; - return state.context.type; - } else { - return states.atBlock(type, stream, state); - } - }; - - states.atBlock = function(type, stream, state) { - if (type == "(") return pushContext(state, stream, "atBlock_parens"); - if (type == "}" || type == ";") return popAndPass(type, stream, state); - if (type == "{") return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top"); - - if (type == "interpolation") return pushContext(state, stream, "interpolation"); - - if (type == "word") { - var word = stream.current().toLowerCase(); - if (word == "only" || word == "not" || word == "and" || word == "or") - override = "keyword"; - else if (mediaTypes.hasOwnProperty(word)) - override = "attribute"; - else if (mediaFeatures.hasOwnProperty(word)) - override = "property"; - else if (mediaValueKeywords.hasOwnProperty(word)) - override = "keyword"; - else if (propertyKeywords.hasOwnProperty(word)) - override = "property"; - else if (nonStandardPropertyKeywords.hasOwnProperty(word)) - override = "string-2"; - else if (valueKeywords.hasOwnProperty(word)) - override = "atom"; - else if (colorKeywords.hasOwnProperty(word)) - override = "keyword"; - else - override = "error"; - } - return state.context.type; - }; - - states.atComponentBlock = function(type, stream, state) { - if (type == "}") - return popAndPass(type, stream, state); - if (type == "{") - return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top", false); - if (type == "word") - override = "error"; - return state.context.type; - }; - - states.atBlock_parens = function(type, stream, state) { - if (type == ")") return popContext(state); - if (type == "{" || type == "}") return popAndPass(type, stream, state, 2); - return states.atBlock(type, stream, state); - }; - - states.restricted_atBlock_before = function(type, stream, state) { - if (type == "{") - return pushContext(state, stream, "restricted_atBlock"); - if (type == "word" && state.stateArg == "@counter-style") { - override = "variable"; - return "restricted_atBlock_before"; - } - return pass(type, stream, state); - }; - - states.restricted_atBlock = function(type, stream, state) { - if (type == "}") { - state.stateArg = null; - return popContext(state); - } - if (type == "word") { - if ((state.stateArg == "@font-face" && !fontProperties.hasOwnProperty(stream.current().toLowerCase())) || - (state.stateArg == "@counter-style" && !counterDescriptors.hasOwnProperty(stream.current().toLowerCase()))) - override = "error"; - else - override = "property"; - return "maybeprop"; - } - return "restricted_atBlock"; - }; - - states.keyframes = function(type, stream, state) { - if (type == "word") { override = "variable"; return "keyframes"; } - if (type == "{") return pushContext(state, stream, "top"); - return pass(type, stream, state); - }; - - states.at = function(type, stream, state) { - if (type == ";") return popContext(state); - if (type == "{" || type == "}") return popAndPass(type, stream, state); - if (type == "word") override = "tag"; - else if (type == "hash") override = "builtin"; - return "at"; - }; - - states.interpolation = function(type, stream, state) { - if (type == "}") return popContext(state); - if (type == "{" || type == ";") return popAndPass(type, stream, state); - if (type == "word") override = "variable"; - else if (type != "variable" && type != "(" && type != ")") override = "error"; - return "interpolation"; - }; - - return { - startState: function(base) { - return {tokenize: null, - state: inline ? "block" : "top", - stateArg: null, - context: new Context(inline ? "block" : "top", base || 0, null)}; - }, - - token: function(stream, state) { - if (!state.tokenize && stream.eatSpace()) return null; - var style = (state.tokenize || tokenBase)(stream, state); - if (style && typeof style == "object") { - type = style[1]; - style = style[0]; - } - override = style; - if (type != "comment") - state.state = states[state.state](type, stream, state); - return override; - }, - - indent: function(state, textAfter) { - var cx = state.context, ch = textAfter && textAfter.charAt(0); - var indent = cx.indent; - if (cx.type == "prop" && (ch == "}" || ch == ")")) cx = cx.prev; - if (cx.prev) { - if (ch == "}" && (cx.type == "block" || cx.type == "top" || - cx.type == "interpolation" || cx.type == "restricted_atBlock")) { - // Resume indentation from parent context. - cx = cx.prev; - indent = cx.indent; - } else if (ch == ")" && (cx.type == "parens" || cx.type == "atBlock_parens") || - ch == "{" && (cx.type == "at" || cx.type == "atBlock")) { - // Dedent relative to current context. - indent = Math.max(0, cx.indent - indentUnit); - } - } - return indent; - }, - - electricChars: "}", - blockCommentStart: "/*", - blockCommentEnd: "*/", - blockCommentContinue: " * ", - lineComment: lineComment, - fold: "brace" - }; -}); - - function keySet(array) { - var keys = {}; - for (var i = 0; i < array.length; ++i) { - keys[array[i].toLowerCase()] = true; - } - return keys; - } - - var documentTypes_ = [ - "domain", "regexp", "url", "url-prefix" - ], documentTypes = keySet(documentTypes_); - - var mediaTypes_ = [ - "all", "aural", "braille", "handheld", "print", "projection", "screen", - "tty", "tv", "embossed" - ], mediaTypes = keySet(mediaTypes_); - - var mediaFeatures_ = [ - "width", "min-width", "max-width", "height", "min-height", "max-height", - "device-width", "min-device-width", "max-device-width", "device-height", - "min-device-height", "max-device-height", "aspect-ratio", - "min-aspect-ratio", "max-aspect-ratio", "device-aspect-ratio", - "min-device-aspect-ratio", "max-device-aspect-ratio", "color", "min-color", - "max-color", "color-index", "min-color-index", "max-color-index", - "monochrome", "min-monochrome", "max-monochrome", "resolution", - "min-resolution", "max-resolution", "scan", "grid", "orientation", - "device-pixel-ratio", "min-device-pixel-ratio", "max-device-pixel-ratio", - "pointer", "any-pointer", "hover", "any-hover" - ], mediaFeatures = keySet(mediaFeatures_); - - var mediaValueKeywords_ = [ - "landscape", "portrait", "none", "coarse", "fine", "on-demand", "hover", - "interlace", "progressive" - ], mediaValueKeywords = keySet(mediaValueKeywords_); - - var propertyKeywords_ = [ - "align-content", "align-items", "align-self", "alignment-adjust", - "alignment-baseline", "anchor-point", "animation", "animation-delay", - "animation-direction", "animation-duration", "animation-fill-mode", - "animation-iteration-count", "animation-name", "animation-play-state", - "animation-timing-function", "appearance", "azimuth", "backdrop-filter", - "backface-visibility", "background", "background-attachment", - "background-blend-mode", "background-clip", "background-color", - "background-image", "background-origin", "background-position", - "background-position-x", "background-position-y", "background-repeat", - "background-size", "baseline-shift", "binding", "bleed", "block-size", - "bookmark-label", "bookmark-level", "bookmark-state", "bookmark-target", - "border", "border-bottom", "border-bottom-color", "border-bottom-left-radius", - "border-bottom-right-radius", "border-bottom-style", "border-bottom-width", - "border-collapse", "border-color", "border-image", "border-image-outset", - "border-image-repeat", "border-image-slice", "border-image-source", - "border-image-width", "border-left", "border-left-color", "border-left-style", - "border-left-width", "border-radius", "border-right", "border-right-color", - "border-right-style", "border-right-width", "border-spacing", "border-style", - "border-top", "border-top-color", "border-top-left-radius", - "border-top-right-radius", "border-top-style", "border-top-width", - "border-width", "bottom", "box-decoration-break", "box-shadow", "box-sizing", - "break-after", "break-before", "break-inside", "caption-side", "caret-color", - "clear", "clip", "color", "color-profile", "column-count", "column-fill", - "column-gap", "column-rule", "column-rule-color", "column-rule-style", - "column-rule-width", "column-span", "column-width", "columns", "contain", - "content", "counter-increment", "counter-reset", "crop", "cue", "cue-after", - "cue-before", "cursor", "direction", "display", "dominant-baseline", - "drop-initial-after-adjust", "drop-initial-after-align", - "drop-initial-before-adjust", "drop-initial-before-align", "drop-initial-size", - "drop-initial-value", "elevation", "empty-cells", "fit", "fit-position", - "flex", "flex-basis", "flex-direction", "flex-flow", "flex-grow", - "flex-shrink", "flex-wrap", "float", "float-offset", "flow-from", "flow-into", - "font", "font-family", "font-feature-settings", "font-kerning", - "font-language-override", "font-optical-sizing", "font-size", - "font-size-adjust", "font-stretch", "font-style", "font-synthesis", - "font-variant", "font-variant-alternates", "font-variant-caps", - "font-variant-east-asian", "font-variant-ligatures", "font-variant-numeric", - "font-variant-position", "font-variation-settings", "font-weight", "gap", - "grid", "grid-area", "grid-auto-columns", "grid-auto-flow", "grid-auto-rows", - "grid-column", "grid-column-end", "grid-column-gap", "grid-column-start", - "grid-gap", "grid-row", "grid-row-end", "grid-row-gap", "grid-row-start", - "grid-template", "grid-template-areas", "grid-template-columns", - "grid-template-rows", "hanging-punctuation", "height", "hyphens", "icon", - "image-orientation", "image-rendering", "image-resolution", "inline-box-align", - "inset", "inset-block", "inset-block-end", "inset-block-start", "inset-inline", - "inset-inline-end", "inset-inline-start", "isolation", "justify-content", - "justify-items", "justify-self", "left", "letter-spacing", "line-break", - "line-height", "line-height-step", "line-stacking", "line-stacking-ruby", - "line-stacking-shift", "line-stacking-strategy", "list-style", - "list-style-image", "list-style-position", "list-style-type", "margin", - "margin-bottom", "margin-left", "margin-right", "margin-top", "marks", - "marquee-direction", "marquee-loop", "marquee-play-count", "marquee-speed", - "marquee-style", "max-block-size", "max-height", "max-inline-size", - "max-width", "min-block-size", "min-height", "min-inline-size", "min-width", - "mix-blend-mode", "move-to", "nav-down", "nav-index", "nav-left", "nav-right", - "nav-up", "object-fit", "object-position", "offset", "offset-anchor", - "offset-distance", "offset-path", "offset-position", "offset-rotate", - "opacity", "order", "orphans", "outline", "outline-color", "outline-offset", - "outline-style", "outline-width", "overflow", "overflow-style", - "overflow-wrap", "overflow-x", "overflow-y", "padding", "padding-bottom", - "padding-left", "padding-right", "padding-top", "page", "page-break-after", - "page-break-before", "page-break-inside", "page-policy", "pause", - "pause-after", "pause-before", "perspective", "perspective-origin", "pitch", - "pitch-range", "place-content", "place-items", "place-self", "play-during", - "position", "presentation-level", "punctuation-trim", "quotes", - "region-break-after", "region-break-before", "region-break-inside", - "region-fragment", "rendering-intent", "resize", "rest", "rest-after", - "rest-before", "richness", "right", "rotate", "rotation", "rotation-point", - "row-gap", "ruby-align", "ruby-overhang", "ruby-position", "ruby-span", - "scale", "scroll-behavior", "scroll-margin", "scroll-margin-block", - "scroll-margin-block-end", "scroll-margin-block-start", "scroll-margin-bottom", - "scroll-margin-inline", "scroll-margin-inline-end", - "scroll-margin-inline-start", "scroll-margin-left", "scroll-margin-right", - "scroll-margin-top", "scroll-padding", "scroll-padding-block", - "scroll-padding-block-end", "scroll-padding-block-start", - "scroll-padding-bottom", "scroll-padding-inline", "scroll-padding-inline-end", - "scroll-padding-inline-start", "scroll-padding-left", "scroll-padding-right", - "scroll-padding-top", "scroll-snap-align", "scroll-snap-type", - "shape-image-threshold", "shape-inside", "shape-margin", "shape-outside", - "size", "speak", "speak-as", "speak-header", "speak-numeral", - "speak-punctuation", "speech-rate", "stress", "string-set", "tab-size", - "table-layout", "target", "target-name", "target-new", "target-position", - "text-align", "text-align-last", "text-combine-upright", "text-decoration", - "text-decoration-color", "text-decoration-line", "text-decoration-skip", - "text-decoration-skip-ink", "text-decoration-style", "text-emphasis", - "text-emphasis-color", "text-emphasis-position", "text-emphasis-style", - "text-height", "text-indent", "text-justify", "text-orientation", - "text-outline", "text-overflow", "text-rendering", "text-shadow", - "text-size-adjust", "text-space-collapse", "text-transform", - "text-underline-position", "text-wrap", "top", "transform", "transform-origin", - "transform-style", "transition", "transition-delay", "transition-duration", - "transition-property", "transition-timing-function", "translate", - "unicode-bidi", "user-select", "vertical-align", "visibility", "voice-balance", - "voice-duration", "voice-family", "voice-pitch", "voice-range", "voice-rate", - "voice-stress", "voice-volume", "volume", "white-space", "widows", "width", - "will-change", "word-break", "word-spacing", "word-wrap", "writing-mode", "z-index", - // SVG-specific - "clip-path", "clip-rule", "mask", "enable-background", "filter", "flood-color", - "flood-opacity", "lighting-color", "stop-color", "stop-opacity", "pointer-events", - "color-interpolation", "color-interpolation-filters", - "color-rendering", "fill", "fill-opacity", "fill-rule", "image-rendering", - "marker", "marker-end", "marker-mid", "marker-start", "shape-rendering", "stroke", - "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", - "stroke-miterlimit", "stroke-opacity", "stroke-width", "text-rendering", - "baseline-shift", "dominant-baseline", "glyph-orientation-horizontal", - "glyph-orientation-vertical", "text-anchor", "writing-mode" - ], propertyKeywords = keySet(propertyKeywords_); - - var nonStandardPropertyKeywords_ = [ - "border-block", "border-block-color", "border-block-end", - "border-block-end-color", "border-block-end-style", "border-block-end-width", - "border-block-start", "border-block-start-color", "border-block-start-style", - "border-block-start-width", "border-block-style", "border-block-width", - "border-inline", "border-inline-color", "border-inline-end", - "border-inline-end-color", "border-inline-end-style", - "border-inline-end-width", "border-inline-start", "border-inline-start-color", - "border-inline-start-style", "border-inline-start-width", - "border-inline-style", "border-inline-width", "margin-block", - "margin-block-end", "margin-block-start", "margin-inline", "margin-inline-end", - "margin-inline-start", "padding-block", "padding-block-end", - "padding-block-start", "padding-inline", "padding-inline-end", - "padding-inline-start", "scroll-snap-stop", "scrollbar-3d-light-color", - "scrollbar-arrow-color", "scrollbar-base-color", "scrollbar-dark-shadow-color", - "scrollbar-face-color", "scrollbar-highlight-color", "scrollbar-shadow-color", - "scrollbar-track-color", "searchfield-cancel-button", "searchfield-decoration", - "searchfield-results-button", "searchfield-results-decoration", "shape-inside", "zoom" - ], nonStandardPropertyKeywords = keySet(nonStandardPropertyKeywords_); - - var fontProperties_ = [ - "font-display", "font-family", "src", "unicode-range", "font-variant", - "font-feature-settings", "font-stretch", "font-weight", "font-style" - ], fontProperties = keySet(fontProperties_); - - var counterDescriptors_ = [ - "additive-symbols", "fallback", "negative", "pad", "prefix", "range", - "speak-as", "suffix", "symbols", "system" - ], counterDescriptors = keySet(counterDescriptors_); - - var colorKeywords_ = [ - "aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige", - "bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown", - "burlywood", "cadetblue", "chartreuse", "chocolate", "coral", "cornflowerblue", - "cornsilk", "crimson", "cyan", "darkblue", "darkcyan", "darkgoldenrod", - "darkgray", "darkgreen", "darkkhaki", "darkmagenta", "darkolivegreen", - "darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen", - "darkslateblue", "darkslategray", "darkturquoise", "darkviolet", - "deeppink", "deepskyblue", "dimgray", "dodgerblue", "firebrick", - "floralwhite", "forestgreen", "fuchsia", "gainsboro", "ghostwhite", - "gold", "goldenrod", "gray", "grey", "green", "greenyellow", "honeydew", - "hotpink", "indianred", "indigo", "ivory", "khaki", "lavender", - "lavenderblush", "lawngreen", "lemonchiffon", "lightblue", "lightcoral", - "lightcyan", "lightgoldenrodyellow", "lightgray", "lightgreen", "lightpink", - "lightsalmon", "lightseagreen", "lightskyblue", "lightslategray", - "lightsteelblue", "lightyellow", "lime", "limegreen", "linen", "magenta", - "maroon", "mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple", - "mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise", - "mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin", - "navajowhite", "navy", "oldlace", "olive", "olivedrab", "orange", "orangered", - "orchid", "palegoldenrod", "palegreen", "paleturquoise", "palevioletred", - "papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue", - "purple", "rebeccapurple", "red", "rosybrown", "royalblue", "saddlebrown", - "salmon", "sandybrown", "seagreen", "seashell", "sienna", "silver", "skyblue", - "slateblue", "slategray", "snow", "springgreen", "steelblue", "tan", - "teal", "thistle", "tomato", "turquoise", "violet", "wheat", "white", - "whitesmoke", "yellow", "yellowgreen" - ], colorKeywords = keySet(colorKeywords_); - - var valueKeywords_ = [ - "above", "absolute", "activeborder", "additive", "activecaption", "afar", - "after-white-space", "ahead", "alias", "all", "all-scroll", "alphabetic", "alternate", - "always", "amharic", "amharic-abegede", "antialiased", "appworkspace", - "arabic-indic", "armenian", "asterisks", "attr", "auto", "auto-flow", "avoid", "avoid-column", "avoid-page", - "avoid-region", "background", "backwards", "baseline", "below", "bidi-override", "binary", - "bengali", "blink", "block", "block-axis", "bold", "bolder", "border", "border-box", - "both", "bottom", "break", "break-all", "break-word", "bullets", "button", "button-bevel", - "buttonface", "buttonhighlight", "buttonshadow", "buttontext", "calc", "cambodian", - "capitalize", "caps-lock-indicator", "caption", "captiontext", "caret", - "cell", "center", "checkbox", "circle", "cjk-decimal", "cjk-earthly-branch", - "cjk-heavenly-stem", "cjk-ideographic", "clear", "clip", "close-quote", - "col-resize", "collapse", "color", "color-burn", "color-dodge", "column", "column-reverse", - "compact", "condensed", "contain", "content", "contents", - "content-box", "context-menu", "continuous", "copy", "counter", "counters", "cover", "crop", - "cross", "crosshair", "currentcolor", "cursive", "cyclic", "darken", "dashed", "decimal", - "decimal-leading-zero", "default", "default-button", "dense", "destination-atop", - "destination-in", "destination-out", "destination-over", "devanagari", "difference", - "disc", "discard", "disclosure-closed", "disclosure-open", "document", - "dot-dash", "dot-dot-dash", - "dotted", "double", "down", "e-resize", "ease", "ease-in", "ease-in-out", "ease-out", - "element", "ellipse", "ellipsis", "embed", "end", "ethiopic", "ethiopic-abegede", - "ethiopic-abegede-am-et", "ethiopic-abegede-gez", "ethiopic-abegede-ti-er", - "ethiopic-abegede-ti-et", "ethiopic-halehame-aa-er", - "ethiopic-halehame-aa-et", "ethiopic-halehame-am-et", - "ethiopic-halehame-gez", "ethiopic-halehame-om-et", - "ethiopic-halehame-sid-et", "ethiopic-halehame-so-et", - "ethiopic-halehame-ti-er", "ethiopic-halehame-ti-et", "ethiopic-halehame-tig", - "ethiopic-numeric", "ew-resize", "exclusion", "expanded", "extends", "extra-condensed", - "extra-expanded", "fantasy", "fast", "fill", "fixed", "flat", "flex", "flex-end", "flex-start", "footnotes", - "forwards", "from", "geometricPrecision", "georgian", "graytext", "grid", "groove", - "gujarati", "gurmukhi", "hand", "hangul", "hangul-consonant", "hard-light", "hebrew", - "help", "hidden", "hide", "higher", "highlight", "highlighttext", - "hiragana", "hiragana-iroha", "horizontal", "hsl", "hsla", "hue", "icon", "ignore", - "inactiveborder", "inactivecaption", "inactivecaptiontext", "infinite", - "infobackground", "infotext", "inherit", "initial", "inline", "inline-axis", - "inline-block", "inline-flex", "inline-grid", "inline-table", "inset", "inside", "intrinsic", "invert", - "italic", "japanese-formal", "japanese-informal", "justify", "kannada", - "katakana", "katakana-iroha", "keep-all", "khmer", - "korean-hangul-formal", "korean-hanja-formal", "korean-hanja-informal", - "landscape", "lao", "large", "larger", "left", "level", "lighter", "lighten", - "line-through", "linear", "linear-gradient", "lines", "list-item", "listbox", "listitem", - "local", "logical", "loud", "lower", "lower-alpha", "lower-armenian", - "lower-greek", "lower-hexadecimal", "lower-latin", "lower-norwegian", - "lower-roman", "lowercase", "ltr", "luminosity", "malayalam", "match", "matrix", "matrix3d", - "media-controls-background", "media-current-time-display", - "media-fullscreen-button", "media-mute-button", "media-play-button", - "media-return-to-realtime-button", "media-rewind-button", - "media-seek-back-button", "media-seek-forward-button", "media-slider", - "media-sliderthumb", "media-time-remaining-display", "media-volume-slider", - "media-volume-slider-container", "media-volume-sliderthumb", "medium", - "menu", "menulist", "menulist-button", "menulist-text", - "menulist-textfield", "menutext", "message-box", "middle", "min-intrinsic", - "mix", "mongolian", "monospace", "move", "multiple", "multiply", "myanmar", "n-resize", - "narrower", "ne-resize", "nesw-resize", "no-close-quote", "no-drop", - "no-open-quote", "no-repeat", "none", "normal", "not-allowed", "nowrap", - "ns-resize", "numbers", "numeric", "nw-resize", "nwse-resize", "oblique", "octal", "opacity", "open-quote", - "optimizeLegibility", "optimizeSpeed", "oriya", "oromo", "outset", - "outside", "outside-shape", "overlay", "overline", "padding", "padding-box", - "painted", "page", "paused", "persian", "perspective", "plus-darker", "plus-lighter", - "pointer", "polygon", "portrait", "pre", "pre-line", "pre-wrap", "preserve-3d", - "progress", "push-button", "radial-gradient", "radio", "read-only", - "read-write", "read-write-plaintext-only", "rectangle", "region", - "relative", "repeat", "repeating-linear-gradient", - "repeating-radial-gradient", "repeat-x", "repeat-y", "reset", "reverse", - "rgb", "rgba", "ridge", "right", "rotate", "rotate3d", "rotateX", "rotateY", - "rotateZ", "round", "row", "row-resize", "row-reverse", "rtl", "run-in", "running", - "s-resize", "sans-serif", "saturation", "scale", "scale3d", "scaleX", "scaleY", "scaleZ", "screen", - "scroll", "scrollbar", "scroll-position", "se-resize", "searchfield", - "searchfield-cancel-button", "searchfield-decoration", - "searchfield-results-button", "searchfield-results-decoration", "self-start", "self-end", - "semi-condensed", "semi-expanded", "separate", "serif", "show", "sidama", - "simp-chinese-formal", "simp-chinese-informal", "single", - "skew", "skewX", "skewY", "skip-white-space", "slide", "slider-horizontal", - "slider-vertical", "sliderthumb-horizontal", "sliderthumb-vertical", "slow", - "small", "small-caps", "small-caption", "smaller", "soft-light", "solid", "somali", - "source-atop", "source-in", "source-out", "source-over", "space", "space-around", "space-between", "space-evenly", "spell-out", "square", - "square-button", "start", "static", "status-bar", "stretch", "stroke", "sub", - "subpixel-antialiased", "super", "sw-resize", "symbolic", "symbols", "system-ui", "table", - "table-caption", "table-cell", "table-column", "table-column-group", - "table-footer-group", "table-header-group", "table-row", "table-row-group", - "tamil", - "telugu", "text", "text-bottom", "text-top", "textarea", "textfield", "thai", - "thick", "thin", "threeddarkshadow", "threedface", "threedhighlight", - "threedlightshadow", "threedshadow", "tibetan", "tigre", "tigrinya-er", - "tigrinya-er-abegede", "tigrinya-et", "tigrinya-et-abegede", "to", "top", - "trad-chinese-formal", "trad-chinese-informal", "transform", - "translate", "translate3d", "translateX", "translateY", "translateZ", - "transparent", "ultra-condensed", "ultra-expanded", "underline", "unset", "up", - "upper-alpha", "upper-armenian", "upper-greek", "upper-hexadecimal", - "upper-latin", "upper-norwegian", "upper-roman", "uppercase", "urdu", "url", - "var", "vertical", "vertical-text", "visible", "visibleFill", "visiblePainted", - "visibleStroke", "visual", "w-resize", "wait", "wave", "wider", - "window", "windowframe", "windowtext", "words", "wrap", "wrap-reverse", "x-large", "x-small", "xor", - "xx-large", "xx-small" - ], valueKeywords = keySet(valueKeywords_); - - var allWords = documentTypes_.concat(mediaTypes_).concat(mediaFeatures_).concat(mediaValueKeywords_) - .concat(propertyKeywords_).concat(nonStandardPropertyKeywords_).concat(colorKeywords_) - .concat(valueKeywords_); - CodeMirror.registerHelper("hintWords", "css", allWords); - - function tokenCComment(stream, state) { - var maybeEnd = false, ch; - while ((ch = stream.next()) != null) { - if (maybeEnd && ch == "/") { - state.tokenize = null; - break; - } - maybeEnd = (ch == "*"); - } - return ["comment", "comment"]; - } - - CodeMirror.defineMIME("text/css", { - documentTypes: documentTypes, - mediaTypes: mediaTypes, - mediaFeatures: mediaFeatures, - mediaValueKeywords: mediaValueKeywords, - propertyKeywords: propertyKeywords, - nonStandardPropertyKeywords: nonStandardPropertyKeywords, - fontProperties: fontProperties, - counterDescriptors: counterDescriptors, - colorKeywords: colorKeywords, - valueKeywords: valueKeywords, - tokenHooks: { - "/": function(stream, state) { - if (!stream.eat("*")) return false; - state.tokenize = tokenCComment; - return tokenCComment(stream, state); - } - }, - name: "css" - }); - - CodeMirror.defineMIME("text/x-scss", { - mediaTypes: mediaTypes, - mediaFeatures: mediaFeatures, - mediaValueKeywords: mediaValueKeywords, - propertyKeywords: propertyKeywords, - nonStandardPropertyKeywords: nonStandardPropertyKeywords, - colorKeywords: colorKeywords, - valueKeywords: valueKeywords, - fontProperties: fontProperties, - allowNested: true, - lineComment: "//", - tokenHooks: { - "/": function(stream, state) { - if (stream.eat("/")) { - stream.skipToEnd(); - return ["comment", "comment"]; - } else if (stream.eat("*")) { - state.tokenize = tokenCComment; - return tokenCComment(stream, state); - } else { - return ["operator", "operator"]; - } - }, - ":": function(stream) { - if (stream.match(/\s*\{/, false)) - return [null, null] - return false; - }, - "$": function(stream) { - stream.match(/^[\w-]+/); - if (stream.match(/^\s*:/, false)) - return ["variable-2", "variable-definition"]; - return ["variable-2", "variable"]; - }, - "#": function(stream) { - if (!stream.eat("{")) return false; - return [null, "interpolation"]; - } - }, - name: "css", - helperType: "scss" - }); - - CodeMirror.defineMIME("text/x-less", { - mediaTypes: mediaTypes, - mediaFeatures: mediaFeatures, - mediaValueKeywords: mediaValueKeywords, - propertyKeywords: propertyKeywords, - nonStandardPropertyKeywords: nonStandardPropertyKeywords, - colorKeywords: colorKeywords, - valueKeywords: valueKeywords, - fontProperties: fontProperties, - allowNested: true, - lineComment: "//", - tokenHooks: { - "/": function(stream, state) { - if (stream.eat("/")) { - stream.skipToEnd(); - return ["comment", "comment"]; - } else if (stream.eat("*")) { - state.tokenize = tokenCComment; - return tokenCComment(stream, state); - } else { - return ["operator", "operator"]; - } - }, - "@": function(stream) { - if (stream.eat("{")) return [null, "interpolation"]; - if (stream.match(/^(charset|document|font-face|import|(-(moz|ms|o|webkit)-)?keyframes|media|namespace|page|supports)\b/i, false)) return false; - stream.eatWhile(/[\w\\\-]/); - if (stream.match(/^\s*:/, false)) - return ["variable-2", "variable-definition"]; - return ["variable-2", "variable"]; - }, - "&": function() { - return ["atom", "atom"]; - } - }, - name: "css", - helperType: "less" - }); - - CodeMirror.defineMIME("text/x-gss", { - documentTypes: documentTypes, - mediaTypes: mediaTypes, - mediaFeatures: mediaFeatures, - propertyKeywords: propertyKeywords, - nonStandardPropertyKeywords: nonStandardPropertyKeywords, - fontProperties: fontProperties, - counterDescriptors: counterDescriptors, - colorKeywords: colorKeywords, - valueKeywords: valueKeywords, - supportsAtComponent: true, - tokenHooks: { - "/": function(stream, state) { - if (!stream.eat("*")) return false; - state.tokenize = tokenCComment; - return tokenCComment(stream, state); - } - }, - name: "css", - helperType: "gss" - }); - -}); - - -/* ---- mode/go.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -CodeMirror.defineMode("go", function(config) { - var indentUnit = config.indentUnit; - - var keywords = { - "break":true, "case":true, "chan":true, "const":true, "continue":true, - "default":true, "defer":true, "else":true, "fallthrough":true, "for":true, - "func":true, "go":true, "goto":true, "if":true, "import":true, - "interface":true, "map":true, "package":true, "range":true, "return":true, - "select":true, "struct":true, "switch":true, "type":true, "var":true, - "bool":true, "byte":true, "complex64":true, "complex128":true, - "float32":true, "float64":true, "int8":true, "int16":true, "int32":true, - "int64":true, "string":true, "uint8":true, "uint16":true, "uint32":true, - "uint64":true, "int":true, "uint":true, "uintptr":true, "error": true, - "rune":true - }; - - var atoms = { - "true":true, "false":true, "iota":true, "nil":true, "append":true, - "cap":true, "close":true, "complex":true, "copy":true, "delete":true, "imag":true, - "len":true, "make":true, "new":true, "panic":true, "print":true, - "println":true, "real":true, "recover":true - }; - - var isOperatorChar = /[+\-*&^%:=<>!|\/]/; - - var curPunc; - - function tokenBase(stream, state) { - var ch = stream.next(); - if (ch == '"' || ch == "'" || ch == "`") { - state.tokenize = tokenString(ch); - return state.tokenize(stream, state); - } - if (/[\d\.]/.test(ch)) { - if (ch == ".") { - stream.match(/^[0-9]+([eE][\-+]?[0-9]+)?/); - } else if (ch == "0") { - stream.match(/^[xX][0-9a-fA-F]+/) || stream.match(/^0[0-7]+/); - } else { - stream.match(/^[0-9]*\.?[0-9]*([eE][\-+]?[0-9]+)?/); - } - return "number"; - } - if (/[\[\]{}\(\),;\:\.]/.test(ch)) { - curPunc = ch; - return null; - } - if (ch == "/") { - if (stream.eat("*")) { - state.tokenize = tokenComment; - return tokenComment(stream, state); - } - if (stream.eat("/")) { - stream.skipToEnd(); - return "comment"; - } - } - if (isOperatorChar.test(ch)) { - stream.eatWhile(isOperatorChar); - return "operator"; - } - stream.eatWhile(/[\w\$_\xa1-\uffff]/); - var cur = stream.current(); - if (keywords.propertyIsEnumerable(cur)) { - if (cur == "case" || cur == "default") curPunc = "case"; - return "keyword"; - } - if (atoms.propertyIsEnumerable(cur)) return "atom"; - return "variable"; - } - - function tokenString(quote) { - return function(stream, state) { - var escaped = false, next, end = false; - while ((next = stream.next()) != null) { - if (next == quote && !escaped) {end = true; break;} - escaped = !escaped && quote != "`" && next == "\\"; - } - if (end || !(escaped || quote == "`")) - state.tokenize = tokenBase; - return "string"; - }; - } - - function tokenComment(stream, state) { - var maybeEnd = false, ch; - while (ch = stream.next()) { - if (ch == "/" && maybeEnd) { - state.tokenize = tokenBase; - break; - } - maybeEnd = (ch == "*"); - } - return "comment"; - } - - function Context(indented, column, type, align, prev) { - this.indented = indented; - this.column = column; - this.type = type; - this.align = align; - this.prev = prev; - } - function pushContext(state, col, type) { - return state.context = new Context(state.indented, col, type, null, state.context); - } - function popContext(state) { - if (!state.context.prev) return; - var t = state.context.type; - if (t == ")" || t == "]" || t == "}") - state.indented = state.context.indented; - return state.context = state.context.prev; - } - - // Interface - - return { - startState: function(basecolumn) { - return { - tokenize: null, - context: new Context((basecolumn || 0) - indentUnit, 0, "top", false), - indented: 0, - startOfLine: true - }; - }, - - token: function(stream, state) { - var ctx = state.context; - if (stream.sol()) { - if (ctx.align == null) ctx.align = false; - state.indented = stream.indentation(); - state.startOfLine = true; - if (ctx.type == "case") ctx.type = "}"; - } - if (stream.eatSpace()) return null; - curPunc = null; - var style = (state.tokenize || tokenBase)(stream, state); - if (style == "comment") return style; - if (ctx.align == null) ctx.align = true; - - if (curPunc == "{") pushContext(state, stream.column(), "}"); - else if (curPunc == "[") pushContext(state, stream.column(), "]"); - else if (curPunc == "(") pushContext(state, stream.column(), ")"); - else if (curPunc == "case") ctx.type = "case"; - else if (curPunc == "}" && ctx.type == "}") popContext(state); - else if (curPunc == ctx.type) popContext(state); - state.startOfLine = false; - return style; - }, - - indent: function(state, textAfter) { - if (state.tokenize != tokenBase && state.tokenize != null) return CodeMirror.Pass; - var ctx = state.context, firstChar = textAfter && textAfter.charAt(0); - if (ctx.type == "case" && /^(?:case|default)\b/.test(textAfter)) { - state.context.type = "}"; - return ctx.indented; - } - var closing = firstChar == ctx.type; - if (ctx.align) return ctx.column + (closing ? 0 : 1); - else return ctx.indented + (closing ? 0 : indentUnit); - }, - - electricChars: "{}):", - closeBrackets: "()[]{}''\"\"``", - fold: "brace", - blockCommentStart: "/*", - blockCommentEnd: "*/", - lineComment: "//" - }; -}); - -CodeMirror.defineMIME("text/x-go", "go"); - -}); - - -/* ---- mode/htmlembedded.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("../htmlmixed/htmlmixed"), - require("../../addon/mode/multiplex")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "../htmlmixed/htmlmixed", - "../../addon/mode/multiplex"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - CodeMirror.defineMode("htmlembedded", function(config, parserConfig) { - var closeComment = parserConfig.closeComment || "--%>" - return CodeMirror.multiplexingMode(CodeMirror.getMode(config, "htmlmixed"), { - open: parserConfig.openComment || "<%--", - close: closeComment, - delimStyle: "comment", - mode: {token: function(stream) { - stream.skipTo(closeComment) || stream.skipToEnd() - return "comment" - }} - }, { - open: parserConfig.open || parserConfig.scriptStartRegex || "<%", - close: parserConfig.close || parserConfig.scriptEndRegex || "%>", - mode: CodeMirror.getMode(config, parserConfig.scriptingModeSpec) - }); - }, "htmlmixed"); - - CodeMirror.defineMIME("application/x-ejs", {name: "htmlembedded", scriptingModeSpec:"javascript"}); - CodeMirror.defineMIME("application/x-aspx", {name: "htmlembedded", scriptingModeSpec:"text/x-csharp"}); - CodeMirror.defineMIME("application/x-jsp", {name: "htmlembedded", scriptingModeSpec:"text/x-java"}); - CodeMirror.defineMIME("application/x-erb", {name: "htmlembedded", scriptingModeSpec:"ruby"}); -}); - - -/* ---- mode/htmlmixed.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("../xml/xml"), require("../javascript/javascript"), require("../css/css")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "../xml/xml", "../javascript/javascript", "../css/css"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - var defaultTags = { - script: [ - ["lang", /(javascript|babel)/i, "javascript"], - ["type", /^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^module$|^$/i, "javascript"], - ["type", /./, "text/plain"], - [null, null, "javascript"] - ], - style: [ - ["lang", /^css$/i, "css"], - ["type", /^(text\/)?(x-)?(stylesheet|css)$/i, "css"], - ["type", /./, "text/plain"], - [null, null, "css"] - ] - }; - - function maybeBackup(stream, pat, style) { - var cur = stream.current(), close = cur.search(pat); - if (close > -1) { - stream.backUp(cur.length - close); - } else if (cur.match(/<\/?$/)) { - stream.backUp(cur.length); - if (!stream.match(pat, false)) stream.match(cur); - } - return style; - } - - var attrRegexpCache = {}; - function getAttrRegexp(attr) { - var regexp = attrRegexpCache[attr]; - if (regexp) return regexp; - return attrRegexpCache[attr] = new RegExp("\\s+" + attr + "\\s*=\\s*('|\")?([^'\"]+)('|\")?\\s*"); - } - - function getAttrValue(text, attr) { - var match = text.match(getAttrRegexp(attr)) - return match ? /^\s*(.*?)\s*$/.exec(match[2])[1] : "" - } - - function getTagRegexp(tagName, anchored) { - return new RegExp((anchored ? "^" : "") + "<\/\s*" + tagName + "\s*>", "i"); - } - - function addTags(from, to) { - for (var tag in from) { - var dest = to[tag] || (to[tag] = []); - var source = from[tag]; - for (var i = source.length - 1; i >= 0; i--) - dest.unshift(source[i]) - } - } - - function findMatchingMode(tagInfo, tagText) { - for (var i = 0; i < tagInfo.length; i++) { - var spec = tagInfo[i]; - if (!spec[0] || spec[1].test(getAttrValue(tagText, spec[0]))) return spec[2]; - } - } - - CodeMirror.defineMode("htmlmixed", function (config, parserConfig) { - var htmlMode = CodeMirror.getMode(config, { - name: "xml", - htmlMode: true, - multilineTagIndentFactor: parserConfig.multilineTagIndentFactor, - multilineTagIndentPastTag: parserConfig.multilineTagIndentPastTag - }); - - var tags = {}; - var configTags = parserConfig && parserConfig.tags, configScript = parserConfig && parserConfig.scriptTypes; - addTags(defaultTags, tags); - if (configTags) addTags(configTags, tags); - if (configScript) for (var i = configScript.length - 1; i >= 0; i--) - tags.script.unshift(["type", configScript[i].matches, configScript[i].mode]) - - function html(stream, state) { - var style = htmlMode.token(stream, state.htmlState), tag = /\btag\b/.test(style), tagName - if (tag && !/[<>\s\/]/.test(stream.current()) && - (tagName = state.htmlState.tagName && state.htmlState.tagName.toLowerCase()) && - tags.hasOwnProperty(tagName)) { - state.inTag = tagName + " " - } else if (state.inTag && tag && />$/.test(stream.current())) { - var inTag = /^([\S]+) (.*)/.exec(state.inTag) - state.inTag = null - var modeSpec = stream.current() == ">" && findMatchingMode(tags[inTag[1]], inTag[2]) - var mode = CodeMirror.getMode(config, modeSpec) - var endTagA = getTagRegexp(inTag[1], true), endTag = getTagRegexp(inTag[1], false); - state.token = function (stream, state) { - if (stream.match(endTagA, false)) { - state.token = html; - state.localState = state.localMode = null; - return null; - } - return maybeBackup(stream, endTag, state.localMode.token(stream, state.localState)); - }; - state.localMode = mode; - state.localState = CodeMirror.startState(mode, htmlMode.indent(state.htmlState, "", "")); - } else if (state.inTag) { - state.inTag += stream.current() - if (stream.eol()) state.inTag += " " - } - return style; - }; - - return { - startState: function () { - var state = CodeMirror.startState(htmlMode); - return {token: html, inTag: null, localMode: null, localState: null, htmlState: state}; - }, - - copyState: function (state) { - var local; - if (state.localState) { - local = CodeMirror.copyState(state.localMode, state.localState); - } - return {token: state.token, inTag: state.inTag, - localMode: state.localMode, localState: local, - htmlState: CodeMirror.copyState(htmlMode, state.htmlState)}; - }, - - token: function (stream, state) { - return state.token(stream, state); - }, - - indent: function (state, textAfter, line) { - if (!state.localMode || /^\s*<\//.test(textAfter)) - return htmlMode.indent(state.htmlState, textAfter, line); - else if (state.localMode.indent) - return state.localMode.indent(state.localState, textAfter, line); - else - return CodeMirror.Pass; - }, - - innerMode: function (state) { - return {state: state.localState || state.htmlState, mode: state.localMode || htmlMode}; - } - }; - }, "xml", "javascript", "css"); - - CodeMirror.defineMIME("text/html", "htmlmixed"); -}); - - -/* ---- mode/javascript.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -CodeMirror.defineMode("javascript", function(config, parserConfig) { - var indentUnit = config.indentUnit; - var statementIndent = parserConfig.statementIndent; - var jsonldMode = parserConfig.jsonld; - var jsonMode = parserConfig.json || jsonldMode; - var isTS = parserConfig.typescript; - var wordRE = parserConfig.wordCharacters || /[\w$\xa1-\uffff]/; - - // Tokenizer - - var keywords = function(){ - function kw(type) {return {type: type, style: "keyword"};} - var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"), D = kw("keyword d"); - var operator = kw("operator"), atom = {type: "atom", style: "atom"}; - - return { - "if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B, - "return": D, "break": D, "continue": D, "new": kw("new"), "delete": C, "void": C, "throw": C, - "debugger": kw("debugger"), "var": kw("var"), "const": kw("var"), "let": kw("var"), - "function": kw("function"), "catch": kw("catch"), - "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"), - "in": operator, "typeof": operator, "instanceof": operator, - "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom, - "this": kw("this"), "class": kw("class"), "super": kw("atom"), - "yield": C, "export": kw("export"), "import": kw("import"), "extends": C, - "await": C - }; - }(); - - var isOperatorChar = /[+\-*&%=<>!?|~^@]/; - var isJsonldKeyword = /^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/; - - function readRegexp(stream) { - var escaped = false, next, inSet = false; - while ((next = stream.next()) != null) { - if (!escaped) { - if (next == "/" && !inSet) return; - if (next == "[") inSet = true; - else if (inSet && next == "]") inSet = false; - } - escaped = !escaped && next == "\\"; - } - } - - // Used as scratch variables to communicate multiple values without - // consing up tons of objects. - var type, content; - function ret(tp, style, cont) { - type = tp; content = cont; - return style; - } - function tokenBase(stream, state) { - var ch = stream.next(); - if (ch == '"' || ch == "'") { - state.tokenize = tokenString(ch); - return state.tokenize(stream, state); - } else if (ch == "." && stream.match(/^\d[\d_]*(?:[eE][+\-]?[\d_]+)?/)) { - return ret("number", "number"); - } else if (ch == "." && stream.match("..")) { - return ret("spread", "meta"); - } else if (/[\[\]{}\(\),;\:\.]/.test(ch)) { - return ret(ch); - } else if (ch == "=" && stream.eat(">")) { - return ret("=>", "operator"); - } else if (ch == "0" && stream.match(/^(?:x[\dA-Fa-f_]+|o[0-7_]+|b[01_]+)n?/)) { - return ret("number", "number"); - } else if (/\d/.test(ch)) { - stream.match(/^[\d_]*(?:n|(?:\.[\d_]*)?(?:[eE][+\-]?[\d_]+)?)?/); - return ret("number", "number"); - } else if (ch == "/") { - if (stream.eat("*")) { - state.tokenize = tokenComment; - return tokenComment(stream, state); - } else if (stream.eat("/")) { - stream.skipToEnd(); - return ret("comment", "comment"); - } else if (expressionAllowed(stream, state, 1)) { - readRegexp(stream); - stream.match(/^\b(([gimyus])(?![gimyus]*\2))+\b/); - return ret("regexp", "string-2"); - } else { - stream.eat("="); - return ret("operator", "operator", stream.current()); - } - } else if (ch == "`") { - state.tokenize = tokenQuasi; - return tokenQuasi(stream, state); - } else if (ch == "#" && stream.peek() == "!") { - stream.skipToEnd(); - return ret("meta", "meta"); - } else if (ch == "#" && stream.eatWhile(wordRE)) { - return ret("variable", "property") - } else if (ch == "<" && stream.match("!--") || - (ch == "-" && stream.match("->") && !/\S/.test(stream.string.slice(0, stream.start)))) { - stream.skipToEnd() - return ret("comment", "comment") - } else if (isOperatorChar.test(ch)) { - if (ch != ">" || !state.lexical || state.lexical.type != ">") { - if (stream.eat("=")) { - if (ch == "!" || ch == "=") stream.eat("=") - } else if (/[<>*+\-]/.test(ch)) { - stream.eat(ch) - if (ch == ">") stream.eat(ch) - } - } - if (ch == "?" && stream.eat(".")) return ret(".") - return ret("operator", "operator", stream.current()); - } else if (wordRE.test(ch)) { - stream.eatWhile(wordRE); - var word = stream.current() - if (state.lastType != ".") { - if (keywords.propertyIsEnumerable(word)) { - var kw = keywords[word] - return ret(kw.type, kw.style, word) - } - if (word == "async" && stream.match(/^(\s|\/\*.*?\*\/)*[\[\(\w]/, false)) - return ret("async", "keyword", word) - } - return ret("variable", "variable", word) - } - } - - function tokenString(quote) { - return function(stream, state) { - var escaped = false, next; - if (jsonldMode && stream.peek() == "@" && stream.match(isJsonldKeyword)){ - state.tokenize = tokenBase; - return ret("jsonld-keyword", "meta"); - } - while ((next = stream.next()) != null) { - if (next == quote && !escaped) break; - escaped = !escaped && next == "\\"; - } - if (!escaped) state.tokenize = tokenBase; - return ret("string", "string"); - }; - } - - function tokenComment(stream, state) { - var maybeEnd = false, ch; - while (ch = stream.next()) { - if (ch == "/" && maybeEnd) { - state.tokenize = tokenBase; - break; - } - maybeEnd = (ch == "*"); - } - return ret("comment", "comment"); - } - - function tokenQuasi(stream, state) { - var escaped = false, next; - while ((next = stream.next()) != null) { - if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) { - state.tokenize = tokenBase; - break; - } - escaped = !escaped && next == "\\"; - } - return ret("quasi", "string-2", stream.current()); - } - - var brackets = "([{}])"; - // This is a crude lookahead trick to try and notice that we're - // parsing the argument patterns for a fat-arrow function before we - // actually hit the arrow token. It only works if the arrow is on - // the same line as the arguments and there's no strange noise - // (comments) in between. Fallback is to only notice when we hit the - // arrow, and not declare the arguments as locals for the arrow - // body. - function findFatArrow(stream, state) { - if (state.fatArrowAt) state.fatArrowAt = null; - var arrow = stream.string.indexOf("=>", stream.start); - if (arrow < 0) return; - - if (isTS) { // Try to skip TypeScript return type declarations after the arguments - var m = /:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(stream.string.slice(stream.start, arrow)) - if (m) arrow = m.index - } - - var depth = 0, sawSomething = false; - for (var pos = arrow - 1; pos >= 0; --pos) { - var ch = stream.string.charAt(pos); - var bracket = brackets.indexOf(ch); - if (bracket >= 0 && bracket < 3) { - if (!depth) { ++pos; break; } - if (--depth == 0) { if (ch == "(") sawSomething = true; break; } - } else if (bracket >= 3 && bracket < 6) { - ++depth; - } else if (wordRE.test(ch)) { - sawSomething = true; - } else if (/["'\/`]/.test(ch)) { - for (;; --pos) { - if (pos == 0) return - var next = stream.string.charAt(pos - 1) - if (next == ch && stream.string.charAt(pos - 2) != "\\") { pos--; break } - } - } else if (sawSomething && !depth) { - ++pos; - break; - } - } - if (sawSomething && !depth) state.fatArrowAt = pos; - } - - // Parser - - var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true, "this": true, "jsonld-keyword": true}; - - function JSLexical(indented, column, type, align, prev, info) { - this.indented = indented; - this.column = column; - this.type = type; - this.prev = prev; - this.info = info; - if (align != null) this.align = align; - } - - function inScope(state, varname) { - for (var v = state.localVars; v; v = v.next) - if (v.name == varname) return true; - for (var cx = state.context; cx; cx = cx.prev) { - for (var v = cx.vars; v; v = v.next) - if (v.name == varname) return true; - } - } - - function parseJS(state, style, type, content, stream) { - var cc = state.cc; - // Communicate our context to the combinators. - // (Less wasteful than consing up a hundred closures on every call.) - cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; cx.style = style; - - if (!state.lexical.hasOwnProperty("align")) - state.lexical.align = true; - - while(true) { - var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement; - if (combinator(type, content)) { - while(cc.length && cc[cc.length - 1].lex) - cc.pop()(); - if (cx.marked) return cx.marked; - if (type == "variable" && inScope(state, content)) return "variable-2"; - return style; - } - } - } - - // Combinator utils - - var cx = {state: null, column: null, marked: null, cc: null}; - function pass() { - for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]); - } - function cont() { - pass.apply(null, arguments); - return true; - } - function inList(name, list) { - for (var v = list; v; v = v.next) if (v.name == name) return true - return false; - } - function register(varname) { - var state = cx.state; - cx.marked = "def"; - if (state.context) { - if (state.lexical.info == "var" && state.context && state.context.block) { - // FIXME function decls are also not block scoped - var newContext = registerVarScoped(varname, state.context) - if (newContext != null) { - state.context = newContext - return - } - } else if (!inList(varname, state.localVars)) { - state.localVars = new Var(varname, state.localVars) - return - } - } - // Fall through means this is global - if (parserConfig.globalVars && !inList(varname, state.globalVars)) - state.globalVars = new Var(varname, state.globalVars) - } - function registerVarScoped(varname, context) { - if (!context) { - return null - } else if (context.block) { - var inner = registerVarScoped(varname, context.prev) - if (!inner) return null - if (inner == context.prev) return context - return new Context(inner, context.vars, true) - } else if (inList(varname, context.vars)) { - return context - } else { - return new Context(context.prev, new Var(varname, context.vars), false) - } - } - - function isModifier(name) { - return name == "public" || name == "private" || name == "protected" || name == "abstract" || name == "readonly" - } - - // Combinators - - function Context(prev, vars, block) { this.prev = prev; this.vars = vars; this.block = block } - function Var(name, next) { this.name = name; this.next = next } - - var defaultVars = new Var("this", new Var("arguments", null)) - function pushcontext() { - cx.state.context = new Context(cx.state.context, cx.state.localVars, false) - cx.state.localVars = defaultVars - } - function pushblockcontext() { - cx.state.context = new Context(cx.state.context, cx.state.localVars, true) - cx.state.localVars = null - } - function popcontext() { - cx.state.localVars = cx.state.context.vars - cx.state.context = cx.state.context.prev - } - popcontext.lex = true - function pushlex(type, info) { - var result = function() { - var state = cx.state, indent = state.indented; - if (state.lexical.type == "stat") indent = state.lexical.indented; - else for (var outer = state.lexical; outer && outer.type == ")" && outer.align; outer = outer.prev) - indent = outer.indented; - state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info); - }; - result.lex = true; - return result; - } - function poplex() { - var state = cx.state; - if (state.lexical.prev) { - if (state.lexical.type == ")") - state.indented = state.lexical.indented; - state.lexical = state.lexical.prev; - } - } - poplex.lex = true; - - function expect(wanted) { - function exp(type) { - if (type == wanted) return cont(); - else if (wanted == ";" || type == "}" || type == ")" || type == "]") return pass(); - else return cont(exp); - }; - return exp; - } - - function statement(type, value) { - if (type == "var") return cont(pushlex("vardef", value), vardef, expect(";"), poplex); - if (type == "keyword a") return cont(pushlex("form"), parenExpr, statement, poplex); - if (type == "keyword b") return cont(pushlex("form"), statement, poplex); - if (type == "keyword d") return cx.stream.match(/^\s*$/, false) ? cont() : cont(pushlex("stat"), maybeexpression, expect(";"), poplex); - if (type == "debugger") return cont(expect(";")); - if (type == "{") return cont(pushlex("}"), pushblockcontext, block, poplex, popcontext); - if (type == ";") return cont(); - if (type == "if") { - if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex) - cx.state.cc.pop()(); - return cont(pushlex("form"), parenExpr, statement, poplex, maybeelse); - } - if (type == "function") return cont(functiondef); - if (type == "for") return cont(pushlex("form"), forspec, statement, poplex); - if (type == "class" || (isTS && value == "interface")) { - cx.marked = "keyword" - return cont(pushlex("form", type == "class" ? type : value), className, poplex) - } - if (type == "variable") { - if (isTS && value == "declare") { - cx.marked = "keyword" - return cont(statement) - } else if (isTS && (value == "module" || value == "enum" || value == "type") && cx.stream.match(/^\s*\w/, false)) { - cx.marked = "keyword" - if (value == "enum") return cont(enumdef); - else if (value == "type") return cont(typename, expect("operator"), typeexpr, expect(";")); - else return cont(pushlex("form"), pattern, expect("{"), pushlex("}"), block, poplex, poplex) - } else if (isTS && value == "namespace") { - cx.marked = "keyword" - return cont(pushlex("form"), expression, statement, poplex) - } else if (isTS && value == "abstract") { - cx.marked = "keyword" - return cont(statement) - } else { - return cont(pushlex("stat"), maybelabel); - } - } - if (type == "switch") return cont(pushlex("form"), parenExpr, expect("{"), pushlex("}", "switch"), pushblockcontext, - block, poplex, poplex, popcontext); - if (type == "case") return cont(expression, expect(":")); - if (type == "default") return cont(expect(":")); - if (type == "catch") return cont(pushlex("form"), pushcontext, maybeCatchBinding, statement, poplex, popcontext); - if (type == "export") return cont(pushlex("stat"), afterExport, poplex); - if (type == "import") return cont(pushlex("stat"), afterImport, poplex); - if (type == "async") return cont(statement) - if (value == "@") return cont(expression, statement) - return pass(pushlex("stat"), expression, expect(";"), poplex); - } - function maybeCatchBinding(type) { - if (type == "(") return cont(funarg, expect(")")) - } - function expression(type, value) { - return expressionInner(type, value, false); - } - function expressionNoComma(type, value) { - return expressionInner(type, value, true); - } - function parenExpr(type) { - if (type != "(") return pass() - return cont(pushlex(")"), maybeexpression, expect(")"), poplex) - } - function expressionInner(type, value, noComma) { - if (cx.state.fatArrowAt == cx.stream.start) { - var body = noComma ? arrowBodyNoComma : arrowBody; - if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, expect("=>"), body, popcontext); - else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext); - } - - var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma; - if (atomicTypes.hasOwnProperty(type)) return cont(maybeop); - if (type == "function") return cont(functiondef, maybeop); - if (type == "class" || (isTS && value == "interface")) { cx.marked = "keyword"; return cont(pushlex("form"), classExpression, poplex); } - if (type == "keyword c" || type == "async") return cont(noComma ? expressionNoComma : expression); - if (type == "(") return cont(pushlex(")"), maybeexpression, expect(")"), poplex, maybeop); - if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression); - if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop); - if (type == "{") return contCommasep(objprop, "}", null, maybeop); - if (type == "quasi") return pass(quasi, maybeop); - if (type == "new") return cont(maybeTarget(noComma)); - if (type == "import") return cont(expression); - return cont(); - } - function maybeexpression(type) { - if (type.match(/[;\}\)\],]/)) return pass(); - return pass(expression); - } - - function maybeoperatorComma(type, value) { - if (type == ",") return cont(maybeexpression); - return maybeoperatorNoComma(type, value, false); - } - function maybeoperatorNoComma(type, value, noComma) { - var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma; - var expr = noComma == false ? expression : expressionNoComma; - if (type == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext); - if (type == "operator") { - if (/\+\+|--/.test(value) || isTS && value == "!") return cont(me); - if (isTS && value == "<" && cx.stream.match(/^([^<>]|<[^<>]*>)*>\s*\(/, false)) - return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, me); - if (value == "?") return cont(expression, expect(":"), expr); - return cont(expr); - } - if (type == "quasi") { return pass(quasi, me); } - if (type == ";") return; - if (type == "(") return contCommasep(expressionNoComma, ")", "call", me); - if (type == ".") return cont(property, me); - if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me); - if (isTS && value == "as") { cx.marked = "keyword"; return cont(typeexpr, me) } - if (type == "regexp") { - cx.state.lastType = cx.marked = "operator" - cx.stream.backUp(cx.stream.pos - cx.stream.start - 1) - return cont(expr) - } - } - function quasi(type, value) { - if (type != "quasi") return pass(); - if (value.slice(value.length - 2) != "${") return cont(quasi); - return cont(expression, continueQuasi); - } - function continueQuasi(type) { - if (type == "}") { - cx.marked = "string-2"; - cx.state.tokenize = tokenQuasi; - return cont(quasi); - } - } - function arrowBody(type) { - findFatArrow(cx.stream, cx.state); - return pass(type == "{" ? statement : expression); - } - function arrowBodyNoComma(type) { - findFatArrow(cx.stream, cx.state); - return pass(type == "{" ? statement : expressionNoComma); - } - function maybeTarget(noComma) { - return function(type) { - if (type == ".") return cont(noComma ? targetNoComma : target); - else if (type == "variable" && isTS) return cont(maybeTypeArgs, noComma ? maybeoperatorNoComma : maybeoperatorComma) - else return pass(noComma ? expressionNoComma : expression); - }; - } - function target(_, value) { - if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorComma); } - } - function targetNoComma(_, value) { - if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorNoComma); } - } - function maybelabel(type) { - if (type == ":") return cont(poplex, statement); - return pass(maybeoperatorComma, expect(";"), poplex); - } - function property(type) { - if (type == "variable") {cx.marked = "property"; return cont();} - } - function objprop(type, value) { - if (type == "async") { - cx.marked = "property"; - return cont(objprop); - } else if (type == "variable" || cx.style == "keyword") { - cx.marked = "property"; - if (value == "get" || value == "set") return cont(getterSetter); - var m // Work around fat-arrow-detection complication for detecting typescript typed arrow params - if (isTS && cx.state.fatArrowAt == cx.stream.start && (m = cx.stream.match(/^\s*:\s*/, false))) - cx.state.fatArrowAt = cx.stream.pos + m[0].length - return cont(afterprop); - } else if (type == "number" || type == "string") { - cx.marked = jsonldMode ? "property" : (cx.style + " property"); - return cont(afterprop); - } else if (type == "jsonld-keyword") { - return cont(afterprop); - } else if (isTS && isModifier(value)) { - cx.marked = "keyword" - return cont(objprop) - } else if (type == "[") { - return cont(expression, maybetype, expect("]"), afterprop); - } else if (type == "spread") { - return cont(expressionNoComma, afterprop); - } else if (value == "*") { - cx.marked = "keyword"; - return cont(objprop); - } else if (type == ":") { - return pass(afterprop) - } - } - function getterSetter(type) { - if (type != "variable") return pass(afterprop); - cx.marked = "property"; - return cont(functiondef); - } - function afterprop(type) { - if (type == ":") return cont(expressionNoComma); - if (type == "(") return pass(functiondef); - } - function commasep(what, end, sep) { - function proceed(type, value) { - if (sep ? sep.indexOf(type) > -1 : type == ",") { - var lex = cx.state.lexical; - if (lex.info == "call") lex.pos = (lex.pos || 0) + 1; - return cont(function(type, value) { - if (type == end || value == end) return pass() - return pass(what) - }, proceed); - } - if (type == end || value == end) return cont(); - if (sep && sep.indexOf(";") > -1) return pass(what) - return cont(expect(end)); - } - return function(type, value) { - if (type == end || value == end) return cont(); - return pass(what, proceed); - }; - } - function contCommasep(what, end, info) { - for (var i = 3; i < arguments.length; i++) - cx.cc.push(arguments[i]); - return cont(pushlex(end, info), commasep(what, end), poplex); - } - function block(type) { - if (type == "}") return cont(); - return pass(statement, block); - } - function maybetype(type, value) { - if (isTS) { - if (type == ":") return cont(typeexpr); - if (value == "?") return cont(maybetype); - } - } - function maybetypeOrIn(type, value) { - if (isTS && (type == ":" || value == "in")) return cont(typeexpr) - } - function mayberettype(type) { - if (isTS && type == ":") { - if (cx.stream.match(/^\s*\w+\s+is\b/, false)) return cont(expression, isKW, typeexpr) - else return cont(typeexpr) - } - } - function isKW(_, value) { - if (value == "is") { - cx.marked = "keyword" - return cont() - } - } - function typeexpr(type, value) { - if (value == "keyof" || value == "typeof" || value == "infer") { - cx.marked = "keyword" - return cont(value == "typeof" ? expressionNoComma : typeexpr) - } - if (type == "variable" || value == "void") { - cx.marked = "type" - return cont(afterType) - } - if (value == "|" || value == "&") return cont(typeexpr) - if (type == "string" || type == "number" || type == "atom") return cont(afterType); - if (type == "[") return cont(pushlex("]"), commasep(typeexpr, "]", ","), poplex, afterType) - if (type == "{") return cont(pushlex("}"), commasep(typeprop, "}", ",;"), poplex, afterType) - if (type == "(") return cont(commasep(typearg, ")"), maybeReturnType, afterType) - if (type == "<") return cont(commasep(typeexpr, ">"), typeexpr) - } - function maybeReturnType(type) { - if (type == "=>") return cont(typeexpr) - } - function typeprop(type, value) { - if (type == "variable" || cx.style == "keyword") { - cx.marked = "property" - return cont(typeprop) - } else if (value == "?" || type == "number" || type == "string") { - return cont(typeprop) - } else if (type == ":") { - return cont(typeexpr) - } else if (type == "[") { - return cont(expect("variable"), maybetypeOrIn, expect("]"), typeprop) - } else if (type == "(") { - return pass(functiondecl, typeprop) - } - } - function typearg(type, value) { - if (type == "variable" && cx.stream.match(/^\s*[?:]/, false) || value == "?") return cont(typearg) - if (type == ":") return cont(typeexpr) - if (type == "spread") return cont(typearg) - return pass(typeexpr) - } - function afterType(type, value) { - if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType) - if (value == "|" || type == "." || value == "&") return cont(typeexpr) - if (type == "[") return cont(typeexpr, expect("]"), afterType) - if (value == "extends" || value == "implements") { cx.marked = "keyword"; return cont(typeexpr) } - if (value == "?") return cont(typeexpr, expect(":"), typeexpr) - } - function maybeTypeArgs(_, value) { - if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType) - } - function typeparam() { - return pass(typeexpr, maybeTypeDefault) - } - function maybeTypeDefault(_, value) { - if (value == "=") return cont(typeexpr) - } - function vardef(_, value) { - if (value == "enum") {cx.marked = "keyword"; return cont(enumdef)} - return pass(pattern, maybetype, maybeAssign, vardefCont); - } - function pattern(type, value) { - if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(pattern) } - if (type == "variable") { register(value); return cont(); } - if (type == "spread") return cont(pattern); - if (type == "[") return contCommasep(eltpattern, "]"); - if (type == "{") return contCommasep(proppattern, "}"); - } - function proppattern(type, value) { - if (type == "variable" && !cx.stream.match(/^\s*:/, false)) { - register(value); - return cont(maybeAssign); - } - if (type == "variable") cx.marked = "property"; - if (type == "spread") return cont(pattern); - if (type == "}") return pass(); - if (type == "[") return cont(expression, expect(']'), expect(':'), proppattern); - return cont(expect(":"), pattern, maybeAssign); - } - function eltpattern() { - return pass(pattern, maybeAssign) - } - function maybeAssign(_type, value) { - if (value == "=") return cont(expressionNoComma); - } - function vardefCont(type) { - if (type == ",") return cont(vardef); - } - function maybeelse(type, value) { - if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex); - } - function forspec(type, value) { - if (value == "await") return cont(forspec); - if (type == "(") return cont(pushlex(")"), forspec1, poplex); - } - function forspec1(type) { - if (type == "var") return cont(vardef, forspec2); - if (type == "variable") return cont(forspec2); - return pass(forspec2) - } - function forspec2(type, value) { - if (type == ")") return cont() - if (type == ";") return cont(forspec2) - if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression, forspec2) } - return pass(expression, forspec2) - } - function functiondef(type, value) { - if (value == "*") {cx.marked = "keyword"; return cont(functiondef);} - if (type == "variable") {register(value); return cont(functiondef);} - if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, statement, popcontext); - if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondef) - } - function functiondecl(type, value) { - if (value == "*") {cx.marked = "keyword"; return cont(functiondecl);} - if (type == "variable") {register(value); return cont(functiondecl);} - if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, popcontext); - if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondecl) - } - function typename(type, value) { - if (type == "keyword" || type == "variable") { - cx.marked = "type" - return cont(typename) - } else if (value == "<") { - return cont(pushlex(">"), commasep(typeparam, ">"), poplex) - } - } - function funarg(type, value) { - if (value == "@") cont(expression, funarg) - if (type == "spread") return cont(funarg); - if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(funarg); } - if (isTS && type == "this") return cont(maybetype, maybeAssign) - return pass(pattern, maybetype, maybeAssign); - } - function classExpression(type, value) { - // Class expressions may have an optional name. - if (type == "variable") return className(type, value); - return classNameAfter(type, value); - } - function className(type, value) { - if (type == "variable") {register(value); return cont(classNameAfter);} - } - function classNameAfter(type, value) { - if (value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, classNameAfter) - if (value == "extends" || value == "implements" || (isTS && type == ",")) { - if (value == "implements") cx.marked = "keyword"; - return cont(isTS ? typeexpr : expression, classNameAfter); - } - if (type == "{") return cont(pushlex("}"), classBody, poplex); - } - function classBody(type, value) { - if (type == "async" || - (type == "variable" && - (value == "static" || value == "get" || value == "set" || (isTS && isModifier(value))) && - cx.stream.match(/^\s+[\w$\xa1-\uffff]/, false))) { - cx.marked = "keyword"; - return cont(classBody); - } - if (type == "variable" || cx.style == "keyword") { - cx.marked = "property"; - return cont(classfield, classBody); - } - if (type == "number" || type == "string") return cont(classfield, classBody); - if (type == "[") - return cont(expression, maybetype, expect("]"), classfield, classBody) - if (value == "*") { - cx.marked = "keyword"; - return cont(classBody); - } - if (isTS && type == "(") return pass(functiondecl, classBody) - if (type == ";" || type == ",") return cont(classBody); - if (type == "}") return cont(); - if (value == "@") return cont(expression, classBody) - } - function classfield(type, value) { - if (value == "?") return cont(classfield) - if (type == ":") return cont(typeexpr, maybeAssign) - if (value == "=") return cont(expressionNoComma) - var context = cx.state.lexical.prev, isInterface = context && context.info == "interface" - return pass(isInterface ? functiondecl : functiondef) - } - function afterExport(type, value) { - if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); } - if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); } - if (type == "{") return cont(commasep(exportField, "}"), maybeFrom, expect(";")); - return pass(statement); - } - function exportField(type, value) { - if (value == "as") { cx.marked = "keyword"; return cont(expect("variable")); } - if (type == "variable") return pass(expressionNoComma, exportField); - } - function afterImport(type) { - if (type == "string") return cont(); - if (type == "(") return pass(expression); - return pass(importSpec, maybeMoreImports, maybeFrom); - } - function importSpec(type, value) { - if (type == "{") return contCommasep(importSpec, "}"); - if (type == "variable") register(value); - if (value == "*") cx.marked = "keyword"; - return cont(maybeAs); - } - function maybeMoreImports(type) { - if (type == ",") return cont(importSpec, maybeMoreImports) - } - function maybeAs(_type, value) { - if (value == "as") { cx.marked = "keyword"; return cont(importSpec); } - } - function maybeFrom(_type, value) { - if (value == "from") { cx.marked = "keyword"; return cont(expression); } - } - function arrayLiteral(type) { - if (type == "]") return cont(); - return pass(commasep(expressionNoComma, "]")); - } - function enumdef() { - return pass(pushlex("form"), pattern, expect("{"), pushlex("}"), commasep(enummember, "}"), poplex, poplex) - } - function enummember() { - return pass(pattern, maybeAssign); - } - - function isContinuedStatement(state, textAfter) { - return state.lastType == "operator" || state.lastType == "," || - isOperatorChar.test(textAfter.charAt(0)) || - /[,.]/.test(textAfter.charAt(0)); - } - - function expressionAllowed(stream, state, backUp) { - return state.tokenize == tokenBase && - /^(?:operator|sof|keyword [bcd]|case|new|export|default|spread|[\[{}\(,;:]|=>)$/.test(state.lastType) || - (state.lastType == "quasi" && /\{\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0)))) - } - - // Interface - - return { - startState: function(basecolumn) { - var state = { - tokenize: tokenBase, - lastType: "sof", - cc: [], - lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false), - localVars: parserConfig.localVars, - context: parserConfig.localVars && new Context(null, null, false), - indented: basecolumn || 0 - }; - if (parserConfig.globalVars && typeof parserConfig.globalVars == "object") - state.globalVars = parserConfig.globalVars; - return state; - }, - - token: function(stream, state) { - if (stream.sol()) { - if (!state.lexical.hasOwnProperty("align")) - state.lexical.align = false; - state.indented = stream.indentation(); - findFatArrow(stream, state); - } - if (state.tokenize != tokenComment && stream.eatSpace()) return null; - var style = state.tokenize(stream, state); - if (type == "comment") return style; - state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type; - return parseJS(state, style, type, content, stream); - }, - - indent: function(state, textAfter) { - if (state.tokenize == tokenComment) return CodeMirror.Pass; - if (state.tokenize != tokenBase) return 0; - var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical, top - // Kludge to prevent 'maybelse' from blocking lexical scope pops - if (!/^\s*else\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) { - var c = state.cc[i]; - if (c == poplex) lexical = lexical.prev; - else if (c != maybeelse) break; - } - while ((lexical.type == "stat" || lexical.type == "form") && - (firstChar == "}" || ((top = state.cc[state.cc.length - 1]) && - (top == maybeoperatorComma || top == maybeoperatorNoComma) && - !/^[,\.=+\-*:?[\(]/.test(textAfter)))) - lexical = lexical.prev; - if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat") - lexical = lexical.prev; - var type = lexical.type, closing = firstChar == type; - - if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info.length + 1 : 0); - else if (type == "form" && firstChar == "{") return lexical.indented; - else if (type == "form") return lexical.indented + indentUnit; - else if (type == "stat") - return lexical.indented + (isContinuedStatement(state, textAfter) ? statementIndent || indentUnit : 0); - else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false) - return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit); - else if (lexical.align) return lexical.column + (closing ? 0 : 1); - else return lexical.indented + (closing ? 0 : indentUnit); - }, - - electricInput: /^\s*(?:case .*?:|default:|\{|\})$/, - blockCommentStart: jsonMode ? null : "/*", - blockCommentEnd: jsonMode ? null : "*/", - blockCommentContinue: jsonMode ? null : " * ", - lineComment: jsonMode ? null : "//", - fold: "brace", - closeBrackets: "()[]{}''\"\"``", - - helperType: jsonMode ? "json" : "javascript", - jsonldMode: jsonldMode, - jsonMode: jsonMode, - - expressionAllowed: expressionAllowed, - - skipExpression: function(state) { - var top = state.cc[state.cc.length - 1] - if (top == expression || top == expressionNoComma) state.cc.pop() - } - }; -}); - -CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/); - -CodeMirror.defineMIME("text/javascript", "javascript"); -CodeMirror.defineMIME("text/ecmascript", "javascript"); -CodeMirror.defineMIME("application/javascript", "javascript"); -CodeMirror.defineMIME("application/x-javascript", "javascript"); -CodeMirror.defineMIME("application/ecmascript", "javascript"); -CodeMirror.defineMIME("application/json", {name: "javascript", json: true}); -CodeMirror.defineMIME("application/x-json", {name: "javascript", json: true}); -CodeMirror.defineMIME("application/ld+json", {name: "javascript", jsonld: true}); -CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true }); -CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true }); - -}); - - -/* ---- mode/markdown.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("../xml/xml"), require("../meta")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "../xml/xml", "../meta"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) { - - var htmlMode = CodeMirror.getMode(cmCfg, "text/html"); - var htmlModeMissing = htmlMode.name == "null" - - function getMode(name) { - if (CodeMirror.findModeByName) { - var found = CodeMirror.findModeByName(name); - if (found) name = found.mime || found.mimes[0]; - } - var mode = CodeMirror.getMode(cmCfg, name); - return mode.name == "null" ? null : mode; - } - - // Should characters that affect highlighting be highlighted separate? - // Does not include characters that will be output (such as `1.` and `-` for lists) - if (modeCfg.highlightFormatting === undefined) - modeCfg.highlightFormatting = false; - - // Maximum number of nested blockquotes. Set to 0 for infinite nesting. - // Excess `>` will emit `error` token. - if (modeCfg.maxBlockquoteDepth === undefined) - modeCfg.maxBlockquoteDepth = 0; - - // Turn on task lists? ("- [ ] " and "- [x] ") - if (modeCfg.taskLists === undefined) modeCfg.taskLists = false; - - // Turn on strikethrough syntax - if (modeCfg.strikethrough === undefined) - modeCfg.strikethrough = false; - - if (modeCfg.emoji === undefined) - modeCfg.emoji = false; - - if (modeCfg.fencedCodeBlockHighlighting === undefined) - modeCfg.fencedCodeBlockHighlighting = true; - - if (modeCfg.fencedCodeBlockDefaultMode === undefined) - modeCfg.fencedCodeBlockDefaultMode = 'text/plain'; - - if (modeCfg.xml === undefined) - modeCfg.xml = true; - - // Allow token types to be overridden by user-provided token types. - if (modeCfg.tokenTypeOverrides === undefined) - modeCfg.tokenTypeOverrides = {}; - - var tokenTypes = { - header: "header", - code: "comment", - quote: "quote", - list1: "variable-2", - list2: "variable-3", - list3: "keyword", - hr: "hr", - image: "image", - imageAltText: "image-alt-text", - imageMarker: "image-marker", - formatting: "formatting", - linkInline: "link", - linkEmail: "link", - linkText: "link", - linkHref: "string", - em: "em", - strong: "strong", - strikethrough: "strikethrough", - emoji: "builtin" - }; - - for (var tokenType in tokenTypes) { - if (tokenTypes.hasOwnProperty(tokenType) && modeCfg.tokenTypeOverrides[tokenType]) { - tokenTypes[tokenType] = modeCfg.tokenTypeOverrides[tokenType]; - } - } - - var hrRE = /^([*\-_])(?:\s*\1){2,}\s*$/ - , listRE = /^(?:[*\-+]|^[0-9]+([.)]))\s+/ - , taskListRE = /^\[(x| )\](?=\s)/i // Must follow listRE - , atxHeaderRE = modeCfg.allowAtxHeaderWithoutSpace ? /^(#+)/ : /^(#+)(?: |$)/ - , setextHeaderRE = /^ {0,3}(?:\={1,}|-{2,})\s*$/ - , textRE = /^[^#!\[\]*_\\<>` "'(~:]+/ - , fencedCodeRE = /^(~~~+|```+)[ \t]*([\w\/+#-]*)[^\n`]*$/ - , linkDefRE = /^\s*\[[^\]]+?\]:.*$/ // naive link-definition - , punctuation = /[!"#$%&'()*+,\-.\/:;<=>?@\[\\\]^_`{|}~\xA1\xA7\xAB\xB6\xB7\xBB\xBF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u0AF0\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166D\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E42\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]|\uD800[\uDD00-\uDD02\uDF9F\uDFD0]|\uD801\uDD6F|\uD802[\uDC57\uDD1F\uDD3F\uDE50-\uDE58\uDE7F\uDEF0-\uDEF6\uDF39-\uDF3F\uDF99-\uDF9C]|\uD804[\uDC47-\uDC4D\uDCBB\uDCBC\uDCBE-\uDCC1\uDD40-\uDD43\uDD74\uDD75\uDDC5-\uDDC9\uDDCD\uDDDB\uDDDD-\uDDDF\uDE38-\uDE3D\uDEA9]|\uD805[\uDCC6\uDDC1-\uDDD7\uDE41-\uDE43\uDF3C-\uDF3E]|\uD809[\uDC70-\uDC74]|\uD81A[\uDE6E\uDE6F\uDEF5\uDF37-\uDF3B\uDF44]|\uD82F\uDC9F|\uD836[\uDE87-\uDE8B]/ - , expandedTab = " " // CommonMark specifies tab as 4 spaces - - function switchInline(stream, state, f) { - state.f = state.inline = f; - return f(stream, state); - } - - function switchBlock(stream, state, f) { - state.f = state.block = f; - return f(stream, state); - } - - function lineIsEmpty(line) { - return !line || !/\S/.test(line.string) - } - - // Blocks - - function blankLine(state) { - // Reset linkTitle state - state.linkTitle = false; - state.linkHref = false; - state.linkText = false; - // Reset EM state - state.em = false; - // Reset STRONG state - state.strong = false; - // Reset strikethrough state - state.strikethrough = false; - // Reset state.quote - state.quote = 0; - // Reset state.indentedCode - state.indentedCode = false; - if (state.f == htmlBlock) { - var exit = htmlModeMissing - if (!exit) { - var inner = CodeMirror.innerMode(htmlMode, state.htmlState) - exit = inner.mode.name == "xml" && inner.state.tagStart === null && - (!inner.state.context && inner.state.tokenize.isInText) - } - if (exit) { - state.f = inlineNormal; - state.block = blockNormal; - state.htmlState = null; - } - } - // Reset state.trailingSpace - state.trailingSpace = 0; - state.trailingSpaceNewLine = false; - // Mark this line as blank - state.prevLine = state.thisLine - state.thisLine = {stream: null} - return null; - } - - function blockNormal(stream, state) { - var firstTokenOnLine = stream.column() === state.indentation; - var prevLineLineIsEmpty = lineIsEmpty(state.prevLine.stream); - var prevLineIsIndentedCode = state.indentedCode; - var prevLineIsHr = state.prevLine.hr; - var prevLineIsList = state.list !== false; - var maxNonCodeIndentation = (state.listStack[state.listStack.length - 1] || 0) + 3; - - state.indentedCode = false; - - var lineIndentation = state.indentation; - // compute once per line (on first token) - if (state.indentationDiff === null) { - state.indentationDiff = state.indentation; - if (prevLineIsList) { - state.list = null; - // While this list item's marker's indentation is less than the deepest - // list item's content's indentation,pop the deepest list item - // indentation off the stack, and update block indentation state - while (lineIndentation < state.listStack[state.listStack.length - 1]) { - state.listStack.pop(); - if (state.listStack.length) { - state.indentation = state.listStack[state.listStack.length - 1]; - // less than the first list's indent -> the line is no longer a list - } else { - state.list = false; - } - } - if (state.list !== false) { - state.indentationDiff = lineIndentation - state.listStack[state.listStack.length - 1] - } - } - } - - // not comprehensive (currently only for setext detection purposes) - var allowsInlineContinuation = ( - !prevLineLineIsEmpty && !prevLineIsHr && !state.prevLine.header && - (!prevLineIsList || !prevLineIsIndentedCode) && - !state.prevLine.fencedCodeEnd - ); - - var isHr = (state.list === false || prevLineIsHr || prevLineLineIsEmpty) && - state.indentation <= maxNonCodeIndentation && stream.match(hrRE); - - var match = null; - if (state.indentationDiff >= 4 && (prevLineIsIndentedCode || state.prevLine.fencedCodeEnd || - state.prevLine.header || prevLineLineIsEmpty)) { - stream.skipToEnd(); - state.indentedCode = true; - return tokenTypes.code; - } else if (stream.eatSpace()) { - return null; - } else if (firstTokenOnLine && state.indentation <= maxNonCodeIndentation && (match = stream.match(atxHeaderRE)) && match[1].length <= 6) { - state.quote = 0; - state.header = match[1].length; - state.thisLine.header = true; - if (modeCfg.highlightFormatting) state.formatting = "header"; - state.f = state.inline; - return getType(state); - } else if (state.indentation <= maxNonCodeIndentation && stream.eat('>')) { - state.quote = firstTokenOnLine ? 1 : state.quote + 1; - if (modeCfg.highlightFormatting) state.formatting = "quote"; - stream.eatSpace(); - return getType(state); - } else if (!isHr && !state.setext && firstTokenOnLine && state.indentation <= maxNonCodeIndentation && (match = stream.match(listRE))) { - var listType = match[1] ? "ol" : "ul"; - - state.indentation = lineIndentation + stream.current().length; - state.list = true; - state.quote = 0; - - // Add this list item's content's indentation to the stack - state.listStack.push(state.indentation); - // Reset inline styles which shouldn't propagate aross list items - state.em = false; - state.strong = false; - state.code = false; - state.strikethrough = false; - - if (modeCfg.taskLists && stream.match(taskListRE, false)) { - state.taskList = true; - } - state.f = state.inline; - if (modeCfg.highlightFormatting) state.formatting = ["list", "list-" + listType]; - return getType(state); - } else if (firstTokenOnLine && state.indentation <= maxNonCodeIndentation && (match = stream.match(fencedCodeRE, true))) { - state.quote = 0; - state.fencedEndRE = new RegExp(match[1] + "+ *$"); - // try switching mode - state.localMode = modeCfg.fencedCodeBlockHighlighting && getMode(match[2] || modeCfg.fencedCodeBlockDefaultMode ); - if (state.localMode) state.localState = CodeMirror.startState(state.localMode); - state.f = state.block = local; - if (modeCfg.highlightFormatting) state.formatting = "code-block"; - state.code = -1 - return getType(state); - // SETEXT has lowest block-scope precedence after HR, so check it after - // the others (code, blockquote, list...) - } else if ( - // if setext set, indicates line after ---/=== - state.setext || ( - // line before ---/=== - (!allowsInlineContinuation || !prevLineIsList) && !state.quote && state.list === false && - !state.code && !isHr && !linkDefRE.test(stream.string) && - (match = stream.lookAhead(1)) && (match = match.match(setextHeaderRE)) - ) - ) { - if ( !state.setext ) { - state.header = match[0].charAt(0) == '=' ? 1 : 2; - state.setext = state.header; - } else { - state.header = state.setext; - // has no effect on type so we can reset it now - state.setext = 0; - stream.skipToEnd(); - if (modeCfg.highlightFormatting) state.formatting = "header"; - } - state.thisLine.header = true; - state.f = state.inline; - return getType(state); - } else if (isHr) { - stream.skipToEnd(); - state.hr = true; - state.thisLine.hr = true; - return tokenTypes.hr; - } else if (stream.peek() === '[') { - return switchInline(stream, state, footnoteLink); - } - - return switchInline(stream, state, state.inline); - } - - function htmlBlock(stream, state) { - var style = htmlMode.token(stream, state.htmlState); - if (!htmlModeMissing) { - var inner = CodeMirror.innerMode(htmlMode, state.htmlState) - if ((inner.mode.name == "xml" && inner.state.tagStart === null && - (!inner.state.context && inner.state.tokenize.isInText)) || - (state.md_inside && stream.current().indexOf(">") > -1)) { - state.f = inlineNormal; - state.block = blockNormal; - state.htmlState = null; - } - } - return style; - } - - function local(stream, state) { - var currListInd = state.listStack[state.listStack.length - 1] || 0; - var hasExitedList = state.indentation < currListInd; - var maxFencedEndInd = currListInd + 3; - if (state.fencedEndRE && state.indentation <= maxFencedEndInd && (hasExitedList || stream.match(state.fencedEndRE))) { - if (modeCfg.highlightFormatting) state.formatting = "code-block"; - var returnType; - if (!hasExitedList) returnType = getType(state) - state.localMode = state.localState = null; - state.block = blockNormal; - state.f = inlineNormal; - state.fencedEndRE = null; - state.code = 0 - state.thisLine.fencedCodeEnd = true; - if (hasExitedList) return switchBlock(stream, state, state.block); - return returnType; - } else if (state.localMode) { - return state.localMode.token(stream, state.localState); - } else { - stream.skipToEnd(); - return tokenTypes.code; - } - } - - // Inline - function getType(state) { - var styles = []; - - if (state.formatting) { - styles.push(tokenTypes.formatting); - - if (typeof state.formatting === "string") state.formatting = [state.formatting]; - - for (var i = 0; i < state.formatting.length; i++) { - styles.push(tokenTypes.formatting + "-" + state.formatting[i]); - - if (state.formatting[i] === "header") { - styles.push(tokenTypes.formatting + "-" + state.formatting[i] + "-" + state.header); - } - - // Add `formatting-quote` and `formatting-quote-#` for blockquotes - // Add `error` instead if the maximum blockquote nesting depth is passed - if (state.formatting[i] === "quote") { - if (!modeCfg.maxBlockquoteDepth || modeCfg.maxBlockquoteDepth >= state.quote) { - styles.push(tokenTypes.formatting + "-" + state.formatting[i] + "-" + state.quote); - } else { - styles.push("error"); - } - } - } - } - - if (state.taskOpen) { - styles.push("meta"); - return styles.length ? styles.join(' ') : null; - } - if (state.taskClosed) { - styles.push("property"); - return styles.length ? styles.join(' ') : null; - } - - if (state.linkHref) { - styles.push(tokenTypes.linkHref, "url"); - } else { // Only apply inline styles to non-url text - if (state.strong) { styles.push(tokenTypes.strong); } - if (state.em) { styles.push(tokenTypes.em); } - if (state.strikethrough) { styles.push(tokenTypes.strikethrough); } - if (state.emoji) { styles.push(tokenTypes.emoji); } - if (state.linkText) { styles.push(tokenTypes.linkText); } - if (state.code) { styles.push(tokenTypes.code); } - if (state.image) { styles.push(tokenTypes.image); } - if (state.imageAltText) { styles.push(tokenTypes.imageAltText, "link"); } - if (state.imageMarker) { styles.push(tokenTypes.imageMarker); } - } - - if (state.header) { styles.push(tokenTypes.header, tokenTypes.header + "-" + state.header); } - - if (state.quote) { - styles.push(tokenTypes.quote); - - // Add `quote-#` where the maximum for `#` is modeCfg.maxBlockquoteDepth - if (!modeCfg.maxBlockquoteDepth || modeCfg.maxBlockquoteDepth >= state.quote) { - styles.push(tokenTypes.quote + "-" + state.quote); - } else { - styles.push(tokenTypes.quote + "-" + modeCfg.maxBlockquoteDepth); - } - } - - if (state.list !== false) { - var listMod = (state.listStack.length - 1) % 3; - if (!listMod) { - styles.push(tokenTypes.list1); - } else if (listMod === 1) { - styles.push(tokenTypes.list2); - } else { - styles.push(tokenTypes.list3); - } - } - - if (state.trailingSpaceNewLine) { - styles.push("trailing-space-new-line"); - } else if (state.trailingSpace) { - styles.push("trailing-space-" + (state.trailingSpace % 2 ? "a" : "b")); - } - - return styles.length ? styles.join(' ') : null; - } - - function handleText(stream, state) { - if (stream.match(textRE, true)) { - return getType(state); - } - return undefined; - } - - function inlineNormal(stream, state) { - var style = state.text(stream, state); - if (typeof style !== 'undefined') - return style; - - if (state.list) { // List marker (*, +, -, 1., etc) - state.list = null; - return getType(state); - } - - if (state.taskList) { - var taskOpen = stream.match(taskListRE, true)[1] === " "; - if (taskOpen) state.taskOpen = true; - else state.taskClosed = true; - if (modeCfg.highlightFormatting) state.formatting = "task"; - state.taskList = false; - return getType(state); - } - - state.taskOpen = false; - state.taskClosed = false; - - if (state.header && stream.match(/^#+$/, true)) { - if (modeCfg.highlightFormatting) state.formatting = "header"; - return getType(state); - } - - var ch = stream.next(); - - // Matches link titles present on next line - if (state.linkTitle) { - state.linkTitle = false; - var matchCh = ch; - if (ch === '(') { - matchCh = ')'; - } - matchCh = (matchCh+'').replace(/([.?*+^\[\]\\(){}|-])/g, "\\$1"); - var regex = '^\\s*(?:[^' + matchCh + '\\\\]+|\\\\\\\\|\\\\.)' + matchCh; - if (stream.match(new RegExp(regex), true)) { - return tokenTypes.linkHref; - } - } - - // If this block is changed, it may need to be updated in GFM mode - if (ch === '`') { - var previousFormatting = state.formatting; - if (modeCfg.highlightFormatting) state.formatting = "code"; - stream.eatWhile('`'); - var count = stream.current().length - if (state.code == 0 && (!state.quote || count == 1)) { - state.code = count - return getType(state) - } else if (count == state.code) { // Must be exact - var t = getType(state) - state.code = 0 - return t - } else { - state.formatting = previousFormatting - return getType(state) - } - } else if (state.code) { - return getType(state); - } - - if (ch === '\\') { - stream.next(); - if (modeCfg.highlightFormatting) { - var type = getType(state); - var formattingEscape = tokenTypes.formatting + "-escape"; - return type ? type + " " + formattingEscape : formattingEscape; - } - } - - if (ch === '!' && stream.match(/\[[^\]]*\] ?(?:\(|\[)/, false)) { - state.imageMarker = true; - state.image = true; - if (modeCfg.highlightFormatting) state.formatting = "image"; - return getType(state); - } - - if (ch === '[' && state.imageMarker && stream.match(/[^\]]*\](\(.*?\)| ?\[.*?\])/, false)) { - state.imageMarker = false; - state.imageAltText = true - if (modeCfg.highlightFormatting) state.formatting = "image"; - return getType(state); - } - - if (ch === ']' && state.imageAltText) { - if (modeCfg.highlightFormatting) state.formatting = "image"; - var type = getType(state); - state.imageAltText = false; - state.image = false; - state.inline = state.f = linkHref; - return type; - } - - if (ch === '[' && !state.image) { - if (state.linkText && stream.match(/^.*?\]/)) return getType(state) - state.linkText = true; - if (modeCfg.highlightFormatting) state.formatting = "link"; - return getType(state); - } - - if (ch === ']' && state.linkText) { - if (modeCfg.highlightFormatting) state.formatting = "link"; - var type = getType(state); - state.linkText = false; - state.inline = state.f = stream.match(/\(.*?\)| ?\[.*?\]/, false) ? linkHref : inlineNormal - return type; - } - - if (ch === '<' && stream.match(/^(https?|ftps?):\/\/(?:[^\\>]|\\.)+>/, false)) { - state.f = state.inline = linkInline; - if (modeCfg.highlightFormatting) state.formatting = "link"; - var type = getType(state); - if (type){ - type += " "; - } else { - type = ""; - } - return type + tokenTypes.linkInline; - } - - if (ch === '<' && stream.match(/^[^> \\]+@(?:[^\\>]|\\.)+>/, false)) { - state.f = state.inline = linkInline; - if (modeCfg.highlightFormatting) state.formatting = "link"; - var type = getType(state); - if (type){ - type += " "; - } else { - type = ""; - } - return type + tokenTypes.linkEmail; - } - - if (modeCfg.xml && ch === '<' && stream.match(/^(!--|\?|!\[CDATA\[|[a-z][a-z0-9-]*(?:\s+[a-z_:.\-]+(?:\s*=\s*[^>]+)?)*\s*(?:>|$))/i, false)) { - var end = stream.string.indexOf(">", stream.pos); - if (end != -1) { - var atts = stream.string.substring(stream.start, end); - if (/markdown\s*=\s*('|"){0,1}1('|"){0,1}/.test(atts)) state.md_inside = true; - } - stream.backUp(1); - state.htmlState = CodeMirror.startState(htmlMode); - return switchBlock(stream, state, htmlBlock); - } - - if (modeCfg.xml && ch === '<' && stream.match(/^\/\w*?>/)) { - state.md_inside = false; - return "tag"; - } else if (ch === "*" || ch === "_") { - var len = 1, before = stream.pos == 1 ? " " : stream.string.charAt(stream.pos - 2) - while (len < 3 && stream.eat(ch)) len++ - var after = stream.peek() || " " - // See http://spec.commonmark.org/0.27/#emphasis-and-strong-emphasis - var leftFlanking = !/\s/.test(after) && (!punctuation.test(after) || /\s/.test(before) || punctuation.test(before)) - var rightFlanking = !/\s/.test(before) && (!punctuation.test(before) || /\s/.test(after) || punctuation.test(after)) - var setEm = null, setStrong = null - if (len % 2) { // Em - if (!state.em && leftFlanking && (ch === "*" || !rightFlanking || punctuation.test(before))) - setEm = true - else if (state.em == ch && rightFlanking && (ch === "*" || !leftFlanking || punctuation.test(after))) - setEm = false - } - if (len > 1) { // Strong - if (!state.strong && leftFlanking && (ch === "*" || !rightFlanking || punctuation.test(before))) - setStrong = true - else if (state.strong == ch && rightFlanking && (ch === "*" || !leftFlanking || punctuation.test(after))) - setStrong = false - } - if (setStrong != null || setEm != null) { - if (modeCfg.highlightFormatting) state.formatting = setEm == null ? "strong" : setStrong == null ? "em" : "strong em" - if (setEm === true) state.em = ch - if (setStrong === true) state.strong = ch - var t = getType(state) - if (setEm === false) state.em = false - if (setStrong === false) state.strong = false - return t - } - } else if (ch === ' ') { - if (stream.eat('*') || stream.eat('_')) { // Probably surrounded by spaces - if (stream.peek() === ' ') { // Surrounded by spaces, ignore - return getType(state); - } else { // Not surrounded by spaces, back up pointer - stream.backUp(1); - } - } - } - - if (modeCfg.strikethrough) { - if (ch === '~' && stream.eatWhile(ch)) { - if (state.strikethrough) {// Remove strikethrough - if (modeCfg.highlightFormatting) state.formatting = "strikethrough"; - var t = getType(state); - state.strikethrough = false; - return t; - } else if (stream.match(/^[^\s]/, false)) {// Add strikethrough - state.strikethrough = true; - if (modeCfg.highlightFormatting) state.formatting = "strikethrough"; - return getType(state); - } - } else if (ch === ' ') { - if (stream.match(/^~~/, true)) { // Probably surrounded by space - if (stream.peek() === ' ') { // Surrounded by spaces, ignore - return getType(state); - } else { // Not surrounded by spaces, back up pointer - stream.backUp(2); - } - } - } - } - - if (modeCfg.emoji && ch === ":" && stream.match(/^(?:[a-z_\d+][a-z_\d+-]*|\-[a-z_\d+][a-z_\d+-]*):/)) { - state.emoji = true; - if (modeCfg.highlightFormatting) state.formatting = "emoji"; - var retType = getType(state); - state.emoji = false; - return retType; - } - - if (ch === ' ') { - if (stream.match(/^ +$/, false)) { - state.trailingSpace++; - } else if (state.trailingSpace) { - state.trailingSpaceNewLine = true; - } - } - - return getType(state); - } - - function linkInline(stream, state) { - var ch = stream.next(); - - if (ch === ">") { - state.f = state.inline = inlineNormal; - if (modeCfg.highlightFormatting) state.formatting = "link"; - var type = getType(state); - if (type){ - type += " "; - } else { - type = ""; - } - return type + tokenTypes.linkInline; - } - - stream.match(/^[^>]+/, true); - - return tokenTypes.linkInline; - } - - function linkHref(stream, state) { - // Check if space, and return NULL if so (to avoid marking the space) - if(stream.eatSpace()){ - return null; - } - var ch = stream.next(); - if (ch === '(' || ch === '[') { - state.f = state.inline = getLinkHrefInside(ch === "(" ? ")" : "]"); - if (modeCfg.highlightFormatting) state.formatting = "link-string"; - state.linkHref = true; - return getType(state); - } - return 'error'; - } - - var linkRE = { - ")": /^(?:[^\\\(\)]|\\.|\((?:[^\\\(\)]|\\.)*\))*?(?=\))/, - "]": /^(?:[^\\\[\]]|\\.|\[(?:[^\\\[\]]|\\.)*\])*?(?=\])/ - } - - function getLinkHrefInside(endChar) { - return function(stream, state) { - var ch = stream.next(); - - if (ch === endChar) { - state.f = state.inline = inlineNormal; - if (modeCfg.highlightFormatting) state.formatting = "link-string"; - var returnState = getType(state); - state.linkHref = false; - return returnState; - } - - stream.match(linkRE[endChar]) - state.linkHref = true; - return getType(state); - }; - } - - function footnoteLink(stream, state) { - if (stream.match(/^([^\]\\]|\\.)*\]:/, false)) { - state.f = footnoteLinkInside; - stream.next(); // Consume [ - if (modeCfg.highlightFormatting) state.formatting = "link"; - state.linkText = true; - return getType(state); - } - return switchInline(stream, state, inlineNormal); - } - - function footnoteLinkInside(stream, state) { - if (stream.match(/^\]:/, true)) { - state.f = state.inline = footnoteUrl; - if (modeCfg.highlightFormatting) state.formatting = "link"; - var returnType = getType(state); - state.linkText = false; - return returnType; - } - - stream.match(/^([^\]\\]|\\.)+/, true); - - return tokenTypes.linkText; - } - - function footnoteUrl(stream, state) { - // Check if space, and return NULL if so (to avoid marking the space) - if(stream.eatSpace()){ - return null; - } - // Match URL - stream.match(/^[^\s]+/, true); - // Check for link title - if (stream.peek() === undefined) { // End of line, set flag to check next line - state.linkTitle = true; - } else { // More content on line, check if link title - stream.match(/^(?:\s+(?:"(?:[^"\\]|\\\\|\\.)+"|'(?:[^'\\]|\\\\|\\.)+'|\((?:[^)\\]|\\\\|\\.)+\)))?/, true); - } - state.f = state.inline = inlineNormal; - return tokenTypes.linkHref + " url"; - } - - var mode = { - startState: function() { - return { - f: blockNormal, - - prevLine: {stream: null}, - thisLine: {stream: null}, - - block: blockNormal, - htmlState: null, - indentation: 0, - - inline: inlineNormal, - text: handleText, - - formatting: false, - linkText: false, - linkHref: false, - linkTitle: false, - code: 0, - em: false, - strong: false, - header: 0, - setext: 0, - hr: false, - taskList: false, - list: false, - listStack: [], - quote: 0, - trailingSpace: 0, - trailingSpaceNewLine: false, - strikethrough: false, - emoji: false, - fencedEndRE: null - }; - }, - - copyState: function(s) { - return { - f: s.f, - - prevLine: s.prevLine, - thisLine: s.thisLine, - - block: s.block, - htmlState: s.htmlState && CodeMirror.copyState(htmlMode, s.htmlState), - indentation: s.indentation, - - localMode: s.localMode, - localState: s.localMode ? CodeMirror.copyState(s.localMode, s.localState) : null, - - inline: s.inline, - text: s.text, - formatting: false, - linkText: s.linkText, - linkTitle: s.linkTitle, - linkHref: s.linkHref, - code: s.code, - em: s.em, - strong: s.strong, - strikethrough: s.strikethrough, - emoji: s.emoji, - header: s.header, - setext: s.setext, - hr: s.hr, - taskList: s.taskList, - list: s.list, - listStack: s.listStack.slice(0), - quote: s.quote, - indentedCode: s.indentedCode, - trailingSpace: s.trailingSpace, - trailingSpaceNewLine: s.trailingSpaceNewLine, - md_inside: s.md_inside, - fencedEndRE: s.fencedEndRE - }; - }, - - token: function(stream, state) { - - // Reset state.formatting - state.formatting = false; - - if (stream != state.thisLine.stream) { - state.header = 0; - state.hr = false; - - if (stream.match(/^\s*$/, true)) { - blankLine(state); - return null; - } - - state.prevLine = state.thisLine - state.thisLine = {stream: stream} - - // Reset state.taskList - state.taskList = false; - - // Reset state.trailingSpace - state.trailingSpace = 0; - state.trailingSpaceNewLine = false; - - if (!state.localState) { - state.f = state.block; - if (state.f != htmlBlock) { - var indentation = stream.match(/^\s*/, true)[0].replace(/\t/g, expandedTab).length; - state.indentation = indentation; - state.indentationDiff = null; - if (indentation > 0) return null; - } - } - } - return state.f(stream, state); - }, - - innerMode: function(state) { - if (state.block == htmlBlock) return {state: state.htmlState, mode: htmlMode}; - if (state.localState) return {state: state.localState, mode: state.localMode}; - return {state: state, mode: mode}; - }, - - indent: function(state, textAfter, line) { - if (state.block == htmlBlock && htmlMode.indent) return htmlMode.indent(state.htmlState, textAfter, line) - if (state.localState && state.localMode.indent) return state.localMode.indent(state.localState, textAfter, line) - return CodeMirror.Pass - }, - - blankLine: blankLine, - - getType: getType, - - blockCommentStart: "", - closeBrackets: "()[]{}''\"\"``", - fold: "markdown" - }; - return mode; -}, "xml"); - -CodeMirror.defineMIME("text/markdown", "markdown"); - -CodeMirror.defineMIME("text/x-markdown", "markdown"); - -}); - - -/* ---- mode/python.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - function wordRegexp(words) { - return new RegExp("^((" + words.join(")|(") + "))\\b"); - } - - var wordOperators = wordRegexp(["and", "or", "not", "is"]); - var commonKeywords = ["as", "assert", "break", "class", "continue", - "def", "del", "elif", "else", "except", "finally", - "for", "from", "global", "if", "import", - "lambda", "pass", "raise", "return", - "try", "while", "with", "yield", "in"]; - var commonBuiltins = ["abs", "all", "any", "bin", "bool", "bytearray", "callable", "chr", - "classmethod", "compile", "complex", "delattr", "dict", "dir", "divmod", - "enumerate", "eval", "filter", "float", "format", "frozenset", - "getattr", "globals", "hasattr", "hash", "help", "hex", "id", - "input", "int", "isinstance", "issubclass", "iter", "len", - "list", "locals", "map", "max", "memoryview", "min", "next", - "object", "oct", "open", "ord", "pow", "property", "range", - "repr", "reversed", "round", "set", "setattr", "slice", - "sorted", "staticmethod", "str", "sum", "super", "tuple", - "type", "vars", "zip", "__import__", "NotImplemented", - "Ellipsis", "__debug__"]; - CodeMirror.registerHelper("hintWords", "python", commonKeywords.concat(commonBuiltins)); - - function top(state) { - return state.scopes[state.scopes.length - 1]; - } - - CodeMirror.defineMode("python", function(conf, parserConf) { - var ERRORCLASS = "error"; - - var delimiters = parserConf.delimiters || parserConf.singleDelimiters || /^[\(\)\[\]\{\}@,:`=;\.\\]/; - // (Backwards-compatibility with old, cumbersome config system) - var operators = [parserConf.singleOperators, parserConf.doubleOperators, parserConf.doubleDelimiters, parserConf.tripleDelimiters, - parserConf.operators || /^([-+*/%\/&|^]=?|[<>=]+|\/\/=?|\*\*=?|!=|[~!@]|\.\.\.)/] - for (var i = 0; i < operators.length; i++) if (!operators[i]) operators.splice(i--, 1) - - var hangingIndent = parserConf.hangingIndent || conf.indentUnit; - - var myKeywords = commonKeywords, myBuiltins = commonBuiltins; - if (parserConf.extra_keywords != undefined) - myKeywords = myKeywords.concat(parserConf.extra_keywords); - - if (parserConf.extra_builtins != undefined) - myBuiltins = myBuiltins.concat(parserConf.extra_builtins); - - var py3 = !(parserConf.version && Number(parserConf.version) < 3) - if (py3) { - // since http://legacy.python.org/dev/peps/pep-0465/ @ is also an operator - var identifiers = parserConf.identifiers|| /^[_A-Za-z\u00A1-\uFFFF][_A-Za-z0-9\u00A1-\uFFFF]*/; - myKeywords = myKeywords.concat(["nonlocal", "False", "True", "None", "async", "await"]); - myBuiltins = myBuiltins.concat(["ascii", "bytes", "exec", "print"]); - var stringPrefixes = new RegExp("^(([rbuf]|(br)|(fr))?('{3}|\"{3}|['\"]))", "i"); - } else { - var identifiers = parserConf.identifiers|| /^[_A-Za-z][_A-Za-z0-9]*/; - myKeywords = myKeywords.concat(["exec", "print"]); - myBuiltins = myBuiltins.concat(["apply", "basestring", "buffer", "cmp", "coerce", "execfile", - "file", "intern", "long", "raw_input", "reduce", "reload", - "unichr", "unicode", "xrange", "False", "True", "None"]); - var stringPrefixes = new RegExp("^(([rubf]|(ur)|(br))?('{3}|\"{3}|['\"]))", "i"); - } - var keywords = wordRegexp(myKeywords); - var builtins = wordRegexp(myBuiltins); - - // tokenizers - function tokenBase(stream, state) { - var sol = stream.sol() && state.lastToken != "\\" - if (sol) state.indent = stream.indentation() - // Handle scope changes - if (sol && top(state).type == "py") { - var scopeOffset = top(state).offset; - if (stream.eatSpace()) { - var lineOffset = stream.indentation(); - if (lineOffset > scopeOffset) - pushPyScope(state); - else if (lineOffset < scopeOffset && dedent(stream, state) && stream.peek() != "#") - state.errorToken = true; - return null; - } else { - var style = tokenBaseInner(stream, state); - if (scopeOffset > 0 && dedent(stream, state)) - style += " " + ERRORCLASS; - return style; - } - } - return tokenBaseInner(stream, state); - } - - function tokenBaseInner(stream, state, inFormat) { - if (stream.eatSpace()) return null; - - // Handle Comments - if (!inFormat && stream.match(/^#.*/)) return "comment"; - - // Handle Number Literals - if (stream.match(/^[0-9\.]/, false)) { - var floatLiteral = false; - // Floats - if (stream.match(/^[\d_]*\.\d+(e[\+\-]?\d+)?/i)) { floatLiteral = true; } - if (stream.match(/^[\d_]+\.\d*/)) { floatLiteral = true; } - if (stream.match(/^\.\d+/)) { floatLiteral = true; } - if (floatLiteral) { - // Float literals may be "imaginary" - stream.eat(/J/i); - return "number"; - } - // Integers - var intLiteral = false; - // Hex - if (stream.match(/^0x[0-9a-f_]+/i)) intLiteral = true; - // Binary - if (stream.match(/^0b[01_]+/i)) intLiteral = true; - // Octal - if (stream.match(/^0o[0-7_]+/i)) intLiteral = true; - // Decimal - if (stream.match(/^[1-9][\d_]*(e[\+\-]?[\d_]+)?/)) { - // Decimal literals may be "imaginary" - stream.eat(/J/i); - // TODO - Can you have imaginary longs? - intLiteral = true; - } - // Zero by itself with no other piece of number. - if (stream.match(/^0(?![\dx])/i)) intLiteral = true; - if (intLiteral) { - // Integer literals may be "long" - stream.eat(/L/i); - return "number"; - } - } - - // Handle Strings - if (stream.match(stringPrefixes)) { - var isFmtString = stream.current().toLowerCase().indexOf('f') !== -1; - if (!isFmtString) { - state.tokenize = tokenStringFactory(stream.current(), state.tokenize); - return state.tokenize(stream, state); - } else { - state.tokenize = formatStringFactory(stream.current(), state.tokenize); - return state.tokenize(stream, state); - } - } - - for (var i = 0; i < operators.length; i++) - if (stream.match(operators[i])) return "operator" - - if (stream.match(delimiters)) return "punctuation"; - - if (state.lastToken == "." && stream.match(identifiers)) - return "property"; - - if (stream.match(keywords) || stream.match(wordOperators)) - return "keyword"; - - if (stream.match(builtins)) - return "builtin"; - - if (stream.match(/^(self|cls)\b/)) - return "variable-2"; - - if (stream.match(identifiers)) { - if (state.lastToken == "def" || state.lastToken == "class") - return "def"; - return "variable"; - } - - // Handle non-detected items - stream.next(); - return inFormat ? null :ERRORCLASS; - } - - function formatStringFactory(delimiter, tokenOuter) { - while ("rubf".indexOf(delimiter.charAt(0).toLowerCase()) >= 0) - delimiter = delimiter.substr(1); - - var singleline = delimiter.length == 1; - var OUTCLASS = "string"; - - function tokenNestedExpr(depth) { - return function(stream, state) { - var inner = tokenBaseInner(stream, state, true) - if (inner == "punctuation") { - if (stream.current() == "{") { - state.tokenize = tokenNestedExpr(depth + 1) - } else if (stream.current() == "}") { - if (depth > 1) state.tokenize = tokenNestedExpr(depth - 1) - else state.tokenize = tokenString - } - } - return inner - } - } - - function tokenString(stream, state) { - while (!stream.eol()) { - stream.eatWhile(/[^'"\{\}\\]/); - if (stream.eat("\\")) { - stream.next(); - if (singleline && stream.eol()) - return OUTCLASS; - } else if (stream.match(delimiter)) { - state.tokenize = tokenOuter; - return OUTCLASS; - } else if (stream.match('{{')) { - // ignore {{ in f-str - return OUTCLASS; - } else if (stream.match('{', false)) { - // switch to nested mode - state.tokenize = tokenNestedExpr(0) - if (stream.current()) return OUTCLASS; - else return state.tokenize(stream, state) - } else if (stream.match('}}')) { - return OUTCLASS; - } else if (stream.match('}')) { - // single } in f-string is an error - return ERRORCLASS; - } else { - stream.eat(/['"]/); - } - } - if (singleline) { - if (parserConf.singleLineStringErrors) - return ERRORCLASS; - else - state.tokenize = tokenOuter; - } - return OUTCLASS; - } - tokenString.isString = true; - return tokenString; - } - - function tokenStringFactory(delimiter, tokenOuter) { - while ("rubf".indexOf(delimiter.charAt(0).toLowerCase()) >= 0) - delimiter = delimiter.substr(1); - - var singleline = delimiter.length == 1; - var OUTCLASS = "string"; - - function tokenString(stream, state) { - while (!stream.eol()) { - stream.eatWhile(/[^'"\\]/); - if (stream.eat("\\")) { - stream.next(); - if (singleline && stream.eol()) - return OUTCLASS; - } else if (stream.match(delimiter)) { - state.tokenize = tokenOuter; - return OUTCLASS; - } else { - stream.eat(/['"]/); - } - } - if (singleline) { - if (parserConf.singleLineStringErrors) - return ERRORCLASS; - else - state.tokenize = tokenOuter; - } - return OUTCLASS; - } - tokenString.isString = true; - return tokenString; - } - - function pushPyScope(state) { - while (top(state).type != "py") state.scopes.pop() - state.scopes.push({offset: top(state).offset + conf.indentUnit, - type: "py", - align: null}) - } - - function pushBracketScope(stream, state, type) { - var align = stream.match(/^([\s\[\{\(]|#.*)*$/, false) ? null : stream.column() + 1 - state.scopes.push({offset: state.indent + hangingIndent, - type: type, - align: align}) - } - - function dedent(stream, state) { - var indented = stream.indentation(); - while (state.scopes.length > 1 && top(state).offset > indented) { - if (top(state).type != "py") return true; - state.scopes.pop(); - } - return top(state).offset != indented; - } - - function tokenLexer(stream, state) { - if (stream.sol()) state.beginningOfLine = true; - - var style = state.tokenize(stream, state); - var current = stream.current(); - - // Handle decorators - if (state.beginningOfLine && current == "@") - return stream.match(identifiers, false) ? "meta" : py3 ? "operator" : ERRORCLASS; - - if (/\S/.test(current)) state.beginningOfLine = false; - - if ((style == "variable" || style == "builtin") - && state.lastToken == "meta") - style = "meta"; - - // Handle scope changes. - if (current == "pass" || current == "return") - state.dedent += 1; - - if (current == "lambda") state.lambda = true; - if (current == ":" && !state.lambda && top(state).type == "py") - pushPyScope(state); - - if (current.length == 1 && !/string|comment/.test(style)) { - var delimiter_index = "[({".indexOf(current); - if (delimiter_index != -1) - pushBracketScope(stream, state, "])}".slice(delimiter_index, delimiter_index+1)); - - delimiter_index = "])}".indexOf(current); - if (delimiter_index != -1) { - if (top(state).type == current) state.indent = state.scopes.pop().offset - hangingIndent - else return ERRORCLASS; - } - } - if (state.dedent > 0 && stream.eol() && top(state).type == "py") { - if (state.scopes.length > 1) state.scopes.pop(); - state.dedent -= 1; - } - - return style; - } - - var external = { - startState: function(basecolumn) { - return { - tokenize: tokenBase, - scopes: [{offset: basecolumn || 0, type: "py", align: null}], - indent: basecolumn || 0, - lastToken: null, - lambda: false, - dedent: 0 - }; - }, - - token: function(stream, state) { - var addErr = state.errorToken; - if (addErr) state.errorToken = false; - var style = tokenLexer(stream, state); - - if (style && style != "comment") - state.lastToken = (style == "keyword" || style == "punctuation") ? stream.current() : style; - if (style == "punctuation") style = null; - - if (stream.eol() && state.lambda) - state.lambda = false; - return addErr ? style + " " + ERRORCLASS : style; - }, - - indent: function(state, textAfter) { - if (state.tokenize != tokenBase) - return state.tokenize.isString ? CodeMirror.Pass : 0; - - var scope = top(state), closing = scope.type == textAfter.charAt(0) - if (scope.align != null) - return scope.align - (closing ? 1 : 0) - else - return scope.offset - (closing ? hangingIndent : 0) - }, - - electricInput: /^\s*[\}\]\)]$/, - closeBrackets: {triples: "'\""}, - lineComment: "#", - fold: "indent" - }; - return external; - }); - - CodeMirror.defineMIME("text/x-python", "python"); - - var words = function(str) { return str.split(" "); }; - - CodeMirror.defineMIME("text/x-cython", { - name: "python", - extra_keywords: words("by cdef cimport cpdef ctypedef enum except "+ - "extern gil include nogil property public "+ - "readonly struct union DEF IF ELIF ELSE") - }); - -}); - - -/* ---- mode/rust.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("../../addon/mode/simple")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "../../addon/mode/simple"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -CodeMirror.defineSimpleMode("rust",{ - start: [ - // string and byte string - {regex: /b?"/, token: "string", next: "string"}, - // raw string and raw byte string - {regex: /b?r"/, token: "string", next: "string_raw"}, - {regex: /b?r#+"/, token: "string", next: "string_raw_hash"}, - // character - {regex: /'(?:[^'\\]|\\(?:[nrt0'"]|x[\da-fA-F]{2}|u\{[\da-fA-F]{6}\}))'/, token: "string-2"}, - // byte - {regex: /b'(?:[^']|\\(?:['\\nrt0]|x[\da-fA-F]{2}))'/, token: "string-2"}, - - {regex: /(?:(?:[0-9][0-9_]*)(?:(?:[Ee][+-]?[0-9_]+)|\.[0-9_]+(?:[Ee][+-]?[0-9_]+)?)(?:f32|f64)?)|(?:0(?:b[01_]+|(?:o[0-7_]+)|(?:x[0-9a-fA-F_]+))|(?:[0-9][0-9_]*))(?:u8|u16|u32|u64|i8|i16|i32|i64|isize|usize)?/, - token: "number"}, - {regex: /(let(?:\s+mut)?|fn|enum|mod|struct|type|union)(\s+)([a-zA-Z_][a-zA-Z0-9_]*)/, token: ["keyword", null, "def"]}, - {regex: /(?:abstract|alignof|as|async|await|box|break|continue|const|crate|do|dyn|else|enum|extern|fn|for|final|if|impl|in|loop|macro|match|mod|move|offsetof|override|priv|proc|pub|pure|ref|return|self|sizeof|static|struct|super|trait|type|typeof|union|unsafe|unsized|use|virtual|where|while|yield)\b/, token: "keyword"}, - {regex: /\b(?:Self|isize|usize|char|bool|u8|u16|u32|u64|f16|f32|f64|i8|i16|i32|i64|str|Option)\b/, token: "atom"}, - {regex: /\b(?:true|false|Some|None|Ok|Err)\b/, token: "builtin"}, - {regex: /\b(fn)(\s+)([a-zA-Z_][a-zA-Z0-9_]*)/, - token: ["keyword", null ,"def"]}, - {regex: /#!?\[.*\]/, token: "meta"}, - {regex: /\/\/.*/, token: "comment"}, - {regex: /\/\*/, token: "comment", next: "comment"}, - {regex: /[-+\/*=<>!]+/, token: "operator"}, - {regex: /[a-zA-Z_]\w*!/,token: "variable-3"}, - {regex: /[a-zA-Z_]\w*/, token: "variable"}, - {regex: /[\{\[\(]/, indent: true}, - {regex: /[\}\]\)]/, dedent: true} - ], - string: [ - {regex: /"/, token: "string", next: "start"}, - {regex: /(?:[^\\"]|\\(?:.|$))*/, token: "string"} - ], - string_raw: [ - {regex: /"/, token: "string", next: "start"}, - {regex: /[^"]*/, token: "string"} - ], - string_raw_hash: [ - {regex: /"#+/, token: "string", next: "start"}, - {regex: /(?:[^"]|"(?!#))*/, token: "string"} - ], - comment: [ - {regex: /.*?\*\//, token: "comment", next: "start"}, - {regex: /.*/, token: "comment"} - ], - meta: { - dontIndentStates: ["comment"], - electricInput: /^\s*\}$/, - blockCommentStart: "/*", - blockCommentEnd: "*/", - lineComment: "//", - fold: "brace" - } -}); - - -CodeMirror.defineMIME("text/x-rustsrc", "rust"); -CodeMirror.defineMIME("text/rust", "rust"); -}); - - -/* ---- mode/xml.js ---- */ - - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -var htmlConfig = { - autoSelfClosers: {'area': true, 'base': true, 'br': true, 'col': true, 'command': true, - 'embed': true, 'frame': true, 'hr': true, 'img': true, 'input': true, - 'keygen': true, 'link': true, 'meta': true, 'param': true, 'source': true, - 'track': true, 'wbr': true, 'menuitem': true}, - implicitlyClosed: {'dd': true, 'li': true, 'optgroup': true, 'option': true, 'p': true, - 'rp': true, 'rt': true, 'tbody': true, 'td': true, 'tfoot': true, - 'th': true, 'tr': true}, - contextGrabbers: { - 'dd': {'dd': true, 'dt': true}, - 'dt': {'dd': true, 'dt': true}, - 'li': {'li': true}, - 'option': {'option': true, 'optgroup': true}, - 'optgroup': {'optgroup': true}, - 'p': {'address': true, 'article': true, 'aside': true, 'blockquote': true, 'dir': true, - 'div': true, 'dl': true, 'fieldset': true, 'footer': true, 'form': true, - 'h1': true, 'h2': true, 'h3': true, 'h4': true, 'h5': true, 'h6': true, - 'header': true, 'hgroup': true, 'hr': true, 'menu': true, 'nav': true, 'ol': true, - 'p': true, 'pre': true, 'section': true, 'table': true, 'ul': true}, - 'rp': {'rp': true, 'rt': true}, - 'rt': {'rp': true, 'rt': true}, - 'tbody': {'tbody': true, 'tfoot': true}, - 'td': {'td': true, 'th': true}, - 'tfoot': {'tbody': true}, - 'th': {'td': true, 'th': true}, - 'thead': {'tbody': true, 'tfoot': true}, - 'tr': {'tr': true} - }, - doNotIndent: {"pre": true}, - allowUnquoted: true, - allowMissing: true, - caseFold: true -} - -var xmlConfig = { - autoSelfClosers: {}, - implicitlyClosed: {}, - contextGrabbers: {}, - doNotIndent: {}, - allowUnquoted: false, - allowMissing: false, - allowMissingTagName: false, - caseFold: false -} - -CodeMirror.defineMode("xml", function(editorConf, config_) { - var indentUnit = editorConf.indentUnit - var config = {} - var defaults = config_.htmlMode ? htmlConfig : xmlConfig - for (var prop in defaults) config[prop] = defaults[prop] - for (var prop in config_) config[prop] = config_[prop] - - // Return variables for tokenizers - var type, setStyle; - - function inText(stream, state) { - function chain(parser) { - state.tokenize = parser; - return parser(stream, state); - } - - var ch = stream.next(); - if (ch == "<") { - if (stream.eat("!")) { - if (stream.eat("[")) { - if (stream.match("CDATA[")) return chain(inBlock("atom", "]]>")); - else return null; - } else if (stream.match("--")) { - return chain(inBlock("comment", "-->")); - } else if (stream.match("DOCTYPE", true, true)) { - stream.eatWhile(/[\w\._\-]/); - return chain(doctype(1)); - } else { - return null; - } - } else if (stream.eat("?")) { - stream.eatWhile(/[\w\._\-]/); - state.tokenize = inBlock("meta", "?>"); - return "meta"; - } else { - type = stream.eat("/") ? "closeTag" : "openTag"; - state.tokenize = inTag; - return "tag bracket"; - } - } else if (ch == "&") { - var ok; - if (stream.eat("#")) { - if (stream.eat("x")) { - ok = stream.eatWhile(/[a-fA-F\d]/) && stream.eat(";"); - } else { - ok = stream.eatWhile(/[\d]/) && stream.eat(";"); - } - } else { - ok = stream.eatWhile(/[\w\.\-:]/) && stream.eat(";"); - } - return ok ? "atom" : "error"; - } else { - stream.eatWhile(/[^&<]/); - return null; - } - } - inText.isInText = true; - - function inTag(stream, state) { - var ch = stream.next(); - if (ch == ">" || (ch == "/" && stream.eat(">"))) { - state.tokenize = inText; - type = ch == ">" ? "endTag" : "selfcloseTag"; - return "tag bracket"; - } else if (ch == "=") { - type = "equals"; - return null; - } else if (ch == "<") { - state.tokenize = inText; - state.state = baseState; - state.tagName = state.tagStart = null; - var next = state.tokenize(stream, state); - return next ? next + " tag error" : "tag error"; - } else if (/[\'\"]/.test(ch)) { - state.tokenize = inAttribute(ch); - state.stringStartCol = stream.column(); - return state.tokenize(stream, state); - } else { - stream.match(/^[^\s\u00a0=<>\"\']*[^\s\u00a0=<>\"\'\/]/); - return "word"; - } - } - - function inAttribute(quote) { - var closure = function(stream, state) { - while (!stream.eol()) { - if (stream.next() == quote) { - state.tokenize = inTag; - break; - } - } - return "string"; - }; - closure.isInAttribute = true; - return closure; - } - - function inBlock(style, terminator) { - return function(stream, state) { - while (!stream.eol()) { - if (stream.match(terminator)) { - state.tokenize = inText; - break; - } - stream.next(); - } - return style; - } - } - - function doctype(depth) { - return function(stream, state) { - var ch; - while ((ch = stream.next()) != null) { - if (ch == "<") { - state.tokenize = doctype(depth + 1); - return state.tokenize(stream, state); - } else if (ch == ">") { - if (depth == 1) { - state.tokenize = inText; - break; - } else { - state.tokenize = doctype(depth - 1); - return state.tokenize(stream, state); - } - } - } - return "meta"; - }; - } - - function Context(state, tagName, startOfLine) { - this.prev = state.context; - this.tagName = tagName; - this.indent = state.indented; - this.startOfLine = startOfLine; - if (config.doNotIndent.hasOwnProperty(tagName) || (state.context && state.context.noIndent)) - this.noIndent = true; - } - function popContext(state) { - if (state.context) state.context = state.context.prev; - } - function maybePopContext(state, nextTagName) { - var parentTagName; - while (true) { - if (!state.context) { - return; - } - parentTagName = state.context.tagName; - if (!config.contextGrabbers.hasOwnProperty(parentTagName) || - !config.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) { - return; - } - popContext(state); - } - } - - function baseState(type, stream, state) { - if (type == "openTag") { - state.tagStart = stream.column(); - return tagNameState; - } else if (type == "closeTag") { - return closeTagNameState; - } else { - return baseState; - } - } - function tagNameState(type, stream, state) { - if (type == "word") { - state.tagName = stream.current(); - setStyle = "tag"; - return attrState; - } else if (config.allowMissingTagName && type == "endTag") { - setStyle = "tag bracket"; - return attrState(type, stream, state); - } else { - setStyle = "error"; - return tagNameState; - } - } - function closeTagNameState(type, stream, state) { - if (type == "word") { - var tagName = stream.current(); - if (state.context && state.context.tagName != tagName && - config.implicitlyClosed.hasOwnProperty(state.context.tagName)) - popContext(state); - if ((state.context && state.context.tagName == tagName) || config.matchClosing === false) { - setStyle = "tag"; - return closeState; - } else { - setStyle = "tag error"; - return closeStateErr; - } - } else if (config.allowMissingTagName && type == "endTag") { - setStyle = "tag bracket"; - return closeState(type, stream, state); - } else { - setStyle = "error"; - return closeStateErr; - } - } - - function closeState(type, _stream, state) { - if (type != "endTag") { - setStyle = "error"; - return closeState; - } - popContext(state); - return baseState; - } - function closeStateErr(type, stream, state) { - setStyle = "error"; - return closeState(type, stream, state); - } - - function attrState(type, _stream, state) { - if (type == "word") { - setStyle = "attribute"; - return attrEqState; - } else if (type == "endTag" || type == "selfcloseTag") { - var tagName = state.tagName, tagStart = state.tagStart; - state.tagName = state.tagStart = null; - if (type == "selfcloseTag" || - config.autoSelfClosers.hasOwnProperty(tagName)) { - maybePopContext(state, tagName); - } else { - maybePopContext(state, tagName); - state.context = new Context(state, tagName, tagStart == state.indented); - } - return baseState; - } - setStyle = "error"; - return attrState; - } - function attrEqState(type, stream, state) { - if (type == "equals") return attrValueState; - if (!config.allowMissing) setStyle = "error"; - return attrState(type, stream, state); - } - function attrValueState(type, stream, state) { - if (type == "string") return attrContinuedState; - if (type == "word" && config.allowUnquoted) {setStyle = "string"; return attrState;} - setStyle = "error"; - return attrState(type, stream, state); - } - function attrContinuedState(type, stream, state) { - if (type == "string") return attrContinuedState; - return attrState(type, stream, state); - } - - return { - startState: function(baseIndent) { - var state = {tokenize: inText, - state: baseState, - indented: baseIndent || 0, - tagName: null, tagStart: null, - context: null} - if (baseIndent != null) state.baseIndent = baseIndent - return state - }, - - token: function(stream, state) { - if (!state.tagName && stream.sol()) - state.indented = stream.indentation(); - - if (stream.eatSpace()) return null; - type = null; - var style = state.tokenize(stream, state); - if ((style || type) && style != "comment") { - setStyle = null; - state.state = state.state(type || style, stream, state); - if (setStyle) - style = setStyle == "error" ? style + " error" : setStyle; - } - return style; - }, - - indent: function(state, textAfter, fullLine) { - var context = state.context; - // Indent multi-line strings (e.g. css). - if (state.tokenize.isInAttribute) { - if (state.tagStart == state.indented) - return state.stringStartCol + 1; - else - return state.indented + indentUnit; - } - if (context && context.noIndent) return CodeMirror.Pass; - if (state.tokenize != inTag && state.tokenize != inText) - return fullLine ? fullLine.match(/^(\s*)/)[0].length : 0; - // Indent the starts of attribute names. - if (state.tagName) { - if (config.multilineTagIndentPastTag !== false) - return state.tagStart + state.tagName.length + 2; - else - return state.tagStart + indentUnit * (config.multilineTagIndentFactor || 1); - } - if (config.alignCDATA && /$/, - blockCommentStart: "", - - configuration: config.htmlMode ? "html" : "xml", - helperType: config.htmlMode ? "html" : "xml", - - skipAttribute: function(state) { - if (state.state == attrValueState) - state.state = attrState - }, - - xmlCurrentTag: function(state) { - return state.tagName ? {name: state.tagName, close: state.type == "closeTag"} : null - }, - - xmlCurrentContext: function(state) { - var context = [] - for (var cx = state.context; cx; cx = cx.prev) - if (cx.tagName) context.push(cx.tagName) - return context.reverse() - } - }; -}); - -CodeMirror.defineMIME("text/xml", "xml"); -CodeMirror.defineMIME("application/xml", "xml"); -if (!CodeMirror.mimeModes.hasOwnProperty("text/html")) - CodeMirror.defineMIME("text/html", {name: "xml", htmlMode: true}); - -}); diff --git a/plugins/UiFileManager/media/codemirror/base/codemirror.css b/plugins/UiFileManager/media/codemirror/base/codemirror.css deleted file mode 100644 index 56896500..00000000 --- a/plugins/UiFileManager/media/codemirror/base/codemirror.css +++ /dev/null @@ -1,349 +0,0 @@ -/* BASICS */ - -.CodeMirror { - /* Set height, width, borders, and global font properties here */ - font-family: monospace; - height: 300px; - color: black; - direction: ltr; -} - -/* PADDING */ - -.CodeMirror-lines { - padding: 4px 0; /* Vertical padding around content */ -} -.CodeMirror pre.CodeMirror-line, -.CodeMirror pre.CodeMirror-line-like { - padding: 0 4px; /* Horizontal padding of content */ -} - -.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { - background-color: white; /* The little square between H and V scrollbars */ -} - -/* GUTTER */ - -.CodeMirror-gutters { - border-right: 1px solid #ddd; - background-color: #f7f7f7; - white-space: nowrap; -} -.CodeMirror-linenumbers {} -.CodeMirror-linenumber { - padding: 0 3px 0 5px; - min-width: 20px; - text-align: right; - color: #999; - white-space: nowrap; -} - -.CodeMirror-guttermarker { color: black; } -.CodeMirror-guttermarker-subtle { color: #999; } - -/* CURSOR */ - -.CodeMirror-cursor { - border-left: 1px solid black; - border-right: none; - width: 0; -} -/* Shown when moving in bi-directional text */ -.CodeMirror div.CodeMirror-secondarycursor { - border-left: 1px solid silver; -} -.cm-fat-cursor .CodeMirror-cursor { - width: auto; - border: 0 !important; - background: #7e7; -} -.cm-fat-cursor div.CodeMirror-cursors { - z-index: 1; -} -.cm-fat-cursor-mark { - background-color: rgba(20, 255, 20, 0.5); - -webkit-animation: blink 1.06s steps(1) infinite; - -moz-animation: blink 1.06s steps(1) infinite; - animation: blink 1.06s steps(1) infinite; -} -.cm-animate-fat-cursor { - width: auto; - border: 0; - -webkit-animation: blink 1.06s steps(1) infinite; - -moz-animation: blink 1.06s steps(1) infinite; - animation: blink 1.06s steps(1) infinite; - background-color: #7e7; -} -@-moz-keyframes blink { - 0% {} - 50% { background-color: transparent; } - 100% {} -} -@-webkit-keyframes blink { - 0% {} - 50% { background-color: transparent; } - 100% {} -} -@keyframes blink { - 0% {} - 50% { background-color: transparent; } - 100% {} -} - -/* Can style cursor different in overwrite (non-insert) mode */ -.CodeMirror-overwrite .CodeMirror-cursor {} - -.cm-tab { display: inline-block; text-decoration: inherit; } - -.CodeMirror-rulers { - position: absolute; - left: 0; right: 0; top: -50px; bottom: 0; - overflow: hidden; -} -.CodeMirror-ruler { - border-left: 1px solid #ccc; - top: 0; bottom: 0; - position: absolute; -} - -/* DEFAULT THEME */ - -.cm-s-default .cm-header {color: blue;} -.cm-s-default .cm-quote {color: #090;} -.cm-negative {color: #d44;} -.cm-positive {color: #292;} -.cm-header, .cm-strong {font-weight: bold;} -.cm-em {font-style: italic;} -.cm-link {text-decoration: underline;} -.cm-strikethrough {text-decoration: line-through;} - -.cm-s-default .cm-keyword {color: #708;} -.cm-s-default .cm-atom {color: #219;} -.cm-s-default .cm-number {color: #164;} -.cm-s-default .cm-def {color: #00f;} -.cm-s-default .cm-variable, -.cm-s-default .cm-punctuation, -.cm-s-default .cm-property, -.cm-s-default .cm-operator {} -.cm-s-default .cm-variable-2 {color: #05a;} -.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;} -.cm-s-default .cm-comment {color: #a50;} -.cm-s-default .cm-string {color: #a11;} -.cm-s-default .cm-string-2 {color: #f50;} -.cm-s-default .cm-meta {color: #555;} -.cm-s-default .cm-qualifier {color: #555;} -.cm-s-default .cm-builtin {color: #30a;} -.cm-s-default .cm-bracket {color: #997;} -.cm-s-default .cm-tag {color: #170;} -.cm-s-default .cm-attribute {color: #00c;} -.cm-s-default .cm-hr {color: #999;} -.cm-s-default .cm-link {color: #00c;} - -.cm-s-default .cm-error {color: #f00;} -.cm-invalidchar {color: #f00;} - -.CodeMirror-composing { border-bottom: 2px solid; } - -/* Default styles for common addons */ - -div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;} -div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;} -.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } -.CodeMirror-activeline-background {background: #e8f2ff;} - -/* STOP */ - -/* The rest of this file contains styles related to the mechanics of - the editor. You probably shouldn't touch them. */ - -.CodeMirror { - position: relative; - overflow: hidden; - background: white; -} - -.CodeMirror-scroll { - overflow: scroll !important; /* Things will break if this is overridden */ - /* 50px is the magic margin used to hide the element's real scrollbars */ - /* See overflow: hidden in .CodeMirror */ - margin-bottom: -50px; margin-right: -50px; - padding-bottom: 50px; - height: 100%; - outline: none; /* Prevent dragging from highlighting the element */ - position: relative; -} -.CodeMirror-sizer { - position: relative; - border-right: 50px solid transparent; -} - -/* The fake, visible scrollbars. Used to force redraw during scrolling - before actual scrolling happens, thus preventing shaking and - flickering artifacts. */ -.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { - position: absolute; - z-index: 6; - display: none; -} -.CodeMirror-vscrollbar { - right: 0; top: 0; - overflow-x: hidden; - overflow-y: scroll; -} -.CodeMirror-hscrollbar { - bottom: 0; left: 0; - overflow-y: hidden; - overflow-x: scroll; -} -.CodeMirror-scrollbar-filler { - right: 0; bottom: 0; -} -.CodeMirror-gutter-filler { - left: 0; bottom: 0; -} - -.CodeMirror-gutters { - position: absolute; left: 0; top: 0; - min-height: 100%; - z-index: 3; -} -.CodeMirror-gutter { - white-space: normal; - height: 100%; - display: inline-block; - vertical-align: top; - margin-bottom: -50px; -} -.CodeMirror-gutter-wrapper { - position: absolute; - z-index: 4; - background: none !important; - border: none !important; -} -.CodeMirror-gutter-background { - position: absolute; - top: 0; bottom: 0; - z-index: 4; -} -.CodeMirror-gutter-elt { - position: absolute; - cursor: default; - z-index: 4; -} -.CodeMirror-gutter-wrapper ::selection { background-color: transparent } -.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent } - -.CodeMirror-lines { - cursor: text; - min-height: 1px; /* prevents collapsing before first draw */ -} -.CodeMirror pre.CodeMirror-line, -.CodeMirror pre.CodeMirror-line-like { - /* Reset some styles that the rest of the page might have set */ - -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; - border-width: 0; - background: transparent; - font-family: inherit; - font-size: inherit; - margin: 0; - white-space: pre; - word-wrap: normal; - line-height: inherit; - color: inherit; - z-index: 2; - position: relative; - overflow: visible; - -webkit-tap-highlight-color: transparent; - -webkit-font-variant-ligatures: contextual; - font-variant-ligatures: contextual; -} -.CodeMirror-wrap pre.CodeMirror-line, -.CodeMirror-wrap pre.CodeMirror-line-like { - word-wrap: break-word; - white-space: pre-wrap; - word-break: normal; -} - -.CodeMirror-linebackground { - position: absolute; - left: 0; right: 0; top: 0; bottom: 0; - z-index: 0; -} - -.CodeMirror-linewidget { - position: relative; - z-index: 2; - padding: 0.1px; /* Force widget margins to stay inside of the container */ -} - -.CodeMirror-widget {} - -.CodeMirror-rtl pre { direction: rtl; } - -.CodeMirror-code { - outline: none; -} - -/* Force content-box sizing for the elements where we expect it */ -.CodeMirror-scroll, -.CodeMirror-sizer, -.CodeMirror-gutter, -.CodeMirror-gutters, -.CodeMirror-linenumber { - -moz-box-sizing: content-box; - box-sizing: content-box; -} - -.CodeMirror-measure { - position: absolute; - width: 100%; - height: 0; - overflow: hidden; - visibility: hidden; -} - -.CodeMirror-cursor { - position: absolute; - pointer-events: none; -} -.CodeMirror-measure pre { position: static; } - -div.CodeMirror-cursors { - visibility: hidden; - position: relative; - z-index: 3; -} -div.CodeMirror-dragcursors { - visibility: visible; -} - -.CodeMirror-focused div.CodeMirror-cursors { - visibility: visible; -} - -.CodeMirror-selected { background: #d9d9d9; } -.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } -.CodeMirror-crosshair { cursor: crosshair; } -.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } -.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } - -.cm-searching { - background-color: #ffa; - background-color: rgba(255, 255, 0, .4); -} - -/* Used to force a border model for a node */ -.cm-force-border { padding-right: .1px; } - -@media print { - /* Hide the cursor when printing */ - .CodeMirror div.CodeMirror-cursors { - visibility: hidden; - } -} - -/* See issue #2901 */ -.cm-tab-wrap-hack:after { content: ''; } - -/* Help users use markselection to safely style text background */ -span.CodeMirror-selectedtext { background: none; } diff --git a/plugins/UiFileManager/media/codemirror/base/codemirror.js b/plugins/UiFileManager/media/codemirror/base/codemirror.js deleted file mode 100644 index 06f0f868..00000000 --- a/plugins/UiFileManager/media/codemirror/base/codemirror.js +++ /dev/null @@ -1,9778 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -// This is CodeMirror (https://codemirror.net), a code editor -// implemented in JavaScript on top of the browser's DOM. -// -// You can find some technical background for some of the code below -// at http://marijnhaverbeke.nl/blog/#cm-internals . - -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - (global = global || self, global.CodeMirror = factory()); -}(this, (function () { 'use strict'; - - // Kludges for bugs and behavior differences that can't be feature - // detected are enabled based on userAgent etc sniffing. - var userAgent = navigator.userAgent; - var platform = navigator.platform; - - var gecko = /gecko\/\d/i.test(userAgent); - var ie_upto10 = /MSIE \d/.test(userAgent); - var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(userAgent); - var edge = /Edge\/(\d+)/.exec(userAgent); - var ie = ie_upto10 || ie_11up || edge; - var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : +(edge || ie_11up)[1]); - var webkit = !edge && /WebKit\//.test(userAgent); - var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(userAgent); - var chrome = !edge && /Chrome\//.test(userAgent); - var presto = /Opera\//.test(userAgent); - var safari = /Apple Computer/.test(navigator.vendor); - var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(userAgent); - var phantom = /PhantomJS/.test(userAgent); - - var ios = !edge && /AppleWebKit/.test(userAgent) && /Mobile\/\w+/.test(userAgent); - var android = /Android/.test(userAgent); - // This is woefully incomplete. Suggestions for alternative methods welcome. - var mobile = ios || android || /webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(userAgent); - var mac = ios || /Mac/.test(platform); - var chromeOS = /\bCrOS\b/.test(userAgent); - var windows = /win/i.test(platform); - - var presto_version = presto && userAgent.match(/Version\/(\d*\.\d*)/); - if (presto_version) { presto_version = Number(presto_version[1]); } - if (presto_version && presto_version >= 15) { presto = false; webkit = true; } - // Some browsers use the wrong event properties to signal cmd/ctrl on OS X - var flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11)); - var captureRightClick = gecko || (ie && ie_version >= 9); - - function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*") } - - var rmClass = function(node, cls) { - var current = node.className; - var match = classTest(cls).exec(current); - if (match) { - var after = current.slice(match.index + match[0].length); - node.className = current.slice(0, match.index) + (after ? match[1] + after : ""); - } - }; - - function removeChildren(e) { - for (var count = e.childNodes.length; count > 0; --count) - { e.removeChild(e.firstChild); } - return e - } - - function removeChildrenAndAdd(parent, e) { - return removeChildren(parent).appendChild(e) - } - - function elt(tag, content, className, style) { - var e = document.createElement(tag); - if (className) { e.className = className; } - if (style) { e.style.cssText = style; } - if (typeof content == "string") { e.appendChild(document.createTextNode(content)); } - else if (content) { for (var i = 0; i < content.length; ++i) { e.appendChild(content[i]); } } - return e - } - // wrapper for elt, which removes the elt from the accessibility tree - function eltP(tag, content, className, style) { - var e = elt(tag, content, className, style); - e.setAttribute("role", "presentation"); - return e - } - - var range; - if (document.createRange) { range = function(node, start, end, endNode) { - var r = document.createRange(); - r.setEnd(endNode || node, end); - r.setStart(node, start); - return r - }; } - else { range = function(node, start, end) { - var r = document.body.createTextRange(); - try { r.moveToElementText(node.parentNode); } - catch(e) { return r } - r.collapse(true); - r.moveEnd("character", end); - r.moveStart("character", start); - return r - }; } - - function contains(parent, child) { - if (child.nodeType == 3) // Android browser always returns false when child is a textnode - { child = child.parentNode; } - if (parent.contains) - { return parent.contains(child) } - do { - if (child.nodeType == 11) { child = child.host; } - if (child == parent) { return true } - } while (child = child.parentNode) - } - - function activeElt() { - // IE and Edge may throw an "Unspecified Error" when accessing document.activeElement. - // IE < 10 will throw when accessed while the page is loading or in an iframe. - // IE > 9 and Edge will throw when accessed in an iframe if document.body is unavailable. - var activeElement; - try { - activeElement = document.activeElement; - } catch(e) { - activeElement = document.body || null; - } - while (activeElement && activeElement.shadowRoot && activeElement.shadowRoot.activeElement) - { activeElement = activeElement.shadowRoot.activeElement; } - return activeElement - } - - function addClass(node, cls) { - var current = node.className; - if (!classTest(cls).test(current)) { node.className += (current ? " " : "") + cls; } - } - function joinClasses(a, b) { - var as = a.split(" "); - for (var i = 0; i < as.length; i++) - { if (as[i] && !classTest(as[i]).test(b)) { b += " " + as[i]; } } - return b - } - - var selectInput = function(node) { node.select(); }; - if (ios) // Mobile Safari apparently has a bug where select() is broken. - { selectInput = function(node) { node.selectionStart = 0; node.selectionEnd = node.value.length; }; } - else if (ie) // Suppress mysterious IE10 errors - { selectInput = function(node) { try { node.select(); } catch(_e) {} }; } - - function bind(f) { - var args = Array.prototype.slice.call(arguments, 1); - return function(){return f.apply(null, args)} - } - - function copyObj(obj, target, overwrite) { - if (!target) { target = {}; } - for (var prop in obj) - { if (obj.hasOwnProperty(prop) && (overwrite !== false || !target.hasOwnProperty(prop))) - { target[prop] = obj[prop]; } } - return target - } - - // Counts the column offset in a string, taking tabs into account. - // Used mostly to find indentation. - function countColumn(string, end, tabSize, startIndex, startValue) { - if (end == null) { - end = string.search(/[^\s\u00a0]/); - if (end == -1) { end = string.length; } - } - for (var i = startIndex || 0, n = startValue || 0;;) { - var nextTab = string.indexOf("\t", i); - if (nextTab < 0 || nextTab >= end) - { return n + (end - i) } - n += nextTab - i; - n += tabSize - (n % tabSize); - i = nextTab + 1; - } - } - - var Delayed = function() { - this.id = null; - this.f = null; - this.time = 0; - this.handler = bind(this.onTimeout, this); - }; - Delayed.prototype.onTimeout = function (self) { - self.id = 0; - if (self.time <= +new Date) { - self.f(); - } else { - setTimeout(self.handler, self.time - +new Date); - } - }; - Delayed.prototype.set = function (ms, f) { - this.f = f; - var time = +new Date + ms; - if (!this.id || time < this.time) { - clearTimeout(this.id); - this.id = setTimeout(this.handler, ms); - this.time = time; - } - }; - - function indexOf(array, elt) { - for (var i = 0; i < array.length; ++i) - { if (array[i] == elt) { return i } } - return -1 - } - - // Number of pixels added to scroller and sizer to hide scrollbar - var scrollerGap = 50; - - // Returned or thrown by various protocols to signal 'I'm not - // handling this'. - var Pass = {toString: function(){return "CodeMirror.Pass"}}; - - // Reused option objects for setSelection & friends - var sel_dontScroll = {scroll: false}, sel_mouse = {origin: "*mouse"}, sel_move = {origin: "+move"}; - - // The inverse of countColumn -- find the offset that corresponds to - // a particular column. - function findColumn(string, goal, tabSize) { - for (var pos = 0, col = 0;;) { - var nextTab = string.indexOf("\t", pos); - if (nextTab == -1) { nextTab = string.length; } - var skipped = nextTab - pos; - if (nextTab == string.length || col + skipped >= goal) - { return pos + Math.min(skipped, goal - col) } - col += nextTab - pos; - col += tabSize - (col % tabSize); - pos = nextTab + 1; - if (col >= goal) { return pos } - } - } - - var spaceStrs = [""]; - function spaceStr(n) { - while (spaceStrs.length <= n) - { spaceStrs.push(lst(spaceStrs) + " "); } - return spaceStrs[n] - } - - function lst(arr) { return arr[arr.length-1] } - - function map(array, f) { - var out = []; - for (var i = 0; i < array.length; i++) { out[i] = f(array[i], i); } - return out - } - - function insertSorted(array, value, score) { - var pos = 0, priority = score(value); - while (pos < array.length && score(array[pos]) <= priority) { pos++; } - array.splice(pos, 0, value); - } - - function nothing() {} - - function createObj(base, props) { - var inst; - if (Object.create) { - inst = Object.create(base); - } else { - nothing.prototype = base; - inst = new nothing(); - } - if (props) { copyObj(props, inst); } - return inst - } - - var nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/; - function isWordCharBasic(ch) { - return /\w/.test(ch) || ch > "\x80" && - (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch)) - } - function isWordChar(ch, helper) { - if (!helper) { return isWordCharBasic(ch) } - if (helper.source.indexOf("\\w") > -1 && isWordCharBasic(ch)) { return true } - return helper.test(ch) - } - - function isEmpty(obj) { - for (var n in obj) { if (obj.hasOwnProperty(n) && obj[n]) { return false } } - return true - } - - // Extending unicode characters. A series of a non-extending char + - // any number of extending chars is treated as a single unit as far - // as editing and measuring is concerned. This is not fully correct, - // since some scripts/fonts/browsers also treat other configurations - // of code points as a group. - var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/; - function isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch) } - - // Returns a number from the range [`0`; `str.length`] unless `pos` is outside that range. - function skipExtendingChars(str, pos, dir) { - while ((dir < 0 ? pos > 0 : pos < str.length) && isExtendingChar(str.charAt(pos))) { pos += dir; } - return pos - } - - // Returns the value from the range [`from`; `to`] that satisfies - // `pred` and is closest to `from`. Assumes that at least `to` - // satisfies `pred`. Supports `from` being greater than `to`. - function findFirst(pred, from, to) { - // At any point we are certain `to` satisfies `pred`, don't know - // whether `from` does. - var dir = from > to ? -1 : 1; - for (;;) { - if (from == to) { return from } - var midF = (from + to) / 2, mid = dir < 0 ? Math.ceil(midF) : Math.floor(midF); - if (mid == from) { return pred(mid) ? from : to } - if (pred(mid)) { to = mid; } - else { from = mid + dir; } - } - } - - // BIDI HELPERS - - function iterateBidiSections(order, from, to, f) { - if (!order) { return f(from, to, "ltr", 0) } - var found = false; - for (var i = 0; i < order.length; ++i) { - var part = order[i]; - if (part.from < to && part.to > from || from == to && part.to == from) { - f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr", i); - found = true; - } - } - if (!found) { f(from, to, "ltr"); } - } - - var bidiOther = null; - function getBidiPartAt(order, ch, sticky) { - var found; - bidiOther = null; - for (var i = 0; i < order.length; ++i) { - var cur = order[i]; - if (cur.from < ch && cur.to > ch) { return i } - if (cur.to == ch) { - if (cur.from != cur.to && sticky == "before") { found = i; } - else { bidiOther = i; } - } - if (cur.from == ch) { - if (cur.from != cur.to && sticky != "before") { found = i; } - else { bidiOther = i; } - } - } - return found != null ? found : bidiOther - } - - // Bidirectional ordering algorithm - // See http://unicode.org/reports/tr9/tr9-13.html for the algorithm - // that this (partially) implements. - - // One-char codes used for character types: - // L (L): Left-to-Right - // R (R): Right-to-Left - // r (AL): Right-to-Left Arabic - // 1 (EN): European Number - // + (ES): European Number Separator - // % (ET): European Number Terminator - // n (AN): Arabic Number - // , (CS): Common Number Separator - // m (NSM): Non-Spacing Mark - // b (BN): Boundary Neutral - // s (B): Paragraph Separator - // t (S): Segment Separator - // w (WS): Whitespace - // N (ON): Other Neutrals - - // Returns null if characters are ordered as they appear - // (left-to-right), or an array of sections ({from, to, level} - // objects) in the order in which they occur visually. - var bidiOrdering = (function() { - // Character types for codepoints 0 to 0xff - var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN"; - // Character types for codepoints 0x600 to 0x6f9 - var arabicTypes = "nnnnnnNNr%%r,rNNmmmmmmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmmmnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmnNmmmmmmrrmmNmmmmrr1111111111"; - function charType(code) { - if (code <= 0xf7) { return lowTypes.charAt(code) } - else if (0x590 <= code && code <= 0x5f4) { return "R" } - else if (0x600 <= code && code <= 0x6f9) { return arabicTypes.charAt(code - 0x600) } - else if (0x6ee <= code && code <= 0x8ac) { return "r" } - else if (0x2000 <= code && code <= 0x200b) { return "w" } - else if (code == 0x200c) { return "b" } - else { return "L" } - } - - var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/; - var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/; - - function BidiSpan(level, from, to) { - this.level = level; - this.from = from; this.to = to; - } - - return function(str, direction) { - var outerType = direction == "ltr" ? "L" : "R"; - - if (str.length == 0 || direction == "ltr" && !bidiRE.test(str)) { return false } - var len = str.length, types = []; - for (var i = 0; i < len; ++i) - { types.push(charType(str.charCodeAt(i))); } - - // W1. Examine each non-spacing mark (NSM) in the level run, and - // change the type of the NSM to the type of the previous - // character. If the NSM is at the start of the level run, it will - // get the type of sor. - for (var i$1 = 0, prev = outerType; i$1 < len; ++i$1) { - var type = types[i$1]; - if (type == "m") { types[i$1] = prev; } - else { prev = type; } - } - - // W2. Search backwards from each instance of a European number - // until the first strong type (R, L, AL, or sor) is found. If an - // AL is found, change the type of the European number to Arabic - // number. - // W3. Change all ALs to R. - for (var i$2 = 0, cur = outerType; i$2 < len; ++i$2) { - var type$1 = types[i$2]; - if (type$1 == "1" && cur == "r") { types[i$2] = "n"; } - else if (isStrong.test(type$1)) { cur = type$1; if (type$1 == "r") { types[i$2] = "R"; } } - } - - // W4. A single European separator between two European numbers - // changes to a European number. A single common separator between - // two numbers of the same type changes to that type. - for (var i$3 = 1, prev$1 = types[0]; i$3 < len - 1; ++i$3) { - var type$2 = types[i$3]; - if (type$2 == "+" && prev$1 == "1" && types[i$3+1] == "1") { types[i$3] = "1"; } - else if (type$2 == "," && prev$1 == types[i$3+1] && - (prev$1 == "1" || prev$1 == "n")) { types[i$3] = prev$1; } - prev$1 = type$2; - } - - // W5. A sequence of European terminators adjacent to European - // numbers changes to all European numbers. - // W6. Otherwise, separators and terminators change to Other - // Neutral. - for (var i$4 = 0; i$4 < len; ++i$4) { - var type$3 = types[i$4]; - if (type$3 == ",") { types[i$4] = "N"; } - else if (type$3 == "%") { - var end = (void 0); - for (end = i$4 + 1; end < len && types[end] == "%"; ++end) {} - var replace = (i$4 && types[i$4-1] == "!") || (end < len && types[end] == "1") ? "1" : "N"; - for (var j = i$4; j < end; ++j) { types[j] = replace; } - i$4 = end - 1; - } - } - - // W7. Search backwards from each instance of a European number - // until the first strong type (R, L, or sor) is found. If an L is - // found, then change the type of the European number to L. - for (var i$5 = 0, cur$1 = outerType; i$5 < len; ++i$5) { - var type$4 = types[i$5]; - if (cur$1 == "L" && type$4 == "1") { types[i$5] = "L"; } - else if (isStrong.test(type$4)) { cur$1 = type$4; } - } - - // N1. A sequence of neutrals takes the direction of the - // surrounding strong text if the text on both sides has the same - // direction. European and Arabic numbers act as if they were R in - // terms of their influence on neutrals. Start-of-level-run (sor) - // and end-of-level-run (eor) are used at level run boundaries. - // N2. Any remaining neutrals take the embedding direction. - for (var i$6 = 0; i$6 < len; ++i$6) { - if (isNeutral.test(types[i$6])) { - var end$1 = (void 0); - for (end$1 = i$6 + 1; end$1 < len && isNeutral.test(types[end$1]); ++end$1) {} - var before = (i$6 ? types[i$6-1] : outerType) == "L"; - var after = (end$1 < len ? types[end$1] : outerType) == "L"; - var replace$1 = before == after ? (before ? "L" : "R") : outerType; - for (var j$1 = i$6; j$1 < end$1; ++j$1) { types[j$1] = replace$1; } - i$6 = end$1 - 1; - } - } - - // Here we depart from the documented algorithm, in order to avoid - // building up an actual levels array. Since there are only three - // levels (0, 1, 2) in an implementation that doesn't take - // explicit embedding into account, we can build up the order on - // the fly, without following the level-based algorithm. - var order = [], m; - for (var i$7 = 0; i$7 < len;) { - if (countsAsLeft.test(types[i$7])) { - var start = i$7; - for (++i$7; i$7 < len && countsAsLeft.test(types[i$7]); ++i$7) {} - order.push(new BidiSpan(0, start, i$7)); - } else { - var pos = i$7, at = order.length, isRTL = direction == "rtl" ? 1 : 0; - for (++i$7; i$7 < len && types[i$7] != "L"; ++i$7) {} - for (var j$2 = pos; j$2 < i$7;) { - if (countsAsNum.test(types[j$2])) { - if (pos < j$2) { order.splice(at, 0, new BidiSpan(1, pos, j$2)); at += isRTL; } - var nstart = j$2; - for (++j$2; j$2 < i$7 && countsAsNum.test(types[j$2]); ++j$2) {} - order.splice(at, 0, new BidiSpan(2, nstart, j$2)); - at += isRTL; - pos = j$2; - } else { ++j$2; } - } - if (pos < i$7) { order.splice(at, 0, new BidiSpan(1, pos, i$7)); } - } - } - if (direction == "ltr") { - if (order[0].level == 1 && (m = str.match(/^\s+/))) { - order[0].from = m[0].length; - order.unshift(new BidiSpan(0, 0, m[0].length)); - } - if (lst(order).level == 1 && (m = str.match(/\s+$/))) { - lst(order).to -= m[0].length; - order.push(new BidiSpan(0, len - m[0].length, len)); - } - } - - return direction == "rtl" ? order.reverse() : order - } - })(); - - // Get the bidi ordering for the given line (and cache it). Returns - // false for lines that are fully left-to-right, and an array of - // BidiSpan objects otherwise. - function getOrder(line, direction) { - var order = line.order; - if (order == null) { order = line.order = bidiOrdering(line.text, direction); } - return order - } - - // EVENT HANDLING - - // Lightweight event framework. on/off also work on DOM nodes, - // registering native DOM handlers. - - var noHandlers = []; - - var on = function(emitter, type, f) { - if (emitter.addEventListener) { - emitter.addEventListener(type, f, false); - } else if (emitter.attachEvent) { - emitter.attachEvent("on" + type, f); - } else { - var map = emitter._handlers || (emitter._handlers = {}); - map[type] = (map[type] || noHandlers).concat(f); - } - }; - - function getHandlers(emitter, type) { - return emitter._handlers && emitter._handlers[type] || noHandlers - } - - function off(emitter, type, f) { - if (emitter.removeEventListener) { - emitter.removeEventListener(type, f, false); - } else if (emitter.detachEvent) { - emitter.detachEvent("on" + type, f); - } else { - var map = emitter._handlers, arr = map && map[type]; - if (arr) { - var index = indexOf(arr, f); - if (index > -1) - { map[type] = arr.slice(0, index).concat(arr.slice(index + 1)); } - } - } - } - - function signal(emitter, type /*, values...*/) { - var handlers = getHandlers(emitter, type); - if (!handlers.length) { return } - var args = Array.prototype.slice.call(arguments, 2); - for (var i = 0; i < handlers.length; ++i) { handlers[i].apply(null, args); } - } - - // The DOM events that CodeMirror handles can be overridden by - // registering a (non-DOM) handler on the editor for the event name, - // and preventDefault-ing the event in that handler. - function signalDOMEvent(cm, e, override) { - if (typeof e == "string") - { e = {type: e, preventDefault: function() { this.defaultPrevented = true; }}; } - signal(cm, override || e.type, cm, e); - return e_defaultPrevented(e) || e.codemirrorIgnore - } - - function signalCursorActivity(cm) { - var arr = cm._handlers && cm._handlers.cursorActivity; - if (!arr) { return } - var set = cm.curOp.cursorActivityHandlers || (cm.curOp.cursorActivityHandlers = []); - for (var i = 0; i < arr.length; ++i) { if (indexOf(set, arr[i]) == -1) - { set.push(arr[i]); } } - } - - function hasHandler(emitter, type) { - return getHandlers(emitter, type).length > 0 - } - - // Add on and off methods to a constructor's prototype, to make - // registering events on such objects more convenient. - function eventMixin(ctor) { - ctor.prototype.on = function(type, f) {on(this, type, f);}; - ctor.prototype.off = function(type, f) {off(this, type, f);}; - } - - // Due to the fact that we still support jurassic IE versions, some - // compatibility wrappers are needed. - - function e_preventDefault(e) { - if (e.preventDefault) { e.preventDefault(); } - else { e.returnValue = false; } - } - function e_stopPropagation(e) { - if (e.stopPropagation) { e.stopPropagation(); } - else { e.cancelBubble = true; } - } - function e_defaultPrevented(e) { - return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false - } - function e_stop(e) {e_preventDefault(e); e_stopPropagation(e);} - - function e_target(e) {return e.target || e.srcElement} - function e_button(e) { - var b = e.which; - if (b == null) { - if (e.button & 1) { b = 1; } - else if (e.button & 2) { b = 3; } - else if (e.button & 4) { b = 2; } - } - if (mac && e.ctrlKey && b == 1) { b = 3; } - return b - } - - // Detect drag-and-drop - var dragAndDrop = function() { - // There is *some* kind of drag-and-drop support in IE6-8, but I - // couldn't get it to work yet. - if (ie && ie_version < 9) { return false } - var div = elt('div'); - return "draggable" in div || "dragDrop" in div - }(); - - var zwspSupported; - function zeroWidthElement(measure) { - if (zwspSupported == null) { - var test = elt("span", "\u200b"); - removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")])); - if (measure.firstChild.offsetHeight != 0) - { zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !(ie && ie_version < 8); } - } - var node = zwspSupported ? elt("span", "\u200b") : - elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px"); - node.setAttribute("cm-text", ""); - return node - } - - // Feature-detect IE's crummy client rect reporting for bidi text - var badBidiRects; - function hasBadBidiRects(measure) { - if (badBidiRects != null) { return badBidiRects } - var txt = removeChildrenAndAdd(measure, document.createTextNode("A\u062eA")); - var r0 = range(txt, 0, 1).getBoundingClientRect(); - var r1 = range(txt, 1, 2).getBoundingClientRect(); - removeChildren(measure); - if (!r0 || r0.left == r0.right) { return false } // Safari returns null in some cases (#2780) - return badBidiRects = (r1.right - r0.right < 3) - } - - // See if "".split is the broken IE version, if so, provide an - // alternative way to split lines. - var splitLinesAuto = "\n\nb".split(/\n/).length != 3 ? function (string) { - var pos = 0, result = [], l = string.length; - while (pos <= l) { - var nl = string.indexOf("\n", pos); - if (nl == -1) { nl = string.length; } - var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl); - var rt = line.indexOf("\r"); - if (rt != -1) { - result.push(line.slice(0, rt)); - pos += rt + 1; - } else { - result.push(line); - pos = nl + 1; - } - } - return result - } : function (string) { return string.split(/\r\n?|\n/); }; - - var hasSelection = window.getSelection ? function (te) { - try { return te.selectionStart != te.selectionEnd } - catch(e) { return false } - } : function (te) { - var range; - try {range = te.ownerDocument.selection.createRange();} - catch(e) {} - if (!range || range.parentElement() != te) { return false } - return range.compareEndPoints("StartToEnd", range) != 0 - }; - - var hasCopyEvent = (function () { - var e = elt("div"); - if ("oncopy" in e) { return true } - e.setAttribute("oncopy", "return;"); - return typeof e.oncopy == "function" - })(); - - var badZoomedRects = null; - function hasBadZoomedRects(measure) { - if (badZoomedRects != null) { return badZoomedRects } - var node = removeChildrenAndAdd(measure, elt("span", "x")); - var normal = node.getBoundingClientRect(); - var fromRange = range(node, 0, 1).getBoundingClientRect(); - return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1 - } - - // Known modes, by name and by MIME - var modes = {}, mimeModes = {}; - - // Extra arguments are stored as the mode's dependencies, which is - // used by (legacy) mechanisms like loadmode.js to automatically - // load a mode. (Preferred mechanism is the require/define calls.) - function defineMode(name, mode) { - if (arguments.length > 2) - { mode.dependencies = Array.prototype.slice.call(arguments, 2); } - modes[name] = mode; - } - - function defineMIME(mime, spec) { - mimeModes[mime] = spec; - } - - // Given a MIME type, a {name, ...options} config object, or a name - // string, return a mode config object. - function resolveMode(spec) { - if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) { - spec = mimeModes[spec]; - } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) { - var found = mimeModes[spec.name]; - if (typeof found == "string") { found = {name: found}; } - spec = createObj(found, spec); - spec.name = found.name; - } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) { - return resolveMode("application/xml") - } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+json$/.test(spec)) { - return resolveMode("application/json") - } - if (typeof spec == "string") { return {name: spec} } - else { return spec || {name: "null"} } - } - - // Given a mode spec (anything that resolveMode accepts), find and - // initialize an actual mode object. - function getMode(options, spec) { - spec = resolveMode(spec); - var mfactory = modes[spec.name]; - if (!mfactory) { return getMode(options, "text/plain") } - var modeObj = mfactory(options, spec); - if (modeExtensions.hasOwnProperty(spec.name)) { - var exts = modeExtensions[spec.name]; - for (var prop in exts) { - if (!exts.hasOwnProperty(prop)) { continue } - if (modeObj.hasOwnProperty(prop)) { modeObj["_" + prop] = modeObj[prop]; } - modeObj[prop] = exts[prop]; - } - } - modeObj.name = spec.name; - if (spec.helperType) { modeObj.helperType = spec.helperType; } - if (spec.modeProps) { for (var prop$1 in spec.modeProps) - { modeObj[prop$1] = spec.modeProps[prop$1]; } } - - return modeObj - } - - // This can be used to attach properties to mode objects from - // outside the actual mode definition. - var modeExtensions = {}; - function extendMode(mode, properties) { - var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {}); - copyObj(properties, exts); - } - - function copyState(mode, state) { - if (state === true) { return state } - if (mode.copyState) { return mode.copyState(state) } - var nstate = {}; - for (var n in state) { - var val = state[n]; - if (val instanceof Array) { val = val.concat([]); } - nstate[n] = val; - } - return nstate - } - - // Given a mode and a state (for that mode), find the inner mode and - // state at the position that the state refers to. - function innerMode(mode, state) { - var info; - while (mode.innerMode) { - info = mode.innerMode(state); - if (!info || info.mode == mode) { break } - state = info.state; - mode = info.mode; - } - return info || {mode: mode, state: state} - } - - function startState(mode, a1, a2) { - return mode.startState ? mode.startState(a1, a2) : true - } - - // STRING STREAM - - // Fed to the mode parsers, provides helper functions to make - // parsers more succinct. - - var StringStream = function(string, tabSize, lineOracle) { - this.pos = this.start = 0; - this.string = string; - this.tabSize = tabSize || 8; - this.lastColumnPos = this.lastColumnValue = 0; - this.lineStart = 0; - this.lineOracle = lineOracle; - }; - - StringStream.prototype.eol = function () {return this.pos >= this.string.length}; - StringStream.prototype.sol = function () {return this.pos == this.lineStart}; - StringStream.prototype.peek = function () {return this.string.charAt(this.pos) || undefined}; - StringStream.prototype.next = function () { - if (this.pos < this.string.length) - { return this.string.charAt(this.pos++) } - }; - StringStream.prototype.eat = function (match) { - var ch = this.string.charAt(this.pos); - var ok; - if (typeof match == "string") { ok = ch == match; } - else { ok = ch && (match.test ? match.test(ch) : match(ch)); } - if (ok) {++this.pos; return ch} - }; - StringStream.prototype.eatWhile = function (match) { - var start = this.pos; - while (this.eat(match)){} - return this.pos > start - }; - StringStream.prototype.eatSpace = function () { - var start = this.pos; - while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) { ++this.pos; } - return this.pos > start - }; - StringStream.prototype.skipToEnd = function () {this.pos = this.string.length;}; - StringStream.prototype.skipTo = function (ch) { - var found = this.string.indexOf(ch, this.pos); - if (found > -1) {this.pos = found; return true} - }; - StringStream.prototype.backUp = function (n) {this.pos -= n;}; - StringStream.prototype.column = function () { - if (this.lastColumnPos < this.start) { - this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue); - this.lastColumnPos = this.start; - } - return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0) - }; - StringStream.prototype.indentation = function () { - return countColumn(this.string, null, this.tabSize) - - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0) - }; - StringStream.prototype.match = function (pattern, consume, caseInsensitive) { - if (typeof pattern == "string") { - var cased = function (str) { return caseInsensitive ? str.toLowerCase() : str; }; - var substr = this.string.substr(this.pos, pattern.length); - if (cased(substr) == cased(pattern)) { - if (consume !== false) { this.pos += pattern.length; } - return true - } - } else { - var match = this.string.slice(this.pos).match(pattern); - if (match && match.index > 0) { return null } - if (match && consume !== false) { this.pos += match[0].length; } - return match - } - }; - StringStream.prototype.current = function (){return this.string.slice(this.start, this.pos)}; - StringStream.prototype.hideFirstChars = function (n, inner) { - this.lineStart += n; - try { return inner() } - finally { this.lineStart -= n; } - }; - StringStream.prototype.lookAhead = function (n) { - var oracle = this.lineOracle; - return oracle && oracle.lookAhead(n) - }; - StringStream.prototype.baseToken = function () { - var oracle = this.lineOracle; - return oracle && oracle.baseToken(this.pos) - }; - - // Find the line object corresponding to the given line number. - function getLine(doc, n) { - n -= doc.first; - if (n < 0 || n >= doc.size) { throw new Error("There is no line " + (n + doc.first) + " in the document.") } - var chunk = doc; - while (!chunk.lines) { - for (var i = 0;; ++i) { - var child = chunk.children[i], sz = child.chunkSize(); - if (n < sz) { chunk = child; break } - n -= sz; - } - } - return chunk.lines[n] - } - - // Get the part of a document between two positions, as an array of - // strings. - function getBetween(doc, start, end) { - var out = [], n = start.line; - doc.iter(start.line, end.line + 1, function (line) { - var text = line.text; - if (n == end.line) { text = text.slice(0, end.ch); } - if (n == start.line) { text = text.slice(start.ch); } - out.push(text); - ++n; - }); - return out - } - // Get the lines between from and to, as array of strings. - function getLines(doc, from, to) { - var out = []; - doc.iter(from, to, function (line) { out.push(line.text); }); // iter aborts when callback returns truthy value - return out - } - - // Update the height of a line, propagating the height change - // upwards to parent nodes. - function updateLineHeight(line, height) { - var diff = height - line.height; - if (diff) { for (var n = line; n; n = n.parent) { n.height += diff; } } - } - - // Given a line object, find its line number by walking up through - // its parent links. - function lineNo(line) { - if (line.parent == null) { return null } - var cur = line.parent, no = indexOf(cur.lines, line); - for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) { - for (var i = 0;; ++i) { - if (chunk.children[i] == cur) { break } - no += chunk.children[i].chunkSize(); - } - } - return no + cur.first - } - - // Find the line at the given vertical position, using the height - // information in the document tree. - function lineAtHeight(chunk, h) { - var n = chunk.first; - outer: do { - for (var i$1 = 0; i$1 < chunk.children.length; ++i$1) { - var child = chunk.children[i$1], ch = child.height; - if (h < ch) { chunk = child; continue outer } - h -= ch; - n += child.chunkSize(); - } - return n - } while (!chunk.lines) - var i = 0; - for (; i < chunk.lines.length; ++i) { - var line = chunk.lines[i], lh = line.height; - if (h < lh) { break } - h -= lh; - } - return n + i - } - - function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size} - - function lineNumberFor(options, i) { - return String(options.lineNumberFormatter(i + options.firstLineNumber)) - } - - // A Pos instance represents a position within the text. - function Pos(line, ch, sticky) { - if ( sticky === void 0 ) sticky = null; - - if (!(this instanceof Pos)) { return new Pos(line, ch, sticky) } - this.line = line; - this.ch = ch; - this.sticky = sticky; - } - - // Compare two positions, return 0 if they are the same, a negative - // number when a is less, and a positive number otherwise. - function cmp(a, b) { return a.line - b.line || a.ch - b.ch } - - function equalCursorPos(a, b) { return a.sticky == b.sticky && cmp(a, b) == 0 } - - function copyPos(x) {return Pos(x.line, x.ch)} - function maxPos(a, b) { return cmp(a, b) < 0 ? b : a } - function minPos(a, b) { return cmp(a, b) < 0 ? a : b } - - // Most of the external API clips given positions to make sure they - // actually exist within the document. - function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1))} - function clipPos(doc, pos) { - if (pos.line < doc.first) { return Pos(doc.first, 0) } - var last = doc.first + doc.size - 1; - if (pos.line > last) { return Pos(last, getLine(doc, last).text.length) } - return clipToLen(pos, getLine(doc, pos.line).text.length) - } - function clipToLen(pos, linelen) { - var ch = pos.ch; - if (ch == null || ch > linelen) { return Pos(pos.line, linelen) } - else if (ch < 0) { return Pos(pos.line, 0) } - else { return pos } - } - function clipPosArray(doc, array) { - var out = []; - for (var i = 0; i < array.length; i++) { out[i] = clipPos(doc, array[i]); } - return out - } - - var SavedContext = function(state, lookAhead) { - this.state = state; - this.lookAhead = lookAhead; - }; - - var Context = function(doc, state, line, lookAhead) { - this.state = state; - this.doc = doc; - this.line = line; - this.maxLookAhead = lookAhead || 0; - this.baseTokens = null; - this.baseTokenPos = 1; - }; - - Context.prototype.lookAhead = function (n) { - var line = this.doc.getLine(this.line + n); - if (line != null && n > this.maxLookAhead) { this.maxLookAhead = n; } - return line - }; - - Context.prototype.baseToken = function (n) { - if (!this.baseTokens) { return null } - while (this.baseTokens[this.baseTokenPos] <= n) - { this.baseTokenPos += 2; } - var type = this.baseTokens[this.baseTokenPos + 1]; - return {type: type && type.replace(/( |^)overlay .*/, ""), - size: this.baseTokens[this.baseTokenPos] - n} - }; - - Context.prototype.nextLine = function () { - this.line++; - if (this.maxLookAhead > 0) { this.maxLookAhead--; } - }; - - Context.fromSaved = function (doc, saved, line) { - if (saved instanceof SavedContext) - { return new Context(doc, copyState(doc.mode, saved.state), line, saved.lookAhead) } - else - { return new Context(doc, copyState(doc.mode, saved), line) } - }; - - Context.prototype.save = function (copy) { - var state = copy !== false ? copyState(this.doc.mode, this.state) : this.state; - return this.maxLookAhead > 0 ? new SavedContext(state, this.maxLookAhead) : state - }; - - - // Compute a style array (an array starting with a mode generation - // -- for invalidation -- followed by pairs of end positions and - // style strings), which is used to highlight the tokens on the - // line. - function highlightLine(cm, line, context, forceToEnd) { - // A styles array always starts with a number identifying the - // mode/overlays that it is based on (for easy invalidation). - var st = [cm.state.modeGen], lineClasses = {}; - // Compute the base array of styles - runMode(cm, line.text, cm.doc.mode, context, function (end, style) { return st.push(end, style); }, - lineClasses, forceToEnd); - var state = context.state; - - // Run overlays, adjust style array. - var loop = function ( o ) { - context.baseTokens = st; - var overlay = cm.state.overlays[o], i = 1, at = 0; - context.state = true; - runMode(cm, line.text, overlay.mode, context, function (end, style) { - var start = i; - // Ensure there's a token end at the current position, and that i points at it - while (at < end) { - var i_end = st[i]; - if (i_end > end) - { st.splice(i, 1, end, st[i+1], i_end); } - i += 2; - at = Math.min(end, i_end); - } - if (!style) { return } - if (overlay.opaque) { - st.splice(start, i - start, end, "overlay " + style); - i = start + 2; - } else { - for (; start < i; start += 2) { - var cur = st[start+1]; - st[start+1] = (cur ? cur + " " : "") + "overlay " + style; - } - } - }, lineClasses); - context.state = state; - context.baseTokens = null; - context.baseTokenPos = 1; - }; - - for (var o = 0; o < cm.state.overlays.length; ++o) loop( o ); - - return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null} - } - - function getLineStyles(cm, line, updateFrontier) { - if (!line.styles || line.styles[0] != cm.state.modeGen) { - var context = getContextBefore(cm, lineNo(line)); - var resetState = line.text.length > cm.options.maxHighlightLength && copyState(cm.doc.mode, context.state); - var result = highlightLine(cm, line, context); - if (resetState) { context.state = resetState; } - line.stateAfter = context.save(!resetState); - line.styles = result.styles; - if (result.classes) { line.styleClasses = result.classes; } - else if (line.styleClasses) { line.styleClasses = null; } - if (updateFrontier === cm.doc.highlightFrontier) - { cm.doc.modeFrontier = Math.max(cm.doc.modeFrontier, ++cm.doc.highlightFrontier); } - } - return line.styles - } - - function getContextBefore(cm, n, precise) { - var doc = cm.doc, display = cm.display; - if (!doc.mode.startState) { return new Context(doc, true, n) } - var start = findStartLine(cm, n, precise); - var saved = start > doc.first && getLine(doc, start - 1).stateAfter; - var context = saved ? Context.fromSaved(doc, saved, start) : new Context(doc, startState(doc.mode), start); - - doc.iter(start, n, function (line) { - processLine(cm, line.text, context); - var pos = context.line; - line.stateAfter = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo ? context.save() : null; - context.nextLine(); - }); - if (precise) { doc.modeFrontier = context.line; } - return context - } - - // Lightweight form of highlight -- proceed over this line and - // update state, but don't save a style array. Used for lines that - // aren't currently visible. - function processLine(cm, text, context, startAt) { - var mode = cm.doc.mode; - var stream = new StringStream(text, cm.options.tabSize, context); - stream.start = stream.pos = startAt || 0; - if (text == "") { callBlankLine(mode, context.state); } - while (!stream.eol()) { - readToken(mode, stream, context.state); - stream.start = stream.pos; - } - } - - function callBlankLine(mode, state) { - if (mode.blankLine) { return mode.blankLine(state) } - if (!mode.innerMode) { return } - var inner = innerMode(mode, state); - if (inner.mode.blankLine) { return inner.mode.blankLine(inner.state) } - } - - function readToken(mode, stream, state, inner) { - for (var i = 0; i < 10; i++) { - if (inner) { inner[0] = innerMode(mode, state).mode; } - var style = mode.token(stream, state); - if (stream.pos > stream.start) { return style } - } - throw new Error("Mode " + mode.name + " failed to advance stream.") - } - - var Token = function(stream, type, state) { - this.start = stream.start; this.end = stream.pos; - this.string = stream.current(); - this.type = type || null; - this.state = state; - }; - - // Utility for getTokenAt and getLineTokens - function takeToken(cm, pos, precise, asArray) { - var doc = cm.doc, mode = doc.mode, style; - pos = clipPos(doc, pos); - var line = getLine(doc, pos.line), context = getContextBefore(cm, pos.line, precise); - var stream = new StringStream(line.text, cm.options.tabSize, context), tokens; - if (asArray) { tokens = []; } - while ((asArray || stream.pos < pos.ch) && !stream.eol()) { - stream.start = stream.pos; - style = readToken(mode, stream, context.state); - if (asArray) { tokens.push(new Token(stream, style, copyState(doc.mode, context.state))); } - } - return asArray ? tokens : new Token(stream, style, context.state) - } - - function extractLineClasses(type, output) { - if (type) { for (;;) { - var lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/); - if (!lineClass) { break } - type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length); - var prop = lineClass[1] ? "bgClass" : "textClass"; - if (output[prop] == null) - { output[prop] = lineClass[2]; } - else if (!(new RegExp("(?:^|\\s)" + lineClass[2] + "(?:$|\\s)")).test(output[prop])) - { output[prop] += " " + lineClass[2]; } - } } - return type - } - - // Run the given mode's parser over a line, calling f for each token. - function runMode(cm, text, mode, context, f, lineClasses, forceToEnd) { - var flattenSpans = mode.flattenSpans; - if (flattenSpans == null) { flattenSpans = cm.options.flattenSpans; } - var curStart = 0, curStyle = null; - var stream = new StringStream(text, cm.options.tabSize, context), style; - var inner = cm.options.addModeClass && [null]; - if (text == "") { extractLineClasses(callBlankLine(mode, context.state), lineClasses); } - while (!stream.eol()) { - if (stream.pos > cm.options.maxHighlightLength) { - flattenSpans = false; - if (forceToEnd) { processLine(cm, text, context, stream.pos); } - stream.pos = text.length; - style = null; - } else { - style = extractLineClasses(readToken(mode, stream, context.state, inner), lineClasses); - } - if (inner) { - var mName = inner[0].name; - if (mName) { style = "m-" + (style ? mName + " " + style : mName); } - } - if (!flattenSpans || curStyle != style) { - while (curStart < stream.start) { - curStart = Math.min(stream.start, curStart + 5000); - f(curStart, curStyle); - } - curStyle = style; - } - stream.start = stream.pos; - } - while (curStart < stream.pos) { - // Webkit seems to refuse to render text nodes longer than 57444 - // characters, and returns inaccurate measurements in nodes - // starting around 5000 chars. - var pos = Math.min(stream.pos, curStart + 5000); - f(pos, curStyle); - curStart = pos; - } - } - - // Finds the line to start with when starting a parse. Tries to - // find a line with a stateAfter, so that it can start with a - // valid state. If that fails, it returns the line with the - // smallest indentation, which tends to need the least context to - // parse correctly. - function findStartLine(cm, n, precise) { - var minindent, minline, doc = cm.doc; - var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100); - for (var search = n; search > lim; --search) { - if (search <= doc.first) { return doc.first } - var line = getLine(doc, search - 1), after = line.stateAfter; - if (after && (!precise || search + (after instanceof SavedContext ? after.lookAhead : 0) <= doc.modeFrontier)) - { return search } - var indented = countColumn(line.text, null, cm.options.tabSize); - if (minline == null || minindent > indented) { - minline = search - 1; - minindent = indented; - } - } - return minline - } - - function retreatFrontier(doc, n) { - doc.modeFrontier = Math.min(doc.modeFrontier, n); - if (doc.highlightFrontier < n - 10) { return } - var start = doc.first; - for (var line = n - 1; line > start; line--) { - var saved = getLine(doc, line).stateAfter; - // change is on 3 - // state on line 1 looked ahead 2 -- so saw 3 - // test 1 + 2 < 3 should cover this - if (saved && (!(saved instanceof SavedContext) || line + saved.lookAhead < n)) { - start = line + 1; - break - } - } - doc.highlightFrontier = Math.min(doc.highlightFrontier, start); - } - - // Optimize some code when these features are not used. - var sawReadOnlySpans = false, sawCollapsedSpans = false; - - function seeReadOnlySpans() { - sawReadOnlySpans = true; - } - - function seeCollapsedSpans() { - sawCollapsedSpans = true; - } - - // TEXTMARKER SPANS - - function MarkedSpan(marker, from, to) { - this.marker = marker; - this.from = from; this.to = to; - } - - // Search an array of spans for a span matching the given marker. - function getMarkedSpanFor(spans, marker) { - if (spans) { for (var i = 0; i < spans.length; ++i) { - var span = spans[i]; - if (span.marker == marker) { return span } - } } - } - // Remove a span from an array, returning undefined if no spans are - // left (we don't store arrays for lines without spans). - function removeMarkedSpan(spans, span) { - var r; - for (var i = 0; i < spans.length; ++i) - { if (spans[i] != span) { (r || (r = [])).push(spans[i]); } } - return r - } - // Add a span to a line. - function addMarkedSpan(line, span) { - line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span]; - span.marker.attachLine(line); - } - - // Used for the algorithm that adjusts markers for a change in the - // document. These functions cut an array of spans at a given - // character position, returning an array of remaining chunks (or - // undefined if nothing remains). - function markedSpansBefore(old, startCh, isInsert) { - var nw; - if (old) { for (var i = 0; i < old.length; ++i) { - var span = old[i], marker = span.marker; - var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh); - if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) { - var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh) - ;(nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to)); - } - } } - return nw - } - function markedSpansAfter(old, endCh, isInsert) { - var nw; - if (old) { for (var i = 0; i < old.length; ++i) { - var span = old[i], marker = span.marker; - var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh); - if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) { - var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh) - ;(nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh, - span.to == null ? null : span.to - endCh)); - } - } } - return nw - } - - // Given a change object, compute the new set of marker spans that - // cover the line in which the change took place. Removes spans - // entirely within the change, reconnects spans belonging to the - // same marker that appear on both sides of the change, and cuts off - // spans partially within the change. Returns an array of span - // arrays with one element for each line in (after) the change. - function stretchSpansOverChange(doc, change) { - if (change.full) { return null } - var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans; - var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans; - if (!oldFirst && !oldLast) { return null } - - var startCh = change.from.ch, endCh = change.to.ch, isInsert = cmp(change.from, change.to) == 0; - // Get the spans that 'stick out' on both sides - var first = markedSpansBefore(oldFirst, startCh, isInsert); - var last = markedSpansAfter(oldLast, endCh, isInsert); - - // Next, merge those two ends - var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0); - if (first) { - // Fix up .to properties of first - for (var i = 0; i < first.length; ++i) { - var span = first[i]; - if (span.to == null) { - var found = getMarkedSpanFor(last, span.marker); - if (!found) { span.to = startCh; } - else if (sameLine) { span.to = found.to == null ? null : found.to + offset; } - } - } - } - if (last) { - // Fix up .from in last (or move them into first in case of sameLine) - for (var i$1 = 0; i$1 < last.length; ++i$1) { - var span$1 = last[i$1]; - if (span$1.to != null) { span$1.to += offset; } - if (span$1.from == null) { - var found$1 = getMarkedSpanFor(first, span$1.marker); - if (!found$1) { - span$1.from = offset; - if (sameLine) { (first || (first = [])).push(span$1); } - } - } else { - span$1.from += offset; - if (sameLine) { (first || (first = [])).push(span$1); } - } - } - } - // Make sure we didn't create any zero-length spans - if (first) { first = clearEmptySpans(first); } - if (last && last != first) { last = clearEmptySpans(last); } - - var newMarkers = [first]; - if (!sameLine) { - // Fill gap with whole-line-spans - var gap = change.text.length - 2, gapMarkers; - if (gap > 0 && first) - { for (var i$2 = 0; i$2 < first.length; ++i$2) - { if (first[i$2].to == null) - { (gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i$2].marker, null, null)); } } } - for (var i$3 = 0; i$3 < gap; ++i$3) - { newMarkers.push(gapMarkers); } - newMarkers.push(last); - } - return newMarkers - } - - // Remove spans that are empty and don't have a clearWhenEmpty - // option of false. - function clearEmptySpans(spans) { - for (var i = 0; i < spans.length; ++i) { - var span = spans[i]; - if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false) - { spans.splice(i--, 1); } - } - if (!spans.length) { return null } - return spans - } - - // Used to 'clip' out readOnly ranges when making a change. - function removeReadOnlyRanges(doc, from, to) { - var markers = null; - doc.iter(from.line, to.line + 1, function (line) { - if (line.markedSpans) { for (var i = 0; i < line.markedSpans.length; ++i) { - var mark = line.markedSpans[i].marker; - if (mark.readOnly && (!markers || indexOf(markers, mark) == -1)) - { (markers || (markers = [])).push(mark); } - } } - }); - if (!markers) { return null } - var parts = [{from: from, to: to}]; - for (var i = 0; i < markers.length; ++i) { - var mk = markers[i], m = mk.find(0); - for (var j = 0; j < parts.length; ++j) { - var p = parts[j]; - if (cmp(p.to, m.from) < 0 || cmp(p.from, m.to) > 0) { continue } - var newParts = [j, 1], dfrom = cmp(p.from, m.from), dto = cmp(p.to, m.to); - if (dfrom < 0 || !mk.inclusiveLeft && !dfrom) - { newParts.push({from: p.from, to: m.from}); } - if (dto > 0 || !mk.inclusiveRight && !dto) - { newParts.push({from: m.to, to: p.to}); } - parts.splice.apply(parts, newParts); - j += newParts.length - 3; - } - } - return parts - } - - // Connect or disconnect spans from a line. - function detachMarkedSpans(line) { - var spans = line.markedSpans; - if (!spans) { return } - for (var i = 0; i < spans.length; ++i) - { spans[i].marker.detachLine(line); } - line.markedSpans = null; - } - function attachMarkedSpans(line, spans) { - if (!spans) { return } - for (var i = 0; i < spans.length; ++i) - { spans[i].marker.attachLine(line); } - line.markedSpans = spans; - } - - // Helpers used when computing which overlapping collapsed span - // counts as the larger one. - function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0 } - function extraRight(marker) { return marker.inclusiveRight ? 1 : 0 } - - // Returns a number indicating which of two overlapping collapsed - // spans is larger (and thus includes the other). Falls back to - // comparing ids when the spans cover exactly the same range. - function compareCollapsedMarkers(a, b) { - var lenDiff = a.lines.length - b.lines.length; - if (lenDiff != 0) { return lenDiff } - var aPos = a.find(), bPos = b.find(); - var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b); - if (fromCmp) { return -fromCmp } - var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b); - if (toCmp) { return toCmp } - return b.id - a.id - } - - // Find out whether a line ends or starts in a collapsed span. If - // so, return the marker for that span. - function collapsedSpanAtSide(line, start) { - var sps = sawCollapsedSpans && line.markedSpans, found; - if (sps) { for (var sp = (void 0), i = 0; i < sps.length; ++i) { - sp = sps[i]; - if (sp.marker.collapsed && (start ? sp.from : sp.to) == null && - (!found || compareCollapsedMarkers(found, sp.marker) < 0)) - { found = sp.marker; } - } } - return found - } - function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true) } - function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false) } - - function collapsedSpanAround(line, ch) { - var sps = sawCollapsedSpans && line.markedSpans, found; - if (sps) { for (var i = 0; i < sps.length; ++i) { - var sp = sps[i]; - if (sp.marker.collapsed && (sp.from == null || sp.from < ch) && (sp.to == null || sp.to > ch) && - (!found || compareCollapsedMarkers(found, sp.marker) < 0)) { found = sp.marker; } - } } - return found - } - - // Test whether there exists a collapsed span that partially - // overlaps (covers the start or end, but not both) of a new span. - // Such overlap is not allowed. - function conflictingCollapsedRange(doc, lineNo, from, to, marker) { - var line = getLine(doc, lineNo); - var sps = sawCollapsedSpans && line.markedSpans; - if (sps) { for (var i = 0; i < sps.length; ++i) { - var sp = sps[i]; - if (!sp.marker.collapsed) { continue } - var found = sp.marker.find(0); - var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker); - var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker); - if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) { continue } - if (fromCmp <= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.to, from) >= 0 : cmp(found.to, from) > 0) || - fromCmp >= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.from, to) <= 0 : cmp(found.from, to) < 0)) - { return true } - } } - } - - // A visual line is a line as drawn on the screen. Folding, for - // example, can cause multiple logical lines to appear on the same - // visual line. This finds the start of the visual line that the - // given line is part of (usually that is the line itself). - function visualLine(line) { - var merged; - while (merged = collapsedSpanAtStart(line)) - { line = merged.find(-1, true).line; } - return line - } - - function visualLineEnd(line) { - var merged; - while (merged = collapsedSpanAtEnd(line)) - { line = merged.find(1, true).line; } - return line - } - - // Returns an array of logical lines that continue the visual line - // started by the argument, or undefined if there are no such lines. - function visualLineContinued(line) { - var merged, lines; - while (merged = collapsedSpanAtEnd(line)) { - line = merged.find(1, true).line - ;(lines || (lines = [])).push(line); - } - return lines - } - - // Get the line number of the start of the visual line that the - // given line number is part of. - function visualLineNo(doc, lineN) { - var line = getLine(doc, lineN), vis = visualLine(line); - if (line == vis) { return lineN } - return lineNo(vis) - } - - // Get the line number of the start of the next visual line after - // the given line. - function visualLineEndNo(doc, lineN) { - if (lineN > doc.lastLine()) { return lineN } - var line = getLine(doc, lineN), merged; - if (!lineIsHidden(doc, line)) { return lineN } - while (merged = collapsedSpanAtEnd(line)) - { line = merged.find(1, true).line; } - return lineNo(line) + 1 - } - - // Compute whether a line is hidden. Lines count as hidden when they - // are part of a visual line that starts with another line, or when - // they are entirely covered by collapsed, non-widget span. - function lineIsHidden(doc, line) { - var sps = sawCollapsedSpans && line.markedSpans; - if (sps) { for (var sp = (void 0), i = 0; i < sps.length; ++i) { - sp = sps[i]; - if (!sp.marker.collapsed) { continue } - if (sp.from == null) { return true } - if (sp.marker.widgetNode) { continue } - if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp)) - { return true } - } } - } - function lineIsHiddenInner(doc, line, span) { - if (span.to == null) { - var end = span.marker.find(1, true); - return lineIsHiddenInner(doc, end.line, getMarkedSpanFor(end.line.markedSpans, span.marker)) - } - if (span.marker.inclusiveRight && span.to == line.text.length) - { return true } - for (var sp = (void 0), i = 0; i < line.markedSpans.length; ++i) { - sp = line.markedSpans[i]; - if (sp.marker.collapsed && !sp.marker.widgetNode && sp.from == span.to && - (sp.to == null || sp.to != span.from) && - (sp.marker.inclusiveLeft || span.marker.inclusiveRight) && - lineIsHiddenInner(doc, line, sp)) { return true } - } - } - - // Find the height above the given line. - function heightAtLine(lineObj) { - lineObj = visualLine(lineObj); - - var h = 0, chunk = lineObj.parent; - for (var i = 0; i < chunk.lines.length; ++i) { - var line = chunk.lines[i]; - if (line == lineObj) { break } - else { h += line.height; } - } - for (var p = chunk.parent; p; chunk = p, p = chunk.parent) { - for (var i$1 = 0; i$1 < p.children.length; ++i$1) { - var cur = p.children[i$1]; - if (cur == chunk) { break } - else { h += cur.height; } - } - } - return h - } - - // Compute the character length of a line, taking into account - // collapsed ranges (see markText) that might hide parts, and join - // other lines onto it. - function lineLength(line) { - if (line.height == 0) { return 0 } - var len = line.text.length, merged, cur = line; - while (merged = collapsedSpanAtStart(cur)) { - var found = merged.find(0, true); - cur = found.from.line; - len += found.from.ch - found.to.ch; - } - cur = line; - while (merged = collapsedSpanAtEnd(cur)) { - var found$1 = merged.find(0, true); - len -= cur.text.length - found$1.from.ch; - cur = found$1.to.line; - len += cur.text.length - found$1.to.ch; - } - return len - } - - // Find the longest line in the document. - function findMaxLine(cm) { - var d = cm.display, doc = cm.doc; - d.maxLine = getLine(doc, doc.first); - d.maxLineLength = lineLength(d.maxLine); - d.maxLineChanged = true; - doc.iter(function (line) { - var len = lineLength(line); - if (len > d.maxLineLength) { - d.maxLineLength = len; - d.maxLine = line; - } - }); - } - - // LINE DATA STRUCTURE - - // Line objects. These hold state related to a line, including - // highlighting info (the styles array). - var Line = function(text, markedSpans, estimateHeight) { - this.text = text; - attachMarkedSpans(this, markedSpans); - this.height = estimateHeight ? estimateHeight(this) : 1; - }; - - Line.prototype.lineNo = function () { return lineNo(this) }; - eventMixin(Line); - - // Change the content (text, markers) of a line. Automatically - // invalidates cached information and tries to re-estimate the - // line's height. - function updateLine(line, text, markedSpans, estimateHeight) { - line.text = text; - if (line.stateAfter) { line.stateAfter = null; } - if (line.styles) { line.styles = null; } - if (line.order != null) { line.order = null; } - detachMarkedSpans(line); - attachMarkedSpans(line, markedSpans); - var estHeight = estimateHeight ? estimateHeight(line) : 1; - if (estHeight != line.height) { updateLineHeight(line, estHeight); } - } - - // Detach a line from the document tree and its markers. - function cleanUpLine(line) { - line.parent = null; - detachMarkedSpans(line); - } - - // Convert a style as returned by a mode (either null, or a string - // containing one or more styles) to a CSS style. This is cached, - // and also looks for line-wide styles. - var styleToClassCache = {}, styleToClassCacheWithMode = {}; - function interpretTokenStyle(style, options) { - if (!style || /^\s*$/.test(style)) { return null } - var cache = options.addModeClass ? styleToClassCacheWithMode : styleToClassCache; - return cache[style] || - (cache[style] = style.replace(/\S+/g, "cm-$&")) - } - - // Render the DOM representation of the text of a line. Also builds - // up a 'line map', which points at the DOM nodes that represent - // specific stretches of text, and is used by the measuring code. - // The returned object contains the DOM node, this map, and - // information about line-wide styles that were set by the mode. - function buildLineContent(cm, lineView) { - // The padding-right forces the element to have a 'border', which - // is needed on Webkit to be able to get line-level bounding - // rectangles for it (in measureChar). - var content = eltP("span", null, null, webkit ? "padding-right: .1px" : null); - var builder = {pre: eltP("pre", [content], "CodeMirror-line"), content: content, - col: 0, pos: 0, cm: cm, - trailingSpace: false, - splitSpaces: cm.getOption("lineWrapping")}; - lineView.measure = {}; - - // Iterate over the logical lines that make up this visual line. - for (var i = 0; i <= (lineView.rest ? lineView.rest.length : 0); i++) { - var line = i ? lineView.rest[i - 1] : lineView.line, order = (void 0); - builder.pos = 0; - builder.addToken = buildToken; - // Optionally wire in some hacks into the token-rendering - // algorithm, to deal with browser quirks. - if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line, cm.doc.direction))) - { builder.addToken = buildTokenBadBidi(builder.addToken, order); } - builder.map = []; - var allowFrontierUpdate = lineView != cm.display.externalMeasured && lineNo(line); - insertLineContent(line, builder, getLineStyles(cm, line, allowFrontierUpdate)); - if (line.styleClasses) { - if (line.styleClasses.bgClass) - { builder.bgClass = joinClasses(line.styleClasses.bgClass, builder.bgClass || ""); } - if (line.styleClasses.textClass) - { builder.textClass = joinClasses(line.styleClasses.textClass, builder.textClass || ""); } - } - - // Ensure at least a single node is present, for measuring. - if (builder.map.length == 0) - { builder.map.push(0, 0, builder.content.appendChild(zeroWidthElement(cm.display.measure))); } - - // Store the map and a cache object for the current logical line - if (i == 0) { - lineView.measure.map = builder.map; - lineView.measure.cache = {}; - } else { - (lineView.measure.maps || (lineView.measure.maps = [])).push(builder.map) - ;(lineView.measure.caches || (lineView.measure.caches = [])).push({}); - } - } - - // See issue #2901 - if (webkit) { - var last = builder.content.lastChild; - if (/\bcm-tab\b/.test(last.className) || (last.querySelector && last.querySelector(".cm-tab"))) - { builder.content.className = "cm-tab-wrap-hack"; } - } - - signal(cm, "renderLine", cm, lineView.line, builder.pre); - if (builder.pre.className) - { builder.textClass = joinClasses(builder.pre.className, builder.textClass || ""); } - - return builder - } - - function defaultSpecialCharPlaceholder(ch) { - var token = elt("span", "\u2022", "cm-invalidchar"); - token.title = "\\u" + ch.charCodeAt(0).toString(16); - token.setAttribute("aria-label", token.title); - return token - } - - // Build up the DOM representation for a single token, and add it to - // the line map. Takes care to render special characters separately. - function buildToken(builder, text, style, startStyle, endStyle, css, attributes) { - if (!text) { return } - var displayText = builder.splitSpaces ? splitSpaces(text, builder.trailingSpace) : text; - var special = builder.cm.state.specialChars, mustWrap = false; - var content; - if (!special.test(text)) { - builder.col += text.length; - content = document.createTextNode(displayText); - builder.map.push(builder.pos, builder.pos + text.length, content); - if (ie && ie_version < 9) { mustWrap = true; } - builder.pos += text.length; - } else { - content = document.createDocumentFragment(); - var pos = 0; - while (true) { - special.lastIndex = pos; - var m = special.exec(text); - var skipped = m ? m.index - pos : text.length - pos; - if (skipped) { - var txt = document.createTextNode(displayText.slice(pos, pos + skipped)); - if (ie && ie_version < 9) { content.appendChild(elt("span", [txt])); } - else { content.appendChild(txt); } - builder.map.push(builder.pos, builder.pos + skipped, txt); - builder.col += skipped; - builder.pos += skipped; - } - if (!m) { break } - pos += skipped + 1; - var txt$1 = (void 0); - if (m[0] == "\t") { - var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize; - txt$1 = content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab")); - txt$1.setAttribute("role", "presentation"); - txt$1.setAttribute("cm-text", "\t"); - builder.col += tabWidth; - } else if (m[0] == "\r" || m[0] == "\n") { - txt$1 = content.appendChild(elt("span", m[0] == "\r" ? "\u240d" : "\u2424", "cm-invalidchar")); - txt$1.setAttribute("cm-text", m[0]); - builder.col += 1; - } else { - txt$1 = builder.cm.options.specialCharPlaceholder(m[0]); - txt$1.setAttribute("cm-text", m[0]); - if (ie && ie_version < 9) { content.appendChild(elt("span", [txt$1])); } - else { content.appendChild(txt$1); } - builder.col += 1; - } - builder.map.push(builder.pos, builder.pos + 1, txt$1); - builder.pos++; - } - } - builder.trailingSpace = displayText.charCodeAt(text.length - 1) == 32; - if (style || startStyle || endStyle || mustWrap || css) { - var fullStyle = style || ""; - if (startStyle) { fullStyle += startStyle; } - if (endStyle) { fullStyle += endStyle; } - var token = elt("span", [content], fullStyle, css); - if (attributes) { - for (var attr in attributes) { if (attributes.hasOwnProperty(attr) && attr != "style" && attr != "class") - { token.setAttribute(attr, attributes[attr]); } } - } - return builder.content.appendChild(token) - } - builder.content.appendChild(content); - } - - // Change some spaces to NBSP to prevent the browser from collapsing - // trailing spaces at the end of a line when rendering text (issue #1362). - function splitSpaces(text, trailingBefore) { - if (text.length > 1 && !/ /.test(text)) { return text } - var spaceBefore = trailingBefore, result = ""; - for (var i = 0; i < text.length; i++) { - var ch = text.charAt(i); - if (ch == " " && spaceBefore && (i == text.length - 1 || text.charCodeAt(i + 1) == 32)) - { ch = "\u00a0"; } - result += ch; - spaceBefore = ch == " "; - } - return result - } - - // Work around nonsense dimensions being reported for stretches of - // right-to-left text. - function buildTokenBadBidi(inner, order) { - return function (builder, text, style, startStyle, endStyle, css, attributes) { - style = style ? style + " cm-force-border" : "cm-force-border"; - var start = builder.pos, end = start + text.length; - for (;;) { - // Find the part that overlaps with the start of this text - var part = (void 0); - for (var i = 0; i < order.length; i++) { - part = order[i]; - if (part.to > start && part.from <= start) { break } - } - if (part.to >= end) { return inner(builder, text, style, startStyle, endStyle, css, attributes) } - inner(builder, text.slice(0, part.to - start), style, startStyle, null, css, attributes); - startStyle = null; - text = text.slice(part.to - start); - start = part.to; - } - } - } - - function buildCollapsedSpan(builder, size, marker, ignoreWidget) { - var widget = !ignoreWidget && marker.widgetNode; - if (widget) { builder.map.push(builder.pos, builder.pos + size, widget); } - if (!ignoreWidget && builder.cm.display.input.needsContentAttribute) { - if (!widget) - { widget = builder.content.appendChild(document.createElement("span")); } - widget.setAttribute("cm-marker", marker.id); - } - if (widget) { - builder.cm.display.input.setUneditable(widget); - builder.content.appendChild(widget); - } - builder.pos += size; - builder.trailingSpace = false; - } - - // Outputs a number of spans to make up a line, taking highlighting - // and marked text into account. - function insertLineContent(line, builder, styles) { - var spans = line.markedSpans, allText = line.text, at = 0; - if (!spans) { - for (var i$1 = 1; i$1 < styles.length; i$1+=2) - { builder.addToken(builder, allText.slice(at, at = styles[i$1]), interpretTokenStyle(styles[i$1+1], builder.cm.options)); } - return - } - - var len = allText.length, pos = 0, i = 1, text = "", style, css; - var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, collapsed, attributes; - for (;;) { - if (nextChange == pos) { // Update current marker set - spanStyle = spanEndStyle = spanStartStyle = css = ""; - attributes = null; - collapsed = null; nextChange = Infinity; - var foundBookmarks = [], endStyles = (void 0); - for (var j = 0; j < spans.length; ++j) { - var sp = spans[j], m = sp.marker; - if (m.type == "bookmark" && sp.from == pos && m.widgetNode) { - foundBookmarks.push(m); - } else if (sp.from <= pos && (sp.to == null || sp.to > pos || m.collapsed && sp.to == pos && sp.from == pos)) { - if (sp.to != null && sp.to != pos && nextChange > sp.to) { - nextChange = sp.to; - spanEndStyle = ""; - } - if (m.className) { spanStyle += " " + m.className; } - if (m.css) { css = (css ? css + ";" : "") + m.css; } - if (m.startStyle && sp.from == pos) { spanStartStyle += " " + m.startStyle; } - if (m.endStyle && sp.to == nextChange) { (endStyles || (endStyles = [])).push(m.endStyle, sp.to); } - // support for the old title property - // https://github.com/codemirror/CodeMirror/pull/5673 - if (m.title) { (attributes || (attributes = {})).title = m.title; } - if (m.attributes) { - for (var attr in m.attributes) - { (attributes || (attributes = {}))[attr] = m.attributes[attr]; } - } - if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0)) - { collapsed = sp; } - } else if (sp.from > pos && nextChange > sp.from) { - nextChange = sp.from; - } - } - if (endStyles) { for (var j$1 = 0; j$1 < endStyles.length; j$1 += 2) - { if (endStyles[j$1 + 1] == nextChange) { spanEndStyle += " " + endStyles[j$1]; } } } - - if (!collapsed || collapsed.from == pos) { for (var j$2 = 0; j$2 < foundBookmarks.length; ++j$2) - { buildCollapsedSpan(builder, 0, foundBookmarks[j$2]); } } - if (collapsed && (collapsed.from || 0) == pos) { - buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos, - collapsed.marker, collapsed.from == null); - if (collapsed.to == null) { return } - if (collapsed.to == pos) { collapsed = false; } - } - } - if (pos >= len) { break } - - var upto = Math.min(len, nextChange); - while (true) { - if (text) { - var end = pos + text.length; - if (!collapsed) { - var tokenText = end > upto ? text.slice(0, upto - pos) : text; - builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle, - spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", css, attributes); - } - if (end >= upto) {text = text.slice(upto - pos); pos = upto; break} - pos = end; - spanStartStyle = ""; - } - text = allText.slice(at, at = styles[i++]); - style = interpretTokenStyle(styles[i++], builder.cm.options); - } - } - } - - - // These objects are used to represent the visible (currently drawn) - // part of the document. A LineView may correspond to multiple - // logical lines, if those are connected by collapsed ranges. - function LineView(doc, line, lineN) { - // The starting line - this.line = line; - // Continuing lines, if any - this.rest = visualLineContinued(line); - // Number of logical lines in this visual line - this.size = this.rest ? lineNo(lst(this.rest)) - lineN + 1 : 1; - this.node = this.text = null; - this.hidden = lineIsHidden(doc, line); - } - - // Create a range of LineView objects for the given lines. - function buildViewArray(cm, from, to) { - var array = [], nextPos; - for (var pos = from; pos < to; pos = nextPos) { - var view = new LineView(cm.doc, getLine(cm.doc, pos), pos); - nextPos = pos + view.size; - array.push(view); - } - return array - } - - var operationGroup = null; - - function pushOperation(op) { - if (operationGroup) { - operationGroup.ops.push(op); - } else { - op.ownsGroup = operationGroup = { - ops: [op], - delayedCallbacks: [] - }; - } - } - - function fireCallbacksForOps(group) { - // Calls delayed callbacks and cursorActivity handlers until no - // new ones appear - var callbacks = group.delayedCallbacks, i = 0; - do { - for (; i < callbacks.length; i++) - { callbacks[i].call(null); } - for (var j = 0; j < group.ops.length; j++) { - var op = group.ops[j]; - if (op.cursorActivityHandlers) - { while (op.cursorActivityCalled < op.cursorActivityHandlers.length) - { op.cursorActivityHandlers[op.cursorActivityCalled++].call(null, op.cm); } } - } - } while (i < callbacks.length) - } - - function finishOperation(op, endCb) { - var group = op.ownsGroup; - if (!group) { return } - - try { fireCallbacksForOps(group); } - finally { - operationGroup = null; - endCb(group); - } - } - - var orphanDelayedCallbacks = null; - - // Often, we want to signal events at a point where we are in the - // middle of some work, but don't want the handler to start calling - // other methods on the editor, which might be in an inconsistent - // state or simply not expect any other events to happen. - // signalLater looks whether there are any handlers, and schedules - // them to be executed when the last operation ends, or, if no - // operation is active, when a timeout fires. - function signalLater(emitter, type /*, values...*/) { - var arr = getHandlers(emitter, type); - if (!arr.length) { return } - var args = Array.prototype.slice.call(arguments, 2), list; - if (operationGroup) { - list = operationGroup.delayedCallbacks; - } else if (orphanDelayedCallbacks) { - list = orphanDelayedCallbacks; - } else { - list = orphanDelayedCallbacks = []; - setTimeout(fireOrphanDelayed, 0); - } - var loop = function ( i ) { - list.push(function () { return arr[i].apply(null, args); }); - }; - - for (var i = 0; i < arr.length; ++i) - loop( i ); - } - - function fireOrphanDelayed() { - var delayed = orphanDelayedCallbacks; - orphanDelayedCallbacks = null; - for (var i = 0; i < delayed.length; ++i) { delayed[i](); } - } - - // When an aspect of a line changes, a string is added to - // lineView.changes. This updates the relevant part of the line's - // DOM structure. - function updateLineForChanges(cm, lineView, lineN, dims) { - for (var j = 0; j < lineView.changes.length; j++) { - var type = lineView.changes[j]; - if (type == "text") { updateLineText(cm, lineView); } - else if (type == "gutter") { updateLineGutter(cm, lineView, lineN, dims); } - else if (type == "class") { updateLineClasses(cm, lineView); } - else if (type == "widget") { updateLineWidgets(cm, lineView, dims); } - } - lineView.changes = null; - } - - // Lines with gutter elements, widgets or a background class need to - // be wrapped, and have the extra elements added to the wrapper div - function ensureLineWrapped(lineView) { - if (lineView.node == lineView.text) { - lineView.node = elt("div", null, null, "position: relative"); - if (lineView.text.parentNode) - { lineView.text.parentNode.replaceChild(lineView.node, lineView.text); } - lineView.node.appendChild(lineView.text); - if (ie && ie_version < 8) { lineView.node.style.zIndex = 2; } - } - return lineView.node - } - - function updateLineBackground(cm, lineView) { - var cls = lineView.bgClass ? lineView.bgClass + " " + (lineView.line.bgClass || "") : lineView.line.bgClass; - if (cls) { cls += " CodeMirror-linebackground"; } - if (lineView.background) { - if (cls) { lineView.background.className = cls; } - else { lineView.background.parentNode.removeChild(lineView.background); lineView.background = null; } - } else if (cls) { - var wrap = ensureLineWrapped(lineView); - lineView.background = wrap.insertBefore(elt("div", null, cls), wrap.firstChild); - cm.display.input.setUneditable(lineView.background); - } - } - - // Wrapper around buildLineContent which will reuse the structure - // in display.externalMeasured when possible. - function getLineContent(cm, lineView) { - var ext = cm.display.externalMeasured; - if (ext && ext.line == lineView.line) { - cm.display.externalMeasured = null; - lineView.measure = ext.measure; - return ext.built - } - return buildLineContent(cm, lineView) - } - - // Redraw the line's text. Interacts with the background and text - // classes because the mode may output tokens that influence these - // classes. - function updateLineText(cm, lineView) { - var cls = lineView.text.className; - var built = getLineContent(cm, lineView); - if (lineView.text == lineView.node) { lineView.node = built.pre; } - lineView.text.parentNode.replaceChild(built.pre, lineView.text); - lineView.text = built.pre; - if (built.bgClass != lineView.bgClass || built.textClass != lineView.textClass) { - lineView.bgClass = built.bgClass; - lineView.textClass = built.textClass; - updateLineClasses(cm, lineView); - } else if (cls) { - lineView.text.className = cls; - } - } - - function updateLineClasses(cm, lineView) { - updateLineBackground(cm, lineView); - if (lineView.line.wrapClass) - { ensureLineWrapped(lineView).className = lineView.line.wrapClass; } - else if (lineView.node != lineView.text) - { lineView.node.className = ""; } - var textClass = lineView.textClass ? lineView.textClass + " " + (lineView.line.textClass || "") : lineView.line.textClass; - lineView.text.className = textClass || ""; - } - - function updateLineGutter(cm, lineView, lineN, dims) { - if (lineView.gutter) { - lineView.node.removeChild(lineView.gutter); - lineView.gutter = null; - } - if (lineView.gutterBackground) { - lineView.node.removeChild(lineView.gutterBackground); - lineView.gutterBackground = null; - } - if (lineView.line.gutterClass) { - var wrap = ensureLineWrapped(lineView); - lineView.gutterBackground = elt("div", null, "CodeMirror-gutter-background " + lineView.line.gutterClass, - ("left: " + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px; width: " + (dims.gutterTotalWidth) + "px")); - cm.display.input.setUneditable(lineView.gutterBackground); - wrap.insertBefore(lineView.gutterBackground, lineView.text); - } - var markers = lineView.line.gutterMarkers; - if (cm.options.lineNumbers || markers) { - var wrap$1 = ensureLineWrapped(lineView); - var gutterWrap = lineView.gutter = elt("div", null, "CodeMirror-gutter-wrapper", ("left: " + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px")); - cm.display.input.setUneditable(gutterWrap); - wrap$1.insertBefore(gutterWrap, lineView.text); - if (lineView.line.gutterClass) - { gutterWrap.className += " " + lineView.line.gutterClass; } - if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"])) - { lineView.lineNumber = gutterWrap.appendChild( - elt("div", lineNumberFor(cm.options, lineN), - "CodeMirror-linenumber CodeMirror-gutter-elt", - ("left: " + (dims.gutterLeft["CodeMirror-linenumbers"]) + "px; width: " + (cm.display.lineNumInnerWidth) + "px"))); } - if (markers) { for (var k = 0; k < cm.display.gutterSpecs.length; ++k) { - var id = cm.display.gutterSpecs[k].className, found = markers.hasOwnProperty(id) && markers[id]; - if (found) - { gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", - ("left: " + (dims.gutterLeft[id]) + "px; width: " + (dims.gutterWidth[id]) + "px"))); } - } } - } - } - - function updateLineWidgets(cm, lineView, dims) { - if (lineView.alignable) { lineView.alignable = null; } - var isWidget = classTest("CodeMirror-linewidget"); - for (var node = lineView.node.firstChild, next = (void 0); node; node = next) { - next = node.nextSibling; - if (isWidget.test(node.className)) { lineView.node.removeChild(node); } - } - insertLineWidgets(cm, lineView, dims); - } - - // Build a line's DOM representation from scratch - function buildLineElement(cm, lineView, lineN, dims) { - var built = getLineContent(cm, lineView); - lineView.text = lineView.node = built.pre; - if (built.bgClass) { lineView.bgClass = built.bgClass; } - if (built.textClass) { lineView.textClass = built.textClass; } - - updateLineClasses(cm, lineView); - updateLineGutter(cm, lineView, lineN, dims); - insertLineWidgets(cm, lineView, dims); - return lineView.node - } - - // A lineView may contain multiple logical lines (when merged by - // collapsed spans). The widgets for all of them need to be drawn. - function insertLineWidgets(cm, lineView, dims) { - insertLineWidgetsFor(cm, lineView.line, lineView, dims, true); - if (lineView.rest) { for (var i = 0; i < lineView.rest.length; i++) - { insertLineWidgetsFor(cm, lineView.rest[i], lineView, dims, false); } } - } - - function insertLineWidgetsFor(cm, line, lineView, dims, allowAbove) { - if (!line.widgets) { return } - var wrap = ensureLineWrapped(lineView); - for (var i = 0, ws = line.widgets; i < ws.length; ++i) { - var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget" + (widget.className ? " " + widget.className : "")); - if (!widget.handleMouseEvents) { node.setAttribute("cm-ignore-events", "true"); } - positionLineWidget(widget, node, lineView, dims); - cm.display.input.setUneditable(node); - if (allowAbove && widget.above) - { wrap.insertBefore(node, lineView.gutter || lineView.text); } - else - { wrap.appendChild(node); } - signalLater(widget, "redraw"); - } - } - - function positionLineWidget(widget, node, lineView, dims) { - if (widget.noHScroll) { - (lineView.alignable || (lineView.alignable = [])).push(node); - var width = dims.wrapperWidth; - node.style.left = dims.fixedPos + "px"; - if (!widget.coverGutter) { - width -= dims.gutterTotalWidth; - node.style.paddingLeft = dims.gutterTotalWidth + "px"; - } - node.style.width = width + "px"; - } - if (widget.coverGutter) { - node.style.zIndex = 5; - node.style.position = "relative"; - if (!widget.noHScroll) { node.style.marginLeft = -dims.gutterTotalWidth + "px"; } - } - } - - function widgetHeight(widget) { - if (widget.height != null) { return widget.height } - var cm = widget.doc.cm; - if (!cm) { return 0 } - if (!contains(document.body, widget.node)) { - var parentStyle = "position: relative;"; - if (widget.coverGutter) - { parentStyle += "margin-left: -" + cm.display.gutters.offsetWidth + "px;"; } - if (widget.noHScroll) - { parentStyle += "width: " + cm.display.wrapper.clientWidth + "px;"; } - removeChildrenAndAdd(cm.display.measure, elt("div", [widget.node], null, parentStyle)); - } - return widget.height = widget.node.parentNode.offsetHeight - } - - // Return true when the given mouse event happened in a widget - function eventInWidget(display, e) { - for (var n = e_target(e); n != display.wrapper; n = n.parentNode) { - if (!n || (n.nodeType == 1 && n.getAttribute("cm-ignore-events") == "true") || - (n.parentNode == display.sizer && n != display.mover)) - { return true } - } - } - - // POSITION MEASUREMENT - - function paddingTop(display) {return display.lineSpace.offsetTop} - function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight} - function paddingH(display) { - if (display.cachedPaddingH) { return display.cachedPaddingH } - var e = removeChildrenAndAdd(display.measure, elt("pre", "x", "CodeMirror-line-like")); - var style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle; - var data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)}; - if (!isNaN(data.left) && !isNaN(data.right)) { display.cachedPaddingH = data; } - return data - } - - function scrollGap(cm) { return scrollerGap - cm.display.nativeBarWidth } - function displayWidth(cm) { - return cm.display.scroller.clientWidth - scrollGap(cm) - cm.display.barWidth - } - function displayHeight(cm) { - return cm.display.scroller.clientHeight - scrollGap(cm) - cm.display.barHeight - } - - // Ensure the lineView.wrapping.heights array is populated. This is - // an array of bottom offsets for the lines that make up a drawn - // line. When lineWrapping is on, there might be more than one - // height. - function ensureLineHeights(cm, lineView, rect) { - var wrapping = cm.options.lineWrapping; - var curWidth = wrapping && displayWidth(cm); - if (!lineView.measure.heights || wrapping && lineView.measure.width != curWidth) { - var heights = lineView.measure.heights = []; - if (wrapping) { - lineView.measure.width = curWidth; - var rects = lineView.text.firstChild.getClientRects(); - for (var i = 0; i < rects.length - 1; i++) { - var cur = rects[i], next = rects[i + 1]; - if (Math.abs(cur.bottom - next.bottom) > 2) - { heights.push((cur.bottom + next.top) / 2 - rect.top); } - } - } - heights.push(rect.bottom - rect.top); - } - } - - // Find a line map (mapping character offsets to text nodes) and a - // measurement cache for the given line number. (A line view might - // contain multiple lines when collapsed ranges are present.) - function mapFromLineView(lineView, line, lineN) { - if (lineView.line == line) - { return {map: lineView.measure.map, cache: lineView.measure.cache} } - for (var i = 0; i < lineView.rest.length; i++) - { if (lineView.rest[i] == line) - { return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]} } } - for (var i$1 = 0; i$1 < lineView.rest.length; i$1++) - { if (lineNo(lineView.rest[i$1]) > lineN) - { return {map: lineView.measure.maps[i$1], cache: lineView.measure.caches[i$1], before: true} } } - } - - // Render a line into the hidden node display.externalMeasured. Used - // when measurement is needed for a line that's not in the viewport. - function updateExternalMeasurement(cm, line) { - line = visualLine(line); - var lineN = lineNo(line); - var view = cm.display.externalMeasured = new LineView(cm.doc, line, lineN); - view.lineN = lineN; - var built = view.built = buildLineContent(cm, view); - view.text = built.pre; - removeChildrenAndAdd(cm.display.lineMeasure, built.pre); - return view - } - - // Get a {top, bottom, left, right} box (in line-local coordinates) - // for a given character. - function measureChar(cm, line, ch, bias) { - return measureCharPrepared(cm, prepareMeasureForLine(cm, line), ch, bias) - } - - // Find a line view that corresponds to the given line number. - function findViewForLine(cm, lineN) { - if (lineN >= cm.display.viewFrom && lineN < cm.display.viewTo) - { return cm.display.view[findViewIndex(cm, lineN)] } - var ext = cm.display.externalMeasured; - if (ext && lineN >= ext.lineN && lineN < ext.lineN + ext.size) - { return ext } - } - - // Measurement can be split in two steps, the set-up work that - // applies to the whole line, and the measurement of the actual - // character. Functions like coordsChar, that need to do a lot of - // measurements in a row, can thus ensure that the set-up work is - // only done once. - function prepareMeasureForLine(cm, line) { - var lineN = lineNo(line); - var view = findViewForLine(cm, lineN); - if (view && !view.text) { - view = null; - } else if (view && view.changes) { - updateLineForChanges(cm, view, lineN, getDimensions(cm)); - cm.curOp.forceUpdate = true; - } - if (!view) - { view = updateExternalMeasurement(cm, line); } - - var info = mapFromLineView(view, line, lineN); - return { - line: line, view: view, rect: null, - map: info.map, cache: info.cache, before: info.before, - hasHeights: false - } - } - - // Given a prepared measurement object, measures the position of an - // actual character (or fetches it from the cache). - function measureCharPrepared(cm, prepared, ch, bias, varHeight) { - if (prepared.before) { ch = -1; } - var key = ch + (bias || ""), found; - if (prepared.cache.hasOwnProperty(key)) { - found = prepared.cache[key]; - } else { - if (!prepared.rect) - { prepared.rect = prepared.view.text.getBoundingClientRect(); } - if (!prepared.hasHeights) { - ensureLineHeights(cm, prepared.view, prepared.rect); - prepared.hasHeights = true; - } - found = measureCharInner(cm, prepared, ch, bias); - if (!found.bogus) { prepared.cache[key] = found; } - } - return {left: found.left, right: found.right, - top: varHeight ? found.rtop : found.top, - bottom: varHeight ? found.rbottom : found.bottom} - } - - var nullRect = {left: 0, right: 0, top: 0, bottom: 0}; - - function nodeAndOffsetInLineMap(map, ch, bias) { - var node, start, end, collapse, mStart, mEnd; - // First, search the line map for the text node corresponding to, - // or closest to, the target character. - for (var i = 0; i < map.length; i += 3) { - mStart = map[i]; - mEnd = map[i + 1]; - if (ch < mStart) { - start = 0; end = 1; - collapse = "left"; - } else if (ch < mEnd) { - start = ch - mStart; - end = start + 1; - } else if (i == map.length - 3 || ch == mEnd && map[i + 3] > ch) { - end = mEnd - mStart; - start = end - 1; - if (ch >= mEnd) { collapse = "right"; } - } - if (start != null) { - node = map[i + 2]; - if (mStart == mEnd && bias == (node.insertLeft ? "left" : "right")) - { collapse = bias; } - if (bias == "left" && start == 0) - { while (i && map[i - 2] == map[i - 3] && map[i - 1].insertLeft) { - node = map[(i -= 3) + 2]; - collapse = "left"; - } } - if (bias == "right" && start == mEnd - mStart) - { while (i < map.length - 3 && map[i + 3] == map[i + 4] && !map[i + 5].insertLeft) { - node = map[(i += 3) + 2]; - collapse = "right"; - } } - break - } - } - return {node: node, start: start, end: end, collapse: collapse, coverStart: mStart, coverEnd: mEnd} - } - - function getUsefulRect(rects, bias) { - var rect = nullRect; - if (bias == "left") { for (var i = 0; i < rects.length; i++) { - if ((rect = rects[i]).left != rect.right) { break } - } } else { for (var i$1 = rects.length - 1; i$1 >= 0; i$1--) { - if ((rect = rects[i$1]).left != rect.right) { break } - } } - return rect - } - - function measureCharInner(cm, prepared, ch, bias) { - var place = nodeAndOffsetInLineMap(prepared.map, ch, bias); - var node = place.node, start = place.start, end = place.end, collapse = place.collapse; - - var rect; - if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates. - for (var i$1 = 0; i$1 < 4; i$1++) { // Retry a maximum of 4 times when nonsense rectangles are returned - while (start && isExtendingChar(prepared.line.text.charAt(place.coverStart + start))) { --start; } - while (place.coverStart + end < place.coverEnd && isExtendingChar(prepared.line.text.charAt(place.coverStart + end))) { ++end; } - if (ie && ie_version < 9 && start == 0 && end == place.coverEnd - place.coverStart) - { rect = node.parentNode.getBoundingClientRect(); } - else - { rect = getUsefulRect(range(node, start, end).getClientRects(), bias); } - if (rect.left || rect.right || start == 0) { break } - end = start; - start = start - 1; - collapse = "right"; - } - if (ie && ie_version < 11) { rect = maybeUpdateRectForZooming(cm.display.measure, rect); } - } else { // If it is a widget, simply get the box for the whole widget. - if (start > 0) { collapse = bias = "right"; } - var rects; - if (cm.options.lineWrapping && (rects = node.getClientRects()).length > 1) - { rect = rects[bias == "right" ? rects.length - 1 : 0]; } - else - { rect = node.getBoundingClientRect(); } - } - if (ie && ie_version < 9 && !start && (!rect || !rect.left && !rect.right)) { - var rSpan = node.parentNode.getClientRects()[0]; - if (rSpan) - { rect = {left: rSpan.left, right: rSpan.left + charWidth(cm.display), top: rSpan.top, bottom: rSpan.bottom}; } - else - { rect = nullRect; } - } - - var rtop = rect.top - prepared.rect.top, rbot = rect.bottom - prepared.rect.top; - var mid = (rtop + rbot) / 2; - var heights = prepared.view.measure.heights; - var i = 0; - for (; i < heights.length - 1; i++) - { if (mid < heights[i]) { break } } - var top = i ? heights[i - 1] : 0, bot = heights[i]; - var result = {left: (collapse == "right" ? rect.right : rect.left) - prepared.rect.left, - right: (collapse == "left" ? rect.left : rect.right) - prepared.rect.left, - top: top, bottom: bot}; - if (!rect.left && !rect.right) { result.bogus = true; } - if (!cm.options.singleCursorHeightPerLine) { result.rtop = rtop; result.rbottom = rbot; } - - return result - } - - // Work around problem with bounding client rects on ranges being - // returned incorrectly when zoomed on IE10 and below. - function maybeUpdateRectForZooming(measure, rect) { - if (!window.screen || screen.logicalXDPI == null || - screen.logicalXDPI == screen.deviceXDPI || !hasBadZoomedRects(measure)) - { return rect } - var scaleX = screen.logicalXDPI / screen.deviceXDPI; - var scaleY = screen.logicalYDPI / screen.deviceYDPI; - return {left: rect.left * scaleX, right: rect.right * scaleX, - top: rect.top * scaleY, bottom: rect.bottom * scaleY} - } - - function clearLineMeasurementCacheFor(lineView) { - if (lineView.measure) { - lineView.measure.cache = {}; - lineView.measure.heights = null; - if (lineView.rest) { for (var i = 0; i < lineView.rest.length; i++) - { lineView.measure.caches[i] = {}; } } - } - } - - function clearLineMeasurementCache(cm) { - cm.display.externalMeasure = null; - removeChildren(cm.display.lineMeasure); - for (var i = 0; i < cm.display.view.length; i++) - { clearLineMeasurementCacheFor(cm.display.view[i]); } - } - - function clearCaches(cm) { - clearLineMeasurementCache(cm); - cm.display.cachedCharWidth = cm.display.cachedTextHeight = cm.display.cachedPaddingH = null; - if (!cm.options.lineWrapping) { cm.display.maxLineChanged = true; } - cm.display.lineNumChars = null; - } - - function pageScrollX() { - // Work around https://bugs.chromium.org/p/chromium/issues/detail?id=489206 - // which causes page_Offset and bounding client rects to use - // different reference viewports and invalidate our calculations. - if (chrome && android) { return -(document.body.getBoundingClientRect().left - parseInt(getComputedStyle(document.body).marginLeft)) } - return window.pageXOffset || (document.documentElement || document.body).scrollLeft - } - function pageScrollY() { - if (chrome && android) { return -(document.body.getBoundingClientRect().top - parseInt(getComputedStyle(document.body).marginTop)) } - return window.pageYOffset || (document.documentElement || document.body).scrollTop - } - - function widgetTopHeight(lineObj) { - var height = 0; - if (lineObj.widgets) { for (var i = 0; i < lineObj.widgets.length; ++i) { if (lineObj.widgets[i].above) - { height += widgetHeight(lineObj.widgets[i]); } } } - return height - } - - // Converts a {top, bottom, left, right} box from line-local - // coordinates into another coordinate system. Context may be one of - // "line", "div" (display.lineDiv), "local"./null (editor), "window", - // or "page". - function intoCoordSystem(cm, lineObj, rect, context, includeWidgets) { - if (!includeWidgets) { - var height = widgetTopHeight(lineObj); - rect.top += height; rect.bottom += height; - } - if (context == "line") { return rect } - if (!context) { context = "local"; } - var yOff = heightAtLine(lineObj); - if (context == "local") { yOff += paddingTop(cm.display); } - else { yOff -= cm.display.viewOffset; } - if (context == "page" || context == "window") { - var lOff = cm.display.lineSpace.getBoundingClientRect(); - yOff += lOff.top + (context == "window" ? 0 : pageScrollY()); - var xOff = lOff.left + (context == "window" ? 0 : pageScrollX()); - rect.left += xOff; rect.right += xOff; - } - rect.top += yOff; rect.bottom += yOff; - return rect - } - - // Coverts a box from "div" coords to another coordinate system. - // Context may be "window", "page", "div", or "local"./null. - function fromCoordSystem(cm, coords, context) { - if (context == "div") { return coords } - var left = coords.left, top = coords.top; - // First move into "page" coordinate system - if (context == "page") { - left -= pageScrollX(); - top -= pageScrollY(); - } else if (context == "local" || !context) { - var localBox = cm.display.sizer.getBoundingClientRect(); - left += localBox.left; - top += localBox.top; - } - - var lineSpaceBox = cm.display.lineSpace.getBoundingClientRect(); - return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top} - } - - function charCoords(cm, pos, context, lineObj, bias) { - if (!lineObj) { lineObj = getLine(cm.doc, pos.line); } - return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, bias), context) - } - - // Returns a box for a given cursor position, which may have an - // 'other' property containing the position of the secondary cursor - // on a bidi boundary. - // A cursor Pos(line, char, "before") is on the same visual line as `char - 1` - // and after `char - 1` in writing order of `char - 1` - // A cursor Pos(line, char, "after") is on the same visual line as `char` - // and before `char` in writing order of `char` - // Examples (upper-case letters are RTL, lower-case are LTR): - // Pos(0, 1, ...) - // before after - // ab a|b a|b - // aB a|B aB| - // Ab |Ab A|b - // AB B|A B|A - // Every position after the last character on a line is considered to stick - // to the last character on the line. - function cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeight) { - lineObj = lineObj || getLine(cm.doc, pos.line); - if (!preparedMeasure) { preparedMeasure = prepareMeasureForLine(cm, lineObj); } - function get(ch, right) { - var m = measureCharPrepared(cm, preparedMeasure, ch, right ? "right" : "left", varHeight); - if (right) { m.left = m.right; } else { m.right = m.left; } - return intoCoordSystem(cm, lineObj, m, context) - } - var order = getOrder(lineObj, cm.doc.direction), ch = pos.ch, sticky = pos.sticky; - if (ch >= lineObj.text.length) { - ch = lineObj.text.length; - sticky = "before"; - } else if (ch <= 0) { - ch = 0; - sticky = "after"; - } - if (!order) { return get(sticky == "before" ? ch - 1 : ch, sticky == "before") } - - function getBidi(ch, partPos, invert) { - var part = order[partPos], right = part.level == 1; - return get(invert ? ch - 1 : ch, right != invert) - } - var partPos = getBidiPartAt(order, ch, sticky); - var other = bidiOther; - var val = getBidi(ch, partPos, sticky == "before"); - if (other != null) { val.other = getBidi(ch, other, sticky != "before"); } - return val - } - - // Used to cheaply estimate the coordinates for a position. Used for - // intermediate scroll updates. - function estimateCoords(cm, pos) { - var left = 0; - pos = clipPos(cm.doc, pos); - if (!cm.options.lineWrapping) { left = charWidth(cm.display) * pos.ch; } - var lineObj = getLine(cm.doc, pos.line); - var top = heightAtLine(lineObj) + paddingTop(cm.display); - return {left: left, right: left, top: top, bottom: top + lineObj.height} - } - - // Positions returned by coordsChar contain some extra information. - // xRel is the relative x position of the input coordinates compared - // to the found position (so xRel > 0 means the coordinates are to - // the right of the character position, for example). When outside - // is true, that means the coordinates lie outside the line's - // vertical range. - function PosWithInfo(line, ch, sticky, outside, xRel) { - var pos = Pos(line, ch, sticky); - pos.xRel = xRel; - if (outside) { pos.outside = outside; } - return pos - } - - // Compute the character position closest to the given coordinates. - // Input must be lineSpace-local ("div" coordinate system). - function coordsChar(cm, x, y) { - var doc = cm.doc; - y += cm.display.viewOffset; - if (y < 0) { return PosWithInfo(doc.first, 0, null, -1, -1) } - var lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1; - if (lineN > last) - { return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, null, 1, 1) } - if (x < 0) { x = 0; } - - var lineObj = getLine(doc, lineN); - for (;;) { - var found = coordsCharInner(cm, lineObj, lineN, x, y); - var collapsed = collapsedSpanAround(lineObj, found.ch + (found.xRel > 0 || found.outside > 0 ? 1 : 0)); - if (!collapsed) { return found } - var rangeEnd = collapsed.find(1); - if (rangeEnd.line == lineN) { return rangeEnd } - lineObj = getLine(doc, lineN = rangeEnd.line); - } - } - - function wrappedLineExtent(cm, lineObj, preparedMeasure, y) { - y -= widgetTopHeight(lineObj); - var end = lineObj.text.length; - var begin = findFirst(function (ch) { return measureCharPrepared(cm, preparedMeasure, ch - 1).bottom <= y; }, end, 0); - end = findFirst(function (ch) { return measureCharPrepared(cm, preparedMeasure, ch).top > y; }, begin, end); - return {begin: begin, end: end} - } - - function wrappedLineExtentChar(cm, lineObj, preparedMeasure, target) { - if (!preparedMeasure) { preparedMeasure = prepareMeasureForLine(cm, lineObj); } - var targetTop = intoCoordSystem(cm, lineObj, measureCharPrepared(cm, preparedMeasure, target), "line").top; - return wrappedLineExtent(cm, lineObj, preparedMeasure, targetTop) - } - - // Returns true if the given side of a box is after the given - // coordinates, in top-to-bottom, left-to-right order. - function boxIsAfter(box, x, y, left) { - return box.bottom <= y ? false : box.top > y ? true : (left ? box.left : box.right) > x - } - - function coordsCharInner(cm, lineObj, lineNo, x, y) { - // Move y into line-local coordinate space - y -= heightAtLine(lineObj); - var preparedMeasure = prepareMeasureForLine(cm, lineObj); - // When directly calling `measureCharPrepared`, we have to adjust - // for the widgets at this line. - var widgetHeight = widgetTopHeight(lineObj); - var begin = 0, end = lineObj.text.length, ltr = true; - - var order = getOrder(lineObj, cm.doc.direction); - // If the line isn't plain left-to-right text, first figure out - // which bidi section the coordinates fall into. - if (order) { - var part = (cm.options.lineWrapping ? coordsBidiPartWrapped : coordsBidiPart) - (cm, lineObj, lineNo, preparedMeasure, order, x, y); - ltr = part.level != 1; - // The awkward -1 offsets are needed because findFirst (called - // on these below) will treat its first bound as inclusive, - // second as exclusive, but we want to actually address the - // characters in the part's range - begin = ltr ? part.from : part.to - 1; - end = ltr ? part.to : part.from - 1; - } - - // A binary search to find the first character whose bounding box - // starts after the coordinates. If we run across any whose box wrap - // the coordinates, store that. - var chAround = null, boxAround = null; - var ch = findFirst(function (ch) { - var box = measureCharPrepared(cm, preparedMeasure, ch); - box.top += widgetHeight; box.bottom += widgetHeight; - if (!boxIsAfter(box, x, y, false)) { return false } - if (box.top <= y && box.left <= x) { - chAround = ch; - boxAround = box; - } - return true - }, begin, end); - - var baseX, sticky, outside = false; - // If a box around the coordinates was found, use that - if (boxAround) { - // Distinguish coordinates nearer to the left or right side of the box - var atLeft = x - boxAround.left < boxAround.right - x, atStart = atLeft == ltr; - ch = chAround + (atStart ? 0 : 1); - sticky = atStart ? "after" : "before"; - baseX = atLeft ? boxAround.left : boxAround.right; - } else { - // (Adjust for extended bound, if necessary.) - if (!ltr && (ch == end || ch == begin)) { ch++; } - // To determine which side to associate with, get the box to the - // left of the character and compare it's vertical position to the - // coordinates - sticky = ch == 0 ? "after" : ch == lineObj.text.length ? "before" : - (measureCharPrepared(cm, preparedMeasure, ch - (ltr ? 1 : 0)).bottom + widgetHeight <= y) == ltr ? - "after" : "before"; - // Now get accurate coordinates for this place, in order to get a - // base X position - var coords = cursorCoords(cm, Pos(lineNo, ch, sticky), "line", lineObj, preparedMeasure); - baseX = coords.left; - outside = y < coords.top ? -1 : y >= coords.bottom ? 1 : 0; - } - - ch = skipExtendingChars(lineObj.text, ch, 1); - return PosWithInfo(lineNo, ch, sticky, outside, x - baseX) - } - - function coordsBidiPart(cm, lineObj, lineNo, preparedMeasure, order, x, y) { - // Bidi parts are sorted left-to-right, and in a non-line-wrapping - // situation, we can take this ordering to correspond to the visual - // ordering. This finds the first part whose end is after the given - // coordinates. - var index = findFirst(function (i) { - var part = order[i], ltr = part.level != 1; - return boxIsAfter(cursorCoords(cm, Pos(lineNo, ltr ? part.to : part.from, ltr ? "before" : "after"), - "line", lineObj, preparedMeasure), x, y, true) - }, 0, order.length - 1); - var part = order[index]; - // If this isn't the first part, the part's start is also after - // the coordinates, and the coordinates aren't on the same line as - // that start, move one part back. - if (index > 0) { - var ltr = part.level != 1; - var start = cursorCoords(cm, Pos(lineNo, ltr ? part.from : part.to, ltr ? "after" : "before"), - "line", lineObj, preparedMeasure); - if (boxIsAfter(start, x, y, true) && start.top > y) - { part = order[index - 1]; } - } - return part - } - - function coordsBidiPartWrapped(cm, lineObj, _lineNo, preparedMeasure, order, x, y) { - // In a wrapped line, rtl text on wrapping boundaries can do things - // that don't correspond to the ordering in our `order` array at - // all, so a binary search doesn't work, and we want to return a - // part that only spans one line so that the binary search in - // coordsCharInner is safe. As such, we first find the extent of the - // wrapped line, and then do a flat search in which we discard any - // spans that aren't on the line. - var ref = wrappedLineExtent(cm, lineObj, preparedMeasure, y); - var begin = ref.begin; - var end = ref.end; - if (/\s/.test(lineObj.text.charAt(end - 1))) { end--; } - var part = null, closestDist = null; - for (var i = 0; i < order.length; i++) { - var p = order[i]; - if (p.from >= end || p.to <= begin) { continue } - var ltr = p.level != 1; - var endX = measureCharPrepared(cm, preparedMeasure, ltr ? Math.min(end, p.to) - 1 : Math.max(begin, p.from)).right; - // Weigh against spans ending before this, so that they are only - // picked if nothing ends after - var dist = endX < x ? x - endX + 1e9 : endX - x; - if (!part || closestDist > dist) { - part = p; - closestDist = dist; - } - } - if (!part) { part = order[order.length - 1]; } - // Clip the part to the wrapped line. - if (part.from < begin) { part = {from: begin, to: part.to, level: part.level}; } - if (part.to > end) { part = {from: part.from, to: end, level: part.level}; } - return part - } - - var measureText; - // Compute the default text height. - function textHeight(display) { - if (display.cachedTextHeight != null) { return display.cachedTextHeight } - if (measureText == null) { - measureText = elt("pre", null, "CodeMirror-line-like"); - // Measure a bunch of lines, for browsers that compute - // fractional heights. - for (var i = 0; i < 49; ++i) { - measureText.appendChild(document.createTextNode("x")); - measureText.appendChild(elt("br")); - } - measureText.appendChild(document.createTextNode("x")); - } - removeChildrenAndAdd(display.measure, measureText); - var height = measureText.offsetHeight / 50; - if (height > 3) { display.cachedTextHeight = height; } - removeChildren(display.measure); - return height || 1 - } - - // Compute the default character width. - function charWidth(display) { - if (display.cachedCharWidth != null) { return display.cachedCharWidth } - var anchor = elt("span", "xxxxxxxxxx"); - var pre = elt("pre", [anchor], "CodeMirror-line-like"); - removeChildrenAndAdd(display.measure, pre); - var rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10; - if (width > 2) { display.cachedCharWidth = width; } - return width || 10 - } - - // Do a bulk-read of the DOM positions and sizes needed to draw the - // view, so that we don't interleave reading and writing to the DOM. - function getDimensions(cm) { - var d = cm.display, left = {}, width = {}; - var gutterLeft = d.gutters.clientLeft; - for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) { - var id = cm.display.gutterSpecs[i].className; - left[id] = n.offsetLeft + n.clientLeft + gutterLeft; - width[id] = n.clientWidth; - } - return {fixedPos: compensateForHScroll(d), - gutterTotalWidth: d.gutters.offsetWidth, - gutterLeft: left, - gutterWidth: width, - wrapperWidth: d.wrapper.clientWidth} - } - - // Computes display.scroller.scrollLeft + display.gutters.offsetWidth, - // but using getBoundingClientRect to get a sub-pixel-accurate - // result. - function compensateForHScroll(display) { - return display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left - } - - // Returns a function that estimates the height of a line, to use as - // first approximation until the line becomes visible (and is thus - // properly measurable). - function estimateHeight(cm) { - var th = textHeight(cm.display), wrapping = cm.options.lineWrapping; - var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3); - return function (line) { - if (lineIsHidden(cm.doc, line)) { return 0 } - - var widgetsHeight = 0; - if (line.widgets) { for (var i = 0; i < line.widgets.length; i++) { - if (line.widgets[i].height) { widgetsHeight += line.widgets[i].height; } - } } - - if (wrapping) - { return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th } - else - { return widgetsHeight + th } - } - } - - function estimateLineHeights(cm) { - var doc = cm.doc, est = estimateHeight(cm); - doc.iter(function (line) { - var estHeight = est(line); - if (estHeight != line.height) { updateLineHeight(line, estHeight); } - }); - } - - // Given a mouse event, find the corresponding position. If liberal - // is false, it checks whether a gutter or scrollbar was clicked, - // and returns null if it was. forRect is used by rectangular - // selections, and tries to estimate a character position even for - // coordinates beyond the right of the text. - function posFromMouse(cm, e, liberal, forRect) { - var display = cm.display; - if (!liberal && e_target(e).getAttribute("cm-not-content") == "true") { return null } - - var x, y, space = display.lineSpace.getBoundingClientRect(); - // Fails unpredictably on IE[67] when mouse is dragged around quickly. - try { x = e.clientX - space.left; y = e.clientY - space.top; } - catch (e$1) { return null } - var coords = coordsChar(cm, x, y), line; - if (forRect && coords.xRel > 0 && (line = getLine(cm.doc, coords.line).text).length == coords.ch) { - var colDiff = countColumn(line, line.length, cm.options.tabSize) - line.length; - coords = Pos(coords.line, Math.max(0, Math.round((x - paddingH(cm.display).left) / charWidth(cm.display)) - colDiff)); - } - return coords - } - - // Find the view element corresponding to a given line. Return null - // when the line isn't visible. - function findViewIndex(cm, n) { - if (n >= cm.display.viewTo) { return null } - n -= cm.display.viewFrom; - if (n < 0) { return null } - var view = cm.display.view; - for (var i = 0; i < view.length; i++) { - n -= view[i].size; - if (n < 0) { return i } - } - } - - // Updates the display.view data structure for a given change to the - // document. From and to are in pre-change coordinates. Lendiff is - // the amount of lines added or subtracted by the change. This is - // used for changes that span multiple lines, or change the way - // lines are divided into visual lines. regLineChange (below) - // registers single-line changes. - function regChange(cm, from, to, lendiff) { - if (from == null) { from = cm.doc.first; } - if (to == null) { to = cm.doc.first + cm.doc.size; } - if (!lendiff) { lendiff = 0; } - - var display = cm.display; - if (lendiff && to < display.viewTo && - (display.updateLineNumbers == null || display.updateLineNumbers > from)) - { display.updateLineNumbers = from; } - - cm.curOp.viewChanged = true; - - if (from >= display.viewTo) { // Change after - if (sawCollapsedSpans && visualLineNo(cm.doc, from) < display.viewTo) - { resetView(cm); } - } else if (to <= display.viewFrom) { // Change before - if (sawCollapsedSpans && visualLineEndNo(cm.doc, to + lendiff) > display.viewFrom) { - resetView(cm); - } else { - display.viewFrom += lendiff; - display.viewTo += lendiff; - } - } else if (from <= display.viewFrom && to >= display.viewTo) { // Full overlap - resetView(cm); - } else if (from <= display.viewFrom) { // Top overlap - var cut = viewCuttingPoint(cm, to, to + lendiff, 1); - if (cut) { - display.view = display.view.slice(cut.index); - display.viewFrom = cut.lineN; - display.viewTo += lendiff; - } else { - resetView(cm); - } - } else if (to >= display.viewTo) { // Bottom overlap - var cut$1 = viewCuttingPoint(cm, from, from, -1); - if (cut$1) { - display.view = display.view.slice(0, cut$1.index); - display.viewTo = cut$1.lineN; - } else { - resetView(cm); - } - } else { // Gap in the middle - var cutTop = viewCuttingPoint(cm, from, from, -1); - var cutBot = viewCuttingPoint(cm, to, to + lendiff, 1); - if (cutTop && cutBot) { - display.view = display.view.slice(0, cutTop.index) - .concat(buildViewArray(cm, cutTop.lineN, cutBot.lineN)) - .concat(display.view.slice(cutBot.index)); - display.viewTo += lendiff; - } else { - resetView(cm); - } - } - - var ext = display.externalMeasured; - if (ext) { - if (to < ext.lineN) - { ext.lineN += lendiff; } - else if (from < ext.lineN + ext.size) - { display.externalMeasured = null; } - } - } - - // Register a change to a single line. Type must be one of "text", - // "gutter", "class", "widget" - function regLineChange(cm, line, type) { - cm.curOp.viewChanged = true; - var display = cm.display, ext = cm.display.externalMeasured; - if (ext && line >= ext.lineN && line < ext.lineN + ext.size) - { display.externalMeasured = null; } - - if (line < display.viewFrom || line >= display.viewTo) { return } - var lineView = display.view[findViewIndex(cm, line)]; - if (lineView.node == null) { return } - var arr = lineView.changes || (lineView.changes = []); - if (indexOf(arr, type) == -1) { arr.push(type); } - } - - // Clear the view. - function resetView(cm) { - cm.display.viewFrom = cm.display.viewTo = cm.doc.first; - cm.display.view = []; - cm.display.viewOffset = 0; - } - - function viewCuttingPoint(cm, oldN, newN, dir) { - var index = findViewIndex(cm, oldN), diff, view = cm.display.view; - if (!sawCollapsedSpans || newN == cm.doc.first + cm.doc.size) - { return {index: index, lineN: newN} } - var n = cm.display.viewFrom; - for (var i = 0; i < index; i++) - { n += view[i].size; } - if (n != oldN) { - if (dir > 0) { - if (index == view.length - 1) { return null } - diff = (n + view[index].size) - oldN; - index++; - } else { - diff = n - oldN; - } - oldN += diff; newN += diff; - } - while (visualLineNo(cm.doc, newN) != newN) { - if (index == (dir < 0 ? 0 : view.length - 1)) { return null } - newN += dir * view[index - (dir < 0 ? 1 : 0)].size; - index += dir; - } - return {index: index, lineN: newN} - } - - // Force the view to cover a given range, adding empty view element - // or clipping off existing ones as needed. - function adjustView(cm, from, to) { - var display = cm.display, view = display.view; - if (view.length == 0 || from >= display.viewTo || to <= display.viewFrom) { - display.view = buildViewArray(cm, from, to); - display.viewFrom = from; - } else { - if (display.viewFrom > from) - { display.view = buildViewArray(cm, from, display.viewFrom).concat(display.view); } - else if (display.viewFrom < from) - { display.view = display.view.slice(findViewIndex(cm, from)); } - display.viewFrom = from; - if (display.viewTo < to) - { display.view = display.view.concat(buildViewArray(cm, display.viewTo, to)); } - else if (display.viewTo > to) - { display.view = display.view.slice(0, findViewIndex(cm, to)); } - } - display.viewTo = to; - } - - // Count the number of lines in the view whose DOM representation is - // out of date (or nonexistent). - function countDirtyView(cm) { - var view = cm.display.view, dirty = 0; - for (var i = 0; i < view.length; i++) { - var lineView = view[i]; - if (!lineView.hidden && (!lineView.node || lineView.changes)) { ++dirty; } - } - return dirty - } - - function updateSelection(cm) { - cm.display.input.showSelection(cm.display.input.prepareSelection()); - } - - function prepareSelection(cm, primary) { - if ( primary === void 0 ) primary = true; - - var doc = cm.doc, result = {}; - var curFragment = result.cursors = document.createDocumentFragment(); - var selFragment = result.selection = document.createDocumentFragment(); - - for (var i = 0; i < doc.sel.ranges.length; i++) { - if (!primary && i == doc.sel.primIndex) { continue } - var range = doc.sel.ranges[i]; - if (range.from().line >= cm.display.viewTo || range.to().line < cm.display.viewFrom) { continue } - var collapsed = range.empty(); - if (collapsed || cm.options.showCursorWhenSelecting) - { drawSelectionCursor(cm, range.head, curFragment); } - if (!collapsed) - { drawSelectionRange(cm, range, selFragment); } - } - return result - } - - // Draws a cursor for the given range - function drawSelectionCursor(cm, head, output) { - var pos = cursorCoords(cm, head, "div", null, null, !cm.options.singleCursorHeightPerLine); - - var cursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor")); - cursor.style.left = pos.left + "px"; - cursor.style.top = pos.top + "px"; - cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px"; - - if (pos.other) { - // Secondary cursor, shown when on a 'jump' in bi-directional text - var otherCursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor")); - otherCursor.style.display = ""; - otherCursor.style.left = pos.other.left + "px"; - otherCursor.style.top = pos.other.top + "px"; - otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px"; - } - } - - function cmpCoords(a, b) { return a.top - b.top || a.left - b.left } - - // Draws the given range as a highlighted selection - function drawSelectionRange(cm, range, output) { - var display = cm.display, doc = cm.doc; - var fragment = document.createDocumentFragment(); - var padding = paddingH(cm.display), leftSide = padding.left; - var rightSide = Math.max(display.sizerWidth, displayWidth(cm) - display.sizer.offsetLeft) - padding.right; - var docLTR = doc.direction == "ltr"; - - function add(left, top, width, bottom) { - if (top < 0) { top = 0; } - top = Math.round(top); - bottom = Math.round(bottom); - fragment.appendChild(elt("div", null, "CodeMirror-selected", ("position: absolute; left: " + left + "px;\n top: " + top + "px; width: " + (width == null ? rightSide - left : width) + "px;\n height: " + (bottom - top) + "px"))); - } - - function drawForLine(line, fromArg, toArg) { - var lineObj = getLine(doc, line); - var lineLen = lineObj.text.length; - var start, end; - function coords(ch, bias) { - return charCoords(cm, Pos(line, ch), "div", lineObj, bias) - } - - function wrapX(pos, dir, side) { - var extent = wrappedLineExtentChar(cm, lineObj, null, pos); - var prop = (dir == "ltr") == (side == "after") ? "left" : "right"; - var ch = side == "after" ? extent.begin : extent.end - (/\s/.test(lineObj.text.charAt(extent.end - 1)) ? 2 : 1); - return coords(ch, prop)[prop] - } - - var order = getOrder(lineObj, doc.direction); - iterateBidiSections(order, fromArg || 0, toArg == null ? lineLen : toArg, function (from, to, dir, i) { - var ltr = dir == "ltr"; - var fromPos = coords(from, ltr ? "left" : "right"); - var toPos = coords(to - 1, ltr ? "right" : "left"); - - var openStart = fromArg == null && from == 0, openEnd = toArg == null && to == lineLen; - var first = i == 0, last = !order || i == order.length - 1; - if (toPos.top - fromPos.top <= 3) { // Single line - var openLeft = (docLTR ? openStart : openEnd) && first; - var openRight = (docLTR ? openEnd : openStart) && last; - var left = openLeft ? leftSide : (ltr ? fromPos : toPos).left; - var right = openRight ? rightSide : (ltr ? toPos : fromPos).right; - add(left, fromPos.top, right - left, fromPos.bottom); - } else { // Multiple lines - var topLeft, topRight, botLeft, botRight; - if (ltr) { - topLeft = docLTR && openStart && first ? leftSide : fromPos.left; - topRight = docLTR ? rightSide : wrapX(from, dir, "before"); - botLeft = docLTR ? leftSide : wrapX(to, dir, "after"); - botRight = docLTR && openEnd && last ? rightSide : toPos.right; - } else { - topLeft = !docLTR ? leftSide : wrapX(from, dir, "before"); - topRight = !docLTR && openStart && first ? rightSide : fromPos.right; - botLeft = !docLTR && openEnd && last ? leftSide : toPos.left; - botRight = !docLTR ? rightSide : wrapX(to, dir, "after"); - } - add(topLeft, fromPos.top, topRight - topLeft, fromPos.bottom); - if (fromPos.bottom < toPos.top) { add(leftSide, fromPos.bottom, null, toPos.top); } - add(botLeft, toPos.top, botRight - botLeft, toPos.bottom); - } - - if (!start || cmpCoords(fromPos, start) < 0) { start = fromPos; } - if (cmpCoords(toPos, start) < 0) { start = toPos; } - if (!end || cmpCoords(fromPos, end) < 0) { end = fromPos; } - if (cmpCoords(toPos, end) < 0) { end = toPos; } - }); - return {start: start, end: end} - } - - var sFrom = range.from(), sTo = range.to(); - if (sFrom.line == sTo.line) { - drawForLine(sFrom.line, sFrom.ch, sTo.ch); - } else { - var fromLine = getLine(doc, sFrom.line), toLine = getLine(doc, sTo.line); - var singleVLine = visualLine(fromLine) == visualLine(toLine); - var leftEnd = drawForLine(sFrom.line, sFrom.ch, singleVLine ? fromLine.text.length + 1 : null).end; - var rightStart = drawForLine(sTo.line, singleVLine ? 0 : null, sTo.ch).start; - if (singleVLine) { - if (leftEnd.top < rightStart.top - 2) { - add(leftEnd.right, leftEnd.top, null, leftEnd.bottom); - add(leftSide, rightStart.top, rightStart.left, rightStart.bottom); - } else { - add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom); - } - } - if (leftEnd.bottom < rightStart.top) - { add(leftSide, leftEnd.bottom, null, rightStart.top); } - } - - output.appendChild(fragment); - } - - // Cursor-blinking - function restartBlink(cm) { - if (!cm.state.focused) { return } - var display = cm.display; - clearInterval(display.blinker); - var on = true; - display.cursorDiv.style.visibility = ""; - if (cm.options.cursorBlinkRate > 0) - { display.blinker = setInterval(function () { return display.cursorDiv.style.visibility = (on = !on) ? "" : "hidden"; }, - cm.options.cursorBlinkRate); } - else if (cm.options.cursorBlinkRate < 0) - { display.cursorDiv.style.visibility = "hidden"; } - } - - function ensureFocus(cm) { - if (!cm.state.focused) { cm.display.input.focus(); onFocus(cm); } - } - - function delayBlurEvent(cm) { - cm.state.delayingBlurEvent = true; - setTimeout(function () { if (cm.state.delayingBlurEvent) { - cm.state.delayingBlurEvent = false; - onBlur(cm); - } }, 100); - } - - function onFocus(cm, e) { - if (cm.state.delayingBlurEvent) { cm.state.delayingBlurEvent = false; } - - if (cm.options.readOnly == "nocursor") { return } - if (!cm.state.focused) { - signal(cm, "focus", cm, e); - cm.state.focused = true; - addClass(cm.display.wrapper, "CodeMirror-focused"); - // This test prevents this from firing when a context - // menu is closed (since the input reset would kill the - // select-all detection hack) - if (!cm.curOp && cm.display.selForContextMenu != cm.doc.sel) { - cm.display.input.reset(); - if (webkit) { setTimeout(function () { return cm.display.input.reset(true); }, 20); } // Issue #1730 - } - cm.display.input.receivedFocus(); - } - restartBlink(cm); - } - function onBlur(cm, e) { - if (cm.state.delayingBlurEvent) { return } - - if (cm.state.focused) { - signal(cm, "blur", cm, e); - cm.state.focused = false; - rmClass(cm.display.wrapper, "CodeMirror-focused"); - } - clearInterval(cm.display.blinker); - setTimeout(function () { if (!cm.state.focused) { cm.display.shift = false; } }, 150); - } - - // Read the actual heights of the rendered lines, and update their - // stored heights to match. - function updateHeightsInViewport(cm) { - var display = cm.display; - var prevBottom = display.lineDiv.offsetTop; - for (var i = 0; i < display.view.length; i++) { - var cur = display.view[i], wrapping = cm.options.lineWrapping; - var height = (void 0), width = 0; - if (cur.hidden) { continue } - if (ie && ie_version < 8) { - var bot = cur.node.offsetTop + cur.node.offsetHeight; - height = bot - prevBottom; - prevBottom = bot; - } else { - var box = cur.node.getBoundingClientRect(); - height = box.bottom - box.top; - // Check that lines don't extend past the right of the current - // editor width - if (!wrapping && cur.text.firstChild) - { width = cur.text.firstChild.getBoundingClientRect().right - box.left - 1; } - } - var diff = cur.line.height - height; - if (diff > .005 || diff < -.005) { - updateLineHeight(cur.line, height); - updateWidgetHeight(cur.line); - if (cur.rest) { for (var j = 0; j < cur.rest.length; j++) - { updateWidgetHeight(cur.rest[j]); } } - } - if (width > cm.display.sizerWidth) { - var chWidth = Math.ceil(width / charWidth(cm.display)); - if (chWidth > cm.display.maxLineLength) { - cm.display.maxLineLength = chWidth; - cm.display.maxLine = cur.line; - cm.display.maxLineChanged = true; - } - } - } - } - - // Read and store the height of line widgets associated with the - // given line. - function updateWidgetHeight(line) { - if (line.widgets) { for (var i = 0; i < line.widgets.length; ++i) { - var w = line.widgets[i], parent = w.node.parentNode; - if (parent) { w.height = parent.offsetHeight; } - } } - } - - // Compute the lines that are visible in a given viewport (defaults - // the the current scroll position). viewport may contain top, - // height, and ensure (see op.scrollToPos) properties. - function visibleLines(display, doc, viewport) { - var top = viewport && viewport.top != null ? Math.max(0, viewport.top) : display.scroller.scrollTop; - top = Math.floor(top - paddingTop(display)); - var bottom = viewport && viewport.bottom != null ? viewport.bottom : top + display.wrapper.clientHeight; - - var from = lineAtHeight(doc, top), to = lineAtHeight(doc, bottom); - // Ensure is a {from: {line, ch}, to: {line, ch}} object, and - // forces those lines into the viewport (if possible). - if (viewport && viewport.ensure) { - var ensureFrom = viewport.ensure.from.line, ensureTo = viewport.ensure.to.line; - if (ensureFrom < from) { - from = ensureFrom; - to = lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight); - } else if (Math.min(ensureTo, doc.lastLine()) >= to) { - from = lineAtHeight(doc, heightAtLine(getLine(doc, ensureTo)) - display.wrapper.clientHeight); - to = ensureTo; - } - } - return {from: from, to: Math.max(to, from + 1)} - } - - // SCROLLING THINGS INTO VIEW - - // If an editor sits on the top or bottom of the window, partially - // scrolled out of view, this ensures that the cursor is visible. - function maybeScrollWindow(cm, rect) { - if (signalDOMEvent(cm, "scrollCursorIntoView")) { return } - - var display = cm.display, box = display.sizer.getBoundingClientRect(), doScroll = null; - if (rect.top + box.top < 0) { doScroll = true; } - else if (rect.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) { doScroll = false; } - if (doScroll != null && !phantom) { - var scrollNode = elt("div", "\u200b", null, ("position: absolute;\n top: " + (rect.top - display.viewOffset - paddingTop(cm.display)) + "px;\n height: " + (rect.bottom - rect.top + scrollGap(cm) + display.barHeight) + "px;\n left: " + (rect.left) + "px; width: " + (Math.max(2, rect.right - rect.left)) + "px;")); - cm.display.lineSpace.appendChild(scrollNode); - scrollNode.scrollIntoView(doScroll); - cm.display.lineSpace.removeChild(scrollNode); - } - } - - // Scroll a given position into view (immediately), verifying that - // it actually became visible (as line heights are accurately - // measured, the position of something may 'drift' during drawing). - function scrollPosIntoView(cm, pos, end, margin) { - if (margin == null) { margin = 0; } - var rect; - if (!cm.options.lineWrapping && pos == end) { - // Set pos and end to the cursor positions around the character pos sticks to - // If pos.sticky == "before", that is around pos.ch - 1, otherwise around pos.ch - // If pos == Pos(_, 0, "before"), pos and end are unchanged - pos = pos.ch ? Pos(pos.line, pos.sticky == "before" ? pos.ch - 1 : pos.ch, "after") : pos; - end = pos.sticky == "before" ? Pos(pos.line, pos.ch + 1, "before") : pos; - } - for (var limit = 0; limit < 5; limit++) { - var changed = false; - var coords = cursorCoords(cm, pos); - var endCoords = !end || end == pos ? coords : cursorCoords(cm, end); - rect = {left: Math.min(coords.left, endCoords.left), - top: Math.min(coords.top, endCoords.top) - margin, - right: Math.max(coords.left, endCoords.left), - bottom: Math.max(coords.bottom, endCoords.bottom) + margin}; - var scrollPos = calculateScrollPos(cm, rect); - var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft; - if (scrollPos.scrollTop != null) { - updateScrollTop(cm, scrollPos.scrollTop); - if (Math.abs(cm.doc.scrollTop - startTop) > 1) { changed = true; } - } - if (scrollPos.scrollLeft != null) { - setScrollLeft(cm, scrollPos.scrollLeft); - if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) { changed = true; } - } - if (!changed) { break } - } - return rect - } - - // Scroll a given set of coordinates into view (immediately). - function scrollIntoView(cm, rect) { - var scrollPos = calculateScrollPos(cm, rect); - if (scrollPos.scrollTop != null) { updateScrollTop(cm, scrollPos.scrollTop); } - if (scrollPos.scrollLeft != null) { setScrollLeft(cm, scrollPos.scrollLeft); } - } - - // Calculate a new scroll position needed to scroll the given - // rectangle into view. Returns an object with scrollTop and - // scrollLeft properties. When these are undefined, the - // vertical/horizontal position does not need to be adjusted. - function calculateScrollPos(cm, rect) { - var display = cm.display, snapMargin = textHeight(cm.display); - if (rect.top < 0) { rect.top = 0; } - var screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop; - var screen = displayHeight(cm), result = {}; - if (rect.bottom - rect.top > screen) { rect.bottom = rect.top + screen; } - var docBottom = cm.doc.height + paddingVert(display); - var atTop = rect.top < snapMargin, atBottom = rect.bottom > docBottom - snapMargin; - if (rect.top < screentop) { - result.scrollTop = atTop ? 0 : rect.top; - } else if (rect.bottom > screentop + screen) { - var newTop = Math.min(rect.top, (atBottom ? docBottom : rect.bottom) - screen); - if (newTop != screentop) { result.scrollTop = newTop; } - } - - var screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft; - var screenw = displayWidth(cm) - (cm.options.fixedGutter ? display.gutters.offsetWidth : 0); - var tooWide = rect.right - rect.left > screenw; - if (tooWide) { rect.right = rect.left + screenw; } - if (rect.left < 10) - { result.scrollLeft = 0; } - else if (rect.left < screenleft) - { result.scrollLeft = Math.max(0, rect.left - (tooWide ? 0 : 10)); } - else if (rect.right > screenw + screenleft - 3) - { result.scrollLeft = rect.right + (tooWide ? 0 : 10) - screenw; } - return result - } - - // Store a relative adjustment to the scroll position in the current - // operation (to be applied when the operation finishes). - function addToScrollTop(cm, top) { - if (top == null) { return } - resolveScrollToPos(cm); - cm.curOp.scrollTop = (cm.curOp.scrollTop == null ? cm.doc.scrollTop : cm.curOp.scrollTop) + top; - } - - // Make sure that at the end of the operation the current cursor is - // shown. - function ensureCursorVisible(cm) { - resolveScrollToPos(cm); - var cur = cm.getCursor(); - cm.curOp.scrollToPos = {from: cur, to: cur, margin: cm.options.cursorScrollMargin}; - } - - function scrollToCoords(cm, x, y) { - if (x != null || y != null) { resolveScrollToPos(cm); } - if (x != null) { cm.curOp.scrollLeft = x; } - if (y != null) { cm.curOp.scrollTop = y; } - } - - function scrollToRange(cm, range) { - resolveScrollToPos(cm); - cm.curOp.scrollToPos = range; - } - - // When an operation has its scrollToPos property set, and another - // scroll action is applied before the end of the operation, this - // 'simulates' scrolling that position into view in a cheap way, so - // that the effect of intermediate scroll commands is not ignored. - function resolveScrollToPos(cm) { - var range = cm.curOp.scrollToPos; - if (range) { - cm.curOp.scrollToPos = null; - var from = estimateCoords(cm, range.from), to = estimateCoords(cm, range.to); - scrollToCoordsRange(cm, from, to, range.margin); - } - } - - function scrollToCoordsRange(cm, from, to, margin) { - var sPos = calculateScrollPos(cm, { - left: Math.min(from.left, to.left), - top: Math.min(from.top, to.top) - margin, - right: Math.max(from.right, to.right), - bottom: Math.max(from.bottom, to.bottom) + margin - }); - scrollToCoords(cm, sPos.scrollLeft, sPos.scrollTop); - } - - // Sync the scrollable area and scrollbars, ensure the viewport - // covers the visible area. - function updateScrollTop(cm, val) { - if (Math.abs(cm.doc.scrollTop - val) < 2) { return } - if (!gecko) { updateDisplaySimple(cm, {top: val}); } - setScrollTop(cm, val, true); - if (gecko) { updateDisplaySimple(cm); } - startWorker(cm, 100); - } - - function setScrollTop(cm, val, forceScroll) { - val = Math.max(0, Math.min(cm.display.scroller.scrollHeight - cm.display.scroller.clientHeight, val)); - if (cm.display.scroller.scrollTop == val && !forceScroll) { return } - cm.doc.scrollTop = val; - cm.display.scrollbars.setScrollTop(val); - if (cm.display.scroller.scrollTop != val) { cm.display.scroller.scrollTop = val; } - } - - // Sync scroller and scrollbar, ensure the gutter elements are - // aligned. - function setScrollLeft(cm, val, isScroller, forceScroll) { - val = Math.max(0, Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth)); - if ((isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) && !forceScroll) { return } - cm.doc.scrollLeft = val; - alignHorizontally(cm); - if (cm.display.scroller.scrollLeft != val) { cm.display.scroller.scrollLeft = val; } - cm.display.scrollbars.setScrollLeft(val); - } - - // SCROLLBARS - - // Prepare DOM reads needed to update the scrollbars. Done in one - // shot to minimize update/measure roundtrips. - function measureForScrollbars(cm) { - var d = cm.display, gutterW = d.gutters.offsetWidth; - var docH = Math.round(cm.doc.height + paddingVert(cm.display)); - return { - clientHeight: d.scroller.clientHeight, - viewHeight: d.wrapper.clientHeight, - scrollWidth: d.scroller.scrollWidth, clientWidth: d.scroller.clientWidth, - viewWidth: d.wrapper.clientWidth, - barLeft: cm.options.fixedGutter ? gutterW : 0, - docHeight: docH, - scrollHeight: docH + scrollGap(cm) + d.barHeight, - nativeBarWidth: d.nativeBarWidth, - gutterWidth: gutterW - } - } - - var NativeScrollbars = function(place, scroll, cm) { - this.cm = cm; - var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar"); - var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar"); - vert.tabIndex = horiz.tabIndex = -1; - place(vert); place(horiz); - - on(vert, "scroll", function () { - if (vert.clientHeight) { scroll(vert.scrollTop, "vertical"); } - }); - on(horiz, "scroll", function () { - if (horiz.clientWidth) { scroll(horiz.scrollLeft, "horizontal"); } - }); - - this.checkedZeroWidth = false; - // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8). - if (ie && ie_version < 8) { this.horiz.style.minHeight = this.vert.style.minWidth = "18px"; } - }; - - NativeScrollbars.prototype.update = function (measure) { - var needsH = measure.scrollWidth > measure.clientWidth + 1; - var needsV = measure.scrollHeight > measure.clientHeight + 1; - var sWidth = measure.nativeBarWidth; - - if (needsV) { - this.vert.style.display = "block"; - this.vert.style.bottom = needsH ? sWidth + "px" : "0"; - var totalHeight = measure.viewHeight - (needsH ? sWidth : 0); - // A bug in IE8 can cause this value to be negative, so guard it. - this.vert.firstChild.style.height = - Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px"; - } else { - this.vert.style.display = ""; - this.vert.firstChild.style.height = "0"; - } - - if (needsH) { - this.horiz.style.display = "block"; - this.horiz.style.right = needsV ? sWidth + "px" : "0"; - this.horiz.style.left = measure.barLeft + "px"; - var totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0); - this.horiz.firstChild.style.width = - Math.max(0, measure.scrollWidth - measure.clientWidth + totalWidth) + "px"; - } else { - this.horiz.style.display = ""; - this.horiz.firstChild.style.width = "0"; - } - - if (!this.checkedZeroWidth && measure.clientHeight > 0) { - if (sWidth == 0) { this.zeroWidthHack(); } - this.checkedZeroWidth = true; - } - - return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0} - }; - - NativeScrollbars.prototype.setScrollLeft = function (pos) { - if (this.horiz.scrollLeft != pos) { this.horiz.scrollLeft = pos; } - if (this.disableHoriz) { this.enableZeroWidthBar(this.horiz, this.disableHoriz, "horiz"); } - }; - - NativeScrollbars.prototype.setScrollTop = function (pos) { - if (this.vert.scrollTop != pos) { this.vert.scrollTop = pos; } - if (this.disableVert) { this.enableZeroWidthBar(this.vert, this.disableVert, "vert"); } - }; - - NativeScrollbars.prototype.zeroWidthHack = function () { - var w = mac && !mac_geMountainLion ? "12px" : "18px"; - this.horiz.style.height = this.vert.style.width = w; - this.horiz.style.pointerEvents = this.vert.style.pointerEvents = "none"; - this.disableHoriz = new Delayed; - this.disableVert = new Delayed; - }; - - NativeScrollbars.prototype.enableZeroWidthBar = function (bar, delay, type) { - bar.style.pointerEvents = "auto"; - function maybeDisable() { - // To find out whether the scrollbar is still visible, we - // check whether the element under the pixel in the bottom - // right corner of the scrollbar box is the scrollbar box - // itself (when the bar is still visible) or its filler child - // (when the bar is hidden). If it is still visible, we keep - // it enabled, if it's hidden, we disable pointer events. - var box = bar.getBoundingClientRect(); - var elt = type == "vert" ? document.elementFromPoint(box.right - 1, (box.top + box.bottom) / 2) - : document.elementFromPoint((box.right + box.left) / 2, box.bottom - 1); - if (elt != bar) { bar.style.pointerEvents = "none"; } - else { delay.set(1000, maybeDisable); } - } - delay.set(1000, maybeDisable); - }; - - NativeScrollbars.prototype.clear = function () { - var parent = this.horiz.parentNode; - parent.removeChild(this.horiz); - parent.removeChild(this.vert); - }; - - var NullScrollbars = function () {}; - - NullScrollbars.prototype.update = function () { return {bottom: 0, right: 0} }; - NullScrollbars.prototype.setScrollLeft = function () {}; - NullScrollbars.prototype.setScrollTop = function () {}; - NullScrollbars.prototype.clear = function () {}; - - function updateScrollbars(cm, measure) { - if (!measure) { measure = measureForScrollbars(cm); } - var startWidth = cm.display.barWidth, startHeight = cm.display.barHeight; - updateScrollbarsInner(cm, measure); - for (var i = 0; i < 4 && startWidth != cm.display.barWidth || startHeight != cm.display.barHeight; i++) { - if (startWidth != cm.display.barWidth && cm.options.lineWrapping) - { updateHeightsInViewport(cm); } - updateScrollbarsInner(cm, measureForScrollbars(cm)); - startWidth = cm.display.barWidth; startHeight = cm.display.barHeight; - } - } - - // Re-synchronize the fake scrollbars with the actual size of the - // content. - function updateScrollbarsInner(cm, measure) { - var d = cm.display; - var sizes = d.scrollbars.update(measure); - - d.sizer.style.paddingRight = (d.barWidth = sizes.right) + "px"; - d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + "px"; - d.heightForcer.style.borderBottom = sizes.bottom + "px solid transparent"; - - if (sizes.right && sizes.bottom) { - d.scrollbarFiller.style.display = "block"; - d.scrollbarFiller.style.height = sizes.bottom + "px"; - d.scrollbarFiller.style.width = sizes.right + "px"; - } else { d.scrollbarFiller.style.display = ""; } - if (sizes.bottom && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) { - d.gutterFiller.style.display = "block"; - d.gutterFiller.style.height = sizes.bottom + "px"; - d.gutterFiller.style.width = measure.gutterWidth + "px"; - } else { d.gutterFiller.style.display = ""; } - } - - var scrollbarModel = {"native": NativeScrollbars, "null": NullScrollbars}; - - function initScrollbars(cm) { - if (cm.display.scrollbars) { - cm.display.scrollbars.clear(); - if (cm.display.scrollbars.addClass) - { rmClass(cm.display.wrapper, cm.display.scrollbars.addClass); } - } - - cm.display.scrollbars = new scrollbarModel[cm.options.scrollbarStyle](function (node) { - cm.display.wrapper.insertBefore(node, cm.display.scrollbarFiller); - // Prevent clicks in the scrollbars from killing focus - on(node, "mousedown", function () { - if (cm.state.focused) { setTimeout(function () { return cm.display.input.focus(); }, 0); } - }); - node.setAttribute("cm-not-content", "true"); - }, function (pos, axis) { - if (axis == "horizontal") { setScrollLeft(cm, pos); } - else { updateScrollTop(cm, pos); } - }, cm); - if (cm.display.scrollbars.addClass) - { addClass(cm.display.wrapper, cm.display.scrollbars.addClass); } - } - - // Operations are used to wrap a series of changes to the editor - // state in such a way that each change won't have to update the - // cursor and display (which would be awkward, slow, and - // error-prone). Instead, display updates are batched and then all - // combined and executed at once. - - var nextOpId = 0; - // Start a new operation. - function startOperation(cm) { - cm.curOp = { - cm: cm, - viewChanged: false, // Flag that indicates that lines might need to be redrawn - startHeight: cm.doc.height, // Used to detect need to update scrollbar - forceUpdate: false, // Used to force a redraw - updateInput: 0, // Whether to reset the input textarea - typing: false, // Whether this reset should be careful to leave existing text (for compositing) - changeObjs: null, // Accumulated changes, for firing change events - cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on - cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already - selectionChanged: false, // Whether the selection needs to be redrawn - updateMaxLine: false, // Set when the widest line needs to be determined anew - scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet - scrollToPos: null, // Used to scroll to a specific position - focus: false, - id: ++nextOpId // Unique ID - }; - pushOperation(cm.curOp); - } - - // Finish an operation, updating the display and signalling delayed events - function endOperation(cm) { - var op = cm.curOp; - if (op) { finishOperation(op, function (group) { - for (var i = 0; i < group.ops.length; i++) - { group.ops[i].cm.curOp = null; } - endOperations(group); - }); } - } - - // The DOM updates done when an operation finishes are batched so - // that the minimum number of relayouts are required. - function endOperations(group) { - var ops = group.ops; - for (var i = 0; i < ops.length; i++) // Read DOM - { endOperation_R1(ops[i]); } - for (var i$1 = 0; i$1 < ops.length; i$1++) // Write DOM (maybe) - { endOperation_W1(ops[i$1]); } - for (var i$2 = 0; i$2 < ops.length; i$2++) // Read DOM - { endOperation_R2(ops[i$2]); } - for (var i$3 = 0; i$3 < ops.length; i$3++) // Write DOM (maybe) - { endOperation_W2(ops[i$3]); } - for (var i$4 = 0; i$4 < ops.length; i$4++) // Read DOM - { endOperation_finish(ops[i$4]); } - } - - function endOperation_R1(op) { - var cm = op.cm, display = cm.display; - maybeClipScrollbars(cm); - if (op.updateMaxLine) { findMaxLine(cm); } - - op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null || - op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom || - op.scrollToPos.to.line >= display.viewTo) || - display.maxLineChanged && cm.options.lineWrapping; - op.update = op.mustUpdate && - new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate); - } - - function endOperation_W1(op) { - op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update); - } - - function endOperation_R2(op) { - var cm = op.cm, display = cm.display; - if (op.updatedDisplay) { updateHeightsInViewport(cm); } - - op.barMeasure = measureForScrollbars(cm); - - // If the max line changed since it was last measured, measure it, - // and ensure the document's width matches it. - // updateDisplay_W2 will use these properties to do the actual resizing - if (display.maxLineChanged && !cm.options.lineWrapping) { - op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3; - cm.display.sizerWidth = op.adjustWidthTo; - op.barMeasure.scrollWidth = - Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth); - op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm)); - } - - if (op.updatedDisplay || op.selectionChanged) - { op.preparedSelection = display.input.prepareSelection(); } - } - - function endOperation_W2(op) { - var cm = op.cm; - - if (op.adjustWidthTo != null) { - cm.display.sizer.style.minWidth = op.adjustWidthTo + "px"; - if (op.maxScrollLeft < cm.doc.scrollLeft) - { setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true); } - cm.display.maxLineChanged = false; - } - - var takeFocus = op.focus && op.focus == activeElt(); - if (op.preparedSelection) - { cm.display.input.showSelection(op.preparedSelection, takeFocus); } - if (op.updatedDisplay || op.startHeight != cm.doc.height) - { updateScrollbars(cm, op.barMeasure); } - if (op.updatedDisplay) - { setDocumentHeight(cm, op.barMeasure); } - - if (op.selectionChanged) { restartBlink(cm); } - - if (cm.state.focused && op.updateInput) - { cm.display.input.reset(op.typing); } - if (takeFocus) { ensureFocus(op.cm); } - } - - function endOperation_finish(op) { - var cm = op.cm, display = cm.display, doc = cm.doc; - - if (op.updatedDisplay) { postUpdateDisplay(cm, op.update); } - - // Abort mouse wheel delta measurement, when scrolling explicitly - if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos)) - { display.wheelStartX = display.wheelStartY = null; } - - // Propagate the scroll position to the actual DOM scroller - if (op.scrollTop != null) { setScrollTop(cm, op.scrollTop, op.forceScroll); } - - if (op.scrollLeft != null) { setScrollLeft(cm, op.scrollLeft, true, true); } - // If we need to scroll a specific position into view, do so. - if (op.scrollToPos) { - var rect = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from), - clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin); - maybeScrollWindow(cm, rect); - } - - // Fire events for markers that are hidden/unidden by editing or - // undoing - var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers; - if (hidden) { for (var i = 0; i < hidden.length; ++i) - { if (!hidden[i].lines.length) { signal(hidden[i], "hide"); } } } - if (unhidden) { for (var i$1 = 0; i$1 < unhidden.length; ++i$1) - { if (unhidden[i$1].lines.length) { signal(unhidden[i$1], "unhide"); } } } - - if (display.wrapper.offsetHeight) - { doc.scrollTop = cm.display.scroller.scrollTop; } - - // Fire change events, and delayed event handlers - if (op.changeObjs) - { signal(cm, "changes", cm, op.changeObjs); } - if (op.update) - { op.update.finish(); } - } - - // Run the given function in an operation - function runInOp(cm, f) { - if (cm.curOp) { return f() } - startOperation(cm); - try { return f() } - finally { endOperation(cm); } - } - // Wraps a function in an operation. Returns the wrapped function. - function operation(cm, f) { - return function() { - if (cm.curOp) { return f.apply(cm, arguments) } - startOperation(cm); - try { return f.apply(cm, arguments) } - finally { endOperation(cm); } - } - } - // Used to add methods to editor and doc instances, wrapping them in - // operations. - function methodOp(f) { - return function() { - if (this.curOp) { return f.apply(this, arguments) } - startOperation(this); - try { return f.apply(this, arguments) } - finally { endOperation(this); } - } - } - function docMethodOp(f) { - return function() { - var cm = this.cm; - if (!cm || cm.curOp) { return f.apply(this, arguments) } - startOperation(cm); - try { return f.apply(this, arguments) } - finally { endOperation(cm); } - } - } - - // HIGHLIGHT WORKER - - function startWorker(cm, time) { - if (cm.doc.highlightFrontier < cm.display.viewTo) - { cm.state.highlight.set(time, bind(highlightWorker, cm)); } - } - - function highlightWorker(cm) { - var doc = cm.doc; - if (doc.highlightFrontier >= cm.display.viewTo) { return } - var end = +new Date + cm.options.workTime; - var context = getContextBefore(cm, doc.highlightFrontier); - var changedLines = []; - - doc.iter(context.line, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function (line) { - if (context.line >= cm.display.viewFrom) { // Visible - var oldStyles = line.styles; - var resetState = line.text.length > cm.options.maxHighlightLength ? copyState(doc.mode, context.state) : null; - var highlighted = highlightLine(cm, line, context, true); - if (resetState) { context.state = resetState; } - line.styles = highlighted.styles; - var oldCls = line.styleClasses, newCls = highlighted.classes; - if (newCls) { line.styleClasses = newCls; } - else if (oldCls) { line.styleClasses = null; } - var ischange = !oldStyles || oldStyles.length != line.styles.length || - oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass); - for (var i = 0; !ischange && i < oldStyles.length; ++i) { ischange = oldStyles[i] != line.styles[i]; } - if (ischange) { changedLines.push(context.line); } - line.stateAfter = context.save(); - context.nextLine(); - } else { - if (line.text.length <= cm.options.maxHighlightLength) - { processLine(cm, line.text, context); } - line.stateAfter = context.line % 5 == 0 ? context.save() : null; - context.nextLine(); - } - if (+new Date > end) { - startWorker(cm, cm.options.workDelay); - return true - } - }); - doc.highlightFrontier = context.line; - doc.modeFrontier = Math.max(doc.modeFrontier, context.line); - if (changedLines.length) { runInOp(cm, function () { - for (var i = 0; i < changedLines.length; i++) - { regLineChange(cm, changedLines[i], "text"); } - }); } - } - - // DISPLAY DRAWING - - var DisplayUpdate = function(cm, viewport, force) { - var display = cm.display; - - this.viewport = viewport; - // Store some values that we'll need later (but don't want to force a relayout for) - this.visible = visibleLines(display, cm.doc, viewport); - this.editorIsHidden = !display.wrapper.offsetWidth; - this.wrapperHeight = display.wrapper.clientHeight; - this.wrapperWidth = display.wrapper.clientWidth; - this.oldDisplayWidth = displayWidth(cm); - this.force = force; - this.dims = getDimensions(cm); - this.events = []; - }; - - DisplayUpdate.prototype.signal = function (emitter, type) { - if (hasHandler(emitter, type)) - { this.events.push(arguments); } - }; - DisplayUpdate.prototype.finish = function () { - for (var i = 0; i < this.events.length; i++) - { signal.apply(null, this.events[i]); } - }; - - function maybeClipScrollbars(cm) { - var display = cm.display; - if (!display.scrollbarsClipped && display.scroller.offsetWidth) { - display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth; - display.heightForcer.style.height = scrollGap(cm) + "px"; - display.sizer.style.marginBottom = -display.nativeBarWidth + "px"; - display.sizer.style.borderRightWidth = scrollGap(cm) + "px"; - display.scrollbarsClipped = true; - } - } - - function selectionSnapshot(cm) { - if (cm.hasFocus()) { return null } - var active = activeElt(); - if (!active || !contains(cm.display.lineDiv, active)) { return null } - var result = {activeElt: active}; - if (window.getSelection) { - var sel = window.getSelection(); - if (sel.anchorNode && sel.extend && contains(cm.display.lineDiv, sel.anchorNode)) { - result.anchorNode = sel.anchorNode; - result.anchorOffset = sel.anchorOffset; - result.focusNode = sel.focusNode; - result.focusOffset = sel.focusOffset; - } - } - return result - } - - function restoreSelection(snapshot) { - if (!snapshot || !snapshot.activeElt || snapshot.activeElt == activeElt()) { return } - snapshot.activeElt.focus(); - if (!/^(INPUT|TEXTAREA)$/.test(snapshot.activeElt.nodeName) && - snapshot.anchorNode && contains(document.body, snapshot.anchorNode) && contains(document.body, snapshot.focusNode)) { - var sel = window.getSelection(), range = document.createRange(); - range.setEnd(snapshot.anchorNode, snapshot.anchorOffset); - range.collapse(false); - sel.removeAllRanges(); - sel.addRange(range); - sel.extend(snapshot.focusNode, snapshot.focusOffset); - } - } - - // Does the actual updating of the line display. Bails out - // (returning false) when there is nothing to be done and forced is - // false. - function updateDisplayIfNeeded(cm, update) { - var display = cm.display, doc = cm.doc; - - if (update.editorIsHidden) { - resetView(cm); - return false - } - - // Bail out if the visible area is already rendered and nothing changed. - if (!update.force && - update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo && - (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) && - display.renderedView == display.view && countDirtyView(cm) == 0) - { return false } - - if (maybeUpdateLineNumberWidth(cm)) { - resetView(cm); - update.dims = getDimensions(cm); - } - - // Compute a suitable new viewport (from & to) - var end = doc.first + doc.size; - var from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first); - var to = Math.min(end, update.visible.to + cm.options.viewportMargin); - if (display.viewFrom < from && from - display.viewFrom < 20) { from = Math.max(doc.first, display.viewFrom); } - if (display.viewTo > to && display.viewTo - to < 20) { to = Math.min(end, display.viewTo); } - if (sawCollapsedSpans) { - from = visualLineNo(cm.doc, from); - to = visualLineEndNo(cm.doc, to); - } - - var different = from != display.viewFrom || to != display.viewTo || - display.lastWrapHeight != update.wrapperHeight || display.lastWrapWidth != update.wrapperWidth; - adjustView(cm, from, to); - - display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom)); - // Position the mover div to align with the current scroll position - cm.display.mover.style.top = display.viewOffset + "px"; - - var toUpdate = countDirtyView(cm); - if (!different && toUpdate == 0 && !update.force && display.renderedView == display.view && - (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo)) - { return false } - - // For big changes, we hide the enclosing element during the - // update, since that speeds up the operations on most browsers. - var selSnapshot = selectionSnapshot(cm); - if (toUpdate > 4) { display.lineDiv.style.display = "none"; } - patchDisplay(cm, display.updateLineNumbers, update.dims); - if (toUpdate > 4) { display.lineDiv.style.display = ""; } - display.renderedView = display.view; - // There might have been a widget with a focused element that got - // hidden or updated, if so re-focus it. - restoreSelection(selSnapshot); - - // Prevent selection and cursors from interfering with the scroll - // width and height. - removeChildren(display.cursorDiv); - removeChildren(display.selectionDiv); - display.gutters.style.height = display.sizer.style.minHeight = 0; - - if (different) { - display.lastWrapHeight = update.wrapperHeight; - display.lastWrapWidth = update.wrapperWidth; - startWorker(cm, 400); - } - - display.updateLineNumbers = null; - - return true - } - - function postUpdateDisplay(cm, update) { - var viewport = update.viewport; - - for (var first = true;; first = false) { - if (!first || !cm.options.lineWrapping || update.oldDisplayWidth == displayWidth(cm)) { - // Clip forced viewport to actual scrollable area. - if (viewport && viewport.top != null) - { viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - displayHeight(cm), viewport.top)}; } - // Updated line heights might result in the drawn area not - // actually covering the viewport. Keep looping until it does. - update.visible = visibleLines(cm.display, cm.doc, viewport); - if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo) - { break } - } else if (first) { - update.visible = visibleLines(cm.display, cm.doc, viewport); - } - if (!updateDisplayIfNeeded(cm, update)) { break } - updateHeightsInViewport(cm); - var barMeasure = measureForScrollbars(cm); - updateSelection(cm); - updateScrollbars(cm, barMeasure); - setDocumentHeight(cm, barMeasure); - update.force = false; - } - - update.signal(cm, "update", cm); - if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) { - update.signal(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo); - cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo; - } - } - - function updateDisplaySimple(cm, viewport) { - var update = new DisplayUpdate(cm, viewport); - if (updateDisplayIfNeeded(cm, update)) { - updateHeightsInViewport(cm); - postUpdateDisplay(cm, update); - var barMeasure = measureForScrollbars(cm); - updateSelection(cm); - updateScrollbars(cm, barMeasure); - setDocumentHeight(cm, barMeasure); - update.finish(); - } - } - - // Sync the actual display DOM structure with display.view, removing - // nodes for lines that are no longer in view, and creating the ones - // that are not there yet, and updating the ones that are out of - // date. - function patchDisplay(cm, updateNumbersFrom, dims) { - var display = cm.display, lineNumbers = cm.options.lineNumbers; - var container = display.lineDiv, cur = container.firstChild; - - function rm(node) { - var next = node.nextSibling; - // Works around a throw-scroll bug in OS X Webkit - if (webkit && mac && cm.display.currentWheelTarget == node) - { node.style.display = "none"; } - else - { node.parentNode.removeChild(node); } - return next - } - - var view = display.view, lineN = display.viewFrom; - // Loop over the elements in the view, syncing cur (the DOM nodes - // in display.lineDiv) with the view as we go. - for (var i = 0; i < view.length; i++) { - var lineView = view[i]; - if (lineView.hidden) ; else if (!lineView.node || lineView.node.parentNode != container) { // Not drawn yet - var node = buildLineElement(cm, lineView, lineN, dims); - container.insertBefore(node, cur); - } else { // Already drawn - while (cur != lineView.node) { cur = rm(cur); } - var updateNumber = lineNumbers && updateNumbersFrom != null && - updateNumbersFrom <= lineN && lineView.lineNumber; - if (lineView.changes) { - if (indexOf(lineView.changes, "gutter") > -1) { updateNumber = false; } - updateLineForChanges(cm, lineView, lineN, dims); - } - if (updateNumber) { - removeChildren(lineView.lineNumber); - lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN))); - } - cur = lineView.node.nextSibling; - } - lineN += lineView.size; - } - while (cur) { cur = rm(cur); } - } - - function updateGutterSpace(display) { - var width = display.gutters.offsetWidth; - display.sizer.style.marginLeft = width + "px"; - } - - function setDocumentHeight(cm, measure) { - cm.display.sizer.style.minHeight = measure.docHeight + "px"; - cm.display.heightForcer.style.top = measure.docHeight + "px"; - cm.display.gutters.style.height = (measure.docHeight + cm.display.barHeight + scrollGap(cm)) + "px"; - } - - // Re-align line numbers and gutter marks to compensate for - // horizontal scrolling. - function alignHorizontally(cm) { - var display = cm.display, view = display.view; - if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) { return } - var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft; - var gutterW = display.gutters.offsetWidth, left = comp + "px"; - for (var i = 0; i < view.length; i++) { if (!view[i].hidden) { - if (cm.options.fixedGutter) { - if (view[i].gutter) - { view[i].gutter.style.left = left; } - if (view[i].gutterBackground) - { view[i].gutterBackground.style.left = left; } - } - var align = view[i].alignable; - if (align) { for (var j = 0; j < align.length; j++) - { align[j].style.left = left; } } - } } - if (cm.options.fixedGutter) - { display.gutters.style.left = (comp + gutterW) + "px"; } - } - - // Used to ensure that the line number gutter is still the right - // size for the current document size. Returns true when an update - // is needed. - function maybeUpdateLineNumberWidth(cm) { - if (!cm.options.lineNumbers) { return false } - var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display; - if (last.length != display.lineNumChars) { - var test = display.measure.appendChild(elt("div", [elt("div", last)], - "CodeMirror-linenumber CodeMirror-gutter-elt")); - var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW; - display.lineGutter.style.width = ""; - display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding) + 1; - display.lineNumWidth = display.lineNumInnerWidth + padding; - display.lineNumChars = display.lineNumInnerWidth ? last.length : -1; - display.lineGutter.style.width = display.lineNumWidth + "px"; - updateGutterSpace(cm.display); - return true - } - return false - } - - function getGutters(gutters, lineNumbers) { - var result = [], sawLineNumbers = false; - for (var i = 0; i < gutters.length; i++) { - var name = gutters[i], style = null; - if (typeof name != "string") { style = name.style; name = name.className; } - if (name == "CodeMirror-linenumbers") { - if (!lineNumbers) { continue } - else { sawLineNumbers = true; } - } - result.push({className: name, style: style}); - } - if (lineNumbers && !sawLineNumbers) { result.push({className: "CodeMirror-linenumbers", style: null}); } - return result - } - - // Rebuild the gutter elements, ensure the margin to the left of the - // code matches their width. - function renderGutters(display) { - var gutters = display.gutters, specs = display.gutterSpecs; - removeChildren(gutters); - display.lineGutter = null; - for (var i = 0; i < specs.length; ++i) { - var ref = specs[i]; - var className = ref.className; - var style = ref.style; - var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + className)); - if (style) { gElt.style.cssText = style; } - if (className == "CodeMirror-linenumbers") { - display.lineGutter = gElt; - gElt.style.width = (display.lineNumWidth || 1) + "px"; - } - } - gutters.style.display = specs.length ? "" : "none"; - updateGutterSpace(display); - } - - function updateGutters(cm) { - renderGutters(cm.display); - regChange(cm); - alignHorizontally(cm); - } - - // The display handles the DOM integration, both for input reading - // and content drawing. It holds references to DOM nodes and - // display-related state. - - function Display(place, doc, input, options) { - var d = this; - this.input = input; - - // Covers bottom-right square when both scrollbars are present. - d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler"); - d.scrollbarFiller.setAttribute("cm-not-content", "true"); - // Covers bottom of gutter when coverGutterNextToScrollbar is on - // and h scrollbar is present. - d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler"); - d.gutterFiller.setAttribute("cm-not-content", "true"); - // Will contain the actual code, positioned to cover the viewport. - d.lineDiv = eltP("div", null, "CodeMirror-code"); - // Elements are added to these to represent selection and cursors. - d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1"); - d.cursorDiv = elt("div", null, "CodeMirror-cursors"); - // A visibility: hidden element used to find the size of things. - d.measure = elt("div", null, "CodeMirror-measure"); - // When lines outside of the viewport are measured, they are drawn in this. - d.lineMeasure = elt("div", null, "CodeMirror-measure"); - // Wraps everything that needs to exist inside the vertically-padded coordinate system - d.lineSpace = eltP("div", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv], - null, "position: relative; outline: none"); - var lines = eltP("div", [d.lineSpace], "CodeMirror-lines"); - // Moved around its parent to cover visible view. - d.mover = elt("div", [lines], null, "position: relative"); - // Set to the height of the document, allowing scrolling. - d.sizer = elt("div", [d.mover], "CodeMirror-sizer"); - d.sizerWidth = null; - // Behavior of elts with overflow: auto and padding is - // inconsistent across browsers. This is used to ensure the - // scrollable area is big enough. - d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerGap + "px; width: 1px;"); - // Will contain the gutters, if any. - d.gutters = elt("div", null, "CodeMirror-gutters"); - d.lineGutter = null; - // Actual scrollable element. - d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll"); - d.scroller.setAttribute("tabIndex", "-1"); - // The element in which the editor lives. - d.wrapper = elt("div", [d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror"); - - // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported) - if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; } - if (!webkit && !(gecko && mobile)) { d.scroller.draggable = true; } - - if (place) { - if (place.appendChild) { place.appendChild(d.wrapper); } - else { place(d.wrapper); } - } - - // Current rendered range (may be bigger than the view window). - d.viewFrom = d.viewTo = doc.first; - d.reportedViewFrom = d.reportedViewTo = doc.first; - // Information about the rendered lines. - d.view = []; - d.renderedView = null; - // Holds info about a single rendered line when it was rendered - // for measurement, while not in view. - d.externalMeasured = null; - // Empty space (in pixels) above the view - d.viewOffset = 0; - d.lastWrapHeight = d.lastWrapWidth = 0; - d.updateLineNumbers = null; - - d.nativeBarWidth = d.barHeight = d.barWidth = 0; - d.scrollbarsClipped = false; - - // Used to only resize the line number gutter when necessary (when - // the amount of lines crosses a boundary that makes its width change) - d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null; - // Set to true when a non-horizontal-scrolling line widget is - // added. As an optimization, line widget aligning is skipped when - // this is false. - d.alignWidgets = false; - - d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; - - // Tracks the maximum line length so that the horizontal scrollbar - // can be kept static when scrolling. - d.maxLine = null; - d.maxLineLength = 0; - d.maxLineChanged = false; - - // Used for measuring wheel scrolling granularity - d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null; - - // True when shift is held down. - d.shift = false; - - // Used to track whether anything happened since the context menu - // was opened. - d.selForContextMenu = null; - - d.activeTouch = null; - - d.gutterSpecs = getGutters(options.gutters, options.lineNumbers); - renderGutters(d); - - input.init(d); - } - - // Since the delta values reported on mouse wheel events are - // unstandardized between browsers and even browser versions, and - // generally horribly unpredictable, this code starts by measuring - // the scroll effect that the first few mouse wheel events have, - // and, from that, detects the way it can convert deltas to pixel - // offsets afterwards. - // - // The reason we want to know the amount a wheel event will scroll - // is that it gives us a chance to update the display before the - // actual scrolling happens, reducing flickering. - - var wheelSamples = 0, wheelPixelsPerUnit = null; - // Fill in a browser-detected starting value on browsers where we - // know one. These don't have to be accurate -- the result of them - // being wrong would just be a slight flicker on the first wheel - // scroll (if it is large enough). - if (ie) { wheelPixelsPerUnit = -.53; } - else if (gecko) { wheelPixelsPerUnit = 15; } - else if (chrome) { wheelPixelsPerUnit = -.7; } - else if (safari) { wheelPixelsPerUnit = -1/3; } - - function wheelEventDelta(e) { - var dx = e.wheelDeltaX, dy = e.wheelDeltaY; - if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) { dx = e.detail; } - if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) { dy = e.detail; } - else if (dy == null) { dy = e.wheelDelta; } - return {x: dx, y: dy} - } - function wheelEventPixels(e) { - var delta = wheelEventDelta(e); - delta.x *= wheelPixelsPerUnit; - delta.y *= wheelPixelsPerUnit; - return delta - } - - function onScrollWheel(cm, e) { - var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y; - - var display = cm.display, scroll = display.scroller; - // Quit if there's nothing to scroll here - var canScrollX = scroll.scrollWidth > scroll.clientWidth; - var canScrollY = scroll.scrollHeight > scroll.clientHeight; - if (!(dx && canScrollX || dy && canScrollY)) { return } - - // Webkit browsers on OS X abort momentum scrolls when the target - // of the scroll event is removed from the scrollable element. - // This hack (see related code in patchDisplay) makes sure the - // element is kept around. - if (dy && mac && webkit) { - outer: for (var cur = e.target, view = display.view; cur != scroll; cur = cur.parentNode) { - for (var i = 0; i < view.length; i++) { - if (view[i].node == cur) { - cm.display.currentWheelTarget = cur; - break outer - } - } - } - } - - // On some browsers, horizontal scrolling will cause redraws to - // happen before the gutter has been realigned, causing it to - // wriggle around in a most unseemly way. When we have an - // estimated pixels/delta value, we just handle horizontal - // scrolling entirely here. It'll be slightly off from native, but - // better than glitching out. - if (dx && !gecko && !presto && wheelPixelsPerUnit != null) { - if (dy && canScrollY) - { updateScrollTop(cm, Math.max(0, scroll.scrollTop + dy * wheelPixelsPerUnit)); } - setScrollLeft(cm, Math.max(0, scroll.scrollLeft + dx * wheelPixelsPerUnit)); - // Only prevent default scrolling if vertical scrolling is - // actually possible. Otherwise, it causes vertical scroll - // jitter on OSX trackpads when deltaX is small and deltaY - // is large (issue #3579) - if (!dy || (dy && canScrollY)) - { e_preventDefault(e); } - display.wheelStartX = null; // Abort measurement, if in progress - return - } - - // 'Project' the visible viewport to cover the area that is being - // scrolled into view (if we know enough to estimate it). - if (dy && wheelPixelsPerUnit != null) { - var pixels = dy * wheelPixelsPerUnit; - var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight; - if (pixels < 0) { top = Math.max(0, top + pixels - 50); } - else { bot = Math.min(cm.doc.height, bot + pixels + 50); } - updateDisplaySimple(cm, {top: top, bottom: bot}); - } - - if (wheelSamples < 20) { - if (display.wheelStartX == null) { - display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop; - display.wheelDX = dx; display.wheelDY = dy; - setTimeout(function () { - if (display.wheelStartX == null) { return } - var movedX = scroll.scrollLeft - display.wheelStartX; - var movedY = scroll.scrollTop - display.wheelStartY; - var sample = (movedY && display.wheelDY && movedY / display.wheelDY) || - (movedX && display.wheelDX && movedX / display.wheelDX); - display.wheelStartX = display.wheelStartY = null; - if (!sample) { return } - wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1); - ++wheelSamples; - }, 200); - } else { - display.wheelDX += dx; display.wheelDY += dy; - } - } - } - - // Selection objects are immutable. A new one is created every time - // the selection changes. A selection is one or more non-overlapping - // (and non-touching) ranges, sorted, and an integer that indicates - // which one is the primary selection (the one that's scrolled into - // view, that getCursor returns, etc). - var Selection = function(ranges, primIndex) { - this.ranges = ranges; - this.primIndex = primIndex; - }; - - Selection.prototype.primary = function () { return this.ranges[this.primIndex] }; - - Selection.prototype.equals = function (other) { - if (other == this) { return true } - if (other.primIndex != this.primIndex || other.ranges.length != this.ranges.length) { return false } - for (var i = 0; i < this.ranges.length; i++) { - var here = this.ranges[i], there = other.ranges[i]; - if (!equalCursorPos(here.anchor, there.anchor) || !equalCursorPos(here.head, there.head)) { return false } - } - return true - }; - - Selection.prototype.deepCopy = function () { - var out = []; - for (var i = 0; i < this.ranges.length; i++) - { out[i] = new Range(copyPos(this.ranges[i].anchor), copyPos(this.ranges[i].head)); } - return new Selection(out, this.primIndex) - }; - - Selection.prototype.somethingSelected = function () { - for (var i = 0; i < this.ranges.length; i++) - { if (!this.ranges[i].empty()) { return true } } - return false - }; - - Selection.prototype.contains = function (pos, end) { - if (!end) { end = pos; } - for (var i = 0; i < this.ranges.length; i++) { - var range = this.ranges[i]; - if (cmp(end, range.from()) >= 0 && cmp(pos, range.to()) <= 0) - { return i } - } - return -1 - }; - - var Range = function(anchor, head) { - this.anchor = anchor; this.head = head; - }; - - Range.prototype.from = function () { return minPos(this.anchor, this.head) }; - Range.prototype.to = function () { return maxPos(this.anchor, this.head) }; - Range.prototype.empty = function () { return this.head.line == this.anchor.line && this.head.ch == this.anchor.ch }; - - // Take an unsorted, potentially overlapping set of ranges, and - // build a selection out of it. 'Consumes' ranges array (modifying - // it). - function normalizeSelection(cm, ranges, primIndex) { - var mayTouch = cm && cm.options.selectionsMayTouch; - var prim = ranges[primIndex]; - ranges.sort(function (a, b) { return cmp(a.from(), b.from()); }); - primIndex = indexOf(ranges, prim); - for (var i = 1; i < ranges.length; i++) { - var cur = ranges[i], prev = ranges[i - 1]; - var diff = cmp(prev.to(), cur.from()); - if (mayTouch && !cur.empty() ? diff > 0 : diff >= 0) { - var from = minPos(prev.from(), cur.from()), to = maxPos(prev.to(), cur.to()); - var inv = prev.empty() ? cur.from() == cur.head : prev.from() == prev.head; - if (i <= primIndex) { --primIndex; } - ranges.splice(--i, 2, new Range(inv ? to : from, inv ? from : to)); - } - } - return new Selection(ranges, primIndex) - } - - function simpleSelection(anchor, head) { - return new Selection([new Range(anchor, head || anchor)], 0) - } - - // Compute the position of the end of a change (its 'to' property - // refers to the pre-change end). - function changeEnd(change) { - if (!change.text) { return change.to } - return Pos(change.from.line + change.text.length - 1, - lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0)) - } - - // Adjust a position to refer to the post-change position of the - // same text, or the end of the change if the change covers it. - function adjustForChange(pos, change) { - if (cmp(pos, change.from) < 0) { return pos } - if (cmp(pos, change.to) <= 0) { return changeEnd(change) } - - var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch; - if (pos.line == change.to.line) { ch += changeEnd(change).ch - change.to.ch; } - return Pos(line, ch) - } - - function computeSelAfterChange(doc, change) { - var out = []; - for (var i = 0; i < doc.sel.ranges.length; i++) { - var range = doc.sel.ranges[i]; - out.push(new Range(adjustForChange(range.anchor, change), - adjustForChange(range.head, change))); - } - return normalizeSelection(doc.cm, out, doc.sel.primIndex) - } - - function offsetPos(pos, old, nw) { - if (pos.line == old.line) - { return Pos(nw.line, pos.ch - old.ch + nw.ch) } - else - { return Pos(nw.line + (pos.line - old.line), pos.ch) } - } - - // Used by replaceSelections to allow moving the selection to the - // start or around the replaced test. Hint may be "start" or "around". - function computeReplacedSel(doc, changes, hint) { - var out = []; - var oldPrev = Pos(doc.first, 0), newPrev = oldPrev; - for (var i = 0; i < changes.length; i++) { - var change = changes[i]; - var from = offsetPos(change.from, oldPrev, newPrev); - var to = offsetPos(changeEnd(change), oldPrev, newPrev); - oldPrev = change.to; - newPrev = to; - if (hint == "around") { - var range = doc.sel.ranges[i], inv = cmp(range.head, range.anchor) < 0; - out[i] = new Range(inv ? to : from, inv ? from : to); - } else { - out[i] = new Range(from, from); - } - } - return new Selection(out, doc.sel.primIndex) - } - - // Used to get the editor into a consistent state again when options change. - - function loadMode(cm) { - cm.doc.mode = getMode(cm.options, cm.doc.modeOption); - resetModeState(cm); - } - - function resetModeState(cm) { - cm.doc.iter(function (line) { - if (line.stateAfter) { line.stateAfter = null; } - if (line.styles) { line.styles = null; } - }); - cm.doc.modeFrontier = cm.doc.highlightFrontier = cm.doc.first; - startWorker(cm, 100); - cm.state.modeGen++; - if (cm.curOp) { regChange(cm); } - } - - // DOCUMENT DATA STRUCTURE - - // By default, updates that start and end at the beginning of a line - // are treated specially, in order to make the association of line - // widgets and marker elements with the text behave more intuitive. - function isWholeLineUpdate(doc, change) { - return change.from.ch == 0 && change.to.ch == 0 && lst(change.text) == "" && - (!doc.cm || doc.cm.options.wholeLineUpdateBefore) - } - - // Perform a change on the document data structure. - function updateDoc(doc, change, markedSpans, estimateHeight) { - function spansFor(n) {return markedSpans ? markedSpans[n] : null} - function update(line, text, spans) { - updateLine(line, text, spans, estimateHeight); - signalLater(line, "change", line, change); - } - function linesFor(start, end) { - var result = []; - for (var i = start; i < end; ++i) - { result.push(new Line(text[i], spansFor(i), estimateHeight)); } - return result - } - - var from = change.from, to = change.to, text = change.text; - var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line); - var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line; - - // Adjust the line structure - if (change.full) { - doc.insert(0, linesFor(0, text.length)); - doc.remove(text.length, doc.size - text.length); - } else if (isWholeLineUpdate(doc, change)) { - // This is a whole-line replace. Treated specially to make - // sure line objects move the way they are supposed to. - var added = linesFor(0, text.length - 1); - update(lastLine, lastLine.text, lastSpans); - if (nlines) { doc.remove(from.line, nlines); } - if (added.length) { doc.insert(from.line, added); } - } else if (firstLine == lastLine) { - if (text.length == 1) { - update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans); - } else { - var added$1 = linesFor(1, text.length - 1); - added$1.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight)); - update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); - doc.insert(from.line + 1, added$1); - } - } else if (text.length == 1) { - update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0)); - doc.remove(from.line + 1, nlines); - } else { - update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); - update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans); - var added$2 = linesFor(1, text.length - 1); - if (nlines > 1) { doc.remove(from.line + 1, nlines - 1); } - doc.insert(from.line + 1, added$2); - } - - signalLater(doc, "change", doc, change); - } - - // Call f for all linked documents. - function linkedDocs(doc, f, sharedHistOnly) { - function propagate(doc, skip, sharedHist) { - if (doc.linked) { for (var i = 0; i < doc.linked.length; ++i) { - var rel = doc.linked[i]; - if (rel.doc == skip) { continue } - var shared = sharedHist && rel.sharedHist; - if (sharedHistOnly && !shared) { continue } - f(rel.doc, shared); - propagate(rel.doc, doc, shared); - } } - } - propagate(doc, null, true); - } - - // Attach a document to an editor. - function attachDoc(cm, doc) { - if (doc.cm) { throw new Error("This document is already in use.") } - cm.doc = doc; - doc.cm = cm; - estimateLineHeights(cm); - loadMode(cm); - setDirectionClass(cm); - if (!cm.options.lineWrapping) { findMaxLine(cm); } - cm.options.mode = doc.modeOption; - regChange(cm); - } - - function setDirectionClass(cm) { - (cm.doc.direction == "rtl" ? addClass : rmClass)(cm.display.lineDiv, "CodeMirror-rtl"); - } - - function directionChanged(cm) { - runInOp(cm, function () { - setDirectionClass(cm); - regChange(cm); - }); - } - - function History(startGen) { - // Arrays of change events and selections. Doing something adds an - // event to done and clears undo. Undoing moves events from done - // to undone, redoing moves them in the other direction. - this.done = []; this.undone = []; - this.undoDepth = Infinity; - // Used to track when changes can be merged into a single undo - // event - this.lastModTime = this.lastSelTime = 0; - this.lastOp = this.lastSelOp = null; - this.lastOrigin = this.lastSelOrigin = null; - // Used by the isClean() method - this.generation = this.maxGeneration = startGen || 1; - } - - // Create a history change event from an updateDoc-style change - // object. - function historyChangeFromChange(doc, change) { - var histChange = {from: copyPos(change.from), to: changeEnd(change), text: getBetween(doc, change.from, change.to)}; - attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); - linkedDocs(doc, function (doc) { return attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); }, true); - return histChange - } - - // Pop all selection events off the end of a history array. Stop at - // a change event. - function clearSelectionEvents(array) { - while (array.length) { - var last = lst(array); - if (last.ranges) { array.pop(); } - else { break } - } - } - - // Find the top change event in the history. Pop off selection - // events that are in the way. - function lastChangeEvent(hist, force) { - if (force) { - clearSelectionEvents(hist.done); - return lst(hist.done) - } else if (hist.done.length && !lst(hist.done).ranges) { - return lst(hist.done) - } else if (hist.done.length > 1 && !hist.done[hist.done.length - 2].ranges) { - hist.done.pop(); - return lst(hist.done) - } - } - - // Register a change in the history. Merges changes that are within - // a single operation, or are close together with an origin that - // allows merging (starting with "+") into a single event. - function addChangeToHistory(doc, change, selAfter, opId) { - var hist = doc.history; - hist.undone.length = 0; - var time = +new Date, cur; - var last; - - if ((hist.lastOp == opId || - hist.lastOrigin == change.origin && change.origin && - ((change.origin.charAt(0) == "+" && hist.lastModTime > time - (doc.cm ? doc.cm.options.historyEventDelay : 500)) || - change.origin.charAt(0) == "*")) && - (cur = lastChangeEvent(hist, hist.lastOp == opId))) { - // Merge this change into the last event - last = lst(cur.changes); - if (cmp(change.from, change.to) == 0 && cmp(change.from, last.to) == 0) { - // Optimized case for simple insertion -- don't want to add - // new changesets for every character typed - last.to = changeEnd(change); - } else { - // Add new sub-event - cur.changes.push(historyChangeFromChange(doc, change)); - } - } else { - // Can not be merged, start a new event. - var before = lst(hist.done); - if (!before || !before.ranges) - { pushSelectionToHistory(doc.sel, hist.done); } - cur = {changes: [historyChangeFromChange(doc, change)], - generation: hist.generation}; - hist.done.push(cur); - while (hist.done.length > hist.undoDepth) { - hist.done.shift(); - if (!hist.done[0].ranges) { hist.done.shift(); } - } - } - hist.done.push(selAfter); - hist.generation = ++hist.maxGeneration; - hist.lastModTime = hist.lastSelTime = time; - hist.lastOp = hist.lastSelOp = opId; - hist.lastOrigin = hist.lastSelOrigin = change.origin; - - if (!last) { signal(doc, "historyAdded"); } - } - - function selectionEventCanBeMerged(doc, origin, prev, sel) { - var ch = origin.charAt(0); - return ch == "*" || - ch == "+" && - prev.ranges.length == sel.ranges.length && - prev.somethingSelected() == sel.somethingSelected() && - new Date - doc.history.lastSelTime <= (doc.cm ? doc.cm.options.historyEventDelay : 500) - } - - // Called whenever the selection changes, sets the new selection as - // the pending selection in the history, and pushes the old pending - // selection into the 'done' array when it was significantly - // different (in number of selected ranges, emptiness, or time). - function addSelectionToHistory(doc, sel, opId, options) { - var hist = doc.history, origin = options && options.origin; - - // A new event is started when the previous origin does not match - // the current, or the origins don't allow matching. Origins - // starting with * are always merged, those starting with + are - // merged when similar and close together in time. - if (opId == hist.lastSelOp || - (origin && hist.lastSelOrigin == origin && - (hist.lastModTime == hist.lastSelTime && hist.lastOrigin == origin || - selectionEventCanBeMerged(doc, origin, lst(hist.done), sel)))) - { hist.done[hist.done.length - 1] = sel; } - else - { pushSelectionToHistory(sel, hist.done); } - - hist.lastSelTime = +new Date; - hist.lastSelOrigin = origin; - hist.lastSelOp = opId; - if (options && options.clearRedo !== false) - { clearSelectionEvents(hist.undone); } - } - - function pushSelectionToHistory(sel, dest) { - var top = lst(dest); - if (!(top && top.ranges && top.equals(sel))) - { dest.push(sel); } - } - - // Used to store marked span information in the history. - function attachLocalSpans(doc, change, from, to) { - var existing = change["spans_" + doc.id], n = 0; - doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function (line) { - if (line.markedSpans) - { (existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans; } - ++n; - }); - } - - // When un/re-doing restores text containing marked spans, those - // that have been explicitly cleared should not be restored. - function removeClearedSpans(spans) { - if (!spans) { return null } - var out; - for (var i = 0; i < spans.length; ++i) { - if (spans[i].marker.explicitlyCleared) { if (!out) { out = spans.slice(0, i); } } - else if (out) { out.push(spans[i]); } - } - return !out ? spans : out.length ? out : null - } - - // Retrieve and filter the old marked spans stored in a change event. - function getOldSpans(doc, change) { - var found = change["spans_" + doc.id]; - if (!found) { return null } - var nw = []; - for (var i = 0; i < change.text.length; ++i) - { nw.push(removeClearedSpans(found[i])); } - return nw - } - - // Used for un/re-doing changes from the history. Combines the - // result of computing the existing spans with the set of spans that - // existed in the history (so that deleting around a span and then - // undoing brings back the span). - function mergeOldSpans(doc, change) { - var old = getOldSpans(doc, change); - var stretched = stretchSpansOverChange(doc, change); - if (!old) { return stretched } - if (!stretched) { return old } - - for (var i = 0; i < old.length; ++i) { - var oldCur = old[i], stretchCur = stretched[i]; - if (oldCur && stretchCur) { - spans: for (var j = 0; j < stretchCur.length; ++j) { - var span = stretchCur[j]; - for (var k = 0; k < oldCur.length; ++k) - { if (oldCur[k].marker == span.marker) { continue spans } } - oldCur.push(span); - } - } else if (stretchCur) { - old[i] = stretchCur; - } - } - return old - } - - // Used both to provide a JSON-safe object in .getHistory, and, when - // detaching a document, to split the history in two - function copyHistoryArray(events, newGroup, instantiateSel) { - var copy = []; - for (var i = 0; i < events.length; ++i) { - var event = events[i]; - if (event.ranges) { - copy.push(instantiateSel ? Selection.prototype.deepCopy.call(event) : event); - continue - } - var changes = event.changes, newChanges = []; - copy.push({changes: newChanges}); - for (var j = 0; j < changes.length; ++j) { - var change = changes[j], m = (void 0); - newChanges.push({from: change.from, to: change.to, text: change.text}); - if (newGroup) { for (var prop in change) { if (m = prop.match(/^spans_(\d+)$/)) { - if (indexOf(newGroup, Number(m[1])) > -1) { - lst(newChanges)[prop] = change[prop]; - delete change[prop]; - } - } } } - } - } - return copy - } - - // The 'scroll' parameter given to many of these indicated whether - // the new cursor position should be scrolled into view after - // modifying the selection. - - // If shift is held or the extend flag is set, extends a range to - // include a given position (and optionally a second position). - // Otherwise, simply returns the range between the given positions. - // Used for cursor motion and such. - function extendRange(range, head, other, extend) { - if (extend) { - var anchor = range.anchor; - if (other) { - var posBefore = cmp(head, anchor) < 0; - if (posBefore != (cmp(other, anchor) < 0)) { - anchor = head; - head = other; - } else if (posBefore != (cmp(head, other) < 0)) { - head = other; - } - } - return new Range(anchor, head) - } else { - return new Range(other || head, head) - } - } - - // Extend the primary selection range, discard the rest. - function extendSelection(doc, head, other, options, extend) { - if (extend == null) { extend = doc.cm && (doc.cm.display.shift || doc.extend); } - setSelection(doc, new Selection([extendRange(doc.sel.primary(), head, other, extend)], 0), options); - } - - // Extend all selections (pos is an array of selections with length - // equal the number of selections) - function extendSelections(doc, heads, options) { - var out = []; - var extend = doc.cm && (doc.cm.display.shift || doc.extend); - for (var i = 0; i < doc.sel.ranges.length; i++) - { out[i] = extendRange(doc.sel.ranges[i], heads[i], null, extend); } - var newSel = normalizeSelection(doc.cm, out, doc.sel.primIndex); - setSelection(doc, newSel, options); - } - - // Updates a single range in the selection. - function replaceOneSelection(doc, i, range, options) { - var ranges = doc.sel.ranges.slice(0); - ranges[i] = range; - setSelection(doc, normalizeSelection(doc.cm, ranges, doc.sel.primIndex), options); - } - - // Reset the selection to a single range. - function setSimpleSelection(doc, anchor, head, options) { - setSelection(doc, simpleSelection(anchor, head), options); - } - - // Give beforeSelectionChange handlers a change to influence a - // selection update. - function filterSelectionChange(doc, sel, options) { - var obj = { - ranges: sel.ranges, - update: function(ranges) { - this.ranges = []; - for (var i = 0; i < ranges.length; i++) - { this.ranges[i] = new Range(clipPos(doc, ranges[i].anchor), - clipPos(doc, ranges[i].head)); } - }, - origin: options && options.origin - }; - signal(doc, "beforeSelectionChange", doc, obj); - if (doc.cm) { signal(doc.cm, "beforeSelectionChange", doc.cm, obj); } - if (obj.ranges != sel.ranges) { return normalizeSelection(doc.cm, obj.ranges, obj.ranges.length - 1) } - else { return sel } - } - - function setSelectionReplaceHistory(doc, sel, options) { - var done = doc.history.done, last = lst(done); - if (last && last.ranges) { - done[done.length - 1] = sel; - setSelectionNoUndo(doc, sel, options); - } else { - setSelection(doc, sel, options); - } - } - - // Set a new selection. - function setSelection(doc, sel, options) { - setSelectionNoUndo(doc, sel, options); - addSelectionToHistory(doc, doc.sel, doc.cm ? doc.cm.curOp.id : NaN, options); - } - - function setSelectionNoUndo(doc, sel, options) { - if (hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) - { sel = filterSelectionChange(doc, sel, options); } - - var bias = options && options.bias || - (cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1); - setSelectionInner(doc, skipAtomicInSelection(doc, sel, bias, true)); - - if (!(options && options.scroll === false) && doc.cm) - { ensureCursorVisible(doc.cm); } - } - - function setSelectionInner(doc, sel) { - if (sel.equals(doc.sel)) { return } - - doc.sel = sel; - - if (doc.cm) { - doc.cm.curOp.updateInput = 1; - doc.cm.curOp.selectionChanged = true; - signalCursorActivity(doc.cm); - } - signalLater(doc, "cursorActivity", doc); - } - - // Verify that the selection does not partially select any atomic - // marked ranges. - function reCheckSelection(doc) { - setSelectionInner(doc, skipAtomicInSelection(doc, doc.sel, null, false)); - } - - // Return a selection that does not partially select any atomic - // ranges. - function skipAtomicInSelection(doc, sel, bias, mayClear) { - var out; - for (var i = 0; i < sel.ranges.length; i++) { - var range = sel.ranges[i]; - var old = sel.ranges.length == doc.sel.ranges.length && doc.sel.ranges[i]; - var newAnchor = skipAtomic(doc, range.anchor, old && old.anchor, bias, mayClear); - var newHead = skipAtomic(doc, range.head, old && old.head, bias, mayClear); - if (out || newAnchor != range.anchor || newHead != range.head) { - if (!out) { out = sel.ranges.slice(0, i); } - out[i] = new Range(newAnchor, newHead); - } - } - return out ? normalizeSelection(doc.cm, out, sel.primIndex) : sel - } - - function skipAtomicInner(doc, pos, oldPos, dir, mayClear) { - var line = getLine(doc, pos.line); - if (line.markedSpans) { for (var i = 0; i < line.markedSpans.length; ++i) { - var sp = line.markedSpans[i], m = sp.marker; - - // Determine if we should prevent the cursor being placed to the left/right of an atomic marker - // Historically this was determined using the inclusiveLeft/Right option, but the new way to control it - // is with selectLeft/Right - var preventCursorLeft = ("selectLeft" in m) ? !m.selectLeft : m.inclusiveLeft; - var preventCursorRight = ("selectRight" in m) ? !m.selectRight : m.inclusiveRight; - - if ((sp.from == null || (preventCursorLeft ? sp.from <= pos.ch : sp.from < pos.ch)) && - (sp.to == null || (preventCursorRight ? sp.to >= pos.ch : sp.to > pos.ch))) { - if (mayClear) { - signal(m, "beforeCursorEnter"); - if (m.explicitlyCleared) { - if (!line.markedSpans) { break } - else {--i; continue} - } - } - if (!m.atomic) { continue } - - if (oldPos) { - var near = m.find(dir < 0 ? 1 : -1), diff = (void 0); - if (dir < 0 ? preventCursorRight : preventCursorLeft) - { near = movePos(doc, near, -dir, near && near.line == pos.line ? line : null); } - if (near && near.line == pos.line && (diff = cmp(near, oldPos)) && (dir < 0 ? diff < 0 : diff > 0)) - { return skipAtomicInner(doc, near, pos, dir, mayClear) } - } - - var far = m.find(dir < 0 ? -1 : 1); - if (dir < 0 ? preventCursorLeft : preventCursorRight) - { far = movePos(doc, far, dir, far.line == pos.line ? line : null); } - return far ? skipAtomicInner(doc, far, pos, dir, mayClear) : null - } - } } - return pos - } - - // Ensure a given position is not inside an atomic range. - function skipAtomic(doc, pos, oldPos, bias, mayClear) { - var dir = bias || 1; - var found = skipAtomicInner(doc, pos, oldPos, dir, mayClear) || - (!mayClear && skipAtomicInner(doc, pos, oldPos, dir, true)) || - skipAtomicInner(doc, pos, oldPos, -dir, mayClear) || - (!mayClear && skipAtomicInner(doc, pos, oldPos, -dir, true)); - if (!found) { - doc.cantEdit = true; - return Pos(doc.first, 0) - } - return found - } - - function movePos(doc, pos, dir, line) { - if (dir < 0 && pos.ch == 0) { - if (pos.line > doc.first) { return clipPos(doc, Pos(pos.line - 1)) } - else { return null } - } else if (dir > 0 && pos.ch == (line || getLine(doc, pos.line)).text.length) { - if (pos.line < doc.first + doc.size - 1) { return Pos(pos.line + 1, 0) } - else { return null } - } else { - return new Pos(pos.line, pos.ch + dir) - } - } - - function selectAll(cm) { - cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()), sel_dontScroll); - } - - // UPDATING - - // Allow "beforeChange" event handlers to influence a change - function filterChange(doc, change, update) { - var obj = { - canceled: false, - from: change.from, - to: change.to, - text: change.text, - origin: change.origin, - cancel: function () { return obj.canceled = true; } - }; - if (update) { obj.update = function (from, to, text, origin) { - if (from) { obj.from = clipPos(doc, from); } - if (to) { obj.to = clipPos(doc, to); } - if (text) { obj.text = text; } - if (origin !== undefined) { obj.origin = origin; } - }; } - signal(doc, "beforeChange", doc, obj); - if (doc.cm) { signal(doc.cm, "beforeChange", doc.cm, obj); } - - if (obj.canceled) { - if (doc.cm) { doc.cm.curOp.updateInput = 2; } - return null - } - return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin} - } - - // Apply a change to a document, and add it to the document's - // history, and propagating it to all linked documents. - function makeChange(doc, change, ignoreReadOnly) { - if (doc.cm) { - if (!doc.cm.curOp) { return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly) } - if (doc.cm.state.suppressEdits) { return } - } - - if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) { - change = filterChange(doc, change, true); - if (!change) { return } - } - - // Possibly split or suppress the update based on the presence - // of read-only spans in its range. - var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to); - if (split) { - for (var i = split.length - 1; i >= 0; --i) - { makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text, origin: change.origin}); } - } else { - makeChangeInner(doc, change); - } - } - - function makeChangeInner(doc, change) { - if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) { return } - var selAfter = computeSelAfterChange(doc, change); - addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN); - - makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change)); - var rebased = []; - - linkedDocs(doc, function (doc, sharedHist) { - if (!sharedHist && indexOf(rebased, doc.history) == -1) { - rebaseHist(doc.history, change); - rebased.push(doc.history); - } - makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change)); - }); - } - - // Revert a change stored in a document's history. - function makeChangeFromHistory(doc, type, allowSelectionOnly) { - var suppress = doc.cm && doc.cm.state.suppressEdits; - if (suppress && !allowSelectionOnly) { return } - - var hist = doc.history, event, selAfter = doc.sel; - var source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done; - - // Verify that there is a useable event (so that ctrl-z won't - // needlessly clear selection events) - var i = 0; - for (; i < source.length; i++) { - event = source[i]; - if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges) - { break } - } - if (i == source.length) { return } - hist.lastOrigin = hist.lastSelOrigin = null; - - for (;;) { - event = source.pop(); - if (event.ranges) { - pushSelectionToHistory(event, dest); - if (allowSelectionOnly && !event.equals(doc.sel)) { - setSelection(doc, event, {clearRedo: false}); - return - } - selAfter = event; - } else if (suppress) { - source.push(event); - return - } else { break } - } - - // Build up a reverse change object to add to the opposite history - // stack (redo when undoing, and vice versa). - var antiChanges = []; - pushSelectionToHistory(selAfter, dest); - dest.push({changes: antiChanges, generation: hist.generation}); - hist.generation = event.generation || ++hist.maxGeneration; - - var filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange"); - - var loop = function ( i ) { - var change = event.changes[i]; - change.origin = type; - if (filter && !filterChange(doc, change, false)) { - source.length = 0; - return {} - } - - antiChanges.push(historyChangeFromChange(doc, change)); - - var after = i ? computeSelAfterChange(doc, change) : lst(source); - makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change)); - if (!i && doc.cm) { doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)}); } - var rebased = []; - - // Propagate to the linked documents - linkedDocs(doc, function (doc, sharedHist) { - if (!sharedHist && indexOf(rebased, doc.history) == -1) { - rebaseHist(doc.history, change); - rebased.push(doc.history); - } - makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change)); - }); - }; - - for (var i$1 = event.changes.length - 1; i$1 >= 0; --i$1) { - var returned = loop( i$1 ); - - if ( returned ) return returned.v; - } - } - - // Sub-views need their line numbers shifted when text is added - // above or below them in the parent document. - function shiftDoc(doc, distance) { - if (distance == 0) { return } - doc.first += distance; - doc.sel = new Selection(map(doc.sel.ranges, function (range) { return new Range( - Pos(range.anchor.line + distance, range.anchor.ch), - Pos(range.head.line + distance, range.head.ch) - ); }), doc.sel.primIndex); - if (doc.cm) { - regChange(doc.cm, doc.first, doc.first - distance, distance); - for (var d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++) - { regLineChange(doc.cm, l, "gutter"); } - } - } - - // More lower-level change function, handling only a single document - // (not linked ones). - function makeChangeSingleDoc(doc, change, selAfter, spans) { - if (doc.cm && !doc.cm.curOp) - { return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans) } - - if (change.to.line < doc.first) { - shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line)); - return - } - if (change.from.line > doc.lastLine()) { return } - - // Clip the change to the size of this doc - if (change.from.line < doc.first) { - var shift = change.text.length - 1 - (doc.first - change.from.line); - shiftDoc(doc, shift); - change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch), - text: [lst(change.text)], origin: change.origin}; - } - var last = doc.lastLine(); - if (change.to.line > last) { - change = {from: change.from, to: Pos(last, getLine(doc, last).text.length), - text: [change.text[0]], origin: change.origin}; - } - - change.removed = getBetween(doc, change.from, change.to); - - if (!selAfter) { selAfter = computeSelAfterChange(doc, change); } - if (doc.cm) { makeChangeSingleDocInEditor(doc.cm, change, spans); } - else { updateDoc(doc, change, spans); } - setSelectionNoUndo(doc, selAfter, sel_dontScroll); - - if (doc.cantEdit && skipAtomic(doc, Pos(doc.firstLine(), 0))) - { doc.cantEdit = false; } - } - - // Handle the interaction of a change to a document with the editor - // that this document is part of. - function makeChangeSingleDocInEditor(cm, change, spans) { - var doc = cm.doc, display = cm.display, from = change.from, to = change.to; - - var recomputeMaxLength = false, checkWidthStart = from.line; - if (!cm.options.lineWrapping) { - checkWidthStart = lineNo(visualLine(getLine(doc, from.line))); - doc.iter(checkWidthStart, to.line + 1, function (line) { - if (line == display.maxLine) { - recomputeMaxLength = true; - return true - } - }); - } - - if (doc.sel.contains(change.from, change.to) > -1) - { signalCursorActivity(cm); } - - updateDoc(doc, change, spans, estimateHeight(cm)); - - if (!cm.options.lineWrapping) { - doc.iter(checkWidthStart, from.line + change.text.length, function (line) { - var len = lineLength(line); - if (len > display.maxLineLength) { - display.maxLine = line; - display.maxLineLength = len; - display.maxLineChanged = true; - recomputeMaxLength = false; - } - }); - if (recomputeMaxLength) { cm.curOp.updateMaxLine = true; } - } - - retreatFrontier(doc, from.line); - startWorker(cm, 400); - - var lendiff = change.text.length - (to.line - from.line) - 1; - // Remember that these lines changed, for updating the display - if (change.full) - { regChange(cm); } - else if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change)) - { regLineChange(cm, from.line, "text"); } - else - { regChange(cm, from.line, to.line + 1, lendiff); } - - var changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change"); - if (changeHandler || changesHandler) { - var obj = { - from: from, to: to, - text: change.text, - removed: change.removed, - origin: change.origin - }; - if (changeHandler) { signalLater(cm, "change", cm, obj); } - if (changesHandler) { (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj); } - } - cm.display.selForContextMenu = null; - } - - function replaceRange(doc, code, from, to, origin) { - var assign; - - if (!to) { to = from; } - if (cmp(to, from) < 0) { (assign = [to, from], from = assign[0], to = assign[1]); } - if (typeof code == "string") { code = doc.splitLines(code); } - makeChange(doc, {from: from, to: to, text: code, origin: origin}); - } - - // Rebasing/resetting history to deal with externally-sourced changes - - function rebaseHistSelSingle(pos, from, to, diff) { - if (to < pos.line) { - pos.line += diff; - } else if (from < pos.line) { - pos.line = from; - pos.ch = 0; - } - } - - // Tries to rebase an array of history events given a change in the - // document. If the change touches the same lines as the event, the - // event, and everything 'behind' it, is discarded. If the change is - // before the event, the event's positions are updated. Uses a - // copy-on-write scheme for the positions, to avoid having to - // reallocate them all on every rebase, but also avoid problems with - // shared position objects being unsafely updated. - function rebaseHistArray(array, from, to, diff) { - for (var i = 0; i < array.length; ++i) { - var sub = array[i], ok = true; - if (sub.ranges) { - if (!sub.copied) { sub = array[i] = sub.deepCopy(); sub.copied = true; } - for (var j = 0; j < sub.ranges.length; j++) { - rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff); - rebaseHistSelSingle(sub.ranges[j].head, from, to, diff); - } - continue - } - for (var j$1 = 0; j$1 < sub.changes.length; ++j$1) { - var cur = sub.changes[j$1]; - if (to < cur.from.line) { - cur.from = Pos(cur.from.line + diff, cur.from.ch); - cur.to = Pos(cur.to.line + diff, cur.to.ch); - } else if (from <= cur.to.line) { - ok = false; - break - } - } - if (!ok) { - array.splice(0, i + 1); - i = 0; - } - } - } - - function rebaseHist(hist, change) { - var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1; - rebaseHistArray(hist.done, from, to, diff); - rebaseHistArray(hist.undone, from, to, diff); - } - - // Utility for applying a change to a line by handle or number, - // returning the number and optionally registering the line as - // changed. - function changeLine(doc, handle, changeType, op) { - var no = handle, line = handle; - if (typeof handle == "number") { line = getLine(doc, clipLine(doc, handle)); } - else { no = lineNo(handle); } - if (no == null) { return null } - if (op(line, no) && doc.cm) { regLineChange(doc.cm, no, changeType); } - return line - } - - // The document is represented as a BTree consisting of leaves, with - // chunk of lines in them, and branches, with up to ten leaves or - // other branch nodes below them. The top node is always a branch - // node, and is the document object itself (meaning it has - // additional methods and properties). - // - // All nodes have parent links. The tree is used both to go from - // line numbers to line objects, and to go from objects to numbers. - // It also indexes by height, and is used to convert between height - // and line object, and to find the total height of the document. - // - // See also http://marijnhaverbeke.nl/blog/codemirror-line-tree.html - - function LeafChunk(lines) { - this.lines = lines; - this.parent = null; - var height = 0; - for (var i = 0; i < lines.length; ++i) { - lines[i].parent = this; - height += lines[i].height; - } - this.height = height; - } - - LeafChunk.prototype = { - chunkSize: function() { return this.lines.length }, - - // Remove the n lines at offset 'at'. - removeInner: function(at, n) { - for (var i = at, e = at + n; i < e; ++i) { - var line = this.lines[i]; - this.height -= line.height; - cleanUpLine(line); - signalLater(line, "delete"); - } - this.lines.splice(at, n); - }, - - // Helper used to collapse a small branch into a single leaf. - collapse: function(lines) { - lines.push.apply(lines, this.lines); - }, - - // Insert the given array of lines at offset 'at', count them as - // having the given height. - insertInner: function(at, lines, height) { - this.height += height; - this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at)); - for (var i = 0; i < lines.length; ++i) { lines[i].parent = this; } - }, - - // Used to iterate over a part of the tree. - iterN: function(at, n, op) { - for (var e = at + n; at < e; ++at) - { if (op(this.lines[at])) { return true } } - } - }; - - function BranchChunk(children) { - this.children = children; - var size = 0, height = 0; - for (var i = 0; i < children.length; ++i) { - var ch = children[i]; - size += ch.chunkSize(); height += ch.height; - ch.parent = this; - } - this.size = size; - this.height = height; - this.parent = null; - } - - BranchChunk.prototype = { - chunkSize: function() { return this.size }, - - removeInner: function(at, n) { - this.size -= n; - for (var i = 0; i < this.children.length; ++i) { - var child = this.children[i], sz = child.chunkSize(); - if (at < sz) { - var rm = Math.min(n, sz - at), oldHeight = child.height; - child.removeInner(at, rm); - this.height -= oldHeight - child.height; - if (sz == rm) { this.children.splice(i--, 1); child.parent = null; } - if ((n -= rm) == 0) { break } - at = 0; - } else { at -= sz; } - } - // If the result is smaller than 25 lines, ensure that it is a - // single leaf node. - if (this.size - n < 25 && - (this.children.length > 1 || !(this.children[0] instanceof LeafChunk))) { - var lines = []; - this.collapse(lines); - this.children = [new LeafChunk(lines)]; - this.children[0].parent = this; - } - }, - - collapse: function(lines) { - for (var i = 0; i < this.children.length; ++i) { this.children[i].collapse(lines); } - }, - - insertInner: function(at, lines, height) { - this.size += lines.length; - this.height += height; - for (var i = 0; i < this.children.length; ++i) { - var child = this.children[i], sz = child.chunkSize(); - if (at <= sz) { - child.insertInner(at, lines, height); - if (child.lines && child.lines.length > 50) { - // To avoid memory thrashing when child.lines is huge (e.g. first view of a large file), it's never spliced. - // Instead, small slices are taken. They're taken in order because sequential memory accesses are fastest. - var remaining = child.lines.length % 25 + 25; - for (var pos = remaining; pos < child.lines.length;) { - var leaf = new LeafChunk(child.lines.slice(pos, pos += 25)); - child.height -= leaf.height; - this.children.splice(++i, 0, leaf); - leaf.parent = this; - } - child.lines = child.lines.slice(0, remaining); - this.maybeSpill(); - } - break - } - at -= sz; - } - }, - - // When a node has grown, check whether it should be split. - maybeSpill: function() { - if (this.children.length <= 10) { return } - var me = this; - do { - var spilled = me.children.splice(me.children.length - 5, 5); - var sibling = new BranchChunk(spilled); - if (!me.parent) { // Become the parent node - var copy = new BranchChunk(me.children); - copy.parent = me; - me.children = [copy, sibling]; - me = copy; - } else { - me.size -= sibling.size; - me.height -= sibling.height; - var myIndex = indexOf(me.parent.children, me); - me.parent.children.splice(myIndex + 1, 0, sibling); - } - sibling.parent = me.parent; - } while (me.children.length > 10) - me.parent.maybeSpill(); - }, - - iterN: function(at, n, op) { - for (var i = 0; i < this.children.length; ++i) { - var child = this.children[i], sz = child.chunkSize(); - if (at < sz) { - var used = Math.min(n, sz - at); - if (child.iterN(at, used, op)) { return true } - if ((n -= used) == 0) { break } - at = 0; - } else { at -= sz; } - } - } - }; - - // Line widgets are block elements displayed above or below a line. - - var LineWidget = function(doc, node, options) { - if (options) { for (var opt in options) { if (options.hasOwnProperty(opt)) - { this[opt] = options[opt]; } } } - this.doc = doc; - this.node = node; - }; - - LineWidget.prototype.clear = function () { - var cm = this.doc.cm, ws = this.line.widgets, line = this.line, no = lineNo(line); - if (no == null || !ws) { return } - for (var i = 0; i < ws.length; ++i) { if (ws[i] == this) { ws.splice(i--, 1); } } - if (!ws.length) { line.widgets = null; } - var height = widgetHeight(this); - updateLineHeight(line, Math.max(0, line.height - height)); - if (cm) { - runInOp(cm, function () { - adjustScrollWhenAboveVisible(cm, line, -height); - regLineChange(cm, no, "widget"); - }); - signalLater(cm, "lineWidgetCleared", cm, this, no); - } - }; - - LineWidget.prototype.changed = function () { - var this$1 = this; - - var oldH = this.height, cm = this.doc.cm, line = this.line; - this.height = null; - var diff = widgetHeight(this) - oldH; - if (!diff) { return } - if (!lineIsHidden(this.doc, line)) { updateLineHeight(line, line.height + diff); } - if (cm) { - runInOp(cm, function () { - cm.curOp.forceUpdate = true; - adjustScrollWhenAboveVisible(cm, line, diff); - signalLater(cm, "lineWidgetChanged", cm, this$1, lineNo(line)); - }); - } - }; - eventMixin(LineWidget); - - function adjustScrollWhenAboveVisible(cm, line, diff) { - if (heightAtLine(line) < ((cm.curOp && cm.curOp.scrollTop) || cm.doc.scrollTop)) - { addToScrollTop(cm, diff); } - } - - function addLineWidget(doc, handle, node, options) { - var widget = new LineWidget(doc, node, options); - var cm = doc.cm; - if (cm && widget.noHScroll) { cm.display.alignWidgets = true; } - changeLine(doc, handle, "widget", function (line) { - var widgets = line.widgets || (line.widgets = []); - if (widget.insertAt == null) { widgets.push(widget); } - else { widgets.splice(Math.min(widgets.length - 1, Math.max(0, widget.insertAt)), 0, widget); } - widget.line = line; - if (cm && !lineIsHidden(doc, line)) { - var aboveVisible = heightAtLine(line) < doc.scrollTop; - updateLineHeight(line, line.height + widgetHeight(widget)); - if (aboveVisible) { addToScrollTop(cm, widget.height); } - cm.curOp.forceUpdate = true; - } - return true - }); - if (cm) { signalLater(cm, "lineWidgetAdded", cm, widget, typeof handle == "number" ? handle : lineNo(handle)); } - return widget - } - - // TEXTMARKERS - - // Created with markText and setBookmark methods. A TextMarker is a - // handle that can be used to clear or find a marked position in the - // document. Line objects hold arrays (markedSpans) containing - // {from, to, marker} object pointing to such marker objects, and - // indicating that such a marker is present on that line. Multiple - // lines may point to the same marker when it spans across lines. - // The spans will have null for their from/to properties when the - // marker continues beyond the start/end of the line. Markers have - // links back to the lines they currently touch. - - // Collapsed markers have unique ids, in order to be able to order - // them, which is needed for uniquely determining an outer marker - // when they overlap (they may nest, but not partially overlap). - var nextMarkerId = 0; - - var TextMarker = function(doc, type) { - this.lines = []; - this.type = type; - this.doc = doc; - this.id = ++nextMarkerId; - }; - - // Clear the marker. - TextMarker.prototype.clear = function () { - if (this.explicitlyCleared) { return } - var cm = this.doc.cm, withOp = cm && !cm.curOp; - if (withOp) { startOperation(cm); } - if (hasHandler(this, "clear")) { - var found = this.find(); - if (found) { signalLater(this, "clear", found.from, found.to); } - } - var min = null, max = null; - for (var i = 0; i < this.lines.length; ++i) { - var line = this.lines[i]; - var span = getMarkedSpanFor(line.markedSpans, this); - if (cm && !this.collapsed) { regLineChange(cm, lineNo(line), "text"); } - else if (cm) { - if (span.to != null) { max = lineNo(line); } - if (span.from != null) { min = lineNo(line); } - } - line.markedSpans = removeMarkedSpan(line.markedSpans, span); - if (span.from == null && this.collapsed && !lineIsHidden(this.doc, line) && cm) - { updateLineHeight(line, textHeight(cm.display)); } - } - if (cm && this.collapsed && !cm.options.lineWrapping) { for (var i$1 = 0; i$1 < this.lines.length; ++i$1) { - var visual = visualLine(this.lines[i$1]), len = lineLength(visual); - if (len > cm.display.maxLineLength) { - cm.display.maxLine = visual; - cm.display.maxLineLength = len; - cm.display.maxLineChanged = true; - } - } } - - if (min != null && cm && this.collapsed) { regChange(cm, min, max + 1); } - this.lines.length = 0; - this.explicitlyCleared = true; - if (this.atomic && this.doc.cantEdit) { - this.doc.cantEdit = false; - if (cm) { reCheckSelection(cm.doc); } - } - if (cm) { signalLater(cm, "markerCleared", cm, this, min, max); } - if (withOp) { endOperation(cm); } - if (this.parent) { this.parent.clear(); } - }; - - // Find the position of the marker in the document. Returns a {from, - // to} object by default. Side can be passed to get a specific side - // -- 0 (both), -1 (left), or 1 (right). When lineObj is true, the - // Pos objects returned contain a line object, rather than a line - // number (used to prevent looking up the same line twice). - TextMarker.prototype.find = function (side, lineObj) { - if (side == null && this.type == "bookmark") { side = 1; } - var from, to; - for (var i = 0; i < this.lines.length; ++i) { - var line = this.lines[i]; - var span = getMarkedSpanFor(line.markedSpans, this); - if (span.from != null) { - from = Pos(lineObj ? line : lineNo(line), span.from); - if (side == -1) { return from } - } - if (span.to != null) { - to = Pos(lineObj ? line : lineNo(line), span.to); - if (side == 1) { return to } - } - } - return from && {from: from, to: to} - }; - - // Signals that the marker's widget changed, and surrounding layout - // should be recomputed. - TextMarker.prototype.changed = function () { - var this$1 = this; - - var pos = this.find(-1, true), widget = this, cm = this.doc.cm; - if (!pos || !cm) { return } - runInOp(cm, function () { - var line = pos.line, lineN = lineNo(pos.line); - var view = findViewForLine(cm, lineN); - if (view) { - clearLineMeasurementCacheFor(view); - cm.curOp.selectionChanged = cm.curOp.forceUpdate = true; - } - cm.curOp.updateMaxLine = true; - if (!lineIsHidden(widget.doc, line) && widget.height != null) { - var oldHeight = widget.height; - widget.height = null; - var dHeight = widgetHeight(widget) - oldHeight; - if (dHeight) - { updateLineHeight(line, line.height + dHeight); } - } - signalLater(cm, "markerChanged", cm, this$1); - }); - }; - - TextMarker.prototype.attachLine = function (line) { - if (!this.lines.length && this.doc.cm) { - var op = this.doc.cm.curOp; - if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1) - { (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this); } - } - this.lines.push(line); - }; - - TextMarker.prototype.detachLine = function (line) { - this.lines.splice(indexOf(this.lines, line), 1); - if (!this.lines.length && this.doc.cm) { - var op = this.doc.cm.curOp - ;(op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this); - } - }; - eventMixin(TextMarker); - - // Create a marker, wire it up to the right lines, and - function markText(doc, from, to, options, type) { - // Shared markers (across linked documents) are handled separately - // (markTextShared will call out to this again, once per - // document). - if (options && options.shared) { return markTextShared(doc, from, to, options, type) } - // Ensure we are in an operation. - if (doc.cm && !doc.cm.curOp) { return operation(doc.cm, markText)(doc, from, to, options, type) } - - var marker = new TextMarker(doc, type), diff = cmp(from, to); - if (options) { copyObj(options, marker, false); } - // Don't connect empty markers unless clearWhenEmpty is false - if (diff > 0 || diff == 0 && marker.clearWhenEmpty !== false) - { return marker } - if (marker.replacedWith) { - // Showing up as a widget implies collapsed (widget replaces text) - marker.collapsed = true; - marker.widgetNode = eltP("span", [marker.replacedWith], "CodeMirror-widget"); - if (!options.handleMouseEvents) { marker.widgetNode.setAttribute("cm-ignore-events", "true"); } - if (options.insertLeft) { marker.widgetNode.insertLeft = true; } - } - if (marker.collapsed) { - if (conflictingCollapsedRange(doc, from.line, from, to, marker) || - from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker)) - { throw new Error("Inserting collapsed marker partially overlapping an existing one") } - seeCollapsedSpans(); - } - - if (marker.addToHistory) - { addChangeToHistory(doc, {from: from, to: to, origin: "markText"}, doc.sel, NaN); } - - var curLine = from.line, cm = doc.cm, updateMaxLine; - doc.iter(curLine, to.line + 1, function (line) { - if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(line) == cm.display.maxLine) - { updateMaxLine = true; } - if (marker.collapsed && curLine != from.line) { updateLineHeight(line, 0); } - addMarkedSpan(line, new MarkedSpan(marker, - curLine == from.line ? from.ch : null, - curLine == to.line ? to.ch : null)); - ++curLine; - }); - // lineIsHidden depends on the presence of the spans, so needs a second pass - if (marker.collapsed) { doc.iter(from.line, to.line + 1, function (line) { - if (lineIsHidden(doc, line)) { updateLineHeight(line, 0); } - }); } - - if (marker.clearOnEnter) { on(marker, "beforeCursorEnter", function () { return marker.clear(); }); } - - if (marker.readOnly) { - seeReadOnlySpans(); - if (doc.history.done.length || doc.history.undone.length) - { doc.clearHistory(); } - } - if (marker.collapsed) { - marker.id = ++nextMarkerId; - marker.atomic = true; - } - if (cm) { - // Sync editor state - if (updateMaxLine) { cm.curOp.updateMaxLine = true; } - if (marker.collapsed) - { regChange(cm, from.line, to.line + 1); } - else if (marker.className || marker.startStyle || marker.endStyle || marker.css || - marker.attributes || marker.title) - { for (var i = from.line; i <= to.line; i++) { regLineChange(cm, i, "text"); } } - if (marker.atomic) { reCheckSelection(cm.doc); } - signalLater(cm, "markerAdded", cm, marker); - } - return marker - } - - // SHARED TEXTMARKERS - - // A shared marker spans multiple linked documents. It is - // implemented as a meta-marker-object controlling multiple normal - // markers. - var SharedTextMarker = function(markers, primary) { - this.markers = markers; - this.primary = primary; - for (var i = 0; i < markers.length; ++i) - { markers[i].parent = this; } - }; - - SharedTextMarker.prototype.clear = function () { - if (this.explicitlyCleared) { return } - this.explicitlyCleared = true; - for (var i = 0; i < this.markers.length; ++i) - { this.markers[i].clear(); } - signalLater(this, "clear"); - }; - - SharedTextMarker.prototype.find = function (side, lineObj) { - return this.primary.find(side, lineObj) - }; - eventMixin(SharedTextMarker); - - function markTextShared(doc, from, to, options, type) { - options = copyObj(options); - options.shared = false; - var markers = [markText(doc, from, to, options, type)], primary = markers[0]; - var widget = options.widgetNode; - linkedDocs(doc, function (doc) { - if (widget) { options.widgetNode = widget.cloneNode(true); } - markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type)); - for (var i = 0; i < doc.linked.length; ++i) - { if (doc.linked[i].isParent) { return } } - primary = lst(markers); - }); - return new SharedTextMarker(markers, primary) - } - - function findSharedMarkers(doc) { - return doc.findMarks(Pos(doc.first, 0), doc.clipPos(Pos(doc.lastLine())), function (m) { return m.parent; }) - } - - function copySharedMarkers(doc, markers) { - for (var i = 0; i < markers.length; i++) { - var marker = markers[i], pos = marker.find(); - var mFrom = doc.clipPos(pos.from), mTo = doc.clipPos(pos.to); - if (cmp(mFrom, mTo)) { - var subMark = markText(doc, mFrom, mTo, marker.primary, marker.primary.type); - marker.markers.push(subMark); - subMark.parent = marker; - } - } - } - - function detachSharedMarkers(markers) { - var loop = function ( i ) { - var marker = markers[i], linked = [marker.primary.doc]; - linkedDocs(marker.primary.doc, function (d) { return linked.push(d); }); - for (var j = 0; j < marker.markers.length; j++) { - var subMarker = marker.markers[j]; - if (indexOf(linked, subMarker.doc) == -1) { - subMarker.parent = null; - marker.markers.splice(j--, 1); - } - } - }; - - for (var i = 0; i < markers.length; i++) loop( i ); - } - - var nextDocId = 0; - var Doc = function(text, mode, firstLine, lineSep, direction) { - if (!(this instanceof Doc)) { return new Doc(text, mode, firstLine, lineSep, direction) } - if (firstLine == null) { firstLine = 0; } - - BranchChunk.call(this, [new LeafChunk([new Line("", null)])]); - this.first = firstLine; - this.scrollTop = this.scrollLeft = 0; - this.cantEdit = false; - this.cleanGeneration = 1; - this.modeFrontier = this.highlightFrontier = firstLine; - var start = Pos(firstLine, 0); - this.sel = simpleSelection(start); - this.history = new History(null); - this.id = ++nextDocId; - this.modeOption = mode; - this.lineSep = lineSep; - this.direction = (direction == "rtl") ? "rtl" : "ltr"; - this.extend = false; - - if (typeof text == "string") { text = this.splitLines(text); } - updateDoc(this, {from: start, to: start, text: text}); - setSelection(this, simpleSelection(start), sel_dontScroll); - }; - - Doc.prototype = createObj(BranchChunk.prototype, { - constructor: Doc, - // Iterate over the document. Supports two forms -- with only one - // argument, it calls that for each line in the document. With - // three, it iterates over the range given by the first two (with - // the second being non-inclusive). - iter: function(from, to, op) { - if (op) { this.iterN(from - this.first, to - from, op); } - else { this.iterN(this.first, this.first + this.size, from); } - }, - - // Non-public interface for adding and removing lines. - insert: function(at, lines) { - var height = 0; - for (var i = 0; i < lines.length; ++i) { height += lines[i].height; } - this.insertInner(at - this.first, lines, height); - }, - remove: function(at, n) { this.removeInner(at - this.first, n); }, - - // From here, the methods are part of the public interface. Most - // are also available from CodeMirror (editor) instances. - - getValue: function(lineSep) { - var lines = getLines(this, this.first, this.first + this.size); - if (lineSep === false) { return lines } - return lines.join(lineSep || this.lineSeparator()) - }, - setValue: docMethodOp(function(code) { - var top = Pos(this.first, 0), last = this.first + this.size - 1; - makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length), - text: this.splitLines(code), origin: "setValue", full: true}, true); - if (this.cm) { scrollToCoords(this.cm, 0, 0); } - setSelection(this, simpleSelection(top), sel_dontScroll); - }), - replaceRange: function(code, from, to, origin) { - from = clipPos(this, from); - to = to ? clipPos(this, to) : from; - replaceRange(this, code, from, to, origin); - }, - getRange: function(from, to, lineSep) { - var lines = getBetween(this, clipPos(this, from), clipPos(this, to)); - if (lineSep === false) { return lines } - return lines.join(lineSep || this.lineSeparator()) - }, - - getLine: function(line) {var l = this.getLineHandle(line); return l && l.text}, - - getLineHandle: function(line) {if (isLine(this, line)) { return getLine(this, line) }}, - getLineNumber: function(line) {return lineNo(line)}, - - getLineHandleVisualStart: function(line) { - if (typeof line == "number") { line = getLine(this, line); } - return visualLine(line) - }, - - lineCount: function() {return this.size}, - firstLine: function() {return this.first}, - lastLine: function() {return this.first + this.size - 1}, - - clipPos: function(pos) {return clipPos(this, pos)}, - - getCursor: function(start) { - var range = this.sel.primary(), pos; - if (start == null || start == "head") { pos = range.head; } - else if (start == "anchor") { pos = range.anchor; } - else if (start == "end" || start == "to" || start === false) { pos = range.to(); } - else { pos = range.from(); } - return pos - }, - listSelections: function() { return this.sel.ranges }, - somethingSelected: function() {return this.sel.somethingSelected()}, - - setCursor: docMethodOp(function(line, ch, options) { - setSimpleSelection(this, clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line), null, options); - }), - setSelection: docMethodOp(function(anchor, head, options) { - setSimpleSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), options); - }), - extendSelection: docMethodOp(function(head, other, options) { - extendSelection(this, clipPos(this, head), other && clipPos(this, other), options); - }), - extendSelections: docMethodOp(function(heads, options) { - extendSelections(this, clipPosArray(this, heads), options); - }), - extendSelectionsBy: docMethodOp(function(f, options) { - var heads = map(this.sel.ranges, f); - extendSelections(this, clipPosArray(this, heads), options); - }), - setSelections: docMethodOp(function(ranges, primary, options) { - if (!ranges.length) { return } - var out = []; - for (var i = 0; i < ranges.length; i++) - { out[i] = new Range(clipPos(this, ranges[i].anchor), - clipPos(this, ranges[i].head)); } - if (primary == null) { primary = Math.min(ranges.length - 1, this.sel.primIndex); } - setSelection(this, normalizeSelection(this.cm, out, primary), options); - }), - addSelection: docMethodOp(function(anchor, head, options) { - var ranges = this.sel.ranges.slice(0); - ranges.push(new Range(clipPos(this, anchor), clipPos(this, head || anchor))); - setSelection(this, normalizeSelection(this.cm, ranges, ranges.length - 1), options); - }), - - getSelection: function(lineSep) { - var ranges = this.sel.ranges, lines; - for (var i = 0; i < ranges.length; i++) { - var sel = getBetween(this, ranges[i].from(), ranges[i].to()); - lines = lines ? lines.concat(sel) : sel; - } - if (lineSep === false) { return lines } - else { return lines.join(lineSep || this.lineSeparator()) } - }, - getSelections: function(lineSep) { - var parts = [], ranges = this.sel.ranges; - for (var i = 0; i < ranges.length; i++) { - var sel = getBetween(this, ranges[i].from(), ranges[i].to()); - if (lineSep !== false) { sel = sel.join(lineSep || this.lineSeparator()); } - parts[i] = sel; - } - return parts - }, - replaceSelection: function(code, collapse, origin) { - var dup = []; - for (var i = 0; i < this.sel.ranges.length; i++) - { dup[i] = code; } - this.replaceSelections(dup, collapse, origin || "+input"); - }, - replaceSelections: docMethodOp(function(code, collapse, origin) { - var changes = [], sel = this.sel; - for (var i = 0; i < sel.ranges.length; i++) { - var range = sel.ranges[i]; - changes[i] = {from: range.from(), to: range.to(), text: this.splitLines(code[i]), origin: origin}; - } - var newSel = collapse && collapse != "end" && computeReplacedSel(this, changes, collapse); - for (var i$1 = changes.length - 1; i$1 >= 0; i$1--) - { makeChange(this, changes[i$1]); } - if (newSel) { setSelectionReplaceHistory(this, newSel); } - else if (this.cm) { ensureCursorVisible(this.cm); } - }), - undo: docMethodOp(function() {makeChangeFromHistory(this, "undo");}), - redo: docMethodOp(function() {makeChangeFromHistory(this, "redo");}), - undoSelection: docMethodOp(function() {makeChangeFromHistory(this, "undo", true);}), - redoSelection: docMethodOp(function() {makeChangeFromHistory(this, "redo", true);}), - - setExtending: function(val) {this.extend = val;}, - getExtending: function() {return this.extend}, - - historySize: function() { - var hist = this.history, done = 0, undone = 0; - for (var i = 0; i < hist.done.length; i++) { if (!hist.done[i].ranges) { ++done; } } - for (var i$1 = 0; i$1 < hist.undone.length; i$1++) { if (!hist.undone[i$1].ranges) { ++undone; } } - return {undo: done, redo: undone} - }, - clearHistory: function() { - var this$1 = this; - - this.history = new History(this.history.maxGeneration); - linkedDocs(this, function (doc) { return doc.history = this$1.history; }, true); - }, - - markClean: function() { - this.cleanGeneration = this.changeGeneration(true); - }, - changeGeneration: function(forceSplit) { - if (forceSplit) - { this.history.lastOp = this.history.lastSelOp = this.history.lastOrigin = null; } - return this.history.generation - }, - isClean: function (gen) { - return this.history.generation == (gen || this.cleanGeneration) - }, - - getHistory: function() { - return {done: copyHistoryArray(this.history.done), - undone: copyHistoryArray(this.history.undone)} - }, - setHistory: function(histData) { - var hist = this.history = new History(this.history.maxGeneration); - hist.done = copyHistoryArray(histData.done.slice(0), null, true); - hist.undone = copyHistoryArray(histData.undone.slice(0), null, true); - }, - - setGutterMarker: docMethodOp(function(line, gutterID, value) { - return changeLine(this, line, "gutter", function (line) { - var markers = line.gutterMarkers || (line.gutterMarkers = {}); - markers[gutterID] = value; - if (!value && isEmpty(markers)) { line.gutterMarkers = null; } - return true - }) - }), - - clearGutter: docMethodOp(function(gutterID) { - var this$1 = this; - - this.iter(function (line) { - if (line.gutterMarkers && line.gutterMarkers[gutterID]) { - changeLine(this$1, line, "gutter", function () { - line.gutterMarkers[gutterID] = null; - if (isEmpty(line.gutterMarkers)) { line.gutterMarkers = null; } - return true - }); - } - }); - }), - - lineInfo: function(line) { - var n; - if (typeof line == "number") { - if (!isLine(this, line)) { return null } - n = line; - line = getLine(this, line); - if (!line) { return null } - } else { - n = lineNo(line); - if (n == null) { return null } - } - return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers, - textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass, - widgets: line.widgets} - }, - - addLineClass: docMethodOp(function(handle, where, cls) { - return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function (line) { - var prop = where == "text" ? "textClass" - : where == "background" ? "bgClass" - : where == "gutter" ? "gutterClass" : "wrapClass"; - if (!line[prop]) { line[prop] = cls; } - else if (classTest(cls).test(line[prop])) { return false } - else { line[prop] += " " + cls; } - return true - }) - }), - removeLineClass: docMethodOp(function(handle, where, cls) { - return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function (line) { - var prop = where == "text" ? "textClass" - : where == "background" ? "bgClass" - : where == "gutter" ? "gutterClass" : "wrapClass"; - var cur = line[prop]; - if (!cur) { return false } - else if (cls == null) { line[prop] = null; } - else { - var found = cur.match(classTest(cls)); - if (!found) { return false } - var end = found.index + found[0].length; - line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null; - } - return true - }) - }), - - addLineWidget: docMethodOp(function(handle, node, options) { - return addLineWidget(this, handle, node, options) - }), - removeLineWidget: function(widget) { widget.clear(); }, - - markText: function(from, to, options) { - return markText(this, clipPos(this, from), clipPos(this, to), options, options && options.type || "range") - }, - setBookmark: function(pos, options) { - var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options), - insertLeft: options && options.insertLeft, - clearWhenEmpty: false, shared: options && options.shared, - handleMouseEvents: options && options.handleMouseEvents}; - pos = clipPos(this, pos); - return markText(this, pos, pos, realOpts, "bookmark") - }, - findMarksAt: function(pos) { - pos = clipPos(this, pos); - var markers = [], spans = getLine(this, pos.line).markedSpans; - if (spans) { for (var i = 0; i < spans.length; ++i) { - var span = spans[i]; - if ((span.from == null || span.from <= pos.ch) && - (span.to == null || span.to >= pos.ch)) - { markers.push(span.marker.parent || span.marker); } - } } - return markers - }, - findMarks: function(from, to, filter) { - from = clipPos(this, from); to = clipPos(this, to); - var found = [], lineNo = from.line; - this.iter(from.line, to.line + 1, function (line) { - var spans = line.markedSpans; - if (spans) { for (var i = 0; i < spans.length; i++) { - var span = spans[i]; - if (!(span.to != null && lineNo == from.line && from.ch >= span.to || - span.from == null && lineNo != from.line || - span.from != null && lineNo == to.line && span.from >= to.ch) && - (!filter || filter(span.marker))) - { found.push(span.marker.parent || span.marker); } - } } - ++lineNo; - }); - return found - }, - getAllMarks: function() { - var markers = []; - this.iter(function (line) { - var sps = line.markedSpans; - if (sps) { for (var i = 0; i < sps.length; ++i) - { if (sps[i].from != null) { markers.push(sps[i].marker); } } } - }); - return markers - }, - - posFromIndex: function(off) { - var ch, lineNo = this.first, sepSize = this.lineSeparator().length; - this.iter(function (line) { - var sz = line.text.length + sepSize; - if (sz > off) { ch = off; return true } - off -= sz; - ++lineNo; - }); - return clipPos(this, Pos(lineNo, ch)) - }, - indexFromPos: function (coords) { - coords = clipPos(this, coords); - var index = coords.ch; - if (coords.line < this.first || coords.ch < 0) { return 0 } - var sepSize = this.lineSeparator().length; - this.iter(this.first, coords.line, function (line) { // iter aborts when callback returns a truthy value - index += line.text.length + sepSize; - }); - return index - }, - - copy: function(copyHistory) { - var doc = new Doc(getLines(this, this.first, this.first + this.size), - this.modeOption, this.first, this.lineSep, this.direction); - doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft; - doc.sel = this.sel; - doc.extend = false; - if (copyHistory) { - doc.history.undoDepth = this.history.undoDepth; - doc.setHistory(this.getHistory()); - } - return doc - }, - - linkedDoc: function(options) { - if (!options) { options = {}; } - var from = this.first, to = this.first + this.size; - if (options.from != null && options.from > from) { from = options.from; } - if (options.to != null && options.to < to) { to = options.to; } - var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from, this.lineSep, this.direction); - if (options.sharedHist) { copy.history = this.history - ; }(this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist}); - copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}]; - copySharedMarkers(copy, findSharedMarkers(this)); - return copy - }, - unlinkDoc: function(other) { - if (other instanceof CodeMirror) { other = other.doc; } - if (this.linked) { for (var i = 0; i < this.linked.length; ++i) { - var link = this.linked[i]; - if (link.doc != other) { continue } - this.linked.splice(i, 1); - other.unlinkDoc(this); - detachSharedMarkers(findSharedMarkers(this)); - break - } } - // If the histories were shared, split them again - if (other.history == this.history) { - var splitIds = [other.id]; - linkedDocs(other, function (doc) { return splitIds.push(doc.id); }, true); - other.history = new History(null); - other.history.done = copyHistoryArray(this.history.done, splitIds); - other.history.undone = copyHistoryArray(this.history.undone, splitIds); - } - }, - iterLinkedDocs: function(f) {linkedDocs(this, f);}, - - getMode: function() {return this.mode}, - getEditor: function() {return this.cm}, - - splitLines: function(str) { - if (this.lineSep) { return str.split(this.lineSep) } - return splitLinesAuto(str) - }, - lineSeparator: function() { return this.lineSep || "\n" }, - - setDirection: docMethodOp(function (dir) { - if (dir != "rtl") { dir = "ltr"; } - if (dir == this.direction) { return } - this.direction = dir; - this.iter(function (line) { return line.order = null; }); - if (this.cm) { directionChanged(this.cm); } - }) - }); - - // Public alias. - Doc.prototype.eachLine = Doc.prototype.iter; - - // Kludge to work around strange IE behavior where it'll sometimes - // re-fire a series of drag-related events right after the drop (#1551) - var lastDrop = 0; - - function onDrop(e) { - var cm = this; - clearDragCursor(cm); - if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) - { return } - e_preventDefault(e); - if (ie) { lastDrop = +new Date; } - var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files; - if (!pos || cm.isReadOnly()) { return } - // Might be a file drop, in which case we simply extract the text - // and insert it. - if (files && files.length && window.FileReader && window.File) { - var n = files.length, text = Array(n), read = 0; - var markAsReadAndPasteIfAllFilesAreRead = function () { - if (++read == n) { - operation(cm, function () { - pos = clipPos(cm.doc, pos); - var change = {from: pos, to: pos, - text: cm.doc.splitLines( - text.filter(function (t) { return t != null; }).join(cm.doc.lineSeparator())), - origin: "paste"}; - makeChange(cm.doc, change); - setSelectionReplaceHistory(cm.doc, simpleSelection(clipPos(cm.doc, pos), clipPos(cm.doc, changeEnd(change)))); - })(); - } - }; - var readTextFromFile = function (file, i) { - if (cm.options.allowDropFileTypes && - indexOf(cm.options.allowDropFileTypes, file.type) == -1) { - markAsReadAndPasteIfAllFilesAreRead(); - return - } - var reader = new FileReader; - reader.onerror = function () { return markAsReadAndPasteIfAllFilesAreRead(); }; - reader.onload = function () { - var content = reader.result; - if (/[\x00-\x08\x0e-\x1f]{2}/.test(content)) { - markAsReadAndPasteIfAllFilesAreRead(); - return - } - text[i] = content; - markAsReadAndPasteIfAllFilesAreRead(); - }; - reader.readAsText(file); - }; - for (var i = 0; i < files.length; i++) { readTextFromFile(files[i], i); } - } else { // Normal drop - // Don't do a replace if the drop happened inside of the selected text. - if (cm.state.draggingText && cm.doc.sel.contains(pos) > -1) { - cm.state.draggingText(e); - // Ensure the editor is re-focused - setTimeout(function () { return cm.display.input.focus(); }, 20); - return - } - try { - var text$1 = e.dataTransfer.getData("Text"); - if (text$1) { - var selected; - if (cm.state.draggingText && !cm.state.draggingText.copy) - { selected = cm.listSelections(); } - setSelectionNoUndo(cm.doc, simpleSelection(pos, pos)); - if (selected) { for (var i$1 = 0; i$1 < selected.length; ++i$1) - { replaceRange(cm.doc, "", selected[i$1].anchor, selected[i$1].head, "drag"); } } - cm.replaceSelection(text$1, "around", "paste"); - cm.display.input.focus(); - } - } - catch(e$1){} - } - } - - function onDragStart(cm, e) { - if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return } - if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) { return } - - e.dataTransfer.setData("Text", cm.getSelection()); - e.dataTransfer.effectAllowed = "copyMove"; - - // Use dummy image instead of default browsers image. - // Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there. - if (e.dataTransfer.setDragImage && !safari) { - var img = elt("img", null, null, "position: fixed; left: 0; top: 0;"); - img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; - if (presto) { - img.width = img.height = 1; - cm.display.wrapper.appendChild(img); - // Force a relayout, or Opera won't use our image for some obscure reason - img._top = img.offsetTop; - } - e.dataTransfer.setDragImage(img, 0, 0); - if (presto) { img.parentNode.removeChild(img); } - } - } - - function onDragOver(cm, e) { - var pos = posFromMouse(cm, e); - if (!pos) { return } - var frag = document.createDocumentFragment(); - drawSelectionCursor(cm, pos, frag); - if (!cm.display.dragCursor) { - cm.display.dragCursor = elt("div", null, "CodeMirror-cursors CodeMirror-dragcursors"); - cm.display.lineSpace.insertBefore(cm.display.dragCursor, cm.display.cursorDiv); - } - removeChildrenAndAdd(cm.display.dragCursor, frag); - } - - function clearDragCursor(cm) { - if (cm.display.dragCursor) { - cm.display.lineSpace.removeChild(cm.display.dragCursor); - cm.display.dragCursor = null; - } - } - - // These must be handled carefully, because naively registering a - // handler for each editor will cause the editors to never be - // garbage collected. - - function forEachCodeMirror(f) { - if (!document.getElementsByClassName) { return } - var byClass = document.getElementsByClassName("CodeMirror"), editors = []; - for (var i = 0; i < byClass.length; i++) { - var cm = byClass[i].CodeMirror; - if (cm) { editors.push(cm); } - } - if (editors.length) { editors[0].operation(function () { - for (var i = 0; i < editors.length; i++) { f(editors[i]); } - }); } - } - - var globalsRegistered = false; - function ensureGlobalHandlers() { - if (globalsRegistered) { return } - registerGlobalHandlers(); - globalsRegistered = true; - } - function registerGlobalHandlers() { - // When the window resizes, we need to refresh active editors. - var resizeTimer; - on(window, "resize", function () { - if (resizeTimer == null) { resizeTimer = setTimeout(function () { - resizeTimer = null; - forEachCodeMirror(onResize); - }, 100); } - }); - // When the window loses focus, we want to show the editor as blurred - on(window, "blur", function () { return forEachCodeMirror(onBlur); }); - } - // Called when the window resizes - function onResize(cm) { - var d = cm.display; - // Might be a text scaling operation, clear size caches. - d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; - d.scrollbarsClipped = false; - cm.setSize(); - } - - var keyNames = { - 3: "Pause", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", - 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", - 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", - 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", - 106: "*", 107: "=", 109: "-", 110: ".", 111: "/", 145: "ScrollLock", - 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", - 221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete", - 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert" - }; - - // Number keys - for (var i = 0; i < 10; i++) { keyNames[i + 48] = keyNames[i + 96] = String(i); } - // Alphabetic keys - for (var i$1 = 65; i$1 <= 90; i$1++) { keyNames[i$1] = String.fromCharCode(i$1); } - // Function keys - for (var i$2 = 1; i$2 <= 12; i$2++) { keyNames[i$2 + 111] = keyNames[i$2 + 63235] = "F" + i$2; } - - var keyMap = {}; - - keyMap.basic = { - "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown", - "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown", - "Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore", - "Tab": "defaultTab", "Shift-Tab": "indentAuto", - "Enter": "newlineAndIndent", "Insert": "toggleOverwrite", - "Esc": "singleSelection" - }; - // Note that the save and find-related commands aren't defined by - // default. User code or addons can define them. Unknown commands - // are simply ignored. - keyMap.pcDefault = { - "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo", - "Ctrl-Home": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Up": "goLineUp", "Ctrl-Down": "goLineDown", - "Ctrl-Left": "goGroupLeft", "Ctrl-Right": "goGroupRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd", - "Ctrl-Backspace": "delGroupBefore", "Ctrl-Delete": "delGroupAfter", "Ctrl-S": "save", "Ctrl-F": "find", - "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll", - "Ctrl-[": "indentLess", "Ctrl-]": "indentMore", - "Ctrl-U": "undoSelection", "Shift-Ctrl-U": "redoSelection", "Alt-U": "redoSelection", - "fallthrough": "basic" - }; - // Very basic readline/emacs-style bindings, which are standard on Mac. - keyMap.emacsy = { - "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", - "Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", - "Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore", - "Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars", - "Ctrl-O": "openLine" - }; - keyMap.macDefault = { - "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo", - "Cmd-Home": "goDocStart", "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft", - "Alt-Right": "goGroupRight", "Cmd-Left": "goLineLeft", "Cmd-Right": "goLineRight", "Alt-Backspace": "delGroupBefore", - "Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find", - "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll", - "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight", - "Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", "Ctrl-Up": "goDocStart", "Ctrl-Down": "goDocEnd", - "fallthrough": ["basic", "emacsy"] - }; - keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; - - // KEYMAP DISPATCH - - function normalizeKeyName(name) { - var parts = name.split(/-(?!$)/); - name = parts[parts.length - 1]; - var alt, ctrl, shift, cmd; - for (var i = 0; i < parts.length - 1; i++) { - var mod = parts[i]; - if (/^(cmd|meta|m)$/i.test(mod)) { cmd = true; } - else if (/^a(lt)?$/i.test(mod)) { alt = true; } - else if (/^(c|ctrl|control)$/i.test(mod)) { ctrl = true; } - else if (/^s(hift)?$/i.test(mod)) { shift = true; } - else { throw new Error("Unrecognized modifier name: " + mod) } - } - if (alt) { name = "Alt-" + name; } - if (ctrl) { name = "Ctrl-" + name; } - if (cmd) { name = "Cmd-" + name; } - if (shift) { name = "Shift-" + name; } - return name - } - - // This is a kludge to keep keymaps mostly working as raw objects - // (backwards compatibility) while at the same time support features - // like normalization and multi-stroke key bindings. It compiles a - // new normalized keymap, and then updates the old object to reflect - // this. - function normalizeKeyMap(keymap) { - var copy = {}; - for (var keyname in keymap) { if (keymap.hasOwnProperty(keyname)) { - var value = keymap[keyname]; - if (/^(name|fallthrough|(de|at)tach)$/.test(keyname)) { continue } - if (value == "...") { delete keymap[keyname]; continue } - - var keys = map(keyname.split(" "), normalizeKeyName); - for (var i = 0; i < keys.length; i++) { - var val = (void 0), name = (void 0); - if (i == keys.length - 1) { - name = keys.join(" "); - val = value; - } else { - name = keys.slice(0, i + 1).join(" "); - val = "..."; - } - var prev = copy[name]; - if (!prev) { copy[name] = val; } - else if (prev != val) { throw new Error("Inconsistent bindings for " + name) } - } - delete keymap[keyname]; - } } - for (var prop in copy) { keymap[prop] = copy[prop]; } - return keymap - } - - function lookupKey(key, map, handle, context) { - map = getKeyMap(map); - var found = map.call ? map.call(key, context) : map[key]; - if (found === false) { return "nothing" } - if (found === "...") { return "multi" } - if (found != null && handle(found)) { return "handled" } - - if (map.fallthrough) { - if (Object.prototype.toString.call(map.fallthrough) != "[object Array]") - { return lookupKey(key, map.fallthrough, handle, context) } - for (var i = 0; i < map.fallthrough.length; i++) { - var result = lookupKey(key, map.fallthrough[i], handle, context); - if (result) { return result } - } - } - } - - // Modifier key presses don't count as 'real' key presses for the - // purpose of keymap fallthrough. - function isModifierKey(value) { - var name = typeof value == "string" ? value : keyNames[value.keyCode]; - return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod" - } - - function addModifierNames(name, event, noShift) { - var base = name; - if (event.altKey && base != "Alt") { name = "Alt-" + name; } - if ((flipCtrlCmd ? event.metaKey : event.ctrlKey) && base != "Ctrl") { name = "Ctrl-" + name; } - if ((flipCtrlCmd ? event.ctrlKey : event.metaKey) && base != "Cmd") { name = "Cmd-" + name; } - if (!noShift && event.shiftKey && base != "Shift") { name = "Shift-" + name; } - return name - } - - // Look up the name of a key as indicated by an event object. - function keyName(event, noShift) { - if (presto && event.keyCode == 34 && event["char"]) { return false } - var name = keyNames[event.keyCode]; - if (name == null || event.altGraphKey) { return false } - // Ctrl-ScrollLock has keyCode 3, same as Ctrl-Pause, - // so we'll use event.code when available (Chrome 48+, FF 38+, Safari 10.1+) - if (event.keyCode == 3 && event.code) { name = event.code; } - return addModifierNames(name, event, noShift) - } - - function getKeyMap(val) { - return typeof val == "string" ? keyMap[val] : val - } - - // Helper for deleting text near the selection(s), used to implement - // backspace, delete, and similar functionality. - function deleteNearSelection(cm, compute) { - var ranges = cm.doc.sel.ranges, kill = []; - // Build up a set of ranges to kill first, merging overlapping - // ranges. - for (var i = 0; i < ranges.length; i++) { - var toKill = compute(ranges[i]); - while (kill.length && cmp(toKill.from, lst(kill).to) <= 0) { - var replaced = kill.pop(); - if (cmp(replaced.from, toKill.from) < 0) { - toKill.from = replaced.from; - break - } - } - kill.push(toKill); - } - // Next, remove those actual ranges. - runInOp(cm, function () { - for (var i = kill.length - 1; i >= 0; i--) - { replaceRange(cm.doc, "", kill[i].from, kill[i].to, "+delete"); } - ensureCursorVisible(cm); - }); - } - - function moveCharLogically(line, ch, dir) { - var target = skipExtendingChars(line.text, ch + dir, dir); - return target < 0 || target > line.text.length ? null : target - } - - function moveLogically(line, start, dir) { - var ch = moveCharLogically(line, start.ch, dir); - return ch == null ? null : new Pos(start.line, ch, dir < 0 ? "after" : "before") - } - - function endOfLine(visually, cm, lineObj, lineNo, dir) { - if (visually) { - if (cm.doc.direction == "rtl") { dir = -dir; } - var order = getOrder(lineObj, cm.doc.direction); - if (order) { - var part = dir < 0 ? lst(order) : order[0]; - var moveInStorageOrder = (dir < 0) == (part.level == 1); - var sticky = moveInStorageOrder ? "after" : "before"; - var ch; - // With a wrapped rtl chunk (possibly spanning multiple bidi parts), - // it could be that the last bidi part is not on the last visual line, - // since visual lines contain content order-consecutive chunks. - // Thus, in rtl, we are looking for the first (content-order) character - // in the rtl chunk that is on the last line (that is, the same line - // as the last (content-order) character). - if (part.level > 0 || cm.doc.direction == "rtl") { - var prep = prepareMeasureForLine(cm, lineObj); - ch = dir < 0 ? lineObj.text.length - 1 : 0; - var targetTop = measureCharPrepared(cm, prep, ch).top; - ch = findFirst(function (ch) { return measureCharPrepared(cm, prep, ch).top == targetTop; }, (dir < 0) == (part.level == 1) ? part.from : part.to - 1, ch); - if (sticky == "before") { ch = moveCharLogically(lineObj, ch, 1); } - } else { ch = dir < 0 ? part.to : part.from; } - return new Pos(lineNo, ch, sticky) - } - } - return new Pos(lineNo, dir < 0 ? lineObj.text.length : 0, dir < 0 ? "before" : "after") - } - - function moveVisually(cm, line, start, dir) { - var bidi = getOrder(line, cm.doc.direction); - if (!bidi) { return moveLogically(line, start, dir) } - if (start.ch >= line.text.length) { - start.ch = line.text.length; - start.sticky = "before"; - } else if (start.ch <= 0) { - start.ch = 0; - start.sticky = "after"; - } - var partPos = getBidiPartAt(bidi, start.ch, start.sticky), part = bidi[partPos]; - if (cm.doc.direction == "ltr" && part.level % 2 == 0 && (dir > 0 ? part.to > start.ch : part.from < start.ch)) { - // Case 1: We move within an ltr part in an ltr editor. Even with wrapped lines, - // nothing interesting happens. - return moveLogically(line, start, dir) - } - - var mv = function (pos, dir) { return moveCharLogically(line, pos instanceof Pos ? pos.ch : pos, dir); }; - var prep; - var getWrappedLineExtent = function (ch) { - if (!cm.options.lineWrapping) { return {begin: 0, end: line.text.length} } - prep = prep || prepareMeasureForLine(cm, line); - return wrappedLineExtentChar(cm, line, prep, ch) - }; - var wrappedLineExtent = getWrappedLineExtent(start.sticky == "before" ? mv(start, -1) : start.ch); - - if (cm.doc.direction == "rtl" || part.level == 1) { - var moveInStorageOrder = (part.level == 1) == (dir < 0); - var ch = mv(start, moveInStorageOrder ? 1 : -1); - if (ch != null && (!moveInStorageOrder ? ch >= part.from && ch >= wrappedLineExtent.begin : ch <= part.to && ch <= wrappedLineExtent.end)) { - // Case 2: We move within an rtl part or in an rtl editor on the same visual line - var sticky = moveInStorageOrder ? "before" : "after"; - return new Pos(start.line, ch, sticky) - } - } - - // Case 3: Could not move within this bidi part in this visual line, so leave - // the current bidi part - - var searchInVisualLine = function (partPos, dir, wrappedLineExtent) { - var getRes = function (ch, moveInStorageOrder) { return moveInStorageOrder - ? new Pos(start.line, mv(ch, 1), "before") - : new Pos(start.line, ch, "after"); }; - - for (; partPos >= 0 && partPos < bidi.length; partPos += dir) { - var part = bidi[partPos]; - var moveInStorageOrder = (dir > 0) == (part.level != 1); - var ch = moveInStorageOrder ? wrappedLineExtent.begin : mv(wrappedLineExtent.end, -1); - if (part.from <= ch && ch < part.to) { return getRes(ch, moveInStorageOrder) } - ch = moveInStorageOrder ? part.from : mv(part.to, -1); - if (wrappedLineExtent.begin <= ch && ch < wrappedLineExtent.end) { return getRes(ch, moveInStorageOrder) } - } - }; - - // Case 3a: Look for other bidi parts on the same visual line - var res = searchInVisualLine(partPos + dir, dir, wrappedLineExtent); - if (res) { return res } - - // Case 3b: Look for other bidi parts on the next visual line - var nextCh = dir > 0 ? wrappedLineExtent.end : mv(wrappedLineExtent.begin, -1); - if (nextCh != null && !(dir > 0 && nextCh == line.text.length)) { - res = searchInVisualLine(dir > 0 ? 0 : bidi.length - 1, dir, getWrappedLineExtent(nextCh)); - if (res) { return res } - } - - // Case 4: Nowhere to move - return null - } - - // Commands are parameter-less actions that can be performed on an - // editor, mostly used for keybindings. - var commands = { - selectAll: selectAll, - singleSelection: function (cm) { return cm.setSelection(cm.getCursor("anchor"), cm.getCursor("head"), sel_dontScroll); }, - killLine: function (cm) { return deleteNearSelection(cm, function (range) { - if (range.empty()) { - var len = getLine(cm.doc, range.head.line).text.length; - if (range.head.ch == len && range.head.line < cm.lastLine()) - { return {from: range.head, to: Pos(range.head.line + 1, 0)} } - else - { return {from: range.head, to: Pos(range.head.line, len)} } - } else { - return {from: range.from(), to: range.to()} - } - }); }, - deleteLine: function (cm) { return deleteNearSelection(cm, function (range) { return ({ - from: Pos(range.from().line, 0), - to: clipPos(cm.doc, Pos(range.to().line + 1, 0)) - }); }); }, - delLineLeft: function (cm) { return deleteNearSelection(cm, function (range) { return ({ - from: Pos(range.from().line, 0), to: range.from() - }); }); }, - delWrappedLineLeft: function (cm) { return deleteNearSelection(cm, function (range) { - var top = cm.charCoords(range.head, "div").top + 5; - var leftPos = cm.coordsChar({left: 0, top: top}, "div"); - return {from: leftPos, to: range.from()} - }); }, - delWrappedLineRight: function (cm) { return deleteNearSelection(cm, function (range) { - var top = cm.charCoords(range.head, "div").top + 5; - var rightPos = cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"); - return {from: range.from(), to: rightPos } - }); }, - undo: function (cm) { return cm.undo(); }, - redo: function (cm) { return cm.redo(); }, - undoSelection: function (cm) { return cm.undoSelection(); }, - redoSelection: function (cm) { return cm.redoSelection(); }, - goDocStart: function (cm) { return cm.extendSelection(Pos(cm.firstLine(), 0)); }, - goDocEnd: function (cm) { return cm.extendSelection(Pos(cm.lastLine())); }, - goLineStart: function (cm) { return cm.extendSelectionsBy(function (range) { return lineStart(cm, range.head.line); }, - {origin: "+move", bias: 1} - ); }, - goLineStartSmart: function (cm) { return cm.extendSelectionsBy(function (range) { return lineStartSmart(cm, range.head); }, - {origin: "+move", bias: 1} - ); }, - goLineEnd: function (cm) { return cm.extendSelectionsBy(function (range) { return lineEnd(cm, range.head.line); }, - {origin: "+move", bias: -1} - ); }, - goLineRight: function (cm) { return cm.extendSelectionsBy(function (range) { - var top = cm.cursorCoords(range.head, "div").top + 5; - return cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div") - }, sel_move); }, - goLineLeft: function (cm) { return cm.extendSelectionsBy(function (range) { - var top = cm.cursorCoords(range.head, "div").top + 5; - return cm.coordsChar({left: 0, top: top}, "div") - }, sel_move); }, - goLineLeftSmart: function (cm) { return cm.extendSelectionsBy(function (range) { - var top = cm.cursorCoords(range.head, "div").top + 5; - var pos = cm.coordsChar({left: 0, top: top}, "div"); - if (pos.ch < cm.getLine(pos.line).search(/\S/)) { return lineStartSmart(cm, range.head) } - return pos - }, sel_move); }, - goLineUp: function (cm) { return cm.moveV(-1, "line"); }, - goLineDown: function (cm) { return cm.moveV(1, "line"); }, - goPageUp: function (cm) { return cm.moveV(-1, "page"); }, - goPageDown: function (cm) { return cm.moveV(1, "page"); }, - goCharLeft: function (cm) { return cm.moveH(-1, "char"); }, - goCharRight: function (cm) { return cm.moveH(1, "char"); }, - goColumnLeft: function (cm) { return cm.moveH(-1, "column"); }, - goColumnRight: function (cm) { return cm.moveH(1, "column"); }, - goWordLeft: function (cm) { return cm.moveH(-1, "word"); }, - goGroupRight: function (cm) { return cm.moveH(1, "group"); }, - goGroupLeft: function (cm) { return cm.moveH(-1, "group"); }, - goWordRight: function (cm) { return cm.moveH(1, "word"); }, - delCharBefore: function (cm) { return cm.deleteH(-1, "char"); }, - delCharAfter: function (cm) { return cm.deleteH(1, "char"); }, - delWordBefore: function (cm) { return cm.deleteH(-1, "word"); }, - delWordAfter: function (cm) { return cm.deleteH(1, "word"); }, - delGroupBefore: function (cm) { return cm.deleteH(-1, "group"); }, - delGroupAfter: function (cm) { return cm.deleteH(1, "group"); }, - indentAuto: function (cm) { return cm.indentSelection("smart"); }, - indentMore: function (cm) { return cm.indentSelection("add"); }, - indentLess: function (cm) { return cm.indentSelection("subtract"); }, - insertTab: function (cm) { return cm.replaceSelection("\t"); }, - insertSoftTab: function (cm) { - var spaces = [], ranges = cm.listSelections(), tabSize = cm.options.tabSize; - for (var i = 0; i < ranges.length; i++) { - var pos = ranges[i].from(); - var col = countColumn(cm.getLine(pos.line), pos.ch, tabSize); - spaces.push(spaceStr(tabSize - col % tabSize)); - } - cm.replaceSelections(spaces); - }, - defaultTab: function (cm) { - if (cm.somethingSelected()) { cm.indentSelection("add"); } - else { cm.execCommand("insertTab"); } - }, - // Swap the two chars left and right of each selection's head. - // Move cursor behind the two swapped characters afterwards. - // - // Doesn't consider line feeds a character. - // Doesn't scan more than one line above to find a character. - // Doesn't do anything on an empty line. - // Doesn't do anything with non-empty selections. - transposeChars: function (cm) { return runInOp(cm, function () { - var ranges = cm.listSelections(), newSel = []; - for (var i = 0; i < ranges.length; i++) { - if (!ranges[i].empty()) { continue } - var cur = ranges[i].head, line = getLine(cm.doc, cur.line).text; - if (line) { - if (cur.ch == line.length) { cur = new Pos(cur.line, cur.ch - 1); } - if (cur.ch > 0) { - cur = new Pos(cur.line, cur.ch + 1); - cm.replaceRange(line.charAt(cur.ch - 1) + line.charAt(cur.ch - 2), - Pos(cur.line, cur.ch - 2), cur, "+transpose"); - } else if (cur.line > cm.doc.first) { - var prev = getLine(cm.doc, cur.line - 1).text; - if (prev) { - cur = new Pos(cur.line, 1); - cm.replaceRange(line.charAt(0) + cm.doc.lineSeparator() + - prev.charAt(prev.length - 1), - Pos(cur.line - 1, prev.length - 1), cur, "+transpose"); - } - } - } - newSel.push(new Range(cur, cur)); - } - cm.setSelections(newSel); - }); }, - newlineAndIndent: function (cm) { return runInOp(cm, function () { - var sels = cm.listSelections(); - for (var i = sels.length - 1; i >= 0; i--) - { cm.replaceRange(cm.doc.lineSeparator(), sels[i].anchor, sels[i].head, "+input"); } - sels = cm.listSelections(); - for (var i$1 = 0; i$1 < sels.length; i$1++) - { cm.indentLine(sels[i$1].from().line, null, true); } - ensureCursorVisible(cm); - }); }, - openLine: function (cm) { return cm.replaceSelection("\n", "start"); }, - toggleOverwrite: function (cm) { return cm.toggleOverwrite(); } - }; - - - function lineStart(cm, lineN) { - var line = getLine(cm.doc, lineN); - var visual = visualLine(line); - if (visual != line) { lineN = lineNo(visual); } - return endOfLine(true, cm, visual, lineN, 1) - } - function lineEnd(cm, lineN) { - var line = getLine(cm.doc, lineN); - var visual = visualLineEnd(line); - if (visual != line) { lineN = lineNo(visual); } - return endOfLine(true, cm, line, lineN, -1) - } - function lineStartSmart(cm, pos) { - var start = lineStart(cm, pos.line); - var line = getLine(cm.doc, start.line); - var order = getOrder(line, cm.doc.direction); - if (!order || order[0].level == 0) { - var firstNonWS = Math.max(start.ch, line.text.search(/\S/)); - var inWS = pos.line == start.line && pos.ch <= firstNonWS && pos.ch; - return Pos(start.line, inWS ? 0 : firstNonWS, start.sticky) - } - return start - } - - // Run a handler that was bound to a key. - function doHandleBinding(cm, bound, dropShift) { - if (typeof bound == "string") { - bound = commands[bound]; - if (!bound) { return false } - } - // Ensure previous input has been read, so that the handler sees a - // consistent view of the document - cm.display.input.ensurePolled(); - var prevShift = cm.display.shift, done = false; - try { - if (cm.isReadOnly()) { cm.state.suppressEdits = true; } - if (dropShift) { cm.display.shift = false; } - done = bound(cm) != Pass; - } finally { - cm.display.shift = prevShift; - cm.state.suppressEdits = false; - } - return done - } - - function lookupKeyForEditor(cm, name, handle) { - for (var i = 0; i < cm.state.keyMaps.length; i++) { - var result = lookupKey(name, cm.state.keyMaps[i], handle, cm); - if (result) { return result } - } - return (cm.options.extraKeys && lookupKey(name, cm.options.extraKeys, handle, cm)) - || lookupKey(name, cm.options.keyMap, handle, cm) - } - - // Note that, despite the name, this function is also used to check - // for bound mouse clicks. - - var stopSeq = new Delayed; - - function dispatchKey(cm, name, e, handle) { - var seq = cm.state.keySeq; - if (seq) { - if (isModifierKey(name)) { return "handled" } - if (/\'$/.test(name)) - { cm.state.keySeq = null; } - else - { stopSeq.set(50, function () { - if (cm.state.keySeq == seq) { - cm.state.keySeq = null; - cm.display.input.reset(); - } - }); } - if (dispatchKeyInner(cm, seq + " " + name, e, handle)) { return true } - } - return dispatchKeyInner(cm, name, e, handle) - } - - function dispatchKeyInner(cm, name, e, handle) { - var result = lookupKeyForEditor(cm, name, handle); - - if (result == "multi") - { cm.state.keySeq = name; } - if (result == "handled") - { signalLater(cm, "keyHandled", cm, name, e); } - - if (result == "handled" || result == "multi") { - e_preventDefault(e); - restartBlink(cm); - } - - return !!result - } - - // Handle a key from the keydown event. - function handleKeyBinding(cm, e) { - var name = keyName(e, true); - if (!name) { return false } - - if (e.shiftKey && !cm.state.keySeq) { - // First try to resolve full name (including 'Shift-'). Failing - // that, see if there is a cursor-motion command (starting with - // 'go') bound to the keyname without 'Shift-'. - return dispatchKey(cm, "Shift-" + name, e, function (b) { return doHandleBinding(cm, b, true); }) - || dispatchKey(cm, name, e, function (b) { - if (typeof b == "string" ? /^go[A-Z]/.test(b) : b.motion) - { return doHandleBinding(cm, b) } - }) - } else { - return dispatchKey(cm, name, e, function (b) { return doHandleBinding(cm, b); }) - } - } - - // Handle a key from the keypress event - function handleCharBinding(cm, e, ch) { - return dispatchKey(cm, "'" + ch + "'", e, function (b) { return doHandleBinding(cm, b, true); }) - } - - var lastStoppedKey = null; - function onKeyDown(e) { - var cm = this; - if (e.target && e.target != cm.display.input.getField()) { return } - cm.curOp.focus = activeElt(); - if (signalDOMEvent(cm, e)) { return } - // IE does strange things with escape. - if (ie && ie_version < 11 && e.keyCode == 27) { e.returnValue = false; } - var code = e.keyCode; - cm.display.shift = code == 16 || e.shiftKey; - var handled = handleKeyBinding(cm, e); - if (presto) { - lastStoppedKey = handled ? code : null; - // Opera has no cut event... we try to at least catch the key combo - if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey)) - { cm.replaceSelection("", null, "cut"); } - } - if (gecko && !mac && !handled && code == 46 && e.shiftKey && !e.ctrlKey && document.execCommand) - { document.execCommand("cut"); } - - // Turn mouse into crosshair when Alt is held on Mac. - if (code == 18 && !/\bCodeMirror-crosshair\b/.test(cm.display.lineDiv.className)) - { showCrossHair(cm); } - } - - function showCrossHair(cm) { - var lineDiv = cm.display.lineDiv; - addClass(lineDiv, "CodeMirror-crosshair"); - - function up(e) { - if (e.keyCode == 18 || !e.altKey) { - rmClass(lineDiv, "CodeMirror-crosshair"); - off(document, "keyup", up); - off(document, "mouseover", up); - } - } - on(document, "keyup", up); - on(document, "mouseover", up); - } - - function onKeyUp(e) { - if (e.keyCode == 16) { this.doc.sel.shift = false; } - signalDOMEvent(this, e); - } - - function onKeyPress(e) { - var cm = this; - if (e.target && e.target != cm.display.input.getField()) { return } - if (eventInWidget(cm.display, e) || signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) { return } - var keyCode = e.keyCode, charCode = e.charCode; - if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return} - if ((presto && (!e.which || e.which < 10)) && handleKeyBinding(cm, e)) { return } - var ch = String.fromCharCode(charCode == null ? keyCode : charCode); - // Some browsers fire keypress events for backspace - if (ch == "\x08") { return } - if (handleCharBinding(cm, e, ch)) { return } - cm.display.input.onKeyPress(e); - } - - var DOUBLECLICK_DELAY = 400; - - var PastClick = function(time, pos, button) { - this.time = time; - this.pos = pos; - this.button = button; - }; - - PastClick.prototype.compare = function (time, pos, button) { - return this.time + DOUBLECLICK_DELAY > time && - cmp(pos, this.pos) == 0 && button == this.button - }; - - var lastClick, lastDoubleClick; - function clickRepeat(pos, button) { - var now = +new Date; - if (lastDoubleClick && lastDoubleClick.compare(now, pos, button)) { - lastClick = lastDoubleClick = null; - return "triple" - } else if (lastClick && lastClick.compare(now, pos, button)) { - lastDoubleClick = new PastClick(now, pos, button); - lastClick = null; - return "double" - } else { - lastClick = new PastClick(now, pos, button); - lastDoubleClick = null; - return "single" - } - } - - // A mouse down can be a single click, double click, triple click, - // start of selection drag, start of text drag, new cursor - // (ctrl-click), rectangle drag (alt-drag), or xwin - // middle-click-paste. Or it might be a click on something we should - // not interfere with, such as a scrollbar or widget. - function onMouseDown(e) { - var cm = this, display = cm.display; - if (signalDOMEvent(cm, e) || display.activeTouch && display.input.supportsTouch()) { return } - display.input.ensurePolled(); - display.shift = e.shiftKey; - - if (eventInWidget(display, e)) { - if (!webkit) { - // Briefly turn off draggability, to allow widgets to do - // normal dragging things. - display.scroller.draggable = false; - setTimeout(function () { return display.scroller.draggable = true; }, 100); - } - return - } - if (clickInGutter(cm, e)) { return } - var pos = posFromMouse(cm, e), button = e_button(e), repeat = pos ? clickRepeat(pos, button) : "single"; - window.focus(); - - // #3261: make sure, that we're not starting a second selection - if (button == 1 && cm.state.selectingText) - { cm.state.selectingText(e); } - - if (pos && handleMappedButton(cm, button, pos, repeat, e)) { return } - - if (button == 1) { - if (pos) { leftButtonDown(cm, pos, repeat, e); } - else if (e_target(e) == display.scroller) { e_preventDefault(e); } - } else if (button == 2) { - if (pos) { extendSelection(cm.doc, pos); } - setTimeout(function () { return display.input.focus(); }, 20); - } else if (button == 3) { - if (captureRightClick) { cm.display.input.onContextMenu(e); } - else { delayBlurEvent(cm); } - } - } - - function handleMappedButton(cm, button, pos, repeat, event) { - var name = "Click"; - if (repeat == "double") { name = "Double" + name; } - else if (repeat == "triple") { name = "Triple" + name; } - name = (button == 1 ? "Left" : button == 2 ? "Middle" : "Right") + name; - - return dispatchKey(cm, addModifierNames(name, event), event, function (bound) { - if (typeof bound == "string") { bound = commands[bound]; } - if (!bound) { return false } - var done = false; - try { - if (cm.isReadOnly()) { cm.state.suppressEdits = true; } - done = bound(cm, pos) != Pass; - } finally { - cm.state.suppressEdits = false; - } - return done - }) - } - - function configureMouse(cm, repeat, event) { - var option = cm.getOption("configureMouse"); - var value = option ? option(cm, repeat, event) : {}; - if (value.unit == null) { - var rect = chromeOS ? event.shiftKey && event.metaKey : event.altKey; - value.unit = rect ? "rectangle" : repeat == "single" ? "char" : repeat == "double" ? "word" : "line"; - } - if (value.extend == null || cm.doc.extend) { value.extend = cm.doc.extend || event.shiftKey; } - if (value.addNew == null) { value.addNew = mac ? event.metaKey : event.ctrlKey; } - if (value.moveOnDrag == null) { value.moveOnDrag = !(mac ? event.altKey : event.ctrlKey); } - return value - } - - function leftButtonDown(cm, pos, repeat, event) { - if (ie) { setTimeout(bind(ensureFocus, cm), 0); } - else { cm.curOp.focus = activeElt(); } - - var behavior = configureMouse(cm, repeat, event); - - var sel = cm.doc.sel, contained; - if (cm.options.dragDrop && dragAndDrop && !cm.isReadOnly() && - repeat == "single" && (contained = sel.contains(pos)) > -1 && - (cmp((contained = sel.ranges[contained]).from(), pos) < 0 || pos.xRel > 0) && - (cmp(contained.to(), pos) > 0 || pos.xRel < 0)) - { leftButtonStartDrag(cm, event, pos, behavior); } - else - { leftButtonSelect(cm, event, pos, behavior); } - } - - // Start a text drag. When it ends, see if any dragging actually - // happen, and treat as a click if it didn't. - function leftButtonStartDrag(cm, event, pos, behavior) { - var display = cm.display, moved = false; - var dragEnd = operation(cm, function (e) { - if (webkit) { display.scroller.draggable = false; } - cm.state.draggingText = false; - off(display.wrapper.ownerDocument, "mouseup", dragEnd); - off(display.wrapper.ownerDocument, "mousemove", mouseMove); - off(display.scroller, "dragstart", dragStart); - off(display.scroller, "drop", dragEnd); - if (!moved) { - e_preventDefault(e); - if (!behavior.addNew) - { extendSelection(cm.doc, pos, null, null, behavior.extend); } - // Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081) - if ((webkit && !safari) || ie && ie_version == 9) - { setTimeout(function () {display.wrapper.ownerDocument.body.focus({preventScroll: true}); display.input.focus();}, 20); } - else - { display.input.focus(); } - } - }); - var mouseMove = function(e2) { - moved = moved || Math.abs(event.clientX - e2.clientX) + Math.abs(event.clientY - e2.clientY) >= 10; - }; - var dragStart = function () { return moved = true; }; - // Let the drag handler handle this. - if (webkit) { display.scroller.draggable = true; } - cm.state.draggingText = dragEnd; - dragEnd.copy = !behavior.moveOnDrag; - // IE's approach to draggable - if (display.scroller.dragDrop) { display.scroller.dragDrop(); } - on(display.wrapper.ownerDocument, "mouseup", dragEnd); - on(display.wrapper.ownerDocument, "mousemove", mouseMove); - on(display.scroller, "dragstart", dragStart); - on(display.scroller, "drop", dragEnd); - - delayBlurEvent(cm); - setTimeout(function () { return display.input.focus(); }, 20); - } - - function rangeForUnit(cm, pos, unit) { - if (unit == "char") { return new Range(pos, pos) } - if (unit == "word") { return cm.findWordAt(pos) } - if (unit == "line") { return new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0))) } - var result = unit(cm, pos); - return new Range(result.from, result.to) - } - - // Normal selection, as opposed to text dragging. - function leftButtonSelect(cm, event, start, behavior) { - var display = cm.display, doc = cm.doc; - e_preventDefault(event); - - var ourRange, ourIndex, startSel = doc.sel, ranges = startSel.ranges; - if (behavior.addNew && !behavior.extend) { - ourIndex = doc.sel.contains(start); - if (ourIndex > -1) - { ourRange = ranges[ourIndex]; } - else - { ourRange = new Range(start, start); } - } else { - ourRange = doc.sel.primary(); - ourIndex = doc.sel.primIndex; - } - - if (behavior.unit == "rectangle") { - if (!behavior.addNew) { ourRange = new Range(start, start); } - start = posFromMouse(cm, event, true, true); - ourIndex = -1; - } else { - var range = rangeForUnit(cm, start, behavior.unit); - if (behavior.extend) - { ourRange = extendRange(ourRange, range.anchor, range.head, behavior.extend); } - else - { ourRange = range; } - } - - if (!behavior.addNew) { - ourIndex = 0; - setSelection(doc, new Selection([ourRange], 0), sel_mouse); - startSel = doc.sel; - } else if (ourIndex == -1) { - ourIndex = ranges.length; - setSelection(doc, normalizeSelection(cm, ranges.concat([ourRange]), ourIndex), - {scroll: false, origin: "*mouse"}); - } else if (ranges.length > 1 && ranges[ourIndex].empty() && behavior.unit == "char" && !behavior.extend) { - setSelection(doc, normalizeSelection(cm, ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0), - {scroll: false, origin: "*mouse"}); - startSel = doc.sel; - } else { - replaceOneSelection(doc, ourIndex, ourRange, sel_mouse); - } - - var lastPos = start; - function extendTo(pos) { - if (cmp(lastPos, pos) == 0) { return } - lastPos = pos; - - if (behavior.unit == "rectangle") { - var ranges = [], tabSize = cm.options.tabSize; - var startCol = countColumn(getLine(doc, start.line).text, start.ch, tabSize); - var posCol = countColumn(getLine(doc, pos.line).text, pos.ch, tabSize); - var left = Math.min(startCol, posCol), right = Math.max(startCol, posCol); - for (var line = Math.min(start.line, pos.line), end = Math.min(cm.lastLine(), Math.max(start.line, pos.line)); - line <= end; line++) { - var text = getLine(doc, line).text, leftPos = findColumn(text, left, tabSize); - if (left == right) - { ranges.push(new Range(Pos(line, leftPos), Pos(line, leftPos))); } - else if (text.length > leftPos) - { ranges.push(new Range(Pos(line, leftPos), Pos(line, findColumn(text, right, tabSize)))); } - } - if (!ranges.length) { ranges.push(new Range(start, start)); } - setSelection(doc, normalizeSelection(cm, startSel.ranges.slice(0, ourIndex).concat(ranges), ourIndex), - {origin: "*mouse", scroll: false}); - cm.scrollIntoView(pos); - } else { - var oldRange = ourRange; - var range = rangeForUnit(cm, pos, behavior.unit); - var anchor = oldRange.anchor, head; - if (cmp(range.anchor, anchor) > 0) { - head = range.head; - anchor = minPos(oldRange.from(), range.anchor); - } else { - head = range.anchor; - anchor = maxPos(oldRange.to(), range.head); - } - var ranges$1 = startSel.ranges.slice(0); - ranges$1[ourIndex] = bidiSimplify(cm, new Range(clipPos(doc, anchor), head)); - setSelection(doc, normalizeSelection(cm, ranges$1, ourIndex), sel_mouse); - } - } - - var editorSize = display.wrapper.getBoundingClientRect(); - // Used to ensure timeout re-tries don't fire when another extend - // happened in the meantime (clearTimeout isn't reliable -- at - // least on Chrome, the timeouts still happen even when cleared, - // if the clear happens after their scheduled firing time). - var counter = 0; - - function extend(e) { - var curCount = ++counter; - var cur = posFromMouse(cm, e, true, behavior.unit == "rectangle"); - if (!cur) { return } - if (cmp(cur, lastPos) != 0) { - cm.curOp.focus = activeElt(); - extendTo(cur); - var visible = visibleLines(display, doc); - if (cur.line >= visible.to || cur.line < visible.from) - { setTimeout(operation(cm, function () {if (counter == curCount) { extend(e); }}), 150); } - } else { - var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0; - if (outside) { setTimeout(operation(cm, function () { - if (counter != curCount) { return } - display.scroller.scrollTop += outside; - extend(e); - }), 50); } - } - } - - function done(e) { - cm.state.selectingText = false; - counter = Infinity; - // If e is null or undefined we interpret this as someone trying - // to explicitly cancel the selection rather than the user - // letting go of the mouse button. - if (e) { - e_preventDefault(e); - display.input.focus(); - } - off(display.wrapper.ownerDocument, "mousemove", move); - off(display.wrapper.ownerDocument, "mouseup", up); - doc.history.lastSelOrigin = null; - } - - var move = operation(cm, function (e) { - if (e.buttons === 0 || !e_button(e)) { done(e); } - else { extend(e); } - }); - var up = operation(cm, done); - cm.state.selectingText = up; - on(display.wrapper.ownerDocument, "mousemove", move); - on(display.wrapper.ownerDocument, "mouseup", up); - } - - // Used when mouse-selecting to adjust the anchor to the proper side - // of a bidi jump depending on the visual position of the head. - function bidiSimplify(cm, range) { - var anchor = range.anchor; - var head = range.head; - var anchorLine = getLine(cm.doc, anchor.line); - if (cmp(anchor, head) == 0 && anchor.sticky == head.sticky) { return range } - var order = getOrder(anchorLine); - if (!order) { return range } - var index = getBidiPartAt(order, anchor.ch, anchor.sticky), part = order[index]; - if (part.from != anchor.ch && part.to != anchor.ch) { return range } - var boundary = index + ((part.from == anchor.ch) == (part.level != 1) ? 0 : 1); - if (boundary == 0 || boundary == order.length) { return range } - - // Compute the relative visual position of the head compared to the - // anchor (<0 is to the left, >0 to the right) - var leftSide; - if (head.line != anchor.line) { - leftSide = (head.line - anchor.line) * (cm.doc.direction == "ltr" ? 1 : -1) > 0; - } else { - var headIndex = getBidiPartAt(order, head.ch, head.sticky); - var dir = headIndex - index || (head.ch - anchor.ch) * (part.level == 1 ? -1 : 1); - if (headIndex == boundary - 1 || headIndex == boundary) - { leftSide = dir < 0; } - else - { leftSide = dir > 0; } - } - - var usePart = order[boundary + (leftSide ? -1 : 0)]; - var from = leftSide == (usePart.level == 1); - var ch = from ? usePart.from : usePart.to, sticky = from ? "after" : "before"; - return anchor.ch == ch && anchor.sticky == sticky ? range : new Range(new Pos(anchor.line, ch, sticky), head) - } - - - // Determines whether an event happened in the gutter, and fires the - // handlers for the corresponding event. - function gutterEvent(cm, e, type, prevent) { - var mX, mY; - if (e.touches) { - mX = e.touches[0].clientX; - mY = e.touches[0].clientY; - } else { - try { mX = e.clientX; mY = e.clientY; } - catch(e$1) { return false } - } - if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) { return false } - if (prevent) { e_preventDefault(e); } - - var display = cm.display; - var lineBox = display.lineDiv.getBoundingClientRect(); - - if (mY > lineBox.bottom || !hasHandler(cm, type)) { return e_defaultPrevented(e) } - mY -= lineBox.top - display.viewOffset; - - for (var i = 0; i < cm.display.gutterSpecs.length; ++i) { - var g = display.gutters.childNodes[i]; - if (g && g.getBoundingClientRect().right >= mX) { - var line = lineAtHeight(cm.doc, mY); - var gutter = cm.display.gutterSpecs[i]; - signal(cm, type, cm, line, gutter.className, e); - return e_defaultPrevented(e) - } - } - } - - function clickInGutter(cm, e) { - return gutterEvent(cm, e, "gutterClick", true) - } - - // CONTEXT MENU HANDLING - - // To make the context menu work, we need to briefly unhide the - // textarea (making it as unobtrusive as possible) to let the - // right-click take effect on it. - function onContextMenu(cm, e) { - if (eventInWidget(cm.display, e) || contextMenuInGutter(cm, e)) { return } - if (signalDOMEvent(cm, e, "contextmenu")) { return } - if (!captureRightClick) { cm.display.input.onContextMenu(e); } - } - - function contextMenuInGutter(cm, e) { - if (!hasHandler(cm, "gutterContextMenu")) { return false } - return gutterEvent(cm, e, "gutterContextMenu", false) - } - - function themeChanged(cm) { - cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") + - cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-"); - clearCaches(cm); - } - - var Init = {toString: function(){return "CodeMirror.Init"}}; - - var defaults = {}; - var optionHandlers = {}; - - function defineOptions(CodeMirror) { - var optionHandlers = CodeMirror.optionHandlers; - - function option(name, deflt, handle, notOnInit) { - CodeMirror.defaults[name] = deflt; - if (handle) { optionHandlers[name] = - notOnInit ? function (cm, val, old) {if (old != Init) { handle(cm, val, old); }} : handle; } - } - - CodeMirror.defineOption = option; - - // Passed to option handlers when there is no old value. - CodeMirror.Init = Init; - - // These two are, on init, called from the constructor because they - // have to be initialized before the editor can start at all. - option("value", "", function (cm, val) { return cm.setValue(val); }, true); - option("mode", null, function (cm, val) { - cm.doc.modeOption = val; - loadMode(cm); - }, true); - - option("indentUnit", 2, loadMode, true); - option("indentWithTabs", false); - option("smartIndent", true); - option("tabSize", 4, function (cm) { - resetModeState(cm); - clearCaches(cm); - regChange(cm); - }, true); - - option("lineSeparator", null, function (cm, val) { - cm.doc.lineSep = val; - if (!val) { return } - var newBreaks = [], lineNo = cm.doc.first; - cm.doc.iter(function (line) { - for (var pos = 0;;) { - var found = line.text.indexOf(val, pos); - if (found == -1) { break } - pos = found + val.length; - newBreaks.push(Pos(lineNo, found)); - } - lineNo++; - }); - for (var i = newBreaks.length - 1; i >= 0; i--) - { replaceRange(cm.doc, val, newBreaks[i], Pos(newBreaks[i].line, newBreaks[i].ch + val.length)); } - }); - option("specialChars", /[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b-\u200c\u200e\u200f\u2028\u2029\ufeff\ufff9-\ufffc]/g, function (cm, val, old) { - cm.state.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g"); - if (old != Init) { cm.refresh(); } - }); - option("specialCharPlaceholder", defaultSpecialCharPlaceholder, function (cm) { return cm.refresh(); }, true); - option("electricChars", true); - option("inputStyle", mobile ? "contenteditable" : "textarea", function () { - throw new Error("inputStyle can not (yet) be changed in a running editor") // FIXME - }, true); - option("spellcheck", false, function (cm, val) { return cm.getInputField().spellcheck = val; }, true); - option("autocorrect", false, function (cm, val) { return cm.getInputField().autocorrect = val; }, true); - option("autocapitalize", false, function (cm, val) { return cm.getInputField().autocapitalize = val; }, true); - option("rtlMoveVisually", !windows); - option("wholeLineUpdateBefore", true); - - option("theme", "default", function (cm) { - themeChanged(cm); - updateGutters(cm); - }, true); - option("keyMap", "default", function (cm, val, old) { - var next = getKeyMap(val); - var prev = old != Init && getKeyMap(old); - if (prev && prev.detach) { prev.detach(cm, next); } - if (next.attach) { next.attach(cm, prev || null); } - }); - option("extraKeys", null); - option("configureMouse", null); - - option("lineWrapping", false, wrappingChanged, true); - option("gutters", [], function (cm, val) { - cm.display.gutterSpecs = getGutters(val, cm.options.lineNumbers); - updateGutters(cm); - }, true); - option("fixedGutter", true, function (cm, val) { - cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0"; - cm.refresh(); - }, true); - option("coverGutterNextToScrollbar", false, function (cm) { return updateScrollbars(cm); }, true); - option("scrollbarStyle", "native", function (cm) { - initScrollbars(cm); - updateScrollbars(cm); - cm.display.scrollbars.setScrollTop(cm.doc.scrollTop); - cm.display.scrollbars.setScrollLeft(cm.doc.scrollLeft); - }, true); - option("lineNumbers", false, function (cm, val) { - cm.display.gutterSpecs = getGutters(cm.options.gutters, val); - updateGutters(cm); - }, true); - option("firstLineNumber", 1, updateGutters, true); - option("lineNumberFormatter", function (integer) { return integer; }, updateGutters, true); - option("showCursorWhenSelecting", false, updateSelection, true); - - option("resetSelectionOnContextMenu", true); - option("lineWiseCopyCut", true); - option("pasteLinesPerSelection", true); - option("selectionsMayTouch", false); - - option("readOnly", false, function (cm, val) { - if (val == "nocursor") { - onBlur(cm); - cm.display.input.blur(); - } - cm.display.input.readOnlyChanged(val); - }); - - option("screenReaderLabel", null, function (cm, val) { - val = (val === '') ? null : val; - cm.display.input.screenReaderLabelChanged(val); - }); - - option("disableInput", false, function (cm, val) {if (!val) { cm.display.input.reset(); }}, true); - option("dragDrop", true, dragDropChanged); - option("allowDropFileTypes", null); - - option("cursorBlinkRate", 530); - option("cursorScrollMargin", 0); - option("cursorHeight", 1, updateSelection, true); - option("singleCursorHeightPerLine", true, updateSelection, true); - option("workTime", 100); - option("workDelay", 100); - option("flattenSpans", true, resetModeState, true); - option("addModeClass", false, resetModeState, true); - option("pollInterval", 100); - option("undoDepth", 200, function (cm, val) { return cm.doc.history.undoDepth = val; }); - option("historyEventDelay", 1250); - option("viewportMargin", 10, function (cm) { return cm.refresh(); }, true); - option("maxHighlightLength", 10000, resetModeState, true); - option("moveInputWithCursor", true, function (cm, val) { - if (!val) { cm.display.input.resetPosition(); } - }); - - option("tabindex", null, function (cm, val) { return cm.display.input.getField().tabIndex = val || ""; }); - option("autofocus", null); - option("direction", "ltr", function (cm, val) { return cm.doc.setDirection(val); }, true); - option("phrases", null); - } - - function dragDropChanged(cm, value, old) { - var wasOn = old && old != Init; - if (!value != !wasOn) { - var funcs = cm.display.dragFunctions; - var toggle = value ? on : off; - toggle(cm.display.scroller, "dragstart", funcs.start); - toggle(cm.display.scroller, "dragenter", funcs.enter); - toggle(cm.display.scroller, "dragover", funcs.over); - toggle(cm.display.scroller, "dragleave", funcs.leave); - toggle(cm.display.scroller, "drop", funcs.drop); - } - } - - function wrappingChanged(cm) { - if (cm.options.lineWrapping) { - addClass(cm.display.wrapper, "CodeMirror-wrap"); - cm.display.sizer.style.minWidth = ""; - cm.display.sizerWidth = null; - } else { - rmClass(cm.display.wrapper, "CodeMirror-wrap"); - findMaxLine(cm); - } - estimateLineHeights(cm); - regChange(cm); - clearCaches(cm); - setTimeout(function () { return updateScrollbars(cm); }, 100); - } - - // A CodeMirror instance represents an editor. This is the object - // that user code is usually dealing with. - - function CodeMirror(place, options) { - var this$1 = this; - - if (!(this instanceof CodeMirror)) { return new CodeMirror(place, options) } - - this.options = options = options ? copyObj(options) : {}; - // Determine effective options based on given values and defaults. - copyObj(defaults, options, false); - - var doc = options.value; - if (typeof doc == "string") { doc = new Doc(doc, options.mode, null, options.lineSeparator, options.direction); } - else if (options.mode) { doc.modeOption = options.mode; } - this.doc = doc; - - var input = new CodeMirror.inputStyles[options.inputStyle](this); - var display = this.display = new Display(place, doc, input, options); - display.wrapper.CodeMirror = this; - themeChanged(this); - if (options.lineWrapping) - { this.display.wrapper.className += " CodeMirror-wrap"; } - initScrollbars(this); - - this.state = { - keyMaps: [], // stores maps added by addKeyMap - overlays: [], // highlighting overlays, as added by addOverlay - modeGen: 0, // bumped when mode/overlay changes, used to invalidate highlighting info - overwrite: false, - delayingBlurEvent: false, - focused: false, - suppressEdits: false, // used to disable editing during key handlers when in readOnly mode - pasteIncoming: -1, cutIncoming: -1, // help recognize paste/cut edits in input.poll - selectingText: false, - draggingText: false, - highlight: new Delayed(), // stores highlight worker timeout - keySeq: null, // Unfinished key sequence - specialChars: null - }; - - if (options.autofocus && !mobile) { display.input.focus(); } - - // Override magic textarea content restore that IE sometimes does - // on our hidden textarea on reload - if (ie && ie_version < 11) { setTimeout(function () { return this$1.display.input.reset(true); }, 20); } - - registerEventHandlers(this); - ensureGlobalHandlers(); - - startOperation(this); - this.curOp.forceUpdate = true; - attachDoc(this, doc); - - if ((options.autofocus && !mobile) || this.hasFocus()) - { setTimeout(bind(onFocus, this), 20); } - else - { onBlur(this); } - - for (var opt in optionHandlers) { if (optionHandlers.hasOwnProperty(opt)) - { optionHandlers[opt](this, options[opt], Init); } } - maybeUpdateLineNumberWidth(this); - if (options.finishInit) { options.finishInit(this); } - for (var i = 0; i < initHooks.length; ++i) { initHooks[i](this); } - endOperation(this); - // Suppress optimizelegibility in Webkit, since it breaks text - // measuring on line wrapping boundaries. - if (webkit && options.lineWrapping && - getComputedStyle(display.lineDiv).textRendering == "optimizelegibility") - { display.lineDiv.style.textRendering = "auto"; } - } - - // The default configuration options. - CodeMirror.defaults = defaults; - // Functions to run when options are changed. - CodeMirror.optionHandlers = optionHandlers; - - // Attach the necessary event handlers when initializing the editor - function registerEventHandlers(cm) { - var d = cm.display; - on(d.scroller, "mousedown", operation(cm, onMouseDown)); - // Older IE's will not fire a second mousedown for a double click - if (ie && ie_version < 11) - { on(d.scroller, "dblclick", operation(cm, function (e) { - if (signalDOMEvent(cm, e)) { return } - var pos = posFromMouse(cm, e); - if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) { return } - e_preventDefault(e); - var word = cm.findWordAt(pos); - extendSelection(cm.doc, word.anchor, word.head); - })); } - else - { on(d.scroller, "dblclick", function (e) { return signalDOMEvent(cm, e) || e_preventDefault(e); }); } - // Some browsers fire contextmenu *after* opening the menu, at - // which point we can't mess with it anymore. Context menu is - // handled in onMouseDown for these browsers. - on(d.scroller, "contextmenu", function (e) { return onContextMenu(cm, e); }); - on(d.input.getField(), "contextmenu", function (e) { - if (!d.scroller.contains(e.target)) { onContextMenu(cm, e); } - }); - - // Used to suppress mouse event handling when a touch happens - var touchFinished, prevTouch = {end: 0}; - function finishTouch() { - if (d.activeTouch) { - touchFinished = setTimeout(function () { return d.activeTouch = null; }, 1000); - prevTouch = d.activeTouch; - prevTouch.end = +new Date; - } - } - function isMouseLikeTouchEvent(e) { - if (e.touches.length != 1) { return false } - var touch = e.touches[0]; - return touch.radiusX <= 1 && touch.radiusY <= 1 - } - function farAway(touch, other) { - if (other.left == null) { return true } - var dx = other.left - touch.left, dy = other.top - touch.top; - return dx * dx + dy * dy > 20 * 20 - } - on(d.scroller, "touchstart", function (e) { - if (!signalDOMEvent(cm, e) && !isMouseLikeTouchEvent(e) && !clickInGutter(cm, e)) { - d.input.ensurePolled(); - clearTimeout(touchFinished); - var now = +new Date; - d.activeTouch = {start: now, moved: false, - prev: now - prevTouch.end <= 300 ? prevTouch : null}; - if (e.touches.length == 1) { - d.activeTouch.left = e.touches[0].pageX; - d.activeTouch.top = e.touches[0].pageY; - } - } - }); - on(d.scroller, "touchmove", function () { - if (d.activeTouch) { d.activeTouch.moved = true; } - }); - on(d.scroller, "touchend", function (e) { - var touch = d.activeTouch; - if (touch && !eventInWidget(d, e) && touch.left != null && - !touch.moved && new Date - touch.start < 300) { - var pos = cm.coordsChar(d.activeTouch, "page"), range; - if (!touch.prev || farAway(touch, touch.prev)) // Single tap - { range = new Range(pos, pos); } - else if (!touch.prev.prev || farAway(touch, touch.prev.prev)) // Double tap - { range = cm.findWordAt(pos); } - else // Triple tap - { range = new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0))); } - cm.setSelection(range.anchor, range.head); - cm.focus(); - e_preventDefault(e); - } - finishTouch(); - }); - on(d.scroller, "touchcancel", finishTouch); - - // Sync scrolling between fake scrollbars and real scrollable - // area, ensure viewport is updated when scrolling. - on(d.scroller, "scroll", function () { - if (d.scroller.clientHeight) { - updateScrollTop(cm, d.scroller.scrollTop); - setScrollLeft(cm, d.scroller.scrollLeft, true); - signal(cm, "scroll", cm); - } - }); - - // Listen to wheel events in order to try and update the viewport on time. - on(d.scroller, "mousewheel", function (e) { return onScrollWheel(cm, e); }); - on(d.scroller, "DOMMouseScroll", function (e) { return onScrollWheel(cm, e); }); - - // Prevent wrapper from ever scrolling - on(d.wrapper, "scroll", function () { return d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; }); - - d.dragFunctions = { - enter: function (e) {if (!signalDOMEvent(cm, e)) { e_stop(e); }}, - over: function (e) {if (!signalDOMEvent(cm, e)) { onDragOver(cm, e); e_stop(e); }}, - start: function (e) { return onDragStart(cm, e); }, - drop: operation(cm, onDrop), - leave: function (e) {if (!signalDOMEvent(cm, e)) { clearDragCursor(cm); }} - }; - - var inp = d.input.getField(); - on(inp, "keyup", function (e) { return onKeyUp.call(cm, e); }); - on(inp, "keydown", operation(cm, onKeyDown)); - on(inp, "keypress", operation(cm, onKeyPress)); - on(inp, "focus", function (e) { return onFocus(cm, e); }); - on(inp, "blur", function (e) { return onBlur(cm, e); }); - } - - var initHooks = []; - CodeMirror.defineInitHook = function (f) { return initHooks.push(f); }; - - // Indent the given line. The how parameter can be "smart", - // "add"/null, "subtract", or "prev". When aggressive is false - // (typically set to true for forced single-line indents), empty - // lines are not indented, and places where the mode returns Pass - // are left alone. - function indentLine(cm, n, how, aggressive) { - var doc = cm.doc, state; - if (how == null) { how = "add"; } - if (how == "smart") { - // Fall back to "prev" when the mode doesn't have an indentation - // method. - if (!doc.mode.indent) { how = "prev"; } - else { state = getContextBefore(cm, n).state; } - } - - var tabSize = cm.options.tabSize; - var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize); - if (line.stateAfter) { line.stateAfter = null; } - var curSpaceString = line.text.match(/^\s*/)[0], indentation; - if (!aggressive && !/\S/.test(line.text)) { - indentation = 0; - how = "not"; - } else if (how == "smart") { - indentation = doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text); - if (indentation == Pass || indentation > 150) { - if (!aggressive) { return } - how = "prev"; - } - } - if (how == "prev") { - if (n > doc.first) { indentation = countColumn(getLine(doc, n-1).text, null, tabSize); } - else { indentation = 0; } - } else if (how == "add") { - indentation = curSpace + cm.options.indentUnit; - } else if (how == "subtract") { - indentation = curSpace - cm.options.indentUnit; - } else if (typeof how == "number") { - indentation = curSpace + how; - } - indentation = Math.max(0, indentation); - - var indentString = "", pos = 0; - if (cm.options.indentWithTabs) - { for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";} } - if (pos < indentation) { indentString += spaceStr(indentation - pos); } - - if (indentString != curSpaceString) { - replaceRange(doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input"); - line.stateAfter = null; - return true - } else { - // Ensure that, if the cursor was in the whitespace at the start - // of the line, it is moved to the end of that space. - for (var i$1 = 0; i$1 < doc.sel.ranges.length; i$1++) { - var range = doc.sel.ranges[i$1]; - if (range.head.line == n && range.head.ch < curSpaceString.length) { - var pos$1 = Pos(n, curSpaceString.length); - replaceOneSelection(doc, i$1, new Range(pos$1, pos$1)); - break - } - } - } - } - - // This will be set to a {lineWise: bool, text: [string]} object, so - // that, when pasting, we know what kind of selections the copied - // text was made out of. - var lastCopied = null; - - function setLastCopied(newLastCopied) { - lastCopied = newLastCopied; - } - - function applyTextInput(cm, inserted, deleted, sel, origin) { - var doc = cm.doc; - cm.display.shift = false; - if (!sel) { sel = doc.sel; } - - var recent = +new Date - 200; - var paste = origin == "paste" || cm.state.pasteIncoming > recent; - var textLines = splitLinesAuto(inserted), multiPaste = null; - // When pasting N lines into N selections, insert one line per selection - if (paste && sel.ranges.length > 1) { - if (lastCopied && lastCopied.text.join("\n") == inserted) { - if (sel.ranges.length % lastCopied.text.length == 0) { - multiPaste = []; - for (var i = 0; i < lastCopied.text.length; i++) - { multiPaste.push(doc.splitLines(lastCopied.text[i])); } - } - } else if (textLines.length == sel.ranges.length && cm.options.pasteLinesPerSelection) { - multiPaste = map(textLines, function (l) { return [l]; }); - } - } - - var updateInput = cm.curOp.updateInput; - // Normal behavior is to insert the new text into every selection - for (var i$1 = sel.ranges.length - 1; i$1 >= 0; i$1--) { - var range = sel.ranges[i$1]; - var from = range.from(), to = range.to(); - if (range.empty()) { - if (deleted && deleted > 0) // Handle deletion - { from = Pos(from.line, from.ch - deleted); } - else if (cm.state.overwrite && !paste) // Handle overwrite - { to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length)); } - else if (paste && lastCopied && lastCopied.lineWise && lastCopied.text.join("\n") == textLines.join("\n")) - { from = to = Pos(from.line, 0); } - } - var changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i$1 % multiPaste.length] : textLines, - origin: origin || (paste ? "paste" : cm.state.cutIncoming > recent ? "cut" : "+input")}; - makeChange(cm.doc, changeEvent); - signalLater(cm, "inputRead", cm, changeEvent); - } - if (inserted && !paste) - { triggerElectric(cm, inserted); } - - ensureCursorVisible(cm); - if (cm.curOp.updateInput < 2) { cm.curOp.updateInput = updateInput; } - cm.curOp.typing = true; - cm.state.pasteIncoming = cm.state.cutIncoming = -1; - } - - function handlePaste(e, cm) { - var pasted = e.clipboardData && e.clipboardData.getData("Text"); - if (pasted) { - e.preventDefault(); - if (!cm.isReadOnly() && !cm.options.disableInput) - { runInOp(cm, function () { return applyTextInput(cm, pasted, 0, null, "paste"); }); } - return true - } - } - - function triggerElectric(cm, inserted) { - // When an 'electric' character is inserted, immediately trigger a reindent - if (!cm.options.electricChars || !cm.options.smartIndent) { return } - var sel = cm.doc.sel; - - for (var i = sel.ranges.length - 1; i >= 0; i--) { - var range = sel.ranges[i]; - if (range.head.ch > 100 || (i && sel.ranges[i - 1].head.line == range.head.line)) { continue } - var mode = cm.getModeAt(range.head); - var indented = false; - if (mode.electricChars) { - for (var j = 0; j < mode.electricChars.length; j++) - { if (inserted.indexOf(mode.electricChars.charAt(j)) > -1) { - indented = indentLine(cm, range.head.line, "smart"); - break - } } - } else if (mode.electricInput) { - if (mode.electricInput.test(getLine(cm.doc, range.head.line).text.slice(0, range.head.ch))) - { indented = indentLine(cm, range.head.line, "smart"); } - } - if (indented) { signalLater(cm, "electricInput", cm, range.head.line); } - } - } - - function copyableRanges(cm) { - var text = [], ranges = []; - for (var i = 0; i < cm.doc.sel.ranges.length; i++) { - var line = cm.doc.sel.ranges[i].head.line; - var lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)}; - ranges.push(lineRange); - text.push(cm.getRange(lineRange.anchor, lineRange.head)); - } - return {text: text, ranges: ranges} - } - - function disableBrowserMagic(field, spellcheck, autocorrect, autocapitalize) { - field.setAttribute("autocorrect", autocorrect ? "" : "off"); - field.setAttribute("autocapitalize", autocapitalize ? "" : "off"); - field.setAttribute("spellcheck", !!spellcheck); - } - - function hiddenTextarea() { - var te = elt("textarea", null, null, "position: absolute; bottom: -1em; padding: 0; width: 1px; height: 1em; outline: none"); - var div = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); - // The textarea is kept positioned near the cursor to prevent the - // fact that it'll be scrolled into view on input from scrolling - // our fake cursor out of view. On webkit, when wrap=off, paste is - // very slow. So make the area wide instead. - if (webkit) { te.style.width = "1000px"; } - else { te.setAttribute("wrap", "off"); } - // If border: 0; -- iOS fails to open keyboard (issue #1287) - if (ios) { te.style.border = "1px solid black"; } - disableBrowserMagic(te); - return div - } - - // The publicly visible API. Note that methodOp(f) means - // 'wrap f in an operation, performed on its `this` parameter'. - - // This is not the complete set of editor methods. Most of the - // methods defined on the Doc type are also injected into - // CodeMirror.prototype, for backwards compatibility and - // convenience. - - function addEditorMethods(CodeMirror) { - var optionHandlers = CodeMirror.optionHandlers; - - var helpers = CodeMirror.helpers = {}; - - CodeMirror.prototype = { - constructor: CodeMirror, - focus: function(){window.focus(); this.display.input.focus();}, - - setOption: function(option, value) { - var options = this.options, old = options[option]; - if (options[option] == value && option != "mode") { return } - options[option] = value; - if (optionHandlers.hasOwnProperty(option)) - { operation(this, optionHandlers[option])(this, value, old); } - signal(this, "optionChange", this, option); - }, - - getOption: function(option) {return this.options[option]}, - getDoc: function() {return this.doc}, - - addKeyMap: function(map, bottom) { - this.state.keyMaps[bottom ? "push" : "unshift"](getKeyMap(map)); - }, - removeKeyMap: function(map) { - var maps = this.state.keyMaps; - for (var i = 0; i < maps.length; ++i) - { if (maps[i] == map || maps[i].name == map) { - maps.splice(i, 1); - return true - } } - }, - - addOverlay: methodOp(function(spec, options) { - var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec); - if (mode.startState) { throw new Error("Overlays may not be stateful.") } - insertSorted(this.state.overlays, - {mode: mode, modeSpec: spec, opaque: options && options.opaque, - priority: (options && options.priority) || 0}, - function (overlay) { return overlay.priority; }); - this.state.modeGen++; - regChange(this); - }), - removeOverlay: methodOp(function(spec) { - var overlays = this.state.overlays; - for (var i = 0; i < overlays.length; ++i) { - var cur = overlays[i].modeSpec; - if (cur == spec || typeof spec == "string" && cur.name == spec) { - overlays.splice(i, 1); - this.state.modeGen++; - regChange(this); - return - } - } - }), - - indentLine: methodOp(function(n, dir, aggressive) { - if (typeof dir != "string" && typeof dir != "number") { - if (dir == null) { dir = this.options.smartIndent ? "smart" : "prev"; } - else { dir = dir ? "add" : "subtract"; } - } - if (isLine(this.doc, n)) { indentLine(this, n, dir, aggressive); } - }), - indentSelection: methodOp(function(how) { - var ranges = this.doc.sel.ranges, end = -1; - for (var i = 0; i < ranges.length; i++) { - var range = ranges[i]; - if (!range.empty()) { - var from = range.from(), to = range.to(); - var start = Math.max(end, from.line); - end = Math.min(this.lastLine(), to.line - (to.ch ? 0 : 1)) + 1; - for (var j = start; j < end; ++j) - { indentLine(this, j, how); } - var newRanges = this.doc.sel.ranges; - if (from.ch == 0 && ranges.length == newRanges.length && newRanges[i].from().ch > 0) - { replaceOneSelection(this.doc, i, new Range(from, newRanges[i].to()), sel_dontScroll); } - } else if (range.head.line > end) { - indentLine(this, range.head.line, how, true); - end = range.head.line; - if (i == this.doc.sel.primIndex) { ensureCursorVisible(this); } - } - } - }), - - // Fetch the parser token for a given character. Useful for hacks - // that want to inspect the mode state (say, for completion). - getTokenAt: function(pos, precise) { - return takeToken(this, pos, precise) - }, - - getLineTokens: function(line, precise) { - return takeToken(this, Pos(line), precise, true) - }, - - getTokenTypeAt: function(pos) { - pos = clipPos(this.doc, pos); - var styles = getLineStyles(this, getLine(this.doc, pos.line)); - var before = 0, after = (styles.length - 1) / 2, ch = pos.ch; - var type; - if (ch == 0) { type = styles[2]; } - else { for (;;) { - var mid = (before + after) >> 1; - if ((mid ? styles[mid * 2 - 1] : 0) >= ch) { after = mid; } - else if (styles[mid * 2 + 1] < ch) { before = mid + 1; } - else { type = styles[mid * 2 + 2]; break } - } } - var cut = type ? type.indexOf("overlay ") : -1; - return cut < 0 ? type : cut == 0 ? null : type.slice(0, cut - 1) - }, - - getModeAt: function(pos) { - var mode = this.doc.mode; - if (!mode.innerMode) { return mode } - return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode - }, - - getHelper: function(pos, type) { - return this.getHelpers(pos, type)[0] - }, - - getHelpers: function(pos, type) { - var found = []; - if (!helpers.hasOwnProperty(type)) { return found } - var help = helpers[type], mode = this.getModeAt(pos); - if (typeof mode[type] == "string") { - if (help[mode[type]]) { found.push(help[mode[type]]); } - } else if (mode[type]) { - for (var i = 0; i < mode[type].length; i++) { - var val = help[mode[type][i]]; - if (val) { found.push(val); } - } - } else if (mode.helperType && help[mode.helperType]) { - found.push(help[mode.helperType]); - } else if (help[mode.name]) { - found.push(help[mode.name]); - } - for (var i$1 = 0; i$1 < help._global.length; i$1++) { - var cur = help._global[i$1]; - if (cur.pred(mode, this) && indexOf(found, cur.val) == -1) - { found.push(cur.val); } - } - return found - }, - - getStateAfter: function(line, precise) { - var doc = this.doc; - line = clipLine(doc, line == null ? doc.first + doc.size - 1: line); - return getContextBefore(this, line + 1, precise).state - }, - - cursorCoords: function(start, mode) { - var pos, range = this.doc.sel.primary(); - if (start == null) { pos = range.head; } - else if (typeof start == "object") { pos = clipPos(this.doc, start); } - else { pos = start ? range.from() : range.to(); } - return cursorCoords(this, pos, mode || "page") - }, - - charCoords: function(pos, mode) { - return charCoords(this, clipPos(this.doc, pos), mode || "page") - }, - - coordsChar: function(coords, mode) { - coords = fromCoordSystem(this, coords, mode || "page"); - return coordsChar(this, coords.left, coords.top) - }, - - lineAtHeight: function(height, mode) { - height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top; - return lineAtHeight(this.doc, height + this.display.viewOffset) - }, - heightAtLine: function(line, mode, includeWidgets) { - var end = false, lineObj; - if (typeof line == "number") { - var last = this.doc.first + this.doc.size - 1; - if (line < this.doc.first) { line = this.doc.first; } - else if (line > last) { line = last; end = true; } - lineObj = getLine(this.doc, line); - } else { - lineObj = line; - } - return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || "page", includeWidgets || end).top + - (end ? this.doc.height - heightAtLine(lineObj) : 0) - }, - - defaultTextHeight: function() { return textHeight(this.display) }, - defaultCharWidth: function() { return charWidth(this.display) }, - - getViewport: function() { return {from: this.display.viewFrom, to: this.display.viewTo}}, - - addWidget: function(pos, node, scroll, vert, horiz) { - var display = this.display; - pos = cursorCoords(this, clipPos(this.doc, pos)); - var top = pos.bottom, left = pos.left; - node.style.position = "absolute"; - node.setAttribute("cm-ignore-events", "true"); - this.display.input.setUneditable(node); - display.sizer.appendChild(node); - if (vert == "over") { - top = pos.top; - } else if (vert == "above" || vert == "near") { - var vspace = Math.max(display.wrapper.clientHeight, this.doc.height), - hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth); - // Default to positioning above (if specified and possible); otherwise default to positioning below - if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight) - { top = pos.top - node.offsetHeight; } - else if (pos.bottom + node.offsetHeight <= vspace) - { top = pos.bottom; } - if (left + node.offsetWidth > hspace) - { left = hspace - node.offsetWidth; } - } - node.style.top = top + "px"; - node.style.left = node.style.right = ""; - if (horiz == "right") { - left = display.sizer.clientWidth - node.offsetWidth; - node.style.right = "0px"; - } else { - if (horiz == "left") { left = 0; } - else if (horiz == "middle") { left = (display.sizer.clientWidth - node.offsetWidth) / 2; } - node.style.left = left + "px"; - } - if (scroll) - { scrollIntoView(this, {left: left, top: top, right: left + node.offsetWidth, bottom: top + node.offsetHeight}); } - }, - - triggerOnKeyDown: methodOp(onKeyDown), - triggerOnKeyPress: methodOp(onKeyPress), - triggerOnKeyUp: onKeyUp, - triggerOnMouseDown: methodOp(onMouseDown), - - execCommand: function(cmd) { - if (commands.hasOwnProperty(cmd)) - { return commands[cmd].call(null, this) } - }, - - triggerElectric: methodOp(function(text) { triggerElectric(this, text); }), - - findPosH: function(from, amount, unit, visually) { - var dir = 1; - if (amount < 0) { dir = -1; amount = -amount; } - var cur = clipPos(this.doc, from); - for (var i = 0; i < amount; ++i) { - cur = findPosH(this.doc, cur, dir, unit, visually); - if (cur.hitSide) { break } - } - return cur - }, - - moveH: methodOp(function(dir, unit) { - var this$1 = this; - - this.extendSelectionsBy(function (range) { - if (this$1.display.shift || this$1.doc.extend || range.empty()) - { return findPosH(this$1.doc, range.head, dir, unit, this$1.options.rtlMoveVisually) } - else - { return dir < 0 ? range.from() : range.to() } - }, sel_move); - }), - - deleteH: methodOp(function(dir, unit) { - var sel = this.doc.sel, doc = this.doc; - if (sel.somethingSelected()) - { doc.replaceSelection("", null, "+delete"); } - else - { deleteNearSelection(this, function (range) { - var other = findPosH(doc, range.head, dir, unit, false); - return dir < 0 ? {from: other, to: range.head} : {from: range.head, to: other} - }); } - }), - - findPosV: function(from, amount, unit, goalColumn) { - var dir = 1, x = goalColumn; - if (amount < 0) { dir = -1; amount = -amount; } - var cur = clipPos(this.doc, from); - for (var i = 0; i < amount; ++i) { - var coords = cursorCoords(this, cur, "div"); - if (x == null) { x = coords.left; } - else { coords.left = x; } - cur = findPosV(this, coords, dir, unit); - if (cur.hitSide) { break } - } - return cur - }, - - moveV: methodOp(function(dir, unit) { - var this$1 = this; - - var doc = this.doc, goals = []; - var collapse = !this.display.shift && !doc.extend && doc.sel.somethingSelected(); - doc.extendSelectionsBy(function (range) { - if (collapse) - { return dir < 0 ? range.from() : range.to() } - var headPos = cursorCoords(this$1, range.head, "div"); - if (range.goalColumn != null) { headPos.left = range.goalColumn; } - goals.push(headPos.left); - var pos = findPosV(this$1, headPos, dir, unit); - if (unit == "page" && range == doc.sel.primary()) - { addToScrollTop(this$1, charCoords(this$1, pos, "div").top - headPos.top); } - return pos - }, sel_move); - if (goals.length) { for (var i = 0; i < doc.sel.ranges.length; i++) - { doc.sel.ranges[i].goalColumn = goals[i]; } } - }), - - // Find the word at the given position (as returned by coordsChar). - findWordAt: function(pos) { - var doc = this.doc, line = getLine(doc, pos.line).text; - var start = pos.ch, end = pos.ch; - if (line) { - var helper = this.getHelper(pos, "wordChars"); - if ((pos.sticky == "before" || end == line.length) && start) { --start; } else { ++end; } - var startChar = line.charAt(start); - var check = isWordChar(startChar, helper) - ? function (ch) { return isWordChar(ch, helper); } - : /\s/.test(startChar) ? function (ch) { return /\s/.test(ch); } - : function (ch) { return (!/\s/.test(ch) && !isWordChar(ch)); }; - while (start > 0 && check(line.charAt(start - 1))) { --start; } - while (end < line.length && check(line.charAt(end))) { ++end; } - } - return new Range(Pos(pos.line, start), Pos(pos.line, end)) - }, - - toggleOverwrite: function(value) { - if (value != null && value == this.state.overwrite) { return } - if (this.state.overwrite = !this.state.overwrite) - { addClass(this.display.cursorDiv, "CodeMirror-overwrite"); } - else - { rmClass(this.display.cursorDiv, "CodeMirror-overwrite"); } - - signal(this, "overwriteToggle", this, this.state.overwrite); - }, - hasFocus: function() { return this.display.input.getField() == activeElt() }, - isReadOnly: function() { return !!(this.options.readOnly || this.doc.cantEdit) }, - - scrollTo: methodOp(function (x, y) { scrollToCoords(this, x, y); }), - getScrollInfo: function() { - var scroller = this.display.scroller; - return {left: scroller.scrollLeft, top: scroller.scrollTop, - height: scroller.scrollHeight - scrollGap(this) - this.display.barHeight, - width: scroller.scrollWidth - scrollGap(this) - this.display.barWidth, - clientHeight: displayHeight(this), clientWidth: displayWidth(this)} - }, - - scrollIntoView: methodOp(function(range, margin) { - if (range == null) { - range = {from: this.doc.sel.primary().head, to: null}; - if (margin == null) { margin = this.options.cursorScrollMargin; } - } else if (typeof range == "number") { - range = {from: Pos(range, 0), to: null}; - } else if (range.from == null) { - range = {from: range, to: null}; - } - if (!range.to) { range.to = range.from; } - range.margin = margin || 0; - - if (range.from.line != null) { - scrollToRange(this, range); - } else { - scrollToCoordsRange(this, range.from, range.to, range.margin); - } - }), - - setSize: methodOp(function(width, height) { - var this$1 = this; - - var interpret = function (val) { return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val; }; - if (width != null) { this.display.wrapper.style.width = interpret(width); } - if (height != null) { this.display.wrapper.style.height = interpret(height); } - if (this.options.lineWrapping) { clearLineMeasurementCache(this); } - var lineNo = this.display.viewFrom; - this.doc.iter(lineNo, this.display.viewTo, function (line) { - if (line.widgets) { for (var i = 0; i < line.widgets.length; i++) - { if (line.widgets[i].noHScroll) { regLineChange(this$1, lineNo, "widget"); break } } } - ++lineNo; - }); - this.curOp.forceUpdate = true; - signal(this, "refresh", this); - }), - - operation: function(f){return runInOp(this, f)}, - startOperation: function(){return startOperation(this)}, - endOperation: function(){return endOperation(this)}, - - refresh: methodOp(function() { - var oldHeight = this.display.cachedTextHeight; - regChange(this); - this.curOp.forceUpdate = true; - clearCaches(this); - scrollToCoords(this, this.doc.scrollLeft, this.doc.scrollTop); - updateGutterSpace(this.display); - if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5 || this.options.lineWrapping) - { estimateLineHeights(this); } - signal(this, "refresh", this); - }), - - swapDoc: methodOp(function(doc) { - var old = this.doc; - old.cm = null; - // Cancel the current text selection if any (#5821) - if (this.state.selectingText) { this.state.selectingText(); } - attachDoc(this, doc); - clearCaches(this); - this.display.input.reset(); - scrollToCoords(this, doc.scrollLeft, doc.scrollTop); - this.curOp.forceScroll = true; - signalLater(this, "swapDoc", this, old); - return old - }), - - phrase: function(phraseText) { - var phrases = this.options.phrases; - return phrases && Object.prototype.hasOwnProperty.call(phrases, phraseText) ? phrases[phraseText] : phraseText - }, - - getInputField: function(){return this.display.input.getField()}, - getWrapperElement: function(){return this.display.wrapper}, - getScrollerElement: function(){return this.display.scroller}, - getGutterElement: function(){return this.display.gutters} - }; - eventMixin(CodeMirror); - - CodeMirror.registerHelper = function(type, name, value) { - if (!helpers.hasOwnProperty(type)) { helpers[type] = CodeMirror[type] = {_global: []}; } - helpers[type][name] = value; - }; - CodeMirror.registerGlobalHelper = function(type, name, predicate, value) { - CodeMirror.registerHelper(type, name, value); - helpers[type]._global.push({pred: predicate, val: value}); - }; - } - - // Used for horizontal relative motion. Dir is -1 or 1 (left or - // right), unit can be "char", "column" (like char, but doesn't - // cross line boundaries), "word" (across next word), or "group" (to - // the start of next group of word or non-word-non-whitespace - // chars). The visually param controls whether, in right-to-left - // text, direction 1 means to move towards the next index in the - // string, or towards the character to the right of the current - // position. The resulting position will have a hitSide=true - // property if it reached the end of the document. - function findPosH(doc, pos, dir, unit, visually) { - var oldPos = pos; - var origDir = dir; - var lineObj = getLine(doc, pos.line); - var lineDir = visually && doc.direction == "rtl" ? -dir : dir; - function findNextLine() { - var l = pos.line + lineDir; - if (l < doc.first || l >= doc.first + doc.size) { return false } - pos = new Pos(l, pos.ch, pos.sticky); - return lineObj = getLine(doc, l) - } - function moveOnce(boundToLine) { - var next; - if (visually) { - next = moveVisually(doc.cm, lineObj, pos, dir); - } else { - next = moveLogically(lineObj, pos, dir); - } - if (next == null) { - if (!boundToLine && findNextLine()) - { pos = endOfLine(visually, doc.cm, lineObj, pos.line, lineDir); } - else - { return false } - } else { - pos = next; - } - return true - } - - if (unit == "char") { - moveOnce(); - } else if (unit == "column") { - moveOnce(true); - } else if (unit == "word" || unit == "group") { - var sawType = null, group = unit == "group"; - var helper = doc.cm && doc.cm.getHelper(pos, "wordChars"); - for (var first = true;; first = false) { - if (dir < 0 && !moveOnce(!first)) { break } - var cur = lineObj.text.charAt(pos.ch) || "\n"; - var type = isWordChar(cur, helper) ? "w" - : group && cur == "\n" ? "n" - : !group || /\s/.test(cur) ? null - : "p"; - if (group && !first && !type) { type = "s"; } - if (sawType && sawType != type) { - if (dir < 0) {dir = 1; moveOnce(); pos.sticky = "after";} - break - } - - if (type) { sawType = type; } - if (dir > 0 && !moveOnce(!first)) { break } - } - } - var result = skipAtomic(doc, pos, oldPos, origDir, true); - if (equalCursorPos(oldPos, result)) { result.hitSide = true; } - return result - } - - // For relative vertical movement. Dir may be -1 or 1. Unit can be - // "page" or "line". The resulting position will have a hitSide=true - // property if it reached the end of the document. - function findPosV(cm, pos, dir, unit) { - var doc = cm.doc, x = pos.left, y; - if (unit == "page") { - var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight); - var moveAmount = Math.max(pageSize - .5 * textHeight(cm.display), 3); - y = (dir > 0 ? pos.bottom : pos.top) + dir * moveAmount; - - } else if (unit == "line") { - y = dir > 0 ? pos.bottom + 3 : pos.top - 3; - } - var target; - for (;;) { - target = coordsChar(cm, x, y); - if (!target.outside) { break } - if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break } - y += dir * 5; - } - return target - } - - // CONTENTEDITABLE INPUT STYLE - - var ContentEditableInput = function(cm) { - this.cm = cm; - this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null; - this.polling = new Delayed(); - this.composing = null; - this.gracePeriod = false; - this.readDOMTimeout = null; - }; - - ContentEditableInput.prototype.init = function (display) { - var this$1 = this; - - var input = this, cm = input.cm; - var div = input.div = display.lineDiv; - disableBrowserMagic(div, cm.options.spellcheck, cm.options.autocorrect, cm.options.autocapitalize); - - function belongsToInput(e) { - for (var t = e.target; t; t = t.parentNode) { - if (t == div) { return true } - if (/\bCodeMirror-(?:line)?widget\b/.test(t.className)) { break } - } - return false - } - - on(div, "paste", function (e) { - if (!belongsToInput(e) || signalDOMEvent(cm, e) || handlePaste(e, cm)) { return } - // IE doesn't fire input events, so we schedule a read for the pasted content in this way - if (ie_version <= 11) { setTimeout(operation(cm, function () { return this$1.updateFromDOM(); }), 20); } - }); - - on(div, "compositionstart", function (e) { - this$1.composing = {data: e.data, done: false}; - }); - on(div, "compositionupdate", function (e) { - if (!this$1.composing) { this$1.composing = {data: e.data, done: false}; } - }); - on(div, "compositionend", function (e) { - if (this$1.composing) { - if (e.data != this$1.composing.data) { this$1.readFromDOMSoon(); } - this$1.composing.done = true; - } - }); - - on(div, "touchstart", function () { return input.forceCompositionEnd(); }); - - on(div, "input", function () { - if (!this$1.composing) { this$1.readFromDOMSoon(); } - }); - - function onCopyCut(e) { - if (!belongsToInput(e) || signalDOMEvent(cm, e)) { return } - if (cm.somethingSelected()) { - setLastCopied({lineWise: false, text: cm.getSelections()}); - if (e.type == "cut") { cm.replaceSelection("", null, "cut"); } - } else if (!cm.options.lineWiseCopyCut) { - return - } else { - var ranges = copyableRanges(cm); - setLastCopied({lineWise: true, text: ranges.text}); - if (e.type == "cut") { - cm.operation(function () { - cm.setSelections(ranges.ranges, 0, sel_dontScroll); - cm.replaceSelection("", null, "cut"); - }); - } - } - if (e.clipboardData) { - e.clipboardData.clearData(); - var content = lastCopied.text.join("\n"); - // iOS exposes the clipboard API, but seems to discard content inserted into it - e.clipboardData.setData("Text", content); - if (e.clipboardData.getData("Text") == content) { - e.preventDefault(); - return - } - } - // Old-fashioned briefly-focus-a-textarea hack - var kludge = hiddenTextarea(), te = kludge.firstChild; - cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild); - te.value = lastCopied.text.join("\n"); - var hadFocus = document.activeElement; - selectInput(te); - setTimeout(function () { - cm.display.lineSpace.removeChild(kludge); - hadFocus.focus(); - if (hadFocus == div) { input.showPrimarySelection(); } - }, 50); - } - on(div, "copy", onCopyCut); - on(div, "cut", onCopyCut); - }; - - ContentEditableInput.prototype.screenReaderLabelChanged = function (label) { - // Label for screenreaders, accessibility - if(label) { - this.div.setAttribute('aria-label', label); - } else { - this.div.removeAttribute('aria-label'); - } - }; - - ContentEditableInput.prototype.prepareSelection = function () { - var result = prepareSelection(this.cm, false); - result.focus = document.activeElement == this.div; - return result - }; - - ContentEditableInput.prototype.showSelection = function (info, takeFocus) { - if (!info || !this.cm.display.view.length) { return } - if (info.focus || takeFocus) { this.showPrimarySelection(); } - this.showMultipleSelections(info); - }; - - ContentEditableInput.prototype.getSelection = function () { - return this.cm.display.wrapper.ownerDocument.getSelection() - }; - - ContentEditableInput.prototype.showPrimarySelection = function () { - var sel = this.getSelection(), cm = this.cm, prim = cm.doc.sel.primary(); - var from = prim.from(), to = prim.to(); - - if (cm.display.viewTo == cm.display.viewFrom || from.line >= cm.display.viewTo || to.line < cm.display.viewFrom) { - sel.removeAllRanges(); - return - } - - var curAnchor = domToPos(cm, sel.anchorNode, sel.anchorOffset); - var curFocus = domToPos(cm, sel.focusNode, sel.focusOffset); - if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad && - cmp(minPos(curAnchor, curFocus), from) == 0 && - cmp(maxPos(curAnchor, curFocus), to) == 0) - { return } - - var view = cm.display.view; - var start = (from.line >= cm.display.viewFrom && posToDOM(cm, from)) || - {node: view[0].measure.map[2], offset: 0}; - var end = to.line < cm.display.viewTo && posToDOM(cm, to); - if (!end) { - var measure = view[view.length - 1].measure; - var map = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map; - end = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]}; - } - - if (!start || !end) { - sel.removeAllRanges(); - return - } - - var old = sel.rangeCount && sel.getRangeAt(0), rng; - try { rng = range(start.node, start.offset, end.offset, end.node); } - catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible - if (rng) { - if (!gecko && cm.state.focused) { - sel.collapse(start.node, start.offset); - if (!rng.collapsed) { - sel.removeAllRanges(); - sel.addRange(rng); - } - } else { - sel.removeAllRanges(); - sel.addRange(rng); - } - if (old && sel.anchorNode == null) { sel.addRange(old); } - else if (gecko) { this.startGracePeriod(); } - } - this.rememberSelection(); - }; - - ContentEditableInput.prototype.startGracePeriod = function () { - var this$1 = this; - - clearTimeout(this.gracePeriod); - this.gracePeriod = setTimeout(function () { - this$1.gracePeriod = false; - if (this$1.selectionChanged()) - { this$1.cm.operation(function () { return this$1.cm.curOp.selectionChanged = true; }); } - }, 20); - }; - - ContentEditableInput.prototype.showMultipleSelections = function (info) { - removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors); - removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection); - }; - - ContentEditableInput.prototype.rememberSelection = function () { - var sel = this.getSelection(); - this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset; - this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset; - }; - - ContentEditableInput.prototype.selectionInEditor = function () { - var sel = this.getSelection(); - if (!sel.rangeCount) { return false } - var node = sel.getRangeAt(0).commonAncestorContainer; - return contains(this.div, node) - }; - - ContentEditableInput.prototype.focus = function () { - if (this.cm.options.readOnly != "nocursor") { - if (!this.selectionInEditor() || document.activeElement != this.div) - { this.showSelection(this.prepareSelection(), true); } - this.div.focus(); - } - }; - ContentEditableInput.prototype.blur = function () { this.div.blur(); }; - ContentEditableInput.prototype.getField = function () { return this.div }; - - ContentEditableInput.prototype.supportsTouch = function () { return true }; - - ContentEditableInput.prototype.receivedFocus = function () { - var input = this; - if (this.selectionInEditor()) - { this.pollSelection(); } - else - { runInOp(this.cm, function () { return input.cm.curOp.selectionChanged = true; }); } - - function poll() { - if (input.cm.state.focused) { - input.pollSelection(); - input.polling.set(input.cm.options.pollInterval, poll); - } - } - this.polling.set(this.cm.options.pollInterval, poll); - }; - - ContentEditableInput.prototype.selectionChanged = function () { - var sel = this.getSelection(); - return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset || - sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset - }; - - ContentEditableInput.prototype.pollSelection = function () { - if (this.readDOMTimeout != null || this.gracePeriod || !this.selectionChanged()) { return } - var sel = this.getSelection(), cm = this.cm; - // On Android Chrome (version 56, at least), backspacing into an - // uneditable block element will put the cursor in that element, - // and then, because it's not editable, hide the virtual keyboard. - // Because Android doesn't allow us to actually detect backspace - // presses in a sane way, this code checks for when that happens - // and simulates a backspace press in this case. - if (android && chrome && this.cm.display.gutterSpecs.length && isInGutter(sel.anchorNode)) { - this.cm.triggerOnKeyDown({type: "keydown", keyCode: 8, preventDefault: Math.abs}); - this.blur(); - this.focus(); - return - } - if (this.composing) { return } - this.rememberSelection(); - var anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset); - var head = domToPos(cm, sel.focusNode, sel.focusOffset); - if (anchor && head) { runInOp(cm, function () { - setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll); - if (anchor.bad || head.bad) { cm.curOp.selectionChanged = true; } - }); } - }; - - ContentEditableInput.prototype.pollContent = function () { - if (this.readDOMTimeout != null) { - clearTimeout(this.readDOMTimeout); - this.readDOMTimeout = null; - } - - var cm = this.cm, display = cm.display, sel = cm.doc.sel.primary(); - var from = sel.from(), to = sel.to(); - if (from.ch == 0 && from.line > cm.firstLine()) - { from = Pos(from.line - 1, getLine(cm.doc, from.line - 1).length); } - if (to.ch == getLine(cm.doc, to.line).text.length && to.line < cm.lastLine()) - { to = Pos(to.line + 1, 0); } - if (from.line < display.viewFrom || to.line > display.viewTo - 1) { return false } - - var fromIndex, fromLine, fromNode; - if (from.line == display.viewFrom || (fromIndex = findViewIndex(cm, from.line)) == 0) { - fromLine = lineNo(display.view[0].line); - fromNode = display.view[0].node; - } else { - fromLine = lineNo(display.view[fromIndex].line); - fromNode = display.view[fromIndex - 1].node.nextSibling; - } - var toIndex = findViewIndex(cm, to.line); - var toLine, toNode; - if (toIndex == display.view.length - 1) { - toLine = display.viewTo - 1; - toNode = display.lineDiv.lastChild; - } else { - toLine = lineNo(display.view[toIndex + 1].line) - 1; - toNode = display.view[toIndex + 1].node.previousSibling; - } - - if (!fromNode) { return false } - var newText = cm.doc.splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine)); - var oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length)); - while (newText.length > 1 && oldText.length > 1) { - if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine--; } - else if (newText[0] == oldText[0]) { newText.shift(); oldText.shift(); fromLine++; } - else { break } - } - - var cutFront = 0, cutEnd = 0; - var newTop = newText[0], oldTop = oldText[0], maxCutFront = Math.min(newTop.length, oldTop.length); - while (cutFront < maxCutFront && newTop.charCodeAt(cutFront) == oldTop.charCodeAt(cutFront)) - { ++cutFront; } - var newBot = lst(newText), oldBot = lst(oldText); - var maxCutEnd = Math.min(newBot.length - (newText.length == 1 ? cutFront : 0), - oldBot.length - (oldText.length == 1 ? cutFront : 0)); - while (cutEnd < maxCutEnd && - newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) - { ++cutEnd; } - // Try to move start of change to start of selection if ambiguous - if (newText.length == 1 && oldText.length == 1 && fromLine == from.line) { - while (cutFront && cutFront > from.ch && - newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) { - cutFront--; - cutEnd++; - } - } - - newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd).replace(/^\u200b+/, ""); - newText[0] = newText[0].slice(cutFront).replace(/\u200b+$/, ""); - - var chFrom = Pos(fromLine, cutFront); - var chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0); - if (newText.length > 1 || newText[0] || cmp(chFrom, chTo)) { - replaceRange(cm.doc, newText, chFrom, chTo, "+input"); - return true - } - }; - - ContentEditableInput.prototype.ensurePolled = function () { - this.forceCompositionEnd(); - }; - ContentEditableInput.prototype.reset = function () { - this.forceCompositionEnd(); - }; - ContentEditableInput.prototype.forceCompositionEnd = function () { - if (!this.composing) { return } - clearTimeout(this.readDOMTimeout); - this.composing = null; - this.updateFromDOM(); - this.div.blur(); - this.div.focus(); - }; - ContentEditableInput.prototype.readFromDOMSoon = function () { - var this$1 = this; - - if (this.readDOMTimeout != null) { return } - this.readDOMTimeout = setTimeout(function () { - this$1.readDOMTimeout = null; - if (this$1.composing) { - if (this$1.composing.done) { this$1.composing = null; } - else { return } - } - this$1.updateFromDOM(); - }, 80); - }; - - ContentEditableInput.prototype.updateFromDOM = function () { - var this$1 = this; - - if (this.cm.isReadOnly() || !this.pollContent()) - { runInOp(this.cm, function () { return regChange(this$1.cm); }); } - }; - - ContentEditableInput.prototype.setUneditable = function (node) { - node.contentEditable = "false"; - }; - - ContentEditableInput.prototype.onKeyPress = function (e) { - if (e.charCode == 0 || this.composing) { return } - e.preventDefault(); - if (!this.cm.isReadOnly()) - { operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0); } - }; - - ContentEditableInput.prototype.readOnlyChanged = function (val) { - this.div.contentEditable = String(val != "nocursor"); - }; - - ContentEditableInput.prototype.onContextMenu = function () {}; - ContentEditableInput.prototype.resetPosition = function () {}; - - ContentEditableInput.prototype.needsContentAttribute = true; - - function posToDOM(cm, pos) { - var view = findViewForLine(cm, pos.line); - if (!view || view.hidden) { return null } - var line = getLine(cm.doc, pos.line); - var info = mapFromLineView(view, line, pos.line); - - var order = getOrder(line, cm.doc.direction), side = "left"; - if (order) { - var partPos = getBidiPartAt(order, pos.ch); - side = partPos % 2 ? "right" : "left"; - } - var result = nodeAndOffsetInLineMap(info.map, pos.ch, side); - result.offset = result.collapse == "right" ? result.end : result.start; - return result - } - - function isInGutter(node) { - for (var scan = node; scan; scan = scan.parentNode) - { if (/CodeMirror-gutter-wrapper/.test(scan.className)) { return true } } - return false - } - - function badPos(pos, bad) { if (bad) { pos.bad = true; } return pos } - - function domTextBetween(cm, from, to, fromLine, toLine) { - var text = "", closing = false, lineSep = cm.doc.lineSeparator(), extraLinebreak = false; - function recognizeMarker(id) { return function (marker) { return marker.id == id; } } - function close() { - if (closing) { - text += lineSep; - if (extraLinebreak) { text += lineSep; } - closing = extraLinebreak = false; - } - } - function addText(str) { - if (str) { - close(); - text += str; - } - } - function walk(node) { - if (node.nodeType == 1) { - var cmText = node.getAttribute("cm-text"); - if (cmText) { - addText(cmText); - return - } - var markerID = node.getAttribute("cm-marker"), range; - if (markerID) { - var found = cm.findMarks(Pos(fromLine, 0), Pos(toLine + 1, 0), recognizeMarker(+markerID)); - if (found.length && (range = found[0].find(0))) - { addText(getBetween(cm.doc, range.from, range.to).join(lineSep)); } - return - } - if (node.getAttribute("contenteditable") == "false") { return } - var isBlock = /^(pre|div|p|li|table|br)$/i.test(node.nodeName); - if (!/^br$/i.test(node.nodeName) && node.textContent.length == 0) { return } - - if (isBlock) { close(); } - for (var i = 0; i < node.childNodes.length; i++) - { walk(node.childNodes[i]); } - - if (/^(pre|p)$/i.test(node.nodeName)) { extraLinebreak = true; } - if (isBlock) { closing = true; } - } else if (node.nodeType == 3) { - addText(node.nodeValue.replace(/\u200b/g, "").replace(/\u00a0/g, " ")); - } - } - for (;;) { - walk(from); - if (from == to) { break } - from = from.nextSibling; - extraLinebreak = false; - } - return text - } - - function domToPos(cm, node, offset) { - var lineNode; - if (node == cm.display.lineDiv) { - lineNode = cm.display.lineDiv.childNodes[offset]; - if (!lineNode) { return badPos(cm.clipPos(Pos(cm.display.viewTo - 1)), true) } - node = null; offset = 0; - } else { - for (lineNode = node;; lineNode = lineNode.parentNode) { - if (!lineNode || lineNode == cm.display.lineDiv) { return null } - if (lineNode.parentNode && lineNode.parentNode == cm.display.lineDiv) { break } - } - } - for (var i = 0; i < cm.display.view.length; i++) { - var lineView = cm.display.view[i]; - if (lineView.node == lineNode) - { return locateNodeInLineView(lineView, node, offset) } - } - } - - function locateNodeInLineView(lineView, node, offset) { - var wrapper = lineView.text.firstChild, bad = false; - if (!node || !contains(wrapper, node)) { return badPos(Pos(lineNo(lineView.line), 0), true) } - if (node == wrapper) { - bad = true; - node = wrapper.childNodes[offset]; - offset = 0; - if (!node) { - var line = lineView.rest ? lst(lineView.rest) : lineView.line; - return badPos(Pos(lineNo(line), line.text.length), bad) - } - } - - var textNode = node.nodeType == 3 ? node : null, topNode = node; - if (!textNode && node.childNodes.length == 1 && node.firstChild.nodeType == 3) { - textNode = node.firstChild; - if (offset) { offset = textNode.nodeValue.length; } - } - while (topNode.parentNode != wrapper) { topNode = topNode.parentNode; } - var measure = lineView.measure, maps = measure.maps; - - function find(textNode, topNode, offset) { - for (var i = -1; i < (maps ? maps.length : 0); i++) { - var map = i < 0 ? measure.map : maps[i]; - for (var j = 0; j < map.length; j += 3) { - var curNode = map[j + 2]; - if (curNode == textNode || curNode == topNode) { - var line = lineNo(i < 0 ? lineView.line : lineView.rest[i]); - var ch = map[j] + offset; - if (offset < 0 || curNode != textNode) { ch = map[j + (offset ? 1 : 0)]; } - return Pos(line, ch) - } - } - } - } - var found = find(textNode, topNode, offset); - if (found) { return badPos(found, bad) } - - // FIXME this is all really shaky. might handle the few cases it needs to handle, but likely to cause problems - for (var after = topNode.nextSibling, dist = textNode ? textNode.nodeValue.length - offset : 0; after; after = after.nextSibling) { - found = find(after, after.firstChild, 0); - if (found) - { return badPos(Pos(found.line, found.ch - dist), bad) } - else - { dist += after.textContent.length; } - } - for (var before = topNode.previousSibling, dist$1 = offset; before; before = before.previousSibling) { - found = find(before, before.firstChild, -1); - if (found) - { return badPos(Pos(found.line, found.ch + dist$1), bad) } - else - { dist$1 += before.textContent.length; } - } - } - - // TEXTAREA INPUT STYLE - - var TextareaInput = function(cm) { - this.cm = cm; - // See input.poll and input.reset - this.prevInput = ""; - - // Flag that indicates whether we expect input to appear real soon - // now (after some event like 'keypress' or 'input') and are - // polling intensively. - this.pollingFast = false; - // Self-resetting timeout for the poller - this.polling = new Delayed(); - // Used to work around IE issue with selection being forgotten when focus moves away from textarea - this.hasSelection = false; - this.composing = null; - }; - - TextareaInput.prototype.init = function (display) { - var this$1 = this; - - var input = this, cm = this.cm; - this.createField(display); - var te = this.textarea; - - display.wrapper.insertBefore(this.wrapper, display.wrapper.firstChild); - - // Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore) - if (ios) { te.style.width = "0px"; } - - on(te, "input", function () { - if (ie && ie_version >= 9 && this$1.hasSelection) { this$1.hasSelection = null; } - input.poll(); - }); - - on(te, "paste", function (e) { - if (signalDOMEvent(cm, e) || handlePaste(e, cm)) { return } - - cm.state.pasteIncoming = +new Date; - input.fastPoll(); - }); - - function prepareCopyCut(e) { - if (signalDOMEvent(cm, e)) { return } - if (cm.somethingSelected()) { - setLastCopied({lineWise: false, text: cm.getSelections()}); - } else if (!cm.options.lineWiseCopyCut) { - return - } else { - var ranges = copyableRanges(cm); - setLastCopied({lineWise: true, text: ranges.text}); - if (e.type == "cut") { - cm.setSelections(ranges.ranges, null, sel_dontScroll); - } else { - input.prevInput = ""; - te.value = ranges.text.join("\n"); - selectInput(te); - } - } - if (e.type == "cut") { cm.state.cutIncoming = +new Date; } - } - on(te, "cut", prepareCopyCut); - on(te, "copy", prepareCopyCut); - - on(display.scroller, "paste", function (e) { - if (eventInWidget(display, e) || signalDOMEvent(cm, e)) { return } - if (!te.dispatchEvent) { - cm.state.pasteIncoming = +new Date; - input.focus(); - return - } - - // Pass the `paste` event to the textarea so it's handled by its event listener. - var event = new Event("paste"); - event.clipboardData = e.clipboardData; - te.dispatchEvent(event); - }); - - // Prevent normal selection in the editor (we handle our own) - on(display.lineSpace, "selectstart", function (e) { - if (!eventInWidget(display, e)) { e_preventDefault(e); } - }); - - on(te, "compositionstart", function () { - var start = cm.getCursor("from"); - if (input.composing) { input.composing.range.clear(); } - input.composing = { - start: start, - range: cm.markText(start, cm.getCursor("to"), {className: "CodeMirror-composing"}) - }; - }); - on(te, "compositionend", function () { - if (input.composing) { - input.poll(); - input.composing.range.clear(); - input.composing = null; - } - }); - }; - - TextareaInput.prototype.createField = function (_display) { - // Wraps and hides input textarea - this.wrapper = hiddenTextarea(); - // The semihidden textarea that is focused when the editor is - // focused, and receives input. - this.textarea = this.wrapper.firstChild; - }; - - TextareaInput.prototype.screenReaderLabelChanged = function (label) { - // Label for screenreaders, accessibility - if(label) { - this.textarea.setAttribute('aria-label', label); - } else { - this.textarea.removeAttribute('aria-label'); - } - }; - - TextareaInput.prototype.prepareSelection = function () { - // Redraw the selection and/or cursor - var cm = this.cm, display = cm.display, doc = cm.doc; - var result = prepareSelection(cm); - - // Move the hidden textarea near the cursor to prevent scrolling artifacts - if (cm.options.moveInputWithCursor) { - var headPos = cursorCoords(cm, doc.sel.primary().head, "div"); - var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect(); - result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10, - headPos.top + lineOff.top - wrapOff.top)); - result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10, - headPos.left + lineOff.left - wrapOff.left)); - } - - return result - }; - - TextareaInput.prototype.showSelection = function (drawn) { - var cm = this.cm, display = cm.display; - removeChildrenAndAdd(display.cursorDiv, drawn.cursors); - removeChildrenAndAdd(display.selectionDiv, drawn.selection); - if (drawn.teTop != null) { - this.wrapper.style.top = drawn.teTop + "px"; - this.wrapper.style.left = drawn.teLeft + "px"; - } - }; - - // Reset the input to correspond to the selection (or to be empty, - // when not typing and nothing is selected) - TextareaInput.prototype.reset = function (typing) { - if (this.contextMenuPending || this.composing) { return } - var cm = this.cm; - if (cm.somethingSelected()) { - this.prevInput = ""; - var content = cm.getSelection(); - this.textarea.value = content; - if (cm.state.focused) { selectInput(this.textarea); } - if (ie && ie_version >= 9) { this.hasSelection = content; } - } else if (!typing) { - this.prevInput = this.textarea.value = ""; - if (ie && ie_version >= 9) { this.hasSelection = null; } - } - }; - - TextareaInput.prototype.getField = function () { return this.textarea }; - - TextareaInput.prototype.supportsTouch = function () { return false }; - - TextareaInput.prototype.focus = function () { - if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) { - try { this.textarea.focus(); } - catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM - } - }; - - TextareaInput.prototype.blur = function () { this.textarea.blur(); }; - - TextareaInput.prototype.resetPosition = function () { - this.wrapper.style.top = this.wrapper.style.left = 0; - }; - - TextareaInput.prototype.receivedFocus = function () { this.slowPoll(); }; - - // Poll for input changes, using the normal rate of polling. This - // runs as long as the editor is focused. - TextareaInput.prototype.slowPoll = function () { - var this$1 = this; - - if (this.pollingFast) { return } - this.polling.set(this.cm.options.pollInterval, function () { - this$1.poll(); - if (this$1.cm.state.focused) { this$1.slowPoll(); } - }); - }; - - // When an event has just come in that is likely to add or change - // something in the input textarea, we poll faster, to ensure that - // the change appears on the screen quickly. - TextareaInput.prototype.fastPoll = function () { - var missed = false, input = this; - input.pollingFast = true; - function p() { - var changed = input.poll(); - if (!changed && !missed) {missed = true; input.polling.set(60, p);} - else {input.pollingFast = false; input.slowPoll();} - } - input.polling.set(20, p); - }; - - // Read input from the textarea, and update the document to match. - // When something is selected, it is present in the textarea, and - // selected (unless it is huge, in which case a placeholder is - // used). When nothing is selected, the cursor sits after previously - // seen text (can be empty), which is stored in prevInput (we must - // not reset the textarea when typing, because that breaks IME). - TextareaInput.prototype.poll = function () { - var this$1 = this; - - var cm = this.cm, input = this.textarea, prevInput = this.prevInput; - // Since this is called a *lot*, try to bail out as cheaply as - // possible when it is clear that nothing happened. hasSelection - // will be the case when there is a lot of text in the textarea, - // in which case reading its value would be expensive. - if (this.contextMenuPending || !cm.state.focused || - (hasSelection(input) && !prevInput && !this.composing) || - cm.isReadOnly() || cm.options.disableInput || cm.state.keySeq) - { return false } - - var text = input.value; - // If nothing changed, bail. - if (text == prevInput && !cm.somethingSelected()) { return false } - // Work around nonsensical selection resetting in IE9/10, and - // inexplicable appearance of private area unicode characters on - // some key combos in Mac (#2689). - if (ie && ie_version >= 9 && this.hasSelection === text || - mac && /[\uf700-\uf7ff]/.test(text)) { - cm.display.input.reset(); - return false - } - - if (cm.doc.sel == cm.display.selForContextMenu) { - var first = text.charCodeAt(0); - if (first == 0x200b && !prevInput) { prevInput = "\u200b"; } - if (first == 0x21da) { this.reset(); return this.cm.execCommand("undo") } - } - // Find the part of the input that is actually new - var same = 0, l = Math.min(prevInput.length, text.length); - while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) { ++same; } - - runInOp(cm, function () { - applyTextInput(cm, text.slice(same), prevInput.length - same, - null, this$1.composing ? "*compose" : null); - - // Don't leave long text in the textarea, since it makes further polling slow - if (text.length > 1000 || text.indexOf("\n") > -1) { input.value = this$1.prevInput = ""; } - else { this$1.prevInput = text; } - - if (this$1.composing) { - this$1.composing.range.clear(); - this$1.composing.range = cm.markText(this$1.composing.start, cm.getCursor("to"), - {className: "CodeMirror-composing"}); - } - }); - return true - }; - - TextareaInput.prototype.ensurePolled = function () { - if (this.pollingFast && this.poll()) { this.pollingFast = false; } - }; - - TextareaInput.prototype.onKeyPress = function () { - if (ie && ie_version >= 9) { this.hasSelection = null; } - this.fastPoll(); - }; - - TextareaInput.prototype.onContextMenu = function (e) { - var input = this, cm = input.cm, display = cm.display, te = input.textarea; - if (input.contextMenuPending) { input.contextMenuPending(); } - var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop; - if (!pos || presto) { return } // Opera is difficult. - - // Reset the current text selection only if the click is done outside of the selection - // and 'resetSelectionOnContextMenu' option is true. - var reset = cm.options.resetSelectionOnContextMenu; - if (reset && cm.doc.sel.contains(pos) == -1) - { operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll); } - - var oldCSS = te.style.cssText, oldWrapperCSS = input.wrapper.style.cssText; - var wrapperBox = input.wrapper.offsetParent.getBoundingClientRect(); - input.wrapper.style.cssText = "position: static"; - te.style.cssText = "position: absolute; width: 30px; height: 30px;\n top: " + (e.clientY - wrapperBox.top - 5) + "px; left: " + (e.clientX - wrapperBox.left - 5) + "px;\n z-index: 1000; background: " + (ie ? "rgba(255, 255, 255, .05)" : "transparent") + ";\n outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; - var oldScrollY; - if (webkit) { oldScrollY = window.scrollY; } // Work around Chrome issue (#2712) - display.input.focus(); - if (webkit) { window.scrollTo(null, oldScrollY); } - display.input.reset(); - // Adds "Select all" to context menu in FF - if (!cm.somethingSelected()) { te.value = input.prevInput = " "; } - input.contextMenuPending = rehide; - display.selForContextMenu = cm.doc.sel; - clearTimeout(display.detectingSelectAll); - - // Select-all will be greyed out if there's nothing to select, so - // this adds a zero-width space so that we can later check whether - // it got selected. - function prepareSelectAllHack() { - if (te.selectionStart != null) { - var selected = cm.somethingSelected(); - var extval = "\u200b" + (selected ? te.value : ""); - te.value = "\u21da"; // Used to catch context-menu undo - te.value = extval; - input.prevInput = selected ? "" : "\u200b"; - te.selectionStart = 1; te.selectionEnd = extval.length; - // Re-set this, in case some other handler touched the - // selection in the meantime. - display.selForContextMenu = cm.doc.sel; - } - } - function rehide() { - if (input.contextMenuPending != rehide) { return } - input.contextMenuPending = false; - input.wrapper.style.cssText = oldWrapperCSS; - te.style.cssText = oldCSS; - if (ie && ie_version < 9) { display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos); } - - // Try to detect the user choosing select-all - if (te.selectionStart != null) { - if (!ie || (ie && ie_version < 9)) { prepareSelectAllHack(); } - var i = 0, poll = function () { - if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 && - te.selectionEnd > 0 && input.prevInput == "\u200b") { - operation(cm, selectAll)(cm); - } else if (i++ < 10) { - display.detectingSelectAll = setTimeout(poll, 500); - } else { - display.selForContextMenu = null; - display.input.reset(); - } - }; - display.detectingSelectAll = setTimeout(poll, 200); - } - } - - if (ie && ie_version >= 9) { prepareSelectAllHack(); } - if (captureRightClick) { - e_stop(e); - var mouseup = function () { - off(window, "mouseup", mouseup); - setTimeout(rehide, 20); - }; - on(window, "mouseup", mouseup); - } else { - setTimeout(rehide, 50); - } - }; - - TextareaInput.prototype.readOnlyChanged = function (val) { - if (!val) { this.reset(); } - this.textarea.disabled = val == "nocursor"; - }; - - TextareaInput.prototype.setUneditable = function () {}; - - TextareaInput.prototype.needsContentAttribute = false; - - function fromTextArea(textarea, options) { - options = options ? copyObj(options) : {}; - options.value = textarea.value; - if (!options.tabindex && textarea.tabIndex) - { options.tabindex = textarea.tabIndex; } - if (!options.placeholder && textarea.placeholder) - { options.placeholder = textarea.placeholder; } - // Set autofocus to true if this textarea is focused, or if it has - // autofocus and no other element is focused. - if (options.autofocus == null) { - var hasFocus = activeElt(); - options.autofocus = hasFocus == textarea || - textarea.getAttribute("autofocus") != null && hasFocus == document.body; - } - - function save() {textarea.value = cm.getValue();} - - var realSubmit; - if (textarea.form) { - on(textarea.form, "submit", save); - // Deplorable hack to make the submit method do the right thing. - if (!options.leaveSubmitMethodAlone) { - var form = textarea.form; - realSubmit = form.submit; - try { - var wrappedSubmit = form.submit = function () { - save(); - form.submit = realSubmit; - form.submit(); - form.submit = wrappedSubmit; - }; - } catch(e) {} - } - } - - options.finishInit = function (cm) { - cm.save = save; - cm.getTextArea = function () { return textarea; }; - cm.toTextArea = function () { - cm.toTextArea = isNaN; // Prevent this from being ran twice - save(); - textarea.parentNode.removeChild(cm.getWrapperElement()); - textarea.style.display = ""; - if (textarea.form) { - off(textarea.form, "submit", save); - if (!options.leaveSubmitMethodAlone && typeof textarea.form.submit == "function") - { textarea.form.submit = realSubmit; } - } - }; - }; - - textarea.style.display = "none"; - var cm = CodeMirror(function (node) { return textarea.parentNode.insertBefore(node, textarea.nextSibling); }, - options); - return cm - } - - function addLegacyProps(CodeMirror) { - CodeMirror.off = off; - CodeMirror.on = on; - CodeMirror.wheelEventPixels = wheelEventPixels; - CodeMirror.Doc = Doc; - CodeMirror.splitLines = splitLinesAuto; - CodeMirror.countColumn = countColumn; - CodeMirror.findColumn = findColumn; - CodeMirror.isWordChar = isWordCharBasic; - CodeMirror.Pass = Pass; - CodeMirror.signal = signal; - CodeMirror.Line = Line; - CodeMirror.changeEnd = changeEnd; - CodeMirror.scrollbarModel = scrollbarModel; - CodeMirror.Pos = Pos; - CodeMirror.cmpPos = cmp; - CodeMirror.modes = modes; - CodeMirror.mimeModes = mimeModes; - CodeMirror.resolveMode = resolveMode; - CodeMirror.getMode = getMode; - CodeMirror.modeExtensions = modeExtensions; - CodeMirror.extendMode = extendMode; - CodeMirror.copyState = copyState; - CodeMirror.startState = startState; - CodeMirror.innerMode = innerMode; - CodeMirror.commands = commands; - CodeMirror.keyMap = keyMap; - CodeMirror.keyName = keyName; - CodeMirror.isModifierKey = isModifierKey; - CodeMirror.lookupKey = lookupKey; - CodeMirror.normalizeKeyMap = normalizeKeyMap; - CodeMirror.StringStream = StringStream; - CodeMirror.SharedTextMarker = SharedTextMarker; - CodeMirror.TextMarker = TextMarker; - CodeMirror.LineWidget = LineWidget; - CodeMirror.e_preventDefault = e_preventDefault; - CodeMirror.e_stopPropagation = e_stopPropagation; - CodeMirror.e_stop = e_stop; - CodeMirror.addClass = addClass; - CodeMirror.contains = contains; - CodeMirror.rmClass = rmClass; - CodeMirror.keyNames = keyNames; - } - - // EDITOR CONSTRUCTOR - - defineOptions(CodeMirror); - - addEditorMethods(CodeMirror); - - // Set up methods on CodeMirror's prototype to redirect to the editor's document. - var dontDelegate = "iter insert remove copy getEditor constructor".split(" "); - for (var prop in Doc.prototype) { if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0) - { CodeMirror.prototype[prop] = (function(method) { - return function() {return method.apply(this.doc, arguments)} - })(Doc.prototype[prop]); } } - - eventMixin(Doc); - CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput}; - - // Extra arguments are stored as the mode's dependencies, which is - // used by (legacy) mechanisms like loadmode.js to automatically - // load a mode. (Preferred mechanism is the require/define calls.) - CodeMirror.defineMode = function(name/*, mode, …*/) { - if (!CodeMirror.defaults.mode && name != "null") { CodeMirror.defaults.mode = name; } - defineMode.apply(this, arguments); - }; - - CodeMirror.defineMIME = defineMIME; - - // Minimal default mode. - CodeMirror.defineMode("null", function () { return ({token: function (stream) { return stream.skipToEnd(); }}); }); - CodeMirror.defineMIME("text/plain", "null"); - - // EXTENSIONS - - CodeMirror.defineExtension = function (name, func) { - CodeMirror.prototype[name] = func; - }; - CodeMirror.defineDocExtension = function (name, func) { - Doc.prototype[name] = func; - }; - - CodeMirror.fromTextArea = fromTextArea; - - addLegacyProps(CodeMirror); - - CodeMirror.version = "5.56.0"; - - return CodeMirror; - -}))); diff --git a/plugins/UiFileManager/media/codemirror/extension/dialog/dialog.css b/plugins/UiFileManager/media/codemirror/extension/dialog/dialog.css deleted file mode 100644 index 677c0783..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/dialog/dialog.css +++ /dev/null @@ -1,32 +0,0 @@ -.CodeMirror-dialog { - position: absolute; - left: 0; right: 0; - background: inherit; - z-index: 15; - padding: .1em .8em; - overflow: hidden; - color: inherit; -} - -.CodeMirror-dialog-top { - border-bottom: 1px solid #eee; - top: 0; -} - -.CodeMirror-dialog-bottom { - border-top: 1px solid #eee; - bottom: 0; -} - -.CodeMirror-dialog input { - border: none; - outline: none; - background: transparent; - width: 20em; - color: inherit; - font-family: monospace; -} - -.CodeMirror-dialog button { - font-size: 70%; -} diff --git a/plugins/UiFileManager/media/codemirror/extension/dialog/dialog.js b/plugins/UiFileManager/media/codemirror/extension/dialog/dialog.js deleted file mode 100644 index 5f1f4aa4..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/dialog/dialog.js +++ /dev/null @@ -1,163 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -// Open simple dialogs on top of an editor. Relies on dialog.css. - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - function dialogDiv(cm, template, bottom) { - var wrap = cm.getWrapperElement(); - var dialog; - dialog = wrap.appendChild(document.createElement("div")); - if (bottom) - dialog.className = "CodeMirror-dialog CodeMirror-dialog-bottom"; - else - dialog.className = "CodeMirror-dialog CodeMirror-dialog-top"; - - if (typeof template == "string") { - dialog.innerHTML = template; - } else { // Assuming it's a detached DOM element. - dialog.appendChild(template); - } - CodeMirror.addClass(wrap, 'dialog-opened'); - return dialog; - } - - function closeNotification(cm, newVal) { - if (cm.state.currentNotificationClose) - cm.state.currentNotificationClose(); - cm.state.currentNotificationClose = newVal; - } - - CodeMirror.defineExtension("openDialog", function(template, callback, options) { - if (!options) options = {}; - - closeNotification(this, null); - - var dialog = dialogDiv(this, template, options.bottom); - var closed = false, me = this; - function close(newVal) { - if (typeof newVal == 'string') { - inp.value = newVal; - } else { - if (closed) return; - closed = true; - CodeMirror.rmClass(dialog.parentNode, 'dialog-opened'); - dialog.parentNode.removeChild(dialog); - me.focus(); - - if (options.onClose) options.onClose(dialog); - } - } - - var inp = dialog.getElementsByTagName("input")[0], button; - if (inp) { - inp.focus(); - - if (options.value) { - inp.value = options.value; - if (options.selectValueOnOpen !== false) { - inp.select(); - } - } - - if (options.onInput) - CodeMirror.on(inp, "input", function(e) { options.onInput(e, inp.value, close);}); - if (options.onKeyUp) - CodeMirror.on(inp, "keyup", function(e) {options.onKeyUp(e, inp.value, close);}); - - CodeMirror.on(inp, "keydown", function(e) { - if (options && options.onKeyDown && options.onKeyDown(e, inp.value, close)) { return; } - if (e.keyCode == 27 || (options.closeOnEnter !== false && e.keyCode == 13)) { - inp.blur(); - CodeMirror.e_stop(e); - close(); - } - if (e.keyCode == 13) callback(inp.value, e); - }); - - if (options.closeOnBlur !== false) CodeMirror.on(dialog, "focusout", function (evt) { - if (evt.relatedTarget !== null) close(); - }); - } else if (button = dialog.getElementsByTagName("button")[0]) { - CodeMirror.on(button, "click", function() { - close(); - me.focus(); - }); - - if (options.closeOnBlur !== false) CodeMirror.on(button, "blur", close); - - button.focus(); - } - return close; - }); - - CodeMirror.defineExtension("openConfirm", function(template, callbacks, options) { - closeNotification(this, null); - var dialog = dialogDiv(this, template, options && options.bottom); - var buttons = dialog.getElementsByTagName("button"); - var closed = false, me = this, blurring = 1; - function close() { - if (closed) return; - closed = true; - CodeMirror.rmClass(dialog.parentNode, 'dialog-opened'); - dialog.parentNode.removeChild(dialog); - me.focus(); - } - buttons[0].focus(); - for (var i = 0; i < buttons.length; ++i) { - var b = buttons[i]; - (function(callback) { - CodeMirror.on(b, "click", function(e) { - CodeMirror.e_preventDefault(e); - close(); - if (callback) callback(me); - }); - })(callbacks[i]); - CodeMirror.on(b, "blur", function() { - --blurring; - setTimeout(function() { if (blurring <= 0) close(); }, 200); - }); - CodeMirror.on(b, "focus", function() { ++blurring; }); - } - }); - - /* - * openNotification - * Opens a notification, that can be closed with an optional timer - * (default 5000ms timer) and always closes on click. - * - * If a notification is opened while another is opened, it will close the - * currently opened one and open the new one immediately. - */ - CodeMirror.defineExtension("openNotification", function(template, options) { - closeNotification(this, close); - var dialog = dialogDiv(this, template, options && options.bottom); - var closed = false, doneTimer; - var duration = options && typeof options.duration !== "undefined" ? options.duration : 5000; - - function close() { - if (closed) return; - closed = true; - clearTimeout(doneTimer); - CodeMirror.rmClass(dialog.parentNode, 'dialog-opened'); - dialog.parentNode.removeChild(dialog); - } - - CodeMirror.on(dialog, 'click', function(e) { - CodeMirror.e_preventDefault(e); - close(); - }); - - if (duration) - doneTimer = setTimeout(close, duration); - - return close; - }); -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/edit/closebrackets.js b/plugins/UiFileManager/media/codemirror/extension/edit/closebrackets.js deleted file mode 100644 index 4415c393..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/edit/closebrackets.js +++ /dev/null @@ -1,191 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - var defaults = { - pairs: "()[]{}''\"\"", - closeBefore: ")]}'\":;>", - triples: "", - explode: "[]{}" - }; - - var Pos = CodeMirror.Pos; - - CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) { - if (old && old != CodeMirror.Init) { - cm.removeKeyMap(keyMap); - cm.state.closeBrackets = null; - } - if (val) { - ensureBound(getOption(val, "pairs")) - cm.state.closeBrackets = val; - cm.addKeyMap(keyMap); - } - }); - - function getOption(conf, name) { - if (name == "pairs" && typeof conf == "string") return conf; - if (typeof conf == "object" && conf[name] != null) return conf[name]; - return defaults[name]; - } - - var keyMap = {Backspace: handleBackspace, Enter: handleEnter}; - function ensureBound(chars) { - for (var i = 0; i < chars.length; i++) { - var ch = chars.charAt(i), key = "'" + ch + "'" - if (!keyMap[key]) keyMap[key] = handler(ch) - } - } - ensureBound(defaults.pairs + "`") - - function handler(ch) { - return function(cm) { return handleChar(cm, ch); }; - } - - function getConfig(cm) { - var deflt = cm.state.closeBrackets; - if (!deflt || deflt.override) return deflt; - var mode = cm.getModeAt(cm.getCursor()); - return mode.closeBrackets || deflt; - } - - function handleBackspace(cm) { - var conf = getConfig(cm); - if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass; - - var pairs = getOption(conf, "pairs"); - var ranges = cm.listSelections(); - for (var i = 0; i < ranges.length; i++) { - if (!ranges[i].empty()) return CodeMirror.Pass; - var around = charsAround(cm, ranges[i].head); - if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass; - } - for (var i = ranges.length - 1; i >= 0; i--) { - var cur = ranges[i].head; - cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1), "+delete"); - } - } - - function handleEnter(cm) { - var conf = getConfig(cm); - var explode = conf && getOption(conf, "explode"); - if (!explode || cm.getOption("disableInput")) return CodeMirror.Pass; - - var ranges = cm.listSelections(); - for (var i = 0; i < ranges.length; i++) { - if (!ranges[i].empty()) return CodeMirror.Pass; - var around = charsAround(cm, ranges[i].head); - if (!around || explode.indexOf(around) % 2 != 0) return CodeMirror.Pass; - } - cm.operation(function() { - var linesep = cm.lineSeparator() || "\n"; - cm.replaceSelection(linesep + linesep, null); - cm.execCommand("goCharLeft"); - ranges = cm.listSelections(); - for (var i = 0; i < ranges.length; i++) { - var line = ranges[i].head.line; - cm.indentLine(line, null, true); - cm.indentLine(line + 1, null, true); - } - }); - } - - function contractSelection(sel) { - var inverted = CodeMirror.cmpPos(sel.anchor, sel.head) > 0; - return {anchor: new Pos(sel.anchor.line, sel.anchor.ch + (inverted ? -1 : 1)), - head: new Pos(sel.head.line, sel.head.ch + (inverted ? 1 : -1))}; - } - - function handleChar(cm, ch) { - var conf = getConfig(cm); - if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass; - - var pairs = getOption(conf, "pairs"); - var pos = pairs.indexOf(ch); - if (pos == -1) return CodeMirror.Pass; - - var closeBefore = getOption(conf,"closeBefore"); - - var triples = getOption(conf, "triples"); - - var identical = pairs.charAt(pos + 1) == ch; - var ranges = cm.listSelections(); - var opening = pos % 2 == 0; - - var type; - for (var i = 0; i < ranges.length; i++) { - var range = ranges[i], cur = range.head, curType; - var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1)); - if (opening && !range.empty()) { - curType = "surround"; - } else if ((identical || !opening) && next == ch) { - if (identical && stringStartsAfter(cm, cur)) - curType = "both"; - else if (triples.indexOf(ch) >= 0 && cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch) - curType = "skipThree"; - else - curType = "skip"; - } else if (identical && cur.ch > 1 && triples.indexOf(ch) >= 0 && - cm.getRange(Pos(cur.line, cur.ch - 2), cur) == ch + ch) { - if (cur.ch > 2 && /\bstring/.test(cm.getTokenTypeAt(Pos(cur.line, cur.ch - 2)))) return CodeMirror.Pass; - curType = "addFour"; - } else if (identical) { - var prev = cur.ch == 0 ? " " : cm.getRange(Pos(cur.line, cur.ch - 1), cur) - if (!CodeMirror.isWordChar(next) && prev != ch && !CodeMirror.isWordChar(prev)) curType = "both"; - else return CodeMirror.Pass; - } else if (opening && (next.length === 0 || /\s/.test(next) || closeBefore.indexOf(next) > -1)) { - curType = "both"; - } else { - return CodeMirror.Pass; - } - if (!type) type = curType; - else if (type != curType) return CodeMirror.Pass; - } - - var left = pos % 2 ? pairs.charAt(pos - 1) : ch; - var right = pos % 2 ? ch : pairs.charAt(pos + 1); - cm.operation(function() { - if (type == "skip") { - cm.execCommand("goCharRight"); - } else if (type == "skipThree") { - for (var i = 0; i < 3; i++) - cm.execCommand("goCharRight"); - } else if (type == "surround") { - var sels = cm.getSelections(); - for (var i = 0; i < sels.length; i++) - sels[i] = left + sels[i] + right; - cm.replaceSelections(sels, "around"); - sels = cm.listSelections().slice(); - for (var i = 0; i < sels.length; i++) - sels[i] = contractSelection(sels[i]); - cm.setSelections(sels); - } else if (type == "both") { - cm.replaceSelection(left + right, null); - cm.triggerElectric(left + right); - cm.execCommand("goCharLeft"); - } else if (type == "addFour") { - cm.replaceSelection(left + left + left + left, "before"); - cm.execCommand("goCharRight"); - } - }); - } - - function charsAround(cm, pos) { - var str = cm.getRange(Pos(pos.line, pos.ch - 1), - Pos(pos.line, pos.ch + 1)); - return str.length == 2 ? str : null; - } - - function stringStartsAfter(cm, pos) { - var token = cm.getTokenAt(Pos(pos.line, pos.ch + 1)) - return /\bstring/.test(token.type) && token.start == pos.ch && - (pos.ch == 0 || !/\bstring/.test(cm.getTokenTypeAt(pos))) - } -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/edit/closetag.js b/plugins/UiFileManager/media/codemirror/extension/edit/closetag.js deleted file mode 100644 index 8689765e..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/edit/closetag.js +++ /dev/null @@ -1,184 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -/** - * Tag-closer extension for CodeMirror. - * - * This extension adds an "autoCloseTags" option that can be set to - * either true to get the default behavior, or an object to further - * configure its behavior. - * - * These are supported options: - * - * `whenClosing` (default true) - * Whether to autoclose when the '/' of a closing tag is typed. - * `whenOpening` (default true) - * Whether to autoclose the tag when the final '>' of an opening - * tag is typed. - * `dontCloseTags` (default is empty tags for HTML, none for XML) - * An array of tag names that should not be autoclosed. - * `indentTags` (default is block tags for HTML, none for XML) - * An array of tag names that should, when opened, cause a - * blank line to be added inside the tag, and the blank line and - * closing line to be indented. - * `emptyTags` (default is none) - * An array of XML tag names that should be autoclosed with '/>'. - * - * See demos/closetag.html for a usage example. - */ - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("../fold/xml-fold")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "../fold/xml-fold"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - CodeMirror.defineOption("autoCloseTags", false, function(cm, val, old) { - if (old != CodeMirror.Init && old) - cm.removeKeyMap("autoCloseTags"); - if (!val) return; - var map = {name: "autoCloseTags"}; - if (typeof val != "object" || val.whenClosing !== false) - map["'/'"] = function(cm) { return autoCloseSlash(cm); }; - if (typeof val != "object" || val.whenOpening !== false) - map["'>'"] = function(cm) { return autoCloseGT(cm); }; - cm.addKeyMap(map); - }); - - var htmlDontClose = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", - "source", "track", "wbr"]; - var htmlIndent = ["applet", "blockquote", "body", "button", "div", "dl", "fieldset", "form", "frameset", "h1", "h2", "h3", "h4", - "h5", "h6", "head", "html", "iframe", "layer", "legend", "object", "ol", "p", "select", "table", "ul"]; - - function autoCloseGT(cm) { - if (cm.getOption("disableInput")) return CodeMirror.Pass; - var ranges = cm.listSelections(), replacements = []; - var opt = cm.getOption("autoCloseTags"); - for (var i = 0; i < ranges.length; i++) { - if (!ranges[i].empty()) return CodeMirror.Pass; - var pos = ranges[i].head, tok = cm.getTokenAt(pos); - var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state; - var tagInfo = inner.mode.xmlCurrentTag && inner.mode.xmlCurrentTag(state) - var tagName = tagInfo && tagInfo.name - if (!tagName) return CodeMirror.Pass - - var html = inner.mode.configuration == "html"; - var dontCloseTags = (typeof opt == "object" && opt.dontCloseTags) || (html && htmlDontClose); - var indentTags = (typeof opt == "object" && opt.indentTags) || (html && htmlIndent); - - if (tok.end > pos.ch) tagName = tagName.slice(0, tagName.length - tok.end + pos.ch); - var lowerTagName = tagName.toLowerCase(); - // Don't process the '>' at the end of an end-tag or self-closing tag - if (!tagName || - tok.type == "string" && (tok.end != pos.ch || !/[\"\']/.test(tok.string.charAt(tok.string.length - 1)) || tok.string.length == 1) || - tok.type == "tag" && tagInfo.close || - tok.string.indexOf("/") == (pos.ch - tok.start - 1) || // match something like - dontCloseTags && indexOf(dontCloseTags, lowerTagName) > -1 || - closingTagExists(cm, inner.mode.xmlCurrentContext && inner.mode.xmlCurrentContext(state) || [], tagName, pos, true)) - return CodeMirror.Pass; - - var emptyTags = typeof opt == "object" && opt.emptyTags; - if (emptyTags && indexOf(emptyTags, tagName) > -1) { - replacements[i] = { text: "/>", newPos: CodeMirror.Pos(pos.line, pos.ch + 2) }; - continue; - } - - var indent = indentTags && indexOf(indentTags, lowerTagName) > -1; - replacements[i] = {indent: indent, - text: ">" + (indent ? "\n\n" : "") + "", - newPos: indent ? CodeMirror.Pos(pos.line + 1, 0) : CodeMirror.Pos(pos.line, pos.ch + 1)}; - } - - var dontIndentOnAutoClose = (typeof opt == "object" && opt.dontIndentOnAutoClose); - for (var i = ranges.length - 1; i >= 0; i--) { - var info = replacements[i]; - cm.replaceRange(info.text, ranges[i].head, ranges[i].anchor, "+insert"); - var sel = cm.listSelections().slice(0); - sel[i] = {head: info.newPos, anchor: info.newPos}; - cm.setSelections(sel); - if (!dontIndentOnAutoClose && info.indent) { - cm.indentLine(info.newPos.line, null, true); - cm.indentLine(info.newPos.line + 1, null, true); - } - } - } - - function autoCloseCurrent(cm, typingSlash) { - var ranges = cm.listSelections(), replacements = []; - var head = typingSlash ? "/" : "") replacement += ">"; - replacements[i] = replacement; - } - cm.replaceSelections(replacements); - ranges = cm.listSelections(); - if (!dontIndentOnAutoClose) { - for (var i = 0; i < ranges.length; i++) - if (i == ranges.length - 1 || ranges[i].head.line < ranges[i + 1].head.line) - cm.indentLine(ranges[i].head.line); - } - } - - function autoCloseSlash(cm) { - if (cm.getOption("disableInput")) return CodeMirror.Pass; - return autoCloseCurrent(cm, true); - } - - CodeMirror.commands.closeTag = function(cm) { return autoCloseCurrent(cm); }; - - function indexOf(collection, elt) { - if (collection.indexOf) return collection.indexOf(elt); - for (var i = 0, e = collection.length; i < e; ++i) - if (collection[i] == elt) return i; - return -1; - } - - // If xml-fold is loaded, we use its functionality to try and verify - // whether a given tag is actually unclosed. - function closingTagExists(cm, context, tagName, pos, newTag) { - if (!CodeMirror.scanForClosingTag) return false; - var end = Math.min(cm.lastLine() + 1, pos.line + 500); - var nextClose = CodeMirror.scanForClosingTag(cm, pos, null, end); - if (!nextClose || nextClose.tag != tagName) return false; - // If the immediate wrapping context contains onCx instances of - // the same tag, a closing tag only exists if there are at least - // that many closing tags of that type following. - var onCx = newTag ? 1 : 0 - for (var i = context.length - 1; i >= 0; i--) { - if (context[i] == tagName) ++onCx - else break - } - pos = nextClose.to; - for (var i = 1; i < onCx; i++) { - var next = CodeMirror.scanForClosingTag(cm, pos, null, end); - if (!next || next.tag != tagName) return false; - pos = next.to; - } - return true; - } -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/edit/continuelist.js b/plugins/UiFileManager/media/codemirror/extension/edit/continuelist.js deleted file mode 100644 index 2e5625ad..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/edit/continuelist.js +++ /dev/null @@ -1,101 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - var listRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/, - emptyListRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/, - unorderedListRE = /[*+-]\s/; - - CodeMirror.commands.newlineAndIndentContinueMarkdownList = function(cm) { - if (cm.getOption("disableInput")) return CodeMirror.Pass; - var ranges = cm.listSelections(), replacements = []; - for (var i = 0; i < ranges.length; i++) { - var pos = ranges[i].head; - - // If we're not in Markdown mode, fall back to normal newlineAndIndent - var eolState = cm.getStateAfter(pos.line); - var inner = CodeMirror.innerMode(cm.getMode(), eolState); - if (inner.mode.name !== "markdown") { - cm.execCommand("newlineAndIndent"); - return; - } else { - eolState = inner.state; - } - - var inList = eolState.list !== false; - var inQuote = eolState.quote !== 0; - - var line = cm.getLine(pos.line), match = listRE.exec(line); - var cursorBeforeBullet = /^\s*$/.test(line.slice(0, pos.ch)); - if (!ranges[i].empty() || (!inList && !inQuote) || !match || cursorBeforeBullet) { - cm.execCommand("newlineAndIndent"); - return; - } - if (emptyListRE.test(line)) { - var endOfQuote = inQuote && />\s*$/.test(line) - var endOfList = !/>\s*$/.test(line) - if (endOfQuote || endOfList) cm.replaceRange("", { - line: pos.line, ch: 0 - }, { - line: pos.line, ch: pos.ch + 1 - }); - replacements[i] = "\n"; - } else { - var indent = match[1], after = match[5]; - var numbered = !(unorderedListRE.test(match[2]) || match[2].indexOf(">") >= 0); - var bullet = numbered ? (parseInt(match[3], 10) + 1) + match[4] : match[2].replace("x", " "); - replacements[i] = "\n" + indent + bullet + after; - - if (numbered) incrementRemainingMarkdownListNumbers(cm, pos); - } - } - - cm.replaceSelections(replacements); - }; - - // Auto-updating Markdown list numbers when a new item is added to the - // middle of a list - function incrementRemainingMarkdownListNumbers(cm, pos) { - var startLine = pos.line, lookAhead = 0, skipCount = 0; - var startItem = listRE.exec(cm.getLine(startLine)), startIndent = startItem[1]; - - do { - lookAhead += 1; - var nextLineNumber = startLine + lookAhead; - var nextLine = cm.getLine(nextLineNumber), nextItem = listRE.exec(nextLine); - - if (nextItem) { - var nextIndent = nextItem[1]; - var newNumber = (parseInt(startItem[3], 10) + lookAhead - skipCount); - var nextNumber = (parseInt(nextItem[3], 10)), itemNumber = nextNumber; - - if (startIndent === nextIndent && !isNaN(nextNumber)) { - if (newNumber === nextNumber) itemNumber = nextNumber + 1; - if (newNumber > nextNumber) itemNumber = newNumber + 1; - cm.replaceRange( - nextLine.replace(listRE, nextIndent + itemNumber + nextItem[4] + nextItem[5]), - { - line: nextLineNumber, ch: 0 - }, { - line: nextLineNumber, ch: nextLine.length - }); - } else { - if (startIndent.length > nextIndent.length) return; - // This doesn't run if the next line immediatley indents, as it is - // not clear of the users intention (new indented item or same level) - if ((startIndent.length < nextIndent.length) && (lookAhead === 1)) return; - skipCount += 1; - } - } - } while (nextItem); - } -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/edit/matchbrackets.js b/plugins/UiFileManager/media/codemirror/extension/edit/matchbrackets.js deleted file mode 100644 index 2c47e070..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/edit/matchbrackets.js +++ /dev/null @@ -1,158 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - var ie_lt8 = /MSIE \d/.test(navigator.userAgent) && - (document.documentMode == null || document.documentMode < 8); - - var Pos = CodeMirror.Pos; - - var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<", "<": ">>", ">": "<<"}; - - function bracketRegex(config) { - return config && config.bracketRegex || /[(){}[\]]/ - } - - function findMatchingBracket(cm, where, config) { - var line = cm.getLineHandle(where.line), pos = where.ch - 1; - var afterCursor = config && config.afterCursor - if (afterCursor == null) - afterCursor = /(^| )cm-fat-cursor($| )/.test(cm.getWrapperElement().className) - var re = bracketRegex(config) - - // A cursor is defined as between two characters, but in in vim command mode - // (i.e. not insert mode), the cursor is visually represented as a - // highlighted box on top of the 2nd character. Otherwise, we allow matches - // from before or after the cursor. - var match = (!afterCursor && pos >= 0 && re.test(line.text.charAt(pos)) && matching[line.text.charAt(pos)]) || - re.test(line.text.charAt(pos + 1)) && matching[line.text.charAt(++pos)]; - if (!match) return null; - var dir = match.charAt(1) == ">" ? 1 : -1; - if (config && config.strict && (dir > 0) != (pos == where.ch)) return null; - var style = cm.getTokenTypeAt(Pos(where.line, pos + 1)); - - var found = scanForBracket(cm, Pos(where.line, pos + (dir > 0 ? 1 : 0)), dir, style || null, config); - if (found == null) return null; - return {from: Pos(where.line, pos), to: found && found.pos, - match: found && found.ch == match.charAt(0), forward: dir > 0}; - } - - // bracketRegex is used to specify which type of bracket to scan - // should be a regexp, e.g. /[[\]]/ - // - // Note: If "where" is on an open bracket, then this bracket is ignored. - // - // Returns false when no bracket was found, null when it reached - // maxScanLines and gave up - function scanForBracket(cm, where, dir, style, config) { - var maxScanLen = (config && config.maxScanLineLength) || 10000; - var maxScanLines = (config && config.maxScanLines) || 1000; - - var stack = []; - var re = bracketRegex(config) - var lineEnd = dir > 0 ? Math.min(where.line + maxScanLines, cm.lastLine() + 1) - : Math.max(cm.firstLine() - 1, where.line - maxScanLines); - for (var lineNo = where.line; lineNo != lineEnd; lineNo += dir) { - var line = cm.getLine(lineNo); - if (!line) continue; - var pos = dir > 0 ? 0 : line.length - 1, end = dir > 0 ? line.length : -1; - if (line.length > maxScanLen) continue; - if (lineNo == where.line) pos = where.ch - (dir < 0 ? 1 : 0); - for (; pos != end; pos += dir) { - var ch = line.charAt(pos); - if (re.test(ch) && (style === undefined || cm.getTokenTypeAt(Pos(lineNo, pos + 1)) == style)) { - var match = matching[ch]; - if (match && (match.charAt(1) == ">") == (dir > 0)) stack.push(ch); - else if (!stack.length) return {pos: Pos(lineNo, pos), ch: ch}; - else stack.pop(); - } - } - } - return lineNo - dir == (dir > 0 ? cm.lastLine() : cm.firstLine()) ? false : null; - } - - function matchBrackets(cm, autoclear, config) { - // Disable brace matching in long lines, since it'll cause hugely slow updates - var maxHighlightLen = cm.state.matchBrackets.maxHighlightLineLength || 1000; - var marks = [], ranges = cm.listSelections(); - for (var i = 0; i < ranges.length; i++) { - var match = ranges[i].empty() && findMatchingBracket(cm, ranges[i].head, config); - if (match && cm.getLine(match.from.line).length <= maxHighlightLen) { - var style = match.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket"; - marks.push(cm.markText(match.from, Pos(match.from.line, match.from.ch + 1), {className: style})); - if (match.to && cm.getLine(match.to.line).length <= maxHighlightLen) - marks.push(cm.markText(match.to, Pos(match.to.line, match.to.ch + 1), {className: style})); - } - } - - if (marks.length) { - // Kludge to work around the IE bug from issue #1193, where text - // input stops going to the textare whever this fires. - if (ie_lt8 && cm.state.focused) cm.focus(); - - var clear = function() { - cm.operation(function() { - for (var i = 0; i < marks.length; i++) marks[i].clear(); - }); - }; - if (autoclear) setTimeout(clear, 800); - else return clear; - } - } - - function doMatchBrackets(cm) { - cm.operation(function() { - if (cm.state.matchBrackets.currentlyHighlighted) { - cm.state.matchBrackets.currentlyHighlighted(); - cm.state.matchBrackets.currentlyHighlighted = null; - } - cm.state.matchBrackets.currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets); - }); - } - - CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) { - function clear(cm) { - if (cm.state.matchBrackets && cm.state.matchBrackets.currentlyHighlighted) { - cm.state.matchBrackets.currentlyHighlighted(); - cm.state.matchBrackets.currentlyHighlighted = null; - } - } - - if (old && old != CodeMirror.Init) { - cm.off("cursorActivity", doMatchBrackets); - cm.off("focus", doMatchBrackets) - cm.off("blur", clear) - clear(cm); - } - if (val) { - cm.state.matchBrackets = typeof val == "object" ? val : {}; - cm.on("cursorActivity", doMatchBrackets); - cm.on("focus", doMatchBrackets) - cm.on("blur", clear) - } - }); - - CodeMirror.defineExtension("matchBrackets", function() {matchBrackets(this, true);}); - CodeMirror.defineExtension("findMatchingBracket", function(pos, config, oldConfig){ - // Backwards-compatibility kludge - if (oldConfig || typeof config == "boolean") { - if (!oldConfig) { - config = config ? {strict: true} : null - } else { - oldConfig.strict = config - config = oldConfig - } - } - return findMatchingBracket(this, pos, config) - }); - CodeMirror.defineExtension("scanForBracket", function(pos, dir, style, config){ - return scanForBracket(this, pos, dir, style, config); - }); -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/edit/matchtags.js b/plugins/UiFileManager/media/codemirror/extension/edit/matchtags.js deleted file mode 100644 index 2203d939..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/edit/matchtags.js +++ /dev/null @@ -1,66 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("../fold/xml-fold")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "../fold/xml-fold"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - CodeMirror.defineOption("matchTags", false, function(cm, val, old) { - if (old && old != CodeMirror.Init) { - cm.off("cursorActivity", doMatchTags); - cm.off("viewportChange", maybeUpdateMatch); - clear(cm); - } - if (val) { - cm.state.matchBothTags = typeof val == "object" && val.bothTags; - cm.on("cursorActivity", doMatchTags); - cm.on("viewportChange", maybeUpdateMatch); - doMatchTags(cm); - } - }); - - function clear(cm) { - if (cm.state.tagHit) cm.state.tagHit.clear(); - if (cm.state.tagOther) cm.state.tagOther.clear(); - cm.state.tagHit = cm.state.tagOther = null; - } - - function doMatchTags(cm) { - cm.state.failedTagMatch = false; - cm.operation(function() { - clear(cm); - if (cm.somethingSelected()) return; - var cur = cm.getCursor(), range = cm.getViewport(); - range.from = Math.min(range.from, cur.line); range.to = Math.max(cur.line + 1, range.to); - var match = CodeMirror.findMatchingTag(cm, cur, range); - if (!match) return; - if (cm.state.matchBothTags) { - var hit = match.at == "open" ? match.open : match.close; - if (hit) cm.state.tagHit = cm.markText(hit.from, hit.to, {className: "CodeMirror-matchingtag"}); - } - var other = match.at == "close" ? match.open : match.close; - if (other) - cm.state.tagOther = cm.markText(other.from, other.to, {className: "CodeMirror-matchingtag"}); - else - cm.state.failedTagMatch = true; - }); - } - - function maybeUpdateMatch(cm) { - if (cm.state.failedTagMatch) doMatchTags(cm); - } - - CodeMirror.commands.toMatchingTag = function(cm) { - var found = CodeMirror.findMatchingTag(cm, cm.getCursor()); - if (found) { - var other = found.at == "close" ? found.open : found.close; - if (other) cm.extendSelection(other.to, other.from); - } - }; -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/edit/trailingspace.js b/plugins/UiFileManager/media/codemirror/extension/edit/trailingspace.js deleted file mode 100644 index c39c310a..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/edit/trailingspace.js +++ /dev/null @@ -1,27 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - CodeMirror.defineOption("showTrailingSpace", false, function(cm, val, prev) { - if (prev == CodeMirror.Init) prev = false; - if (prev && !val) - cm.removeOverlay("trailingspace"); - else if (!prev && val) - cm.addOverlay({ - token: function(stream) { - for (var l = stream.string.length, i = l; i && /\s/.test(stream.string.charAt(i - 1)); --i) {} - if (i > stream.pos) { stream.pos = i; return null; } - stream.pos = l; - return "trailingspace"; - }, - name: "trailingspace" - }); - }); -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/fold/brace-fold.js b/plugins/UiFileManager/media/codemirror/extension/fold/brace-fold.js deleted file mode 100644 index 654d1fb6..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/fold/brace-fold.js +++ /dev/null @@ -1,105 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -CodeMirror.registerHelper("fold", "brace", function(cm, start) { - var line = start.line, lineText = cm.getLine(line); - var tokenType; - - function findOpening(openCh) { - for (var at = start.ch, pass = 0;;) { - var found = at <= 0 ? -1 : lineText.lastIndexOf(openCh, at - 1); - if (found == -1) { - if (pass == 1) break; - pass = 1; - at = lineText.length; - continue; - } - if (pass == 1 && found < start.ch) break; - tokenType = cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1)); - if (!/^(comment|string)/.test(tokenType)) return found + 1; - at = found - 1; - } - } - - var startToken = "{", endToken = "}", startCh = findOpening("{"); - if (startCh == null) { - startToken = "[", endToken = "]"; - startCh = findOpening("["); - } - - if (startCh == null) return; - var count = 1, lastLine = cm.lastLine(), end, endCh; - outer: for (var i = line; i <= lastLine; ++i) { - var text = cm.getLine(i), pos = i == line ? startCh : 0; - for (;;) { - var nextOpen = text.indexOf(startToken, pos), nextClose = text.indexOf(endToken, pos); - if (nextOpen < 0) nextOpen = text.length; - if (nextClose < 0) nextClose = text.length; - pos = Math.min(nextOpen, nextClose); - if (pos == text.length) break; - if (cm.getTokenTypeAt(CodeMirror.Pos(i, pos + 1)) == tokenType) { - if (pos == nextOpen) ++count; - else if (!--count) { end = i; endCh = pos; break outer; } - } - ++pos; - } - } - if (end == null || line == end) return; - return {from: CodeMirror.Pos(line, startCh), - to: CodeMirror.Pos(end, endCh)}; -}); - -CodeMirror.registerHelper("fold", "import", function(cm, start) { - function hasImport(line) { - if (line < cm.firstLine() || line > cm.lastLine()) return null; - var start = cm.getTokenAt(CodeMirror.Pos(line, 1)); - if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1)); - if (start.type != "keyword" || start.string != "import") return null; - // Now find closing semicolon, return its position - for (var i = line, e = Math.min(cm.lastLine(), line + 10); i <= e; ++i) { - var text = cm.getLine(i), semi = text.indexOf(";"); - if (semi != -1) return {startCh: start.end, end: CodeMirror.Pos(i, semi)}; - } - } - - var startLine = start.line, has = hasImport(startLine), prev; - if (!has || hasImport(startLine - 1) || ((prev = hasImport(startLine - 2)) && prev.end.line == startLine - 1)) - return null; - for (var end = has.end;;) { - var next = hasImport(end.line + 1); - if (next == null) break; - end = next.end; - } - return {from: cm.clipPos(CodeMirror.Pos(startLine, has.startCh + 1)), to: end}; -}); - -CodeMirror.registerHelper("fold", "include", function(cm, start) { - function hasInclude(line) { - if (line < cm.firstLine() || line > cm.lastLine()) return null; - var start = cm.getTokenAt(CodeMirror.Pos(line, 1)); - if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1)); - if (start.type == "meta" && start.string.slice(0, 8) == "#include") return start.start + 8; - } - - var startLine = start.line, has = hasInclude(startLine); - if (has == null || hasInclude(startLine - 1) != null) return null; - for (var end = startLine;;) { - var next = hasInclude(end + 1); - if (next == null) break; - ++end; - } - return {from: CodeMirror.Pos(startLine, has + 1), - to: cm.clipPos(CodeMirror.Pos(end))}; -}); - -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/fold/comment-fold.js b/plugins/UiFileManager/media/codemirror/extension/fold/comment-fold.js deleted file mode 100644 index 836101d8..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/fold/comment-fold.js +++ /dev/null @@ -1,59 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -CodeMirror.registerGlobalHelper("fold", "comment", function(mode) { - return mode.blockCommentStart && mode.blockCommentEnd; -}, function(cm, start) { - var mode = cm.getModeAt(start), startToken = mode.blockCommentStart, endToken = mode.blockCommentEnd; - if (!startToken || !endToken) return; - var line = start.line, lineText = cm.getLine(line); - - var startCh; - for (var at = start.ch, pass = 0;;) { - var found = at <= 0 ? -1 : lineText.lastIndexOf(startToken, at - 1); - if (found == -1) { - if (pass == 1) return; - pass = 1; - at = lineText.length; - continue; - } - if (pass == 1 && found < start.ch) return; - if (/comment/.test(cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1))) && - (found == 0 || lineText.slice(found - endToken.length, found) == endToken || - !/comment/.test(cm.getTokenTypeAt(CodeMirror.Pos(line, found))))) { - startCh = found + startToken.length; - break; - } - at = found - 1; - } - - var depth = 1, lastLine = cm.lastLine(), end, endCh; - outer: for (var i = line; i <= lastLine; ++i) { - var text = cm.getLine(i), pos = i == line ? startCh : 0; - for (;;) { - var nextOpen = text.indexOf(startToken, pos), nextClose = text.indexOf(endToken, pos); - if (nextOpen < 0) nextOpen = text.length; - if (nextClose < 0) nextClose = text.length; - pos = Math.min(nextOpen, nextClose); - if (pos == text.length) break; - if (pos == nextOpen) ++depth; - else if (!--depth) { end = i; endCh = pos; break outer; } - ++pos; - } - } - if (end == null || line == end && endCh == startCh) return; - return {from: CodeMirror.Pos(line, startCh), - to: CodeMirror.Pos(end, endCh)}; -}); - -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/fold/foldcode.js b/plugins/UiFileManager/media/codemirror/extension/fold/foldcode.js deleted file mode 100644 index 887df3fe..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/fold/foldcode.js +++ /dev/null @@ -1,157 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - function doFold(cm, pos, options, force) { - if (options && options.call) { - var finder = options; - options = null; - } else { - var finder = getOption(cm, options, "rangeFinder"); - } - if (typeof pos == "number") pos = CodeMirror.Pos(pos, 0); - var minSize = getOption(cm, options, "minFoldSize"); - - function getRange(allowFolded) { - var range = finder(cm, pos); - if (!range || range.to.line - range.from.line < minSize) return null; - var marks = cm.findMarksAt(range.from); - for (var i = 0; i < marks.length; ++i) { - if (marks[i].__isFold && force !== "fold") { - if (!allowFolded) return null; - range.cleared = true; - marks[i].clear(); - } - } - return range; - } - - var range = getRange(true); - if (getOption(cm, options, "scanUp")) while (!range && pos.line > cm.firstLine()) { - pos = CodeMirror.Pos(pos.line - 1, 0); - range = getRange(false); - } - if (!range || range.cleared || force === "unfold") return; - - var myWidget = makeWidget(cm, options, range); - CodeMirror.on(myWidget, "mousedown", function(e) { - myRange.clear(); - CodeMirror.e_preventDefault(e); - }); - var myRange = cm.markText(range.from, range.to, { - replacedWith: myWidget, - clearOnEnter: getOption(cm, options, "clearOnEnter"), - __isFold: true - }); - myRange.on("clear", function(from, to) { - CodeMirror.signal(cm, "unfold", cm, from, to); - }); - CodeMirror.signal(cm, "fold", cm, range.from, range.to); - } - - function makeWidget(cm, options, range) { - var widget = getOption(cm, options, "widget"); - - if (typeof widget == "function") { - widget = widget(range.from, range.to); - } - - if (typeof widget == "string") { - var text = document.createTextNode(widget); - widget = document.createElement("span"); - widget.appendChild(text); - widget.className = "CodeMirror-foldmarker"; - } else if (widget) { - widget = widget.cloneNode(true) - } - return widget; - } - - // Clumsy backwards-compatible interface - CodeMirror.newFoldFunction = function(rangeFinder, widget) { - return function(cm, pos) { doFold(cm, pos, {rangeFinder: rangeFinder, widget: widget}); }; - }; - - // New-style interface - CodeMirror.defineExtension("foldCode", function(pos, options, force) { - doFold(this, pos, options, force); - }); - - CodeMirror.defineExtension("isFolded", function(pos) { - var marks = this.findMarksAt(pos); - for (var i = 0; i < marks.length; ++i) - if (marks[i].__isFold) return true; - }); - - CodeMirror.commands.toggleFold = function(cm) { - cm.foldCode(cm.getCursor()); - }; - CodeMirror.commands.fold = function(cm) { - cm.foldCode(cm.getCursor(), null, "fold"); - }; - CodeMirror.commands.unfold = function(cm) { - cm.foldCode(cm.getCursor(), null, "unfold"); - }; - CodeMirror.commands.foldAll = function(cm) { - cm.operation(function() { - for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) - cm.foldCode(CodeMirror.Pos(i, 0), null, "fold"); - }); - }; - CodeMirror.commands.unfoldAll = function(cm) { - cm.operation(function() { - for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) - cm.foldCode(CodeMirror.Pos(i, 0), null, "unfold"); - }); - }; - - CodeMirror.registerHelper("fold", "combine", function() { - var funcs = Array.prototype.slice.call(arguments, 0); - return function(cm, start) { - for (var i = 0; i < funcs.length; ++i) { - var found = funcs[i](cm, start); - if (found) return found; - } - }; - }); - - CodeMirror.registerHelper("fold", "auto", function(cm, start) { - var helpers = cm.getHelpers(start, "fold"); - for (var i = 0; i < helpers.length; i++) { - var cur = helpers[i](cm, start); - if (cur) return cur; - } - }); - - var defaultOptions = { - rangeFinder: CodeMirror.fold.auto, - widget: "\u2194", - minFoldSize: 0, - scanUp: false, - clearOnEnter: true - }; - - CodeMirror.defineOption("foldOptions", null); - - function getOption(cm, options, name) { - if (options && options[name] !== undefined) - return options[name]; - var editorOptions = cm.options.foldOptions; - if (editorOptions && editorOptions[name] !== undefined) - return editorOptions[name]; - return defaultOptions[name]; - } - - CodeMirror.defineExtension("foldOption", function(options, name) { - return getOption(this, options, name); - }); -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/fold/foldgutter.css b/plugins/UiFileManager/media/codemirror/extension/fold/foldgutter.css deleted file mode 100644 index ad19ae2d..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/fold/foldgutter.css +++ /dev/null @@ -1,20 +0,0 @@ -.CodeMirror-foldmarker { - color: blue; - text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px; - font-family: arial; - line-height: .3; - cursor: pointer; -} -.CodeMirror-foldgutter { - width: .7em; -} -.CodeMirror-foldgutter-open, -.CodeMirror-foldgutter-folded { - cursor: pointer; -} -.CodeMirror-foldgutter-open:after { - content: "\25BE"; -} -.CodeMirror-foldgutter-folded:after { - content: "\25B8"; -} diff --git a/plugins/UiFileManager/media/codemirror/extension/fold/foldgutter.js b/plugins/UiFileManager/media/codemirror/extension/fold/foldgutter.js deleted file mode 100644 index 7d46a609..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/fold/foldgutter.js +++ /dev/null @@ -1,163 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("./foldcode")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "./foldcode"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - CodeMirror.defineOption("foldGutter", false, function(cm, val, old) { - if (old && old != CodeMirror.Init) { - cm.clearGutter(cm.state.foldGutter.options.gutter); - cm.state.foldGutter = null; - cm.off("gutterClick", onGutterClick); - cm.off("changes", onChange); - cm.off("viewportChange", onViewportChange); - cm.off("fold", onFold); - cm.off("unfold", onFold); - cm.off("swapDoc", onChange); - } - if (val) { - cm.state.foldGutter = new State(parseOptions(val)); - updateInViewport(cm); - cm.on("gutterClick", onGutterClick); - cm.on("changes", onChange); - cm.on("viewportChange", onViewportChange); - cm.on("fold", onFold); - cm.on("unfold", onFold); - cm.on("swapDoc", onChange); - } - }); - - var Pos = CodeMirror.Pos; - - function State(options) { - this.options = options; - this.from = this.to = 0; - } - - function parseOptions(opts) { - if (opts === true) opts = {}; - if (opts.gutter == null) opts.gutter = "CodeMirror-foldgutter"; - if (opts.indicatorOpen == null) opts.indicatorOpen = "CodeMirror-foldgutter-open"; - if (opts.indicatorFolded == null) opts.indicatorFolded = "CodeMirror-foldgutter-folded"; - return opts; - } - - function isFolded(cm, line) { - var marks = cm.findMarks(Pos(line, 0), Pos(line + 1, 0)); - for (var i = 0; i < marks.length; ++i) { - if (marks[i].__isFold) { - var fromPos = marks[i].find(-1); - if (fromPos && fromPos.line === line) - return marks[i]; - } - } - } - - function marker(spec) { - if (typeof spec == "string") { - var elt = document.createElement("div"); - elt.className = spec + " CodeMirror-guttermarker-subtle"; - return elt; - } else { - return spec.cloneNode(true); - } - } - - function updateFoldInfo(cm, from, to) { - var opts = cm.state.foldGutter.options, cur = from - 1; - var minSize = cm.foldOption(opts, "minFoldSize"); - var func = cm.foldOption(opts, "rangeFinder"); - // we can reuse the built-in indicator element if its className matches the new state - var clsFolded = typeof opts.indicatorFolded == "string" && classTest(opts.indicatorFolded); - var clsOpen = typeof opts.indicatorOpen == "string" && classTest(opts.indicatorOpen); - cm.eachLine(from, to, function(line) { - ++cur; - var mark = null; - var old = line.gutterMarkers; - if (old) old = old[opts.gutter]; - if (isFolded(cm, cur)) { - if (clsFolded && old && clsFolded.test(old.className)) return; - mark = marker(opts.indicatorFolded); - } else { - var pos = Pos(cur, 0); - var range = func && func(cm, pos); - if (range && range.to.line - range.from.line >= minSize) { - if (clsOpen && old && clsOpen.test(old.className)) return; - mark = marker(opts.indicatorOpen); - } - } - if (!mark && !old) return; - cm.setGutterMarker(line, opts.gutter, mark); - }); - } - - // copied from CodeMirror/src/util/dom.js - function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*") } - - function updateInViewport(cm) { - var vp = cm.getViewport(), state = cm.state.foldGutter; - if (!state) return; - cm.operation(function() { - updateFoldInfo(cm, vp.from, vp.to); - }); - state.from = vp.from; state.to = vp.to; - } - - function onGutterClick(cm, line, gutter) { - var state = cm.state.foldGutter; - if (!state) return; - var opts = state.options; - if (gutter != opts.gutter) return; - var folded = isFolded(cm, line); - if (folded) folded.clear(); - else cm.foldCode(Pos(line, 0), opts); - } - - function onChange(cm) { - var state = cm.state.foldGutter; - if (!state) return; - var opts = state.options; - state.from = state.to = 0; - clearTimeout(state.changeUpdate); - state.changeUpdate = setTimeout(function() { updateInViewport(cm); }, opts.foldOnChangeTimeSpan || 600); - } - - function onViewportChange(cm) { - var state = cm.state.foldGutter; - if (!state) return; - var opts = state.options; - clearTimeout(state.changeUpdate); - state.changeUpdate = setTimeout(function() { - var vp = cm.getViewport(); - if (state.from == state.to || vp.from - state.to > 20 || state.from - vp.to > 20) { - updateInViewport(cm); - } else { - cm.operation(function() { - if (vp.from < state.from) { - updateFoldInfo(cm, vp.from, state.from); - state.from = vp.from; - } - if (vp.to > state.to) { - updateFoldInfo(cm, state.to, vp.to); - state.to = vp.to; - } - }); - } - }, opts.updateViewportTimeSpan || 400); - } - - function onFold(cm, from) { - var state = cm.state.foldGutter; - if (!state) return; - var line = from.line; - if (line >= state.from && line < state.to) - updateFoldInfo(cm, line, line + 1); - } -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/fold/indent-fold.js b/plugins/UiFileManager/media/codemirror/extension/fold/indent-fold.js deleted file mode 100644 index 0cc11264..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/fold/indent-fold.js +++ /dev/null @@ -1,48 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -function lineIndent(cm, lineNo) { - var text = cm.getLine(lineNo) - var spaceTo = text.search(/\S/) - if (spaceTo == -1 || /\bcomment\b/.test(cm.getTokenTypeAt(CodeMirror.Pos(lineNo, spaceTo + 1)))) - return -1 - return CodeMirror.countColumn(text, null, cm.getOption("tabSize")) -} - -CodeMirror.registerHelper("fold", "indent", function(cm, start) { - var myIndent = lineIndent(cm, start.line) - if (myIndent < 0) return - var lastLineInFold = null - - // Go through lines until we find a line that definitely doesn't belong in - // the block we're folding, or to the end. - for (var i = start.line + 1, end = cm.lastLine(); i <= end; ++i) { - var indent = lineIndent(cm, i) - if (indent == -1) { - } else if (indent > myIndent) { - // Lines with a greater indent are considered part of the block. - lastLineInFold = i; - } else { - // If this line has non-space, non-comment content, and is - // indented less or equal to the start line, it is the start of - // another block. - break; - } - } - if (lastLineInFold) return { - from: CodeMirror.Pos(start.line, cm.getLine(start.line).length), - to: CodeMirror.Pos(lastLineInFold, cm.getLine(lastLineInFold).length) - }; -}); - -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/fold/markdown-fold.js b/plugins/UiFileManager/media/codemirror/extension/fold/markdown-fold.js deleted file mode 100644 index 6a551786..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/fold/markdown-fold.js +++ /dev/null @@ -1,49 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -CodeMirror.registerHelper("fold", "markdown", function(cm, start) { - var maxDepth = 100; - - function isHeader(lineNo) { - var tokentype = cm.getTokenTypeAt(CodeMirror.Pos(lineNo, 0)); - return tokentype && /\bheader\b/.test(tokentype); - } - - function headerLevel(lineNo, line, nextLine) { - var match = line && line.match(/^#+/); - if (match && isHeader(lineNo)) return match[0].length; - match = nextLine && nextLine.match(/^[=\-]+\s*$/); - if (match && isHeader(lineNo + 1)) return nextLine[0] == "=" ? 1 : 2; - return maxDepth; - } - - var firstLine = cm.getLine(start.line), nextLine = cm.getLine(start.line + 1); - var level = headerLevel(start.line, firstLine, nextLine); - if (level === maxDepth) return undefined; - - var lastLineNo = cm.lastLine(); - var end = start.line, nextNextLine = cm.getLine(end + 2); - while (end < lastLineNo) { - if (headerLevel(end + 1, nextLine, nextNextLine) <= level) break; - ++end; - nextLine = nextNextLine; - nextNextLine = cm.getLine(end + 2); - } - - return { - from: CodeMirror.Pos(start.line, firstLine.length), - to: CodeMirror.Pos(end, cm.getLine(end).length) - }; -}); - -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/fold/xml-fold.js b/plugins/UiFileManager/media/codemirror/extension/fold/xml-fold.js deleted file mode 100644 index 13bc3838..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/fold/xml-fold.js +++ /dev/null @@ -1,184 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - var Pos = CodeMirror.Pos; - function cmp(a, b) { return a.line - b.line || a.ch - b.ch; } - - var nameStartChar = "A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD"; - var nameChar = nameStartChar + "\-\:\.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040"; - var xmlTagStart = new RegExp("<(/?)([" + nameStartChar + "][" + nameChar + "]*)", "g"); - - function Iter(cm, line, ch, range) { - this.line = line; this.ch = ch; - this.cm = cm; this.text = cm.getLine(line); - this.min = range ? Math.max(range.from, cm.firstLine()) : cm.firstLine(); - this.max = range ? Math.min(range.to - 1, cm.lastLine()) : cm.lastLine(); - } - - function tagAt(iter, ch) { - var type = iter.cm.getTokenTypeAt(Pos(iter.line, ch)); - return type && /\btag\b/.test(type); - } - - function nextLine(iter) { - if (iter.line >= iter.max) return; - iter.ch = 0; - iter.text = iter.cm.getLine(++iter.line); - return true; - } - function prevLine(iter) { - if (iter.line <= iter.min) return; - iter.text = iter.cm.getLine(--iter.line); - iter.ch = iter.text.length; - return true; - } - - function toTagEnd(iter) { - for (;;) { - var gt = iter.text.indexOf(">", iter.ch); - if (gt == -1) { if (nextLine(iter)) continue; else return; } - if (!tagAt(iter, gt + 1)) { iter.ch = gt + 1; continue; } - var lastSlash = iter.text.lastIndexOf("/", gt); - var selfClose = lastSlash > -1 && !/\S/.test(iter.text.slice(lastSlash + 1, gt)); - iter.ch = gt + 1; - return selfClose ? "selfClose" : "regular"; - } - } - function toTagStart(iter) { - for (;;) { - var lt = iter.ch ? iter.text.lastIndexOf("<", iter.ch - 1) : -1; - if (lt == -1) { if (prevLine(iter)) continue; else return; } - if (!tagAt(iter, lt + 1)) { iter.ch = lt; continue; } - xmlTagStart.lastIndex = lt; - iter.ch = lt; - var match = xmlTagStart.exec(iter.text); - if (match && match.index == lt) return match; - } - } - - function toNextTag(iter) { - for (;;) { - xmlTagStart.lastIndex = iter.ch; - var found = xmlTagStart.exec(iter.text); - if (!found) { if (nextLine(iter)) continue; else return; } - if (!tagAt(iter, found.index + 1)) { iter.ch = found.index + 1; continue; } - iter.ch = found.index + found[0].length; - return found; - } - } - function toPrevTag(iter) { - for (;;) { - var gt = iter.ch ? iter.text.lastIndexOf(">", iter.ch - 1) : -1; - if (gt == -1) { if (prevLine(iter)) continue; else return; } - if (!tagAt(iter, gt + 1)) { iter.ch = gt; continue; } - var lastSlash = iter.text.lastIndexOf("/", gt); - var selfClose = lastSlash > -1 && !/\S/.test(iter.text.slice(lastSlash + 1, gt)); - iter.ch = gt + 1; - return selfClose ? "selfClose" : "regular"; - } - } - - function findMatchingClose(iter, tag) { - var stack = []; - for (;;) { - var next = toNextTag(iter), end, startLine = iter.line, startCh = iter.ch - (next ? next[0].length : 0); - if (!next || !(end = toTagEnd(iter))) return; - if (end == "selfClose") continue; - if (next[1]) { // closing tag - for (var i = stack.length - 1; i >= 0; --i) if (stack[i] == next[2]) { - stack.length = i; - break; - } - if (i < 0 && (!tag || tag == next[2])) return { - tag: next[2], - from: Pos(startLine, startCh), - to: Pos(iter.line, iter.ch) - }; - } else { // opening tag - stack.push(next[2]); - } - } - } - function findMatchingOpen(iter, tag) { - var stack = []; - for (;;) { - var prev = toPrevTag(iter); - if (!prev) return; - if (prev == "selfClose") { toTagStart(iter); continue; } - var endLine = iter.line, endCh = iter.ch; - var start = toTagStart(iter); - if (!start) return; - if (start[1]) { // closing tag - stack.push(start[2]); - } else { // opening tag - for (var i = stack.length - 1; i >= 0; --i) if (stack[i] == start[2]) { - stack.length = i; - break; - } - if (i < 0 && (!tag || tag == start[2])) return { - tag: start[2], - from: Pos(iter.line, iter.ch), - to: Pos(endLine, endCh) - }; - } - } - } - - CodeMirror.registerHelper("fold", "xml", function(cm, start) { - var iter = new Iter(cm, start.line, 0); - for (;;) { - var openTag = toNextTag(iter) - if (!openTag || iter.line != start.line) return - var end = toTagEnd(iter) - if (!end) return - if (!openTag[1] && end != "selfClose") { - var startPos = Pos(iter.line, iter.ch); - var endPos = findMatchingClose(iter, openTag[2]); - return endPos && cmp(endPos.from, startPos) > 0 ? {from: startPos, to: endPos.from} : null - } - } - }); - CodeMirror.findMatchingTag = function(cm, pos, range) { - var iter = new Iter(cm, pos.line, pos.ch, range); - if (iter.text.indexOf(">") == -1 && iter.text.indexOf("<") == -1) return; - var end = toTagEnd(iter), to = end && Pos(iter.line, iter.ch); - var start = end && toTagStart(iter); - if (!end || !start || cmp(iter, pos) > 0) return; - var here = {from: Pos(iter.line, iter.ch), to: to, tag: start[2]}; - if (end == "selfClose") return {open: here, close: null, at: "open"}; - - if (start[1]) { // closing tag - return {open: findMatchingOpen(iter, start[2]), close: here, at: "close"}; - } else { // opening tag - iter = new Iter(cm, to.line, to.ch, range); - return {open: here, close: findMatchingClose(iter, start[2]), at: "open"}; - } - }; - - CodeMirror.findEnclosingTag = function(cm, pos, range, tag) { - var iter = new Iter(cm, pos.line, pos.ch, range); - for (;;) { - var open = findMatchingOpen(iter, tag); - if (!open) break; - var forward = new Iter(cm, pos.line, pos.ch, range); - var close = findMatchingClose(forward, open.tag); - if (close) return {open: open, close: close}; - } - }; - - // Used by addon/edit/closetag.js - CodeMirror.scanForClosingTag = function(cm, pos, name, end) { - var iter = new Iter(cm, pos.line, pos.ch, end ? {from: 0, to: end} : null); - return findMatchingClose(iter, name); - }; -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/hint/anyword-hint.js b/plugins/UiFileManager/media/codemirror/extension/hint/anyword-hint.js deleted file mode 100644 index d27a9ec0..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/hint/anyword-hint.js +++ /dev/null @@ -1,41 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - var WORD = /[\w$]+/, RANGE = 500; - - CodeMirror.registerHelper("hint", "anyword", function(editor, options) { - var word = options && options.word || WORD; - var range = options && options.range || RANGE; - var cur = editor.getCursor(), curLine = editor.getLine(cur.line); - var end = cur.ch, start = end; - while (start && word.test(curLine.charAt(start - 1))) --start; - var curWord = start != end && curLine.slice(start, end); - - var list = options && options.list || [], seen = {}; - var re = new RegExp(word.source, "g"); - for (var dir = -1; dir <= 1; dir += 2) { - var line = cur.line, endLine = Math.min(Math.max(line + dir * range, editor.firstLine()), editor.lastLine()) + dir; - for (; line != endLine; line += dir) { - var text = editor.getLine(line), m; - while (m = re.exec(text)) { - if (line == cur.line && m[0] === curWord) continue; - if ((!curWord || m[0].lastIndexOf(curWord, 0) == 0) && !Object.prototype.hasOwnProperty.call(seen, m[0])) { - seen[m[0]] = true; - list.push(m[0]); - } - } - } - } - return {list: list, from: CodeMirror.Pos(cur.line, start), to: CodeMirror.Pos(cur.line, end)}; - }); -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/hint/html-hint.js b/plugins/UiFileManager/media/codemirror/extension/hint/html-hint.js deleted file mode 100644 index d0cca4f6..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/hint/html-hint.js +++ /dev/null @@ -1,350 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("./xml-hint")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "./xml-hint"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - var langs = "ab aa af ak sq am ar an hy as av ae ay az bm ba eu be bn bh bi bs br bg my ca ch ce ny zh cv kw co cr hr cs da dv nl dz en eo et ee fo fj fi fr ff gl ka de el gn gu ht ha he hz hi ho hu ia id ie ga ig ik io is it iu ja jv kl kn kr ks kk km ki rw ky kv kg ko ku kj la lb lg li ln lo lt lu lv gv mk mg ms ml mt mi mr mh mn na nv nb nd ne ng nn no ii nr oc oj cu om or os pa pi fa pl ps pt qu rm rn ro ru sa sc sd se sm sg sr gd sn si sk sl so st es su sw ss sv ta te tg th ti bo tk tl tn to tr ts tt tw ty ug uk ur uz ve vi vo wa cy wo fy xh yi yo za zu".split(" "); - var targets = ["_blank", "_self", "_top", "_parent"]; - var charsets = ["ascii", "utf-8", "utf-16", "latin1", "latin1"]; - var methods = ["get", "post", "put", "delete"]; - var encs = ["application/x-www-form-urlencoded", "multipart/form-data", "text/plain"]; - var media = ["all", "screen", "print", "embossed", "braille", "handheld", "print", "projection", "screen", "tty", "tv", "speech", - "3d-glasses", "resolution [>][<][=] [X]", "device-aspect-ratio: X/Y", "orientation:portrait", - "orientation:landscape", "device-height: [X]", "device-width: [X]"]; - var s = { attrs: {} }; // Simple tag, reused for a whole lot of tags - - var data = { - a: { - attrs: { - href: null, ping: null, type: null, - media: media, - target: targets, - hreflang: langs - } - }, - abbr: s, - acronym: s, - address: s, - applet: s, - area: { - attrs: { - alt: null, coords: null, href: null, target: null, ping: null, - media: media, hreflang: langs, type: null, - shape: ["default", "rect", "circle", "poly"] - } - }, - article: s, - aside: s, - audio: { - attrs: { - src: null, mediagroup: null, - crossorigin: ["anonymous", "use-credentials"], - preload: ["none", "metadata", "auto"], - autoplay: ["", "autoplay"], - loop: ["", "loop"], - controls: ["", "controls"] - } - }, - b: s, - base: { attrs: { href: null, target: targets } }, - basefont: s, - bdi: s, - bdo: s, - big: s, - blockquote: { attrs: { cite: null } }, - body: s, - br: s, - button: { - attrs: { - form: null, formaction: null, name: null, value: null, - autofocus: ["", "autofocus"], - disabled: ["", "autofocus"], - formenctype: encs, - formmethod: methods, - formnovalidate: ["", "novalidate"], - formtarget: targets, - type: ["submit", "reset", "button"] - } - }, - canvas: { attrs: { width: null, height: null } }, - caption: s, - center: s, - cite: s, - code: s, - col: { attrs: { span: null } }, - colgroup: { attrs: { span: null } }, - command: { - attrs: { - type: ["command", "checkbox", "radio"], - label: null, icon: null, radiogroup: null, command: null, title: null, - disabled: ["", "disabled"], - checked: ["", "checked"] - } - }, - data: { attrs: { value: null } }, - datagrid: { attrs: { disabled: ["", "disabled"], multiple: ["", "multiple"] } }, - datalist: { attrs: { data: null } }, - dd: s, - del: { attrs: { cite: null, datetime: null } }, - details: { attrs: { open: ["", "open"] } }, - dfn: s, - dir: s, - div: s, - dl: s, - dt: s, - em: s, - embed: { attrs: { src: null, type: null, width: null, height: null } }, - eventsource: { attrs: { src: null } }, - fieldset: { attrs: { disabled: ["", "disabled"], form: null, name: null } }, - figcaption: s, - figure: s, - font: s, - footer: s, - form: { - attrs: { - action: null, name: null, - "accept-charset": charsets, - autocomplete: ["on", "off"], - enctype: encs, - method: methods, - novalidate: ["", "novalidate"], - target: targets - } - }, - frame: s, - frameset: s, - h1: s, h2: s, h3: s, h4: s, h5: s, h6: s, - head: { - attrs: {}, - children: ["title", "base", "link", "style", "meta", "script", "noscript", "command"] - }, - header: s, - hgroup: s, - hr: s, - html: { - attrs: { manifest: null }, - children: ["head", "body"] - }, - i: s, - iframe: { - attrs: { - src: null, srcdoc: null, name: null, width: null, height: null, - sandbox: ["allow-top-navigation", "allow-same-origin", "allow-forms", "allow-scripts"], - seamless: ["", "seamless"] - } - }, - img: { - attrs: { - alt: null, src: null, ismap: null, usemap: null, width: null, height: null, - crossorigin: ["anonymous", "use-credentials"] - } - }, - input: { - attrs: { - alt: null, dirname: null, form: null, formaction: null, - height: null, list: null, max: null, maxlength: null, min: null, - name: null, pattern: null, placeholder: null, size: null, src: null, - step: null, value: null, width: null, - accept: ["audio/*", "video/*", "image/*"], - autocomplete: ["on", "off"], - autofocus: ["", "autofocus"], - checked: ["", "checked"], - disabled: ["", "disabled"], - formenctype: encs, - formmethod: methods, - formnovalidate: ["", "novalidate"], - formtarget: targets, - multiple: ["", "multiple"], - readonly: ["", "readonly"], - required: ["", "required"], - type: ["hidden", "text", "search", "tel", "url", "email", "password", "datetime", "date", "month", - "week", "time", "datetime-local", "number", "range", "color", "checkbox", "radio", - "file", "submit", "image", "reset", "button"] - } - }, - ins: { attrs: { cite: null, datetime: null } }, - kbd: s, - keygen: { - attrs: { - challenge: null, form: null, name: null, - autofocus: ["", "autofocus"], - disabled: ["", "disabled"], - keytype: ["RSA"] - } - }, - label: { attrs: { "for": null, form: null } }, - legend: s, - li: { attrs: { value: null } }, - link: { - attrs: { - href: null, type: null, - hreflang: langs, - media: media, - sizes: ["all", "16x16", "16x16 32x32", "16x16 32x32 64x64"] - } - }, - map: { attrs: { name: null } }, - mark: s, - menu: { attrs: { label: null, type: ["list", "context", "toolbar"] } }, - meta: { - attrs: { - content: null, - charset: charsets, - name: ["viewport", "application-name", "author", "description", "generator", "keywords"], - "http-equiv": ["content-language", "content-type", "default-style", "refresh"] - } - }, - meter: { attrs: { value: null, min: null, low: null, high: null, max: null, optimum: null } }, - nav: s, - noframes: s, - noscript: s, - object: { - attrs: { - data: null, type: null, name: null, usemap: null, form: null, width: null, height: null, - typemustmatch: ["", "typemustmatch"] - } - }, - ol: { attrs: { reversed: ["", "reversed"], start: null, type: ["1", "a", "A", "i", "I"] } }, - optgroup: { attrs: { disabled: ["", "disabled"], label: null } }, - option: { attrs: { disabled: ["", "disabled"], label: null, selected: ["", "selected"], value: null } }, - output: { attrs: { "for": null, form: null, name: null } }, - p: s, - param: { attrs: { name: null, value: null } }, - pre: s, - progress: { attrs: { value: null, max: null } }, - q: { attrs: { cite: null } }, - rp: s, - rt: s, - ruby: s, - s: s, - samp: s, - script: { - attrs: { - type: ["text/javascript"], - src: null, - async: ["", "async"], - defer: ["", "defer"], - charset: charsets - } - }, - section: s, - select: { - attrs: { - form: null, name: null, size: null, - autofocus: ["", "autofocus"], - disabled: ["", "disabled"], - multiple: ["", "multiple"] - } - }, - small: s, - source: { attrs: { src: null, type: null, media: null } }, - span: s, - strike: s, - strong: s, - style: { - attrs: { - type: ["text/css"], - media: media, - scoped: null - } - }, - sub: s, - summary: s, - sup: s, - table: s, - tbody: s, - td: { attrs: { colspan: null, rowspan: null, headers: null } }, - textarea: { - attrs: { - dirname: null, form: null, maxlength: null, name: null, placeholder: null, - rows: null, cols: null, - autofocus: ["", "autofocus"], - disabled: ["", "disabled"], - readonly: ["", "readonly"], - required: ["", "required"], - wrap: ["soft", "hard"] - } - }, - tfoot: s, - th: { attrs: { colspan: null, rowspan: null, headers: null, scope: ["row", "col", "rowgroup", "colgroup"] } }, - thead: s, - time: { attrs: { datetime: null } }, - title: s, - tr: s, - track: { - attrs: { - src: null, label: null, "default": null, - kind: ["subtitles", "captions", "descriptions", "chapters", "metadata"], - srclang: langs - } - }, - tt: s, - u: s, - ul: s, - "var": s, - video: { - attrs: { - src: null, poster: null, width: null, height: null, - crossorigin: ["anonymous", "use-credentials"], - preload: ["auto", "metadata", "none"], - autoplay: ["", "autoplay"], - mediagroup: ["movie"], - muted: ["", "muted"], - controls: ["", "controls"] - } - }, - wbr: s - }; - - var globalAttrs = { - accesskey: ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], - "class": null, - contenteditable: ["true", "false"], - contextmenu: null, - dir: ["ltr", "rtl", "auto"], - draggable: ["true", "false", "auto"], - dropzone: ["copy", "move", "link", "string:", "file:"], - hidden: ["hidden"], - id: null, - inert: ["inert"], - itemid: null, - itemprop: null, - itemref: null, - itemscope: ["itemscope"], - itemtype: null, - lang: ["en", "es"], - spellcheck: ["true", "false"], - autocorrect: ["true", "false"], - autocapitalize: ["true", "false"], - style: null, - tabindex: ["1", "2", "3", "4", "5", "6", "7", "8", "9"], - title: null, - translate: ["yes", "no"], - onclick: null, - rel: ["stylesheet", "alternate", "author", "bookmark", "help", "license", "next", "nofollow", "noreferrer", "prefetch", "prev", "search", "tag"] - }; - function populate(obj) { - for (var attr in globalAttrs) if (globalAttrs.hasOwnProperty(attr)) - obj.attrs[attr] = globalAttrs[attr]; - } - - populate(s); - for (var tag in data) if (data.hasOwnProperty(tag) && data[tag] != s) - populate(data[tag]); - - CodeMirror.htmlSchema = data; - function htmlHint(cm, options) { - var local = {schemaInfo: data}; - if (options) for (var opt in options) local[opt] = options[opt]; - return CodeMirror.hint.xml(cm, local); - } - CodeMirror.registerHelper("hint", "html", htmlHint); -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/hint/show-hint.css b/plugins/UiFileManager/media/codemirror/extension/hint/show-hint.css deleted file mode 100644 index 5617ccca..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/hint/show-hint.css +++ /dev/null @@ -1,36 +0,0 @@ -.CodeMirror-hints { - position: absolute; - z-index: 10; - overflow: hidden; - list-style: none; - - margin: 0; - padding: 2px; - - -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2); - -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2); - box-shadow: 2px 3px 5px rgba(0,0,0,.2); - border-radius: 3px; - border: 1px solid silver; - - background: white; - font-size: 90%; - font-family: monospace; - - max-height: 20em; - overflow-y: auto; -} - -.CodeMirror-hint { - margin: 0; - padding: 0 4px; - border-radius: 2px; - white-space: pre; - color: black; - cursor: pointer; -} - -li.CodeMirror-hint-active { - background: #08f; - color: white; -} diff --git a/plugins/UiFileManager/media/codemirror/extension/hint/show-hint.js b/plugins/UiFileManager/media/codemirror/extension/hint/show-hint.js deleted file mode 100644 index cd0d6a7b..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/hint/show-hint.js +++ /dev/null @@ -1,479 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - var HINT_ELEMENT_CLASS = "CodeMirror-hint"; - var ACTIVE_HINT_ELEMENT_CLASS = "CodeMirror-hint-active"; - - // This is the old interface, kept around for now to stay - // backwards-compatible. - CodeMirror.showHint = function(cm, getHints, options) { - if (!getHints) return cm.showHint(options); - if (options && options.async) getHints.async = true; - var newOpts = {hint: getHints}; - if (options) for (var prop in options) newOpts[prop] = options[prop]; - return cm.showHint(newOpts); - }; - - CodeMirror.defineExtension("showHint", function(options) { - options = parseOptions(this, this.getCursor("start"), options); - var selections = this.listSelections() - if (selections.length > 1) return; - // By default, don't allow completion when something is selected. - // A hint function can have a `supportsSelection` property to - // indicate that it can handle selections. - if (this.somethingSelected()) { - if (!options.hint.supportsSelection) return; - // Don't try with cross-line selections - for (var i = 0; i < selections.length; i++) - if (selections[i].head.line != selections[i].anchor.line) return; - } - - if (this.state.completionActive) this.state.completionActive.close(); - var completion = this.state.completionActive = new Completion(this, options); - if (!completion.options.hint) return; - - CodeMirror.signal(this, "startCompletion", this); - completion.update(true); - }); - - CodeMirror.defineExtension("closeHint", function() { - if (this.state.completionActive) this.state.completionActive.close() - }) - - function Completion(cm, options) { - this.cm = cm; - this.options = options; - this.widget = null; - this.debounce = 0; - this.tick = 0; - this.startPos = this.cm.getCursor("start"); - this.startLen = this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length; - - var self = this; - cm.on("cursorActivity", this.activityFunc = function() { self.cursorActivity(); }); - } - - var requestAnimationFrame = window.requestAnimationFrame || function(fn) { - return setTimeout(fn, 1000/60); - }; - var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout; - - Completion.prototype = { - close: function() { - if (!this.active()) return; - this.cm.state.completionActive = null; - this.tick = null; - this.cm.off("cursorActivity", this.activityFunc); - - if (this.widget && this.data) CodeMirror.signal(this.data, "close"); - if (this.widget) this.widget.close(); - CodeMirror.signal(this.cm, "endCompletion", this.cm); - }, - - active: function() { - return this.cm.state.completionActive == this; - }, - - pick: function(data, i) { - var completion = data.list[i], self = this; - this.cm.operation(function() { - if (completion.hint) - completion.hint(self.cm, data, completion); - else - self.cm.replaceRange(getText(completion), completion.from || data.from, - completion.to || data.to, "complete"); - CodeMirror.signal(data, "pick", completion); - self.cm.scrollIntoView(); - }) - this.close(); - }, - - cursorActivity: function() { - if (this.debounce) { - cancelAnimationFrame(this.debounce); - this.debounce = 0; - } - - var identStart = this.startPos; - if(this.data) { - identStart = this.data.from; - } - - var pos = this.cm.getCursor(), line = this.cm.getLine(pos.line); - if (pos.line != this.startPos.line || line.length - pos.ch != this.startLen - this.startPos.ch || - pos.ch < identStart.ch || this.cm.somethingSelected() || - (!pos.ch || this.options.closeCharacters.test(line.charAt(pos.ch - 1)))) { - this.close(); - } else { - var self = this; - this.debounce = requestAnimationFrame(function() {self.update();}); - if (this.widget) this.widget.disable(); - } - }, - - update: function(first) { - if (this.tick == null) return - var self = this, myTick = ++this.tick - fetchHints(this.options.hint, this.cm, this.options, function(data) { - if (self.tick == myTick) self.finishUpdate(data, first) - }) - }, - - finishUpdate: function(data, first) { - if (this.data) CodeMirror.signal(this.data, "update"); - - var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle); - if (this.widget) this.widget.close(); - - this.data = data; - - if (data && data.list.length) { - if (picked && data.list.length == 1) { - this.pick(data, 0); - } else { - this.widget = new Widget(this, data); - CodeMirror.signal(data, "shown"); - } - } - } - }; - - function parseOptions(cm, pos, options) { - var editor = cm.options.hintOptions; - var out = {}; - for (var prop in defaultOptions) out[prop] = defaultOptions[prop]; - if (editor) for (var prop in editor) - if (editor[prop] !== undefined) out[prop] = editor[prop]; - if (options) for (var prop in options) - if (options[prop] !== undefined) out[prop] = options[prop]; - if (out.hint.resolve) out.hint = out.hint.resolve(cm, pos) - return out; - } - - function getText(completion) { - if (typeof completion == "string") return completion; - else return completion.text; - } - - function buildKeyMap(completion, handle) { - var baseMap = { - Up: function() {handle.moveFocus(-1);}, - Down: function() {handle.moveFocus(1);}, - PageUp: function() {handle.moveFocus(-handle.menuSize() + 1, true);}, - PageDown: function() {handle.moveFocus(handle.menuSize() - 1, true);}, - Home: function() {handle.setFocus(0);}, - End: function() {handle.setFocus(handle.length - 1);}, - Enter: handle.pick, - Tab: handle.pick, - Esc: handle.close - }; - - var mac = /Mac/.test(navigator.platform); - - if (mac) { - baseMap["Ctrl-P"] = function() {handle.moveFocus(-1);}; - baseMap["Ctrl-N"] = function() {handle.moveFocus(1);}; - } - - var custom = completion.options.customKeys; - var ourMap = custom ? {} : baseMap; - function addBinding(key, val) { - var bound; - if (typeof val != "string") - bound = function(cm) { return val(cm, handle); }; - // This mechanism is deprecated - else if (baseMap.hasOwnProperty(val)) - bound = baseMap[val]; - else - bound = val; - ourMap[key] = bound; - } - if (custom) - for (var key in custom) if (custom.hasOwnProperty(key)) - addBinding(key, custom[key]); - var extra = completion.options.extraKeys; - if (extra) - for (var key in extra) if (extra.hasOwnProperty(key)) - addBinding(key, extra[key]); - return ourMap; - } - - function getHintElement(hintsElement, el) { - while (el && el != hintsElement) { - if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el; - el = el.parentNode; - } - } - - function Widget(completion, data) { - this.completion = completion; - this.data = data; - this.picked = false; - var widget = this, cm = completion.cm; - var ownerDocument = cm.getInputField().ownerDocument; - var parentWindow = ownerDocument.defaultView || ownerDocument.parentWindow; - - var hints = this.hints = ownerDocument.createElement("ul"); - var theme = completion.cm.options.theme; - hints.className = "CodeMirror-hints " + theme; - this.selectedHint = data.selectedHint || 0; - - var completions = data.list; - for (var i = 0; i < completions.length; ++i) { - var elt = hints.appendChild(ownerDocument.createElement("li")), cur = completions[i]; - var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS); - if (cur.className != null) className = cur.className + " " + className; - elt.className = className; - if (cur.render) cur.render(elt, data, cur); - else elt.appendChild(ownerDocument.createTextNode(cur.displayText || getText(cur))); - elt.hintId = i; - } - - var container = completion.options.container || ownerDocument.body; - var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null); - var left = pos.left, top = pos.bottom, below = true; - var offsetLeft = 0, offsetTop = 0; - if (container !== ownerDocument.body) { - // We offset the cursor position because left and top are relative to the offsetParent's top left corner. - var isContainerPositioned = ['absolute', 'relative', 'fixed'].indexOf(parentWindow.getComputedStyle(container).position) !== -1; - var offsetParent = isContainerPositioned ? container : container.offsetParent; - var offsetParentPosition = offsetParent.getBoundingClientRect(); - var bodyPosition = ownerDocument.body.getBoundingClientRect(); - offsetLeft = (offsetParentPosition.left - bodyPosition.left - offsetParent.scrollLeft); - offsetTop = (offsetParentPosition.top - bodyPosition.top - offsetParent.scrollTop); - } - hints.style.left = (left - offsetLeft) + "px"; - hints.style.top = (top - offsetTop) + "px"; - - // If we're at the edge of the screen, then we want the menu to appear on the left of the cursor. - var winW = parentWindow.innerWidth || Math.max(ownerDocument.body.offsetWidth, ownerDocument.documentElement.offsetWidth); - var winH = parentWindow.innerHeight || Math.max(ownerDocument.body.offsetHeight, ownerDocument.documentElement.offsetHeight); - container.appendChild(hints); - var box = hints.getBoundingClientRect(), overlapY = box.bottom - winH; - var scrolls = hints.scrollHeight > hints.clientHeight + 1 - var startScroll = cm.getScrollInfo(); - - if (overlapY > 0) { - var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top); - if (curTop - height > 0) { // Fits above cursor - hints.style.top = (top = pos.top - height - offsetTop) + "px"; - below = false; - } else if (height > winH) { - hints.style.height = (winH - 5) + "px"; - hints.style.top = (top = pos.bottom - box.top - offsetTop) + "px"; - var cursor = cm.getCursor(); - if (data.from.ch != cursor.ch) { - pos = cm.cursorCoords(cursor); - hints.style.left = (left = pos.left - offsetLeft) + "px"; - box = hints.getBoundingClientRect(); - } - } - } - var overlapX = box.right - winW; - if (overlapX > 0) { - if (box.right - box.left > winW) { - hints.style.width = (winW - 5) + "px"; - overlapX -= (box.right - box.left) - winW; - } - hints.style.left = (left = pos.left - overlapX - offsetLeft) + "px"; - } - if (scrolls) for (var node = hints.firstChild; node; node = node.nextSibling) - node.style.paddingRight = cm.display.nativeBarWidth + "px" - - cm.addKeyMap(this.keyMap = buildKeyMap(completion, { - moveFocus: function(n, avoidWrap) { widget.changeActive(widget.selectedHint + n, avoidWrap); }, - setFocus: function(n) { widget.changeActive(n); }, - menuSize: function() { return widget.screenAmount(); }, - length: completions.length, - close: function() { completion.close(); }, - pick: function() { widget.pick(); }, - data: data - })); - - if (completion.options.closeOnUnfocus) { - var closingOnBlur; - cm.on("blur", this.onBlur = function() { closingOnBlur = setTimeout(function() { completion.close(); }, 100); }); - cm.on("focus", this.onFocus = function() { clearTimeout(closingOnBlur); }); - } - - cm.on("scroll", this.onScroll = function() { - var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect(); - var newTop = top + startScroll.top - curScroll.top; - var point = newTop - (parentWindow.pageYOffset || (ownerDocument.documentElement || ownerDocument.body).scrollTop); - if (!below) point += hints.offsetHeight; - if (point <= editor.top || point >= editor.bottom) return completion.close(); - hints.style.top = newTop + "px"; - hints.style.left = (left + startScroll.left - curScroll.left) + "px"; - }); - - CodeMirror.on(hints, "dblclick", function(e) { - var t = getHintElement(hints, e.target || e.srcElement); - if (t && t.hintId != null) {widget.changeActive(t.hintId); widget.pick();} - }); - - CodeMirror.on(hints, "click", function(e) { - var t = getHintElement(hints, e.target || e.srcElement); - if (t && t.hintId != null) { - widget.changeActive(t.hintId); - if (completion.options.completeOnSingleClick) widget.pick(); - } - }); - - CodeMirror.on(hints, "mousedown", function() { - setTimeout(function(){cm.focus();}, 20); - }); - this.scrollToActive() - - CodeMirror.signal(data, "select", completions[this.selectedHint], hints.childNodes[this.selectedHint]); - return true; - } - - Widget.prototype = { - close: function() { - if (this.completion.widget != this) return; - this.completion.widget = null; - this.hints.parentNode.removeChild(this.hints); - this.completion.cm.removeKeyMap(this.keyMap); - - var cm = this.completion.cm; - if (this.completion.options.closeOnUnfocus) { - cm.off("blur", this.onBlur); - cm.off("focus", this.onFocus); - } - cm.off("scroll", this.onScroll); - }, - - disable: function() { - this.completion.cm.removeKeyMap(this.keyMap); - var widget = this; - this.keyMap = {Enter: function() { widget.picked = true; }}; - this.completion.cm.addKeyMap(this.keyMap); - }, - - pick: function() { - this.completion.pick(this.data, this.selectedHint); - }, - - changeActive: function(i, avoidWrap) { - if (i >= this.data.list.length) - i = avoidWrap ? this.data.list.length - 1 : 0; - else if (i < 0) - i = avoidWrap ? 0 : this.data.list.length - 1; - if (this.selectedHint == i) return; - var node = this.hints.childNodes[this.selectedHint]; - if (node) node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, ""); - node = this.hints.childNodes[this.selectedHint = i]; - node.className += " " + ACTIVE_HINT_ELEMENT_CLASS; - this.scrollToActive() - CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node); - }, - - scrollToActive: function() { - var margin = this.completion.options.scrollMargin || 0; - var node1 = this.hints.childNodes[Math.max(0, this.selectedHint - margin)]; - var node2 = this.hints.childNodes[Math.min(this.data.list.length - 1, this.selectedHint + margin)]; - var firstNode = this.hints.firstChild; - if (node1.offsetTop < this.hints.scrollTop) - this.hints.scrollTop = node1.offsetTop - firstNode.offsetTop; - else if (node2.offsetTop + node2.offsetHeight > this.hints.scrollTop + this.hints.clientHeight) - this.hints.scrollTop = node2.offsetTop + node2.offsetHeight - this.hints.clientHeight + firstNode.offsetTop; - }, - - screenAmount: function() { - return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1; - } - }; - - function applicableHelpers(cm, helpers) { - if (!cm.somethingSelected()) return helpers - var result = [] - for (var i = 0; i < helpers.length; i++) - if (helpers[i].supportsSelection) result.push(helpers[i]) - return result - } - - function fetchHints(hint, cm, options, callback) { - if (hint.async) { - hint(cm, callback, options) - } else { - var result = hint(cm, options) - if (result && result.then) result.then(callback) - else callback(result) - } - } - - function resolveAutoHints(cm, pos) { - var helpers = cm.getHelpers(pos, "hint"), words - if (helpers.length) { - var resolved = function(cm, callback, options) { - var app = applicableHelpers(cm, helpers); - function run(i) { - if (i == app.length) return callback(null) - fetchHints(app[i], cm, options, function(result) { - if (result && result.list.length > 0) callback(result) - else run(i + 1) - }) - } - run(0) - } - resolved.async = true - resolved.supportsSelection = true - return resolved - } else if (words = cm.getHelper(cm.getCursor(), "hintWords")) { - return function(cm) { return CodeMirror.hint.fromList(cm, {words: words}) } - } else if (CodeMirror.hint.anyword) { - return function(cm, options) { return CodeMirror.hint.anyword(cm, options) } - } else { - return function() {} - } - } - - CodeMirror.registerHelper("hint", "auto", { - resolve: resolveAutoHints - }); - - CodeMirror.registerHelper("hint", "fromList", function(cm, options) { - var cur = cm.getCursor(), token = cm.getTokenAt(cur) - var term, from = CodeMirror.Pos(cur.line, token.start), to = cur - if (token.start < cur.ch && /\w/.test(token.string.charAt(cur.ch - token.start - 1))) { - term = token.string.substr(0, cur.ch - token.start) - } else { - term = "" - from = cur - } - var found = []; - for (var i = 0; i < options.words.length; i++) { - var word = options.words[i]; - if (word.slice(0, term.length) == term) - found.push(word); - } - - if (found.length) return {list: found, from: from, to: to}; - }); - - CodeMirror.commands.autocomplete = CodeMirror.showHint; - - var defaultOptions = { - hint: CodeMirror.hint.auto, - completeSingle: true, - alignWithWord: true, - closeCharacters: /[\s()\[\]{};:>,]/, - closeOnUnfocus: true, - completeOnSingleClick: true, - container: null, - customKeys: null, - extraKeys: null - }; - - CodeMirror.defineOption("hintOptions", null); -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/hint/sql-hint.js b/plugins/UiFileManager/media/codemirror/extension/hint/sql-hint.js deleted file mode 100644 index de84707d..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/hint/sql-hint.js +++ /dev/null @@ -1,304 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("../../mode/sql/sql")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "../../mode/sql/sql"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - var tables; - var defaultTable; - var keywords; - var identifierQuote; - var CONS = { - QUERY_DIV: ";", - ALIAS_KEYWORD: "AS" - }; - var Pos = CodeMirror.Pos, cmpPos = CodeMirror.cmpPos; - - function isArray(val) { return Object.prototype.toString.call(val) == "[object Array]" } - - function getKeywords(editor) { - var mode = editor.doc.modeOption; - if (mode === "sql") mode = "text/x-sql"; - return CodeMirror.resolveMode(mode).keywords; - } - - function getIdentifierQuote(editor) { - var mode = editor.doc.modeOption; - if (mode === "sql") mode = "text/x-sql"; - return CodeMirror.resolveMode(mode).identifierQuote || "`"; - } - - function getText(item) { - return typeof item == "string" ? item : item.text; - } - - function wrapTable(name, value) { - if (isArray(value)) value = {columns: value} - if (!value.text) value.text = name - return value - } - - function parseTables(input) { - var result = {} - if (isArray(input)) { - for (var i = input.length - 1; i >= 0; i--) { - var item = input[i] - result[getText(item).toUpperCase()] = wrapTable(getText(item), item) - } - } else if (input) { - for (var name in input) - result[name.toUpperCase()] = wrapTable(name, input[name]) - } - return result - } - - function getTable(name) { - return tables[name.toUpperCase()] - } - - function shallowClone(object) { - var result = {}; - for (var key in object) if (object.hasOwnProperty(key)) - result[key] = object[key]; - return result; - } - - function match(string, word) { - var len = string.length; - var sub = getText(word).substr(0, len); - return string.toUpperCase() === sub.toUpperCase(); - } - - function addMatches(result, search, wordlist, formatter) { - if (isArray(wordlist)) { - for (var i = 0; i < wordlist.length; i++) - if (match(search, wordlist[i])) result.push(formatter(wordlist[i])) - } else { - for (var word in wordlist) if (wordlist.hasOwnProperty(word)) { - var val = wordlist[word] - if (!val || val === true) - val = word - else - val = val.displayText ? {text: val.text, displayText: val.displayText} : val.text - if (match(search, val)) result.push(formatter(val)) - } - } - } - - function cleanName(name) { - // Get rid name from identifierQuote and preceding dot(.) - if (name.charAt(0) == ".") { - name = name.substr(1); - } - // replace doublicated identifierQuotes with single identifierQuotes - // and remove single identifierQuotes - var nameParts = name.split(identifierQuote+identifierQuote); - for (var i = 0; i < nameParts.length; i++) - nameParts[i] = nameParts[i].replace(new RegExp(identifierQuote,"g"), ""); - return nameParts.join(identifierQuote); - } - - function insertIdentifierQuotes(name) { - var nameParts = getText(name).split("."); - for (var i = 0; i < nameParts.length; i++) - nameParts[i] = identifierQuote + - // doublicate identifierQuotes - nameParts[i].replace(new RegExp(identifierQuote,"g"), identifierQuote+identifierQuote) + - identifierQuote; - var escaped = nameParts.join("."); - if (typeof name == "string") return escaped; - name = shallowClone(name); - name.text = escaped; - return name; - } - - function nameCompletion(cur, token, result, editor) { - // Try to complete table, column names and return start position of completion - var useIdentifierQuotes = false; - var nameParts = []; - var start = token.start; - var cont = true; - while (cont) { - cont = (token.string.charAt(0) == "."); - useIdentifierQuotes = useIdentifierQuotes || (token.string.charAt(0) == identifierQuote); - - start = token.start; - nameParts.unshift(cleanName(token.string)); - - token = editor.getTokenAt(Pos(cur.line, token.start)); - if (token.string == ".") { - cont = true; - token = editor.getTokenAt(Pos(cur.line, token.start)); - } - } - - // Try to complete table names - var string = nameParts.join("."); - addMatches(result, string, tables, function(w) { - return useIdentifierQuotes ? insertIdentifierQuotes(w) : w; - }); - - // Try to complete columns from defaultTable - addMatches(result, string, defaultTable, function(w) { - return useIdentifierQuotes ? insertIdentifierQuotes(w) : w; - }); - - // Try to complete columns - string = nameParts.pop(); - var table = nameParts.join("."); - - var alias = false; - var aliasTable = table; - // Check if table is available. If not, find table by Alias - if (!getTable(table)) { - var oldTable = table; - table = findTableByAlias(table, editor); - if (table !== oldTable) alias = true; - } - - var columns = getTable(table); - if (columns && columns.columns) - columns = columns.columns; - - if (columns) { - addMatches(result, string, columns, function(w) { - var tableInsert = table; - if (alias == true) tableInsert = aliasTable; - if (typeof w == "string") { - w = tableInsert + "." + w; - } else { - w = shallowClone(w); - w.text = tableInsert + "." + w.text; - } - return useIdentifierQuotes ? insertIdentifierQuotes(w) : w; - }); - } - - return start; - } - - function eachWord(lineText, f) { - var words = lineText.split(/\s+/) - for (var i = 0; i < words.length; i++) - if (words[i]) f(words[i].replace(/[,;]/g, '')) - } - - function findTableByAlias(alias, editor) { - var doc = editor.doc; - var fullQuery = doc.getValue(); - var aliasUpperCase = alias.toUpperCase(); - var previousWord = ""; - var table = ""; - var separator = []; - var validRange = { - start: Pos(0, 0), - end: Pos(editor.lastLine(), editor.getLineHandle(editor.lastLine()).length) - }; - - //add separator - var indexOfSeparator = fullQuery.indexOf(CONS.QUERY_DIV); - while(indexOfSeparator != -1) { - separator.push(doc.posFromIndex(indexOfSeparator)); - indexOfSeparator = fullQuery.indexOf(CONS.QUERY_DIV, indexOfSeparator+1); - } - separator.unshift(Pos(0, 0)); - separator.push(Pos(editor.lastLine(), editor.getLineHandle(editor.lastLine()).text.length)); - - //find valid range - var prevItem = null; - var current = editor.getCursor() - for (var i = 0; i < separator.length; i++) { - if ((prevItem == null || cmpPos(current, prevItem) > 0) && cmpPos(current, separator[i]) <= 0) { - validRange = {start: prevItem, end: separator[i]}; - break; - } - prevItem = separator[i]; - } - - if (validRange.start) { - var query = doc.getRange(validRange.start, validRange.end, false); - - for (var i = 0; i < query.length; i++) { - var lineText = query[i]; - eachWord(lineText, function(word) { - var wordUpperCase = word.toUpperCase(); - if (wordUpperCase === aliasUpperCase && getTable(previousWord)) - table = previousWord; - if (wordUpperCase !== CONS.ALIAS_KEYWORD) - previousWord = word; - }); - if (table) break; - } - } - return table; - } - - CodeMirror.registerHelper("hint", "sql", function(editor, options) { - tables = parseTables(options && options.tables) - var defaultTableName = options && options.defaultTable; - var disableKeywords = options && options.disableKeywords; - defaultTable = defaultTableName && getTable(defaultTableName); - keywords = getKeywords(editor); - identifierQuote = getIdentifierQuote(editor); - - if (defaultTableName && !defaultTable) - defaultTable = findTableByAlias(defaultTableName, editor); - - defaultTable = defaultTable || []; - - if (defaultTable.columns) - defaultTable = defaultTable.columns; - - var cur = editor.getCursor(); - var result = []; - var token = editor.getTokenAt(cur), start, end, search; - if (token.end > cur.ch) { - token.end = cur.ch; - token.string = token.string.slice(0, cur.ch - token.start); - } - - if (token.string.match(/^[.`"'\w@][\w$#]*$/g)) { - search = token.string; - start = token.start; - end = token.end; - } else { - start = end = cur.ch; - search = ""; - } - if (search.charAt(0) == "." || search.charAt(0) == identifierQuote) { - start = nameCompletion(cur, token, result, editor); - } else { - var objectOrClass = function(w, className) { - if (typeof w === "object") { - w.className = className; - } else { - w = { text: w, className: className }; - } - return w; - }; - addMatches(result, search, defaultTable, function(w) { - return objectOrClass(w, "CodeMirror-hint-table CodeMirror-hint-default-table"); - }); - addMatches( - result, - search, - tables, function(w) { - return objectOrClass(w, "CodeMirror-hint-table"); - } - ); - if (!disableKeywords) - addMatches(result, search, keywords, function(w) { - return objectOrClass(w.toUpperCase(), "CodeMirror-hint-keyword"); - }); - } - - return {list: result, from: Pos(cur.line, start), to: Pos(cur.line, end)}; - }); -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/hint/xml-hint.js b/plugins/UiFileManager/media/codemirror/extension/hint/xml-hint.js deleted file mode 100644 index 7575b370..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/hint/xml-hint.js +++ /dev/null @@ -1,123 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - var Pos = CodeMirror.Pos; - - function matches(hint, typed, matchInMiddle) { - if (matchInMiddle) return hint.indexOf(typed) >= 0; - else return hint.lastIndexOf(typed, 0) == 0; - } - - function getHints(cm, options) { - var tags = options && options.schemaInfo; - var quote = (options && options.quoteChar) || '"'; - var matchInMiddle = options && options.matchInMiddle; - if (!tags) return; - var cur = cm.getCursor(), token = cm.getTokenAt(cur); - if (token.end > cur.ch) { - token.end = cur.ch; - token.string = token.string.slice(0, cur.ch - token.start); - } - var inner = CodeMirror.innerMode(cm.getMode(), token.state); - if (!inner.mode.xmlCurrentTag) return - var result = [], replaceToken = false, prefix; - var tag = /\btag\b/.test(token.type) && !/>$/.test(token.string); - var tagName = tag && /^\w/.test(token.string), tagStart; - - if (tagName) { - var before = cm.getLine(cur.line).slice(Math.max(0, token.start - 2), token.start); - var tagType = /<\/$/.test(before) ? "close" : /<$/.test(before) ? "open" : null; - if (tagType) tagStart = token.start - (tagType == "close" ? 2 : 1); - } else if (tag && token.string == "<") { - tagType = "open"; - } else if (tag && token.string == ""); - } else { - // Attribute completion - var curTag = tagInfo && tags[tagInfo.name], attrs = curTag && curTag.attrs; - var globalAttrs = tags["!attrs"]; - if (!attrs && !globalAttrs) return; - if (!attrs) { - attrs = globalAttrs; - } else if (globalAttrs) { // Combine tag-local and global attributes - var set = {}; - for (var nm in globalAttrs) if (globalAttrs.hasOwnProperty(nm)) set[nm] = globalAttrs[nm]; - for (var nm in attrs) if (attrs.hasOwnProperty(nm)) set[nm] = attrs[nm]; - attrs = set; - } - if (token.type == "string" || token.string == "=") { // A value - var before = cm.getRange(Pos(cur.line, Math.max(0, cur.ch - 60)), - Pos(cur.line, token.type == "string" ? token.start : token.end)); - var atName = before.match(/([^\s\u00a0=<>\"\']+)=$/), atValues; - if (!atName || !attrs.hasOwnProperty(atName[1]) || !(atValues = attrs[atName[1]])) return; - if (typeof atValues == 'function') atValues = atValues.call(this, cm); // Functions can be used to supply values for autocomplete widget - if (token.type == "string") { - prefix = token.string; - var n = 0; - if (/['"]/.test(token.string.charAt(0))) { - quote = token.string.charAt(0); - prefix = token.string.slice(1); - n++; - } - var len = token.string.length; - if (/['"]/.test(token.string.charAt(len - 1))) { - quote = token.string.charAt(len - 1); - prefix = token.string.substr(n, len - 2); - } - if (n) { // an opening quote - var line = cm.getLine(cur.line); - if (line.length > token.end && line.charAt(token.end) == quote) token.end++; // include a closing quote - } - replaceToken = true; - } - for (var i = 0; i < atValues.length; ++i) if (!prefix || matches(atValues[i], prefix, matchInMiddle)) - result.push(quote + atValues[i] + quote); - } else { // An attribute name - if (token.type == "attribute") { - prefix = token.string; - replaceToken = true; - } - for (var attr in attrs) if (attrs.hasOwnProperty(attr) && (!prefix || matches(attr, prefix, matchInMiddle))) - result.push(attr); - } - } - return { - list: result, - from: replaceToken ? Pos(cur.line, tagStart == null ? token.start : tagStart) : cur, - to: replaceToken ? Pos(cur.line, token.end) : cur - }; - } - - CodeMirror.registerHelper("hint", "xml", getHints); -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/lint/json-lint.js b/plugins/UiFileManager/media/codemirror/extension/lint/json-lint.js deleted file mode 100644 index ac1d6ec2..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/lint/json-lint.js +++ /dev/null @@ -1,40 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -// Depends on jsonlint.js from https://github.com/zaach/jsonlint - -// declare global: jsonlint - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -CodeMirror.registerHelper("lint", "json", function(text) { - var found = []; - if (!window.jsonlint) { - if (window.console) { - window.console.error("Error: window.jsonlint not defined, CodeMirror JSON linting cannot run."); - } - return found; - } - // for jsonlint's web dist jsonlint is exported as an object with a single property parser, of which parseError - // is a subproperty - var jsonlint = window.jsonlint.parser || window.jsonlint - jsonlint.parseError = function(str, hash) { - var loc = hash.loc; - found.push({from: CodeMirror.Pos(loc.first_line - 1, loc.first_column), - to: CodeMirror.Pos(loc.last_line - 1, loc.last_column), - message: str}); - }; - try { jsonlint.parse(text); } - catch(e) {} - return found; -}); - -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/lint/jsonlint.js b/plugins/UiFileManager/media/codemirror/extension/lint/jsonlint.js deleted file mode 100644 index f34759e8..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/lint/jsonlint.js +++ /dev/null @@ -1 +0,0 @@ -var jsonlint=function(){var a=!0,b=!1,c={},d=function(){var a={trace:function(){},yy:{},symbols_:{error:2,JSONString:3,STRING:4,JSONNumber:5,NUMBER:6,JSONNullLiteral:7,NULL:8,JSONBooleanLiteral:9,TRUE:10,FALSE:11,JSONText:12,JSONValue:13,EOF:14,JSONObject:15,JSONArray:16,"{":17,"}":18,JSONMemberList:19,JSONMember:20,":":21,",":22,"[":23,"]":24,JSONElementList:25,$accept:0,$end:1},terminals_:{2:"error",4:"STRING",6:"NUMBER",8:"NULL",10:"TRUE",11:"FALSE",14:"EOF",17:"{",18:"}",21:":",22:",",23:"[",24:"]"},productions_:[0,[3,1],[5,1],[7,1],[9,1],[9,1],[12,2],[13,1],[13,1],[13,1],[13,1],[13,1],[13,1],[15,2],[15,3],[20,3],[19,1],[19,3],[16,2],[16,3],[25,1],[25,3]],performAction:function(b,c,d,e,f,g,h){var i=g.length-1;switch(f){case 1:this.$=b.replace(/\\(\\|")/g,"$1").replace(/\\n/g,"\n").replace(/\\r/g,"\r").replace(/\\t/g," ").replace(/\\v/g," ").replace(/\\f/g,"\f").replace(/\\b/g,"\b");break;case 2:this.$=Number(b);break;case 3:this.$=null;break;case 4:this.$=!0;break;case 5:this.$=!1;break;case 6:return this.$=g[i-1];case 13:this.$={};break;case 14:this.$=g[i-1];break;case 15:this.$=[g[i-2],g[i]];break;case 16:this.$={},this.$[g[i][0]]=g[i][1];break;case 17:this.$=g[i-2],g[i-2][g[i][0]]=g[i][1];break;case 18:this.$=[];break;case 19:this.$=g[i-1];break;case 20:this.$=[g[i]];break;case 21:this.$=g[i-2],g[i-2].push(g[i])}},table:[{3:5,4:[1,12],5:6,6:[1,13],7:3,8:[1,9],9:4,10:[1,10],11:[1,11],12:1,13:2,15:7,16:8,17:[1,14],23:[1,15]},{1:[3]},{14:[1,16]},{14:[2,7],18:[2,7],22:[2,7],24:[2,7]},{14:[2,8],18:[2,8],22:[2,8],24:[2,8]},{14:[2,9],18:[2,9],22:[2,9],24:[2,9]},{14:[2,10],18:[2,10],22:[2,10],24:[2,10]},{14:[2,11],18:[2,11],22:[2,11],24:[2,11]},{14:[2,12],18:[2,12],22:[2,12],24:[2,12]},{14:[2,3],18:[2,3],22:[2,3],24:[2,3]},{14:[2,4],18:[2,4],22:[2,4],24:[2,4]},{14:[2,5],18:[2,5],22:[2,5],24:[2,5]},{14:[2,1],18:[2,1],21:[2,1],22:[2,1],24:[2,1]},{14:[2,2],18:[2,2],22:[2,2],24:[2,2]},{3:20,4:[1,12],18:[1,17],19:18,20:19},{3:5,4:[1,12],5:6,6:[1,13],7:3,8:[1,9],9:4,10:[1,10],11:[1,11],13:23,15:7,16:8,17:[1,14],23:[1,15],24:[1,21],25:22},{1:[2,6]},{14:[2,13],18:[2,13],22:[2,13],24:[2,13]},{18:[1,24],22:[1,25]},{18:[2,16],22:[2,16]},{21:[1,26]},{14:[2,18],18:[2,18],22:[2,18],24:[2,18]},{22:[1,28],24:[1,27]},{22:[2,20],24:[2,20]},{14:[2,14],18:[2,14],22:[2,14],24:[2,14]},{3:20,4:[1,12],20:29},{3:5,4:[1,12],5:6,6:[1,13],7:3,8:[1,9],9:4,10:[1,10],11:[1,11],13:30,15:7,16:8,17:[1,14],23:[1,15]},{14:[2,19],18:[2,19],22:[2,19],24:[2,19]},{3:5,4:[1,12],5:6,6:[1,13],7:3,8:[1,9],9:4,10:[1,10],11:[1,11],13:31,15:7,16:8,17:[1,14],23:[1,15]},{18:[2,17],22:[2,17]},{18:[2,15],22:[2,15]},{22:[2,21],24:[2,21]}],defaultActions:{16:[2,6]},parseError:function(b,c){throw new Error(b)},parse:function(b){function o(a){d.length=d.length-2*a,e.length=e.length-a,f.length=f.length-a}function p(){var a;return a=c.lexer.lex()||1,typeof a!="number"&&(a=c.symbols_[a]||a),a}var c=this,d=[0],e=[null],f=[],g=this.table,h="",i=0,j=0,k=0,l=2,m=1;this.lexer.setInput(b),this.lexer.yy=this.yy,this.yy.lexer=this.lexer,typeof this.lexer.yylloc=="undefined"&&(this.lexer.yylloc={});var n=this.lexer.yylloc;f.push(n),typeof this.yy.parseError=="function"&&(this.parseError=this.yy.parseError);var q,r,s,t,u,v,w={},x,y,z,A;for(;;){s=d[d.length-1],this.defaultActions[s]?t=this.defaultActions[s]:(q==null&&(q=p()),t=g[s]&&g[s][q]);if(typeof t=="undefined"||!t.length||!t[0]){if(!k){A=[];for(x in g[s])this.terminals_[x]&&x>2&&A.push("'"+this.terminals_[x]+"'");var B="";this.lexer.showPosition?B="Parse error on line "+(i+1)+":\n"+this.lexer.showPosition()+"\nExpecting "+A.join(", ")+", got '"+this.terminals_[q]+"'":B="Parse error on line "+(i+1)+": Unexpected "+(q==1?"end of input":"'"+(this.terminals_[q]||q)+"'"),this.parseError(B,{text:this.lexer.match,token:this.terminals_[q]||q,line:this.lexer.yylineno,loc:n,expected:A})}if(k==3){if(q==m)throw new Error(B||"Parsing halted.");j=this.lexer.yyleng,h=this.lexer.yytext,i=this.lexer.yylineno,n=this.lexer.yylloc,q=p()}for(;;){if(l.toString()in g[s])break;if(s==0)throw new Error(B||"Parsing halted.");o(1),s=d[d.length-1]}r=q,q=l,s=d[d.length-1],t=g[s]&&g[s][l],k=3}if(t[0]instanceof Array&&t.length>1)throw new Error("Parse Error: multiple actions possible at state: "+s+", token: "+q);switch(t[0]){case 1:d.push(q),e.push(this.lexer.yytext),f.push(this.lexer.yylloc),d.push(t[1]),q=null,r?(q=r,r=null):(j=this.lexer.yyleng,h=this.lexer.yytext,i=this.lexer.yylineno,n=this.lexer.yylloc,k>0&&k--);break;case 2:y=this.productions_[t[1]][1],w.$=e[e.length-y],w._$={first_line:f[f.length-(y||1)].first_line,last_line:f[f.length-1].last_line,first_column:f[f.length-(y||1)].first_column,last_column:f[f.length-1].last_column},v=this.performAction.call(w,h,j,i,this.yy,t[1],e,f);if(typeof v!="undefined")return v;y&&(d=d.slice(0,-1*y*2),e=e.slice(0,-1*y),f=f.slice(0,-1*y)),d.push(this.productions_[t[1]][0]),e.push(w.$),f.push(w._$),z=g[d[d.length-2]][d[d.length-1]],d.push(z);break;case 3:return!0}}return!0}},b=function(){var a={EOF:1,parseError:function(b,c){if(!this.yy.parseError)throw new Error(b);this.yy.parseError(b,c)},setInput:function(a){return this._input=a,this._more=this._less=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this},input:function(){var a=this._input[0];this.yytext+=a,this.yyleng++,this.match+=a,this.matched+=a;var b=a.match(/\n/);return b&&this.yylineno++,this._input=this._input.slice(1),a},unput:function(a){return this._input=a+this._input,this},more:function(){return this._more=!0,this},less:function(a){this._input=this.match.slice(a)+this._input},pastInput:function(){var a=this.matched.substr(0,this.matched.length-this.match.length);return(a.length>20?"...":"")+a.substr(-20).replace(/\n/g,"")},upcomingInput:function(){var a=this.match;return a.length<20&&(a+=this._input.substr(0,20-a.length)),(a.substr(0,20)+(a.length>20?"...":"")).replace(/\n/g,"")},showPosition:function(){var a=this.pastInput(),b=(new Array(a.length+1)).join("-");return a+this.upcomingInput()+"\n"+b+"^"},next:function(){if(this.done)return this.EOF;this._input||(this.done=!0);var a,b,c,d,e,f;this._more||(this.yytext="",this.match="");var g=this._currentRules();for(var h=0;hb[0].length)){b=c,d=h;if(!this.options.flex)break}}if(b){f=b[0].match(/\n.*/g),f&&(this.yylineno+=f.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:f?f[f.length-1].length-1:this.yylloc.last_column+b[0].length},this.yytext+=b[0],this.match+=b[0],this.yyleng=this.yytext.length,this._more=!1,this._input=this._input.slice(b[0].length),this.matched+=b[0],a=this.performAction.call(this,this.yy,this,g[d],this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1);if(a)return a;return}if(this._input==="")return this.EOF;this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){var b=this.next();return typeof b!="undefined"?b:this.lex()},begin:function(b){this.conditionStack.push(b)},popState:function(){return this.conditionStack.pop()},_currentRules:function(){return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules},topState:function(){return this.conditionStack[this.conditionStack.length-2]},pushState:function(b){this.begin(b)}};return a.options={},a.performAction=function(b,c,d,e){var f=e;switch(d){case 0:break;case 1:return 6;case 2:return c.yytext=c.yytext.substr(1,c.yyleng-2),4;case 3:return 17;case 4:return 18;case 5:return 23;case 6:return 24;case 7:return 22;case 8:return 21;case 9:return 10;case 10:return 11;case 11:return 8;case 12:return 14;case 13:return"INVALID"}},a.rules=[/^(?:\s+)/,/^(?:(-?([0-9]|[1-9][0-9]+))(\.[0-9]+)?([eE][-+]?[0-9]+)?\b)/,/^(?:"(?:\\[\\"bfnrt/]|\\u[a-fA-F0-9]{4}|[^\\\0-\x09\x0a-\x1f"])*")/,/^(?:\{)/,/^(?:\})/,/^(?:\[)/,/^(?:\])/,/^(?:,)/,/^(?::)/,/^(?:true\b)/,/^(?:false\b)/,/^(?:null\b)/,/^(?:$)/,/^(?:.)/],a.conditions={INITIAL:{rules:[0,1,2,3,4,5,6,7,8,9,10,11,12,13],inclusive:!0}},a}();return a.lexer=b,a}();return typeof a!="undefined"&&typeof c!="undefined"&&(c.parser=d,c.parse=function(){return d.parse.apply(d,arguments)},c.main=function(d){if(!d[1])throw new Error("Usage: "+d[0]+" FILE");if(typeof process!="undefined")var e=a("fs").readFileSync(a("path").join(process.cwd(),d[1]),"utf8");else var f=a("file").path(a("file").cwd()),e=f.join(d[1]).read({charset:"utf-8"});return c.parser.parse(e)},typeof b!="undefined"&&a.main===b&&c.main(typeof process!="undefined"?process.argv.slice(1):a("system").args)),c}(); \ No newline at end of file diff --git a/plugins/UiFileManager/media/codemirror/extension/lint/lint.css b/plugins/UiFileManager/media/codemirror/extension/lint/lint.css deleted file mode 100644 index f097cfe3..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/lint/lint.css +++ /dev/null @@ -1,73 +0,0 @@ -/* The lint marker gutter */ -.CodeMirror-lint-markers { - width: 16px; -} - -.CodeMirror-lint-tooltip { - background-color: #ffd; - border: 1px solid black; - border-radius: 4px 4px 4px 4px; - color: black; - font-family: monospace; - font-size: 10pt; - overflow: hidden; - padding: 2px 5px; - position: fixed; - white-space: pre; - white-space: pre-wrap; - z-index: 100; - max-width: 600px; - opacity: 0; - transition: opacity .4s; - -moz-transition: opacity .4s; - -webkit-transition: opacity .4s; - -o-transition: opacity .4s; - -ms-transition: opacity .4s; -} - -.CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning { - background-position: left bottom; - background-repeat: repeat-x; -} - -.CodeMirror-lint-mark-error { - background-image: - url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==") - ; -} - -.CodeMirror-lint-mark-warning { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII="); -} - -.CodeMirror-lint-marker-error, .CodeMirror-lint-marker-warning { - background-position: center center; - background-repeat: no-repeat; - cursor: pointer; - display: inline-block; - height: 16px; - width: 16px; - vertical-align: middle; - position: relative; -} - -.CodeMirror-lint-message-error, .CodeMirror-lint-message-warning { - padding-left: 18px; - background-position: top left; - background-repeat: no-repeat; -} - -.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII="); -} - -.CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII="); -} - -.CodeMirror-lint-marker-multiple { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC"); - background-repeat: no-repeat; - background-position: right bottom; - width: 100%; height: 100%; -} diff --git a/plugins/UiFileManager/media/codemirror/extension/lint/lint.js b/plugins/UiFileManager/media/codemirror/extension/lint/lint.js deleted file mode 100644 index 5bc1af18..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/lint/lint.js +++ /dev/null @@ -1,255 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - var GUTTER_ID = "CodeMirror-lint-markers"; - - function showTooltip(cm, e, content) { - var tt = document.createElement("div"); - tt.className = "CodeMirror-lint-tooltip cm-s-" + cm.options.theme; - tt.appendChild(content.cloneNode(true)); - if (cm.state.lint.options.selfContain) - cm.getWrapperElement().appendChild(tt); - else - document.body.appendChild(tt); - - function position(e) { - if (!tt.parentNode) return CodeMirror.off(document, "mousemove", position); - tt.style.top = Math.max(0, e.clientY - tt.offsetHeight - 5) + "px"; - tt.style.left = (e.clientX + 5) + "px"; - } - CodeMirror.on(document, "mousemove", position); - position(e); - if (tt.style.opacity != null) tt.style.opacity = 1; - return tt; - } - function rm(elt) { - if (elt.parentNode) elt.parentNode.removeChild(elt); - } - function hideTooltip(tt) { - if (!tt.parentNode) return; - if (tt.style.opacity == null) rm(tt); - tt.style.opacity = 0; - setTimeout(function() { rm(tt); }, 600); - } - - function showTooltipFor(cm, e, content, node) { - var tooltip = showTooltip(cm, e, content); - function hide() { - CodeMirror.off(node, "mouseout", hide); - if (tooltip) { hideTooltip(tooltip); tooltip = null; } - } - var poll = setInterval(function() { - if (tooltip) for (var n = node;; n = n.parentNode) { - if (n && n.nodeType == 11) n = n.host; - if (n == document.body) return; - if (!n) { hide(); break; } - } - if (!tooltip) return clearInterval(poll); - }, 400); - CodeMirror.on(node, "mouseout", hide); - } - - function LintState(cm, options, hasGutter) { - this.marked = []; - this.options = options; - this.timeout = null; - this.hasGutter = hasGutter; - this.onMouseOver = function(e) { onMouseOver(cm, e); }; - this.waitingFor = 0 - } - - function parseOptions(_cm, options) { - if (options instanceof Function) return {getAnnotations: options}; - if (!options || options === true) options = {}; - return options; - } - - function clearMarks(cm) { - var state = cm.state.lint; - if (state.hasGutter) cm.clearGutter(GUTTER_ID); - for (var i = 0; i < state.marked.length; ++i) - state.marked[i].clear(); - state.marked.length = 0; - } - - function makeMarker(cm, labels, severity, multiple, tooltips) { - var marker = document.createElement("div"), inner = marker; - marker.className = "CodeMirror-lint-marker-" + severity; - if (multiple) { - inner = marker.appendChild(document.createElement("div")); - inner.className = "CodeMirror-lint-marker-multiple"; - } - - if (tooltips != false) CodeMirror.on(inner, "mouseover", function(e) { - showTooltipFor(cm, e, labels, inner); - }); - - return marker; - } - - function getMaxSeverity(a, b) { - if (a == "error") return a; - else return b; - } - - function groupByLine(annotations) { - var lines = []; - for (var i = 0; i < annotations.length; ++i) { - var ann = annotations[i], line = ann.from.line; - (lines[line] || (lines[line] = [])).push(ann); - } - return lines; - } - - function annotationTooltip(ann) { - var severity = ann.severity; - if (!severity) severity = "error"; - var tip = document.createElement("div"); - tip.className = "CodeMirror-lint-message-" + severity; - if (typeof ann.messageHTML != 'undefined') { - tip.innerHTML = ann.messageHTML; - } else { - tip.appendChild(document.createTextNode(ann.message)); - } - return tip; - } - - function lintAsync(cm, getAnnotations, passOptions) { - var state = cm.state.lint - var id = ++state.waitingFor - function abort() { - id = -1 - cm.off("change", abort) - } - cm.on("change", abort) - getAnnotations(cm.getValue(), function(annotations, arg2) { - cm.off("change", abort) - if (state.waitingFor != id) return - if (arg2 && annotations instanceof CodeMirror) annotations = arg2 - cm.operation(function() {updateLinting(cm, annotations)}) - }, passOptions, cm); - } - - function startLinting(cm) { - var state = cm.state.lint, options = state.options; - /* - * Passing rules in `options` property prevents JSHint (and other linters) from complaining - * about unrecognized rules like `onUpdateLinting`, `delay`, `lintOnChange`, etc. - */ - var passOptions = options.options || options; - var getAnnotations = options.getAnnotations || cm.getHelper(CodeMirror.Pos(0, 0), "lint"); - if (!getAnnotations) return; - if (options.async || getAnnotations.async) { - lintAsync(cm, getAnnotations, passOptions) - } else { - var annotations = getAnnotations(cm.getValue(), passOptions, cm); - if (!annotations) return; - if (annotations.then) annotations.then(function(issues) { - cm.operation(function() {updateLinting(cm, issues)}) - }); - else cm.operation(function() {updateLinting(cm, annotations)}) - } - } - - function updateLinting(cm, annotationsNotSorted) { - clearMarks(cm); - var state = cm.state.lint, options = state.options; - - var annotations = groupByLine(annotationsNotSorted); - - for (var line = 0; line < annotations.length; ++line) { - var anns = annotations[line]; - if (!anns) continue; - - var maxSeverity = null; - var tipLabel = state.hasGutter && document.createDocumentFragment(); - - for (var i = 0; i < anns.length; ++i) { - var ann = anns[i]; - var severity = ann.severity; - if (!severity) severity = "error"; - maxSeverity = getMaxSeverity(maxSeverity, severity); - - if (options.formatAnnotation) ann = options.formatAnnotation(ann); - if (state.hasGutter) tipLabel.appendChild(annotationTooltip(ann)); - - if (ann.to) state.marked.push(cm.markText(ann.from, ann.to, { - className: "CodeMirror-lint-mark-" + severity, - __annotation: ann - })); - } - - if (state.hasGutter) - cm.setGutterMarker(line, GUTTER_ID, makeMarker(cm, tipLabel, maxSeverity, anns.length > 1, - state.options.tooltips)); - } - if (options.onUpdateLinting) options.onUpdateLinting(annotationsNotSorted, annotations, cm); - } - - function onChange(cm) { - var state = cm.state.lint; - if (!state) return; - clearTimeout(state.timeout); - state.timeout = setTimeout(function(){startLinting(cm);}, state.options.delay || 500); - } - - function popupTooltips(cm, annotations, e) { - var target = e.target || e.srcElement; - var tooltip = document.createDocumentFragment(); - for (var i = 0; i < annotations.length; i++) { - var ann = annotations[i]; - tooltip.appendChild(annotationTooltip(ann)); - } - showTooltipFor(cm, e, tooltip, target); - } - - function onMouseOver(cm, e) { - var target = e.target || e.srcElement; - if (!/\bCodeMirror-lint-mark-/.test(target.className)) return; - var box = target.getBoundingClientRect(), x = (box.left + box.right) / 2, y = (box.top + box.bottom) / 2; - var spans = cm.findMarksAt(cm.coordsChar({left: x, top: y}, "client")); - - var annotations = []; - for (var i = 0; i < spans.length; ++i) { - var ann = spans[i].__annotation; - if (ann) annotations.push(ann); - } - if (annotations.length) popupTooltips(cm, annotations, e); - } - - CodeMirror.defineOption("lint", false, function(cm, val, old) { - if (old && old != CodeMirror.Init) { - clearMarks(cm); - if (cm.state.lint.options.lintOnChange !== false) - cm.off("change", onChange); - CodeMirror.off(cm.getWrapperElement(), "mouseover", cm.state.lint.onMouseOver); - clearTimeout(cm.state.lint.timeout); - delete cm.state.lint; - } - - if (val) { - var gutters = cm.getOption("gutters"), hasLintGutter = false; - for (var i = 0; i < gutters.length; ++i) if (gutters[i] == GUTTER_ID) hasLintGutter = true; - var state = cm.state.lint = new LintState(cm, parseOptions(cm, val), hasLintGutter); - if (state.options.lintOnChange !== false) - cm.on("change", onChange); - if (state.options.tooltips != false && state.options.tooltips != "gutter") - CodeMirror.on(cm.getWrapperElement(), "mouseover", state.onMouseOver); - - startLinting(cm); - } - }); - - CodeMirror.defineExtension("performLint", function() { - if (this.state.lint) startLinting(this); - }); -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/mdn-like-custom.css b/plugins/UiFileManager/media/codemirror/extension/mdn-like-custom.css deleted file mode 100644 index 7a523119..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/mdn-like-custom.css +++ /dev/null @@ -1,44 +0,0 @@ -/* - MDN-LIKE Theme - Mozilla - Ported to CodeMirror by Peter Kroon - Report bugs/issues here: https://github.com/codemirror/CodeMirror/issues - GitHub: @peterkroon - - The mdn-like theme is inspired on the displayed code examples at: https://developer.mozilla.org/en-US/docs/Web/CSS/animation - -*/ -.cm-s-mdn-like.CodeMirror { color: #666; background-color: #fff; } -.cm-s-mdn-like div.CodeMirror-selected { background: #fefdb5; } -.cm-s-mdn-like .CodeMirror-line::selection, .cm-s-mdn-like .CodeMirror-line > span::selection, .cm-s-mdn-like .CodeMirror-line > span > span::selection { background: #fefdb5; } -.cm-s-mdn-like .CodeMirror-line::-moz-selection, .cm-s-mdn-like .CodeMirror-line > span::-moz-selection, .cm-s-mdn-like .CodeMirror-line > span > span::-moz-selection { background: #fefdb5; } - -.cm-s-mdn-like .CodeMirror-gutters { background: #f8f8f8; color: #333; } -.cm-s-mdn-like .CodeMirror-linenumber { color: #aaa; padding-left: 8px; } -.cm-s-mdn-like .CodeMirror-cursor { border-left: 2px solid #222; } - -.cm-s-mdn-like .cm-keyword { color: #6262FF; } -.cm-s-mdn-like .cm-atom { color: #F90; } -.cm-s-mdn-like .cm-number { color: #ca7841; } -.cm-s-mdn-like .cm-def { color: #8DA6CE; } -.cm-s-mdn-like span.cm-variable-2, .cm-s-mdn-like span.cm-tag { color: #690; } -.cm-s-mdn-like span.cm-variable-3, .cm-s-mdn-like span.cm-def, .cm-s-mdn-like span.cm-type { color: #07a; } - -.cm-s-mdn-like .cm-variable { color: #07a; } -.cm-s-mdn-like .cm-property { color: #905; } -.cm-s-mdn-like .cm-qualifier { color: #690; } - -.cm-s-mdn-like .cm-operator { color: #cda869; } -.cm-s-mdn-like .cm-comment { color:#777; font-weight:normal; } -.cm-s-mdn-like .cm-string { color:#07a; } -.cm-s-mdn-like .cm-string-2 { color:#bd6b18; } /*?*/ -.cm-s-mdn-like .cm-meta { color: #000; } /*?*/ -.cm-s-mdn-like .cm-builtin { color: #9B7536; } /*?*/ -.cm-s-mdn-like .cm-tag { color: #997643; } -.cm-s-mdn-like .cm-attribute { color: #d6bb6d; } /*?*/ -.cm-s-mdn-like .cm-header { color: #FF6400; } -.cm-s-mdn-like .cm-hr { color: #AEAEAE; } -.cm-s-mdn-like .cm-link { color:#ad9361; font-style:italic; text-decoration:none; } -.cm-s-mdn-like .cm-error { border-bottom: 1px solid red; } - -div.cm-s-mdn-like .CodeMirror-activeline-background { background: #efefff; } -div.cm-s-mdn-like span.CodeMirror-matchingbracket { outline:1px solid grey; color: inherit; } diff --git a/plugins/UiFileManager/media/codemirror/extension/scroll/annotatescrollbar.js b/plugins/UiFileManager/media/codemirror/extension/scroll/annotatescrollbar.js deleted file mode 100644 index 9fe61ec1..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/scroll/annotatescrollbar.js +++ /dev/null @@ -1,122 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - CodeMirror.defineExtension("annotateScrollbar", function(options) { - if (typeof options == "string") options = {className: options}; - return new Annotation(this, options); - }); - - CodeMirror.defineOption("scrollButtonHeight", 0); - - function Annotation(cm, options) { - this.cm = cm; - this.options = options; - this.buttonHeight = options.scrollButtonHeight || cm.getOption("scrollButtonHeight"); - this.annotations = []; - this.doRedraw = this.doUpdate = null; - this.div = cm.getWrapperElement().appendChild(document.createElement("div")); - this.div.style.cssText = "position: absolute; right: 0; top: 0; z-index: 7; pointer-events: none"; - this.computeScale(); - - function scheduleRedraw(delay) { - clearTimeout(self.doRedraw); - self.doRedraw = setTimeout(function() { self.redraw(); }, delay); - } - - var self = this; - cm.on("refresh", this.resizeHandler = function() { - clearTimeout(self.doUpdate); - self.doUpdate = setTimeout(function() { - if (self.computeScale()) scheduleRedraw(20); - }, 100); - }); - cm.on("markerAdded", this.resizeHandler); - cm.on("markerCleared", this.resizeHandler); - if (options.listenForChanges !== false) - cm.on("changes", this.changeHandler = function() { - scheduleRedraw(250); - }); - } - - Annotation.prototype.computeScale = function() { - var cm = this.cm; - var hScale = (cm.getWrapperElement().clientHeight - cm.display.barHeight - this.buttonHeight * 2) / - cm.getScrollerElement().scrollHeight - if (hScale != this.hScale) { - this.hScale = hScale; - return true; - } - }; - - Annotation.prototype.update = function(annotations) { - this.annotations = annotations; - this.redraw(); - }; - - Annotation.prototype.redraw = function(compute) { - if (compute !== false) this.computeScale(); - var cm = this.cm, hScale = this.hScale; - - var frag = document.createDocumentFragment(), anns = this.annotations; - - var wrapping = cm.getOption("lineWrapping"); - var singleLineH = wrapping && cm.defaultTextHeight() * 1.5; - var curLine = null, curLineObj = null; - function getY(pos, top) { - if (curLine != pos.line) { - curLine = pos.line; - curLineObj = cm.getLineHandle(curLine); - } - if ((curLineObj.widgets && curLineObj.widgets.length) || - (wrapping && curLineObj.height > singleLineH)) - return cm.charCoords(pos, "local")[top ? "top" : "bottom"]; - var topY = cm.heightAtLine(curLineObj, "local"); - return topY + (top ? 0 : curLineObj.height); - } - - var lastLine = cm.lastLine() - if (cm.display.barWidth) for (var i = 0, nextTop; i < anns.length; i++) { - var ann = anns[i]; - if (ann.to.line > lastLine) continue; - var top = nextTop || getY(ann.from, true) * hScale; - var bottom = getY(ann.to, false) * hScale; - while (i < anns.length - 1) { - if (anns[i + 1].to.line > lastLine) break; - nextTop = getY(anns[i + 1].from, true) * hScale; - if (nextTop > bottom + .9) break; - ann = anns[++i]; - bottom = getY(ann.to, false) * hScale; - } - if (bottom == top) continue; - var height = Math.max(bottom - top, 3); - - var elt = frag.appendChild(document.createElement("div")); - elt.style.cssText = "position: absolute; right: 0px; width: " + Math.max(cm.display.barWidth - 1, 2) + "px; top: " - + (top + this.buttonHeight) + "px; height: " + height + "px"; - elt.className = this.options.className; - if (ann.id) { - elt.setAttribute("annotation-id", ann.id); - } - } - this.div.textContent = ""; - this.div.appendChild(frag); - }; - - Annotation.prototype.clear = function() { - this.cm.off("refresh", this.resizeHandler); - this.cm.off("markerAdded", this.resizeHandler); - this.cm.off("markerCleared", this.resizeHandler); - if (this.changeHandler) this.cm.off("changes", this.changeHandler); - this.div.parentNode.removeChild(this.div); - }; -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/scroll/scrollpastend.js b/plugins/UiFileManager/media/codemirror/extension/scroll/scrollpastend.js deleted file mode 100644 index 2ed9d95e..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/scroll/scrollpastend.js +++ /dev/null @@ -1,48 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - CodeMirror.defineOption("scrollPastEnd", false, function(cm, val, old) { - if (old && old != CodeMirror.Init) { - cm.off("change", onChange); - cm.off("refresh", updateBottomMargin); - cm.display.lineSpace.parentNode.style.paddingBottom = ""; - cm.state.scrollPastEndPadding = null; - } - if (val) { - cm.on("change", onChange); - cm.on("refresh", updateBottomMargin); - updateBottomMargin(cm); - } - }); - - function onChange(cm, change) { - if (CodeMirror.changeEnd(change).line == cm.lastLine()) - updateBottomMargin(cm); - } - - function updateBottomMargin(cm) { - var padding = ""; - if (cm.lineCount() > 1) { - var totalH = cm.display.scroller.clientHeight - 30, - lastLineH = cm.getLineHandle(cm.lastLine()).height; - padding = (totalH - lastLineH) + "px"; - } - if (cm.state.scrollPastEndPadding != padding) { - cm.state.scrollPastEndPadding = padding; - cm.display.lineSpace.parentNode.style.paddingBottom = padding; - cm.off("refresh", updateBottomMargin); - cm.setSize(); - cm.on("refresh", updateBottomMargin); - } - } -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/scroll/simplescrollbars.css b/plugins/UiFileManager/media/codemirror/extension/scroll/simplescrollbars.css deleted file mode 100644 index 5eea7aa1..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/scroll/simplescrollbars.css +++ /dev/null @@ -1,66 +0,0 @@ -.CodeMirror-simplescroll-horizontal div, .CodeMirror-simplescroll-vertical div { - position: absolute; - background: #ccc; - -moz-box-sizing: border-box; - box-sizing: border-box; - border: 1px solid #bbb; - border-radius: 2px; -} - -.CodeMirror-simplescroll-horizontal, .CodeMirror-simplescroll-vertical { - position: absolute; - z-index: 6; - background: #eee; -} - -.CodeMirror-simplescroll-horizontal { - bottom: 0; left: 0; - height: 8px; -} -.CodeMirror-simplescroll-horizontal div { - bottom: 0; - height: 100%; -} - -.CodeMirror-simplescroll-vertical { - right: 0; top: 0; - width: 8px; -} -.CodeMirror-simplescroll-vertical div { - right: 0; - width: 100%; -} - - -.CodeMirror-overlayscroll .CodeMirror-scrollbar-filler, .CodeMirror-overlayscroll .CodeMirror-gutter-filler { - display: none; -} - -.CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div { - position: absolute; - background: #bcd; - border-radius: 3px; -} - -.CodeMirror-overlayscroll-horizontal, .CodeMirror-overlayscroll-vertical { - position: absolute; - z-index: 6; -} - -.CodeMirror-overlayscroll-horizontal { - bottom: 0; left: 0; - height: 6px; -} -.CodeMirror-overlayscroll-horizontal div { - bottom: 0; - height: 100%; -} - -.CodeMirror-overlayscroll-vertical { - right: 0; top: 0; - width: 6px; -} -.CodeMirror-overlayscroll-vertical div { - right: 0; - width: 100%; -} diff --git a/plugins/UiFileManager/media/codemirror/extension/scroll/simplescrollbars.js b/plugins/UiFileManager/media/codemirror/extension/scroll/simplescrollbars.js deleted file mode 100644 index 750a2bd3..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/scroll/simplescrollbars.js +++ /dev/null @@ -1,152 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - function Bar(cls, orientation, scroll) { - this.orientation = orientation; - this.scroll = scroll; - this.screen = this.total = this.size = 1; - this.pos = 0; - - this.node = document.createElement("div"); - this.node.className = cls + "-" + orientation; - this.inner = this.node.appendChild(document.createElement("div")); - - var self = this; - CodeMirror.on(this.inner, "mousedown", function(e) { - if (e.which != 1) return; - CodeMirror.e_preventDefault(e); - var axis = self.orientation == "horizontal" ? "pageX" : "pageY"; - var start = e[axis], startpos = self.pos; - function done() { - CodeMirror.off(document, "mousemove", move); - CodeMirror.off(document, "mouseup", done); - } - function move(e) { - if (e.which != 1) return done(); - self.moveTo(startpos + (e[axis] - start) * (self.total / self.size)); - } - CodeMirror.on(document, "mousemove", move); - CodeMirror.on(document, "mouseup", done); - }); - - CodeMirror.on(this.node, "click", function(e) { - CodeMirror.e_preventDefault(e); - var innerBox = self.inner.getBoundingClientRect(), where; - if (self.orientation == "horizontal") - where = e.clientX < innerBox.left ? -1 : e.clientX > innerBox.right ? 1 : 0; - else - where = e.clientY < innerBox.top ? -1 : e.clientY > innerBox.bottom ? 1 : 0; - self.moveTo(self.pos + where * self.screen); - }); - - function onWheel(e) { - var moved = CodeMirror.wheelEventPixels(e)[self.orientation == "horizontal" ? "x" : "y"]; - var oldPos = self.pos; - self.moveTo(self.pos + moved); - if (self.pos != oldPos) CodeMirror.e_preventDefault(e); - } - CodeMirror.on(this.node, "mousewheel", onWheel); - CodeMirror.on(this.node, "DOMMouseScroll", onWheel); - } - - Bar.prototype.setPos = function(pos, force) { - if (pos < 0) pos = 0; - if (pos > this.total - this.screen) pos = this.total - this.screen; - if (!force && pos == this.pos) return false; - this.pos = pos; - this.inner.style[this.orientation == "horizontal" ? "left" : "top"] = - (pos * (this.size / this.total)) + "px"; - return true - }; - - Bar.prototype.moveTo = function(pos) { - if (this.setPos(pos)) this.scroll(pos, this.orientation); - } - - var minButtonSize = 10; - - Bar.prototype.update = function(scrollSize, clientSize, barSize) { - var sizeChanged = this.screen != clientSize || this.total != scrollSize || this.size != barSize - if (sizeChanged) { - this.screen = clientSize; - this.total = scrollSize; - this.size = barSize; - } - - var buttonSize = this.screen * (this.size / this.total); - if (buttonSize < minButtonSize) { - this.size -= minButtonSize - buttonSize; - buttonSize = minButtonSize; - } - this.inner.style[this.orientation == "horizontal" ? "width" : "height"] = - buttonSize + "px"; - this.setPos(this.pos, sizeChanged); - }; - - function SimpleScrollbars(cls, place, scroll) { - this.addClass = cls; - this.horiz = new Bar(cls, "horizontal", scroll); - place(this.horiz.node); - this.vert = new Bar(cls, "vertical", scroll); - place(this.vert.node); - this.width = null; - } - - SimpleScrollbars.prototype.update = function(measure) { - if (this.width == null) { - var style = window.getComputedStyle ? window.getComputedStyle(this.horiz.node) : this.horiz.node.currentStyle; - if (style) this.width = parseInt(style.height); - } - var width = this.width || 0; - - var needsH = measure.scrollWidth > measure.clientWidth + 1; - var needsV = measure.scrollHeight > measure.clientHeight + 1; - this.vert.node.style.display = needsV ? "block" : "none"; - this.horiz.node.style.display = needsH ? "block" : "none"; - - if (needsV) { - this.vert.update(measure.scrollHeight, measure.clientHeight, - measure.viewHeight - (needsH ? width : 0)); - this.vert.node.style.bottom = needsH ? width + "px" : "0"; - } - if (needsH) { - this.horiz.update(measure.scrollWidth, measure.clientWidth, - measure.viewWidth - (needsV ? width : 0) - measure.barLeft); - this.horiz.node.style.right = needsV ? width + "px" : "0"; - this.horiz.node.style.left = measure.barLeft + "px"; - } - - return {right: needsV ? width : 0, bottom: needsH ? width : 0}; - }; - - SimpleScrollbars.prototype.setScrollTop = function(pos) { - this.vert.setPos(pos); - }; - - SimpleScrollbars.prototype.setScrollLeft = function(pos) { - this.horiz.setPos(pos); - }; - - SimpleScrollbars.prototype.clear = function() { - var parent = this.horiz.node.parentNode; - parent.removeChild(this.horiz.node); - parent.removeChild(this.vert.node); - }; - - CodeMirror.scrollbarModel.simple = function(place, scroll) { - return new SimpleScrollbars("CodeMirror-simplescroll", place, scroll); - }; - CodeMirror.scrollbarModel.overlay = function(place, scroll) { - return new SimpleScrollbars("CodeMirror-overlayscroll", place, scroll); - }; -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/search/jump-to-line.js b/plugins/UiFileManager/media/codemirror/extension/search/jump-to-line.js deleted file mode 100644 index 1f3526d2..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/search/jump-to-line.js +++ /dev/null @@ -1,50 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -// Defines jumpToLine command. Uses dialog.js if present. - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("../dialog/dialog")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "../dialog/dialog"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - function dialog(cm, text, shortText, deflt, f) { - if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true}); - else f(prompt(shortText, deflt)); - } - - function getJumpDialog(cm) { - return cm.phrase("Jump to line:") + ' ' + cm.phrase("(Use line:column or scroll% syntax)") + ''; - } - - function interpretLine(cm, string) { - var num = Number(string) - if (/^[-+]/.test(string)) return cm.getCursor().line + num - else return num - 1 - } - - CodeMirror.commands.jumpToLine = function(cm) { - var cur = cm.getCursor(); - dialog(cm, getJumpDialog(cm), cm.phrase("Jump to line:"), (cur.line + 1) + ":" + cur.ch, function(posStr) { - if (!posStr) return; - - var match; - if (match = /^\s*([\+\-]?\d+)\s*\:\s*(\d+)\s*$/.exec(posStr)) { - cm.setCursor(interpretLine(cm, match[1]), Number(match[2])) - } else if (match = /^\s*([\+\-]?\d+(\.\d+)?)\%\s*/.exec(posStr)) { - var line = Math.round(cm.lineCount() * Number(match[1]) / 100); - if (/^[-+]/.test(match[1])) line = cur.line + line + 1; - cm.setCursor(line - 1, cur.ch); - } else if (match = /^\s*\:?\s*([\+\-]?\d+)\s*/.exec(posStr)) { - cm.setCursor(interpretLine(cm, match[1]), cur.ch); - } - }); - }; - - CodeMirror.keyMap["default"]["Alt-G"] = "jumpToLine"; -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/search/match-highlighter.js b/plugins/UiFileManager/media/codemirror/extension/search/match-highlighter.js deleted file mode 100644 index 3a4a7ded..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/search/match-highlighter.js +++ /dev/null @@ -1,167 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -// Highlighting text that matches the selection -// -// Defines an option highlightSelectionMatches, which, when enabled, -// will style strings that match the selection throughout the -// document. -// -// The option can be set to true to simply enable it, or to a -// {minChars, style, wordsOnly, showToken, delay} object to explicitly -// configure it. minChars is the minimum amount of characters that should be -// selected for the behavior to occur, and style is the token style to -// apply to the matches. This will be prefixed by "cm-" to create an -// actual CSS class name. If wordsOnly is enabled, the matches will be -// highlighted only if the selected text is a word. showToken, when enabled, -// will cause the current token to be highlighted when nothing is selected. -// delay is used to specify how much time to wait, in milliseconds, before -// highlighting the matches. If annotateScrollbar is enabled, the occurences -// will be highlighted on the scrollbar via the matchesonscrollbar addon. - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("./matchesonscrollbar")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "./matchesonscrollbar"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - var defaults = { - style: "matchhighlight", - minChars: 2, - delay: 100, - wordsOnly: false, - annotateScrollbar: false, - showToken: false, - trim: true - } - - function State(options) { - this.options = {} - for (var name in defaults) - this.options[name] = (options && options.hasOwnProperty(name) ? options : defaults)[name] - this.overlay = this.timeout = null; - this.matchesonscroll = null; - this.active = false; - } - - CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) { - if (old && old != CodeMirror.Init) { - removeOverlay(cm); - clearTimeout(cm.state.matchHighlighter.timeout); - cm.state.matchHighlighter = null; - cm.off("cursorActivity", cursorActivity); - cm.off("focus", onFocus) - } - if (val) { - var state = cm.state.matchHighlighter = new State(val); - if (cm.hasFocus()) { - state.active = true - highlightMatches(cm) - } else { - cm.on("focus", onFocus) - } - cm.on("cursorActivity", cursorActivity); - } - }); - - function cursorActivity(cm) { - var state = cm.state.matchHighlighter; - if (state.active || cm.hasFocus()) scheduleHighlight(cm, state) - } - - function onFocus(cm) { - var state = cm.state.matchHighlighter - if (!state.active) { - state.active = true - scheduleHighlight(cm, state) - } - } - - function scheduleHighlight(cm, state) { - clearTimeout(state.timeout); - state.timeout = setTimeout(function() {highlightMatches(cm);}, state.options.delay); - } - - function addOverlay(cm, query, hasBoundary, style) { - var state = cm.state.matchHighlighter; - cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style)); - if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) { - var searchFor = hasBoundary ? new RegExp((/\w/.test(query.charAt(0)) ? "\\b" : "") + - query.replace(/[\\\[.+*?(){|^$]/g, "\\$&") + - (/\w/.test(query.charAt(query.length - 1)) ? "\\b" : "")) : query; - state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false, - {className: "CodeMirror-selection-highlight-scrollbar"}); - } - } - - function removeOverlay(cm) { - var state = cm.state.matchHighlighter; - if (state.overlay) { - cm.removeOverlay(state.overlay); - state.overlay = null; - if (state.matchesonscroll) { - state.matchesonscroll.clear(); - state.matchesonscroll = null; - } - } - } - - function highlightMatches(cm) { - cm.operation(function() { - var state = cm.state.matchHighlighter; - removeOverlay(cm); - if (!cm.somethingSelected() && state.options.showToken) { - var re = state.options.showToken === true ? /[\w$]/ : state.options.showToken; - var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start; - while (start && re.test(line.charAt(start - 1))) --start; - while (end < line.length && re.test(line.charAt(end))) ++end; - if (start < end) - addOverlay(cm, line.slice(start, end), re, state.options.style); - return; - } - var from = cm.getCursor("from"), to = cm.getCursor("to"); - if (from.line != to.line) return; - if (state.options.wordsOnly && !isWord(cm, from, to)) return; - var selection = cm.getRange(from, to) - if (state.options.trim) selection = selection.replace(/^\s+|\s+$/g, "") - if (selection.length >= state.options.minChars) - addOverlay(cm, selection, false, state.options.style); - }); - } - - function isWord(cm, from, to) { - var str = cm.getRange(from, to); - if (str.match(/^\w+$/) !== null) { - if (from.ch > 0) { - var pos = {line: from.line, ch: from.ch - 1}; - var chr = cm.getRange(pos, from); - if (chr.match(/\W/) === null) return false; - } - if (to.ch < cm.getLine(from.line).length) { - var pos = {line: to.line, ch: to.ch + 1}; - var chr = cm.getRange(to, pos); - if (chr.match(/\W/) === null) return false; - } - return true; - } else return false; - } - - function boundariesAround(stream, re) { - return (!stream.start || !re.test(stream.string.charAt(stream.start - 1))) && - (stream.pos == stream.string.length || !re.test(stream.string.charAt(stream.pos))); - } - - function makeOverlay(query, hasBoundary, style) { - return {token: function(stream) { - if (stream.match(query) && - (!hasBoundary || boundariesAround(stream, hasBoundary))) - return style; - stream.next(); - stream.skipTo(query.charAt(0)) || stream.skipToEnd(); - }}; - } -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/search/matchesonscrollbar.css b/plugins/UiFileManager/media/codemirror/extension/search/matchesonscrollbar.css deleted file mode 100644 index 77932cc9..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/search/matchesonscrollbar.css +++ /dev/null @@ -1,8 +0,0 @@ -.CodeMirror-search-match { - background: gold; - border-top: 1px solid orange; - border-bottom: 1px solid orange; - -moz-box-sizing: border-box; - box-sizing: border-box; - opacity: .5; -} diff --git a/plugins/UiFileManager/media/codemirror/extension/search/matchesonscrollbar.js b/plugins/UiFileManager/media/codemirror/extension/search/matchesonscrollbar.js deleted file mode 100644 index 8a4a8275..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/search/matchesonscrollbar.js +++ /dev/null @@ -1,97 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("./searchcursor"), require("../scroll/annotatescrollbar")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "./searchcursor", "../scroll/annotatescrollbar"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - CodeMirror.defineExtension("showMatchesOnScrollbar", function(query, caseFold, options) { - if (typeof options == "string") options = {className: options}; - if (!options) options = {}; - return new SearchAnnotation(this, query, caseFold, options); - }); - - function SearchAnnotation(cm, query, caseFold, options) { - this.cm = cm; - this.options = options; - var annotateOptions = {listenForChanges: false}; - for (var prop in options) annotateOptions[prop] = options[prop]; - if (!annotateOptions.className) annotateOptions.className = "CodeMirror-search-match"; - this.annotation = cm.annotateScrollbar(annotateOptions); - this.query = query; - this.caseFold = caseFold; - this.gap = {from: cm.firstLine(), to: cm.lastLine() + 1}; - this.matches = []; - this.update = null; - - this.findMatches(); - this.annotation.update(this.matches); - - var self = this; - cm.on("change", this.changeHandler = function(_cm, change) { self.onChange(change); }); - } - - var MAX_MATCHES = 1000; - - SearchAnnotation.prototype.findMatches = function() { - if (!this.gap) return; - for (var i = 0; i < this.matches.length; i++) { - var match = this.matches[i]; - if (match.from.line >= this.gap.to) break; - if (match.to.line >= this.gap.from) this.matches.splice(i--, 1); - } - var cursor = this.cm.getSearchCursor(this.query, CodeMirror.Pos(this.gap.from, 0), {caseFold: this.caseFold, multiline: this.options.multiline}); - var maxMatches = this.options && this.options.maxMatches || MAX_MATCHES; - while (cursor.findNext()) { - var match = {from: cursor.from(), to: cursor.to()}; - if (match.from.line >= this.gap.to) break; - this.matches.splice(i++, 0, match); - if (this.matches.length > maxMatches) break; - } - this.gap = null; - }; - - function offsetLine(line, changeStart, sizeChange) { - if (line <= changeStart) return line; - return Math.max(changeStart, line + sizeChange); - } - - SearchAnnotation.prototype.onChange = function(change) { - var startLine = change.from.line; - var endLine = CodeMirror.changeEnd(change).line; - var sizeChange = endLine - change.to.line; - if (this.gap) { - this.gap.from = Math.min(offsetLine(this.gap.from, startLine, sizeChange), change.from.line); - this.gap.to = Math.max(offsetLine(this.gap.to, startLine, sizeChange), change.from.line); - } else { - this.gap = {from: change.from.line, to: endLine + 1}; - } - - if (sizeChange) for (var i = 0; i < this.matches.length; i++) { - var match = this.matches[i]; - var newFrom = offsetLine(match.from.line, startLine, sizeChange); - if (newFrom != match.from.line) match.from = CodeMirror.Pos(newFrom, match.from.ch); - var newTo = offsetLine(match.to.line, startLine, sizeChange); - if (newTo != match.to.line) match.to = CodeMirror.Pos(newTo, match.to.ch); - } - clearTimeout(this.update); - var self = this; - this.update = setTimeout(function() { self.updateAfterChange(); }, 250); - }; - - SearchAnnotation.prototype.updateAfterChange = function() { - this.findMatches(); - this.annotation.update(this.matches); - }; - - SearchAnnotation.prototype.clear = function() { - this.cm.off("change", this.changeHandler); - this.annotation.clear(); - }; -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/search/search.js b/plugins/UiFileManager/media/codemirror/extension/search/search.js deleted file mode 100644 index cecdd52e..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/search/search.js +++ /dev/null @@ -1,260 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -// Define search commands. Depends on dialog.js or another -// implementation of the openDialog method. - -// Replace works a little oddly -- it will do the replace on the next -// Ctrl-G (or whatever is bound to findNext) press. You prevent a -// replace by making sure the match is no longer selected when hitting -// Ctrl-G. - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("./searchcursor"), require("../dialog/dialog")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "./searchcursor", "../dialog/dialog"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - function searchOverlay(query, caseInsensitive) { - if (typeof query == "string") - query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), caseInsensitive ? "gi" : "g"); - else if (!query.global) - query = new RegExp(query.source, query.ignoreCase ? "gi" : "g"); - - return {token: function(stream) { - query.lastIndex = stream.pos; - var match = query.exec(stream.string); - if (match && match.index == stream.pos) { - stream.pos += match[0].length || 1; - return "searching"; - } else if (match) { - stream.pos = match.index; - } else { - stream.skipToEnd(); - } - }}; - } - - function SearchState() { - this.posFrom = this.posTo = this.lastQuery = this.query = null; - this.overlay = null; - } - - function getSearchState(cm) { - return cm.state.search || (cm.state.search = new SearchState()); - } - - function queryCaseInsensitive(query) { - return typeof query == "string" && query == query.toLowerCase(); - } - - function getSearchCursor(cm, query, pos) { - // Heuristic: if the query string is all lowercase, do a case insensitive search. - return cm.getSearchCursor(query, pos, {caseFold: queryCaseInsensitive(query), multiline: true}); - } - - function persistentDialog(cm, text, deflt, onEnter, onKeyDown) { - cm.openDialog(text, onEnter, { - value: deflt, - selectValueOnOpen: true, - closeOnEnter: false, - onClose: function() { clearSearch(cm); }, - onKeyDown: onKeyDown - }); - } - - function dialog(cm, text, shortText, deflt, f) { - if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true}); - else f(prompt(shortText, deflt)); - } - - function confirmDialog(cm, text, shortText, fs) { - if (cm.openConfirm) cm.openConfirm(text, fs); - else if (confirm(shortText)) fs[0](); - } - - function parseString(string) { - return string.replace(/\\([nrt\\])/g, function(match, ch) { - if (ch == "n") return "\n" - if (ch == "r") return "\r" - if (ch == "t") return "\t" - if (ch == "\\") return "\\" - return match - }) - } - - function parseQuery(query) { - var isRE = query.match(/^\/(.*)\/([a-z]*)$/); - if (isRE) { - try { query = new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i"); } - catch(e) {} // Not a regular expression after all, do a string search - } else { - query = parseString(query) - } - if (typeof query == "string" ? query == "" : query.test("")) - query = /x^/; - return query; - } - - function startSearch(cm, state, query) { - state.queryText = query; - state.query = parseQuery(query); - cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query)); - state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query)); - cm.addOverlay(state.overlay); - if (cm.showMatchesOnScrollbar) { - if (state.annotate) { state.annotate.clear(); state.annotate = null; } - state.annotate = cm.showMatchesOnScrollbar(state.query, queryCaseInsensitive(state.query)); - } - } - - function doSearch(cm, rev, persistent, immediate) { - var state = getSearchState(cm); - if (state.query) return findNext(cm, rev); - var q = cm.getSelection() || state.lastQuery; - if (q instanceof RegExp && q.source == "x^") q = null - if (persistent && cm.openDialog) { - var hiding = null - var searchNext = function(query, event) { - CodeMirror.e_stop(event); - if (!query) return; - if (query != state.queryText) { - startSearch(cm, state, query); - state.posFrom = state.posTo = cm.getCursor(); - } - if (hiding) hiding.style.opacity = 1 - findNext(cm, event.shiftKey, function(_, to) { - var dialog - if (to.line < 3 && document.querySelector && - (dialog = cm.display.wrapper.querySelector(".CodeMirror-dialog")) && - dialog.getBoundingClientRect().bottom - 4 > cm.cursorCoords(to, "window").top) - (hiding = dialog).style.opacity = .4 - }) - }; - persistentDialog(cm, getQueryDialog(cm), q, searchNext, function(event, query) { - var keyName = CodeMirror.keyName(event) - var extra = cm.getOption('extraKeys'), cmd = (extra && extra[keyName]) || CodeMirror.keyMap[cm.getOption("keyMap")][keyName] - if (cmd == "findNext" || cmd == "findPrev" || - cmd == "findPersistentNext" || cmd == "findPersistentPrev") { - CodeMirror.e_stop(event); - startSearch(cm, getSearchState(cm), query); - cm.execCommand(cmd); - } else if (cmd == "find" || cmd == "findPersistent") { - CodeMirror.e_stop(event); - searchNext(query, event); - } - }); - if (immediate && q) { - startSearch(cm, state, q); - findNext(cm, rev); - } - } else { - dialog(cm, getQueryDialog(cm), "Search for:", q, function(query) { - if (query && !state.query) cm.operation(function() { - startSearch(cm, state, query); - state.posFrom = state.posTo = cm.getCursor(); - findNext(cm, rev); - }); - }); - } - } - - function findNext(cm, rev, callback) {cm.operation(function() { - var state = getSearchState(cm); - var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo); - if (!cursor.find(rev)) { - cursor = getSearchCursor(cm, state.query, rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0)); - if (!cursor.find(rev)) return; - } - cm.setSelection(cursor.from(), cursor.to()); - cm.scrollIntoView({from: cursor.from(), to: cursor.to()}, 20); - state.posFrom = cursor.from(); state.posTo = cursor.to(); - if (callback) callback(cursor.from(), cursor.to()) - });} - - function clearSearch(cm) {cm.operation(function() { - var state = getSearchState(cm); - state.lastQuery = state.query; - if (!state.query) return; - state.query = state.queryText = null; - cm.removeOverlay(state.overlay); - if (state.annotate) { state.annotate.clear(); state.annotate = null; } - });} - - - function getQueryDialog(cm) { - return '' + cm.phrase("Search:") + ' ' + cm.phrase("(Use /re/ syntax for regexp search)") + ''; - } - function getReplaceQueryDialog(cm) { - return ' ' + cm.phrase("(Use /re/ syntax for regexp search)") + ''; - } - function getReplacementQueryDialog(cm) { - return '' + cm.phrase("With:") + ' '; - } - function getDoReplaceConfirm(cm) { - return '' + cm.phrase("Replace?") + ' '; - } - - function replaceAll(cm, query, text) { - cm.operation(function() { - for (var cursor = getSearchCursor(cm, query); cursor.findNext();) { - if (typeof query != "string") { - var match = cm.getRange(cursor.from(), cursor.to()).match(query); - cursor.replace(text.replace(/\$(\d)/g, function(_, i) {return match[i];})); - } else cursor.replace(text); - } - }); - } - - function replace(cm, all) { - if (cm.getOption("readOnly")) return; - var query = cm.getSelection() || getSearchState(cm).lastQuery; - var dialogText = '' + (all ? cm.phrase("Replace all:") : cm.phrase("Replace:")) + ''; - dialog(cm, dialogText + getReplaceQueryDialog(cm), dialogText, query, function(query) { - if (!query) return; - query = parseQuery(query); - dialog(cm, getReplacementQueryDialog(cm), cm.phrase("Replace with:"), "", function(text) { - text = parseString(text) - if (all) { - replaceAll(cm, query, text) - } else { - clearSearch(cm); - var cursor = getSearchCursor(cm, query, cm.getCursor("from")); - var advance = function() { - var start = cursor.from(), match; - if (!(match = cursor.findNext())) { - cursor = getSearchCursor(cm, query); - if (!(match = cursor.findNext()) || - (start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return; - } - cm.setSelection(cursor.from(), cursor.to()); - cm.scrollIntoView({from: cursor.from(), to: cursor.to()}); - confirmDialog(cm, getDoReplaceConfirm(cm), cm.phrase("Replace?"), - [function() {doReplace(match);}, advance, - function() {replaceAll(cm, query, text)}]); - }; - var doReplace = function(match) { - cursor.replace(typeof query == "string" ? text : - text.replace(/\$(\d)/g, function(_, i) {return match[i];})); - advance(); - }; - advance(); - } - }); - }); - } - - CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);}; - CodeMirror.commands.findPersistent = function(cm) {clearSearch(cm); doSearch(cm, false, true);}; - CodeMirror.commands.findPersistentNext = function(cm) {doSearch(cm, false, true, true);}; - CodeMirror.commands.findPersistentPrev = function(cm) {doSearch(cm, true, true, true);}; - CodeMirror.commands.findNext = doSearch; - CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);}; - CodeMirror.commands.clearSearch = clearSearch; - CodeMirror.commands.replace = replace; - CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);}; -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/search/searchcursor.js b/plugins/UiFileManager/media/codemirror/extension/search/searchcursor.js deleted file mode 100644 index d5869578..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/search/searchcursor.js +++ /dev/null @@ -1,296 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")) - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod) - else // Plain browser env - mod(CodeMirror) -})(function(CodeMirror) { - "use strict" - var Pos = CodeMirror.Pos - - function regexpFlags(regexp) { - var flags = regexp.flags - return flags != null ? flags : (regexp.ignoreCase ? "i" : "") - + (regexp.global ? "g" : "") - + (regexp.multiline ? "m" : "") - } - - function ensureFlags(regexp, flags) { - var current = regexpFlags(regexp), target = current - for (var i = 0; i < flags.length; i++) if (target.indexOf(flags.charAt(i)) == -1) - target += flags.charAt(i) - return current == target ? regexp : new RegExp(regexp.source, target) - } - - function maybeMultiline(regexp) { - return /\\s|\\n|\n|\\W|\\D|\[\^/.test(regexp.source) - } - - function searchRegexpForward(doc, regexp, start) { - regexp = ensureFlags(regexp, "g") - for (var line = start.line, ch = start.ch, last = doc.lastLine(); line <= last; line++, ch = 0) { - regexp.lastIndex = ch - var string = doc.getLine(line), match = regexp.exec(string) - if (match) - return {from: Pos(line, match.index), - to: Pos(line, match.index + match[0].length), - match: match} - } - } - - function searchRegexpForwardMultiline(doc, regexp, start) { - if (!maybeMultiline(regexp)) return searchRegexpForward(doc, regexp, start) - - regexp = ensureFlags(regexp, "gm") - var string, chunk = 1 - for (var line = start.line, last = doc.lastLine(); line <= last;) { - // This grows the search buffer in exponentially-sized chunks - // between matches, so that nearby matches are fast and don't - // require concatenating the whole document (in case we're - // searching for something that has tons of matches), but at the - // same time, the amount of retries is limited. - for (var i = 0; i < chunk; i++) { - if (line > last) break - var curLine = doc.getLine(line++) - string = string == null ? curLine : string + "\n" + curLine - } - chunk = chunk * 2 - regexp.lastIndex = start.ch - var match = regexp.exec(string) - if (match) { - var before = string.slice(0, match.index).split("\n"), inside = match[0].split("\n") - var startLine = start.line + before.length - 1, startCh = before[before.length - 1].length - return {from: Pos(startLine, startCh), - to: Pos(startLine + inside.length - 1, - inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length), - match: match} - } - } - } - - function lastMatchIn(string, regexp, endMargin) { - var match, from = 0 - while (from <= string.length) { - regexp.lastIndex = from - var newMatch = regexp.exec(string) - if (!newMatch) break - var end = newMatch.index + newMatch[0].length - if (end > string.length - endMargin) break - if (!match || end > match.index + match[0].length) - match = newMatch - from = newMatch.index + 1 - } - return match - } - - function searchRegexpBackward(doc, regexp, start) { - regexp = ensureFlags(regexp, "g") - for (var line = start.line, ch = start.ch, first = doc.firstLine(); line >= first; line--, ch = -1) { - var string = doc.getLine(line) - var match = lastMatchIn(string, regexp, ch < 0 ? 0 : string.length - ch) - if (match) - return {from: Pos(line, match.index), - to: Pos(line, match.index + match[0].length), - match: match} - } - } - - function searchRegexpBackwardMultiline(doc, regexp, start) { - if (!maybeMultiline(regexp)) return searchRegexpBackward(doc, regexp, start) - regexp = ensureFlags(regexp, "gm") - var string, chunkSize = 1, endMargin = doc.getLine(start.line).length - start.ch - for (var line = start.line, first = doc.firstLine(); line >= first;) { - for (var i = 0; i < chunkSize && line >= first; i++) { - var curLine = doc.getLine(line--) - string = string == null ? curLine : curLine + "\n" + string - } - chunkSize *= 2 - - var match = lastMatchIn(string, regexp, endMargin) - if (match) { - var before = string.slice(0, match.index).split("\n"), inside = match[0].split("\n") - var startLine = line + before.length, startCh = before[before.length - 1].length - return {from: Pos(startLine, startCh), - to: Pos(startLine + inside.length - 1, - inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length), - match: match} - } - } - } - - var doFold, noFold - if (String.prototype.normalize) { - doFold = function(str) { return str.normalize("NFD").toLowerCase() } - noFold = function(str) { return str.normalize("NFD") } - } else { - doFold = function(str) { return str.toLowerCase() } - noFold = function(str) { return str } - } - - // Maps a position in a case-folded line back to a position in the original line - // (compensating for codepoints increasing in number during folding) - function adjustPos(orig, folded, pos, foldFunc) { - if (orig.length == folded.length) return pos - for (var min = 0, max = pos + Math.max(0, orig.length - folded.length);;) { - if (min == max) return min - var mid = (min + max) >> 1 - var len = foldFunc(orig.slice(0, mid)).length - if (len == pos) return mid - else if (len > pos) max = mid - else min = mid + 1 - } - } - - function searchStringForward(doc, query, start, caseFold) { - // Empty string would match anything and never progress, so we - // define it to match nothing instead. - if (!query.length) return null - var fold = caseFold ? doFold : noFold - var lines = fold(query).split(/\r|\n\r?/) - - search: for (var line = start.line, ch = start.ch, last = doc.lastLine() + 1 - lines.length; line <= last; line++, ch = 0) { - var orig = doc.getLine(line).slice(ch), string = fold(orig) - if (lines.length == 1) { - var found = string.indexOf(lines[0]) - if (found == -1) continue search - var start = adjustPos(orig, string, found, fold) + ch - return {from: Pos(line, adjustPos(orig, string, found, fold) + ch), - to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold) + ch)} - } else { - var cutFrom = string.length - lines[0].length - if (string.slice(cutFrom) != lines[0]) continue search - for (var i = 1; i < lines.length - 1; i++) - if (fold(doc.getLine(line + i)) != lines[i]) continue search - var end = doc.getLine(line + lines.length - 1), endString = fold(end), lastLine = lines[lines.length - 1] - if (endString.slice(0, lastLine.length) != lastLine) continue search - return {from: Pos(line, adjustPos(orig, string, cutFrom, fold) + ch), - to: Pos(line + lines.length - 1, adjustPos(end, endString, lastLine.length, fold))} - } - } - } - - function searchStringBackward(doc, query, start, caseFold) { - if (!query.length) return null - var fold = caseFold ? doFold : noFold - var lines = fold(query).split(/\r|\n\r?/) - - search: for (var line = start.line, ch = start.ch, first = doc.firstLine() - 1 + lines.length; line >= first; line--, ch = -1) { - var orig = doc.getLine(line) - if (ch > -1) orig = orig.slice(0, ch) - var string = fold(orig) - if (lines.length == 1) { - var found = string.lastIndexOf(lines[0]) - if (found == -1) continue search - return {from: Pos(line, adjustPos(orig, string, found, fold)), - to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold))} - } else { - var lastLine = lines[lines.length - 1] - if (string.slice(0, lastLine.length) != lastLine) continue search - for (var i = 1, start = line - lines.length + 1; i < lines.length - 1; i++) - if (fold(doc.getLine(start + i)) != lines[i]) continue search - var top = doc.getLine(line + 1 - lines.length), topString = fold(top) - if (topString.slice(topString.length - lines[0].length) != lines[0]) continue search - return {from: Pos(line + 1 - lines.length, adjustPos(top, topString, top.length - lines[0].length, fold)), - to: Pos(line, adjustPos(orig, string, lastLine.length, fold))} - } - } - } - - function SearchCursor(doc, query, pos, options) { - this.atOccurrence = false - this.doc = doc - pos = pos ? doc.clipPos(pos) : Pos(0, 0) - this.pos = {from: pos, to: pos} - - var caseFold - if (typeof options == "object") { - caseFold = options.caseFold - } else { // Backwards compat for when caseFold was the 4th argument - caseFold = options - options = null - } - - if (typeof query == "string") { - if (caseFold == null) caseFold = false - this.matches = function(reverse, pos) { - return (reverse ? searchStringBackward : searchStringForward)(doc, query, pos, caseFold) - } - } else { - query = ensureFlags(query, "gm") - if (!options || options.multiline !== false) - this.matches = function(reverse, pos) { - return (reverse ? searchRegexpBackwardMultiline : searchRegexpForwardMultiline)(doc, query, pos) - } - else - this.matches = function(reverse, pos) { - return (reverse ? searchRegexpBackward : searchRegexpForward)(doc, query, pos) - } - } - } - - SearchCursor.prototype = { - findNext: function() {return this.find(false)}, - findPrevious: function() {return this.find(true)}, - - find: function(reverse) { - var result = this.matches(reverse, this.doc.clipPos(reverse ? this.pos.from : this.pos.to)) - - // Implements weird auto-growing behavior on null-matches for - // backwards-compatibility with the vim code (unfortunately) - while (result && CodeMirror.cmpPos(result.from, result.to) == 0) { - if (reverse) { - if (result.from.ch) result.from = Pos(result.from.line, result.from.ch - 1) - else if (result.from.line == this.doc.firstLine()) result = null - else result = this.matches(reverse, this.doc.clipPos(Pos(result.from.line - 1))) - } else { - if (result.to.ch < this.doc.getLine(result.to.line).length) result.to = Pos(result.to.line, result.to.ch + 1) - else if (result.to.line == this.doc.lastLine()) result = null - else result = this.matches(reverse, Pos(result.to.line + 1, 0)) - } - } - - if (result) { - this.pos = result - this.atOccurrence = true - return this.pos.match || true - } else { - var end = Pos(reverse ? this.doc.firstLine() : this.doc.lastLine() + 1, 0) - this.pos = {from: end, to: end} - return this.atOccurrence = false - } - }, - - from: function() {if (this.atOccurrence) return this.pos.from}, - to: function() {if (this.atOccurrence) return this.pos.to}, - - replace: function(newText, origin) { - if (!this.atOccurrence) return - var lines = CodeMirror.splitLines(newText) - this.doc.replaceRange(lines, this.pos.from, this.pos.to, origin) - this.pos.to = Pos(this.pos.from.line + lines.length - 1, - lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0)) - } - } - - CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) { - return new SearchCursor(this.doc, query, pos, caseFold) - }) - CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) { - return new SearchCursor(this, query, pos, caseFold) - }) - - CodeMirror.defineExtension("selectMatches", function(query, caseFold) { - var ranges = [] - var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold) - while (cur.findNext()) { - if (CodeMirror.cmpPos(cur.to(), this.getCursor("to")) > 0) break - ranges.push({anchor: cur.from(), head: cur.to()}) - } - if (ranges.length) - this.setSelections(ranges, 0) - }) -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/selection/active-line.js b/plugins/UiFileManager/media/codemirror/extension/selection/active-line.js deleted file mode 100644 index c7b14ce0..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/selection/active-line.js +++ /dev/null @@ -1,72 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - var WRAP_CLASS = "CodeMirror-activeline"; - var BACK_CLASS = "CodeMirror-activeline-background"; - var GUTT_CLASS = "CodeMirror-activeline-gutter"; - - CodeMirror.defineOption("styleActiveLine", false, function(cm, val, old) { - var prev = old == CodeMirror.Init ? false : old; - if (val == prev) return - if (prev) { - cm.off("beforeSelectionChange", selectionChange); - clearActiveLines(cm); - delete cm.state.activeLines; - } - if (val) { - cm.state.activeLines = []; - updateActiveLines(cm, cm.listSelections()); - cm.on("beforeSelectionChange", selectionChange); - } - }); - - function clearActiveLines(cm) { - for (var i = 0; i < cm.state.activeLines.length; i++) { - cm.removeLineClass(cm.state.activeLines[i], "wrap", WRAP_CLASS); - cm.removeLineClass(cm.state.activeLines[i], "background", BACK_CLASS); - cm.removeLineClass(cm.state.activeLines[i], "gutter", GUTT_CLASS); - } - } - - function sameArray(a, b) { - if (a.length != b.length) return false; - for (var i = 0; i < a.length; i++) - if (a[i] != b[i]) return false; - return true; - } - - function updateActiveLines(cm, ranges) { - var active = []; - for (var i = 0; i < ranges.length; i++) { - var range = ranges[i]; - var option = cm.getOption("styleActiveLine"); - if (typeof option == "object" && option.nonEmpty ? range.anchor.line != range.head.line : !range.empty()) - continue - var line = cm.getLineHandleVisualStart(range.head.line); - if (active[active.length - 1] != line) active.push(line); - } - if (sameArray(cm.state.activeLines, active)) return; - cm.operation(function() { - clearActiveLines(cm); - for (var i = 0; i < active.length; i++) { - cm.addLineClass(active[i], "wrap", WRAP_CLASS); - cm.addLineClass(active[i], "background", BACK_CLASS); - cm.addLineClass(active[i], "gutter", GUTT_CLASS); - } - cm.state.activeLines = active; - }); - } - - function selectionChange(cm, sel) { - updateActiveLines(cm, sel.ranges); - } -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/selection/mark-selection.js b/plugins/UiFileManager/media/codemirror/extension/selection/mark-selection.js deleted file mode 100644 index adfaa62d..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/selection/mark-selection.js +++ /dev/null @@ -1,119 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -// Because sometimes you need to mark the selected *text*. -// -// Adds an option 'styleSelectedText' which, when enabled, gives -// selected text the CSS class given as option value, or -// "CodeMirror-selectedtext" when the value is not a string. - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - CodeMirror.defineOption("styleSelectedText", false, function(cm, val, old) { - var prev = old && old != CodeMirror.Init; - if (val && !prev) { - cm.state.markedSelection = []; - cm.state.markedSelectionStyle = typeof val == "string" ? val : "CodeMirror-selectedtext"; - reset(cm); - cm.on("cursorActivity", onCursorActivity); - cm.on("change", onChange); - } else if (!val && prev) { - cm.off("cursorActivity", onCursorActivity); - cm.off("change", onChange); - clear(cm); - cm.state.markedSelection = cm.state.markedSelectionStyle = null; - } - }); - - function onCursorActivity(cm) { - if (cm.state.markedSelection) - cm.operation(function() { update(cm); }); - } - - function onChange(cm) { - if (cm.state.markedSelection && cm.state.markedSelection.length) - cm.operation(function() { clear(cm); }); - } - - var CHUNK_SIZE = 8; - var Pos = CodeMirror.Pos; - var cmp = CodeMirror.cmpPos; - - function coverRange(cm, from, to, addAt) { - if (cmp(from, to) == 0) return; - var array = cm.state.markedSelection; - var cls = cm.state.markedSelectionStyle; - for (var line = from.line;;) { - var start = line == from.line ? from : Pos(line, 0); - var endLine = line + CHUNK_SIZE, atEnd = endLine >= to.line; - var end = atEnd ? to : Pos(endLine, 0); - var mark = cm.markText(start, end, {className: cls}); - if (addAt == null) array.push(mark); - else array.splice(addAt++, 0, mark); - if (atEnd) break; - line = endLine; - } - } - - function clear(cm) { - var array = cm.state.markedSelection; - for (var i = 0; i < array.length; ++i) array[i].clear(); - array.length = 0; - } - - function reset(cm) { - clear(cm); - var ranges = cm.listSelections(); - for (var i = 0; i < ranges.length; i++) - coverRange(cm, ranges[i].from(), ranges[i].to()); - } - - function update(cm) { - if (!cm.somethingSelected()) return clear(cm); - if (cm.listSelections().length > 1) return reset(cm); - - var from = cm.getCursor("start"), to = cm.getCursor("end"); - - var array = cm.state.markedSelection; - if (!array.length) return coverRange(cm, from, to); - - var coverStart = array[0].find(), coverEnd = array[array.length - 1].find(); - if (!coverStart || !coverEnd || to.line - from.line <= CHUNK_SIZE || - cmp(from, coverEnd.to) >= 0 || cmp(to, coverStart.from) <= 0) - return reset(cm); - - while (cmp(from, coverStart.from) > 0) { - array.shift().clear(); - coverStart = array[0].find(); - } - if (cmp(from, coverStart.from) < 0) { - if (coverStart.to.line - from.line < CHUNK_SIZE) { - array.shift().clear(); - coverRange(cm, from, coverStart.to, 0); - } else { - coverRange(cm, from, coverStart.from, 0); - } - } - - while (cmp(to, coverEnd.to) < 0) { - array.pop().clear(); - coverEnd = array[array.length - 1].find(); - } - if (cmp(to, coverEnd.to) > 0) { - if (to.line - coverEnd.from.line < CHUNK_SIZE) { - array.pop().clear(); - coverRange(cm, coverEnd.from, to); - } else { - coverRange(cm, coverEnd.to, to); - } - } - } -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/selection/selection-pointer.js b/plugins/UiFileManager/media/codemirror/extension/selection/selection-pointer.js deleted file mode 100644 index f0bd61a3..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/selection/selection-pointer.js +++ /dev/null @@ -1,98 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - CodeMirror.defineOption("selectionPointer", false, function(cm, val) { - var data = cm.state.selectionPointer; - if (data) { - CodeMirror.off(cm.getWrapperElement(), "mousemove", data.mousemove); - CodeMirror.off(cm.getWrapperElement(), "mouseout", data.mouseout); - CodeMirror.off(window, "scroll", data.windowScroll); - cm.off("cursorActivity", reset); - cm.off("scroll", reset); - cm.state.selectionPointer = null; - cm.display.lineDiv.style.cursor = ""; - } - if (val) { - data = cm.state.selectionPointer = { - value: typeof val == "string" ? val : "default", - mousemove: function(event) { mousemove(cm, event); }, - mouseout: function(event) { mouseout(cm, event); }, - windowScroll: function() { reset(cm); }, - rects: null, - mouseX: null, mouseY: null, - willUpdate: false - }; - CodeMirror.on(cm.getWrapperElement(), "mousemove", data.mousemove); - CodeMirror.on(cm.getWrapperElement(), "mouseout", data.mouseout); - CodeMirror.on(window, "scroll", data.windowScroll); - cm.on("cursorActivity", reset); - cm.on("scroll", reset); - } - }); - - function mousemove(cm, event) { - var data = cm.state.selectionPointer; - if (event.buttons == null ? event.which : event.buttons) { - data.mouseX = data.mouseY = null; - } else { - data.mouseX = event.clientX; - data.mouseY = event.clientY; - } - scheduleUpdate(cm); - } - - function mouseout(cm, event) { - if (!cm.getWrapperElement().contains(event.relatedTarget)) { - var data = cm.state.selectionPointer; - data.mouseX = data.mouseY = null; - scheduleUpdate(cm); - } - } - - function reset(cm) { - cm.state.selectionPointer.rects = null; - scheduleUpdate(cm); - } - - function scheduleUpdate(cm) { - if (!cm.state.selectionPointer.willUpdate) { - cm.state.selectionPointer.willUpdate = true; - setTimeout(function() { - update(cm); - cm.state.selectionPointer.willUpdate = false; - }, 50); - } - } - - function update(cm) { - var data = cm.state.selectionPointer; - if (!data) return; - if (data.rects == null && data.mouseX != null) { - data.rects = []; - if (cm.somethingSelected()) { - for (var sel = cm.display.selectionDiv.firstChild; sel; sel = sel.nextSibling) - data.rects.push(sel.getBoundingClientRect()); - } - } - var inside = false; - if (data.mouseX != null) for (var i = 0; i < data.rects.length; i++) { - var rect = data.rects[i]; - if (rect.left <= data.mouseX && rect.right >= data.mouseX && - rect.top <= data.mouseY && rect.bottom >= data.mouseY) - inside = true; - } - var cursor = inside ? data.value : ""; - if (cm.display.lineDiv.style.cursor != cursor) - cm.display.lineDiv.style.cursor = cursor; - } -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/simple.js b/plugins/UiFileManager/media/codemirror/extension/simple.js deleted file mode 100644 index 655f9914..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/simple.js +++ /dev/null @@ -1,216 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - CodeMirror.defineSimpleMode = function(name, states) { - CodeMirror.defineMode(name, function(config) { - return CodeMirror.simpleMode(config, states); - }); - }; - - CodeMirror.simpleMode = function(config, states) { - ensureState(states, "start"); - var states_ = {}, meta = states.meta || {}, hasIndentation = false; - for (var state in states) if (state != meta && states.hasOwnProperty(state)) { - var list = states_[state] = [], orig = states[state]; - for (var i = 0; i < orig.length; i++) { - var data = orig[i]; - list.push(new Rule(data, states)); - if (data.indent || data.dedent) hasIndentation = true; - } - } - var mode = { - startState: function() { - return {state: "start", pending: null, - local: null, localState: null, - indent: hasIndentation ? [] : null}; - }, - copyState: function(state) { - var s = {state: state.state, pending: state.pending, - local: state.local, localState: null, - indent: state.indent && state.indent.slice(0)}; - if (state.localState) - s.localState = CodeMirror.copyState(state.local.mode, state.localState); - if (state.stack) - s.stack = state.stack.slice(0); - for (var pers = state.persistentStates; pers; pers = pers.next) - s.persistentStates = {mode: pers.mode, - spec: pers.spec, - state: pers.state == state.localState ? s.localState : CodeMirror.copyState(pers.mode, pers.state), - next: s.persistentStates}; - return s; - }, - token: tokenFunction(states_, config), - innerMode: function(state) { return state.local && {mode: state.local.mode, state: state.localState}; }, - indent: indentFunction(states_, meta) - }; - if (meta) for (var prop in meta) if (meta.hasOwnProperty(prop)) - mode[prop] = meta[prop]; - return mode; - }; - - function ensureState(states, name) { - if (!states.hasOwnProperty(name)) - throw new Error("Undefined state " + name + " in simple mode"); - } - - function toRegex(val, caret) { - if (!val) return /(?:)/; - var flags = ""; - if (val instanceof RegExp) { - if (val.ignoreCase) flags = "i"; - val = val.source; - } else { - val = String(val); - } - return new RegExp((caret === false ? "" : "^") + "(?:" + val + ")", flags); - } - - function asToken(val) { - if (!val) return null; - if (val.apply) return val - if (typeof val == "string") return val.replace(/\./g, " "); - var result = []; - for (var i = 0; i < val.length; i++) - result.push(val[i] && val[i].replace(/\./g, " ")); - return result; - } - - function Rule(data, states) { - if (data.next || data.push) ensureState(states, data.next || data.push); - this.regex = toRegex(data.regex); - this.token = asToken(data.token); - this.data = data; - } - - function tokenFunction(states, config) { - return function(stream, state) { - if (state.pending) { - var pend = state.pending.shift(); - if (state.pending.length == 0) state.pending = null; - stream.pos += pend.text.length; - return pend.token; - } - - if (state.local) { - if (state.local.end && stream.match(state.local.end)) { - var tok = state.local.endToken || null; - state.local = state.localState = null; - return tok; - } else { - var tok = state.local.mode.token(stream, state.localState), m; - if (state.local.endScan && (m = state.local.endScan.exec(stream.current()))) - stream.pos = stream.start + m.index; - return tok; - } - } - - var curState = states[state.state]; - for (var i = 0; i < curState.length; i++) { - var rule = curState[i]; - var matches = (!rule.data.sol || stream.sol()) && stream.match(rule.regex); - if (matches) { - if (rule.data.next) { - state.state = rule.data.next; - } else if (rule.data.push) { - (state.stack || (state.stack = [])).push(state.state); - state.state = rule.data.push; - } else if (rule.data.pop && state.stack && state.stack.length) { - state.state = state.stack.pop(); - } - - if (rule.data.mode) - enterLocalMode(config, state, rule.data.mode, rule.token); - if (rule.data.indent) - state.indent.push(stream.indentation() + config.indentUnit); - if (rule.data.dedent) - state.indent.pop(); - var token = rule.token - if (token && token.apply) token = token(matches) - if (matches.length > 2 && rule.token && typeof rule.token != "string") { - state.pending = []; - for (var j = 2; j < matches.length; j++) - if (matches[j]) - state.pending.push({text: matches[j], token: rule.token[j - 1]}); - stream.backUp(matches[0].length - (matches[1] ? matches[1].length : 0)); - return token[0]; - } else if (token && token.join) { - return token[0]; - } else { - return token; - } - } - } - stream.next(); - return null; - }; - } - - function cmp(a, b) { - if (a === b) return true; - if (!a || typeof a != "object" || !b || typeof b != "object") return false; - var props = 0; - for (var prop in a) if (a.hasOwnProperty(prop)) { - if (!b.hasOwnProperty(prop) || !cmp(a[prop], b[prop])) return false; - props++; - } - for (var prop in b) if (b.hasOwnProperty(prop)) props--; - return props == 0; - } - - function enterLocalMode(config, state, spec, token) { - var pers; - if (spec.persistent) for (var p = state.persistentStates; p && !pers; p = p.next) - if (spec.spec ? cmp(spec.spec, p.spec) : spec.mode == p.mode) pers = p; - var mode = pers ? pers.mode : spec.mode || CodeMirror.getMode(config, spec.spec); - var lState = pers ? pers.state : CodeMirror.startState(mode); - if (spec.persistent && !pers) - state.persistentStates = {mode: mode, spec: spec.spec, state: lState, next: state.persistentStates}; - - state.localState = lState; - state.local = {mode: mode, - end: spec.end && toRegex(spec.end), - endScan: spec.end && spec.forceEnd !== false && toRegex(spec.end, false), - endToken: token && token.join ? token[token.length - 1] : token}; - } - - function indexOf(val, arr) { - for (var i = 0; i < arr.length; i++) if (arr[i] === val) return true; - } - - function indentFunction(states, meta) { - return function(state, textAfter, line) { - if (state.local && state.local.mode.indent) - return state.local.mode.indent(state.localState, textAfter, line); - if (state.indent == null || state.local || meta.dontIndentStates && indexOf(state.state, meta.dontIndentStates) > -1) - return CodeMirror.Pass; - - var pos = state.indent.length - 1, rules = states[state.state]; - scan: for (;;) { - for (var i = 0; i < rules.length; i++) { - var rule = rules[i]; - if (rule.data.dedent && rule.data.dedentIfLineStart !== false) { - var m = rule.regex.exec(textAfter); - if (m && m[0]) { - pos--; - if (rule.next || rule.push) rules = states[rule.next || rule.push]; - textAfter = textAfter.slice(m[0].length); - continue scan; - } - } - } - break; - } - return pos < 0 ? 0 : state.indent[pos]; - }; - } -}); diff --git a/plugins/UiFileManager/media/codemirror/extension/sublime.js b/plugins/UiFileManager/media/codemirror/extension/sublime.js deleted file mode 100644 index 7edf172e..00000000 --- a/plugins/UiFileManager/media/codemirror/extension/sublime.js +++ /dev/null @@ -1,714 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -// A rough approximation of Sublime Text's keybindings -// Depends on addon/search/searchcursor.js and optionally addon/dialog/dialogs.js - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../lib/codemirror"), require("../addon/search/searchcursor"), require("../addon/edit/matchbrackets")); - else if (typeof define == "function" && define.amd) // AMD - define(["../lib/codemirror", "../addon/search/searchcursor", "../addon/edit/matchbrackets"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - var cmds = CodeMirror.commands; - var Pos = CodeMirror.Pos; - - // This is not exactly Sublime's algorithm. I couldn't make heads or tails of that. - function findPosSubword(doc, start, dir) { - if (dir < 0 && start.ch == 0) return doc.clipPos(Pos(start.line - 1)); - var line = doc.getLine(start.line); - if (dir > 0 && start.ch >= line.length) return doc.clipPos(Pos(start.line + 1, 0)); - var state = "start", type, startPos = start.ch; - for (var pos = startPos, e = dir < 0 ? 0 : line.length, i = 0; pos != e; pos += dir, i++) { - var next = line.charAt(dir < 0 ? pos - 1 : pos); - var cat = next != "_" && CodeMirror.isWordChar(next) ? "w" : "o"; - if (cat == "w" && next.toUpperCase() == next) cat = "W"; - if (state == "start") { - if (cat != "o") { state = "in"; type = cat; } - else startPos = pos + dir - } else if (state == "in") { - if (type != cat) { - if (type == "w" && cat == "W" && dir < 0) pos--; - if (type == "W" && cat == "w" && dir > 0) { // From uppercase to lowercase - if (pos == startPos + 1) { type = "w"; continue; } - else pos--; - } - break; - } - } - } - return Pos(start.line, pos); - } - - function moveSubword(cm, dir) { - cm.extendSelectionsBy(function(range) { - if (cm.display.shift || cm.doc.extend || range.empty()) - return findPosSubword(cm.doc, range.head, dir); - else - return dir < 0 ? range.from() : range.to(); - }); - } - - cmds.goSubwordLeft = function(cm) { moveSubword(cm, -1); }; - cmds.goSubwordRight = function(cm) { moveSubword(cm, 1); }; - - cmds.scrollLineUp = function(cm) { - var info = cm.getScrollInfo(); - if (!cm.somethingSelected()) { - var visibleBottomLine = cm.lineAtHeight(info.top + info.clientHeight, "local"); - if (cm.getCursor().line >= visibleBottomLine) - cm.execCommand("goLineUp"); - } - cm.scrollTo(null, info.top - cm.defaultTextHeight()); - }; - cmds.scrollLineDown = function(cm) { - var info = cm.getScrollInfo(); - if (!cm.somethingSelected()) { - var visibleTopLine = cm.lineAtHeight(info.top, "local")+1; - if (cm.getCursor().line <= visibleTopLine) - cm.execCommand("goLineDown"); - } - cm.scrollTo(null, info.top + cm.defaultTextHeight()); - }; - - cmds.splitSelectionByLine = function(cm) { - var ranges = cm.listSelections(), lineRanges = []; - for (var i = 0; i < ranges.length; i++) { - var from = ranges[i].from(), to = ranges[i].to(); - for (var line = from.line; line <= to.line; ++line) - if (!(to.line > from.line && line == to.line && to.ch == 0)) - lineRanges.push({anchor: line == from.line ? from : Pos(line, 0), - head: line == to.line ? to : Pos(line)}); - } - cm.setSelections(lineRanges, 0); - }; - - cmds.singleSelectionTop = function(cm) { - var range = cm.listSelections()[0]; - cm.setSelection(range.anchor, range.head, {scroll: false}); - }; - - cmds.selectLine = function(cm) { - var ranges = cm.listSelections(), extended = []; - for (var i = 0; i < ranges.length; i++) { - var range = ranges[i]; - extended.push({anchor: Pos(range.from().line, 0), - head: Pos(range.to().line + 1, 0)}); - } - cm.setSelections(extended); - }; - - function insertLine(cm, above) { - if (cm.isReadOnly()) return CodeMirror.Pass - cm.operation(function() { - var len = cm.listSelections().length, newSelection = [], last = -1; - for (var i = 0; i < len; i++) { - var head = cm.listSelections()[i].head; - if (head.line <= last) continue; - var at = Pos(head.line + (above ? 0 : 1), 0); - cm.replaceRange("\n", at, null, "+insertLine"); - cm.indentLine(at.line, null, true); - newSelection.push({head: at, anchor: at}); - last = head.line + 1; - } - cm.setSelections(newSelection); - }); - cm.execCommand("indentAuto"); - } - - cmds.insertLineAfter = function(cm) { return insertLine(cm, false); }; - - cmds.insertLineBefore = function(cm) { return insertLine(cm, true); }; - - function wordAt(cm, pos) { - var start = pos.ch, end = start, line = cm.getLine(pos.line); - while (start && CodeMirror.isWordChar(line.charAt(start - 1))) --start; - while (end < line.length && CodeMirror.isWordChar(line.charAt(end))) ++end; - return {from: Pos(pos.line, start), to: Pos(pos.line, end), word: line.slice(start, end)}; - } - - cmds.selectNextOccurrence = function(cm) { - var from = cm.getCursor("from"), to = cm.getCursor("to"); - var fullWord = cm.state.sublimeFindFullWord == cm.doc.sel; - if (CodeMirror.cmpPos(from, to) == 0) { - var word = wordAt(cm, from); - if (!word.word) return; - cm.setSelection(word.from, word.to); - fullWord = true; - } else { - var text = cm.getRange(from, to); - var query = fullWord ? new RegExp("\\b" + text + "\\b") : text; - var cur = cm.getSearchCursor(query, to); - var found = cur.findNext(); - if (!found) { - cur = cm.getSearchCursor(query, Pos(cm.firstLine(), 0)); - found = cur.findNext(); - } - if (!found || isSelectedRange(cm.listSelections(), cur.from(), cur.to())) return - cm.addSelection(cur.from(), cur.to()); - } - if (fullWord) - cm.state.sublimeFindFullWord = cm.doc.sel; - }; - - cmds.skipAndSelectNextOccurrence = function(cm) { - var prevAnchor = cm.getCursor("anchor"), prevHead = cm.getCursor("head"); - cmds.selectNextOccurrence(cm); - if (CodeMirror.cmpPos(prevAnchor, prevHead) != 0) { - cm.doc.setSelections(cm.doc.listSelections() - .filter(function (sel) { - return sel.anchor != prevAnchor || sel.head != prevHead; - })); - } - } - - function addCursorToSelection(cm, dir) { - var ranges = cm.listSelections(), newRanges = []; - for (var i = 0; i < ranges.length; i++) { - var range = ranges[i]; - var newAnchor = cm.findPosV( - range.anchor, dir, "line", range.anchor.goalColumn); - var newHead = cm.findPosV( - range.head, dir, "line", range.head.goalColumn); - newAnchor.goalColumn = range.anchor.goalColumn != null ? - range.anchor.goalColumn : cm.cursorCoords(range.anchor, "div").left; - newHead.goalColumn = range.head.goalColumn != null ? - range.head.goalColumn : cm.cursorCoords(range.head, "div").left; - var newRange = {anchor: newAnchor, head: newHead}; - newRanges.push(range); - newRanges.push(newRange); - } - cm.setSelections(newRanges); - } - cmds.addCursorToPrevLine = function(cm) { addCursorToSelection(cm, -1); }; - cmds.addCursorToNextLine = function(cm) { addCursorToSelection(cm, 1); }; - - function isSelectedRange(ranges, from, to) { - for (var i = 0; i < ranges.length; i++) - if (CodeMirror.cmpPos(ranges[i].from(), from) == 0 && - CodeMirror.cmpPos(ranges[i].to(), to) == 0) return true - return false - } - - var mirror = "(){}[]"; - function selectBetweenBrackets(cm) { - var ranges = cm.listSelections(), newRanges = [] - for (var i = 0; i < ranges.length; i++) { - var range = ranges[i], pos = range.head, opening = cm.scanForBracket(pos, -1); - if (!opening) return false; - for (;;) { - var closing = cm.scanForBracket(pos, 1); - if (!closing) return false; - if (closing.ch == mirror.charAt(mirror.indexOf(opening.ch) + 1)) { - var startPos = Pos(opening.pos.line, opening.pos.ch + 1); - if (CodeMirror.cmpPos(startPos, range.from()) == 0 && - CodeMirror.cmpPos(closing.pos, range.to()) == 0) { - opening = cm.scanForBracket(opening.pos, -1); - if (!opening) return false; - } else { - newRanges.push({anchor: startPos, head: closing.pos}); - break; - } - } - pos = Pos(closing.pos.line, closing.pos.ch + 1); - } - } - cm.setSelections(newRanges); - return true; - } - - cmds.selectScope = function(cm) { - selectBetweenBrackets(cm) || cm.execCommand("selectAll"); - }; - cmds.selectBetweenBrackets = function(cm) { - if (!selectBetweenBrackets(cm)) return CodeMirror.Pass; - }; - - function puncType(type) { - return !type ? null : /\bpunctuation\b/.test(type) ? type : undefined - } - - cmds.goToBracket = function(cm) { - cm.extendSelectionsBy(function(range) { - var next = cm.scanForBracket(range.head, 1, puncType(cm.getTokenTypeAt(range.head))); - if (next && CodeMirror.cmpPos(next.pos, range.head) != 0) return next.pos; - var prev = cm.scanForBracket(range.head, -1, puncType(cm.getTokenTypeAt(Pos(range.head.line, range.head.ch + 1)))); - return prev && Pos(prev.pos.line, prev.pos.ch + 1) || range.head; - }); - }; - - cmds.swapLineUp = function(cm) { - if (cm.isReadOnly()) return CodeMirror.Pass - var ranges = cm.listSelections(), linesToMove = [], at = cm.firstLine() - 1, newSels = []; - for (var i = 0; i < ranges.length; i++) { - var range = ranges[i], from = range.from().line - 1, to = range.to().line; - newSels.push({anchor: Pos(range.anchor.line - 1, range.anchor.ch), - head: Pos(range.head.line - 1, range.head.ch)}); - if (range.to().ch == 0 && !range.empty()) --to; - if (from > at) linesToMove.push(from, to); - else if (linesToMove.length) linesToMove[linesToMove.length - 1] = to; - at = to; - } - cm.operation(function() { - for (var i = 0; i < linesToMove.length; i += 2) { - var from = linesToMove[i], to = linesToMove[i + 1]; - var line = cm.getLine(from); - cm.replaceRange("", Pos(from, 0), Pos(from + 1, 0), "+swapLine"); - if (to > cm.lastLine()) - cm.replaceRange("\n" + line, Pos(cm.lastLine()), null, "+swapLine"); - else - cm.replaceRange(line + "\n", Pos(to, 0), null, "+swapLine"); - } - cm.setSelections(newSels); - cm.scrollIntoView(); - }); - }; - - cmds.swapLineDown = function(cm) { - if (cm.isReadOnly()) return CodeMirror.Pass - var ranges = cm.listSelections(), linesToMove = [], at = cm.lastLine() + 1; - for (var i = ranges.length - 1; i >= 0; i--) { - var range = ranges[i], from = range.to().line + 1, to = range.from().line; - if (range.to().ch == 0 && !range.empty()) from--; - if (from < at) linesToMove.push(from, to); - else if (linesToMove.length) linesToMove[linesToMove.length - 1] = to; - at = to; - } - cm.operation(function() { - for (var i = linesToMove.length - 2; i >= 0; i -= 2) { - var from = linesToMove[i], to = linesToMove[i + 1]; - var line = cm.getLine(from); - if (from == cm.lastLine()) - cm.replaceRange("", Pos(from - 1), Pos(from), "+swapLine"); - else - cm.replaceRange("", Pos(from, 0), Pos(from + 1, 0), "+swapLine"); - cm.replaceRange(line + "\n", Pos(to, 0), null, "+swapLine"); - } - cm.scrollIntoView(); - }); - }; - - cmds.toggleCommentIndented = function(cm) { - cm.toggleComment({ indent: true }); - } - - cmds.joinLines = function(cm) { - var ranges = cm.listSelections(), joined = []; - for (var i = 0; i < ranges.length; i++) { - var range = ranges[i], from = range.from(); - var start = from.line, end = range.to().line; - while (i < ranges.length - 1 && ranges[i + 1].from().line == end) - end = ranges[++i].to().line; - joined.push({start: start, end: end, anchor: !range.empty() && from}); - } - cm.operation(function() { - var offset = 0, ranges = []; - for (var i = 0; i < joined.length; i++) { - var obj = joined[i]; - var anchor = obj.anchor && Pos(obj.anchor.line - offset, obj.anchor.ch), head; - for (var line = obj.start; line <= obj.end; line++) { - var actual = line - offset; - if (line == obj.end) head = Pos(actual, cm.getLine(actual).length + 1); - if (actual < cm.lastLine()) { - cm.replaceRange(" ", Pos(actual), Pos(actual + 1, /^\s*/.exec(cm.getLine(actual + 1))[0].length)); - ++offset; - } - } - ranges.push({anchor: anchor || head, head: head}); - } - cm.setSelections(ranges, 0); - }); - }; - - cmds.duplicateLine = function(cm) { - cm.operation(function() { - var rangeCount = cm.listSelections().length; - for (var i = 0; i < rangeCount; i++) { - var range = cm.listSelections()[i]; - if (range.empty()) - cm.replaceRange(cm.getLine(range.head.line) + "\n", Pos(range.head.line, 0)); - else - cm.replaceRange(cm.getRange(range.from(), range.to()), range.from()); - } - cm.scrollIntoView(); - }); - }; - - - function sortLines(cm, caseSensitive) { - if (cm.isReadOnly()) return CodeMirror.Pass - var ranges = cm.listSelections(), toSort = [], selected; - for (var i = 0; i < ranges.length; i++) { - var range = ranges[i]; - if (range.empty()) continue; - var from = range.from().line, to = range.to().line; - while (i < ranges.length - 1 && ranges[i + 1].from().line == to) - to = ranges[++i].to().line; - if (!ranges[i].to().ch) to--; - toSort.push(from, to); - } - if (toSort.length) selected = true; - else toSort.push(cm.firstLine(), cm.lastLine()); - - cm.operation(function() { - var ranges = []; - for (var i = 0; i < toSort.length; i += 2) { - var from = toSort[i], to = toSort[i + 1]; - var start = Pos(from, 0), end = Pos(to); - var lines = cm.getRange(start, end, false); - if (caseSensitive) - lines.sort(); - else - lines.sort(function(a, b) { - var au = a.toUpperCase(), bu = b.toUpperCase(); - if (au != bu) { a = au; b = bu; } - return a < b ? -1 : a == b ? 0 : 1; - }); - cm.replaceRange(lines, start, end); - if (selected) ranges.push({anchor: start, head: Pos(to + 1, 0)}); - } - if (selected) cm.setSelections(ranges, 0); - }); - } - - cmds.sortLines = function(cm) { sortLines(cm, true); }; - cmds.sortLinesInsensitive = function(cm) { sortLines(cm, false); }; - - cmds.nextBookmark = function(cm) { - var marks = cm.state.sublimeBookmarks; - if (marks) while (marks.length) { - var current = marks.shift(); - var found = current.find(); - if (found) { - marks.push(current); - return cm.setSelection(found.from, found.to); - } - } - }; - - cmds.prevBookmark = function(cm) { - var marks = cm.state.sublimeBookmarks; - if (marks) while (marks.length) { - marks.unshift(marks.pop()); - var found = marks[marks.length - 1].find(); - if (!found) - marks.pop(); - else - return cm.setSelection(found.from, found.to); - } - }; - - cmds.toggleBookmark = function(cm) { - var ranges = cm.listSelections(); - var marks = cm.state.sublimeBookmarks || (cm.state.sublimeBookmarks = []); - for (var i = 0; i < ranges.length; i++) { - var from = ranges[i].from(), to = ranges[i].to(); - var found = ranges[i].empty() ? cm.findMarksAt(from) : cm.findMarks(from, to); - for (var j = 0; j < found.length; j++) { - if (found[j].sublimeBookmark) { - found[j].clear(); - for (var k = 0; k < marks.length; k++) - if (marks[k] == found[j]) - marks.splice(k--, 1); - break; - } - } - if (j == found.length) - marks.push(cm.markText(from, to, {sublimeBookmark: true, clearWhenEmpty: false})); - } - }; - - cmds.clearBookmarks = function(cm) { - var marks = cm.state.sublimeBookmarks; - if (marks) for (var i = 0; i < marks.length; i++) marks[i].clear(); - marks.length = 0; - }; - - cmds.selectBookmarks = function(cm) { - var marks = cm.state.sublimeBookmarks, ranges = []; - if (marks) for (var i = 0; i < marks.length; i++) { - var found = marks[i].find(); - if (!found) - marks.splice(i--, 0); - else - ranges.push({anchor: found.from, head: found.to}); - } - if (ranges.length) - cm.setSelections(ranges, 0); - }; - - function modifyWordOrSelection(cm, mod) { - cm.operation(function() { - var ranges = cm.listSelections(), indices = [], replacements = []; - for (var i = 0; i < ranges.length; i++) { - var range = ranges[i]; - if (range.empty()) { indices.push(i); replacements.push(""); } - else replacements.push(mod(cm.getRange(range.from(), range.to()))); - } - cm.replaceSelections(replacements, "around", "case"); - for (var i = indices.length - 1, at; i >= 0; i--) { - var range = ranges[indices[i]]; - if (at && CodeMirror.cmpPos(range.head, at) > 0) continue; - var word = wordAt(cm, range.head); - at = word.from; - cm.replaceRange(mod(word.word), word.from, word.to); - } - }); - } - - cmds.smartBackspace = function(cm) { - if (cm.somethingSelected()) return CodeMirror.Pass; - - cm.operation(function() { - var cursors = cm.listSelections(); - var indentUnit = cm.getOption("indentUnit"); - - for (var i = cursors.length - 1; i >= 0; i--) { - var cursor = cursors[i].head; - var toStartOfLine = cm.getRange({line: cursor.line, ch: 0}, cursor); - var column = CodeMirror.countColumn(toStartOfLine, null, cm.getOption("tabSize")); - - // Delete by one character by default - var deletePos = cm.findPosH(cursor, -1, "char", false); - - if (toStartOfLine && !/\S/.test(toStartOfLine) && column % indentUnit == 0) { - var prevIndent = new Pos(cursor.line, - CodeMirror.findColumn(toStartOfLine, column - indentUnit, indentUnit)); - - // Smart delete only if we found a valid prevIndent location - if (prevIndent.ch != cursor.ch) deletePos = prevIndent; - } - - cm.replaceRange("", deletePos, cursor, "+delete"); - } - }); - }; - - cmds.delLineRight = function(cm) { - cm.operation(function() { - var ranges = cm.listSelections(); - for (var i = ranges.length - 1; i >= 0; i--) - cm.replaceRange("", ranges[i].anchor, Pos(ranges[i].to().line), "+delete"); - cm.scrollIntoView(); - }); - }; - - cmds.upcaseAtCursor = function(cm) { - modifyWordOrSelection(cm, function(str) { return str.toUpperCase(); }); - }; - cmds.downcaseAtCursor = function(cm) { - modifyWordOrSelection(cm, function(str) { return str.toLowerCase(); }); - }; - - cmds.setSublimeMark = function(cm) { - if (cm.state.sublimeMark) cm.state.sublimeMark.clear(); - cm.state.sublimeMark = cm.setBookmark(cm.getCursor()); - }; - cmds.selectToSublimeMark = function(cm) { - var found = cm.state.sublimeMark && cm.state.sublimeMark.find(); - if (found) cm.setSelection(cm.getCursor(), found); - }; - cmds.deleteToSublimeMark = function(cm) { - var found = cm.state.sublimeMark && cm.state.sublimeMark.find(); - if (found) { - var from = cm.getCursor(), to = found; - if (CodeMirror.cmpPos(from, to) > 0) { var tmp = to; to = from; from = tmp; } - cm.state.sublimeKilled = cm.getRange(from, to); - cm.replaceRange("", from, to); - } - }; - cmds.swapWithSublimeMark = function(cm) { - var found = cm.state.sublimeMark && cm.state.sublimeMark.find(); - if (found) { - cm.state.sublimeMark.clear(); - cm.state.sublimeMark = cm.setBookmark(cm.getCursor()); - cm.setCursor(found); - } - }; - cmds.sublimeYank = function(cm) { - if (cm.state.sublimeKilled != null) - cm.replaceSelection(cm.state.sublimeKilled, null, "paste"); - }; - - cmds.showInCenter = function(cm) { - var pos = cm.cursorCoords(null, "local"); - cm.scrollTo(null, (pos.top + pos.bottom) / 2 - cm.getScrollInfo().clientHeight / 2); - }; - - function getTarget(cm) { - var from = cm.getCursor("from"), to = cm.getCursor("to"); - if (CodeMirror.cmpPos(from, to) == 0) { - var word = wordAt(cm, from); - if (!word.word) return; - from = word.from; - to = word.to; - } - return {from: from, to: to, query: cm.getRange(from, to), word: word}; - } - - function findAndGoTo(cm, forward) { - var target = getTarget(cm); - if (!target) return; - var query = target.query; - var cur = cm.getSearchCursor(query, forward ? target.to : target.from); - - if (forward ? cur.findNext() : cur.findPrevious()) { - cm.setSelection(cur.from(), cur.to()); - } else { - cur = cm.getSearchCursor(query, forward ? Pos(cm.firstLine(), 0) - : cm.clipPos(Pos(cm.lastLine()))); - if (forward ? cur.findNext() : cur.findPrevious()) - cm.setSelection(cur.from(), cur.to()); - else if (target.word) - cm.setSelection(target.from, target.to); - } - }; - cmds.findUnder = function(cm) { findAndGoTo(cm, true); }; - cmds.findUnderPrevious = function(cm) { findAndGoTo(cm,false); }; - cmds.findAllUnder = function(cm) { - var target = getTarget(cm); - if (!target) return; - var cur = cm.getSearchCursor(target.query); - var matches = []; - var primaryIndex = -1; - while (cur.findNext()) { - matches.push({anchor: cur.from(), head: cur.to()}); - if (cur.from().line <= target.from.line && cur.from().ch <= target.from.ch) - primaryIndex++; - } - cm.setSelections(matches, primaryIndex); - }; - - - var keyMap = CodeMirror.keyMap; - keyMap.macSublime = { - "Cmd-Left": "goLineStartSmart", - "Shift-Tab": "indentLess", - "Shift-Ctrl-K": "deleteLine", - "Alt-Q": "wrapLines", - "Ctrl-Left": "goSubwordLeft", - "Ctrl-Right": "goSubwordRight", - "Ctrl-Alt-Up": "scrollLineUp", - "Ctrl-Alt-Down": "scrollLineDown", - "Cmd-L": "selectLine", - "Shift-Cmd-L": "splitSelectionByLine", - "Esc": "singleSelectionTop", - "Cmd-Enter": "insertLineAfter", - "Shift-Cmd-Enter": "insertLineBefore", - "Cmd-D": "selectNextOccurrence", - "Shift-Cmd-Space": "selectScope", - "Shift-Cmd-M": "selectBetweenBrackets", - "Cmd-M": "goToBracket", - "Cmd-Ctrl-Up": "swapLineUp", - "Cmd-Ctrl-Down": "swapLineDown", - "Cmd-/": "toggleCommentIndented", - "Cmd-J": "joinLines", - "Shift-Cmd-D": "duplicateLine", - "F5": "sortLines", - "Cmd-F5": "sortLinesInsensitive", - "F2": "nextBookmark", - "Shift-F2": "prevBookmark", - "Cmd-F2": "toggleBookmark", - "Shift-Cmd-F2": "clearBookmarks", - "Alt-F2": "selectBookmarks", - "Backspace": "smartBackspace", - "Cmd-K Cmd-D": "skipAndSelectNextOccurrence", - "Cmd-K Cmd-K": "delLineRight", - "Cmd-K Cmd-U": "upcaseAtCursor", - "Cmd-K Cmd-L": "downcaseAtCursor", - "Cmd-K Cmd-Space": "setSublimeMark", - "Cmd-K Cmd-A": "selectToSublimeMark", - "Cmd-K Cmd-W": "deleteToSublimeMark", - "Cmd-K Cmd-X": "swapWithSublimeMark", - "Cmd-K Cmd-Y": "sublimeYank", - "Cmd-K Cmd-C": "showInCenter", - "Cmd-K Cmd-G": "clearBookmarks", - "Cmd-K Cmd-Backspace": "delLineLeft", - "Cmd-K Cmd-1": "foldAll", - "Cmd-K Cmd-0": "unfoldAll", - "Cmd-K Cmd-J": "unfoldAll", - "Ctrl-Shift-Up": "addCursorToPrevLine", - "Ctrl-Shift-Down": "addCursorToNextLine", - "Cmd-F3": "findUnder", - "Shift-Cmd-F3": "findUnderPrevious", - "Alt-F3": "findAllUnder", - "Shift-Cmd-[": "fold", - "Shift-Cmd-]": "unfold", - "Cmd-I": "findIncremental", - "Shift-Cmd-I": "findIncrementalReverse", - "Cmd-H": "replace", - "F3": "findNext", - "Shift-F3": "findPrev", - "fallthrough": "macDefault" - }; - CodeMirror.normalizeKeyMap(keyMap.macSublime); - - keyMap.pcSublime = { - "Shift-Tab": "indentLess", - "Shift-Ctrl-K": "deleteLine", - "Alt-Q": "wrapLines", - "Ctrl-T": "transposeChars", - "Alt-Left": "goSubwordLeft", - "Alt-Right": "goSubwordRight", - "Ctrl-Up": "scrollLineUp", - "Ctrl-Down": "scrollLineDown", - "Ctrl-L": "selectLine", - "Shift-Ctrl-L": "splitSelectionByLine", - "Esc": "singleSelectionTop", - "Ctrl-Enter": "insertLineAfter", - "Shift-Ctrl-Enter": "insertLineBefore", - "Ctrl-D": "selectNextOccurrence", - "Shift-Ctrl-Space": "selectScope", - "Shift-Ctrl-M": "selectBetweenBrackets", - "Ctrl-M": "goToBracket", - "Shift-Ctrl-Up": "swapLineUp", - "Shift-Ctrl-Down": "swapLineDown", - "Ctrl-/": "toggleCommentIndented", - "Ctrl-J": "joinLines", - "Shift-Ctrl-D": "duplicateLine", - "F9": "sortLines", - "Ctrl-F9": "sortLinesInsensitive", - "F2": "nextBookmark", - "Shift-F2": "prevBookmark", - "Ctrl-F2": "toggleBookmark", - "Shift-Ctrl-F2": "clearBookmarks", - "Alt-F2": "selectBookmarks", - "Backspace": "smartBackspace", - "Ctrl-K Ctrl-D": "skipAndSelectNextOccurrence", - "Ctrl-K Ctrl-K": "delLineRight", - "Ctrl-K Ctrl-U": "upcaseAtCursor", - "Ctrl-K Ctrl-L": "downcaseAtCursor", - "Ctrl-K Ctrl-Space": "setSublimeMark", - "Ctrl-K Ctrl-A": "selectToSublimeMark", - "Ctrl-K Ctrl-W": "deleteToSublimeMark", - "Ctrl-K Ctrl-X": "swapWithSublimeMark", - "Ctrl-K Ctrl-Y": "sublimeYank", - "Ctrl-K Ctrl-C": "showInCenter", - "Ctrl-K Ctrl-G": "clearBookmarks", - "Ctrl-K Ctrl-Backspace": "delLineLeft", - "Ctrl-K Ctrl-1": "foldAll", - "Ctrl-K Ctrl-0": "unfoldAll", - "Ctrl-K Ctrl-J": "unfoldAll", - "Ctrl-Alt-Up": "addCursorToPrevLine", - "Ctrl-Alt-Down": "addCursorToNextLine", - "Ctrl-F3": "findUnder", - "Shift-Ctrl-F3": "findUnderPrevious", - "Alt-F3": "findAllUnder", - "Shift-Ctrl-[": "fold", - "Shift-Ctrl-]": "unfold", - "Ctrl-I": "findIncremental", - "Shift-Ctrl-I": "findIncrementalReverse", - "Ctrl-H": "replace", - "F3": "findNext", - "Shift-F3": "findPrev", - "fallthrough": "pcDefault" - }; - CodeMirror.normalizeKeyMap(keyMap.pcSublime); - - var mac = keyMap.default == keyMap.macDefault; - keyMap.sublime = mac ? keyMap.macSublime : keyMap.pcSublime; -}); diff --git a/plugins/UiFileManager/media/codemirror/mode/coffeescript.js b/plugins/UiFileManager/media/codemirror/mode/coffeescript.js deleted file mode 100644 index a54e9d5e..00000000 --- a/plugins/UiFileManager/media/codemirror/mode/coffeescript.js +++ /dev/null @@ -1,359 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -/** - * Link to the project's GitHub page: - * https://github.com/pickhardt/coffeescript-codemirror-mode - */ -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -CodeMirror.defineMode("coffeescript", function(conf, parserConf) { - var ERRORCLASS = "error"; - - function wordRegexp(words) { - return new RegExp("^((" + words.join(")|(") + "))\\b"); - } - - var operators = /^(?:->|=>|\+[+=]?|-[\-=]?|\*[\*=]?|\/[\/=]?|[=!]=|<[><]?=?|>>?=?|%=?|&=?|\|=?|\^=?|\~|!|\?|(or|and|\|\||&&|\?)=)/; - var delimiters = /^(?:[()\[\]{},:`=;]|\.\.?\.?)/; - var identifiers = /^[_A-Za-z$][_A-Za-z$0-9]*/; - var atProp = /^@[_A-Za-z$][_A-Za-z$0-9]*/; - - var wordOperators = wordRegexp(["and", "or", "not", - "is", "isnt", "in", - "instanceof", "typeof"]); - var indentKeywords = ["for", "while", "loop", "if", "unless", "else", - "switch", "try", "catch", "finally", "class"]; - var commonKeywords = ["break", "by", "continue", "debugger", "delete", - "do", "in", "of", "new", "return", "then", - "this", "@", "throw", "when", "until", "extends"]; - - var keywords = wordRegexp(indentKeywords.concat(commonKeywords)); - - indentKeywords = wordRegexp(indentKeywords); - - - var stringPrefixes = /^('{3}|\"{3}|['\"])/; - var regexPrefixes = /^(\/{3}|\/)/; - var commonConstants = ["Infinity", "NaN", "undefined", "null", "true", "false", "on", "off", "yes", "no"]; - var constants = wordRegexp(commonConstants); - - // Tokenizers - function tokenBase(stream, state) { - // Handle scope changes - if (stream.sol()) { - if (state.scope.align === null) state.scope.align = false; - var scopeOffset = state.scope.offset; - if (stream.eatSpace()) { - var lineOffset = stream.indentation(); - if (lineOffset > scopeOffset && state.scope.type == "coffee") { - return "indent"; - } else if (lineOffset < scopeOffset) { - return "dedent"; - } - return null; - } else { - if (scopeOffset > 0) { - dedent(stream, state); - } - } - } - if (stream.eatSpace()) { - return null; - } - - var ch = stream.peek(); - - // Handle docco title comment (single line) - if (stream.match("####")) { - stream.skipToEnd(); - return "comment"; - } - - // Handle multi line comments - if (stream.match("###")) { - state.tokenize = longComment; - return state.tokenize(stream, state); - } - - // Single line comment - if (ch === "#") { - stream.skipToEnd(); - return "comment"; - } - - // Handle number literals - if (stream.match(/^-?[0-9\.]/, false)) { - var floatLiteral = false; - // Floats - if (stream.match(/^-?\d*\.\d+(e[\+\-]?\d+)?/i)) { - floatLiteral = true; - } - if (stream.match(/^-?\d+\.\d*/)) { - floatLiteral = true; - } - if (stream.match(/^-?\.\d+/)) { - floatLiteral = true; - } - - if (floatLiteral) { - // prevent from getting extra . on 1.. - if (stream.peek() == "."){ - stream.backUp(1); - } - return "number"; - } - // Integers - var intLiteral = false; - // Hex - if (stream.match(/^-?0x[0-9a-f]+/i)) { - intLiteral = true; - } - // Decimal - if (stream.match(/^-?[1-9]\d*(e[\+\-]?\d+)?/)) { - intLiteral = true; - } - // Zero by itself with no other piece of number. - if (stream.match(/^-?0(?![\dx])/i)) { - intLiteral = true; - } - if (intLiteral) { - return "number"; - } - } - - // Handle strings - if (stream.match(stringPrefixes)) { - state.tokenize = tokenFactory(stream.current(), false, "string"); - return state.tokenize(stream, state); - } - // Handle regex literals - if (stream.match(regexPrefixes)) { - if (stream.current() != "/" || stream.match(/^.*\//, false)) { // prevent highlight of division - state.tokenize = tokenFactory(stream.current(), true, "string-2"); - return state.tokenize(stream, state); - } else { - stream.backUp(1); - } - } - - - - // Handle operators and delimiters - if (stream.match(operators) || stream.match(wordOperators)) { - return "operator"; - } - if (stream.match(delimiters)) { - return "punctuation"; - } - - if (stream.match(constants)) { - return "atom"; - } - - if (stream.match(atProp) || state.prop && stream.match(identifiers)) { - return "property"; - } - - if (stream.match(keywords)) { - return "keyword"; - } - - if (stream.match(identifiers)) { - return "variable"; - } - - // Handle non-detected items - stream.next(); - return ERRORCLASS; - } - - function tokenFactory(delimiter, singleline, outclass) { - return function(stream, state) { - while (!stream.eol()) { - stream.eatWhile(/[^'"\/\\]/); - if (stream.eat("\\")) { - stream.next(); - if (singleline && stream.eol()) { - return outclass; - } - } else if (stream.match(delimiter)) { - state.tokenize = tokenBase; - return outclass; - } else { - stream.eat(/['"\/]/); - } - } - if (singleline) { - if (parserConf.singleLineStringErrors) { - outclass = ERRORCLASS; - } else { - state.tokenize = tokenBase; - } - } - return outclass; - }; - } - - function longComment(stream, state) { - while (!stream.eol()) { - stream.eatWhile(/[^#]/); - if (stream.match("###")) { - state.tokenize = tokenBase; - break; - } - stream.eatWhile("#"); - } - return "comment"; - } - - function indent(stream, state, type) { - type = type || "coffee"; - var offset = 0, align = false, alignOffset = null; - for (var scope = state.scope; scope; scope = scope.prev) { - if (scope.type === "coffee" || scope.type == "}") { - offset = scope.offset + conf.indentUnit; - break; - } - } - if (type !== "coffee") { - align = null; - alignOffset = stream.column() + stream.current().length; - } else if (state.scope.align) { - state.scope.align = false; - } - state.scope = { - offset: offset, - type: type, - prev: state.scope, - align: align, - alignOffset: alignOffset - }; - } - - function dedent(stream, state) { - if (!state.scope.prev) return; - if (state.scope.type === "coffee") { - var _indent = stream.indentation(); - var matched = false; - for (var scope = state.scope; scope; scope = scope.prev) { - if (_indent === scope.offset) { - matched = true; - break; - } - } - if (!matched) { - return true; - } - while (state.scope.prev && state.scope.offset !== _indent) { - state.scope = state.scope.prev; - } - return false; - } else { - state.scope = state.scope.prev; - return false; - } - } - - function tokenLexer(stream, state) { - var style = state.tokenize(stream, state); - var current = stream.current(); - - // Handle scope changes. - if (current === "return") { - state.dedent = true; - } - if (((current === "->" || current === "=>") && stream.eol()) - || style === "indent") { - indent(stream, state); - } - var delimiter_index = "[({".indexOf(current); - if (delimiter_index !== -1) { - indent(stream, state, "])}".slice(delimiter_index, delimiter_index+1)); - } - if (indentKeywords.exec(current)){ - indent(stream, state); - } - if (current == "then"){ - dedent(stream, state); - } - - - if (style === "dedent") { - if (dedent(stream, state)) { - return ERRORCLASS; - } - } - delimiter_index = "])}".indexOf(current); - if (delimiter_index !== -1) { - while (state.scope.type == "coffee" && state.scope.prev) - state.scope = state.scope.prev; - if (state.scope.type == current) - state.scope = state.scope.prev; - } - if (state.dedent && stream.eol()) { - if (state.scope.type == "coffee" && state.scope.prev) - state.scope = state.scope.prev; - state.dedent = false; - } - - return style; - } - - var external = { - startState: function(basecolumn) { - return { - tokenize: tokenBase, - scope: {offset:basecolumn || 0, type:"coffee", prev: null, align: false}, - prop: false, - dedent: 0 - }; - }, - - token: function(stream, state) { - var fillAlign = state.scope.align === null && state.scope; - if (fillAlign && stream.sol()) fillAlign.align = false; - - var style = tokenLexer(stream, state); - if (style && style != "comment") { - if (fillAlign) fillAlign.align = true; - state.prop = style == "punctuation" && stream.current() == "." - } - - return style; - }, - - indent: function(state, text) { - if (state.tokenize != tokenBase) return 0; - var scope = state.scope; - var closer = text && "])}".indexOf(text.charAt(0)) > -1; - if (closer) while (scope.type == "coffee" && scope.prev) scope = scope.prev; - var closes = closer && scope.type === text.charAt(0); - if (scope.align) - return scope.alignOffset - (closes ? 1 : 0); - else - return (closes ? scope.prev : scope).offset; - }, - - lineComment: "#", - fold: "indent" - }; - return external; -}); - -// IANA registered media type -// https://www.iana.org/assignments/media-types/ -CodeMirror.defineMIME("application/vnd.coffeescript", "coffeescript"); - -CodeMirror.defineMIME("text/x-coffeescript", "coffeescript"); -CodeMirror.defineMIME("text/coffeescript", "coffeescript"); - -}); diff --git a/plugins/UiFileManager/media/codemirror/mode/css.js b/plugins/UiFileManager/media/codemirror/mode/css.js deleted file mode 100644 index 441ba4ab..00000000 --- a/plugins/UiFileManager/media/codemirror/mode/css.js +++ /dev/null @@ -1,860 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -CodeMirror.defineMode("css", function(config, parserConfig) { - var inline = parserConfig.inline - if (!parserConfig.propertyKeywords) parserConfig = CodeMirror.resolveMode("text/css"); - - var indentUnit = config.indentUnit, - tokenHooks = parserConfig.tokenHooks, - documentTypes = parserConfig.documentTypes || {}, - mediaTypes = parserConfig.mediaTypes || {}, - mediaFeatures = parserConfig.mediaFeatures || {}, - mediaValueKeywords = parserConfig.mediaValueKeywords || {}, - propertyKeywords = parserConfig.propertyKeywords || {}, - nonStandardPropertyKeywords = parserConfig.nonStandardPropertyKeywords || {}, - fontProperties = parserConfig.fontProperties || {}, - counterDescriptors = parserConfig.counterDescriptors || {}, - colorKeywords = parserConfig.colorKeywords || {}, - valueKeywords = parserConfig.valueKeywords || {}, - allowNested = parserConfig.allowNested, - lineComment = parserConfig.lineComment, - supportsAtComponent = parserConfig.supportsAtComponent === true; - - var type, override; - function ret(style, tp) { type = tp; return style; } - - // Tokenizers - - function tokenBase(stream, state) { - var ch = stream.next(); - if (tokenHooks[ch]) { - var result = tokenHooks[ch](stream, state); - if (result !== false) return result; - } - if (ch == "@") { - stream.eatWhile(/[\w\\\-]/); - return ret("def", stream.current()); - } else if (ch == "=" || (ch == "~" || ch == "|") && stream.eat("=")) { - return ret(null, "compare"); - } else if (ch == "\"" || ch == "'") { - state.tokenize = tokenString(ch); - return state.tokenize(stream, state); - } else if (ch == "#") { - stream.eatWhile(/[\w\\\-]/); - return ret("atom", "hash"); - } else if (ch == "!") { - stream.match(/^\s*\w*/); - return ret("keyword", "important"); - } else if (/\d/.test(ch) || ch == "." && stream.eat(/\d/)) { - stream.eatWhile(/[\w.%]/); - return ret("number", "unit"); - } else if (ch === "-") { - if (/[\d.]/.test(stream.peek())) { - stream.eatWhile(/[\w.%]/); - return ret("number", "unit"); - } else if (stream.match(/^-[\w\\\-]*/)) { - stream.eatWhile(/[\w\\\-]/); - if (stream.match(/^\s*:/, false)) - return ret("variable-2", "variable-definition"); - return ret("variable-2", "variable"); - } else if (stream.match(/^\w+-/)) { - return ret("meta", "meta"); - } - } else if (/[,+>*\/]/.test(ch)) { - return ret(null, "select-op"); - } else if (ch == "." && stream.match(/^-?[_a-z][_a-z0-9-]*/i)) { - return ret("qualifier", "qualifier"); - } else if (/[:;{}\[\]\(\)]/.test(ch)) { - return ret(null, ch); - } else if (stream.match(/[\w-.]+(?=\()/)) { - if (/^(url(-prefix)?|domain|regexp)$/.test(stream.current().toLowerCase())) { - state.tokenize = tokenParenthesized; - } - return ret("variable callee", "variable"); - } else if (/[\w\\\-]/.test(ch)) { - stream.eatWhile(/[\w\\\-]/); - return ret("property", "word"); - } else { - return ret(null, null); - } - } - - function tokenString(quote) { - return function(stream, state) { - var escaped = false, ch; - while ((ch = stream.next()) != null) { - if (ch == quote && !escaped) { - if (quote == ")") stream.backUp(1); - break; - } - escaped = !escaped && ch == "\\"; - } - if (ch == quote || !escaped && quote != ")") state.tokenize = null; - return ret("string", "string"); - }; - } - - function tokenParenthesized(stream, state) { - stream.next(); // Must be '(' - if (!stream.match(/\s*[\"\')]/, false)) - state.tokenize = tokenString(")"); - else - state.tokenize = null; - return ret(null, "("); - } - - // Context management - - function Context(type, indent, prev) { - this.type = type; - this.indent = indent; - this.prev = prev; - } - - function pushContext(state, stream, type, indent) { - state.context = new Context(type, stream.indentation() + (indent === false ? 0 : indentUnit), state.context); - return type; - } - - function popContext(state) { - if (state.context.prev) - state.context = state.context.prev; - return state.context.type; - } - - function pass(type, stream, state) { - return states[state.context.type](type, stream, state); - } - function popAndPass(type, stream, state, n) { - for (var i = n || 1; i > 0; i--) - state.context = state.context.prev; - return pass(type, stream, state); - } - - // Parser - - function wordAsValue(stream) { - var word = stream.current().toLowerCase(); - if (valueKeywords.hasOwnProperty(word)) - override = "atom"; - else if (colorKeywords.hasOwnProperty(word)) - override = "keyword"; - else - override = "variable"; - } - - var states = {}; - - states.top = function(type, stream, state) { - if (type == "{") { - return pushContext(state, stream, "block"); - } else if (type == "}" && state.context.prev) { - return popContext(state); - } else if (supportsAtComponent && /@component/i.test(type)) { - return pushContext(state, stream, "atComponentBlock"); - } else if (/^@(-moz-)?document$/i.test(type)) { - return pushContext(state, stream, "documentTypes"); - } else if (/^@(media|supports|(-moz-)?document|import)$/i.test(type)) { - return pushContext(state, stream, "atBlock"); - } else if (/^@(font-face|counter-style)/i.test(type)) { - state.stateArg = type; - return "restricted_atBlock_before"; - } else if (/^@(-(moz|ms|o|webkit)-)?keyframes$/i.test(type)) { - return "keyframes"; - } else if (type && type.charAt(0) == "@") { - return pushContext(state, stream, "at"); - } else if (type == "hash") { - override = "builtin"; - } else if (type == "word") { - override = "tag"; - } else if (type == "variable-definition") { - return "maybeprop"; - } else if (type == "interpolation") { - return pushContext(state, stream, "interpolation"); - } else if (type == ":") { - return "pseudo"; - } else if (allowNested && type == "(") { - return pushContext(state, stream, "parens"); - } - return state.context.type; - }; - - states.block = function(type, stream, state) { - if (type == "word") { - var word = stream.current().toLowerCase(); - if (propertyKeywords.hasOwnProperty(word)) { - override = "property"; - return "maybeprop"; - } else if (nonStandardPropertyKeywords.hasOwnProperty(word)) { - override = "string-2"; - return "maybeprop"; - } else if (allowNested) { - override = stream.match(/^\s*:(?:\s|$)/, false) ? "property" : "tag"; - return "block"; - } else { - override += " error"; - return "maybeprop"; - } - } else if (type == "meta") { - return "block"; - } else if (!allowNested && (type == "hash" || type == "qualifier")) { - override = "error"; - return "block"; - } else { - return states.top(type, stream, state); - } - }; - - states.maybeprop = function(type, stream, state) { - if (type == ":") return pushContext(state, stream, "prop"); - return pass(type, stream, state); - }; - - states.prop = function(type, stream, state) { - if (type == ";") return popContext(state); - if (type == "{" && allowNested) return pushContext(state, stream, "propBlock"); - if (type == "}" || type == "{") return popAndPass(type, stream, state); - if (type == "(") return pushContext(state, stream, "parens"); - - if (type == "hash" && !/^#([0-9a-fA-f]{3,4}|[0-9a-fA-f]{6}|[0-9a-fA-f]{8})$/.test(stream.current())) { - override += " error"; - } else if (type == "word") { - wordAsValue(stream); - } else if (type == "interpolation") { - return pushContext(state, stream, "interpolation"); - } - return "prop"; - }; - - states.propBlock = function(type, _stream, state) { - if (type == "}") return popContext(state); - if (type == "word") { override = "property"; return "maybeprop"; } - return state.context.type; - }; - - states.parens = function(type, stream, state) { - if (type == "{" || type == "}") return popAndPass(type, stream, state); - if (type == ")") return popContext(state); - if (type == "(") return pushContext(state, stream, "parens"); - if (type == "interpolation") return pushContext(state, stream, "interpolation"); - if (type == "word") wordAsValue(stream); - return "parens"; - }; - - states.pseudo = function(type, stream, state) { - if (type == "meta") return "pseudo"; - - if (type == "word") { - override = "variable-3"; - return state.context.type; - } - return pass(type, stream, state); - }; - - states.documentTypes = function(type, stream, state) { - if (type == "word" && documentTypes.hasOwnProperty(stream.current())) { - override = "tag"; - return state.context.type; - } else { - return states.atBlock(type, stream, state); - } - }; - - states.atBlock = function(type, stream, state) { - if (type == "(") return pushContext(state, stream, "atBlock_parens"); - if (type == "}" || type == ";") return popAndPass(type, stream, state); - if (type == "{") return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top"); - - if (type == "interpolation") return pushContext(state, stream, "interpolation"); - - if (type == "word") { - var word = stream.current().toLowerCase(); - if (word == "only" || word == "not" || word == "and" || word == "or") - override = "keyword"; - else if (mediaTypes.hasOwnProperty(word)) - override = "attribute"; - else if (mediaFeatures.hasOwnProperty(word)) - override = "property"; - else if (mediaValueKeywords.hasOwnProperty(word)) - override = "keyword"; - else if (propertyKeywords.hasOwnProperty(word)) - override = "property"; - else if (nonStandardPropertyKeywords.hasOwnProperty(word)) - override = "string-2"; - else if (valueKeywords.hasOwnProperty(word)) - override = "atom"; - else if (colorKeywords.hasOwnProperty(word)) - override = "keyword"; - else - override = "error"; - } - return state.context.type; - }; - - states.atComponentBlock = function(type, stream, state) { - if (type == "}") - return popAndPass(type, stream, state); - if (type == "{") - return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top", false); - if (type == "word") - override = "error"; - return state.context.type; - }; - - states.atBlock_parens = function(type, stream, state) { - if (type == ")") return popContext(state); - if (type == "{" || type == "}") return popAndPass(type, stream, state, 2); - return states.atBlock(type, stream, state); - }; - - states.restricted_atBlock_before = function(type, stream, state) { - if (type == "{") - return pushContext(state, stream, "restricted_atBlock"); - if (type == "word" && state.stateArg == "@counter-style") { - override = "variable"; - return "restricted_atBlock_before"; - } - return pass(type, stream, state); - }; - - states.restricted_atBlock = function(type, stream, state) { - if (type == "}") { - state.stateArg = null; - return popContext(state); - } - if (type == "word") { - if ((state.stateArg == "@font-face" && !fontProperties.hasOwnProperty(stream.current().toLowerCase())) || - (state.stateArg == "@counter-style" && !counterDescriptors.hasOwnProperty(stream.current().toLowerCase()))) - override = "error"; - else - override = "property"; - return "maybeprop"; - } - return "restricted_atBlock"; - }; - - states.keyframes = function(type, stream, state) { - if (type == "word") { override = "variable"; return "keyframes"; } - if (type == "{") return pushContext(state, stream, "top"); - return pass(type, stream, state); - }; - - states.at = function(type, stream, state) { - if (type == ";") return popContext(state); - if (type == "{" || type == "}") return popAndPass(type, stream, state); - if (type == "word") override = "tag"; - else if (type == "hash") override = "builtin"; - return "at"; - }; - - states.interpolation = function(type, stream, state) { - if (type == "}") return popContext(state); - if (type == "{" || type == ";") return popAndPass(type, stream, state); - if (type == "word") override = "variable"; - else if (type != "variable" && type != "(" && type != ")") override = "error"; - return "interpolation"; - }; - - return { - startState: function(base) { - return {tokenize: null, - state: inline ? "block" : "top", - stateArg: null, - context: new Context(inline ? "block" : "top", base || 0, null)}; - }, - - token: function(stream, state) { - if (!state.tokenize && stream.eatSpace()) return null; - var style = (state.tokenize || tokenBase)(stream, state); - if (style && typeof style == "object") { - type = style[1]; - style = style[0]; - } - override = style; - if (type != "comment") - state.state = states[state.state](type, stream, state); - return override; - }, - - indent: function(state, textAfter) { - var cx = state.context, ch = textAfter && textAfter.charAt(0); - var indent = cx.indent; - if (cx.type == "prop" && (ch == "}" || ch == ")")) cx = cx.prev; - if (cx.prev) { - if (ch == "}" && (cx.type == "block" || cx.type == "top" || - cx.type == "interpolation" || cx.type == "restricted_atBlock")) { - // Resume indentation from parent context. - cx = cx.prev; - indent = cx.indent; - } else if (ch == ")" && (cx.type == "parens" || cx.type == "atBlock_parens") || - ch == "{" && (cx.type == "at" || cx.type == "atBlock")) { - // Dedent relative to current context. - indent = Math.max(0, cx.indent - indentUnit); - } - } - return indent; - }, - - electricChars: "}", - blockCommentStart: "/*", - blockCommentEnd: "*/", - blockCommentContinue: " * ", - lineComment: lineComment, - fold: "brace" - }; -}); - - function keySet(array) { - var keys = {}; - for (var i = 0; i < array.length; ++i) { - keys[array[i].toLowerCase()] = true; - } - return keys; - } - - var documentTypes_ = [ - "domain", "regexp", "url", "url-prefix" - ], documentTypes = keySet(documentTypes_); - - var mediaTypes_ = [ - "all", "aural", "braille", "handheld", "print", "projection", "screen", - "tty", "tv", "embossed" - ], mediaTypes = keySet(mediaTypes_); - - var mediaFeatures_ = [ - "width", "min-width", "max-width", "height", "min-height", "max-height", - "device-width", "min-device-width", "max-device-width", "device-height", - "min-device-height", "max-device-height", "aspect-ratio", - "min-aspect-ratio", "max-aspect-ratio", "device-aspect-ratio", - "min-device-aspect-ratio", "max-device-aspect-ratio", "color", "min-color", - "max-color", "color-index", "min-color-index", "max-color-index", - "monochrome", "min-monochrome", "max-monochrome", "resolution", - "min-resolution", "max-resolution", "scan", "grid", "orientation", - "device-pixel-ratio", "min-device-pixel-ratio", "max-device-pixel-ratio", - "pointer", "any-pointer", "hover", "any-hover" - ], mediaFeatures = keySet(mediaFeatures_); - - var mediaValueKeywords_ = [ - "landscape", "portrait", "none", "coarse", "fine", "on-demand", "hover", - "interlace", "progressive" - ], mediaValueKeywords = keySet(mediaValueKeywords_); - - var propertyKeywords_ = [ - "align-content", "align-items", "align-self", "alignment-adjust", - "alignment-baseline", "anchor-point", "animation", "animation-delay", - "animation-direction", "animation-duration", "animation-fill-mode", - "animation-iteration-count", "animation-name", "animation-play-state", - "animation-timing-function", "appearance", "azimuth", "backdrop-filter", - "backface-visibility", "background", "background-attachment", - "background-blend-mode", "background-clip", "background-color", - "background-image", "background-origin", "background-position", - "background-position-x", "background-position-y", "background-repeat", - "background-size", "baseline-shift", "binding", "bleed", "block-size", - "bookmark-label", "bookmark-level", "bookmark-state", "bookmark-target", - "border", "border-bottom", "border-bottom-color", "border-bottom-left-radius", - "border-bottom-right-radius", "border-bottom-style", "border-bottom-width", - "border-collapse", "border-color", "border-image", "border-image-outset", - "border-image-repeat", "border-image-slice", "border-image-source", - "border-image-width", "border-left", "border-left-color", "border-left-style", - "border-left-width", "border-radius", "border-right", "border-right-color", - "border-right-style", "border-right-width", "border-spacing", "border-style", - "border-top", "border-top-color", "border-top-left-radius", - "border-top-right-radius", "border-top-style", "border-top-width", - "border-width", "bottom", "box-decoration-break", "box-shadow", "box-sizing", - "break-after", "break-before", "break-inside", "caption-side", "caret-color", - "clear", "clip", "color", "color-profile", "column-count", "column-fill", - "column-gap", "column-rule", "column-rule-color", "column-rule-style", - "column-rule-width", "column-span", "column-width", "columns", "contain", - "content", "counter-increment", "counter-reset", "crop", "cue", "cue-after", - "cue-before", "cursor", "direction", "display", "dominant-baseline", - "drop-initial-after-adjust", "drop-initial-after-align", - "drop-initial-before-adjust", "drop-initial-before-align", "drop-initial-size", - "drop-initial-value", "elevation", "empty-cells", "fit", "fit-position", - "flex", "flex-basis", "flex-direction", "flex-flow", "flex-grow", - "flex-shrink", "flex-wrap", "float", "float-offset", "flow-from", "flow-into", - "font", "font-family", "font-feature-settings", "font-kerning", - "font-language-override", "font-optical-sizing", "font-size", - "font-size-adjust", "font-stretch", "font-style", "font-synthesis", - "font-variant", "font-variant-alternates", "font-variant-caps", - "font-variant-east-asian", "font-variant-ligatures", "font-variant-numeric", - "font-variant-position", "font-variation-settings", "font-weight", "gap", - "grid", "grid-area", "grid-auto-columns", "grid-auto-flow", "grid-auto-rows", - "grid-column", "grid-column-end", "grid-column-gap", "grid-column-start", - "grid-gap", "grid-row", "grid-row-end", "grid-row-gap", "grid-row-start", - "grid-template", "grid-template-areas", "grid-template-columns", - "grid-template-rows", "hanging-punctuation", "height", "hyphens", "icon", - "image-orientation", "image-rendering", "image-resolution", "inline-box-align", - "inset", "inset-block", "inset-block-end", "inset-block-start", "inset-inline", - "inset-inline-end", "inset-inline-start", "isolation", "justify-content", - "justify-items", "justify-self", "left", "letter-spacing", "line-break", - "line-height", "line-height-step", "line-stacking", "line-stacking-ruby", - "line-stacking-shift", "line-stacking-strategy", "list-style", - "list-style-image", "list-style-position", "list-style-type", "margin", - "margin-bottom", "margin-left", "margin-right", "margin-top", "marks", - "marquee-direction", "marquee-loop", "marquee-play-count", "marquee-speed", - "marquee-style", "max-block-size", "max-height", "max-inline-size", - "max-width", "min-block-size", "min-height", "min-inline-size", "min-width", - "mix-blend-mode", "move-to", "nav-down", "nav-index", "nav-left", "nav-right", - "nav-up", "object-fit", "object-position", "offset", "offset-anchor", - "offset-distance", "offset-path", "offset-position", "offset-rotate", - "opacity", "order", "orphans", "outline", "outline-color", "outline-offset", - "outline-style", "outline-width", "overflow", "overflow-style", - "overflow-wrap", "overflow-x", "overflow-y", "padding", "padding-bottom", - "padding-left", "padding-right", "padding-top", "page", "page-break-after", - "page-break-before", "page-break-inside", "page-policy", "pause", - "pause-after", "pause-before", "perspective", "perspective-origin", "pitch", - "pitch-range", "place-content", "place-items", "place-self", "play-during", - "position", "presentation-level", "punctuation-trim", "quotes", - "region-break-after", "region-break-before", "region-break-inside", - "region-fragment", "rendering-intent", "resize", "rest", "rest-after", - "rest-before", "richness", "right", "rotate", "rotation", "rotation-point", - "row-gap", "ruby-align", "ruby-overhang", "ruby-position", "ruby-span", - "scale", "scroll-behavior", "scroll-margin", "scroll-margin-block", - "scroll-margin-block-end", "scroll-margin-block-start", "scroll-margin-bottom", - "scroll-margin-inline", "scroll-margin-inline-end", - "scroll-margin-inline-start", "scroll-margin-left", "scroll-margin-right", - "scroll-margin-top", "scroll-padding", "scroll-padding-block", - "scroll-padding-block-end", "scroll-padding-block-start", - "scroll-padding-bottom", "scroll-padding-inline", "scroll-padding-inline-end", - "scroll-padding-inline-start", "scroll-padding-left", "scroll-padding-right", - "scroll-padding-top", "scroll-snap-align", "scroll-snap-type", - "shape-image-threshold", "shape-inside", "shape-margin", "shape-outside", - "size", "speak", "speak-as", "speak-header", "speak-numeral", - "speak-punctuation", "speech-rate", "stress", "string-set", "tab-size", - "table-layout", "target", "target-name", "target-new", "target-position", - "text-align", "text-align-last", "text-combine-upright", "text-decoration", - "text-decoration-color", "text-decoration-line", "text-decoration-skip", - "text-decoration-skip-ink", "text-decoration-style", "text-emphasis", - "text-emphasis-color", "text-emphasis-position", "text-emphasis-style", - "text-height", "text-indent", "text-justify", "text-orientation", - "text-outline", "text-overflow", "text-rendering", "text-shadow", - "text-size-adjust", "text-space-collapse", "text-transform", - "text-underline-position", "text-wrap", "top", "transform", "transform-origin", - "transform-style", "transition", "transition-delay", "transition-duration", - "transition-property", "transition-timing-function", "translate", - "unicode-bidi", "user-select", "vertical-align", "visibility", "voice-balance", - "voice-duration", "voice-family", "voice-pitch", "voice-range", "voice-rate", - "voice-stress", "voice-volume", "volume", "white-space", "widows", "width", - "will-change", "word-break", "word-spacing", "word-wrap", "writing-mode", "z-index", - // SVG-specific - "clip-path", "clip-rule", "mask", "enable-background", "filter", "flood-color", - "flood-opacity", "lighting-color", "stop-color", "stop-opacity", "pointer-events", - "color-interpolation", "color-interpolation-filters", - "color-rendering", "fill", "fill-opacity", "fill-rule", "image-rendering", - "marker", "marker-end", "marker-mid", "marker-start", "shape-rendering", "stroke", - "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", - "stroke-miterlimit", "stroke-opacity", "stroke-width", "text-rendering", - "baseline-shift", "dominant-baseline", "glyph-orientation-horizontal", - "glyph-orientation-vertical", "text-anchor", "writing-mode" - ], propertyKeywords = keySet(propertyKeywords_); - - var nonStandardPropertyKeywords_ = [ - "border-block", "border-block-color", "border-block-end", - "border-block-end-color", "border-block-end-style", "border-block-end-width", - "border-block-start", "border-block-start-color", "border-block-start-style", - "border-block-start-width", "border-block-style", "border-block-width", - "border-inline", "border-inline-color", "border-inline-end", - "border-inline-end-color", "border-inline-end-style", - "border-inline-end-width", "border-inline-start", "border-inline-start-color", - "border-inline-start-style", "border-inline-start-width", - "border-inline-style", "border-inline-width", "margin-block", - "margin-block-end", "margin-block-start", "margin-inline", "margin-inline-end", - "margin-inline-start", "padding-block", "padding-block-end", - "padding-block-start", "padding-inline", "padding-inline-end", - "padding-inline-start", "scroll-snap-stop", "scrollbar-3d-light-color", - "scrollbar-arrow-color", "scrollbar-base-color", "scrollbar-dark-shadow-color", - "scrollbar-face-color", "scrollbar-highlight-color", "scrollbar-shadow-color", - "scrollbar-track-color", "searchfield-cancel-button", "searchfield-decoration", - "searchfield-results-button", "searchfield-results-decoration", "shape-inside", "zoom" - ], nonStandardPropertyKeywords = keySet(nonStandardPropertyKeywords_); - - var fontProperties_ = [ - "font-display", "font-family", "src", "unicode-range", "font-variant", - "font-feature-settings", "font-stretch", "font-weight", "font-style" - ], fontProperties = keySet(fontProperties_); - - var counterDescriptors_ = [ - "additive-symbols", "fallback", "negative", "pad", "prefix", "range", - "speak-as", "suffix", "symbols", "system" - ], counterDescriptors = keySet(counterDescriptors_); - - var colorKeywords_ = [ - "aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige", - "bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown", - "burlywood", "cadetblue", "chartreuse", "chocolate", "coral", "cornflowerblue", - "cornsilk", "crimson", "cyan", "darkblue", "darkcyan", "darkgoldenrod", - "darkgray", "darkgreen", "darkkhaki", "darkmagenta", "darkolivegreen", - "darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen", - "darkslateblue", "darkslategray", "darkturquoise", "darkviolet", - "deeppink", "deepskyblue", "dimgray", "dodgerblue", "firebrick", - "floralwhite", "forestgreen", "fuchsia", "gainsboro", "ghostwhite", - "gold", "goldenrod", "gray", "grey", "green", "greenyellow", "honeydew", - "hotpink", "indianred", "indigo", "ivory", "khaki", "lavender", - "lavenderblush", "lawngreen", "lemonchiffon", "lightblue", "lightcoral", - "lightcyan", "lightgoldenrodyellow", "lightgray", "lightgreen", "lightpink", - "lightsalmon", "lightseagreen", "lightskyblue", "lightslategray", - "lightsteelblue", "lightyellow", "lime", "limegreen", "linen", "magenta", - "maroon", "mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple", - "mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise", - "mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin", - "navajowhite", "navy", "oldlace", "olive", "olivedrab", "orange", "orangered", - "orchid", "palegoldenrod", "palegreen", "paleturquoise", "palevioletred", - "papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue", - "purple", "rebeccapurple", "red", "rosybrown", "royalblue", "saddlebrown", - "salmon", "sandybrown", "seagreen", "seashell", "sienna", "silver", "skyblue", - "slateblue", "slategray", "snow", "springgreen", "steelblue", "tan", - "teal", "thistle", "tomato", "turquoise", "violet", "wheat", "white", - "whitesmoke", "yellow", "yellowgreen" - ], colorKeywords = keySet(colorKeywords_); - - var valueKeywords_ = [ - "above", "absolute", "activeborder", "additive", "activecaption", "afar", - "after-white-space", "ahead", "alias", "all", "all-scroll", "alphabetic", "alternate", - "always", "amharic", "amharic-abegede", "antialiased", "appworkspace", - "arabic-indic", "armenian", "asterisks", "attr", "auto", "auto-flow", "avoid", "avoid-column", "avoid-page", - "avoid-region", "background", "backwards", "baseline", "below", "bidi-override", "binary", - "bengali", "blink", "block", "block-axis", "bold", "bolder", "border", "border-box", - "both", "bottom", "break", "break-all", "break-word", "bullets", "button", "button-bevel", - "buttonface", "buttonhighlight", "buttonshadow", "buttontext", "calc", "cambodian", - "capitalize", "caps-lock-indicator", "caption", "captiontext", "caret", - "cell", "center", "checkbox", "circle", "cjk-decimal", "cjk-earthly-branch", - "cjk-heavenly-stem", "cjk-ideographic", "clear", "clip", "close-quote", - "col-resize", "collapse", "color", "color-burn", "color-dodge", "column", "column-reverse", - "compact", "condensed", "contain", "content", "contents", - "content-box", "context-menu", "continuous", "copy", "counter", "counters", "cover", "crop", - "cross", "crosshair", "currentcolor", "cursive", "cyclic", "darken", "dashed", "decimal", - "decimal-leading-zero", "default", "default-button", "dense", "destination-atop", - "destination-in", "destination-out", "destination-over", "devanagari", "difference", - "disc", "discard", "disclosure-closed", "disclosure-open", "document", - "dot-dash", "dot-dot-dash", - "dotted", "double", "down", "e-resize", "ease", "ease-in", "ease-in-out", "ease-out", - "element", "ellipse", "ellipsis", "embed", "end", "ethiopic", "ethiopic-abegede", - "ethiopic-abegede-am-et", "ethiopic-abegede-gez", "ethiopic-abegede-ti-er", - "ethiopic-abegede-ti-et", "ethiopic-halehame-aa-er", - "ethiopic-halehame-aa-et", "ethiopic-halehame-am-et", - "ethiopic-halehame-gez", "ethiopic-halehame-om-et", - "ethiopic-halehame-sid-et", "ethiopic-halehame-so-et", - "ethiopic-halehame-ti-er", "ethiopic-halehame-ti-et", "ethiopic-halehame-tig", - "ethiopic-numeric", "ew-resize", "exclusion", "expanded", "extends", "extra-condensed", - "extra-expanded", "fantasy", "fast", "fill", "fixed", "flat", "flex", "flex-end", "flex-start", "footnotes", - "forwards", "from", "geometricPrecision", "georgian", "graytext", "grid", "groove", - "gujarati", "gurmukhi", "hand", "hangul", "hangul-consonant", "hard-light", "hebrew", - "help", "hidden", "hide", "higher", "highlight", "highlighttext", - "hiragana", "hiragana-iroha", "horizontal", "hsl", "hsla", "hue", "icon", "ignore", - "inactiveborder", "inactivecaption", "inactivecaptiontext", "infinite", - "infobackground", "infotext", "inherit", "initial", "inline", "inline-axis", - "inline-block", "inline-flex", "inline-grid", "inline-table", "inset", "inside", "intrinsic", "invert", - "italic", "japanese-formal", "japanese-informal", "justify", "kannada", - "katakana", "katakana-iroha", "keep-all", "khmer", - "korean-hangul-formal", "korean-hanja-formal", "korean-hanja-informal", - "landscape", "lao", "large", "larger", "left", "level", "lighter", "lighten", - "line-through", "linear", "linear-gradient", "lines", "list-item", "listbox", "listitem", - "local", "logical", "loud", "lower", "lower-alpha", "lower-armenian", - "lower-greek", "lower-hexadecimal", "lower-latin", "lower-norwegian", - "lower-roman", "lowercase", "ltr", "luminosity", "malayalam", "match", "matrix", "matrix3d", - "media-controls-background", "media-current-time-display", - "media-fullscreen-button", "media-mute-button", "media-play-button", - "media-return-to-realtime-button", "media-rewind-button", - "media-seek-back-button", "media-seek-forward-button", "media-slider", - "media-sliderthumb", "media-time-remaining-display", "media-volume-slider", - "media-volume-slider-container", "media-volume-sliderthumb", "medium", - "menu", "menulist", "menulist-button", "menulist-text", - "menulist-textfield", "menutext", "message-box", "middle", "min-intrinsic", - "mix", "mongolian", "monospace", "move", "multiple", "multiply", "myanmar", "n-resize", - "narrower", "ne-resize", "nesw-resize", "no-close-quote", "no-drop", - "no-open-quote", "no-repeat", "none", "normal", "not-allowed", "nowrap", - "ns-resize", "numbers", "numeric", "nw-resize", "nwse-resize", "oblique", "octal", "opacity", "open-quote", - "optimizeLegibility", "optimizeSpeed", "oriya", "oromo", "outset", - "outside", "outside-shape", "overlay", "overline", "padding", "padding-box", - "painted", "page", "paused", "persian", "perspective", "plus-darker", "plus-lighter", - "pointer", "polygon", "portrait", "pre", "pre-line", "pre-wrap", "preserve-3d", - "progress", "push-button", "radial-gradient", "radio", "read-only", - "read-write", "read-write-plaintext-only", "rectangle", "region", - "relative", "repeat", "repeating-linear-gradient", - "repeating-radial-gradient", "repeat-x", "repeat-y", "reset", "reverse", - "rgb", "rgba", "ridge", "right", "rotate", "rotate3d", "rotateX", "rotateY", - "rotateZ", "round", "row", "row-resize", "row-reverse", "rtl", "run-in", "running", - "s-resize", "sans-serif", "saturation", "scale", "scale3d", "scaleX", "scaleY", "scaleZ", "screen", - "scroll", "scrollbar", "scroll-position", "se-resize", "searchfield", - "searchfield-cancel-button", "searchfield-decoration", - "searchfield-results-button", "searchfield-results-decoration", "self-start", "self-end", - "semi-condensed", "semi-expanded", "separate", "serif", "show", "sidama", - "simp-chinese-formal", "simp-chinese-informal", "single", - "skew", "skewX", "skewY", "skip-white-space", "slide", "slider-horizontal", - "slider-vertical", "sliderthumb-horizontal", "sliderthumb-vertical", "slow", - "small", "small-caps", "small-caption", "smaller", "soft-light", "solid", "somali", - "source-atop", "source-in", "source-out", "source-over", "space", "space-around", "space-between", "space-evenly", "spell-out", "square", - "square-button", "start", "static", "status-bar", "stretch", "stroke", "sub", - "subpixel-antialiased", "super", "sw-resize", "symbolic", "symbols", "system-ui", "table", - "table-caption", "table-cell", "table-column", "table-column-group", - "table-footer-group", "table-header-group", "table-row", "table-row-group", - "tamil", - "telugu", "text", "text-bottom", "text-top", "textarea", "textfield", "thai", - "thick", "thin", "threeddarkshadow", "threedface", "threedhighlight", - "threedlightshadow", "threedshadow", "tibetan", "tigre", "tigrinya-er", - "tigrinya-er-abegede", "tigrinya-et", "tigrinya-et-abegede", "to", "top", - "trad-chinese-formal", "trad-chinese-informal", "transform", - "translate", "translate3d", "translateX", "translateY", "translateZ", - "transparent", "ultra-condensed", "ultra-expanded", "underline", "unset", "up", - "upper-alpha", "upper-armenian", "upper-greek", "upper-hexadecimal", - "upper-latin", "upper-norwegian", "upper-roman", "uppercase", "urdu", "url", - "var", "vertical", "vertical-text", "visible", "visibleFill", "visiblePainted", - "visibleStroke", "visual", "w-resize", "wait", "wave", "wider", - "window", "windowframe", "windowtext", "words", "wrap", "wrap-reverse", "x-large", "x-small", "xor", - "xx-large", "xx-small" - ], valueKeywords = keySet(valueKeywords_); - - var allWords = documentTypes_.concat(mediaTypes_).concat(mediaFeatures_).concat(mediaValueKeywords_) - .concat(propertyKeywords_).concat(nonStandardPropertyKeywords_).concat(colorKeywords_) - .concat(valueKeywords_); - CodeMirror.registerHelper("hintWords", "css", allWords); - - function tokenCComment(stream, state) { - var maybeEnd = false, ch; - while ((ch = stream.next()) != null) { - if (maybeEnd && ch == "/") { - state.tokenize = null; - break; - } - maybeEnd = (ch == "*"); - } - return ["comment", "comment"]; - } - - CodeMirror.defineMIME("text/css", { - documentTypes: documentTypes, - mediaTypes: mediaTypes, - mediaFeatures: mediaFeatures, - mediaValueKeywords: mediaValueKeywords, - propertyKeywords: propertyKeywords, - nonStandardPropertyKeywords: nonStandardPropertyKeywords, - fontProperties: fontProperties, - counterDescriptors: counterDescriptors, - colorKeywords: colorKeywords, - valueKeywords: valueKeywords, - tokenHooks: { - "/": function(stream, state) { - if (!stream.eat("*")) return false; - state.tokenize = tokenCComment; - return tokenCComment(stream, state); - } - }, - name: "css" - }); - - CodeMirror.defineMIME("text/x-scss", { - mediaTypes: mediaTypes, - mediaFeatures: mediaFeatures, - mediaValueKeywords: mediaValueKeywords, - propertyKeywords: propertyKeywords, - nonStandardPropertyKeywords: nonStandardPropertyKeywords, - colorKeywords: colorKeywords, - valueKeywords: valueKeywords, - fontProperties: fontProperties, - allowNested: true, - lineComment: "//", - tokenHooks: { - "/": function(stream, state) { - if (stream.eat("/")) { - stream.skipToEnd(); - return ["comment", "comment"]; - } else if (stream.eat("*")) { - state.tokenize = tokenCComment; - return tokenCComment(stream, state); - } else { - return ["operator", "operator"]; - } - }, - ":": function(stream) { - if (stream.match(/\s*\{/, false)) - return [null, null] - return false; - }, - "$": function(stream) { - stream.match(/^[\w-]+/); - if (stream.match(/^\s*:/, false)) - return ["variable-2", "variable-definition"]; - return ["variable-2", "variable"]; - }, - "#": function(stream) { - if (!stream.eat("{")) return false; - return [null, "interpolation"]; - } - }, - name: "css", - helperType: "scss" - }); - - CodeMirror.defineMIME("text/x-less", { - mediaTypes: mediaTypes, - mediaFeatures: mediaFeatures, - mediaValueKeywords: mediaValueKeywords, - propertyKeywords: propertyKeywords, - nonStandardPropertyKeywords: nonStandardPropertyKeywords, - colorKeywords: colorKeywords, - valueKeywords: valueKeywords, - fontProperties: fontProperties, - allowNested: true, - lineComment: "//", - tokenHooks: { - "/": function(stream, state) { - if (stream.eat("/")) { - stream.skipToEnd(); - return ["comment", "comment"]; - } else if (stream.eat("*")) { - state.tokenize = tokenCComment; - return tokenCComment(stream, state); - } else { - return ["operator", "operator"]; - } - }, - "@": function(stream) { - if (stream.eat("{")) return [null, "interpolation"]; - if (stream.match(/^(charset|document|font-face|import|(-(moz|ms|o|webkit)-)?keyframes|media|namespace|page|supports)\b/i, false)) return false; - stream.eatWhile(/[\w\\\-]/); - if (stream.match(/^\s*:/, false)) - return ["variable-2", "variable-definition"]; - return ["variable-2", "variable"]; - }, - "&": function() { - return ["atom", "atom"]; - } - }, - name: "css", - helperType: "less" - }); - - CodeMirror.defineMIME("text/x-gss", { - documentTypes: documentTypes, - mediaTypes: mediaTypes, - mediaFeatures: mediaFeatures, - propertyKeywords: propertyKeywords, - nonStandardPropertyKeywords: nonStandardPropertyKeywords, - fontProperties: fontProperties, - counterDescriptors: counterDescriptors, - colorKeywords: colorKeywords, - valueKeywords: valueKeywords, - supportsAtComponent: true, - tokenHooks: { - "/": function(stream, state) { - if (!stream.eat("*")) return false; - state.tokenize = tokenCComment; - return tokenCComment(stream, state); - } - }, - name: "css", - helperType: "gss" - }); - -}); diff --git a/plugins/UiFileManager/media/codemirror/mode/go.js b/plugins/UiFileManager/media/codemirror/mode/go.js deleted file mode 100644 index c005e42d..00000000 --- a/plugins/UiFileManager/media/codemirror/mode/go.js +++ /dev/null @@ -1,187 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -CodeMirror.defineMode("go", function(config) { - var indentUnit = config.indentUnit; - - var keywords = { - "break":true, "case":true, "chan":true, "const":true, "continue":true, - "default":true, "defer":true, "else":true, "fallthrough":true, "for":true, - "func":true, "go":true, "goto":true, "if":true, "import":true, - "interface":true, "map":true, "package":true, "range":true, "return":true, - "select":true, "struct":true, "switch":true, "type":true, "var":true, - "bool":true, "byte":true, "complex64":true, "complex128":true, - "float32":true, "float64":true, "int8":true, "int16":true, "int32":true, - "int64":true, "string":true, "uint8":true, "uint16":true, "uint32":true, - "uint64":true, "int":true, "uint":true, "uintptr":true, "error": true, - "rune":true - }; - - var atoms = { - "true":true, "false":true, "iota":true, "nil":true, "append":true, - "cap":true, "close":true, "complex":true, "copy":true, "delete":true, "imag":true, - "len":true, "make":true, "new":true, "panic":true, "print":true, - "println":true, "real":true, "recover":true - }; - - var isOperatorChar = /[+\-*&^%:=<>!|\/]/; - - var curPunc; - - function tokenBase(stream, state) { - var ch = stream.next(); - if (ch == '"' || ch == "'" || ch == "`") { - state.tokenize = tokenString(ch); - return state.tokenize(stream, state); - } - if (/[\d\.]/.test(ch)) { - if (ch == ".") { - stream.match(/^[0-9]+([eE][\-+]?[0-9]+)?/); - } else if (ch == "0") { - stream.match(/^[xX][0-9a-fA-F]+/) || stream.match(/^0[0-7]+/); - } else { - stream.match(/^[0-9]*\.?[0-9]*([eE][\-+]?[0-9]+)?/); - } - return "number"; - } - if (/[\[\]{}\(\),;\:\.]/.test(ch)) { - curPunc = ch; - return null; - } - if (ch == "/") { - if (stream.eat("*")) { - state.tokenize = tokenComment; - return tokenComment(stream, state); - } - if (stream.eat("/")) { - stream.skipToEnd(); - return "comment"; - } - } - if (isOperatorChar.test(ch)) { - stream.eatWhile(isOperatorChar); - return "operator"; - } - stream.eatWhile(/[\w\$_\xa1-\uffff]/); - var cur = stream.current(); - if (keywords.propertyIsEnumerable(cur)) { - if (cur == "case" || cur == "default") curPunc = "case"; - return "keyword"; - } - if (atoms.propertyIsEnumerable(cur)) return "atom"; - return "variable"; - } - - function tokenString(quote) { - return function(stream, state) { - var escaped = false, next, end = false; - while ((next = stream.next()) != null) { - if (next == quote && !escaped) {end = true; break;} - escaped = !escaped && quote != "`" && next == "\\"; - } - if (end || !(escaped || quote == "`")) - state.tokenize = tokenBase; - return "string"; - }; - } - - function tokenComment(stream, state) { - var maybeEnd = false, ch; - while (ch = stream.next()) { - if (ch == "/" && maybeEnd) { - state.tokenize = tokenBase; - break; - } - maybeEnd = (ch == "*"); - } - return "comment"; - } - - function Context(indented, column, type, align, prev) { - this.indented = indented; - this.column = column; - this.type = type; - this.align = align; - this.prev = prev; - } - function pushContext(state, col, type) { - return state.context = new Context(state.indented, col, type, null, state.context); - } - function popContext(state) { - if (!state.context.prev) return; - var t = state.context.type; - if (t == ")" || t == "]" || t == "}") - state.indented = state.context.indented; - return state.context = state.context.prev; - } - - // Interface - - return { - startState: function(basecolumn) { - return { - tokenize: null, - context: new Context((basecolumn || 0) - indentUnit, 0, "top", false), - indented: 0, - startOfLine: true - }; - }, - - token: function(stream, state) { - var ctx = state.context; - if (stream.sol()) { - if (ctx.align == null) ctx.align = false; - state.indented = stream.indentation(); - state.startOfLine = true; - if (ctx.type == "case") ctx.type = "}"; - } - if (stream.eatSpace()) return null; - curPunc = null; - var style = (state.tokenize || tokenBase)(stream, state); - if (style == "comment") return style; - if (ctx.align == null) ctx.align = true; - - if (curPunc == "{") pushContext(state, stream.column(), "}"); - else if (curPunc == "[") pushContext(state, stream.column(), "]"); - else if (curPunc == "(") pushContext(state, stream.column(), ")"); - else if (curPunc == "case") ctx.type = "case"; - else if (curPunc == "}" && ctx.type == "}") popContext(state); - else if (curPunc == ctx.type) popContext(state); - state.startOfLine = false; - return style; - }, - - indent: function(state, textAfter) { - if (state.tokenize != tokenBase && state.tokenize != null) return CodeMirror.Pass; - var ctx = state.context, firstChar = textAfter && textAfter.charAt(0); - if (ctx.type == "case" && /^(?:case|default)\b/.test(textAfter)) { - state.context.type = "}"; - return ctx.indented; - } - var closing = firstChar == ctx.type; - if (ctx.align) return ctx.column + (closing ? 0 : 1); - else return ctx.indented + (closing ? 0 : indentUnit); - }, - - electricChars: "{}):", - closeBrackets: "()[]{}''\"\"``", - fold: "brace", - blockCommentStart: "/*", - blockCommentEnd: "*/", - lineComment: "//" - }; -}); - -CodeMirror.defineMIME("text/x-go", "go"); - -}); diff --git a/plugins/UiFileManager/media/codemirror/mode/htmlembedded.js b/plugins/UiFileManager/media/codemirror/mode/htmlembedded.js deleted file mode 100644 index 439e63a4..00000000 --- a/plugins/UiFileManager/media/codemirror/mode/htmlembedded.js +++ /dev/null @@ -1,37 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("../htmlmixed/htmlmixed"), - require("../../addon/mode/multiplex")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "../htmlmixed/htmlmixed", - "../../addon/mode/multiplex"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - CodeMirror.defineMode("htmlembedded", function(config, parserConfig) { - var closeComment = parserConfig.closeComment || "--%>" - return CodeMirror.multiplexingMode(CodeMirror.getMode(config, "htmlmixed"), { - open: parserConfig.openComment || "<%--", - close: closeComment, - delimStyle: "comment", - mode: {token: function(stream) { - stream.skipTo(closeComment) || stream.skipToEnd() - return "comment" - }} - }, { - open: parserConfig.open || parserConfig.scriptStartRegex || "<%", - close: parserConfig.close || parserConfig.scriptEndRegex || "%>", - mode: CodeMirror.getMode(config, parserConfig.scriptingModeSpec) - }); - }, "htmlmixed"); - - CodeMirror.defineMIME("application/x-ejs", {name: "htmlembedded", scriptingModeSpec:"javascript"}); - CodeMirror.defineMIME("application/x-aspx", {name: "htmlembedded", scriptingModeSpec:"text/x-csharp"}); - CodeMirror.defineMIME("application/x-jsp", {name: "htmlembedded", scriptingModeSpec:"text/x-java"}); - CodeMirror.defineMIME("application/x-erb", {name: "htmlembedded", scriptingModeSpec:"ruby"}); -}); diff --git a/plugins/UiFileManager/media/codemirror/mode/htmlmixed.js b/plugins/UiFileManager/media/codemirror/mode/htmlmixed.js deleted file mode 100644 index 8341ac82..00000000 --- a/plugins/UiFileManager/media/codemirror/mode/htmlmixed.js +++ /dev/null @@ -1,152 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("../xml/xml"), require("../javascript/javascript"), require("../css/css")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "../xml/xml", "../javascript/javascript", "../css/css"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - var defaultTags = { - script: [ - ["lang", /(javascript|babel)/i, "javascript"], - ["type", /^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^module$|^$/i, "javascript"], - ["type", /./, "text/plain"], - [null, null, "javascript"] - ], - style: [ - ["lang", /^css$/i, "css"], - ["type", /^(text\/)?(x-)?(stylesheet|css)$/i, "css"], - ["type", /./, "text/plain"], - [null, null, "css"] - ] - }; - - function maybeBackup(stream, pat, style) { - var cur = stream.current(), close = cur.search(pat); - if (close > -1) { - stream.backUp(cur.length - close); - } else if (cur.match(/<\/?$/)) { - stream.backUp(cur.length); - if (!stream.match(pat, false)) stream.match(cur); - } - return style; - } - - var attrRegexpCache = {}; - function getAttrRegexp(attr) { - var regexp = attrRegexpCache[attr]; - if (regexp) return regexp; - return attrRegexpCache[attr] = new RegExp("\\s+" + attr + "\\s*=\\s*('|\")?([^'\"]+)('|\")?\\s*"); - } - - function getAttrValue(text, attr) { - var match = text.match(getAttrRegexp(attr)) - return match ? /^\s*(.*?)\s*$/.exec(match[2])[1] : "" - } - - function getTagRegexp(tagName, anchored) { - return new RegExp((anchored ? "^" : "") + "<\/\s*" + tagName + "\s*>", "i"); - } - - function addTags(from, to) { - for (var tag in from) { - var dest = to[tag] || (to[tag] = []); - var source = from[tag]; - for (var i = source.length - 1; i >= 0; i--) - dest.unshift(source[i]) - } - } - - function findMatchingMode(tagInfo, tagText) { - for (var i = 0; i < tagInfo.length; i++) { - var spec = tagInfo[i]; - if (!spec[0] || spec[1].test(getAttrValue(tagText, spec[0]))) return spec[2]; - } - } - - CodeMirror.defineMode("htmlmixed", function (config, parserConfig) { - var htmlMode = CodeMirror.getMode(config, { - name: "xml", - htmlMode: true, - multilineTagIndentFactor: parserConfig.multilineTagIndentFactor, - multilineTagIndentPastTag: parserConfig.multilineTagIndentPastTag - }); - - var tags = {}; - var configTags = parserConfig && parserConfig.tags, configScript = parserConfig && parserConfig.scriptTypes; - addTags(defaultTags, tags); - if (configTags) addTags(configTags, tags); - if (configScript) for (var i = configScript.length - 1; i >= 0; i--) - tags.script.unshift(["type", configScript[i].matches, configScript[i].mode]) - - function html(stream, state) { - var style = htmlMode.token(stream, state.htmlState), tag = /\btag\b/.test(style), tagName - if (tag && !/[<>\s\/]/.test(stream.current()) && - (tagName = state.htmlState.tagName && state.htmlState.tagName.toLowerCase()) && - tags.hasOwnProperty(tagName)) { - state.inTag = tagName + " " - } else if (state.inTag && tag && />$/.test(stream.current())) { - var inTag = /^([\S]+) (.*)/.exec(state.inTag) - state.inTag = null - var modeSpec = stream.current() == ">" && findMatchingMode(tags[inTag[1]], inTag[2]) - var mode = CodeMirror.getMode(config, modeSpec) - var endTagA = getTagRegexp(inTag[1], true), endTag = getTagRegexp(inTag[1], false); - state.token = function (stream, state) { - if (stream.match(endTagA, false)) { - state.token = html; - state.localState = state.localMode = null; - return null; - } - return maybeBackup(stream, endTag, state.localMode.token(stream, state.localState)); - }; - state.localMode = mode; - state.localState = CodeMirror.startState(mode, htmlMode.indent(state.htmlState, "", "")); - } else if (state.inTag) { - state.inTag += stream.current() - if (stream.eol()) state.inTag += " " - } - return style; - }; - - return { - startState: function () { - var state = CodeMirror.startState(htmlMode); - return {token: html, inTag: null, localMode: null, localState: null, htmlState: state}; - }, - - copyState: function (state) { - var local; - if (state.localState) { - local = CodeMirror.copyState(state.localMode, state.localState); - } - return {token: state.token, inTag: state.inTag, - localMode: state.localMode, localState: local, - htmlState: CodeMirror.copyState(htmlMode, state.htmlState)}; - }, - - token: function (stream, state) { - return state.token(stream, state); - }, - - indent: function (state, textAfter, line) { - if (!state.localMode || /^\s*<\//.test(textAfter)) - return htmlMode.indent(state.htmlState, textAfter, line); - else if (state.localMode.indent) - return state.localMode.indent(state.localState, textAfter, line); - else - return CodeMirror.Pass; - }, - - innerMode: function (state) { - return {state: state.localState || state.htmlState, mode: state.localMode || htmlMode}; - } - }; - }, "xml", "javascript", "css"); - - CodeMirror.defineMIME("text/html", "htmlmixed"); -}); diff --git a/plugins/UiFileManager/media/codemirror/mode/javascript.js b/plugins/UiFileManager/media/codemirror/mode/javascript.js deleted file mode 100644 index 9c751d23..00000000 --- a/plugins/UiFileManager/media/codemirror/mode/javascript.js +++ /dev/null @@ -1,934 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -CodeMirror.defineMode("javascript", function(config, parserConfig) { - var indentUnit = config.indentUnit; - var statementIndent = parserConfig.statementIndent; - var jsonldMode = parserConfig.jsonld; - var jsonMode = parserConfig.json || jsonldMode; - var isTS = parserConfig.typescript; - var wordRE = parserConfig.wordCharacters || /[\w$\xa1-\uffff]/; - - // Tokenizer - - var keywords = function(){ - function kw(type) {return {type: type, style: "keyword"};} - var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"), D = kw("keyword d"); - var operator = kw("operator"), atom = {type: "atom", style: "atom"}; - - return { - "if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B, - "return": D, "break": D, "continue": D, "new": kw("new"), "delete": C, "void": C, "throw": C, - "debugger": kw("debugger"), "var": kw("var"), "const": kw("var"), "let": kw("var"), - "function": kw("function"), "catch": kw("catch"), - "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"), - "in": operator, "typeof": operator, "instanceof": operator, - "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom, - "this": kw("this"), "class": kw("class"), "super": kw("atom"), - "yield": C, "export": kw("export"), "import": kw("import"), "extends": C, - "await": C - }; - }(); - - var isOperatorChar = /[+\-*&%=<>!?|~^@]/; - var isJsonldKeyword = /^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/; - - function readRegexp(stream) { - var escaped = false, next, inSet = false; - while ((next = stream.next()) != null) { - if (!escaped) { - if (next == "/" && !inSet) return; - if (next == "[") inSet = true; - else if (inSet && next == "]") inSet = false; - } - escaped = !escaped && next == "\\"; - } - } - - // Used as scratch variables to communicate multiple values without - // consing up tons of objects. - var type, content; - function ret(tp, style, cont) { - type = tp; content = cont; - return style; - } - function tokenBase(stream, state) { - var ch = stream.next(); - if (ch == '"' || ch == "'") { - state.tokenize = tokenString(ch); - return state.tokenize(stream, state); - } else if (ch == "." && stream.match(/^\d[\d_]*(?:[eE][+\-]?[\d_]+)?/)) { - return ret("number", "number"); - } else if (ch == "." && stream.match("..")) { - return ret("spread", "meta"); - } else if (/[\[\]{}\(\),;\:\.]/.test(ch)) { - return ret(ch); - } else if (ch == "=" && stream.eat(">")) { - return ret("=>", "operator"); - } else if (ch == "0" && stream.match(/^(?:x[\dA-Fa-f_]+|o[0-7_]+|b[01_]+)n?/)) { - return ret("number", "number"); - } else if (/\d/.test(ch)) { - stream.match(/^[\d_]*(?:n|(?:\.[\d_]*)?(?:[eE][+\-]?[\d_]+)?)?/); - return ret("number", "number"); - } else if (ch == "/") { - if (stream.eat("*")) { - state.tokenize = tokenComment; - return tokenComment(stream, state); - } else if (stream.eat("/")) { - stream.skipToEnd(); - return ret("comment", "comment"); - } else if (expressionAllowed(stream, state, 1)) { - readRegexp(stream); - stream.match(/^\b(([gimyus])(?![gimyus]*\2))+\b/); - return ret("regexp", "string-2"); - } else { - stream.eat("="); - return ret("operator", "operator", stream.current()); - } - } else if (ch == "`") { - state.tokenize = tokenQuasi; - return tokenQuasi(stream, state); - } else if (ch == "#" && stream.peek() == "!") { - stream.skipToEnd(); - return ret("meta", "meta"); - } else if (ch == "#" && stream.eatWhile(wordRE)) { - return ret("variable", "property") - } else if (ch == "<" && stream.match("!--") || - (ch == "-" && stream.match("->") && !/\S/.test(stream.string.slice(0, stream.start)))) { - stream.skipToEnd() - return ret("comment", "comment") - } else if (isOperatorChar.test(ch)) { - if (ch != ">" || !state.lexical || state.lexical.type != ">") { - if (stream.eat("=")) { - if (ch == "!" || ch == "=") stream.eat("=") - } else if (/[<>*+\-]/.test(ch)) { - stream.eat(ch) - if (ch == ">") stream.eat(ch) - } - } - if (ch == "?" && stream.eat(".")) return ret(".") - return ret("operator", "operator", stream.current()); - } else if (wordRE.test(ch)) { - stream.eatWhile(wordRE); - var word = stream.current() - if (state.lastType != ".") { - if (keywords.propertyIsEnumerable(word)) { - var kw = keywords[word] - return ret(kw.type, kw.style, word) - } - if (word == "async" && stream.match(/^(\s|\/\*.*?\*\/)*[\[\(\w]/, false)) - return ret("async", "keyword", word) - } - return ret("variable", "variable", word) - } - } - - function tokenString(quote) { - return function(stream, state) { - var escaped = false, next; - if (jsonldMode && stream.peek() == "@" && stream.match(isJsonldKeyword)){ - state.tokenize = tokenBase; - return ret("jsonld-keyword", "meta"); - } - while ((next = stream.next()) != null) { - if (next == quote && !escaped) break; - escaped = !escaped && next == "\\"; - } - if (!escaped) state.tokenize = tokenBase; - return ret("string", "string"); - }; - } - - function tokenComment(stream, state) { - var maybeEnd = false, ch; - while (ch = stream.next()) { - if (ch == "/" && maybeEnd) { - state.tokenize = tokenBase; - break; - } - maybeEnd = (ch == "*"); - } - return ret("comment", "comment"); - } - - function tokenQuasi(stream, state) { - var escaped = false, next; - while ((next = stream.next()) != null) { - if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) { - state.tokenize = tokenBase; - break; - } - escaped = !escaped && next == "\\"; - } - return ret("quasi", "string-2", stream.current()); - } - - var brackets = "([{}])"; - // This is a crude lookahead trick to try and notice that we're - // parsing the argument patterns for a fat-arrow function before we - // actually hit the arrow token. It only works if the arrow is on - // the same line as the arguments and there's no strange noise - // (comments) in between. Fallback is to only notice when we hit the - // arrow, and not declare the arguments as locals for the arrow - // body. - function findFatArrow(stream, state) { - if (state.fatArrowAt) state.fatArrowAt = null; - var arrow = stream.string.indexOf("=>", stream.start); - if (arrow < 0) return; - - if (isTS) { // Try to skip TypeScript return type declarations after the arguments - var m = /:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(stream.string.slice(stream.start, arrow)) - if (m) arrow = m.index - } - - var depth = 0, sawSomething = false; - for (var pos = arrow - 1; pos >= 0; --pos) { - var ch = stream.string.charAt(pos); - var bracket = brackets.indexOf(ch); - if (bracket >= 0 && bracket < 3) { - if (!depth) { ++pos; break; } - if (--depth == 0) { if (ch == "(") sawSomething = true; break; } - } else if (bracket >= 3 && bracket < 6) { - ++depth; - } else if (wordRE.test(ch)) { - sawSomething = true; - } else if (/["'\/`]/.test(ch)) { - for (;; --pos) { - if (pos == 0) return - var next = stream.string.charAt(pos - 1) - if (next == ch && stream.string.charAt(pos - 2) != "\\") { pos--; break } - } - } else if (sawSomething && !depth) { - ++pos; - break; - } - } - if (sawSomething && !depth) state.fatArrowAt = pos; - } - - // Parser - - var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true, "this": true, "jsonld-keyword": true}; - - function JSLexical(indented, column, type, align, prev, info) { - this.indented = indented; - this.column = column; - this.type = type; - this.prev = prev; - this.info = info; - if (align != null) this.align = align; - } - - function inScope(state, varname) { - for (var v = state.localVars; v; v = v.next) - if (v.name == varname) return true; - for (var cx = state.context; cx; cx = cx.prev) { - for (var v = cx.vars; v; v = v.next) - if (v.name == varname) return true; - } - } - - function parseJS(state, style, type, content, stream) { - var cc = state.cc; - // Communicate our context to the combinators. - // (Less wasteful than consing up a hundred closures on every call.) - cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; cx.style = style; - - if (!state.lexical.hasOwnProperty("align")) - state.lexical.align = true; - - while(true) { - var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement; - if (combinator(type, content)) { - while(cc.length && cc[cc.length - 1].lex) - cc.pop()(); - if (cx.marked) return cx.marked; - if (type == "variable" && inScope(state, content)) return "variable-2"; - return style; - } - } - } - - // Combinator utils - - var cx = {state: null, column: null, marked: null, cc: null}; - function pass() { - for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]); - } - function cont() { - pass.apply(null, arguments); - return true; - } - function inList(name, list) { - for (var v = list; v; v = v.next) if (v.name == name) return true - return false; - } - function register(varname) { - var state = cx.state; - cx.marked = "def"; - if (state.context) { - if (state.lexical.info == "var" && state.context && state.context.block) { - // FIXME function decls are also not block scoped - var newContext = registerVarScoped(varname, state.context) - if (newContext != null) { - state.context = newContext - return - } - } else if (!inList(varname, state.localVars)) { - state.localVars = new Var(varname, state.localVars) - return - } - } - // Fall through means this is global - if (parserConfig.globalVars && !inList(varname, state.globalVars)) - state.globalVars = new Var(varname, state.globalVars) - } - function registerVarScoped(varname, context) { - if (!context) { - return null - } else if (context.block) { - var inner = registerVarScoped(varname, context.prev) - if (!inner) return null - if (inner == context.prev) return context - return new Context(inner, context.vars, true) - } else if (inList(varname, context.vars)) { - return context - } else { - return new Context(context.prev, new Var(varname, context.vars), false) - } - } - - function isModifier(name) { - return name == "public" || name == "private" || name == "protected" || name == "abstract" || name == "readonly" - } - - // Combinators - - function Context(prev, vars, block) { this.prev = prev; this.vars = vars; this.block = block } - function Var(name, next) { this.name = name; this.next = next } - - var defaultVars = new Var("this", new Var("arguments", null)) - function pushcontext() { - cx.state.context = new Context(cx.state.context, cx.state.localVars, false) - cx.state.localVars = defaultVars - } - function pushblockcontext() { - cx.state.context = new Context(cx.state.context, cx.state.localVars, true) - cx.state.localVars = null - } - function popcontext() { - cx.state.localVars = cx.state.context.vars - cx.state.context = cx.state.context.prev - } - popcontext.lex = true - function pushlex(type, info) { - var result = function() { - var state = cx.state, indent = state.indented; - if (state.lexical.type == "stat") indent = state.lexical.indented; - else for (var outer = state.lexical; outer && outer.type == ")" && outer.align; outer = outer.prev) - indent = outer.indented; - state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info); - }; - result.lex = true; - return result; - } - function poplex() { - var state = cx.state; - if (state.lexical.prev) { - if (state.lexical.type == ")") - state.indented = state.lexical.indented; - state.lexical = state.lexical.prev; - } - } - poplex.lex = true; - - function expect(wanted) { - function exp(type) { - if (type == wanted) return cont(); - else if (wanted == ";" || type == "}" || type == ")" || type == "]") return pass(); - else return cont(exp); - }; - return exp; - } - - function statement(type, value) { - if (type == "var") return cont(pushlex("vardef", value), vardef, expect(";"), poplex); - if (type == "keyword a") return cont(pushlex("form"), parenExpr, statement, poplex); - if (type == "keyword b") return cont(pushlex("form"), statement, poplex); - if (type == "keyword d") return cx.stream.match(/^\s*$/, false) ? cont() : cont(pushlex("stat"), maybeexpression, expect(";"), poplex); - if (type == "debugger") return cont(expect(";")); - if (type == "{") return cont(pushlex("}"), pushblockcontext, block, poplex, popcontext); - if (type == ";") return cont(); - if (type == "if") { - if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex) - cx.state.cc.pop()(); - return cont(pushlex("form"), parenExpr, statement, poplex, maybeelse); - } - if (type == "function") return cont(functiondef); - if (type == "for") return cont(pushlex("form"), forspec, statement, poplex); - if (type == "class" || (isTS && value == "interface")) { - cx.marked = "keyword" - return cont(pushlex("form", type == "class" ? type : value), className, poplex) - } - if (type == "variable") { - if (isTS && value == "declare") { - cx.marked = "keyword" - return cont(statement) - } else if (isTS && (value == "module" || value == "enum" || value == "type") && cx.stream.match(/^\s*\w/, false)) { - cx.marked = "keyword" - if (value == "enum") return cont(enumdef); - else if (value == "type") return cont(typename, expect("operator"), typeexpr, expect(";")); - else return cont(pushlex("form"), pattern, expect("{"), pushlex("}"), block, poplex, poplex) - } else if (isTS && value == "namespace") { - cx.marked = "keyword" - return cont(pushlex("form"), expression, statement, poplex) - } else if (isTS && value == "abstract") { - cx.marked = "keyword" - return cont(statement) - } else { - return cont(pushlex("stat"), maybelabel); - } - } - if (type == "switch") return cont(pushlex("form"), parenExpr, expect("{"), pushlex("}", "switch"), pushblockcontext, - block, poplex, poplex, popcontext); - if (type == "case") return cont(expression, expect(":")); - if (type == "default") return cont(expect(":")); - if (type == "catch") return cont(pushlex("form"), pushcontext, maybeCatchBinding, statement, poplex, popcontext); - if (type == "export") return cont(pushlex("stat"), afterExport, poplex); - if (type == "import") return cont(pushlex("stat"), afterImport, poplex); - if (type == "async") return cont(statement) - if (value == "@") return cont(expression, statement) - return pass(pushlex("stat"), expression, expect(";"), poplex); - } - function maybeCatchBinding(type) { - if (type == "(") return cont(funarg, expect(")")) - } - function expression(type, value) { - return expressionInner(type, value, false); - } - function expressionNoComma(type, value) { - return expressionInner(type, value, true); - } - function parenExpr(type) { - if (type != "(") return pass() - return cont(pushlex(")"), maybeexpression, expect(")"), poplex) - } - function expressionInner(type, value, noComma) { - if (cx.state.fatArrowAt == cx.stream.start) { - var body = noComma ? arrowBodyNoComma : arrowBody; - if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, expect("=>"), body, popcontext); - else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext); - } - - var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma; - if (atomicTypes.hasOwnProperty(type)) return cont(maybeop); - if (type == "function") return cont(functiondef, maybeop); - if (type == "class" || (isTS && value == "interface")) { cx.marked = "keyword"; return cont(pushlex("form"), classExpression, poplex); } - if (type == "keyword c" || type == "async") return cont(noComma ? expressionNoComma : expression); - if (type == "(") return cont(pushlex(")"), maybeexpression, expect(")"), poplex, maybeop); - if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression); - if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop); - if (type == "{") return contCommasep(objprop, "}", null, maybeop); - if (type == "quasi") return pass(quasi, maybeop); - if (type == "new") return cont(maybeTarget(noComma)); - if (type == "import") return cont(expression); - return cont(); - } - function maybeexpression(type) { - if (type.match(/[;\}\)\],]/)) return pass(); - return pass(expression); - } - - function maybeoperatorComma(type, value) { - if (type == ",") return cont(maybeexpression); - return maybeoperatorNoComma(type, value, false); - } - function maybeoperatorNoComma(type, value, noComma) { - var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma; - var expr = noComma == false ? expression : expressionNoComma; - if (type == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext); - if (type == "operator") { - if (/\+\+|--/.test(value) || isTS && value == "!") return cont(me); - if (isTS && value == "<" && cx.stream.match(/^([^<>]|<[^<>]*>)*>\s*\(/, false)) - return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, me); - if (value == "?") return cont(expression, expect(":"), expr); - return cont(expr); - } - if (type == "quasi") { return pass(quasi, me); } - if (type == ";") return; - if (type == "(") return contCommasep(expressionNoComma, ")", "call", me); - if (type == ".") return cont(property, me); - if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me); - if (isTS && value == "as") { cx.marked = "keyword"; return cont(typeexpr, me) } - if (type == "regexp") { - cx.state.lastType = cx.marked = "operator" - cx.stream.backUp(cx.stream.pos - cx.stream.start - 1) - return cont(expr) - } - } - function quasi(type, value) { - if (type != "quasi") return pass(); - if (value.slice(value.length - 2) != "${") return cont(quasi); - return cont(expression, continueQuasi); - } - function continueQuasi(type) { - if (type == "}") { - cx.marked = "string-2"; - cx.state.tokenize = tokenQuasi; - return cont(quasi); - } - } - function arrowBody(type) { - findFatArrow(cx.stream, cx.state); - return pass(type == "{" ? statement : expression); - } - function arrowBodyNoComma(type) { - findFatArrow(cx.stream, cx.state); - return pass(type == "{" ? statement : expressionNoComma); - } - function maybeTarget(noComma) { - return function(type) { - if (type == ".") return cont(noComma ? targetNoComma : target); - else if (type == "variable" && isTS) return cont(maybeTypeArgs, noComma ? maybeoperatorNoComma : maybeoperatorComma) - else return pass(noComma ? expressionNoComma : expression); - }; - } - function target(_, value) { - if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorComma); } - } - function targetNoComma(_, value) { - if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorNoComma); } - } - function maybelabel(type) { - if (type == ":") return cont(poplex, statement); - return pass(maybeoperatorComma, expect(";"), poplex); - } - function property(type) { - if (type == "variable") {cx.marked = "property"; return cont();} - } - function objprop(type, value) { - if (type == "async") { - cx.marked = "property"; - return cont(objprop); - } else if (type == "variable" || cx.style == "keyword") { - cx.marked = "property"; - if (value == "get" || value == "set") return cont(getterSetter); - var m // Work around fat-arrow-detection complication for detecting typescript typed arrow params - if (isTS && cx.state.fatArrowAt == cx.stream.start && (m = cx.stream.match(/^\s*:\s*/, false))) - cx.state.fatArrowAt = cx.stream.pos + m[0].length - return cont(afterprop); - } else if (type == "number" || type == "string") { - cx.marked = jsonldMode ? "property" : (cx.style + " property"); - return cont(afterprop); - } else if (type == "jsonld-keyword") { - return cont(afterprop); - } else if (isTS && isModifier(value)) { - cx.marked = "keyword" - return cont(objprop) - } else if (type == "[") { - return cont(expression, maybetype, expect("]"), afterprop); - } else if (type == "spread") { - return cont(expressionNoComma, afterprop); - } else if (value == "*") { - cx.marked = "keyword"; - return cont(objprop); - } else if (type == ":") { - return pass(afterprop) - } - } - function getterSetter(type) { - if (type != "variable") return pass(afterprop); - cx.marked = "property"; - return cont(functiondef); - } - function afterprop(type) { - if (type == ":") return cont(expressionNoComma); - if (type == "(") return pass(functiondef); - } - function commasep(what, end, sep) { - function proceed(type, value) { - if (sep ? sep.indexOf(type) > -1 : type == ",") { - var lex = cx.state.lexical; - if (lex.info == "call") lex.pos = (lex.pos || 0) + 1; - return cont(function(type, value) { - if (type == end || value == end) return pass() - return pass(what) - }, proceed); - } - if (type == end || value == end) return cont(); - if (sep && sep.indexOf(";") > -1) return pass(what) - return cont(expect(end)); - } - return function(type, value) { - if (type == end || value == end) return cont(); - return pass(what, proceed); - }; - } - function contCommasep(what, end, info) { - for (var i = 3; i < arguments.length; i++) - cx.cc.push(arguments[i]); - return cont(pushlex(end, info), commasep(what, end), poplex); - } - function block(type) { - if (type == "}") return cont(); - return pass(statement, block); - } - function maybetype(type, value) { - if (isTS) { - if (type == ":") return cont(typeexpr); - if (value == "?") return cont(maybetype); - } - } - function maybetypeOrIn(type, value) { - if (isTS && (type == ":" || value == "in")) return cont(typeexpr) - } - function mayberettype(type) { - if (isTS && type == ":") { - if (cx.stream.match(/^\s*\w+\s+is\b/, false)) return cont(expression, isKW, typeexpr) - else return cont(typeexpr) - } - } - function isKW(_, value) { - if (value == "is") { - cx.marked = "keyword" - return cont() - } - } - function typeexpr(type, value) { - if (value == "keyof" || value == "typeof" || value == "infer") { - cx.marked = "keyword" - return cont(value == "typeof" ? expressionNoComma : typeexpr) - } - if (type == "variable" || value == "void") { - cx.marked = "type" - return cont(afterType) - } - if (value == "|" || value == "&") return cont(typeexpr) - if (type == "string" || type == "number" || type == "atom") return cont(afterType); - if (type == "[") return cont(pushlex("]"), commasep(typeexpr, "]", ","), poplex, afterType) - if (type == "{") return cont(pushlex("}"), commasep(typeprop, "}", ",;"), poplex, afterType) - if (type == "(") return cont(commasep(typearg, ")"), maybeReturnType, afterType) - if (type == "<") return cont(commasep(typeexpr, ">"), typeexpr) - } - function maybeReturnType(type) { - if (type == "=>") return cont(typeexpr) - } - function typeprop(type, value) { - if (type == "variable" || cx.style == "keyword") { - cx.marked = "property" - return cont(typeprop) - } else if (value == "?" || type == "number" || type == "string") { - return cont(typeprop) - } else if (type == ":") { - return cont(typeexpr) - } else if (type == "[") { - return cont(expect("variable"), maybetypeOrIn, expect("]"), typeprop) - } else if (type == "(") { - return pass(functiondecl, typeprop) - } - } - function typearg(type, value) { - if (type == "variable" && cx.stream.match(/^\s*[?:]/, false) || value == "?") return cont(typearg) - if (type == ":") return cont(typeexpr) - if (type == "spread") return cont(typearg) - return pass(typeexpr) - } - function afterType(type, value) { - if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType) - if (value == "|" || type == "." || value == "&") return cont(typeexpr) - if (type == "[") return cont(typeexpr, expect("]"), afterType) - if (value == "extends" || value == "implements") { cx.marked = "keyword"; return cont(typeexpr) } - if (value == "?") return cont(typeexpr, expect(":"), typeexpr) - } - function maybeTypeArgs(_, value) { - if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType) - } - function typeparam() { - return pass(typeexpr, maybeTypeDefault) - } - function maybeTypeDefault(_, value) { - if (value == "=") return cont(typeexpr) - } - function vardef(_, value) { - if (value == "enum") {cx.marked = "keyword"; return cont(enumdef)} - return pass(pattern, maybetype, maybeAssign, vardefCont); - } - function pattern(type, value) { - if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(pattern) } - if (type == "variable") { register(value); return cont(); } - if (type == "spread") return cont(pattern); - if (type == "[") return contCommasep(eltpattern, "]"); - if (type == "{") return contCommasep(proppattern, "}"); - } - function proppattern(type, value) { - if (type == "variable" && !cx.stream.match(/^\s*:/, false)) { - register(value); - return cont(maybeAssign); - } - if (type == "variable") cx.marked = "property"; - if (type == "spread") return cont(pattern); - if (type == "}") return pass(); - if (type == "[") return cont(expression, expect(']'), expect(':'), proppattern); - return cont(expect(":"), pattern, maybeAssign); - } - function eltpattern() { - return pass(pattern, maybeAssign) - } - function maybeAssign(_type, value) { - if (value == "=") return cont(expressionNoComma); - } - function vardefCont(type) { - if (type == ",") return cont(vardef); - } - function maybeelse(type, value) { - if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex); - } - function forspec(type, value) { - if (value == "await") return cont(forspec); - if (type == "(") return cont(pushlex(")"), forspec1, poplex); - } - function forspec1(type) { - if (type == "var") return cont(vardef, forspec2); - if (type == "variable") return cont(forspec2); - return pass(forspec2) - } - function forspec2(type, value) { - if (type == ")") return cont() - if (type == ";") return cont(forspec2) - if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression, forspec2) } - return pass(expression, forspec2) - } - function functiondef(type, value) { - if (value == "*") {cx.marked = "keyword"; return cont(functiondef);} - if (type == "variable") {register(value); return cont(functiondef);} - if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, statement, popcontext); - if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondef) - } - function functiondecl(type, value) { - if (value == "*") {cx.marked = "keyword"; return cont(functiondecl);} - if (type == "variable") {register(value); return cont(functiondecl);} - if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, popcontext); - if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondecl) - } - function typename(type, value) { - if (type == "keyword" || type == "variable") { - cx.marked = "type" - return cont(typename) - } else if (value == "<") { - return cont(pushlex(">"), commasep(typeparam, ">"), poplex) - } - } - function funarg(type, value) { - if (value == "@") cont(expression, funarg) - if (type == "spread") return cont(funarg); - if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(funarg); } - if (isTS && type == "this") return cont(maybetype, maybeAssign) - return pass(pattern, maybetype, maybeAssign); - } - function classExpression(type, value) { - // Class expressions may have an optional name. - if (type == "variable") return className(type, value); - return classNameAfter(type, value); - } - function className(type, value) { - if (type == "variable") {register(value); return cont(classNameAfter);} - } - function classNameAfter(type, value) { - if (value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, classNameAfter) - if (value == "extends" || value == "implements" || (isTS && type == ",")) { - if (value == "implements") cx.marked = "keyword"; - return cont(isTS ? typeexpr : expression, classNameAfter); - } - if (type == "{") return cont(pushlex("}"), classBody, poplex); - } - function classBody(type, value) { - if (type == "async" || - (type == "variable" && - (value == "static" || value == "get" || value == "set" || (isTS && isModifier(value))) && - cx.stream.match(/^\s+[\w$\xa1-\uffff]/, false))) { - cx.marked = "keyword"; - return cont(classBody); - } - if (type == "variable" || cx.style == "keyword") { - cx.marked = "property"; - return cont(classfield, classBody); - } - if (type == "number" || type == "string") return cont(classfield, classBody); - if (type == "[") - return cont(expression, maybetype, expect("]"), classfield, classBody) - if (value == "*") { - cx.marked = "keyword"; - return cont(classBody); - } - if (isTS && type == "(") return pass(functiondecl, classBody) - if (type == ";" || type == ",") return cont(classBody); - if (type == "}") return cont(); - if (value == "@") return cont(expression, classBody) - } - function classfield(type, value) { - if (value == "?") return cont(classfield) - if (type == ":") return cont(typeexpr, maybeAssign) - if (value == "=") return cont(expressionNoComma) - var context = cx.state.lexical.prev, isInterface = context && context.info == "interface" - return pass(isInterface ? functiondecl : functiondef) - } - function afterExport(type, value) { - if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); } - if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); } - if (type == "{") return cont(commasep(exportField, "}"), maybeFrom, expect(";")); - return pass(statement); - } - function exportField(type, value) { - if (value == "as") { cx.marked = "keyword"; return cont(expect("variable")); } - if (type == "variable") return pass(expressionNoComma, exportField); - } - function afterImport(type) { - if (type == "string") return cont(); - if (type == "(") return pass(expression); - return pass(importSpec, maybeMoreImports, maybeFrom); - } - function importSpec(type, value) { - if (type == "{") return contCommasep(importSpec, "}"); - if (type == "variable") register(value); - if (value == "*") cx.marked = "keyword"; - return cont(maybeAs); - } - function maybeMoreImports(type) { - if (type == ",") return cont(importSpec, maybeMoreImports) - } - function maybeAs(_type, value) { - if (value == "as") { cx.marked = "keyword"; return cont(importSpec); } - } - function maybeFrom(_type, value) { - if (value == "from") { cx.marked = "keyword"; return cont(expression); } - } - function arrayLiteral(type) { - if (type == "]") return cont(); - return pass(commasep(expressionNoComma, "]")); - } - function enumdef() { - return pass(pushlex("form"), pattern, expect("{"), pushlex("}"), commasep(enummember, "}"), poplex, poplex) - } - function enummember() { - return pass(pattern, maybeAssign); - } - - function isContinuedStatement(state, textAfter) { - return state.lastType == "operator" || state.lastType == "," || - isOperatorChar.test(textAfter.charAt(0)) || - /[,.]/.test(textAfter.charAt(0)); - } - - function expressionAllowed(stream, state, backUp) { - return state.tokenize == tokenBase && - /^(?:operator|sof|keyword [bcd]|case|new|export|default|spread|[\[{}\(,;:]|=>)$/.test(state.lastType) || - (state.lastType == "quasi" && /\{\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0)))) - } - - // Interface - - return { - startState: function(basecolumn) { - var state = { - tokenize: tokenBase, - lastType: "sof", - cc: [], - lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false), - localVars: parserConfig.localVars, - context: parserConfig.localVars && new Context(null, null, false), - indented: basecolumn || 0 - }; - if (parserConfig.globalVars && typeof parserConfig.globalVars == "object") - state.globalVars = parserConfig.globalVars; - return state; - }, - - token: function(stream, state) { - if (stream.sol()) { - if (!state.lexical.hasOwnProperty("align")) - state.lexical.align = false; - state.indented = stream.indentation(); - findFatArrow(stream, state); - } - if (state.tokenize != tokenComment && stream.eatSpace()) return null; - var style = state.tokenize(stream, state); - if (type == "comment") return style; - state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type; - return parseJS(state, style, type, content, stream); - }, - - indent: function(state, textAfter) { - if (state.tokenize == tokenComment) return CodeMirror.Pass; - if (state.tokenize != tokenBase) return 0; - var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical, top - // Kludge to prevent 'maybelse' from blocking lexical scope pops - if (!/^\s*else\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) { - var c = state.cc[i]; - if (c == poplex) lexical = lexical.prev; - else if (c != maybeelse) break; - } - while ((lexical.type == "stat" || lexical.type == "form") && - (firstChar == "}" || ((top = state.cc[state.cc.length - 1]) && - (top == maybeoperatorComma || top == maybeoperatorNoComma) && - !/^[,\.=+\-*:?[\(]/.test(textAfter)))) - lexical = lexical.prev; - if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat") - lexical = lexical.prev; - var type = lexical.type, closing = firstChar == type; - - if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info.length + 1 : 0); - else if (type == "form" && firstChar == "{") return lexical.indented; - else if (type == "form") return lexical.indented + indentUnit; - else if (type == "stat") - return lexical.indented + (isContinuedStatement(state, textAfter) ? statementIndent || indentUnit : 0); - else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false) - return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit); - else if (lexical.align) return lexical.column + (closing ? 0 : 1); - else return lexical.indented + (closing ? 0 : indentUnit); - }, - - electricInput: /^\s*(?:case .*?:|default:|\{|\})$/, - blockCommentStart: jsonMode ? null : "/*", - blockCommentEnd: jsonMode ? null : "*/", - blockCommentContinue: jsonMode ? null : " * ", - lineComment: jsonMode ? null : "//", - fold: "brace", - closeBrackets: "()[]{}''\"\"``", - - helperType: jsonMode ? "json" : "javascript", - jsonldMode: jsonldMode, - jsonMode: jsonMode, - - expressionAllowed: expressionAllowed, - - skipExpression: function(state) { - var top = state.cc[state.cc.length - 1] - if (top == expression || top == expressionNoComma) state.cc.pop() - } - }; -}); - -CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/); - -CodeMirror.defineMIME("text/javascript", "javascript"); -CodeMirror.defineMIME("text/ecmascript", "javascript"); -CodeMirror.defineMIME("application/javascript", "javascript"); -CodeMirror.defineMIME("application/x-javascript", "javascript"); -CodeMirror.defineMIME("application/ecmascript", "javascript"); -CodeMirror.defineMIME("application/json", {name: "javascript", json: true}); -CodeMirror.defineMIME("application/x-json", {name: "javascript", json: true}); -CodeMirror.defineMIME("application/ld+json", {name: "javascript", jsonld: true}); -CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true }); -CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true }); - -}); diff --git a/plugins/UiFileManager/media/codemirror/mode/markdown.js b/plugins/UiFileManager/media/codemirror/mode/markdown.js deleted file mode 100644 index 287f39b5..00000000 --- a/plugins/UiFileManager/media/codemirror/mode/markdown.js +++ /dev/null @@ -1,886 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("../xml/xml"), require("../meta")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "../xml/xml", "../meta"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) { - - var htmlMode = CodeMirror.getMode(cmCfg, "text/html"); - var htmlModeMissing = htmlMode.name == "null" - - function getMode(name) { - if (CodeMirror.findModeByName) { - var found = CodeMirror.findModeByName(name); - if (found) name = found.mime || found.mimes[0]; - } - var mode = CodeMirror.getMode(cmCfg, name); - return mode.name == "null" ? null : mode; - } - - // Should characters that affect highlighting be highlighted separate? - // Does not include characters that will be output (such as `1.` and `-` for lists) - if (modeCfg.highlightFormatting === undefined) - modeCfg.highlightFormatting = false; - - // Maximum number of nested blockquotes. Set to 0 for infinite nesting. - // Excess `>` will emit `error` token. - if (modeCfg.maxBlockquoteDepth === undefined) - modeCfg.maxBlockquoteDepth = 0; - - // Turn on task lists? ("- [ ] " and "- [x] ") - if (modeCfg.taskLists === undefined) modeCfg.taskLists = false; - - // Turn on strikethrough syntax - if (modeCfg.strikethrough === undefined) - modeCfg.strikethrough = false; - - if (modeCfg.emoji === undefined) - modeCfg.emoji = false; - - if (modeCfg.fencedCodeBlockHighlighting === undefined) - modeCfg.fencedCodeBlockHighlighting = true; - - if (modeCfg.fencedCodeBlockDefaultMode === undefined) - modeCfg.fencedCodeBlockDefaultMode = 'text/plain'; - - if (modeCfg.xml === undefined) - modeCfg.xml = true; - - // Allow token types to be overridden by user-provided token types. - if (modeCfg.tokenTypeOverrides === undefined) - modeCfg.tokenTypeOverrides = {}; - - var tokenTypes = { - header: "header", - code: "comment", - quote: "quote", - list1: "variable-2", - list2: "variable-3", - list3: "keyword", - hr: "hr", - image: "image", - imageAltText: "image-alt-text", - imageMarker: "image-marker", - formatting: "formatting", - linkInline: "link", - linkEmail: "link", - linkText: "link", - linkHref: "string", - em: "em", - strong: "strong", - strikethrough: "strikethrough", - emoji: "builtin" - }; - - for (var tokenType in tokenTypes) { - if (tokenTypes.hasOwnProperty(tokenType) && modeCfg.tokenTypeOverrides[tokenType]) { - tokenTypes[tokenType] = modeCfg.tokenTypeOverrides[tokenType]; - } - } - - var hrRE = /^([*\-_])(?:\s*\1){2,}\s*$/ - , listRE = /^(?:[*\-+]|^[0-9]+([.)]))\s+/ - , taskListRE = /^\[(x| )\](?=\s)/i // Must follow listRE - , atxHeaderRE = modeCfg.allowAtxHeaderWithoutSpace ? /^(#+)/ : /^(#+)(?: |$)/ - , setextHeaderRE = /^ {0,3}(?:\={1,}|-{2,})\s*$/ - , textRE = /^[^#!\[\]*_\\<>` "'(~:]+/ - , fencedCodeRE = /^(~~~+|```+)[ \t]*([\w\/+#-]*)[^\n`]*$/ - , linkDefRE = /^\s*\[[^\]]+?\]:.*$/ // naive link-definition - , punctuation = /[!"#$%&'()*+,\-.\/:;<=>?@\[\\\]^_`{|}~\xA1\xA7\xAB\xB6\xB7\xBB\xBF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u0AF0\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166D\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E42\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]|\uD800[\uDD00-\uDD02\uDF9F\uDFD0]|\uD801\uDD6F|\uD802[\uDC57\uDD1F\uDD3F\uDE50-\uDE58\uDE7F\uDEF0-\uDEF6\uDF39-\uDF3F\uDF99-\uDF9C]|\uD804[\uDC47-\uDC4D\uDCBB\uDCBC\uDCBE-\uDCC1\uDD40-\uDD43\uDD74\uDD75\uDDC5-\uDDC9\uDDCD\uDDDB\uDDDD-\uDDDF\uDE38-\uDE3D\uDEA9]|\uD805[\uDCC6\uDDC1-\uDDD7\uDE41-\uDE43\uDF3C-\uDF3E]|\uD809[\uDC70-\uDC74]|\uD81A[\uDE6E\uDE6F\uDEF5\uDF37-\uDF3B\uDF44]|\uD82F\uDC9F|\uD836[\uDE87-\uDE8B]/ - , expandedTab = " " // CommonMark specifies tab as 4 spaces - - function switchInline(stream, state, f) { - state.f = state.inline = f; - return f(stream, state); - } - - function switchBlock(stream, state, f) { - state.f = state.block = f; - return f(stream, state); - } - - function lineIsEmpty(line) { - return !line || !/\S/.test(line.string) - } - - // Blocks - - function blankLine(state) { - // Reset linkTitle state - state.linkTitle = false; - state.linkHref = false; - state.linkText = false; - // Reset EM state - state.em = false; - // Reset STRONG state - state.strong = false; - // Reset strikethrough state - state.strikethrough = false; - // Reset state.quote - state.quote = 0; - // Reset state.indentedCode - state.indentedCode = false; - if (state.f == htmlBlock) { - var exit = htmlModeMissing - if (!exit) { - var inner = CodeMirror.innerMode(htmlMode, state.htmlState) - exit = inner.mode.name == "xml" && inner.state.tagStart === null && - (!inner.state.context && inner.state.tokenize.isInText) - } - if (exit) { - state.f = inlineNormal; - state.block = blockNormal; - state.htmlState = null; - } - } - // Reset state.trailingSpace - state.trailingSpace = 0; - state.trailingSpaceNewLine = false; - // Mark this line as blank - state.prevLine = state.thisLine - state.thisLine = {stream: null} - return null; - } - - function blockNormal(stream, state) { - var firstTokenOnLine = stream.column() === state.indentation; - var prevLineLineIsEmpty = lineIsEmpty(state.prevLine.stream); - var prevLineIsIndentedCode = state.indentedCode; - var prevLineIsHr = state.prevLine.hr; - var prevLineIsList = state.list !== false; - var maxNonCodeIndentation = (state.listStack[state.listStack.length - 1] || 0) + 3; - - state.indentedCode = false; - - var lineIndentation = state.indentation; - // compute once per line (on first token) - if (state.indentationDiff === null) { - state.indentationDiff = state.indentation; - if (prevLineIsList) { - state.list = null; - // While this list item's marker's indentation is less than the deepest - // list item's content's indentation,pop the deepest list item - // indentation off the stack, and update block indentation state - while (lineIndentation < state.listStack[state.listStack.length - 1]) { - state.listStack.pop(); - if (state.listStack.length) { - state.indentation = state.listStack[state.listStack.length - 1]; - // less than the first list's indent -> the line is no longer a list - } else { - state.list = false; - } - } - if (state.list !== false) { - state.indentationDiff = lineIndentation - state.listStack[state.listStack.length - 1] - } - } - } - - // not comprehensive (currently only for setext detection purposes) - var allowsInlineContinuation = ( - !prevLineLineIsEmpty && !prevLineIsHr && !state.prevLine.header && - (!prevLineIsList || !prevLineIsIndentedCode) && - !state.prevLine.fencedCodeEnd - ); - - var isHr = (state.list === false || prevLineIsHr || prevLineLineIsEmpty) && - state.indentation <= maxNonCodeIndentation && stream.match(hrRE); - - var match = null; - if (state.indentationDiff >= 4 && (prevLineIsIndentedCode || state.prevLine.fencedCodeEnd || - state.prevLine.header || prevLineLineIsEmpty)) { - stream.skipToEnd(); - state.indentedCode = true; - return tokenTypes.code; - } else if (stream.eatSpace()) { - return null; - } else if (firstTokenOnLine && state.indentation <= maxNonCodeIndentation && (match = stream.match(atxHeaderRE)) && match[1].length <= 6) { - state.quote = 0; - state.header = match[1].length; - state.thisLine.header = true; - if (modeCfg.highlightFormatting) state.formatting = "header"; - state.f = state.inline; - return getType(state); - } else if (state.indentation <= maxNonCodeIndentation && stream.eat('>')) { - state.quote = firstTokenOnLine ? 1 : state.quote + 1; - if (modeCfg.highlightFormatting) state.formatting = "quote"; - stream.eatSpace(); - return getType(state); - } else if (!isHr && !state.setext && firstTokenOnLine && state.indentation <= maxNonCodeIndentation && (match = stream.match(listRE))) { - var listType = match[1] ? "ol" : "ul"; - - state.indentation = lineIndentation + stream.current().length; - state.list = true; - state.quote = 0; - - // Add this list item's content's indentation to the stack - state.listStack.push(state.indentation); - // Reset inline styles which shouldn't propagate aross list items - state.em = false; - state.strong = false; - state.code = false; - state.strikethrough = false; - - if (modeCfg.taskLists && stream.match(taskListRE, false)) { - state.taskList = true; - } - state.f = state.inline; - if (modeCfg.highlightFormatting) state.formatting = ["list", "list-" + listType]; - return getType(state); - } else if (firstTokenOnLine && state.indentation <= maxNonCodeIndentation && (match = stream.match(fencedCodeRE, true))) { - state.quote = 0; - state.fencedEndRE = new RegExp(match[1] + "+ *$"); - // try switching mode - state.localMode = modeCfg.fencedCodeBlockHighlighting && getMode(match[2] || modeCfg.fencedCodeBlockDefaultMode ); - if (state.localMode) state.localState = CodeMirror.startState(state.localMode); - state.f = state.block = local; - if (modeCfg.highlightFormatting) state.formatting = "code-block"; - state.code = -1 - return getType(state); - // SETEXT has lowest block-scope precedence after HR, so check it after - // the others (code, blockquote, list...) - } else if ( - // if setext set, indicates line after ---/=== - state.setext || ( - // line before ---/=== - (!allowsInlineContinuation || !prevLineIsList) && !state.quote && state.list === false && - !state.code && !isHr && !linkDefRE.test(stream.string) && - (match = stream.lookAhead(1)) && (match = match.match(setextHeaderRE)) - ) - ) { - if ( !state.setext ) { - state.header = match[0].charAt(0) == '=' ? 1 : 2; - state.setext = state.header; - } else { - state.header = state.setext; - // has no effect on type so we can reset it now - state.setext = 0; - stream.skipToEnd(); - if (modeCfg.highlightFormatting) state.formatting = "header"; - } - state.thisLine.header = true; - state.f = state.inline; - return getType(state); - } else if (isHr) { - stream.skipToEnd(); - state.hr = true; - state.thisLine.hr = true; - return tokenTypes.hr; - } else if (stream.peek() === '[') { - return switchInline(stream, state, footnoteLink); - } - - return switchInline(stream, state, state.inline); - } - - function htmlBlock(stream, state) { - var style = htmlMode.token(stream, state.htmlState); - if (!htmlModeMissing) { - var inner = CodeMirror.innerMode(htmlMode, state.htmlState) - if ((inner.mode.name == "xml" && inner.state.tagStart === null && - (!inner.state.context && inner.state.tokenize.isInText)) || - (state.md_inside && stream.current().indexOf(">") > -1)) { - state.f = inlineNormal; - state.block = blockNormal; - state.htmlState = null; - } - } - return style; - } - - function local(stream, state) { - var currListInd = state.listStack[state.listStack.length - 1] || 0; - var hasExitedList = state.indentation < currListInd; - var maxFencedEndInd = currListInd + 3; - if (state.fencedEndRE && state.indentation <= maxFencedEndInd && (hasExitedList || stream.match(state.fencedEndRE))) { - if (modeCfg.highlightFormatting) state.formatting = "code-block"; - var returnType; - if (!hasExitedList) returnType = getType(state) - state.localMode = state.localState = null; - state.block = blockNormal; - state.f = inlineNormal; - state.fencedEndRE = null; - state.code = 0 - state.thisLine.fencedCodeEnd = true; - if (hasExitedList) return switchBlock(stream, state, state.block); - return returnType; - } else if (state.localMode) { - return state.localMode.token(stream, state.localState); - } else { - stream.skipToEnd(); - return tokenTypes.code; - } - } - - // Inline - function getType(state) { - var styles = []; - - if (state.formatting) { - styles.push(tokenTypes.formatting); - - if (typeof state.formatting === "string") state.formatting = [state.formatting]; - - for (var i = 0; i < state.formatting.length; i++) { - styles.push(tokenTypes.formatting + "-" + state.formatting[i]); - - if (state.formatting[i] === "header") { - styles.push(tokenTypes.formatting + "-" + state.formatting[i] + "-" + state.header); - } - - // Add `formatting-quote` and `formatting-quote-#` for blockquotes - // Add `error` instead if the maximum blockquote nesting depth is passed - if (state.formatting[i] === "quote") { - if (!modeCfg.maxBlockquoteDepth || modeCfg.maxBlockquoteDepth >= state.quote) { - styles.push(tokenTypes.formatting + "-" + state.formatting[i] + "-" + state.quote); - } else { - styles.push("error"); - } - } - } - } - - if (state.taskOpen) { - styles.push("meta"); - return styles.length ? styles.join(' ') : null; - } - if (state.taskClosed) { - styles.push("property"); - return styles.length ? styles.join(' ') : null; - } - - if (state.linkHref) { - styles.push(tokenTypes.linkHref, "url"); - } else { // Only apply inline styles to non-url text - if (state.strong) { styles.push(tokenTypes.strong); } - if (state.em) { styles.push(tokenTypes.em); } - if (state.strikethrough) { styles.push(tokenTypes.strikethrough); } - if (state.emoji) { styles.push(tokenTypes.emoji); } - if (state.linkText) { styles.push(tokenTypes.linkText); } - if (state.code) { styles.push(tokenTypes.code); } - if (state.image) { styles.push(tokenTypes.image); } - if (state.imageAltText) { styles.push(tokenTypes.imageAltText, "link"); } - if (state.imageMarker) { styles.push(tokenTypes.imageMarker); } - } - - if (state.header) { styles.push(tokenTypes.header, tokenTypes.header + "-" + state.header); } - - if (state.quote) { - styles.push(tokenTypes.quote); - - // Add `quote-#` where the maximum for `#` is modeCfg.maxBlockquoteDepth - if (!modeCfg.maxBlockquoteDepth || modeCfg.maxBlockquoteDepth >= state.quote) { - styles.push(tokenTypes.quote + "-" + state.quote); - } else { - styles.push(tokenTypes.quote + "-" + modeCfg.maxBlockquoteDepth); - } - } - - if (state.list !== false) { - var listMod = (state.listStack.length - 1) % 3; - if (!listMod) { - styles.push(tokenTypes.list1); - } else if (listMod === 1) { - styles.push(tokenTypes.list2); - } else { - styles.push(tokenTypes.list3); - } - } - - if (state.trailingSpaceNewLine) { - styles.push("trailing-space-new-line"); - } else if (state.trailingSpace) { - styles.push("trailing-space-" + (state.trailingSpace % 2 ? "a" : "b")); - } - - return styles.length ? styles.join(' ') : null; - } - - function handleText(stream, state) { - if (stream.match(textRE, true)) { - return getType(state); - } - return undefined; - } - - function inlineNormal(stream, state) { - var style = state.text(stream, state); - if (typeof style !== 'undefined') - return style; - - if (state.list) { // List marker (*, +, -, 1., etc) - state.list = null; - return getType(state); - } - - if (state.taskList) { - var taskOpen = stream.match(taskListRE, true)[1] === " "; - if (taskOpen) state.taskOpen = true; - else state.taskClosed = true; - if (modeCfg.highlightFormatting) state.formatting = "task"; - state.taskList = false; - return getType(state); - } - - state.taskOpen = false; - state.taskClosed = false; - - if (state.header && stream.match(/^#+$/, true)) { - if (modeCfg.highlightFormatting) state.formatting = "header"; - return getType(state); - } - - var ch = stream.next(); - - // Matches link titles present on next line - if (state.linkTitle) { - state.linkTitle = false; - var matchCh = ch; - if (ch === '(') { - matchCh = ')'; - } - matchCh = (matchCh+'').replace(/([.?*+^\[\]\\(){}|-])/g, "\\$1"); - var regex = '^\\s*(?:[^' + matchCh + '\\\\]+|\\\\\\\\|\\\\.)' + matchCh; - if (stream.match(new RegExp(regex), true)) { - return tokenTypes.linkHref; - } - } - - // If this block is changed, it may need to be updated in GFM mode - if (ch === '`') { - var previousFormatting = state.formatting; - if (modeCfg.highlightFormatting) state.formatting = "code"; - stream.eatWhile('`'); - var count = stream.current().length - if (state.code == 0 && (!state.quote || count == 1)) { - state.code = count - return getType(state) - } else if (count == state.code) { // Must be exact - var t = getType(state) - state.code = 0 - return t - } else { - state.formatting = previousFormatting - return getType(state) - } - } else if (state.code) { - return getType(state); - } - - if (ch === '\\') { - stream.next(); - if (modeCfg.highlightFormatting) { - var type = getType(state); - var formattingEscape = tokenTypes.formatting + "-escape"; - return type ? type + " " + formattingEscape : formattingEscape; - } - } - - if (ch === '!' && stream.match(/\[[^\]]*\] ?(?:\(|\[)/, false)) { - state.imageMarker = true; - state.image = true; - if (modeCfg.highlightFormatting) state.formatting = "image"; - return getType(state); - } - - if (ch === '[' && state.imageMarker && stream.match(/[^\]]*\](\(.*?\)| ?\[.*?\])/, false)) { - state.imageMarker = false; - state.imageAltText = true - if (modeCfg.highlightFormatting) state.formatting = "image"; - return getType(state); - } - - if (ch === ']' && state.imageAltText) { - if (modeCfg.highlightFormatting) state.formatting = "image"; - var type = getType(state); - state.imageAltText = false; - state.image = false; - state.inline = state.f = linkHref; - return type; - } - - if (ch === '[' && !state.image) { - if (state.linkText && stream.match(/^.*?\]/)) return getType(state) - state.linkText = true; - if (modeCfg.highlightFormatting) state.formatting = "link"; - return getType(state); - } - - if (ch === ']' && state.linkText) { - if (modeCfg.highlightFormatting) state.formatting = "link"; - var type = getType(state); - state.linkText = false; - state.inline = state.f = stream.match(/\(.*?\)| ?\[.*?\]/, false) ? linkHref : inlineNormal - return type; - } - - if (ch === '<' && stream.match(/^(https?|ftps?):\/\/(?:[^\\>]|\\.)+>/, false)) { - state.f = state.inline = linkInline; - if (modeCfg.highlightFormatting) state.formatting = "link"; - var type = getType(state); - if (type){ - type += " "; - } else { - type = ""; - } - return type + tokenTypes.linkInline; - } - - if (ch === '<' && stream.match(/^[^> \\]+@(?:[^\\>]|\\.)+>/, false)) { - state.f = state.inline = linkInline; - if (modeCfg.highlightFormatting) state.formatting = "link"; - var type = getType(state); - if (type){ - type += " "; - } else { - type = ""; - } - return type + tokenTypes.linkEmail; - } - - if (modeCfg.xml && ch === '<' && stream.match(/^(!--|\?|!\[CDATA\[|[a-z][a-z0-9-]*(?:\s+[a-z_:.\-]+(?:\s*=\s*[^>]+)?)*\s*(?:>|$))/i, false)) { - var end = stream.string.indexOf(">", stream.pos); - if (end != -1) { - var atts = stream.string.substring(stream.start, end); - if (/markdown\s*=\s*('|"){0,1}1('|"){0,1}/.test(atts)) state.md_inside = true; - } - stream.backUp(1); - state.htmlState = CodeMirror.startState(htmlMode); - return switchBlock(stream, state, htmlBlock); - } - - if (modeCfg.xml && ch === '<' && stream.match(/^\/\w*?>/)) { - state.md_inside = false; - return "tag"; - } else if (ch === "*" || ch === "_") { - var len = 1, before = stream.pos == 1 ? " " : stream.string.charAt(stream.pos - 2) - while (len < 3 && stream.eat(ch)) len++ - var after = stream.peek() || " " - // See http://spec.commonmark.org/0.27/#emphasis-and-strong-emphasis - var leftFlanking = !/\s/.test(after) && (!punctuation.test(after) || /\s/.test(before) || punctuation.test(before)) - var rightFlanking = !/\s/.test(before) && (!punctuation.test(before) || /\s/.test(after) || punctuation.test(after)) - var setEm = null, setStrong = null - if (len % 2) { // Em - if (!state.em && leftFlanking && (ch === "*" || !rightFlanking || punctuation.test(before))) - setEm = true - else if (state.em == ch && rightFlanking && (ch === "*" || !leftFlanking || punctuation.test(after))) - setEm = false - } - if (len > 1) { // Strong - if (!state.strong && leftFlanking && (ch === "*" || !rightFlanking || punctuation.test(before))) - setStrong = true - else if (state.strong == ch && rightFlanking && (ch === "*" || !leftFlanking || punctuation.test(after))) - setStrong = false - } - if (setStrong != null || setEm != null) { - if (modeCfg.highlightFormatting) state.formatting = setEm == null ? "strong" : setStrong == null ? "em" : "strong em" - if (setEm === true) state.em = ch - if (setStrong === true) state.strong = ch - var t = getType(state) - if (setEm === false) state.em = false - if (setStrong === false) state.strong = false - return t - } - } else if (ch === ' ') { - if (stream.eat('*') || stream.eat('_')) { // Probably surrounded by spaces - if (stream.peek() === ' ') { // Surrounded by spaces, ignore - return getType(state); - } else { // Not surrounded by spaces, back up pointer - stream.backUp(1); - } - } - } - - if (modeCfg.strikethrough) { - if (ch === '~' && stream.eatWhile(ch)) { - if (state.strikethrough) {// Remove strikethrough - if (modeCfg.highlightFormatting) state.formatting = "strikethrough"; - var t = getType(state); - state.strikethrough = false; - return t; - } else if (stream.match(/^[^\s]/, false)) {// Add strikethrough - state.strikethrough = true; - if (modeCfg.highlightFormatting) state.formatting = "strikethrough"; - return getType(state); - } - } else if (ch === ' ') { - if (stream.match(/^~~/, true)) { // Probably surrounded by space - if (stream.peek() === ' ') { // Surrounded by spaces, ignore - return getType(state); - } else { // Not surrounded by spaces, back up pointer - stream.backUp(2); - } - } - } - } - - if (modeCfg.emoji && ch === ":" && stream.match(/^(?:[a-z_\d+][a-z_\d+-]*|\-[a-z_\d+][a-z_\d+-]*):/)) { - state.emoji = true; - if (modeCfg.highlightFormatting) state.formatting = "emoji"; - var retType = getType(state); - state.emoji = false; - return retType; - } - - if (ch === ' ') { - if (stream.match(/^ +$/, false)) { - state.trailingSpace++; - } else if (state.trailingSpace) { - state.trailingSpaceNewLine = true; - } - } - - return getType(state); - } - - function linkInline(stream, state) { - var ch = stream.next(); - - if (ch === ">") { - state.f = state.inline = inlineNormal; - if (modeCfg.highlightFormatting) state.formatting = "link"; - var type = getType(state); - if (type){ - type += " "; - } else { - type = ""; - } - return type + tokenTypes.linkInline; - } - - stream.match(/^[^>]+/, true); - - return tokenTypes.linkInline; - } - - function linkHref(stream, state) { - // Check if space, and return NULL if so (to avoid marking the space) - if(stream.eatSpace()){ - return null; - } - var ch = stream.next(); - if (ch === '(' || ch === '[') { - state.f = state.inline = getLinkHrefInside(ch === "(" ? ")" : "]"); - if (modeCfg.highlightFormatting) state.formatting = "link-string"; - state.linkHref = true; - return getType(state); - } - return 'error'; - } - - var linkRE = { - ")": /^(?:[^\\\(\)]|\\.|\((?:[^\\\(\)]|\\.)*\))*?(?=\))/, - "]": /^(?:[^\\\[\]]|\\.|\[(?:[^\\\[\]]|\\.)*\])*?(?=\])/ - } - - function getLinkHrefInside(endChar) { - return function(stream, state) { - var ch = stream.next(); - - if (ch === endChar) { - state.f = state.inline = inlineNormal; - if (modeCfg.highlightFormatting) state.formatting = "link-string"; - var returnState = getType(state); - state.linkHref = false; - return returnState; - } - - stream.match(linkRE[endChar]) - state.linkHref = true; - return getType(state); - }; - } - - function footnoteLink(stream, state) { - if (stream.match(/^([^\]\\]|\\.)*\]:/, false)) { - state.f = footnoteLinkInside; - stream.next(); // Consume [ - if (modeCfg.highlightFormatting) state.formatting = "link"; - state.linkText = true; - return getType(state); - } - return switchInline(stream, state, inlineNormal); - } - - function footnoteLinkInside(stream, state) { - if (stream.match(/^\]:/, true)) { - state.f = state.inline = footnoteUrl; - if (modeCfg.highlightFormatting) state.formatting = "link"; - var returnType = getType(state); - state.linkText = false; - return returnType; - } - - stream.match(/^([^\]\\]|\\.)+/, true); - - return tokenTypes.linkText; - } - - function footnoteUrl(stream, state) { - // Check if space, and return NULL if so (to avoid marking the space) - if(stream.eatSpace()){ - return null; - } - // Match URL - stream.match(/^[^\s]+/, true); - // Check for link title - if (stream.peek() === undefined) { // End of line, set flag to check next line - state.linkTitle = true; - } else { // More content on line, check if link title - stream.match(/^(?:\s+(?:"(?:[^"\\]|\\\\|\\.)+"|'(?:[^'\\]|\\\\|\\.)+'|\((?:[^)\\]|\\\\|\\.)+\)))?/, true); - } - state.f = state.inline = inlineNormal; - return tokenTypes.linkHref + " url"; - } - - var mode = { - startState: function() { - return { - f: blockNormal, - - prevLine: {stream: null}, - thisLine: {stream: null}, - - block: blockNormal, - htmlState: null, - indentation: 0, - - inline: inlineNormal, - text: handleText, - - formatting: false, - linkText: false, - linkHref: false, - linkTitle: false, - code: 0, - em: false, - strong: false, - header: 0, - setext: 0, - hr: false, - taskList: false, - list: false, - listStack: [], - quote: 0, - trailingSpace: 0, - trailingSpaceNewLine: false, - strikethrough: false, - emoji: false, - fencedEndRE: null - }; - }, - - copyState: function(s) { - return { - f: s.f, - - prevLine: s.prevLine, - thisLine: s.thisLine, - - block: s.block, - htmlState: s.htmlState && CodeMirror.copyState(htmlMode, s.htmlState), - indentation: s.indentation, - - localMode: s.localMode, - localState: s.localMode ? CodeMirror.copyState(s.localMode, s.localState) : null, - - inline: s.inline, - text: s.text, - formatting: false, - linkText: s.linkText, - linkTitle: s.linkTitle, - linkHref: s.linkHref, - code: s.code, - em: s.em, - strong: s.strong, - strikethrough: s.strikethrough, - emoji: s.emoji, - header: s.header, - setext: s.setext, - hr: s.hr, - taskList: s.taskList, - list: s.list, - listStack: s.listStack.slice(0), - quote: s.quote, - indentedCode: s.indentedCode, - trailingSpace: s.trailingSpace, - trailingSpaceNewLine: s.trailingSpaceNewLine, - md_inside: s.md_inside, - fencedEndRE: s.fencedEndRE - }; - }, - - token: function(stream, state) { - - // Reset state.formatting - state.formatting = false; - - if (stream != state.thisLine.stream) { - state.header = 0; - state.hr = false; - - if (stream.match(/^\s*$/, true)) { - blankLine(state); - return null; - } - - state.prevLine = state.thisLine - state.thisLine = {stream: stream} - - // Reset state.taskList - state.taskList = false; - - // Reset state.trailingSpace - state.trailingSpace = 0; - state.trailingSpaceNewLine = false; - - if (!state.localState) { - state.f = state.block; - if (state.f != htmlBlock) { - var indentation = stream.match(/^\s*/, true)[0].replace(/\t/g, expandedTab).length; - state.indentation = indentation; - state.indentationDiff = null; - if (indentation > 0) return null; - } - } - } - return state.f(stream, state); - }, - - innerMode: function(state) { - if (state.block == htmlBlock) return {state: state.htmlState, mode: htmlMode}; - if (state.localState) return {state: state.localState, mode: state.localMode}; - return {state: state, mode: mode}; - }, - - indent: function(state, textAfter, line) { - if (state.block == htmlBlock && htmlMode.indent) return htmlMode.indent(state.htmlState, textAfter, line) - if (state.localState && state.localMode.indent) return state.localMode.indent(state.localState, textAfter, line) - return CodeMirror.Pass - }, - - blankLine: blankLine, - - getType: getType, - - blockCommentStart: "", - closeBrackets: "()[]{}''\"\"``", - fold: "markdown" - }; - return mode; -}, "xml"); - -CodeMirror.defineMIME("text/markdown", "markdown"); - -CodeMirror.defineMIME("text/x-markdown", "markdown"); - -}); diff --git a/plugins/UiFileManager/media/codemirror/mode/python.js b/plugins/UiFileManager/media/codemirror/mode/python.js deleted file mode 100644 index de5fd38a..00000000 --- a/plugins/UiFileManager/media/codemirror/mode/python.js +++ /dev/null @@ -1,399 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { - "use strict"; - - function wordRegexp(words) { - return new RegExp("^((" + words.join(")|(") + "))\\b"); - } - - var wordOperators = wordRegexp(["and", "or", "not", "is"]); - var commonKeywords = ["as", "assert", "break", "class", "continue", - "def", "del", "elif", "else", "except", "finally", - "for", "from", "global", "if", "import", - "lambda", "pass", "raise", "return", - "try", "while", "with", "yield", "in"]; - var commonBuiltins = ["abs", "all", "any", "bin", "bool", "bytearray", "callable", "chr", - "classmethod", "compile", "complex", "delattr", "dict", "dir", "divmod", - "enumerate", "eval", "filter", "float", "format", "frozenset", - "getattr", "globals", "hasattr", "hash", "help", "hex", "id", - "input", "int", "isinstance", "issubclass", "iter", "len", - "list", "locals", "map", "max", "memoryview", "min", "next", - "object", "oct", "open", "ord", "pow", "property", "range", - "repr", "reversed", "round", "set", "setattr", "slice", - "sorted", "staticmethod", "str", "sum", "super", "tuple", - "type", "vars", "zip", "__import__", "NotImplemented", - "Ellipsis", "__debug__"]; - CodeMirror.registerHelper("hintWords", "python", commonKeywords.concat(commonBuiltins)); - - function top(state) { - return state.scopes[state.scopes.length - 1]; - } - - CodeMirror.defineMode("python", function(conf, parserConf) { - var ERRORCLASS = "error"; - - var delimiters = parserConf.delimiters || parserConf.singleDelimiters || /^[\(\)\[\]\{\}@,:`=;\.\\]/; - // (Backwards-compatibility with old, cumbersome config system) - var operators = [parserConf.singleOperators, parserConf.doubleOperators, parserConf.doubleDelimiters, parserConf.tripleDelimiters, - parserConf.operators || /^([-+*/%\/&|^]=?|[<>=]+|\/\/=?|\*\*=?|!=|[~!@]|\.\.\.)/] - for (var i = 0; i < operators.length; i++) if (!operators[i]) operators.splice(i--, 1) - - var hangingIndent = parserConf.hangingIndent || conf.indentUnit; - - var myKeywords = commonKeywords, myBuiltins = commonBuiltins; - if (parserConf.extra_keywords != undefined) - myKeywords = myKeywords.concat(parserConf.extra_keywords); - - if (parserConf.extra_builtins != undefined) - myBuiltins = myBuiltins.concat(parserConf.extra_builtins); - - var py3 = !(parserConf.version && Number(parserConf.version) < 3) - if (py3) { - // since http://legacy.python.org/dev/peps/pep-0465/ @ is also an operator - var identifiers = parserConf.identifiers|| /^[_A-Za-z\u00A1-\uFFFF][_A-Za-z0-9\u00A1-\uFFFF]*/; - myKeywords = myKeywords.concat(["nonlocal", "False", "True", "None", "async", "await"]); - myBuiltins = myBuiltins.concat(["ascii", "bytes", "exec", "print"]); - var stringPrefixes = new RegExp("^(([rbuf]|(br)|(fr))?('{3}|\"{3}|['\"]))", "i"); - } else { - var identifiers = parserConf.identifiers|| /^[_A-Za-z][_A-Za-z0-9]*/; - myKeywords = myKeywords.concat(["exec", "print"]); - myBuiltins = myBuiltins.concat(["apply", "basestring", "buffer", "cmp", "coerce", "execfile", - "file", "intern", "long", "raw_input", "reduce", "reload", - "unichr", "unicode", "xrange", "False", "True", "None"]); - var stringPrefixes = new RegExp("^(([rubf]|(ur)|(br))?('{3}|\"{3}|['\"]))", "i"); - } - var keywords = wordRegexp(myKeywords); - var builtins = wordRegexp(myBuiltins); - - // tokenizers - function tokenBase(stream, state) { - var sol = stream.sol() && state.lastToken != "\\" - if (sol) state.indent = stream.indentation() - // Handle scope changes - if (sol && top(state).type == "py") { - var scopeOffset = top(state).offset; - if (stream.eatSpace()) { - var lineOffset = stream.indentation(); - if (lineOffset > scopeOffset) - pushPyScope(state); - else if (lineOffset < scopeOffset && dedent(stream, state) && stream.peek() != "#") - state.errorToken = true; - return null; - } else { - var style = tokenBaseInner(stream, state); - if (scopeOffset > 0 && dedent(stream, state)) - style += " " + ERRORCLASS; - return style; - } - } - return tokenBaseInner(stream, state); - } - - function tokenBaseInner(stream, state, inFormat) { - if (stream.eatSpace()) return null; - - // Handle Comments - if (!inFormat && stream.match(/^#.*/)) return "comment"; - - // Handle Number Literals - if (stream.match(/^[0-9\.]/, false)) { - var floatLiteral = false; - // Floats - if (stream.match(/^[\d_]*\.\d+(e[\+\-]?\d+)?/i)) { floatLiteral = true; } - if (stream.match(/^[\d_]+\.\d*/)) { floatLiteral = true; } - if (stream.match(/^\.\d+/)) { floatLiteral = true; } - if (floatLiteral) { - // Float literals may be "imaginary" - stream.eat(/J/i); - return "number"; - } - // Integers - var intLiteral = false; - // Hex - if (stream.match(/^0x[0-9a-f_]+/i)) intLiteral = true; - // Binary - if (stream.match(/^0b[01_]+/i)) intLiteral = true; - // Octal - if (stream.match(/^0o[0-7_]+/i)) intLiteral = true; - // Decimal - if (stream.match(/^[1-9][\d_]*(e[\+\-]?[\d_]+)?/)) { - // Decimal literals may be "imaginary" - stream.eat(/J/i); - // TODO - Can you have imaginary longs? - intLiteral = true; - } - // Zero by itself with no other piece of number. - if (stream.match(/^0(?![\dx])/i)) intLiteral = true; - if (intLiteral) { - // Integer literals may be "long" - stream.eat(/L/i); - return "number"; - } - } - - // Handle Strings - if (stream.match(stringPrefixes)) { - var isFmtString = stream.current().toLowerCase().indexOf('f') !== -1; - if (!isFmtString) { - state.tokenize = tokenStringFactory(stream.current(), state.tokenize); - return state.tokenize(stream, state); - } else { - state.tokenize = formatStringFactory(stream.current(), state.tokenize); - return state.tokenize(stream, state); - } - } - - for (var i = 0; i < operators.length; i++) - if (stream.match(operators[i])) return "operator" - - if (stream.match(delimiters)) return "punctuation"; - - if (state.lastToken == "." && stream.match(identifiers)) - return "property"; - - if (stream.match(keywords) || stream.match(wordOperators)) - return "keyword"; - - if (stream.match(builtins)) - return "builtin"; - - if (stream.match(/^(self|cls)\b/)) - return "variable-2"; - - if (stream.match(identifiers)) { - if (state.lastToken == "def" || state.lastToken == "class") - return "def"; - return "variable"; - } - - // Handle non-detected items - stream.next(); - return inFormat ? null :ERRORCLASS; - } - - function formatStringFactory(delimiter, tokenOuter) { - while ("rubf".indexOf(delimiter.charAt(0).toLowerCase()) >= 0) - delimiter = delimiter.substr(1); - - var singleline = delimiter.length == 1; - var OUTCLASS = "string"; - - function tokenNestedExpr(depth) { - return function(stream, state) { - var inner = tokenBaseInner(stream, state, true) - if (inner == "punctuation") { - if (stream.current() == "{") { - state.tokenize = tokenNestedExpr(depth + 1) - } else if (stream.current() == "}") { - if (depth > 1) state.tokenize = tokenNestedExpr(depth - 1) - else state.tokenize = tokenString - } - } - return inner - } - } - - function tokenString(stream, state) { - while (!stream.eol()) { - stream.eatWhile(/[^'"\{\}\\]/); - if (stream.eat("\\")) { - stream.next(); - if (singleline && stream.eol()) - return OUTCLASS; - } else if (stream.match(delimiter)) { - state.tokenize = tokenOuter; - return OUTCLASS; - } else if (stream.match('{{')) { - // ignore {{ in f-str - return OUTCLASS; - } else if (stream.match('{', false)) { - // switch to nested mode - state.tokenize = tokenNestedExpr(0) - if (stream.current()) return OUTCLASS; - else return state.tokenize(stream, state) - } else if (stream.match('}}')) { - return OUTCLASS; - } else if (stream.match('}')) { - // single } in f-string is an error - return ERRORCLASS; - } else { - stream.eat(/['"]/); - } - } - if (singleline) { - if (parserConf.singleLineStringErrors) - return ERRORCLASS; - else - state.tokenize = tokenOuter; - } - return OUTCLASS; - } - tokenString.isString = true; - return tokenString; - } - - function tokenStringFactory(delimiter, tokenOuter) { - while ("rubf".indexOf(delimiter.charAt(0).toLowerCase()) >= 0) - delimiter = delimiter.substr(1); - - var singleline = delimiter.length == 1; - var OUTCLASS = "string"; - - function tokenString(stream, state) { - while (!stream.eol()) { - stream.eatWhile(/[^'"\\]/); - if (stream.eat("\\")) { - stream.next(); - if (singleline && stream.eol()) - return OUTCLASS; - } else if (stream.match(delimiter)) { - state.tokenize = tokenOuter; - return OUTCLASS; - } else { - stream.eat(/['"]/); - } - } - if (singleline) { - if (parserConf.singleLineStringErrors) - return ERRORCLASS; - else - state.tokenize = tokenOuter; - } - return OUTCLASS; - } - tokenString.isString = true; - return tokenString; - } - - function pushPyScope(state) { - while (top(state).type != "py") state.scopes.pop() - state.scopes.push({offset: top(state).offset + conf.indentUnit, - type: "py", - align: null}) - } - - function pushBracketScope(stream, state, type) { - var align = stream.match(/^([\s\[\{\(]|#.*)*$/, false) ? null : stream.column() + 1 - state.scopes.push({offset: state.indent + hangingIndent, - type: type, - align: align}) - } - - function dedent(stream, state) { - var indented = stream.indentation(); - while (state.scopes.length > 1 && top(state).offset > indented) { - if (top(state).type != "py") return true; - state.scopes.pop(); - } - return top(state).offset != indented; - } - - function tokenLexer(stream, state) { - if (stream.sol()) state.beginningOfLine = true; - - var style = state.tokenize(stream, state); - var current = stream.current(); - - // Handle decorators - if (state.beginningOfLine && current == "@") - return stream.match(identifiers, false) ? "meta" : py3 ? "operator" : ERRORCLASS; - - if (/\S/.test(current)) state.beginningOfLine = false; - - if ((style == "variable" || style == "builtin") - && state.lastToken == "meta") - style = "meta"; - - // Handle scope changes. - if (current == "pass" || current == "return") - state.dedent += 1; - - if (current == "lambda") state.lambda = true; - if (current == ":" && !state.lambda && top(state).type == "py") - pushPyScope(state); - - if (current.length == 1 && !/string|comment/.test(style)) { - var delimiter_index = "[({".indexOf(current); - if (delimiter_index != -1) - pushBracketScope(stream, state, "])}".slice(delimiter_index, delimiter_index+1)); - - delimiter_index = "])}".indexOf(current); - if (delimiter_index != -1) { - if (top(state).type == current) state.indent = state.scopes.pop().offset - hangingIndent - else return ERRORCLASS; - } - } - if (state.dedent > 0 && stream.eol() && top(state).type == "py") { - if (state.scopes.length > 1) state.scopes.pop(); - state.dedent -= 1; - } - - return style; - } - - var external = { - startState: function(basecolumn) { - return { - tokenize: tokenBase, - scopes: [{offset: basecolumn || 0, type: "py", align: null}], - indent: basecolumn || 0, - lastToken: null, - lambda: false, - dedent: 0 - }; - }, - - token: function(stream, state) { - var addErr = state.errorToken; - if (addErr) state.errorToken = false; - var style = tokenLexer(stream, state); - - if (style && style != "comment") - state.lastToken = (style == "keyword" || style == "punctuation") ? stream.current() : style; - if (style == "punctuation") style = null; - - if (stream.eol() && state.lambda) - state.lambda = false; - return addErr ? style + " " + ERRORCLASS : style; - }, - - indent: function(state, textAfter) { - if (state.tokenize != tokenBase) - return state.tokenize.isString ? CodeMirror.Pass : 0; - - var scope = top(state), closing = scope.type == textAfter.charAt(0) - if (scope.align != null) - return scope.align - (closing ? 1 : 0) - else - return scope.offset - (closing ? hangingIndent : 0) - }, - - electricInput: /^\s*[\}\]\)]$/, - closeBrackets: {triples: "'\""}, - lineComment: "#", - fold: "indent" - }; - return external; - }); - - CodeMirror.defineMIME("text/x-python", "python"); - - var words = function(str) { return str.split(" "); }; - - CodeMirror.defineMIME("text/x-cython", { - name: "python", - extra_keywords: words("by cdef cimport cpdef ctypedef enum except "+ - "extern gil include nogil property public "+ - "readonly struct union DEF IF ELIF ELSE") - }); - -}); diff --git a/plugins/UiFileManager/media/codemirror/mode/rust.js b/plugins/UiFileManager/media/codemirror/mode/rust.js deleted file mode 100644 index f95f320d..00000000 --- a/plugins/UiFileManager/media/codemirror/mode/rust.js +++ /dev/null @@ -1,72 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("../../addon/mode/simple")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "../../addon/mode/simple"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -CodeMirror.defineSimpleMode("rust",{ - start: [ - // string and byte string - {regex: /b?"/, token: "string", next: "string"}, - // raw string and raw byte string - {regex: /b?r"/, token: "string", next: "string_raw"}, - {regex: /b?r#+"/, token: "string", next: "string_raw_hash"}, - // character - {regex: /'(?:[^'\\]|\\(?:[nrt0'"]|x[\da-fA-F]{2}|u\{[\da-fA-F]{6}\}))'/, token: "string-2"}, - // byte - {regex: /b'(?:[^']|\\(?:['\\nrt0]|x[\da-fA-F]{2}))'/, token: "string-2"}, - - {regex: /(?:(?:[0-9][0-9_]*)(?:(?:[Ee][+-]?[0-9_]+)|\.[0-9_]+(?:[Ee][+-]?[0-9_]+)?)(?:f32|f64)?)|(?:0(?:b[01_]+|(?:o[0-7_]+)|(?:x[0-9a-fA-F_]+))|(?:[0-9][0-9_]*))(?:u8|u16|u32|u64|i8|i16|i32|i64|isize|usize)?/, - token: "number"}, - {regex: /(let(?:\s+mut)?|fn|enum|mod|struct|type|union)(\s+)([a-zA-Z_][a-zA-Z0-9_]*)/, token: ["keyword", null, "def"]}, - {regex: /(?:abstract|alignof|as|async|await|box|break|continue|const|crate|do|dyn|else|enum|extern|fn|for|final|if|impl|in|loop|macro|match|mod|move|offsetof|override|priv|proc|pub|pure|ref|return|self|sizeof|static|struct|super|trait|type|typeof|union|unsafe|unsized|use|virtual|where|while|yield)\b/, token: "keyword"}, - {regex: /\b(?:Self|isize|usize|char|bool|u8|u16|u32|u64|f16|f32|f64|i8|i16|i32|i64|str|Option)\b/, token: "atom"}, - {regex: /\b(?:true|false|Some|None|Ok|Err)\b/, token: "builtin"}, - {regex: /\b(fn)(\s+)([a-zA-Z_][a-zA-Z0-9_]*)/, - token: ["keyword", null ,"def"]}, - {regex: /#!?\[.*\]/, token: "meta"}, - {regex: /\/\/.*/, token: "comment"}, - {regex: /\/\*/, token: "comment", next: "comment"}, - {regex: /[-+\/*=<>!]+/, token: "operator"}, - {regex: /[a-zA-Z_]\w*!/,token: "variable-3"}, - {regex: /[a-zA-Z_]\w*/, token: "variable"}, - {regex: /[\{\[\(]/, indent: true}, - {regex: /[\}\]\)]/, dedent: true} - ], - string: [ - {regex: /"/, token: "string", next: "start"}, - {regex: /(?:[^\\"]|\\(?:.|$))*/, token: "string"} - ], - string_raw: [ - {regex: /"/, token: "string", next: "start"}, - {regex: /[^"]*/, token: "string"} - ], - string_raw_hash: [ - {regex: /"#+/, token: "string", next: "start"}, - {regex: /(?:[^"]|"(?!#))*/, token: "string"} - ], - comment: [ - {regex: /.*?\*\//, token: "comment", next: "start"}, - {regex: /.*/, token: "comment"} - ], - meta: { - dontIndentStates: ["comment"], - electricInput: /^\s*\}$/, - blockCommentStart: "/*", - blockCommentEnd: "*/", - lineComment: "//", - fold: "brace" - } -}); - - -CodeMirror.defineMIME("text/x-rustsrc", "rust"); -CodeMirror.defineMIME("text/rust", "rust"); -}); diff --git a/plugins/UiFileManager/media/codemirror/mode/xml.js b/plugins/UiFileManager/media/codemirror/mode/xml.js deleted file mode 100644 index 73c6e0e0..00000000 --- a/plugins/UiFileManager/media/codemirror/mode/xml.js +++ /dev/null @@ -1,413 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/LICENSE - -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function(CodeMirror) { -"use strict"; - -var htmlConfig = { - autoSelfClosers: {'area': true, 'base': true, 'br': true, 'col': true, 'command': true, - 'embed': true, 'frame': true, 'hr': true, 'img': true, 'input': true, - 'keygen': true, 'link': true, 'meta': true, 'param': true, 'source': true, - 'track': true, 'wbr': true, 'menuitem': true}, - implicitlyClosed: {'dd': true, 'li': true, 'optgroup': true, 'option': true, 'p': true, - 'rp': true, 'rt': true, 'tbody': true, 'td': true, 'tfoot': true, - 'th': true, 'tr': true}, - contextGrabbers: { - 'dd': {'dd': true, 'dt': true}, - 'dt': {'dd': true, 'dt': true}, - 'li': {'li': true}, - 'option': {'option': true, 'optgroup': true}, - 'optgroup': {'optgroup': true}, - 'p': {'address': true, 'article': true, 'aside': true, 'blockquote': true, 'dir': true, - 'div': true, 'dl': true, 'fieldset': true, 'footer': true, 'form': true, - 'h1': true, 'h2': true, 'h3': true, 'h4': true, 'h5': true, 'h6': true, - 'header': true, 'hgroup': true, 'hr': true, 'menu': true, 'nav': true, 'ol': true, - 'p': true, 'pre': true, 'section': true, 'table': true, 'ul': true}, - 'rp': {'rp': true, 'rt': true}, - 'rt': {'rp': true, 'rt': true}, - 'tbody': {'tbody': true, 'tfoot': true}, - 'td': {'td': true, 'th': true}, - 'tfoot': {'tbody': true}, - 'th': {'td': true, 'th': true}, - 'thead': {'tbody': true, 'tfoot': true}, - 'tr': {'tr': true} - }, - doNotIndent: {"pre": true}, - allowUnquoted: true, - allowMissing: true, - caseFold: true -} - -var xmlConfig = { - autoSelfClosers: {}, - implicitlyClosed: {}, - contextGrabbers: {}, - doNotIndent: {}, - allowUnquoted: false, - allowMissing: false, - allowMissingTagName: false, - caseFold: false -} - -CodeMirror.defineMode("xml", function(editorConf, config_) { - var indentUnit = editorConf.indentUnit - var config = {} - var defaults = config_.htmlMode ? htmlConfig : xmlConfig - for (var prop in defaults) config[prop] = defaults[prop] - for (var prop in config_) config[prop] = config_[prop] - - // Return variables for tokenizers - var type, setStyle; - - function inText(stream, state) { - function chain(parser) { - state.tokenize = parser; - return parser(stream, state); - } - - var ch = stream.next(); - if (ch == "<") { - if (stream.eat("!")) { - if (stream.eat("[")) { - if (stream.match("CDATA[")) return chain(inBlock("atom", "]]>")); - else return null; - } else if (stream.match("--")) { - return chain(inBlock("comment", "-->")); - } else if (stream.match("DOCTYPE", true, true)) { - stream.eatWhile(/[\w\._\-]/); - return chain(doctype(1)); - } else { - return null; - } - } else if (stream.eat("?")) { - stream.eatWhile(/[\w\._\-]/); - state.tokenize = inBlock("meta", "?>"); - return "meta"; - } else { - type = stream.eat("/") ? "closeTag" : "openTag"; - state.tokenize = inTag; - return "tag bracket"; - } - } else if (ch == "&") { - var ok; - if (stream.eat("#")) { - if (stream.eat("x")) { - ok = stream.eatWhile(/[a-fA-F\d]/) && stream.eat(";"); - } else { - ok = stream.eatWhile(/[\d]/) && stream.eat(";"); - } - } else { - ok = stream.eatWhile(/[\w\.\-:]/) && stream.eat(";"); - } - return ok ? "atom" : "error"; - } else { - stream.eatWhile(/[^&<]/); - return null; - } - } - inText.isInText = true; - - function inTag(stream, state) { - var ch = stream.next(); - if (ch == ">" || (ch == "/" && stream.eat(">"))) { - state.tokenize = inText; - type = ch == ">" ? "endTag" : "selfcloseTag"; - return "tag bracket"; - } else if (ch == "=") { - type = "equals"; - return null; - } else if (ch == "<") { - state.tokenize = inText; - state.state = baseState; - state.tagName = state.tagStart = null; - var next = state.tokenize(stream, state); - return next ? next + " tag error" : "tag error"; - } else if (/[\'\"]/.test(ch)) { - state.tokenize = inAttribute(ch); - state.stringStartCol = stream.column(); - return state.tokenize(stream, state); - } else { - stream.match(/^[^\s\u00a0=<>\"\']*[^\s\u00a0=<>\"\'\/]/); - return "word"; - } - } - - function inAttribute(quote) { - var closure = function(stream, state) { - while (!stream.eol()) { - if (stream.next() == quote) { - state.tokenize = inTag; - break; - } - } - return "string"; - }; - closure.isInAttribute = true; - return closure; - } - - function inBlock(style, terminator) { - return function(stream, state) { - while (!stream.eol()) { - if (stream.match(terminator)) { - state.tokenize = inText; - break; - } - stream.next(); - } - return style; - } - } - - function doctype(depth) { - return function(stream, state) { - var ch; - while ((ch = stream.next()) != null) { - if (ch == "<") { - state.tokenize = doctype(depth + 1); - return state.tokenize(stream, state); - } else if (ch == ">") { - if (depth == 1) { - state.tokenize = inText; - break; - } else { - state.tokenize = doctype(depth - 1); - return state.tokenize(stream, state); - } - } - } - return "meta"; - }; - } - - function Context(state, tagName, startOfLine) { - this.prev = state.context; - this.tagName = tagName; - this.indent = state.indented; - this.startOfLine = startOfLine; - if (config.doNotIndent.hasOwnProperty(tagName) || (state.context && state.context.noIndent)) - this.noIndent = true; - } - function popContext(state) { - if (state.context) state.context = state.context.prev; - } - function maybePopContext(state, nextTagName) { - var parentTagName; - while (true) { - if (!state.context) { - return; - } - parentTagName = state.context.tagName; - if (!config.contextGrabbers.hasOwnProperty(parentTagName) || - !config.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) { - return; - } - popContext(state); - } - } - - function baseState(type, stream, state) { - if (type == "openTag") { - state.tagStart = stream.column(); - return tagNameState; - } else if (type == "closeTag") { - return closeTagNameState; - } else { - return baseState; - } - } - function tagNameState(type, stream, state) { - if (type == "word") { - state.tagName = stream.current(); - setStyle = "tag"; - return attrState; - } else if (config.allowMissingTagName && type == "endTag") { - setStyle = "tag bracket"; - return attrState(type, stream, state); - } else { - setStyle = "error"; - return tagNameState; - } - } - function closeTagNameState(type, stream, state) { - if (type == "word") { - var tagName = stream.current(); - if (state.context && state.context.tagName != tagName && - config.implicitlyClosed.hasOwnProperty(state.context.tagName)) - popContext(state); - if ((state.context && state.context.tagName == tagName) || config.matchClosing === false) { - setStyle = "tag"; - return closeState; - } else { - setStyle = "tag error"; - return closeStateErr; - } - } else if (config.allowMissingTagName && type == "endTag") { - setStyle = "tag bracket"; - return closeState(type, stream, state); - } else { - setStyle = "error"; - return closeStateErr; - } - } - - function closeState(type, _stream, state) { - if (type != "endTag") { - setStyle = "error"; - return closeState; - } - popContext(state); - return baseState; - } - function closeStateErr(type, stream, state) { - setStyle = "error"; - return closeState(type, stream, state); - } - - function attrState(type, _stream, state) { - if (type == "word") { - setStyle = "attribute"; - return attrEqState; - } else if (type == "endTag" || type == "selfcloseTag") { - var tagName = state.tagName, tagStart = state.tagStart; - state.tagName = state.tagStart = null; - if (type == "selfcloseTag" || - config.autoSelfClosers.hasOwnProperty(tagName)) { - maybePopContext(state, tagName); - } else { - maybePopContext(state, tagName); - state.context = new Context(state, tagName, tagStart == state.indented); - } - return baseState; - } - setStyle = "error"; - return attrState; - } - function attrEqState(type, stream, state) { - if (type == "equals") return attrValueState; - if (!config.allowMissing) setStyle = "error"; - return attrState(type, stream, state); - } - function attrValueState(type, stream, state) { - if (type == "string") return attrContinuedState; - if (type == "word" && config.allowUnquoted) {setStyle = "string"; return attrState;} - setStyle = "error"; - return attrState(type, stream, state); - } - function attrContinuedState(type, stream, state) { - if (type == "string") return attrContinuedState; - return attrState(type, stream, state); - } - - return { - startState: function(baseIndent) { - var state = {tokenize: inText, - state: baseState, - indented: baseIndent || 0, - tagName: null, tagStart: null, - context: null} - if (baseIndent != null) state.baseIndent = baseIndent - return state - }, - - token: function(stream, state) { - if (!state.tagName && stream.sol()) - state.indented = stream.indentation(); - - if (stream.eatSpace()) return null; - type = null; - var style = state.tokenize(stream, state); - if ((style || type) && style != "comment") { - setStyle = null; - state.state = state.state(type || style, stream, state); - if (setStyle) - style = setStyle == "error" ? style + " error" : setStyle; - } - return style; - }, - - indent: function(state, textAfter, fullLine) { - var context = state.context; - // Indent multi-line strings (e.g. css). - if (state.tokenize.isInAttribute) { - if (state.tagStart == state.indented) - return state.stringStartCol + 1; - else - return state.indented + indentUnit; - } - if (context && context.noIndent) return CodeMirror.Pass; - if (state.tokenize != inTag && state.tokenize != inText) - return fullLine ? fullLine.match(/^(\s*)/)[0].length : 0; - // Indent the starts of attribute names. - if (state.tagName) { - if (config.multilineTagIndentPastTag !== false) - return state.tagStart + state.tagName.length + 2; - else - return state.tagStart + indentUnit * (config.multilineTagIndentFactor || 1); - } - if (config.alignCDATA && /$/, - blockCommentStart: "", - - configuration: config.htmlMode ? "html" : "xml", - helperType: config.htmlMode ? "html" : "xml", - - skipAttribute: function(state) { - if (state.state == attrValueState) - state.state = attrState - }, - - xmlCurrentTag: function(state) { - return state.tagName ? {name: state.tagName, close: state.type == "closeTag"} : null - }, - - xmlCurrentContext: function(state) { - var context = [] - for (var cx = state.context; cx; cx = cx.prev) - if (cx.tagName) context.push(cx.tagName) - return context.reverse() - } - }; -}); - -CodeMirror.defineMIME("text/xml", "xml"); -CodeMirror.defineMIME("application/xml", "xml"); -if (!CodeMirror.mimeModes.hasOwnProperty("text/html")) - CodeMirror.defineMIME("text/html", {name: "xml", htmlMode: true}); - -}); diff --git a/plugins/UiFileManager/media/css/Menu.css b/plugins/UiFileManager/media/css/Menu.css deleted file mode 100644 index b61b8be6..00000000 --- a/plugins/UiFileManager/media/css/Menu.css +++ /dev/null @@ -1,33 +0,0 @@ -.menu { - background-color: white; padding: 10px 0px; position: absolute; top: 0px; max-height: 0px; overflow: hidden; transform: translate(-100%, -30px); pointer-events: none; - box-shadow: 0px 2px 8px rgba(0,0,0,0.3); border-radius: 2px; opacity: 0; transition: opacity 0.2s ease-out, transform 1s ease-out, max-height 0.2s ease-in-out; z-index: 99; - display: inline-block; z-index: 999; transform-style: preserve-3d; -} -.menu.menu-left { transform: translate(0%, -30px); } -.menu.menu-left.visible { transform: translate(0%, 0px); } -.menu.visible { - opacity: 1; transform: translate(-100%, 0px); pointer-events: all; - transition: opacity 0.1s ease-out, transform 0.3s ease-out, max-height 0.3s cubic-bezier(0.86, 0, 0.07, 1); -} - -.menu-item { - display: block; text-decoration: none; color: black; padding: 6px 24px; transition: all 0.2s; border-bottom: none; font-weight: normal; - max-height: 150px; overflow: hidden; text-overflow: ellipsis; -webkit-line-clamp: 6; -webkit-box-orient: vertical; display: -webkit-box; -} -.menu-item-separator { margin-top: 3px; margin-bottom: 3px; border-top: 1px solid #eee } - -.menu-item.noaction { cursor: default } -.menu-item:hover:not(.noaction) { background-color: #F6F6F6; transition: none; color: inherit; cursor: pointer; color: black } -.menu-item:active:not(.noaction), .menu-item:focus:not(.noaction) { background-color: #AF3BFF !important; color: white !important; transition: none } -.menu-item.selected:before { - content: "L"; display: inline-block; transform: rotateZ(45deg) scaleX(-1); - font-weight: bold; position: absolute; margin-left: -14px; font-size: 12px; margin-top: 2px; -} - -.menu-radio { white-space: normal; line-height: 26px } -.menu-radio a { - background-color: #EEE; width: 18.5%;; text-align: center; margin-top: 2px; margin-bottom: 2px; color: #666; font-weight: bold; - text-decoration: none; font-size: 13px; transition: all 0.3s; text-transform: uppercase; display: inline-block; -} -.menu-radio a:hover, .menu-radio a.selected { transition: none; background-color: #AF3BFF !important; color: white !important } -.menu-radio a.long { font-size: 10px; vertical-align: -1px; } diff --git a/plugins/UiFileManager/media/css/Selectbar.css b/plugins/UiFileManager/media/css/Selectbar.css deleted file mode 100644 index ceaf72e0..00000000 --- a/plugins/UiFileManager/media/css/Selectbar.css +++ /dev/null @@ -1,17 +0,0 @@ -.selectbar.visible { margin-top: 0px; visibility: visible } -.selectbar { - position: fixed; top: 0; left: 0; background-color: white; box-shadow: 0px 0px 25px rgba(22, 39, 97, 0.2); margin-top: -75px; - transition: all 0.3s; visibility: hidden; z-index: 9999; color: black; border-left: 5px solid #ede1f582; width: 100%; - padding: 13px; font-size: 13px; font-weight: lighter; backface-visibility: hidden; -} - -.selectbar .num { margin-left: 15px; min-width: 30px; text-align: right; display: inline-block; } -.selectbar .size { margin-left: 10px; color: #9f9ba2; min-width: 75px; display: inline-block; } -.selectbar .actions { display: inline-block; margin-left: 20px; font-size: 13px; text-transform: uppercase; line-height: 20px; } -.selectbar .action { padding: 5px 20px; border: 1px solid #edd4ff; margin-left: 10px; border-radius: 30px; color: #af3bff; text-decoration: none; transition: all 0.3s } -.selectbar .action:hover { border-color: #c788f3; transition: none; color: #9700ff } -.selectbar .delete { color: #AAA; border-color: #DDD; } -.selectbar .delete:hover { color: #333; border-color: #AAA } -.selectbar .action:active { background-color: #af3bff; color: white; border-color: #af3bff; transition: none } -.selectbar .cancel { margin: 20px; font-size: 10px; text-decoration: none; color: #999; text-transform: uppercase; } -.selectbar .cancel:hover { color: #333; transition: none } \ No newline at end of file diff --git a/plugins/UiFileManager/media/css/UiFileManager.css b/plugins/UiFileManager/media/css/UiFileManager.css deleted file mode 100644 index be884bcb..00000000 --- a/plugins/UiFileManager/media/css/UiFileManager.css +++ /dev/null @@ -1,148 +0,0 @@ -body { background-color: #EEEEF5; font-family: "Segoe UI", Helvetica, Arial; height: 95000px; overflow: hidden; } -body.loaded { height: auto; overflow: auto } -h1 { font-weight: lighter; } - -a { color: #333 } -a:hover { text-decoration: none } -input::placeholder { color: rgba(255, 255, 255, 0.3) } - -h2 { font-weight: lighter; } - -.link { background-color: transparent; outline: 5px solid transparent; transition: all 0.3s } -.link:active { background-color: #fbf5ff; outline: 5px solid #fbf5ff; transition: none } - -.manager.editing .files { float: left; width: 280px; } - -.sidebar-button { - display: inline-block; padding: 25px 19px; text-decoration: none; position: absolute; - border-right: 1px solid #EEE; line-height: 10px; color: #7801F5; transition: all 0.3s -} -.sidebar-button:active { background-color: #f5e7ff; transition: none } -/*.sidebar-button:hover { background-color: #fbf5ff; }*/ -.sidebar-button span { transition: 1s all; transform-origin: 2.5px 7px; display: inline-block; } -.manager.sidebar_closed .sidebar-button span { transform: rotateZ(180deg); } -.manager.sidebar_closed .files { margin-left: -300px; } -.manager.sidebar_closed .editor { width: 100%; } - -.button { - padding: 5px 10px; margin-left: 10px; background-color: #752ff2; border-bottom: 2px solid #caadff; background-position: -50px center; - border-radius: 2px; text-decoration: none; transition: all 0.5s ease-out; display: inline-block; - color: #333; font-size: 12px; vertical-align: 2px; text-transform: uppercase; color: white; max-width: 100px; -} -.button:hover { background-color: #9e71ed; transition: none; } -.button:active { position: relative; top: 1px } -.button.loading, .button.disabled { color: rgba(255,255,255,0.7);; pointer-events: none; border-bottom: 2px solid #666; background-color: #999; } -.button.loading { background: #999 url(../img/loading.gif) no-repeat center center; transition: all 0.5s ease-out; color: rgba(0,0,0,0); } -.button.done { background-color: #4dc758; transition: all 0.3s; border-color: #4dc758; pointer-events: none; } -.button.hidden { max-width: 0px; display: inline-block; padding-left: 0px; padding-right: 0px; margin: 0px; } - -/* List */ - -.files { - width: 97%; box-sizing: border-box; color: #555; position: relative; z-index: 1; transition: all 0.6s; - font-size: 14px; box-shadow: 0px 9px 20px -15px #a5cbec; max-width: 400px; border: 1px solid #EEEEF5; -} -.files .tr { white-space: nowrap } -.files .td { display: inline-block; width: 60px } -.files .tbody .td { line-height: 18px; vertical-align: bottom; } -.files .td.name { min-width: 100px } -.files .td.size { width: 60px; text-align: right; padding-left: 5px; } -.files .td.status { text-align: right; } -.files .td.peer { width: 60px } -.files .td.uploaded { width: 130px; text-align: right; } -.files .td.added { width: 90px } -.files .orderby { color: inherit; text-decoration: none; transition: all 0.3s; outline: 5px solid transparent; } -.files .orderby:hover { text-decoration: underline; } -.files .orderby .icon-arrow-down { opacity: 0; transition: all 0.3s ease-in-out; } -.files .orderby.selected .icon-arrow-down { opacity: 0.3; } -.files .orderby:active { background-color: rgba(133, 239, 255, 0.09); outline: 5px solid rgba(133, 239, 255, 0.09); transition: none; } -.files .orderby:hover .icon-arrow-down { opacity: 0.5; } -.files .orderby:not(.desc) .icon-arrow-down { transform: rotateZ(180deg); } -.files .tr.editing .td { background-color: #ede1f582; border-top-color: #ece9ef; } -.files .thead { /*background: linear-gradient(358deg, #e7f1f7, #e9f2f72e);*/ } -.files .thead .td { - border-top: none; color: #8984c2; background-color: #f7f7fc; - font-size: 12px; /*text-transform: uppercase; background-color: transparent; font-weight: bold;*/ -} -.files .thead .td a:last-of-type { font-weight: bold; } -.files .thead .td a { text-decoration: none; } -.files .thead .td a:hover { text-decoration: underline; } -.files .tbody { max-height: calc(100vh - 95px); overflow-y: auto; overflow-x: hidden; } -.files .tr { background-color: white; } -.files .td { padding: 10px 20px; border-top: 1px solid #EEE; font-size: 13px; white-space: nowrap; } -.files .td.full { width: 100%; box-sizing: border-box; white-space: pre-line; } -.files .td.pre { width: 0px; color: transparent; padding-left: 0px; border-left: 2px solid transparent; } -.files .tbody .td { height: 18px; } -.files .tbody .td.full { height: auto; } -.files .td.pre .checkbox-outer { opacity: 0.6; margin-left: -11px; margin-top: -15px; width: 18px; height: 12px; display: inline-block; } -.files .tr.modified .td.pre { border-left-color: #7801F5 } -.files .tr.added .td.pre { border-left-color: #00ec93 } -.files .tr.ignored .td.pre { border-left-color: #999; } -.files .tr.ignored { opacity: 0.5; } -.files .tr.optional { background: linear-gradient(90deg, #fff6dd, 30%, white, 10%, white); } -.files .tr.optional_empty { color: #999; font-style: italic; } -.files .td.error { background-color: #F44336; color: white; } -.files .td.site { width: 70px } -.files .td.site .link { color: inherit; text-decoration: none } -.files .td.status .percent { - transition: all 1s ease-in-out; display: inline-block; width: 80px; background-color: #EEE; font-size: 10px; - height: 15px; line-height: 15px; text-align: center; margin-right: 20px; -} -.files .td.name { padding-left: 10px; width: calc(100% - 167px); max-height: 18px; padding-right: 10px; } -.files .tr.nobuttons .td.name { width: calc(100% - 127px); } -.files .tr.nobuttons .td.buttons { width: 0px; } -.files .td.name .title { color: inherit; text-decoration: none } -.files .td.name .link { display: inline-block; overflow: hidden; text-overflow: ellipsis; vertical-align: -4px; max-width: 100%; } -.files .pinned .td.name .link { max-width: calc(100% - 40px); } -.files .thead .td.uploaded { text-align: left } -.files .thead .td.uploaded .title { padding-left: 7px; } -.files .peer .icon-profile { background: currentColor; color: #47d094; font-size: 10px; top: 1px; margin-right: 13px } -.files .peer .icon-profile:before { background: currentColor } -.files .peer .num { color: #969696; } -.files .uploaded .uploaded-text { display: inline-block; text-align: right; } -.files .uploaded .dots-container { display: inline-block; width: 0px; padding-right: 65px;; } -.files .td.buttons { width: 40px; padding-left: 0px; padding-right: 0px; } -.files .td.buttons .edit { - background-color: #2196f336; border-radius: 15px; padding: 1px 9px; font-size: 80%; text-decoration: none; color: #1976D2; -} -.files .checkbox-outer { padding: 15px; padding-left: 20px; padding-right: 0px; } -.files .checkbox { - display: inline-block; width: 12px; height: 12px; border: 2px solid #00000014; - border-radius: 3px; vertical-align: -3px; margin-right: 10px; -} -.files .selected .checkbox { border-color: #dedede } -.files .selected .checkbox:after { - background-color: #dedede; content: ""; text-decoration: none; display: block; width: 10px; height: 10px; margin-left: 1px; margin-top: 1px; -} -.files .tbody .td.size { font-size: 13px } -.files .tbody .td.added, #PageFiles .files .td.access { font-size: 12px; color: #999 } -.files .tr.type-dir .name { font-weight: bold; } -.files .tr.type-parent .name .link { display: inline-block; width: 100%; padding: 5px; margin-top: -5px; } -.files .foot .td { color: #a4a4a4; background-color: #f7f7fc; } -.files .foot .create { float: right; text-decoration: none; position: relative; } -.files .foot .create .link { color: #8c42ed; text-decoration: none; } -.files .foot .create .link:active { background-color: #8c42ed3b; outline: 5px solid #8c42ed3b; } -.files .foot .create .menu { top: 40px; } - - -/* Editor */ - -.editor { background-color: #F7F7FC; float: left; width: calc(100% - 280px); box-sizing: border-box; transition: all 0.6s; } -.editor .CodeMirror { height: calc(100vh - 79px); visibility: hidden; } -.editor textarea { width: 100%; height: 800px; white-space: pre; } -.editor .title { margin-left: 20px; } -.editor .editor-head { - padding: 15px 20px; padding-left: 45px; font-size: 18px; font-weight: lighter; border: 1px solid #EEEEF5; - white-space: nowrap; overflow: hidden; -} -.editor.loaded .CodeMirror { visibility: inherit; } -.editor.error .CodeMirror { display: none; } -.editor .button.save { min-width: 30px; text-align: center; transition: all 0.3s; } -.editor .button.save.done { min-width: 80px; } -.editor .error-message { text-align: center; padding: 50px; } - -.editor .CodeMirror-foldmarker { - line-height: .3; cursor: pointer; background-color: #ffeb3b61; text-shadow: none; font-family: inherit; - color: #050505; border: 1px solid #ffdf7f; padding: 0px 5px; -} -.editor .CodeMirror-activeline-background { background-color: #F6F6F6 !important; } \ No newline at end of file diff --git a/plugins/UiFileManager/media/css/all.css b/plugins/UiFileManager/media/css/all.css deleted file mode 100644 index 2ff099c8..00000000 --- a/plugins/UiFileManager/media/css/all.css +++ /dev/null @@ -1,211 +0,0 @@ - -/* ---- Menu.css ---- */ - - -.menu { - background-color: white; padding: 10px 0px; position: absolute; top: 0px; max-height: 0px; overflow: hidden; -webkit-transform: translate(-100%, -30px); -moz-transform: translate(-100%, -30px); -o-transform: translate(-100%, -30px); -ms-transform: translate(-100%, -30px); transform: translate(-100%, -30px) ; pointer-events: none; - -webkit-box-shadow: 0px 2px 8px rgba(0,0,0,0.3); -moz-box-shadow: 0px 2px 8px rgba(0,0,0,0.3); -o-box-shadow: 0px 2px 8px rgba(0,0,0,0.3); -ms-box-shadow: 0px 2px 8px rgba(0,0,0,0.3); box-shadow: 0px 2px 8px rgba(0,0,0,0.3) ; -webkit-border-radius: 2px; -moz-border-radius: 2px; -o-border-radius: 2px; -ms-border-radius: 2px; border-radius: 2px ; opacity: 0; -webkit-transition: opacity 0.2s ease-out, transform 1s ease-out, max-height 0.2s ease-in-out; -moz-transition: opacity 0.2s ease-out, transform 1s ease-out, max-height 0.2s ease-in-out; -o-transition: opacity 0.2s ease-out, transform 1s ease-out, max-height 0.2s ease-in-out; -ms-transition: opacity 0.2s ease-out, transform 1s ease-out, max-height 0.2s ease-in-out; transition: opacity 0.2s ease-out, transform 1s ease-out, max-height 0.2s ease-in-out ; z-index: 99; - display: inline-block; z-index: 999; transform-style: preserve-3d; -} -.menu.menu-left { -webkit-transform: translate(0%, -30px); -moz-transform: translate(0%, -30px); -o-transform: translate(0%, -30px); -ms-transform: translate(0%, -30px); transform: translate(0%, -30px) ; } -.menu.menu-left.visible { -webkit-transform: translate(0%, 0px); -moz-transform: translate(0%, 0px); -o-transform: translate(0%, 0px); -ms-transform: translate(0%, 0px); transform: translate(0%, 0px) ; } -.menu.visible { - opacity: 1; -webkit-transform: translate(-100%, 0px); -moz-transform: translate(-100%, 0px); -o-transform: translate(-100%, 0px); -ms-transform: translate(-100%, 0px); transform: translate(-100%, 0px) ; pointer-events: all; - -webkit-transition: opacity 0.1s ease-out, transform 0.3s ease-out, max-height 0.3s cubic-bezier(0.86, 0, 0.07, 1); -moz-transition: opacity 0.1s ease-out, transform 0.3s ease-out, max-height 0.3s cubic-bezier(0.86, 0, 0.07, 1); -o-transition: opacity 0.1s ease-out, transform 0.3s ease-out, max-height 0.3s cubic-bezier(0.86, 0, 0.07, 1); -ms-transition: opacity 0.1s ease-out, transform 0.3s ease-out, max-height 0.3s cubic-bezier(0.86, 0, 0.07, 1); transition: opacity 0.1s ease-out, transform 0.3s ease-out, max-height 0.3s cubic-bezier(0.86, 0, 0.07, 1) ; -} - -.menu-item { - display: block; text-decoration: none; color: black; padding: 6px 24px; -webkit-transition: all 0.2s; -moz-transition: all 0.2s; -o-transition: all 0.2s; -ms-transition: all 0.2s; transition: all 0.2s ; border-bottom: none; font-weight: normal; - max-height: 150px; overflow: hidden; text-overflow: ellipsis; -webkit-line-clamp: 6; -webkit-box-orient: vertical; display: -webkit-box; -} -.menu-item-separator { margin-top: 3px; margin-bottom: 3px; border-top: 1px solid #eee } - -.menu-item.noaction { cursor: default } -.menu-item:hover:not(.noaction) { background-color: #F6F6F6; -webkit-transition: none; -moz-transition: none; -o-transition: none; -ms-transition: none; transition: none ; color: inherit; cursor: pointer; color: black } -.menu-item:active:not(.noaction), .menu-item:focus:not(.noaction) { background-color: #AF3BFF !important; color: white !important; -webkit-transition: none ; -moz-transition: none ; -o-transition: none ; -ms-transition: none ; transition: none } -.menu-item.selected:before { - content: "L"; display: inline-block; -webkit-transform: rotateZ(45deg) scaleX(-1); -moz-transform: rotateZ(45deg) scaleX(-1); -o-transform: rotateZ(45deg) scaleX(-1); -ms-transform: rotateZ(45deg) scaleX(-1); transform: rotateZ(45deg) scaleX(-1) ; - font-weight: bold; position: absolute; margin-left: -14px; font-size: 12px; margin-top: 2px; -} - -.menu-radio { white-space: normal; line-height: 26px } -.menu-radio a { - background-color: #EEE; width: 18.5%;; text-align: center; margin-top: 2px; margin-bottom: 2px; color: #666; font-weight: bold; - text-decoration: none; font-size: 13px; -webkit-transition: all 0.3s; -moz-transition: all 0.3s; -o-transition: all 0.3s; -ms-transition: all 0.3s; transition: all 0.3s ; text-transform: uppercase; display: inline-block; -} -.menu-radio a:hover, .menu-radio a.selected { -webkit-transition: none; -moz-transition: none; -o-transition: none; -ms-transition: none; transition: none ; background-color: #AF3BFF !important; color: white !important } -.menu-radio a.long { font-size: 10px; vertical-align: -1px; } - - -/* ---- Selectbar.css ---- */ - - -.selectbar.visible { margin-top: 0px; visibility: visible } -.selectbar { - position: fixed; top: 0; left: 0; background-color: white; -webkit-box-shadow: 0px 0px 25px rgba(22, 39, 97, 0.2); -moz-box-shadow: 0px 0px 25px rgba(22, 39, 97, 0.2); -o-box-shadow: 0px 0px 25px rgba(22, 39, 97, 0.2); -ms-box-shadow: 0px 0px 25px rgba(22, 39, 97, 0.2); box-shadow: 0px 0px 25px rgba(22, 39, 97, 0.2) ; margin-top: -75px; - -webkit-transition: all 0.3s; -moz-transition: all 0.3s; -o-transition: all 0.3s; -ms-transition: all 0.3s; transition: all 0.3s ; visibility: hidden; z-index: 9999; color: black; border-left: 5px solid #ede1f582; width: 100%; - padding: 13px; font-size: 13px; font-weight: lighter; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden ; -} - -.selectbar .num { margin-left: 15px; min-width: 30px; text-align: right; display: inline-block; } -.selectbar .size { margin-left: 10px; color: #9f9ba2; min-width: 75px; display: inline-block; } -.selectbar .actions { display: inline-block; margin-left: 20px; font-size: 13px; text-transform: uppercase; line-height: 20px; } -.selectbar .action { padding: 5px 20px; border: 1px solid #edd4ff; margin-left: 10px; -webkit-border-radius: 30px; -moz-border-radius: 30px; -o-border-radius: 30px; -ms-border-radius: 30px; border-radius: 30px ; color: #af3bff; text-decoration: none; -webkit-transition: all 0.3s ; -moz-transition: all 0.3s ; -o-transition: all 0.3s ; -ms-transition: all 0.3s ; transition: all 0.3s } -.selectbar .action:hover { border-color: #c788f3; -webkit-transition: none; -moz-transition: none; -o-transition: none; -ms-transition: none; transition: none ; color: #9700ff } -.selectbar .delete { color: #AAA; border-color: #DDD; } -.selectbar .delete:hover { color: #333; border-color: #AAA } -.selectbar .action:active { background-color: #af3bff; color: white; border-color: #af3bff; -webkit-transition: none ; -moz-transition: none ; -o-transition: none ; -ms-transition: none ; transition: none } -.selectbar .cancel { margin: 20px; font-size: 10px; text-decoration: none; color: #999; text-transform: uppercase; } -.selectbar .cancel:hover { color: #333; -webkit-transition: none ; -moz-transition: none ; -o-transition: none ; -ms-transition: none ; transition: none } - -/* ---- UiFileManager.css ---- */ - - -body { background-color: #EEEEF5; font-family: "Segoe UI", Helvetica, Arial; height: 95000px; overflow: hidden; } -body.loaded { height: auto; overflow: auto } -h1 { font-weight: lighter; } - -a { color: #333 } -a:hover { text-decoration: none } -input::placeholder { color: rgba(255, 255, 255, 0.3) } - -h2 { font-weight: lighter; } - -.link { background-color: transparent; outline: 5px solid transparent; -webkit-transition: all 0.3s ; -moz-transition: all 0.3s ; -o-transition: all 0.3s ; -ms-transition: all 0.3s ; transition: all 0.3s } -.link:active { background-color: #fbf5ff; outline: 5px solid #fbf5ff; -webkit-transition: none ; -moz-transition: none ; -o-transition: none ; -ms-transition: none ; transition: none } - -.manager.editing .files { float: left; width: 280px; } - -.sidebar-button { - display: inline-block; padding: 25px 19px; text-decoration: none; position: absolute; - border-right: 1px solid #EEE; line-height: 10px; color: #7801F5; transition: all 0.3s -} -.sidebar-button:active { background-color: #f5e7ff; -webkit-transition: none ; -moz-transition: none ; -o-transition: none ; -ms-transition: none ; transition: none } -/*.sidebar-button:hover { background-color: #fbf5ff; }*/ -.sidebar-button span { -webkit-transition: 1s all; -moz-transition: 1s all; -o-transition: 1s all; -ms-transition: 1s all; transition: 1s all ; transform-origin: 2.5px 7px; display: inline-block; } -.manager.sidebar_closed .sidebar-button span { -webkit-transform: rotateZ(180deg); -moz-transform: rotateZ(180deg); -o-transform: rotateZ(180deg); -ms-transform: rotateZ(180deg); transform: rotateZ(180deg) ; } -.manager.sidebar_closed .files { margin-left: -300px; } -.manager.sidebar_closed .editor { width: 100%; } - -.button { - padding: 5px 10px; margin-left: 10px; background-color: #752ff2; border-bottom: 2px solid #caadff; background-position: -50px center; - -webkit-border-radius: 2px; -moz-border-radius: 2px; -o-border-radius: 2px; -ms-border-radius: 2px; border-radius: 2px ; text-decoration: none; -webkit-transition: all 0.5s ease-out; -moz-transition: all 0.5s ease-out; -o-transition: all 0.5s ease-out; -ms-transition: all 0.5s ease-out; transition: all 0.5s ease-out ; display: inline-block; - color: #333; font-size: 12px; vertical-align: 2px; text-transform: uppercase; color: white; max-width: 100px; -} -.button:hover { background-color: #9e71ed; -webkit-transition: none; -moz-transition: none; -o-transition: none; -ms-transition: none; transition: none ; } -.button:active { position: relative; top: 1px } -.button.loading, .button.disabled { color: rgba(255,255,255,0.7);; pointer-events: none; border-bottom: 2px solid #666; background-color: #999; } -.button.loading { background: #999 url(../img/loading.gif) no-repeat center center; -webkit-transition: all 0.5s ease-out; -moz-transition: all 0.5s ease-out; -o-transition: all 0.5s ease-out; -ms-transition: all 0.5s ease-out; transition: all 0.5s ease-out ; color: rgba(0,0,0,0); } -.button.done { background-color: #4dc758; -webkit-transition: all 0.3s; -moz-transition: all 0.3s; -o-transition: all 0.3s; -ms-transition: all 0.3s; transition: all 0.3s ; border-color: #4dc758; pointer-events: none; } -.button.hidden { max-width: 0px; display: inline-block; padding-left: 0px; padding-right: 0px; margin: 0px; } - -/* List */ - -.files { - width: 97%; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; color: #555; position: relative; z-index: 1; -webkit-transition: all 0.6s; -moz-transition: all 0.6s; -o-transition: all 0.6s; -ms-transition: all 0.6s; transition: all 0.6s ; - font-size: 14px; -webkit-box-shadow: 0px 9px 20px -15px #a5cbec; -moz-box-shadow: 0px 9px 20px -15px #a5cbec; -o-box-shadow: 0px 9px 20px -15px #a5cbec; -ms-box-shadow: 0px 9px 20px -15px #a5cbec; box-shadow: 0px 9px 20px -15px #a5cbec ; max-width: 400px; border: 1px solid #EEEEF5; -} -.files .tr { white-space: nowrap } -.files .td { display: inline-block; width: 60px } -.files .tbody .td { line-height: 18px; vertical-align: bottom; } -.files .td.name { min-width: 100px } -.files .td.size { width: 60px; text-align: right; padding-left: 5px; } -.files .td.status { text-align: right; } -.files .td.peer { width: 60px } -.files .td.uploaded { width: 130px; text-align: right; } -.files .td.added { width: 90px } -.files .orderby { color: inherit; text-decoration: none; -webkit-transition: all 0.3s; -moz-transition: all 0.3s; -o-transition: all 0.3s; -ms-transition: all 0.3s; transition: all 0.3s ; outline: 5px solid transparent; } -.files .orderby:hover { text-decoration: underline; } -.files .orderby .icon-arrow-down { opacity: 0; -webkit-transition: all 0.3s ease-in-out; -moz-transition: all 0.3s ease-in-out; -o-transition: all 0.3s ease-in-out; -ms-transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out ; } -.files .orderby.selected .icon-arrow-down { opacity: 0.3; } -.files .orderby:active { background-color: rgba(133, 239, 255, 0.09); outline: 5px solid rgba(133, 239, 255, 0.09); -webkit-transition: none; -moz-transition: none; -o-transition: none; -ms-transition: none; transition: none ; } -.files .orderby:hover .icon-arrow-down { opacity: 0.5; } -.files .orderby:not(.desc) .icon-arrow-down { -webkit-transform: rotateZ(180deg); -moz-transform: rotateZ(180deg); -o-transform: rotateZ(180deg); -ms-transform: rotateZ(180deg); transform: rotateZ(180deg) ; } -.files .tr.editing .td { background-color: #ede1f582; border-top-color: #ece9ef; } -.files .thead { /*background: -webkit-linear-gradient(358deg, #e7f1f7, #e9f2f72e);background: -moz-linear-gradient(358deg, #e7f1f7, #e9f2f72e);background: -o-linear-gradient(358deg, #e7f1f7, #e9f2f72e);background: -ms-linear-gradient(358deg, #e7f1f7, #e9f2f72e);background: linear-gradient(358deg, #e7f1f7, #e9f2f72e);*/ } -.files .thead .td { - border-top: none; color: #8984c2; background-color: #f7f7fc; - font-size: 12px; /*text-transform: uppercase; background-color: transparent; font-weight: bold;*/ -} -.files .thead .td a:last-of-type { font-weight: bold; } -.files .thead .td a { text-decoration: none; } -.files .thead .td a:hover { text-decoration: underline; } -.files .tbody { max-height: calc(100vh - 95px); overflow-y: auto; overflow-x: hidden; } -.files .tr { background-color: white; } -.files .td { padding: 10px 20px; border-top: 1px solid #EEE; font-size: 13px; white-space: nowrap; } -.files .td.full { width: 100%; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; white-space: pre-line; } -.files .td.pre { width: 0px; color: transparent; padding-left: 0px; border-left: 2px solid transparent; } -.files .tbody .td { height: 18px; } -.files .tbody .td.full { height: auto; } -.files .td.pre .checkbox-outer { opacity: 0.6; margin-left: -11px; margin-top: -15px; width: 18px; height: 12px; display: inline-block; } -.files .tr.modified .td.pre { border-left-color: #7801F5 } -.files .tr.added .td.pre { border-left-color: #00ec93 } -.files .tr.ignored .td.pre { border-left-color: #999; } -.files .tr.ignored { opacity: 0.5; } -.files .tr.optional { background: -webkit-linear-gradient(90deg, #fff6dd, 30%, white, 10%, white);background: -moz-linear-gradient(90deg, #fff6dd, 30%, white, 10%, white);background: -o-linear-gradient(90deg, #fff6dd, 30%, white, 10%, white);background: -ms-linear-gradient(90deg, #fff6dd, 30%, white, 10%, white);background: linear-gradient(90deg, #fff6dd, 30%, white, 10%, white); } -.files .tr.optional_empty { color: #999; font-style: italic; } -.files .td.error { background-color: #F44336; color: white; } -.files .td.site { width: 70px } -.files .td.site .link { color: inherit; text-decoration: none } -.files .td.status .percent { - -webkit-transition: all 1s ease-in-out; -moz-transition: all 1s ease-in-out; -o-transition: all 1s ease-in-out; -ms-transition: all 1s ease-in-out; transition: all 1s ease-in-out ; display: inline-block; width: 80px; background-color: #EEE; font-size: 10px; - height: 15px; line-height: 15px; text-align: center; margin-right: 20px; -} -.files .td.name { padding-left: 10px; width: calc(100% - 167px); max-height: 18px; padding-right: 10px; } -.files .tr.nobuttons .td.name { width: calc(100% - 127px); } -.files .tr.nobuttons .td.buttons { width: 0px; } -.files .td.name .title { color: inherit; text-decoration: none } -.files .td.name .link { display: inline-block; overflow: hidden; text-overflow: ellipsis; vertical-align: -4px; max-width: 100%; } -.files .pinned .td.name .link { max-width: calc(100% - 40px); } -.files .thead .td.uploaded { text-align: left } -.files .thead .td.uploaded .title { padding-left: 7px; } -.files .peer .icon-profile { background: currentColor; color: #47d094; font-size: 10px; top: 1px; margin-right: 13px } -.files .peer .icon-profile:before { background: currentColor } -.files .peer .num { color: #969696; } -.files .uploaded .uploaded-text { display: inline-block; text-align: right; } -.files .uploaded .dots-container { display: inline-block; width: 0px; padding-right: 65px;; } -.files .td.buttons { width: 40px; padding-left: 0px; padding-right: 0px; } -.files .td.buttons .edit { - background-color: #2196f336; -webkit-border-radius: 15px; -moz-border-radius: 15px; -o-border-radius: 15px; -ms-border-radius: 15px; border-radius: 15px ; padding: 1px 9px; font-size: 80%; text-decoration: none; color: #1976D2; -} -.files .checkbox-outer { padding: 15px; padding-left: 20px; padding-right: 0px; } -.files .checkbox { - display: inline-block; width: 12px; height: 12px; border: 2px solid #00000014; - -webkit-border-radius: 3px; -moz-border-radius: 3px; -o-border-radius: 3px; -ms-border-radius: 3px; border-radius: 3px ; vertical-align: -3px; margin-right: 10px; -} -.files .selected .checkbox { border-color: #dedede } -.files .selected .checkbox:after { - background-color: #dedede; content: ""; text-decoration: none; display: block; width: 10px; height: 10px; margin-left: 1px; margin-top: 1px; -} -.files .tbody .td.size { font-size: 13px } -.files .tbody .td.added, #PageFiles .files .td.access { font-size: 12px; color: #999 } -.files .tr.type-dir .name { font-weight: bold; } -.files .tr.type-parent .name .link { display: inline-block; width: 100%; padding: 5px; margin-top: -5px; } -.files .foot .td { color: #a4a4a4; background-color: #f7f7fc; } -.files .foot .create { float: right; text-decoration: none; position: relative; } -.files .foot .create .link { color: #8c42ed; text-decoration: none; } -.files .foot .create .link:active { background-color: #8c42ed3b; outline: 5px solid #8c42ed3b; } -.files .foot .create .menu { top: 40px; } - - -/* Editor */ - -.editor { background-color: #F7F7FC; float: left; width: calc(100% - 280px); -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; -webkit-transition: all 0.6s; -moz-transition: all 0.6s; -o-transition: all 0.6s; -ms-transition: all 0.6s; transition: all 0.6s ; } -.editor .CodeMirror { height: calc(100vh - 79px); visibility: hidden; } -.editor textarea { width: 100%; height: 800px; white-space: pre; } -.editor .title { margin-left: 20px; } -.editor .editor-head { - padding: 15px 20px; padding-left: 45px; font-size: 18px; font-weight: lighter; border: 1px solid #EEEEF5; - white-space: nowrap; overflow: hidden; -} -.editor.loaded .CodeMirror { visibility: inherit; } -.editor.error .CodeMirror { display: none; } -.editor .button.save { min-width: 30px; text-align: center; -webkit-transition: all 0.3s; -moz-transition: all 0.3s; -o-transition: all 0.3s; -ms-transition: all 0.3s; transition: all 0.3s ; } -.editor .button.save.done { min-width: 80px; } -.editor .error-message { text-align: center; padding: 50px; } - -.editor .CodeMirror-foldmarker { - line-height: .3; cursor: pointer; background-color: #ffeb3b61; text-shadow: none; font-family: inherit; - color: #050505; border: 1px solid #ffdf7f; padding: 0px 5px; -} -.editor .CodeMirror-activeline-background { background-color: #F6F6F6 !important; } \ No newline at end of file diff --git a/plugins/UiFileManager/media/img/loading.gif b/plugins/UiFileManager/media/img/loading.gif deleted file mode 100644 index 27d0aa8108b0800f9cddcf613f787347d9981e05..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 723 zcmZ?wbhEHb6ky0iXa9k4z>53@HBR_Hzvhc6JPKHPSO+W(0~W{*!Vp zN=+!e)X>LZSp~!n_rkGVK%h z9k_L9<(o^(d!N7A`+9eTzQ!EZMr*-N2_|eB&45;SC+a-zP~lXP;z?eTv`FKm^!Y8l zuZ^S*OlLmOv^VZNyYQx zE1Q1d^QD!~t!MErXFkzlm$bqCmuUZ)iN%&IQkAQ(b??%e8>EQMBqK<8T-y}!%q4L0 z4v$MoL7}cEx5PfOihDclHe=f1_`ny+jJ+qGonTF#=e6?cS1GK1Glv+XQW)E^VpGzx z%$u!=(=#3~+Lk*jmQUf$-=^(}f)AMWru(Y&&oE(%*JUs>JH24vgCGuUPSS^%^#tgi z6`S6zDw0tR+QR$5bp7w`G6mDQzjYm%RoE)?D^8cegv~i}{SvGJM6wyypd - @need_update = true - @on_loaded = new Promise() - @is_loading = false - @content = "" - @node_cm = null - @cm = null - @error = null - @is_loaded = false - @is_modified = false - @is_saving = false - @mode = "Loading" - - update: -> - is_required = Page.url_params.get("edit_mode") != "new" - - Page.cmd "fileGet", {inner_path: @inner_path, required: is_required}, (res) => - if res?.error - @error = res.error - @content = res.error - @log "Error loading: #{@error}" - else - if res - @content = res - else - @content = "" - @mode = "Create" - if not @content - @cm.getDoc().clearHistory() - @cm.setValue(@content) - if not @error - @is_loaded = true - Page.projector.scheduleRender() - - isModified: => - return @content != @cm.getValue() - - storeCmNode: (node) => - @node_cm = node - - getMode: (inner_path) -> - ext = inner_path.split(".").pop() - types = { - "py": "python", - "json": "application/json", - "js": "javascript", - "coffee": "coffeescript", - "html": "htmlmixed", - "htm": "htmlmixed", - "php": "htmlmixed", - "rs": "rust", - "css": "css", - "md": "markdown", - "xml": "xml", - "svg": "xml" - } - return types[ext] - - foldJson: (from, to) => - @log "foldJson", from, to - # Get open / close token - startToken = '{' - endToken = '}' - prevLine = @cm.getLine(from.line) - if prevLine.lastIndexOf('[') > prevLine.lastIndexOf('{') - startToken = '[' - endToken = ']' - - # Get json content - internal = @cm.getRange(from, to) - toParse = startToken + internal + endToken - - #Get key count - try - parsed = JSON.parse(toParse) - count = Object.keys(parsed).length - catch e - null - - return if count then "\u21A4#{count}\u21A6" else "\u2194" - - createCodeMirror: -> - mode = @getMode(@inner_path) - @log "Creating CodeMirror", @inner_path, mode - options = { - value: "Loading...", - mode: mode, - lineNumbers: true, - styleActiveLine: true, - matchBrackets: true, - keyMap: "sublime", - theme: "mdn-like", - extraKeys: {"Ctrl-Space": "autocomplete"}, - foldGutter: true, - gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"] - - } - if mode == "application/json" - options.gutters.unshift("CodeMirror-lint-markers") - options.lint = true - options.foldOptions = { widget: @foldJson } - - @cm = CodeMirror(@node_cm, options) - @cm.on "changes", (changes) => - if @is_loaded and not @is_modified - @is_modified = true - Page.projector.scheduleRender() - - - loadEditor: -> - if not @is_loading - document.getElementsByTagName("head")[0].insertAdjacentHTML( - "beforeend", - """""" - ) - script = document.createElement('script') - script.src = "codemirror/all.js" - script.onload = => - @createCodeMirror() - @on_loaded.resolve() - document.head.appendChild(script) - return @on_loaded - - handleSidebarButtonClick: => - Page.is_sidebar_closed = not Page.is_sidebar_closed - return false - - handleSaveClick: => - num_errors = (mark for mark in Page.file_editor.cm.getAllMarks() when mark.className == "CodeMirror-lint-mark-error").length - if num_errors > 0 - Page.cmd "wrapperConfirm", ["Warning: The file looks invalid.", "Save anyway"], @save - else - @save() - return false - - save: => - Page.projector.scheduleRender() - @is_saving = true - Page.cmd "fileWrite", [@inner_path, Text.fileEncode(@cm.getValue())], (res) => - @is_saving = false - if res.error - Page.cmd "wrapperNotification", ["error", "Error saving #{res.error}"] - else - @is_save_done = true - setTimeout (() => - @is_save_done = false - Page.projector.scheduleRender() - ), 2000 - @content = @cm.getValue() - @is_modified = false - if @mode == "Create" - @mode = "Edit" - Page.file_list.need_update = true - Page.projector.scheduleRender() - - render: -> - if @need_update - @loadEditor().then => - @update() - @need_update = false - h("div.editor", {afterCreate: @storeCmNode, classes: {error: @error, loaded: @is_loaded}}, [ - h("a.sidebar-button", {href: "#Sidebar", onclick: @handleSidebarButtonClick}, h("span", "\u2039")), - h("div.editor-head", [ - if @mode in ["Edit", "Create"] - h("a.save.button", - {href: "#Save", classes: {loading: @is_saving, done: @is_save_done, disabled: not @is_modified}, onclick: @handleSaveClick}, - if @is_save_done then "Save: done!" else "Save" - ) - h("span.title", @mode, ": ", @inner_path) - ]), - if @error - h("div.error-message", - h("h2", "Unable to load the file: #{@error}") - h("a", {href: Page.file_list.getHref(@inner_path)}, "View in browser") - ) - ]) - -window.FileEditor = FileEditor \ No newline at end of file diff --git a/plugins/UiFileManager/media/js/FileItemList.coffee b/plugins/UiFileManager/media/js/FileItemList.coffee deleted file mode 100644 index df22b81a..00000000 --- a/plugins/UiFileManager/media/js/FileItemList.coffee +++ /dev/null @@ -1,194 +0,0 @@ -class FileItemList extends Class - constructor: (@inner_path) -> - @items = [] - @updating = false - @files_modified = {} - @dirs_modified = {} - @files_added = {} - @dirs_added = {} - @files_optional = {} - @items_by_name = {} - - # Update item list - update: (cb) -> - @updating = true - @logStart("Updating dirlist") - Page.cmd "dirList", {inner_path: @inner_path, stats: true}, (res) => - if res.error - @error = res.error - else - @error = null - pattern_ignore = RegExp("^" + Page.site_info.content?.ignore) - - @items.splice(0, @items.length) # Remove all items - - @items_by_name = {} - for row in res - row.type = @getFileType(row) - row.inner_path = @inner_path + row.name - if Page.site_info.content?.ignore and row.inner_path.match(pattern_ignore) - row.ignored = true - @items.push(row) - @items_by_name[row.name] = row - - @sort() - - if Page.site_info?.settings?.own - @updateAddedFiles() - - @updateOptionalFiles => - @updating = false - cb?() - @logEnd("Updating dirlist", @inner_path) - Page.projector.scheduleRender() - - @updateModifiedFiles => - Page.projector.scheduleRender() - - - updateModifiedFiles: (cb) => - # Add modified attribute to changed files - Page.cmd "siteListModifiedFiles", [], (res) => - @files_modified = {} - @dirs_modified = {} - for inner_path in res.modified_files - @files_modified[inner_path] = true - dir_inner_path = "" - dir_parts = inner_path.split("/") - for dir_part in dir_parts[..-2] - if dir_inner_path - dir_inner_path += "/#{dir_part}" - else - dir_inner_path = dir_part - @dirs_modified[dir_inner_path] = true - - cb?() - - # Update newly added items list since last sign - updateAddedFiles: => - Page.cmd "fileGet", "content.json", (res) => - if not res - return false - - content = JSON.parse(res) - - # Check new files - if not content.files? - return false - - @files_added = {} - - for file in @items - if file.name == "content.json" or file.is_dir - continue - if not content.files[@inner_path + file.name] - @files_added[@inner_path + file.name] = true - - # Check new dirs - @dirs_added = {} - - dirs_content = {} - for file_name of Object.assign({}, content.files, content.files_optional) - if not file_name.startsWith(@inner_path) - continue - - pattern = new RegExp("#{@inner_path}(.*?)/") - match = file_name.match(pattern) - - if not match - continue - - dirs_content[match[1]] = true - - for file in @items - if not file.is_dir - continue - if not dirs_content[file.name] - @dirs_added[@inner_path + file.name] = true - - # Update optional files list - updateOptionalFiles: (cb) => - Page.cmd "optionalFileList", {filter: ""}, (res) => - @files_optional = {} - for optional_file in res - @files_optional[optional_file.inner_path] = optional_file - - @addOptionalFilesToItems() - - cb?() - - # Add optional files to item list - addOptionalFilesToItems: => - is_added = false - for inner_path, optional_file of @files_optional - if optional_file.inner_path.startsWith(@inner_path) - if @getDirectory(optional_file.inner_path) == @inner_path - # Add optional file to list - file_name = @getFileName(optional_file.inner_path) - if not @items_by_name[file_name] - row = { - "name": file_name, "type": "file", "optional_empty": true, - "size": optional_file.size, "is_dir": false, "inner_path": optional_file.inner_path - } - @items.push(row) - @items_by_name[file_name] = row - is_added = true - else - # Add optional dir to list - dir_name = optional_file.inner_path.replace(@inner_path, "").match(/(.*?)\//, "")?[1] - if dir_name and not @items_by_name[dir_name] - row = { - "name": dir_name, "type": "dir", "optional_empty": true, - "size": 0, "is_dir": true, "inner_path": optional_file.inner_path - } - @items.push(row) - @items_by_name[dir_name] = row - is_added = true - - if is_added - @sort() - - getFileType: (file) => - if file.is_dir - return "dir" - else - return "unknown" - - getDirectory: (inner_path) -> - if inner_path.indexOf("/") != -1 - return inner_path.replace(/^(.*\/)(.*?)$/, "$1") - else - return "" - - getFileName: (inner_path) -> - return inner_path.replace(/^(.*\/)(.*?)$/, "$2") - - - isModified: (inner_path) => - return @files_modified[inner_path] or @dirs_modified[inner_path] - - isAdded: (inner_path) => - return @files_added[inner_path] or @dirs_added[inner_path] - - hasPermissionDelete: (file) => - if file.type in ["dir", "parent"] - return false - - if file.inner_path == "content.json" - return false - - optional_info = @getOptionalInfo(file.inner_path) - if optional_info and optional_info.downloaded_percent > 0 - return true - else - return Page.site_info?.settings?.own - - getOptionalInfo: (inner_path) => - return @files_optional[inner_path] - - sort: => - @items.sort (a, b) -> - return (b.is_dir - a.is_dir) || a.name.localeCompare(b.name) - - -window.FileItemList = FileItemList \ No newline at end of file diff --git a/plugins/UiFileManager/media/js/FileList.coffee b/plugins/UiFileManager/media/js/FileList.coffee deleted file mode 100644 index 1b5089b0..00000000 --- a/plugins/UiFileManager/media/js/FileList.coffee +++ /dev/null @@ -1,268 +0,0 @@ -class FileList extends Class - constructor: (@site, @inner_path, @is_owner=false) -> - @need_update = true - @error = null - @url_root = "/list/" + @site + "/" - if @inner_path - @inner_path += "/" - @url_root += @inner_path - @log("inited", @url_root) - @item_list = new FileItemList(@inner_path) - @item_list.items = @item_list.items - @menu_create = new Menu() - - @select_action = null - @selected = {} - @selected_items_num = 0 - @selected_items_size = 0 - @selected_optional_empty_num = 0 - - isSelectedAll: -> - false - - update: => - @item_list.update => - document.body.classList.add("loaded") - - getHref: (inner_path) => - return "/" + @site + "/" + inner_path - - getListHref: (inner_path) => - return "/list/" + @site + "/" + inner_path - - getEditHref: (inner_path, mode=null) => - href = @url_root + "?file=" + inner_path - if mode - href += "&edit_mode=#{mode}" - return href - - checkSelectedItems: => - @selected_items_num = 0 - @selected_items_size = 0 - @selected_optional_empty_num = 0 - for item in @item_list.items - if @selected[item.inner_path] - @selected_items_num += 1 - @selected_items_size += item.size - optional_info = @item_list.getOptionalInfo(item.inner_path) - if optional_info and not optional_info.downloaded_percent > 0 - @selected_optional_empty_num += 1 - - handleMenuCreateClick: => - @menu_create.items = [] - @menu_create.items.push ["File", @handleNewFileClick] - @menu_create.items.push ["Directory", @handleNewDirectoryClick] - @menu_create.toggle() - return false - - handleNewFileClick: => - Page.cmd "wrapperPrompt", "New file name:", (file_name) => - window.top.location.href = @getEditHref(@inner_path + file_name, "new") - return false - - handleNewDirectoryClick: => - Page.cmd "wrapperPrompt", "New directory name:", (res) => - alert("directory name #{res}") - return false - - handleSelectClick: (e) => - return false - - handleSelectEnd: (e) => - document.body.removeEventListener('mouseup', @handleSelectEnd) - @select_action = null - - handleSelectMousedown: (e) => - inner_path = e.currentTarget.attributes.inner_path.value - if @selected[inner_path] - delete @selected[inner_path] - @select_action = "deselect" - else - @selected[inner_path] = true - @select_action = "select" - @checkSelectedItems() - document.body.addEventListener('mouseup', @handleSelectEnd) - e.stopPropagation() - Page.projector.scheduleRender() - return false - - handleRowMouseenter: (e) => - if e.buttons and @select_action - inner_path = e.target.attributes.inner_path.value - if @select_action == "select" - @selected[inner_path] = true - else - delete @selected[inner_path] - @checkSelectedItems() - Page.projector.scheduleRender() - return false - - handleSelectbarCancel: => - @selected = {} - @checkSelectedItems() - Page.projector.scheduleRender() - return false - - handleSelectbarDelete: (e, remove_optional=false) => - for inner_path of @selected - optional_info = @item_list.getOptionalInfo(inner_path) - delete @selected[inner_path] - if optional_info and not remove_optional - Page.cmd "optionalFileDelete", inner_path - else - Page.cmd "fileDelete", inner_path - @need_update = true - Page.projector.scheduleRender() - @checkSelectedItems() - return false - - handleSelectbarRemoveOptional: (e) => - return @handleSelectbarDelete(e, true) - - renderSelectbar: => - h("div.selectbar", {classes: {visible: @selected_items_num > 0}}, [ - "Selected:", - h("span.info", [ - h("span.num", "#{@selected_items_num} files"), - h("span.size", "(#{Text.formatSize(@selected_items_size)})"), - ]) - h("div.actions", [ - if @selected_optional_empty_num > 0 - h("a.action.delete.remove_optional", {href: "#", onclick: @handleSelectbarRemoveOptional}, "Delete and remove optional") - else - h("a.action.delete", {href: "#", onclick: @handleSelectbarDelete}, "Delete") - ]) - h("a.cancel.link", {href: "#", onclick: @handleSelectbarCancel}, "Cancel") - ]) - - renderHead: => - parent_links = [] - inner_path_parent = "" - for parent_dir in @inner_path.split("/") - if not parent_dir - continue - if inner_path_parent - inner_path_parent += "/" - inner_path_parent += "#{parent_dir}" - parent_links.push( - [" / ", h("a", {href: @getListHref(inner_path_parent)}, parent_dir)] - ) - return h("div.tr.thead", h("div.td.full", - h("a", {href: @getListHref("")}, "root"), - parent_links - )) - - renderItemCheckbox: (item) => - if not @item_list.hasPermissionDelete(item) - return [" "] - - return h("a.checkbox-outer", { - href: "#Select", - onmousedown: @handleSelectMousedown, - onclick: @handleSelectClick, - inner_path: item.inner_path - }, h("span.checkbox")) - - renderItem: (item) => - if item.type == "parent" - href = @url_root.replace(/^(.*)\/.{2,255}?$/, "$1/") - else if item.type == "dir" - href = @url_root + item.name - else - href = @url_root.replace(/^\/list\//, "/") + item.name - - inner_path = @inner_path + item.name - href_edit = @getEditHref(inner_path) - is_dir = item.type in ["dir", "parent"] - ext = item.name.split(".").pop() - - is_editing = inner_path == Page.file_editor?.inner_path - is_editable = not is_dir and item.size < 1024 * 1024 and ext not in window.BINARY_EXTENSIONS - is_modified = @item_list.isModified(inner_path) - is_added = @item_list.isAdded(inner_path) - optional_info = @item_list.getOptionalInfo(inner_path) - - style = "" - title = "" - - if optional_info - downloaded_percent = optional_info.downloaded_percent - if not downloaded_percent - downloaded_percent = 0 - style += "background: linear-gradient(90deg, #fff6dd, #{downloaded_percent}%, white, #{downloaded_percent}%, white);" - is_added = false - - if item.ignored - is_added = false - - if is_modified then title += " (modified)" - if is_added then title += " (new)" - if optional_info or item.optional_empty then title += " (optional)" - if item.ignored then title += " (ignored from content.json)" - - classes = { - "type-#{item.type}": true, editing: is_editing, nobuttons: not is_editable, selected: @selected[inner_path], - modified: is_modified, added: is_added, ignored: item.ignored, optional: optional_info, optional_empty: item.optional_empty - } - - h("div.tr", {key: item.name, classes: classes, style: style, onmouseenter: @handleRowMouseenter, inner_path: inner_path}, [ - h("div.td.pre", {title: title}, - @renderItemCheckbox(item) - ), - h("div.td.name", h("a.link", {href: href}, item.name)) - h("div.td.buttons", if is_editable then h("a.edit", {href: href_edit}, if Page.site_info.settings.own then "Edit" else "View")) - h("div.td.size", if is_dir then "[DIR]" else Text.formatSize(item.size)) - ]) - - - renderItems: => - return [ - if @item_list.error and not @item_list.items.length and not @item_list.updating then [ - h("div.tr", {key: "error"}, h("div.td.full.error", @item_list.error)) - ], - if @inner_path then @renderItem({"name": "..", type: "parent", size: 0}) - @item_list.items.map @renderItem - ] - - renderFoot: => - files = (item for item in @item_list.items when item.type not in ["parent", "dir"]) - dirs = (item for item in @item_list.items when item.type == "dir") - if files.length - total_size = (item.size for file in files).reduce (a, b) -> a + b - else - total_size = 0 - - foot_text = "Total: " - foot_text += "#{dirs.length} dir, #{files.length} file in #{Text.formatSize(total_size)}" - - return [ - if dirs.length or files.length or Page.site_info?.settings?.own - h("div.tr.foot-info.foot", h("div.td.full", [ - if @item_list.updating - "Updating file list..." - else - if dirs.length or files.length then foot_text - if Page.site_info?.settings?.own - h("div.create", [ - h("a.link", {href: "#Create+new+file", onclick: @handleNewFileClick}, "+ New") - @menu_create.render() - ]) - ])) - ] - - render: => - if @need_update - @update() - @need_update = false - - if not @item_list.items - return [] - - return h("div.files", [ - @renderSelectbar(), - @renderHead(), - h("div.tbody", @renderItems()), - @renderFoot() - ]) - -window.FileList = FileList diff --git a/plugins/UiFileManager/media/js/UiFileManager.coffee b/plugins/UiFileManager/media/js/UiFileManager.coffee deleted file mode 100644 index 2126f3b1..00000000 --- a/plugins/UiFileManager/media/js/UiFileManager.coffee +++ /dev/null @@ -1,79 +0,0 @@ -window.h = maquette.h - -class UiFileManager extends ZeroFrame - init: -> - @url_params = new URLSearchParams(window.location.search) - @list_site = @url_params.get("site") - @list_address = @url_params.get("address") - @list_inner_path = @url_params.get("inner_path") - @editor_inner_path = @url_params.get("file") - @file_list = new FileList(@list_site, @list_inner_path) - - @site_info = null - @server_info = null - - @is_sidebar_closed = false - - if @editor_inner_path - @file_editor = new FileEditor(@editor_inner_path) - - window.onbeforeunload = => - if @file_editor?.isModified() - return true - else - return null - - window.onresize = => - @checkBodyWidth() - - @checkBodyWidth() - - @cmd("wrapperSetViewport", "width=device-width, initial-scale=0.8") - - @cmd "serverInfo", {}, (server_info) => - @server_info = server_info - @cmd "siteInfo", {}, (site_info) => - @cmd("wrapperSetTitle", "List: /#{@list_inner_path} - #{site_info.content.title} - ZeroNet") - @site_info = site_info - if @file_editor then @file_editor.on_loaded.then => - @file_editor.cm.setOption("readOnly", not site_info.settings.own) - @file_editor.mode = if site_info.settings.own then "Edit" else "View" - @projector.scheduleRender() - - checkBodyWidth: => - if not @file_editor - return false - - if document.body.offsetWidth < 960 and not @is_sidebar_closed - @is_sidebar_closed = true - @projector?.scheduleRender() - else if document.body.offsetWidth > 960 and @is_sidebar_closed - @is_sidebar_closed = false - @projector?.scheduleRender() - - onRequest: (cmd, message) => - if cmd == "setSiteInfo" - @site_info = message - RateLimitCb 1000, (cb_done) => - @file_list.update(cb_done) - @projector.scheduleRender() - else if cmd == "setServerInfo" - @server_info = message - @projector.scheduleRender() - else - @log "Unknown incoming message:", cmd - - createProjector: => - @projector = maquette.createProjector() - @projector.replace($("#content"), @render) - - render: => - return h("div.content#content", [ - h("div.manager", {classes: {editing: @file_editor, sidebar_closed: @is_sidebar_closed}}, [ - @file_list.render(), - if @file_editor then @file_editor.render() - ]) - ]) - -window.Page = new UiFileManager() -window.Page.createProjector() diff --git a/plugins/UiFileManager/media/js/all.js b/plugins/UiFileManager/media/js/all.js deleted file mode 100644 index 4f9b2cbd..00000000 --- a/plugins/UiFileManager/media/js/all.js +++ /dev/null @@ -1,3042 +0,0 @@ - -/* ---- lib/Animation.coffee ---- */ - - -(function() { - var Animation; - - Animation = (function() { - function Animation() {} - - Animation.prototype.slideDown = function(elem, props) { - var cstyle, h, margin_bottom, margin_top, padding_bottom, padding_top, transition; - if (elem.offsetTop > 2000) { - return; - } - h = elem.offsetHeight; - cstyle = window.getComputedStyle(elem); - margin_top = cstyle.marginTop; - margin_bottom = cstyle.marginBottom; - padding_top = cstyle.paddingTop; - padding_bottom = cstyle.paddingBottom; - transition = cstyle.transition; - elem.style.boxSizing = "border-box"; - elem.style.overflow = "hidden"; - elem.style.transform = "scale(0.6)"; - elem.style.opacity = "0"; - elem.style.height = "0px"; - elem.style.marginTop = "0px"; - elem.style.marginBottom = "0px"; - elem.style.paddingTop = "0px"; - elem.style.paddingBottom = "0px"; - elem.style.transition = "none"; - setTimeout((function() { - elem.className += " animate-inout"; - elem.style.height = h + "px"; - elem.style.transform = "scale(1)"; - elem.style.opacity = "1"; - elem.style.marginTop = margin_top; - elem.style.marginBottom = margin_bottom; - elem.style.paddingTop = padding_top; - return elem.style.paddingBottom = padding_bottom; - }), 1); - return elem.addEventListener("transitionend", function() { - elem.classList.remove("animate-inout"); - elem.style.transition = elem.style.transform = elem.style.opacity = elem.style.height = null; - elem.style.boxSizing = elem.style.marginTop = elem.style.marginBottom = null; - elem.style.paddingTop = elem.style.paddingBottom = elem.style.overflow = null; - return elem.removeEventListener("transitionend", arguments.callee, false); - }); - }; - - Animation.prototype.slideUp = function(elem, remove_func, props) { - if (elem.offsetTop > 1000) { - return remove_func(); - } - elem.className += " animate-back"; - elem.style.boxSizing = "border-box"; - elem.style.height = elem.offsetHeight + "px"; - elem.style.overflow = "hidden"; - elem.style.transform = "scale(1)"; - elem.style.opacity = "1"; - elem.style.pointerEvents = "none"; - setTimeout((function() { - elem.style.height = "0px"; - elem.style.marginTop = "0px"; - elem.style.marginBottom = "0px"; - elem.style.paddingTop = "0px"; - elem.style.paddingBottom = "0px"; - elem.style.transform = "scale(0.8)"; - elem.style.borderTopWidth = "0px"; - elem.style.borderBottomWidth = "0px"; - return elem.style.opacity = "0"; - }), 1); - return elem.addEventListener("transitionend", function(e) { - if (e.propertyName === "opacity" || e.elapsedTime >= 0.6) { - elem.removeEventListener("transitionend", arguments.callee, false); - return remove_func(); - } - }); - }; - - Animation.prototype.slideUpInout = function(elem, remove_func, props) { - elem.className += " animate-inout"; - elem.style.boxSizing = "border-box"; - elem.style.height = elem.offsetHeight + "px"; - elem.style.overflow = "hidden"; - elem.style.transform = "scale(1)"; - elem.style.opacity = "1"; - elem.style.pointerEvents = "none"; - setTimeout((function() { - elem.style.height = "0px"; - elem.style.marginTop = "0px"; - elem.style.marginBottom = "0px"; - elem.style.paddingTop = "0px"; - elem.style.paddingBottom = "0px"; - elem.style.transform = "scale(0.8)"; - elem.style.borderTopWidth = "0px"; - elem.style.borderBottomWidth = "0px"; - return elem.style.opacity = "0"; - }), 1); - return elem.addEventListener("transitionend", function(e) { - if (e.propertyName === "opacity" || e.elapsedTime >= 0.6) { - elem.removeEventListener("transitionend", arguments.callee, false); - return remove_func(); - } - }); - }; - - Animation.prototype.showRight = function(elem, props) { - elem.className += " animate"; - elem.style.opacity = 0; - elem.style.transform = "TranslateX(-20px) Scale(1.01)"; - setTimeout((function() { - elem.style.opacity = 1; - return elem.style.transform = "TranslateX(0px) Scale(1)"; - }), 1); - return elem.addEventListener("transitionend", function() { - elem.classList.remove("animate"); - return elem.style.transform = elem.style.opacity = null; - }); - }; - - Animation.prototype.show = function(elem, props) { - var delay, ref; - delay = ((ref = arguments[arguments.length - 2]) != null ? ref.delay : void 0) * 1000 || 1; - elem.style.opacity = 0; - setTimeout((function() { - return elem.className += " animate"; - }), 1); - setTimeout((function() { - return elem.style.opacity = 1; - }), delay); - return elem.addEventListener("transitionend", function() { - elem.classList.remove("animate"); - elem.style.opacity = null; - return elem.removeEventListener("transitionend", arguments.callee, false); - }); - }; - - Animation.prototype.hide = function(elem, remove_func, props) { - var delay, ref; - delay = ((ref = arguments[arguments.length - 2]) != null ? ref.delay : void 0) * 1000 || 1; - elem.className += " animate"; - setTimeout((function() { - return elem.style.opacity = 0; - }), delay); - return elem.addEventListener("transitionend", function(e) { - if (e.propertyName === "opacity") { - return remove_func(); - } - }); - }; - - Animation.prototype.addVisibleClass = function(elem, props) { - return setTimeout(function() { - return elem.classList.add("visible"); - }); - }; - - return Animation; - - })(); - - window.Animation = new Animation(); - -}).call(this); - -/* ---- lib/Class.coffee ---- */ - - -(function() { - var Class, - slice = [].slice; - - Class = (function() { - function Class() {} - - Class.prototype.trace = true; - - Class.prototype.log = function() { - var args; - args = 1 <= arguments.length ? slice.call(arguments, 0) : []; - if (!this.trace) { - return; - } - if (typeof console === 'undefined') { - return; - } - args.unshift("[" + this.constructor.name + "]"); - console.log.apply(console, args); - return this; - }; - - Class.prototype.logStart = function() { - var args, name; - name = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; - if (!this.trace) { - return; - } - this.logtimers || (this.logtimers = {}); - this.logtimers[name] = +(new Date); - if (args.length > 0) { - this.log.apply(this, ["" + name].concat(slice.call(args), ["(started)"])); - } - return this; - }; - - Class.prototype.logEnd = function() { - var args, ms, name; - name = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; - ms = +(new Date) - this.logtimers[name]; - this.log.apply(this, ["" + name].concat(slice.call(args), ["(Done in " + ms + "ms)"])); - return this; - }; - - return Class; - - })(); - - window.Class = Class; - -}).call(this); - -/* ---- lib/Dollar.coffee ---- */ - - -(function() { - window.$ = function(selector) { - if (selector.startsWith("#")) { - return document.getElementById(selector.replace("#", "")); - } - }; - -}).call(this); - -/* ---- lib/ItemList.coffee ---- */ - - -(function() { - var ItemList; - - ItemList = (function() { - function ItemList(item_class1, key1) { - this.item_class = item_class1; - this.key = key1; - this.items = []; - this.items_bykey = {}; - } - - ItemList.prototype.sync = function(rows, item_class, key) { - var current_obj, i, item, len, results, row; - this.items.splice(0, this.items.length); - results = []; - for (i = 0, len = rows.length; i < len; i++) { - row = rows[i]; - current_obj = this.items_bykey[row[this.key]]; - if (current_obj) { - current_obj.row = row; - results.push(this.items.push(current_obj)); - } else { - item = new this.item_class(row, this); - this.items_bykey[row[this.key]] = item; - results.push(this.items.push(item)); - } - } - return results; - }; - - ItemList.prototype.deleteItem = function(item) { - var index; - index = this.items.indexOf(item); - if (index > -1) { - this.items.splice(index, 1); - } else { - console.log("Can't delete item", item); - } - return delete this.items_bykey[item.row[this.key]]; - }; - - return ItemList; - - })(); - - window.ItemList = ItemList; - -}).call(this); - -/* ---- lib/Menu.coffee ---- */ - - -(function() { - var Menu, - bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, - indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; - - Menu = (function() { - function Menu() { - this.render = bind(this.render, this); - this.getStyle = bind(this.getStyle, this); - this.renderItem = bind(this.renderItem, this); - this.handleClick = bind(this.handleClick, this); - this.getDirection = bind(this.getDirection, this); - this.storeNode = bind(this.storeNode, this); - this.toggle = bind(this.toggle, this); - this.hide = bind(this.hide, this); - this.show = bind(this.show, this); - this.visible = false; - this.items = []; - this.node = null; - this.height = 0; - this.direction = "bottom"; - } - - Menu.prototype.show = function() { - var ref; - if ((ref = window.visible_menu) != null) { - ref.hide(); - } - this.visible = true; - window.visible_menu = this; - return this.direction = this.getDirection(); - }; - - Menu.prototype.hide = function() { - return this.visible = false; - }; - - Menu.prototype.toggle = function() { - if (this.visible) { - this.hide(); - } else { - this.show(); - } - return Page.projector.scheduleRender(); - }; - - Menu.prototype.addItem = function(title, cb, selected) { - if (selected == null) { - selected = false; - } - return this.items.push([title, cb, selected]); - }; - - Menu.prototype.storeNode = function(node) { - this.node = node; - if (this.visible) { - node.className = node.className.replace("visible", ""); - setTimeout(((function(_this) { - return function() { - node.className += " visible"; - return node.attributes.style.value = _this.getStyle(); - }; - })(this)), 20); - node.style.maxHeight = "none"; - this.height = node.offsetHeight; - node.style.maxHeight = "0px"; - return this.direction = this.getDirection(); - } - }; - - Menu.prototype.getDirection = function() { - if (this.node && this.node.parentNode.getBoundingClientRect().top + this.height + 60 > document.body.clientHeight && this.node.parentNode.getBoundingClientRect().top - this.height > 0) { - return "top"; - } else { - return "bottom"; - } - }; - - Menu.prototype.handleClick = function(e) { - var cb, i, item, keep_menu, len, ref, selected, title; - keep_menu = false; - ref = this.items; - for (i = 0, len = ref.length; i < len; i++) { - item = ref[i]; - title = item[0], cb = item[1], selected = item[2]; - if (title === e.currentTarget.textContent || e.currentTarget["data-title"] === title) { - keep_menu = typeof cb === "function" ? cb(item) : void 0; - break; - } - } - if (keep_menu !== true && cb !== null) { - this.hide(); - } - return false; - }; - - Menu.prototype.renderItem = function(item) { - var cb, classes, href, onclick, selected, title; - title = item[0], cb = item[1], selected = item[2]; - if (typeof selected === "function") { - selected = selected(); - } - if (title === "---") { - return h("div.menu-item-separator", { - key: Time.timestamp() - }); - } else { - if (cb === null) { - href = void 0; - onclick = this.handleClick; - } else if (typeof cb === "string") { - href = cb; - onclick = true; - } else { - href = "#" + title; - onclick = this.handleClick; - } - classes = { - "selected": selected, - "noaction": cb === null - }; - return h("a.menu-item", { - href: href, - onclick: onclick, - "data-title": title, - key: title, - classes: classes - }, title); - } - }; - - Menu.prototype.getStyle = function() { - var max_height, style; - if (this.visible) { - max_height = this.height; - } else { - max_height = 0; - } - style = "max-height: " + max_height + "px"; - if (this.direction === "top") { - style += ";margin-top: " + (0 - this.height - 50) + "px"; - } else { - style += ";margin-top: 0px"; - } - return style; - }; - - Menu.prototype.render = function(class_name) { - if (class_name == null) { - class_name = ""; - } - if (this.visible || this.node) { - return h("div.menu" + class_name, { - classes: { - "visible": this.visible - }, - style: this.getStyle(), - afterCreate: this.storeNode - }, this.items.map(this.renderItem)); - } - }; - - return Menu; - - })(); - - window.Menu = Menu; - - document.body.addEventListener("mouseup", function(e) { - var menu_node, menu_parents, ref, ref1; - if (!window.visible_menu || !window.visible_menu.node) { - return false; - } - menu_node = window.visible_menu.node; - menu_parents = [menu_node, menu_node.parentNode]; - if ((ref = e.target.parentNode, indexOf.call(menu_parents, ref) < 0) && (ref1 = e.target.parentNode.parentNode, indexOf.call(menu_parents, ref1) < 0)) { - window.visible_menu.hide(); - return Page.projector.scheduleRender(); - } - }); - -}).call(this); - -/* ---- lib/Promise.coffee ---- */ - - -(function() { - var Promise, - slice = [].slice; - - Promise = (function() { - Promise.when = function() { - var args, fn, i, len, num_uncompleted, promise, task, task_id, tasks; - tasks = 1 <= arguments.length ? slice.call(arguments, 0) : []; - num_uncompleted = tasks.length; - args = new Array(num_uncompleted); - promise = new Promise(); - fn = function(task_id) { - return task.then(function() { - args[task_id] = Array.prototype.slice.call(arguments); - num_uncompleted--; - if (num_uncompleted === 0) { - return promise.complete.apply(promise, args); - } - }); - }; - for (task_id = i = 0, len = tasks.length; i < len; task_id = ++i) { - task = tasks[task_id]; - fn(task_id); - } - return promise; - }; - - function Promise() { - this.resolved = false; - this.end_promise = null; - this.result = null; - this.callbacks = []; - } - - Promise.prototype.resolve = function() { - var back, callback, i, len, ref; - if (this.resolved) { - return false; - } - this.resolved = true; - this.data = arguments; - if (!arguments.length) { - this.data = [true]; - } - this.result = this.data[0]; - ref = this.callbacks; - for (i = 0, len = ref.length; i < len; i++) { - callback = ref[i]; - back = callback.apply(callback, this.data); - } - if (this.end_promise) { - return this.end_promise.resolve(back); - } - }; - - Promise.prototype.fail = function() { - return this.resolve(false); - }; - - Promise.prototype.then = function(callback) { - if (this.resolved === true) { - callback.apply(callback, this.data); - return; - } - this.callbacks.push(callback); - return this.end_promise = new Promise(); - }; - - return Promise; - - })(); - - window.Promise = Promise; - - - /* - s = Date.now() - log = (text) -> - console.log Date.now()-s, Array.prototype.slice.call(arguments).join(", ") - - log "Started" - - cmd = (query) -> - p = new Promise() - setTimeout ( -> - p.resolve query+" Result" - ), 100 - return p - - back = cmd("SELECT * FROM message").then (res) -> - log res - return "Return from query" - .then (res) -> - log "Back then", res - - log "Query started", back - */ - -}).call(this); - -/* ---- lib/Prototypes.coffee ---- */ - - -(function() { - String.prototype.startsWith = function(s) { - return this.slice(0, s.length) === s; - }; - - String.prototype.endsWith = function(s) { - return s === '' || this.slice(-s.length) === s; - }; - - String.prototype.repeat = function(count) { - return new Array(count + 1).join(this); - }; - - window.isEmpty = function(obj) { - var key; - for (key in obj) { - return false; - } - return true; - }; - -}).call(this); - -/* ---- lib/RateLimitCb.coffee ---- */ - - -(function() { - var call_after_interval, calling, calling_iterval, last_time, - slice = [].slice; - - last_time = {}; - - calling = {}; - - calling_iterval = {}; - - call_after_interval = {}; - - window.RateLimitCb = function(interval, fn, args) { - var cb; - if (args == null) { - args = []; - } - cb = function() { - var left; - left = interval - (Date.now() - last_time[fn]); - if (left <= 0) { - delete last_time[fn]; - if (calling[fn]) { - RateLimitCb(interval, fn, calling[fn]); - } - return delete calling[fn]; - } else { - return setTimeout((function() { - delete last_time[fn]; - if (calling[fn]) { - RateLimitCb(interval, fn, calling[fn]); - } - return delete calling[fn]; - }), left); - } - }; - if (last_time[fn]) { - return calling[fn] = args; - } else { - last_time[fn] = Date.now(); - return fn.apply(this, [cb].concat(slice.call(args))); - } - }; - - window.RateLimit = function(interval, fn) { - if (calling_iterval[fn] > interval) { - clearInterval(calling[fn]); - delete calling[fn]; - } - if (!calling[fn]) { - call_after_interval[fn] = false; - fn(); - calling_iterval[fn] = interval; - return calling[fn] = setTimeout((function() { - if (call_after_interval[fn]) { - fn(); - } - delete calling[fn]; - return delete call_after_interval[fn]; - }), interval); - } else { - return call_after_interval[fn] = true; - } - }; - - - /* - window.s = Date.now() - window.load = (done, num) -> - console.log "Loading #{num}...", Date.now()-window.s - setTimeout (-> done()), 1000 - - RateLimit 500, window.load, [0] # Called instantly - RateLimit 500, window.load, [1] - setTimeout (-> RateLimit 500, window.load, [300]), 300 - setTimeout (-> RateLimit 500, window.load, [600]), 600 # Called after 1000ms - setTimeout (-> RateLimit 500, window.load, [1000]), 1000 - setTimeout (-> RateLimit 500, window.load, [1200]), 1200 # Called after 2000ms - setTimeout (-> RateLimit 500, window.load, [3000]), 3000 # Called after 3000ms - */ - -}).call(this); - -/* ---- lib/Text.coffee ---- */ - - -(function() { - var Text, - indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; - - Text = (function() { - function Text() {} - - Text.prototype.toColor = function(text, saturation, lightness) { - var hash, i, j, ref; - if (saturation == null) { - saturation = 30; - } - if (lightness == null) { - lightness = 50; - } - hash = 0; - for (i = j = 0, ref = text.length - 1; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) { - hash += text.charCodeAt(i) * i; - hash = hash % 1777; - } - return "hsl(" + (hash % 360) + ("," + saturation + "%," + lightness + "%)"); - }; - - Text.prototype.renderMarked = function(text, options) { - if (options == null) { - options = {}; - } - options["gfm"] = true; - options["breaks"] = true; - options["sanitize"] = true; - options["renderer"] = marked_renderer; - text = marked(text, options); - return this.fixHtmlLinks(text); - }; - - Text.prototype.emailLinks = function(text) { - return text.replace(/([a-zA-Z0-9]+)@zeroid.bit/g, "$1@zeroid.bit"); - }; - - Text.prototype.fixHtmlLinks = function(text) { - if (window.is_proxy) { - return text.replace(/href="http:\/\/(127.0.0.1|localhost):43110/g, 'href="http://zero'); - } else { - return text.replace(/href="http:\/\/(127.0.0.1|localhost):43110/g, 'href="'); - } - }; - - Text.prototype.fixLink = function(link) { - var back; - if (window.is_proxy) { - back = link.replace(/http:\/\/(127.0.0.1|localhost):43110/, 'http://zero'); - return back.replace(/http:\/\/zero\/([^\/]+\.bit)/, "http://$1"); - } else { - return link.replace(/http:\/\/(127.0.0.1|localhost):43110/, ''); - } - }; - - Text.prototype.toUrl = function(text) { - return text.replace(/[^A-Za-z0-9]/g, "+").replace(/[+]+/g, "+").replace(/[+]+$/, ""); - }; - - Text.prototype.getSiteUrl = function(address) { - if (window.is_proxy) { - if (indexOf.call(address, ".") >= 0) { - return "http://" + address + "/"; - } else { - return "http://zero/" + address + "/"; - } - } else { - return "/" + address + "/"; - } - }; - - Text.prototype.fixReply = function(text) { - return text.replace(/(>.*\n)([^\n>])/gm, "$1\n$2"); - }; - - Text.prototype.toBitcoinAddress = function(text) { - return text.replace(/[^A-Za-z0-9]/g, ""); - }; - - Text.prototype.jsonEncode = function(obj) { - return unescape(encodeURIComponent(JSON.stringify(obj))); - }; - - Text.prototype.jsonDecode = function(obj) { - return JSON.parse(decodeURIComponent(escape(obj))); - }; - - Text.prototype.fileEncode = function(obj) { - if (typeof obj === "string") { - return btoa(unescape(encodeURIComponent(obj))); - } else { - return btoa(unescape(encodeURIComponent(JSON.stringify(obj, void 0, '\t')))); - } - }; - - Text.prototype.utf8Encode = function(s) { - return unescape(encodeURIComponent(s)); - }; - - Text.prototype.utf8Decode = function(s) { - return decodeURIComponent(escape(s)); - }; - - Text.prototype.distance = function(s1, s2) { - var char, extra_parts, j, key, len, match, next_find, next_find_i, val; - s1 = s1.toLocaleLowerCase(); - s2 = s2.toLocaleLowerCase(); - next_find_i = 0; - next_find = s2[0]; - match = true; - extra_parts = {}; - for (j = 0, len = s1.length; j < len; j++) { - char = s1[j]; - if (char !== next_find) { - if (extra_parts[next_find_i]) { - extra_parts[next_find_i] += char; - } else { - extra_parts[next_find_i] = char; - } - } else { - next_find_i++; - next_find = s2[next_find_i]; - } - } - if (extra_parts[next_find_i]) { - extra_parts[next_find_i] = ""; - } - extra_parts = (function() { - var results; - results = []; - for (key in extra_parts) { - val = extra_parts[key]; - results.push(val); - } - return results; - })(); - if (next_find_i >= s2.length) { - return extra_parts.length + extra_parts.join("").length; - } else { - return false; - } - }; - - Text.prototype.parseQuery = function(query) { - var j, key, len, params, part, parts, ref, val; - params = {}; - parts = query.split('&'); - for (j = 0, len = parts.length; j < len; j++) { - part = parts[j]; - ref = part.split("="), key = ref[0], val = ref[1]; - if (val) { - params[decodeURIComponent(key)] = decodeURIComponent(val); - } else { - params["url"] = decodeURIComponent(key); - } - } - return params; - }; - - Text.prototype.encodeQuery = function(params) { - var back, key, val; - back = []; - if (params.url) { - back.push(params.url); - } - for (key in params) { - val = params[key]; - if (!val || key === "url") { - continue; - } - back.push((encodeURIComponent(key)) + "=" + (encodeURIComponent(val))); - } - return back.join("&"); - }; - - Text.prototype.highlight = function(text, search) { - var back, i, j, len, part, parts; - if (!text) { - return [""]; - } - parts = text.split(RegExp(search, "i")); - back = []; - for (i = j = 0, len = parts.length; j < len; i = ++j) { - part = parts[i]; - back.push(part); - if (i < parts.length - 1) { - back.push(h("span.highlight", { - key: i - }, search)); - } - } - return back; - }; - - Text.prototype.formatSize = function(size) { - var size_mb; - if (isNaN(parseInt(size))) { - return ""; - } - size_mb = size / 1024 / 1024; - if (size_mb >= 1000) { - return (size_mb / 1024).toFixed(1) + " GB"; - } else if (size_mb >= 100) { - return size_mb.toFixed(0) + " MB"; - } else if (size / 1024 >= 1000) { - return size_mb.toFixed(2) + " MB"; - } else { - return (parseInt(size) / 1024).toFixed(2) + " KB"; - } - }; - - return Text; - - })(); - - window.is_proxy = document.location.host === "zero" || window.location.pathname === "/"; - - window.Text = new Text(); - -}).call(this); - -/* ---- lib/Time.coffee ---- */ - - -(function() { - var Time; - - Time = (function() { - function Time() {} - - Time.prototype.since = function(timestamp) { - var back, minutes, now, secs; - now = +(new Date) / 1000; - if (timestamp > 1000000000000) { - timestamp = timestamp / 1000; - } - secs = now - timestamp; - if (secs < 60) { - back = "Just now"; - } else if (secs < 60 * 60) { - minutes = Math.round(secs / 60); - back = "" + minutes + " minutes ago"; - } else if (secs < 60 * 60 * 24) { - back = (Math.round(secs / 60 / 60)) + " hours ago"; - } else if (secs < 60 * 60 * 24 * 3) { - back = (Math.round(secs / 60 / 60 / 24)) + " days ago"; - } else { - back = "on " + this.date(timestamp); - } - back = back.replace(/^1 ([a-z]+)s/, "1 $1"); - return back; - }; - - Time.prototype.dateIso = function(timestamp) { - var tzoffset; - if (timestamp == null) { - timestamp = null; - } - if (!timestamp) { - timestamp = window.Time.timestamp(); - } - if (timestamp > 1000000000000) { - timestamp = timestamp / 1000; - } - tzoffset = (new Date()).getTimezoneOffset() * 60; - return (new Date((timestamp - tzoffset) * 1000)).toISOString().split("T")[0]; - }; - - Time.prototype.date = function(timestamp, format) { - var display, parts; - if (timestamp == null) { - timestamp = null; - } - if (format == null) { - format = "short"; - } - if (!timestamp) { - timestamp = window.Time.timestamp(); - } - if (timestamp > 1000000000000) { - timestamp = timestamp / 1000; - } - parts = (new Date(timestamp * 1000)).toString().split(" "); - if (format === "short") { - display = parts.slice(1, 4); - } else if (format === "day") { - display = parts.slice(1, 3); - } else if (format === "month") { - display = [parts[1], parts[3]]; - } else if (format === "long") { - display = parts.slice(1, 5); - } - return display.join(" ").replace(/( [0-9]{4})/, ",$1"); - }; - - Time.prototype.weekDay = function(timestamp) { - if (timestamp > 1000000000000) { - timestamp = timestamp / 1000; - } - return ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][(new Date(timestamp * 1000)).getDay()]; - }; - - Time.prototype.timestamp = function(date) { - if (date == null) { - date = ""; - } - if (date === "now" || date === "") { - return parseInt(+(new Date) / 1000); - } else { - return parseInt(Date.parse(date) / 1000); - } - }; - - return Time; - - })(); - - window.Time = new Time; - -}).call(this); - -/* ---- lib/ZeroFrame.coffee ---- */ - - -(function() { - var ZeroFrame, - bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, - extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, - hasProp = {}.hasOwnProperty; - - ZeroFrame = (function(superClass) { - extend(ZeroFrame, superClass); - - function ZeroFrame(url) { - this.onCloseWebsocket = bind(this.onCloseWebsocket, this); - this.onOpenWebsocket = bind(this.onOpenWebsocket, this); - this.onRequest = bind(this.onRequest, this); - this.onMessage = bind(this.onMessage, this); - this.url = url; - this.waiting_cb = {}; - this.wrapper_nonce = document.location.href.replace(/.*wrapper_nonce=([A-Za-z0-9]+).*/, "$1"); - this.connect(); - this.next_message_id = 1; - this.history_state = {}; - this.init(); - } - - ZeroFrame.prototype.init = function() { - return this; - }; - - ZeroFrame.prototype.connect = function() { - this.target = window.parent; - window.addEventListener("message", this.onMessage, false); - this.cmd("innerReady"); - window.addEventListener("beforeunload", (function(_this) { - return function(e) { - _this.log("save scrollTop", window.pageYOffset); - _this.history_state["scrollTop"] = window.pageYOffset; - return _this.cmd("wrapperReplaceState", [_this.history_state, null]); - }; - })(this)); - return this.cmd("wrapperGetState", [], (function(_this) { - return function(state) { - if (state != null) { - _this.history_state = state; - } - _this.log("restore scrollTop", state, window.pageYOffset); - if (window.pageYOffset === 0 && state) { - return window.scroll(window.pageXOffset, state.scrollTop); - } - }; - })(this)); - }; - - ZeroFrame.prototype.onMessage = function(e) { - var cmd, message; - message = e.data; - cmd = message.cmd; - if (cmd === "response") { - if (this.waiting_cb[message.to] != null) { - return this.waiting_cb[message.to](message.result); - } else { - return this.log("Websocket callback not found:", message); - } - } else if (cmd === "wrapperReady") { - return this.cmd("innerReady"); - } else if (cmd === "ping") { - return this.response(message.id, "pong"); - } else if (cmd === "wrapperOpenedWebsocket") { - return this.onOpenWebsocket(); - } else if (cmd === "wrapperClosedWebsocket") { - return this.onCloseWebsocket(); - } else { - return this.onRequest(cmd, message.params); - } - }; - - ZeroFrame.prototype.onRequest = function(cmd, message) { - return this.log("Unknown request", message); - }; - - ZeroFrame.prototype.response = function(to, result) { - return this.send({ - "cmd": "response", - "to": to, - "result": result - }); - }; - - ZeroFrame.prototype.cmd = function(cmd, params, cb) { - if (params == null) { - params = {}; - } - if (cb == null) { - cb = null; - } - return this.send({ - "cmd": cmd, - "params": params - }, cb); - }; - - ZeroFrame.prototype.send = function(message, cb) { - if (cb == null) { - cb = null; - } - message.wrapper_nonce = this.wrapper_nonce; - message.id = this.next_message_id; - this.next_message_id += 1; - this.target.postMessage(message, "*"); - if (cb) { - return this.waiting_cb[message.id] = cb; - } - }; - - ZeroFrame.prototype.onOpenWebsocket = function() { - return this.log("Websocket open"); - }; - - ZeroFrame.prototype.onCloseWebsocket = function() { - return this.log("Websocket close"); - }; - - return ZeroFrame; - - })(Class); - - window.ZeroFrame = ZeroFrame; - -}).call(this); - -/* ---- lib/maquette.js ---- */ - - -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(['exports'], factory); - } else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') { - // CommonJS - factory(exports); - } else { - // Browser globals - factory(root.maquette = {}); - } -}(this, function (exports) { - 'use strict'; - ; - ; - ; - ; - var NAMESPACE_W3 = 'http://www.w3.org/'; - var NAMESPACE_SVG = NAMESPACE_W3 + '2000/svg'; - var NAMESPACE_XLINK = NAMESPACE_W3 + '1999/xlink'; - // Utilities - var emptyArray = []; - var extend = function (base, overrides) { - var result = {}; - Object.keys(base).forEach(function (key) { - result[key] = base[key]; - }); - if (overrides) { - Object.keys(overrides).forEach(function (key) { - result[key] = overrides[key]; - }); - } - return result; - }; - // Hyperscript helper functions - var same = function (vnode1, vnode2) { - if (vnode1.vnodeSelector !== vnode2.vnodeSelector) { - return false; - } - if (vnode1.properties && vnode2.properties) { - if (vnode1.properties.key !== vnode2.properties.key) { - return false; - } - return vnode1.properties.bind === vnode2.properties.bind; - } - return !vnode1.properties && !vnode2.properties; - }; - var toTextVNode = function (data) { - return { - vnodeSelector: '', - properties: undefined, - children: undefined, - text: data.toString(), - domNode: null - }; - }; - var appendChildren = function (parentSelector, insertions, main) { - for (var i = 0; i < insertions.length; i++) { - var item = insertions[i]; - if (Array.isArray(item)) { - appendChildren(parentSelector, item, main); - } else { - if (item !== null && item !== undefined) { - if (!item.hasOwnProperty('vnodeSelector')) { - item = toTextVNode(item); - } - main.push(item); - } - } - } - }; - // Render helper functions - var missingTransition = function () { - throw new Error('Provide a transitions object to the projectionOptions to do animations'); - }; - var DEFAULT_PROJECTION_OPTIONS = { - namespace: undefined, - eventHandlerInterceptor: undefined, - styleApplyer: function (domNode, styleName, value) { - // Provides a hook to add vendor prefixes for browsers that still need it. - domNode.style[styleName] = value; - }, - transitions: { - enter: missingTransition, - exit: missingTransition - } - }; - var applyDefaultProjectionOptions = function (projectorOptions) { - return extend(DEFAULT_PROJECTION_OPTIONS, projectorOptions); - }; - var checkStyleValue = function (styleValue) { - if (typeof styleValue !== 'string') { - throw new Error('Style values must be strings'); - } - }; - var setProperties = function (domNode, properties, projectionOptions) { - if (!properties) { - return; - } - var eventHandlerInterceptor = projectionOptions.eventHandlerInterceptor; - var propNames = Object.keys(properties); - var propCount = propNames.length; - for (var i = 0; i < propCount; i++) { - var propName = propNames[i]; - /* tslint:disable:no-var-keyword: edge case */ - var propValue = properties[propName]; - /* tslint:enable:no-var-keyword */ - if (propName === 'className') { - throw new Error('Property "className" is not supported, use "class".'); - } else if (propName === 'class') { - if (domNode.className) { - // May happen if classes is specified before class - domNode.className += ' ' + propValue; - } else { - domNode.className = propValue; - } - } else if (propName === 'classes') { - // object with string keys and boolean values - var classNames = Object.keys(propValue); - var classNameCount = classNames.length; - for (var j = 0; j < classNameCount; j++) { - var className = classNames[j]; - if (propValue[className]) { - domNode.classList.add(className); - } - } - } else if (propName === 'styles') { - // object with string keys and string (!) values - var styleNames = Object.keys(propValue); - var styleCount = styleNames.length; - for (var j = 0; j < styleCount; j++) { - var styleName = styleNames[j]; - var styleValue = propValue[styleName]; - if (styleValue) { - checkStyleValue(styleValue); - projectionOptions.styleApplyer(domNode, styleName, styleValue); - } - } - } else if (propName === 'key') { - continue; - } else if (propValue === null || propValue === undefined) { - continue; - } else { - var type = typeof propValue; - if (type === 'function') { - if (propName.lastIndexOf('on', 0) === 0) { - if (eventHandlerInterceptor) { - propValue = eventHandlerInterceptor(propName, propValue, domNode, properties); // intercept eventhandlers - } - if (propName === 'oninput') { - (function () { - // record the evt.target.value, because IE and Edge sometimes do a requestAnimationFrame between changing value and running oninput - var oldPropValue = propValue; - propValue = function (evt) { - evt.target['oninput-value'] = evt.target.value; - // may be HTMLTextAreaElement as well - oldPropValue.apply(this, [evt]); - }; - }()); - } - domNode[propName] = propValue; - } - } else if (type === 'string' && propName !== 'value' && propName !== 'innerHTML') { - if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') { - domNode.setAttributeNS(NAMESPACE_XLINK, propName, propValue); - } else { - domNode.setAttribute(propName, propValue); - } - } else { - domNode[propName] = propValue; - } - } - } - }; - var updateProperties = function (domNode, previousProperties, properties, projectionOptions) { - if (!properties) { - return; - } - var propertiesUpdated = false; - var propNames = Object.keys(properties); - var propCount = propNames.length; - for (var i = 0; i < propCount; i++) { - var propName = propNames[i]; - // assuming that properties will be nullified instead of missing is by design - var propValue = properties[propName]; - var previousValue = previousProperties[propName]; - if (propName === 'class') { - if (previousValue !== propValue) { - throw new Error('"class" property may not be updated. Use the "classes" property for conditional css classes.'); - } - } else if (propName === 'classes') { - var classList = domNode.classList; - var classNames = Object.keys(propValue); - var classNameCount = classNames.length; - for (var j = 0; j < classNameCount; j++) { - var className = classNames[j]; - var on = !!propValue[className]; - var previousOn = !!previousValue[className]; - if (on === previousOn) { - continue; - } - propertiesUpdated = true; - if (on) { - classList.add(className); - } else { - classList.remove(className); - } - } - } else if (propName === 'styles') { - var styleNames = Object.keys(propValue); - var styleCount = styleNames.length; - for (var j = 0; j < styleCount; j++) { - var styleName = styleNames[j]; - var newStyleValue = propValue[styleName]; - var oldStyleValue = previousValue[styleName]; - if (newStyleValue === oldStyleValue) { - continue; - } - propertiesUpdated = true; - if (newStyleValue) { - checkStyleValue(newStyleValue); - projectionOptions.styleApplyer(domNode, styleName, newStyleValue); - } else { - projectionOptions.styleApplyer(domNode, styleName, ''); - } - } - } else { - if (!propValue && typeof previousValue === 'string') { - propValue = ''; - } - if (propName === 'value') { - if (domNode[propName] !== propValue && domNode['oninput-value'] !== propValue) { - domNode[propName] = propValue; - // Reset the value, even if the virtual DOM did not change - domNode['oninput-value'] = undefined; - } - // else do not update the domNode, otherwise the cursor position would be changed - if (propValue !== previousValue) { - propertiesUpdated = true; - } - } else if (propValue !== previousValue) { - var type = typeof propValue; - if (type === 'function') { - throw new Error('Functions may not be updated on subsequent renders (property: ' + propName + '). Hint: declare event handler functions outside the render() function.'); - } - if (type === 'string' && propName !== 'innerHTML') { - if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') { - domNode.setAttributeNS(NAMESPACE_XLINK, propName, propValue); - } else { - domNode.setAttribute(propName, propValue); - } - } else { - if (domNode[propName] !== propValue) { - domNode[propName] = propValue; - } - } - propertiesUpdated = true; - } - } - } - return propertiesUpdated; - }; - var findIndexOfChild = function (children, sameAs, start) { - if (sameAs.vnodeSelector !== '') { - // Never scan for text-nodes - for (var i = start; i < children.length; i++) { - if (same(children[i], sameAs)) { - return i; - } - } - } - return -1; - }; - var nodeAdded = function (vNode, transitions) { - if (vNode.properties) { - var enterAnimation = vNode.properties.enterAnimation; - if (enterAnimation) { - if (typeof enterAnimation === 'function') { - enterAnimation(vNode.domNode, vNode.properties); - } else { - transitions.enter(vNode.domNode, vNode.properties, enterAnimation); - } - } - } - }; - var nodeToRemove = function (vNode, transitions) { - var domNode = vNode.domNode; - if (vNode.properties) { - var exitAnimation = vNode.properties.exitAnimation; - if (exitAnimation) { - domNode.style.pointerEvents = 'none'; - var removeDomNode = function () { - if (domNode.parentNode) { - domNode.parentNode.removeChild(domNode); - } - }; - if (typeof exitAnimation === 'function') { - exitAnimation(domNode, removeDomNode, vNode.properties); - return; - } else { - transitions.exit(vNode.domNode, vNode.properties, exitAnimation, removeDomNode); - return; - } - } - } - if (domNode.parentNode) { - domNode.parentNode.removeChild(domNode); - } - }; - var checkDistinguishable = function (childNodes, indexToCheck, parentVNode, operation) { - var childNode = childNodes[indexToCheck]; - if (childNode.vnodeSelector === '') { - return; // Text nodes need not be distinguishable - } - var properties = childNode.properties; - var key = properties ? properties.key === undefined ? properties.bind : properties.key : undefined; - if (!key) { - for (var i = 0; i < childNodes.length; i++) { - if (i !== indexToCheck) { - var node = childNodes[i]; - if (same(node, childNode)) { - if (operation === 'added') { - throw new Error(parentVNode.vnodeSelector + ' had a ' + childNode.vnodeSelector + ' child ' + 'added, but there is now more than one. You must add unique key properties to make them distinguishable.'); - } else { - throw new Error(parentVNode.vnodeSelector + ' had a ' + childNode.vnodeSelector + ' child ' + 'removed, but there were more than one. You must add unique key properties to make them distinguishable.'); - } - } - } - } - } - }; - var createDom; - var updateDom; - var updateChildren = function (vnode, domNode, oldChildren, newChildren, projectionOptions) { - if (oldChildren === newChildren) { - return false; - } - oldChildren = oldChildren || emptyArray; - newChildren = newChildren || emptyArray; - var oldChildrenLength = oldChildren.length; - var newChildrenLength = newChildren.length; - var transitions = projectionOptions.transitions; - var oldIndex = 0; - var newIndex = 0; - var i; - var textUpdated = false; - while (newIndex < newChildrenLength) { - var oldChild = oldIndex < oldChildrenLength ? oldChildren[oldIndex] : undefined; - var newChild = newChildren[newIndex]; - if (oldChild !== undefined && same(oldChild, newChild)) { - textUpdated = updateDom(oldChild, newChild, projectionOptions) || textUpdated; - oldIndex++; - } else { - var findOldIndex = findIndexOfChild(oldChildren, newChild, oldIndex + 1); - if (findOldIndex >= 0) { - // Remove preceding missing children - for (i = oldIndex; i < findOldIndex; i++) { - nodeToRemove(oldChildren[i], transitions); - checkDistinguishable(oldChildren, i, vnode, 'removed'); - } - textUpdated = updateDom(oldChildren[findOldIndex], newChild, projectionOptions) || textUpdated; - oldIndex = findOldIndex + 1; - } else { - // New child - createDom(newChild, domNode, oldIndex < oldChildrenLength ? oldChildren[oldIndex].domNode : undefined, projectionOptions); - nodeAdded(newChild, transitions); - checkDistinguishable(newChildren, newIndex, vnode, 'added'); - } - } - newIndex++; - } - if (oldChildrenLength > oldIndex) { - // Remove child fragments - for (i = oldIndex; i < oldChildrenLength; i++) { - nodeToRemove(oldChildren[i], transitions); - checkDistinguishable(oldChildren, i, vnode, 'removed'); - } - } - return textUpdated; - }; - var addChildren = function (domNode, children, projectionOptions) { - if (!children) { - return; - } - for (var i = 0; i < children.length; i++) { - createDom(children[i], domNode, undefined, projectionOptions); - } - }; - var initPropertiesAndChildren = function (domNode, vnode, projectionOptions) { - addChildren(domNode, vnode.children, projectionOptions); - // children before properties, needed for value property of . - if (vnode.text) { - domNode.textContent = vnode.text; - } - setProperties(domNode, vnode.properties, projectionOptions); - if (vnode.properties && vnode.properties.afterCreate) { - vnode.properties.afterCreate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children); - } - }; - createDom = function (vnode, parentNode, insertBefore, projectionOptions) { - var domNode, i, c, start = 0, type, found; - var vnodeSelector = vnode.vnodeSelector; - if (vnodeSelector === '') { - domNode = vnode.domNode = document.createTextNode(vnode.text); - if (insertBefore !== undefined) { - parentNode.insertBefore(domNode, insertBefore); - } else { - parentNode.appendChild(domNode); - } - } else { - for (i = 0; i <= vnodeSelector.length; ++i) { - c = vnodeSelector.charAt(i); - if (i === vnodeSelector.length || c === '.' || c === '#') { - type = vnodeSelector.charAt(start - 1); - found = vnodeSelector.slice(start, i); - if (type === '.') { - domNode.classList.add(found); - } else if (type === '#') { - domNode.id = found; - } else { - if (found === 'svg') { - projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG }); - } - if (projectionOptions.namespace !== undefined) { - domNode = vnode.domNode = document.createElementNS(projectionOptions.namespace, found); - } else { - domNode = vnode.domNode = document.createElement(found); - } - if (insertBefore !== undefined) { - parentNode.insertBefore(domNode, insertBefore); - } else { - parentNode.appendChild(domNode); - } - } - start = i + 1; - } - } - initPropertiesAndChildren(domNode, vnode, projectionOptions); - } - }; - updateDom = function (previous, vnode, projectionOptions) { - var domNode = previous.domNode; - var textUpdated = false; - if (previous === vnode) { - return false; // By contract, VNode objects may not be modified anymore after passing them to maquette - } - var updated = false; - if (vnode.vnodeSelector === '') { - if (vnode.text !== previous.text) { - var newVNode = document.createTextNode(vnode.text); - domNode.parentNode.replaceChild(newVNode, domNode); - vnode.domNode = newVNode; - textUpdated = true; - return textUpdated; - } - } else { - if (vnode.vnodeSelector.lastIndexOf('svg', 0) === 0) { - projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG }); - } - if (previous.text !== vnode.text) { - updated = true; - if (vnode.text === undefined) { - domNode.removeChild(domNode.firstChild); // the only textnode presumably - } else { - domNode.textContent = vnode.text; - } - } - updated = updateChildren(vnode, domNode, previous.children, vnode.children, projectionOptions) || updated; - updated = updateProperties(domNode, previous.properties, vnode.properties, projectionOptions) || updated; - if (vnode.properties && vnode.properties.afterUpdate) { - vnode.properties.afterUpdate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children); - } - } - if (updated && vnode.properties && vnode.properties.updateAnimation) { - vnode.properties.updateAnimation(domNode, vnode.properties, previous.properties); - } - vnode.domNode = previous.domNode; - return textUpdated; - }; - var createProjection = function (vnode, projectionOptions) { - return { - update: function (updatedVnode) { - if (vnode.vnodeSelector !== updatedVnode.vnodeSelector) { - throw new Error('The selector for the root VNode may not be changed. (consider using dom.merge and add one extra level to the virtual DOM)'); - } - updateDom(vnode, updatedVnode, projectionOptions); - vnode = updatedVnode; - }, - domNode: vnode.domNode - }; - }; - ; - // The other two parameters are not added here, because the Typescript compiler creates surrogate code for desctructuring 'children'. - exports.h = function (selector) { - var properties = arguments[1]; - if (typeof selector !== 'string') { - throw new Error(); - } - var childIndex = 1; - if (properties && !properties.hasOwnProperty('vnodeSelector') && !Array.isArray(properties) && typeof properties === 'object') { - childIndex = 2; - } else { - // Optional properties argument was omitted - properties = undefined; - } - var text = undefined; - var children = undefined; - var argsLength = arguments.length; - // Recognize a common special case where there is only a single text node - if (argsLength === childIndex + 1) { - var onlyChild = arguments[childIndex]; - if (typeof onlyChild === 'string') { - text = onlyChild; - } else if (onlyChild !== undefined && onlyChild.length === 1 && typeof onlyChild[0] === 'string') { - text = onlyChild[0]; - } - } - if (text === undefined) { - children = []; - for (; childIndex < arguments.length; childIndex++) { - var child = arguments[childIndex]; - if (child === null || child === undefined) { - continue; - } else if (Array.isArray(child)) { - appendChildren(selector, child, children); - } else if (child.hasOwnProperty('vnodeSelector')) { - children.push(child); - } else { - children.push(toTextVNode(child)); - } - } - } - return { - vnodeSelector: selector, - properties: properties, - children: children, - text: text === '' ? undefined : text, - domNode: null - }; - }; - /** - * Contains simple low-level utility functions to manipulate the real DOM. - */ - exports.dom = { - /** - * Creates a real DOM tree from `vnode`. The [[Projection]] object returned will contain the resulting DOM Node in - * its [[Projection.domNode|domNode]] property. - * This is a low-level method. Users wil typically use a [[Projector]] instead. - * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]] - * objects may only be rendered once. - * @param projectionOptions - Options to be used to create and update the projection. - * @returns The [[Projection]] which also contains the DOM Node that was created. - */ - create: function (vnode, projectionOptions) { - projectionOptions = applyDefaultProjectionOptions(projectionOptions); - createDom(vnode, document.createElement('div'), undefined, projectionOptions); - return createProjection(vnode, projectionOptions); - }, - /** - * Appends a new childnode to the DOM which is generated from a [[VNode]]. - * This is a low-level method. Users wil typically use a [[Projector]] instead. - * @param parentNode - The parent node for the new childNode. - * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]] - * objects may only be rendered once. - * @param projectionOptions - Options to be used to create and update the [[Projection]]. - * @returns The [[Projection]] that was created. - */ - append: function (parentNode, vnode, projectionOptions) { - projectionOptions = applyDefaultProjectionOptions(projectionOptions); - createDom(vnode, parentNode, undefined, projectionOptions); - return createProjection(vnode, projectionOptions); - }, - /** - * Inserts a new DOM node which is generated from a [[VNode]]. - * This is a low-level method. Users wil typically use a [[Projector]] instead. - * @param beforeNode - The node that the DOM Node is inserted before. - * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. - * NOTE: [[VNode]] objects may only be rendered once. - * @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]]. - * @returns The [[Projection]] that was created. - */ - insertBefore: function (beforeNode, vnode, projectionOptions) { - projectionOptions = applyDefaultProjectionOptions(projectionOptions); - createDom(vnode, beforeNode.parentNode, beforeNode, projectionOptions); - return createProjection(vnode, projectionOptions); - }, - /** - * Merges a new DOM node which is generated from a [[VNode]] with an existing DOM Node. - * This means that the virtual DOM and the real DOM will have one overlapping element. - * Therefore the selector for the root [[VNode]] will be ignored, but its properties and children will be applied to the Element provided. - * This is a low-level method. Users wil typically use a [[Projector]] instead. - * @param domNode - The existing element to adopt as the root of the new virtual DOM. Existing attributes and childnodes are preserved. - * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]] objects - * may only be rendered once. - * @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]]. - * @returns The [[Projection]] that was created. - */ - merge: function (element, vnode, projectionOptions) { - projectionOptions = applyDefaultProjectionOptions(projectionOptions); - vnode.domNode = element; - initPropertiesAndChildren(element, vnode, projectionOptions); - return createProjection(vnode, projectionOptions); - } - }; - /** - * Creates a [[CalculationCache]] object, useful for caching [[VNode]] trees. - * In practice, caching of [[VNode]] trees is not needed, because achieving 60 frames per second is almost never a problem. - * For more information, see [[CalculationCache]]. - * - * @param The type of the value that is cached. - */ - exports.createCache = function () { - var cachedInputs = undefined; - var cachedOutcome = undefined; - var result = { - invalidate: function () { - cachedOutcome = undefined; - cachedInputs = undefined; - }, - result: function (inputs, calculation) { - if (cachedInputs) { - for (var i = 0; i < inputs.length; i++) { - if (cachedInputs[i] !== inputs[i]) { - cachedOutcome = undefined; - } - } - } - if (!cachedOutcome) { - cachedOutcome = calculation(); - cachedInputs = inputs; - } - return cachedOutcome; - } - }; - return result; - }; - /** - * Creates a {@link Mapping} instance that keeps an array of result objects synchronized with an array of source objects. - * See {@link http://maquettejs.org/docs/arrays.html|Working with arrays}. - * - * @param The type of source items. A database-record for instance. - * @param The type of target items. A [[Component]] for instance. - * @param getSourceKey `function(source)` that must return a key to identify each source object. The result must either be a string or a number. - * @param createResult `function(source, index)` that must create a new result object from a given source. This function is identical - * to the `callback` argument in `Array.map(callback)`. - * @param updateResult `function(source, target, index)` that updates a result to an updated source. - */ - exports.createMapping = function (getSourceKey, createResult, updateResult) { - var keys = []; - var results = []; - return { - results: results, - map: function (newSources) { - var newKeys = newSources.map(getSourceKey); - var oldTargets = results.slice(); - var oldIndex = 0; - for (var i = 0; i < newSources.length; i++) { - var source = newSources[i]; - var sourceKey = newKeys[i]; - if (sourceKey === keys[oldIndex]) { - results[i] = oldTargets[oldIndex]; - updateResult(source, oldTargets[oldIndex], i); - oldIndex++; - } else { - var found = false; - for (var j = 1; j < keys.length; j++) { - var searchIndex = (oldIndex + j) % keys.length; - if (keys[searchIndex] === sourceKey) { - results[i] = oldTargets[searchIndex]; - updateResult(newSources[i], oldTargets[searchIndex], i); - oldIndex = searchIndex + 1; - found = true; - break; - } - } - if (!found) { - results[i] = createResult(source, i); - } - } - } - results.length = newSources.length; - keys = newKeys; - } - }; - }; - /** - * Creates a [[Projector]] instance using the provided projectionOptions. - * - * For more information, see [[Projector]]. - * - * @param projectionOptions Options that influence how the DOM is rendered and updated. - */ - exports.createProjector = function (projectorOptions) { - var projector; - var projectionOptions = applyDefaultProjectionOptions(projectorOptions); - projectionOptions.eventHandlerInterceptor = function (propertyName, eventHandler, domNode, properties) { - return function () { - // intercept function calls (event handlers) to do a render afterwards. - projector.scheduleRender(); - return eventHandler.apply(properties.bind || this, arguments); - }; - }; - var renderCompleted = true; - var scheduled; - var stopped = false; - var projections = []; - var renderFunctions = []; - // matches the projections array - var doRender = function () { - scheduled = undefined; - if (!renderCompleted) { - return; // The last render threw an error, it should be logged in the browser console. - } - renderCompleted = false; - for (var i = 0; i < projections.length; i++) { - var updatedVnode = renderFunctions[i](); - projections[i].update(updatedVnode); - } - renderCompleted = true; - }; - projector = { - scheduleRender: function () { - if (!scheduled && !stopped) { - scheduled = requestAnimationFrame(doRender); - } - }, - stop: function () { - if (scheduled) { - cancelAnimationFrame(scheduled); - scheduled = undefined; - } - stopped = true; - }, - resume: function () { - stopped = false; - renderCompleted = true; - projector.scheduleRender(); - }, - append: function (parentNode, renderMaquetteFunction) { - projections.push(exports.dom.append(parentNode, renderMaquetteFunction(), projectionOptions)); - renderFunctions.push(renderMaquetteFunction); - }, - insertBefore: function (beforeNode, renderMaquetteFunction) { - projections.push(exports.dom.insertBefore(beforeNode, renderMaquetteFunction(), projectionOptions)); - renderFunctions.push(renderMaquetteFunction); - }, - merge: function (domNode, renderMaquetteFunction) { - projections.push(exports.dom.merge(domNode, renderMaquetteFunction(), projectionOptions)); - renderFunctions.push(renderMaquetteFunction); - }, - replace: function (domNode, renderMaquetteFunction) { - var vnode = renderMaquetteFunction(); - createDom(vnode, domNode.parentNode, domNode, projectionOptions); - domNode.parentNode.removeChild(domNode); - projections.push(createProjection(vnode, projectionOptions)); - renderFunctions.push(renderMaquetteFunction); - }, - detach: function (renderMaquetteFunction) { - for (var i = 0; i < renderFunctions.length; i++) { - if (renderFunctions[i] === renderMaquetteFunction) { - renderFunctions.splice(i, 1); - return projections.splice(i, 1)[0]; - } - } - throw new Error('renderMaquetteFunction was not found'); - } - }; - return projector; - }; -})); diff --git a/plugins/UiFileManager/media/list.html b/plugins/UiFileManager/media/list.html deleted file mode 100644 index 26851b4c..00000000 --- a/plugins/UiFileManager/media/list.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - List - ZeroNet - - - - - - - - -
      - - - - \ No newline at end of file diff --git a/plugins/UiPluginManager/UiPluginManagerPlugin.py b/plugins/UiPluginManager/UiPluginManagerPlugin.py deleted file mode 100644 index 1ab80f53..00000000 --- a/plugins/UiPluginManager/UiPluginManagerPlugin.py +++ /dev/null @@ -1,221 +0,0 @@ -import io -import os -import json -import shutil -import time - -from Plugin import PluginManager -from Config import config -from Debug import Debug -from Translate import Translate -from util.Flag import flag - - -plugin_dir = os.path.dirname(__file__) - -if "_" not in locals(): - _ = Translate(plugin_dir + "/languages/") - - -# Convert non-str,int,float values to str in a dict -def restrictDictValues(input_dict): - allowed_types = (int, str, float) - return { - key: val if type(val) in allowed_types else str(val) - for key, val in input_dict.items() - } - - -@PluginManager.registerTo("UiRequest") -class UiRequestPlugin(object): - def actionWrapper(self, path, extra_headers=None): - if path.strip("/") != "Plugins": - return super(UiRequestPlugin, self).actionWrapper(path, extra_headers) - - if not extra_headers: - extra_headers = {} - - script_nonce = self.getScriptNonce() - - self.sendHeader(extra_headers=extra_headers, script_nonce=script_nonce) - site = self.server.site_manager.get(config.homepage) - return iter([super(UiRequestPlugin, self).renderWrapper( - site, path, "uimedia/plugins/plugin_manager/plugin_manager.html", - "Plugin Manager", extra_headers, show_loadingscreen=False, script_nonce=script_nonce - )]) - - def actionUiMedia(self, path, *args, **kwargs): - if path.startswith("/uimedia/plugins/plugin_manager/"): - file_path = path.replace("/uimedia/plugins/plugin_manager/", plugin_dir + "/media/") - if config.debug and (file_path.endswith("all.js") or file_path.endswith("all.css")): - # If debugging merge *.css to all.css and *.js to all.js - from Debug import DebugMedia - DebugMedia.merge(file_path) - - if file_path.endswith("js"): - data = _.translateData(open(file_path).read(), mode="js").encode("utf8") - elif file_path.endswith("html"): - data = _.translateData(open(file_path).read(), mode="html").encode("utf8") - else: - data = open(file_path, "rb").read() - - return self.actionFile(file_path, file_obj=io.BytesIO(data), file_size=len(data)) - else: - return super(UiRequestPlugin, self).actionUiMedia(path) - - -@PluginManager.registerTo("UiWebsocket") -class UiWebsocketPlugin(object): - @flag.admin - def actionPluginList(self, to): - plugins = [] - for plugin in PluginManager.plugin_manager.listPlugins(list_disabled=True): - plugin_info_path = plugin["dir_path"] + "/plugin_info.json" - plugin_info = {} - if os.path.isfile(plugin_info_path): - try: - plugin_info = json.load(open(plugin_info_path)) - except Exception as err: - self.log.error( - "Error loading plugin info for %s: %s" % - (plugin["name"], Debug.formatException(err)) - ) - if plugin_info: - plugin_info = restrictDictValues(plugin_info) # For security reasons don't allow complex values - plugin["info"] = plugin_info - - if plugin["source"] != "builtin": - plugin_site = self.server.sites.get(plugin["source"]) - if plugin_site: - try: - plugin_site_info = plugin_site.storage.loadJson(plugin["inner_path"] + "/plugin_info.json") - plugin_site_info = restrictDictValues(plugin_site_info) - plugin["site_info"] = plugin_site_info - plugin["site_title"] = plugin_site.content_manager.contents["content.json"].get("title") - plugin_key = "%s/%s" % (plugin["source"], plugin["inner_path"]) - plugin["updated"] = plugin_key in PluginManager.plugin_manager.plugins_updated - except Exception: - pass - - plugins.append(plugin) - - return {"plugins": plugins} - - @flag.admin - @flag.no_multiuser - def actionPluginConfigSet(self, to, source, inner_path, key, value): - plugin_manager = PluginManager.plugin_manager - plugins = plugin_manager.listPlugins(list_disabled=True) - plugin = None - for item in plugins: - if item["source"] == source and item["inner_path"] in (inner_path, "disabled-" + inner_path): - plugin = item - break - - if not plugin: - return {"error": "Plugin not found"} - - config_source = plugin_manager.config.setdefault(source, {}) - config_plugin = config_source.setdefault(inner_path, {}) - - if key in config_plugin and value is None: - del config_plugin[key] - else: - config_plugin[key] = value - - plugin_manager.saveConfig() - - return "ok" - - def pluginAction(self, action, address, inner_path): - site = self.server.sites.get(address) - plugin_manager = PluginManager.plugin_manager - - # Install/update path should exists - if action in ("add", "update", "add_request"): - if not site: - raise Exception("Site not found") - - if not site.storage.isDir(inner_path): - raise Exception("Directory not found on the site") - - try: - plugin_info = site.storage.loadJson(inner_path + "/plugin_info.json") - plugin_data = (plugin_info["rev"], plugin_info["description"], plugin_info["name"]) - except Exception as err: - raise Exception("Invalid plugin_info.json: %s" % Debug.formatExceptionMessage(err)) - - source_path = site.storage.getPath(inner_path) - - target_path = plugin_manager.path_installed_plugins + "/" + address + "/" + inner_path - plugin_config = plugin_manager.config.setdefault(site.address, {}).setdefault(inner_path, {}) - - # Make sure plugin (not)installed - if action in ("add", "add_request") and os.path.isdir(target_path): - raise Exception("Plugin already installed") - - if action in ("update", "remove") and not os.path.isdir(target_path): - raise Exception("Plugin not installed") - - # Do actions - if action == "add": - shutil.copytree(source_path, target_path) - - plugin_config["date_added"] = int(time.time()) - plugin_config["rev"] = plugin_info["rev"] - plugin_config["enabled"] = True - - if action == "update": - shutil.rmtree(target_path) - - shutil.copytree(source_path, target_path) - - plugin_config["rev"] = plugin_info["rev"] - plugin_config["date_updated"] = time.time() - - if action == "remove": - del plugin_manager.config[address][inner_path] - shutil.rmtree(target_path) - - def doPluginAdd(self, to, inner_path, res): - if not res: - return None - - self.pluginAction("add", self.site.address, inner_path) - PluginManager.plugin_manager.saveConfig() - - self.cmd( - "confirm", - ["Plugin installed!
      You have to restart the client to load the plugin", "Restart"], - lambda res: self.actionServerShutdown(to, restart=True) - ) - - self.response(to, "ok") - - @flag.no_multiuser - def actionPluginAddRequest(self, to, inner_path): - self.pluginAction("add_request", self.site.address, inner_path) - plugin_info = self.site.storage.loadJson(inner_path + "/plugin_info.json") - warning = "Warning!
      Plugins has the same permissions as the ZeroNet client.
      " - warning += "Do not install it if you don't trust the developer.
      " - - self.cmd( - "confirm", - ["Install new plugin: %s?
      %s" % (plugin_info["name"], warning), "Trust & Install"], - lambda res: self.doPluginAdd(to, inner_path, res) - ) - - @flag.admin - @flag.no_multiuser - def actionPluginRemove(self, to, address, inner_path): - self.pluginAction("remove", address, inner_path) - PluginManager.plugin_manager.saveConfig() - return "ok" - - @flag.admin - @flag.no_multiuser - def actionPluginUpdate(self, to, address, inner_path): - self.pluginAction("update", address, inner_path) - PluginManager.plugin_manager.saveConfig() - PluginManager.plugin_manager.plugins_updated["%s/%s" % (address, inner_path)] = True - return "ok" diff --git a/plugins/UiPluginManager/__init__.py b/plugins/UiPluginManager/__init__.py deleted file mode 100644 index d29ae44b..00000000 --- a/plugins/UiPluginManager/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import UiPluginManagerPlugin diff --git a/plugins/UiPluginManager/media/css/PluginManager.css b/plugins/UiPluginManager/media/css/PluginManager.css deleted file mode 100644 index 30f36717..00000000 --- a/plugins/UiPluginManager/media/css/PluginManager.css +++ /dev/null @@ -1,75 +0,0 @@ -body { background-color: #EDF2F5; font-family: Roboto, 'Segoe UI', Arial, 'Helvetica Neue'; margin: 0px; padding: 0px; backface-visibility: hidden; } -h1, h2, h3, h4 { font-family: 'Roboto', Arial, sans-serif; font-weight: 200; font-size: 30px; margin: 0px; padding: 0px } -h1 { background: linear-gradient(33deg,#af3bff,#0d99c9); color: white; padding: 16px 30px; } -h2 { margin-top: 10px; } -h3 { font-weight: normal } -h4 { font-size: 19px; font-weight: lighter; margin-right: 100px; margin-top: 30px; } -a { color: #9760F9 } -a:hover { text-decoration: none } - -.link { background-color: transparent; outline: 5px solid transparent; transition: all 0.3s } -.link:active { background-color: #EFEFEF; outline: 5px solid #EFEFEF; transition: none } - -.content { max-width: 800px; margin: auto; background-color: white; padding: 60px 20px; box-sizing: border-box; padding-bottom: 150px; } -.section { margin: 0px 10%; } -.plugins { font-size: 19px; margin-top: 25px; margin-bottom: 75px; } -.plugin { transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); position: relative; padding-bottom: 20px; padding-top: 10px; } -.plugin.hidden { opacity: 0; height: 0px; padding: 0px; } -.plugin .title { display: inline-block; line-height: 36px; } -.plugin .title h3 { font-size: 20px; font-weight: lighter; margin-right: 100px; } -.plugin .title .version { font-size: 70%; margin-left: 5px; } -.plugin .title .version .version-latest { color: #2ecc71; font-weight: normal; } -.plugin .title .version .version-missing { color: #ffa200; font-weight: normal; } -.plugin .title .version .version-update { padding: 0px 15px; margin-left: 5px; line-height: 28px; } -.plugin .description { font-size: 14px; color: #666; line-height: 24px; } -.plugin .description .source { color: #999; font-size: 90%; } -.plugin .description .source a { color: #666; } -.plugin .value { display: inline-block; white-space: nowrap; } -.plugin .value-right { right: 0px; position: absolute; } -.plugin .value-fullwidth { width: 100% } -.plugin .marker { - font-weight: bold; text-decoration: none; font-size: 25px; position: absolute; padding: 2px 15px; line-height: 32px; - opacity: 0; pointer-events: none; transition: all 0.6s; transform: scale(2); color: #9760F9; -} -.plugin .marker.visible { opacity: 1; pointer-events: all; transform: scale(1); } -.plugin .marker.changed { color: #2ecc71; } -.plugin .marker.pending { color: #ffa200; } - - -.input-text, .input-select { padding: 8px 18px; border: 1px solid #CCC; border-radius: 3px; font-size: 17px; box-sizing: border-box; } -.input-text:focus, .input-select:focus { border: 1px solid #3396ff; outline: none; } -.input-textarea { overflow-x: auto; overflow-y: hidden; white-space: pre; line-height: 22px; } - -.input-select { width: initial; font-size: 14px; padding-right: 10px; padding-left: 10px; } - -.value-right .input-text { text-align: right; width: 100px; } -.value-fullwidth .input-text { width: 100%; font-size: 14px; font-family: 'Segoe UI', Arial, 'Helvetica Neue'; } -.value-fullwidth { margin-top: 10px; } - -/* Checkbox */ -.checkbox-skin { background-color: #CCC; width: 50px; height: 24px; border-radius: 15px; transition: all 0.3s ease-in-out; display: inline-block; } -.checkbox-skin:before { - content: ""; position: relative; width: 20px; background-color: white; height: 20px; display: block; border-radius: 100%; margin-top: 2px; margin-left: 2px; - transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); -} -.checkbox { font-size: 14px; font-weight: normal; display: inline-block; cursor: pointer; margin-top: 5px; } -.checkbox .title { display: inline; line-height: 30px; vertical-align: 4px; margin-left: 11px } -.checkbox.checked .checkbox-skin:before { margin-left: 27px; } -.checkbox.checked .checkbox-skin { background-color: #2ECC71 } - -/* Bottom */ - -.bottom { - width: 100%; text-align: center; background-color: #ffffffde; padding: 25px; bottom: -120px; -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px); - transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); position: fixed; backface-visibility: hidden; box-sizing: border-box; -} -.bottom-content { max-width: 750px; width: 100%; margin: 0px auto; } -.bottom .button { float: right; } -.bottom.visible { bottom: 0px; box-shadow: 0px 0px 35px #dcdcdc; } -.bottom .title { padding: 10px 10px; color: #363636; float: left; text-transform: uppercase; letter-spacing: 1px; } -.bottom .title:before { content: "•"; display: inline-block; color: #2ecc71; font-size: 31px; vertical-align: -7px; margin-right: 8px; line-height: 25px; } -.bottom-restart .title:before { color: #ffa200; } - -.animate { transition: all 0.3s ease-out !important; } -.animate-back { transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; } -.animate-inout { transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; } \ No newline at end of file diff --git a/plugins/UiPluginManager/media/css/all.css b/plugins/UiPluginManager/media/css/all.css deleted file mode 100644 index ba72fa0d..00000000 --- a/plugins/UiPluginManager/media/css/all.css +++ /dev/null @@ -1,129 +0,0 @@ - -/* ---- PluginManager.css ---- */ - - -body { background-color: #EDF2F5; font-family: Roboto, 'Segoe UI', Arial, 'Helvetica Neue'; margin: 0px; padding: 0px; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden ; } -h1, h2, h3, h4 { font-family: 'Roboto', Arial, sans-serif; font-weight: 200; font-size: 30px; margin: 0px; padding: 0px } -h1 { background: -webkit-linear-gradient(33deg,#af3bff,#0d99c9);background: -moz-linear-gradient(33deg,#af3bff,#0d99c9);background: -o-linear-gradient(33deg,#af3bff,#0d99c9);background: -ms-linear-gradient(33deg,#af3bff,#0d99c9);background: linear-gradient(33deg,#af3bff,#0d99c9); color: white; padding: 16px 30px; } -h2 { margin-top: 10px; } -h3 { font-weight: normal } -h4 { font-size: 19px; font-weight: lighter; margin-right: 100px; margin-top: 30px; } -a { color: #9760F9 } -a:hover { text-decoration: none } - -.link { background-color: transparent; outline: 5px solid transparent; -webkit-transition: all 0.3s ; -moz-transition: all 0.3s ; -o-transition: all 0.3s ; -ms-transition: all 0.3s ; transition: all 0.3s } -.link:active { background-color: #EFEFEF; outline: 5px solid #EFEFEF; -webkit-transition: none ; -moz-transition: none ; -o-transition: none ; -ms-transition: none ; transition: none } - -.content { max-width: 800px; margin: auto; background-color: white; padding: 60px 20px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; padding-bottom: 150px; } -.section { margin: 0px 10%; } -.plugins { font-size: 19px; margin-top: 25px; margin-bottom: 75px; } -.plugin { -webkit-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -moz-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -o-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -ms-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1) ; position: relative; padding-bottom: 20px; padding-top: 10px; } -.plugin.hidden { opacity: 0; height: 0px; padding: 0px; } -.plugin .title { display: inline-block; line-height: 36px; } -.plugin .title h3 { font-size: 20px; font-weight: lighter; margin-right: 100px; } -.plugin .title .version { font-size: 70%; margin-left: 5px; } -.plugin .title .version .version-latest { color: #2ecc71; font-weight: normal; } -.plugin .title .version .version-missing { color: #ffa200; font-weight: normal; } -.plugin .title .version .version-update { padding: 0px 15px; margin-left: 5px; line-height: 28px; } -.plugin .description { font-size: 14px; color: #666; line-height: 24px; } -.plugin .description .source { color: #999; font-size: 90%; } -.plugin .description .source a { color: #666; } -.plugin .value { display: inline-block; white-space: nowrap; } -.plugin .value-right { right: 0px; position: absolute; } -.plugin .value-fullwidth { width: 100% } -.plugin .marker { - font-weight: bold; text-decoration: none; font-size: 25px; position: absolute; padding: 2px 15px; line-height: 32px; - opacity: 0; pointer-events: none; -webkit-transition: all 0.6s; -moz-transition: all 0.6s; -o-transition: all 0.6s; -ms-transition: all 0.6s; transition: all 0.6s ; -webkit-transform: scale(2); -moz-transform: scale(2); -o-transform: scale(2); -ms-transform: scale(2); transform: scale(2) ; color: #9760F9; -} -.plugin .marker.visible { opacity: 1; pointer-events: all; -webkit-transform: scale(1); -moz-transform: scale(1); -o-transform: scale(1); -ms-transform: scale(1); transform: scale(1) ; } -.plugin .marker.changed { color: #2ecc71; } -.plugin .marker.pending { color: #ffa200; } - - -.input-text, .input-select { padding: 8px 18px; border: 1px solid #CCC; -webkit-border-radius: 3px; -moz-border-radius: 3px; -o-border-radius: 3px; -ms-border-radius: 3px; border-radius: 3px ; font-size: 17px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; } -.input-text:focus, .input-select:focus { border: 1px solid #3396ff; outline: none; } -.input-textarea { overflow-x: auto; overflow-y: hidden; white-space: pre; line-height: 22px; } - -.input-select { width: initial; font-size: 14px; padding-right: 10px; padding-left: 10px; } - -.value-right .input-text { text-align: right; width: 100px; } -.value-fullwidth .input-text { width: 100%; font-size: 14px; font-family: 'Segoe UI', Arial, 'Helvetica Neue'; } -.value-fullwidth { margin-top: 10px; } - -/* Checkbox */ -.checkbox-skin { background-color: #CCC; width: 50px; height: 24px; -webkit-border-radius: 15px; -moz-border-radius: 15px; -o-border-radius: 15px; -ms-border-radius: 15px; border-radius: 15px ; -webkit-transition: all 0.3s ease-in-out; -moz-transition: all 0.3s ease-in-out; -o-transition: all 0.3s ease-in-out; -ms-transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out ; display: inline-block; } -.checkbox-skin:before { - content: ""; position: relative; width: 20px; background-color: white; height: 20px; display: block; -webkit-border-radius: 100%; -moz-border-radius: 100%; -o-border-radius: 100%; -ms-border-radius: 100%; border-radius: 100% ; margin-top: 2px; margin-left: 2px; - -webkit-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); -moz-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); -o-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); -ms-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86) ; -} -.checkbox { font-size: 14px; font-weight: normal; display: inline-block; cursor: pointer; margin-top: 5px; } -.checkbox .title { display: inline; line-height: 30px; vertical-align: 4px; margin-left: 11px } -.checkbox.checked .checkbox-skin:before { margin-left: 27px; } -.checkbox.checked .checkbox-skin { background-color: #2ECC71 } - -/* Bottom */ - -.bottom { - width: 100%; text-align: center; background-color: #ffffffde; padding: 25px; bottom: -120px; -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px); - -webkit-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -moz-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -o-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -ms-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1) ; position: fixed; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden ; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; -} -.bottom-content { max-width: 750px; width: 100%; margin: 0px auto; } -.bottom .button { float: right; } -.bottom.visible { bottom: 0px; -webkit-box-shadow: 0px 0px 35px #dcdcdc; -moz-box-shadow: 0px 0px 35px #dcdcdc; -o-box-shadow: 0px 0px 35px #dcdcdc; -ms-box-shadow: 0px 0px 35px #dcdcdc; box-shadow: 0px 0px 35px #dcdcdc ; } -.bottom .title { padding: 10px 10px; color: #363636; float: left; text-transform: uppercase; letter-spacing: 1px; } -.bottom .title:before { content: "•"; display: inline-block; color: #2ecc71; font-size: 31px; vertical-align: -7px; margin-right: 8px; line-height: 25px; } -.bottom-restart .title:before { color: #ffa200; } - -.animate { -webkit-transition: all 0.3s ease-out !important; -moz-transition: all 0.3s ease-out !important; -o-transition: all 0.3s ease-out !important; -ms-transition: all 0.3s ease-out !important; transition: all 0.3s ease-out !important ; } -.animate-back { -webkit-transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; -moz-transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; -o-transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; -ms-transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important ; } -.animate-inout { -webkit-transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; -moz-transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; -o-transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; -ms-transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important ; } - -/* ---- button.css ---- */ - - -/* Button */ -.button { - background-color: #FFDC00; color: black; padding: 10px 20px; display: inline-block; background-position: left center; - -webkit-border-radius: 2px; -moz-border-radius: 2px; -o-border-radius: 2px; -ms-border-radius: 2px; border-radius: 2px ; border-bottom: 2px solid #E8BE29; -webkit-transition: all 0.5s ease-out; -moz-transition: all 0.5s ease-out; -o-transition: all 0.5s ease-out; -ms-transition: all 0.5s ease-out; transition: all 0.5s ease-out ; text-decoration: none; -} -.button:hover { border-color: white; border-bottom: 2px solid #BD960C; -webkit-transition: none ; -moz-transition: none ; -o-transition: none ; -ms-transition: none ; transition: none ; background-color: #FDEB07 } -.button:active { position: relative; top: 1px } -.button.loading { - color: rgba(0,0,0,0); background: #999 url(../img/loading.gif) no-repeat center center; - -webkit-transition: all 0.5s ease-out ; -moz-transition: all 0.5s ease-out ; -o-transition: all 0.5s ease-out ; -ms-transition: all 0.5s ease-out ; transition: all 0.5s ease-out ; pointer-events: none; border-bottom: 2px solid #666 -} -.button.disabled { color: #DDD; background-color: #999; pointer-events: none; border-bottom: 2px solid #666 } - -/* ---- fonts.css ---- */ - - -/* Base64 encoder: http://www.motobit.com/util/base64-decoder-encoder.asp */ -/* Generated by Font Squirrel (http://www.fontsquirrel.com) on January 21, 2015 */ - - -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 400; - src: - local('Roboto'), - url(data:application/x-font-woff;charset=utf-8;base64,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) format('woff'); -} - -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: bold; - src: - local('Roboto Medium'), - url(data:application/x-font-woff;charset=utf-8;base64,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) format('woff'), -} - -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 200; - src: - local('Roboto Light'), - url(data:application/x-font-woff;charset=utf-8;base64,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) format('woff'); -} \ No newline at end of file diff --git a/plugins/UiPluginManager/media/css/button.css b/plugins/UiPluginManager/media/css/button.css deleted file mode 100644 index 9f46d478..00000000 --- a/plugins/UiPluginManager/media/css/button.css +++ /dev/null @@ -1,12 +0,0 @@ -/* Button */ -.button { - background-color: #FFDC00; color: black; padding: 10px 20px; display: inline-block; background-position: left center; - border-radius: 2px; border-bottom: 2px solid #E8BE29; transition: all 0.5s ease-out; text-decoration: none; -} -.button:hover { border-color: white; border-bottom: 2px solid #BD960C; transition: none ; background-color: #FDEB07 } -.button:active { position: relative; top: 1px } -.button.loading { - color: rgba(0,0,0,0); background: #999 url(../img/loading.gif) no-repeat center center; - transition: all 0.5s ease-out ; pointer-events: none; border-bottom: 2px solid #666 -} -.button.disabled { color: #DDD; background-color: #999; pointer-events: none; border-bottom: 2px solid #666 } \ No newline at end of file diff --git a/plugins/UiPluginManager/media/css/fonts.css b/plugins/UiPluginManager/media/css/fonts.css deleted file mode 100644 index f5576c5a..00000000 --- a/plugins/UiPluginManager/media/css/fonts.css +++ /dev/null @@ -1,30 +0,0 @@ -/* Base64 encoder: http://www.motobit.com/util/base64-decoder-encoder.asp */ -/* Generated by Font Squirrel (http://www.fontsquirrel.com) on January 21, 2015 */ - - -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 400; - src: - local('Roboto'), - url(data:application/x-font-woff;charset=utf-8;base64,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) format('woff'); -} - -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: bold; - src: - local('Roboto Medium'), - url(data:application/x-font-woff;charset=utf-8;base64,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) format('woff'), -} - -@font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 200; - src: - local('Roboto Light'), - url(data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAAEScABMAAAAAdFQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAABqAAAABwAAAAcXzC5yUdERUYAAAHEAAAAHgAAACAAzgAER1BPUwAAAeQAAAVxAAANIkezYOlHU1VCAAAHWAAAACwAAAAwuP+4/k9TLzIAAAeEAAAAVgAAAGC3ouDrY21hcAAAB9wAAAG+AAACioYHy/VjdnQgAAAJnAAAADQAAAA0CnAOGGZwZ20AAAnQAAABsQAAAmVTtC+nZ2FzcAAAC4QAAAAIAAAACAAAABBnbHlmAAALjAAAMaIAAFTUMXgLR2hlYWQAAD0wAAAAMQAAADYBsFYkaGhlYQAAPWQAAAAfAAAAJA7cBhlobXR4AAA9hAAAAeEAAAKEbjk+b2xvY2EAAD9oAAABNgAAAUQwY0cibWF4cAAAQKAAAAAgAAAAIAG+AZluYW1lAABAwAAAAZAAAANoT6qDDHBvc3QAAEJQAAABjAAAAktoPRGfcHJlcAAAQ9wAAAC2AAABI0qzIoZ3ZWJmAABElAAAAAYAAAAGVU1R3QAAAAEAAAAAzD2izwAAAADE8BEuAAAAAM4DBct42mNgZGBg4ANiCQYQYGJgBMIFQMwC5jEAAAsqANMAAHjapZZ5bNRFFMff79dtd7u03UNsORWwKYhWGwFLsRBiGuSKkdIDsBg0kRCVGq6GcpSEFINKghzlMDFBVBITNRpDJEGCBlBBRSEQIQYJyLHd/pA78a99fn6zy3ZbykJxXr7zm3nz5s2b7xy/EUtE/FIiY8SuGDe5SvLeeHlhvfQRD3pRFbc9tWy9/ur8evG5JQOP2Hxt8ds7xLJrjO1AmYxUyiyZLQtlpayRmOWx/FbQGmSVWM9aVdZs6z1rk/WZFbU9dtgutIeCsVivND1dsWSG9JAMKZOeMkrCUi756MI6AN0g3Se1ellm6GlqOXpBxuoNmYXGlgn6D/qo9JOA5ksIFOoBKY79K6V4qtC/ZJy2yXNgPJgIKkEVqMbPNHpO14jUgXr6LcK+gbbFoBEsoX0pWE55Bd8W/G8BW9WNboZ+b/KPyWslDy5K9biU6TkZpY6U6ymiLdUv0Vyi9jvt1boT+x9lTmyXzNUhaHKIcqyEaDkLfw8YTQBNDpo2NHmsVjZtrl2u/kZLmDlHaT0BJ1HTZ45+gbdfTSznJVOK4WQkWAAWgiYQQB/EVzAxYhheIvASgZcIvETgJGK8NfDdgN1GsAlsBllYO1g7WDtYO1g7WDrMcAK+a2UA6xci+kp0i0EjWA4s2nMZO6DNrE4zDDbDYDMMNptIHSJ1iNQhUodI3R4DafGzG8JSKEUyRB6VJ+RJGSbDZQSrWsb+KJfR7OAJ8rxUM/Z0xq6Tl6Re3iTyjUS9WezsQ+7e9L7j24G//uznFl2th/WAOrqPNelG0hq5z6Srk6Ub4Kau0Mv6qe7W7ZQPsxIhPcgeX3sPns6DCDjYSX/9rj3/7ka8bbeNGQXHE/UzyZb3Naqtt/W+FAepZ1J3mVOWPoW7ipYzFE8hSiE3Erfcabyo/I+kF7TVzPBMiq6VU3Wr/FGy9F2y1MD5aLfeG7ukh3SKztOQHtOldxmvgTW/3uWKBeLrqifdSuxbPeNypiOTPb/StfqBbgBrYCOIKkifoH6ou3S//oxFky4jLzLWvTSoV/RrU96pR/UY36Mdx9VzerNDbA+b/M8UzXE97TKTYCcvdY079Fxl8v2duY3vJb3Y3lvbjK+QWdMjScujKb226ze6V0+AH9gHId3G3ghxPk5yZs+m2BVzo4j+otuYZ3wX5ibGa4uP3R5tYufcaU32pGm7er+ninU2ffVaVz47Mt+tHXstTVvae0Cv3PeYTjqG4n5v927ukWDyTnDucuZXdXEerpqzcsc10D9M3nKnmNPFnZ6n7nOlY/RxrdBhYDA7yovKyx/Mq5N0vr6l67EIaA4ne4k5369QP6Kvpd4r8RRjZ+hP4PPkPrp4i832qOJ/AP1E1+ke7uE9nPDWJJ+Jrx4Cu92zEZtr6m93h6H2O7CDtjENA6eSpZOdzwL/84C8m3g93kuyeVN44C/L1LyIT7J5D3gNqz0SVjloc7lZuAc7/RfC3NHu/+dBU8tP6vORAnN/90poeoM+5H3vIaYsM3omo/oYwfVdgLgpk6+vWxvGSuQWfkuMV4v5+Q1TAaIMIr2ZVYhyIWLzCipijKGIT4qRPvIU4uNFNJz8aaQvL6NSeBqJ+HkjlcHUKCRHnkEKeDGVw9dopJdUIBkyTsbD80TEIy/IFKKoRLJkKpIpVYhHahCvTEPyeGVNJ7oXkX68tuooz0SCvLrqiXCezCeSBbz//bIIyZAGxCOLpRGfS2QpHpYhPlmOZEkT4pcVSJ6sk/XM1325WdKC5JsXnCVbZCtlG75djiSFI9uwkwE37hv6Md6G2cx+NJYVzKs3MxtPlJOQ/sxtqjzEO7FaBpk5PMIMZtKznvgGm/hKiKsJPjcw3oj/AIgWgIQAAAB42mNgZGBg4GLQYdBjYHJx8wlh4MtJLMljkGBgAYoz/P8PJBAsIAAAnsoHa3jaY2BmvsGow8DKwMI6i9WYgYFRHkIzX2RIY2JgYABhCHjAwPQ/gEEhGshUAPHd8/PTgRTvAwa2tH9pDAwcSUzBCgyM8/0ZGRhYrFg3gNUxAQCExA4aAAB42mNgYGBmgGAZBkYgycDYAuQxgvksjBlAOozBgYGVQQzI4mWoY1jAsJhhKcNKhtUM6xi2MOxg2M1wkOEkw1mGywzXGG4x3GF4yPCS4S3DZ4ZvDL8Y/jAGMhYyHWO6xXRHgUtBREFKQU5BTUFfwUohXmGNotIDhv//QTYCzVUAmrsIaO4KoLlriTA3gLEAai6DgoCChIIM2FxLJHMZ/3/9//j/of8H/x/4v+//3v97/m//v+X/pv9r/y/7v/j/vP9z/s/8P+P/lP+9/7v+t/5v/t/wv/6/zn++v7v+Lv+77EHzg7oH1Q+qHhQ/yH6Q9MDu/qf7tQoLIOFDC8DIxgA3nJEJSDChKwBGEQsrGzsHJxc3Dy8fv4CgkLCIqJi4hKSUtIysnLyCopKyiqqauoamlraOrp6+gaGRsYmpmbmFpZW1ja2dvYOjk7OLq5u7h6eXt4+vn39AYFBwSGhYeERkVHRMbFx8QiLIlnyGopJSiIVlQFwOYlQwMFQyVDEwVDMwJKeABLLS52enQZ2ViumVjNyZSWDGxEnTpk+eAmbOmz0HRE2dASTyGBgKgFQhEBcDcUMTkGjMARIAqVuf0QAAAAAEOgWvAGYAqABiAGUAZwBoAGkAagBrAHUApABcAHgAZQBsAHIAeAB8AHAAegBaAEQFEXjaXVG7TltBEN0NDwOBxNggOdoUs5mQxnuhBQnE1Y1iZDuF5QhpN3KRi3EBH0CBRA3arxmgoaRImwYhF0h8Qj4hEjNriKI0Ozuzc86ZM0vKkap36WvPU+ckkMLdBs02/U5ItbMA96Tr642MtIMHWmxm9Mp1+/4LBpvRlDtqAOU9bykPGU07gVq0p/7R/AqG+/wf8zsYtDTT9NQ6CekhBOabcUuD7xnNussP+oLV4WIwMKSYpuIuP6ZS/rc052rLsLWR0byDMxH5yTRAU2ttBJr+1CHV83EUS5DLprE2mJiy/iQTwYXJdFVTtcz42sFdsrPoYIMqzYEH2MNWeQweDg8mFNK3JMosDRH2YqvECBGTHAo55dzJ/qRA+UgSxrxJSjvjhrUGxpHXwKA2T7P/PJtNbW8dwvhZHMF3vxlLOvjIhtoYEWI7YimACURCRlX5hhrPvSwG5FL7z0CUgOXxj3+dCLTu2EQ8l7V1DjFWCHp+29zyy4q7VrnOi0J3b6pqqNIpzftezr7HA54eC8NBY8Gbz/v+SoH6PCyuNGgOBEN6N3r/orXqiKu8Fz6yJ9O/sVoAAAAAAQAB//8AD3jarXwHfBRl+v/7TtuWLbMlm54smwIJJLBLCKGJCOqJgIp6NBEiiUgNiCb0IgiIFU9FkKCABKXNbAIqcoAUC3Y9I6ioh5yaE8RT9CeQHf7P885sCgS4/+/zE7OZzO7O+z79+5QZwpG+hHBjxNsIT0wkX6WkoEfEJCScDKmS+FWPCM/BIVF5PC3i6YhJSmzoEaF4PiwH5KyAHOjLZWiZdIU2Vrzt7Ka+wvsELkmqCKHtRYVdt4BE4FyeSoX6iMiRPKqYCxShTiEh1eSsV7iQaqF5RBWp7FaE4o6dwoVhHy+H5apHH6iorqZf85805OM15wrd6edSAhGJjfSCa1KSp0jhWk4gFiFPMYeoEleg0DpVcNXXii6SBCcFl2qieaoVztjYGdUOS3XslExxjbAHX+fyZYFqoTQgdCfnvz6snaPcl/AK611DiLAGaEgm6fRmEkkCGiK++MRwOBwxARkRsy0OjmsJTTLZ82o4OSU10x9WiaO+xutPSM70h2pFgb3Fu9LS8S1RrK+RLFY7vEWVjAIlqU5NdNUrifomza76iMlszavpbRIsQI9LjYezPjjri8ezPg+c9blUG5yNc9WrAZqndEna2etfp3OJL8+6s9e3p514oCS5argkkwfWZa8SvsIiNZZEMxzEu2qs8TYPXqrG7ouDD7jYq8xevfiKn/Gzz8C3Eti34JrJseukxK6Tip+pSYt9Mh3P871dHI9EumTkQkpqWnr+Bf8pvZNABJ7CgCcAP2Eef8K+IB/wBfigB3+K4K1rqGuwVk/bDRoziHaDl3/9z2ByXjs1YMwA7S14uY92G6y9SVfeQV8bRZ/X2M8o7bo7tDK6En/gPKggqTzfkY9Kj5AO5CkSyQMJKm1BDub6SJ6IPM3LteRFZBCm4g2rKZb6iJyCp2W3BbQ0v0Bx1KnpoKIko05WOXe9ku5SZWB7bkj1guDahhSvSzXDicSQmuWsV/3uerUAxCOngyrHFSteucYmprTJ9BcrZrcSLCZqiii7txPq8CdkwVngQlHYGx8OdSnsnJ2TTws7dykClUyjThrsnB1sI/m88f406vNKJl+wMJ9W8uWHHvvblsd3fPT225vLtu3l+PLnH//bs0ve+PCtj5TS7afoc5L63KqKSQ9f3WfnS2vfcxw65Pr+gLhi96r7py7r3e+V6g1vOXb/3fYxWNCk8z+JC8WDxI7aDdzpTh7S+aN2ctRHBOCImuCor+2amSfY89SucCjb2KHsqKdKjwKF1KkOYIHDpXp13UWFzYDDfDjMd6md4bAtaGlP+O11yO4am5ACRlCsds6HP1Iz89LgD6J27SS71ZT04mI1QYaj1LRiZArwIRyKT6VeKdgmu4gxqCfVGeKhfpp1mfcnrZ43d/Vzc+ZXjbprxNDRJcOG3VXLvXVDtJjOgTeqVsMbo0v0N0qE/gPmbt06d8CcLVvmDJk1a8iAIXPmDGmQhakdzz26euCcrVvnDIy9NXD4jJnDCHiz4ed/El4DvrUhHUlPUkEiKegVMpBx2VJ9xIqM684Di3oxFgVBeYK6eXeCw04utSsc2kGT7C7VB4fxcr16FfxGPmy3ChnZHWRkks8OTHInprZjTOqeLbt3EJM9MbVDZ11rOne5ijJ1ATaAdjgp7QUeDdTEbwrmOGgjV4rgUzkmB/WAHhXBRxiPhj+x1HnzwMiqx18adtsa+lynLpP+0u81bumM2w7d9/Hpyk1rR2y7VisRTVzBtEEPXXW12q3TPSPLJtN7K98YYxvz4l+rNq+dOWzB1TO09OuUMfM+/+th8ZGBt9ZFZlVffw09JpqEzJEruEN9Hr1pYYeSroPGLgAbnCb0IceY387WvbbhsqkiXeCvkVGN3nmauSxb6EOt7+3XThK05Ye1TtxEaSiRiYdQxc0YbAWr87AveQpdpCidSpzsc7mBDdnkYRq/SUp64vDhJ5KkLdoJrqeTjud6l9C/3B39Vdvu1bZHfx1/7RiuM17brXWivza/Nl+n2puu3cUtF7q4nKJwPIHLE1PQ/fiRow8nSS/TeO3EZkmrKOPc9EYv/QvnK7u2JLpXe8qpPRx9bwzbdyo3m78B4oiD3EMgpIKzoQVUcbL9cyB7EczExZy5kp1EIQjnv0NUQvPfQfd+ovP+TPTqDoW4FMdeQaEuhdvLqZwjP58qDnSmVBU58Dc20BQeY6jE/IrIh/ksv+gx2WiOJzWD3iiMNdO+Aa3mm9vq3rvtiHBr6Uw6VVs2t/Re7YuraCft4560PWH77U+WC52EHRBlbyEKKVBMYZXa6hUxBMJD70is4DQpwUPKo6OEsGutY3EcdFwIRSxWfM9igo9ZLXhoJZZY5AW3D6EdXL0clPvTyHT6utZvOjetnH6i5ZdrafSYvofBmkadZBfoTBbuATXG2kxjQDJoUwKSKxY3qszgfhXj4Iv+6pe1E/p1OnHdOBe3Biy3DV5HpVI9/lBFKAAW59XyXtREwB7G3nyd6Ddct9JS/G41vHQk6+G77WIIxl7feICXQAny3nr2o18CsUv10vXr8ftp5x/g/s0wkEwAMiHwgVX1z/lpmKZxoyZEX5gtdTjzKcNMi8G3BA2f3I1EbLiQLMW8MTqVFN3vOpv8LjAi1fCwqk0oRlZ4ZJc7HHInUhcXbMN59PAi695x8ekjR/44feTw/1SqGzZsU6qrt3KFtB9NpCHtA+0H7XXte+0j2omavv799Dd0/Lf/+c+3QMeu82e4DWItyKI7iQjo7zjcEeVcGXsLEO8wsQjACidslkeBC9SiGzNoMxMRMjcLRL6L/rtSNN865Gw/sRvyaDJgLBloToKjiAMptgHFaCRqPF8fiWdXi09CLUvWAZPMABPYpSrBcpIHPyDZQdU8Eh56HLByCrzrSZTdEd5mLQamqDbgj+IsVuLliEQ8xSzIZBvO00T9oI6FNOYefcHJ4h+f7Dr2zGJtMsf93FBJjy6c+OzDGzZPFjw7Gg7vqPyfFVo3sXQEl/rUOyOWrH91JdIx9vxP/GmgIxe0JtIW6RCBDrEtbkkEZkRSkCQvkORlCMObYMmrtce1TYGQakfR5unuACID51L8iDcS4DihADEFnEKUgRBDyXIp6fiuDMdyAaKTiJzOMEscEN4ewYcfYgegjrYsdsQB4FBJVnGxYpeVNgBJ3GpienFL5JEHxsMOGPU5jYxhyCPYJnMsV/7Gs6u27nhp2bI161eueLimnBP/3L3/h3nTliw+d3CP9jNdJC1TXnj62SfL1sxesvbFxdLLx+p23729fc5rc/Z9fQR1ux/IuT/YgpU4yRASscS0qJbYLJwdgDoAZ6lekQAYuwoUS50SF0LlVvhQxMxciFkCJloYPLagN5FRuWyoXLRY4WTFwVSMhmVAkqBnkJjkmPpxax44frwi+h2XKoVpeV++oSGrVHuclpfyvbiJzD9sBZszw77SyX4SSW2UW2qj3FwoN4+tvsaR6jLn1fptqS4Qmd9WzxC8s64myUkceSoHcRxFlOSMAXPmyx1O9OVOh+7Lr9p8ZjH6clFxuhTXXjBixbN351UP/tkVztpqvA6PJy8CrxkPZTwUlEBli4nizacRl8erw2aqmtHTpxYrSaABbtRsB8g3QsxJxRfIFERpyvEgpO5Fi7q4fV5wBtlbufHVy9a+8MITDz8ZGH0ztz+6rkvRwik7jx/9uvYXOl168rkDO9cdHDrMxadOjp4JdeH58+TwUe3PdwjzTyuAV+nMVnPIXSSSgNxKi/knG19f685MQIjoFoE5bZk+J6OrCinJLmSK6gPmtIPfgWTQUMHkTmAampkGGupzAgS0uYE4c7EiyIoJqZE7E9BEvykfAI2UCgYKbo0RQoqak7mCpn3cf3lxenH5wLWf9dg55cDx3w+8o52r3Pv08m0vV03fHuBS6OQG2qtNRklGWsP78weO1H498rn2I23f8PGv/3pxW92cu5guDAAdRV2II51JxIwaik5bJWie9gLFXIfpaixFg8CnOlAHiRk2zRfr0cNKeVOwyE08A/jXT5zNtVXacqn5C/GGsjLtx+gebemMGXQq91dqIoglxwA/7cBPPwlCjnw/ifiQo8nAUQuu2wE4mhPwWYCjObiFjoyjCcBRCR1AJhwkuNQ04KcbDnPxXBwwuBOcyM0ENGnhfckBJ2MxMlx1E3ACObLq5OF3B7caJxXrULKoGZJkNi+AzTfnsKfZ8ZiqRfcuPvn3Xf956N5FL2hnP/hEi1bse27FgbefXnGg3ZYli7aqCxdvpgvm72nXVrl/10cfv36/2rbdnnkHPv3kwGNr1z360JYtXMH8Vavmz6l+HnVqKPjNfxk6BejIGot5LAJkAQcS0qw8cCBBatIpbz0qFIQ/JRBSTV5dp5LRFdhZymV18LpmyVb9XAK6BzUL9Yz4dKIJi5BeAkaRU5RGWQKBuJkzcLNO7FByftenmnb6i4Grr4vvu2jwhgOFNZPe+m3W5uULtmVtX/XIK/zuozRXO6md1QZHtfq09DEZKV9/uHzEGOr9cuOxRSUrP/zytG47GCSCQldWD+nQhCYYIEAsYUbSADshlAAvyBCFpRFR8PCzculSwBX83xBbcARhTo7QDWKyhXQiEROgalXCC1ljAEkxh7D8IeH1CljR4AK0ZMOXcYCY0pbGMJOwAq+u28IMfgn/EVydgFf1UZPPT30D+O7RlRMmcGX099F0xhztlxQpRTs9B/fzFN3Af85vYvQl6UjLqlNnZdQZxKCNUPh5iu/TsJvvQzeMG0dXjRunrzkL1nxHX7OokBYV5lBYeRZXOWFCdAk/YMYs6k4GL+CcqT04mvH0ZjCi65nupJFJJJKMPE2xx9CDrSV6SNfRg5uhB4CiSnIIzaU2zUu6C3lKXCOkYElsXBLoCh8PhuKRVYsLHW18CjpaKe4C8OCgviB42Bh4MAWRqzfzdRtq3l00o1dyBc29Y8JdS+bcD1GHtlkmlLy4+9DmxR9PLRwx6oG7byt/Ztq8h5fed279ypVAzwytu/S5+DAJk2vIFhJxYrXCElaLxHolLaR0KlBzHfXK1QWqD35lFqg8Aq++zCRyIOfO0X2sBMlEP70ydNW+s1P11KGnS+m1FzzLGSVpL6lJSu7ZC+swtPGIhZYcsCCVtgWaA3Jvi4WXM3PzOxV2w+KF5FZNbZAJzlz4TId88NVXFwE7EhINdrhJIIPwEsYYI/3s4mauO8xLzJ70D3AkAMd++EQGofobPWiRh/n3GW76Ga2gi+lS2Vr3wcB75MLnyh5Y4vGf2Dhyaj+OD1lvKnr0RZtbU7Sntb9rI2QPnUhvHlLbK733B3dqC7VRXLHr1lG3P9KZFmQM7PigQr+mGzlJS9WGHNb2lQ0fNfqXgxoNFxZx0X0LR515iy6i27R22jxtkdahfbB/u470Nzp11au3T4UMlsvwJ/0M8oCsXvgG4oEJMqH2us0qfJgFhVrJTCi4JQlxQFwBy21UipHAigVMAPdBPsB7AkAo124KlzXr6Wjp07u5G7WvJVE5exN9WhvHUcg9WBzYA+ssZvmhH9Ycb3gHJ3hBFn8y0Av62XLMCwaYyJ3o/kMAJJje2pz1NaLNYwYDgPMpYHagyG0o/slCKlH9TpYioi+ECJuhY3JIxJojvayA7uUDhbGDPfSl76JzJy7aEP2HNo/Oe+HV6jXaRDqoasurivaBqOzZW74hI+HQwv2flK557IGNpcsWP7RMt+WFENs2g22mkrGGZXqAHk8yg+jxgKsYaIgDPBwn4Lk4CxppGiPNBSS4WPVTsYQYDDaF1HQslrhA+4TkYqRClRJRIeM8cMqUoFeNXODVBUj9UZ+4VOp1o4KF/RLEM7KQ5v72I3V5uPKEd17d88MPe1495C/nPNrP3/+m1XGjT9J4OvqPb6Tte7XDP5z6t3Zk1+vSl+fonehnUD7vg3wsxEM6GtKxxqTjwdDsjdUiFKsLUQHzIz7dfcug+FgzCAB3SU/amSBXq6mNjtDWa79DutXxMPVrP36ufSQq2nNa/evaj1pVKc3/Yfdxms94iesPhfVt5DpjdUtsdQF0Q9RVUeSZKuJGYmk4S9EtgFQUa0jPx40kXE/A9Z89/FMNx7i/R6/hg6JSFj1aFl1fShrXHcXo7q2ve/GaJj3itLamsaDtggX38C801HEHoj1wsbfujt6ur7Uc9OUD0JcMrKmlxfSlFSWpTUhMQ5DJ8uFAK/qCkNMUisQzVYuHNIvZga46aaA6yTKzhwRQHCW5WI2DNNFAmy3Uxyfr6iODMchMg5bTwj9+ohYfNzlp364Dp7T3n3g3S5tNz3XSogc17XVuCMjUQW/9aZe0fLt2/Gvtt+PaVzd3pLPKomevm0mHNfG0nsnyKsOjmHSPoojhWivPuGptkqSN9UcUm15lFljDpFGG2IAJQ64DTK3ge1RUNBwQleit3OazN3FV0RJ9PUi+6M2sBhFoJsPG2gVcDX/ExiseqUT/pH/3FsBmKnzXg3rnaMyNHI25kYVdCpTfHctcWQ5k05Vfz1UcwGsL5CiKu3l+AithZpmTXdj5Fq5843OLNlee3PV+xVS6TKpat32F4Dl38q2fxpXtNcd49jPzjzGeWZp4xtsZz3j0jM7G8ggXwooaUXm7nlFQPaNACsE5+y0U4nQQ2PYW13MxF93ALeIejT7/NrCvhKsSo8XRgMhtiQ421jbB2mIsAuBKBg+lGA8jPNN6XrTEKphMOL49lRwY9dntTfYkdYRryeQ241qmuHAjJbGKJkvsdUaa9AKkKhPGSMUs13BinB0jskmv92F1JcLbHCwKM9ooaoQnhwapySPvWc35JS6xqsIqRb8bHD0u2WA7msiBhjzAzebOakIDjS6Jzm7SzVNMN6+9SDebKyRoo2Dszo7ixt1xLGszG1tSeUtsQ0WootQk76nku0ugowchAJ5Lo8I/z94kHKfnUsG/zgLb//7Cupc5VveyXLHuJdj0uhf4/5ivzSAeNF83+Fssgvlm0Y6UUIF20d7VGs4T7cPK+o8+O3nqHx/9iK4/kY7U1mo/nNS+19bTETTpZ+1bmn7q1AmaoX17QsfvyJu/sfqFh/Rp7g3B/9dabEwHLS1DgS2E0cCJBV4jGqgem9wy8AYDibQp1v7+r3Pn/qUtoHNqt9du1xaISv3efT9G13H7X1n28Gv6Pmadby86gFcesOebSURGXvljvEpDXrVhG/DCBrwuNcngVRBLE17Muh2yjbWjZEiMABXIumalyaBOzVjo5Ux+UxbDaZdg5MTSs4O1P7s/cP0lubleOzP4RP8zqakXs5Qju4CfH4nbALsHSamhbS5d29QgsDQxmbE0EVmayShKAoqSQ0qSnvmlM/SuiCE1C9UgSTfzOFmRgapEomMd5uqV4EVYB6BBvN8Hfp41jZqJYBc9+e+zD85YXJGRNSMrbcsqbSy9++CO7a9oD4nb3j847ZXcNtsWLu07oU1C5oJrFz24KjqJ+3PN4sdXge1gLl8JculAyluv/2GTUU2BUJYi47mUhJYdxvbNOoytNBTN7bGmZ5ODLK/FJmKNw5fVvtUWYmY45AdCfaaWLUQhKKG7HcNN0jZv+Sxy9NQf1HP4nw89yE/6UN12cMc3P/2ufXf0i7VVdIX08voVsyue6dZj77rqT2ZP3yqK0vJdz02b9GTXHu9Vb/2AThp3SEJ/0QFk+BjDx2C1UvN6icKHWEor1aHuR0RWmRUBFEQk1naVsILXlBFiL6CDUKLZKrFScnaHeAPzR9Ws14b+skjPhlTJ8L2KtdFd8lgkdOHFWPUD3SWkLljsZaVwiDONAQfLGtWVX6m1xyq0o//+QTtGP+O/bMja+e6h1/H3zw1R3Q8i7v+Q4Z6AUakkHBs1QKzDAI1KLLGiT5j6w0WI9zMW0B2pkJ9uXxD95xTwcdeOHi3shFBKSTH4fewD+EitXuNRnGF2yQjFAACXjWekUEjVqUuNww4hyl7P4t7485erWVufuBTfXofe/9m5r+rkcaOUmO9Q5L2q2XdGVEzwxuyfb8FqIsSQGpfs9ORF4LVZQbGGM7tklv3t4Exmp0v2NXXlKaxthGziQ8fKvDiQmE6RRP9VFAmlOUETDRbPpJb2UhHtPIV2LpQKqGmG9tAU7bVsKUvbMRXIP/EN/VbwnjvxT/wFvv6OZ589t07nb3fgr8LiTLZh+eYwKwYbcUbPpjiMI4KVxREL1f8PWmh3elpLfoI+S1c9oaXQ049pt2m3c8e4D6LLuUnRUDSNWxCdA2sEYI2dsIYZEbupUYY8LGApUEx1DKFbEambWPQCivUDpBfWooirltG9dP+y6MkKUWn4nG/XMCZ6gkvWaYDEQBjPdCQ/FstjeJXn65sUxaRXqAE0G425cCENYBEk4LuTH9bwBv9xwzp+9gjh57K/noszcMI67W16UpoHdlXIKimA7LGSQvlYnajW5CV2IQ9RDphX7C8+FDMpgB5BOexbR2/45BPtbdOrZWe8ZXDdjucf4MVYP4q07EeBkIMd7+NG3ScqZz6FzxLYQ3+2h15EMRXoRl2A2J/twVQHy9VK+sKSS6VghRTs3RXbjClW8fFB+AcEHfj0U9pf2/6JdKLsz+uxvsQd4RoY/xp7YwbLYC8sfQYt4wfQvGE0d9qBNCntDfjC59F29Pi4cVqKzid6fhU/lWXQSc2wGR40IywM7oXyUxoeK2XfuUPYSfeLB4hA2hC9AcELxIWdRZFxFnLyOAG0Qt9IUdgTvINbeeg+cY+o/YHx927AxG8LAyFq5ZMTemarJIUjAVw9xwoZLhbizBDA+PYBD+JSLNIUMPPGgm2mS7Ghp2cTAECvG09hDTcipOaGQiFI0zGtVzsatn/tb/2Z7SfnC0rqXlFNij8jKAl7d+799XcLs/IEV01iQpInT0l11aSkJoO5w59N5h6Bc8zqExJTUmM1n8SURnvPtLNBFTUNgEnEE8hhzTI+AJbnx1zJLEdszni9xNM5s3usQVYAJt+5iFXAwL36IZAWNp85KITP3E35r0499eDsFydxk6Ztr/nC7pwdZ+3x9uyqbRXTx89/s/1/1u2nGU/XPjht4ZzhVJKkqcNG7Xg5eqJ4QmHRTe1uK9+4dMjk6SOPLWOYZzXEAUlKAE1JJ6MN7GVHhvsA+EjI8BQ8YH01iWJczWAMd+uJgOyqV9wuNQHnwPTujOpG2OPSywh2JDkF3Z2LN0CrzDoNst4zyTF5jPowIiDJtLqyy8Zp+7/66o2KzYV2ue2a+1dXPb969rNZUkK0cvhd2jta1Peb9s2dQ9fRjJGTfzzg+5Dys0Yz3RsNuvMO051RRNeYeNDX+ECsSBkRkBYnYAQnS3edNqRFRz8eoMXjUhNBL+JCaqqM5V0GfRKxACIEWHEuHg7NqcYEjbslDEDMg4Ew7Pf6vCbIvbjRv34Zuf9ebvy2uVurNygVO8ZxlbPXH/0PZ849QTveU7ZOEqUFq878PXfvn0umS5L4aEkpLWDymAx0fGrI404dr+vhGeUhxOQhMHkI5pbyMARhsoGux6SR4EYSnKBvVhmU0ZBGnMko6rBCImYROc0L9LKepU/+8sCUDUUV46xdXr5335eVq6umrcpr9/T0qjX0vI/ytGjUEG7BmR9X3z6CBn478OPYEbRh5H1a9ENGxwig4yOQRzzQMYxEvEiCXTJISMWqm8UrxKpuGc1LPIlG+oO7T7QirLZ7/Swtk1WXjLKw2FGhZEMWhE0rBXz61rH+2YZ4/AHdnEZQ2+63jkeFfVXlVV3DPV+f/67223yOm7Hh0UW1NFr0Iw01fFKW+sofvbrd0rs/bU8nimmP7H4X9KkPEFEjdSB+ciuJxDOrwPgjWQAk4WykHFaJCGoDWCyhQIlnExo+rJWEmk0URuJ9TP8QkSVixJLQJVjYvsN6W6ixAacjtT41654M9A06E8JtSsZSTtMq+cMlVesiVstdkmlWeVVJQ1v+MNMTrT9fB/xNJXlkmlEFDIBmmGFzOpPbmpkb9GIVtT1jcBrsL83FsE9mKMZuNl1WoHYAbqcR3XL9co0g25ONyToTcDwZ0htA/2pbe/OKIFOeIr3a0HqnJ6ZIRw/eu7HIUfrDBwOVPum9H7256oWijeX7j1Y+DyqVm/PM9Kq1hkqVjthy7h8f/5odKM0I7Fi75JahtM2v++vH3UH/GFmpNXygx6YqCEtfgI14yAAD41jDuq9yoq9yNvkqb6N9cyE0cZvhp7CCYvMw1ACmTQy8GfNO4HmD+kyHSa6q7FJbuemVymUzZr6YA27ontET/vFNtJRbrTw7f3xUYrq+BTaVCfthc76x/BWVBAOl0KIB5dQbUM7GBhQsiQ2oLRUVFUK3c2+K5Rs34jXPP6L1p3lwTSdQ2ZUwsaI0BQvAFZdCMc5hT99VoMp2PTMG2ODSpeoOGfVRXpdJrCKUje2Te+2urr6hYyqefzStkAoV2shS0TqzUnjy3MTq7VZTeqxHtQZ4jHNljlhdFOtCIs6X8XYiYvA11Ud4OyvNMFZfuj4ktlofWlM5hy5/mNMG0a/5pVr/h6SEhpH0gKglRF8VOWf0P7CHJr6mkEbo0XppbUuFlHDmR/jOCsgH5oJdZGGuyHCLKwXrQGgWqCJKXBjtRPGB4Wazi2Xp2pHlYkUPVuJng6hY+lRzcDJE1w8lVQZ1UVLQgBVZVuN86IsCLSoyfqY+/guUyNtcoVaMt3XeUjmrOrPT9gVbdlU+MmfZCjed/tjsuU+lCd1q7hxbOXPq/O//E13KTX/7xa1LTElStIKbfuCl+ROj5pjuHwH6Wuh+I3VoAJfXeo9BjE2+SPf9F+n+OFtndbryauWyeXPWBIVufx8z8fPj0Ync8p0rF02K2pnu48xmAuznorkq+v83V8X8OEllXWNS1KIsAhjm8BEqaecOf6Gdrdz9cvWevRs37ubiAqdwsupU4BftQ9rpl13ncZoq8Bo6TaOes1obJYiwN4ylQ4kBa6T6ZuyCWApJQCwAybrtcC5WJGyOaWRO5xpgGrt0AabxGJxrxDSJtCWmKXV22cRAzdRNXdqtmrZ63fqq6c9ka6PELzYOK4lhmttvin7IbRtadmK/7wMq3DtC9/Gj+A+M/d9pZOm4/yYfnwKZg63gAgwA4kaY29K/IxW2RixglplbbwULFGGJs3UsMLm6S9zYiqINkxgWKH+2fbtn7m3EAnfcvuZsNpc/6FbEAj+V/pVzD52infsw5q+554EOF+RcTd5R76vHxYGKyI2tBsizcNrHjf4jjsTuWQAO+3TLMuUwxbzHWVA10Z/ncA2d8kS60K02bky5SSiX5k6O+mC9SYA9VsN6Hci8S9SL6GXrRaT1epHPD7gKC0YOI+80p8vuWjFODuI0mJIlKwmx+hFx+BpH0HUXHBtBb71+xMr1RZ0Bz5vUygVPz16377WPN78yvoyb/My8Bx6Y8tIbe7+sfbN8PKXtpPvGTb35xqmZuQ/NmbVp2O3zAd4PXTjlxv4lWXlPzVtcPXLoDInxPPv8T9wUcRDgl9tIxIM8iItBF1GHLqbm0CXWYYpvHC6Nt7SELtgMRHBAZMWpAxhZnwdrhruyC+Xs16f//POA3qlFme602/OmzgX4Qn3aTyXRq8YNFaWhdsfjz3FvwP5Wgow+F7rpfgwtUy+3SmZjk1iE8l5QhFLsrDDJ/BirQ8msKoklFSqx2kqzqlRRI6rNXlm5eNaStRmV46ydlcpN++hb3L3RZW9unjGe5869qd55N8aN9uBX98N+mtWl6JXrUu1n0dyglE2zZ2mlo4RuDZ/NncvnnXsTvno1IeIBuJ6PfGPMHjmcEIfwojXUhH2GVktT3sbS1L6bfj7dSmnqtxPvtihNWUS9NNXzvVND9XmEOEiD94qKHSead+7bd/IelsuaXDVmkwVy2cbSFfzZLJeFc5jLbufMFptew4J8treVM8HfjmaVLCO51YtYBjc8wI3Yq1FcCF4961A7Kfz93d93ljocnKUdLPulQOp44m6hWzTrjTe4L6NZb77JfXnuTe74669HU4ArIeB/LfCrZd2K/nd1qxCdqz3xCA3SrEe1J+ich7X3tPe4HM6jXUt3Rk9Gj9D3tTCsEQTMfIjJxJiVh2tjh9UeVmVEyfEFyHwgTW4uaJAz0yID4F5Fg4tou2yJXveglpv74HxfD4cjrjBu4MhAMSjAT/P5p88lTlppEcdw4uS/Lme2iDc3bGG61aKehU6IN/139axh3MPRJbwzOoXbM4SfeffQhoVGPauvNoFbKfUkaeRGAuZc63eQRCGPzQhBbLMU1JrZCTajk8wwKHYvIM3NYJT6gZ8ebPpTGY3b4lZFux4OWABjdo23gsQK+ya9rt/3/imrXkmae9/wO+4YXjEv9ZVVU7j0sQ/OPL7pVNGgdoceOz5pbVbOuonHHjuYe1PRyZePzVjK9hrRfqV+ViNLIS1bpa569mOUy8ByI6Xar9LuM33Y9yxA450xGtMKaolOo79AjQcaHQW1ziYa+TrFqvep3QaNfhIbbIjHqKc43KrVzWjsRRmJOkkoXpbH+1g+L5kscytH3nXXyPvmJu14rryionzVK9qu3IOPHStfmxlcO+X44++0G1R0atPxGYvHLp1x7OWTRbo8HqPVQj3vIYnkJoLo3GKtR73iUb+SGLHGXWnM3IHmZCyuJyKIZJNQFuylk0S2W1XywG8eQrTdmCbEEKjHE7+edLHk0fdY1cy/Pjn0qvHFAyaUrJ0+5IkhvSd2HXQP/eKBHTfcWByeV+Kcv+u6QV0Kp4/R9zjjvI3/TswmQTJDr5UoaWE1XqyPBJj7D2QY5RK8OcEJpwWWUQniRRWTDL1vns6yGoyWRgklSa5HKWAJJT0D6MEyl15CqbHaEpP1yFjY2d3yfqymKko8uyUrm5vxwd8rq97l+cYyynhO+MdTlbvf58y5R2hOwldfyu+tblZIWbrP/d1xP80BGvH+wo7sXqJn9fuI1FRIlxJDEQnTeAdfX0toimTPU9xhVn/1hmpsKZIZKAyy+1Nk7DwzdMATnLfgUyzoOxUfYoM2QHCbAoULs5QfFC0ePh3fhgVML346Ppl9Wkfe7no1E6ck0KoTEXmrksMAvWGeybTxjjScKQbJmnBmPtyLFuZc867tH5HXd/F8+dLK2U/Y6D7talM4n6cNg63XXmviFpTRtu/Vf7hV+ttSZY12uEwZv693aanz+0ol1kNaDvYWjxUCR7M6fa1LdhA7G4BzIYIM1Xp97ARAAy+vQwM/wiGkzc7GHSN2NppgtwFhUijiYJmfwwV/eUMMKtsdsVq/r0WtH0jx6bUNcGX4r8MyWk03LtOK6b3acPqiNrxCv8GQThWVaAfu06hctq1M20mvhV86jl8revgs437XHiTWNVeJnWEWvS/WOOeJVeYErNizRjqWzOGvxn5YGBnrW7uVtt0ielbDf1jhHn/+J/EP8QDEHj8g1FV6/FedDmPa0QcHmQwx4gGrvGWCidSG8yyZkAiH4WxemN3wWIAW0oXtIs5F8vTRxwT9Zj2lrUvN18dqO8Jf6SGlowtxbq3EPqkW4e19bWX3DovTx2emhPXx7TzZvV2Kc6eTjrrR6C1kvQnf7NiYMW7NksBLjKdVtC3NoVXaaO0L7bBWchudSAVK6WRtuaZpDdqTNGnHM09uELjhk8ZNmjVz8vgJwznhxSef2cEdod2pot2kHdQOaANphPbQ6rW5dD71Ux/E3PnatorNn1c9JU2ZVD2/cuGLE6ZJT1d9xmQ2k6zle/ObiASZIU65YqA2fs2kOfdoJ6j3HkfsgEv10JnaTG0WnWkcXHB/EWlx9xCoNSkDmf1qyCxEuuNM50VSqwWQgPPNeNdlJyahToD0lbah2sTu7I3ExvstL5BXCCQUDikhFxNLu/YA/FPBVwfbhkJKagux4S2YRSHIA1BsGXh7oTsV9D8HhNcJpwKDxUpYrgUREnxT6Y43GFxGjpfoo+fRRBq7naTMkOYakOYRXZqTIAPj6CQmzai2HKTLPVn1l759e5gtZVbhxqG7tg8aP+Le568kzehA/pY5M/relZY4rn/Xtn18Lt/NuV1uvUF7ju65+frb9L7xNGEXPSK+CRJor1tiLblEj0flMfByen6fTMN+ftqHT/Jn4PtWSWvAa5VoA+hKuKoTpz5MDP7H1SvOWIBnd6uY6motumgsLpU37s5m96dIRL8P2CTrFVU9ySoKG/OWJcNmDh6bekfcoNFVT2qrenYv7mCe29syaPDwiUw/F4B+DojpZxE6Kh/Dk/BrAfVqJ+6hOdqRTxqP1tKFdJG2yKMtajzQ50vZHKspnc2xui47ySoX6Gltq5OsvAf4c9E4axEyrPlMKyU68/SZmaGwLq56xclF+UqTi+6LJhcpbqjZ+GL0XX0vxhCj5DOkiLw8BC8FsBeBmEkWiYgYaSQG7ywFiljHCj7YDjaLLKE31MFGAecdwqveUWlc7sxPxoAcr88tmTqzulIG6dnq5FKgtcpSm9g90YKN3RN9heElRuelJ5joZNzgFeeYuC90dgjGvpONe7+DpKyVnWNJLCOspkL8CoRikMogIwVcS7oewdIZwKoN6n8Fm0hEXJWRjiTKCbYrkxiLepemcjbGwysSyeezgMnpsyMgbxmQRffWpkf8rU2PJBhZe8Tp9hUXtz5BwqTRcozkLRTARcMkYodG/eON/YA/gMwukZRcvCMcZ4kPqx5gOD4dIqn59tCX+3QW+9ica22i/ldi09YRo8djrcwpXWLjMR632PtnyNaLtz4/hjtYv1v8GvQbrI/8j37Xl+IP6zO6mdb6iKux490uzRXreHdi2w/A9gMXd7wDLtxtREjKwY435nq+kBq6oOOdkC8oSXtF1Y8db1+zjrfPVRPv8+uPpEhMSvBgB8vfrEoA51jH2xefmKR3vP0J8YmNHe+A0fFOtgFscaVltu+AsEXxymp+AWt+411C3mSj+W33tNL8zr5s55uFkWbtb6m+ttX29x9MaZp64NP3tNYA52+OKRGv9ytBFtivzCQjrtSxzGqtY5ltdCy3Y8cyI/i/7VkyIi/XuDzHqLtk95K+0sw3PwuBVhPfbumb6X/lm5/VfbOwm13uXB/sT5HYcxoSxKMX+uYWVf/L+2bjeRVXKPwzb9B69Z+2ZX75cj0AbkPMJ+v7PdDok8c223EqeohAGO9tUjJCzQj4v/HKlyYu5jFap68L88iXJe+s7kbw/jespYKMPSQB51YvUU1NvEQ1NSnml2WvHwzyv6qoMslcWFa9k6nlRcVV/iddDryxT5x594MkFly4Ux+KIhEyUDuO6TRtPCW28RovT/A24cYEr4mKmuQ4C7yVoL+VUFCbrOd92GdKwCKXLOm3J1yRtJhcLqBuIvPlFxEn9GZSiMX9UUzHAiSHXN8qYmnbmlW0M6xiByKWNsFsfYRYzcy64uQ18xTBInilwUtH91/qFvG/l/1KzU9w2uEpVw7zNiqCvCQq6E7EsB/JcjFtLSz+8rShxbdC26XtozltrdvISy3puqyxfN6Sphhm6A+YwU9ScSb/YhST1hqKSTesZTugmITEFKQnTlaTki8HaAwqWuKa61vs/mKUMLL5jpntCFbxNMHKYjr2dC5h5RmXsPKAse9asPKkNGPbDtz25c2huRguMIlvW1JwsW2ktGA6Jc8Lx7l3xTqIRHns2Scie76YLOjBCJJH0UvMYLTWWKlfv3eosCgMiXCO6fnvSr4vr94gHPcd/dbNxiTA920SltKz4iesDnAjwYK3XgxWfAW1vJFGJsQy/CQ9wzfSd3wmDoZudxz4BwuPrPBByg6JZVO11dfsKUh6dN5017V9S0b3u65kYGF2VjiclV0otu83Gk6MGHFdTudw27aFXZDWMuEUdx5ipAd3BdhMEtmwBi/G+vO1Hj2t9TAx1Vr1cgJrbeHUGc9G59i8EClWeZeRM+q7aioAI2gqmzD46vWF+X1umnTLDSu7FPQW6e33Tbq+yDtk2qRru1y+jvK/f+9FbqvwHST7PPCddRv4en2ItmnqFb7yotCL21qG87FLuK3i3it+fonY1fj8cCFEZfZco8Zn1MSeakTY4Dt7Ro2o3x7Dvu0J877hk6+7SghtpV21t7fq+7zMdS7zrJvhV1VMhi923FGjvW9c53wHKlH+v76Onz3+bnjnijGfUut7+zS8LwP2wpmNZ+z1YRZw0RP2dNoU0cUqKDbjLiCDTEWS2egGu+k0RnK4kfB5zYg3WKCvab/8msYt7bHH+RlrGqRgeUUqVqzslqiWz/ZDJm1vxiiDXTgT0oX+Qd3/V2vqrDTWDFeO2di5cswhmrN9m/YpfAde0Z/jPS93s+cJYSWmn1EREczhMD4KQBUtoVCzpwvFxZ4uZJSJ8UkHism4w87beBegAQXwZ9dSKi8l55euZ//pOjGBrKUNrIYUIFQxxVyYTZ8XN8cEJ+jCYrXPCReVPOE6pXCd31teR+FCxqWarkPxOkapqrSVyhTb002Asd4TD4KHhXwyBwnOMB6dptjCqszjhGItoTlWO8Na2PpIxmcpshP4GEUeM8YaR44VeyHtC5TcOpWTsP4JMvImABdTc7F+lIodjvhQJJc9zSWXWLAThLVRlGOHZg9pseNDWuzGQ1p+nfzGNL197WAPabFjr3rn6bq951j6aXPVxEFamKe4XDVOlwPST/izWfoJ5zD9hICGqactzulq1o/OYNVWfbQyiOOV5ILxSvavecbVk9700ksvUedXxZN7W7pM6br5bS4YPYo/724qLu9s6XJf96+0U5yvbGNZ1mkadDnHuTw/vpUDf3rePCHLY50u2uZ3jx6HRvHPCNew+3X8pFKvjELOh0+w1MMR3/iAL3zWjtnpgfScRSapzng+W+t38qArAA2o9evRy+/C2bpaZ1P0ciG6tdoNPBVgD+iB7M0D/+Aohw/yJnkUnbfiBtpx5CZp65C/SM+HX5TE8f36ae3pP7T2XKI2lFZHf6BzqTaPPka1qUyPEPh1Zc/UIJ3kgIzH597+f+LPPhMAAHjaY2BkYGAAYqY1CuLx/DZfGeQ5GEDgHDPraRj9v/efIdsr9gQgl4OBCSQKAP2qCgwAAAB42mNgZGDgSPq7Fkgy/O/9f4rtFQNQBAUsBACcywcFAHjaNZJNSFRRGIafc853Z2rTohZu+lGiAknINv1trKZFP0ZWmxorNf8ycVqMkDpQlJQLIxCCEjWzRCmScBEExmyCpEXRrqBlizLJKGpr771Ni4f3fOec7573e7l+kcwKwP0s8ZYxf4Qr9of9luNytECXLZJ19eT9VQb9IKtDC+usn8NugBP+ENXuK1OhivX2mJvqmRM50S4OiBlxV9SKZnHKzTLsntNhZdrr445tohAmqEsfpdeWKbffFKMK+qMaijYiRlX3MBRNU/SVfLQ2jkdrtb+DYmpJZzOiiYL9kp6nEGXk4Z3eeklVdJYpW6I8Xcku+8Ie+0SFzXPOfeNh2MI2KeEktSGP8wc5Y7W0WZ5ReWqU5mwD9f4B+6xb6zxj7j1P3eflW+E79+N1ukyzaV9kkz71+Beq19Dlp9msejgssDW1ir3S7WKjOO0fkXGvmJWujHq5HWdvWc0/pNxfUxWKTKRauBgm6YszTnXQ6mvI615TGOdaktNIksebePYEzZrMG88g326eeyVfMcMxSU6qk3uxt0uMy8OTUKA1PIN0g/Ioqe/W//BB7P4Hi9IeabvO5Ok/0Q0mU9cZcJ36T2IayfpmcUHU6a0K5uI+30inaIm/adUcsx802E74C0holcIAAAB42mNgYNCBwjCGPsYCxj9MM5iNmMOYW5g3sXCx+LAUsPSxrGM5xirE6sC6hM2ErYFdjL2NfR+HA8cWjjucPJwqnG6ccZzHuPq4DnHrcE/ivsTDx+PCs4PnAy8fbxDvBN5tfGx8TnxT+G7w2/AvEZAT8BPoEtgkaCWYIzhH8JTgNyEeIRuhOKEKoRnCQcLbRKRE6kTuieqJrhH9IiYnFie2QGyXuJZ4kfgBCQWJFok9knaSfZLXJP9JTZM6Ic0ibSTdIb1E+peMDxDuk3WQXSJ7Ra5OboHcOvks+Qny5+Q/KegplCjMU/ilmKO4RUlA6Zqyk3KO8hEVE5UOlW+qKarn1NTUOtQ2qf1Td8EBg9QT1PPU29TnqR9Sf6bBoeGkUaOxTeODxgdNEU0rIPymFaeVBQDd1FqqAAAAAQAAAKEARAAFAAAAAAACAAEAAgAWAAABAAFRAAAAAHjadVLLSsNQED1Jq9IaRYuULoMLV22aVhGJIBVfWIoLLRbETfqyxT4kjYh7P8OvcVV/QvwUT26mNSlKuJMzcydnzswEQAZfSEBLpgAc8YRYg0EvxDrSqApOwEZdcBI5vAleQh7vgpcZnwpeQQXfglMwNFPwKra0vGADO1pF8Bruta7gddS1D8EbMPSs4E2k9W3BGeT0Gc8UWf1U8Cds/Q7nGGMEHybacPl2iVqMPeEVHvp4QE/dXjA2pjdAh16ZPZZorxlr8vg8tXn2LNdhZjTDjOQ4wmLj4N+cW9byMKEfaDRZ0eKxVe092sO5kt0YRyHCEefuk81UPfpkdtlzB0O+PTwyNkZ3oVMr5sVvgikNccIqnuL1aV2lM6wZaPcZD7QHelqMjOh3WNXEM3Fb5QRaemqqx5y6y7zQi3+TZ2RxHmWqsFWXPr90UOTzoh6LPL9cFvM96i5SeZRzwkgNl+zhDFe4oS0I5997/W9PDXI1ObvZn1RSHA3ptMpeBypq0wb7drivfdoy8XyDP0JQfA542m3Ou0+TcRTG8e+hpTcol9JSoCqKIiqI71taCqJCtS3ekIsWARVoUmxrgDaFd2hiTEx0AXVkZ1Q3Edlw0cHEwcEBBv1XlNLfAAnP8slzknNyKGM//56R5Kisg5SJCRNmyrFgxYYdBxVU4qSKamqoxUUdbjzU46WBRprwcYzjnKCZk5yihdOcoZWztHGO81ygnQ4u0sklNHT8dBEgSDcheujlMn1c4SrX6GeAMNe5QYQoMQa5yS1uc4e7DHGPYUYYZYz7PCDOOA+ZYJIpHvGYJ0wzwywJMfOK16zxjlXeSzkrvOUvH/jBHD/5RYrfpMmQY5kCz3nBS7GIVWxiZ4c/7IpDKqRSnFIl1VIjteKSOnGLR+rFyyc2+MIW3/jMJt/5KA1s81UapYk34rOk5gu5tG41FjOapkVKhjVlxDmcNhZTibyxMJ8wlp3ZQy1+qBkHW3Hfv3dQqSv9yi5lQBlUditDyh5lrzJcUld3dd3xNJMy8nPJxFK6NPLHSgZj5qiRzxZLdO+P/+/adfZ42j3OKRLCQBAF0Bkm+0JWE0Ex6LkCksTEUKikiuIGWCwYcHABOEQHReE5BYcJHWjG9fst/n/w/gj8zGpwlk3H+aXtKks1M4jbGvIVHod2ApZaNwyELEGoBRiyvItipL4wEcaUYMnyyUy+ZWQbn9ab4CDsF8FFODeCh3CvBB/hnQgBwq8IISL4V40RofyBQ0TTUkwj7OhEtUMmyHSjGSOTuWY2rI32PdNJPiQZL3TSQq4+STRSagAAAAFR3VVMAAA=) format('woff'); -} \ No newline at end of file diff --git a/plugins/UiPluginManager/media/img/loading.gif b/plugins/UiPluginManager/media/img/loading.gif deleted file mode 100644 index 27d0aa8108b0800f9cddcf613f787347d9981e05..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 723 zcmZ?wbhEHb6ky0iXa9k4z>53@HBR_Hzvhc6JPKHPSO+W(0~W{*!Vp zN=+!e)X>LZSp~!n_rkGVK%h z9k_L9<(o^(d!N7A`+9eTzQ!EZMr*-N2_|eB&45;SC+a-zP~lXP;z?eTv`FKm^!Y8l zuZ^S*OlLmOv^VZNyYQx zE1Q1d^QD!~t!MErXFkzlm$bqCmuUZ)iN%&IQkAQ(b??%e8>EQMBqK<8T-y}!%q4L0 z4v$MoL7}cEx5PfOihDclHe=f1_`ny+jJ+qGonTF#=e6?cS1GK1Glv+XQW)E^VpGzx z%$u!=(=#3~+Lk*jmQUf$-=^(}f)AMWru(Y&&oE(%*JUs>JH24vgCGuUPSS^%^#tgi z6`S6zDw0tR+QR$5bp7w`G6mDQzjYm%RoE)?D^8cegv~i}{SvGJM6wyypd - @plugins = plugins - - savePluginStatus: (plugin, is_enabled) => - Page.cmd "pluginConfigSet", [plugin.source, plugin.inner_path, "enabled", is_enabled], (res) => - if res == "ok" - Page.updatePlugins() - else - Page.cmd "wrapperNotification", ["error", res.error] - - Page.projector.scheduleRender() - - handleCheckboxChange: (e) => - node = e.currentTarget - plugin = node["data-plugin"] - node.classList.toggle("checked") - value = node.classList.contains("checked") - - @savePluginStatus(plugin, value) - - handleResetClick: (e) => - node = e.currentTarget - plugin = node["data-plugin"] - - @savePluginStatus(plugin, null) - - handleUpdateClick: (e) => - node = e.currentTarget - plugin = node["data-plugin"] - node.classList.add("loading") - - Page.cmd "pluginUpdate", [plugin.source, plugin.inner_path], (res) => - if res == "ok" - Page.cmd "wrapperNotification", ["done", "Plugin #{plugin.name} updated to latest version"] - Page.updatePlugins() - else - Page.cmd "wrapperNotification", ["error", res.error] - node.classList.remove("loading") - - return false - - handleDeleteClick: (e) => - node = e.currentTarget - plugin = node["data-plugin"] - if plugin.loaded - Page.cmd "wrapperNotification", ["info", "You can only delete plugin that are not currently active"] - return false - - node.classList.add("loading") - - Page.cmd "wrapperConfirm", ["Delete #{plugin.name} plugin?", "Delete"], (res) => - if not res - node.classList.remove("loading") - return false - - Page.cmd "pluginRemove", [plugin.source, plugin.inner_path], (res) => - if res == "ok" - Page.cmd "wrapperNotification", ["done", "Plugin #{plugin.name} deleted"] - Page.updatePlugins() - else - Page.cmd "wrapperNotification", ["error", res.error] - node.classList.remove("loading") - - return false - - render: -> - h("div.plugins", @plugins.map (plugin) => - if not plugin.info - return - descr = plugin.info.description - plugin.info.default ?= "enabled" - if plugin.info.default - descr += " (default: #{plugin.info.default})" - - tag_version = "" - tag_source = "" - tag_delete = "" - if plugin.source != "builtin" - tag_update = "" - if plugin.site_info?.rev - if plugin.site_info.rev > plugin.info.rev - tag_update = h("a.version-update.button", - {href: "#Update+plugin", onclick: @handleUpdateClick, "data-plugin": plugin}, - "Update to rev#{plugin.site_info.rev}" - ) - - else - tag_update = h("span.version-missing", "(unable to get latest vesion: update site missing)") - - tag_version = h("span.version",[ - "rev#{plugin.info.rev} ", - tag_update, - ]) - - tag_source = h("div.source",[ - "Source: ", - h("a", {"href": "/#{plugin.source}", "target": "_top"}, if plugin.site_title then plugin.site_title else plugin.source), - " /" + plugin.inner_path - ]) - - tag_delete = h("a.delete", {"href": "#Delete+plugin", onclick: @handleDeleteClick, "data-plugin": plugin}, "Delete plugin") - - - enabled_default = plugin.info.default == "enabled" - if plugin.enabled != plugin.loaded or plugin.updated - marker_title = "Change pending" - is_pending = true - else - marker_title = "Changed from default status (click to reset to #{plugin.info.default})" - is_pending = false - - is_changed = plugin.enabled != enabled_default and plugin.owner == "builtin" - - h("div.plugin", {key: plugin.name}, [ - h("div.title", [ - h("h3", [plugin.name, tag_version]), - h("div.description", [descr, tag_source, tag_delete]), - ]) - h("div.value.value-right", - h("div.checkbox", {onclick: @handleCheckboxChange, "data-plugin": plugin, classes: {checked: plugin.enabled}}, h("div.checkbox-skin")) - h("a.marker", { - href: "#Reset", title: marker_title, - onclick: @handleResetClick, "data-plugin": plugin, - classes: {visible: is_pending or is_changed, pending: is_pending} - }, "\u2022") - ) - ]) - ) - - -window.PluginList = PluginList \ No newline at end of file diff --git a/plugins/UiPluginManager/media/js/UiPluginManager.coffee b/plugins/UiPluginManager/media/js/UiPluginManager.coffee deleted file mode 100644 index 6a0adee5..00000000 --- a/plugins/UiPluginManager/media/js/UiPluginManager.coffee +++ /dev/null @@ -1,71 +0,0 @@ -window.h = maquette.h - -class UiPluginManager extends ZeroFrame - init: -> - @plugin_list_builtin = new PluginList() - @plugin_list_custom = new PluginList() - @plugins_changed = null - @need_restart = null - @ - - onOpenWebsocket: => - @cmd("wrapperSetTitle", "Plugin manager - ZeroNet") - @cmd "serverInfo", {}, (server_info) => - @server_info = server_info - @updatePlugins() - - updatePlugins: (cb) => - @cmd "pluginList", [], (res) => - @plugins_changed = (item for item in res.plugins when item.enabled != item.loaded or item.updated) - - plugins_builtin = (item for item in res.plugins when item.source == "builtin") - @plugin_list_builtin.plugins = plugins_builtin.sort (a, b) -> - return a.name.localeCompare(b.name) - - plugins_custom = (item for item in res.plugins when item.source != "builtin") - @plugin_list_custom.plugins = plugins_custom.sort (a, b) -> - return a.name.localeCompare(b.name) - - @projector.scheduleRender() - cb?() - - createProjector: => - @projector = maquette.createProjector() - @projector.replace($("#content"), @render) - @projector.replace($("#bottom-restart"), @renderBottomRestart) - - render: => - if not @plugin_list_builtin.plugins - return h("div.content") - - h("div.content", [ - h("div.section", [ - if @plugin_list_custom.plugins?.length - [ - h("h2", "Installed third-party plugins"), - @plugin_list_custom.render() - ] - h("h2", "Built-in plugins") - @plugin_list_builtin.render() - ]) - ]) - - handleRestartClick: => - @restart_loading = true - setTimeout ( => - Page.cmd("serverShutdown", {restart: true}) - ), 300 - Page.projector.scheduleRender() - return false - - renderBottomRestart: => - h("div.bottom.bottom-restart", {classes: {visible: @plugins_changed?.length}}, h("div.bottom-content", [ - h("div.title", "Some plugins status has been changed"), - h("a.button.button-submit.button-restart", - {href: "#Restart", classes: {loading: @restart_loading}, onclick: @handleRestartClick}, - "Restart ZeroNet client" - ) - ])) - -window.Page = new UiPluginManager() -window.Page.createProjector() diff --git a/plugins/UiPluginManager/media/js/all.js b/plugins/UiPluginManager/media/js/all.js deleted file mode 100644 index 25632862..00000000 --- a/plugins/UiPluginManager/media/js/all.js +++ /dev/null @@ -1,1606 +0,0 @@ - -/* ---- lib/Class.coffee ---- */ - - -(function() { - var Class, - slice = [].slice; - - Class = (function() { - function Class() {} - - Class.prototype.trace = true; - - Class.prototype.log = function() { - var args; - args = 1 <= arguments.length ? slice.call(arguments, 0) : []; - if (!this.trace) { - return; - } - if (typeof console === 'undefined') { - return; - } - args.unshift("[" + this.constructor.name + "]"); - console.log.apply(console, args); - return this; - }; - - Class.prototype.logStart = function() { - var args, name; - name = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; - if (!this.trace) { - return; - } - this.logtimers || (this.logtimers = {}); - this.logtimers[name] = +(new Date); - if (args.length > 0) { - this.log.apply(this, ["" + name].concat(slice.call(args), ["(started)"])); - } - return this; - }; - - Class.prototype.logEnd = function() { - var args, ms, name; - name = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; - ms = +(new Date) - this.logtimers[name]; - this.log.apply(this, ["" + name].concat(slice.call(args), ["(Done in " + ms + "ms)"])); - return this; - }; - - return Class; - - })(); - - window.Class = Class; - -}).call(this); - - -/* ---- lib/Promise.coffee ---- */ - - -(function() { - var Promise, - slice = [].slice; - - Promise = (function() { - Promise.when = function() { - var args, fn, i, len, num_uncompleted, promise, task, task_id, tasks; - tasks = 1 <= arguments.length ? slice.call(arguments, 0) : []; - num_uncompleted = tasks.length; - args = new Array(num_uncompleted); - promise = new Promise(); - fn = function(task_id) { - return task.then(function() { - args[task_id] = Array.prototype.slice.call(arguments); - num_uncompleted--; - if (num_uncompleted === 0) { - return promise.complete.apply(promise, args); - } - }); - }; - for (task_id = i = 0, len = tasks.length; i < len; task_id = ++i) { - task = tasks[task_id]; - fn(task_id); - } - return promise; - }; - - function Promise() { - this.resolved = false; - this.end_promise = null; - this.result = null; - this.callbacks = []; - } - - Promise.prototype.resolve = function() { - var back, callback, i, len, ref; - if (this.resolved) { - return false; - } - this.resolved = true; - this.data = arguments; - if (!arguments.length) { - this.data = [true]; - } - this.result = this.data[0]; - ref = this.callbacks; - for (i = 0, len = ref.length; i < len; i++) { - callback = ref[i]; - back = callback.apply(callback, this.data); - } - if (this.end_promise) { - return this.end_promise.resolve(back); - } - }; - - Promise.prototype.fail = function() { - return this.resolve(false); - }; - - Promise.prototype.then = function(callback) { - if (this.resolved === true) { - callback.apply(callback, this.data); - return; - } - this.callbacks.push(callback); - return this.end_promise = new Promise(); - }; - - return Promise; - - })(); - - window.Promise = Promise; - - - /* - s = Date.now() - log = (text) -> - console.log Date.now()-s, Array.prototype.slice.call(arguments).join(", ") - - log "Started" - - cmd = (query) -> - p = new Promise() - setTimeout ( -> - p.resolve query+" Result" - ), 100 - return p - - back = cmd("SELECT * FROM message").then (res) -> - log res - return "Return from query" - .then (res) -> - log "Back then", res - - log "Query started", back - */ - -}).call(this); - - -/* ---- lib/Prototypes.coffee ---- */ - - -(function() { - String.prototype.startsWith = function(s) { - return this.slice(0, s.length) === s; - }; - - String.prototype.endsWith = function(s) { - return s === '' || this.slice(-s.length) === s; - }; - - String.prototype.repeat = function(count) { - return new Array(count + 1).join(this); - }; - - window.isEmpty = function(obj) { - var key; - for (key in obj) { - return false; - } - return true; - }; - -}).call(this); - - -/* ---- lib/maquette.js ---- */ - - -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(['exports'], factory); - } else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') { - // CommonJS - factory(exports); - } else { - // Browser globals - factory(root.maquette = {}); - } -}(this, function (exports) { - 'use strict'; - ; - ; - ; - ; - var NAMESPACE_W3 = 'http://www.w3.org/'; - var NAMESPACE_SVG = NAMESPACE_W3 + '2000/svg'; - var NAMESPACE_XLINK = NAMESPACE_W3 + '1999/xlink'; - // Utilities - var emptyArray = []; - var extend = function (base, overrides) { - var result = {}; - Object.keys(base).forEach(function (key) { - result[key] = base[key]; - }); - if (overrides) { - Object.keys(overrides).forEach(function (key) { - result[key] = overrides[key]; - }); - } - return result; - }; - // Hyperscript helper functions - var same = function (vnode1, vnode2) { - if (vnode1.vnodeSelector !== vnode2.vnodeSelector) { - return false; - } - if (vnode1.properties && vnode2.properties) { - if (vnode1.properties.key !== vnode2.properties.key) { - return false; - } - return vnode1.properties.bind === vnode2.properties.bind; - } - return !vnode1.properties && !vnode2.properties; - }; - var toTextVNode = function (data) { - return { - vnodeSelector: '', - properties: undefined, - children: undefined, - text: data.toString(), - domNode: null - }; - }; - var appendChildren = function (parentSelector, insertions, main) { - for (var i = 0; i < insertions.length; i++) { - var item = insertions[i]; - if (Array.isArray(item)) { - appendChildren(parentSelector, item, main); - } else { - if (item !== null && item !== undefined) { - if (!item.hasOwnProperty('vnodeSelector')) { - item = toTextVNode(item); - } - main.push(item); - } - } - } - }; - // Render helper functions - var missingTransition = function () { - throw new Error('Provide a transitions object to the projectionOptions to do animations'); - }; - var DEFAULT_PROJECTION_OPTIONS = { - namespace: undefined, - eventHandlerInterceptor: undefined, - styleApplyer: function (domNode, styleName, value) { - // Provides a hook to add vendor prefixes for browsers that still need it. - domNode.style[styleName] = value; - }, - transitions: { - enter: missingTransition, - exit: missingTransition - } - }; - var applyDefaultProjectionOptions = function (projectorOptions) { - return extend(DEFAULT_PROJECTION_OPTIONS, projectorOptions); - }; - var checkStyleValue = function (styleValue) { - if (typeof styleValue !== 'string') { - throw new Error('Style values must be strings'); - } - }; - var setProperties = function (domNode, properties, projectionOptions) { - if (!properties) { - return; - } - var eventHandlerInterceptor = projectionOptions.eventHandlerInterceptor; - var propNames = Object.keys(properties); - var propCount = propNames.length; - for (var i = 0; i < propCount; i++) { - var propName = propNames[i]; - /* tslint:disable:no-var-keyword: edge case */ - var propValue = properties[propName]; - /* tslint:enable:no-var-keyword */ - if (propName === 'className') { - throw new Error('Property "className" is not supported, use "class".'); - } else if (propName === 'class') { - if (domNode.className) { - // May happen if classes is specified before class - domNode.className += ' ' + propValue; - } else { - domNode.className = propValue; - } - } else if (propName === 'classes') { - // object with string keys and boolean values - var classNames = Object.keys(propValue); - var classNameCount = classNames.length; - for (var j = 0; j < classNameCount; j++) { - var className = classNames[j]; - if (propValue[className]) { - domNode.classList.add(className); - } - } - } else if (propName === 'styles') { - // object with string keys and string (!) values - var styleNames = Object.keys(propValue); - var styleCount = styleNames.length; - for (var j = 0; j < styleCount; j++) { - var styleName = styleNames[j]; - var styleValue = propValue[styleName]; - if (styleValue) { - checkStyleValue(styleValue); - projectionOptions.styleApplyer(domNode, styleName, styleValue); - } - } - } else if (propName === 'key') { - continue; - } else if (propValue === null || propValue === undefined) { - continue; - } else { - var type = typeof propValue; - if (type === 'function') { - if (propName.lastIndexOf('on', 0) === 0) { - if (eventHandlerInterceptor) { - propValue = eventHandlerInterceptor(propName, propValue, domNode, properties); // intercept eventhandlers - } - if (propName === 'oninput') { - (function () { - // record the evt.target.value, because IE and Edge sometimes do a requestAnimationFrame between changing value and running oninput - var oldPropValue = propValue; - propValue = function (evt) { - evt.target['oninput-value'] = evt.target.value; - // may be HTMLTextAreaElement as well - oldPropValue.apply(this, [evt]); - }; - }()); - } - domNode[propName] = propValue; - } - } else if (type === 'string' && propName !== 'value' && propName !== 'innerHTML') { - if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') { - domNode.setAttributeNS(NAMESPACE_XLINK, propName, propValue); - } else { - domNode.setAttribute(propName, propValue); - } - } else { - domNode[propName] = propValue; - } - } - } - }; - var updateProperties = function (domNode, previousProperties, properties, projectionOptions) { - if (!properties) { - return; - } - var propertiesUpdated = false; - var propNames = Object.keys(properties); - var propCount = propNames.length; - for (var i = 0; i < propCount; i++) { - var propName = propNames[i]; - // assuming that properties will be nullified instead of missing is by design - var propValue = properties[propName]; - var previousValue = previousProperties[propName]; - if (propName === 'class') { - if (previousValue !== propValue) { - throw new Error('"class" property may not be updated. Use the "classes" property for conditional css classes.'); - } - } else if (propName === 'classes') { - var classList = domNode.classList; - var classNames = Object.keys(propValue); - var classNameCount = classNames.length; - for (var j = 0; j < classNameCount; j++) { - var className = classNames[j]; - var on = !!propValue[className]; - var previousOn = !!previousValue[className]; - if (on === previousOn) { - continue; - } - propertiesUpdated = true; - if (on) { - classList.add(className); - } else { - classList.remove(className); - } - } - } else if (propName === 'styles') { - var styleNames = Object.keys(propValue); - var styleCount = styleNames.length; - for (var j = 0; j < styleCount; j++) { - var styleName = styleNames[j]; - var newStyleValue = propValue[styleName]; - var oldStyleValue = previousValue[styleName]; - if (newStyleValue === oldStyleValue) { - continue; - } - propertiesUpdated = true; - if (newStyleValue) { - checkStyleValue(newStyleValue); - projectionOptions.styleApplyer(domNode, styleName, newStyleValue); - } else { - projectionOptions.styleApplyer(domNode, styleName, ''); - } - } - } else { - if (!propValue && typeof previousValue === 'string') { - propValue = ''; - } - if (propName === 'value') { - if (domNode[propName] !== propValue && domNode['oninput-value'] !== propValue) { - domNode[propName] = propValue; - // Reset the value, even if the virtual DOM did not change - domNode['oninput-value'] = undefined; - } - // else do not update the domNode, otherwise the cursor position would be changed - if (propValue !== previousValue) { - propertiesUpdated = true; - } - } else if (propValue !== previousValue) { - var type = typeof propValue; - if (type === 'function') { - throw new Error('Functions may not be updated on subsequent renders (property: ' + propName + '). Hint: declare event handler functions outside the render() function.'); - } - if (type === 'string' && propName !== 'innerHTML') { - if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') { - domNode.setAttributeNS(NAMESPACE_XLINK, propName, propValue); - } else { - domNode.setAttribute(propName, propValue); - } - } else { - if (domNode[propName] !== propValue) { - domNode[propName] = propValue; - } - } - propertiesUpdated = true; - } - } - } - return propertiesUpdated; - }; - var findIndexOfChild = function (children, sameAs, start) { - if (sameAs.vnodeSelector !== '') { - // Never scan for text-nodes - for (var i = start; i < children.length; i++) { - if (same(children[i], sameAs)) { - return i; - } - } - } - return -1; - }; - var nodeAdded = function (vNode, transitions) { - if (vNode.properties) { - var enterAnimation = vNode.properties.enterAnimation; - if (enterAnimation) { - if (typeof enterAnimation === 'function') { - enterAnimation(vNode.domNode, vNode.properties); - } else { - transitions.enter(vNode.domNode, vNode.properties, enterAnimation); - } - } - } - }; - var nodeToRemove = function (vNode, transitions) { - var domNode = vNode.domNode; - if (vNode.properties) { - var exitAnimation = vNode.properties.exitAnimation; - if (exitAnimation) { - domNode.style.pointerEvents = 'none'; - var removeDomNode = function () { - if (domNode.parentNode) { - domNode.parentNode.removeChild(domNode); - } - }; - if (typeof exitAnimation === 'function') { - exitAnimation(domNode, removeDomNode, vNode.properties); - return; - } else { - transitions.exit(vNode.domNode, vNode.properties, exitAnimation, removeDomNode); - return; - } - } - } - if (domNode.parentNode) { - domNode.parentNode.removeChild(domNode); - } - }; - var checkDistinguishable = function (childNodes, indexToCheck, parentVNode, operation) { - var childNode = childNodes[indexToCheck]; - if (childNode.vnodeSelector === '') { - return; // Text nodes need not be distinguishable - } - var properties = childNode.properties; - var key = properties ? properties.key === undefined ? properties.bind : properties.key : undefined; - if (!key) { - for (var i = 0; i < childNodes.length; i++) { - if (i !== indexToCheck) { - var node = childNodes[i]; - if (same(node, childNode)) { - if (operation === 'added') { - throw new Error(parentVNode.vnodeSelector + ' had a ' + childNode.vnodeSelector + ' child ' + 'added, but there is now more than one. You must add unique key properties to make them distinguishable.'); - } else { - throw new Error(parentVNode.vnodeSelector + ' had a ' + childNode.vnodeSelector + ' child ' + 'removed, but there were more than one. You must add unique key properties to make them distinguishable.'); - } - } - } - } - } - }; - var createDom; - var updateDom; - var updateChildren = function (vnode, domNode, oldChildren, newChildren, projectionOptions) { - if (oldChildren === newChildren) { - return false; - } - oldChildren = oldChildren || emptyArray; - newChildren = newChildren || emptyArray; - var oldChildrenLength = oldChildren.length; - var newChildrenLength = newChildren.length; - var transitions = projectionOptions.transitions; - var oldIndex = 0; - var newIndex = 0; - var i; - var textUpdated = false; - while (newIndex < newChildrenLength) { - var oldChild = oldIndex < oldChildrenLength ? oldChildren[oldIndex] : undefined; - var newChild = newChildren[newIndex]; - if (oldChild !== undefined && same(oldChild, newChild)) { - textUpdated = updateDom(oldChild, newChild, projectionOptions) || textUpdated; - oldIndex++; - } else { - var findOldIndex = findIndexOfChild(oldChildren, newChild, oldIndex + 1); - if (findOldIndex >= 0) { - // Remove preceding missing children - for (i = oldIndex; i < findOldIndex; i++) { - nodeToRemove(oldChildren[i], transitions); - checkDistinguishable(oldChildren, i, vnode, 'removed'); - } - textUpdated = updateDom(oldChildren[findOldIndex], newChild, projectionOptions) || textUpdated; - oldIndex = findOldIndex + 1; - } else { - // New child - createDom(newChild, domNode, oldIndex < oldChildrenLength ? oldChildren[oldIndex].domNode : undefined, projectionOptions); - nodeAdded(newChild, transitions); - checkDistinguishable(newChildren, newIndex, vnode, 'added'); - } - } - newIndex++; - } - if (oldChildrenLength > oldIndex) { - // Remove child fragments - for (i = oldIndex; i < oldChildrenLength; i++) { - nodeToRemove(oldChildren[i], transitions); - checkDistinguishable(oldChildren, i, vnode, 'removed'); - } - } - return textUpdated; - }; - var addChildren = function (domNode, children, projectionOptions) { - if (!children) { - return; - } - for (var i = 0; i < children.length; i++) { - createDom(children[i], domNode, undefined, projectionOptions); - } - }; - var initPropertiesAndChildren = function (domNode, vnode, projectionOptions) { - addChildren(domNode, vnode.children, projectionOptions); - // children before properties, needed for value property of . - if (vnode.text) { - domNode.textContent = vnode.text; - } - setProperties(domNode, vnode.properties, projectionOptions); - if (vnode.properties && vnode.properties.afterCreate) { - vnode.properties.afterCreate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children); - } - }; - createDom = function (vnode, parentNode, insertBefore, projectionOptions) { - var domNode, i, c, start = 0, type, found; - var vnodeSelector = vnode.vnodeSelector; - if (vnodeSelector === '') { - domNode = vnode.domNode = document.createTextNode(vnode.text); - if (insertBefore !== undefined) { - parentNode.insertBefore(domNode, insertBefore); - } else { - parentNode.appendChild(domNode); - } - } else { - for (i = 0; i <= vnodeSelector.length; ++i) { - c = vnodeSelector.charAt(i); - if (i === vnodeSelector.length || c === '.' || c === '#') { - type = vnodeSelector.charAt(start - 1); - found = vnodeSelector.slice(start, i); - if (type === '.') { - domNode.classList.add(found); - } else if (type === '#') { - domNode.id = found; - } else { - if (found === 'svg') { - projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG }); - } - if (projectionOptions.namespace !== undefined) { - domNode = vnode.domNode = document.createElementNS(projectionOptions.namespace, found); - } else { - domNode = vnode.domNode = document.createElement(found); - } - if (insertBefore !== undefined) { - parentNode.insertBefore(domNode, insertBefore); - } else { - parentNode.appendChild(domNode); - } - } - start = i + 1; - } - } - initPropertiesAndChildren(domNode, vnode, projectionOptions); - } - }; - updateDom = function (previous, vnode, projectionOptions) { - var domNode = previous.domNode; - var textUpdated = false; - if (previous === vnode) { - return false; // By contract, VNode objects may not be modified anymore after passing them to maquette - } - var updated = false; - if (vnode.vnodeSelector === '') { - if (vnode.text !== previous.text) { - var newVNode = document.createTextNode(vnode.text); - domNode.parentNode.replaceChild(newVNode, domNode); - vnode.domNode = newVNode; - textUpdated = true; - return textUpdated; - } - } else { - if (vnode.vnodeSelector.lastIndexOf('svg', 0) === 0) { - projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG }); - } - if (previous.text !== vnode.text) { - updated = true; - if (vnode.text === undefined) { - domNode.removeChild(domNode.firstChild); // the only textnode presumably - } else { - domNode.textContent = vnode.text; - } - } - updated = updateChildren(vnode, domNode, previous.children, vnode.children, projectionOptions) || updated; - updated = updateProperties(domNode, previous.properties, vnode.properties, projectionOptions) || updated; - if (vnode.properties && vnode.properties.afterUpdate) { - vnode.properties.afterUpdate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children); - } - } - if (updated && vnode.properties && vnode.properties.updateAnimation) { - vnode.properties.updateAnimation(domNode, vnode.properties, previous.properties); - } - vnode.domNode = previous.domNode; - return textUpdated; - }; - var createProjection = function (vnode, projectionOptions) { - return { - update: function (updatedVnode) { - if (vnode.vnodeSelector !== updatedVnode.vnodeSelector) { - throw new Error('The selector for the root VNode may not be changed. (consider using dom.merge and add one extra level to the virtual DOM)'); - } - updateDom(vnode, updatedVnode, projectionOptions); - vnode = updatedVnode; - }, - domNode: vnode.domNode - }; - }; - ; - // The other two parameters are not added here, because the Typescript compiler creates surrogate code for desctructuring 'children'. - exports.h = function (selector) { - var properties = arguments[1]; - if (typeof selector !== 'string') { - throw new Error(); - } - var childIndex = 1; - if (properties && !properties.hasOwnProperty('vnodeSelector') && !Array.isArray(properties) && typeof properties === 'object') { - childIndex = 2; - } else { - // Optional properties argument was omitted - properties = undefined; - } - var text = undefined; - var children = undefined; - var argsLength = arguments.length; - // Recognize a common special case where there is only a single text node - if (argsLength === childIndex + 1) { - var onlyChild = arguments[childIndex]; - if (typeof onlyChild === 'string') { - text = onlyChild; - } else if (onlyChild !== undefined && onlyChild.length === 1 && typeof onlyChild[0] === 'string') { - text = onlyChild[0]; - } - } - if (text === undefined) { - children = []; - for (; childIndex < arguments.length; childIndex++) { - var child = arguments[childIndex]; - if (child === null || child === undefined) { - continue; - } else if (Array.isArray(child)) { - appendChildren(selector, child, children); - } else if (child.hasOwnProperty('vnodeSelector')) { - children.push(child); - } else { - children.push(toTextVNode(child)); - } - } - } - return { - vnodeSelector: selector, - properties: properties, - children: children, - text: text === '' ? undefined : text, - domNode: null - }; - }; - /** - * Contains simple low-level utility functions to manipulate the real DOM. - */ - exports.dom = { - /** - * Creates a real DOM tree from `vnode`. The [[Projection]] object returned will contain the resulting DOM Node in - * its [[Projection.domNode|domNode]] property. - * This is a low-level method. Users wil typically use a [[Projector]] instead. - * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]] - * objects may only be rendered once. - * @param projectionOptions - Options to be used to create and update the projection. - * @returns The [[Projection]] which also contains the DOM Node that was created. - */ - create: function (vnode, projectionOptions) { - projectionOptions = applyDefaultProjectionOptions(projectionOptions); - createDom(vnode, document.createElement('div'), undefined, projectionOptions); - return createProjection(vnode, projectionOptions); - }, - /** - * Appends a new childnode to the DOM which is generated from a [[VNode]]. - * This is a low-level method. Users wil typically use a [[Projector]] instead. - * @param parentNode - The parent node for the new childNode. - * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]] - * objects may only be rendered once. - * @param projectionOptions - Options to be used to create and update the [[Projection]]. - * @returns The [[Projection]] that was created. - */ - append: function (parentNode, vnode, projectionOptions) { - projectionOptions = applyDefaultProjectionOptions(projectionOptions); - createDom(vnode, parentNode, undefined, projectionOptions); - return createProjection(vnode, projectionOptions); - }, - /** - * Inserts a new DOM node which is generated from a [[VNode]]. - * This is a low-level method. Users wil typically use a [[Projector]] instead. - * @param beforeNode - The node that the DOM Node is inserted before. - * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. - * NOTE: [[VNode]] objects may only be rendered once. - * @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]]. - * @returns The [[Projection]] that was created. - */ - insertBefore: function (beforeNode, vnode, projectionOptions) { - projectionOptions = applyDefaultProjectionOptions(projectionOptions); - createDom(vnode, beforeNode.parentNode, beforeNode, projectionOptions); - return createProjection(vnode, projectionOptions); - }, - /** - * Merges a new DOM node which is generated from a [[VNode]] with an existing DOM Node. - * This means that the virtual DOM and the real DOM will have one overlapping element. - * Therefore the selector for the root [[VNode]] will be ignored, but its properties and children will be applied to the Element provided. - * This is a low-level method. Users wil typically use a [[Projector]] instead. - * @param domNode - The existing element to adopt as the root of the new virtual DOM. Existing attributes and childnodes are preserved. - * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]] objects - * may only be rendered once. - * @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]]. - * @returns The [[Projection]] that was created. - */ - merge: function (element, vnode, projectionOptions) { - projectionOptions = applyDefaultProjectionOptions(projectionOptions); - vnode.domNode = element; - initPropertiesAndChildren(element, vnode, projectionOptions); - return createProjection(vnode, projectionOptions); - } - }; - /** - * Creates a [[CalculationCache]] object, useful for caching [[VNode]] trees. - * In practice, caching of [[VNode]] trees is not needed, because achieving 60 frames per second is almost never a problem. - * For more information, see [[CalculationCache]]. - * - * @param The type of the value that is cached. - */ - exports.createCache = function () { - var cachedInputs = undefined; - var cachedOutcome = undefined; - var result = { - invalidate: function () { - cachedOutcome = undefined; - cachedInputs = undefined; - }, - result: function (inputs, calculation) { - if (cachedInputs) { - for (var i = 0; i < inputs.length; i++) { - if (cachedInputs[i] !== inputs[i]) { - cachedOutcome = undefined; - } - } - } - if (!cachedOutcome) { - cachedOutcome = calculation(); - cachedInputs = inputs; - } - return cachedOutcome; - } - }; - return result; - }; - /** - * Creates a {@link Mapping} instance that keeps an array of result objects synchronized with an array of source objects. - * See {@link http://maquettejs.org/docs/arrays.html|Working with arrays}. - * - * @param The type of source items. A database-record for instance. - * @param The type of target items. A [[Component]] for instance. - * @param getSourceKey `function(source)` that must return a key to identify each source object. The result must either be a string or a number. - * @param createResult `function(source, index)` that must create a new result object from a given source. This function is identical - * to the `callback` argument in `Array.map(callback)`. - * @param updateResult `function(source, target, index)` that updates a result to an updated source. - */ - exports.createMapping = function (getSourceKey, createResult, updateResult) { - var keys = []; - var results = []; - return { - results: results, - map: function (newSources) { - var newKeys = newSources.map(getSourceKey); - var oldTargets = results.slice(); - var oldIndex = 0; - for (var i = 0; i < newSources.length; i++) { - var source = newSources[i]; - var sourceKey = newKeys[i]; - if (sourceKey === keys[oldIndex]) { - results[i] = oldTargets[oldIndex]; - updateResult(source, oldTargets[oldIndex], i); - oldIndex++; - } else { - var found = false; - for (var j = 1; j < keys.length; j++) { - var searchIndex = (oldIndex + j) % keys.length; - if (keys[searchIndex] === sourceKey) { - results[i] = oldTargets[searchIndex]; - updateResult(newSources[i], oldTargets[searchIndex], i); - oldIndex = searchIndex + 1; - found = true; - break; - } - } - if (!found) { - results[i] = createResult(source, i); - } - } - } - results.length = newSources.length; - keys = newKeys; - } - }; - }; - /** - * Creates a [[Projector]] instance using the provided projectionOptions. - * - * For more information, see [[Projector]]. - * - * @param projectionOptions Options that influence how the DOM is rendered and updated. - */ - exports.createProjector = function (projectorOptions) { - var projector; - var projectionOptions = applyDefaultProjectionOptions(projectorOptions); - projectionOptions.eventHandlerInterceptor = function (propertyName, eventHandler, domNode, properties) { - return function () { - // intercept function calls (event handlers) to do a render afterwards. - projector.scheduleRender(); - return eventHandler.apply(properties.bind || this, arguments); - }; - }; - var renderCompleted = true; - var scheduled; - var stopped = false; - var projections = []; - var renderFunctions = []; - // matches the projections array - var doRender = function () { - scheduled = undefined; - if (!renderCompleted) { - return; // The last render threw an error, it should be logged in the browser console. - } - renderCompleted = false; - for (var i = 0; i < projections.length; i++) { - var updatedVnode = renderFunctions[i](); - projections[i].update(updatedVnode); - } - renderCompleted = true; - }; - projector = { - scheduleRender: function () { - if (!scheduled && !stopped) { - scheduled = requestAnimationFrame(doRender); - } - }, - stop: function () { - if (scheduled) { - cancelAnimationFrame(scheduled); - scheduled = undefined; - } - stopped = true; - }, - resume: function () { - stopped = false; - renderCompleted = true; - projector.scheduleRender(); - }, - append: function (parentNode, renderMaquetteFunction) { - projections.push(exports.dom.append(parentNode, renderMaquetteFunction(), projectionOptions)); - renderFunctions.push(renderMaquetteFunction); - }, - insertBefore: function (beforeNode, renderMaquetteFunction) { - projections.push(exports.dom.insertBefore(beforeNode, renderMaquetteFunction(), projectionOptions)); - renderFunctions.push(renderMaquetteFunction); - }, - merge: function (domNode, renderMaquetteFunction) { - projections.push(exports.dom.merge(domNode, renderMaquetteFunction(), projectionOptions)); - renderFunctions.push(renderMaquetteFunction); - }, - replace: function (domNode, renderMaquetteFunction) { - var vnode = renderMaquetteFunction(); - createDom(vnode, domNode.parentNode, domNode, projectionOptions); - domNode.parentNode.removeChild(domNode); - projections.push(createProjection(vnode, projectionOptions)); - renderFunctions.push(renderMaquetteFunction); - }, - detach: function (renderMaquetteFunction) { - for (var i = 0; i < renderFunctions.length; i++) { - if (renderFunctions[i] === renderMaquetteFunction) { - renderFunctions.splice(i, 1); - return projections.splice(i, 1)[0]; - } - } - throw new Error('renderMaquetteFunction was not found'); - } - }; - return projector; - }; -})); diff --git a/plugins/UiPluginManager/media/js/utils/Animation.coffee b/plugins/UiPluginManager/media/js/utils/Animation.coffee deleted file mode 100644 index 271b88c1..00000000 --- a/plugins/UiPluginManager/media/js/utils/Animation.coffee +++ /dev/null @@ -1,138 +0,0 @@ -class Animation - slideDown: (elem, props) -> - if elem.offsetTop > 2000 - return - - h = elem.offsetHeight - cstyle = window.getComputedStyle(elem) - margin_top = cstyle.marginTop - margin_bottom = cstyle.marginBottom - padding_top = cstyle.paddingTop - padding_bottom = cstyle.paddingBottom - transition = cstyle.transition - - elem.style.boxSizing = "border-box" - elem.style.overflow = "hidden" - elem.style.transform = "scale(0.6)" - elem.style.opacity = "0" - elem.style.height = "0px" - elem.style.marginTop = "0px" - elem.style.marginBottom = "0px" - elem.style.paddingTop = "0px" - elem.style.paddingBottom = "0px" - elem.style.transition = "none" - - setTimeout (-> - elem.className += " animate-inout" - elem.style.height = h+"px" - elem.style.transform = "scale(1)" - elem.style.opacity = "1" - elem.style.marginTop = margin_top - elem.style.marginBottom = margin_bottom - elem.style.paddingTop = padding_top - elem.style.paddingBottom = padding_bottom - ), 1 - - elem.addEventListener "transitionend", -> - elem.classList.remove("animate-inout") - elem.style.transition = elem.style.transform = elem.style.opacity = elem.style.height = null - elem.style.boxSizing = elem.style.marginTop = elem.style.marginBottom = null - elem.style.paddingTop = elem.style.paddingBottom = elem.style.overflow = null - elem.removeEventListener "transitionend", arguments.callee, false - - - slideUp: (elem, remove_func, props) -> - if elem.offsetTop > 1000 - return remove_func() - - elem.className += " animate-back" - elem.style.boxSizing = "border-box" - elem.style.height = elem.offsetHeight+"px" - elem.style.overflow = "hidden" - elem.style.transform = "scale(1)" - elem.style.opacity = "1" - elem.style.pointerEvents = "none" - setTimeout (-> - elem.style.height = "0px" - elem.style.marginTop = "0px" - elem.style.marginBottom = "0px" - elem.style.paddingTop = "0px" - elem.style.paddingBottom = "0px" - elem.style.transform = "scale(0.8)" - elem.style.borderTopWidth = "0px" - elem.style.borderBottomWidth = "0px" - elem.style.opacity = "0" - ), 1 - elem.addEventListener "transitionend", (e) -> - if e.propertyName == "opacity" or e.elapsedTime >= 0.6 - elem.removeEventListener "transitionend", arguments.callee, false - remove_func() - - - slideUpInout: (elem, remove_func, props) -> - elem.className += " animate-inout" - elem.style.boxSizing = "border-box" - elem.style.height = elem.offsetHeight+"px" - elem.style.overflow = "hidden" - elem.style.transform = "scale(1)" - elem.style.opacity = "1" - elem.style.pointerEvents = "none" - setTimeout (-> - elem.style.height = "0px" - elem.style.marginTop = "0px" - elem.style.marginBottom = "0px" - elem.style.paddingTop = "0px" - elem.style.paddingBottom = "0px" - elem.style.transform = "scale(0.8)" - elem.style.borderTopWidth = "0px" - elem.style.borderBottomWidth = "0px" - elem.style.opacity = "0" - ), 1 - elem.addEventListener "transitionend", (e) -> - if e.propertyName == "opacity" or e.elapsedTime >= 0.6 - elem.removeEventListener "transitionend", arguments.callee, false - remove_func() - - - showRight: (elem, props) -> - elem.className += " animate" - elem.style.opacity = 0 - elem.style.transform = "TranslateX(-20px) Scale(1.01)" - setTimeout (-> - elem.style.opacity = 1 - elem.style.transform = "TranslateX(0px) Scale(1)" - ), 1 - elem.addEventListener "transitionend", -> - elem.classList.remove("animate") - elem.style.transform = elem.style.opacity = null - - - show: (elem, props) -> - delay = arguments[arguments.length-2]?.delay*1000 or 1 - elem.style.opacity = 0 - setTimeout (-> - elem.className += " animate" - ), 1 - setTimeout (-> - elem.style.opacity = 1 - ), delay - elem.addEventListener "transitionend", -> - elem.classList.remove("animate") - elem.style.opacity = null - elem.removeEventListener "transitionend", arguments.callee, false - - hide: (elem, remove_func, props) -> - delay = arguments[arguments.length-2]?.delay*1000 or 1 - elem.className += " animate" - setTimeout (-> - elem.style.opacity = 0 - ), delay - elem.addEventListener "transitionend", (e) -> - if e.propertyName == "opacity" - remove_func() - - addVisibleClass: (elem, props) -> - setTimeout -> - elem.classList.add("visible") - -window.Animation = new Animation() \ No newline at end of file diff --git a/plugins/UiPluginManager/media/js/utils/Dollar.coffee b/plugins/UiPluginManager/media/js/utils/Dollar.coffee deleted file mode 100644 index 7f19f551..00000000 --- a/plugins/UiPluginManager/media/js/utils/Dollar.coffee +++ /dev/null @@ -1,3 +0,0 @@ -window.$ = (selector) -> - if selector.startsWith("#") - return document.getElementById(selector.replace("#", "")) diff --git a/plugins/UiPluginManager/media/js/utils/ZeroFrame.coffee b/plugins/UiPluginManager/media/js/utils/ZeroFrame.coffee deleted file mode 100644 index 11512d16..00000000 --- a/plugins/UiPluginManager/media/js/utils/ZeroFrame.coffee +++ /dev/null @@ -1,85 +0,0 @@ -class ZeroFrame extends Class - constructor: (url) -> - @url = url - @waiting_cb = {} - @wrapper_nonce = document.location.href.replace(/.*wrapper_nonce=([A-Za-z0-9]+).*/, "$1") - @connect() - @next_message_id = 1 - @history_state = {} - @init() - - - init: -> - @ - - - connect: -> - @target = window.parent - window.addEventListener("message", @onMessage, false) - @cmd("innerReady") - - # Save scrollTop - window.addEventListener "beforeunload", (e) => - @log "save scrollTop", window.pageYOffset - @history_state["scrollTop"] = window.pageYOffset - @cmd "wrapperReplaceState", [@history_state, null] - - # Restore scrollTop - @cmd "wrapperGetState", [], (state) => - @history_state = state if state? - @log "restore scrollTop", state, window.pageYOffset - if window.pageYOffset == 0 and state - window.scroll(window.pageXOffset, state.scrollTop) - - - onMessage: (e) => - message = e.data - cmd = message.cmd - if cmd == "response" - if @waiting_cb[message.to]? - @waiting_cb[message.to](message.result) - else - @log "Websocket callback not found:", message - else if cmd == "wrapperReady" # Wrapper inited later - @cmd("innerReady") - else if cmd == "ping" - @response message.id, "pong" - else if cmd == "wrapperOpenedWebsocket" - @onOpenWebsocket() - else if cmd == "wrapperClosedWebsocket" - @onCloseWebsocket() - else - @onRequest cmd, message.params - - - onRequest: (cmd, message) => - @log "Unknown request", message - - - response: (to, result) -> - @send {"cmd": "response", "to": to, "result": result} - - - cmd: (cmd, params={}, cb=null) -> - @send {"cmd": cmd, "params": params}, cb - - - send: (message, cb=null) -> - message.wrapper_nonce = @wrapper_nonce - message.id = @next_message_id - @next_message_id += 1 - @target.postMessage(message, "*") - if cb - @waiting_cb[message.id] = cb - - - onOpenWebsocket: => - @log "Websocket open" - - - onCloseWebsocket: => - @log "Websocket close" - - - -window.ZeroFrame = ZeroFrame diff --git a/plugins/UiPluginManager/media/plugin_manager.html b/plugins/UiPluginManager/media/plugin_manager.html deleted file mode 100644 index 321cbbb3..00000000 --- a/plugins/UiPluginManager/media/plugin_manager.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - Settings - ZeroNet - - - - - - -

      ZeroNet plugin manager

      - -
      -
      - - - - \ No newline at end of file diff --git a/plugins/Zeroname/README.md b/plugins/Zeroname/README.md deleted file mode 100644 index baa43db5..00000000 --- a/plugins/Zeroname/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# ZeroName - -Zeroname plugin to connect Namecoin and register all the .bit domain name. - -## Start - -You can create your own Zeroname. - -### Namecoin node - -You need to run a namecoin node. - -[Namecoin](https://namecoin.org/download/) - -You will need to start it as a RPC server. - -Example of `~/.namecoin/namecoin.conf` minimal setup: -``` -daemon=1 -rpcuser=your-name -rpcpassword=your-password -rpcport=8336 -server=1 -txindex=1 -valueencoding=utf8 -``` - -Don't forget to change the `rpcuser` value and `rpcpassword` value! - -You can start your node : `./namecoind` - -### Create a Zeroname site - -You will also need to create a site `python zeronet.py createSite` and regitser the info. - -In the site you will need to create a file `./data//data/names.json` with this is it: -``` -{} -``` - -### `zeroname_config.json` file - -In `~/.namecoin/zeroname_config.json` -``` -{ - "lastprocessed": 223910, - "zeronet_path": "/root/ZeroNet", # Update with your path - "privatekey": "", # Update with your private key of your site - "site": "" # Update with the address of your site -} -``` - -### Run updater - -You can now run the script : `updater/zeroname_updater.py` and wait until it is fully sync (it might take a while). diff --git a/plugins/Zeroname/SiteManagerPlugin.py b/plugins/Zeroname/SiteManagerPlugin.py deleted file mode 100644 index 2553a50c..00000000 --- a/plugins/Zeroname/SiteManagerPlugin.py +++ /dev/null @@ -1,69 +0,0 @@ -import logging -import re -import time - -from Config import config -from Plugin import PluginManager - -allow_reload = False # No reload supported - -log = logging.getLogger("ZeronamePlugin") - - -@PluginManager.registerTo("SiteManager") -class SiteManagerPlugin(object): - site_zeroname = None - db_domains = {} - db_domains_modified = None - - def load(self, *args, **kwargs): - super(SiteManagerPlugin, self).load(*args, **kwargs) - if not self.get(config.bit_resolver): - self.need(config.bit_resolver) # Need ZeroName site - - # Return: True if the address is .bit domain - def isBitDomain(self, address): - return re.match(r"(.*?)([A-Za-z0-9_-]+\.bit)$", address) - - # Resolve domain - # Return: The address or None - def resolveBitDomain(self, domain): - domain = domain.lower() - if not self.site_zeroname: - self.site_zeroname = self.need(config.bit_resolver) - - site_zeroname_modified = self.site_zeroname.content_manager.contents.get("content.json", {}).get("modified", 0) - if not self.db_domains or self.db_domains_modified != site_zeroname_modified: - self.site_zeroname.needFile("data/names.json", priority=10) - s = time.time() - try: - self.db_domains = self.site_zeroname.storage.loadJson("data/names.json") - except Exception as err: - log.error("Error loading names.json: %s" % err) - - log.debug( - "Domain db with %s entries loaded in %.3fs (modification: %s -> %s)" % - (len(self.db_domains), time.time() - s, self.db_domains_modified, site_zeroname_modified) - ) - self.db_domains_modified = site_zeroname_modified - return self.db_domains.get(domain) - - # Turn domain into address - def resolveDomain(self, domain): - return self.resolveBitDomain(domain) or super(SiteManagerPlugin, self).resolveDomain(domain) - - # Return: True if the address is domain - def isDomain(self, address): - return self.isBitDomain(address) or super(SiteManagerPlugin, self).isDomain(address) - - -@PluginManager.registerTo("ConfigPlugin") -class ConfigPlugin(object): - def createArguments(self): - group = self.parser.add_argument_group("Zeroname plugin") - group.add_argument( - "--bit_resolver", help="ZeroNet site to resolve .bit domains", - default="1Name2NXVi1RDPDgf5617UoW7xA6YrhM9F", metavar="address" - ) - - return super(ConfigPlugin, self).createArguments() diff --git a/plugins/Zeroname/__init__.py b/plugins/Zeroname/__init__.py deleted file mode 100644 index 76826d9a..00000000 --- a/plugins/Zeroname/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import SiteManagerPlugin diff --git a/plugins/Zeroname/updater/zeroname_updater.py b/plugins/Zeroname/updater/zeroname_updater.py deleted file mode 100644 index 1e00332c..00000000 --- a/plugins/Zeroname/updater/zeroname_updater.py +++ /dev/null @@ -1,249 +0,0 @@ -from __future__ import print_function -import time -import json -import os -import sys -import re -import socket - -from six import string_types - -from subprocess import call -from bitcoinrpc.authproxy import AuthServiceProxy - - -def publish(): - print("* Signing and Publishing...") - call(" ".join(command_sign_publish), shell=True) - - -def processNameOp(domain, value, test=False): - if not value.strip().startswith("{"): - return False - try: - data = json.loads(value) - except Exception as err: - print("Json load error: %s" % err) - return False - if "zeronet" not in data and "map" not in data: - # Namecoin standard use {"map": { "blog": {"zeronet": "1D..."} }} - print("No zeronet and no map in ", data.keys()) - return False - if "map" in data: - # If subdomains using the Namecoin standard is present, just re-write in the Zeronet way - # and call the function again - data_map = data["map"] - new_value = {} - for subdomain in data_map: - if "zeronet" in data_map[subdomain]: - new_value[subdomain] = data_map[subdomain]["zeronet"] - if "zeronet" in data and isinstance(data["zeronet"], string_types): - # { - # "zeronet":"19rXKeKptSdQ9qt7omwN82smehzTuuq6S9", - # .... - # } - new_value[""] = data["zeronet"] - if len(new_value) > 0: - return processNameOp(domain, json.dumps({"zeronet": new_value}), test) - else: - return False - if "zeronet" in data and isinstance(data["zeronet"], string_types): - # { - # "zeronet":"19rXKeKptSdQ9qt7omwN82smehzTuuq6S9" - # } is valid - return processNameOp(domain, json.dumps({"zeronet": { "": data["zeronet"]}}), test) - if not isinstance(data["zeronet"], dict): - print("Not dict: ", data["zeronet"]) - return False - if not re.match("^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$", domain): - print("Invalid domain: ", domain) - return False - - if test: - return True - - if "slave" in sys.argv: - print("Waiting for master update arrive") - time.sleep(30) # Wait 30 sec to allow master updater - - # Note: Requires the file data/names.json to exist and contain "{}" to work - names_raw = open(names_path, "rb").read() - names = json.loads(names_raw) - for subdomain, address in data["zeronet"].items(): - subdomain = subdomain.lower() - address = re.sub("[^A-Za-z0-9]", "", address) - print(subdomain, domain, "->", address) - if subdomain: - if re.match("^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$", subdomain): - names["%s.%s.bit" % (subdomain, domain)] = address - else: - print("Invalid subdomain:", domain, subdomain) - else: - names["%s.bit" % domain] = address - - new_names_raw = json.dumps(names, indent=2, sort_keys=True) - if new_names_raw != names_raw: - open(names_path, "wb").write(new_names_raw) - print("-", domain, "Changed") - return True - else: - print("-", domain, "Not changed") - return False - - -def processBlock(block_id, test=False): - print("Processing block #%s..." % block_id) - s = time.time() - block_hash = rpc.getblockhash(block_id) - block = rpc.getblock(block_hash) - - print("Checking %s tx" % len(block["tx"])) - updated = 0 - for tx in block["tx"]: - try: - transaction = rpc.getrawtransaction(tx, 1) - for vout in transaction.get("vout", []): - if "scriptPubKey" in vout and "nameOp" in vout["scriptPubKey"] and "name" in vout["scriptPubKey"]["nameOp"]: - name_op = vout["scriptPubKey"]["nameOp"] - updated += processNameOp(name_op["name"].replace("d/", ""), name_op["value"], test) - except Exception as err: - print("Error processing tx #%s %s" % (tx, err)) - print("Done in %.3fs (updated %s)." % (time.time() - s, updated)) - return updated - -# Connecting to RPC -def initRpc(config): - """Initialize Namecoin RPC""" - rpc_data = { - 'connect': '127.0.0.1', - 'port': '8336', - 'user': 'PLACEHOLDER', - 'password': 'PLACEHOLDER', - 'clienttimeout': '900' - } - try: - fptr = open(config, 'r') - lines = fptr.readlines() - fptr.close() - except: - return None # Or take some other appropriate action - - for line in lines: - if not line.startswith('rpc'): - continue - key_val = line.split(None, 1)[0] - (key, val) = key_val.split('=', 1) - if not key or not val: - continue - rpc_data[key[3:]] = val - - url = 'http://%(user)s:%(password)s@%(connect)s:%(port)s' % rpc_data - - return url, int(rpc_data['clienttimeout']) - -# Loading config... - -# Check whether platform is on windows or linux -# On linux namecoin is installed under ~/.namecoin, while on on windows it is in %appdata%/Namecoin - -if sys.platform == "win32": - namecoin_location = os.getenv('APPDATA') + "/Namecoin/" -else: - namecoin_location = os.path.expanduser("~/.namecoin/") - -config_path = namecoin_location + 'zeroname_config.json' -if not os.path.isfile(config_path): # Create sample config - open(config_path, "w").write( - json.dumps({'site': 'site', 'zeronet_path': '/home/zeronet', 'privatekey': '', 'lastprocessed': 223910}, indent=2) - ) - print("* Example config written to %s" % config_path) - sys.exit(0) - -config = json.load(open(config_path)) -names_path = "%s/data/%s/data/names.json" % (config["zeronet_path"], config["site"]) -os.chdir(config["zeronet_path"]) # Change working dir - tells script where Zeronet install is. - -# Parameters to sign and publish -command_sign_publish = [sys.executable, "zeronet.py", "siteSign", config["site"], config["privatekey"], "--publish"] -if sys.platform == 'win32': - command_sign_publish = ['"%s"' % param for param in command_sign_publish] - -# Initialize rpc connection -rpc_auth, rpc_timeout = initRpc(namecoin_location + "namecoin.conf") -rpc = AuthServiceProxy(rpc_auth, timeout=rpc_timeout) - -node_version = rpc.getnetworkinfo()['version'] - -while 1: - try: - time.sleep(1) - if node_version < 160000 : - last_block = int(rpc.getinfo()["blocks"]) - else: - last_block = int(rpc.getblockchaininfo()["blocks"]) - break # Connection succeeded - except socket.timeout: # Timeout - print(".", end=' ') - sys.stdout.flush() - except Exception as err: - print("Exception", err.__class__, err) - time.sleep(5) - rpc = AuthServiceProxy(rpc_auth, timeout=rpc_timeout) - -if not config["lastprocessed"]: # First startup: Start processing from last block - config["lastprocessed"] = last_block - - -print("- Testing domain parsing...") -assert processBlock(223911, test=True) # Testing zeronetwork.bit -assert processBlock(227052, test=True) # Testing brainwallets.bit -assert not processBlock(236824, test=True) # Utf8 domain name (invalid should skip) -assert not processBlock(236752, test=True) # Uppercase domain (invalid should skip) -assert processBlock(236870, test=True) # Encoded domain (should pass) -assert processBlock(438317, test=True) # Testing namecoin standard artifaxradio.bit (should pass) -# sys.exit(0) - -print("- Parsing skipped blocks...") -should_publish = False -for block_id in range(config["lastprocessed"], last_block + 1): - if processBlock(block_id): - should_publish = True -config["lastprocessed"] = last_block - -if should_publish: - publish() - -while 1: - print("- Waiting for new block") - sys.stdout.flush() - while 1: - try: - time.sleep(1) - if node_version < 160000 : - rpc.waitforblock() - else: - rpc.waitfornewblock() - print("Found") - break # Block found - except socket.timeout: # Timeout - print(".", end=' ') - sys.stdout.flush() - except Exception as err: - print("Exception", err.__class__, err) - time.sleep(5) - rpc = AuthServiceProxy(rpc_auth, timeout=rpc_timeout) - - if node_version < 160000 : - last_block = int(rpc.getinfo()["blocks"]) - else: - last_block = int(rpc.getblockchaininfo()["blocks"]) - should_publish = False - for block_id in range(config["lastprocessed"] + 1, last_block + 1): - if processBlock(block_id): - should_publish = True - - config["lastprocessed"] = last_block - open(config_path, "w").write(json.dumps(config, indent=2)) - - if should_publish: - publish() diff --git a/plugins/__init__.py b/plugins/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/plugins/disabled-Bootstrapper/BootstrapperDb.py b/plugins/disabled-Bootstrapper/BootstrapperDb.py deleted file mode 100644 index 0866dc3e..00000000 --- a/plugins/disabled-Bootstrapper/BootstrapperDb.py +++ /dev/null @@ -1,156 +0,0 @@ -import time -import re - -import gevent - -from Config import config -from Db import Db -from util import helper - - -class BootstrapperDb(Db.Db): - def __init__(self): - self.version = 7 - self.hash_ids = {} # hash -> id cache - super(BootstrapperDb, self).__init__({"db_name": "Bootstrapper"}, "%s/bootstrapper.db" % config.data_dir) - self.foreign_keys = True - self.checkTables() - self.updateHashCache() - gevent.spawn(self.cleanup) - - def cleanup(self): - while 1: - time.sleep(4 * 60) - timeout = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time() - 60 * 40)) - self.execute("DELETE FROM peer WHERE date_announced < ?", [timeout]) - - def updateHashCache(self): - res = self.execute("SELECT * FROM hash") - self.hash_ids = {row["hash"]: row["hash_id"] for row in res} - self.log.debug("Loaded %s hash_ids" % len(self.hash_ids)) - - def checkTables(self): - version = int(self.execute("PRAGMA user_version").fetchone()[0]) - self.log.debug("Db version: %s, needed: %s" % (version, self.version)) - if version < self.version: - self.createTables() - else: - self.execute("VACUUM") - - def createTables(self): - # Delete all tables - self.execute("PRAGMA writable_schema = 1") - self.execute("DELETE FROM sqlite_master WHERE type IN ('table', 'index', 'trigger')") - self.execute("PRAGMA writable_schema = 0") - self.execute("VACUUM") - self.execute("PRAGMA INTEGRITY_CHECK") - # Create new tables - self.execute(""" - CREATE TABLE peer ( - peer_id INTEGER PRIMARY KEY ASC AUTOINCREMENT NOT NULL UNIQUE, - type TEXT, - address TEXT, - port INTEGER NOT NULL, - date_added DATETIME DEFAULT (CURRENT_TIMESTAMP), - date_announced DATETIME DEFAULT (CURRENT_TIMESTAMP) - ); - """) - self.execute("CREATE UNIQUE INDEX peer_key ON peer (address, port);") - - self.execute(""" - CREATE TABLE peer_to_hash ( - peer_to_hash_id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, - peer_id INTEGER REFERENCES peer (peer_id) ON DELETE CASCADE, - hash_id INTEGER REFERENCES hash (hash_id) - ); - """) - self.execute("CREATE INDEX peer_id ON peer_to_hash (peer_id);") - self.execute("CREATE INDEX hash_id ON peer_to_hash (hash_id);") - - self.execute(""" - CREATE TABLE hash ( - hash_id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, - hash BLOB UNIQUE NOT NULL, - date_added DATETIME DEFAULT (CURRENT_TIMESTAMP) - ); - """) - self.execute("PRAGMA user_version = %s" % self.version) - - def getHashId(self, hash): - if hash not in self.hash_ids: - self.log.debug("New hash: %s" % repr(hash)) - res = self.execute("INSERT OR IGNORE INTO hash ?", {"hash": hash}) - self.hash_ids[hash] = res.lastrowid - return self.hash_ids[hash] - - def peerAnnounce(self, ip_type, address, port=None, hashes=[], onion_signed=False, delete_missing_hashes=False): - hashes_ids_announced = [] - for hash in hashes: - hashes_ids_announced.append(self.getHashId(hash)) - - # Check user - res = self.execute("SELECT peer_id FROM peer WHERE ? LIMIT 1", {"address": address, "port": port}) - - user_row = res.fetchone() - now = time.strftime("%Y-%m-%d %H:%M:%S") - if user_row: - peer_id = user_row["peer_id"] - self.execute("UPDATE peer SET date_announced = ? WHERE peer_id = ?", (now, peer_id)) - else: - self.log.debug("New peer: %s signed: %s" % (address, onion_signed)) - if ip_type == "onion" and not onion_signed: - return len(hashes) - res = self.execute("INSERT INTO peer ?", {"type": ip_type, "address": address, "port": port, "date_announced": now}) - peer_id = res.lastrowid - - # Check user's hashes - res = self.execute("SELECT * FROM peer_to_hash WHERE ?", {"peer_id": peer_id}) - hash_ids_db = [row["hash_id"] for row in res] - if hash_ids_db != hashes_ids_announced: - hash_ids_added = set(hashes_ids_announced) - set(hash_ids_db) - hash_ids_removed = set(hash_ids_db) - set(hashes_ids_announced) - if ip_type != "onion" or onion_signed: - for hash_id in hash_ids_added: - self.execute("INSERT INTO peer_to_hash ?", {"peer_id": peer_id, "hash_id": hash_id}) - if hash_ids_removed and delete_missing_hashes: - self.execute("DELETE FROM peer_to_hash WHERE ?", {"peer_id": peer_id, "hash_id": list(hash_ids_removed)}) - - return len(hash_ids_added) + len(hash_ids_removed) - else: - return 0 - - def peerList(self, hash, address=None, onions=[], port=None, limit=30, need_types=["ipv4", "onion"], order=True): - back = {"ipv4": [], "ipv6": [], "onion": []} - if limit == 0: - return back - hashid = self.getHashId(hash) - - if order: - order_sql = "ORDER BY date_announced DESC" - else: - order_sql = "" - where_sql = "hash_id = :hashid" - if onions: - onions_escaped = ["'%s'" % re.sub("[^a-z0-9,]", "", onion) for onion in onions if type(onion) is str] - where_sql += " AND address NOT IN (%s)" % ",".join(onions_escaped) - elif address: - where_sql += " AND NOT (address = :address AND port = :port)" - - query = """ - SELECT type, address, port - FROM peer_to_hash - LEFT JOIN peer USING (peer_id) - WHERE %s - %s - LIMIT :limit - """ % (where_sql, order_sql) - res = self.execute(query, {"hashid": hashid, "address": address, "port": port, "limit": limit}) - - for row in res: - if row["type"] in need_types: - if row["type"] == "onion": - packed = helper.packOnionAddress(row["address"], row["port"]) - else: - packed = helper.packAddress(str(row["address"]), row["port"]) - back[row["type"]].append(packed) - return back diff --git a/plugins/disabled-Bootstrapper/BootstrapperPlugin.py b/plugins/disabled-Bootstrapper/BootstrapperPlugin.py deleted file mode 100644 index 59e7af7b..00000000 --- a/plugins/disabled-Bootstrapper/BootstrapperPlugin.py +++ /dev/null @@ -1,156 +0,0 @@ -import time - -from util import helper - -from Plugin import PluginManager -from .BootstrapperDb import BootstrapperDb -from Crypt import CryptRsa -from Config import config - -if "db" not in locals().keys(): # Share during reloads - db = BootstrapperDb() - - -@PluginManager.registerTo("FileRequest") -class FileRequestPlugin(object): - def checkOnionSigns(self, onions, onion_signs, onion_sign_this): - if not onion_signs or len(onion_signs) != len(set(onions)): - return False - - if time.time() - float(onion_sign_this) > 3 * 60: - return False # Signed out of allowed 3 minutes - - onions_signed = [] - # Check onion signs - for onion_publickey, onion_sign in onion_signs.items(): - if CryptRsa.verify(onion_sign_this.encode(), onion_publickey, onion_sign): - onions_signed.append(CryptRsa.publickeyToOnion(onion_publickey)) - else: - break - - # Check if the same onion addresses signed as the announced onces - if sorted(onions_signed) == sorted(set(onions)): - return True - else: - return False - - def actionAnnounce(self, params): - time_started = time.time() - s = time.time() - # Backward compatibility - if "ip4" in params["add"]: - params["add"].append("ipv4") - if "ip4" in params["need_types"]: - params["need_types"].append("ipv4") - - hashes = params["hashes"] - - all_onions_signed = self.checkOnionSigns(params.get("onions", []), params.get("onion_signs"), params.get("onion_sign_this")) - - time_onion_check = time.time() - s - - ip_type = helper.getIpType(self.connection.ip) - - if ip_type == "onion" or self.connection.ip in config.ip_local: - is_port_open = False - elif ip_type in params["add"]: - is_port_open = True - else: - is_port_open = False - - s = time.time() - # Separatley add onions to sites or at once if no onions present - i = 0 - onion_to_hash = {} - for onion in params.get("onions", []): - if onion not in onion_to_hash: - onion_to_hash[onion] = [] - onion_to_hash[onion].append(hashes[i]) - i += 1 - - hashes_changed = 0 - for onion, onion_hashes in onion_to_hash.items(): - hashes_changed += db.peerAnnounce( - ip_type="onion", - address=onion, - port=params["port"], - hashes=onion_hashes, - onion_signed=all_onions_signed - ) - time_db_onion = time.time() - s - - s = time.time() - - if is_port_open: - hashes_changed += db.peerAnnounce( - ip_type=ip_type, - address=self.connection.ip, - port=params["port"], - hashes=hashes, - delete_missing_hashes=params.get("delete") - ) - time_db_ip = time.time() - s - - s = time.time() - # Query sites - back = {} - peers = [] - if params.get("onions") and not all_onions_signed and hashes_changed: - back["onion_sign_this"] = "%.0f" % time.time() # Send back nonce for signing - - if len(hashes) > 500 or not hashes_changed: - limit = 5 - order = False - else: - limit = 30 - order = True - for hash in hashes: - if time.time() - time_started > 1: # 1 sec limit on request - self.connection.log("Announce time limit exceeded after %s/%s sites" % (len(peers), len(hashes))) - break - - hash_peers = db.peerList( - hash, - address=self.connection.ip, onions=list(onion_to_hash.keys()), port=params["port"], - limit=min(limit, params["need_num"]), need_types=params["need_types"], order=order - ) - if "ip4" in params["need_types"]: # Backward compatibility - hash_peers["ip4"] = hash_peers["ipv4"] - del(hash_peers["ipv4"]) - peers.append(hash_peers) - time_peerlist = time.time() - s - - back["peers"] = peers - self.connection.log( - "Announce %s sites (onions: %s, onion_check: %.3fs, db_onion: %.3fs, db_ip: %.3fs, peerlist: %.3fs, limit: %s)" % - (len(hashes), len(onion_to_hash), time_onion_check, time_db_onion, time_db_ip, time_peerlist, limit) - ) - self.response(back) - - -@PluginManager.registerTo("UiRequest") -class UiRequestPlugin(object): - @helper.encodeResponse - def actionStatsBootstrapper(self): - self.sendHeader() - - # Style - yield """ - - """ - - hash_rows = db.execute("SELECT * FROM hash").fetchall() - for hash_row in hash_rows: - peer_rows = db.execute( - "SELECT * FROM peer LEFT JOIN peer_to_hash USING (peer_id) WHERE hash_id = :hash_id", - {"hash_id": hash_row["hash_id"]} - ).fetchall() - - yield "
      %s (added: %s, peers: %s)
      " % ( - str(hash_row["hash"]).encode().hex(), hash_row["date_added"], len(peer_rows) - ) - for peer_row in peer_rows: - yield " - {type} {address}:{port} added: {date_added}, announced: {date_announced}
      ".format(**dict(peer_row)) diff --git a/plugins/disabled-Bootstrapper/Test/TestBootstrapper.py b/plugins/disabled-Bootstrapper/Test/TestBootstrapper.py deleted file mode 100644 index 69bdc54c..00000000 --- a/plugins/disabled-Bootstrapper/Test/TestBootstrapper.py +++ /dev/null @@ -1,246 +0,0 @@ -import hashlib -import os - -import pytest - -from Bootstrapper import BootstrapperPlugin -from Bootstrapper.BootstrapperDb import BootstrapperDb -from Peer import Peer -from Crypt import CryptRsa -from util import helper - - -@pytest.fixture() -def bootstrapper_db(request): - BootstrapperPlugin.db.close() - BootstrapperPlugin.db = BootstrapperDb() - BootstrapperPlugin.db.createTables() # Reset db - BootstrapperPlugin.db.cur.logging = True - - def cleanup(): - BootstrapperPlugin.db.close() - os.unlink(BootstrapperPlugin.db.db_path) - - request.addfinalizer(cleanup) - return BootstrapperPlugin.db - - -@pytest.mark.usefixtures("resetSettings") -class TestBootstrapper: - def testHashCache(self, file_server, bootstrapper_db): - ip_type = helper.getIpType(file_server.ip) - peer = Peer(file_server.ip, 1544, connection_server=file_server) - hash1 = hashlib.sha256(b"site1").digest() - hash2 = hashlib.sha256(b"site2").digest() - hash3 = hashlib.sha256(b"site3").digest() - - # Verify empty result - res = peer.request("announce", { - "hashes": [hash1, hash2], - "port": 15441, "need_types": [ip_type], "need_num": 10, "add": [ip_type] - }) - - assert len(res["peers"][0][ip_type]) == 0 # Empty result - - hash_ids_before = bootstrapper_db.hash_ids.copy() - - bootstrapper_db.updateHashCache() - - assert hash_ids_before == bootstrapper_db.hash_ids - - - def testBootstrapperDb(self, file_server, bootstrapper_db): - ip_type = helper.getIpType(file_server.ip) - peer = Peer(file_server.ip, 1544, connection_server=file_server) - hash1 = hashlib.sha256(b"site1").digest() - hash2 = hashlib.sha256(b"site2").digest() - hash3 = hashlib.sha256(b"site3").digest() - - # Verify empty result - res = peer.request("announce", { - "hashes": [hash1, hash2], - "port": 15441, "need_types": [ip_type], "need_num": 10, "add": [ip_type] - }) - - assert len(res["peers"][0][ip_type]) == 0 # Empty result - - # Verify added peer on previous request - bootstrapper_db.peerAnnounce(ip_type, file_server.ip_external, port=15441, hashes=[hash1, hash2], delete_missing_hashes=True) - - res = peer.request("announce", { - "hashes": [hash1, hash2], - "port": 15441, "need_types": [ip_type], "need_num": 10, "add": [ip_type] - }) - assert len(res["peers"][0][ip_type]) == 1 - assert len(res["peers"][1][ip_type]) == 1 - - # hash2 deleted from 1.2.3.4 - bootstrapper_db.peerAnnounce(ip_type, file_server.ip_external, port=15441, hashes=[hash1], delete_missing_hashes=True) - res = peer.request("announce", { - "hashes": [hash1, hash2], - "port": 15441, "need_types": [ip_type], "need_num": 10, "add": [ip_type] - }) - assert len(res["peers"][0][ip_type]) == 1 - assert len(res["peers"][1][ip_type]) == 0 - - # Announce 3 hash again - bootstrapper_db.peerAnnounce(ip_type, file_server.ip_external, port=15441, hashes=[hash1, hash2, hash3], delete_missing_hashes=True) - res = peer.request("announce", { - "hashes": [hash1, hash2, hash3], - "port": 15441, "need_types": [ip_type], "need_num": 10, "add": [ip_type] - }) - assert len(res["peers"][0][ip_type]) == 1 - assert len(res["peers"][1][ip_type]) == 1 - assert len(res["peers"][2][ip_type]) == 1 - - # Single hash announce - res = peer.request("announce", { - "hashes": [hash1], "port": 15441, "need_types": [ip_type], "need_num": 10, "add": [ip_type] - }) - assert len(res["peers"][0][ip_type]) == 1 - - # Test DB cleanup - assert [row[0] for row in bootstrapper_db.execute("SELECT address FROM peer").fetchall()] == [file_server.ip_external] # 127.0.0.1 never get added to db - - # Delete peers - bootstrapper_db.execute("DELETE FROM peer WHERE address = ?", [file_server.ip_external]) - assert bootstrapper_db.execute("SELECT COUNT(*) AS num FROM peer_to_hash").fetchone()["num"] == 0 - - assert bootstrapper_db.execute("SELECT COUNT(*) AS num FROM hash").fetchone()["num"] == 3 # 3 sites - assert bootstrapper_db.execute("SELECT COUNT(*) AS num FROM peer").fetchone()["num"] == 0 # 0 peer - - def testPassive(self, file_server, bootstrapper_db): - peer = Peer(file_server.ip, 1544, connection_server=file_server) - ip_type = helper.getIpType(file_server.ip) - hash1 = hashlib.sha256(b"hash1").digest() - - bootstrapper_db.peerAnnounce(ip_type, address=None, port=15441, hashes=[hash1]) - res = peer.request("announce", { - "hashes": [hash1], "port": 15441, "need_types": [ip_type], "need_num": 10, "add": [] - }) - - assert len(res["peers"][0]["ipv4"]) == 0 # Empty result - - def testAddOnion(self, file_server, site, bootstrapper_db, tor_manager): - onion1 = tor_manager.addOnion() - onion2 = tor_manager.addOnion() - peer = Peer(file_server.ip, 1544, connection_server=file_server) - hash1 = hashlib.sha256(b"site1").digest() - hash2 = hashlib.sha256(b"site2").digest() - hash3 = hashlib.sha256(b"site3").digest() - - bootstrapper_db.peerAnnounce(ip_type="ipv4", address="1.2.3.4", port=1234, hashes=[hash1, hash2, hash3]) - res = peer.request("announce", { - "onions": [onion1, onion1, onion2], - "hashes": [hash1, hash2, hash3], "port": 15441, "need_types": ["ipv4", "onion"], "need_num": 10, "add": ["onion"] - }) - assert len(res["peers"][0]["ipv4"]) == 1 - - # Onion address not added yet - site_peers = bootstrapper_db.peerList(address="1.2.3.4", port=1234, hash=hash1) - assert len(site_peers["onion"]) == 0 - assert "onion_sign_this" in res - - # Sign the nonces - sign1 = CryptRsa.sign(res["onion_sign_this"].encode(), tor_manager.getPrivatekey(onion1)) - sign2 = CryptRsa.sign(res["onion_sign_this"].encode(), tor_manager.getPrivatekey(onion2)) - - # Bad sign (different address) - res = peer.request("announce", { - "onions": [onion1], "onion_sign_this": res["onion_sign_this"], - "onion_signs": {tor_manager.getPublickey(onion2): sign2}, - "hashes": [hash1], "port": 15441, "need_types": ["ipv4", "onion"], "need_num": 10, "add": ["onion"] - }) - assert "onion_sign_this" in res - site_peers1 = bootstrapper_db.peerList(address="1.2.3.4", port=1234, hash=hash1) - assert len(site_peers1["onion"]) == 0 # Not added - - # Bad sign (missing one) - res = peer.request("announce", { - "onions": [onion1, onion1, onion2], "onion_sign_this": res["onion_sign_this"], - "onion_signs": {tor_manager.getPublickey(onion1): sign1}, - "hashes": [hash1, hash2, hash3], "port": 15441, "need_types": ["ipv4", "onion"], "need_num": 10, "add": ["onion"] - }) - assert "onion_sign_this" in res - site_peers1 = bootstrapper_db.peerList(address="1.2.3.4", port=1234, hash=hash1) - assert len(site_peers1["onion"]) == 0 # Not added - - # Good sign - res = peer.request("announce", { - "onions": [onion1, onion1, onion2], "onion_sign_this": res["onion_sign_this"], - "onion_signs": {tor_manager.getPublickey(onion1): sign1, tor_manager.getPublickey(onion2): sign2}, - "hashes": [hash1, hash2, hash3], "port": 15441, "need_types": ["ipv4", "onion"], "need_num": 10, "add": ["onion"] - }) - assert "onion_sign_this" not in res - - # Onion addresses added - site_peers1 = bootstrapper_db.peerList(address="1.2.3.4", port=1234, hash=hash1) - assert len(site_peers1["onion"]) == 1 - site_peers2 = bootstrapper_db.peerList(address="1.2.3.4", port=1234, hash=hash2) - assert len(site_peers2["onion"]) == 1 - site_peers3 = bootstrapper_db.peerList(address="1.2.3.4", port=1234, hash=hash3) - assert len(site_peers3["onion"]) == 1 - - assert site_peers1["onion"][0] == site_peers2["onion"][0] - assert site_peers2["onion"][0] != site_peers3["onion"][0] - assert helper.unpackOnionAddress(site_peers1["onion"][0])[0] == onion1 + ".onion" - assert helper.unpackOnionAddress(site_peers2["onion"][0])[0] == onion1 + ".onion" - assert helper.unpackOnionAddress(site_peers3["onion"][0])[0] == onion2 + ".onion" - - tor_manager.delOnion(onion1) - tor_manager.delOnion(onion2) - - def testRequestPeers(self, file_server, site, bootstrapper_db, tor_manager): - site.connection_server = file_server - file_server.tor_manager = tor_manager - hash = hashlib.sha256(site.address.encode()).digest() - - # Request peers from tracker - assert len(site.peers) == 0 - bootstrapper_db.peerAnnounce(ip_type="ipv4", address="1.2.3.4", port=1234, hashes=[hash]) - site.announcer.announceTracker("zero://%s:%s" % (file_server.ip, file_server.port)) - assert len(site.peers) == 1 - - # Test onion address store - bootstrapper_db.peerAnnounce(ip_type="onion", address="bka4ht2bzxchy44r", port=1234, hashes=[hash], onion_signed=True) - site.announcer.announceTracker("zero://%s:%s" % (file_server.ip, file_server.port)) - assert len(site.peers) == 2 - assert "bka4ht2bzxchy44r.onion:1234" in site.peers - - @pytest.mark.slow - def testAnnounce(self, file_server, tor_manager): - file_server.tor_manager = tor_manager - hash1 = hashlib.sha256(b"1Nekos4fiBqfcazyG1bAxdBT5oBvA76Z").digest() - hash2 = hashlib.sha256(b"1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr").digest() - peer = Peer("zero.booth.moe", 443, connection_server=file_server) - assert peer.request("ping") - peer = Peer("boot3rdez4rzn36x.onion", 15441, connection_server=file_server) - assert peer.request("ping") - res = peer.request("announce", { - "hashes": [hash1, hash2], - "port": 15441, "need_types": ["ip4", "onion"], "need_num": 100, "add": [""] - }) - - assert res - - def testBackwardCompatibility(self, file_server, bootstrapper_db): - peer = Peer(file_server.ip, 1544, connection_server=file_server) - hash1 = hashlib.sha256(b"site1").digest() - - bootstrapper_db.peerAnnounce("ipv4", file_server.ip_external, port=15441, hashes=[hash1], delete_missing_hashes=True) - - # Test with ipv4 need type - res = peer.request("announce", { - "hashes": [hash1], - "port": 15441, "need_types": ["ipv4"], "need_num": 10, "add": [] - }) - - assert len(res["peers"][0]["ipv4"]) == 1 - - # Test with ip4 need type - res = peer.request("announce", { - "hashes": [hash1], - "port": 15441, "need_types": ["ip4"], "need_num": 10, "add": [] - }) - - assert len(res["peers"][0]["ip4"]) == 1 diff --git a/plugins/disabled-Bootstrapper/Test/conftest.py b/plugins/disabled-Bootstrapper/Test/conftest.py deleted file mode 100644 index 8c1df5b2..00000000 --- a/plugins/disabled-Bootstrapper/Test/conftest.py +++ /dev/null @@ -1 +0,0 @@ -from src.Test.conftest import * \ No newline at end of file diff --git a/plugins/disabled-Bootstrapper/Test/pytest.ini b/plugins/disabled-Bootstrapper/Test/pytest.ini deleted file mode 100644 index 8ee21268..00000000 --- a/plugins/disabled-Bootstrapper/Test/pytest.ini +++ /dev/null @@ -1,6 +0,0 @@ -[pytest] -python_files = Test*.py -addopts = -rsxX -v --durations=6 -markers = - slow: mark a tests as slow. - webtest: mark a test as a webtest. \ No newline at end of file diff --git a/plugins/disabled-Bootstrapper/__init__.py b/plugins/disabled-Bootstrapper/__init__.py deleted file mode 100644 index cce30eea..00000000 --- a/plugins/disabled-Bootstrapper/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import BootstrapperPlugin \ No newline at end of file diff --git a/plugins/disabled-Bootstrapper/plugin_info.json b/plugins/disabled-Bootstrapper/plugin_info.json deleted file mode 100644 index 06915d4d..00000000 --- a/plugins/disabled-Bootstrapper/plugin_info.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Bootstrapper", - "description": "Add BitTorrent tracker server like features to your ZeroNet client.", - "default": "disabled" -} \ No newline at end of file diff --git a/plugins/disabled-Dnschain/SiteManagerPlugin.py b/plugins/disabled-Dnschain/SiteManagerPlugin.py deleted file mode 100644 index 8b9508f1..00000000 --- a/plugins/disabled-Dnschain/SiteManagerPlugin.py +++ /dev/null @@ -1,153 +0,0 @@ -import logging, json, os, re, sys, time -import gevent -from Plugin import PluginManager -from Config import config -from util import Http -from Debug import Debug - -allow_reload = False # No reload supported - -log = logging.getLogger("DnschainPlugin") - -@PluginManager.registerTo("SiteManager") -class SiteManagerPlugin(object): - dns_cache_path = "%s/dns_cache.json" % config.data_dir - dns_cache = None - - # Checks if its a valid address - def isAddress(self, address): - if self.isDomain(address): - return True - else: - return super(SiteManagerPlugin, self).isAddress(address) - - - # Return: True if the address is domain - def isDomain(self, address): - return re.match(r"(.*?)([A-Za-z0-9_-]+\.[A-Za-z0-9]+)$", address) - - - # Load dns entries from data/dns_cache.json - def loadDnsCache(self): - if os.path.isfile(self.dns_cache_path): - self.dns_cache = json.load(open(self.dns_cache_path)) - else: - self.dns_cache = {} - log.debug("Loaded dns cache, entries: %s" % len(self.dns_cache)) - - - # Save dns entries to data/dns_cache.json - def saveDnsCache(self): - json.dump(self.dns_cache, open(self.dns_cache_path, "wb"), indent=2) - - - # Resolve domain using dnschain.net - # Return: The address or None - def resolveDomainDnschainNet(self, domain): - try: - match = self.isDomain(domain) - sub_domain = match.group(1).strip(".") - top_domain = match.group(2) - if not sub_domain: sub_domain = "@" - address = None - with gevent.Timeout(5, Exception("Timeout: 5s")): - res = Http.get("https://api.dnschain.net/v1/namecoin/key/%s" % top_domain).read() - data = json.loads(res)["data"]["value"] - if "zeronet" in data: - for key, val in data["zeronet"].items(): - self.dns_cache[key+"."+top_domain] = [val, time.time()+60*60*5] # Cache for 5 hours - self.saveDnsCache() - return data["zeronet"].get(sub_domain) - # Not found - return address - except Exception as err: - log.debug("Dnschain.net %s resolve error: %s" % (domain, Debug.formatException(err))) - - - # Resolve domain using dnschain.info - # Return: The address or None - def resolveDomainDnschainInfo(self, domain): - try: - match = self.isDomain(domain) - sub_domain = match.group(1).strip(".") - top_domain = match.group(2) - if not sub_domain: sub_domain = "@" - address = None - with gevent.Timeout(5, Exception("Timeout: 5s")): - res = Http.get("https://dnschain.info/bit/d/%s" % re.sub(r"\.bit$", "", top_domain)).read() - data = json.loads(res)["value"] - for key, val in data["zeronet"].items(): - self.dns_cache[key+"."+top_domain] = [val, time.time()+60*60*5] # Cache for 5 hours - self.saveDnsCache() - return data["zeronet"].get(sub_domain) - # Not found - return address - except Exception as err: - log.debug("Dnschain.info %s resolve error: %s" % (domain, Debug.formatException(err))) - - - # Resolve domain - # Return: The address or None - def resolveDomain(self, domain): - domain = domain.lower() - if self.dns_cache == None: - self.loadDnsCache() - if domain.count(".") < 2: # Its a topleved request, prepend @. to it - domain = "@."+domain - - domain_details = self.dns_cache.get(domain) - if domain_details and time.time() < domain_details[1]: # Found in cache and its not expired - return domain_details[0] - else: - # Resovle dns using dnschain - thread_dnschain_info = gevent.spawn(self.resolveDomainDnschainInfo, domain) - thread_dnschain_net = gevent.spawn(self.resolveDomainDnschainNet, domain) - gevent.joinall([thread_dnschain_net, thread_dnschain_info]) # Wait for finish - - if thread_dnschain_info.value and thread_dnschain_net.value: # Booth successfull - if thread_dnschain_info.value == thread_dnschain_net.value: # Same returned value - return thread_dnschain_info.value - else: - log.error("Dns %s missmatch: %s != %s" % (domain, thread_dnschain_info.value, thread_dnschain_net.value)) - - # Problem during resolve - if domain_details: # Resolve failed, but we have it in the cache - domain_details[1] = time.time()+60*60 # Dont try again for 1 hour - return domain_details[0] - else: # Not found in cache - self.dns_cache[domain] = [None, time.time()+60] # Don't check again for 1 min - return None - - - # Return or create site and start download site files - # Return: Site or None if dns resolve failed - def need(self, address, all_file=True): - if self.isDomain(address): # Its looks like a domain - address_resolved = self.resolveDomain(address) - if address_resolved: - address = address_resolved - else: - return None - - return super(SiteManagerPlugin, self).need(address, all_file) - - - # Return: Site object or None if not found - def get(self, address): - if self.sites == None: # Not loaded yet - self.load() - if self.isDomain(address): # Its looks like a domain - address_resolved = self.resolveDomain(address) - if address_resolved: # Domain found - site = self.sites.get(address_resolved) - if site: - site_domain = site.settings.get("domain") - if site_domain != address: - site.settings["domain"] = address - else: # Domain not found - site = self.sites.get(address) - - else: # Access by site address - site = self.sites.get(address) - return site - diff --git a/plugins/disabled-Dnschain/UiRequestPlugin.py b/plugins/disabled-Dnschain/UiRequestPlugin.py deleted file mode 100644 index 8ab9d5c5..00000000 --- a/plugins/disabled-Dnschain/UiRequestPlugin.py +++ /dev/null @@ -1,34 +0,0 @@ -import re -from Plugin import PluginManager - -@PluginManager.registerTo("UiRequest") -class UiRequestPlugin(object): - def __init__(self, server = None): - from Site import SiteManager - self.site_manager = SiteManager.site_manager - super(UiRequestPlugin, self).__init__(server) - - - # Media request - def actionSiteMedia(self, path): - match = re.match(r"/media/(?P
      [A-Za-z0-9-]+\.[A-Za-z0-9\.-]+)(?P/.*|$)", path) - if match: # Its a valid domain, resolve first - domain = match.group("address") - address = self.site_manager.resolveDomain(domain) - if address: - path = "/media/"+address+match.group("inner_path") - return super(UiRequestPlugin, self).actionSiteMedia(path) # Get the wrapper frame output - - - # Is mediarequest allowed from that referer - def isMediaRequestAllowed(self, site_address, referer): - referer_path = re.sub("http[s]{0,1}://.*?/", "/", referer).replace("/media", "") # Remove site address - referer_site_address = re.match(r"/(?P
      [A-Za-z0-9\.-]+)(?P/.*|$)", referer_path).group("address") - - if referer_site_address == site_address: # Referer site address as simple address - return True - elif self.site_manager.resolveDomain(referer_site_address) == site_address: # Referer site address as dns - return True - else: # Invalid referer - return False - diff --git a/plugins/disabled-Dnschain/__init__.py b/plugins/disabled-Dnschain/__init__.py deleted file mode 100644 index 2b36af5d..00000000 --- a/plugins/disabled-Dnschain/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This plugin is experimental, if you really want to enable uncomment the following lines: -# import DnschainPlugin -# import SiteManagerPlugin \ No newline at end of file diff --git a/plugins/disabled-DonationMessage/DonationMessagePlugin.py b/plugins/disabled-DonationMessage/DonationMessagePlugin.py deleted file mode 100644 index 8cf0d541..00000000 --- a/plugins/disabled-DonationMessage/DonationMessagePlugin.py +++ /dev/null @@ -1,22 +0,0 @@ -import re -from Plugin import PluginManager - -# Warning: If you modify the donation address then renmae the plugin's directory to "MyDonationMessage" to prevent the update script overwrite - - -@PluginManager.registerTo("UiRequest") -class UiRequestPlugin(object): - # Inject a donation message to every page top right corner - def renderWrapper(self, *args, **kwargs): - body = super(UiRequestPlugin, self).renderWrapper(*args, **kwargs) # Get the wrapper frame output - - inject_html = """ - - Please donate to help to keep this ZeroProxy alive - - - """ - - return re.sub(r"\s*\s*$", inject_html, body) diff --git a/plugins/disabled-DonationMessage/__init__.py b/plugins/disabled-DonationMessage/__init__.py deleted file mode 100644 index 1d4b47c3..00000000 --- a/plugins/disabled-DonationMessage/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import DonationMessagePlugin diff --git a/plugins/disabled-Multiuser/MultiuserPlugin.py b/plugins/disabled-Multiuser/MultiuserPlugin.py deleted file mode 100644 index 799c3337..00000000 --- a/plugins/disabled-Multiuser/MultiuserPlugin.py +++ /dev/null @@ -1,275 +0,0 @@ -import re -import sys -import json - -from Config import config -from Plugin import PluginManager -from Crypt import CryptBitcoin -from . import UserPlugin -from util.Flag import flag -from Translate import translate as _ - -# We can only import plugin host clases after the plugins are loaded -@PluginManager.afterLoad -def importPluginnedClasses(): - global UserManager - from User import UserManager - -try: - local_master_addresses = set(json.load(open("%s/users.json" % config.data_dir)).keys()) # Users in users.json -except Exception as err: - local_master_addresses = set() - - -@PluginManager.registerTo("UiRequest") -class UiRequestPlugin(object): - def __init__(self, *args, **kwargs): - self.user_manager = UserManager.user_manager - super(UiRequestPlugin, self).__init__(*args, **kwargs) - - # Create new user and inject user welcome message if necessary - # Return: Html body also containing the injection - def actionWrapper(self, path, extra_headers=None): - - match = re.match("/(?P
      [A-Za-z0-9\._-]+)(?P/.*|$)", path) - if not match: - return False - - inner_path = match.group("inner_path").lstrip("/") - html_request = "." not in inner_path or inner_path.endswith(".html") # Only inject html to html requests - - user_created = False - if html_request: - user = self.getCurrentUser() # Get user from cookie - if not user: # No user found by cookie - user = self.user_manager.create() - user_created = True - else: - user = None - - # Disable new site creation if --multiuser_no_new_sites enabled - if config.multiuser_no_new_sites: - path_parts = self.parsePath(path) - if not self.server.site_manager.get(match.group("address")) and (not user or user.master_address not in local_master_addresses): - self.sendHeader(404) - return self.formatError("Not Found", "Adding new sites disabled on this proxy", details=False) - - if user_created: - if not extra_headers: - extra_headers = {} - extra_headers['Set-Cookie'] = "master_address=%s;path=/;max-age=2592000;" % user.master_address # = 30 days - - loggedin = self.get.get("login") == "done" - - back_generator = super(UiRequestPlugin, self).actionWrapper(path, extra_headers) # Get the wrapper frame output - - if not back_generator: # Wrapper error or not string returned, injection not possible - return False - - elif loggedin: - back = next(back_generator) - inject_html = """ - - - - - """.replace("\t", "") - if user.master_address in local_master_addresses: - message = "Hello master!" - else: - message = "Hello again!" - inject_html = inject_html.replace("{message}", message) - inject_html = inject_html.replace("{script_nonce}", self.getScriptNonce()) - return iter([re.sub(b"\s*\s*$", inject_html.encode(), back)]) # Replace the tags with the injection - - else: # No injection necessary - return back_generator - - # Get the current user based on request's cookies - # Return: User object or None if no match - def getCurrentUser(self): - cookies = self.getCookies() - user = None - if "master_address" in cookies: - users = self.user_manager.list() - user = users.get(cookies["master_address"]) - return user - - -@PluginManager.registerTo("UiWebsocket") -class UiWebsocketPlugin(object): - def __init__(self, *args, **kwargs): - if config.multiuser_no_new_sites: - flag.no_multiuser(self.actionMergerSiteAdd) - - super(UiWebsocketPlugin, self).__init__(*args, **kwargs) - - # Let the page know we running in multiuser mode - def formatServerInfo(self): - server_info = super(UiWebsocketPlugin, self).formatServerInfo() - server_info["multiuser"] = True - if "ADMIN" in self.site.settings["permissions"]: - server_info["master_address"] = self.user.master_address - is_multiuser_admin = config.multiuser_local or self.user.master_address in local_master_addresses - server_info["multiuser_admin"] = is_multiuser_admin - return server_info - - # Show current user's master seed - @flag.admin - def actionUserShowMasterSeed(self, to): - message = "Your unique private key:" - message += "
      %s
      " % self.user.master_seed - message += "(Save it, you can access your account using this information)" - self.cmd("notification", ["info", message]) - - # Logout user - @flag.admin - def actionUserLogout(self, to): - message = "You have been logged out. Login to another account" - self.cmd("notification", ["done", message, 1000000]) # 1000000 = Show ~forever :) - - script = "document.cookie = 'master_address=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/';" - script += "$('#button_notification').on('click', function() { zeroframe.cmd(\"userLoginForm\", []); });" - self.cmd("injectScript", script) - # Delete from user_manager - user_manager = UserManager.user_manager - if self.user.master_address in user_manager.users: - if not config.multiuser_local: - del user_manager.users[self.user.master_address] - self.response(to, "Successful logout") - else: - self.response(to, "User not found") - - @flag.admin - def actionUserSet(self, to, master_address): - user_manager = UserManager.user_manager - user = user_manager.get(master_address) - if not user: - raise Exception("No user found") - - script = "document.cookie = 'master_address=%s;path=/;max-age=2592000;';" % master_address - script += "zeroframe.cmd('wrapperReload', ['login=done']);" - self.cmd("notification", ["done", "Successful login, reloading page..."]) - self.cmd("injectScript", script) - - self.response(to, "ok") - - @flag.admin - def actionUserSelectForm(self, to): - if not config.multiuser_local: - raise Exception("Only allowed in multiuser local mode") - user_manager = UserManager.user_manager - body = "" + "Change account:" + "" - for master_address, user in user_manager.list().items(): - is_active = self.user.master_address == master_address - if user.certs: - first_cert = next(iter(user.certs.keys())) - title = "%s@%s" % (user.certs[first_cert]["auth_user_name"], first_cert) - else: - title = user.master_address - if len(user.sites) < 2 and not is_active: # Avoid listing ad-hoc created users - continue - if is_active: - css_class = "active" - else: - css_class = "noclass" - body += "%s" % (css_class, user.master_address, title) - - script = """ - $(".notification .select.user").on("click", function() { - $(".notification .select").removeClass('active') - zeroframe.response(%s, this.title) - return false - }) - """ % self.next_message_id - - self.cmd("notification", ["ask", body], lambda master_address: self.actionUserSet(to, master_address)) - self.cmd("injectScript", script) - - # Show login form - def actionUserLoginForm(self, to): - self.cmd("prompt", ["Login
      Your private key:", "password", "Login"], self.responseUserLogin) - - # Login form submit - def responseUserLogin(self, master_seed): - user_manager = UserManager.user_manager - user = user_manager.get(CryptBitcoin.privatekeyToAddress(master_seed)) - if not user: - user = user_manager.create(master_seed=master_seed) - if user.master_address: - script = "document.cookie = 'master_address=%s;path=/;max-age=2592000;';" % user.master_address - script += "zeroframe.cmd('wrapperReload', ['login=done']);" - self.cmd("notification", ["done", "Successful login, reloading page..."]) - self.cmd("injectScript", script) - else: - self.cmd("notification", ["error", "Error: Invalid master seed"]) - self.actionUserLoginForm(0) - - def hasCmdPermission(self, cmd): - flags = flag.db.get(self.getCmdFuncName(cmd), ()) - is_public_proxy_user = not config.multiuser_local and self.user.master_address not in local_master_addresses - if is_public_proxy_user and "no_multiuser" in flags: - self.cmd("notification", ["info", _("This function ({cmd}) is disabled on this proxy!")]) - return False - else: - return super(UiWebsocketPlugin, self).hasCmdPermission(cmd) - - def actionCertAdd(self, *args, **kwargs): - super(UiWebsocketPlugin, self).actionCertAdd(*args, **kwargs) - master_seed = self.user.master_seed - message = """ - - Hello, welcome to ZeroProxy!
      A new, unique account created for you:
      - - -
      - This is your private key, save it, so you can login next time.
      - Warning: Without this key, your account will be lost forever! -

      - Ok, Saved it!

      - This site allows you to browse ZeroNet content, but if you want to secure your account
      - and help to keep the network alive, then please run your own ZeroNet client.
      - """ - - self.cmd("notification", ["info", message]) - - script = """ - $("#button_notification_masterseed").on("click", function() { - this.value = "{master_seed}"; this.setSelectionRange(0,100); - }) - $("#button_notification_download").on("mousedown", function() { - this.href = window.URL.createObjectURL(new Blob(["ZeroNet user master seed:\\r\\n{master_seed}"])) - }) - """.replace("{master_seed}", master_seed) - self.cmd("injectScript", script) - - def actionPermissionAdd(self, to, permission): - is_public_proxy_user = not config.multiuser_local and self.user.master_address not in local_master_addresses - if permission == "NOSANDBOX" and is_public_proxy_user: - self.cmd("notification", ["info", "You can't disable sandbox on this proxy!"]) - self.response(to, {"error": "Denied by proxy"}) - return False - else: - return super(UiWebsocketPlugin, self).actionPermissionAdd(to, permission) - - -@PluginManager.registerTo("ConfigPlugin") -class ConfigPlugin(object): - def createArguments(self): - group = self.parser.add_argument_group("Multiuser plugin") - group.add_argument('--multiuser_local', help="Enable unsafe Ui functions and write users to disk", action='store_true') - group.add_argument('--multiuser_no_new_sites', help="Denies adding new sites by normal users", action='store_true') - - return super(ConfigPlugin, self).createArguments() diff --git a/plugins/disabled-Multiuser/Test/TestMultiuser.py b/plugins/disabled-Multiuser/Test/TestMultiuser.py deleted file mode 100644 index b8ff4267..00000000 --- a/plugins/disabled-Multiuser/Test/TestMultiuser.py +++ /dev/null @@ -1,14 +0,0 @@ -import pytest -import json -from Config import config -from User import UserManager - -@pytest.mark.usefixtures("resetSettings") -@pytest.mark.usefixtures("resetTempSettings") -class TestMultiuser: - def testMemorySave(self, user): - # It should not write users to disk - users_before = open("%s/users.json" % config.data_dir).read() - user = UserManager.user_manager.create() - user.save() - assert open("%s/users.json" % config.data_dir).read() == users_before diff --git a/plugins/disabled-Multiuser/Test/conftest.py b/plugins/disabled-Multiuser/Test/conftest.py deleted file mode 100644 index 634e66e2..00000000 --- a/plugins/disabled-Multiuser/Test/conftest.py +++ /dev/null @@ -1 +0,0 @@ -from src.Test.conftest import * diff --git a/plugins/disabled-Multiuser/Test/pytest.ini b/plugins/disabled-Multiuser/Test/pytest.ini deleted file mode 100644 index d09210d1..00000000 --- a/plugins/disabled-Multiuser/Test/pytest.ini +++ /dev/null @@ -1,5 +0,0 @@ -[pytest] -python_files = Test*.py -addopts = -rsxX -v --durations=6 -markers = - webtest: mark a test as a webtest. \ No newline at end of file diff --git a/plugins/disabled-Multiuser/UserPlugin.py b/plugins/disabled-Multiuser/UserPlugin.py deleted file mode 100644 index 3c9ebae8..00000000 --- a/plugins/disabled-Multiuser/UserPlugin.py +++ /dev/null @@ -1,35 +0,0 @@ -from Config import config -from Plugin import PluginManager - -allow_reload = False - -@PluginManager.registerTo("UserManager") -class UserManagerPlugin(object): - def load(self): - if not config.multiuser_local: - # In multiuser mode do not load the users - if not self.users: - self.users = {} - return self.users - else: - return super(UserManagerPlugin, self).load() - - # Find user by master address - # Return: User or None - def get(self, master_address=None): - users = self.list() - if master_address in users: - user = users[master_address] - else: - user = None - return user - - -@PluginManager.registerTo("User") -class UserPlugin(object): - # In multiuser mode users data only exits in memory, dont write to data/user.json - def save(self): - if not config.multiuser_local: - return False - else: - return super(UserPlugin, self).save() diff --git a/plugins/disabled-Multiuser/__init__.py b/plugins/disabled-Multiuser/__init__.py deleted file mode 100644 index c56ddf84..00000000 --- a/plugins/disabled-Multiuser/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import MultiuserPlugin diff --git a/plugins/disabled-Multiuser/plugin_info.json b/plugins/disabled-Multiuser/plugin_info.json deleted file mode 100644 index e440ed8e..00000000 --- a/plugins/disabled-Multiuser/plugin_info.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "MultiUser", - "description": "Cookie based multi-users support on your ZeroNet web interface.", - "default": "disabled" -} \ No newline at end of file diff --git a/plugins/disabled-StemPort/StemPortPlugin.py b/plugins/disabled-StemPort/StemPortPlugin.py deleted file mode 100644 index c53d38e6..00000000 --- a/plugins/disabled-StemPort/StemPortPlugin.py +++ /dev/null @@ -1,135 +0,0 @@ -import logging -import traceback - -import socket -import stem -from stem import Signal -from stem.control import Controller -from stem.socket import ControlPort - -from Plugin import PluginManager -from Config import config -from Debug import Debug - -if config.tor != "disable": - from gevent import monkey - monkey.patch_time() - monkey.patch_socket(dns=False) - monkey.patch_thread() - print("Stem Port Plugin: modules are patched.") -else: - print("Stem Port Plugin: Tor mode disabled. Module patching skipped.") - - -class PatchedControlPort(ControlPort): - def _make_socket(self): - try: - if "socket_noproxy" in dir(socket): # Socket proxy-patched, use non-proxy one - control_socket = socket.socket_noproxy(socket.AF_INET, socket.SOCK_STREAM) - else: - control_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - # TODO: repeated code - consider making a separate method - - control_socket.connect((self._control_addr, self._control_port)) - return control_socket - except socket.error as exc: - raise stem.SocketError(exc) - -def from_port(address = '127.0.0.1', port = 'default'): - import stem.connection - - if not stem.util.connection.is_valid_ipv4_address(address): - raise ValueError('Invalid IP address: %s' % address) - elif port != 'default' and not stem.util.connection.is_valid_port(port): - raise ValueError('Invalid port: %s' % port) - - if port == 'default': - raise ValueError('Must specify a port') - else: - control_port = PatchedControlPort(address, port) - - return Controller(control_port) - - -@PluginManager.registerTo("TorManager") -class TorManagerPlugin(object): - - def connectController(self): - self.log.info("Authenticate using Stem... %s:%s" % (self.ip, self.port)) - - try: - with self.lock: - if config.tor_password: - controller = from_port(port=self.port, password=config.tor_password) - else: - controller = from_port(port=self.port) - controller.authenticate() - self.controller = controller - self.status = "Connected (via Stem)" - except Exception as err: - print("\n") - traceback.print_exc() - print("\n") - - self.controller = None - self.status = "Error (%s)" % err - self.log.error("Tor stem connect error: %s" % Debug.formatException(err)) - - return self.controller - - - def disconnect(self): - self.controller.close() - self.controller = None - - - def resetCircuits(self): - try: - self.controller.signal(Signal.NEWNYM) - except Exception as err: - self.status = "Stem reset circuits error (%s)" % err - self.log.error("Stem reset circuits error: %s" % err) - - - def makeOnionAndKey(self): - try: - service = self.controller.create_ephemeral_hidden_service( - {self.fileserver_port: self.fileserver_port}, - await_publication = False - ) - if service.private_key_type != "RSA1024": - raise Exception("ZeroNet doesn't support crypto " + service.private_key_type) - - self.log.debug("Stem created %s.onion (async descriptor publication)" % service.service_id) - - return (service.service_id, service.private_key) - - except Exception as err: - self.status = "AddOnion error (Stem: %s)" % err - self.log.error("Failed to create hidden service with Stem: " + err) - return False - - - def delOnion(self, address): - try: - self.controller.remove_ephemeral_hidden_service(address) - return True - except Exception as err: - self.status = "DelOnion error (Stem: %s)" % err - self.log.error("Stem failed to delete %s.onion: %s" % (address, err)) - self.disconnect() # Why? - return False - - - def request(self, cmd): - with self.lock: - if not self.enabled: - return False - else: - self.log.error("[WARNING] StemPort self.request should not be called") - return "" - - def send(self, cmd, conn=None): - self.log.error("[WARNING] StemPort self.send should not be called") - return "" diff --git a/plugins/disabled-StemPort/__init__.py b/plugins/disabled-StemPort/__init__.py deleted file mode 100644 index 33f8e034..00000000 --- a/plugins/disabled-StemPort/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -try: - from stem.control import Controller - stem_found = True -except Exception as err: - print(("STEM NOT FOUND! %s" % err)) - stem_found = False - -if stem_found: - print("Starting Stem plugin...") - from . import StemPortPlugin diff --git a/plugins/disabled-UiPassword/UiPasswordPlugin.py b/plugins/disabled-UiPassword/UiPasswordPlugin.py deleted file mode 100644 index e8a4e4fe..00000000 --- a/plugins/disabled-UiPassword/UiPasswordPlugin.py +++ /dev/null @@ -1,183 +0,0 @@ -import string -import random -import time -import json -import re -import os - -from Config import config -from Plugin import PluginManager -from util import helper - - -plugin_dir = os.path.dirname(__file__) - -if "sessions" not in locals().keys(): # To keep sessions between module reloads - sessions = {} - whitelisted_client_ids = {} - - -def showPasswordAdvice(password): - error_msgs = [] - if not password or not isinstance(password, str): - error_msgs.append("You have enabled UiPassword plugin, but you forgot to set a password!") - elif len(password) < 8: - error_msgs.append("You are using a very short UI password!") - return error_msgs - - -@PluginManager.registerTo("UiRequest") -class UiRequestPlugin(object): - sessions = sessions - whitelisted_client_ids = whitelisted_client_ids - last_cleanup = time.time() - - def getClientId(self): - return self.env["REMOTE_ADDR"] + " - " + self.env["HTTP_USER_AGENT"] - - def whitelistClientId(self, session_id=None): - if not session_id: - session_id = self.getCookies().get("session_id") - client_id = self.getClientId() - if client_id in self.whitelisted_client_ids: - self.whitelisted_client_ids[client_id]["updated"] = time.time() - return False - - self.whitelisted_client_ids[client_id] = { - "added": time.time(), - "updated": time.time(), - "session_id": session_id - } - - def route(self, path): - # Restict Ui access by ip - if config.ui_restrict and self.env['REMOTE_ADDR'] not in config.ui_restrict: - return self.error403(details=False) - if path.endswith("favicon.ico"): - return self.actionFile("src/Ui/media/img/favicon.ico") - else: - if config.ui_password: - if time.time() - self.last_cleanup > 60 * 60: # Cleanup expired sessions every hour - self.sessionCleanup() - # Validate session - session_id = self.getCookies().get("session_id") - if session_id not in self.sessions and self.getClientId() not in self.whitelisted_client_ids: - # Invalid session id and not whitelisted ip: display login - return self.actionLogin() - return super(UiRequestPlugin, self).route(path) - - def actionWrapper(self, path, *args, **kwargs): - if config.ui_password and self.isWrapperNecessary(path): - session_id = self.getCookies().get("session_id") - if session_id not in self.sessions: - # We only accept cookie based auth on wrapper - return self.actionLogin() - else: - self.whitelistClientId() - - return super().actionWrapper(path, *args, **kwargs) - - # Action: Login - @helper.encodeResponse - def actionLogin(self): - template = open(plugin_dir + "/login.html").read() - self.sendHeader() - posted = self.getPosted() - if posted: # Validate http posted data - if self.sessionCheckPassword(posted.get("password")): - # Valid password, create session - session_id = self.randomString(26) - self.sessions[session_id] = { - "added": time.time(), - "keep": posted.get("keep") - } - self.whitelistClientId(session_id) - - # Redirect to homepage or referer - url = self.env.get("HTTP_REFERER", "") - if not url or re.sub(r"\?.*", "", url).endswith("/Login"): - url = "/" + config.homepage - cookie_header = ('Set-Cookie', "session_id=%s;path=/;max-age=2592000;" % session_id) # Max age = 30 days - self.start_response('301 Redirect', [('Location', url), cookie_header]) - yield "Redirecting..." - - else: - # Invalid password, show login form again - template = template.replace("{result}", "bad_password") - yield template - - def randomString(self, nchars): - return ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(nchars)) - - def sessionCheckPassword(self, password): - return password == config.ui_password - - def sessionDelete(self, session_id): - del self.sessions[session_id] - - for client_id in list(self.whitelisted_client_ids): - if self.whitelisted_client_ids[client_id]["session_id"] == session_id: - del self.whitelisted_client_ids[client_id] - - def sessionCleanup(self): - self.last_cleanup = time.time() - for session_id, session in list(self.sessions.items()): - if session["keep"] and time.time() - session["added"] > 60 * 60 * 24 * 60: # Max 60days for keep sessions - self.sessionDelete(session_id) - elif not session["keep"] and time.time() - session["added"] > 60 * 60 * 24: # Max 24h for non-keep sessions - self.sessionDelete(session_id) - - # Action: Display sessions - @helper.encodeResponse - def actionSessions(self): - self.sendHeader() - yield "
      "
      -        yield json.dumps(self.sessions, indent=4)
      -        yield "\r\n"
      -        yield json.dumps(self.whitelisted_client_ids, indent=4)
      -
      -    # Action: Logout
      -    @helper.encodeResponse
      -    def actionLogout(self):
      -        # Session id has to passed as get parameter or called without referer to avoid remote logout
      -        session_id = self.getCookies().get("session_id")
      -        if not self.env.get("HTTP_REFERER") or session_id == self.get.get("session_id"):
      -            if session_id in self.sessions:
      -                self.sessionDelete(session_id)
      -
      -            self.start_response('301 Redirect', [
      -                ('Location', "/"),
      -                ('Set-Cookie', "session_id=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT")
      -            ])
      -            yield "Redirecting..."
      -        else:
      -            self.sendHeader()
      -            yield "Error: Invalid session id"
      -
      -
      -@PluginManager.registerTo("ConfigPlugin")
      -class ConfigPlugin(object):
      -    def createArguments(self):
      -        group = self.parser.add_argument_group("UiPassword plugin")
      -        group.add_argument('--ui_password', help='Password to access UiServer', default=None, metavar="password")
      -
      -        return super(ConfigPlugin, self).createArguments()
      -
      -
      -from Translate import translate as lang
      -@PluginManager.registerTo("UiWebsocket")
      -class UiWebsocketPlugin(object):
      -    def actionUiLogout(self, to):
      -        permissions = self.getPermissions(to)
      -        if "ADMIN" not in permissions:
      -            return self.response(to, "You don't have permission to run this command")
      -
      -        session_id = self.request.getCookies().get("session_id", "")
      -        self.cmd("redirect", '/Logout?session_id=%s' % session_id)
      -
      -    def addHomepageNotifications(self):
      -        error_msgs = showPasswordAdvice(config.ui_password)
      -        for msg in error_msgs:
      -            self.site.notifications.append(["error", lang[msg]])
      -
      -        return super(UiWebsocketPlugin, self).addHomepageNotifications()
      diff --git a/plugins/disabled-UiPassword/__init__.py b/plugins/disabled-UiPassword/__init__.py
      deleted file mode 100644
      index 1779c597..00000000
      --- a/plugins/disabled-UiPassword/__init__.py
      +++ /dev/null
      @@ -1 +0,0 @@
      -from . import UiPasswordPlugin
      \ No newline at end of file
      diff --git a/plugins/disabled-UiPassword/login.html b/plugins/disabled-UiPassword/login.html
      deleted file mode 100644
      index 12d0889d..00000000
      --- a/plugins/disabled-UiPassword/login.html
      +++ /dev/null
      @@ -1,116 +0,0 @@
      -
      -
      - Log In
      - 
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      diff --git a/plugins/disabled-UiPassword/plugin_info.json b/plugins/disabled-UiPassword/plugin_info.json
      deleted file mode 100644
      index d3649a17..00000000
      --- a/plugins/disabled-UiPassword/plugin_info.json
      +++ /dev/null
      @@ -1,5 +0,0 @@
      -{
      -	"name": "UiPassword",
      -	"description": "Password based autentication on the web interface.",
      -	"default": "disabled"
      -}
      \ No newline at end of file
      diff --git a/plugins/disabled-ZeronameLocal/SiteManagerPlugin.py b/plugins/disabled-ZeronameLocal/SiteManagerPlugin.py
      deleted file mode 100644
      index 579e31c1..00000000
      --- a/plugins/disabled-ZeronameLocal/SiteManagerPlugin.py
      +++ /dev/null
      @@ -1,180 +0,0 @@
      -import logging, json, os, re, sys, time, socket
      -from Plugin import PluginManager
      -from Config import config
      -from Debug import Debug
      -from http.client import HTTPSConnection, HTTPConnection, HTTPException
      -from base64 import b64encode
      -
      -allow_reload = False # No reload supported
      -
      -@PluginManager.registerTo("SiteManager")
      -class SiteManagerPlugin(object):
      -    def load(self, *args, **kwargs):
      -        super(SiteManagerPlugin, self).load(*args, **kwargs)
      -        self.log = logging.getLogger("ZeronetLocal Plugin")
      -        self.error_message = None
      -        if not config.namecoin_host or not config.namecoin_rpcport or not config.namecoin_rpcuser or not config.namecoin_rpcpassword:
      -            self.error_message = "Missing parameters"
      -            self.log.error("Missing parameters to connect to namecoin node. Please check all the arguments needed with '--help'. Zeronet will continue working without it.")
      -            return
      -
      -        url = "%(host)s:%(port)s" % {"host": config.namecoin_host, "port": config.namecoin_rpcport}
      -        self.c = HTTPConnection(url, timeout=3)
      -        user_pass = "%(user)s:%(password)s" % {"user": config.namecoin_rpcuser, "password": config.namecoin_rpcpassword}
      -        userAndPass = b64encode(bytes(user_pass, "utf-8")).decode("ascii")
      -        self.headers = {"Authorization" : "Basic %s" %  userAndPass, "Content-Type": " application/json " }
      -
      -        payload = json.dumps({
      -            "jsonrpc": "2.0",
      -            "id": "zeronet",
      -            "method": "ping",
      -            "params": []
      -        })
      -
      -        try:
      -            self.c.request("POST", "/", payload, headers=self.headers)
      -            response = self.c.getresponse()
      -            data = response.read()
      -            self.c.close()
      -            if response.status == 200:
      -                result = json.loads(data.decode())["result"]
      -            else:
      -                raise Exception(response.reason)
      -        except Exception as err:
      -            self.log.error("The Namecoin node is unreachable. Please check the configuration value are correct. Zeronet will continue working without it.")
      -            self.error_message = err
      -        self.cache = dict()
      -
      -    # Checks if it's a valid address
      -    def isAddress(self, address):
      -        return self.isBitDomain(address) or super(SiteManagerPlugin, self).isAddress(address)
      -
      -    # Return: True if the address is domain
      -    def isDomain(self, address):
      -        return self.isBitDomain(address) or super(SiteManagerPlugin, self).isDomain(address)
      -
      -    # Return: True if the address is .bit domain
      -    def isBitDomain(self, address):
      -        return re.match(r"(.*?)([A-Za-z0-9_-]+\.bit)$", address)
      -
      -    # Return: Site object or None if not found
      -    def get(self, address):
      -        if self.isBitDomain(address):  # Its looks like a domain
      -            address_resolved = self.resolveDomain(address)
      -            if address_resolved:  # Domain found
      -                site = self.sites.get(address_resolved)
      -                if site:
      -                    site_domain = site.settings.get("domain")
      -                    if site_domain != address:
      -                        site.settings["domain"] = address
      -            else:  # Domain not found
      -                site = self.sites.get(address)
      -
      -        else:  # Access by site address
      -            site = super(SiteManagerPlugin, self).get(address)
      -        return site
      -
      -    # Return or create site and start download site files
      -    # Return: Site or None if dns resolve failed
      -    def need(self, address, *args, **kwargs):
      -        if self.isBitDomain(address):  # Its looks like a domain
      -            address_resolved = self.resolveDomain(address)
      -            if address_resolved:
      -                address = address_resolved
      -            else:
      -                return None
      -
      -        return super(SiteManagerPlugin, self).need(address, *args, **kwargs)
      -
      -    # Resolve domain
      -    # Return: The address or None
      -    def resolveDomain(self, domain):
      -        domain = domain.lower()
      -
      -        #remove .bit on end
      -        if domain[-4:] == ".bit":
      -            domain = domain[0:-4]
      -
      -        domain_array = domain.split(".")
      -
      -        if self.error_message:
      -            self.log.error("Not able to connect to Namecoin node : {!s}".format(self.error_message))
      -            return None
      -
      -        if len(domain_array) > 2:
      -            self.log.error("Too many subdomains! Can only handle one level (eg. staging.mixtape.bit)")
      -            return None
      -
      -        subdomain = ""
      -        if len(domain_array) == 1:
      -            domain = domain_array[0]
      -        else:
      -            subdomain = domain_array[0]
      -            domain = domain_array[1]
      -
      -        if domain in self.cache:
      -            delta = time.time() - self.cache[domain]["time"]
      -            if delta < 3600:
      -                # Must have been less than 1hour
      -                return self.cache[domain]["addresses_resolved"][subdomain]
      -
      -        payload = json.dumps({
      -            "jsonrpc": "2.0",
      -            "id": "zeronet",
      -            "method": "name_show",
      -            "params": ["d/"+domain]
      -        })
      -
      -        try:
      -            self.c.request("POST", "/", payload, headers=self.headers)
      -            response = self.c.getresponse()
      -            data = response.read()
      -            self.c.close()
      -            domain_object = json.loads(data.decode())["result"]
      -        except Exception as err:
      -            #domain doesn't exist
      -            return None
      -
      -        if "zeronet" in domain_object["value"]:
      -            zeronet_domains = json.loads(domain_object["value"])["zeronet"]
      -
      -            if isinstance(zeronet_domains, str):
      -                # {
      -                #    "zeronet":"19rXKeKptSdQ9qt7omwN82smehzTuuq6S9"
      -                # } is valid
      -                zeronet_domains = {"": zeronet_domains}
      -
      -            self.cache[domain] = {"addresses_resolved": zeronet_domains, "time": time.time()}
      -
      -        elif "map" in domain_object["value"]:
      -            # Namecoin standard use {"map": { "blog": {"zeronet": "1D..."} }}
      -            data_map = json.loads(domain_object["value"])["map"]
      -
      -            zeronet_domains = dict()
      -            for subdomain in data_map:
      -                if "zeronet" in data_map[subdomain]:
      -                    zeronet_domains[subdomain] = data_map[subdomain]["zeronet"]
      -            if "zeronet" in data_map and isinstance(data_map["zeronet"], str):
      -                # {"map":{
      -                #    "zeronet":"19rXKeKptSdQ9qt7omwN82smehzTuuq6S9",
      -                # }}
      -                zeronet_domains[""] = data_map["zeronet"]
      -
      -            self.cache[domain] = {"addresses_resolved": zeronet_domains, "time": time.time()}
      -
      -        else:
      -            # No Zeronet address registered
      -            return None
      -
      -        return self.cache[domain]["addresses_resolved"][subdomain]
      -
      -@PluginManager.registerTo("ConfigPlugin")
      -class ConfigPlugin(object):
      -    def createArguments(self):
      -        group = self.parser.add_argument_group("Zeroname Local plugin")
      -        group.add_argument('--namecoin_host', help="Host to namecoin node (eg. 127.0.0.1)")
      -        group.add_argument('--namecoin_rpcport', help="Port to connect (eg. 8336)")
      -        group.add_argument('--namecoin_rpcuser', help="RPC user to connect to the namecoin node (eg. nofish)")
      -        group.add_argument('--namecoin_rpcpassword', help="RPC password to connect to namecoin node")
      -
      -        return super(ConfigPlugin, self).createArguments()
      diff --git a/plugins/disabled-ZeronameLocal/UiRequestPlugin.py b/plugins/disabled-ZeronameLocal/UiRequestPlugin.py
      deleted file mode 100644
      index 0ccfb530..00000000
      --- a/plugins/disabled-ZeronameLocal/UiRequestPlugin.py
      +++ /dev/null
      @@ -1,39 +0,0 @@
      -import re
      -from Plugin import PluginManager
      -
      -@PluginManager.registerTo("UiRequest")
      -class UiRequestPlugin(object):
      -    def __init__(self, *args, **kwargs):
      -        from Site import SiteManager
      -        self.site_manager = SiteManager.site_manager
      -        super(UiRequestPlugin, self).__init__(*args, **kwargs)
      -
      -
      -    # Media request
      -    def actionSiteMedia(self, path):
      -        match = re.match(r"/media/(?P
      [A-Za-z0-9-]+\.[A-Za-z0-9\.-]+)(?P/.*|$)", path) - if match: # Its a valid domain, resolve first - domain = match.group("address") - address = self.site_manager.resolveDomain(domain) - if address: - path = "/media/"+address+match.group("inner_path") - return super(UiRequestPlugin, self).actionSiteMedia(path) # Get the wrapper frame output - - - # Is mediarequest allowed from that referer - def isMediaRequestAllowed(self, site_address, referer): - referer_path = re.sub("http[s]{0,1}://.*?/", "/", referer).replace("/media", "") # Remove site address - referer_path = re.sub(r"\?.*", "", referer_path) # Remove http params - - if self.isProxyRequest(): # Match to site domain - referer = re.sub("^http://zero[/]+", "http://", referer) # Allow /zero access - referer_site_address = re.match("http[s]{0,1}://(.*?)(/|$)", referer).group(1) - else: # Match to request path - referer_site_address = re.match(r"/(?P
      [A-Za-z0-9\.-]+)(?P/.*|$)", referer_path).group("address") - - if referer_site_address == site_address: # Referer site address as simple address - return True - elif self.site_manager.resolveDomain(referer_site_address) == site_address: # Referer site address as dns - return True - else: # Invalid referer - return False diff --git a/plugins/disabled-ZeronameLocal/__init__.py b/plugins/disabled-ZeronameLocal/__init__.py deleted file mode 100644 index cf724069..00000000 --- a/plugins/disabled-ZeronameLocal/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from . import UiRequestPlugin -from . import SiteManagerPlugin \ No newline at end of file diff --git a/src/Config.py b/src/Config.py index 7555f65d..71a725cc 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ import time class Config(object): def __init__(self, argv): - self.version = "0.7.6-internal" + self.version = "0.7.6-internal 2" self.rev = 4560 self.argv = argv self.action = None From 643dd84a287a5f2b6cda5a3a28c77151df27472b Mon Sep 17 00:00:00 2001 From: canewsin Date: Thu, 16 Dec 2021 19:56:24 +0530 Subject: [PATCH 118/191] Fix Tests for Github Actions --- .github/workflows/tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e16a9c65..25ac5d9c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,10 @@ jobs: python-version: [3.6, 3.7, 3.8, 3.9] steps: + - name: Checkout ZeroNet - uses: actions/checkout@v2 + with: + submodules: recursive - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 From 523e399d8c4076fa44596ef554ae0ea0de971442 Mon Sep 17 00:00:00 2001 From: canewsin Date: Thu, 16 Dec 2021 20:00:50 +0530 Subject: [PATCH 119/191] Fix Github Actions --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 25ac5d9c..9436162e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,7 +15,7 @@ jobs: - name: Checkout ZeroNet - uses: actions/checkout@v2 with: - submodules: recursive + submodules: 'true' - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 From 18da16e8d49ef374cadf063f530675b5d43fa89e Mon Sep 17 00:00:00 2001 From: canewsin Date: Thu, 16 Dec 2021 20:03:47 +0530 Subject: [PATCH 120/191] Fix Github Actions --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9436162e..6eaf3c6b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Checkout ZeroNet - - uses: actions/checkout@v2 + uses: actions/checkout@v2 with: submodules: 'true' From 86a73e46654afd95adc33838e277cbbcb25e874e Mon Sep 17 00:00:00 2001 From: Zola Gonano Date: Mon, 20 Dec 2021 14:54:30 +0000 Subject: [PATCH 121/191] UI/UX: Increases Size of Notifications Close Button(#106) --- src/Ui/media/Notifications.coffee | 2 +- src/Ui/media/Wrapper.css | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Ui/media/Notifications.coffee b/src/Ui/media/Notifications.coffee index 8898b645..35d949f3 100644 --- a/src/Ui/media/Notifications.coffee +++ b/src/Ui/media/Notifications.coffee @@ -51,7 +51,7 @@ class Notifications ), timeout # Animate - width = Math.min(elem.outerWidth() + 50, 580) + width = Math.min(elem.outerWidth() + 70, 580) if not timeout then width += 20 # Add space for close button if elem.outerHeight() > 55 then elem.addClass("long") elem.css({"width": "50px", "transform": "scale(0.01)"}) diff --git a/src/Ui/media/Wrapper.css b/src/Ui/media/Wrapper.css index 7c486081..67e35a84 100644 --- a/src/Ui/media/Wrapper.css +++ b/src/Ui/media/Wrapper.css @@ -56,7 +56,7 @@ a { color: black } text-align: center; background-color: #e74c3c; line-height: 45px; vertical-align: bottom; font-size: 40px; color: white; } .notification .body { - padding-left: 14px; padding-right: 60px; height: 50px; vertical-align: middle; display: table; padding-right: 20px; box-sizing: border-box; + border-right: 40px solid transparent; padding-left: 14px; padding-right: 60px; height: 50px; vertical-align: middle; display: table; padding-right: 20px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; background-color: white; left: 50px; top: 0; position: relative; padding-top: 5px; padding-bottom: 5px; } .notification .message-outer { display: table-row } @@ -66,9 +66,12 @@ a { color: black } .notification.visible { max-width: 350px } -.notification .close { position: absolute; top: 0; right: 0; font-size: 19px; line-height: 13px; color: #DDD; padding: 7px; text-decoration: none } -.notification .close:hover { color: black } -.notification .close:active, .notification .close:focus { color: #AF3BFF } +.notification .close:hover { opacity: 0.8 } +.notification .close { + position: absolute; top: 0; right: 0; text-decoration: none; margin: 10px; padding: 0px; display: block; width: 30px; height: 30px; + text-align: center; background-color: tomato; line-height: 30px; vertical-align: bottom; font-size: 30px; color: white; +} + .notification small { color: #AAA } .notification .multiline { white-space: normal; word-break: break-word; max-width: 300px; } .body-white .notification { box-shadow: 0 1px 9px rgba(0,0,0,0.1) } @@ -76,7 +79,7 @@ a { color: black } /* Notification select */ .notification .select { display: block; padding: 10px; margin-right: -32px; text-decoration: none; border-left: 3px solid #EEE; - margin-top: 1px; transition: all 0.3s; color: #666 + margin-top: 10px; transition: all 0.3s; color: #666 } .notification .select:hover, .notification .select.active { background-color: #007AFF; border-left: 3px solid #5D68FF; color: white; transition: none } .notification .select:active, .notification .select:focus { background-color: #3396FF; color: white; transition: none; border-left-color: #3396FF } From 58b2d923517278d652bb1f95e791deeb16045fdf Mon Sep 17 00:00:00 2001 From: Zola Gonano Date: Mon, 20 Dec 2021 14:55:45 +0000 Subject: [PATCH 122/191] Update ZeroNet Repo Urls (#103) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3ec13c21..2b2816a9 100644 --- a/README.md +++ b/README.md @@ -64,18 +64,18 @@ Decentralized websites using Bitcoin crypto and the BitTorrent network - https:/ ### Windows - - Download [ZeroNet-py3-win64.zip](https://github.com/HelloZeroNet/ZeroNet-win/archive/dist-win64/ZeroNet-py3-win64.zip) (18MB) + - Download [ZeroNet-py3-win64.zip](https://github.com/ZeroNetX/ZeroNet-win/archive/dist-win64/ZeroNet-py3-win64.zip) (18MB) - Unpack anywhere - Run `ZeroNet.exe` ### macOS - - Download [ZeroNet-dist-mac.zip](https://github.com/HelloZeroNet/ZeroNet-dist/archive/mac/ZeroNet-dist-mac.zip) (13.2MB) + - Download [ZeroNet-dist-mac.zip](https://github.com/ZeroNetX/ZeroNet-dist/archive/mac/ZeroNet-dist-mac.zip) (13.2MB) - Unpack anywhere - Run `ZeroNet.app` ### Linux (x86-64bit) - - `wget https://github.com/HelloZeroNet/ZeroNet-linux/archive/dist-linux64/ZeroNet-py3-linux64.tar.gz` + - `wget https://github.com/ZeroNetX/ZeroNet-linux/archive/dist-linux64/ZeroNet-py3-linux64.tar.gz` - `tar xvpfz ZeroNet-py3-linux64.tar.gz` - `cd ZeroNet-linux-dist-linux64/` - Start with: `./ZeroNet.sh` From 7575e2d4558ad7d8076f1883179321cace832f31 Mon Sep 17 00:00:00 2001 From: canewsin Date: Wed, 22 Dec 2021 14:05:01 +0530 Subject: [PATCH 123/191] Doubled Site Publish Limits --- README-ru.md | 2 +- README-zh-cn.md | 4 ++-- src/File/FileRequest.py | 2 +- src/Ui/UiServer.py | 1 + src/Ui/UiWebsocket.py | 2 +- src/main.py | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/README-ru.md b/README-ru.md index 75abbfab..d2024600 100644 --- a/README-ru.md +++ b/README-ru.md @@ -34,7 +34,7 @@ * После запуска `zeronet.py` вы сможете посетить зайты (zeronet сайты) используя адрес `http://127.0.0.1:43110/{zeronet_address}` -(например. `http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D`). +(например. `http://127.0.0.1:43110/1HELLoE3sFD9569CLCbHEAVqvqV7U2Ri9d`). * Когда вы посещаете новый сайт zeronet, он пытается найти пиров с помощью BitTorrent чтобы загрузить файлы сайтов (html, css, js ...) из них. * Каждый посещенный зайт также обслуживается вами. (Т.е хранится у вас на компьютере) diff --git a/README-zh-cn.md b/README-zh-cn.md index fabdb0e5..bdcc4e00 100644 --- a/README-zh-cn.md +++ b/README-zh-cn.md @@ -33,7 +33,7 @@ * 在运行 `zeronet.py` 后,您将可以通过 `http://127.0.0.1:43110/{zeronet_address}`(例如: - `http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D`)访问 zeronet 中的站点 + `http://127.0.0.1:43110/1HELLoE3sFD9569CLCbHEAVqvqV7U2Ri9d`)访问 zeronet 中的站点 * 在您浏览 zeronet 站点时,客户端会尝试通过 BitTorrent 网络来寻找可用的节点,从而下载需要的文件(html,css,js...) * 您将会储存每一个浏览过的站点 * 每个站点都包含一个名为 `content.json` 的文件,它储存了其他所有文件的 sha512 散列值以及一个通过站点私钥生成的签名 @@ -99,7 +99,7 @@ ## 如何创建一个 ZeroNet 站点? - * 点击 [ZeroHello](http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D) 站点的 **⋮** > **「新建空站点」** 菜单项 + * 点击 [ZeroHello](http://127.0.0.1:43110/1HELLoE3sFD9569CLCbHEAVqvqV7U2Ri9d) 站点的 **⋮** > **「新建空站点」** 菜单项 * 您将被**重定向**到一个全新的站点,该站点只能由您修改 * 您可以在 **data/[您的站点地址]** 目录中找到并修改网站的内容 * 修改后打开您的网站,将右上角的「0」按钮拖到左侧,然后点击底部的**签名**并**发布**按钮 diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index 65c335a9..85bbcdce 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -165,7 +165,7 @@ class FileRequest(object): peer = site.addPeer(self.connection.ip, self.connection.port, return_peer=True, source="update") # Add or get peer # On complete publish to other peers diffs = params.get("diffs", {}) - site.onComplete.once(lambda: site.publish(inner_path=inner_path, diffs=diffs, limit=3), "publish_%s" % inner_path) + site.onComplete.once(lambda: site.publish(inner_path=inner_path, diffs=diffs, limit=6), "publish_%s" % inner_path) # Load new content file and download changed files in new thread def downloader(): diff --git a/src/Ui/UiServer.py b/src/Ui/UiServer.py index 9d93ccfd..61943ada 100644 --- a/src/Ui/UiServer.py +++ b/src/Ui/UiServer.py @@ -59,6 +59,7 @@ class UiServer: self.ip = "0.0.0.0" # Bind all if config.ui_host: self.allowed_hosts = set(config.ui_host) + #TODO: For proxies allow sub domains(www) as valid hosts, should be user preference. elif config.ui_ip == "127.0.0.1": # IP Addresses are inherently allowed as they are immune to DNS # rebinding attacks. diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 88e395d6..9865a1f1 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -510,7 +510,7 @@ class UiWebsocket(object): progress ]) diffs = site.content_manager.getDiffs(inner_path) - back = site.publish(limit=5, inner_path=inner_path, diffs=diffs, cb_progress=cbProgress) + back = site.publish(limit=10, inner_path=inner_path, diffs=diffs, cb_progress=cbProgress) if back == 0: # Failed to publish to anyone self.cmd("progress", ["publish", _["Content publish failed."], -100]) else: diff --git a/src/main.py b/src/main.py index 8a677193..6ba85052 100644 --- a/src/main.py +++ b/src/main.py @@ -432,7 +432,7 @@ class Actions(object): else: # Just ask the tracker logging.info("Gathering peers from tracker") site.announce() # Gather peers - published = site.publish(5, inner_path) # Push to peers + published = site.publish(10, inner_path) # Push to peers if published > 0: time.sleep(3) logging.info("Serving files (max 60s)...") From 7dbf8da8cbc3b086d96ae2df1a32a17b2651cc6e Mon Sep 17 00:00:00 2001 From: canewsin Date: Wed, 22 Dec 2021 15:09:52 +0530 Subject: [PATCH 124/191] Parallel Shutdown of file_server and ui_server Make Parallel Shutdown of file_server and ui_server at Shutdown of ZeroNet --- src/Debug/DebugHook.py | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/Debug/DebugHook.py b/src/Debug/DebugHook.py index d100a3b8..772d309c 100644 --- a/src/Debug/DebugHook.py +++ b/src/Debug/DebugHook.py @@ -10,19 +10,31 @@ from Config import config from . import Debug last_error = None +thread_shutdown = None + +def shutdownThread(): + import main + try: + if "file_server" in dir(main): + thread = gevent.spawn(main.file_server.stop) + thread.join(timeout=60) + if "ui_server" in dir(main): + thread = gevent.spawn(main.ui_server.stop) + thread.join(timeout=10) + except Exception as err: + print("Error in shutdown thread: %s" % err) + sys.exit(0) + else: + sys.exit(0) def shutdown(reason="Unknown"): + global thread_shutdown logging.info("Shutting down (reason: %s)..." % reason) - import main - if "file_server" in dir(main): - try: - gevent.spawn(main.file_server.stop) - if "ui_server" in dir(main): - gevent.spawn(main.ui_server.stop) - except Exception as err: - print("Proper shutdown error: %s" % err) - sys.exit(0) - else: + try: + if not thread_shutdown: + thread_shutdown = gevent.spawn(shutdownThread) + except Exception as err: + print("Proper shutdown error: %s" % err) sys.exit(0) # Store last error, ignore notify, allow manual error logging From 62f88a11a703521b75fdc9ee4536cf61116347ec Mon Sep 17 00:00:00 2001 From: canewsin Date: Sun, 26 Dec 2021 23:30:36 +0530 Subject: [PATCH 125/191] Add More Plugins to Repo --- plugins | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins b/plugins index 5c22d549..b59467e6 160000 --- a/plugins +++ b/plugins @@ -1 +1 @@ -Subproject commit 5c22d54984363d425d9f7b6aabbf1a871e7e60a1 +Subproject commit b59467e64ecd5176678a7a618781341938d4107f From f2e1c2ad81b7febf69e4b916d6aaa3e177eced90 Mon Sep 17 00:00:00 2001 From: Zola Gonano Date: Sun, 26 Dec 2021 18:54:13 +0000 Subject: [PATCH 126/191] Clean up tor v3 patch (#115) https://github.com/ZeroNetX/ZeroNet/pull/115 --- src/Crypt/{CryptRsa.py => CryptTor.py} | 40 ++++++++++++++----- src/Test/TestTor.py | 12 +++--- src/Tor/TorManager.py | 8 ++-- src/{Crypt/CryptEd25519.py => lib/Ed25519.py} | 0 4 files changed, 41 insertions(+), 19 deletions(-) rename src/Crypt/{CryptRsa.py => CryptTor.py} (70%) rename src/{Crypt/CryptEd25519.py => lib/Ed25519.py} (100%) diff --git a/src/Crypt/CryptRsa.py b/src/Crypt/CryptTor.py similarity index 70% rename from src/Crypt/CryptRsa.py rename to src/Crypt/CryptTor.py index 02df8f41..78ba6fc2 100644 --- a/src/Crypt/CryptRsa.py +++ b/src/Crypt/CryptTor.py @@ -4,14 +4,17 @@ import hashlib def sign(data, privatekey): import rsa from rsa import pkcs1 - from Crypt import CryptEd25519 - ## v3 = 88 + from lib import Ed25519 + + ## Onion Service V3 if len(privatekey) == 88: prv_key = base64.b64decode(privatekey) - pub_key = CryptEd25519.publickey_unsafe(prv_key) - sign = CryptEd25519.signature_unsafe(data, prv_key, pub_key) + pub_key = Ed25519.publickey_unsafe(prv_key) + sign = Ed25519.signature_unsafe(data, prv_key, pub_key) + return sign + ## Onion Service V2 if "BEGIN RSA PRIVATE KEY" not in privatekey: privatekey = "-----BEGIN RSA PRIVATE KEY-----\n%s\n-----END RSA PRIVATE KEY-----" % privatekey @@ -22,44 +25,61 @@ def sign(data, privatekey): def verify(data, publickey, sign): import rsa from rsa import pkcs1 - from Crypt import CryptEd25519 + from lib import Ed25519 + ## Onion Service V3 if len(publickey) == 32: + try: - valid = CryptEd25519.checkvalid(sign, data, publickey) + valid = Ed25519.checkvalid(sign, data, publickey) valid = 'SHA-256' + except Exception as err: print(err) valid = False + return valid + ## Onion Service V2 pub = rsa.PublicKey.load_pkcs1(publickey, format="DER") + try: valid = rsa.pkcs1.verify(data, sign, pub) + except pkcs1.VerificationError: valid = False + return valid def privatekeyToPublickey(privatekey): - from Crypt import CryptEd25519 import rsa from rsa import pkcs1 + from lib import Ed25519 + ## Onion Service V3 if len(privatekey) == 88: prv_key = base64.b64decode(privatekey) - pub_key = CryptEd25519.publickey_unsafe(prv_key) + pub_key = Ed25519.publickey_unsafe(prv_key) + return pub_key + ## Onion Service V2 if "BEGIN RSA PRIVATE KEY" not in privatekey: privatekey = "-----BEGIN RSA PRIVATE KEY-----\n%s\n-----END RSA PRIVATE KEY-----" % privatekey priv = rsa.PrivateKey.load_pkcs1(privatekey) pub = rsa.PublicKey(priv.n, priv.e) + return pub.save_pkcs1("DER") def publickeyToOnion(publickey): - from Crypt import CryptEd25519 + from lib import Ed25519 + + ## Onion Service V3 if len(publickey) == 32: - addr = CryptEd25519.publickey_to_onionaddress(publickey)[:-6] + addr = Ed25519.publickey_to_onionaddress(publickey)[:-6] + return addr + + ## Onion Service V2 return base64.b32encode(hashlib.sha1(publickey).digest()[:10]).lower().decode("ascii") diff --git a/src/Test/TestTor.py b/src/Test/TestTor.py index 0252d73a..e6b82c1a 100644 --- a/src/Test/TestTor.py +++ b/src/Test/TestTor.py @@ -4,7 +4,7 @@ import pytest import mock from File import FileServer -from Crypt import CryptRsa +from Crypt import CryptTor from Config import config @pytest.mark.usefixtures("resetSettings") @@ -34,17 +34,17 @@ class TestTor: address = tor_manager.addOnion() # Sign - sign = CryptRsa.sign(b"hello", tor_manager.getPrivatekey(address)) + sign = CryptTor.sign(b"hello", tor_manager.getPrivatekey(address)) assert len(sign) == 128 # Verify - publickey = CryptRsa.privatekeyToPublickey(tor_manager.getPrivatekey(address)) + publickey = CryptTor.privatekeyToPublickey(tor_manager.getPrivatekey(address)) assert len(publickey) == 140 - assert CryptRsa.verify(b"hello", publickey, sign) - assert not CryptRsa.verify(b"not hello", publickey, sign) + assert CryptTor.verify(b"hello", publickey, sign) + assert not CryptTor.verify(b"not hello", publickey, sign) # Pub to address - assert CryptRsa.publickeyToOnion(publickey) == address + assert CryptTor.publickeyToOnion(publickey) == address # Delete tor_manager.delOnion(address) diff --git a/src/Tor/TorManager.py b/src/Tor/TorManager.py index c0b99759..865d8fbf 100644 --- a/src/Tor/TorManager.py +++ b/src/Tor/TorManager.py @@ -12,8 +12,10 @@ import atexit import gevent from Config import config -from Crypt import CryptEd25519 -from Crypt import CryptRsa + +from lib import Ed25519 +from Crypt import CryptTor + from Site import SiteManager import socks from gevent.lock import RLock @@ -272,7 +274,7 @@ class TorManager(object): return self.privatekeys[address] def getPublickey(self, address): - return CryptRsa.privatekeyToPublickey(self.privatekeys[address]) + return CryptTor.privatekeyToPublickey(self.privatekeys[address]) def getOnion(self, site_address): if not self.enabled: diff --git a/src/Crypt/CryptEd25519.py b/src/lib/Ed25519.py similarity index 100% rename from src/Crypt/CryptEd25519.py rename to src/lib/Ed25519.py From 912688dbbd7c35a8c7a9973c1432ee11c4b4021d Mon Sep 17 00:00:00 2001 From: canewsin Date: Thu, 6 Jan 2022 12:28:53 +0530 Subject: [PATCH 127/191] Update ZeroNet Plugins --- plugins | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins b/plugins index b59467e6..e57b95c7 160000 --- a/plugins +++ b/plugins @@ -1 +1 @@ -Subproject commit b59467e64ecd5176678a7a618781341938d4107f +Subproject commit e57b95c737865c53ffb114f91baa9992b0a993d2 From f2324b8eb469e9b7096e336f80989522a3ded6a9 Mon Sep 17 00:00:00 2001 From: canewsin Date: Thu, 6 Jan 2022 12:32:43 +0530 Subject: [PATCH 128/191] Fetch Latest ZeroNet-plugins Repo --- plugins | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins b/plugins index e57b95c7..7bb1715c 160000 --- a/plugins +++ b/plugins @@ -1 +1 @@ -Subproject commit e57b95c737865c53ffb114f91baa9992b0a993d2 +Subproject commit 7bb1715ce030352ff46ddd43ad58b7ab90ef978b From 36adb63f6129d1c89a80c801219244a82926d726 Mon Sep 17 00:00:00 2001 From: canewsin Date: Thu, 6 Jan 2022 15:07:17 +0530 Subject: [PATCH 129/191] Fix Tests --- plugins | 2 +- src/Content/ContentDb.py | 4 ++-- src/File/FileRequest.py | 2 +- src/Site/Site.py | 2 +- src/Site/SiteManager.py | 10 +++++----- src/Test/TestFileRequest.py | 6 +++--- src/Test/TestPeer.py | 10 +++++----- src/Test/TestSiteDownload.py | 22 +++++++++++----------- src/Test/TestTor.py | 8 ++++---- src/Test/conftest.py | 2 +- src/Ui/UiRequest.py | 12 ++++++------ src/Ui/UiWebsocket.py | 24 ++++++++++++------------ src/User/User.py | 12 ++++++------ 13 files changed, 58 insertions(+), 58 deletions(-) diff --git a/plugins b/plugins index 7bb1715c..9cadd0c6 160000 --- a/plugins +++ b/plugins @@ -1 +1 @@ -Subproject commit 7bb1715ce030352ff46ddd43ad58b7ab90ef978b +Subproject commit 9cadd0c69f5af06739cc34cc79dc650053a9b4bd diff --git a/src/Content/ContentDb.py b/src/Content/ContentDb.py index f284581e..f0401652 100644 --- a/src/Content/ContentDb.py +++ b/src/Content/ContentDb.py @@ -82,7 +82,7 @@ class ContentDb(Db): return schema def initSite(self, site): - self.sites[site.address] = site + self.getSites()[site.address] = site def needSite(self, site): if site.address not in self.site_ids: @@ -97,7 +97,7 @@ class ContentDb(Db): if site_id: self.execute("DELETE FROM site WHERE site_id = :site_id", {"site_id": site_id}) del self.site_ids[site.address] - del self.sites[site.address] + del self.getSites()[site.address] def setContent(self, site, inner_path, content, size=0): self.insertOrUpdate("content", { diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index 8a16e591..1d8a1c52 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -33,7 +33,7 @@ class FileRequest(object): self.connection = connection self.req_id = None - self.sites = self.server.sites + self.sites = self.server.getSites() self.log = server.log self.responded = False # Responded to the request diff --git a/src/Site/Site.py b/src/Site/Site.py index 46e19169..ace2a65e 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -290,7 +290,7 @@ class Site(object): if not SiteManager.site_manager.sites: SiteManager.site_manager.sites = {} if not SiteManager.site_manager.sites.get(self.address): - SiteManager.site_manager.sites[self.address] = self + SiteManager.site_manager.getSites()[self.address] = self SiteManager.site_manager.load(False) SiteManager.site_manager.saveDelayed() diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index 8175a1f5..da199ee8 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -73,7 +73,7 @@ class SiteManager(object): except Exception as err: self.log.debug("Error loading site %s: %s" % (address, err)) continue - self.sites[address] = site + self.getSites()[address] = site self.log.debug("Loaded site %s in %.3fs" % (address, time.time() - s)) added += 1 elif startup: @@ -88,7 +88,7 @@ class SiteManager(object): if cleanup: for address in list(self.sites.keys()): if address not in address_found: - del(self.sites[address]) + del(self.getSites()[address]) self.log.debug("Removed site: %s" % address) # Remove orpan sites from contentdb @@ -106,7 +106,7 @@ class SiteManager(object): if address in content_db.site_ids: del content_db.site_ids[address] if address in content_db.sites: - del content_db.sites[address] + del content_db.getSites()[address] self.loaded = True for address, settings in sites_need: @@ -203,7 +203,7 @@ class SiteManager(object): self.log.debug("Added new site: %s" % address) config.loadTrackersFile() site = Site(address, settings=settings) - self.sites[address] = site + self.getSites()[address] = site if not site.settings["serving"]: # Maybe it was deleted before site.settings["serving"] = True site.saveSettings() @@ -226,7 +226,7 @@ class SiteManager(object): def delete(self, address): self.sites_changed = int(time.time()) self.log.debug("Deleted site: %s" % address) - del(self.sites[address]) + del(self.getSites()[address]) # Delete from sites.json self.save() diff --git a/src/Test/TestFileRequest.py b/src/Test/TestFileRequest.py index 3fabc271..51dad600 100644 --- a/src/Test/TestFileRequest.py +++ b/src/Test/TestFileRequest.py @@ -16,7 +16,7 @@ class TestFileRequest: client = ConnectionServer(file_server.ip, 1545) connection = client.getConnection(file_server.ip, 1544) - file_server.sites[site.address] = site + file_server.getSites()[site.address] = site # Normal request response = connection.request("getFile", {"site": site.address, "inner_path": "content.json", "location": 0}) @@ -61,7 +61,7 @@ class TestFileRequest: file_server.ip_incoming = {} # Reset flood protection client = ConnectionServer(file_server.ip, 1545) connection = client.getConnection(file_server.ip, 1544) - file_server.sites[site.address] = site + file_server.getSites()[site.address] = site buff = io.BytesIO() response = connection.request("streamFile", {"site": site.address, "inner_path": "content.json", "location": 0}, buff) @@ -89,7 +89,7 @@ class TestFileRequest: client.stop() def testPex(self, file_server, site, site_temp): - file_server.sites[site.address] = site + file_server.getSites()[site.address] = site client = FileServer(file_server.ip, 1545) client.sites = {site_temp.address: site_temp} site_temp.connection_server = client diff --git a/src/Test/TestPeer.py b/src/Test/TestPeer.py index f57e046e..2d587ef4 100644 --- a/src/Test/TestPeer.py +++ b/src/Test/TestPeer.py @@ -13,7 +13,7 @@ from . import Spy @pytest.mark.usefixtures("resetTempSettings") class TestPeer: def testPing(self, file_server, site, site_temp): - file_server.sites[site.address] = site + file_server.getSites()[site.address] = site client = FileServer(file_server.ip, 1545) client.sites = {site_temp.address: site_temp} site_temp.connection_server = client @@ -32,7 +32,7 @@ class TestPeer: client.stop() def testDownloadFile(self, file_server, site, site_temp): - file_server.sites[site.address] = site + file_server.getSites()[site.address] = site client = FileServer(file_server.ip, 1545) client.sites = {site_temp.address: site_temp} site_temp.connection_server = client @@ -77,11 +77,11 @@ class TestPeer: def testHashfieldExchange(self, file_server, site, site_temp): server1 = file_server - server1.sites[site.address] = site + server1.getSites()[site.address] = site site.connection_server = server1 server2 = FileServer(file_server.ip, 1545) - server2.sites[site_temp.address] = site_temp + server2.getSites()[site_temp.address] = site_temp site_temp.connection_server = server2 site.storage.verifyFiles(quick_check=True) # Find what optional files we have @@ -127,7 +127,7 @@ class TestPeer: server2.stop() def testFindHash(self, file_server, site, site_temp): - file_server.sites[site.address] = site + file_server.getSites()[site.address] = site client = FileServer(file_server.ip, 1545) client.sites = {site_temp.address: site_temp} site_temp.connection_server = client diff --git a/src/Test/TestSiteDownload.py b/src/Test/TestSiteDownload.py index cd0a4c9f..37fe796b 100644 --- a/src/Test/TestSiteDownload.py +++ b/src/Test/TestSiteDownload.py @@ -23,7 +23,7 @@ class TestSiteDownload: # Init source server site.connection_server = file_server - file_server.sites[site.address] = site + file_server.getSites()[site.address] = site # Init client server client = FileServer(file_server.ip, 1545) @@ -74,7 +74,7 @@ class TestSiteDownload: # Init source server site.connection_server = file_server - file_server.sites[site.address] = site + file_server.getSites()[site.address] = site # Init client server client = FileServer(file_server.ip, 1545) @@ -130,7 +130,7 @@ class TestSiteDownload: def testArchivedDownload(self, file_server, site, site_temp): # Init source server site.connection_server = file_server - file_server.sites[site.address] = site + file_server.getSites()[site.address] = site # Init client server client = FileServer(file_server.ip, 1545) @@ -178,7 +178,7 @@ class TestSiteDownload: def testArchivedBeforeDownload(self, file_server, site, site_temp): # Init source server site.connection_server = file_server - file_server.sites[site.address] = site + file_server.getSites()[site.address] = site # Init client server client = FileServer(file_server.ip, 1545) @@ -229,7 +229,7 @@ class TestSiteDownload: def testOptionalDownload(self, file_server, site, site_temp): # Init source server site.connection_server = file_server - file_server.sites[site.address] = site + file_server.getSites()[site.address] = site # Init client server client = ConnectionServer(file_server.ip, 1545) @@ -271,7 +271,7 @@ class TestSiteDownload: def testFindOptional(self, file_server, site, site_temp): # Init source server site.connection_server = file_server - file_server.sites[site.address] = site + file_server.getSites()[site.address] = site # Init full source server (has optional files) site_full = Site("1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT") @@ -284,7 +284,7 @@ class TestSiteDownload: gevent.spawn(listen) time.sleep(0.001) # Port opening - file_server_full.sites[site_full.address] = site_full # Add site + file_server_full.getSites()[site_full.address] = site_full # Add site site_full.storage.verifyFiles(quick_check=True) # Check optional files site_full_peer = site.addPeer(file_server.ip, 1546) # Add it to source server hashfield = site_full_peer.updateHashfield() # Update hashfield @@ -342,7 +342,7 @@ class TestSiteDownload: # Init source server site.connection_server = file_server - file_server.sites[site.address] = site + file_server.getSites()[site.address] = site # Init client server client = FileServer(file_server.ip, 1545) @@ -423,7 +423,7 @@ class TestSiteDownload: def testBigUpdate(self, file_server, site, site_temp): # Init source server site.connection_server = file_server - file_server.sites[site.address] = site + file_server.getSites()[site.address] = site # Init client server client = FileServer(file_server.ip, 1545) @@ -476,7 +476,7 @@ class TestSiteDownload: def testHugeContentSiteUpdate(self, file_server, site, site_temp): # Init source server site.connection_server = file_server - file_server.sites[site.address] = site + file_server.getSites()[site.address] = site # Init client server client = FileServer(file_server.ip, 1545) @@ -524,7 +524,7 @@ class TestSiteDownload: # Init source server site.connection_server = file_server - file_server.sites[site.address] = site + file_server.getSites()[site.address] = site # Init client server client = FileServer(file_server.ip, 1545) diff --git a/src/Test/TestTor.py b/src/Test/TestTor.py index e6b82c1a..120cd53e 100644 --- a/src/Test/TestTor.py +++ b/src/Test/TestTor.py @@ -75,7 +75,7 @@ class TestTor: assert file_server.getConnection(address + ".onion", 1544, site=site) != file_server.getConnection(address + ".onion", 1544, site=site_temp) # Only allow to query from the locked site - file_server.sites[site.address] = site + file_server.getSites()[site.address] = site connection_locked = file_server.getConnection(address + ".onion", 1544, site=site) assert "body" in connection_locked.request("getFile", {"site": site.address, "inner_path": "content.json", "location": 0}) assert connection_locked.request("getFile", {"site": "1OTHERSITE", "inner_path": "content.json", "location": 0})["error"] == "Invalid site" @@ -83,11 +83,11 @@ class TestTor: def testPex(self, file_server, site, site_temp): # Register site to currently running fileserver site.connection_server = file_server - file_server.sites[site.address] = site + file_server.getSites()[site.address] = site # Create a new file server to emulate new peer connecting to our peer file_server_temp = FileServer(file_server.ip, 1545) site_temp.connection_server = file_server_temp - file_server_temp.sites[site_temp.address] = site_temp + file_server_temp.getSites()[site_temp.address] = site_temp # We will request peers from this peer_source = site_temp.addPeer(file_server.ip, 1544) @@ -113,7 +113,7 @@ class TestTor: def testFindHash(self, tor_manager, file_server, site, site_temp): file_server.ip_incoming = {} # Reset flood protection - file_server.sites[site.address] = site + file_server.getSites()[site.address] = site file_server.tor_manager = tor_manager client = FileServer(file_server.ip, 1545) diff --git a/src/Test/conftest.py b/src/Test/conftest.py index c8739086..1a7c68e7 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -219,7 +219,7 @@ def site(request): site = Site("1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT") # Create new Site object to load content.json files if not SiteManager.site_manager.sites: SiteManager.site_manager.sites = {} - SiteManager.site_manager.sites["1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT"] = site + SiteManager.site_manager.getSites()["1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT"] = site site.settings["serving"] = True return site diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index dbd3ca67..bd742e1e 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -643,7 +643,7 @@ class UiRequest(object): if (config.debug or config.merge_media) and file_path.split("/")[-1].startswith("all."): # If debugging merge *.css to all.css and *.js to all.js - site = self.server.sites.get(address) + site = self.server.getSites().get(address) if site and site.settings["own"]: from Debug import DebugMedia DebugMedia.merge(file_path) @@ -734,7 +734,7 @@ class UiRequest(object): block = block.replace(b"{themeclass}", themeclass.encode("utf8")) if path_parts: - site = self.server.sites.get(path_parts.get("address")) + site = self.server.getSites().get(path_parts.get("address")) if site.settings["own"]: modified = int(time.time()) else: @@ -823,7 +823,7 @@ class UiRequest(object): # Find site by wrapper_key wrapper_key = self.get["wrapper_key"] site = None - for site_check in list(self.server.sites.values()): + for site_check in list(self.server.getSites().values()): if site_check.settings["wrapper_key"] == wrapper_key: site = site_check @@ -841,7 +841,7 @@ class UiRequest(object): self.server.websockets.append(ui_websocket) ui_websocket.start() self.server.websockets.remove(ui_websocket) - for site_check in list(self.server.sites.values()): + for site_check in list(self.server.getSites().values()): # Remove websocket from every site (admin sites allowed to join other sites event channels) if ui_websocket in site_check.websockets: site_check.websockets.remove(ui_websocket) @@ -867,11 +867,11 @@ class UiRequest(object): # Just raise an error to get console def actionConsole(self): import sys - sites = self.server.sites + sites = self.server.getSites() main = sys.modules["main"] def bench(code, times=100, init=None): - sites = self.server.sites + sites = self.server.getSites() main = sys.modules["main"] s = time.time() if init: diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 60d746db..420f6e43 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -330,7 +330,7 @@ class UiWebsocket(object): if not self.hasSitePermission(address, cmd=cmd): return self.response(to, "No permission for site %s" % address) req_self = copy.copy(self) - req_self.site = self.server.sites.get(address) + req_self.site = self.server.getSites().get(address) req_self.hasCmdPermission = self.hasCmdPermission # Use the same permissions as current site req_obj = super(UiWebsocket, req_self) req = {"id": to, "cmd": cmd, "params": params} @@ -388,7 +388,7 @@ class UiWebsocket(object): def actionAnnouncerStats(self, to): back = {} trackers = self.site.announcer.getTrackers() - for site in list(self.server.sites.values()): + for site in list(self.server.getSites().values()): for tracker, stats in site.announcer.stats.items(): if tracker not in trackers: continue @@ -894,7 +894,7 @@ class UiWebsocket(object): @flag.admin def actionSiteList(self, to, connecting_sites=False): ret = [] - for site in list(self.server.sites.values()): + for site in list(self.server.getSites().values()): if not site.content_manager.contents.get("content.json") and not connecting_sites: continue # Incomplete site ret.append(self.formatSiteInfo(site, create_user=False)) # Dont generate the auth_address on listing @@ -906,7 +906,7 @@ class UiWebsocket(object): if channel not in self.channels: # Add channel to channels self.channels.append(channel) - for site in list(self.server.sites.values()): # Add websocket to every channel + for site in list(self.server.getSites().values()): # Add websocket to every channel if self not in site.websockets: site.websockets.append(self) @@ -918,7 +918,7 @@ class UiWebsocket(object): site.update(announce=announce, check_files=check_files, verify_files=verify_files, since=since) self.response(to, "Updated") - site = self.server.sites.get(address) + site = self.server.getSites().get(address) if site and (site.address == self.site.address or "ADMIN" in self.site.settings["permissions"]): if not site.settings["serving"]: site.settings["serving"] = True @@ -931,7 +931,7 @@ class UiWebsocket(object): # Pause site serving @flag.admin def actionSitePause(self, to, address): - site = self.server.sites.get(address) + site = self.server.getSites().get(address) if site: site.settings["serving"] = False site.saveSettings() @@ -944,7 +944,7 @@ class UiWebsocket(object): # Resume site serving @flag.admin def actionSiteResume(self, to, address): - site = self.server.sites.get(address) + site = self.server.getSites().get(address) if site: site.settings["serving"] = True site.saveSettings() @@ -958,7 +958,7 @@ class UiWebsocket(object): @flag.admin @flag.no_multiuser def actionSiteDelete(self, to, address): - site = self.server.sites.get(address) + site = self.server.getSites().get(address) if site: site.delete() self.user.deleteSiteData(address) @@ -970,10 +970,10 @@ class UiWebsocket(object): def cbSiteClone(self, to, address, root_inner_path="", target_address=None, redirect=True): self.cmd("notification", ["info", _["Cloning site..."]]) - site = self.server.sites.get(address) + site = self.server.getSites().get(address) response = {} if target_address: - target_site = self.server.sites.get(target_address) + target_site = self.server.getSites().get(target_address) privatekey = self.user.getSiteData(target_site.address).get("privatekey") site.clone(target_address, privatekey, root_inner_path=root_inner_path) self.cmd("notification", ["done", _["Site source code upgraded!"]]) @@ -999,11 +999,11 @@ class UiWebsocket(object): self.response(to, {"error": "Not a site: %s" % address}) return - if not self.server.sites.get(address): + if not self.server.getSites().get(address): # Don't expose site existence return - site = self.server.sites.get(address) + site = self.server.getSites().get(address) if site.bad_files: for bad_inner_path in list(site.bad_files.keys()): is_user_file = "cert_signers" in site.content_manager.getRules(bad_inner_path) diff --git a/src/User/User.py b/src/User/User.py index dbcfc56f..96695748 100644 --- a/src/User/User.py +++ b/src/User/User.py @@ -61,13 +61,13 @@ class User(object): s = time.time() address_id = self.getAddressAuthIndex(address) # Convert site address to int auth_privatekey = CryptBitcoin.hdPrivatekey(self.master_seed, address_id) - self.sites[address] = { + self.getSites()[address] = { "auth_address": CryptBitcoin.privatekeyToAddress(auth_privatekey), "auth_privatekey": auth_privatekey } self.saveDelayed() self.log.debug("Added new site: %s in %.3fs" % (address, time.time() - s)) - return self.sites[address] + return self.getSites()[address] # Get user site data # Return: {"auth_address": "xxx", "auth_privatekey": "xxx"} @@ -76,11 +76,11 @@ class User(object): if not create: return {"auth_address": None, "auth_privatekey": None} # Dont create user yet self.generateAuthAddress(address) - return self.sites[address] + return self.getSites()[address] def deleteSiteData(self, address): if address in self.sites: - del(self.sites[address]) + del(self.getSites()[address]) self.saveDelayed() self.log.debug("Deleted site: %s" % address) @@ -101,9 +101,9 @@ class User(object): raise Exception("Random error: site exist!") # Save to sites self.getSiteData(site_address) - self.sites[site_address]["privatekey"] = site_privatekey + self.getSites()[site_address]["privatekey"] = site_privatekey self.save() - return site_address, bip32_index, self.sites[site_address] + return site_address, bip32_index, self.getSites()[site_address] # Get BIP32 address from site address # Return: BIP32 auth address From c594e635125bbc0eccc09802a4af457489861c30 Mon Sep 17 00:00:00 2001 From: canewsin Date: Fri, 7 Jan 2022 02:29:04 +0530 Subject: [PATCH 130/191] Bug Fixes --- plugins | 2 +- src/Content/ContentDb.py | 4 +-- src/Site/Site.py | 2 +- src/Site/SiteManager.py | 10 +++--- src/Test/conftest.py | 2 +- src/Test/testdata/chart.db-shm | Bin 0 -> 32768 bytes src/Test/testdata/chart.db-wal | Bin 0 -> 78312 bytes src/Test/testdata/content.db-shm | Bin 0 -> 32768 bytes src/Test/testdata/content.db-wal | Bin 0 -> 98912 bytes src/Test/testdata/filters.json | 1 + src/Test/testdata/openssl.cnf | 58 +++++++++++++++++++++++++++++++ src/Test/testdata/sites.json | 1 + src/Test/testdata/trackers.json | 1 + src/Test/testdata/users.json | 9 +++++ src/User/User.py | 12 +++---- 15 files changed, 86 insertions(+), 16 deletions(-) create mode 100644 src/Test/testdata/chart.db-shm create mode 100644 src/Test/testdata/chart.db-wal create mode 100644 src/Test/testdata/content.db-shm create mode 100644 src/Test/testdata/content.db-wal create mode 100644 src/Test/testdata/filters.json create mode 100644 src/Test/testdata/openssl.cnf create mode 100644 src/Test/testdata/sites.json create mode 100644 src/Test/testdata/trackers.json create mode 100644 src/Test/testdata/users.json diff --git a/plugins b/plugins index 9cadd0c6..a55d1b02 160000 --- a/plugins +++ b/plugins @@ -1 +1 @@ -Subproject commit 9cadd0c69f5af06739cc34cc79dc650053a9b4bd +Subproject commit a55d1b02488de996c32e515d3ecaf701dd515d46 diff --git a/src/Content/ContentDb.py b/src/Content/ContentDb.py index f0401652..f284581e 100644 --- a/src/Content/ContentDb.py +++ b/src/Content/ContentDb.py @@ -82,7 +82,7 @@ class ContentDb(Db): return schema def initSite(self, site): - self.getSites()[site.address] = site + self.sites[site.address] = site def needSite(self, site): if site.address not in self.site_ids: @@ -97,7 +97,7 @@ class ContentDb(Db): if site_id: self.execute("DELETE FROM site WHERE site_id = :site_id", {"site_id": site_id}) del self.site_ids[site.address] - del self.getSites()[site.address] + del self.sites[site.address] def setContent(self, site, inner_path, content, size=0): self.insertOrUpdate("content", { diff --git a/src/Site/Site.py b/src/Site/Site.py index ace2a65e..46e19169 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -290,7 +290,7 @@ class Site(object): if not SiteManager.site_manager.sites: SiteManager.site_manager.sites = {} if not SiteManager.site_manager.sites.get(self.address): - SiteManager.site_manager.getSites()[self.address] = self + SiteManager.site_manager.sites[self.address] = self SiteManager.site_manager.load(False) SiteManager.site_manager.saveDelayed() diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index da199ee8..8175a1f5 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -73,7 +73,7 @@ class SiteManager(object): except Exception as err: self.log.debug("Error loading site %s: %s" % (address, err)) continue - self.getSites()[address] = site + self.sites[address] = site self.log.debug("Loaded site %s in %.3fs" % (address, time.time() - s)) added += 1 elif startup: @@ -88,7 +88,7 @@ class SiteManager(object): if cleanup: for address in list(self.sites.keys()): if address not in address_found: - del(self.getSites()[address]) + del(self.sites[address]) self.log.debug("Removed site: %s" % address) # Remove orpan sites from contentdb @@ -106,7 +106,7 @@ class SiteManager(object): if address in content_db.site_ids: del content_db.site_ids[address] if address in content_db.sites: - del content_db.getSites()[address] + del content_db.sites[address] self.loaded = True for address, settings in sites_need: @@ -203,7 +203,7 @@ class SiteManager(object): self.log.debug("Added new site: %s" % address) config.loadTrackersFile() site = Site(address, settings=settings) - self.getSites()[address] = site + self.sites[address] = site if not site.settings["serving"]: # Maybe it was deleted before site.settings["serving"] = True site.saveSettings() @@ -226,7 +226,7 @@ class SiteManager(object): def delete(self, address): self.sites_changed = int(time.time()) self.log.debug("Deleted site: %s" % address) - del(self.getSites()[address]) + del(self.sites[address]) # Delete from sites.json self.save() diff --git a/src/Test/conftest.py b/src/Test/conftest.py index 1a7c68e7..c8739086 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -219,7 +219,7 @@ def site(request): site = Site("1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT") # Create new Site object to load content.json files if not SiteManager.site_manager.sites: SiteManager.site_manager.sites = {} - SiteManager.site_manager.getSites()["1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT"] = site + SiteManager.site_manager.sites["1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT"] = site site.settings["serving"] = True return site diff --git a/src/Test/testdata/chart.db-shm b/src/Test/testdata/chart.db-shm new file mode 100644 index 0000000000000000000000000000000000000000..7c822be1e7e27ced30c9f50e2208298742b46757 GIT binary patch literal 32768 zcmeI*Jx&5)5C-566vcngh67lUPrFXG0@dAWorivQvYNHbQMA|E=q`@e(dqs0`)#>iJiUB;J-%-?KhMk0>BIbWa^UylQe6h| z`n#9y>D?iIyY-{n`)J4Ue*av4&TE`RPou-=S@b-55xtCFMf2W;009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkL{zXg&jGZVYNADUdsIg+NUK+o5az@1{=>3XIB5Zg2u)1O&)vNoSUca{UiHRkmNOzaFoH=)( zW!e7M)~xRAFWTnD;gQYxqIr)v+T+n4GEK3sN6cP%OnH*W`_zVbb=q@n|JkZXYwX?` z6*V5?4$UZv1rq`YAby@7R_f9JAG{r=k^A zY9`cLUw^^u$mFbtt*p5!+tHQI_L?iLUNha3>+Z;igS)IuE+zIhbJn_C%4u62klHjl zJmhL1Dahsb{A=-o5$~5n?+%>ZCcm?nLkV(?2>}EUKmY**5I_I{1Q0*~0R*O|fV}tb zRe1!*!v~-HW{LhWae=9Mu{kRQ5I_I{1Q0*~0R#|0009Ih2~<2s&~|0y;+62RB{zAF z!2R5S%r|gv|D)#!ys^)n=Lp0L69NbzfB*srAbWI^3lUzpHLxK35dfj$7AIsE#9Dq@xi_WTzt`*JEnr!1 zCa#wymj(mzS@n8v>C8)ha;H#5BMSCf3+003cvrvy*}c;Nkw?n8t^!1sIB%N&E0FY> z*2-bx%*mH)GN-Cfsjihro|`nRoOtW9jM*kez{_=XS!SEnp6=<)nTgh(Y*q|O*C$KY zm%uGVo`H*GD*I`e;If1!57cI zKk@xXE9LkCe&boq_}VySd|o1PhKepP#*W`Fa@_(2Yx)_+vr<0R#|0009ILKmY**5I_KdDI*Zky$!kw5BSAt zInMy7xWI-*;ftT1O0SgT3;2yansL}TV|;7uF4ftKmtz`1Q0*~0R#|0009ILKmY**swCiq0ItOa#+Sag?9jr`hznFnn`RL} z009ILKmY**5I_I{1Q4h#fw1h3iVJ-5!pY!@>5VzY7pSd^L~js4009ILKmY**5I_I{ z1Q4i_Kt#5y;sVhXKOH#oj5R{OK$Wy<76AkhKmY**5I_I{1Q0*~f!Y#qQUg?6;I?RT zZTjfI5%L9U>mtz`1Q0*~0R#|0009ILKmY**sw5B-?Z#AG;EUj$^A7&-<^cHuRnn$e X1Q0*~0R#|0009ILKmY**YDnOBiETRB literal 0 HcmV?d00001 diff --git a/src/Test/testdata/content.db-shm b/src/Test/testdata/content.db-shm new file mode 100644 index 0000000000000000000000000000000000000000..dfcb5880ffa6e96abc2f854c2e00c0c65b9eecfe GIT binary patch literal 32768 zcmeI*Jx)Sl5CzZ=6cv9E|DPMMa|<^1Zh!@7=?-)+tigoZ-kw%BVW~4O#8BASnasJ# z3qux*gq#j-$KLz36`QAUcU2MyJuE=y5dX5ds7V z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7csf&VBlh&>azQ9=mR6BzcJRO-!bVgk8oD0c`6fr1x5(e6mb4g&HvrhDR7E`Kv{vavXdK}z&Qc}Wd$zEPHu1ld6KaZAV7cs0Rq1X Fd;>p`DLViF literal 0 HcmV?d00001 diff --git a/src/Test/testdata/content.db-wal b/src/Test/testdata/content.db-wal new file mode 100644 index 0000000000000000000000000000000000000000..a78922375e66cae01ad55a26b077217f6a50a63f GIT binary patch literal 98912 zcmeI*Z)_Xo9l&v)|K|Utse(H~(J^RaQ#T{6SXND&aMFWjq)wYUUAt-+6JHjCW4pG~ zE<+tkk#l`?lW!m$c?{R#o zE5-&kioO=9efK=~{CuD1@uB?u+>?=qg2z2uFKzI6dOh;-Q2XzDPM!Jk@|(Zt`oY{= zC;uRJY4!Va=l*%-Z?ezhT< z?e^-n8@=>;h9-}7ixRK?S&v>3|L{Nn0R#|0009ILKmY**5J2ElDX_;I3~$+@9jv79 z%UR{aIq~aqs%#xzu<|pjH+^GMW;|&c$@u7mX{@Rn$*1S7m^d=J)UJiX8#ii8mfhw9 z*5Z+LZoyK|URNt>uMttNQrV0#o=BQ^nN!A|sqx+Mse{Jd=0PJqotzv`h=X^ViDXRF zHj?InWXx_`9T2;}T*%io5EbO|_xxJEVEKs`Zhi9T*+0tP*_~8^e8mF+1Q0*~0R#|0 z009ILKmY**)~0~`?%${85yWODPhNNA%~RwB)@EmORtO+~00IagfB*srAbXHN>L!ZzD0tg_000IagfB*srAbmjCJ3mO}Pd3q{K+rNkEm@_Buu zFBVh<`PG7aZA8j)4Weu&W~4Kjl2tCpjAEfwi7u7bg~I*)+QZ}ax4}M$d<+<6cmCG2T6O*H{l2u%&q$}A%{_3ig?7Wo{ZCfj=8`YYz%VrA;`K!yK_w|Ot zgM-?V&(_SAQ<=iie6Eld4b5hAR;o~x@zS|<4ny_Z8dp7elCHWnyuS0}rQjEKE? zja8$Z@33mr&bz4f&qo;4~IqTWq0N z?K@RZj6PN5>XXLhMfnz12Fc2?4Mx|h!j)mupy}%2Gh)TBa--&>J9>iQ-GiFP9v{xa zU+INP!QM`}x+690+SKpu3Wc{0XiEXR?o1(HvGSGFd?Aw+ADoP{-R&B@t1HRDM!Sx?K^(AasuDYBXY13}>uf-TX>tN0%1cL*an|b>VU9I!_(0ky&fU zc~Lh!T#K1~lEcz94CRL1L1oqL_AssPs$D#4WeQd8h;Ho&hT{X*>UnjR!_JG&L|ZVt zsb4!>9VqfT?p#jg=dQg`N7x@obM%BLYbv`;63WmSff35SigAB`m+r96Uh^~d$1*x-Qx0tg_000IagfB*srAb0CsS;OWQdQ}^-XHLcv^xhoc`Q{J_KAj7narT5f(_ofh%yzU3Vb(c0nZK_>Tty2q1s}0tg_000IagfB*srtQmnW z&9_lg`GKG~E&Cn-l^1wn`Q^;dUwQWpxxPS9PkZ#I^hJGHe@jp6FRmE^P6Po25I_I{ z1Q0*~0R#|00D)!+g#B&9+SL;Wf`-3s%-P>?+CboXf7^&tVb!Doe_fmPa|V2M`|GC+ zctuk?syDz5sf2)H0;hlb*W+J5z2$5CJ%X0@3BeIS009ILKmY**5I_I{1Q2MJKu88s^95cV zx#yN29txdjzCg41iGu(F2q1s}0tg_000IagfIv$H?7vo_@&d!d&-_w5cWgWJ1zOrC z1V;b?1Q0*~0R#|0009ILK%iLy?J|(c3%vIBjW^5>{^%L<0?pzl4gv@ufB*srAb_{=g12*i=Q|MAb#jS9yUC79V@(Cnx^#2I~v7v`+|*00IagfB*srAbl>dSFVHN0;vj$k0tg_0 z00IagfB*srAkb2QJ{eu*1@5@@mhatk_To9#7iejp5F7yn5I_I{1Q0*~0R#|00D)!+ ttQUdStGqz*#Y3A Date: Fri, 7 Jan 2022 02:56:14 +0530 Subject: [PATCH 131/191] More Bug Fixes --- plugins | 2 +- src/Ui/UiRequest.py | 12 ++++++------ src/Ui/UiWebsocket.py | 24 ++++++++++++------------ 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/plugins b/plugins index a55d1b02..1dbfb590 160000 --- a/plugins +++ b/plugins @@ -1 +1 @@ -Subproject commit a55d1b02488de996c32e515d3ecaf701dd515d46 +Subproject commit 1dbfb590c82937c805bba79f99a5708436cfc76a diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index bd742e1e..dbd3ca67 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -643,7 +643,7 @@ class UiRequest(object): if (config.debug or config.merge_media) and file_path.split("/")[-1].startswith("all."): # If debugging merge *.css to all.css and *.js to all.js - site = self.server.getSites().get(address) + site = self.server.sites.get(address) if site and site.settings["own"]: from Debug import DebugMedia DebugMedia.merge(file_path) @@ -734,7 +734,7 @@ class UiRequest(object): block = block.replace(b"{themeclass}", themeclass.encode("utf8")) if path_parts: - site = self.server.getSites().get(path_parts.get("address")) + site = self.server.sites.get(path_parts.get("address")) if site.settings["own"]: modified = int(time.time()) else: @@ -823,7 +823,7 @@ class UiRequest(object): # Find site by wrapper_key wrapper_key = self.get["wrapper_key"] site = None - for site_check in list(self.server.getSites().values()): + for site_check in list(self.server.sites.values()): if site_check.settings["wrapper_key"] == wrapper_key: site = site_check @@ -841,7 +841,7 @@ class UiRequest(object): self.server.websockets.append(ui_websocket) ui_websocket.start() self.server.websockets.remove(ui_websocket) - for site_check in list(self.server.getSites().values()): + for site_check in list(self.server.sites.values()): # Remove websocket from every site (admin sites allowed to join other sites event channels) if ui_websocket in site_check.websockets: site_check.websockets.remove(ui_websocket) @@ -867,11 +867,11 @@ class UiRequest(object): # Just raise an error to get console def actionConsole(self): import sys - sites = self.server.getSites() + sites = self.server.sites main = sys.modules["main"] def bench(code, times=100, init=None): - sites = self.server.getSites() + sites = self.server.sites main = sys.modules["main"] s = time.time() if init: diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 420f6e43..60d746db 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -330,7 +330,7 @@ class UiWebsocket(object): if not self.hasSitePermission(address, cmd=cmd): return self.response(to, "No permission for site %s" % address) req_self = copy.copy(self) - req_self.site = self.server.getSites().get(address) + req_self.site = self.server.sites.get(address) req_self.hasCmdPermission = self.hasCmdPermission # Use the same permissions as current site req_obj = super(UiWebsocket, req_self) req = {"id": to, "cmd": cmd, "params": params} @@ -388,7 +388,7 @@ class UiWebsocket(object): def actionAnnouncerStats(self, to): back = {} trackers = self.site.announcer.getTrackers() - for site in list(self.server.getSites().values()): + for site in list(self.server.sites.values()): for tracker, stats in site.announcer.stats.items(): if tracker not in trackers: continue @@ -894,7 +894,7 @@ class UiWebsocket(object): @flag.admin def actionSiteList(self, to, connecting_sites=False): ret = [] - for site in list(self.server.getSites().values()): + for site in list(self.server.sites.values()): if not site.content_manager.contents.get("content.json") and not connecting_sites: continue # Incomplete site ret.append(self.formatSiteInfo(site, create_user=False)) # Dont generate the auth_address on listing @@ -906,7 +906,7 @@ class UiWebsocket(object): if channel not in self.channels: # Add channel to channels self.channels.append(channel) - for site in list(self.server.getSites().values()): # Add websocket to every channel + for site in list(self.server.sites.values()): # Add websocket to every channel if self not in site.websockets: site.websockets.append(self) @@ -918,7 +918,7 @@ class UiWebsocket(object): site.update(announce=announce, check_files=check_files, verify_files=verify_files, since=since) self.response(to, "Updated") - site = self.server.getSites().get(address) + site = self.server.sites.get(address) if site and (site.address == self.site.address or "ADMIN" in self.site.settings["permissions"]): if not site.settings["serving"]: site.settings["serving"] = True @@ -931,7 +931,7 @@ class UiWebsocket(object): # Pause site serving @flag.admin def actionSitePause(self, to, address): - site = self.server.getSites().get(address) + site = self.server.sites.get(address) if site: site.settings["serving"] = False site.saveSettings() @@ -944,7 +944,7 @@ class UiWebsocket(object): # Resume site serving @flag.admin def actionSiteResume(self, to, address): - site = self.server.getSites().get(address) + site = self.server.sites.get(address) if site: site.settings["serving"] = True site.saveSettings() @@ -958,7 +958,7 @@ class UiWebsocket(object): @flag.admin @flag.no_multiuser def actionSiteDelete(self, to, address): - site = self.server.getSites().get(address) + site = self.server.sites.get(address) if site: site.delete() self.user.deleteSiteData(address) @@ -970,10 +970,10 @@ class UiWebsocket(object): def cbSiteClone(self, to, address, root_inner_path="", target_address=None, redirect=True): self.cmd("notification", ["info", _["Cloning site..."]]) - site = self.server.getSites().get(address) + site = self.server.sites.get(address) response = {} if target_address: - target_site = self.server.getSites().get(target_address) + target_site = self.server.sites.get(target_address) privatekey = self.user.getSiteData(target_site.address).get("privatekey") site.clone(target_address, privatekey, root_inner_path=root_inner_path) self.cmd("notification", ["done", _["Site source code upgraded!"]]) @@ -999,11 +999,11 @@ class UiWebsocket(object): self.response(to, {"error": "Not a site: %s" % address}) return - if not self.server.getSites().get(address): + if not self.server.sites.get(address): # Don't expose site existence return - site = self.server.getSites().get(address) + site = self.server.sites.get(address) if site.bad_files: for bad_inner_path in list(site.bad_files.keys()): is_user_file = "cert_signers" in site.content_manager.getRules(bad_inner_path) From 00061c45a5284beb40d6a6962063fa9bdfd958a4 Mon Sep 17 00:00:00 2001 From: canewsin Date: Fri, 7 Jan 2022 03:00:48 +0530 Subject: [PATCH 132/191] Bug Fixes --- plugins | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins b/plugins index 1dbfb590..6327d5c2 160000 --- a/plugins +++ b/plugins @@ -1 +1 @@ -Subproject commit 1dbfb590c82937c805bba79f99a5708436cfc76a +Subproject commit 6327d5c28bc3f89b628ded2f222728698f34aa78 From 1362b7e2e6909b6a79514b7c0da74bcc10302e62 Mon Sep 17 00:00:00 2001 From: canewsin Date: Fri, 7 Jan 2022 03:24:35 +0530 Subject: [PATCH 133/191] Bug Fixes --- plugins | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins b/plugins index 6327d5c2..1f93ad3e 160000 --- a/plugins +++ b/plugins @@ -1 +1 @@ -Subproject commit 6327d5c28bc3f89b628ded2f222728698f34aa78 +Subproject commit 1f93ad3e84b3a3ab1847de48d620f6e8f891e733 From 90b6cb9cdf90ce19c59ef90bb2e807b104fead09 Mon Sep 17 00:00:00 2001 From: canewsin Date: Fri, 7 Jan 2022 03:45:50 +0530 Subject: [PATCH 134/191] Fetch Plugins Repo --- plugins | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins b/plugins index 1f93ad3e..61c87e86 160000 --- a/plugins +++ b/plugins @@ -1 +1 @@ -Subproject commit 1f93ad3e84b3a3ab1847de48d620f6e8f891e733 +Subproject commit 61c87e86f96559f39da92d2543cef0699963d477 From 5e61fe8b0698c7afaf0a681609447571d2e3bc5e Mon Sep 17 00:00:00 2001 From: canewsin Date: Sat, 8 Jan 2022 11:48:30 +0530 Subject: [PATCH 135/191] Catch Missing File Error --- src/Site/SiteStorage.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index 97f720dc..d5df8e95 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -388,8 +388,12 @@ class SiteStorage(object): # Load and parse json file @thread_pool_fs_read.wrap def loadJson(self, inner_path): - with self.open(inner_path, "r", encoding="utf8") as file: - return json.load(file) + try : + with self.open(inner_path) as file: + return json.load(file) + except Exception as err: + self.log.error("Json load error: %s" % Debug.formatException(err)) + return None # Write formatted json file def writeJson(self, inner_path, data): From 45b92157cff577a0836e41fc60e1cea92a46f72b Mon Sep 17 00:00:00 2001 From: canewsin Date: Wed, 12 Jan 2022 01:13:19 +0530 Subject: [PATCH 136/191] More Plugin Changes --- plugins | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins b/plugins index 61c87e86..585d1c91 160000 --- a/plugins +++ b/plugins @@ -1 +1 @@ -Subproject commit 61c87e86f96559f39da92d2543cef0699963d477 +Subproject commit 585d1c91bdda4aa92d057b396c38895a3644ccce From ec8203fff7e3a6c8b9c09b7dca446c3ddc96fddc Mon Sep 17 00:00:00 2001 From: canewsin Date: Wed, 12 Jan 2022 02:33:22 +0530 Subject: [PATCH 137/191] Fix ConnServer GetFile exception https://github.com/ZeroNetX/ZeroNet/issues/136 --- src/Connection/Connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index ad1312f2..8916afbe 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -591,7 +591,7 @@ class Connection(object): buff = 64 * 1024 bytes_left = read_bytes bytes_sent = 0 - while True: + while True and self.sock != None: self.last_send_time = time.time() data = file.read(min(bytes_left, buff)) bytes_sent += len(data) From f498aedb96461ce7e87e73bb2aa24f020b606de1 Mon Sep 17 00:00:00 2001 From: canewsin Date: Wed, 2 Mar 2022 20:17:14 +0530 Subject: [PATCH 138/191] v0.7.8 (4580) - Update Plugins with some bug fixes and Improvements --- README.md | 3 +-- plugins | 2 +- src/Config.py | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e45d5cad..7e805e64 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -# ZeroNet [![tests](https://github.com/ZeroNetX/ZeroNet/actions/workflows/tests.yml/badge.svg)](https://github.com/ZeroNetX/ZeroNet/actions/workflows/tests.yml) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/help_zeronet/donate/) [![Docker Pulls](https://img.shields.io/docker/pulls/canewsin/zeronet)](https://hub.docker.com/r/canewsin/zeronet) - +# ZeroNet [![tests](https://github.com/ZeroNetX/ZeroNet/actions/workflows/tests.yml/badge.svg)](https://github.com/ZeroNetX/ZeroNet/actions/workflows/tests.yml) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/help_zeronet/donate/) [![Docker Pulls](https://img.shields.io/docker/pulls/canewsin/zeronet)](https://hub.docker.com/r/canewsin/zeronet) [![Support me on Patreon](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.vercel.app%2Fapi%3Fusername%3Dpramukesh%26type%3Dpatrons&style=flat)](https://www.patreon.com/PramUkesh) Decentralized websites using Bitcoin crypto and the BitTorrent network - https://zeronet.dev / [ZeroNet Site](http://127.0.0.1:43110/1ZeroNetyV5mKY9JF1gsm82TuBXHpfdLX/), Unlike Bitcoin, ZeroNet Doesn't need a blockchain to run, But uses cryptography used by BTC, to ensure data integrity and validation. diff --git a/plugins b/plugins index d3cbe172..08d78fba 160000 --- a/plugins +++ b/plugins @@ -1 +1 @@ -Subproject commit d3cbe172712951f43bb6589e92e9e9eeb86c3172 +Subproject commit 08d78fbacba0c101dbb7433612fb65061721ff7a diff --git a/src/Config.py b/src/Config.py index b067e13f..38716a40 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,8 +13,8 @@ import time class Config(object): def __init__(self, argv): - self.version = "0.7.6" - self.rev = 4565 + self.version = "0.7.8" + self.rev = 4580 self.argv = argv self.action = None self.test_parser = None From 69d7eacfa45d1b0af53dd79fbb6bbbc81cb98e70 Mon Sep 17 00:00:00 2001 From: canewsin Date: Sun, 6 Mar 2022 18:23:17 +0530 Subject: [PATCH 139/191] v 0.7.9-beta (4581) --- plugins | 2 +- src/Config.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins b/plugins index 08d78fba..278b06c0 160000 --- a/plugins +++ b/plugins @@ -1 +1 @@ -Subproject commit 08d78fbacba0c101dbb7433612fb65061721ff7a +Subproject commit 278b06c0f454c73140eaaff35266affb38def51d diff --git a/src/Config.py b/src/Config.py index 38716a40..d009b937 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,8 +13,8 @@ import time class Config(object): def __init__(self, argv): - self.version = "0.7.8" - self.rev = 4580 + self.version = "0.7.9-beta" + self.rev = 4581 self.argv = argv self.action = None self.test_parser = None From f8c9f2da4ff7aa911261efcfc3fa5c734e56c515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20K=C3=BCthe?= Date: Sat, 12 Mar 2022 06:10:33 +0100 Subject: [PATCH 140/191] remove old v2 onion service (#158) --- src/Config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index d009b937..57835779 100644 --- a/src/Config.py +++ b/src/Config.py @@ -82,7 +82,6 @@ class Config(object): from Crypt import CryptHash access_key_default = CryptHash.random(24, "base64") # Used to allow restrited plugins when multiuser plugin is enabled trackers = [ - "zero://boot3rdez4rzn36x.onion:15441", "http://open.acgnxtracker.com:80/announce", # DE "http://tracker.bt4g.com:2095/announce", # Cloudflare "zero://2602:ffc5::c5b2:5360:26312", # US/ATL From eb397cf4c76a55df9eec6c8cff242beb54216153 Mon Sep 17 00:00:00 2001 From: canewsin Date: Sat, 12 Mar 2022 11:14:10 +0530 Subject: [PATCH 141/191] Update Plugins Repo --- plugins | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins b/plugins index 278b06c0..dbbeaee4 160000 --- a/plugins +++ b/plugins @@ -1 +1 @@ -Subproject commit 278b06c0f454c73140eaaff35266affb38def51d +Subproject commit dbbeaee4057a9dc46a992e2adcefe3a55f875772 From 7ce118d645a3dbb07402a97ae43411cff0c77f54 Mon Sep 17 00:00:00 2001 From: canewsin Date: Sat, 12 Mar 2022 17:38:23 +0530 Subject: [PATCH 142/191] Fix Repo Url for Bug Report --- zeronet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zeronet.py b/zeronet.py index dacd2096..457efb19 100755 --- a/zeronet.py +++ b/zeronet.py @@ -66,7 +66,7 @@ def displayErrorMessage(err, error_log_path): res = ctypes.windll.user32.MessageBoxW(0, err_title, "ZeroNet error", MB_YESNOCANCEL | MB_ICONEXCLAIMATION) if res == ID_YES: import webbrowser - report_url = "https://github.com/HelloZeroNet/ZeroNet/issues/new?assignees=&labels=&template=bug-report.md&title=%s" + report_url = "https://github.com/ZeroNetX/ZeroNet/issues/new?assignees=&labels=&template=bug-report.md&title=%s" webbrowser.open(report_url % urllib.parse.quote("Unhandled exception: %s" % err_message)) if res in [ID_YES, ID_NO]: subprocess.Popen(['notepad.exe', error_log_path]) From 02ceb70a4fe49dc0ca523d31a021e04cfc208b2f Mon Sep 17 00:00:00 2001 From: canewsin Date: Sat, 26 Mar 2022 18:39:01 +0530 Subject: [PATCH 143/191] Tracker Supply improvemets - Removed Non Working Trakers. - Dynamically Load Trackers from Dashboard Site. --- src/Config.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/Config.py b/src/Config.py index 57835779..cc0c032a 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,8 +13,8 @@ import time class Config(object): def __init__(self, argv): - self.version = "0.7.9-beta" - self.rev = 4581 + self.version = "0.7.9-beta2" + self.rev = 4582 self.argv = argv self.action = None self.test_parser = None @@ -36,7 +36,7 @@ class Config(object): self.openssl_lib_file = None self.openssl_bin_file = None - self.trackers_file = False + self.trackers_file = ["{data_dir}/1HELLoE3sFD9569CLCbHEAVqvqV7U2Ri9d/trackers.txt"] self.createParser() self.createArguments() @@ -84,16 +84,10 @@ class Config(object): trackers = [ "http://open.acgnxtracker.com:80/announce", # DE "http://tracker.bt4g.com:2095/announce", # Cloudflare - "zero://2602:ffc5::c5b2:5360:26312", # US/ATL "zero://145.239.95.38:15441", "zero://188.116.183.41:26552", - "zero://145.239.95.38:15441", - "zero://211.125.90.79:22234", - "zero://216.189.144.82:26312", "zero://45.77.23.92:15555", - "zero://51.15.54.182:21041", "https://tracker.lilithraws.cf:443/announce", - "udp://code2chicken.nl:6969/announce", "udp://abufinzio.monocul.us:6969/announce", "udp://tracker.0x.tf:6969/announce", "udp://tracker.zerobytes.xyz:1337/announce", @@ -102,12 +96,8 @@ class Config(object): "zero://k5w77dozo3hy5zualyhni6vrh73iwfkaofa64abbilwyhhd3wgenbjqd.onion:15441", "zero://2kcb2fqesyaevc4lntogupa4mkdssth2ypfwczd2ov5a3zo6ytwwbayd.onion:15441", "zero://gugt43coc5tkyrhrc3esf6t6aeycvcqzw7qafxrjpqbwt4ssz5czgzyd.onion:15441", - "zero://hb6ozikfiaafeuqvgseiik4r46szbpjfu66l67wjinnyv6dtopuwhtqd.onion:15445", - "zero://75pmmcbp4vvo2zndmjnrkandvbg6jyptygvvpwsf2zguj7urq7t4jzyd.onion:7777", - "zero://dw4f4sckg2ultdj5qu7vtkf3jsfxsah3mz6pivwfd6nv3quji3vfvhyd.onion:6969", "zero://5vczpwawviukvd7grfhsfxp7a6huz77hlis4fstjkym5kmf4pu7i7myd.onion:15441", "zero://ow7in4ftwsix5klcbdfqvfqjvimqshbm2o75rhtpdnsderrcbx74wbad.onion:15441", - "zero://agufghdtniyfwty3wk55drxxwj2zxgzzo7dbrtje73gmvcpxy4ngs4ad.onion:15441", "zero://qn65si4gtcwdiliq7vzrwu62qrweoxb6tx2cchwslaervj6szuje66qd.onion:26117", ] # Platform specific From 00db9c9f875f1f26904e92a6471300ee88e61d89 Mon Sep 17 00:00:00 2001 From: canewsin Date: Fri, 8 Apr 2022 23:12:10 +0530 Subject: [PATCH 144/191] Rust Version Compatibility for update Protocol msg and diff patch --- src/File/FileRequest.py | 4 ++++ src/util/Diff.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index 85bbcdce..e57a9f6b 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -134,6 +134,10 @@ class FileRequest(object): if should_validate_content: try: + if type(body) is str: + body = body.encode() + # elif type(body) is list: + # content = json.loads(bytes(list).decode()) content = json.loads(body.decode()) except Exception as err: site.log.debug("Update for %s is invalid JSON: %s" % (inner_path, err)) diff --git a/src/util/Diff.py b/src/util/Diff.py index 8281188b..53b82c5a 100644 --- a/src/util/Diff.py +++ b/src/util/Diff.py @@ -42,6 +42,8 @@ def patch(old_f, actions): continue elif action == "+": # Add lines for add_line in param: + if type(add_line) is str: + add_line = add_line.encode() new_f.write(add_line) else: raise "Unknown action: %s" % action From a5190234ab2625b769d6e34df39f7ef6b4e9abbf Mon Sep 17 00:00:00 2001 From: canewsin Date: Fri, 8 Apr 2022 23:26:28 +0530 Subject: [PATCH 145/191] v 0.7.9(4585) - Tracker Supply Improvements. - First Party Tracker Update Service using Dashboard Site. --- src/Config.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/Config.py b/src/Config.py index cc0c032a..d67cba21 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,8 +13,8 @@ import time class Config(object): def __init__(self, argv): - self.version = "0.7.9-beta2" - self.rev = 4582 + self.version = "0.7.9" + self.rev = 4585 self.argv = argv self.action = None self.test_parser = None @@ -84,15 +84,24 @@ class Config(object): trackers = [ "http://open.acgnxtracker.com:80/announce", # DE "http://tracker.bt4g.com:2095/announce", # Cloudflare + "http://vps02.net.orel.ru:80/announce", + "http://tracker.files.fm:6969/announce", + "http://t.publictracker.xyz:6969/announce", + "https://tracker.lilithraws.cf:443/announce", + "https://tracker.babico.name.tr:443/announce", + "https://tr.abiir.top:443/announce", + "udp://abufinzio.monocul.us:6969/announce", + "udp://vibe.sleepyinternetfun.xyz:1738/announce", + "udp://www.torrent.eu.org:451/announce", + "udp://tracker.0x.tf:6969/announce", + "udp://tracker.zerobytes.xyz:1337/announce", + "udp://tracker.opentrackr.org:1337/announce", + "udp://tracker.birkenwald.de:6969/announce", + "udp://tracker.moeking.me:6969/announce", + "udp://ipv6.babico.name.tr:8000/announce", "zero://145.239.95.38:15441", "zero://188.116.183.41:26552", "zero://45.77.23.92:15555", - "https://tracker.lilithraws.cf:443/announce", - "udp://abufinzio.monocul.us:6969/announce", - "udp://tracker.0x.tf:6969/announce", - "udp://tracker.zerobytes.xyz:1337/announce", - "udp://vibe.sleepyinternetfun.xyz:1738/announce", - "udp://www.torrent.eu.org:451/announce", "zero://k5w77dozo3hy5zualyhni6vrh73iwfkaofa64abbilwyhhd3wgenbjqd.onion:15441", "zero://2kcb2fqesyaevc4lntogupa4mkdssth2ypfwczd2ov5a3zo6ytwwbayd.onion:15441", "zero://gugt43coc5tkyrhrc3esf6t6aeycvcqzw7qafxrjpqbwt4ssz5czgzyd.onion:15441", From b29884db78948e90dd0697fa88d2197b9999b4b8 Mon Sep 17 00:00:00 2001 From: canewsin Date: Thu, 19 May 2022 00:10:38 +0530 Subject: [PATCH 146/191] Create codeql-analysis.yml --- .github/workflows/codeql-analysis.yml | 72 +++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..27b5c924 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,72 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ py3-latest ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ py3-latest ] + schedule: + - cron: '32 19 * * 2' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript', 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 From f9d7ccd83cdf7ad3deaee1a203184247d6b84810 Mon Sep 17 00:00:00 2001 From: canewsin Date: Thu, 26 May 2022 11:46:58 +0530 Subject: [PATCH 147/191] Fix Unhandled File Access Errors --- src/Site/SiteStorage.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index c12a80b0..a2eb0b20 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -255,12 +255,16 @@ class SiteStorage(object): if create_dirs: file_inner_dir = os.path.dirname(inner_path) self.ensureDir(file_inner_dir) - return open(file_path, mode, **kwargs) + try : + return open(file_path, mode, **kwargs) + except IOError as err: + self.log.error("File Access error: %s" % Debug.formatException(err)) + return None # Open file object @thread_pool_fs_read.wrap def read(self, inner_path, mode="rb"): - return open(self.getPath(inner_path), mode).read() + return self.open(inner_path, mode).read() @thread_pool_fs_write.wrap def writeThread(self, inner_path, content): @@ -369,8 +373,12 @@ class SiteStorage(object): # Load and parse json file @thread_pool_fs_read.wrap def loadJson(self, inner_path): - with self.open(inner_path, "r", encoding="utf8") as file: - return json.load(file) + try: + with self.open(inner_path, "r", encoding="utf8") as file: + return json.load(file) + except Exception as err: + self.log.error("Json load error: %s" % Debug.formatException(err)) + return None # Write formatted json file def writeJson(self, inner_path, data): From fe048cd08c3acee62e4b7c651f056512b81e63c1 Mon Sep 17 00:00:00 2001 From: canewsin Date: Thu, 26 May 2022 11:47:25 +0530 Subject: [PATCH 148/191] Update Plugins Repo --- plugins | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins b/plugins index dbbeaee4..1a520d1f 160000 --- a/plugins +++ b/plugins @@ -1 +1 @@ -Subproject commit dbbeaee4057a9dc46a992e2adcefe3a55f875772 +Subproject commit 1a520d1fe95a7aef5307e9e7befd5fbcbfd08809 From 2ad80afa10a2162d492dcdbeac9c1b5c0df14299 Mon Sep 17 00:00:00 2001 From: canewsin Date: Thu, 26 May 2022 11:48:15 +0530 Subject: [PATCH 149/191] actionUpdate response Optimisation --- src/File/FileRequest.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index e57a9f6b..afc26cb7 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -109,19 +109,19 @@ class FileRequest(object): return False inner_path = params.get("inner_path", "") - current_content_modified = site.content_manager.contents.get(inner_path, {}).get("modified", 0) - body = params["body"] - if not inner_path.endswith("content.json"): self.response({"error": "Only content.json update allowed"}) self.connection.badAction(5) return + current_content_modified = site.content_manager.contents.get(inner_path, {}).get("modified", 0) should_validate_content = True if "modified" in params and params["modified"] <= current_content_modified: should_validate_content = False valid = None # Same or earlier content as we have - elif not body: # No body sent, we have to download it first + + body = params["body"] + if not body: # No body sent, we have to download it first site.log.debug("Missing body from update for file %s, downloading ..." % inner_path) peer = site.addPeer(self.connection.ip, self.connection.port, return_peer=True, source="update") # Add or get peer try: From ac70f83879ae649460be08119b5decaf4b26a09e Mon Sep 17 00:00:00 2001 From: canewsin Date: Thu, 26 May 2022 11:49:03 +0530 Subject: [PATCH 150/191] v 0.7.9-patch(4586) --- src/Config.py | 4 ++-- src/Ui/UiRequest.py | 27 +++++++++++++++++++++++---- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/Config.py b/src/Config.py index d67cba21..54afa24b 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,8 +13,8 @@ import time class Config(object): def __init__(self, argv): - self.version = "0.7.9" - self.rev = 4585 + self.version = "0.7.9-patch" + self.rev = 4586 self.argv = argv self.action = None self.test_parser = None diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index dbd3ca67..c9026a24 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -544,17 +544,36 @@ class UiRequest(object): if show_loadingscreen: meta_tags += ''; + def xescape(s): + '''combines parts from re.escape & html.escape''' + # https://github.com/python/cpython/blob/3.10/Lib/re.py#L267 + # '&' is handled otherwise + re_chars = {i: '\\' + chr(i) for i in b'()[]{}*+-|^$\\.~# \t\n\r\v\f'} + # https://github.com/python/cpython/blob/3.10/Lib/html/__init__.py#L12 + html_chars = { + '<' : '<', + '>' : '>', + '"' : '"', + "'" : ''', + } + # we can't replace '&' because it makes certain zites work incorrectly + # it should however in no way interfere with re.sub in render + repl = {} + repl.update(re_chars) + repl.update(html_chars) + return s.translate(repl) + return self.render( "src/Ui/template/wrapper.html", server_url=server_url, inner_path=inner_path, - file_url=re.escape(file_url), - file_inner_path=re.escape(file_inner_path), + file_url=xescape(file_url), + file_inner_path=xescape(file_inner_path), address=site.address, - title=html.escape(title), + title=xescape(title), body_style=body_style, meta_tags=meta_tags, - query_string=re.escape(inner_query_string), + query_string=xescape(inner_query_string), wrapper_key=site.settings["wrapper_key"], ajax_key=site.settings["ajax_key"], wrapper_nonce=wrapper_nonce, From b257338b0a848666ac1b33206a4ccaa7ded83349 Mon Sep 17 00:00:00 2001 From: canewsin Date: Thu, 26 May 2022 17:30:59 +0530 Subject: [PATCH 151/191] v 0.8.0(4590) - Major Version Upgrade to reflect RCE reported by geekless. --- src/Config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index 54afa24b..f48a4a8a 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,8 +13,8 @@ import time class Config(object): def __init__(self, argv): - self.version = "0.7.9-patch" - self.rev = 4586 + self.version = "0.8.0" + self.rev = 4590 self.argv = argv self.action = None self.test_parser = None From c3815c56ea49ee444406eee5cdc5f819fadc6490 Mon Sep 17 00:00:00 2001 From: canewsin Date: Fri, 27 May 2022 08:38:20 +0530 Subject: [PATCH 152/191] Revert File Open to catch File Access Errors. https://github.com/ZeroNetX/ZeroNet/issues/174 --- src/Site/SiteStorage.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index a2eb0b20..89edd8c1 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -255,11 +255,7 @@ class SiteStorage(object): if create_dirs: file_inner_dir = os.path.dirname(inner_path) self.ensureDir(file_inner_dir) - try : - return open(file_path, mode, **kwargs) - except IOError as err: - self.log.error("File Access error: %s" % Debug.formatException(err)) - return None + return open(file_path, mode, **kwargs) # Open file object @thread_pool_fs_read.wrap From 5579c6b3cc9bff939c2bf495d2886759f6745f7c Mon Sep 17 00:00:00 2001 From: canewsin Date: Fri, 27 May 2022 08:58:42 +0530 Subject: [PATCH 153/191] rev4591 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index f48a4a8a..1325f1ec 100644 --- a/src/Config.py +++ b/src/Config.py @@ -14,7 +14,7 @@ class Config(object): def __init__(self, argv): self.version = "0.8.0" - self.rev = 4590 + self.rev = 4591 self.argv = argv self.action = None self.test_parser = None From 712ee1863406012acf2a2bbd97a64fc719dd7a8d Mon Sep 17 00:00:00 2001 From: canewsin Date: Thu, 2 Jun 2022 19:15:22 +0530 Subject: [PATCH 154/191] Update FUNDING.yml --- .github/FUNDING.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 8c9f6621..aab991d5 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,10 @@ -custom: https://zerolink.ml/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/help_zeronet/donate/ +github: canewsin +patreon: # Replace with a single Patreon username e.g., user1 +open_collective: # Replace with a single Open Collective username e.g., user1 +ko_fi: canewsin +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: canewsin +issuehunt: # Replace with a single IssueHunt username e.g., user1 +otechie: # Replace with a single Otechie username e.g., user1 +custom: ['https://paypal.me/PramUkesh', 'https://zerolink.ml/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/help_zeronet/donate/'] From 016cfe9e1641db1d1377407c869bf697da9cd9b7 Mon Sep 17 00:00:00 2001 From: canewsin Date: Thu, 9 Jun 2022 22:38:57 +0530 Subject: [PATCH 155/191] Console Log Updates, Specify min supported ZeroNet version for Rust version Protocol Compatibility Reduce noise(error => warning) on file missing in sites. --- plugins | 2 +- src/Connection/ConnectionServer.py | 13 +++++++++---- src/Site/SiteStorage.py | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/plugins b/plugins index 1a520d1f..859ffbf4 160000 --- a/plugins +++ b/plugins @@ -1 +1 @@ -Subproject commit 1a520d1fe95a7aef5307e9e7befd5fbcbfd08809 +Subproject commit 859ffbf43335796e225525ff01257711485088d9 diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index 090d96a6..c9048398 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -32,6 +32,7 @@ class ConnectionServer(object): self.port = port self.last_connection_id = 0 # Connection id incrementer self.last_connection_id_current_version = 0 # Connection id incrementer for current client version + self.last_connection_id_supported_version = 0 # Connection id incrementer for last supported version self.log = logging.getLogger("ConnServer") self.port_opened = {} self.peer_blacklist = SiteManager.peer_blacklist @@ -157,8 +158,10 @@ class ConnectionServer(object): connection = Connection(self, ip, port, sock) self.connections.append(connection) rev = connection.handshake.get("rev", 0) - if rev > 0 and rev == config.rev: - self.last_connection_id_current_version += 1 + if rev >= 4560: + self.last_connection_id_supported_version += 1 + if rev == config.rev: + self.last_connection_id_current_version += 1 if ip not in config.ip_local: self.ips[ip] = connection connection.handleIncomingConnection(sock) @@ -225,8 +228,10 @@ class ConnectionServer(object): raise Exception("Connection event return error") else: rev = connection.handshake.get("rev", 0) - if rev > 0 and rev == config.rev: - self.last_connection_id_current_version += 1 + if rev >= 4560: + self.last_connection_id_supported_version += 1 + if rev == config.rev: + self.last_connection_id_current_version += 1 except Exception as err: connection.close("%s Connect error: %s" % (ip, Debug.formatException(err))) diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index 89edd8c1..47d4c425 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -373,7 +373,7 @@ class SiteStorage(object): with self.open(inner_path, "r", encoding="utf8") as file: return json.load(file) except Exception as err: - self.log.error("Json load error: %s" % Debug.formatException(err)) + self.log.warning("Json load error: %s" % Debug.formatException(err)) return None # Write formatted json file From 3ac677c9a714046ce62409a08f4ed0ecf0814992 Mon Sep 17 00:00:00 2001 From: canewsin Date: Sat, 11 Jun 2022 01:18:02 +0530 Subject: [PATCH 156/191] Don't Fail Silently When Cert is Not Selected When Site doesn't have cert selected but has userdata, signing userdata fails silently without proper error message --- src/Ui/UiWebsocket.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 9865a1f1..bf561f2e 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -419,10 +419,15 @@ class UiWebsocket(object): is_user_content = file_info and ("cert_signers" in file_info or "cert_signers_pattern" in file_info) if is_user_content and privatekey is None: cert = self.user.getCert(self.site.address) - extend["cert_auth_type"] = cert["auth_type"] - extend["cert_user_id"] = self.user.getCertUserId(site.address) - extend["cert_sign"] = cert["cert_sign"] - self.log.debug("Extending content.json with cert %s" % extend["cert_user_id"]) + if not cert: + error = "Site sign failed: No certificate selected for %s, Try Adding/Selecting User Cert via Site Login" % self.site.address + self.log.error(error) + return self.response(to, {"error": error}) + else: + extend["cert_auth_type"] = cert["auth_type"] + extend["cert_user_id"] = self.user.getCertUserId(site.address) + extend["cert_sign"] = cert["cert_sign"] + self.log.debug("Extending content.json with cert %s" % extend["cert_user_id"]) if not self.hasFilePermission(inner_path): self.log.error("SiteSign error: you don't own this site & site owner doesn't allow you to do so.") From 49e68c3a78c3eabeb3c746c7497461b833e52560 Mon Sep 17 00:00:00 2001 From: canewsin Date: Sat, 11 Jun 2022 01:36:01 +0530 Subject: [PATCH 157/191] Include inner_path of failed request for signing in error msg and response --- src/Ui/UiWebsocket.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index bf561f2e..8514aa0c 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -420,7 +420,7 @@ class UiWebsocket(object): if is_user_content and privatekey is None: cert = self.user.getCert(self.site.address) if not cert: - error = "Site sign failed: No certificate selected for %s, Try Adding/Selecting User Cert via Site Login" % self.site.address + error = "Site sign failed: No certificate selected for Site: %s, Hence Signing inner_path: %s Failed, Try Adding/Selecting User Cert via Site Login" % (self.site.address, inner_path) self.log.error(error) return self.response(to, {"error": error}) else: From 0ed0b746a448ea718a4dfab75e9037be9294a435 Mon Sep 17 00:00:00 2001 From: BratishkaErik <25210740+BratishkaErik@users.noreply.github.com> Date: Mon, 13 Jun 2022 23:36:04 +0600 Subject: [PATCH 158/191] Update README-ru.md (#177) @BratishkaErik Thanks for your contribution --- README-ru.md | 243 ++++++++++++++++++--------------------------------- 1 file changed, 84 insertions(+), 159 deletions(-) diff --git a/README-ru.md b/README-ru.md index 1d0bafc1..7d557727 100644 --- a/README-ru.md +++ b/README-ru.md @@ -3,206 +3,131 @@ [简体中文](./README-zh-cn.md) [English](./README.md) -Децентрализованные вебсайты использующие Bitcoin криптографию и BitTorrent сеть - https://zeronet.dev - +Децентрализованные вебсайты, использующие криптографию Bitcoin и протокол BitTorrent — https://zeronet.dev ([Зеркало в ZeroNet](http://127.0.0.1:43110/1ZeroNetyV5mKY9JF1gsm82TuBXHpfdLX/)). В отличии от Bitcoin, ZeroNet'у не требуется блокчейн для работы, однако он использует ту же криптографию, чтобы обеспечить сохранность и проверку данных. ## Зачем? -* Мы верим в открытую, свободную, и не отцензуренную сеть и коммуникацию. -* Нет единой точки отказа: Сайт онлайн пока по крайней мере 1 пир обслуживает его. -* Никаких затрат на хостинг: Сайты обслуживаются посетителями. -* Невозможно отключить: Он нигде, потому что он везде. -* Быстр и работает оффлайн: Вы можете получить доступ к сайту, даже если Интернет недоступен. - +- Мы верим в открытую, свободную, и неподдающуюся цензуре сеть и связь. +- Нет единой точки отказа: Сайт остаётся онлайн, пока его обслуживает хотя бы 1 пир. +- Нет затрат на хостинг: Сайты обслуживаются посетителями. +- Невозможно отключить: Он нигде, потому что он везде. +- Скорость и возможность работать без Интернета: Вы сможете получить доступ к сайту, потому что его копия хранится на вашем компьютере и у ваших пиров. ## Особенности - * Обновляемые в реальном времени сайты - * Поддержка Namecoin .bit доменов - * Лёгок в установке: распаковал & запустил - * Клонирование вебсайтов в один клик - * Password-less [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) - based authorization: Ваша учетная запись защищена той же криптографией, что и ваш Bitcoin-кошелек - * Встроенный SQL-сервер с синхронизацией данных P2P: Позволяет упростить разработку сайта и ускорить загрузку страницы - * Анонимность: Полная поддержка сети Tor с помощью скрытых служб .onion вместо адресов IPv4 - * TLS зашифрованные связи - * Автоматическое открытие uPnP порта - * Плагин для поддержки многопользовательской (openproxy) - * Работает с любыми браузерами и операционными системами +- Обновление сайтов в реальном времени +- Поддержка доменов `.bit` ([Namecoin](https://www.namecoin.org)) +- Легкая установка: просто распакуйте и запустите +- Клонирование сайтов "в один клик" +- Беспарольная [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) + авторизация: Ваша учетная запись защищена той же криптографией, что и ваш Bitcoin-кошелек +- Встроенный SQL-сервер с синхронизацией данных P2P: Позволяет упростить разработку сайта и ускорить загрузку страницы +- Анонимность: Полная поддержка сети Tor, используя скрытые службы `.onion` вместо адресов IPv4 +- Зашифрованное TLS подключение +- Автоматическое открытие UPnP–порта +- Плагин для поддержки нескольких пользователей (openproxy) +- Работа с любыми браузерами и операционными системами + +## Текущие ограничения + +- Файловые транзакции не сжаты +- Нет приватных сайтов ## Как это работает? -* После запуска `zeronet.py` вы сможете посетить зайты (zeronet сайты) используя адрес - `http://127.0.0.1:43110/{zeronet_address}` -(например. `http://127.0.0.1:43110/1HELLoE3sFD9569CLCbHEAVqvqV7U2Ri9d`). -* Когда вы посещаете новый сайт zeronet, он пытается найти пиров с помощью BitTorrent - чтобы загрузить файлы сайтов (html, css, js ...) из них. -* Каждый посещенный зайт также обслуживается вами. (Т.е хранится у вас на компьютере) -* Каждый сайт содержит файл `content.json`, который содержит все остальные файлы в хэше sha512 - и подпись, созданную с использованием частного ключа сайта. -* Если владелец сайта (у которого есть закрытый ключ для адреса сайта) изменяет сайт, то он/она +- После запуска `zeronet.py` вы сможете посещать сайты в ZeroNet, используя адрес + `http://127.0.0.1:43110/{zeronet_адрес}` + (Например: `http://127.0.0.1:43110/1HELLoE3sFD9569CLCbHEAVqvqV7U2Ri9d`). +- Когда вы посещаете новый сайт в ZeroNet, он пытается найти пиров с помощью протокола BitTorrent, + чтобы скачать у них файлы сайта (HTML, CSS, JS и т.д.). +- После посещения сайта вы тоже становитесь его пиром. +- Каждый сайт содержит файл `content.json`, который содержит SHA512 хеши всех остальные файлы + и подпись, созданную с помощью закрытого ключа сайта. +- Если владелец сайта (тот, кто владеет закрытым ключом для адреса сайта) изменяет сайт, он подписывает новый `content.json` и публикует его для пиров. После этого пиры проверяют целостность `content.json` - (используя подпись), они загружают измененные файлы и публикуют новый контент для других пиров. - -#### [Слайд-шоу о криптографии ZeroNet, обновлениях сайтов, многопользовательских сайтах »](https://docs.google.com/presentation/d/1_2qK1IuOKJ51pgBvllZ9Yu7Au2l551t3XBgyTSvilew/pub?start=false&loop=false&delayms=3000) -#### [Часто задаваемые вопросы »](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/faq/) - -#### [Документация разработчика ZeroNet »](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/site_development/getting_started/) + (используя подпись), скачвают изменённые файлы и распространяют новый контент для других пиров. +[Презентация о криптографии ZeroNet, обновлениях сайтов, многопользовательских сайтах »](https://docs.google.com/presentation/d/1_2qK1IuOKJ51pgBvllZ9Yu7Au2l551t3XBgyTSvilew/pub?start=false&loop=false&delayms=3000) +[Часто задаваемые вопросы »](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/faq/) +[Документация разработчика ZeroNet »](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/site_development/getting_started/) ## Скриншоты ![Screenshot](https://i.imgur.com/H60OAHY.png) ![ZeroTalk](https://zeronet.io/docs/img/zerotalk.png) +[Больше скриншотов в документации ZeroNet »](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/using_zeronet/sample_sites/) -#### [Больше скриншотов в ZeroNet документации »](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/using_zeronet/sample_sites/) +## Как присоединиться? +### Windows -## Как вступить +- Скачайте и распакуйте архив [ZeroNet-win.zip](https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-win.zip) (26МБ) +- Запустите `ZeroNet.exe` -* Скачайте ZeroBundle пакет: - * [Microsoft Windows](https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-win.zip) - * [Apple macOS](https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-mac.zip) - * [Linux 64-bit](https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-linux.zip) - * [Linux 32-bit](https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-linux.zip) -* Распакуйте где угодно -* Запустите `ZeroNet.exe` (win), `ZeroNet(.app)` (osx), `ZeroNet.sh` (linux) +### macOS -### Linux терминал +- Скачайте и распакуйте архив [ZeroNet-mac.zip](https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-mac.zip) (14МБ) +- Запустите `ZeroNet.app` -* `wget https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-linux.zip` -* `unzip ZeroNet-linux.zip` -* `cd ZeroNet-linux` -* Запустите с помощью `./ZeroNet.sh` +### Linux (64 бит) -Он загружает последнюю версию ZeroNet, затем запускает её автоматически. +- Скачайте и распакуйте архив [ZeroNet-linux.zip](https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-linux.zip) (14МБ) +- Запустите `./ZeroNet.sh` -#### Ручная установка для Debian Linux +> **Note** +> Запустите таким образом: `./ZeroNet.sh --ui_ip '*' --ui_restrict ваш_ip_адрес`, чтобы разрешить удалённое подключение к веб–интерфейсу. -* `wget https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-src.zip` -* `unzip ZeroNet-src.zip` -* `cd ZeroNet` -* `sudo apt-get update` -* `sudo apt-get install python3-pip` -* `sudo python3 -m pip install -r requirements.txt` -* Запустите с помощью `python3 zeronet.py` -* Откройте http://127.0.0.1:43110/ в вашем браузере. +### Docker -### [Arch Linux](https://www.archlinux.org) +Официальный образ находится здесь: https://hub.docker.com/r/canewsin/zeronet/ -* `git clone https://aur.archlinux.org/zeronet.git` -* `cd zeronet` -* `makepkg -srci` -* `systemctl start zeronet` -* Откройте http://127.0.0.1:43110/ в вашем браузере. +### Android (arm, arm64, x86) -Смотрите [ArchWiki](https://wiki.archlinux.org)'s [ZeroNet -article](https://wiki.archlinux.org/index.php/ZeroNet) для дальнейшей помощи. +- Для работы требуется Android как минимум версии 5.0 Lollipop +- [Download from Google Play](https://play.google.com/store/apps/details?id=in.canews.zeronetmobile) +- Скачать APK: https://github.com/canewsin/zeronet_mobile/releases -### [Gentoo Linux](https://www.gentoo.org) +### Android (arm, arm64, x86) Облегчённый клиент только для просмотра (1МБ) -* [`layman -a raiagent`](https://github.com/leycec/raiagent) -* `echo '>=net-vpn/zeronet-0.5.4' >> /etc/portage/package.accept_keywords` -* *(Опционально)* Включить поддержку Tor: `echo 'net-vpn/zeronet tor' >> - /etc/portage/package.use` -* `emerge zeronet` -* `rc-service zeronet start` -* Откройте http://127.0.0.1:43110/ в вашем браузере. +- Для работы требуется Android как минимум версии 4.1 Jelly Bean +- [Download from Google Play](https://play.google.com/store/apps/details?id=dev.zeronetx.app.lite) -Смотрите `/usr/share/doc/zeronet-*/README.gentoo.bz2` для дальнейшей помощи. +### Установка из исходного кода -### [FreeBSD](https://www.freebsd.org/) - -* `pkg install zeronet` or `cd /usr/ports/security/zeronet/ && make install clean` -* `sysrc zeronet_enable="YES"` -* `service zeronet start` -* Откройте http://127.0.0.1:43110/ в вашем браузере. - -### [Vagrant](https://www.vagrantup.com/) - -* `vagrant up` -* Подключитесь к VM с помощью `vagrant ssh` -* `cd /vagrant` -* Запустите `python3 zeronet.py --ui_ip 0.0.0.0` -* Откройте http://127.0.0.1:43110/ в вашем браузере. - -### [Docker](https://www.docker.com/) -* `docker run -d -v :/root/data -p 15441:15441 -p 127.0.0.1:43110:43110 canewsin/zeronet` -* Это изображение Docker включает в себя прокси-сервер Tor, который по умолчанию отключён. - Остерегайтесь что некоторые хостинг-провайдеры могут не позволить вам запускать Tor на своих серверах. - Если вы хотите включить его,установите переменную среды `ENABLE_TOR` в` true` (по умолчанию: `false`) Например: - - `docker run -d -e "ENABLE_TOR=true" -v :/root/data -p 15441:15441 -p 127.0.0.1:43110:43110 canewsin/zeronet` -* Откройте http://127.0.0.1:43110/ в вашем браузере. - -### [Virtualenv](https://virtualenv.readthedocs.org/en/latest/) - -* `virtualenv env` -* `source env/bin/activate` -* `pip install msgpack gevent` -* `python3 zeronet.py` -* Откройте http://127.0.0.1:43110/ в вашем браузере. - -## Текущие ограничения - -* Файловые транзакции не сжаты -* Нет приватных сайтов - - -## Как я могу создать сайт в Zeronet? - -Завершите работу zeronet, если он запущен - -```bash -$ zeronet.py siteCreate -... -- Site private key (Приватный ключ сайта): 23DKQpzxhbVBrAtvLEc2uvk7DZweh4qL3fn3jpM3LgHDczMK2TtYUq -- Site address (Адрес сайта): 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2 -... -- Site created! (Сайт создан) -$ zeronet.py -... +```sh +wget https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-src.zip +unzip ZeroNet-src.zip +cd ZeroNet +sudo apt-get update +sudo apt-get install python3-pip +sudo python3 -m pip install -r requirements.txt ``` +- Запустите `python3 zeronet.py` -Поздравляем, вы закончили! Теперь каждый может получить доступ к вашему зайту используя -`http://localhost:43110/13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2` +Откройте приветственную страницу ZeroHello в вашем браузере по ссылке http://127.0.0.1:43110/ -Следующие шаги: [ZeroNet Developer Documentation](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/site_development/getting_started/) +## Как мне создать сайт в ZeroNet? +- Кликните на **⋮** > **"Create new, empty site"** в меню на сайте [ZeroHello](http://127.0.0.1:43110/1HELLoE3sFD9569CLCbHEAVqvqV7U2Ri9d). +- Вы будете **перенаправлены** на совершенно новый сайт, который может быть изменён только вами! +- Вы можете найти и изменить контент вашего сайта в каталоге **data/[адрес_вашего_сайта]** +- После изменений откройте ваш сайт, переключите влево кнопку "0" в правом верхнем углу, затем нажмите кнопки **sign** и **publish** внизу -## Как я могу модифицировать Zeronet сайт? - -* Измените файлы расположенные в data/13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2 директории. - Когда закончите с изменением: - -```bash -$ zeronet.py siteSign 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2 -- Signing site (Подпись сайта): 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2... -Private key (Приватный ключ) (input hidden): -``` - -* Введите секретный ключ, который вы получили при создании сайта, потом: - -```bash -$ zeronet.py sitePublish 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2 -... -Site:13DNDk..bhC2 Publishing to 3/10 peers... -Site:13DNDk..bhC2 Successfuly published to 3 peers -- Serving files.... -``` - -* Вот и всё! Вы успешно подписали и опубликовали свои изменения. - +Следующие шаги: [Документация разработчика ZeroNet](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/site_development/getting_started/) ## Поддержите проект -- Bitcoin: 1ZeroNetyV5mKY9JF1gsm82TuBXHpfdLX (Preferred) + +- Bitcoin: 1ZeroNetyV5mKY9JF1gsm82TuBXHpfdLX (Рекомендуем) - LiberaPay: https://liberapay.com/PramUkesh - Paypal: https://paypal.me/PramUkesh -- Others: [Donate](!https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/help_zeronet/donate/#help-to-keep-zeronet-development-alive) - +- Другие способы: [Donate](!https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/help_zeronet/donate/#help-to-keep-zeronet-development-alive) #### Спасибо! -* Больше информации, помощь, журнал изменений, zeronet сайты: https://www.reddit.com/r/zeronetx/ -* Приходите, пообщайтесь с нами: [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) или на [gitter](https://gitter.im/canewsin/ZeroNet) -* Email: canews.in@gmail.com +- Здесь вы можете получить больше информации, помощь, прочитать список изменений и исследовать ZeroNet сайты: https://www.reddit.com/r/zeronetx/ +- Общение происходит на канале [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) или в [Gitter](https://gitter.im/canewsin/ZeroNet) +- Электронная почта: canews.in@gmail.com From 611fc774c84028601c1d7fd8dd1fa61a20504b93 Mon Sep 17 00:00:00 2001 From: canewsin Date: Mon, 13 Jun 2022 23:07:57 +0530 Subject: [PATCH 159/191] Remove Patreon badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7e805e64..bc22c440 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ZeroNet [![tests](https://github.com/ZeroNetX/ZeroNet/actions/workflows/tests.yml/badge.svg)](https://github.com/ZeroNetX/ZeroNet/actions/workflows/tests.yml) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/help_zeronet/donate/) [![Docker Pulls](https://img.shields.io/docker/pulls/canewsin/zeronet)](https://hub.docker.com/r/canewsin/zeronet) [![Support me on Patreon](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.vercel.app%2Fapi%3Fusername%3Dpramukesh%26type%3Dpatrons&style=flat)](https://www.patreon.com/PramUkesh) +# ZeroNet [![tests](https://github.com/ZeroNetX/ZeroNet/actions/workflows/tests.yml/badge.svg)](https://github.com/ZeroNetX/ZeroNet/actions/workflows/tests.yml) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://docs.zeronet.dev/1DeveLopDZL1cHfKi8UXHh2UBEhzH6HhMp/help_zeronet/donate/) [![Docker Pulls](https://img.shields.io/docker/pulls/canewsin/zeronet)](https://hub.docker.com/r/canewsin/zeronet) Decentralized websites using Bitcoin crypto and the BitTorrent network - https://zeronet.dev / [ZeroNet Site](http://127.0.0.1:43110/1ZeroNetyV5mKY9JF1gsm82TuBXHpfdLX/), Unlike Bitcoin, ZeroNet Doesn't need a blockchain to run, But uses cryptography used by BTC, to ensure data integrity and validation. From 86109ae4b29d3aab15a08c2b9ab0e1f75ae9dd11 Mon Sep 17 00:00:00 2001 From: caryoscelus Date: Wed, 26 Jan 2022 19:28:17 +0000 Subject: [PATCH 160/191] fix readdress loop use better escaping in render fixes #19 --- src/Ui/UiRequest.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index c9026a24..44e71ba6 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -563,6 +563,25 @@ class UiRequest(object): repl.update(html_chars) return s.translate(repl) + def xescape(s): + '''combines parts from re.escape & html.escape''' + # https://github.com/python/cpython/blob/3.10/Lib/re.py#L267 + # '&' is handled otherwise + re_chars = {i: '\\' + chr(i) for i in b'()[]{}*+-|^$\\.~# \t\n\r\v\f'} + # https://github.com/python/cpython/blob/3.10/Lib/html/__init__.py#L12 + html_chars = { + '<' : '<', + '>' : '>', + '"' : '"', + "'" : ''', + } + # we can't replace '&' because it makes certain zites work incorrectly + # it should however in no way interfere with re.sub in render + repl = {} + repl.update(re_chars) + repl.update(html_chars) + return s.translate(repl) + return self.render( "src/Ui/template/wrapper.html", server_url=server_url, From 966f671efe40fdf7a7746cff5073829cc405f002 Mon Sep 17 00:00:00 2001 From: canewsin Date: Sat, 1 Oct 2022 02:21:54 +0530 Subject: [PATCH 161/191] Update CHANGELOG.md --- CHANGELOG.md | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b49b9ef6..80f9b02d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,55 @@ -### ZeroNet 0.7.2 (2020-09-?) Rev4206? +### ZeroNet 0.8.1 (2022-10-01) Rev4600 + - fix readdress loop (cherry-pick previously added commit from conservancy) + - Remove Patreon badge + - Update README-ru.md (#177) + - Include inner_path of failed request for signing in error msg and response + - Don't Fail Silently When Cert is Not Selected + - Console Log Updates, Specify min supported ZeroNet version for Rust version Protocol Compatibility + - Update FUNDING.yml +### ZeroNet 0.8.0 (2022-05-27) Rev4591 + - Revert File Open to catch File Access Errors. + +### ZeroNet 0.7.9-patch (2022-05-26) Rev4586 + - Use xescape(s) from zeronet-conservancy + - actionUpdate response Optimisation + - Fetch Plugins Repo Updates + - Fix Unhandled File Access Errors + - Create codeql-analysis.yml + +### ZeroNet 0.7.9 (2022-05-26) Rev4585 + - Rust Version Compatibility for update Protocol msg + - Removed Non Working Trakers. + - Dynamically Load Trackers from Dashboard Site. + - Tracker Supply Improvements. + - Fix Repo Url for Bug Report + - First Party Tracker Update Service using Dashboard Site. + - remove old v2 onion service [#158](https://github.com/ZeroNetX/ZeroNet/pull/158) + +### ZeroNet 0.7.8 (2022-03-02) Rev4580 + - Update Plugins with some bug fixes and Improvements + +### ZeroNet 0.7.6 (2022-01-12) Rev4565 + - Sync Plugin Updates + - Clean up tor v3 patch [#115](https://github.com/ZeroNetX/ZeroNet/pull/115) + - Add More Default Plugins to Repo + - Doubled Site Publish Limits + - Update ZeroNet Repo Urls [#103](https://github.com/ZeroNetX/ZeroNet/pull/103) + - UI/UX: Increases Size of Notifications Close Button [#106](https://github.com/ZeroNetX/ZeroNet/pull/106) + - Moved Plugins to Seperate Repo + - Added `access_key` variable in Config, this used to access restrited plugins when multiuser plugin is enabled. When MultiUserPlugin is enabled we cannot access some pages like /Stats, this key will remove such restriction with access key. + - Added `last_connection_id_current_version` to ConnectionServer, helpful to estimate no of connection from current client version. + - Added current version: connections to /Stats page. see the previous point. + +### ZeroNet 0.7.5 (2021-11-28) Rev4560 + - Add more default trackers + - Change default homepage address to `1HELLoE3sFD9569CLCbHEAVqvqV7U2Ri9d` + - Change default update site address to `1Update8crprmciJHwp2WXqkx2c4iYp18` + +### ZeroNet 0.7.3 (2021-11-28) Rev4555 + - Fix xrange is undefined error + - Fix Incorrect viewport on mobile while loading + - Tor-V3 Patch by anonymoose ### ZeroNet 0.7.1 (2019-07-01) Rev4206 From fd857985f61d9301c8a1cf75d0eb130daab571d1 Mon Sep 17 00:00:00 2001 From: canewsin Date: Sat, 1 Oct 2022 02:22:50 +0530 Subject: [PATCH 162/191] v0.8.0(4600) --- src/Config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index 1325f1ec..2424d07c 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,8 +13,8 @@ import time class Config(object): def __init__(self, argv): - self.version = "0.8.0" - self.rev = 4591 + self.version = "0.8.1" + self.rev = 4600 self.argv = argv self.action = None self.test_parser = None From ac72d623f08ffa9c4c1bb76c774807aac1ad037a Mon Sep 17 00:00:00 2001 From: canewsin Date: Wed, 5 Oct 2022 03:33:50 +0530 Subject: [PATCH 163/191] remove duplicate xescape(s) --- src/Ui/UiRequest.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 44e71ba6..ba09814c 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -543,25 +543,6 @@ class UiRequest(object): if show_loadingscreen: meta_tags += ''; - - def xescape(s): - '''combines parts from re.escape & html.escape''' - # https://github.com/python/cpython/blob/3.10/Lib/re.py#L267 - # '&' is handled otherwise - re_chars = {i: '\\' + chr(i) for i in b'()[]{}*+-|^$\\.~# \t\n\r\v\f'} - # https://github.com/python/cpython/blob/3.10/Lib/html/__init__.py#L12 - html_chars = { - '<' : '<', - '>' : '>', - '"' : '"', - "'" : ''', - } - # we can't replace '&' because it makes certain zites work incorrectly - # it should however in no way interfere with re.sub in render - repl = {} - repl.update(re_chars) - repl.update(html_chars) - return s.translate(repl) def xescape(s): '''combines parts from re.escape & html.escape''' From ba96654e1d6963ae26187707461d29d9c64f1e6f Mon Sep 17 00:00:00 2001 From: canewsin Date: Wed, 5 Oct 2022 03:36:15 +0530 Subject: [PATCH 164/191] v 0.8.1-patch(4601) --- src/Config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index 2424d07c..421cfd5c 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,8 +13,8 @@ import time class Config(object): def __init__(self, argv): - self.version = "0.8.1" - self.rev = 4600 + self.version = "0.8.1-patch" + self.rev = 4601 self.argv = argv self.action = None self.test_parser = None From d5703541be8c463bb027b99aefc92130d88b8cc8 Mon Sep 17 00:00:00 2001 From: Ganesh Chowdary Nune Date: Sat, 8 Oct 2022 23:28:31 +0530 Subject: [PATCH 165/191] Added documentation for getRandomPort fn --- src/File/FileServer.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index 7f73017e..b7a942fc 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -72,6 +72,12 @@ class FileServer(ConnectionServer): self.ui_server = None def getRandomPort(self, ip, port_range_from, port_range_to): + """Generates Random Port from given range + Args: + ip: IP Address + port_range_from: From Range + port_range_to: to Range + """ self.log.info("Getting random port in range %s-%s..." % (port_range_from, port_range_to)) tried = [] for bind_retry in range(100): From b7870edd2e4b9955be93ec2f2d764ef7e7ebcdb0 Mon Sep 17 00:00:00 2001 From: canewsin Date: Tue, 1 Nov 2022 15:38:57 +0530 Subject: [PATCH 166/191] Fix Startup Error when plugins dir missing --- src/Plugin/PluginManager.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Plugin/PluginManager.py b/src/Plugin/PluginManager.py index dbafa98f..56540e60 100644 --- a/src/Plugin/PluginManager.py +++ b/src/Plugin/PluginManager.py @@ -16,7 +16,9 @@ import plugins class PluginManager: def __init__(self): self.log = logging.getLogger("PluginManager") - self.path_plugins = os.path.abspath(os.path.dirname(plugins.__file__)) + self.path_plugins = None + if plugins.__file__: + self.path_plugins = os.path.dirname(os.path.abspath(plugins.__file__)); self.path_installed_plugins = config.data_dir + "/__plugins__" self.plugins = defaultdict(list) # Registered plugins (key: class name, value: list of plugins for class) self.subclass_order = {} # Record the load order of the plugins, to keep it after reload @@ -32,7 +34,8 @@ class PluginManager: self.config.setdefault("builtin", {}) - sys.path.append(os.path.join(os.getcwd(), self.path_plugins)) + if self.path_plugins: + sys.path.append(os.path.join(os.getcwd(), self.path_plugins)) self.migratePlugins() if config.debug: # Auto reload Plugins on file change @@ -127,6 +130,8 @@ class PluginManager: def loadPlugins(self): all_loaded = True s = time.time() + if self.path_plugins is None: + return for plugin in self.listPlugins(): self.log.debug("Loading plugin: %s (%s)" % (plugin["name"], plugin["source"])) if plugin["source"] != "builtin": From 459b0a73ca232a8a9c105cec22e66afd3498e45e Mon Sep 17 00:00:00 2001 From: canewsin Date: Tue, 1 Nov 2022 17:45:22 +0530 Subject: [PATCH 167/191] Move trackers to seperate file & Add more trackers --- src/Config.py | 34 ++---------- trackers.txt | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 31 deletions(-) create mode 100644 trackers.txt diff --git a/src/Config.py b/src/Config.py index 421cfd5c..7d64dc6b 100644 --- a/src/Config.py +++ b/src/Config.py @@ -36,7 +36,7 @@ class Config(object): self.openssl_lib_file = None self.openssl_bin_file = None - self.trackers_file = ["{data_dir}/1HELLoE3sFD9569CLCbHEAVqvqV7U2Ri9d/trackers.txt"] + self.trackers_file = False self.createParser() self.createArguments() @@ -81,34 +81,7 @@ class Config(object): def createArguments(self): from Crypt import CryptHash access_key_default = CryptHash.random(24, "base64") # Used to allow restrited plugins when multiuser plugin is enabled - trackers = [ - "http://open.acgnxtracker.com:80/announce", # DE - "http://tracker.bt4g.com:2095/announce", # Cloudflare - "http://vps02.net.orel.ru:80/announce", - "http://tracker.files.fm:6969/announce", - "http://t.publictracker.xyz:6969/announce", - "https://tracker.lilithraws.cf:443/announce", - "https://tracker.babico.name.tr:443/announce", - "https://tr.abiir.top:443/announce", - "udp://abufinzio.monocul.us:6969/announce", - "udp://vibe.sleepyinternetfun.xyz:1738/announce", - "udp://www.torrent.eu.org:451/announce", - "udp://tracker.0x.tf:6969/announce", - "udp://tracker.zerobytes.xyz:1337/announce", - "udp://tracker.opentrackr.org:1337/announce", - "udp://tracker.birkenwald.de:6969/announce", - "udp://tracker.moeking.me:6969/announce", - "udp://ipv6.babico.name.tr:8000/announce", - "zero://145.239.95.38:15441", - "zero://188.116.183.41:26552", - "zero://45.77.23.92:15555", - "zero://k5w77dozo3hy5zualyhni6vrh73iwfkaofa64abbilwyhhd3wgenbjqd.onion:15441", - "zero://2kcb2fqesyaevc4lntogupa4mkdssth2ypfwczd2ov5a3zo6ytwwbayd.onion:15441", - "zero://gugt43coc5tkyrhrc3esf6t6aeycvcqzw7qafxrjpqbwt4ssz5czgzyd.onion:15441", - "zero://5vczpwawviukvd7grfhsfxp7a6huz77hlis4fstjkym5kmf4pu7i7myd.onion:15441", - "zero://ow7in4ftwsix5klcbdfqvfqjvimqshbm2o75rhtpdnsderrcbx74wbad.onion:15441", - "zero://qn65si4gtcwdiliq7vzrwu62qrweoxb6tx2cchwslaervj6szuje66qd.onion:26117", - ] + trackers = [] # Platform specific if sys.platform.startswith("win"): coffeescript = "type %s | tools\\coffee\\coffee.cmd" @@ -339,8 +312,7 @@ class Config(object): def loadTrackersFile(self): if not self.trackers_file: - return None - + self.trackers_file = ["trackers.txt", "{data_dir}/1HELLoE3sFD9569CLCbHEAVqvqV7U2Ri9d/trackers.txt"] self.trackers = self.arguments.trackers[:] for trackers_file in self.trackers_file: diff --git a/trackers.txt b/trackers.txt new file mode 100644 index 00000000..a42f8ca4 --- /dev/null +++ b/trackers.txt @@ -0,0 +1,142 @@ +udp://tracker.opentrackr.org:1337/announce +udp://explodie.org:6969/announce +udp://open.stealth.si:80/announce +http://tracker.ipv6tracker.ru:80/announce +udp://tracker.birkenwald.de:6969/announce +udp://tracker.moeking.me:6969/announce +http://tracker.bt4g.com:2095/announce +https://tracker.nanoha.org:443/announce +http://tracker.files.fm:6969/announce +http://open.acgnxtracker.com:80/announce +udp://tracker.army:6969/announce +udp://fe.dealclub.de:6969/announce +udp://tracker.leech.ie:1337/announce +udp://tracker.altrosky.nl:6969/announce +https://tracker.cyber-hub.net:443/announce +https://tracker.lilithraws.cf:443/announce +http://bt.okmp3.ru:2710/announce +udp://vibe.sleepyinternetfun.xyz:1738/announce +udp://open.publictracker.xyz:6969/announce +udp://tracker.bitsearch.to:1337/announce +udp://tracker.pomf.se:80/announce +https://tr.burnabyhighstar.com:443/announce +https://tr.abiir.top:443/announce +udp://open.free-tracker.ga:6969/announce +http://i-p-v-6.tk:6969/announce +http://open-v6.demonoid.ch:6969/announce +udp://aarsen.me:6969/announce +udp://htz3.noho.st:6969/announce +udp://uploads.gamecoast.net:6969/announce +udp://mail.zasaonsk.ga:6969/announce +udp://tracker.joybomb.tw:6969/announce +udp://tracker.jonaslsa.com:6969/announce +udp://leefafa.tk:6969/announce +udp://carr.codes:6969/announce +https://tr.fuckbitcoin.xyz:443/announce +udp://tracker.cubonegro.xyz:6969/announce +udp://tracker.skynetcloud.site:6969/announce +http://tracker4.itzmx.com:2710/announce +https://tracker.lilithraws.org:443/announce +udp://tracker.novaopcj.eu.org:6969/announce +udp://exodus.desync.com:6969/announce +http://t.acg.rip:6699/announce +udp://tracker2.dler.com:80/announce +udp://6ahddutb1ucc3cp.ru:6969/announce +udp://tracker.blacksparrowmedia.net:6969/announce +http://fxtt.ru:80/announce +udp://tracker.auctor.tv:6969/announce +udp://torrentclub.space:6969/announce +udp://zecircle.xyz:6969/announce +udp://psyco.fr:6969/announce +udp://fh2.cmp-gaming.com:6969/announce +udp://new-line.net:6969/announce +udp://torrents.artixlinux.org:6969/announce +udp://bt.ktrackers.com:6666/announce +udp://static.54.161.216.95.clients.your-server.de:6969/announce +udp://cpe-104-34-3-152.socal.res.rr.com:6969/announce +http://t.overflow.biz:6969/announce +udp://tracker1.myporn.club:9337/announce +udp://moonburrow.club:6969/announce +udp://tracker.artixlinux.org:6969/announce +https://t1.hloli.org:443/announce +udp://bt1.archive.org:6969/announce +udp://tracker.theoks.net:6969/announce +udp://tracker.4.babico.name.tr:3131/announce +udp://buddyfly.top:6969/announce +udp://ipv6.tracker.harry.lu:80/announce +udp://public.publictracker.xyz:6969/announce +udp://mail.artixlinux.org:6969/announce +udp://v1046920.hosted-by-vdsina.ru:6969/announce +udp://tracker.cyberia.is:6969/announce +udp://tracker.beeimg.com:6969/announce +udp://creative.7o7.cx:6969/announce +udp://open.dstud.io:6969/announce +udp://laze.cc:6969/announce +udp://download.nerocloud.me:6969/announce +udp://cutscloud.duckdns.org:6969/announce +https://tracker.jiesen.life:8443/announce +udp://jutone.com:6969/announce +udp://wepzone.net:6969/announce +udp://ipv4.tracker.harry.lu:80/announce +udp://tracker.tcp.exchange:6969/announce +udp://f1sh.de:6969/announce +udp://movies.zsw.ca:6969/announce +https://tracker1.ctix.cn:443/announce +udp://sanincode.com:6969/announce +udp://www.torrent.eu.org:451/announce +udp://open.4ever.tk:6969/announce +https://tracker2.ctix.cn:443/announce +udp://bt2.archive.org:6969/announce +http://t.nyaatracker.com:80/announce +udp://yahor.ftp.sh:6969/announce +udp://tracker.openbtba.com:6969/announce +udp://tracker.dler.com:6969/announce +udp://tracker-udp.gbitt.info:80/announce +udp://tracker.srv00.com:6969/announce +udp://tracker.pimpmyworld.to:6969/announce +http://tracker.gbitt.info:80/announce +udp://tracker6.lelux.fi:6969/announce +http://tracker.vrpnet.org:6969/announce +http://00.xxtor.com:443/announce +http://vps02.net.orel.ru:80/announce +udp://tracker.yangxiaoguozi.cn:6969/announce +udp://rep-art.ynh.fr:6969/announce +https://tracker.imgoingto.icu:443/announce +udp://mirror.aptus.co.tz:6969/announce +udp://tracker.lelux.fi:6969/announce +udp://tracker.torrent.eu.org:451/announce +udp://admin.52ywp.com:6969/announce +udp://thouvenin.cloud:6969/announce +http://vps-dd0a0715.vps.ovh.net:6969/announce +udp://bubu.mapfactor.com:6969/announce +udp://94-227-232-84.access.telenet.be:6969/announce +udp://epider.me:6969/announce +udp://camera.lei001.com:6969/announce +udp://tamas3.ynh.fr:6969/announce +https://tracker.tamersunion.org:443/announce +udp://ftp.pet:2710/announce +udp://p4p.arenabg.com:1337/announce +http://tracker.mywaifu.best:6969/announce +udp://tracker.monitorit4.me:6969/announce +udp://ipv6.tracker.monitorit4.me:6969/announce +zero://k5w77dozo3hy5zualyhni6vrh73iwfkaofa64abbilwyhhd3wgenbjqd.onion:15441 +zero://2kcb2fqesyaevc4lntogupa4mkdssth2ypfwczd2ov5a3zo6ytwwbayd.onion:15441 +zero://5vczpwawviukvd7grfhsfxp7a6huz77hlis4fstjkym5kmf4pu7i7myd.onion:15441 +zero://pn4q2zzt2pw4nk7yidxvsxmydko7dfibuzxdswi6gu6ninjpofvqs2id.onion:15441 +zero://6i54dd5th73oelv636ivix6sjnwfgk2qsltnyvswagwphub375t3xcad.onion:15441 +zero://tl74auz4tyqv4bieeclmyoe4uwtoc2dj7fdqv4nc4gl5j2bwg2r26bqd.onion:15441 +zero://wlxav3szbrdhest4j7dib2vgbrd7uj7u7rnuzg22cxbih7yxyg2hsmid.onion:15441 +zero://zy7wttvjtsijt5uwmlar4yguvjc2gppzbdj4v6bujng6xwjmkdg7uvqd.onion:15441 +zero://rlcjomszyitxpwv7kzopmqgzk3bdpsxeull4c3s6goszkk6h2sotfoad.onion:15441 +zero://gugt43coc5tkyrhrc3esf6t6aeycvcqzw7qafxrjpqbwt4ssz5czgzyd.onion:15441 +zero://ow7in4ftwsix5klcbdfqvfqjvimqshbm2o75rhtpdnsderrcbx74wbad.onion:15441 +zero://57hzgtu62yzxqgbvgxs7g3lfck3za4zrda7qkskar3tlak5recxcebyd.onion:15445 +zero://hb6ozikfiaafeuqvgseiik4r46szbpjfu66l67wjinnyv6dtopuwhtqd.onion:15445 +zero://qn65si4gtcwdiliq7vzrwu62qrweoxb6tx2cchwslaervj6szuje66qd.onion:26117 +zero://s3j2s5pjdfesbsmaqx6alsumaxxdxibmhv4eukmqpv3vqj6f627qx5yd.onion:15441 +zero://agufghdtniyfwty3wk55drxxwj2zxgzzo7dbrtje73gmvcpxy4ngs4ad.onion:15441 +zero://kgsvasoakvj4gnjiy7zemu34l3hq46dn5eauqkn76jpowmilci5t2vqd.onion:15445 +zero://dslesoe72bdfwfu4cfqa2wpd4hr3fhlu4zv6mfsjju5xlpmssouv36qd.onion:15441 +zero://f2hnjbggc3c2u2apvxdugirnk6bral54ibdoul3hhvu7pd4fso5fq3yd.onion:15441 +zero://skdeywpgm5xncpxbbr4cuiip6ey4dkambpanog6nruvmef4f3e7o47qd.onion:15441 +zero://tqmo2nffqo4qc5jgmz3me5eri3zpgf3v2zciufzmhnvznjve5c3argad.onion:15441 \ No newline at end of file From ad95eede105907f181cf5a06adfc356d9be632dd Mon Sep 17 00:00:00 2001 From: canewsin Date: Tue, 1 Nov 2022 18:06:32 +0530 Subject: [PATCH 168/191] Config:: Skip loading missing tracker files --- src/Config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Config.py b/src/Config.py index 7d64dc6b..f4ef916f 100644 --- a/src/Config.py +++ b/src/Config.py @@ -324,6 +324,9 @@ class Config(object): else: # Relative to zeronet.py trackers_file_path = self.start_dir + "/" + trackers_file + if not os.path.exists(trackers_file_path): + continue + for line in open(trackers_file_path): tracker = line.strip() if "://" in tracker and tracker not in self.trackers: From 07317875184a856153997a564a09861cf7ed63a9 Mon Sep 17 00:00:00 2001 From: canewsin Date: Tue, 1 Nov 2022 18:10:15 +0530 Subject: [PATCH 169/191] v0.8.2(4610) --- src/Config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index f4ef916f..cf461d4f 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,8 +13,8 @@ import time class Config(object): def __init__(self, argv): - self.version = "0.8.1-patch" - self.rev = 4601 + self.version = "0.8.2" + self.rev = 4610 self.argv = argv self.action = None self.test_parser = None From f79a73cef43c7f77f64c85c87b37631dbc67a010 Mon Sep 17 00:00:00 2001 From: canewsin Date: Wed, 23 Nov 2022 13:02:58 +0530 Subject: [PATCH 170/191] main.py -> Fix accessing unassigned varible --- src/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index 6ba85052..ec90f4d9 100644 --- a/src/main.py +++ b/src/main.py @@ -254,8 +254,9 @@ class Actions(object): file_correct = site.content_manager.verifyFile( content_inner_path, site.storage.open(content_inner_path, "rb"), ignore_same=False ) - except Exception as err: + except Exception as exp: file_correct = False + err = exp if file_correct is True: logging.info("[OK] %s (Done in %.3fs)" % (content_inner_path, time.time() - s)) From f1a71770fa2e0ec19cb665293ffba82f6995a4df Mon Sep 17 00:00:00 2001 From: canewsin Date: Wed, 23 Nov 2022 13:07:16 +0530 Subject: [PATCH 171/191] ContentManager -> Support for multiSig --- src/Content/ContentManager.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 27da402b..8003b7c2 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -727,7 +727,6 @@ class ContentManager(object): elif "files_optional" in new_content: del new_content["files_optional"] - new_content["modified"] = int(time.time()) # Add timestamp if inner_path == "content.json": new_content["zeronet_version"] = config.version new_content["signs_required"] = content.get("signs_required", 1) @@ -747,9 +746,11 @@ class ContentManager(object): ) self.log.info("Correct %s in valid signers: %s" % (privatekey_address, valid_signers)) + signs_required = 1 if inner_path == "content.json" and privatekey_address == self.site.address: # If signing using the root key, then sign the valid signers - signers_data = "%s:%s" % (new_content["signs_required"], ",".join(valid_signers)) + signs_required = new_content["signs_required"] + signers_data = "%s:%s" % (signs_required, ",".join(valid_signers)) new_content["signers_sign"] = CryptBitcoin.sign(str(signers_data), privatekey) if not new_content["signers_sign"]: self.log.info("Old style address, signers_sign is none") @@ -757,15 +758,32 @@ class ContentManager(object): self.log.info("Signing %s..." % inner_path) if "signs" in new_content: - del(new_content["signs"]) # Delete old signs + # del(new_content["signs"]) # Delete old signs + old_signs_content = new_content["signs"] + del(new_content["signs"]) + else: + old_signs_content = None if "sign" in new_content: del(new_content["sign"]) # Delete old sign (backward compatibility) - sign_content = json.dumps(new_content, sort_keys=True) + if signs_required > 1: + has_valid_sign = False + sign_content = json.dumps(new_content, sort_keys=True) + for signer in valid_signers: + res = CryptBitcoin.verify(sign_content,signer,old_signs_content[signer]); + print(res) + if res: + has_valid_sign = has_valid_sign or res + if has_valid_sign: + new_content["modified"] = content["modified"] + sign_content = json.dumps(new_content, sort_keys=True) + else: + new_content["modified"] = int(time.time()) # Add timestamp + sign_content = json.dumps(new_content, sort_keys=True) sign = CryptBitcoin.sign(sign_content, privatekey) # new_content["signs"] = content.get("signs", {}) # TODO: Multisig if sign: # If signing is successful (not an old address) - new_content["signs"] = {} + new_content["signs"] = old_signs_content or {} new_content["signs"][privatekey_address] = sign self.verifyContent(inner_path, new_content) @@ -800,7 +818,9 @@ class ContentManager(object): # Return: The required number of valid signs for the content.json def getSignsRequired(self, inner_path, content=None): - return 1 # Todo: Multisig + if not content: + return 1 + return content.get("signs_required", 1) def verifyCertSign(self, user_address, user_auth_type, user_name, issuer_address, sign): from Crypt import CryptBitcoin From 1500d9356b134db2a62ff8652267cfa86446651c Mon Sep 17 00:00:00 2001 From: canewsin Date: Sun, 11 Dec 2022 02:00:57 +0530 Subject: [PATCH 172/191] SiteStrorage.py -> Fix accessing unassigned varible --- src/Site/SiteStorage.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index 47d4c425..27032e79 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -463,7 +463,8 @@ class SiteStorage(object): else: try: ok = self.site.content_manager.verifyFile(file_inner_path, open(file_path, "rb")) - except Exception as err: + except Exception as _err: + err = _err ok = False if not ok: From 85ef28e6fb953f4cff6117550c71d0874a4fc52b Mon Sep 17 00:00:00 2001 From: canewsin Date: Sun, 11 Dec 2022 02:02:15 +0530 Subject: [PATCH 173/191] ContentManager.py Improve Logging of Valid Signers --- src/Content/ContentManager.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 8003b7c2..623cc707 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -1008,14 +1008,16 @@ class ContentManager(object): if inner_path != "content.json" and not self.verifyCert(inner_path, new_content): # Check if cert valid raise VerifyError("Invalid cert!") - valid_signs = 0 + valid_signs = [] for address in valid_signers: if address in signs: - valid_signs += CryptBitcoin.verify(sign_content, address, signs[address]) - if valid_signs >= signs_required: + result = CryptBitcoin.verify(sign_content, address, signs[address]) + if result: + valid_signs.append(address) + if len(valid_signs) >= signs_required: break # Break if we has enough signs - if valid_signs < signs_required: - raise VerifyError("Valid signs: %s/%s" % (valid_signs, signs_required)) + if len(valid_signs) < signs_required: + raise VerifyError("Valid signs: %s/%s, Valid Signers : %s" % (len(valid_signs), signs_required, valid_signs)) else: return self.verifyContent(inner_path, new_content) else: # Old style signing From 3550a64837d1c48248648668f3d9e6b4906365e8 Mon Sep 17 00:00:00 2001 From: canewsin Date: Sun, 11 Dec 2022 02:12:34 +0530 Subject: [PATCH 174/191] v0.8.3(4611) --- CHANGELOG.md | 12 ++++++++++++ src/Config.py | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80f9b02d..4aef0f74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +### ZeroNet 0.8.3 (2022-12-11) Rev4611 + - main.py -> Fix accessing unassigned varible + - ContentManager -> Support for multiSig + - SiteStrorage.py -> Fix accessing unassigned varible + - ContentManager.py Improve Logging of Valid Signers + +### ZeroNet 0.8.2 (2022-11-01) Rev4610 + - Fix Startup Error when plugins dir missing + - Move trackers to seperate file & Add more trackers + - Config:: Skip loading missing tracker files + - Added documentation for getRandomPort fn + ### ZeroNet 0.8.1 (2022-10-01) Rev4600 - fix readdress loop (cherry-pick previously added commit from conservancy) - Remove Patreon badge diff --git a/src/Config.py b/src/Config.py index cf461d4f..74e31c40 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,8 +13,8 @@ import time class Config(object): def __init__(self, argv): - self.version = "0.8.2" - self.rev = 4610 + self.version = "0.8.3" + self.rev = 4611 self.argv = argv self.action = None self.test_parser = None From 99a8409513596029e7a70c74b3be6d9424288830 Mon Sep 17 00:00:00 2001 From: canewsin Date: Sun, 11 Dec 2022 04:30:31 +0530 Subject: [PATCH 175/191] Increase Def Min Site Size to 25MB --- CHANGELOG.md | 3 +++ src/Config.py | 2 +- src/Site/Site.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4aef0f74..5c21f752 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +### ZeroNet 0.8.4 Unreleased + - Increase Minimum Site size to 25MB. + ### ZeroNet 0.8.3 (2022-12-11) Rev4611 - main.py -> Fix accessing unassigned varible - ContentManager -> Support for multiSig diff --git a/src/Config.py b/src/Config.py index 74e31c40..07571e26 100644 --- a/src/Config.py +++ b/src/Config.py @@ -244,7 +244,7 @@ class Config(object): self.parser.add_argument('--access_key', help='Plugin access key default: Random key generated at startup', default=access_key_default, metavar='key') self.parser.add_argument('--dist_type', help='Type of installed distribution', default='source') - self.parser.add_argument('--size_limit', help='Default site size limit in MB', default=10, type=int, metavar='limit') + self.parser.add_argument('--size_limit', help='Default site size limit in MB', default=25, type=int, metavar='limit') self.parser.add_argument('--file_size_limit', help='Maximum per file size limit in MB', default=10, type=int, metavar='limit') self.parser.add_argument('--connected_limit', help='Max connected peer per site', default=8, type=int, metavar='connected_limit') self.parser.add_argument('--global_connected_limit', help='Max connections', default=512, type=int, metavar='global_connected_limit') diff --git a/src/Site/Site.py b/src/Site/Site.py index 354fe9c0..d6179307 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -143,7 +143,7 @@ class Site(object): # Next size limit based on current size def getNextSizeLimit(self): - size_limits = [10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000] + size_limits = [25, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000] size = self.settings.get("size", 0) for size_limit in size_limits: if size * 1.2 < size_limit * 1024 * 1024: From edc5310cd2593822c7023ec894b1cf316a783aaf Mon Sep 17 00:00:00 2001 From: canewsin Date: Sun, 11 Dec 2022 05:01:55 +0530 Subject: [PATCH 176/191] v0.8.4(4620) --- CHANGELOG.md | 2 +- src/Config.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c21f752..25e27519 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### ZeroNet 0.8.4 Unreleased +### ZeroNet 0.8.4 (2022-12-12) Rev4620 - Increase Minimum Site size to 25MB. ### ZeroNet 0.8.3 (2022-12-11) Rev4611 diff --git a/src/Config.py b/src/Config.py index 07571e26..9deffb3b 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,8 +13,8 @@ import time class Config(object): def __init__(self, argv): - self.version = "0.8.3" - self.rev = 4611 + self.version = "0.8.4" + self.rev = 4620 self.argv = argv self.action = None self.test_parser = None From 77b4297224470b97f590412686bdbc79787d54e3 Mon Sep 17 00:00:00 2001 From: canewsin Date: Sun, 25 Dec 2022 01:26:53 +0530 Subject: [PATCH 177/191] Update Stats Plugin --- plugins | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins b/plugins index 859ffbf4..412d3703 160000 --- a/plugins +++ b/plugins @@ -1 +1 @@ -Subproject commit 859ffbf43335796e225525ff01257711485088d9 +Subproject commit 412d37030beca51244741e138b5f6d97f8f1a652 From c354f9e24da048980553766e53fc24ab0df57814 Mon Sep 17 00:00:00 2001 From: canewsin Date: Sun, 25 Dec 2022 01:28:16 +0530 Subject: [PATCH 178/191] Use default theme-class for corrupt users.json file where settings key is missing etc Fixes Ui.UiServer Error 500: UiWSGIHandler error --- src/Ui/UiRequest.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index ba09814c..4a4e0545 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -749,7 +749,10 @@ class UiRequest(object): def replaceHtmlVariables(self, block, path_parts): user = self.getCurrentUser() - themeclass = "theme-%-6s" % re.sub("[^a-z]", "", user.settings.get("theme", "light")) + if user and user.settings: + themeclass = "theme-%-6s" % re.sub("[^a-z]", "", user.settings.get("theme", "light")) + else: + themeclass = "theme-light" block = block.replace(b"{themeclass}", themeclass.encode("utf8")) if path_parts: From 06a9d1e0ffb99fb9e9fe69c99dce608b18f7d0d4 Mon Sep 17 00:00:00 2001 From: Seto <61304189@qq.com> Date: Wed, 4 Jan 2023 12:18:15 +0800 Subject: [PATCH 179/191] Fix openssl error in windows. --- src/Crypt/CryptConnection.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Crypt/CryptConnection.py b/src/Crypt/CryptConnection.py index ebbc6295..c0903e84 100644 --- a/src/Crypt/CryptConnection.py +++ b/src/Crypt/CryptConnection.py @@ -127,6 +127,10 @@ class CryptConnectionManager: "/C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO RSA Domain Validation Secure Server CA" ] self.openssl_env['CN'] = random.choice(self.fakedomains) + environ = os.environ + environ['OPENSSL_CONF'] = self.openssl_env['OPENSSL_CONF'] + environ['RANDFILE'] = self.openssl_env['RANDFILE'] + environ['CN'] = self.openssl_env['CN'] if os.path.isfile(self.cert_pem) and os.path.isfile(self.key_pem): self.createSslContexts() @@ -152,7 +156,7 @@ class CryptConnectionManager: self.log.debug("Running: %s" % cmd) proc = subprocess.Popen( cmd, shell=True, stderr=subprocess.STDOUT, - stdout=subprocess.PIPE, env=self.openssl_env + stdout=subprocess.PIPE, env=environ ) back = proc.stdout.read().strip().decode(errors="replace").replace("\r", "") proc.wait() @@ -175,7 +179,7 @@ class CryptConnectionManager: self.log.debug("Generating certificate key and signing request...") proc = subprocess.Popen( cmd, shell=True, stderr=subprocess.STDOUT, - stdout=subprocess.PIPE, env=self.openssl_env + stdout=subprocess.PIPE, env=environ ) back = proc.stdout.read().strip().decode(errors="replace").replace("\r", "") proc.wait() @@ -194,7 +198,7 @@ class CryptConnectionManager: self.log.debug("Generating RSA cert...") proc = subprocess.Popen( cmd, shell=True, stderr=subprocess.STDOUT, - stdout=subprocess.PIPE, env=self.openssl_env + stdout=subprocess.PIPE, env=environ ) back = proc.stdout.read().strip().decode(errors="replace").replace("\r", "") proc.wait() From dd2bb07cfbb1d528ec4c861a0be4380d03e7043f Mon Sep 17 00:00:00 2001 From: canewsin Date: Sun, 12 Feb 2023 00:41:38 +0530 Subject: [PATCH 180/191] v0.8.5(4625) --- CHANGELOG.md | 5 +++++ src/Config.py | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25e27519..a4365925 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +### ZeroNet 0.8.5 (2023-02-12) Rev4625 + - Fix(https://github.com/ZeroNetX/ZeroNet/pull/202) for SSL cert gen failed on Windows. + - default theme-class for missing value in `users.json`. + - Fetch Stats Plugin changes. + ### ZeroNet 0.8.4 (2022-12-12) Rev4620 - Increase Minimum Site size to 25MB. diff --git a/src/Config.py b/src/Config.py index 9deffb3b..96c3813e 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,8 +13,8 @@ import time class Config(object): def __init__(self, argv): - self.version = "0.8.4" - self.rev = 4620 + self.version = "0.8.5" + self.rev = 4625 self.argv = argv self.action = None self.test_parser = None From f2ef6e5d9c653fa6d64754a50aa547f4a1a77197 Mon Sep 17 00:00:00 2001 From: canewsin Date: Fri, 24 Feb 2023 16:56:10 +0530 Subject: [PATCH 181/191] Fix Response when site is missing for `actionAs` --- src/Ui/UiWebsocket.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 8514aa0c..2f982e1d 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -327,7 +327,10 @@ class UiWebsocket(object): def actionAs(self, to, address, cmd, params=[]): if not self.hasSitePermission(address, cmd=cmd): + #TODO! Return this as error ? return self.response(to, "No permission for site %s" % address) + if not self.server.sites.get(address): + return self.response(to, {"error": "Site Does Not Exist: %s" % address}) req_self = copy.copy(self) req_self.site = self.server.sites.get(address) req_self.hasCmdPermission = self.hasCmdPermission # Use the same permissions as current site From d8e52eaabd8ee1cbc429514abb0955ebb5a686f9 Mon Sep 17 00:00:00 2001 From: canewsin Date: Fri, 24 Mar 2023 02:23:16 +0530 Subject: [PATCH 182/191] FileRequest -> Remove Unnecessary check --- src/File/FileRequest.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index afc26cb7..f3f8be9a 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -165,21 +165,19 @@ class FileRequest(object): site.onFileDone(inner_path) # Trigger filedone - if inner_path.endswith("content.json"): # Download every changed file from peer - peer = site.addPeer(self.connection.ip, self.connection.port, return_peer=True, source="update") # Add or get peer - # On complete publish to other peers - diffs = params.get("diffs", {}) - site.onComplete.once(lambda: site.publish(inner_path=inner_path, diffs=diffs, limit=6), "publish_%s" % inner_path) + # Download every changed file from peer + peer = site.addPeer(self.connection.ip, self.connection.port, return_peer=True, source="update") # Add or get peer + # On complete publish to other peers + diffs = params.get("diffs", {}) + site.onComplete.once(lambda: site.publish(inner_path=inner_path, diffs=diffs, limit=6), "publish_%s" % inner_path) - # Load new content file and download changed files in new thread - def downloader(): - site.downloadContent(inner_path, peer=peer, diffs=params.get("diffs", {})) - del self.server.files_parsing[file_uri] - - gevent.spawn(downloader) - else: + # Load new content file and download changed files in new thread + def downloader(): + site.downloadContent(inner_path, peer=peer, diffs=params.get("diffs", {})) del self.server.files_parsing[file_uri] + gevent.spawn(downloader) + self.response({"ok": "Thanks, file %s updated!" % inner_path}) self.connection.goodAction() From a429349cd41ad0987d41f17cf41280020805a720 Mon Sep 17 00:00:00 2001 From: canewsin Date: Fri, 24 Mar 2023 02:24:14 +0530 Subject: [PATCH 183/191] FileRequest -> Fix error wording --- src/File/FileRequest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index f3f8be9a..c082c378 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -128,7 +128,7 @@ class FileRequest(object): body = peer.getFile(site.address, inner_path).read() except Exception as err: site.log.debug("Can't download updated file %s: %s" % (inner_path, err)) - self.response({"error": "File invalid update: Can't download updaed file"}) + self.response({"error": "Invalid File update: Failed to download updated file content"}) self.connection.badAction(5) return From 117bcf25d976e7a21d2b19580e1dbb006ee12b8c Mon Sep 17 00:00:00 2001 From: PramUkesh Date: Sat, 1 Jul 2023 02:56:49 +0530 Subject: [PATCH 184/191] Fix pysha3 dep installation issue --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b3df57ea..538a6dfc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ greenlet==0.4.16; python_version <= "3.6" gevent>=20.9.0; python_version >= "3.7" msgpack>=0.4.4 base58 -merkletools +merkletools @ git+https://github.com/ZeroNetX/pymerkletools.git@dev rsa PySocks>=1.6.8 pyasn1 From fedcf9c1c6af23cfa934084a1264d9182bb74cd3 Mon Sep 17 00:00:00 2001 From: PramUkesh Date: Sat, 1 Jul 2023 03:21:32 +0530 Subject: [PATCH 185/191] Added Proxy links --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index bc22c440..70b79adc 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,24 @@ Decentralized websites using Bitcoin crypto and the BitTorrent network - https:/ #### Docker There is an official image, built from source at: https://hub.docker.com/r/canewsin/zeronet/ +### Online Proxies +Proxies are like seed boxes for sites(i.e ZNX runs on a cloud vps), you can try zeronet experience from proxies. Add your proxy below if you have one. + +#### Official ZNX Proxy : + +https://proxy.zeronet.dev/ + +https://zeronet.dev/ + +#### From Community + +https://0net-preview.com/ + +https://portal.ngnoid.tv/ + +https://zeronet.ipfsscan.io/ + + ### Install from source - `wget https://github.com/ZeroNetX/ZeroNet/releases/latest/download/ZeroNet-src.zip` From e8cf14bcf56831454f275cda0bc4dda9630f9c81 Mon Sep 17 00:00:00 2001 From: PramUkesh Date: Sat, 1 Jul 2023 04:25:41 +0530 Subject: [PATCH 186/191] Add trackers to Config.py for failsafety incase missing trackers..txt file --- src/Config.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 96c3813e..9c17168c 100644 --- a/src/Config.py +++ b/src/Config.py @@ -81,7 +81,14 @@ class Config(object): def createArguments(self): from Crypt import CryptHash access_key_default = CryptHash.random(24, "base64") # Used to allow restrited plugins when multiuser plugin is enabled - trackers = [] + trackers = [ + "http://open.acgnxtracker.com:80/announce", # DE + "http://tracker.bt4g.com:2095/announce", # Cloudflare + "http://tracker.files.fm:6969/announce", + "http://t.publictracker.xyz:6969/announce", + "https://tracker.lilithraws.cf:443/announce", + "https://tracker.babico.name.tr:443/announce", + ] # Platform specific if sys.platform.startswith("win"): coffeescript = "type %s | tools\\coffee\\coffee.cmd" From 866179f6a312bcc9d248bf857f754281df016ad1 Mon Sep 17 00:00:00 2001 From: PramUkesh Date: Sat, 1 Jul 2023 04:27:48 +0530 Subject: [PATCH 187/191] v0.8.6(4626) --- src/Config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index 9c17168c..0c6adca8 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,8 +13,8 @@ import time class Config(object): def __init__(self, argv): - self.version = "0.8.5" - self.rev = 4625 + self.version = "0.8.6" + self.rev = 4626 self.argv = argv self.action = None self.test_parser = None From 2970e3a2053fe2f03106a0cfbd9b6fd946df19bc Mon Sep 17 00:00:00 2001 From: canewsin Date: Wed, 12 Jul 2023 01:25:48 +0530 Subject: [PATCH 188/191] Fetch plugins changes --- plugins | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins b/plugins index 412d3703..689d9309 160000 --- a/plugins +++ b/plugins @@ -1 +1 @@ -Subproject commit 412d37030beca51244741e138b5f6d97f8f1a652 +Subproject commit 689d9309f73371f4681191b125ec3f2e14075eeb From 25c5658b72e5a6c1872df3e487b54d2465adc49c Mon Sep 17 00:00:00 2001 From: canewsin Date: Wed, 12 Jul 2023 18:22:16 +0530 Subject: [PATCH 189/191] Upgrade GH runner to 20.04 --- .github/workflows/tests.yml | 71 ++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6eaf3c6b..2bdcaf95 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,49 +4,48 @@ on: [push, pull_request] jobs: test: - - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 strategy: max-parallel: 16 matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: ["3.7", "3.8", "3.9"] steps: - - name: Checkout ZeroNet - uses: actions/checkout@v2 - with: - submodules: 'true' + - name: Checkout ZeroNet + uses: actions/checkout@v2 + with: + submodules: "true" - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} - - name: Prepare for installation - run: | - python3 -m pip install setuptools - python3 -m pip install --upgrade pip wheel - python3 -m pip install --upgrade codecov coveralls flake8 mock pytest==4.6.3 pytest-cov selenium + - name: Prepare for installation + run: | + python3 -m pip install setuptools + python3 -m pip install --upgrade pip wheel + python3 -m pip install --upgrade codecov coveralls flake8 mock pytest==4.6.3 pytest-cov selenium - - name: Install - run: | - python3 -m pip install --upgrade -r requirements.txt - python3 -m pip list + - name: Install + run: | + python3 -m pip install --upgrade -r requirements.txt + python3 -m pip list - - name: Prepare for tests - run: | - openssl version -a - echo 0 | sudo tee /proc/sys/net/ipv6/conf/all/disable_ipv6 + - name: Prepare for tests + run: | + openssl version -a + echo 0 | sudo tee /proc/sys/net/ipv6/conf/all/disable_ipv6 - - name: Test - run: | - catchsegv python3 -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini - export ZERONET_LOG_DIR="log/CryptMessage"; catchsegv python3 -m pytest -x plugins/CryptMessage/Test - export ZERONET_LOG_DIR="log/Bigfile"; catchsegv python3 -m pytest -x plugins/Bigfile/Test - export ZERONET_LOG_DIR="log/AnnounceLocal"; catchsegv python3 -m pytest -x plugins/AnnounceLocal/Test - export ZERONET_LOG_DIR="log/OptionalManager"; catchsegv python3 -m pytest -x plugins/OptionalManager/Test - export ZERONET_LOG_DIR="log/Multiuser"; mv plugins/disabled-Multiuser plugins/Multiuser && catchsegv python -m pytest -x plugins/Multiuser/Test - export ZERONET_LOG_DIR="log/Bootstrapper"; mv plugins/disabled-Bootstrapper plugins/Bootstrapper && catchsegv python -m pytest -x plugins/Bootstrapper/Test - find src -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')" - find plugins -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')" - flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pyaes/ + - name: Test + run: | + catchsegv python3 -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini + export ZERONET_LOG_DIR="log/CryptMessage"; catchsegv python3 -m pytest -x plugins/CryptMessage/Test + export ZERONET_LOG_DIR="log/Bigfile"; catchsegv python3 -m pytest -x plugins/Bigfile/Test + export ZERONET_LOG_DIR="log/AnnounceLocal"; catchsegv python3 -m pytest -x plugins/AnnounceLocal/Test + export ZERONET_LOG_DIR="log/OptionalManager"; catchsegv python3 -m pytest -x plugins/OptionalManager/Test + export ZERONET_LOG_DIR="log/Multiuser"; mv plugins/disabled-Multiuser plugins/Multiuser && catchsegv python -m pytest -x plugins/Multiuser/Test + export ZERONET_LOG_DIR="log/Bootstrapper"; mv plugins/disabled-Bootstrapper plugins/Bootstrapper && catchsegv python -m pytest -x plugins/Bootstrapper/Test + find src -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')" + find plugins -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')" + flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pyaes/ From 290025958f959609f96c02bbd422ae1741d0c6a1 Mon Sep 17 00:00:00 2001 From: canewsin Date: Wed, 12 Jul 2023 18:28:32 +0530 Subject: [PATCH 190/191] v0.9.0(4630) --- CHANGELOG.md | 9 +++++++++ src/Config.py | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4365925..6974d18a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +### ZeroNet 0.9.0 (2023-07-12) Rev4630 + - Fix RDos Issue in Plugins https://github.com/ZeroNetX/ZeroNet-Plugins/pull/9 + - Add trackers to Config.py for failsafety incase missing trackers.txt + - Added Proxy links + - Fix pysha3 dep installation issue + - FileRequest -> Remove Unnecessary check, Fix error wording + - Fix Response when site is missing for `actionAs` + + ### ZeroNet 0.8.5 (2023-02-12) Rev4625 - Fix(https://github.com/ZeroNetX/ZeroNet/pull/202) for SSL cert gen failed on Windows. - default theme-class for missing value in `users.json`. diff --git a/src/Config.py b/src/Config.py index 0c6adca8..a9208d55 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,8 +13,8 @@ import time class Config(object): def __init__(self, argv): - self.version = "0.8.6" - self.rev = 4626 + self.version = "0.9.0" + self.rev = 4630 self.argv = argv self.action = None self.test_parser = None From 7edbda70f50de27e38f48e08a4aa3ebf3475731f Mon Sep 17 00:00:00 2001 From: Merith-TK Date: Sun, 20 Apr 2025 15:59:40 -0700 Subject: [PATCH 191/191] apply build template --- .forgejo/workflows/build-on-commit.yml | 40 ++++++++++++++++++++++++++ .forgejo/workflows/build-on-tag.yml | 37 ++++++++++++++++++++++++ .gitignore | 1 + 3 files changed, 78 insertions(+) create mode 100644 .forgejo/workflows/build-on-commit.yml create mode 100644 .forgejo/workflows/build-on-tag.yml diff --git a/.forgejo/workflows/build-on-commit.yml b/.forgejo/workflows/build-on-commit.yml new file mode 100644 index 00000000..e8f0d2e3 --- /dev/null +++ b/.forgejo/workflows/build-on-commit.yml @@ -0,0 +1,40 @@ +name: Build Docker Image on Commit + +on: + push: + branches: + - main + tags: + - '!' # Exclude tags + +jobs: + build-and-publish: + runs-on: docker-builder + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set REPO_VARS + id: repo-url + run: | + echo "REPO_HOST=$(echo "${{ github.server_url }}" | sed 's~http[s]*://~~g')" >> $GITHUB_ENV + echo "REPO_PATH=${{ github.repository }}" >> $GITHUB_ENV + + - name: Login to OCI registry + run: | + echo "${{ secrets.OCI_TOKEN }}" | docker login $REPO_HOST -u "${{ secrets.OCI_USER }}" --password-stdin + + - name: Build and push Docker images + run: | + # Build Docker image with commit SHA + docker build -t $REPO_HOST/$REPO_PATH:${{ github.sha }} . + docker push $REPO_HOST/$REPO_PATH:${{ github.sha }} + + # Build Docker image with nightly tag + docker tag $REPO_HOST/$REPO_PATH:${{ github.sha }} $REPO_HOST/$REPO_PATH:nightly + docker push $REPO_HOST/$REPO_PATH:nightly + + # Remove local images to save storage + docker rmi $REPO_HOST/$REPO_PATH:${{ github.sha }} + docker rmi $REPO_HOST/$REPO_PATH:nightly diff --git a/.forgejo/workflows/build-on-tag.yml b/.forgejo/workflows/build-on-tag.yml new file mode 100644 index 00000000..888102b6 --- /dev/null +++ b/.forgejo/workflows/build-on-tag.yml @@ -0,0 +1,37 @@ +name: Build and Publish Docker Image on Tag + +on: + push: + tags: + - '*' + +jobs: + build-and-publish: + runs-on: docker-builder + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set REPO_VARS + id: repo-url + run: | + echo "REPO_HOST=$(echo "${{ github.server_url }}" | sed 's~http[s]*://~~g')" >> $GITHUB_ENV + echo "REPO_PATH=${{ github.repository }}" >> $GITHUB_ENV + + - name: Login to OCI registry + run: | + echo "${{ secrets.OCI_TOKEN }}" | docker login $REPO_HOST -u "${{ secrets.OCI_USER }}" --password-stdin + + - name: Build and push Docker image + run: | + TAG=${{ github.ref_name }} # Get the tag name from the context + # Build and push multi-platform Docker images + docker build -t $REPO_HOST/$REPO_PATH:$TAG --push . + # Tag and push latest + docker tag $REPO_HOST/$REPO_PATH:$TAG $REPO_HOST/$REPO_PATH:latest + docker push $REPO_HOST/$REPO_PATH:latest + + # Remove the local image to save storage + docker rmi $REPO_HOST/$REPO_PATH:$TAG + docker rmi $REPO_HOST/$REPO_PATH:latest \ No newline at end of file diff --git a/.gitignore b/.gitignore index 38dd3a34..636cd115 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ __pycache__/ # Hidden files .* +!/.forgejo !/.github !/.gitignore !/.travis.yml
      id type ip open crypt pingbuff bad idle open delay cpu out in last sentwait version time sites