From 270eb677c772bbf895901b6f23d9fcc80f4ae24c Mon Sep 17 00:00:00 2001 From: mishfit Date: Fri, 21 Apr 2017 03:26:17 -0600 Subject: [PATCH 0001/2570] allow empty file query filters --- src/Ui/UiWebsocket.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 35b8ce1c..232a94eb 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -106,7 +106,7 @@ class UiWebsocket(object): except Exception, err: if config.debug: # Allow websocket errors to appear on /Debug sys.modules["main"].DebugHook.handleError() - self.log.error("WebSocket handleRequest error: %s" % Debug.formatException(err)) + self.log.error("WebSocket handleRequest error: %s \n %s" % (Debug.formatException(err), message)) self.cmd("error", "Internal error: %s" % Debug.formatException(err, "html")) # Has permission to run the command @@ -516,10 +516,10 @@ class UiWebsocket(object): ws.event("siteChanged", self.site, {"event": ["file_deleted", inner_path]}) # Find data in json files - def actionFileQuery(self, to, dir_inner_path, query): + def actionFileQuery(self, to, dir_inner_path, query=None): # s = time.time() dir_path = self.site.storage.getPath(dir_inner_path) - rows = list(QueryJson.query(dir_path, query)) + rows = list(QueryJson.query(dir_path, query or "")) # self.log.debug("FileQuery %s %s done in %s" % (dir_inner_path, query, time.time()-s)) return self.response(to, rows) From e42631932b6decd6bc476e45761d863168790756 Mon Sep 17 00:00:00 2001 From: lmath Date: Sat, 6 May 2017 08:13:26 +0800 Subject: [PATCH 0002/2570] Fix Sidebar button for touch device & CHANGELOG-zh-cn & FreeBSD installation instructions (#916) * Disallow change optional limit on public proxy * Add Chinese for Mute plugins * add FreeBSD installation instructions to README-zh-cn.md * Fix Sidebar button for touch device * Add CHANGELOG-zh-cn.md * Why merging junk left? Remove it! --- CHANGELOG-zh-cn.md | 134 +++++++++++++++++++++++++++ README-zh-cn.md | 6 ++ plugins/Mute/languages/zh-tw.json | 2 +- plugins/Mute/languages/zh.json | 2 +- plugins/Sidebar/media/Sidebar.coffee | 36 +++---- plugins/Sidebar/media/all.js | 36 +++---- 6 files changed, 178 insertions(+), 38 deletions(-) create mode 100644 CHANGELOG-zh-cn.md diff --git a/CHANGELOG-zh-cn.md b/CHANGELOG-zh-cn.md new file mode 100644 index 00000000..c09ca401 --- /dev/null +++ b/CHANGELOG-zh-cn.md @@ -0,0 +1,134 @@ +## ZeroNet 0.5.1 (2016-11-18) +### 新增 +- 多语言界面 +- 新插件:为站点 HTML 与 JS 文件提供的翻译助手 +- 每个站点独立的 favicon + +### 修复 +- 并行可选文件下载 + +## ZeroNet 0.5.0 (2016-11-08) +### 新增 +- 新插件:允许在 ZeroHello 列出/删除/固定/管理文件 +- 新的 API 命令来关注用户的可选文件,与可选文件的请求统计 +- 新的可选文件总大小限制 +- 新插件:保存节点到数据库并在重启时保持它们,使得更快的可选文件搜索以及在没有 Tracker 的情况下工作 +- 重写 UPnP 端口打开器 + 退出时关闭端口(感谢 sirMackk!) +- 通过懒惰 PeerHashfield 创建来减少内存占用 +- 在 /Stats 页面加载 JSON 文件统计与数据库信息 + +### 更改 +- 独立的锁定文件来获得更好的 Windows 兼容性 +- 当执行 start.py 时,即使 ZeroNet 已经运行也打开浏览器 +- 在重载时保持插件顺序来允许插件扩展另一个插件 +- 只在完整加载 sites.json 时保存来避免数据丢失 +- 将更多的 Tracker 更改为更可靠的 Tracker +- 更少的 findhashid CPU 使用率 +- 合并下载大量可选文件 +- 更多对于可选文件的其他优化 +- 如果一个站点有 1000 个节点,更积极地清理 +- 为验证错误使用警告而不是错误 +- 首先推送更新到更新的客户端 +- 损坏文件重置改进 + +### 修复 +- 修复启动时出现的站点删除错误 +- 延迟 WebSocket 消息直到连接上 +- 修复如果文件包含额外数据时的数据库导入 +- 修复大站点下载 +- 修复 diff 发送 bug (跟踪它好长时间了) +- 修复当 JSON 文件包含 [] 字符时随机出现的发布错误 +- 修复 siteDelete 与 siteCreate bug +- 修复文件写入确认对话框 + + +## ZeroNet 0.4.1 (2016-09-05) +### 新增 +- 更快启动与更少内存使用的内核改变 +- 尝试连接丢失时重新连接 Tor +- 侧边栏滑入 +- 尝试避免不完整的数据文件被覆盖 +- 更快地打开数据库 +- 在侧边栏显示用户文件大小 +- 依赖 --connection_limit 的并发 worker 数量 + + +### 更改 +- 在空闲 5 分钟后关闭数据库 +- 更好的站点大小计算 +- 允许在域名中使用“-”符号 +- 总是尝试为站点保持连接 +- 移除已合并站点的合并权限 +- 只扫描最后 3 天的新闻源来加快数据库请求 +- 更新 ZeroBundle-win 到 Python 2.7.12 + + +### 修复 +- 修复重要的安全问题:允许任意用户无需有效的来自 ID 提供者的证书发布新内容,感谢 Kaffie 指出 +- 修复在没有选择提供证书提供者时的侧边栏错误 +- 在数据库重建时跳过无效文件 +- 修复随机弹出的 WebSocket 连接错误 +- 修复新的 siteCreate 命令 +- 修复站点大小计算 +- 修复计算机唤醒后的端口打开检查 +- 修复 --size_limit 的命令行解析 + + +## ZeroNet 0.4.0 (2016-08-11) +### 新增 +- 合并站点插件 +- Live source code reloading: Faster core development by allowing me to make changes in ZeroNet source code without restarting it. +- 为合并站点设计的新 JSON 表 +- 从侧边栏重建数据库 +- 允许直接在 JSON 表中存储自定义数据:更简单与快速的 SQL 查询 +- 用户文件存档:允许站点拥有者存档不活跃的用户内容到单个文件(减少初始同步的时间/CPU/内存使用率) +- 在文件删除时同时触发数据库 onUpdated/update +- 从 ZeroFrame API 请求权限 +- 允许使用 fileWrite API 命令在 content.json 存储额外数据 +- 更快的可选文件下载 +- 使用替代源 (Gogs, Gitlab) 来下载更新 +- Track provided sites/connection and prefer to keep the ones with more sites to reduce connection number + +### 更改 +- 保持每个站点至少 5 个连接 +- 将目标站点连接从 10 更改到 15 +- ZeroHello 搜索功能稳定性/速度改进 +- 提升机械硬盘下的客户端性能 + +### 修复 +- 修复 IE11 wrapper nonce 错误 +- 修复在移动设备上的侧边栏 +- 修复站点大小计算 +- 修复 IE10 兼容性 +- Windows XP ZeroBundle 兼容性(感谢中国人民) + + +## ZeroNet 0.3.7 (2016-05-27) +### 更改 +- 通过只传输补丁来减少带宽使用 +- 其他 CPU /内存优化 + + +## ZeroNet 0.3.6 (2016-05-27) +### 新增 +- 新的 ZeroHello +- Newsfeed 函数 + +### 修复 +- 安全性修复 + + +## ZeroNet 0.3.5 (2016-02-02) +### 新增 +- 带有 .onion 隐藏服务的完整 Tor 支持 +- 使用 ZeroNet 协议的 Bootstrap + +### 修复 +- 修复 Gevent 1.0.2 兼容性 + + +## ZeroNet 0.3.4 (2015-12-28) +### 新增 +- AES, ECIES API 函数支持 +- PushState 与 ReplaceState URL 通过 API 的操作支持 +- 多用户 localstorage diff --git a/README-zh-cn.md b/README-zh-cn.md index 7bb1d307..0abff7a8 100644 --- a/README-zh-cn.md +++ b/README-zh-cn.md @@ -85,6 +85,12 @@ * 执行 `python zeronet.py` 来启动 * 在你的浏览器中打开 http://127.0.0.1:43110/ +### [FreeBSD](https://www.freebsd.org/) + +* `pkg install zeronet` 或者 `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/) diff --git a/plugins/Mute/languages/zh-tw.json b/plugins/Mute/languages/zh-tw.json index b8590e38..3bdb5fc0 100644 --- a/plugins/Mute/languages/zh-tw.json +++ b/plugins/Mute/languages/zh-tw.json @@ -3,4 +3,4 @@ "Mute": "屏蔽", "Unmute %s?": "對 %s 解除屏蔽?", "Unmute": "解除屏蔽" -} \ No newline at end of file +} diff --git a/plugins/Mute/languages/zh.json b/plugins/Mute/languages/zh.json index 5c48a7d8..beea7e77 100644 --- a/plugins/Mute/languages/zh.json +++ b/plugins/Mute/languages/zh.json @@ -3,4 +3,4 @@ "Mute": "屏蔽", "Unmute %s?": "对 %s 解除屏蔽?", "Unmute": "解除屏蔽" -} \ No newline at end of file +} diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee index db0bfb5e..69abab69 100644 --- a/plugins/Sidebar/media/Sidebar.coffee +++ b/plugins/Sidebar/media/Sidebar.coffee @@ -42,7 +42,7 @@ class Sidebar extends Class e.preventDefault() # Disable previous listeners - @fixbutton.off "click touchstop touchcancel" + @fixbutton.off "click touchend touchcancel" @fixbutton.off "mousemove touchmove" # Make sure its not a click @@ -54,7 +54,7 @@ class Sidebar extends Class @fixbutton_addx = @fixbutton.offset().left-mousex @startDrag() - @fixbutton.parent().on "click touchstop touchcancel", (e) => + @fixbutton.parent().on "click touchend touchcancel", (e) => @stopDrag() @resized() $(window).on "resize", @resized @@ -96,7 +96,7 @@ class Sidebar extends Class @fixbutton.parents().on "mousemove touchmove" ,@waitMove # Stop dragging listener - @fixbutton.parents().on "mouseup touchstop touchend touchcancel", (e) => + @fixbutton.parents().on "mouseup touchend touchend touchcancel", (e) => e.preventDefault() @stopDrag() @@ -253,13 +253,13 @@ class Sidebar extends Class @scrollable() # Re-calculate height when site admin opened or closed - @tag.find("#checkbox-owned").off("click").on "click", => + @tag.find("#checkbox-owned").off("click touchend").on "click touchend", => setTimeout (=> @scrollable() ), 300 # Site limit button - @tag.find("#button-sitelimit").off("click").on "click", => + @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 @@ -267,14 +267,14 @@ class Sidebar extends Class return false # Database reload - @tag.find("#button-dbreload").off("click").on "click", => + @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").on "click", => + @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 @@ -282,7 +282,7 @@ class Sidebar extends Class return false # Update site - @tag.find("#button-update").off("click").on "click", => + @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 @@ -290,19 +290,19 @@ class Sidebar extends Class return false # Pause site - @tag.find("#button-pause").off("click").on "click", => + @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").on "click", => + @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").on "click", => + @tag.find("#button-delete").off("click touchend").on "click touchend", => wrapper.displayConfirm "Are you sure?", "Delete this site", => @tag.find("#button-delete").addClass("loading") wrapper.ws.cmd "siteDelete", wrapper.site_info.address, -> @@ -310,24 +310,24 @@ class Sidebar extends Class return false # Owned checkbox - @tag.find("#checkbox-owned").off("click").on "click", => + @tag.find("#checkbox-owned").off("click touchend").on "click touchend", => wrapper.ws.cmd "siteSetOwned", [@tag.find("#checkbox-owned").is(":checked")] # Owned checkbox - @tag.find("#checkbox-autodownloadoptional").off("click").on "click", => + @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").on "click", => + @tag.find("#button-identity").off("click touchend").on "click touchend", => wrapper.ws.cmd "certSelect" return false # Owned checkbox - @tag.find("#checkbox-owned").off("click").on "click", => + @tag.find("#checkbox-owned").off("click touchend").on "click touchend", => wrapper.ws.cmd "siteSetOwned", [@tag.find("#checkbox-owned").is(":checked")] # Save settings - @tag.find("#button-settings").off("click").on "click", => + @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() @@ -342,7 +342,7 @@ class Sidebar extends Class return false # Sign content.json - @tag.find("#button-sign").off("click").on "click", => + @tag.find("#button-sign").off("click touchend").on "click touchend", => inner_path = @tag.find("#input-contents").val() if wrapper.site_info.privatekey @@ -360,7 +360,7 @@ class Sidebar extends Class return false # Publish content.json - @tag.find("#button-publish").off("click").on "click", => + @tag.find("#button-publish").off("click touchend").on "click touchend", => inner_path = @tag.find("#input-contents").val() @tag.find("#button-publish").addClass "loading" wrapper.ws.cmd "sitePublish", {"inner_path": inner_path, "sign": false}, => diff --git a/plugins/Sidebar/media/all.js b/plugins/Sidebar/media/all.js index bf05eb49..bed431c2 100644 --- a/plugins/Sidebar/media/all.js +++ b/plugins/Sidebar/media/all.js @@ -241,7 +241,7 @@ window.initScrollable = function () { return; } e.preventDefault(); - _this.fixbutton.off("click touchstop touchcancel"); + _this.fixbutton.off("click touchend touchcancel"); _this.fixbutton.off("mousemove touchmove"); _this.dragStarted = +(new Date); return _this.fixbutton.one("mousemove touchmove", function(e) { @@ -255,7 +255,7 @@ window.initScrollable = function () { }); }; })(this)); - this.fixbutton.parent().on("click touchstop touchcancel", (function(_this) { + this.fixbutton.parent().on("click touchend touchcancel", (function(_this) { return function(e) { return _this.stopDrag(); }; @@ -297,7 +297,7 @@ window.initScrollable = function () { })(this)); this.fixbutton.parents().on("mousemove touchmove", this.animDrag); this.fixbutton.parents().on("mousemove touchmove", this.waitMove); - return this.fixbutton.parents().on("mouseup touchstop touchend touchcancel", (function(_this) { + return this.fixbutton.parents().on("mouseup touchend touchend touchcancel", (function(_this) { return function(e) { e.preventDefault(); return _this.stopDrag(); @@ -473,14 +473,14 @@ window.initScrollable = function () { Sidebar.prototype.onOpened = function() { this.log("Opened"); this.scrollable(); - this.tag.find("#checkbox-owned").off("click").on("click", (function(_this) { + this.tag.find("#checkbox-owned").off("click touchend").on("click touchend", (function(_this) { return function() { return setTimeout((function() { return _this.scrollable(); }), 300); }; })(this)); - this.tag.find("#button-sitelimit").off("click").on("click", (function(_this) { + this.tag.find("#button-sitelimit").off("click touchend").on("click touchend", (function(_this) { return function() { wrapper.ws.cmd("siteSetLimit", $("#input-sitelimit").val(), function(res) { if (res === "ok") { @@ -491,7 +491,7 @@ window.initScrollable = function () { return false; }; })(this)); - this.tag.find("#button-dbreload").off("click").on("click", (function(_this) { + this.tag.find("#button-dbreload").off("click touchend").on("click touchend", (function(_this) { return function() { wrapper.ws.cmd("dbReload", [], function() { wrapper.notifications.add("done-dbreload", "done", "Database schema reloaded!", 5000); @@ -500,7 +500,7 @@ window.initScrollable = function () { return false; }; })(this)); - this.tag.find("#button-dbrebuild").off("click").on("click", (function(_this) { + this.tag.find("#button-dbrebuild").off("click touchend").on("click touchend", (function(_this) { return function() { wrapper.notifications.add("done-dbrebuild", "info", "Database rebuilding...."); wrapper.ws.cmd("dbRebuild", [], function() { @@ -510,7 +510,7 @@ window.initScrollable = function () { return false; }; })(this)); - this.tag.find("#button-update").off("click").on("click", (function(_this) { + this.tag.find("#button-update").off("click touchend").on("click touchend", (function(_this) { return function() { _this.tag.find("#button-update").addClass("loading"); wrapper.ws.cmd("siteUpdate", wrapper.site_info.address, function() { @@ -520,21 +520,21 @@ window.initScrollable = function () { return false; }; })(this)); - this.tag.find("#button-pause").off("click").on("click", (function(_this) { + this.tag.find("#button-pause").off("click touchend").on("click touchend", (function(_this) { return function() { _this.tag.find("#button-pause").addClass("hidden"); wrapper.ws.cmd("sitePause", wrapper.site_info.address); return false; }; })(this)); - this.tag.find("#button-resume").off("click").on("click", (function(_this) { + this.tag.find("#button-resume").off("click touchend").on("click touchend", (function(_this) { return function() { _this.tag.find("#button-resume").addClass("hidden"); wrapper.ws.cmd("siteResume", wrapper.site_info.address); return false; }; })(this)); - this.tag.find("#button-delete").off("click").on("click", (function(_this) { + this.tag.find("#button-delete").off("click touchend").on("click touchend", (function(_this) { return function() { wrapper.displayConfirm("Are you sure?", "Delete this site", function() { _this.tag.find("#button-delete").addClass("loading"); @@ -545,28 +545,28 @@ window.initScrollable = function () { return false; }; })(this)); - this.tag.find("#checkbox-owned").off("click").on("click", (function(_this) { + this.tag.find("#checkbox-owned").off("click touchend").on("click touchend", (function(_this) { return function() { return wrapper.ws.cmd("siteSetOwned", [_this.tag.find("#checkbox-owned").is(":checked")]); }; })(this)); - this.tag.find("#checkbox-autodownloadoptional").off("click").on("click", (function(_this) { + this.tag.find("#checkbox-autodownloadoptional").off("click touchend").on("click touchend", (function(_this) { return function() { return wrapper.ws.cmd("siteSetAutodownloadoptional", [_this.tag.find("#checkbox-autodownloadoptional").is(":checked")]); }; })(this)); - this.tag.find("#button-identity").off("click").on("click", (function(_this) { + this.tag.find("#button-identity").off("click touchend").on("click touchend", (function(_this) { return function() { wrapper.ws.cmd("certSelect"); return false; }; })(this)); - this.tag.find("#checkbox-owned").off("click").on("click", (function(_this) { + this.tag.find("#checkbox-owned").off("click touchend").on("click touchend", (function(_this) { return function() { return wrapper.ws.cmd("siteSetOwned", [_this.tag.find("#checkbox-owned").is(":checked")]); }; })(this)); - this.tag.find("#button-settings").off("click").on("click", (function(_this) { + this.tag.find("#button-settings").off("click touchend").on("click touchend", (function(_this) { return function() { wrapper.ws.cmd("fileGet", "content.json", function(res) { var data, json_raw; @@ -586,7 +586,7 @@ window.initScrollable = function () { return false; }; })(this)); - this.tag.find("#button-sign").off("click").on("click", (function(_this) { + this.tag.find("#button-sign").off("click touchend").on("click touchend", (function(_this) { return function() { var inner_path; inner_path = _this.tag.find("#input-contents").val(); @@ -614,7 +614,7 @@ window.initScrollable = function () { return false; }; })(this)); - this.tag.find("#button-publish").off("click").on("click", (function(_this) { + this.tag.find("#button-publish").off("click touchend").on("click touchend", (function(_this) { return function() { var inner_path; inner_path = _this.tag.find("#input-contents").val(); From 34a6337c014f747fc28ea3022e1754fce852a277 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 6 May 2017 17:27:49 +0200 Subject: [PATCH 0003/2570] Rev2055, Don't add ? to url if start with # using replaceState and pushState --- src/Config.py | 2 +- src/Ui/media/Wrapper.coffee | 4 +++- src/Ui/media/all.js | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Config.py b/src/Config.py index 3a196e86..051181a1 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.4" - self.rev = 2054 + self.rev = 2055 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index d755b108..cda9cc2d 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -149,7 +149,9 @@ class Wrapper back = window.location.pathname if back.match /^\/[^\/]+$/ # Add / after site address if called without it back += "/" - if query.replace("?", "") + if query.startsWith("#") + back = query + else if query.replace("?", "") back += "?"+query.replace("?", "") return back diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index 7d52fc8e..4ff5b41b 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -1005,7 +1005,9 @@ jQuery.extend( jQuery.easing, if (back.match(/^\/[^\/]+$/)) { back += "/"; } - if (query.replace("?", "")) { + if (query.startsWith("#")) { + back = query; + } else if (query.replace("?", "")) { back += "?" + query.replace("?", ""); } return back; From d346a532ffc2b9dcabca423ea84cd0c619e206f0 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 6 May 2017 18:08:32 +0200 Subject: [PATCH 0004/2570] Rev2056, Add local client to sidebar peer numbers --- plugins/Sidebar/SidebarPlugin.py | 9 +++++++++ src/Config.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index 97d8f521..c0a96494 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -70,6 +70,15 @@ class UiWebsocketPlugin(object): connectable = len([peer_id for peer_id in site.peers.keys() if not peer_id.endswith(":0")]) onion = len([peer_id for peer_id in site.peers.keys() if ".onion" in peer_id]) peers_total = len(site.peers) + + # Add myself + if site.settings["serving"]: + peers_total += 1 + if site.connection_server.port_opened: + 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 diff --git a/src/Config.py b/src/Config.py index 051181a1..072af068 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.4" - self.rev = 2055 + self.rev = 2056 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 728e497057d3bc66f190124eefc5169dc0a39b88 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 7 May 2017 18:14:13 +0200 Subject: [PATCH 0005/2570] Display database rebuilding progressbar --- plugins/MergerSite/MergerSitePlugin.py | 4 ++-- src/Site/Site.py | 7 +++++++ src/Site/SiteStorage.py | 20 ++++++++++++++++---- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/plugins/MergerSite/MergerSitePlugin.py b/plugins/MergerSite/MergerSitePlugin.py index af1a8ff5..0fe83448 100644 --- a/plugins/MergerSite/MergerSitePlugin.py +++ b/plugins/MergerSite/MergerSitePlugin.py @@ -230,7 +230,7 @@ class SiteStoragePlugin(object): # 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.open(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 @@ -242,7 +242,7 @@ class SiteStoragePlugin(object): 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.open(file_inner_path) + yield merged_inner_path, merged_site.storage.getPath(file_inner_path) else: merged_site.log.error("[MISSING] %s" % file_inner_path) diff --git a/src/Site/Site.py b/src/Site/Site.py index 54f13794..147d7e72 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -1064,6 +1064,13 @@ class Site(object): for ws in self.websockets: ws.event("siteChanged", self, param) + def messageWebsocket(self, message, type="info", progress=None): + for ws in self.websockets: + if progress is None: + ws.cmd("notification", [type, message]) + else: + ws.cmd("progress", [type, message, progress]) + # File download started @util.Noparallel(blocking=False) def fileStarted(self): diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index fd9d27e5..bb0578df 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -13,6 +13,7 @@ from Debug import Debug from Config import config from util import helper from Plugin import PluginManager +from Translate import translate as _ @PluginManager.acceptPlugins @@ -78,7 +79,7 @@ class SiteStorage(object): for content_inner_path, content in self.site.content_manager.contents.iteritems(): # content.json file itself if self.isFile(content_inner_path): - yield content_inner_path, self.open(content_inner_path) + yield content_inner_path, self.getPath(content_inner_path) else: self.log.error("[MISSING] %s" % content_inner_path) # Data files in content.json @@ -89,7 +90,7 @@ class SiteStorage(object): 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 self.isFile(file_inner_path): - yield file_inner_path, self.open(file_inner_path) + yield file_inner_path, self.getPath(file_inner_path) else: self.log.error("[MISSING] %s" % file_inner_path) @@ -120,16 +121,27 @@ class SiteStorage(object): cur.logging = False found = 0 s = time.time() + db_files = list(self.getDbFiles()) try: - for file_inner_path, file in self.getDbFiles(): + if len(db_files) > 100: + self.site.messageWebsocket(_["Database rebuilding...
Imported {0} of {1} files..."].format("0000", len(db_files)), "rebuild", 0) + for file_inner_path, file_path in db_files: try: - if self.updateDbFile(file_inner_path, file=file, cur=cur): + if self.updateDbFile(file_inner_path, file=open(file_path, "rb"), cur=cur): found += 1 except Exception, err: self.log.error("Error importing %s: %s" % (file_inner_path, Debug.formatException(err))) + if found and found % 100 == 0: + self.site.messageWebsocket( + _["Database rebuilding...
Imported {0} of {1} files..."].format(found, len(db_files)), + "rebuild", + int(float(found) / len(db_files) * 100) + ) finally: cur.execute("END") + if len(db_files) > 100: + self.site.messageWebsocket(_["Database rebuilding...
Imported {0} of {1} files..."].format(found, len(db_files)), "rebuild", 100) self.log.info("Imported %s data file in %ss" % (found, time.time() - s)) self.event_db_busy.set(True) # Event done, notify waiters self.event_db_busy = None # Clear event From 438bdbc141adba8c008fedead1bd78a469c5cb0e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 7 May 2017 18:15:23 +0200 Subject: [PATCH 0006/2570] Protect peers for low traffic sites from closing --- src/File/FileServer.py | 10 +++++++--- src/Site/Site.py | 6 ++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index 9e560224..23a4fc59 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -205,19 +205,21 @@ class FileServer(ConnectionServer): import gc startup = True time.sleep(5 * 60) # Sites already cleaned up on startup + peers_protected = set([]) while 1: # Sites health care every 20 min - self.log.debug("Running site cleanup, connections: %s, internet: %s" % (len(self.connections), self.has_internet)) + self.log.debug("Running site cleanup, connections: %s, internet: %s, protected peers: %s" % (len(self.connections), self.has_internet, peers_protected)) for address, site in self.sites.items(): if not site.settings["serving"]: continue if not startup: - site.cleanupPeers() + site.cleanupPeers(peers_protected) time.sleep(1) # Prevent too quick request + peers_protected = set([]) for address, site in self.sites.items(): if not site.settings["serving"]: continue @@ -231,7 +233,9 @@ class FileServer(ConnectionServer): site.retryBadFiles() if not startup: # Don't do it at start up because checkSite already has needConnections at start up. - site.needConnections(check_site_on_reconnect=True) # Keep active peer connection to get the updates + connected_num = site.needConnections(check_site_on_reconnect=True) # Keep active peer connection to get the updates + 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/Site/Site.py b/src/Site/Site.py index 147d7e72..35a81d98 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -900,9 +900,9 @@ class Site(object): connected += 1 # Successfully connected if connected >= need: break + 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: - self.log.debug("Connected before: %s, after: %s. We need to check the site." % (connected_before, connected)) gevent.spawn(self.update, check_files=False) return connected @@ -949,7 +949,7 @@ class Site(object): return back # Cleanup probably dead peers and close connection if too much - def cleanupPeers(self): + def cleanupPeers(self, peers_protected=[]): peers = self.peers.values() if len(peers) > 20: # Cleanup old peers @@ -982,6 +982,8 @@ class Site(object): for peer in sorted(connected_peers, key=lambda peer: min(peer.connection.sites, 5)): # Try to keep connections with more sites if not peer.connection: continue + if peer.key in peers_protected: + continue if peer.connection.sites > 5: break peer.connection.close("Cleanup peers") From 3f772d1e1e31c9895b514a7cff6b519557f4911c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 7 May 2017 18:19:08 +0200 Subject: [PATCH 0007/2570] Boost priority for essential files to allow concurrent requests --- src/Worker/WorkerManager.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index ae12e976..02f0387e 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -391,16 +391,16 @@ class WorkerManager(object): if "-default" in inner_path: return -4 # Default files are cloning not important elif inner_path.endswith(".css"): - return 7 # boost css files priority + return 17 # boost css files priority elif inner_path.endswith(".js"): - return 6 # boost js files priority + return 16 # boost js files priority elif inner_path.endswith("dbschema.json"): - return 5 # boost database specification + return 15 # boost database specification elif inner_path.endswith("content.json"): return 1 # boost included content.json files priority a bit elif inner_path.endswith(".json"): if len(inner_path) < 50: # Boost non-user json files more - return 4 + return 14 else: return 2 return 0 From db744f576e9035419eb7f10b7276d73e0bca4cba Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 7 May 2017 18:19:24 +0200 Subject: [PATCH 0008/2570] Rev2058 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 072af068..efaa7991 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.4" - self.rev = 2056 + self.rev = 2058 self.argv = argv self.action = None self.config_file = "zeronet.conf" From b8fb53ba2027a99285fc68015293263eb2376b29 Mon Sep 17 00:00:00 2001 From: probonopd Date: Sun, 7 May 2017 19:51:53 +0200 Subject: [PATCH 0009/2570] Allow running from usr/share/zeronet --- src/Config.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Config.py b/src/Config.py index efaa7991..e3bd6216 100644 --- a/src/Config.py +++ b/src/Config.py @@ -80,6 +80,12 @@ class Config(object): config_file = start_dir + "/zeronet.conf" data_dir = start_dir + "/data" log_dir = start_dir + "/log" + elif this_file.endswith("usr/share/zeronet/src/Config.py"): + # Running from non-writeable location, e.g., AppImage + start_dir = os.path.expanduser("~/ZeroNet").decode(sys.getfilesystemencoding()) + config_file = start_dir + "/zeronet.conf" + data_dir = start_dir + "/data" + log_dir = start_dir + "/log" else: config_file = "zeronet.conf" data_dir = "data" From f67cb7b145ad61676acd44e43064d93f32627573 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 7 May 2017 21:21:26 +0200 Subject: [PATCH 0010/2570] Rev2059, Fix download test, boost priority on request by 15, Adjust default priorities --- src/Config.py | 2 +- src/Test/TestSiteDownload.py | 4 ++-- src/Ui/UiRequest.py | 2 +- src/Worker/WorkerManager.py | 10 +++++----- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Config.py b/src/Config.py index efaa7991..5da3993e 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.4" - self.rev = 2058 + self.rev = 2059 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Test/TestSiteDownload.py b/src/Test/TestSiteDownload.py index 146e835e..6fe0387f 100644 --- a/src/Test/TestSiteDownload.py +++ b/src/Test/TestSiteDownload.py @@ -35,8 +35,8 @@ class TestSiteDownload: def boostRequest(inner_path): # I really want these file if inner_path == "index.html": - site_temp.needFile("data/img/multiuser.png", priority=5, blocking=False) - site_temp.needFile("data/img/direct_domains.png", priority=5, blocking=False) + site_temp.needFile("data/img/multiuser.png", priority=15, blocking=False) + site_temp.needFile("data/img/direct_domains.png", priority=15, blocking=False) site_temp.onFileDone.append(boostRequest) site_temp.download(blind_includes=True).join(timeout=5) file_requests = [request[2]["inner_path"] for request in requests if request[0] in ("getFile", "streamFile")] diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 44fbd6af..32b1af7f 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -389,7 +389,7 @@ class UiRequest(object): if path_parts["inner_path"].endswith("favicon.ico"): # Default favicon for all sites return self.actionFile("src/Ui/media/img/favicon.ico") - result = site.needFile(path_parts["inner_path"], priority=5) # Wait until file downloads + result = site.needFile(path_parts["inner_path"], priority=15) # Wait until file downloads if result: return self.actionFile(file_path, header_length=header_length) else: diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 02f0387e..9aa2ce32 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -391,16 +391,16 @@ class WorkerManager(object): if "-default" in inner_path: return -4 # Default files are cloning not important elif inner_path.endswith(".css"): - return 17 # boost css files priority + return 13 # boost css files priority elif inner_path.endswith(".js"): - return 16 # boost js files priority + return 12 # boost js files priority elif inner_path.endswith("dbschema.json"): - return 15 # boost database specification + return 11 # boost database specification elif inner_path.endswith("content.json"): return 1 # boost included content.json files priority a bit elif inner_path.endswith(".json"): - if len(inner_path) < 50: # Boost non-user json files more - return 14 + if len(inner_path) < 50: # Boost non-user json files + return 10 else: return 2 return 0 From bf042ce7a317148a4f863c122d0169187408e6e4 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 7 May 2017 21:34:44 +0200 Subject: [PATCH 0011/2570] Rev2060, Only boost all.js and all.css priority, really fix download priority test --- src/Config.py | 2 +- src/Test/TestSiteDownload.py | 6 +++--- src/Worker/WorkerManager.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Config.py b/src/Config.py index 1c111894..95f58106 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.4" - self.rev = 2059 + self.rev = 2060 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Test/TestSiteDownload.py b/src/Test/TestSiteDownload.py index 6fe0387f..fcd2922c 100644 --- a/src/Test/TestSiteDownload.py +++ b/src/Test/TestSiteDownload.py @@ -42,9 +42,9 @@ class TestSiteDownload: file_requests = [request[2]["inner_path"] for request in requests if request[0] in ("getFile", "streamFile")] # Test priority assert file_requests[0:2] == ["content.json", "index.html"] # Must-have files - assert file_requests[2:4] == ["css/all.css", "js/all.js"] # Important assets - assert file_requests[4] == "dbschema.json" # Database map - assert file_requests[5:7] == ["data/img/multiuser.png", "data/img/direct_domains.png"] # Directly requested files + assert file_requests[2:4] == ["data/img/multiuser.png", "data/img/direct_domains.png"] # Directly requested files + assert file_requests[4:6] == ["css/all.css", "js/all.js"] # Important assets + assert file_requests[6] == "dbschema.json" # Database map assert "-default" in file_requests[-1] # Put default files for cloning to the end # Check files diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 9aa2ce32..39407b40 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -390,9 +390,9 @@ class WorkerManager(object): return 9998 # index.html also important if "-default" in inner_path: return -4 # Default files are cloning not important - elif inner_path.endswith(".css"): + elif inner_path.endswith("all.css"): return 13 # boost css files priority - elif inner_path.endswith(".js"): + elif inner_path.endswith("all.js"): return 12 # boost js files priority elif inner_path.endswith("dbschema.json"): return 11 # boost database specification From 9ec050341bc5987c1cff47dfb7be4aacc75e7024 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 11 May 2017 17:53:29 +0200 Subject: [PATCH 0012/2570] Site blacklist to Mute plugin --- plugins/Mute/MutePlugin.py | 66 +++++++++++++++++++- plugins/Mute/media/blacklisted.html | 64 ++++++++++++++++++++ plugins/Mute/media/js/ZeroFrame.js | 93 +++++++++++++++++++++++++++++ 3 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 plugins/Mute/media/blacklisted.html create mode 100644 plugins/Mute/media/js/ZeroFrame.js diff --git a/plugins/Mute/MutePlugin.py b/plugins/Mute/MutePlugin.py index d16a97cd..fe5968f8 100644 --- a/plugins/Mute/MutePlugin.py +++ b/plugins/Mute/MutePlugin.py @@ -11,12 +11,16 @@ from util import helper if os.path.isfile("%s/mutes.json" % config.data_dir): try: - mutes = json.load(open("%s/mutes.json" % config.data_dir))["mutes"] + data = json.load(open("%s/mutes.json" % config.data_dir)) + mutes = data.get("mutes", {}) + site_blacklist = data.get("site_blacklist", {}) except Exception, err: mutes = {} + site_blacklist = {} else: - open("%s/mutes.json" % config.data_dir, "w").write('{"mutes": {}}') + open("%s/mutes.json" % config.data_dir, "w").write('{"mutes": {}, "site_blacklist": {}}') mutes = {} + site_blacklist = {} if "_" not in locals(): _ = Translate("plugins/Mute/languages/") @@ -81,8 +85,30 @@ class UiWebsocketPlugin(object): else: return self.response(to, {"error": "Only ADMIN sites can list mutes"}) + # Blacklist + def actionBlacklistAdd(self, to, site_address, reason=None): + if "ADMIN" not in self.getPermissions(to): + return self.response(to, {"error": "Forbidden, only admin sites can add to blacklist"}) + site_blacklist[site_address] = {"date_added": time.time(), "reason": reason} + self.saveMutes() + self.response(to, "ok") + + def actionBlacklistRemove(self, to, site_address): + if "ADMIN" not in self.getPermissions(to): + return self.response(to, {"error": "Forbidden, only admin sites can remove from blacklist"}) + del site_blacklist[site_address] + self.saveMutes() + self.response(to, "ok") + + def actionBlacklistList(self, to): + if "ADMIN" in self.getPermissions(to): + self.response(to, site_blacklist) + else: + return self.response(to, {"error": "Only ADMIN sites can list blacklists"}) + + # Write mutes and blacklist to json file def saveMutes(self): - helper.atomicWrite("%s/mutes.json" % config.data_dir, json.dumps({"mutes": mutes}, indent=2, sort_keys=True)) + helper.atomicWrite("%s/mutes.json" % config.data_dir, json.dumps({"mutes": mutes, "site_blacklist": site_blacklist}, indent=2, sort_keys=True)) @PluginManager.registerTo("SiteStorage") @@ -98,3 +124,37 @@ class SiteStoragePlugin(object): return False return super(SiteStoragePlugin, self).updateDbFile(inner_path, file=file, cur=cur) + + +@PluginManager.registerTo("UiRequest") +class UiRequestPlugin(object): + def actionWrapper(self, path, extra_headers=None): + match = re.match("/(?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.server.site_manager.isDomain(address): + address = self.server.site_manager.resolveDomain(address) + + if address in site_blacklist: + site = self.server.site_manager.get(config.homepage) + if not extra_headers: + extra_headers = [] + self.sendHeader(extra_headers=extra_headers[:]) + return iter([super(UiRequestPlugin, self).renderWrapper( + site, path, site.address + "//uimedia/plugins/mute/blacklisted.html?address=" + address, + "Blacklisted site", extra_headers, show_loadingscreen=False + )]) + else: + return super(UiRequestPlugin, self).actionWrapper(path, extra_headers) + + def actionUiMedia(self, path, *args, **kwargs): + if path.startswith("/uimedia/plugins/mute/"): + file_path = path.replace("/uimedia/plugins/mute/", "plugins/Mute/media/") + return self.actionFile(file_path) + else: + return super(UiRequestPlugin, self).actionUiMedia(path) diff --git a/plugins/Mute/media/blacklisted.html b/plugins/Mute/media/blacklisted.html new file mode 100644 index 00000000..5d2bf80c --- /dev/null +++ b/plugins/Mute/media/blacklisted.html @@ -0,0 +1,64 @@ + + + + + +
+

Site blocked

+

This site is on your blacklist:

+
+
Too much image
+
on 2015-01-25 12:32:11
+
+ +
+ + + + + + \ No newline at end of file diff --git a/plugins/Mute/media/js/ZeroFrame.js b/plugins/Mute/media/js/ZeroFrame.js new file mode 100644 index 00000000..f3e2214b --- /dev/null +++ b/plugins/Mute/media/js/ZeroFrame.js @@ -0,0 +1,93 @@ +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) + } + + 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') + } +} + From f183e756f30ee063ededbafe05c4933b8d43a0cc Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 11 May 2017 17:54:48 +0200 Subject: [PATCH 0013/2570] Multi button support for wrapperConfirm --- src/Ui/media/Wrapper.coffee | 25 ++++++++++-------- src/Ui/media/all.css | 2 ++ src/Ui/media/all.js | 51 ++++++++++++++++++++++--------------- 3 files changed, 48 insertions(+), 30 deletions(-) diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index cda9cc2d..5148834b 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -203,13 +203,15 @@ class Wrapper body = $(""+message.params[1]+"") @notifications.add("notification-#{message.id}", message.params[0], body, message.params[2]) - displayConfirm: (message, caption, cb) -> + displayConfirm: (message, captions, cb) -> body = $(""+message+"") - button = $("#{caption}") # Add confirm button - button.on "click", => - cb(true) - return false - body.append(button) + if captions not instanceof Array then captions = [captions] # Convert to list if necessary + for caption, i in captions + button = $("#{caption}") # Add confirm button + button.on "click", (e) => + cb(parseInt(e.currentTarget.dataset.value)) + return false + body.append(button) @notifications.add("notification-#{caption}", "ask", body) button.focus() @@ -219,8 +221,8 @@ class Wrapper actionConfirm: (message, cb=false) -> message.params = @toHtmlSafe(message.params) # Escape html if message.params[1] then caption = message.params[1] else caption = "ok" - @displayConfirm message.params[0], caption, => - @sendInner {"cmd": "response", "to": message.id, "result": "boom"} # Response to confirm + @displayConfirm message.params[0], caption, (res) => + @sendInner {"cmd": "response", "to": message.id, "result": res} # Response to confirm return false @@ -472,8 +474,11 @@ class Wrapper toHtmlSafe: (values) -> if values not instanceof Array then values = [values] # Convert to array if its not for value, i in values - value = String(value).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') # Escape - value = value.replace(/<([\/]{0,1}(br|b|u|i))>/g, "<$1>") # Unescape b, i, u, br tags + if value instanceof Array + value = @toHtmlSafe(value) + else + value = String(value).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') # Escape + value = value.replace(/<([\/]{0,1}(br|b|u|i))>/g, "<$1>") # Unescape b, i, u, br tags values[i] = value return values diff --git a/src/Ui/media/all.css b/src/Ui/media/all.css index e960a0b9..a315403a 100644 --- a/src/Ui/media/all.css +++ b/src/Ui/media/all.css @@ -25,6 +25,8 @@ a { color: black } 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.button-2 { background-color: transparent; border: 1px solid #EEE; color: #555 } +.button.button-2:hover { border: 1px solid #CCC; color: #000 } /* Fixbutton */ diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index 4ff5b41b..6280a746 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -888,7 +888,7 @@ jQuery.extend( jQuery.easing, } else if (cmd === "progress") { return this.actionProgress(message); } else if (cmd === "prompt") { - return this.displayPrompt(message.params[0], message.params[1], message.params[2], (function(_this) { + return this.displayPrompt(message.params[0], message.params[1], message.params[2], message.params[3], (function(_this) { return function(res) { return _this.ws.response(message.id, res); }; @@ -1086,17 +1086,23 @@ jQuery.extend( jQuery.easing, return this.notifications.add("notification-" + message.id, message.params[0], body, message.params[2]); }; - Wrapper.prototype.displayConfirm = function(message, caption, cb) { - var body, button; + Wrapper.prototype.displayConfirm = function(message, captions, cb) { + var body, button, caption, i, j, len; body = $("" + message + ""); - button = $("" + caption + ""); - button.on("click", (function(_this) { - return function() { - cb(true); - return false; - }; - })(this)); - body.append(button); + if (!(captions instanceof Array)) { + captions = [captions]; + } + for (i = j = 0, len = captions.length; j < len; i = ++j) { + caption = captions[i]; + button = $("" + caption + ""); + button.on("click", (function(_this) { + return function(e) { + cb(parseInt(e.currentTarget.dataset.value)); + return false; + }; + })(this)); + body.append(button); + } this.notifications.add("notification-" + caption, "ask", body); button.focus(); return $(".notification").scrollLeft(0); @@ -1114,21 +1120,21 @@ jQuery.extend( jQuery.easing, caption = "ok"; } return this.displayConfirm(message.params[0], caption, (function(_this) { - return function() { + return function(res) { _this.sendInner({ "cmd": "response", "to": message.id, - "result": "boom" + "result": res }); return false; }; })(this)); }; - Wrapper.prototype.displayPrompt = function(message, type, caption, cb) { + Wrapper.prototype.displayPrompt = function(message, type, caption, placeholder, cb) { var body, button, input; body = $("" + message + ""); - input = $(""); + input = $(""); input.on("keyup", (function(_this) { return function(e) { if (e.keyCode === 13) { @@ -1151,15 +1157,16 @@ jQuery.extend( jQuery.easing, }; Wrapper.prototype.actionPrompt = function(message) { - var caption, type; + var caption, placeholder, type; message.params = this.toHtmlSafe(message.params); if (message.params[1]) { type = message.params[1]; } else { type = "text"; } - caption = "OK"; - return this.displayPrompt(message.params[0], type, caption, (function(_this) { + caption = message.params[2] ? message.params[2] : "OK"; + placeholder = message.params[3]; + return this.displayPrompt(message.params[0], type, caption, placeholder, (function(_this) { return function(res) { return _this.sendInner({ "cmd": "response", @@ -1464,8 +1471,12 @@ jQuery.extend( jQuery.easing, } for (i = j = 0, len = values.length; j < len; i = ++j) { value = values[i]; - value = String(value).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); - value = value.replace(/<([\/]{0,1}(br|b|u|i))>/g, "<$1>"); + if (value instanceof Array) { + value = this.toHtmlSafe(value); + } else { + value = String(value).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + value = value.replace(/<([\/]{0,1}(br|b|u|i))>/g, "<$1>"); + } values[i] = value; } return values; From a9b9902e604c5dfee25cb3f5236a30a0a29cb4c6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 11 May 2017 17:55:09 +0200 Subject: [PATCH 0014/2570] Placeholder support for displayPrompt --- src/Ui/media/Wrapper.coffee | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index 5148834b..89538805 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -56,7 +56,7 @@ class Wrapper else if cmd == "progress" # Display notification @actionProgress(message) else if cmd == "prompt" # Prompt input - @displayPrompt message.params[0], message.params[1], message.params[2], (res) => + @displayPrompt message.params[0], message.params[1], message.params[2], message.params[3], (res) => @ws.response message.id, res else if cmd == "confirm" # Confirm action @displayConfirm message.params[0], message.params[1], (res) => @@ -226,10 +226,10 @@ class Wrapper return false - displayPrompt: (message, type, caption, cb) -> + displayPrompt: (message, type, caption, placeholder, cb) -> body = $(""+message+"") - input = $("") # Add input + input = $("") # Add input input.on "keyup", (e) => # Send on enter if e.keyCode == 13 button.trigger "click" # Response to confirm @@ -250,9 +250,10 @@ class Wrapper actionPrompt: (message) -> message.params = @toHtmlSafe(message.params) # Escape html if message.params[1] then type = message.params[1] else type = "text" - caption = "OK" + caption = if message.params[2] then message.params[2] else "OK" + placeholder = message.params[3] - @displayPrompt message.params[0], type, caption, (res) => + @displayPrompt message.params[0], type, caption, placeholder, (res) => @sendInner {"cmd": "response", "to": message.id, "result": res} # Response to confirm actionProgress: (message) -> From cc31081f1ce91d422554711dfccabdce160150b8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 11 May 2017 17:55:38 +0200 Subject: [PATCH 0015/2570] Different design for second button of wrapperConfirm --- src/Ui/media/Wrapper.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Ui/media/Wrapper.css b/src/Ui/media/Wrapper.css index 4b84d4b2..aa32b60c 100644 --- a/src/Ui/media/Wrapper.css +++ b/src/Ui/media/Wrapper.css @@ -20,6 +20,8 @@ a { color: black } 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.button-2 { background-color: transparent; border: 1px solid #EEE; color: #555 } +.button.button-2:hover { border: 1px solid #CCC; color: #000 } /* Fixbutton */ From 487f5cb8c2fd6fab905b1fa8718e48a20c02978e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 11 May 2017 17:56:01 +0200 Subject: [PATCH 0016/2570] Fix unicode url redirects --- src/Ui/UiRequest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 32b1af7f..a8fdf00f 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -183,7 +183,7 @@ class UiRequest(object): # Redirect to an url def actionRedirect(self, url): - self.start_response('301 Redirect', [('Location', url)]) + self.start_response('301 Redirect', [('Location', str(url))]) yield "Location changed: %s" % url def actionIndex(self): From b5d3995874d9c52b9ffbf6cdca2aa89ad35a86c4 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 11 May 2017 17:57:29 +0200 Subject: [PATCH 0017/2570] GetSiteUrl based on if it's proxy request --- src/Ui/UiRequest.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index a8fdf00f..044b93df 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -233,6 +233,12 @@ class UiRequest(object): return False def renderWrapper(self, site, path, inner_path, title, extra_headers): + def getSiteUrl(self, address): + if self.isProxyRequest(): + return "http://zero/" + address + else: + return "/" + address + file_inner_path = inner_path if not file_inner_path: file_inner_path = "index.html" # If inner path defaults to index.html From 545459be88382dd3dd08b40f74ab572c5a8e94c5 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 11 May 2017 17:59:12 +0200 Subject: [PATCH 0018/2570] RenderWrapper with parameters --- src/Ui/UiRequest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 044b93df..bc02f44f 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -248,7 +248,7 @@ class UiRequest(object): address = re.sub("/.*", "", path.lstrip("/")) if self.isProxyRequest() and (not path or "/" in path[1:]): - file_url = re.sub(".*/", "", inner_path) + file_url = re.sub("^.*?/", "", inner_path) if self.env["HTTP_HOST"] == "zero": root_url = "/" + address + "/" else: @@ -268,6 +268,8 @@ class UiRequest(object): if self.env.get("QUERY_STRING"): query_string = "?%s&wrapper_nonce=%s" % (self.env["QUERY_STRING"], wrapper_nonce) + elif "?" in inner_path: + query_string = "&wrapper_nonce=%s" % wrapper_nonce else: query_string = "?wrapper_nonce=%s" % wrapper_nonce From 47245f485ad790b324723cf746857fb791a7bac9 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 11 May 2017 17:59:46 +0200 Subject: [PATCH 0019/2570] ActionFile security check --- src/Ui/UiRequest.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index bc02f44f..50aa0327 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -430,6 +430,8 @@ class UiRequest(object): # Stream a file to client def actionFile(self, file_path, block_size=64 * 1024, send_header=True, header_length=True): + if ".." in file_path: + raise Exception("Invalid path") if os.path.isfile(file_path): # Try to figure out content type by extension content_type = self.getContentType(file_path) @@ -521,6 +523,7 @@ class UiRequest(object): import sys sites = self.server.sites main = sys.modules["main"] + def bench(code, times=100): sites = self.server.sites main = sys.modules["main"] From 27a582634fdb71320e163077544b9a3d51823e85 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 11 May 2017 18:00:57 +0200 Subject: [PATCH 0020/2570] Don't check referrer for html files --- src/Ui/UiRequest.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 50aa0327..71a578dd 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -366,12 +366,12 @@ class UiRequest(object): if wrapper_nonce not in self.server.wrapper_nonces: return self.error403("Wrapper nonce error. Please reload the page.") self.server.wrapper_nonces.remove(self.get["wrapper_nonce"]) - - referer = self.env.get("HTTP_REFERER") - if referer and path_parts: # Only allow same site to receive media - if not self.isMediaRequestAllowed(path_parts["request_address"], referer): - self.log.error("Media referrer error: %s not allowed from %s" % (path_parts["address"], referer)) - return self.error403("Media referrer error") # Referrer not starts same address as requested path + else: + referer = self.env.get("HTTP_REFERER") + if referer and path_parts: # Only allow same site to receive media + if not self.isMediaRequestAllowed(path_parts["request_address"], referer): + self.log.error("Media referrer error: %s not allowed from %s" % (path_parts["address"], referer)) + return self.error403("Media referrer error") # Referrer not starts same address as requested path if path_parts: # Looks like a valid path address = path_parts["address"] From 5908d2ca531b1cf8115e31b34db78d2713ea3c41 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 11 May 2017 18:01:16 +0200 Subject: [PATCH 0021/2570] RenderWrapper showloadingscreen parameter --- src/Ui/UiRequest.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 71a578dd..1c0351eb 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -232,13 +232,13 @@ class UiRequest(object): else: # Bad url return False - def renderWrapper(self, site, path, inner_path, title, extra_headers): def getSiteUrl(self, address): if self.isProxyRequest(): return "http://zero/" + address else: return "/" + address + def renderWrapper(self, site, path, inner_path, title, extra_headers, show_loadingscreen=None): file_inner_path = inner_path if not file_inner_path: file_inner_path = "index.html" # If inner path defaults to index.html @@ -300,6 +300,9 @@ class UiRequest(object): else: sandbox_permissions = "" + if show_loadingscreen is None: + show_loadingscreen = not site.storage.isFile(file_inner_path) + return self.render( "src/Ui/template/wrapper.html", server_url=server_url, @@ -315,7 +318,7 @@ class UiRequest(object): wrapper_nonce=wrapper_nonce, postmessage_nonce_security=postmessage_nonce_security, permissions=json.dumps(site.settings["permissions"]), - show_loadingscreen=json.dumps(not site.storage.isFile(file_inner_path)), + show_loadingscreen=json.dumps(show_loadingscreen), sandbox_permissions=sandbox_permissions, rev=config.rev, lang=config.language, From 84b535767baf11523bd92955c9b89e3230d828b7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 11 May 2017 18:02:05 +0200 Subject: [PATCH 0022/2570] By default nothing is domain --- src/Site/SiteManager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index f3d63fda..046254af 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -106,6 +106,9 @@ class SiteManager(object): def isAddress(self, address): return re.match("^[A-Za-z0-9]{26,35}$", address) + def isDomain(self, address): + return False + # Return: Site object or None if not found def get(self, address): if self.sites is None: # Not loaded yet From 1a3b48a60d707d054c123c957b6e0f4ee1d5c1d0 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 11 May 2017 18:02:37 +0200 Subject: [PATCH 0023/2570] Sidebar blacklist button on deletion --- plugins/Sidebar/media/Sidebar.coffee | 17 +++++++++++++---- plugins/Sidebar/media/all.js | 22 ++++++++++++++++------ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee index 69abab69..0979f817 100644 --- a/plugins/Sidebar/media/Sidebar.coffee +++ b/plugins/Sidebar/media/Sidebar.coffee @@ -303,10 +303,19 @@ class Sidebar extends Class # Delete site @tag.find("#button-delete").off("click touchend").on "click touchend", => - wrapper.displayConfirm "Are you sure?", "Delete this site", => - @tag.find("#button-delete").addClass("loading") - wrapper.ws.cmd "siteDelete", wrapper.site_info.address, -> - document.location = $(".fixbutton-bg").attr("href") + wrapper.displayConfirm "Are you sure?", ["Delete this site", "Blacklist"], (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 "blacklistAdd", [wrapper.site_info.address, reason] + wrapper.ws.cmd "siteDelete", wrapper.site_info.address, -> + document.location = $(".fixbutton-bg").attr("href") + + return false # Owned checkbox diff --git a/plugins/Sidebar/media/all.js b/plugins/Sidebar/media/all.js index bed431c2..793d4408 100644 --- a/plugins/Sidebar/media/all.js +++ b/plugins/Sidebar/media/all.js @@ -536,11 +536,21 @@ window.initScrollable = function () { })(this)); this.tag.find("#button-delete").off("click touchend").on("click touchend", (function(_this) { return function() { - wrapper.displayConfirm("Are you sure?", "Delete this site", function() { - _this.tag.find("#button-delete").addClass("loading"); - return wrapper.ws.cmd("siteDelete", wrapper.site_info.address, function() { - return document.location = $(".fixbutton-bg").attr("href"); - }); + wrapper.displayConfirm("Are you sure?", ["Delete this site", "Blacklist"], function(confirmed) { + if (confirmed === 1) { + _this.tag.find("#button-delete").addClass("loading"); + return wrapper.ws.cmd("siteDelete", wrapper.site_info.address, function() { + return document.location = $(".fixbutton-bg").attr("href"); + }); + } else if (confirmed === 2) { + return wrapper.displayPrompt("Blacklist this site", "text", "Delete and Blacklist", "Reason", function(reason) { + _this.tag.find("#button-delete").addClass("loading"); + wrapper.ws.cmd("blacklistAdd", [wrapper.site_info.address, reason]); + return wrapper.ws.cmd("siteDelete", wrapper.site_info.address, function() { + return document.location = $(".fixbutton-bg").attr("href"); + }); + }); + } }); return false; }; @@ -599,7 +609,7 @@ window.initScrollable = function () { return wrapper.notifications.add("sign", "done", inner_path + " Signed!", 5000); }); } else { - wrapper.displayPrompt("Enter your private key:", "password", "Sign", function(privatekey) { + wrapper.displayPrompt("Enter your private key:", "password", "Sign", "", function(privatekey) { return wrapper.ws.cmd("siteSign", { privatekey: privatekey, inner_path: inner_path, From ffb67ab23e3c1923737d58551c5258a2a32ec8e7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 11 May 2017 18:02:56 +0200 Subject: [PATCH 0024/2570] No placeholder for privatekey prompt --- plugins/Sidebar/media/Sidebar.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee index 0979f817..954602de 100644 --- a/plugins/Sidebar/media/Sidebar.coffee +++ b/plugins/Sidebar/media/Sidebar.coffee @@ -361,7 +361,7 @@ class Sidebar extends Class else # Ask the user for privatekey - wrapper.displayPrompt "Enter your private key:", "password", "Sign", (privatekey) => # Prompt the private key + wrapper.displayPrompt "Enter your private key:", "password", "Sign", "", (privatekey) => # Prompt the private key wrapper.ws.cmd "siteSign", {privatekey: privatekey, inner_path: inner_path, update_changed_files: true}, (res) => if res == "ok" wrapper.notifications.add "sign", "done", "#{inner_path} Signed!", 5000 From f4f4aa0e50372ec1e8557e97c9d9d5c1bb34f89e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 11 May 2017 18:03:12 +0200 Subject: [PATCH 0025/2570] Rev2066 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 95f58106..ec5367e4 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.4" - self.rev = 2060 + self.rev = 2066 self.argv = argv self.action = None self.config_file = "zeronet.conf" From d467aabd4c293a85ec288d56978348e40d3b1f60 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 11 May 2017 18:18:12 +0200 Subject: [PATCH 0026/2570] Rev2067, Fix inner_path stripping --- plugins/Mute/MutePlugin.py | 2 +- src/Config.py | 2 +- src/Ui/UiRequest.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/Mute/MutePlugin.py b/plugins/Mute/MutePlugin.py index fe5968f8..e1d6fb05 100644 --- a/plugins/Mute/MutePlugin.py +++ b/plugins/Mute/MutePlugin.py @@ -146,7 +146,7 @@ class UiRequestPlugin(object): extra_headers = [] self.sendHeader(extra_headers=extra_headers[:]) return iter([super(UiRequestPlugin, self).renderWrapper( - site, path, site.address + "//uimedia/plugins/mute/blacklisted.html?address=" + address, + site, path, "uimedia/plugins/mute/blacklisted.html?address=" + address, "Blacklisted site", extra_headers, show_loadingscreen=False )]) else: diff --git a/src/Config.py b/src/Config.py index ec5367e4..7caa7e31 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.4" - self.rev = 2066 + self.rev = 2067 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 1c0351eb..61a09e56 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -248,10 +248,11 @@ class UiRequest(object): address = re.sub("/.*", "", path.lstrip("/")) if self.isProxyRequest() and (not path or "/" in path[1:]): - file_url = re.sub("^.*?/", "", inner_path) if self.env["HTTP_HOST"] == "zero": root_url = "/" + address + "/" + file_url = "/" + address + "/" + inner_path else: + file_url = "/" + inner_path root_url = "/" else: From 442c01eabfc468ae740a702fa724f4f1ebb58f5d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 14 May 2017 22:59:47 +0200 Subject: [PATCH 0027/2570] Rev2068, Deny blacklist add/remove in multiuse mode --- plugins/disabled-Multiuser/MultiuserPlugin.py | 2 +- src/Config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/disabled-Multiuser/MultiuserPlugin.py b/plugins/disabled-Multiuser/MultiuserPlugin.py index 390cb686..b2b6550a 100644 --- a/plugins/disabled-Multiuser/MultiuserPlugin.py +++ b/plugins/disabled-Multiuser/MultiuserPlugin.py @@ -124,7 +124,7 @@ class UiWebsocketPlugin(object): "siteDelete", "configSet", "serverShutdown", "serverUpdate", "siteClone", "siteSetOwned", "optionalLimitSet", "siteSetAutodownloadoptional", "dbReload", "dbRebuild", "mergerSiteDelete", "siteSetLimit", - "muteAdd", "muteRemove" + "muteAdd", "muteRemove", "blacklistAdd", "blacklistRemove" ) if config.multiuser_no_new_sites: self.multiuser_denied_cmds += ("MergerSiteAdd", ) diff --git a/src/Config.py b/src/Config.py index 7caa7e31..37fcf1c5 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.4" - self.rev = 2067 + self.rev = 2068 self.argv = argv self.action = None self.config_file = "zeronet.conf" From f9c0c217145846a50c8299dada1e6df7380bb404 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 16 May 2017 10:56:12 +0200 Subject: [PATCH 0028/2570] Update CoffeeScript to 1.12.6 --- tools/coffee/coffee-script.js | 790 +++++++++++++++++----------------- 1 file changed, 398 insertions(+), 392 deletions(-) diff --git a/tools/coffee/coffee-script.js b/tools/coffee/coffee-script.js index 63cc086b..7fce39a6 100644 --- a/tools/coffee/coffee-script.js +++ b/tools/coffee/coffee-script.js @@ -1,399 +1,405 @@ /** - * CoffeeScript Compiler v1.12.4 + * CoffeeScript Compiler v1.12.6 * http://coffeescript.org * * Copyright 2011, Jeremy Ashkenas * Released under the MIT License */ -var $jscomp={scope:{},checkStringArgs:function(u,ya,qa){if(null==u)throw new TypeError("The 'this' value for String.prototype."+qa+" must not be null or undefined");if(ya instanceof RegExp)throw new TypeError("First argument to String.prototype."+qa+" must not be a regular expression");return u+""}}; -$jscomp.defineProperty="function"==typeof Object.defineProperties?Object.defineProperty:function(u,ya,qa){if(qa.get||qa.set)throw new TypeError("ES3 does not support getters and setters.");u!=Array.prototype&&u!=Object.prototype&&(u[ya]=qa.value)};$jscomp.getGlobal=function(u){return"undefined"!=typeof window&&window===u?u:"undefined"!=typeof global&&null!=global?global:u};$jscomp.global=$jscomp.getGlobal(this); -$jscomp.polyfill=function(u,ya,qa,g){if(ya){qa=$jscomp.global;u=u.split(".");for(g=0;gu||1342177279>>=1)qa+=qa;return g}},"es6-impl","es3");$jscomp.findInternal=function(u,ya,qa){u instanceof String&&(u=String(u));for(var g=u.length,ua=0;uau||1342177279>>=1)va+=va;return f}},"es6-impl","es3");$jscomp.findInternal=function(u,xa,va){u instanceof String&&(u=String(u));for(var f=u.length,qa=0;qa>>=1,a+=a;return f};g.compact=function(a){var f,c,g,q;q=[];f=0;for(g=a.length;fh)return p.call(this,g,k-1);(t=g[0],0<=E.call(c,t))?h+=1:(m=g[0],0<=E.call(a,m))&&--h;k+=1}return k-1};m.prototype.removeLeadingNewlines=function(){var a,c,f,h,y;h=this.tokens;a=c=0;for(f=h.length;cm;f=0<=m?++h:--h){for(;"HERECOMMENT"===this.tag(c+f+a);)a+=2;if(null!=g[f]&&("string"===typeof g[f]&&(g[f]=[g[f]]),A=this.tag(c+f+a),0>E.call(g[f],A)))return-1}return c+f+a-1};m.prototype.looksObjectish=function(f){var k;if(-1E.call(g,t))&&((A=this.tag(f),0>E.call(c,A))||this.tokens[f].generated)&&(H=this.tag(f),0>E.call(G,H)));)(h=this.tag(f),0<=E.call(a,h))&&k.push(this.tag(f)),(m=this.tag(f),0<=E.call(c, -m))&&k.length&&k.pop(),--f;return l=this.tag(f),0<=E.call(g,l)};m.prototype.addImplicitBracesAndParens=function(){var k,g;k=[];g=null;return this.scanTokens(function(m,h,t){var p,A,y,q,r,I,w,x,z,B,u,C,M,F,J,N,O,K;K=m[0];B=(u=0E.call(a,c):return g[1]; -case "@"!==this.tag(h-2):return h-2;default:return h-1}}.call(this);"HERECOMMENT"===this.tag(A-2);)A-=2;this.insideForDeclaration="FOR"===z;I=0===A||(F=this.tag(A-1),0<=E.call(G,F))||t[A-1].newLine;if(J()&&(w=J(),F=w[0],u=w[1],("{"===F||"INDENT"===F&&"{"===this.tag(u-1))&&(I||","===this.tag(A-1)||"{"===this.tag(A-1))))return y(1);x(A,!!I);return y(2)}w()&&0<=E.call(G,K)&&(J()[2].sameLine=!1);x="OUTDENT"===B||u.newLine;if(0<=E.call(f,K)||0<=E.call(xa,K)&&x)for(;q();)if(x=J(),F=x[0],u=x[1],F=x[2],x= -F.sameLine,I=F.startsLine,r()&&","!==B)p();else if(w()&&!this.insideForDeclaration&&x&&"TERMINATOR"!==K&&":"!==B)A();else if(!w()||"TERMINATOR"!==K||","===B||I&&this.looksObjectish(h+1))break;else{if("HERECOMMENT"===z)return y(1);A()}if(!(","!==K||this.looksObjectish(h+1)||!w()||this.insideForDeclaration||"TERMINATOR"===z&&this.looksObjectish(h+2)))for(z="OUTDENT"===z?1:0;w();)A(h+z);return y(1)})};m.prototype.addLocationDataToGeneratedTokens=function(){return this.scanTokens(function(a,c,f){var h, -k,m;if(a[2]||!a.generated&&!a.explicit)return 1;"{"===a[0]&&(h=null!=(m=f[c+1])?m[2]:void 0)?(k=h.first_line,h=h.first_column):(h=null!=(k=f[c-1])?k[2]:void 0)?(k=h.last_line,h=h.last_column):k=h=0;a[2]={first_line:k,first_column:h,last_line:k,last_column:h};return 1})};m.prototype.fixOutdentLocationData=function(){return this.scanTokens(function(a,c,f){if(!("OUTDENT"===a[0]||a.generated&&"CALL_END"===a[0]||a.generated&&"}"===a[0]))return 1;c=f[c-1][2];a[2]={first_line:c.last_line,first_column:c.last_column, -last_line:c.last_line,last_column:c.last_column};return 1})};m.prototype.normalizeLines=function(){var a,c,f,h,m;m=f=h=null;c=function(a,c){var f,h,k,g;return";"!==a[1]&&(f=a[0],0<=E.call(F,f))&&!("TERMINATOR"===a[0]&&(h=this.tag(c+1),0<=E.call(q,h)))&&!("ELSE"===a[0]&&"THEN"!==m)&&!!("CATCH"!==(k=a[0])&&"FINALLY"!==k||"-\x3e"!==m&&"\x3d\x3e"!==m)||(g=a[0],0<=E.call(xa,g))&&this.tokens[c-1].newLine};a=function(a,c){return this.tokens.splice(","===this.tag(c-1)?c-1:c,0,h)};return this.scanTokens(function(k, -g,t){var p,y,l;k=k[0];if("TERMINATOR"===k){if("ELSE"===this.tag(g+1)&&"OUTDENT"!==this.tag(g-1))return t.splice.apply(t,[g,1].concat(O.call(this.indentation()))),1;if(p=this.tag(g+1),0<=E.call(q,p))return t.splice(g,1),0}if("CATCH"===k)for(p=y=1;2>=y;p=++y)if("OUTDENT"===(l=this.tag(g+p))||"TERMINATOR"===l||"FINALLY"===l)return t.splice.apply(t,[g+p,0].concat(O.call(this.indentation()))),2+p;0<=E.call(x,k)&&"INDENT"!==this.tag(g+1)&&("ELSE"!==k||"IF"!==this.tag(g+1))&&(m=k,l=this.indentation(t[g]), -f=l[0],h=l[1],"THEN"===m&&(f.fromThen=!0),t.splice(g+1,0,f),this.detectEnd(g+2,c,a),"THEN"===k&&t.splice(g,1));return 1})};m.prototype.tagPostfixConditionals=function(){var a,c,f;f=null;c=function(a,c){a=a[0];c=this.tokens[c-1][0];return"TERMINATOR"===a||"INDENT"===a&&0>E.call(x,c)};a=function(a,c){if("INDENT"!==a[0]||a.generated&&!a.fromThen)return f[0]="POST_"+f[0]};return this.scanTokens(function(h,k){if("IF"!==h[0])return 1;f=h;this.detectEnd(k+1,c,a);return 1})};m.prototype.indentation=function(a){var c, -f;c=["INDENT",2];f=["OUTDENT",2];a?(c.generated=f.generated=!0,c.origin=f.origin=a):c.explicit=f.explicit=!0;return[c,f]};m.prototype.generate=v;m.prototype.tag=function(a){var c;return null!=(c=this.tokens[a])?c[0]:void 0};return m}();u=[["(",")"],["[","]"],["{","}"],["INDENT","OUTDENT"],["CALL_START","CALL_END"],["PARAM_START","PARAM_END"],["INDEX_START","INDEX_END"],["STRING_START","STRING_END"],["REGEX_START","REGEX_END"]];g.INVERSES=w={};c=[];a=[];M=0;for(J=u.length;Mthis.indent){if(a)return this.indebt=f-this.indent,this.suppressNewlines(),c.length; -if(!this.tokens.length)return this.baseIndent=this.indent=f,c.length;a=f-this.indent+this.outdebt;this.token("INDENT",a,c.length-f,f);this.indents.push(a);this.ends.push({tag:"OUTDENT"});this.outdebt=this.indebt=0;this.indent=f}else fk&&(m=this.token("+", -"+"),m[2]={first_line:l[2].first_line,first_column:l[2].first_column,last_line:l[2].first_line,last_column:l[2].first_column});(y=this.tokens).push.apply(y,A)}if(r)return a=a[a.length-1],r.origin=["STRING",null,{first_line:r[2].first_line,first_column:r[2].first_column,last_line:a[2].last_line,last_column:a[2].last_column}],r=this.token("STRING_END",")"),r[2]={first_line:a[2].last_line,first_column:a[2].last_column,last_line:a[2].last_line,last_column:a[2].last_column}};g.prototype.pair=function(a){var c; -c=this.ends;c=c[c.length-1];return a!==(c=null!=c?c.tag:void 0)?("OUTDENT"!==c&&this.error("unmatched "+a),c=this.indents,c=c[c.length-1],this.outdentToken(c,!0),this.pair(a)):this.ends.pop()};g.prototype.getLineAndColumnFromChunk=function(a){var c,f;if(0===a)return[this.chunkLine,this.chunkColumn];f=a>=this.chunk.length?this.chunk:this.chunk.slice(0,+(a-1)+1||9E9);a=ea(f,"\n");c=this.chunkColumn;0da.call(ma.call(t).concat(ma.call(Ca)),a):return"keyword '"+c+"' can't be assigned";case 0>da.call(Y,a):return"'"+c+"' can't be assigned";case 0>da.call(W,a):return"reserved word '"+c+"' can't be assigned";default:return!1}};g.isUnassignable=la;ha=function(a){var c;return"IDENTIFIER"===a[0]?("from"===a[1]&&(a[1][0]="IDENTIFIER",!0),!0):"FOR"===a[0]?!1:"{"===(c=a[1])||"["===c||","===c||":"===c?!1:!0};t="true false null this new delete typeof in instanceof return throw break continue debugger yield if else switch for while do try catch finally class extends super import export default".split(" "); -Ca="undefined Infinity NaN then unless until loop of by when".split(" ");c={and:"\x26\x26",or:"||",is:"\x3d\x3d",isnt:"!\x3d",not:"!",yes:"true",no:"false",on:"true",off:"false"};a=function(){var a;a=[];for(oa in c)a.push(oa);return a}();Ca=Ca.concat(a);W="case function var void with const let enum native implements interface package private protected public static".split(" ");Y=["arguments","eval"];g.JS_FORBIDDEN=t.concat(W).concat(Y);ua=65279;J=/^(?!\d)((?:(?!\s)[$\w\x7f-\uffff])+)([^\n\S]*:(?!:))?/; -U=/^0b[01]+|^0o[0-7]+|^0x[\da-f]+|^\d*\.?\d+(?:e[+-]?\d+)?/i;T=/^(?:[-=]>|[-+*\/%<>&|^!?=]=|>>>=?|([-+:])\1|([&|<>*\/%])\2=?|\?(\.|::)|\.{2,3})/;ca=/^[^\n\S]+/;f=/^###([^#][\s\S]*?)(?:###[^\n\S]*|###$)|^(?:\s*#(?!##[^#]).*)+/;q=/^[-=]>/;A=/^(?:\n[^\n\S]*)+/;k=/^`(?!``)((?:[^`\\]|\\[\s\S])*)`/;N=/^```((?:[^`\\]|\\[\s\S]|`(?!``))*)```/;ba=/^(?:'''|"""|'|")/;K=/^(?:[^\\']|\\[\s\S])*/;V=/^(?:[^\\"#]|\\[\s\S]|\#(?!\{))*/;x=/^(?:[^\\']|\\[\s\S]|'(?!''))*/;G=/^(?:[^\\"#]|\\[\s\S]|"(?!"")|\#(?!\{))*/;Z=/((?:\\\\)+)|\\[^\S\n]*\n\s*/g; -X=/\s*\n\s*/g;F=/\n+([^\n\S]*)(?=\S)/g;aa=/^\/(?!\/)((?:[^[\/\n\\]|\\[^\n]|\[(?:\\[^\n]|[^\]\n\\])*\])*)(\/)?/;S=/^\w*/;ka=/^(?!.*(.).*\1)[imgy]*$/;v=/^(?:[^\\\/#]|\\[\s\S]|\/(?!\/\/)|\#(?!\{))*/;M=/((?:\\\\)+)|\\(\s)|\s+(?:#.*)?/g;z=/^(\/|\/{3}\s*)(\*)/;I=/^\/=?\s/;w=/\*\//;y=/^\s*(?:,|\??\.(?![.\d])|::)/;O=/((?:^|[^\\])(?:\\\\)*)\\(?:(0[0-7]|[1-7])|(x(?![\da-fA-F]{2}).{0,2})|(u(?![\da-fA-F]{4}).{0,4}))/;p=/^[^\n\S]*\n/;R=/\n[^\n\S]*$/;qa=/\s+$/;l="-\x3d +\x3d /\x3d *\x3d %\x3d ||\x3d \x26\x26\x3d ?\x3d \x3c\x3c\x3d \x3e\x3e\x3d \x3e\x3e\x3e\x3d \x26\x3d ^\x3d |\x3d **\x3d //\x3d %%\x3d".split(" "); -ya=["NEW","TYPEOF","DELETE","DO"];Xa=["!","~"];Q=["\x3c\x3c","\x3e\x3e","\x3e\x3e\x3e"];D="\x3d\x3d !\x3d \x3c \x3e \x3c\x3d \x3e\x3d".split(" ");P=["*","/","%","//","%%"];B=["IN","OF","INSTANCEOF"];xa="IDENTIFIER PROPERTY ) ] ? @ THIS SUPER".split(" ");E=xa.concat("NUMBER INFINITY NAN STRING STRING_END REGEX REGEX_END BOOL NULL UNDEFINED } ::".split(" "));H=E.concat(["++","--"]);h=["INDENT","OUTDENT","TERMINATOR"];r=[")","}","]"]}).call(this);return g}();u["./parser"]=function(){var g={},ua={exports:g}, -xa=function(){function g(){this.yy={}}var a=function(a,n,pa,b){pa=pa||{};for(b=a.length;b--;pa[a[b]]=n);return pa},c=[1,22],u=[1,25],f=[1,83],D=[1,79],l=[1,84],w=[1,85],G=[1,81],F=[1,82],x=[1,56],v=[1,58],M=[1,59],N=[1,60],J=[1,61],r=[1,62],E=[1,49],O=[1,50],m=[1,32],k=[1,68],t=[1,69],p=[1,78],h=[1,47],y=[1,51],P=[1,52],A=[1,67],H=[1,65],U=[1,66],T=[1,64],I=[1,42],aa=[1,48],S=[1,63],z=[1,73],B=[1,74],W=[1,75],C=[1,76],Q=[1,46],X=[1,72],Y=[1,34],V=[1,35],Z=[1,36],K=[1,37],ba=[1,38],R=[1,39],xa=[1, -86],ua=[1,6,32,42,131],qa=[1,101],ka=[1,89],ca=[1,88],ea=[1,87],ga=[1,90],ha=[1,91],la=[1,92],oa=[1,93],L=[1,94],ja=[1,95],ra=[1,96],da=[1,97],ma=[1,98],sa=[1,99],ia=[1,100],ya=[1,104],ta=[1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],Ja=[2,165],Ta=[1,110],Ga=[1,111],Ua=[1,112],Fa=[1,113],Pa=[1,115],Qa=[1,116],Na=[1,109],za=[1,6,32,42,131,133,135,139,156],na=[2,27],fa=[1,123],Ha=[1,121],Aa=[1,6,31,32,40,41,42,65,70,73, -82,83,84,85,87,89,90,94,113,114,115,120,122,131,133,134,135,139,140,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],Ia=[2,94],b=[1,6,31,32,42,46,65,70,73,82,83,84,85,87,89,90,94,113,114,115,120,122,131,133,134,135,139,140,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],n=[2,73],pa=[1,128],e=[1,133],d=[1,134],va=[1,136],Ka=[1,6,31,32,40,41,42,55,65,70,73,82,83,84,85,87,89,90,94,113,114,115,120,122,131,133,134,135,139,140,156,159,160,163,164,165,166,167,168,169,170,171,172, -173,174],wa=[2,91],Gb=[1,6,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],ab=[2,63],Hb=[1,166],bb=[1,178],Wa=[1,180],Ib=[1,175],Oa=[1,182],ub=[1,184],La=[1,6,31,32,40,41,42,55,65,70,73,82,83,84,85,87,89,90,94,96,113,114,115,120,122,131,133,134,135,139,140,156,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175],Jb=[2,110],Kb=[1,6,31,32,40,41,42,58,65,70,73,82,83,84,85,87,89,90,94,113,114,115,120,122,131,133,134, -135,139,140,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],Lb=[1,6,31,32,40,41,42,46,58,65,70,73,82,83,84,85,87,89,90,94,113,114,115,120,122,131,133,134,135,139,140,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],Mb=[40,41,114],Nb=[1,241],vb=[1,240],Ma=[1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156],Ea=[2,71],Ob=[1,250],Va=[6,31,32,65,70],hb=[6,31,32,55,65,70,73],cb=[1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,159,160,164,166, -167,168,169,170,171,172,173,174],Pb=[40,41,82,83,84,85,87,90,113,114],ib=[1,269],db=[2,62],jb=[1,279],Ya=[1,281],wb=[1,286],eb=[1,288],Qb=[2,186],xb=[1,6,31,32,40,41,42,55,65,70,73,82,83,84,85,87,89,90,94,113,114,115,120,122,131,133,134,135,139,140,146,147,148,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],kb=[1,297],Ra=[6,31,32,70,115,120],Rb=[1,6,31,32,40,41,42,55,58,65,70,73,82,83,84,85,87,89,90,94,96,113,114,115,120,122,131,133,134,135,139,140,146,147,148,156,159,160,161,162,163, -164,165,166,167,168,169,170,171,172,173,174,175],Sb=[1,6,31,32,42,65,70,73,89,94,115,120,122,131,140,156],Za=[1,6,31,32,42,65,70,73,89,94,115,120,122,131,134,140,156],lb=[146,147,148],mb=[70,146,147,148],nb=[6,31,94],Tb=[1,311],Ba=[6,31,32,70,94],Ub=[6,31,32,58,70,94],yb=[6,31,32,55,58,70,94],Vb=[1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,159,160,166,167,168,169,170,171,172,173,174],Wb=[12,28,34,38,40,41,44,45,48,49,50,51,52,53,61,62,63,67,68,89,92,95,97,105,112,117,118,119, -125,129,130,133,135,137,139,149,155,157,158,159,160,161,162],Xb=[2,175],Sa=[6,31,32],fb=[2,72],Yb=[1,323],Zb=[1,324],$b=[1,6,31,32,42,65,70,73,89,94,115,120,122,127,128,131,133,134,135,139,140,151,153,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],ob=[32,151,153],ac=[1,6,32,42,65,70,73,89,94,115,120,122,131,134,140,156],pb=[1,350],zb=[1,356],Ab=[1,6,32,42,131,156],gb=[2,86],qb=[1,366],rb=[1,367],bc=[1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,151,156,159,160,163, -164,165,166,167,168,169,170,171,172,173,174],Bb=[1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,135,139,140,156],cc=[1,380],dc=[1,381],Cb=[6,31,32,94],ec=[6,31,32,70],Db=[1,6,31,32,42,65,70,73,89,94,115,120,122,127,131,133,134,135,139,140,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],fc=[31,70],sb=[1,407],tb=[1,408],Eb=[1,414],Fb=[1,415],gc={trace:function(){},yy:{},symbols_:{error:2,Root:3,Body:4,Line:5,TERMINATOR:6,Expression:7,Statement:8,YieldReturn:9,Return:10,Comment:11,STATEMENT:12, -Import:13,Export:14,Value:15,Invocation:16,Code:17,Operation:18,Assign:19,If:20,Try:21,While:22,For:23,Switch:24,Class:25,Throw:26,Yield:27,YIELD:28,FROM:29,Block:30,INDENT:31,OUTDENT:32,Identifier:33,IDENTIFIER:34,Property:35,PROPERTY:36,AlphaNumeric:37,NUMBER:38,String:39,STRING:40,STRING_START:41,STRING_END:42,Regex:43,REGEX:44,REGEX_START:45,REGEX_END:46,Literal:47,JS:48,UNDEFINED:49,NULL:50,BOOL:51,INFINITY:52,NAN:53,Assignable:54,"\x3d":55,AssignObj:56,ObjAssignable:57,":":58,SimpleObjAssignable:59, -ThisProperty:60,RETURN:61,HERECOMMENT:62,PARAM_START:63,ParamList:64,PARAM_END:65,FuncGlyph:66,"-\x3e":67,"\x3d\x3e":68,OptComma:69,",":70,Param:71,ParamVar:72,"...":73,Array:74,Object:75,Splat:76,SimpleAssignable:77,Accessor:78,Parenthetical:79,Range:80,This:81,".":82,"?.":83,"::":84,"?::":85,Index:86,INDEX_START:87,IndexValue:88,INDEX_END:89,INDEX_SOAK:90,Slice:91,"{":92,AssignList:93,"}":94,CLASS:95,EXTENDS:96,IMPORT:97,ImportDefaultSpecifier:98,ImportNamespaceSpecifier:99,ImportSpecifierList:100, -ImportSpecifier:101,AS:102,DEFAULT:103,IMPORT_ALL:104,EXPORT:105,ExportSpecifierList:106,EXPORT_ALL:107,ExportSpecifier:108,OptFuncExist:109,Arguments:110,Super:111,SUPER:112,FUNC_EXIST:113,CALL_START:114,CALL_END:115,ArgList:116,THIS:117,"@":118,"[":119,"]":120,RangeDots:121,"..":122,Arg:123,SimpleArgs:124,TRY:125,Catch:126,FINALLY:127,CATCH:128,THROW:129,"(":130,")":131,WhileSource:132,WHILE:133,WHEN:134,UNTIL:135,Loop:136,LOOP:137,ForBody:138,FOR:139,BY:140,ForStart:141,ForSource:142,ForVariables:143, -OWN:144,ForValue:145,FORIN:146,FOROF:147,FORFROM:148,SWITCH:149,Whens:150,ELSE:151,When:152,LEADING_WHEN:153,IfBlock:154,IF:155,POST_IF:156,UNARY:157,UNARY_MATH:158,"-":159,"+":160,"--":161,"++":162,"?":163,MATH:164,"**":165,SHIFT:166,COMPARE:167,"\x26":168,"^":169,"|":170,"\x26\x26":171,"||":172,"BIN?":173,RELATION:174,COMPOUND_ASSIGN:175,$accept:0,$end:1},terminals_:{2:"error",6:"TERMINATOR",12:"STATEMENT",28:"YIELD",29:"FROM",31:"INDENT",32:"OUTDENT",34:"IDENTIFIER",36:"PROPERTY",38:"NUMBER",40:"STRING", -41:"STRING_START",42:"STRING_END",44:"REGEX",45:"REGEX_START",46:"REGEX_END",48:"JS",49:"UNDEFINED",50:"NULL",51:"BOOL",52:"INFINITY",53:"NAN",55:"\x3d",58:":",61:"RETURN",62:"HERECOMMENT",63:"PARAM_START",65:"PARAM_END",67:"-\x3e",68:"\x3d\x3e",70:",",73:"...",82:".",83:"?.",84:"::",85:"?::",87:"INDEX_START",89:"INDEX_END",90:"INDEX_SOAK",92:"{",94:"}",95:"CLASS",96:"EXTENDS",97:"IMPORT",102:"AS",103:"DEFAULT",104:"IMPORT_ALL",105:"EXPORT",107:"EXPORT_ALL",112:"SUPER",113:"FUNC_EXIST",114:"CALL_START", -115:"CALL_END",117:"THIS",118:"@",119:"[",120:"]",122:"..",125:"TRY",127:"FINALLY",128:"CATCH",129:"THROW",130:"(",131:")",133:"WHILE",134:"WHEN",135:"UNTIL",137:"LOOP",139:"FOR",140:"BY",144:"OWN",146:"FORIN",147:"FOROF",148:"FORFROM",149:"SWITCH",151:"ELSE",153:"LEADING_WHEN",155:"IF",156:"POST_IF",157:"UNARY",158:"UNARY_MATH",159:"-",160:"+",161:"--",162:"++",163:"?",164:"MATH",165:"**",166:"SHIFT",167:"COMPARE",168:"\x26",169:"^",170:"|",171:"\x26\x26",172:"||",173:"BIN?",174:"RELATION",175:"COMPOUND_ASSIGN"}, -productions_:[0,[3,0],[3,1],[4,1],[4,3],[4,2],[5,1],[5,1],[5,1],[8,1],[8,1],[8,1],[8,1],[8,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[27,1],[27,2],[27,3],[30,2],[30,3],[33,1],[35,1],[37,1],[37,1],[39,1],[39,3],[43,1],[43,3],[47,1],[47,1],[47,1],[47,1],[47,1],[47,1],[47,1],[47,1],[19,3],[19,4],[19,5],[56,1],[56,3],[56,5],[56,3],[56,5],[56,1],[59,1],[59,1],[59,1],[57,1],[57,1],[10,2],[10,1],[9,3],[9,2],[11,1],[17,5],[17,2],[66,1],[66,1],[69,0],[69,1],[64,0],[64, -1],[64,3],[64,4],[64,6],[71,1],[71,2],[71,3],[71,1],[72,1],[72,1],[72,1],[72,1],[76,2],[77,1],[77,2],[77,2],[77,1],[54,1],[54,1],[54,1],[15,1],[15,1],[15,1],[15,1],[15,1],[78,2],[78,2],[78,2],[78,2],[78,1],[78,1],[86,3],[86,2],[88,1],[88,1],[75,4],[93,0],[93,1],[93,3],[93,4],[93,6],[25,1],[25,2],[25,3],[25,4],[25,2],[25,3],[25,4],[25,5],[13,2],[13,4],[13,4],[13,5],[13,7],[13,6],[13,9],[100,1],[100,3],[100,4],[100,4],[100,6],[101,1],[101,3],[101,1],[101,3],[98,1],[99,3],[14,3],[14,5],[14,2],[14,4], -[14,5],[14,6],[14,3],[14,4],[14,7],[106,1],[106,3],[106,4],[106,4],[106,6],[108,1],[108,3],[108,3],[108,1],[16,3],[16,3],[16,3],[16,1],[111,1],[111,2],[109,0],[109,1],[110,2],[110,4],[81,1],[81,1],[60,2],[74,2],[74,4],[121,1],[121,1],[80,5],[91,3],[91,2],[91,2],[91,1],[116,1],[116,3],[116,4],[116,4],[116,6],[123,1],[123,1],[123,1],[124,1],[124,3],[21,2],[21,3],[21,4],[21,5],[126,3],[126,3],[126,2],[26,2],[79,3],[79,5],[132,2],[132,4],[132,2],[132,4],[22,2],[22,2],[22,2],[22,1],[136,2],[136,2],[23, -2],[23,2],[23,2],[138,2],[138,4],[138,2],[141,2],[141,3],[145,1],[145,1],[145,1],[145,1],[143,1],[143,3],[142,2],[142,2],[142,4],[142,4],[142,4],[142,6],[142,6],[142,2],[142,4],[24,5],[24,7],[24,4],[24,6],[150,1],[150,2],[152,3],[152,4],[154,3],[154,5],[20,1],[20,3],[20,3],[20,3],[18,2],[18,2],[18,2],[18,2],[18,2],[18,2],[18,2],[18,2],[18,2],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,5],[18,4],[18,3]],performAction:function(a,n,pa,b,c,e,d){a= -e.length-1;switch(c){case 1:return this.$=b.addLocationDataFn(d[a],d[a])(new b.Block);case 2:return this.$=e[a];case 3:this.$=b.addLocationDataFn(d[a],d[a])(b.Block.wrap([e[a]]));break;case 4:this.$=b.addLocationDataFn(d[a-2],d[a])(e[a-2].push(e[a]));break;case 5:this.$=e[a-1];break;case 6:case 7:case 8:case 9:case 10:case 12:case 13:case 14:case 15:case 16:case 17:case 18:case 19:case 20:case 21:case 22:case 23:case 24:case 25:case 26:case 35:case 40:case 42:case 56:case 57:case 58:case 59:case 60:case 61:case 71:case 72:case 82:case 83:case 84:case 85:case 90:case 91:case 94:case 98:case 104:case 162:case 186:case 187:case 189:case 219:case 220:case 238:case 244:this.$= -e[a];break;case 11:this.$=b.addLocationDataFn(d[a],d[a])(new b.StatementLiteral(e[a]));break;case 27:this.$=b.addLocationDataFn(d[a],d[a])(new b.Op(e[a],new b.Value(new b.Literal(""))));break;case 28:case 248:case 249:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.Op(e[a-1],e[a]));break;case 29:this.$=b.addLocationDataFn(d[a-2],d[a])(new b.Op(e[a-2].concat(e[a-1]),e[a]));break;case 30:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.Block);break;case 31:case 105:this.$=b.addLocationDataFn(d[a-2],d[a])(e[a- -1]);break;case 32:this.$=b.addLocationDataFn(d[a],d[a])(new b.IdentifierLiteral(e[a]));break;case 33:this.$=b.addLocationDataFn(d[a],d[a])(new b.PropertyName(e[a]));break;case 34:this.$=b.addLocationDataFn(d[a],d[a])(new b.NumberLiteral(e[a]));break;case 36:this.$=b.addLocationDataFn(d[a],d[a])(new b.StringLiteral(e[a]));break;case 37:this.$=b.addLocationDataFn(d[a-2],d[a])(new b.StringWithInterpolations(e[a-1]));break;case 38:this.$=b.addLocationDataFn(d[a],d[a])(new b.RegexLiteral(e[a]));break; -case 39:this.$=b.addLocationDataFn(d[a-2],d[a])(new b.RegexWithInterpolations(e[a-1].args));break;case 41:this.$=b.addLocationDataFn(d[a],d[a])(new b.PassthroughLiteral(e[a]));break;case 43:this.$=b.addLocationDataFn(d[a],d[a])(new b.UndefinedLiteral);break;case 44:this.$=b.addLocationDataFn(d[a],d[a])(new b.NullLiteral);break;case 45:this.$=b.addLocationDataFn(d[a],d[a])(new b.BooleanLiteral(e[a]));break;case 46:this.$=b.addLocationDataFn(d[a],d[a])(new b.InfinityLiteral(e[a]));break;case 47:this.$= -b.addLocationDataFn(d[a],d[a])(new b.NaNLiteral);break;case 48:this.$=b.addLocationDataFn(d[a-2],d[a])(new b.Assign(e[a-2],e[a]));break;case 49:this.$=b.addLocationDataFn(d[a-3],d[a])(new b.Assign(e[a-3],e[a]));break;case 50:this.$=b.addLocationDataFn(d[a-4],d[a])(new b.Assign(e[a-4],e[a-1]));break;case 51:case 87:case 92:case 93:case 95:case 96:case 97:case 221:case 222:this.$=b.addLocationDataFn(d[a],d[a])(new b.Value(e[a]));break;case 52:this.$=b.addLocationDataFn(d[a-2],d[a])(new b.Assign(b.addLocationDataFn(d[a- -2])(new b.Value(e[a-2])),e[a],"object",{operatorToken:b.addLocationDataFn(d[a-1])(new b.Literal(e[a-1]))}));break;case 53:this.$=b.addLocationDataFn(d[a-4],d[a])(new b.Assign(b.addLocationDataFn(d[a-4])(new b.Value(e[a-4])),e[a-1],"object",{operatorToken:b.addLocationDataFn(d[a-3])(new b.Literal(e[a-3]))}));break;case 54:this.$=b.addLocationDataFn(d[a-2],d[a])(new b.Assign(b.addLocationDataFn(d[a-2])(new b.Value(e[a-2])),e[a],null,{operatorToken:b.addLocationDataFn(d[a-1])(new b.Literal(e[a-1]))})); -break;case 55:this.$=b.addLocationDataFn(d[a-4],d[a])(new b.Assign(b.addLocationDataFn(d[a-4])(new b.Value(e[a-4])),e[a-1],null,{operatorToken:b.addLocationDataFn(d[a-3])(new b.Literal(e[a-3]))}));break;case 62:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.Return(e[a]));break;case 63:this.$=b.addLocationDataFn(d[a],d[a])(new b.Return);break;case 64:this.$=b.addLocationDataFn(d[a-2],d[a])(new b.YieldReturn(e[a]));break;case 65:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.YieldReturn);break;case 66:this.$= -b.addLocationDataFn(d[a],d[a])(new b.Comment(e[a]));break;case 67:this.$=b.addLocationDataFn(d[a-4],d[a])(new b.Code(e[a-3],e[a],e[a-1]));break;case 68:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.Code([],e[a],e[a-1]));break;case 69:this.$=b.addLocationDataFn(d[a],d[a])("func");break;case 70:this.$=b.addLocationDataFn(d[a],d[a])("boundfunc");break;case 73:case 110:this.$=b.addLocationDataFn(d[a],d[a])([]);break;case 74:case 111:case 130:case 150:case 181:case 223:this.$=b.addLocationDataFn(d[a], -d[a])([e[a]]);break;case 75:case 112:case 131:case 151:case 182:this.$=b.addLocationDataFn(d[a-2],d[a])(e[a-2].concat(e[a]));break;case 76:case 113:case 132:case 152:case 183:this.$=b.addLocationDataFn(d[a-3],d[a])(e[a-3].concat(e[a]));break;case 77:case 114:case 134:case 154:case 185:this.$=b.addLocationDataFn(d[a-5],d[a])(e[a-5].concat(e[a-2]));break;case 78:this.$=b.addLocationDataFn(d[a],d[a])(new b.Param(e[a]));break;case 79:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.Param(e[a-1],null,!0)); -break;case 80:this.$=b.addLocationDataFn(d[a-2],d[a])(new b.Param(e[a-2],e[a]));break;case 81:case 188:this.$=b.addLocationDataFn(d[a],d[a])(new b.Expansion);break;case 86:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.Splat(e[a-1]));break;case 88:this.$=b.addLocationDataFn(d[a-1],d[a])(e[a-1].add(e[a]));break;case 89:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.Value(e[a-1],[].concat(e[a])));break;case 99:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.Access(e[a]));break;case 100:this.$=b.addLocationDataFn(d[a- -1],d[a])(new b.Access(e[a],"soak"));break;case 101:this.$=b.addLocationDataFn(d[a-1],d[a])([b.addLocationDataFn(d[a-1])(new b.Access(new b.PropertyName("prototype"))),b.addLocationDataFn(d[a])(new b.Access(e[a]))]);break;case 102:this.$=b.addLocationDataFn(d[a-1],d[a])([b.addLocationDataFn(d[a-1])(new b.Access(new b.PropertyName("prototype"),"soak")),b.addLocationDataFn(d[a])(new b.Access(e[a]))]);break;case 103:this.$=b.addLocationDataFn(d[a],d[a])(new b.Access(new b.PropertyName("prototype"))); -break;case 106:this.$=b.addLocationDataFn(d[a-1],d[a])(b.extend(e[a],{soak:!0}));break;case 107:this.$=b.addLocationDataFn(d[a],d[a])(new b.Index(e[a]));break;case 108:this.$=b.addLocationDataFn(d[a],d[a])(new b.Slice(e[a]));break;case 109:this.$=b.addLocationDataFn(d[a-3],d[a])(new b.Obj(e[a-2],e[a-3].generated));break;case 115:this.$=b.addLocationDataFn(d[a],d[a])(new b.Class);break;case 116:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.Class(null,null,e[a]));break;case 117:this.$=b.addLocationDataFn(d[a- -2],d[a])(new b.Class(null,e[a]));break;case 118:this.$=b.addLocationDataFn(d[a-3],d[a])(new b.Class(null,e[a-1],e[a]));break;case 119:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.Class(e[a]));break;case 120:this.$=b.addLocationDataFn(d[a-2],d[a])(new b.Class(e[a-1],null,e[a]));break;case 121:this.$=b.addLocationDataFn(d[a-3],d[a])(new b.Class(e[a-2],e[a]));break;case 122:this.$=b.addLocationDataFn(d[a-4],d[a])(new b.Class(e[a-3],e[a-1],e[a]));break;case 123:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.ImportDeclaration(null, -e[a]));break;case 124:this.$=b.addLocationDataFn(d[a-3],d[a])(new b.ImportDeclaration(new b.ImportClause(e[a-2],null),e[a]));break;case 125:this.$=b.addLocationDataFn(d[a-3],d[a])(new b.ImportDeclaration(new b.ImportClause(null,e[a-2]),e[a]));break;case 126:this.$=b.addLocationDataFn(d[a-4],d[a])(new b.ImportDeclaration(new b.ImportClause(null,new b.ImportSpecifierList([])),e[a]));break;case 127:this.$=b.addLocationDataFn(d[a-6],d[a])(new b.ImportDeclaration(new b.ImportClause(null,new b.ImportSpecifierList(e[a- -4])),e[a]));break;case 128:this.$=b.addLocationDataFn(d[a-5],d[a])(new b.ImportDeclaration(new b.ImportClause(e[a-4],e[a-2]),e[a]));break;case 129:this.$=b.addLocationDataFn(d[a-8],d[a])(new b.ImportDeclaration(new b.ImportClause(e[a-7],new b.ImportSpecifierList(e[a-4])),e[a]));break;case 133:case 153:case 168:case 184:this.$=b.addLocationDataFn(d[a-3],d[a])(e[a-2]);break;case 135:this.$=b.addLocationDataFn(d[a],d[a])(new b.ImportSpecifier(e[a]));break;case 136:this.$=b.addLocationDataFn(d[a-2],d[a])(new b.ImportSpecifier(e[a- -2],e[a]));break;case 137:this.$=b.addLocationDataFn(d[a],d[a])(new b.ImportSpecifier(new b.Literal(e[a])));break;case 138:this.$=b.addLocationDataFn(d[a-2],d[a])(new b.ImportSpecifier(new b.Literal(e[a-2]),e[a]));break;case 139:this.$=b.addLocationDataFn(d[a],d[a])(new b.ImportDefaultSpecifier(e[a]));break;case 140:this.$=b.addLocationDataFn(d[a-2],d[a])(new b.ImportNamespaceSpecifier(new b.Literal(e[a-2]),e[a]));break;case 141:this.$=b.addLocationDataFn(d[a-2],d[a])(new b.ExportNamedDeclaration(new b.ExportSpecifierList([]))); -break;case 142:this.$=b.addLocationDataFn(d[a-4],d[a])(new b.ExportNamedDeclaration(new b.ExportSpecifierList(e[a-2])));break;case 143:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.ExportNamedDeclaration(e[a]));break;case 144:this.$=b.addLocationDataFn(d[a-3],d[a])(new b.ExportNamedDeclaration(new b.Assign(e[a-2],e[a],null,{moduleDeclaration:"export"})));break;case 145:this.$=b.addLocationDataFn(d[a-4],d[a])(new b.ExportNamedDeclaration(new b.Assign(e[a-3],e[a],null,{moduleDeclaration:"export"}))); -break;case 146:this.$=b.addLocationDataFn(d[a-5],d[a])(new b.ExportNamedDeclaration(new b.Assign(e[a-4],e[a-1],null,{moduleDeclaration:"export"})));break;case 147:this.$=b.addLocationDataFn(d[a-2],d[a])(new b.ExportDefaultDeclaration(e[a]));break;case 148:this.$=b.addLocationDataFn(d[a-3],d[a])(new b.ExportAllDeclaration(new b.Literal(e[a-2]),e[a]));break;case 149:this.$=b.addLocationDataFn(d[a-6],d[a])(new b.ExportNamedDeclaration(new b.ExportSpecifierList(e[a-4]),e[a]));break;case 155:this.$=b.addLocationDataFn(d[a], -d[a])(new b.ExportSpecifier(e[a]));break;case 156:this.$=b.addLocationDataFn(d[a-2],d[a])(new b.ExportSpecifier(e[a-2],e[a]));break;case 157:this.$=b.addLocationDataFn(d[a-2],d[a])(new b.ExportSpecifier(e[a-2],new b.Literal(e[a])));break;case 158:this.$=b.addLocationDataFn(d[a],d[a])(new b.ExportSpecifier(new b.Literal(e[a])));break;case 159:this.$=b.addLocationDataFn(d[a-2],d[a])(new b.TaggedTemplateCall(e[a-2],e[a],e[a-1]));break;case 160:case 161:this.$=b.addLocationDataFn(d[a-2],d[a])(new b.Call(e[a- -2],e[a],e[a-1]));break;case 163:this.$=b.addLocationDataFn(d[a],d[a])(new b.SuperCall);break;case 164:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.SuperCall(e[a]));break;case 165:this.$=b.addLocationDataFn(d[a],d[a])(!1);break;case 166:this.$=b.addLocationDataFn(d[a],d[a])(!0);break;case 167:this.$=b.addLocationDataFn(d[a-1],d[a])([]);break;case 169:case 170:this.$=b.addLocationDataFn(d[a],d[a])(new b.Value(new b.ThisLiteral));break;case 171:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.Value(b.addLocationDataFn(d[a- -1])(new b.ThisLiteral),[b.addLocationDataFn(d[a])(new b.Access(e[a]))],"this"));break;case 172:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.Arr([]));break;case 173:this.$=b.addLocationDataFn(d[a-3],d[a])(new b.Arr(e[a-2]));break;case 174:this.$=b.addLocationDataFn(d[a],d[a])("inclusive");break;case 175:this.$=b.addLocationDataFn(d[a],d[a])("exclusive");break;case 176:this.$=b.addLocationDataFn(d[a-4],d[a])(new b.Range(e[a-3],e[a-1],e[a-2]));break;case 177:this.$=b.addLocationDataFn(d[a-2],d[a])(new b.Range(e[a- -2],e[a],e[a-1]));break;case 178:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.Range(e[a-1],null,e[a]));break;case 179:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.Range(null,e[a],e[a-1]));break;case 180:this.$=b.addLocationDataFn(d[a],d[a])(new b.Range(null,null,e[a]));break;case 190:this.$=b.addLocationDataFn(d[a-2],d[a])([].concat(e[a-2],e[a]));break;case 191:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.Try(e[a]));break;case 192:this.$=b.addLocationDataFn(d[a-2],d[a])(new b.Try(e[a-1],e[a][0], -e[a][1]));break;case 193:this.$=b.addLocationDataFn(d[a-3],d[a])(new b.Try(e[a-2],null,null,e[a]));break;case 194:this.$=b.addLocationDataFn(d[a-4],d[a])(new b.Try(e[a-3],e[a-2][0],e[a-2][1],e[a]));break;case 195:this.$=b.addLocationDataFn(d[a-2],d[a])([e[a-1],e[a]]);break;case 196:this.$=b.addLocationDataFn(d[a-2],d[a])([b.addLocationDataFn(d[a-1])(new b.Value(e[a-1])),e[a]]);break;case 197:this.$=b.addLocationDataFn(d[a-1],d[a])([null,e[a]]);break;case 198:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.Throw(e[a])); -break;case 199:this.$=b.addLocationDataFn(d[a-2],d[a])(new b.Parens(e[a-1]));break;case 200:this.$=b.addLocationDataFn(d[a-4],d[a])(new b.Parens(e[a-2]));break;case 201:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.While(e[a]));break;case 202:this.$=b.addLocationDataFn(d[a-3],d[a])(new b.While(e[a-2],{guard:e[a]}));break;case 203:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.While(e[a],{invert:!0}));break;case 204:this.$=b.addLocationDataFn(d[a-3],d[a])(new b.While(e[a-2],{invert:!0,guard:e[a]})); -break;case 205:this.$=b.addLocationDataFn(d[a-1],d[a])(e[a-1].addBody(e[a]));break;case 206:case 207:this.$=b.addLocationDataFn(d[a-1],d[a])(e[a].addBody(b.addLocationDataFn(d[a-1])(b.Block.wrap([e[a-1]]))));break;case 208:this.$=b.addLocationDataFn(d[a],d[a])(e[a]);break;case 209:this.$=b.addLocationDataFn(d[a-1],d[a])((new b.While(b.addLocationDataFn(d[a-1])(new b.BooleanLiteral("true")))).addBody(e[a]));break;case 210:this.$=b.addLocationDataFn(d[a-1],d[a])((new b.While(b.addLocationDataFn(d[a- -1])(new b.BooleanLiteral("true")))).addBody(b.addLocationDataFn(d[a])(b.Block.wrap([e[a]]))));break;case 211:case 212:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.For(e[a-1],e[a]));break;case 213:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.For(e[a],e[a-1]));break;case 214:this.$=b.addLocationDataFn(d[a-1],d[a])({source:b.addLocationDataFn(d[a])(new b.Value(e[a]))});break;case 215:this.$=b.addLocationDataFn(d[a-3],d[a])({source:b.addLocationDataFn(d[a-2])(new b.Value(e[a-2])),step:e[a]});break; -case 216:b=b.addLocationDataFn(d[a-1],d[a]);e[a].own=e[a-1].own;e[a].ownTag=e[a-1].ownTag;e[a].name=e[a-1][0];e[a].index=e[a-1][1];this.$=b(e[a]);break;case 217:this.$=b.addLocationDataFn(d[a-1],d[a])(e[a]);break;case 218:c=b.addLocationDataFn(d[a-2],d[a]);e[a].own=!0;e[a].ownTag=b.addLocationDataFn(d[a-1])(new b.Literal(e[a-1]));this.$=c(e[a]);break;case 224:this.$=b.addLocationDataFn(d[a-2],d[a])([e[a-2],e[a]]);break;case 225:this.$=b.addLocationDataFn(d[a-1],d[a])({source:e[a]});break;case 226:this.$= -b.addLocationDataFn(d[a-1],d[a])({source:e[a],object:!0});break;case 227:this.$=b.addLocationDataFn(d[a-3],d[a])({source:e[a-2],guard:e[a]});break;case 228:this.$=b.addLocationDataFn(d[a-3],d[a])({source:e[a-2],guard:e[a],object:!0});break;case 229:this.$=b.addLocationDataFn(d[a-3],d[a])({source:e[a-2],step:e[a]});break;case 230:this.$=b.addLocationDataFn(d[a-5],d[a])({source:e[a-4],guard:e[a-2],step:e[a]});break;case 231:this.$=b.addLocationDataFn(d[a-5],d[a])({source:e[a-4],step:e[a-2],guard:e[a]}); -break;case 232:this.$=b.addLocationDataFn(d[a-1],d[a])({source:e[a],from:!0});break;case 233:this.$=b.addLocationDataFn(d[a-3],d[a])({source:e[a-2],guard:e[a],from:!0});break;case 234:this.$=b.addLocationDataFn(d[a-4],d[a])(new b.Switch(e[a-3],e[a-1]));break;case 235:this.$=b.addLocationDataFn(d[a-6],d[a])(new b.Switch(e[a-5],e[a-3],e[a-1]));break;case 236:this.$=b.addLocationDataFn(d[a-3],d[a])(new b.Switch(null,e[a-1]));break;case 237:this.$=b.addLocationDataFn(d[a-5],d[a])(new b.Switch(null,e[a- -3],e[a-1]));break;case 239:this.$=b.addLocationDataFn(d[a-1],d[a])(e[a-1].concat(e[a]));break;case 240:this.$=b.addLocationDataFn(d[a-2],d[a])([[e[a-1],e[a]]]);break;case 241:this.$=b.addLocationDataFn(d[a-3],d[a])([[e[a-2],e[a-1]]]);break;case 242:this.$=b.addLocationDataFn(d[a-2],d[a])(new b.If(e[a-1],e[a],{type:e[a-2]}));break;case 243:this.$=b.addLocationDataFn(d[a-4],d[a])(e[a-4].addElse(b.addLocationDataFn(d[a-2],d[a])(new b.If(e[a-1],e[a],{type:e[a-2]}))));break;case 245:this.$=b.addLocationDataFn(d[a- -2],d[a])(e[a-2].addElse(e[a]));break;case 246:case 247:this.$=b.addLocationDataFn(d[a-2],d[a])(new b.If(e[a],b.addLocationDataFn(d[a-2])(b.Block.wrap([e[a-2]])),{type:e[a-1],statement:!0}));break;case 250:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.Op("-",e[a]));break;case 251:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.Op("+",e[a]));break;case 252:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.Op("--",e[a]));break;case 253:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.Op("++",e[a]));break;case 254:this.$= -b.addLocationDataFn(d[a-1],d[a])(new b.Op("--",e[a-1],null,!0));break;case 255:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.Op("++",e[a-1],null,!0));break;case 256:this.$=b.addLocationDataFn(d[a-1],d[a])(new b.Existence(e[a-1]));break;case 257:this.$=b.addLocationDataFn(d[a-2],d[a])(new b.Op("+",e[a-2],e[a]));break;case 258:this.$=b.addLocationDataFn(d[a-2],d[a])(new b.Op("-",e[a-2],e[a]));break;case 259:case 260:case 261:case 262:case 263:case 264:case 265:case 266:case 267:case 268:this.$=b.addLocationDataFn(d[a- -2],d[a])(new b.Op(e[a-1],e[a-2],e[a]));break;case 269:d=b.addLocationDataFn(d[a-2],d[a]);e="!"===e[a-1].charAt(0)?(new b.Op(e[a-1].slice(1),e[a-2],e[a])).invert():new b.Op(e[a-1],e[a-2],e[a]);this.$=d(e);break;case 270:this.$=b.addLocationDataFn(d[a-2],d[a])(new b.Assign(e[a-2],e[a],e[a-1]));break;case 271:this.$=b.addLocationDataFn(d[a-4],d[a])(new b.Assign(e[a-4],e[a-1],e[a-3]));break;case 272:this.$=b.addLocationDataFn(d[a-3],d[a])(new b.Assign(e[a-3],e[a],e[a-2]));break;case 273:this.$=b.addLocationDataFn(d[a- -2],d[a])(new b.Extends(e[a-2],e[a]))}},table:[{1:[2,1],3:1,4:2,5:3,7:4,8:5,9:6,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:u,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y, -158:V,159:Z,160:K,161:ba,162:R},{1:[3]},{1:[2,2],6:xa},a(ua,[2,3]),a(ua,[2,6],{141:77,132:102,138:103,133:z,135:B,139:C,156:qa,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a(ua,[2,7],{141:77,132:105,138:106,133:z,135:B,139:C,156:ya}),a(ua,[2,8]),a(ta,[2,14],{109:107,78:108,86:114,40:Ja,41:Ja,114:Ja,82:Ta,83:Ga,84:Ua,85:Fa,87:Pa,90:Qa,113:Na}),a(ta,[2,15],{86:114,109:117,78:118,82:Ta,83:Ga,84:Ua,85:Fa,87:Pa,90:Qa,113:Na,114:Ja}),a(ta,[2,16]),a(ta, -[2,17]),a(ta,[2,18]),a(ta,[2,19]),a(ta,[2,20]),a(ta,[2,21]),a(ta,[2,22]),a(ta,[2,23]),a(ta,[2,24]),a(ta,[2,25]),a(ta,[2,26]),a(za,[2,9]),a(za,[2,10]),a(za,[2,11]),a(za,[2,12]),a(za,[2,13]),a([1,6,32,42,131,133,135,139,156,163,164,165,166,167,168,169,170,171,172,173,174],na,{15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,10:20,11:21,13:23,14:24,54:26,47:27,79:28,80:29,81:30,111:31,66:33,77:40,154:41,132:43,136:44,138:45,74:53,75:54,37:55,43:57,33:70,60:71,141:77,39:80,7:120, -8:122,12:c,28:fa,29:Ha,34:f,38:D,40:l,41:w,44:G,45:F,48:x,49:v,50:M,51:N,52:J,53:r,61:[1,119],62:O,63:m,67:k,68:t,92:p,95:h,97:y,105:P,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,137:W,149:Q,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R}),a(Aa,Ia,{55:[1,124]}),a(Aa,[2,95]),a(Aa,[2,96]),a(Aa,[2,97]),a(Aa,[2,98]),a(b,[2,162]),a([6,31,65,70],n,{64:125,71:126,72:127,33:129,60:130,74:131,75:132,34:f,73:pa,92:p,118:e,119:d}),{30:135,31:va},{7:137,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11, -20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:138,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16, -25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:139,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70, -34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:140,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w, -43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{15:142,16:143,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:144,60:71,74:53,75:54,77:141,79:28,80:29,81:30,92:p,111:31,112:A,117:H,118:U,119:T, -130:S},{15:142,16:143,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:144,60:71,74:53,75:54,77:145,79:28,80:29,81:30,92:p,111:31,112:A,117:H,118:U,119:T,130:S},a(Ka,wa,{96:[1,149],161:[1,146],162:[1,147],175:[1,148]}),a(ta,[2,244],{151:[1,150]}),{30:151,31:va},{30:152,31:va},a(ta,[2,208]),{30:153,31:va},{7:154,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,31:[1,155],33:70,34:f,37:55, -38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},a(Gb,[2,115],{47:27,79:28,80:29,81:30,111:31,74:53,75:54,37:55,43:57,33:70,60:71,39:80,15:142,16:143,54:144,30:156,77:158,31:va,34:f,38:D,40:l,41:w,44:G,45:F,48:x,49:v,50:M, -51:N,52:J,53:r,92:p,96:[1,157],112:A,117:H,118:U,119:T,130:S}),{7:159,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y, -158:V,159:Z,160:K,161:ba,162:R},a(za,ab,{15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,10:20,11:21,13:23,14:24,54:26,47:27,79:28,80:29,81:30,111:31,66:33,77:40,154:41,132:43,136:44,138:45,74:53,75:54,37:55,43:57,33:70,60:71,141:77,39:80,8:122,7:160,12:c,28:fa,34:f,38:D,40:l,41:w,44:G,45:F,48:x,49:v,50:M,51:N,52:J,53:r,61:E,62:O,63:m,67:k,68:t,92:p,95:h,97:y,105:P,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,137:W,149:Q,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R}),a([1,6, -31,32,42,70,94,131,133,135,139,156],[2,66]),{33:165,34:f,39:161,40:l,41:w,92:[1,164],98:162,99:163,104:Hb},{25:168,33:169,34:f,92:[1,167],95:h,103:[1,170],107:[1,171]},a(Ka,[2,92]),a(Ka,[2,93]),a(Aa,[2,40]),a(Aa,[2,41]),a(Aa,[2,42]),a(Aa,[2,43]),a(Aa,[2,44]),a(Aa,[2,45]),a(Aa,[2,46]),a(Aa,[2,47]),{4:172,5:3,7:4,8:5,9:6,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:u,31:[1,173],33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27, -48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:174,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,31:bb,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J, -53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,73:Wa,74:53,75:54,76:179,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,116:176,117:H,118:U,119:T,120:Ib,123:177,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},a(Aa,[2,169]),a(Aa,[2,170],{35:181,36:Oa}),a([1,6,31,32,42,46,65,70,73,82,83,84,85,87,89,90,94,113,115,120,122,131,133,134,135,139,140,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],[2,163], -{110:183,114:ub}),{31:[2,69]},{31:[2,70]},a(La,[2,87]),a(La,[2,90]),{7:185,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X, -157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:186,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba, -162:R},{7:187,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:189,8:122,10:20,11:21, -12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,30:188,31:va,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{33:194,34:f,60:195,74:196,75:197,80:190, -92:p,118:e,119:T,143:191,144:[1,192],145:193},{142:198,146:[1,199],147:[1,200],148:[1,201]},a([6,31,70,94],Jb,{39:80,93:202,56:203,57:204,59:205,11:206,37:207,33:208,35:209,60:210,34:f,36:Oa,38:D,40:l,41:w,62:O,118:e}),a(Kb,[2,34]),a(Kb,[2,35]),a(Aa,[2,38]),{15:142,16:211,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:144,60:71,74:53,75:54,77:212,79:28,80:29,81:30,92:p,111:31,112:A,117:H,118:U,119:T,130:S},a([1,6,29,31,32,40,41,42,55,58,65,70,73,82,83, -84,85,87,89,90,94,96,102,113,114,115,120,122,131,133,134,135,139,140,146,147,148,156,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175],[2,32]),a(Lb,[2,36]),{4:213,5:3,7:4,8:5,9:6,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:u,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31, -112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},a(ua,[2,5],{7:4,8:5,9:6,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,10:20,11:21,13:23,14:24,54:26,47:27,79:28,80:29,81:30,111:31,66:33,77:40,154:41,132:43,136:44,138:45,74:53,75:54,37:55,43:57,33:70,60:71,141:77,39:80,5:214,12:c,28:u,34:f,38:D,40:l,41:w,44:G,45:F,48:x,49:v,50:M,51:N,52:J,53:r,61:E,62:O,63:m,67:k,68:t, -92:p,95:h,97:y,105:P,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,133:z,135:B,137:W,139:C,149:Q,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R}),a(ta,[2,256]),{7:215,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I, -129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:216,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B, -136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:217,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77, -149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:218,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V, -159:Z,160:K,161:ba,162:R},{7:219,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:220, -8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:221,8:122,10:20,11:21,12:c,13:23, -14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:222,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11, -20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:223,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16, -25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:224,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70, -34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:225,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w, -43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:226,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v, -50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:227,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71, -61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:228,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t, -74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},a(ta,[2,207]),a(ta,[2,212]),{7:229,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53, -75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},a(ta,[2,206]),a(ta,[2,211]),{39:230,40:l,41:w,110:231,114:ub},a(La,[2,88]),a(Mb,[2,166]),{35:232,36:Oa},{35:233,36:Oa},a(La,[2,103],{35:234,36:Oa}),{35:235,36:Oa},a(La,[2,104]),{7:237,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19, -28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,73:Nb,74:53,75:54,77:40,79:28,80:29,81:30,88:236,91:238,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,121:239,122:vb,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{86:242,87:Pa,90:Qa},{110:243,114:ub},a(La,[2,89]),a(ua,[2,65],{15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15, -24:16,25:17,26:18,27:19,10:20,11:21,13:23,14:24,54:26,47:27,79:28,80:29,81:30,111:31,66:33,77:40,154:41,132:43,136:44,138:45,74:53,75:54,37:55,43:57,33:70,60:71,141:77,39:80,8:122,7:244,12:c,28:fa,34:f,38:D,40:l,41:w,44:G,45:F,48:x,49:v,50:M,51:N,52:J,53:r,61:E,62:O,63:m,67:k,68:t,92:p,95:h,97:y,105:P,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,133:ab,135:ab,139:ab,156:ab,137:W,149:Q,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R}),a(Ma,[2,28],{141:77,132:102,138:103,159:ka,160:ca,163:ea,164:ga,165:ha, -166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),{7:245,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y, -158:V,159:Z,160:K,161:ba,162:R},{132:105,133:z,135:B,138:106,139:C,141:77,156:ya},a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,163,164,165,166,167,168,169,170,171,172,173,174],na,{15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,10:20,11:21,13:23,14:24,54:26,47:27,79:28,80:29,81:30,111:31,66:33,77:40,154:41,132:43,136:44,138:45,74:53,75:54,37:55,43:57,33:70,60:71,141:77,39:80,7:120,8:122,12:c,28:fa,29:Ha,34:f,38:D,40:l,41:w,44:G,45:F,48:x,49:v,50:M, -51:N,52:J,53:r,61:E,62:O,63:m,67:k,68:t,92:p,95:h,97:y,105:P,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,137:W,149:Q,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R}),{6:[1,247],7:246,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,31:[1,248],33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31, -112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},a([6,31],Ea,{69:251,65:[1,249],70:Ob}),a(Va,[2,74]),a(Va,[2,78],{55:[1,253],73:[1,252]}),a(Va,[2,81]),a(hb,[2,82]),a(hb,[2,83]),a(hb,[2,84]),a(hb,[2,85]),{35:181,36:Oa},{7:254,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,31:bb,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F, -47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,73:Wa,74:53,75:54,76:179,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,116:176,117:H,118:U,119:T,120:Ib,123:177,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},a(ta,[2,68]),{4:256,5:3,7:4,8:5,9:6,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:u,32:[1,255],33:70,34:f,37:55, -38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,159,160,164,165,166,167,168,169,170,171,172,173,174],[2,248],{141:77,132:102,138:103,163:ea}),a(cb, -[2,249],{141:77,132:102,138:103,163:ea,165:ha}),a(cb,[2,250],{141:77,132:102,138:103,163:ea,165:ha}),a(cb,[2,251],{141:77,132:102,138:103,163:ea,165:ha}),a(ta,[2,252],{40:wa,41:wa,82:wa,83:wa,84:wa,85:wa,87:wa,90:wa,113:wa,114:wa}),a(Mb,Ja,{109:107,78:108,86:114,82:Ta,83:Ga,84:Ua,85:Fa,87:Pa,90:Qa,113:Na}),{78:118,82:Ta,83:Ga,84:Ua,85:Fa,86:114,87:Pa,90:Qa,109:117,113:Na,114:Ja},a(Pb,Ia),a(ta,[2,253],{40:wa,41:wa,82:wa,83:wa,84:wa,85:wa,87:wa,90:wa,113:wa,114:wa}),a(ta,[2,254]),a(ta,[2,255]),{6:[1, -259],7:257,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,31:[1,258],33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:260,8:122,10:20, -11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{30:261,31:va,155:[1,262]},a(ta,[2,191],{126:263, -127:[1,264],128:[1,265]}),a(ta,[2,205]),a(ta,[2,213]),{31:[1,266],132:102,133:z,135:B,138:103,139:C,141:77,156:qa,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia},{150:267,152:268,153:ib},a(ta,[2,116]),{7:270,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k, -68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},a(Gb,[2,119],{30:271,31:va,40:wa,41:wa,82:wa,83:wa,84:wa,85:wa,87:wa,90:wa,113:wa,114:wa,96:[1,272]}),a(Ma,[2,198],{141:77,132:102,138:103,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a(za,db,{141:77,132:102,138:103,159:ka,160:ca, -163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a(za,[2,123]),{29:[1,273],70:[1,274]},{29:[1,275]},{31:jb,33:280,34:f,94:[1,276],100:277,101:278,103:Ya},a([29,70],[2,139]),{102:[1,282]},{31:wb,33:287,34:f,94:[1,283],103:eb,106:284,108:285},a(za,[2,143]),{55:[1,289]},{7:290,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M, -51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{29:[1,291]},{6:xa,131:[1,292]},{4:293,5:3,7:4,8:5,9:6,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:u,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x, -49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},a([6,31,70,120],Qb,{141:77,132:102,138:103,121:294,73:[1,295],122:vb,133:z,135:B,139:C,156:qa,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a(xb,[2,172]),a([6,31,120], -Ea,{69:296,70:kb}),a(Ra,[2,181]),{7:254,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,31:bb,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,73:Wa,74:53,75:54,76:179,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,116:298,117:H,118:U,119:T,123:177,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X, -157:Y,158:V,159:Z,160:K,161:ba,162:R},a(Ra,[2,187]),a(Ra,[2,188]),a(Rb,[2,171]),a(Rb,[2,33]),a(b,[2,164]),{7:254,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,31:bb,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,73:Wa,74:53,75:54,76:179,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,115:[1,299],116:300,117:H,118:U,119:T,123:177,125:I, -129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{30:301,31:va,132:102,133:z,135:B,138:103,139:C,141:77,156:qa,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia},a(Sb,[2,201],{141:77,132:102,138:103,133:z,134:[1,302],135:B,139:C,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a(Sb,[2,203],{141:77,132:102,138:103,133:z,134:[1,303], -135:B,139:C,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a(ta,[2,209]),a(Za,[2,210],{141:77,132:102,138:103,133:z,135:B,139:C,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],[2,214],{140:[1,304]}),a(lb,[2,217]),{33:194,34:f,60:195,74:196,75:197,92:p,118:e,119:d,143:305,145:193}, -a(lb,[2,223],{70:[1,306]}),a(mb,[2,219]),a(mb,[2,220]),a(mb,[2,221]),a(mb,[2,222]),a(ta,[2,216]),{7:307,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C, -141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:308,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y, -158:V,159:Z,160:K,161:ba,162:R},{7:309,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R}, -a(nb,Ea,{69:310,70:Tb}),a(Ba,[2,111]),a(Ba,[2,51],{58:[1,312]}),a(Ub,[2,60],{55:[1,313]}),a(Ba,[2,56]),a(Ub,[2,61]),a(yb,[2,57]),a(yb,[2,58]),a(yb,[2,59]),{46:[1,314],78:118,82:Ta,83:Ga,84:Ua,85:Fa,86:114,87:Pa,90:Qa,109:117,113:Na,114:Ja},a(Pb,wa),{6:xa,42:[1,315]},a(ua,[2,4]),a(Vb,[2,257],{141:77,132:102,138:103,163:ea,164:ga,165:ha}),a(Vb,[2,258],{141:77,132:102,138:103,163:ea,164:ga,165:ha}),a(cb,[2,259],{141:77,132:102,138:103,163:ea,165:ha}),a(cb,[2,260],{141:77,132:102,138:103,163:ea,165:ha}), -a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,166,167,168,169,170,171,172,173,174],[2,261],{141:77,132:102,138:103,159:ka,160:ca,163:ea,164:ga,165:ha}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,167,168,169,170,171,172,173],[2,262],{141:77,132:102,138:103,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,174:ia}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,168,169,170,171,172,173],[2,263],{141:77,132:102,138:103,159:ka,160:ca, -163:ea,164:ga,165:ha,166:la,167:oa,174:ia}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,169,170,171,172,173],[2,264],{141:77,132:102,138:103,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,174:ia}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,170,171,172,173],[2,265],{141:77,132:102,138:103,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,174:ia}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,171,172, -173],[2,266],{141:77,132:102,138:103,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,174:ia}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,172,173],[2,267],{141:77,132:102,138:103,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,174:ia}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,173],[2,268],{141:77,132:102,138:103,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma, -174:ia}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,167,168,169,170,171,172,173,174],[2,269],{141:77,132:102,138:103,159:ka,160:ca,163:ea,164:ga,165:ha,166:la}),a(Za,[2,247],{141:77,132:102,138:103,133:z,135:B,139:C,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a(Za,[2,246],{141:77,132:102,138:103,133:z,135:B,139:C,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a(b, -[2,159]),a(b,[2,160]),a(La,[2,99]),a(La,[2,100]),a(La,[2,101]),a(La,[2,102]),{89:[1,316]},{73:Nb,89:[2,107],121:317,122:vb,132:102,133:z,135:B,138:103,139:C,141:77,156:qa,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia},{89:[2,108]},{7:318,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26, -60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,89:[2,180],92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},a(Wb,[2,174]),a(Wb,Xb),a(La,[2,106]),a(b,[2,161]),a(ua,[2,64],{141:77,132:102,138:103,133:db,135:db,139:db,156:db,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a(Ma,[2,29],{141:77,132:102, -138:103,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a(Ma,[2,48],{141:77,132:102,138:103,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),{7:319,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53, -75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:320,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30, -92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{66:321,67:k,68:t},a(Sa,fb,{72:127,33:129,60:130,74:131,75:132,71:322,34:f,73:pa,92:p,118:e,119:d}),{6:Yb,31:Zb},a(Va,[2,79]),{7:325,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M, -51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},a(Ra,Qb,{141:77,132:102,138:103,73:[1,326],133:z,135:B,139:C,156:qa,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a($b,[2,30]),{6:xa,32:[1,327]},a(Ma,[2,270],{141:77,132:102, -138:103,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),{7:328,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W, -138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:329,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41, -155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},a(Ma,[2,273],{141:77,132:102,138:103,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a(ta,[2,245]),{7:330,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y, -105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},a(ta,[2,192],{127:[1,331]}),{30:332,31:va},{30:335,31:va,33:333,34:f,75:334,92:p},{150:336,152:268,153:ib},{32:[1,337],151:[1,338],152:339,153:ib},a(ob,[2,238]),{7:341,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F, -47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,124:340,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},a(ac,[2,117],{141:77,132:102,138:103,30:342,31:va,133:z,135:B,139:C,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a(ta,[2,120]),{7:343,8:122,10:20, -11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{39:344,40:l,41:w},{92:[1,346],99:345,104:Hb},{39:347, -40:l,41:w},{29:[1,348]},a(nb,Ea,{69:349,70:pb}),a(Ba,[2,130]),{31:jb,33:280,34:f,100:351,101:278,103:Ya},a(Ba,[2,135],{102:[1,352]}),a(Ba,[2,137],{102:[1,353]}),{33:354,34:f},a(za,[2,141]),a(nb,Ea,{69:355,70:zb}),a(Ba,[2,150]),{31:wb,33:287,34:f,103:eb,106:357,108:285},a(Ba,[2,155],{102:[1,358]}),a(Ba,[2,158]),{6:[1,360],7:359,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,31:[1,361],33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G, -45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},a(Ab,[2,147],{141:77,132:102,138:103,133:z,135:B,139:C,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),{39:362,40:l,41:w},a(Aa,[2,199]),{6:xa,32:[1,363]}, -{7:364,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},a([12,28,34,38,40,41,44,45,48, -49,50,51,52,53,61,62,63,67,68,92,95,97,105,112,117,118,119,125,129,130,133,135,137,139,149,155,157,158,159,160,161,162],Xb,{6:gb,31:gb,70:gb,120:gb}),{6:qb,31:rb,120:[1,365]},a([6,31,32,115,120],fb,{15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,10:20,11:21,13:23,14:24,54:26,47:27,79:28,80:29,81:30,111:31,66:33,77:40,154:41,132:43,136:44,138:45,74:53,75:54,37:55,43:57,33:70,60:71,141:77,39:80,8:122,76:179,7:254,123:368,12:c,28:fa,34:f,38:D,40:l,41:w,44:G,45:F,48:x,49:v, -50:M,51:N,52:J,53:r,61:E,62:O,63:m,67:k,68:t,73:Wa,92:p,95:h,97:y,105:P,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,133:z,135:B,137:W,139:C,149:Q,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R}),a(Sa,Ea,{69:369,70:kb}),a(b,[2,167]),a([6,31,115],Ea,{69:370,70:kb}),a(bc,[2,242]),{7:371,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E, -62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:372,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53, -75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:373,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30, -92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},a(lb,[2,218]),{33:194,34:f,60:195,74:196,75:197,92:p,118:e,119:d,145:374},a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,135,139,156],[2,225],{141:77,132:102,138:103,134:[1,375],140:[1,376],159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a(Bb,[2,226],{141:77,132:102, -138:103,134:[1,377],159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a(Bb,[2,232],{141:77,132:102,138:103,134:[1,378],159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),{6:cc,31:dc,94:[1,379]},a(Cb,fb,{39:80,57:204,59:205,11:206,37:207,33:208,35:209,60:210,56:382,34:f,36:Oa,38:D,40:l,41:w,62:O,118:e}),{7:383,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17, -26:18,27:19,28:fa,31:[1,384],33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:385,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa, -31:[1,386],33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},a(Aa,[2,39]),a(Lb,[2,37]),a(La,[2,105]),{7:387,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17, -26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,89:[2,178],92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{89:[2,179],132:102,133:z,135:B,138:103,139:C,141:77,156:qa,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra, -171:da,172:ma,173:sa,174:ia},a(Ma,[2,49],{141:77,132:102,138:103,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),{32:[1,388],132:102,133:z,135:B,138:103,139:C,141:77,156:qa,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia},{30:389,31:va},a(Va,[2,75]),{33:129,34:f,60:130,71:390,72:127,73:pa,74:131,75:132,92:p,118:e,119:d},a(ec,n,{71:126,72:127,33:129,60:130,74:131,75:132,64:391,34:f,73:pa,92:p,118:e, -119:d}),a(Va,[2,80],{141:77,132:102,138:103,133:z,135:B,139:C,156:qa,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a(Ra,gb),a($b,[2,31]),{32:[1,392],132:102,133:z,135:B,138:103,139:C,141:77,156:qa,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia},a(Ma,[2,272],{141:77,132:102,138:103,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),{30:393,31:va,132:102, -133:z,135:B,138:103,139:C,141:77,156:qa,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia},{30:394,31:va},a(ta,[2,193]),{30:395,31:va},{30:396,31:va},a(Db,[2,197]),{32:[1,397],151:[1,398],152:339,153:ib},a(ta,[2,236]),{30:399,31:va},a(ob,[2,239]),{30:400,31:va,70:[1,401]},a(fc,[2,189],{141:77,132:102,138:103,133:z,135:B,139:C,156:qa,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a(ta,[2,118]),a(ac,[2, -121],{141:77,132:102,138:103,30:402,31:va,133:z,135:B,139:C,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a(za,[2,124]),{29:[1,403]},{31:jb,33:280,34:f,100:404,101:278,103:Ya},a(za,[2,125]),{39:405,40:l,41:w},{6:sb,31:tb,94:[1,406]},a(Cb,fb,{33:280,101:409,34:f,103:Ya}),a(Sa,Ea,{69:410,70:pb}),{33:411,34:f},{33:412,34:f},{29:[2,140]},{6:Eb,31:Fb,94:[1,413]},a(Cb,fb,{33:287,108:416,34:f,103:eb}),a(Sa,Ea,{69:417,70:zb}),{33:418,34:f,103:[1,419]}, -a(Ab,[2,144],{141:77,132:102,138:103,133:z,135:B,139:C,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),{7:420,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I, -129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:421,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B, -136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},a(za,[2,148]),{131:[1,422]},{120:[1,423],132:102,133:z,135:B,138:103,139:C,141:77,156:qa,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia},a(xb,[2,173]),{7:254,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r, -54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,73:Wa,74:53,75:54,76:179,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,123:424,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:254,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,31:bb,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26, -60:71,61:E,62:O,63:m,66:33,67:k,68:t,73:Wa,74:53,75:54,76:179,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,116:425,117:H,118:U,119:T,123:177,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},a(Ra,[2,182]),{6:qb,31:rb,32:[1,426]},{6:qb,31:rb,115:[1,427]},a(Za,[2,202],{141:77,132:102,138:103,133:z,135:B,139:C,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a(Za, -[2,204],{141:77,132:102,138:103,133:z,135:B,139:C,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a(Za,[2,215],{141:77,132:102,138:103,133:z,135:B,139:C,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a(lb,[2,224]),{7:428,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x, -49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:429,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26, -60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:430,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k, -68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:431,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28, -80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},a(xb,[2,109]),{11:206,33:208,34:f,35:209,36:Oa,37:207,38:D,39:80,40:l,41:w,56:432,57:204,59:205,60:210,62:O,118:e},a(ec,Jb,{39:80,56:203,57:204,59:205,11:206,37:207,33:208,35:209,60:210,93:433,34:f,36:Oa,38:D,40:l,41:w,62:O,118:e}),a(Ba,[2,112]),a(Ba,[2,52],{141:77,132:102,138:103,133:z,135:B,139:C,156:qa,159:ka, -160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),{7:434,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77, -149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},a(Ba,[2,54],{141:77,132:102,138:103,133:z,135:B,139:C,156:qa,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),{7:435,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29, -81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{89:[2,177],132:102,133:z,135:B,138:103,139:C,141:77,156:qa,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia},a(ta,[2,50]),a(ta,[2,67]),a(Va,[2,76]),a(Sa,Ea,{69:436,70:Ob}),a(ta,[2,271]),a(bc,[2,243]),a(ta,[2,194]),a(Db,[2,195]),a(Db,[2,196]),a(ta,[2,234]),{30:437,31:va}, -{32:[1,438]},a(ob,[2,240],{6:[1,439]}),{7:440,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba, -162:R},a(ta,[2,122]),{39:441,40:l,41:w},a(nb,Ea,{69:442,70:pb}),a(za,[2,126]),{29:[1,443]},{33:280,34:f,101:444,103:Ya},{31:jb,33:280,34:f,100:445,101:278,103:Ya},a(Ba,[2,131]),{6:sb,31:tb,32:[1,446]},a(Ba,[2,136]),a(Ba,[2,138]),a(za,[2,142],{29:[1,447]}),{33:287,34:f,103:eb,108:448},{31:wb,33:287,34:f,103:eb,106:449,108:285},a(Ba,[2,151]),{6:Eb,31:Fb,32:[1,450]},a(Ba,[2,156]),a(Ba,[2,157]),a(Ab,[2,145],{141:77,132:102,138:103,133:z,135:B,139:C,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L, -169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),{32:[1,451],132:102,133:z,135:B,138:103,139:C,141:77,156:qa,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia},a(Aa,[2,200]),a(Aa,[2,176]),a(Ra,[2,183]),a(Sa,Ea,{69:452,70:kb}),a(Ra,[2,184]),a(b,[2,168]),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,156],[2,227],{141:77,132:102,138:103,140:[1,453],159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}), -a(Bb,[2,229],{141:77,132:102,138:103,134:[1,454],159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a(Ma,[2,228],{141:77,132:102,138:103,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a(Ma,[2,233],{141:77,132:102,138:103,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a(Ba,[2,113]),a(Sa,Ea,{69:455,70:Tb}),{32:[1,456],132:102,133:z,135:B,138:103,139:C, -141:77,156:qa,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia},{32:[1,457],132:102,133:z,135:B,138:103,139:C,141:77,156:qa,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia},{6:Yb,31:Zb,32:[1,458]},{32:[1,459]},a(ta,[2,237]),a(ob,[2,241]),a(fc,[2,190],{141:77,132:102,138:103,133:z,135:B,139:C,156:qa,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a(za, -[2,128]),{6:sb,31:tb,94:[1,460]},{39:461,40:l,41:w},a(Ba,[2,132]),a(Sa,Ea,{69:462,70:pb}),a(Ba,[2,133]),{39:463,40:l,41:w},a(Ba,[2,152]),a(Sa,Ea,{69:464,70:zb}),a(Ba,[2,153]),a(za,[2,146]),{6:qb,31:rb,32:[1,465]},{7:466,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30, -92:p,95:h,97:y,105:P,111:31,112:A,117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{7:467,8:122,10:20,11:21,12:c,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:fa,33:70,34:f,37:55,38:D,39:80,40:l,41:w,43:57,44:G,45:F,47:27,48:x,49:v,50:M,51:N,52:J,53:r,54:26,60:71,61:E,62:O,63:m,66:33,67:k,68:t,74:53,75:54,77:40,79:28,80:29,81:30,92:p,95:h,97:y,105:P,111:31,112:A, -117:H,118:U,119:T,125:I,129:aa,130:S,132:43,133:z,135:B,136:44,137:W,138:45,139:C,141:77,149:Q,154:41,155:X,157:Y,158:V,159:Z,160:K,161:ba,162:R},{6:cc,31:dc,32:[1,468]},a(Ba,[2,53]),a(Ba,[2,55]),a(Va,[2,77]),a(ta,[2,235]),{29:[1,469]},a(za,[2,127]),{6:sb,31:tb,32:[1,470]},a(za,[2,149]),{6:Eb,31:Fb,32:[1,471]},a(Ra,[2,185]),a(Ma,[2,230],{141:77,132:102,138:103,159:ka,160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a(Ma,[2,231],{141:77,132:102,138:103,159:ka, -160:ca,163:ea,164:ga,165:ha,166:la,167:oa,168:L,169:ja,170:ra,171:da,172:ma,173:sa,174:ia}),a(Ba,[2,114]),{39:472,40:l,41:w},a(Ba,[2,134]),a(Ba,[2,154]),a(za,[2,129])],defaultActions:{68:[2,69],69:[2,70],238:[2,108],354:[2,140]},parseError:function(a,b){if(b.recoverable)this.trace(a);else{var d=function(a,b){this.message=a;this.hash=b};d.prototype=Error;throw new d(a,b);}},parse:function(a){var b=[0],d=[null],e=[],n=this.table,pa="",c=0,f=0,g=0,h=e.slice.call(arguments,1),va=Object.create(this.lexer), -k={},Ka;for(Ka in this.yy)Object.prototype.hasOwnProperty.call(this.yy,Ka)&&(k[Ka]=this.yy[Ka]);va.setInput(a,k);k.lexer=va;k.parser=this;"undefined"==typeof va.yylloc&&(va.yylloc={});Ka=va.yylloc;e.push(Ka);var m=va.options&&va.options.ranges;this.parseError="function"===typeof k.parseError?k.parseError:Object.getPrototypeOf(this).parseError;for(var t,p,Ia,l,r={},y,q;;){Ia=b[b.length-1];if(this.defaultActions[Ia])l=this.defaultActions[Ia];else{if(null===t||"undefined"==typeof t)t=va.lex()||1,"number"!== -typeof t&&(t=this.symbols_[t]||t);l=n[Ia]&&n[Ia][t]}if("undefined"===typeof l||!l.length||!l[0]){var wa;q=[];for(y in n[Ia])this.terminals_[y]&&2=h?this.wrapInBraces(n):n};b.prototype.compileRoot=function(a){var b,e,d,n,c;a.indent=a.bare?"":ca;a.level=A;this.spaced= -!0;a.scope=new K(null,this,null,null!=(d=a.referencedVars)?d:[]);c=a.locals||[];d=0;for(e=c.length;d=y?this.wrapInBraces(b):b};return b}(S);g.StringLiteral=$a=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}na(b,a);return b}(H);g.RegexLiteral=Y=function(a){function b(){return b.__super__.constructor.apply(this,arguments)} -na(b,a);return b}(H);g.PassthroughLiteral=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}na(b,a);return b}(H);g.IdentifierLiteral=r=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}na(b,a);b.prototype.isAssignable=ja;return b}(H);g.PropertyName=Q=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}na(b,a);b.prototype.isAssignable=ja;return b}(H);g.StatementLiteral=ya=function(a){function b(){return b.__super__.constructor.apply(this, -arguments)}na(b,a);b.prototype.isStatement=ja;b.prototype.makeReturn=ea;b.prototype.jumps=function(a){if("break"===this.value&&!(null!=a&&a.loop||null!=a&&a.block)||"continue"===this.value&&(null==a||!a.loop))return this};b.prototype.compileNode=function(a){return[this.makeCode(""+this.tab+this.value+";")]};return b}(H);g.ThisLiteral=ga=function(a){function b(){b.__super__.constructor.call(this,"this")}na(b,a);b.prototype.compileNode=function(a){var b;a=null!=(b=a.scope.method)&&b.bound?a.scope.method.context: -this.value;return[this.makeCode(a)]};return b}(H);g.UndefinedLiteral=oa=function(a){function b(){b.__super__.constructor.call(this,"undefined")}na(b,a);b.prototype.compileNode=function(a){return[this.makeCode(a.level>=t?"(void 0)":"void 0")]};return b}(H);g.NullLiteral=aa=function(a){function b(){b.__super__.constructor.call(this,"null")}na(b,a);return b}(H);g.BooleanLiteral=Ca=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}na(b,a);return b}(H);g.Return=V=function(a){function b(a){this.expression= -a}na(b,a);b.prototype.children=["expression"];b.prototype.isStatement=ja;b.prototype.makeReturn=ea;b.prototype.jumps=ea;b.prototype.compileToFragments=function(a,pa){var e,d;e=null!=(d=this.expression)?d.makeReturn():void 0;return!e||e instanceof b?b.__super__.compileToFragments.call(this,a,pa):e.compileToFragments(a,pa)};b.prototype.compileNode=function(a){var b;b=[];b.push(this.makeCode(this.tab+("return"+(this.expression?" ":""))));this.expression&&(b=b.concat(this.expression.compileToFragments(a, -P)));b.push(this.makeCode(";"));return b};return b}(a);g.YieldReturn=ra=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}na(b,a);b.prototype.compileNode=function(a){null==a.scope.parent&&this.error("yield can only occur inside functions");return b.__super__.compileNode.apply(this,arguments)};return b}(V);g.Value=L=function(a){function b(a,pa,e){if(!pa&&a instanceof b)return a;this.base=a;this.properties=pa||[];e&&(this[e]=!0);return this}na(b,a);b.prototype.children=["base", -"properties"];b.prototype.add=function(a){this.properties=this.properties.concat(a);return this};b.prototype.hasProperties=function(){return!!this.properties.length};b.prototype.bareLiteral=function(a){return!this.properties.length&&this.base instanceof a};b.prototype.isArray=function(){return this.bareLiteral(qa)};b.prototype.isRange=function(){return this.bareLiteral(X)};b.prototype.isComplex=function(){return this.hasProperties()||this.base.isComplex()};b.prototype.isAssignable=function(){return this.hasProperties()|| -this.base.isAssignable()};b.prototype.isNumber=function(){return this.bareLiteral(S)};b.prototype.isString=function(){return this.bareLiteral($a)};b.prototype.isRegex=function(){return this.bareLiteral(Y)};b.prototype.isUndefined=function(){return this.bareLiteral(oa)};b.prototype.isNull=function(){return this.bareLiteral(aa)};b.prototype.isBoolean=function(){return this.bareLiteral(Ca)};b.prototype.isAtomic=function(){var a,b,e,d;d=this.properties.concat(this.base);a=0;for(b=d.length;athis.properties.length&&!this.base.isComplex()&&(null==d||!d.isComplex()))return[this,this];n=new b(this.base,this.properties.slice(0,-1));n.isComplex()&&(e=new r(a.scope.freeVariable("base")),n=new b(new C(new q(e,n))));if(!d)return[n,e];d.isComplex()&&(c=new r(a.scope.freeVariable("name")),d=new m(new q(c,d.index)),c=new m(c));return[n.add(d),new b(e||n.base,[c||d])]};b.prototype.compileNode=function(a){var b,e,d,n,c;this.base.front=this.front;c=this.properties; -b=this.base.compileToFragments(a,c.length?t:null);c.length&&Z.test(Da(b))&&b.push(this.makeCode("."));e=0;for(d=c.length;e=Math.abs(this.fromNum-this.toNum))return b=function(){h=[];for(var a=f=this.fromNum,b=this.toNum;f<=b?a<=b:a>=b;f<=b?a++:a--)h.push(a);return h}.apply(this),this.exclusive&& -b.pop(),[this.makeCode("["+b.join(", ")+"]")];c=this.tab+ca;d=a.scope.freeVariable("i",{single:!0});g=a.scope.freeVariable("results");n="\n"+c+g+" \x3d [];";e?(a.index=d,e=Da(this.compileNode(a))):(k=d+" \x3d "+this.fromC+(this.toC!==this.toVar?", "+this.toC:""),e=this.fromVar+" \x3c\x3d "+this.toVar,e="var "+k+"; "+e+" ? "+d+" \x3c"+this.equals+" "+this.toVar+" : "+d+" \x3e"+this.equals+" "+this.toVar+"; "+e+" ? "+d+"++ : "+d+"--");d="{ "+g+".push("+d+"); }\n"+c+"return "+g+";\n"+a.indent;a=function(a){return null!= -a?a.contains(Ja):void 0};if(a(this.from)||a(this.to))b=", arguments";return[this.makeCode("(function() {"+n+"\n"+c+"for ("+e+")"+d+"}).apply(this"+(null!=b?b:"")+")")]};return b}(a);g.Slice=ba=function(a){function b(a){this.range=a;b.__super__.constructor.call(this)}na(b,a);b.prototype.children=["range"];b.prototype.compileNode=function(a){var b,e,d,c,n;b=this.range;c=b.to;d=(b=b.from)&&b.compileToFragments(a,P)||[this.makeCode("0")];c&&(b=c.compileToFragments(a,P),e=Da(b),this.range.exclusive||-1!== -+e)&&(n=", "+(this.range.exclusive?e:c.isNumber()?""+(+e+1):(b=c.compileToFragments(a,t),"+"+Da(b)+" + 1 || 9e9")));return[this.makeCode(".slice("+Da(d)+(n||"")+")")]};return b}(a);g.Obj=z=function(a){function b(a,b){this.generated=null!=b?b:!1;this.objects=this.properties=a||[]}na(b,a);b.prototype.children=["properties"];b.prototype.compileNode=function(a){var b,e,d,c,n,f,g,h,k,m,t,l,p;p=this.properties;if(this.generated)for(e=0,b=p.length;e=y?this.wrapInBraces(e):e;q=v[0];1===u&&q instanceof x&&q.error("Destructuring assignment has no target");n=this.variable.isObject();if(w&&1===u&&!(q instanceof R))return d=null,q instanceof b&&"object"===q.context?(e=q,f=e.variable,g=f.base,q=e.value,q instanceof b&&(d=q.value,q=q.variable)):(q instanceof b&&(d=q.value,q=q.variable),g=n?q["this"]?q.properties[0].name:new Q(q.unwrap().value):new S(0)),c=g.unwrap()instanceof Q,l=new L(l),l.properties.push(new (c?ua:m)(g)),(p=Ga(q.unwrap().value))&& -q.error(p),d&&(l=new B("?",l,d)),(new b(q,l,null,{param:this.param})).compileToFragments(a,A);C=l.compileToFragments(a,h);z=Da(C);e=[];f=!1;l.unwrap()instanceof r&&!this.variable.assigns(z)||(e.push([this.makeCode((d=a.scope.freeVariable("ref"))+" \x3d ")].concat(Aa.call(C))),C=[this.makeCode(d)],z=d);d=l=0;for(t=v.length;lA?this.wrapInBraces(b):b};return b}(a);g.Code=l=function(a){function b(a,b,e){this.params=a||[];this.body=b||new c;this.bound="boundfunc"===e;this.isGenerator=!!this.body.contains(function(a){return a instanceof B&&a.isYield()||a instanceof ra})}na(b,a);b.prototype.children=["params","body"]; -b.prototype.isStatement=function(){return!!this.ctor};b.prototype.jumps=I;b.prototype.makeScope=function(a){return new K(a,this.body,this)};b.prototype.compileNode=function(a){var g,e,d,n,h,k,m,l,p,y,A,u,v;this.bound&&null!=(e=a.scope.method)&&e.bound&&(this.context=a.scope.method.context);if(this.bound&&!this.context)return this.context="_this",e=new b([new W(new r(this.context))],new c([this])),e=new f(e,[new ga]),e.updateLocationDataIfMissing(this.locationData),e.compileNode(a);a.scope=ma(a,"classScope")|| -this.makeScope(a.scope);a.scope.shared=ma(a,"sharedScope");a.indent+=ca;delete a.bare;delete a.isExistentialEquals;e=[];g=[];l=this.params;n=0;for(k=l.length;n=t?this.wrapInBraces(g):g};b.prototype.eachParamName=function(a){var b,e,d,c,f;c=this.params;f=[];b=0;for(e=c.length;b=c.length)return[];if(1===c.length)return d=c[0],c=d.compileToFragments(a,h),e?c:[].concat(d.makeCode(za("slice",a)+".call("),c,d.makeCode(")"));e=c.slice(n);g=k=0;for(m=e.length;k=t)return(new C(this)).compileToFragments(a);c="+"===d||"-"===d;("new"===d||"typeof"===d||"delete"===d||c&&this.first instanceof b&&this.first.operator=== -d)&&e.push([this.makeCode(" ")]);if(c&&this.first instanceof b||"new"===d&&this.first.isStatement(a))this.first=new C(this.first);e.push(this.first.compileToFragments(a,y));this.flip&&e.reverse();return this.joinFragmentArrays(e,"")};b.prototype.compileYield=function(a){var b,e,c;e=[];b=this.operator;null==a.scope.parent&&this.error("yield can only occur inside functions");0<=Ha.call(Object.keys(this.first),"expression")&&!(this.first instanceof ha)?null!=this.first.expression&&e.push(this.first.expression.compileToFragments(a, -y)):(a.level>=P&&e.push([this.makeCode("(")]),e.push([this.makeCode(b)]),""!==(null!=(c=this.first.base)?c.value:void 0)&&e.push([this.makeCode(" ")]),e.push(this.first.compileToFragments(a,y)),a.level>=P&&e.push([this.makeCode(")")]));return this.joinFragmentArrays(e,"")};b.prototype.compilePower=function(a){var b;b=new L(new r("Math"),[new ua(new Q("pow"))]);return(new f(b,[this.first,this.second])).compileToFragments(a)};b.prototype.compileFloorDivision=function(a){var d,e;e=new L(new r("Math"), -[new ua(new Q("floor"))]);d=this.second.isComplex()?new C(this.second):this.second;d=new b("/",this.first,d);return(new f(e,[d])).compileToFragments(a)};b.prototype.compileModulo=function(a){var b;b=new L(new H(za("modulo",a)));return(new f(b,[this.first,this.second])).compileToFragments(a)};b.prototype.toString=function(a){return b.__super__.toString.call(this,a,this.constructor.name+" "+this.operator)};return b}(a);g.In=O=function(a){function b(a,b){this.object=a;this.array=b}na(b,a);b.prototype.children= -["object","array"];b.prototype.invert=T;b.prototype.compileNode=function(a){var b,e,d,c,f;if(this.array instanceof L&&this.array.isArray()&&this.array.base.objects.length){f=this.array.base.objects;e=0;for(d=f.length;ex,this.step&&null!=x&&m||(e=d.freeVariable("len")),f=""+v+p+" \x3d 0, "+e+" \x3d "+I+".length",g=""+v+p+" \x3d "+I+".length - 1",e=p+" \x3c "+e,d=p+" \x3e\x3d 0",this.step?(null!=x?m&&(e=d,f=g):(e=D+" \x3e 0 ? "+e+" : "+d,f="("+D+" \x3e 0 ? ("+f+") : "+g+")"),p=p+" +\x3d "+D):p=""+(y!==p?"++"+p:p+"++"),f=[this.makeCode(f+"; "+e+"; "+v+p)]));this.returns&&(z=""+this.tab+n+" \x3d [];\n",B="\n"+this.tab+"return "+n+";",b.makeReturn(n)); -this.guard&&(1=p?this.wrapInBraces(b):b};b.prototype.unfoldSoak=function(){return this.soak&&this};return b}(a);la={extend:function(a){return"function(child, parent) { for (var key in parent) { if ("+za("hasProp",a)+".call(parent, key)) child[key] \x3d parent[key]; } function ctor() { this.constructor \x3d child; } ctor.prototype \x3d parent.prototype; child.prototype \x3d new ctor(); child.__super__ \x3d parent.prototype; return child; }"}, -bind:function(){return"function(fn, me){ return function(){ return fn.apply(me, arguments); }; }"},indexOf:function(){return"[].indexOf || function(item) { for (var i \x3d 0, l \x3d this.length; i \x3c l; i++) { if (i in this \x26\x26 this[i] \x3d\x3d\x3d item) return i; } return -1; }"},modulo:function(){return"function(a, b) { return (+a % (b \x3d +b) + b) % b; }"},hasProp:function(){return"{}.hasOwnProperty"},slice:function(){return"[].slice"}};A=1;P=2;h=3;p=4;y=5;t=6;ca=" ";Z=/^[+-]?\d+$/;za= -function(a,b){var c,f;f=b.scope.root;if(a in f.utilities)return f.utilities[a];c=f.freeVariable(a);f.assign(c,la[a](b));return f.utilities[a]=c};Pa=function(a,b){a=a.replace(/\n/g,"$\x26"+b);return a.replace(/\s+$/,"")};Ja=function(a){return a instanceof r&&"arguments"===a.value};Ta=function(a){return a instanceof ga||a instanceof l&&a.bound||a instanceof ka};ta=function(a){return a.isComplex()||("function"===typeof a.isAssignable?a.isAssignable():void 0)};Na=function(a,b,c){if(a=b[c].unfoldSoak(a))return b[c]= -a.body,a.body=new L(b),a}}).call(this);return g}();u["./sourcemap"]=function(){var g={};(function(){var u;u=function(){function g(g){this.line=g;this.columns=[]}g.prototype.add=function(g,a,c){var q;q=a[0];a=a[1];null==c&&(c={});if(!this.columns[g]||!c.noReplace)return this.columns[g]={line:this.line,column:g,sourceLine:q,sourceColumn:a}};g.prototype.sourceLocation=function(g){for(var a;!((a=this.columns[g])||0>=g);)g--;return a&&[a.sourceLine,a.sourceColumn]};return g}();g=function(){function g(){this.lines= -[]}g.prototype.add=function(g,a,c){var q,f;null==c&&(c={});f=a[0];a=a[1];return((q=this.lines)[f]||(q[f]=new u(f))).add(a,g,c)};g.prototype.sourceLocation=function(g){var a,c;a=g[0];for(g=g[1];!((c=this.lines[a])||0>=a);)a--;return c&&c.sourceLocation(g)};g.prototype.generate=function(g,a){var c,q,f,u,l,w,G,F,x,v,M,N,J;null==g&&(g={});null==a&&(a=null);l=w=u=J=0;v=!1;c="";M=this.lines;f=q=0;for(G=M.length;qg?1:0);c||!a;)g=c&31,(c>>=5)&&(g|=32),a+=this.encodeBase64(g);return a};g.prototype.encodeBase64= -function(g){var a;if(!(a="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[g]))throw Error("Cannot Base64 encode value: "+g);return a};return g}()}).call(this);return g}();u["./coffee-script"]=function(){var g={};(function(){var qa,xa,q,a,c,Ca,f,D,l,w,G,F,x,v,M,N,J,r,E,O={}.hasOwnProperty;D=u("fs");E=u("vm");M=u("path");qa=u("./lexer").Lexer;v=u("./parser").parser;w=u("./helpers");xa=u("./sourcemap");c=u("../../package.json");g.VERSION=c.version;g.FILE_EXTENSIONS=[".coffee",".litcoffee", -".coffee.md"];g.helpers=w;q=function(a){switch(!1){case "function"!==typeof Buffer:return(new Buffer(a)).toString("base64");case "function"!==typeof btoa:return btoa(encodeURIComponent(a).replace(/%([0-9A-F]{2})/g,function(a,c){return String.fromCharCode("0x"+c)}));default:throw Error("Unable to base64 encode inline sourcemap.");}};c=function(a){return function(c,f){null==f&&(f={});try{return a.call(this,c,f)}catch(p){if("string"!==typeof c)throw p;throw w.updateSyntaxError(p,c,f.filename);}}};r= -{};J={};g.compile=a=c(function(a,c){var f,g,h,k,m,l,u,D,E,I,F,G,z;h=w.extend;c=h({},c);l=c.sourceMap||c.inlineMap||null==c.filename;h=c.filename||"\x3canonymous\x3e";r[h]=a;l&&(I=new xa);g=x.tokenize(a,c);k=c;E=[];m=0;for(u=g.length;m>>=1,a+=a;return g};f.compact=function(a){var g,b;var n=[];var y=0;for(b=a.length;yc)return m.call(this,L,a-1);(w=L[0],0<=y.call(g,w))?c+=1:(l=L[0],0<=y.call(h,l))&&--c;a+=1}return a-1};l.prototype.removeLeadingNewlines=function(){var a,b;var m=this.tokens;var k=a=0;for(b=m.length;ag;f=0<=g?++b:--b){for(;"HERECOMMENT"===this.tag(l+f+c);)c+=2;if(null!=h[f]&&("string"===typeof h[f]&&(h[f]=[h[f]]),k=this.tag(l+f+c),0>y.call(h[f],k)))return-1}return l+f+c-1};l.prototype.looksObjectish=function(a){if(-1y.call(b,w))&&((f=this.tag(a),0>y.call(g,f))||this.tokens[a].generated)&&(n=this.tag(a),0>y.call(R,n)));)(k=this.tag(a),0<=y.call(h,k))&&c.push(this.tag(a)),(l=this.tag(a),0<=y.call(g, +l))&&c.length&&c.pop(),--a;return x=this.tag(a),0<=y.call(b,x)};l.prototype.addImplicitBracesAndParens=function(){var a=[];var l=null;return this.scanTokens(function(c,k,f){var m,w,n,r;var G=c[0];var K=(m=0y.call(h,a):return l[1];case "@"!==this.tag(k-2):return k-2;default:return k-1}}.call(this);"HERECOMMENT"===this.tag(q-2);)q-=2;this.insideForDeclaration="FOR"===u;m=0===q||(r=this.tag(q-1),0<=y.call(R,r))||f[q-1].newLine;if(B()&&(T=B(),r=T[0],v=T[1],("{"===r||"INDENT"===r&&"{"===this.tag(v-1))&&(m||","===this.tag(q-1)||"{"===this.tag(q-1))))return A(1);M(q,!!m);return A(2)}if(0<=y.call(R,G))for(M=a.length-1;0<=M;M+=-1)r=a[M],E(r)&&(r[2].sameLine= +!1);M="OUTDENT"===K||m.newLine;if(0<=y.call(x,G)||0<=y.call(z,G)&&M)for(;O();)if(M=B(),r=M[0],v=M[1],m=M[2],M=m.sameLine,m=m.startsLine,C()&&","!==K)S();else if(T()&&!this.insideForDeclaration&&M&&"TERMINATOR"!==G&&":"!==K)q();else if(!T()||"TERMINATOR"!==G||","===K||m&&this.looksObjectish(k+1))break;else{if("HERECOMMENT"===u)return A(1);q()}if(!(","!==G||this.looksObjectish(k+1)||!T()||this.insideForDeclaration||"TERMINATOR"===u&&this.looksObjectish(k+2)))for(u="OUTDENT"===u?1:0;T();)q(k+u);return A(1)})}; +l.prototype.addLocationDataToGeneratedTokens=function(){return this.scanTokens(function(a,b,g){var c,l;if(a[2]||!a.generated&&!a.explicit)return 1;if("{"===a[0]&&(c=null!=(l=g[b+1])?l[2]:void 0)){var m=c.first_line;c=c.first_column}else(c=null!=(m=g[b-1])?m[2]:void 0)?(m=c.last_line,c=c.last_column):m=c=0;a[2]={first_line:m,first_column:c,last_line:m,last_column:c};return 1})};l.prototype.fixOutdentLocationData=function(){return this.scanTokens(function(a,b,g){if(!("OUTDENT"===a[0]||a.generated&& +"CALL_END"===a[0]||a.generated&&"}"===a[0]))return 1;b=g[b-1][2];a[2]={first_line:b.last_line,first_column:b.last_column,last_line:b.last_line,last_column:b.last_column};return 1})};l.prototype.normalizeLines=function(){var b,g;var l=b=g=null;var k=function(a,b){var c,g,k,f;return";"!==a[1]&&(c=a[0],0<=y.call(O,c))&&!("TERMINATOR"===a[0]&&(g=this.tag(b+1),0<=y.call(H,g)))&&!("ELSE"===a[0]&&"THEN"!==l)&&!!("CATCH"!==(k=a[0])&&"FINALLY"!==k||"-\x3e"!==l&&"\x3d\x3e"!==l)||(f=a[0],0<=y.call(z,f))&&(this.tokens[b- +1].newLine||"OUTDENT"===this.tokens[b-1][0])};var f=function(a,b){return this.tokens.splice(","===this.tag(b-1)?b-1:b,0,g)};return this.scanTokens(function(c,m,h){var w,n,r;c=c[0];if("TERMINATOR"===c){if("ELSE"===this.tag(m+1)&&"OUTDENT"!==this.tag(m-1))return h.splice.apply(h,[m,1].concat(a.call(this.indentation()))),1;if(w=this.tag(m+1),0<=y.call(H,w))return h.splice(m,1),0}if("CATCH"===c)for(w=n=1;2>=n;w=++n)if("OUTDENT"===(r=this.tag(m+w))||"TERMINATOR"===r||"FINALLY"===r)return h.splice.apply(h, +[m+w,0].concat(a.call(this.indentation()))),2+w;0<=y.call(J,c)&&"INDENT"!==this.tag(m+1)&&("ELSE"!==c||"IF"!==this.tag(m+1))&&(l=c,r=this.indentation(h[m]),b=r[0],g=r[1],"THEN"===l&&(b.fromThen=!0),h.splice(m+1,0,b),this.detectEnd(m+2,k,f),"THEN"===c&&h.splice(m,1));return 1})};l.prototype.tagPostfixConditionals=function(){var a=null;var b=function(a,b){a=a[0];b=this.tokens[b-1][0];return"TERMINATOR"===a||"INDENT"===a&&0>y.call(J,b)};var g=function(b,c){if("INDENT"!==b[0]||b.generated&&!b.fromThen)return a[0]= +"POST_"+a[0]};return this.scanTokens(function(c,l){if("IF"!==c[0])return 1;a=c;this.detectEnd(l+1,b,g);return 1})};l.prototype.indentation=function(a){var b=["INDENT",2];var c=["OUTDENT",2];a?(b.generated=c.generated=!0,b.origin=c.origin=a):b.explicit=c.explicit=!0;return[b,c]};l.prototype.generate=b;l.prototype.tag=function(a){var b;return null!=(b=this.tokens[a])?b[0]:void 0};return l}();var ya=[["(",")"],["[","]"],["{","}"],["INDENT","OUTDENT"],["CALL_START","CALL_END"],["PARAM_START","PARAM_END"], +["INDEX_START","INDEX_END"],["STRING_START","STRING_END"],["REGEX_START","REGEX_END"]];f.INVERSES=u={};var g=[];var h=[];var r=0;for(q=ya.length;rthis.indent){if(c||"RETURN"===this.tag())return this.indebt=b-this.indent,this.suppressNewlines(),a.length;if(!this.tokens.length)return this.baseIndent= +this.indent=b,a.length;c=b-this.indent+this.outdebt;this.token("INDENT",c,a.length-b,b);this.indents.push(c);this.ends.push({tag:"OUTDENT"});this.outdebt=this.indebt=0;this.indent=b}else bl&&(m=this.token("+","+"),m[2]={first_line:w[2].first_line,first_column:w[2].first_column,last_line:w[2].first_line,last_column:w[2].first_column});(f=this.tokens).push.apply(f,r)}if(k)return a=a[a.length-1],k.origin=["STRING",null,{first_line:k[2].first_line,first_column:k[2].first_column,last_line:a[2].last_line,last_column:a[2].last_column}],k=this.token("STRING_END",")"),k[2]={first_line:a[2].last_line,first_column:a[2].last_column, +last_line:a[2].last_line,last_column:a[2].last_column}};a.prototype.pair=function(a){var b=this.ends;b=b[b.length-1];return a!==(b=null!=b?b.tag:void 0)?("OUTDENT"!==b&&this.error("unmatched "+a),b=this.indents,b=b[b.length-1],this.outdentToken(b,!0),this.pair(a)):this.ends.pop()};a.prototype.getLineAndColumnFromChunk=function(a){if(0===a)return[this.chunkLine,this.chunkColumn];var b=a>=this.chunk.length?this.chunk:this.chunk.slice(0,+(a-1)+1||9E9);a=g(b,"\n");var c=this.chunkColumn;0a)return b(a);var c=Math.floor((a-65536)/1024)+55296;a=(a-65536)%1024+56320;return""+b(c)+b(a)};a.prototype.replaceUnicodeCodePointEscapes= +function(a,b){return a.replace(sa,function(a){return function(c,g,k,h){if(g)return g;c=parseInt(k,16);1114111q.call(y.call(I).concat(y.call(F)),a):return"keyword '"+b+"' can't be assigned";case 0>q.call(O, +a):return"'"+b+"' can't be assigned";case 0>q.call(J,a):return"reserved word '"+b+"' can't be assigned";default:return!1}};f.isUnassignable=B;var H=function(a){var b;return"IDENTIFIER"===a[0]?("from"===a[1]&&(a[1][0]="IDENTIFIER",!0),!0):"FOR"===a[0]?!1:"{"===(b=a[1])||"["===b||","===b||":"===b?!1:!0};var I="true false null this new delete typeof in instanceof return throw break continue debugger yield if else switch for while do try catch finally class extends super import export default".split(" "); +var F="undefined Infinity NaN then unless until loop of by when".split(" ");var Q={and:"\x26\x26",or:"||",is:"\x3d\x3d",isnt:"!\x3d",not:"!",yes:"true",no:"false",on:"true",off:"false"};var x=function(){var a=[];for(qa in Q)a.push(qa);return a}();F=F.concat(x);var J="case function var void with const let enum native implements interface package private protected public static".split(" ");var O=["arguments","eval"];f.JS_FORBIDDEN=I.concat(J).concat(O);var R=65279;var z=/^(?!\d)((?:(?!\s)[$\w\x7f-\uffff])+)([^\n\S]*:(?!:))?/; +var l=/^0b[01]+|^0o[0-7]+|^0x[\da-f]+|^\d*\.?\d+(?:e[+-]?\d+)?/i;var c=/^(?:[-=]>|[-+*\/%<>&|^!?=]=|>>>=?|([-+:])\1|([&|<>*\/%])\2=?|\?(\.|::)|\.{2,3})/;var w=/^[^\n\S]+/;var m=/^###([^#][\s\S]*?)(?:###[^\n\S]*|###$)|^(?:\s*#(?!##[^#]).*)+/;var k=/^[-=]>/;var K=/^(?:\n[^\n\S]*)+/;var P=/^`(?!``)((?:[^`\\]|\\[\s\S])*)`/;var L=/^```((?:[^`\\]|\\[\s\S]|`(?!``))*)```/;var V=/^(?:'''|"""|'|")/;var X=/^(?:[^\\']|\\[\s\S])*/;var G=/^(?:[^\\"#]|\\[\s\S]|\#(?!\{))*/;var aa=/^(?:[^\\']|\\[\s\S]|'(?!''))*/; +var U=/^(?:[^\\"#]|\\[\s\S]|"(?!"")|\#(?!\{))*/;var W=/((?:\\\\)+)|\\[^\S\n]*\n\s*/g;var D=/\s*\n\s*/g;var A=/\n+([^\n\S]*)(?=\S)/g;var fc=/^\/(?!\/)((?:[^[\/\n\\]|\\[^\n]|\[(?:\\[^\n]|[^\]\n\\])*\])*)(\/)?/;var E=/^\w*/;var ba=/^(?!.*(.).*\1)[imguy]*$/;var ca=/^(?:[^\\\/#]|\\[\s\S]|\/(?!\/\/)|\#(?!\{))*/;var C=/((?:\\\\)+)|\\(\s)|\s+(?:#.*)?/g;var T=/^(\/|\/{3}\s*)(\*)/;var v=/^\/=?\s/;var Y=/\*\//;var S=/^\s*(?:,|\??\.(?![.\d])|::)/;var M=/((?:^|[^\\])(?:\\\\)*)\\(?:(0[0-7]|[1-7])|(x(?![\da-fA-F]{2}).{0,2})|(u\{(?![\da-fA-F]{1,}\})[^}]*\}?)|(u(?!\{|[\da-fA-F]{4}).{0,4}))/; +var va=/((?:^|[^\\])(?:\\\\)*)\\(?:(0[0-7])|(x(?![\da-fA-F]{2}).{0,2})|(u\{(?![\da-fA-F]{1,}\})[^}]*\}?)|(u(?!\{|[\da-fA-F]{4}).{0,4}))/;var sa=/(\\\\)|\\u\{([\da-fA-F]+)\}/g;var za=/^[^\n\S]*\n/;var ma=/\n[^\n\S]*$/;var Z=/\s+$/;var fa="-\x3d +\x3d /\x3d *\x3d %\x3d ||\x3d \x26\x26\x3d ?\x3d \x3c\x3c\x3d \x3e\x3e\x3d \x3e\x3e\x3e\x3d \x26\x3d ^\x3d |\x3d **\x3d //\x3d %%\x3d".split(" ");var ia=["NEW","TYPEOF","DELETE","DO"];var ga=["!","~"];var ja=["\x3c\x3c","\x3e\x3e","\x3e\x3e\x3e"];var la="\x3d\x3d !\x3d \x3c \x3e \x3c\x3d \x3e\x3d".split(" "); +var oa=["*","/","%","//","%%"];var pa=["IN","OF","INSTANCEOF"];var ha="IDENTIFIER PROPERTY ) ] ? @ THIS SUPER".split(" ");var ka=ha.concat("NUMBER INFINITY NAN STRING STRING_END REGEX REGEX_END BOOL NULL UNDEFINED } ::".split(" "));var na=ka.concat(["++","--"]);var ra=["INDENT","OUTDENT","TERMINATOR"];var da=[")","}","]"]}).call(this);return f}();u["./parser"]=function(){var f={},qa={exports:f},q=function(){function f(){this.yy={}}var a=function(a,p,t,d){t=t||{};for(d=a.length;d--;t[a[d]]=p);return t}, +b=[1,22],u=[1,25],g=[1,83],h=[1,79],r=[1,84],n=[1,85],B=[1,81],H=[1,82],I=[1,56],F=[1,58],Q=[1,59],x=[1,60],J=[1,61],O=[1,62],R=[1,49],z=[1,50],l=[1,32],c=[1,68],w=[1,69],m=[1,78],k=[1,47],K=[1,51],P=[1,52],L=[1,67],V=[1,65],X=[1,66],G=[1,64],aa=[1,42],U=[1,48],W=[1,63],D=[1,73],A=[1,74],q=[1,75],E=[1,76],ba=[1,46],ca=[1,72],C=[1,34],T=[1,35],v=[1,36],Y=[1,37],S=[1,38],M=[1,39],qa=[1,86],sa=[1,6,32,42,131],za=[1,101],ma=[1,89],Z=[1,88],fa=[1,87],ia=[1,90],ga=[1,91],ja=[1,92],la=[1,93],oa=[1,94],pa= +[1,95],ha=[1,96],ka=[1,97],na=[1,98],ra=[1,99],da=[1,100],va=[1,104],N=[1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],xa=[2,166],ta=[1,110],Na=[1,111],Fa=[1,112],Ga=[1,113],Ca=[1,115],Pa=[1,116],Ia=[1,109],Ea=[1,6,32,42,131,133,135,139,156],Va=[2,27],ea=[1,123],Ya=[1,121],Ba=[1,6,31,32,40,41,42,65,70,73,82,83,84,85,87,89,90,94,113,114,115,120,122,131,133,134,135,139,140,156,159,160,163,164,165,166,167,168,169,170,171,172, +173,174],Ha=[2,94],t=[1,6,31,32,42,46,65,70,73,82,83,84,85,87,89,90,94,113,114,115,120,122,131,133,134,135,139,140,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],p=[2,73],d=[1,128],wa=[1,133],e=[1,134],Da=[1,136],Ta=[1,6,31,32,40,41,42,55,65,70,73,82,83,84,85,87,89,90,94,113,114,115,120,122,131,133,134,135,139,140,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],ua=[2,91],Eb=[1,6,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,159,160,163,164,165,166,167,168, +169,170,171,172,173,174],Za=[2,63],Fb=[1,166],$a=[1,178],Ua=[1,180],Gb=[1,175],Oa=[1,182],sb=[1,184],La=[1,6,31,32,40,41,42,55,65,70,73,82,83,84,85,87,89,90,94,96,113,114,115,120,122,131,133,134,135,139,140,156,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175],Hb=[2,110],Ib=[1,6,31,32,40,41,42,58,65,70,73,82,83,84,85,87,89,90,94,113,114,115,120,122,131,133,134,135,139,140,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],Jb=[1,6,31,32,40,41,42,46,58,65,70,73,82,83,84, +85,87,89,90,94,113,114,115,120,122,131,133,134,135,139,140,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],Kb=[40,41,114],Lb=[1,241],tb=[1,240],Ma=[1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156],Ja=[2,71],Mb=[1,250],Sa=[6,31,32,65,70],fb=[6,31,32,55,65,70,73],ab=[1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,159,160,164,166,167,168,169,170,171,172,173,174],Nb=[40,41,82,83,84,85,87,90,113,114],gb=[1,269],bb=[2,62],hb=[1,279],Wa=[1,281],ub=[1, +286],cb=[1,288],Ob=[2,187],vb=[1,6,31,32,40,41,42,55,65,70,73,82,83,84,85,87,89,90,94,113,114,115,120,122,131,133,134,135,139,140,146,147,148,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],ib=[1,297],Qa=[6,31,32,70,115,120],Pb=[1,6,31,32,40,41,42,55,58,65,70,73,82,83,84,85,87,89,90,94,96,113,114,115,120,122,131,133,134,135,139,140,146,147,148,156,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175],Qb=[1,6,31,32,42,65,70,73,89,94,115,120,122,131,140,156],Xa=[1,6,31,32, +42,65,70,73,89,94,115,120,122,131,134,140,156],jb=[146,147,148],kb=[70,146,147,148],lb=[6,31,94],Rb=[1,311],Aa=[6,31,32,70,94],Sb=[6,31,32,58,70,94],wb=[6,31,32,55,58,70,94],Tb=[1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,159,160,166,167,168,169,170,171,172,173,174],Ub=[12,28,34,38,40,41,44,45,48,49,50,51,52,53,61,62,63,67,68,89,92,95,97,105,112,117,118,119,125,129,130,133,135,137,139,149,155,157,158,159,160,161,162],Vb=[2,176],Ra=[6,31,32],db=[2,72],Wb=[1,323],Xb=[1,324], +Yb=[1,6,31,32,42,65,70,73,89,94,115,120,122,127,128,131,133,134,135,139,140,151,153,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],mb=[32,151,153],Zb=[1,6,32,42,65,70,73,89,94,115,120,122,131,134,140,156],nb=[1,350],xb=[1,356],yb=[1,6,32,42,131,156],eb=[2,86],ob=[1,367],pb=[1,368],$b=[1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,151,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],zb=[1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,135,139,140,156],ac= +[1,381],bc=[1,382],Ab=[6,31,32,94],cc=[6,31,32,70],Bb=[1,6,31,32,42,65,70,73,89,94,115,120,122,127,131,133,134,135,139,140,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],dc=[31,70],qb=[1,408],rb=[1,409],Cb=[1,415],Db=[1,416],ec={trace:function(){},yy:{},symbols_:{error:2,Root:3,Body:4,Line:5,TERMINATOR:6,Expression:7,Statement:8,YieldReturn:9,Return:10,Comment:11,STATEMENT:12,Import:13,Export:14,Value:15,Invocation:16,Code:17,Operation:18,Assign:19,If:20,Try:21,While:22,For:23,Switch:24, +Class:25,Throw:26,Yield:27,YIELD:28,FROM:29,Block:30,INDENT:31,OUTDENT:32,Identifier:33,IDENTIFIER:34,Property:35,PROPERTY:36,AlphaNumeric:37,NUMBER:38,String:39,STRING:40,STRING_START:41,STRING_END:42,Regex:43,REGEX:44,REGEX_START:45,REGEX_END:46,Literal:47,JS:48,UNDEFINED:49,NULL:50,BOOL:51,INFINITY:52,NAN:53,Assignable:54,"\x3d":55,AssignObj:56,ObjAssignable:57,":":58,SimpleObjAssignable:59,ThisProperty:60,RETURN:61,HERECOMMENT:62,PARAM_START:63,ParamList:64,PARAM_END:65,FuncGlyph:66,"-\x3e":67, +"\x3d\x3e":68,OptComma:69,",":70,Param:71,ParamVar:72,"...":73,Array:74,Object:75,Splat:76,SimpleAssignable:77,Accessor:78,Parenthetical:79,Range:80,This:81,".":82,"?.":83,"::":84,"?::":85,Index:86,INDEX_START:87,IndexValue:88,INDEX_END:89,INDEX_SOAK:90,Slice:91,"{":92,AssignList:93,"}":94,CLASS:95,EXTENDS:96,IMPORT:97,ImportDefaultSpecifier:98,ImportNamespaceSpecifier:99,ImportSpecifierList:100,ImportSpecifier:101,AS:102,DEFAULT:103,IMPORT_ALL:104,EXPORT:105,ExportSpecifierList:106,EXPORT_ALL:107, +ExportSpecifier:108,OptFuncExist:109,Arguments:110,Super:111,SUPER:112,FUNC_EXIST:113,CALL_START:114,CALL_END:115,ArgList:116,THIS:117,"@":118,"[":119,"]":120,RangeDots:121,"..":122,Arg:123,SimpleArgs:124,TRY:125,Catch:126,FINALLY:127,CATCH:128,THROW:129,"(":130,")":131,WhileSource:132,WHILE:133,WHEN:134,UNTIL:135,Loop:136,LOOP:137,ForBody:138,FOR:139,BY:140,ForStart:141,ForSource:142,ForVariables:143,OWN:144,ForValue:145,FORIN:146,FOROF:147,FORFROM:148,SWITCH:149,Whens:150,ELSE:151,When:152,LEADING_WHEN:153, +IfBlock:154,IF:155,POST_IF:156,UNARY:157,UNARY_MATH:158,"-":159,"+":160,"--":161,"++":162,"?":163,MATH:164,"**":165,SHIFT:166,COMPARE:167,"\x26":168,"^":169,"|":170,"\x26\x26":171,"||":172,"BIN?":173,RELATION:174,COMPOUND_ASSIGN:175,$accept:0,$end:1},terminals_:{2:"error",6:"TERMINATOR",12:"STATEMENT",28:"YIELD",29:"FROM",31:"INDENT",32:"OUTDENT",34:"IDENTIFIER",36:"PROPERTY",38:"NUMBER",40:"STRING",41:"STRING_START",42:"STRING_END",44:"REGEX",45:"REGEX_START",46:"REGEX_END",48:"JS",49:"UNDEFINED", +50:"NULL",51:"BOOL",52:"INFINITY",53:"NAN",55:"\x3d",58:":",61:"RETURN",62:"HERECOMMENT",63:"PARAM_START",65:"PARAM_END",67:"-\x3e",68:"\x3d\x3e",70:",",73:"...",82:".",83:"?.",84:"::",85:"?::",87:"INDEX_START",89:"INDEX_END",90:"INDEX_SOAK",92:"{",94:"}",95:"CLASS",96:"EXTENDS",97:"IMPORT",102:"AS",103:"DEFAULT",104:"IMPORT_ALL",105:"EXPORT",107:"EXPORT_ALL",112:"SUPER",113:"FUNC_EXIST",114:"CALL_START",115:"CALL_END",117:"THIS",118:"@",119:"[",120:"]",122:"..",125:"TRY",127:"FINALLY",128:"CATCH", +129:"THROW",130:"(",131:")",133:"WHILE",134:"WHEN",135:"UNTIL",137:"LOOP",139:"FOR",140:"BY",144:"OWN",146:"FORIN",147:"FOROF",148:"FORFROM",149:"SWITCH",151:"ELSE",153:"LEADING_WHEN",155:"IF",156:"POST_IF",157:"UNARY",158:"UNARY_MATH",159:"-",160:"+",161:"--",162:"++",163:"?",164:"MATH",165:"**",166:"SHIFT",167:"COMPARE",168:"\x26",169:"^",170:"|",171:"\x26\x26",172:"||",173:"BIN?",174:"RELATION",175:"COMPOUND_ASSIGN"},productions_:[0,[3,0],[3,1],[4,1],[4,3],[4,2],[5,1],[5,1],[5,1],[8,1],[8,1],[8, +1],[8,1],[8,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[7,1],[27,1],[27,2],[27,3],[30,2],[30,3],[33,1],[35,1],[37,1],[37,1],[39,1],[39,3],[43,1],[43,3],[47,1],[47,1],[47,1],[47,1],[47,1],[47,1],[47,1],[47,1],[19,3],[19,4],[19,5],[56,1],[56,3],[56,5],[56,3],[56,5],[56,1],[59,1],[59,1],[59,1],[57,1],[57,1],[10,2],[10,1],[9,3],[9,2],[11,1],[17,5],[17,2],[66,1],[66,1],[69,0],[69,1],[64,0],[64,1],[64,3],[64,4],[64,6],[71,1],[71,2],[71,3],[71,1],[72,1],[72,1],[72,1],[72, +1],[76,2],[77,1],[77,2],[77,2],[77,1],[54,1],[54,1],[54,1],[15,1],[15,1],[15,1],[15,1],[15,1],[78,2],[78,2],[78,2],[78,2],[78,1],[78,1],[86,3],[86,2],[88,1],[88,1],[75,4],[93,0],[93,1],[93,3],[93,4],[93,6],[25,1],[25,2],[25,3],[25,4],[25,2],[25,3],[25,4],[25,5],[13,2],[13,4],[13,4],[13,5],[13,7],[13,6],[13,9],[100,1],[100,3],[100,4],[100,4],[100,6],[101,1],[101,3],[101,1],[101,3],[98,1],[99,3],[14,3],[14,5],[14,2],[14,4],[14,5],[14,6],[14,3],[14,4],[14,7],[106,1],[106,3],[106,4],[106,4],[106,6],[108, +1],[108,3],[108,3],[108,1],[108,3],[16,3],[16,3],[16,3],[16,1],[111,1],[111,2],[109,0],[109,1],[110,2],[110,4],[81,1],[81,1],[60,2],[74,2],[74,4],[121,1],[121,1],[80,5],[91,3],[91,2],[91,2],[91,1],[116,1],[116,3],[116,4],[116,4],[116,6],[123,1],[123,1],[123,1],[124,1],[124,3],[21,2],[21,3],[21,4],[21,5],[126,3],[126,3],[126,2],[26,2],[79,3],[79,5],[132,2],[132,4],[132,2],[132,4],[22,2],[22,2],[22,2],[22,1],[136,2],[136,2],[23,2],[23,2],[23,2],[138,2],[138,4],[138,2],[141,2],[141,3],[145,1],[145,1], +[145,1],[145,1],[143,1],[143,3],[142,2],[142,2],[142,4],[142,4],[142,4],[142,6],[142,6],[142,2],[142,4],[24,5],[24,7],[24,4],[24,6],[150,1],[150,2],[152,3],[152,4],[154,3],[154,5],[20,1],[20,3],[20,3],[20,3],[18,2],[18,2],[18,2],[18,2],[18,2],[18,2],[18,2],[18,2],[18,2],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,3],[18,5],[18,4],[18,3]],performAction:function(a,p,t,d,wa,b,e){a=b.length-1;switch(wa){case 1:return this.$=d.addLocationDataFn(e[a],e[a])(new d.Block); +case 2:return this.$=b[a];case 3:this.$=d.addLocationDataFn(e[a],e[a])(d.Block.wrap([b[a]]));break;case 4:this.$=d.addLocationDataFn(e[a-2],e[a])(b[a-2].push(b[a]));break;case 5:this.$=b[a-1];break;case 6:case 7:case 8:case 9:case 10:case 12:case 13:case 14:case 15:case 16:case 17:case 18:case 19:case 20:case 21:case 22:case 23:case 24:case 25:case 26:case 35:case 40:case 42:case 56:case 57:case 58:case 59:case 60:case 61:case 71:case 72:case 82:case 83:case 84:case 85:case 90:case 91:case 94:case 98:case 104:case 163:case 187:case 188:case 190:case 220:case 221:case 239:case 245:this.$= +b[a];break;case 11:this.$=d.addLocationDataFn(e[a],e[a])(new d.StatementLiteral(b[a]));break;case 27:this.$=d.addLocationDataFn(e[a],e[a])(new d.Op(b[a],new d.Value(new d.Literal(""))));break;case 28:case 249:case 250:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Op(b[a-1],b[a]));break;case 29:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Op(b[a-2].concat(b[a-1]),b[a]));break;case 30:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Block);break;case 31:case 105:this.$=d.addLocationDataFn(e[a-2],e[a])(b[a- +1]);break;case 32:this.$=d.addLocationDataFn(e[a],e[a])(new d.IdentifierLiteral(b[a]));break;case 33:this.$=d.addLocationDataFn(e[a],e[a])(new d.PropertyName(b[a]));break;case 34:this.$=d.addLocationDataFn(e[a],e[a])(new d.NumberLiteral(b[a]));break;case 36:this.$=d.addLocationDataFn(e[a],e[a])(new d.StringLiteral(b[a]));break;case 37:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.StringWithInterpolations(b[a-1]));break;case 38:this.$=d.addLocationDataFn(e[a],e[a])(new d.RegexLiteral(b[a]));break; +case 39:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.RegexWithInterpolations(b[a-1].args));break;case 41:this.$=d.addLocationDataFn(e[a],e[a])(new d.PassthroughLiteral(b[a]));break;case 43:this.$=d.addLocationDataFn(e[a],e[a])(new d.UndefinedLiteral);break;case 44:this.$=d.addLocationDataFn(e[a],e[a])(new d.NullLiteral);break;case 45:this.$=d.addLocationDataFn(e[a],e[a])(new d.BooleanLiteral(b[a]));break;case 46:this.$=d.addLocationDataFn(e[a],e[a])(new d.InfinityLiteral(b[a]));break;case 47:this.$= +d.addLocationDataFn(e[a],e[a])(new d.NaNLiteral);break;case 48:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Assign(b[a-2],b[a]));break;case 49:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.Assign(b[a-3],b[a]));break;case 50:this.$=d.addLocationDataFn(e[a-4],e[a])(new d.Assign(b[a-4],b[a-1]));break;case 51:case 87:case 92:case 93:case 95:case 96:case 97:case 222:case 223:this.$=d.addLocationDataFn(e[a],e[a])(new d.Value(b[a]));break;case 52:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Assign(d.addLocationDataFn(e[a- +2])(new d.Value(b[a-2])),b[a],"object",{operatorToken:d.addLocationDataFn(e[a-1])(new d.Literal(b[a-1]))}));break;case 53:this.$=d.addLocationDataFn(e[a-4],e[a])(new d.Assign(d.addLocationDataFn(e[a-4])(new d.Value(b[a-4])),b[a-1],"object",{operatorToken:d.addLocationDataFn(e[a-3])(new d.Literal(b[a-3]))}));break;case 54:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Assign(d.addLocationDataFn(e[a-2])(new d.Value(b[a-2])),b[a],null,{operatorToken:d.addLocationDataFn(e[a-1])(new d.Literal(b[a-1]))})); +break;case 55:this.$=d.addLocationDataFn(e[a-4],e[a])(new d.Assign(d.addLocationDataFn(e[a-4])(new d.Value(b[a-4])),b[a-1],null,{operatorToken:d.addLocationDataFn(e[a-3])(new d.Literal(b[a-3]))}));break;case 62:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Return(b[a]));break;case 63:this.$=d.addLocationDataFn(e[a],e[a])(new d.Return);break;case 64:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.YieldReturn(b[a]));break;case 65:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.YieldReturn);break;case 66:this.$= +d.addLocationDataFn(e[a],e[a])(new d.Comment(b[a]));break;case 67:this.$=d.addLocationDataFn(e[a-4],e[a])(new d.Code(b[a-3],b[a],b[a-1]));break;case 68:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Code([],b[a],b[a-1]));break;case 69:this.$=d.addLocationDataFn(e[a],e[a])("func");break;case 70:this.$=d.addLocationDataFn(e[a],e[a])("boundfunc");break;case 73:case 110:this.$=d.addLocationDataFn(e[a],e[a])([]);break;case 74:case 111:case 130:case 150:case 182:case 224:this.$=d.addLocationDataFn(e[a], +e[a])([b[a]]);break;case 75:case 112:case 131:case 151:case 183:this.$=d.addLocationDataFn(e[a-2],e[a])(b[a-2].concat(b[a]));break;case 76:case 113:case 132:case 152:case 184:this.$=d.addLocationDataFn(e[a-3],e[a])(b[a-3].concat(b[a]));break;case 77:case 114:case 134:case 154:case 186:this.$=d.addLocationDataFn(e[a-5],e[a])(b[a-5].concat(b[a-2]));break;case 78:this.$=d.addLocationDataFn(e[a],e[a])(new d.Param(b[a]));break;case 79:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Param(b[a-1],null,!0)); +break;case 80:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Param(b[a-2],b[a]));break;case 81:case 189:this.$=d.addLocationDataFn(e[a],e[a])(new d.Expansion);break;case 86:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Splat(b[a-1]));break;case 88:this.$=d.addLocationDataFn(e[a-1],e[a])(b[a-1].add(b[a]));break;case 89:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Value(b[a-1],[].concat(b[a])));break;case 99:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Access(b[a]));break;case 100:this.$=d.addLocationDataFn(e[a- +1],e[a])(new d.Access(b[a],"soak"));break;case 101:this.$=d.addLocationDataFn(e[a-1],e[a])([d.addLocationDataFn(e[a-1])(new d.Access(new d.PropertyName("prototype"))),d.addLocationDataFn(e[a])(new d.Access(b[a]))]);break;case 102:this.$=d.addLocationDataFn(e[a-1],e[a])([d.addLocationDataFn(e[a-1])(new d.Access(new d.PropertyName("prototype"),"soak")),d.addLocationDataFn(e[a])(new d.Access(b[a]))]);break;case 103:this.$=d.addLocationDataFn(e[a],e[a])(new d.Access(new d.PropertyName("prototype"))); +break;case 106:this.$=d.addLocationDataFn(e[a-1],e[a])(d.extend(b[a],{soak:!0}));break;case 107:this.$=d.addLocationDataFn(e[a],e[a])(new d.Index(b[a]));break;case 108:this.$=d.addLocationDataFn(e[a],e[a])(new d.Slice(b[a]));break;case 109:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.Obj(b[a-2],b[a-3].generated));break;case 115:this.$=d.addLocationDataFn(e[a],e[a])(new d.Class);break;case 116:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Class(null,null,b[a]));break;case 117:this.$=d.addLocationDataFn(e[a- +2],e[a])(new d.Class(null,b[a]));break;case 118:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.Class(null,b[a-1],b[a]));break;case 119:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Class(b[a]));break;case 120:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Class(b[a-1],null,b[a]));break;case 121:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.Class(b[a-2],b[a]));break;case 122:this.$=d.addLocationDataFn(e[a-4],e[a])(new d.Class(b[a-3],b[a-1],b[a]));break;case 123:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.ImportDeclaration(null, +b[a]));break;case 124:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.ImportDeclaration(new d.ImportClause(b[a-2],null),b[a]));break;case 125:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.ImportDeclaration(new d.ImportClause(null,b[a-2]),b[a]));break;case 126:this.$=d.addLocationDataFn(e[a-4],e[a])(new d.ImportDeclaration(new d.ImportClause(null,new d.ImportSpecifierList([])),b[a]));break;case 127:this.$=d.addLocationDataFn(e[a-6],e[a])(new d.ImportDeclaration(new d.ImportClause(null,new d.ImportSpecifierList(b[a- +4])),b[a]));break;case 128:this.$=d.addLocationDataFn(e[a-5],e[a])(new d.ImportDeclaration(new d.ImportClause(b[a-4],b[a-2]),b[a]));break;case 129:this.$=d.addLocationDataFn(e[a-8],e[a])(new d.ImportDeclaration(new d.ImportClause(b[a-7],new d.ImportSpecifierList(b[a-4])),b[a]));break;case 133:case 153:case 169:case 185:this.$=d.addLocationDataFn(e[a-3],e[a])(b[a-2]);break;case 135:this.$=d.addLocationDataFn(e[a],e[a])(new d.ImportSpecifier(b[a]));break;case 136:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.ImportSpecifier(b[a- +2],b[a]));break;case 137:this.$=d.addLocationDataFn(e[a],e[a])(new d.ImportSpecifier(new d.Literal(b[a])));break;case 138:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.ImportSpecifier(new d.Literal(b[a-2]),b[a]));break;case 139:this.$=d.addLocationDataFn(e[a],e[a])(new d.ImportDefaultSpecifier(b[a]));break;case 140:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.ImportNamespaceSpecifier(new d.Literal(b[a-2]),b[a]));break;case 141:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.ExportNamedDeclaration(new d.ExportSpecifierList([]))); +break;case 142:this.$=d.addLocationDataFn(e[a-4],e[a])(new d.ExportNamedDeclaration(new d.ExportSpecifierList(b[a-2])));break;case 143:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.ExportNamedDeclaration(b[a]));break;case 144:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.ExportNamedDeclaration(new d.Assign(b[a-2],b[a],null,{moduleDeclaration:"export"})));break;case 145:this.$=d.addLocationDataFn(e[a-4],e[a])(new d.ExportNamedDeclaration(new d.Assign(b[a-3],b[a],null,{moduleDeclaration:"export"}))); +break;case 146:this.$=d.addLocationDataFn(e[a-5],e[a])(new d.ExportNamedDeclaration(new d.Assign(b[a-4],b[a-1],null,{moduleDeclaration:"export"})));break;case 147:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.ExportDefaultDeclaration(b[a]));break;case 148:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.ExportAllDeclaration(new d.Literal(b[a-2]),b[a]));break;case 149:this.$=d.addLocationDataFn(e[a-6],e[a])(new d.ExportNamedDeclaration(new d.ExportSpecifierList(b[a-4]),b[a]));break;case 155:this.$=d.addLocationDataFn(e[a], +e[a])(new d.ExportSpecifier(b[a]));break;case 156:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.ExportSpecifier(b[a-2],b[a]));break;case 157:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.ExportSpecifier(b[a-2],new d.Literal(b[a])));break;case 158:this.$=d.addLocationDataFn(e[a],e[a])(new d.ExportSpecifier(new d.Literal(b[a])));break;case 159:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.ExportSpecifier(new d.Literal(b[a-2]),b[a]));break;case 160:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.TaggedTemplateCall(b[a- +2],b[a],b[a-1]));break;case 161:case 162:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Call(b[a-2],b[a],b[a-1]));break;case 164:this.$=d.addLocationDataFn(e[a],e[a])(new d.SuperCall);break;case 165:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.SuperCall(b[a]));break;case 166:this.$=d.addLocationDataFn(e[a],e[a])(!1);break;case 167:this.$=d.addLocationDataFn(e[a],e[a])(!0);break;case 168:this.$=d.addLocationDataFn(e[a-1],e[a])([]);break;case 170:case 171:this.$=d.addLocationDataFn(e[a],e[a])(new d.Value(new d.ThisLiteral)); +break;case 172:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Value(d.addLocationDataFn(e[a-1])(new d.ThisLiteral),[d.addLocationDataFn(e[a])(new d.Access(b[a]))],"this"));break;case 173:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Arr([]));break;case 174:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.Arr(b[a-2]));break;case 175:this.$=d.addLocationDataFn(e[a],e[a])("inclusive");break;case 176:this.$=d.addLocationDataFn(e[a],e[a])("exclusive");break;case 177:this.$=d.addLocationDataFn(e[a-4],e[a])(new d.Range(b[a- +3],b[a-1],b[a-2]));break;case 178:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Range(b[a-2],b[a],b[a-1]));break;case 179:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Range(b[a-1],null,b[a]));break;case 180:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Range(null,b[a],b[a-1]));break;case 181:this.$=d.addLocationDataFn(e[a],e[a])(new d.Range(null,null,b[a]));break;case 191:this.$=d.addLocationDataFn(e[a-2],e[a])([].concat(b[a-2],b[a]));break;case 192:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Try(b[a])); +break;case 193:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Try(b[a-1],b[a][0],b[a][1]));break;case 194:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.Try(b[a-2],null,null,b[a]));break;case 195:this.$=d.addLocationDataFn(e[a-4],e[a])(new d.Try(b[a-3],b[a-2][0],b[a-2][1],b[a]));break;case 196:this.$=d.addLocationDataFn(e[a-2],e[a])([b[a-1],b[a]]);break;case 197:this.$=d.addLocationDataFn(e[a-2],e[a])([d.addLocationDataFn(e[a-1])(new d.Value(b[a-1])),b[a]]);break;case 198:this.$=d.addLocationDataFn(e[a- +1],e[a])([null,b[a]]);break;case 199:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Throw(b[a]));break;case 200:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Parens(b[a-1]));break;case 201:this.$=d.addLocationDataFn(e[a-4],e[a])(new d.Parens(b[a-2]));break;case 202:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.While(b[a]));break;case 203:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.While(b[a-2],{guard:b[a]}));break;case 204:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.While(b[a],{invert:!0}));break; +case 205:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.While(b[a-2],{invert:!0,guard:b[a]}));break;case 206:this.$=d.addLocationDataFn(e[a-1],e[a])(b[a-1].addBody(b[a]));break;case 207:case 208:this.$=d.addLocationDataFn(e[a-1],e[a])(b[a].addBody(d.addLocationDataFn(e[a-1])(d.Block.wrap([b[a-1]]))));break;case 209:this.$=d.addLocationDataFn(e[a],e[a])(b[a]);break;case 210:this.$=d.addLocationDataFn(e[a-1],e[a])((new d.While(d.addLocationDataFn(e[a-1])(new d.BooleanLiteral("true")))).addBody(b[a])); +break;case 211:this.$=d.addLocationDataFn(e[a-1],e[a])((new d.While(d.addLocationDataFn(e[a-1])(new d.BooleanLiteral("true")))).addBody(d.addLocationDataFn(e[a])(d.Block.wrap([b[a]]))));break;case 212:case 213:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.For(b[a-1],b[a]));break;case 214:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.For(b[a],b[a-1]));break;case 215:this.$=d.addLocationDataFn(e[a-1],e[a])({source:d.addLocationDataFn(e[a])(new d.Value(b[a]))});break;case 216:this.$=d.addLocationDataFn(e[a- +3],e[a])({source:d.addLocationDataFn(e[a-2])(new d.Value(b[a-2])),step:b[a]});break;case 217:d=d.addLocationDataFn(e[a-1],e[a]);b[a].own=b[a-1].own;b[a].ownTag=b[a-1].ownTag;b[a].name=b[a-1][0];b[a].index=b[a-1][1];this.$=d(b[a]);break;case 218:this.$=d.addLocationDataFn(e[a-1],e[a])(b[a]);break;case 219:wa=d.addLocationDataFn(e[a-2],e[a]);b[a].own=!0;b[a].ownTag=d.addLocationDataFn(e[a-1])(new d.Literal(b[a-1]));this.$=wa(b[a]);break;case 225:this.$=d.addLocationDataFn(e[a-2],e[a])([b[a-2],b[a]]); +break;case 226:this.$=d.addLocationDataFn(e[a-1],e[a])({source:b[a]});break;case 227:this.$=d.addLocationDataFn(e[a-1],e[a])({source:b[a],object:!0});break;case 228:this.$=d.addLocationDataFn(e[a-3],e[a])({source:b[a-2],guard:b[a]});break;case 229:this.$=d.addLocationDataFn(e[a-3],e[a])({source:b[a-2],guard:b[a],object:!0});break;case 230:this.$=d.addLocationDataFn(e[a-3],e[a])({source:b[a-2],step:b[a]});break;case 231:this.$=d.addLocationDataFn(e[a-5],e[a])({source:b[a-4],guard:b[a-2],step:b[a]}); +break;case 232:this.$=d.addLocationDataFn(e[a-5],e[a])({source:b[a-4],step:b[a-2],guard:b[a]});break;case 233:this.$=d.addLocationDataFn(e[a-1],e[a])({source:b[a],from:!0});break;case 234:this.$=d.addLocationDataFn(e[a-3],e[a])({source:b[a-2],guard:b[a],from:!0});break;case 235:this.$=d.addLocationDataFn(e[a-4],e[a])(new d.Switch(b[a-3],b[a-1]));break;case 236:this.$=d.addLocationDataFn(e[a-6],e[a])(new d.Switch(b[a-5],b[a-3],b[a-1]));break;case 237:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.Switch(null, +b[a-1]));break;case 238:this.$=d.addLocationDataFn(e[a-5],e[a])(new d.Switch(null,b[a-3],b[a-1]));break;case 240:this.$=d.addLocationDataFn(e[a-1],e[a])(b[a-1].concat(b[a]));break;case 241:this.$=d.addLocationDataFn(e[a-2],e[a])([[b[a-1],b[a]]]);break;case 242:this.$=d.addLocationDataFn(e[a-3],e[a])([[b[a-2],b[a-1]]]);break;case 243:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.If(b[a-1],b[a],{type:b[a-2]}));break;case 244:this.$=d.addLocationDataFn(e[a-4],e[a])(b[a-4].addElse(d.addLocationDataFn(e[a- +2],e[a])(new d.If(b[a-1],b[a],{type:b[a-2]}))));break;case 246:this.$=d.addLocationDataFn(e[a-2],e[a])(b[a-2].addElse(b[a]));break;case 247:case 248:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.If(b[a],d.addLocationDataFn(e[a-2])(d.Block.wrap([b[a-2]])),{type:b[a-1],statement:!0}));break;case 251:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Op("-",b[a]));break;case 252:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Op("+",b[a]));break;case 253:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Op("--", +b[a]));break;case 254:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Op("++",b[a]));break;case 255:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Op("--",b[a-1],null,!0));break;case 256:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Op("++",b[a-1],null,!0));break;case 257:this.$=d.addLocationDataFn(e[a-1],e[a])(new d.Existence(b[a-1]));break;case 258:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Op("+",b[a-2],b[a]));break;case 259:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Op("-",b[a-2],b[a]));break; +case 260:case 261:case 262:case 263:case 264:case 265:case 266:case 267:case 268:case 269:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Op(b[a-1],b[a-2],b[a]));break;case 270:e=d.addLocationDataFn(e[a-2],e[a]);b="!"===b[a-1].charAt(0)?(new d.Op(b[a-1].slice(1),b[a-2],b[a])).invert():new d.Op(b[a-1],b[a-2],b[a]);this.$=e(b);break;case 271:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Assign(b[a-2],b[a],b[a-1]));break;case 272:this.$=d.addLocationDataFn(e[a-4],e[a])(new d.Assign(b[a-4],b[a-1],b[a-3])); +break;case 273:this.$=d.addLocationDataFn(e[a-3],e[a])(new d.Assign(b[a-3],b[a],b[a-2]));break;case 274:this.$=d.addLocationDataFn(e[a-2],e[a])(new d.Extends(b[a-2],b[a]))}},table:[{1:[2,1],3:1,4:2,5:3,7:4,8:5,9:6,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:u,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k, +97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{1:[3]},{1:[2,2],6:qa},a(sa,[2,3]),a(sa,[2,6],{141:77,132:102,138:103,133:D,135:A,139:E,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(sa,[2,7],{141:77,132:105,138:106,133:D,135:A,139:E,156:va}),a(sa,[2,8]),a(N,[2,14],{109:107,78:108,86:114,40:xa,41:xa,114:xa,82:ta,83:Na, +84:Fa,85:Ga,87:Ca,90:Pa,113:Ia}),a(N,[2,15],{86:114,109:117,78:118,82:ta,83:Na,84:Fa,85:Ga,87:Ca,90:Pa,113:Ia,114:xa}),a(N,[2,16]),a(N,[2,17]),a(N,[2,18]),a(N,[2,19]),a(N,[2,20]),a(N,[2,21]),a(N,[2,22]),a(N,[2,23]),a(N,[2,24]),a(N,[2,25]),a(N,[2,26]),a(Ea,[2,9]),a(Ea,[2,10]),a(Ea,[2,11]),a(Ea,[2,12]),a(Ea,[2,13]),a([1,6,32,42,131,133,135,139,156,163,164,165,166,167,168,169,170,171,172,173,174],Va,{15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,10:20,11:21,13:23,14:24,54:26, +47:27,79:28,80:29,81:30,111:31,66:33,77:40,154:41,132:43,136:44,138:45,74:53,75:54,37:55,43:57,33:70,60:71,141:77,39:80,7:120,8:122,12:b,28:ea,29:Ya,34:g,38:h,40:r,41:n,44:B,45:H,48:I,49:F,50:Q,51:x,52:J,53:O,61:[1,119],62:z,63:l,67:c,68:w,92:m,95:k,97:K,105:P,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,137:q,149:ba,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M}),a(Ba,Ha,{55:[1,124]}),a(Ba,[2,95]),a(Ba,[2,96]),a(Ba,[2,97]),a(Ba,[2,98]),a(t,[2,163]),a([6,31,65,70],p,{64:125,71:126,72:127,33:129,60:130, +74:131,75:132,34:g,73:d,92:m,118:wa,119:e}),{30:135,31:Da},{7:137,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C, +158:T,159:v,160:Y,161:S,162:M},{7:138,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M}, +{7:139,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:140,8:122,10:20,11:21,12:b, +13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{15:142,16:143,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57, +44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:144,60:71,74:53,75:54,77:141,79:28,80:29,81:30,92:m,111:31,112:L,117:V,118:X,119:G,130:W},{15:142,16:143,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:144,60:71,74:53,75:54,77:145,79:28,80:29,81:30,92:m,111:31,112:L,117:V,118:X,119:G,130:W},a(Ta,ua,{96:[1,149],161:[1,146],162:[1,147],175:[1,148]}),a(N,[2,245],{151:[1,150]}),{30:151,31:Da},{30:152,31:Da},a(N,[2,209]),{30:153,31:Da},{7:154,8:122,10:20,11:21, +12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,31:[1,155],33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(Eb,[2,115],{47:27,79:28,80:29,81:30,111:31, +74:53,75:54,37:55,43:57,33:70,60:71,39:80,15:142,16:143,54:144,30:156,77:158,31:Da,34:g,38:h,40:r,41:n,44:B,45:H,48:I,49:F,50:Q,51:x,52:J,53:O,92:m,96:[1,157],112:L,117:V,118:X,119:G,130:W}),{7:159,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P, +111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(Ea,Za,{15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,10:20,11:21,13:23,14:24,54:26,47:27,79:28,80:29,81:30,111:31,66:33,77:40,154:41,132:43,136:44,138:45,74:53,75:54,37:55,43:57,33:70,60:71,141:77,39:80,8:122,7:160,12:b,28:ea,34:g,38:h,40:r,41:n,44:B,45:H,48:I,49:F,50:Q,51:x,52:J,53:O,61:R,62:z,63:l,67:c,68:w, +92:m,95:k,97:K,105:P,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,137:q,149:ba,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M}),a([1,6,31,32,42,70,94,131,133,135,139,156],[2,66]),{33:165,34:g,39:161,40:r,41:n,92:[1,164],98:162,99:163,104:Fb},{25:168,33:169,34:g,92:[1,167],95:k,103:[1,170],107:[1,171]},a(Ta,[2,92]),a(Ta,[2,93]),a(Ba,[2,40]),a(Ba,[2,41]),a(Ba,[2,42]),a(Ba,[2,43]),a(Ba,[2,44]),a(Ba,[2,45]),a(Ba,[2,46]),a(Ba,[2,47]),{4:172,5:3,7:4,8:5,9:6,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11, +20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:u,31:[1,173],33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:174,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14, +23:15,24:16,25:17,26:18,27:19,28:ea,31:$a,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,73:Ua,74:53,75:54,76:179,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,116:176,117:V,118:X,119:G,120:Gb,123:177,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(Ba,[2,170]),a(Ba,[2,171],{35:181,36:Oa}),a([1,6,31,32,42,46,65,70,73,82, +83,84,85,87,89,90,94,113,115,120,122,131,133,134,135,139,140,156,159,160,163,164,165,166,167,168,169,170,171,172,173,174],[2,164],{110:183,114:sb}),{31:[2,69]},{31:[2,70]},a(La,[2,87]),a(La,[2,90]),{7:185,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K, +105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:186,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X, +119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:187,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43, +133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:189,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,30:188,31:Da,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44, +137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{33:194,34:g,60:195,74:196,75:197,80:190,92:m,118:wa,119:G,143:191,144:[1,192],145:193},{142:198,146:[1,199],147:[1,200],148:[1,201]},a([6,31,70,94],Hb,{39:80,93:202,56:203,57:204,59:205,11:206,37:207,33:208,35:209,60:210,34:g,36:Oa,38:h,40:r,41:n,62:z,118:wa}),a(Ib,[2,34]),a(Ib,[2,35]),a(Ba,[2,38]),{15:142,16:211,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:144,60:71, +74:53,75:54,77:212,79:28,80:29,81:30,92:m,111:31,112:L,117:V,118:X,119:G,130:W},a([1,6,29,31,32,40,41,42,55,58,65,70,73,82,83,84,85,87,89,90,94,96,102,113,114,115,120,122,131,133,134,135,139,140,146,147,148,156,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175],[2,32]),a(Jb,[2,36]),{4:213,5:3,7:4,8:5,9:6,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:u,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F, +50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(sa,[2,5],{7:4,8:5,9:6,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,10:20,11:21,13:23,14:24,54:26,47:27,79:28,80:29,81:30,111:31,66:33,77:40,154:41,132:43,136:44,138:45,74:53,75:54,37:55,43:57, +33:70,60:71,141:77,39:80,5:214,12:b,28:u,34:g,38:h,40:r,41:n,44:B,45:H,48:I,49:F,50:Q,51:x,52:J,53:O,61:R,62:z,63:l,67:c,68:w,92:m,95:k,97:K,105:P,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,133:D,135:A,137:q,139:E,149:ba,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M}),a(N,[2,257]),{7:215,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71, +61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:216,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w, +74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:217,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29, +81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:218,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31, +112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:219,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa, +129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:220,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A, +136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:221,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77, +149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:222,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T, +159:v,160:Y,161:S,162:M},{7:223,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:224, +8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:225,8:122,10:20,11:21,12:b,13:23, +14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:226,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11, +20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:227,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16, +25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:228,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70, +34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(N,[2,208]),a(N,[2,213]),{7:229,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g, +37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(N,[2,207]),a(N,[2,212]),{39:230,40:r,41:n,110:231,114:sb},a(La,[2,88]),a(Kb,[2,167]),{35:232,36:Oa},{35:233,36:Oa},a(La,[2,103],{35:234,36:Oa}),{35:235,36:Oa},a(La, +[2,104]),{7:237,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,73:Lb,74:53,75:54,77:40,79:28,80:29,81:30,88:236,91:238,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,121:239,122:tb,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y, +161:S,162:M},{86:242,87:Ca,90:Pa},{110:243,114:sb},a(La,[2,89]),a(sa,[2,65],{15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,10:20,11:21,13:23,14:24,54:26,47:27,79:28,80:29,81:30,111:31,66:33,77:40,154:41,132:43,136:44,138:45,74:53,75:54,37:55,43:57,33:70,60:71,141:77,39:80,8:122,7:244,12:b,28:ea,34:g,38:h,40:r,41:n,44:B,45:H,48:I,49:F,50:Q,51:x,52:J,53:O,61:R,62:z,63:l,67:c,68:w,92:m,95:k,97:K,105:P,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,133:Za,135:Za,139:Za,156:Za, +137:q,149:ba,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M}),a(Ma,[2,28],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),{7:245,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P, +111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{132:105,133:D,135:A,138:106,139:E,141:77,156:va},a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,163,164,165,166,167,168,169,170,171,172,173,174],Va,{15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,10:20,11:21,13:23,14:24,54:26,47:27,79:28,80:29,81:30,111:31,66:33,77:40,154:41,132:43,136:44, +138:45,74:53,75:54,37:55,43:57,33:70,60:71,141:77,39:80,7:120,8:122,12:b,28:ea,29:Ya,34:g,38:h,40:r,41:n,44:B,45:H,48:I,49:F,50:Q,51:x,52:J,53:O,61:R,62:z,63:l,67:c,68:w,92:m,95:k,97:K,105:P,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,137:q,149:ba,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M}),{6:[1,247],7:246,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,31:[1,248],33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I, +49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a([6,31],Ja,{69:251,65:[1,249],70:Mb}),a(Sa,[2,74]),a(Sa,[2,78],{55:[1,253],73:[1,252]}),a(Sa,[2,81]),a(fb,[2,82]),a(fb,[2,83]),a(fb,[2,84]),a(fb,[2,85]),{35:181,36:Oa},{7:254,8:122,10:20,11:21,12:b,13:23,14:24,15:7, +16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,31:$a,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,73:Ua,74:53,75:54,76:179,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,116:176,117:V,118:X,119:G,120:Gb,123:177,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(N,[2,68]),{4:256,5:3,7:4,8:5,9:6, +10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:u,32:[1,255],33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a([1,6,31,32,42,65,70,73,89,94, +115,120,122,131,133,134,135,139,140,156,159,160,164,165,166,167,168,169,170,171,172,173,174],[2,249],{141:77,132:102,138:103,163:fa}),a(ab,[2,250],{141:77,132:102,138:103,163:fa,165:ga}),a(ab,[2,251],{141:77,132:102,138:103,163:fa,165:ga}),a(ab,[2,252],{141:77,132:102,138:103,163:fa,165:ga}),a(N,[2,253],{40:ua,41:ua,82:ua,83:ua,84:ua,85:ua,87:ua,90:ua,113:ua,114:ua}),a(Kb,xa,{109:107,78:108,86:114,82:ta,83:Na,84:Fa,85:Ga,87:Ca,90:Pa,113:Ia}),{78:118,82:ta,83:Na,84:Fa,85:Ga,86:114,87:Ca,90:Pa,109:117, +113:Ia,114:xa},a(Nb,Ha),a(N,[2,254],{40:ua,41:ua,82:ua,83:ua,84:ua,85:ua,87:ua,90:ua,113:ua,114:ua}),a(N,[2,255]),a(N,[2,256]),{6:[1,259],7:257,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,31:[1,258],33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U, +130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:260,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44, +137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{30:261,31:Da,155:[1,262]},a(N,[2,192],{126:263,127:[1,264],128:[1,265]}),a(N,[2,206]),a(N,[2,214]),{31:[1,266],132:102,133:D,135:A,138:103,139:E,141:77,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da},{150:267,152:268,153:gb},a(N,[2,116]),{7:270,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea, +33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(Eb,[2,119],{30:271,31:Da,40:ua,41:ua,82:ua,83:ua,84:ua,85:ua,87:ua,90:ua,113:ua,114:ua,96:[1,272]}),a(Ma,[2,199],{141:77,132:102,138:103,159:ma,160:Z, +163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Ea,bb,{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Ea,[2,123]),{29:[1,273],70:[1,274]},{29:[1,275]},{31:hb,33:280,34:g,94:[1,276],100:277,101:278,103:Wa},a([29,70],[2,139]),{102:[1,282]},{31:ub,33:287,34:g,94:[1,283],103:cb,106:284,108:285},a(Ea,[2,143]),{55:[1,289]},{7:290,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11, +20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{29:[1,291]},{6:qa,131:[1,292]},{4:293,5:3,7:4,8:5,9:6,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9, +18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:u,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a([6,31,70,120],Ob,{141:77,132:102,138:103,121:294,73:[1,295],122:tb,133:D,135:A,139:E, +156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(vb,[2,173]),a([6,31,120],Ja,{69:296,70:ib}),a(Qa,[2,182]),{7:254,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,31:$a,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,73:Ua,74:53,75:54,76:179,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31, +112:L,116:298,117:V,118:X,119:G,123:177,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(Qa,[2,188]),a(Qa,[2,189]),a(Pb,[2,172]),a(Pb,[2,33]),a(t,[2,165]),{7:254,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,31:$a,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,73:Ua, +74:53,75:54,76:179,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,115:[1,299],116:300,117:V,118:X,119:G,123:177,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{30:301,31:Da,132:102,133:D,135:A,138:103,139:E,141:77,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da},a(Qb,[2,202],{141:77,132:102,138:103,133:D,134:[1,302],135:A,139:E,159:ma,160:Z,163:fa,164:ia, +165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Qb,[2,204],{141:77,132:102,138:103,133:D,134:[1,303],135:A,139:E,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(N,[2,210]),a(Xa,[2,211],{141:77,132:102,138:103,133:D,135:A,139:E,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,156,159,160,163,164,165,166,167,168, +169,170,171,172,173,174],[2,215],{140:[1,304]}),a(jb,[2,218]),{33:194,34:g,60:195,74:196,75:197,92:m,118:wa,119:e,143:305,145:193},a(jb,[2,224],{70:[1,306]}),a(kb,[2,220]),a(kb,[2,221]),a(kb,[2,222]),a(kb,[2,223]),a(N,[2,217]),{7:307,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40, +79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:308,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k, +97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:309,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V, +118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(lb,Ja,{69:310,70:Rb}),a(Aa,[2,111]),a(Aa,[2,51],{58:[1,312]}),a(Sb,[2,60],{55:[1,313]}),a(Aa,[2,56]),a(Sb,[2,61]),a(wb,[2,57]),a(wb,[2,58]),a(wb,[2,59]),{46:[1,314],78:118,82:ta,83:Na,84:Fa,85:Ga,86:114,87:Ca,90:Pa,109:117,113:Ia,114:xa},a(Nb,ua),{6:qa,42:[1,315]},a(sa,[2,4]),a(Tb,[2,258],{141:77,132:102,138:103,163:fa,164:ia,165:ga}),a(Tb,[2,259],{141:77, +132:102,138:103,163:fa,164:ia,165:ga}),a(ab,[2,260],{141:77,132:102,138:103,163:fa,165:ga}),a(ab,[2,261],{141:77,132:102,138:103,163:fa,165:ga}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,166,167,168,169,170,171,172,173,174],[2,262],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,167,168,169,170,171,172,173],[2,263],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,174:da}), +a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,168,169,170,171,172,173],[2,264],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,174:da}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,169,170,171,172,173],[2,265],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,174:da}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,170,171,172,173],[2,266],{141:77,132:102,138:103,159:ma,160:Z, +163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,174:da}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,171,172,173],[2,267],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,174:da}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,172,173],[2,268],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,174:da}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134, +135,139,140,156,173],[2,269],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,174:da}),a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,134,135,139,140,156,167,168,169,170,171,172,173,174],[2,270],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja}),a(Xa,[2,248],{141:77,132:102,138:103,133:D,135:A,139:E,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Xa,[2,247],{141:77,132:102, +138:103,133:D,135:A,139:E,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(t,[2,160]),a(t,[2,161]),a(La,[2,99]),a(La,[2,100]),a(La,[2,101]),a(La,[2,102]),{89:[1,316]},{73:Lb,89:[2,107],121:317,122:tb,132:102,133:D,135:A,138:103,139:E,141:77,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da},{89:[2,108]},{7:318,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15, +24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,89:[2,181],92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(Ub,[2,175]),a(Ub,Vb),a(La,[2,106]),a(t,[2,162]),a(sa,[2,64],{141:77,132:102,138:103,133:bb,135:bb,139:bb,156:bb, +159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Ma,[2,29],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Ma,[2,48],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),{7:319,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g, +37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:320,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57, +44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{66:321,67:c,68:w},a(Ra,db,{72:127,33:129,60:130,74:131,75:132,71:322,34:g,73:d,92:m,118:wa,119:e}),{6:Wb,31:Xb},a(Sa,[2,79]),{7:325,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11, +20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(Qa,Ob,{141:77,132:102,138:103,73:[1,326],133:D,135:A,139:E,156:za,159:ma,160:Z,163:fa,164:ia,165:ga, +166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Yb,[2,30]),{6:qa,32:[1,327]},a(Ma,[2,271],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),{7:328,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40, +79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:329,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k, +97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(Ma,[2,274],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(N,[2,246]),{7:330,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27, +48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(N,[2,193],{127:[1,331]}),{30:332,31:Da},{30:335,31:Da,33:333,34:g,75:334,92:m},{150:336,152:268,153:gb},{32:[1,337],151:[1,338],152:339,153:gb},a(mb,[2,239]),{7:341,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8, +17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,124:340,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(Zb,[2,117],{141:77,132:102,138:103,30:342,31:Da,133:D,135:A,139:E,159:ma, +160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(N,[2,120]),{7:343,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45, +139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{39:344,40:r,41:n},{92:[1,346],99:345,104:Fb},{39:347,40:r,41:n},{29:[1,348]},a(lb,Ja,{69:349,70:nb}),a(Aa,[2,130]),{31:hb,33:280,34:g,100:351,101:278,103:Wa},a(Aa,[2,135],{102:[1,352]}),a(Aa,[2,137],{102:[1,353]}),{33:354,34:g},a(Ea,[2,141]),a(lb,Ja,{69:355,70:xb}),a(Aa,[2,150]),{31:ub,33:287,34:g,103:cb,106:357,108:285},a(Aa,[2,155],{102:[1,358]}),a(Aa,[2,158],{102:[1,359]}),{6:[1,361],7:360,8:122,10:20,11:21,12:b,13:23,14:24, +15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,31:[1,362],33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(yb,[2,147],{141:77,132:102,138:103,133:D,135:A,139:E,159:ma, +160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),{39:363,40:r,41:n},a(Ba,[2,200]),{6:qa,32:[1,364]},{7:365,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W, +132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a([12,28,34,38,40,41,44,45,48,49,50,51,52,53,61,62,63,67,68,92,95,97,105,112,117,118,119,125,129,130,133,135,137,139,149,155,157,158,159,160,161,162],Vb,{6:eb,31:eb,70:eb,120:eb}),{6:ob,31:pb,120:[1,366]},a([6,31,32,115,120],db,{15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,10:20,11:21,13:23,14:24,54:26,47:27,79:28,80:29,81:30,111:31,66:33,77:40,154:41,132:43, +136:44,138:45,74:53,75:54,37:55,43:57,33:70,60:71,141:77,39:80,8:122,76:179,7:254,123:369,12:b,28:ea,34:g,38:h,40:r,41:n,44:B,45:H,48:I,49:F,50:Q,51:x,52:J,53:O,61:R,62:z,63:l,67:c,68:w,73:Ua,92:m,95:k,97:K,105:P,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,133:D,135:A,137:q,139:E,149:ba,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M}),a(Ra,Ja,{69:370,70:ib}),a(t,[2,168]),a([6,31,115],Ja,{69:371,70:ib}),a($b,[2,243]),{7:372,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14, +23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:373,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19, +28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:374,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h, +39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(jb,[2,219]),{33:194,34:g,60:195,74:196,75:197,92:m,118:wa,119:e,145:375},a([1,6,31,32,42,65,70,73,89,94,115,120,122,131,133,135,139,156],[2,226],{141:77,132:102,138:103,134:[1, +376],140:[1,377],159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(zb,[2,227],{141:77,132:102,138:103,134:[1,378],159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(zb,[2,233],{141:77,132:102,138:103,134:[1,379],159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),{6:ac,31:bc,94:[1,380]},a(Ab,db,{39:80,57:204,59:205,11:206,37:207,33:208,35:209,60:210,56:383, +34:g,36:Oa,38:h,40:r,41:n,62:z,118:wa}),{7:384,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,31:[1,385],33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v, +160:Y,161:S,162:M},{7:386,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,31:[1,387],33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M}, +a(Ba,[2,39]),a(Jb,[2,37]),a(La,[2,105]),{7:388,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,89:[2,179],92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v, +160:Y,161:S,162:M},{89:[2,180],132:102,133:D,135:A,138:103,139:E,141:77,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da},a(Ma,[2,49],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),{32:[1,389],132:102,133:D,135:A,138:103,139:E,141:77,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da},{30:390,31:Da},a(Sa,[2,75]),{33:129, +34:g,60:130,71:391,72:127,73:d,74:131,75:132,92:m,118:wa,119:e},a(cc,p,{71:126,72:127,33:129,60:130,74:131,75:132,64:392,34:g,73:d,92:m,118:wa,119:e}),a(Sa,[2,80],{141:77,132:102,138:103,133:D,135:A,139:E,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Qa,eb),a(Yb,[2,31]),{32:[1,393],132:102,133:D,135:A,138:103,139:E,141:77,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da},a(Ma,[2,273], +{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),{30:394,31:Da,132:102,133:D,135:A,138:103,139:E,141:77,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da},{30:395,31:Da},a(N,[2,194]),{30:396,31:Da},{30:397,31:Da},a(Bb,[2,198]),{32:[1,398],151:[1,399],152:339,153:gb},a(N,[2,237]),{30:400,31:Da},a(mb,[2,240]),{30:401,31:Da,70:[1,402]},a(dc,[2,190],{141:77,132:102,138:103,133:D, +135:A,139:E,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(N,[2,118]),a(Zb,[2,121],{141:77,132:102,138:103,30:403,31:Da,133:D,135:A,139:E,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Ea,[2,124]),{29:[1,404]},{31:hb,33:280,34:g,100:405,101:278,103:Wa},a(Ea,[2,125]),{39:406,40:r,41:n},{6:qb,31:rb,94:[1,407]},a(Ab,db,{33:280,101:410,34:g,103:Wa}),a(Ra,Ja,{69:411,70:nb}),{33:412,34:g}, +{33:413,34:g},{29:[2,140]},{6:Cb,31:Db,94:[1,414]},a(Ab,db,{33:287,108:417,34:g,103:cb}),a(Ra,Ja,{69:418,70:xb}),{33:419,34:g,103:[1,420]},{33:421,34:g},a(yb,[2,144],{141:77,132:102,138:103,133:D,135:A,139:E,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),{7:422,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q, +51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:423,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R, +62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(Ea,[2,148]),{131:[1,424]},{120:[1,425],132:102,133:D,135:A,138:103,139:E,141:77,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da},a(vb,[2,174]),{7:254,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9, +18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,73:Ua,74:53,75:54,76:179,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,123:426,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:254,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11, +20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,31:$a,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,73:Ua,74:53,75:54,76:179,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,116:427,117:V,118:X,119:G,123:177,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(Qa,[2,183]),{6:ob,31:pb,32:[1,428]},{6:ob,31:pb,115:[1,429]}, +a(Xa,[2,203],{141:77,132:102,138:103,133:D,135:A,139:E,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Xa,[2,205],{141:77,132:102,138:103,133:D,135:A,139:E,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Xa,[2,216],{141:77,132:102,138:103,133:D,135:A,139:E,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(jb,[2,225]),{7:430,8:122,10:20,11:21, +12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:431,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9, +18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:432,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14, +23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:433,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19, +28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(vb,[2,109]),{11:206,33:208,34:g,35:209,36:Oa,37:207,38:h,39:80,40:r,41:n,56:434,57:204,59:205,60:210,62:z,118:wa},a(cc,Hb,{39:80,56:203,57:204, +59:205,11:206,37:207,33:208,35:209,60:210,93:435,34:g,36:Oa,38:h,40:r,41:n,62:z,118:wa}),a(Aa,[2,112]),a(Aa,[2,52],{141:77,132:102,138:103,133:D,135:A,139:E,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),{7:436,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l, +66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(Aa,[2,54],{141:77,132:102,138:103,133:D,135:A,139:E,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),{7:437,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18, +27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{89:[2,178],132:102,133:D,135:A,138:103,139:E,141:77,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na, +173:ra,174:da},a(N,[2,50]),a(N,[2,67]),a(Sa,[2,76]),a(Ra,Ja,{69:438,70:Mb}),a(N,[2,272]),a($b,[2,244]),a(N,[2,195]),a(Bb,[2,196]),a(Bb,[2,197]),a(N,[2,235]),{30:439,31:Da},{32:[1,440]},a(mb,[2,241],{6:[1,441]}),{7:442,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30, +92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},a(N,[2,122]),{39:443,40:r,41:n},a(lb,Ja,{69:444,70:nb}),a(Ea,[2,126]),{29:[1,445]},{33:280,34:g,101:446,103:Wa},{31:hb,33:280,34:g,100:447,101:278,103:Wa},a(Aa,[2,131]),{6:qb,31:rb,32:[1,448]},a(Aa,[2,136]),a(Aa,[2,138]),a(Ea,[2,142],{29:[1,449]}),{33:287,34:g,103:cb,108:450},{31:ub,33:287,34:g,103:cb,106:451,108:285}, +a(Aa,[2,151]),{6:Cb,31:Db,32:[1,452]},a(Aa,[2,156]),a(Aa,[2,157]),a(Aa,[2,159]),a(yb,[2,145],{141:77,132:102,138:103,133:D,135:A,139:E,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),{32:[1,453],132:102,133:D,135:A,138:103,139:E,141:77,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da},a(Ba,[2,201]),a(Ba,[2,177]),a(Qa,[2,184]),a(Ra,Ja,{69:454,70:ib}),a(Qa,[2,185]),a(t,[2,169]),a([1,6,31,32,42, +65,70,73,89,94,115,120,122,131,133,134,135,139,156],[2,228],{141:77,132:102,138:103,140:[1,455],159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(zb,[2,230],{141:77,132:102,138:103,134:[1,456],159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Ma,[2,229],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Ma,[2,234],{141:77,132:102, +138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Aa,[2,113]),a(Ra,Ja,{69:457,70:Rb}),{32:[1,458],132:102,133:D,135:A,138:103,139:E,141:77,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da},{32:[1,459],132:102,133:D,135:A,138:103,139:E,141:77,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da},{6:Wb,31:Xb,32:[1,460]},{32:[1,461]},a(N, +[2,238]),a(mb,[2,242]),a(dc,[2,191],{141:77,132:102,138:103,133:D,135:A,139:E,156:za,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Ea,[2,128]),{6:qb,31:rb,94:[1,462]},{39:463,40:r,41:n},a(Aa,[2,132]),a(Ra,Ja,{69:464,70:nb}),a(Aa,[2,133]),{39:465,40:r,41:n},a(Aa,[2,152]),a(Ra,Ja,{69:466,70:xb}),a(Aa,[2,153]),a(Ea,[2,146]),{6:ob,31:pb,32:[1,467]},{7:468,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16, +25:17,26:18,27:19,28:ea,33:70,34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{7:469,8:122,10:20,11:21,12:b,13:23,14:24,15:7,16:8,17:9,18:10,19:11,20:12,21:13,22:14,23:15,24:16,25:17,26:18,27:19,28:ea,33:70, +34:g,37:55,38:h,39:80,40:r,41:n,43:57,44:B,45:H,47:27,48:I,49:F,50:Q,51:x,52:J,53:O,54:26,60:71,61:R,62:z,63:l,66:33,67:c,68:w,74:53,75:54,77:40,79:28,80:29,81:30,92:m,95:k,97:K,105:P,111:31,112:L,117:V,118:X,119:G,125:aa,129:U,130:W,132:43,133:D,135:A,136:44,137:q,138:45,139:E,141:77,149:ba,154:41,155:ca,157:C,158:T,159:v,160:Y,161:S,162:M},{6:ac,31:bc,32:[1,470]},a(Aa,[2,53]),a(Aa,[2,55]),a(Sa,[2,77]),a(N,[2,236]),{29:[1,471]},a(Ea,[2,127]),{6:qb,31:rb,32:[1,472]},a(Ea,[2,149]),{6:Cb,31:Db,32:[1, +473]},a(Qa,[2,186]),a(Ma,[2,231],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Ma,[2,232],{141:77,132:102,138:103,159:ma,160:Z,163:fa,164:ia,165:ga,166:ja,167:la,168:oa,169:pa,170:ha,171:ka,172:na,173:ra,174:da}),a(Aa,[2,114]),{39:474,40:r,41:n},a(Aa,[2,134]),a(Aa,[2,154]),a(Ea,[2,129])],defaultActions:{68:[2,69],69:[2,70],238:[2,108],354:[2,140]},parseError:function(a,d){if(d.recoverable)this.trace(a);else{var e=function(a, +d){this.message=a;this.hash=d};e.prototype=Error;throw new e(a,d);}},parse:function(a){var d=[0],e=[null],b=[],p=this.table,t="",wa=0,c=0,g=0,Da=b.slice.call(arguments,1),k=Object.create(this.lexer),h={};for(f in this.yy)Object.prototype.hasOwnProperty.call(this.yy,f)&&(h[f]=this.yy[f]);k.setInput(a,h);h.lexer=k;h.parser=this;"undefined"==typeof k.yylloc&&(k.yylloc={});var f=k.yylloc;b.push(f);var l=k.options&&k.options.ranges;this.parseError="function"===typeof h.parseError?h.parseError:Object.getPrototypeOf(this).parseError; +for(var m,Ta,Ha,n,ua={},y,w;;){Ha=d[d.length-1];if(this.defaultActions[Ha])n=this.defaultActions[Ha];else{if(null===m||"undefined"==typeof m)m=k.lex()||1,"number"!==typeof m&&(m=this.symbols_[m]||m);n=p[Ha]&&p[Ha][m]}if("undefined"===typeof n||!n.length||!n[0]){w=[];for(y in p[Ha])this.terminals_[y]&&2=ta?this.wrapInBraces(d):d};b.prototype.compileRoot=function(a){var d,b;a.indent=a.bare?"":Ca;a.level=N;this.spaced=!0;a.scope=new xa(null,this,null,null!=(b=a.referencedVars)?b:[]);var e=a.locals||[];b=0;for(d=e.length;b=Fa?this.wrapInBraces(d): +d};return b}(w);f.StringLiteral=D=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);return b}(z);f.RegexLiteral=X=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);return b}(z);f.PassthroughLiteral=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);return b}(z);f.IdentifierLiteral=x=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);b.prototype.isAssignable=ha; +return b}(z);f.PropertyName=L=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);b.prototype.isAssignable=ha;return b}(z);f.StatementLiteral=W=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);b.prototype.isStatement=ha;b.prototype.makeReturn=na;b.prototype.jumps=function(a){if("break"===this.value&&!(null!=a&&a.loop||null!=a&&a.block)||"continue"===this.value&&(null==a||!a.loop))return this};b.prototype.compileNode=function(a){return[this.makeCode(""+ +this.tab+this.value+";")]};return b}(z);f.ThisLiteral=E=function(a){function b(){b.__super__.constructor.call(this,"this")}v(b,a);b.prototype.compileNode=function(a){var d;a=null!=(d=a.scope.method)&&d.bound?a.scope.method.context:this.value;return[this.makeCode(a)]};return b}(z);f.UndefinedLiteral=ca=function(a){function b(){b.__super__.constructor.call(this,"undefined")}v(b,a);b.prototype.compileNode=function(a){return[this.makeCode(a.level>=Ga?"(void 0)":"void 0")]};return b}(z);f.NullLiteral= +c=function(a){function b(){b.__super__.constructor.call(this,"null")}v(b,a);return b}(z);f.BooleanLiteral=b=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);return b}(z);f.Return=G=function(a){function b(a){this.expression=a}v(b,a);b.prototype.children=["expression"];b.prototype.isStatement=ha;b.prototype.makeReturn=na;b.prototype.jumps=na;b.prototype.compileToFragments=function(a,d){var p;var e=null!=(p=this.expression)?p.makeReturn():void 0;return!e||e instanceof +b?b.__super__.compileToFragments.call(this,a,d):e.compileToFragments(a,d)};b.prototype.compileNode=function(a){var b=[];b.push(this.makeCode(this.tab+("return"+(this.expression?" ":""))));this.expression&&(b=b.concat(this.expression.compileToFragments(a,Ka)));b.push(this.makeCode(";"));return b};return b}(sa);f.YieldReturn=T=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);b.prototype.compileNode=function(a){null==a.scope.parent&&this.error("yield can only occur inside functions"); +return b.__super__.compileNode.apply(this,arguments)};return b}(G);f.Value=C=function(a){function t(a,b,wa){if(!b&&a instanceof t)return a;this.base=a;this.properties=b||[];wa&&(this[wa]=!0);return this}v(t,a);t.prototype.children=["base","properties"];t.prototype.add=function(a){this.properties=this.properties.concat(a);return this};t.prototype.hasProperties=function(){return!!this.properties.length};t.prototype.bareLiteral=function(a){return!this.properties.length&&this.base instanceof a};t.prototype.isArray= +function(){return this.bareLiteral(q)};t.prototype.isRange=function(){return this.bareLiteral(V)};t.prototype.isComplex=function(){return this.hasProperties()||this.base.isComplex()};t.prototype.isAssignable=function(){return this.hasProperties()||this.base.isAssignable()};t.prototype.isNumber=function(){return this.bareLiteral(w)};t.prototype.isString=function(){return this.bareLiteral(D)};t.prototype.isRegex=function(){return this.bareLiteral(X)};t.prototype.isUndefined=function(){return this.bareLiteral(ca)}; +t.prototype.isNull=function(){return this.bareLiteral(c)};t.prototype.isBoolean=function(){return this.bareLiteral(b)};t.prototype.isAtomic=function(){var a;var b=this.properties.concat(this.base);var wa=0;for(a=b.length;wathis.properties.length&&!this.base.isComplex()&&(null==p||!p.isComplex()))return[this,this];b=new t(this.base,this.properties.slice(0,-1));if(b.isComplex()){var e=new x(a.scope.freeVariable("base"));b=new t(new P(new y(e, +b)))}if(!p)return[b,e];if(p.isComplex()){var c=new x(a.scope.freeVariable("name"));p=new R(new y(c,p.index));c=new R(c)}return[b.add(p),new t(e||b.base,[c||p])]};t.prototype.compileNode=function(a){var b;this.base.front=this.front;var p=this.properties;var e=this.base.compileToFragments(a,p.length?Ga:null);p.length&&Pa.test(da(e))&&e.push(this.makeCode("."));var t=0;for(b=p.length;t=Math.abs(this.fromNum-this.toNum)){var c=function(){e=[];for(var a=p=this.fromNum,b=this.toNum;p<=b?a<=b:a>=b;p<=b?a++:a--)e.push(a);return e}.apply(this);this.exclusive&&c.pop();return[this.makeCode("["+c.join(", ")+"]")]}var t=this.tab+Ca;var f=a.scope.freeVariable("i",{single:!0});var g=a.scope.freeVariable("results");var k="\n"+t+g+" \x3d [];";if(b)a.index=f,b=da(this.compileNode(a));else{var h= +f+" \x3d "+this.fromC+(this.toC!==this.toVar?", "+this.toC:"");b=this.fromVar+" \x3c\x3d "+this.toVar;b="var "+h+"; "+b+" ? "+f+" \x3c"+this.equals+" "+this.toVar+" : "+f+" \x3e"+this.equals+" "+this.toVar+"; "+b+" ? "+f+"++ : "+f+"--"}f="{ "+g+".push("+f+"); }\n"+t+"return "+g+";\n"+a.indent;a=function(a){return null!=a?a.contains(Va):void 0};if(a(this.from)||a(this.to))c=", arguments";return[this.makeCode("(function() {"+k+"\n"+t+"for ("+b+")"+f+"}).apply(this"+(null!=c?c:"")+")")]};return b}(sa); +f.Slice=aa=function(a){function b(a){this.range=a;b.__super__.constructor.call(this)}v(b,a);b.prototype.children=["range"];b.prototype.compileNode=function(a){var b=this.range;var p=b.to;var e=(b=b.from)&&b.compileToFragments(a,Ka)||[this.makeCode("0")];if(p){b=p.compileToFragments(a,Ka);var c=da(b);if(this.range.exclusive||-1!==+c)var t=", "+(this.range.exclusive?c:p.isNumber()?""+(+c+1):(b=p.compileToFragments(a,Ga),"+"+da(b)+" + 1 || 9e9"))}return[this.makeCode(".slice("+da(e)+(t||"")+")")]};return b}(sa); +f.Obj=m=function(a){function b(a,b){this.generated=null!=b?b:!1;this.objects=this.properties=a||[]}v(b,a);b.prototype.children=["properties"];b.prototype.compileNode=function(a){var b,p,e;var c=this.properties;if(this.generated){var t=0;for(b=c.length;t= +Fa?this.wrapInBraces(t):t}var h=g[0];1===e&&h instanceof H&&h.error("Destructuring assignment has no target");var m=this.variable.isObject();if(p&&1===e&&!(h instanceof U)){var l=null;if(h instanceof b&&"object"===h.context){t=h;var n=t.variable;var q=n.base;h=t.value;h instanceof b&&(l=h.value,h=h.variable)}else h instanceof b&&(l=h.value,h=h.variable),q=m?h["this"]?h.properties[0].name:new L(h.unwrap().value):new w(0);var r=q.unwrap()instanceof L;f=new C(f);f.properties.push(new (r?qa:R)(q));(c= +za(h.unwrap().value))&&h.error(c);l&&(f=new k("?",f,l));return(new b(h,f,null,{param:this.param})).compileToFragments(a,N)}var v=f.compileToFragments(a,ta);var y=da(v);t=[];n=!1;f.unwrap()instanceof x&&!this.variable.assigns(y)||(t.push([this.makeCode((l=a.scope.freeVariable("ref"))+" \x3d ")].concat(M.call(v))),v=[this.makeCode(l)],y=l);l=f=0;for(d=g.length;fN?this.wrapInBraces(e):e};return b}(sa);f.Code=h=function(b){function c(b,d,c){this.params=b||[];this.body=d||new a;this.bound="boundfunc"===c;this.isGenerator=!!this.body.contains(function(a){return a instanceof k&&a.isYield()|| +a instanceof T})}v(c,b);c.prototype.children=["params","body"];c.prototype.isStatement=function(){return!!this.ctor};c.prototype.jumps=ka;c.prototype.makeScope=function(a){return new xa(a,this.body,this)};c.prototype.compileNode=function(b){var d,f,e,g;this.bound&&null!=(d=b.scope.method)&&d.bound&&(this.context=b.scope.method.context);if(this.bound&&!this.context)return this.context="_this",d=new c([new K(new x(this.context))],new a([this])),d=new ya(d,[new E]),d.updateLocationDataIfMissing(this.locationData), +d.compileNode(b);b.scope=la(b,"classScope")||this.makeScope(b.scope);b.scope.shared=la(b,"sharedScope");b.indent+=Ca;delete b.bare;delete b.isExistentialEquals;d=[];var p=[];var h=this.params;var t=0;for(e=h.length;t=Ga?this.wrapInBraces(p):p};c.prototype.eachParamName=function(a){var b;var c=this.params;var e=[];var f=0;for(b=c.length;f=d.length)return[];if(1===d.length)return e=d[0],d=e.compileToFragments(a,ta),c?d:[].concat(e.makeCode(Ia("slice",a)+".call("),d,e.makeCode(")"));c=d.slice(f);var h=g=0;for(p=c.length;g< +p;h=++g){e=c[h];var k=e.compileToFragments(a,ta);c[h]=e instanceof b?[].concat(e.makeCode(Ia("slice",a)+".call("),k,e.makeCode(")")):[].concat(e.makeCode("["),k,e.makeCode("]"))}if(0===f)return e=d[0],a=e.joinFragmentArrays(c.slice(1),", "),c[0].concat(e.makeCode(".concat("),a,e.makeCode(")"));g=d.slice(0,f);p=[];k=0;for(h=g.length;k=Ga)return(new P(this)).compileToFragments(a);var f="+"===c||"-"===c;("new"===c||"typeof"===c||"delete"===c||f&&this.first instanceof b&&this.first.operator===c)&&d.push([this.makeCode(" ")]);if(f&&this.first instanceof b||"new"===c&&this.first.isStatement(a))this.first=new P(this.first);d.push(this.first.compileToFragments(a,Fa));this.flip&&d.reverse();return this.joinFragmentArrays(d,"")};b.prototype.compileYield=function(a){var b; +var d=[];var c=this.operator;null==a.scope.parent&&this.error("yield can only occur inside functions");0<=S.call(Object.keys(this.first),"expression")&&!(this.first instanceof ba)?null!=this.first.expression&&d.push(this.first.expression.compileToFragments(a,Fa)):(a.level>=Ka&&d.push([this.makeCode("(")]),d.push([this.makeCode(c)]),""!==(null!=(b=this.first.base)?b.value:void 0)&&d.push([this.makeCode(" ")]),d.push(this.first.compileToFragments(a,Fa)),a.level>=Ka&&d.push([this.makeCode(")")]));return this.joinFragmentArrays(d, +"")};b.prototype.compilePower=function(a){var b=new C(new x("Math"),[new qa(new L("pow"))]);return(new ya(b,[this.first,this.second])).compileToFragments(a)};b.prototype.compileFloorDivision=function(a){var d=new C(new x("Math"),[new qa(new L("floor"))]);var c=this.second.isComplex()?new P(this.second):this.second;c=new b("/",this.first,c);return(new ya(d,[c])).compileToFragments(a)};b.prototype.compileModulo=function(a){var b=new C(new z(Ia("modulo",a)));return(new ya(b,[this.first,this.second])).compileToFragments(a)}; +b.prototype.toString=function(a){return b.__super__.toString.call(this,a,this.constructor.name+" "+this.operator)};return b}(sa);f.In=O=function(a){function b(a,b){this.object=a;this.array=b}v(b,a);b.prototype.children=["object","array"];b.prototype.invert=ra;b.prototype.compileNode=function(a){var b;if(this.array instanceof C&&this.array.isArray()&&this.array.base.objects.length){var c=this.array.base.objects;var e=0;for(b=c.length;e=c.length)?c:this.wrapInBraces(c)};return b}(sa); +f.StringWithInterpolations=A=function(a){function b(){return b.__super__.constructor.apply(this,arguments)}v(b,a);b.prototype.compileNode=function(a){var d;if(!a.inTaggedTemplateCall)return b.__super__.compileNode.apply(this,arguments);var c=this.body.unwrap();var e=[];c.traverseChildren(!1,function(a){if(a instanceof D)e.push(a);else if(a instanceof P)return e.push(a),!1;return!0});c=[];c.push(this.makeCode("`"));var f=0;for(d=e.length;fh,this.step&&null!=h&&e||(d=n.freeVariable("len")),K=""+t+f+" \x3d 0, "+d+" \x3d "+A+".length",w=""+t+f+" \x3d "+A+".length - 1",d=f+" \x3c "+d,n=f+" \x3e\x3d 0",this.step?(null!=h?e&&(d= +n,K=w):(d=r+" \x3e 0 ? "+d+" : "+n,K="("+r+" \x3e 0 ? ("+K+") : "+w+")"),f=f+" +\x3d "+r):f=""+(q!==f?"++"+f:f+"++"),K=[this.makeCode(K+"; "+d+"; "+t+f)])}if(this.returns){var B=""+this.tab+c+" \x3d [];\n";var V="\n"+this.tab+"return "+c+";";l.makeReturn(c)}this.guard&&(1=Na?this.wrapInBraces(e):e};c.prototype.unfoldSoak=function(){return this.soak&&this};return c}(sa);var gc={extend:function(a){return"function(child, parent) { for (var key in parent) { if ("+Ia("hasProp",a)+".call(parent, key)) child[key] \x3d parent[key]; } function ctor() { this.constructor \x3d child; } ctor.prototype \x3d parent.prototype; child.prototype \x3d new ctor(); child.__super__ \x3d parent.prototype; return child; }"},bind:function(){return"function(fn, me){ return function(){ return fn.apply(me, arguments); }; }"}, +indexOf:function(){return"[].indexOf || function(item) { for (var i \x3d 0, l \x3d this.length; i \x3c l; i++) { if (i in this \x26\x26 this[i] \x3d\x3d\x3d item) return i; } return -1; }"},modulo:function(){return"function(a, b) { return (+a % (b \x3d +b) + b) % b; }"},hasProp:function(){return"{}.hasOwnProperty"},slice:function(){return"[].slice"}};var N=1;var Ka=2;var ta=3;var Na=4;var Fa=5;var Ga=6;var Ca=" ";var Pa=/^[+-]?\d+$/;var Ia=function(a,b){var c=b.scope.root;if(a in c.utilities)return c.utilities[a]; +var d=c.freeVariable(a);c.assign(d,gc[a](b));return c.utilities[a]=d};var Ea=function(a,b){a=a.replace(/\n/g,"$\x26"+b);return a.replace(/\s+$/,"")};var Va=function(a){return a instanceof x&&"arguments"===a.value};var ea=function(a){return a instanceof E||a instanceof h&&a.bound||a instanceof va};var Ya=function(a){return a.isComplex()||("function"===typeof a.isAssignable?a.isAssignable():void 0)};var Ba=function(a,b,c){if(a=b[c].unfoldSoak(a))return b[c]=a.body,a.body=new C(b),a}}).call(this);return f}(); +u["./sourcemap"]=function(){var f={};(function(){var u=function(){function f(f){this.line=f;this.columns=[]}f.prototype.add=function(f,a,b){var q=a[0];a=a[1];null==b&&(b={});if(!this.columns[f]||!b.noReplace)return this.columns[f]={line:this.line,column:f,sourceLine:q,sourceColumn:a}};f.prototype.sourceLocation=function(f){for(var a;!((a=this.columns[f])||0>=f);)f--;return a&&[a.sourceLine,a.sourceColumn]};return f}();f=function(){function f(){this.lines=[]}f.prototype.add=function(f,a,b){var q;null== +b&&(b={});var g=a[0];a=a[1];return((q=this.lines)[g]||(q[g]=new u(g))).add(a,f,b)};f.prototype.sourceLocation=function(f){var a;var b=f[0];for(f=f[1];!((a=this.lines[b])||0>=b);)b--;return a&&a.sourceLocation(f)};f.prototype.generate=function(f,a){var b,q,g,h,r,n,u;null==f&&(f={});null==a&&(a=null);var y=g=q=u=0;var I=!1;var F="";var Q=this.lines;var x=b=0;for(h=Q.length;bf?1:0);a||!b;)f=a&31,(a>>=5)&&(f|=32),b+=this.encodeBase64(f);return b};f.prototype.encodeBase64=function(f){var a;if(!(a= +"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[f]))throw Error("Cannot Base64 encode value: "+f);return a};return f}()}).call(this);return f}();u["./coffee-script"]=function(){var f={};(function(){var qa,q,y={}.hasOwnProperty;var a=u("fs");var b=u("vm");var ya=u("path");var g=u("./lexer").Lexer;var h=u("./parser").parser;var r=u("./helpers");var n=u("./sourcemap");var B=u("../../package.json");f.VERSION=B.version;f.FILE_EXTENSIONS=[".coffee",".litcoffee",".coffee.md"];f.helpers= +r;var H=function(a){switch(!1){case "function"!==typeof Buffer:return(new Buffer(a)).toString("base64");case "function"!==typeof btoa:return btoa(encodeURIComponent(a).replace(/%([0-9A-F]{2})/g,function(a,b){return String.fromCharCode("0x"+b)}));default:throw Error("Unable to base64 encode inline sourcemap.");}};B=function(a){return function(b,f){null==f&&(f={});try{return a.call(this,b,f)}catch(m){if("string"!==typeof b)throw m;throw r.updateSyntaxError(m,b,f.filename);}}};var I={};var F={};f.compile= +qa=B(function(a,b){var c,f,g,l;var q=r.extend;b=q({},b);var u=b.sourceMap||b.inlineMap||null==b.filename;q=b.filename||"\x3canonymous\x3e";I[q]=a;u&&(g=new n);var x=O.tokenize(a,b);var y=b;var G=[];var z=0;for(c=x.length;z Date: Thu, 18 May 2017 03:05:02 +0200 Subject: [PATCH 0029/2570] Fix multi-line confirmation dialog --- src/Ui/media/Wrapper.coffee | 10 ++++++---- src/Ui/media/Wrapper.css | 4 +++- src/Ui/media/all.css | 4 +++- src/Ui/media/all.js | 12 +++++++----- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index 89538805..f87ff197 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -204,17 +204,19 @@ class Wrapper @notifications.add("notification-#{message.id}", message.params[0], body, message.params[2]) displayConfirm: (message, captions, cb) -> - body = $(""+message+"") + body = $(""+message+"") + buttons = $("") if captions not instanceof Array then captions = [captions] # Convert to list if necessary for caption, i in captions - button = $("#{caption}") # Add confirm button + button = $("#{caption}") # Add confirm button button.on "click", (e) => cb(parseInt(e.currentTarget.dataset.value)) return false - body.append(button) + buttons.append(button) + body.append(buttons) @notifications.add("notification-#{caption}", "ask", body) - button.focus() + buttons.first().focus() $(".notification").scrollLeft(0) diff --git a/src/Ui/media/Wrapper.css b/src/Ui/media/Wrapper.css index aa32b60c..4859112f 100644 --- a/src/Ui/media/Wrapper.css +++ b/src/Ui/media/Wrapper.css @@ -54,8 +54,10 @@ a { color: black } padding-left: 14px; padding-right: 60px; height: 40px; vertical-align: middle; display: table; background-color: white; left: 50px; top: 0; position: relative; padding-top: 5px; padding-bottom: 5px; } +.notification .message-outer { display: table-row } +.notification .buttons { display: table-cell; vertical-align: top; padding-top: 9px; } .notification.long .body { padding-top: 10px; padding-bottom: 10px } -.notification .message { display: table-cell; vertical-align: middle } +.notification .message { display: table-cell; vertical-align: middle; } .notification.visible { max-width: 350px } diff --git a/src/Ui/media/all.css b/src/Ui/media/all.css index a315403a..ff64e90b 100644 --- a/src/Ui/media/all.css +++ b/src/Ui/media/all.css @@ -59,8 +59,10 @@ a { color: black } padding-left: 14px; padding-right: 60px; height: 40px; vertical-align: middle; display: table; background-color: white; left: 50px; top: 0; position: relative; padding-top: 5px; padding-bottom: 5px; } +.notification .message-outer { display: table-row } +.notification .buttons { display: table-cell; vertical-align: top; padding-top: 9px; } .notification.long .body { padding-top: 10px; padding-bottom: 10px } -.notification .message { display: table-cell; vertical-align: middle } +.notification .message { display: table-cell; vertical-align: middle; } .notification.visible { max-width: 350px } diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index 6280a746..04acc0de 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -1087,24 +1087,26 @@ jQuery.extend( jQuery.easing, }; Wrapper.prototype.displayConfirm = function(message, captions, cb) { - var body, button, caption, i, j, len; - body = $("" + message + ""); + var body, button, buttons, caption, i, j, len; + body = $("" + message + ""); + buttons = $(""); if (!(captions instanceof Array)) { captions = [captions]; } for (i = j = 0, len = captions.length; j < len; i = ++j) { caption = captions[i]; - button = $("" + caption + ""); + button = $("" + caption + ""); button.on("click", (function(_this) { return function(e) { cb(parseInt(e.currentTarget.dataset.value)); return false; }; })(this)); - body.append(button); + buttons.append(button); } + body.append(buttons); this.notifications.add("notification-" + caption, "ask", body); - button.focus(); + buttons.first().focus(); return $(".notification").scrollLeft(0); }; From cbf184846a94cc78fa1aef88c74c0f2942e940fa Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 18 May 2017 03:05:45 +0200 Subject: [PATCH 0030/2570] Fix permissionAdd/Remove admin command --- 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 232a94eb..22680efb 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -35,7 +35,7 @@ class UiWebsocket(object): self.admin_commands = ( "sitePause", "siteResume", "siteDelete", "siteList", "siteSetLimit", "siteClone", "channelJoinAllsite", "serverUpdate", "serverPortcheck", "serverShutdown", "certSet", "configSet", - "actionPermissionAdd", "actionPermissionRemove" + "permissionAdd", "permissionRemove" ) self.async_commands = ("fileGet", "fileList", "dirList") From edd726ac9fded756be7e27af2f41decd4df00f15 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 18 May 2017 03:07:32 +0200 Subject: [PATCH 0031/2570] Site upgrade support in clone command --- src/Ui/UiWebsocket.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 22680efb..ea65d567 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -757,16 +757,24 @@ class UiWebsocket(object): else: self.response(to, {"error": "Unknown site: %s" % address}) - def actionSiteClone(self, to, address, root_inner_path=""): + def actionSiteClone(self, to, address, root_inner_path="", target_address=None): self.cmd("notification", ["info", _["Cloning site..."]]) site = self.server.sites.get(address) - # Generate a new site from user's bip32 seed - new_address, new_address_index, new_site_data = self.user.getNewSiteData() - new_site = site.clone(new_address, new_site_data["privatekey"], address_index=new_address_index, root_inner_path=root_inner_path) - new_site.settings["own"] = True - new_site.saveSettings() - self.cmd("notification", ["done", _["Site cloned"] + "" % new_address]) - gevent.spawn(new_site.announce) + if 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!"]]) + site.publish() + else: + # Generate a new site from user's bip32 seed + new_address, new_address_index, new_site_data = self.user.getNewSiteData() + new_site = site.clone(new_address, new_site_data["privatekey"], address_index=new_address_index, root_inner_path=root_inner_path) + new_site.settings["own"] = True + new_site.saveSettings() + self.cmd("notification", ["done", _["Site cloned"] + "" % new_address]) + gevent.spawn(new_site.announce) + def actionSiteSetLimit(self, to, size_limit): self.site.settings["size_limit"] = int(size_limit) From 077b3df3cd7ba8898a367b24e1e6e9e6a601bc9b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 18 May 2017 03:07:43 +0200 Subject: [PATCH 0032/2570] Rev2080 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 37fcf1c5..14370d1f 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.4" - self.rev = 2068 + self.rev = 2080 self.argv = argv self.action = None self.config_file = "zeronet.conf" From e6680b4f60aec5d3fa576413d207a7842600edad Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 18 May 2017 23:03:26 +0200 Subject: [PATCH 0033/2570] Version 0.5.5, Rev2081, Lack of JS notification --- src/Config.py | 4 ++-- src/Ui/template/wrapper.html | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index 14370d1f..2decbb0a 100644 --- a/src/Config.py +++ b/src/Config.py @@ -9,8 +9,8 @@ import ConfigParser class Config(object): def __init__(self, argv): - self.version = "0.5.4" - self.rev = 2080 + self.version = "0.5.5" + self.rev = 2081 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Ui/template/wrapper.html b/src/Ui/template/wrapper.html index 443723d2..7fd67873 100644 --- a/src/Ui/template/wrapper.html +++ b/src/Ui/template/wrapper.html @@ -11,6 +11,9 @@ + " % session_id self.cmd("notification", ["done", message]) + + 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/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index ea65d567..495bac88 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -48,52 +48,21 @@ class UiWebsocket(object): file_server = sys.modules["main"].file_server if file_server.port_opened is None or file_server.tor_manager.start_onions is None: self.site.page_requested = False # Not ready yet, check next time - elif file_server.port_opened is True: - self.site.notifications.append([ - "done", - _["Congratulation, your port {0} is opened.
You are full member of ZeroNet network!"].format(config.fileserver_port), - 10000 - ]) - elif config.tor == "always" and file_server.tor_manager.start_onions: - self.site.notifications.append([ - "done", - _(u""" - {_[Tor mode active, every connection using Onion route.]}
- {_[Successfully started Tor onion hidden services.]} - """), - 10000 - ]) - elif config.tor == "always" and file_server.tor_manager.start_onions is not False: - self.site.notifications.append([ - "error", - _(u""" - {_[Tor mode active, every connection using Onion route.]}
- {_[Unable to start hidden services, please check your config.]} - """), - 0 - ]) - elif file_server.port_opened is False and file_server.tor_manager.start_onions: - self.site.notifications.append([ - "done", - _(u""" - {_[Successfully started Tor onion hidden services.]}
- {_[For faster connections open {0} port on your router.]} - """).format(config.fileserver_port), - 10000 - ]) else: - self.site.notifications.append([ - "error", - _(u""" - {_[Your connection is restricted. Please, open {0} port on your router]}
- {_[or configure Tor to become full member of ZeroNet network.]} - """).format(config.fileserver_port), - 0 - ]) + try: + self.addHomepageNotifications() + except Exception, err: + self.log.error("Uncaught Exception: " + Debug.formatException(err)) for notification in self.site.notifications: # Send pending notification messages + # send via WebSocket self.cmd("notification", notification) + # just in case, log them to terminal + if notification[0] == "error": + self.log.error("\n*** %s\n" % self.dedent(notification[1])) + self.site.notifications = [] + while True: try: message = ws.receive() @@ -107,7 +76,71 @@ class UiWebsocket(object): if config.debug: # Allow websocket errors to appear on /Debug sys.modules["main"].DebugHook.handleError() self.log.error("WebSocket handleRequest error: %s \n %s" % (Debug.formatException(err), message)) - self.cmd("error", "Internal error: %s" % Debug.formatException(err, "html")) + if not self.hasPlugin("Multiuser"): + self.cmd("error", "Internal error: %s" % Debug.formatException(err, "html")) + + def dedent(self, text): + return re.sub("[\\r\\n\\x20\\t]+", " ", text.strip().replace("
", " ")) + + def addHomepageNotifications(self): + if not(self.hasPlugin("Multiuser")) and not(self.hasPlugin("UiPassword")): + bind_ip = getattr(config, "ui_ip", "") + whitelist = getattr(config, "ui_restrict", []) + # binds to the Internet, no IP whitelist, no UiPassword, no Multiuser + if ("0.0.0.0" == bind_ip or "*" == bind_ip) and (not whitelist): + self.site.notifications.append([ + "error", + _(u"You are not going to set up a public gateway. However, your Web UI is
" + \ + "open to the whole Internet.
" + \ + "Please check your configuration.") + ]) + + file_server = sys.modules["main"].file_server + if file_server.port_opened is True: + self.site.notifications.append([ + "done", + _["Congratulation, your port {0} is opened.
You are full member of ZeroNet network!"].format(config.fileserver_port), + 10000 + ]) + elif config.tor == "always" and file_server.tor_manager.start_onions: + self.site.notifications.append([ + "done", + _(u""" + {_[Tor mode active, every connection using Onion route.]}
+ {_[Successfully started Tor onion hidden services.]} + """), + 10000 + ]) + elif config.tor == "always" and file_server.tor_manager.start_onions is not False: + self.site.notifications.append([ + "error", + _(u""" + {_[Tor mode active, every connection using Onion route.]}
+ {_[Unable to start hidden services, please check your config.]} + """), + 0 + ]) + elif file_server.port_opened is False and file_server.tor_manager.start_onions: + self.site.notifications.append([ + "done", + _(u""" + {_[Successfully started Tor onion hidden services.]}
+ {_[For faster connections open {0} port on your router.]} + """).format(config.fileserver_port), + 10000 + ]) + else: + self.site.notifications.append([ + "error", + _(u""" + {_[Your connection is restricted. Please, open {0} port on your router]}
+ {_[or configure Tor to become full member of ZeroNet network.]} + """).format(config.fileserver_port), + 0 + ]) + + def hasPlugin(self, name): + return name in PluginManager.plugin_manager.plugin_names # Has permission to run the command def hasCmdPermission(self, cmd): From 79005780772350db31042747749b129132f941dd Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 22 May 2017 11:34:16 +0200 Subject: [PATCH 0035/2570] Rev2082, Log websocket receive errors --- src/Config.py | 2 +- src/Ui/UiWebsocket.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Config.py b/src/Config.py index 2decbb0a..13047995 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.5" - self.rev = 2081 + self.rev = 2082 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 495bac88..66a741b4 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -65,9 +65,13 @@ class UiWebsocket(object): while True: try: - message = ws.receive() + if ws.closed: + break + else: + message = ws.receive() except Exception, err: - return "Bye." # Close connection + self.log.error("WebSocket receive error: %s" % Debug.formatException(err)) + break if message: try: From a871b5dbe7fad2145a4421db906e81db28dd4d2a Mon Sep 17 00:00:00 2001 From: Sergei Bondarenko Date: Mon, 22 May 2017 20:31:44 +0300 Subject: [PATCH 0036/2570] fix issue #936 os.path.join(*file_path.split("/")) construction drops leading slash from string thus making absolute path relative. It leads into problems in case if data_dir differs from directory where is zeronet installed. --- src/Debug/DebugMedia.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Debug/DebugMedia.py b/src/Debug/DebugMedia.py index 497d06cb..6ab40a5f 100644 --- a/src/Debug/DebugMedia.py +++ b/src/Debug/DebugMedia.py @@ -85,7 +85,7 @@ def merge(merged_path): return False # No coffeescript compiler, skip this file # Replace / with os separators and escape it - file_path_escaped = helper.shellquote(os.path.join(*file_path.split("/"))) + file_path_escaped = helper.shellquote(file_path.replace(os.path.sep, "/")) if "%s" in config.coffeescript_compiler: # Replace %s with coffeescript file command = config.coffeescript_compiler % file_path_escaped From 3f59727ab6c621db87e134d9d760c7e2e4763d4b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 23 May 2017 12:40:42 +0200 Subject: [PATCH 0037/2570] Formatting --- src/Site/Site.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 35a81d98..831eb122 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -92,7 +92,10 @@ class Site(object): for inner_path in self.bad_files: self.bad_files[inner_path] = 1 else: - self.settings = {"own": False, "serving": True, "permissions": [], "added": int(time.time()), "optional_downloaded": 0, "size_optional": 0} # Default + self.settings = { + "own": False, "serving": True, "permissions": [], + "added": int(time.time()), "optional_downloaded": 0, "size_optional": 0 + } # Default # Add admin permissions to homepage if self.address == config.homepage and "ADMIN" not in self.settings["permissions"]: @@ -342,7 +345,10 @@ class Site(object): if since is None: # No since defined, download from last modification time-1day since = self.settings.get("modified", 60 * 60 * 24) - 60 * 60 * 24 - self.log.debug("Try to get listModifications from peers: %s, connected: %s, since: %s" % (peers_try, peers_connected_num, since)) + self.log.debug( + "Try to get listModifications from peers: %s, connected: %s, since: %s" % + (peers_try, peers_connected_num, since) + ) updaters = [] for i in range(3): @@ -351,7 +357,7 @@ class Site(object): gevent.joinall(updaters, timeout=10) # Wait 10 sec to workers done query modifications 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 really connected peers + 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: @@ -900,7 +906,10 @@ class Site(object): connected += 1 # Successfully connected if connected >= need: break - self.log.debug("Connected before: %s, after: %s. Check site: %s." % (connected_before, connected, check_site_on_reconnect)) + 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) @@ -932,7 +941,11 @@ class Site(object): # Return: Recently found peers def getRecentPeers(self, need_num): - found = sorted(self.peers.values()[0:need_num*50], key=lambda peer: peer.time_found + peer.reputation * 60, reverse=True)[0:need_num*2] + found = sorted( + self.peers.values()[0:need_num * 50], + key=lambda peer: peer.time_found + peer.reputation * 60, + reverse=True + )[0:need_num * 2] random.shuffle(found) return found[0:need_num] @@ -979,7 +992,8 @@ class Site(object): need_to_close = len(connected_peers) - config.connected_limit if closed < need_to_close: - for peer in sorted(connected_peers, key=lambda peer: min(peer.connection.sites, 5)): # Try to keep connections with more sites + # Try to keep connections with more sites + for peer in sorted(connected_peers, key=lambda peer: min(peer.connection.sites, 5)): if not peer.connection: continue if peer.key in peers_protected: From aacde3361401f37f895ba2782e9d91ef464111fe Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 23 May 2017 12:41:37 +0200 Subject: [PATCH 0038/2570] File size limit default to 10MB --- src/Config.py | 3 ++- src/Site/Site.py | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 13047995..9fc54c5f 100644 --- a/src/Config.py +++ b/src/Config.py @@ -187,7 +187,8 @@ class Config(object): metavar='address') self.parser.add_argument('--updatesite', help='Source code update site', default='1UPDatEDxnvHDo7TXvq6AEBARfNkyfxsp', metavar='address') - self.parser.add_argument('--size_limit', help='Default site size limit in MB', default=10, type=int, metavar='size') + 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('--workers', help='Download workers per site', default=5, type=int, metavar='workers') diff --git a/src/Site/Site.py b/src/Site/Site.py index 831eb122..3e99b7fe 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -683,6 +683,12 @@ class Site(object): )) return False self.downloadContent(file_info["content_inner_path"]) + if file_info.get("size", 0) > config.file_size_limit * 1024 * 1024: + self.log.debug( + "File size %s too large: %sMB > %sMB, skipping..." % + (inner_path, file_info.get("size", 0) / 1024 / 1024, config.file_size_limit) + ) + return False task = self.worker_manager.addTask(inner_path, peer, priority=priority) if blocking: From ba9cd9b0caecd816ded0fa6491c268cb2fe2d5fa Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 23 May 2017 12:42:02 +0200 Subject: [PATCH 0039/2570] Fix undefined placeholder --- src/Ui/media/Wrapper.coffee | 5 ++++- src/Ui/media/all.js | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index f87ff197..eae824be 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -253,7 +253,10 @@ class Wrapper message.params = @toHtmlSafe(message.params) # Escape html if message.params[1] then type = message.params[1] else type = "text" caption = if message.params[2] then message.params[2] else "OK" - placeholder = message.params[3] + if message.params[3]? + placeholder = message.params[3] + else + placeholder = "" @displayPrompt message.params[0], type, caption, placeholder, (res) => @sendInner {"cmd": "response", "to": message.id, "result": res} # Response to confirm diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index 04acc0de..325379e5 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -1167,7 +1167,11 @@ jQuery.extend( jQuery.easing, type = "text"; } caption = message.params[2] ? message.params[2] : "OK"; - placeholder = message.params[3]; + if (message.params[3] != null) { + placeholder = message.params[3]; + } else { + placeholder = ""; + } return this.displayPrompt(message.params[0], type, caption, placeholder, (function(_this) { return function(res) { return _this.sendInner({ From 6dc084c3ab4d52bee48528dee063d4426c13bae2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 23 May 2017 12:42:09 +0200 Subject: [PATCH 0040/2570] Rev2087 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 9fc54c5f..e165da0a 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.5" - self.rev = 2082 + self.rev = 2087 self.argv = argv self.action = None self.config_file = "zeronet.conf" From ed11ae283fc28f98d870602af7b0a6aba5bde277 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 24 May 2017 10:27:02 +0200 Subject: [PATCH 0041/2570] Use https for portchecker.co --- 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 23a4fc59..bce41cbb 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -97,7 +97,7 @@ class FileServer(ConnectionServer): def testOpenportPortchecker(self, port=None): self.log.info("Checking port %s using portchecker.co..." % port) try: - data = urllib2.urlopen("http://portchecker.co/check", "port=%s" % port, timeout=20.0).read() + data = urllib2.urlopen("https://portchecker.co/check", "port=%s" % port, timeout=20.0).read() message = re.match('.*
(.*?)
', data, re.DOTALL).group(1) message = re.sub("<.*?>", "", message.replace("
", " ").replace(" ", " ").strip()) # Strip http tags except Exception, err: From 89bb12de71b0aadfb4f02aee73747dc1c38dd69a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 24 May 2017 10:28:59 +0200 Subject: [PATCH 0042/2570] Search for success message instead of error when portchecking --- src/File/FileServer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index bce41cbb..7caf4d47 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -104,7 +104,7 @@ class FileServer(ConnectionServer): message = "Error: %s" % Debug.formatException(err) data = "" - if "closed" in message or "Error" in message: + if "open" not in message: if config.tor != "always": self.log.info("[BAD :(] Port closed: %s" % message) if port == self.port: @@ -137,7 +137,7 @@ class FileServer(ConnectionServer): except Exception, err: message = "Error: %s" % Debug.formatException(err) - if "Error" in message: + if "Success" not in message: if config.tor != "always": self.log.info("[BAD :(] Port closed: %s" % message) if port == self.port: From b73742839b70dda5973ccee4364720d6c02abcc1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 24 May 2017 10:29:19 +0200 Subject: [PATCH 0043/2570] Rev2088 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index e165da0a..dddc2356 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.5" - self.rev = 2087 + self.rev = 2088 self.argv = argv self.action = None self.config_file = "zeronet.conf" From b0d574dfb044b64772fe229cfc9c8f6337498b36 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 24 May 2017 10:40:10 +0200 Subject: [PATCH 0044/2570] Rev2089, Close the transaction on failed peer db save --- plugins/PeerDb/PeerDbPlugin.py | 16 ++++++++++------ src/Config.py | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/plugins/PeerDb/PeerDbPlugin.py b/plugins/PeerDb/PeerDbPlugin.py index 858b22ea..72e585ee 100644 --- a/plugins/PeerDb/PeerDbPlugin.py +++ b/plugins/PeerDb/PeerDbPlugin.py @@ -73,12 +73,16 @@ class ContentDbPlugin(object): site_id = self.site_ids.get(site.address) cur = self.getCursor() cur.execute("BEGIN") - self.execute("DELETE FROM peer WHERE site_id = :site_id", {"site_id": site_id}) - self.cur.cursor.executemany( - "INSERT INTO peer (site_id, address, port, hashfield, time_added) VALUES (?, ?, ?, ?, ?)", - self.iteratePeers(site) - ) - cur.execute("END") + try: + self.execute("DELETE FROM peer WHERE site_id = :site_id", {"site_id": site_id}) + self.cur.cursor.executemany( + "INSERT INTO peer (site_id, address, port, hashfield, time_added) VALUES (?, ?, ?, ?, ?)", + self.iteratePeers(site) + ) + except Exception, err: + site.log.error("Save peer error: %s" % err) + finally: + cur.execute("END") site.log.debug("Peers saved in %.3fs" % (time.time() - s)) def initSite(self, site): diff --git a/src/Config.py b/src/Config.py index dddc2356..84e7b803 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.5" - self.rev = 2088 + self.rev = 2089 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 830f98573eb4047ffe89d61e28d6fddd0e91ba57 Mon Sep 17 00:00:00 2001 From: Sergei Bondarenko Date: Wed, 24 May 2017 19:28:02 +0300 Subject: [PATCH 0045/2570] fix incorrect variable order in file_path.replace --- src/Debug/DebugMedia.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Debug/DebugMedia.py b/src/Debug/DebugMedia.py index 6ab40a5f..7520e0fb 100644 --- a/src/Debug/DebugMedia.py +++ b/src/Debug/DebugMedia.py @@ -85,7 +85,7 @@ def merge(merged_path): return False # No coffeescript compiler, skip this file # Replace / with os separators and escape it - file_path_escaped = helper.shellquote(file_path.replace(os.path.sep, "/")) + file_path_escaped = helper.shellquote(file_path.replace("/", os.path.sep)) if "%s" in config.coffeescript_compiler: # Replace %s with coffeescript file command = config.coffeescript_compiler % file_path_escaped From 0c0ec30560ddbfcd2bcc8a75ca0ca335b7d7be72 Mon Sep 17 00:00:00 2001 From: leycec Date: Thu, 25 May 2017 19:56:21 -0400 Subject: [PATCH 0046/2570] Arch and Gentoo Linux installation instructions --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index cba22d7b..05ebd010 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,29 @@ It downloads the latest version of ZeroNet then starts it automatically. * Start with `python zeronet.py` * Open http://127.0.0.1:43110/ in your browser +### [Arch Linux](https://www.archlinux.org) + +* `git clone https://aur.archlinux.org/zeronet.git` +* `cd zeronet-git` +* `makepkg -srci` +* `systemctl start zeronet` +* Open http://127.0.0.1:43110/ in your browser + +See [ArchWiki](https://wiki.archlinux.org)'s [ZeroNet +article](https://wiki.archlinux.org/index.php/ZeroNet) for further assistance. + +### [Gentoo Linux](https://www.gentoo.org) + +* [`layman -a raiagent`](https://github.com/leycec/raiagent) +* `echo '>=net-vpn/zeronet-0.5.4' >> /etc/portage/package.accept_keywords` +* *(Optional)* Enable Tor support: `echo 'net-vpn/zeronet tor' >> + /etc/portage/package.use` +* `emerge zeronet` +* `rc-service zeronet start` +* Open http://127.0.0.1:43110/ in your browser + +See `/usr/share/doc/zeronet-*/README.gentoo.bz2` for further assistance. + ### [FreeBSD](https://www.freebsd.org/) * `pkg install zeronet` or `cd /usr/ports/security/zeronet/ && make install clean` From e291555e604f9db1a273a39db40642aca5a3e45a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 26 May 2017 12:14:43 +0200 Subject: [PATCH 0047/2570] Rev2090, Fix ssl patch library finding, Changelog for 0.5.5 --- CHANGELOG.md | 19 +++++++++++++++++++ src/Config.py | 2 +- src/util/SslPatch.py | 11 ++++++----- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6955f642..5a7d04ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +## ZeroNet 0.5.5 (2017-05-18) +### Added +- Outgoing socket binding by --bind parameter +- Database rebuilding progress bar +- Protect low traffic site's peers from cleanup closing +- Local site blacklisting +- Cloned site source code upgrade from parent +- Input placeholder support for displayPrompt +- Alternative interaction for wrapperConfirm + +### Changed +- New file priorities for faster site display on first visit +- Don't add ? to url if push/replaceState url starts with # + +### Fixed +- PermissionAdd/Remove admin command requirement +- Multi-line confirmation dialog + + ## ZeroNet 0.5.4 (2017-04-14) ### Added - Major speed and CPU usage enhancements in Tor always mode diff --git a/src/Config.py b/src/Config.py index 84e7b803..90181759 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.5" - self.rev = 2089 + self.rev = 2090 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/util/SslPatch.py b/src/util/SslPatch.py index b5d3fc55..29dba5ed 100644 --- a/src/util/SslPatch.py +++ b/src/util/SslPatch.py @@ -3,6 +3,7 @@ import logging import os +import sys from Config import config @@ -17,12 +18,12 @@ def openLibrary(): dll_path = "/bin/cygcrypto-1.0.0.dll" else: dll_path = "/usr/local/ssl/lib/libcrypto.so" - ssl = ctypes.CDLL(dll_path, ctypes.RTLD_GLOBAL) - assert ssl + ssl_lib = ctypes.CDLL(dll_path, ctypes.RTLD_GLOBAL) + assert ssl_lib except: - dll_path = ctypes.util.find_library('ssl') or ctypes.util.find_library('crypto') or ctypes.util.find_library('libcrypto') - ssl = ctypes.CDLL(dll_path or 'libeay32', ctypes.RTLD_GLOBAL) - return ssl + dll_path = ctypes.util.find_library('ssl.so.1.0') or ctypes.util.find_library('ssl') or ctypes.util.find_library('crypto') or ctypes.util.find_library('libcrypto') + ssl_lib = ctypes.CDLL(dll_path or 'libeay32', ctypes.RTLD_GLOBAL) + return ssl_lib def disableSSLCompression(): From de363b57ce1f30964e202a08ca68a0252f97a140 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 26 May 2017 12:22:35 +0200 Subject: [PATCH 0048/2570] Rev2091, Port opening error should not prevent ZeroNet startup --- src/Config.py | 2 +- src/File/FileServer.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Config.py b/src/Config.py index 90181759..83398056 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.5" - self.rev = 2090 + self.rev = 2091 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/File/FileServer.py b/src/File/FileServer.py index 7caf4d47..835c3c48 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -72,9 +72,8 @@ class FileServer(ConnectionServer): self.log.info("Trying to open port using UpnpPunch...") try: UpnpPunch.ask_to_open_port(self.port, 'ZeroNet', retries=3, protos=["TCP"]) - except (UpnpPunch.UpnpError, UpnpPunch.IGDError, socket.error) as err: - self.log.error("UpnpPunch run error: %s" % - Debug.formatException(err)) + except Exception as err: + self.log.error("UpnpPunch run error: %s" % Debug.formatException(err)) return False if self.testOpenport(port)["result"] is True: From d6721f0656d17fcbce6d739dc937f123f35ff75b Mon Sep 17 00:00:00 2001 From: Artem Molotov Date: Sun, 28 May 2017 18:22:35 +0300 Subject: [PATCH 0049/2570] Small fix russian translation --- src/Translate/languages/ru.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Translate/languages/ru.json b/src/Translate/languages/ru.json index 48e29e77..5bb1c4e4 100644 --- a/src/Translate/languages/ru.json +++ b/src/Translate/languages/ru.json @@ -1,6 +1,6 @@ { - "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "Поздравляем, ваш пор {0} открыт.
Вы полноценный участник сети ZeroNet!", - "Tor mode active, every connection using Onion route.": "Режима Tor включен, все соединения осуществляются через Tor.", + "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "Поздравляем, ваш порт {0} открыт.
Вы полноценный участник сети ZeroNet!", + "Tor mode active, every connection using Onion route.": "Режим Tor включен, все соединения осуществляются через Tor.", "Successfully started Tor onion hidden services.": "Скрытый сервис Tor запущено успешно.", "Unable to start hidden services, please check your config.": "Ошибка при запуске скрытого сервиса, пожалуйста проверьте настройки", "For faster connections open {0} port on your router.": "Для более быстрой работы сети откройте {0} порт на вашем роутере.", From f0f32cf25cdf338e9ddb7c88a572eee41564023d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 30 May 2017 01:04:45 +0200 Subject: [PATCH 0050/2570] Rev2092, Prefer recent peers when starting workers --- src/Config.py | 2 +- src/Worker/WorkerManager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index 83398056..e2fbd928 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.5" - self.rev = 2091 + self.rev = 2092 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 39407b40..c3b48d2e 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -160,7 +160,7 @@ class WorkerManager(object): if not peers: peers = self.site.getConnectedPeers() if len(peers) < self.getMaxWorkers(): - peers += self.site.peers.values()[0:self.getMaxWorkers()] + peers += self.site.getRecentPeers(self.getMaxWorkers()) if type(peers) is set: peers = list(peers) From 0b7d1ad90d327247d4fcf763ebaee72af43c55fd Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 3 Jun 2017 00:53:40 +0200 Subject: [PATCH 0051/2570] Add callback for certSelect --- src/Ui/UiWebsocket.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 66a741b4..411251b9 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -695,11 +695,13 @@ class UiWebsocket(object): - """ + """ % to # Send the notification self.cmd("notification", ["ask", body]) @@ -723,6 +725,7 @@ class UiWebsocket(object): def actionCertSet(self, to, domain): self.user.setCert(self.site.address, domain) self.site.updateWebsocket(cert_changed=domain) + self.response(to, "ok") # List all site info def actionSiteList(self, to): From 6167dda01d0a96c1faa10b349d4d02b0ee37e80d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 3 Jun 2017 00:53:59 +0200 Subject: [PATCH 0052/2570] Compact json list output --- src/Site/SiteStorage.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index bb0578df..103f184e 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -266,6 +266,15 @@ class SiteStorage(object): content = re.sub("\{(\n[^,\[\{]{10,100}?)\}[, ]{0,2}\n", compact_dict, content, flags=re.DOTALL) + def compact_list(match): + if "\n" in match.group(0): + stripped_lines = re.sub("\n[ ]*", "", match.group(1)) + return match.group(0).replace(match.group(1), stripped_lines) + else: + return match.group(0) + + content = re.sub("\[([^\[\{]{2,300}?)\][, ]{0,2}\n", compact_list, content, flags=re.DOTALL) + # Remove end of line whitespace content = re.sub("(?m)[ ]+$", "", content) From 653219d1de0345035a7ccdb5ed236487c50809a0 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 3 Jun 2017 00:54:26 +0200 Subject: [PATCH 0053/2570] Rev2096 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index e2fbd928..8322150c 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.5" - self.rev = 2092 + self.rev = 2096 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 4c11c96164f02391edc9501e955e7d3964ff453d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 5 Jun 2017 01:07:39 +0200 Subject: [PATCH 0054/2570] Fix socket proxy skip on updating --- update.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/update.py b/update.py index bfdeb50c..9aea5a65 100644 --- a/update.py +++ b/update.py @@ -9,10 +9,6 @@ import re import json import cStringIO as StringIO -from gevent import monkey -monkey.patch_all() - - def download(): from src.util import helper @@ -127,9 +123,12 @@ def update(): if __name__ == "__main__": - # Fix broken gevent SSL import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src")) # Imports relative to src + + from gevent import monkey + monkey.patch_all() + from Config import config config.parse() from src.util import SslPatch From eae2d59da7eeb7177cf4949fd754017bb984caa2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 5 Jun 2017 01:08:11 +0200 Subject: [PATCH 0055/2570] Fix zeronet.conf parsing on standalone update.py running --- update.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/update.py b/update.py index 9aea5a65..d3632857 100644 --- a/update.py +++ b/update.py @@ -130,7 +130,8 @@ if __name__ == "__main__": monkey.patch_all() from Config import config - config.parse() + config.parse(silent=True) + from src.util import SslPatch try: From 7fc2681252f9b76b7132ece9b85a6c2cecff04d1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 5 Jun 2017 01:08:27 +0200 Subject: [PATCH 0056/2570] Display update errors instead of crashing --- zeronet.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/zeronet.py b/zeronet.py index 8fd07968..f8a5c4e7 100755 --- a/zeronet.py +++ b/zeronet.py @@ -35,7 +35,10 @@ def main(): sys.modules["main"].lock.close() # Update - update.update() + try: + update.update() + except Exception, err: + print "Update error: %s" % err # Close log files logger = sys.modules["main"].logging.getLogger() From 5801863b764db9ef827c141b81c87bb246768443 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 5 Jun 2017 01:09:17 +0200 Subject: [PATCH 0057/2570] Rev2098 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 8322150c..e9e0c829 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.5" - self.rev = 2096 + self.rev = 2098 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 742c2fe6845366a37f4f29132c9bd64f92841da8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 6 Jun 2017 00:05:43 +0200 Subject: [PATCH 0058/2570] Rev2099, Keep tor client running until update finished --- src/Config.py | 2 +- src/Ui/UiWebsocket.py | 2 -- zeronet.py | 3 +++ 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Config.py b/src/Config.py index e9e0c829..3a437d6e 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.5" - self.rev = 2098 + self.rev = 2099 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 411251b9..0a96cdc3 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -825,8 +825,6 @@ class UiWebsocket(object): def actionServerUpdate(self, to): self.cmd("updating") sys.modules["main"].update_after_shutdown = True - if sys.modules["main"].file_server.tor_manager.tor_process: - sys.modules["main"].file_server.tor_manager.stopTor() SiteManager.site_manager.save() sys.modules["main"].file_server.stop() sys.modules["main"].ui_server.stop() diff --git a/zeronet.py b/zeronet.py index f8a5c4e7..5b68b426 100755 --- a/zeronet.py +++ b/zeronet.py @@ -19,6 +19,7 @@ def main(): if main.update_after_shutdown: # Updater import gc import update + import atexit # Try cleanup openssl try: if "lib.opensslVerify" in sys.modules: @@ -48,6 +49,8 @@ def main(): handler.close() logger.removeHandler(handler) + atexit._run_exitfuncs() + except Exception, err: # Prevent closing import traceback try: From 868e956c3fa30d1d57d0091e7dca22fcc965cd89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=BBipumu?= Date: Mon, 12 Jun 2017 15:33:47 -0400 Subject: [PATCH 0059/2570] Add Spanish Translation --- plugins/MergerSite/languages/es.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 plugins/MergerSite/languages/es.json diff --git a/plugins/MergerSite/languages/es.json b/plugins/MergerSite/languages/es.json new file mode 100644 index 00000000..d554c3a9 --- /dev/null +++ b/plugins/MergerSite/languages/es.json @@ -0,0 +1,5 @@ +{ + "Add %s new site?": "¿Agregar %s nuevo sitio?", + "Added %s new site": "Sitio %s agregado", + "Site deleted: %s": "Sitio removido: %s" +} From fa3a981c196c5f1720e0efd3eb18672d8599a351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=BBipumu?= Date: Mon, 12 Jun 2017 15:40:21 -0400 Subject: [PATCH 0060/2570] Add Spanish Translation First revision, orthography verified. --- plugins/Mute/languages/es.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 plugins/Mute/languages/es.json diff --git a/plugins/Mute/languages/es.json b/plugins/Mute/languages/es.json new file mode 100644 index 00000000..732ccfd8 --- /dev/null +++ b/plugins/Mute/languages/es.json @@ -0,0 +1,6 @@ +{ + "Hide all content from %s?": "Esconder todo el contenido de %s?", + "Mute": "Silenciar", + "Unmute %s?": "Mostrar todo el contenido de %s?", + "Unmute": "Reactivar" +} From 0f6578326624b4bbd3717351841db48feef4c64d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=BBipumu?= Date: Mon, 12 Jun 2017 15:44:37 -0400 Subject: [PATCH 0061/2570] Add Spanish Translation First revision, orthography checked. --- plugins/OptionalManager/languages/es.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 plugins/OptionalManager/languages/es.json diff --git a/plugins/OptionalManager/languages/es.json b/plugins/OptionalManager/languages/es.json new file mode 100644 index 00000000..32ae46ae --- /dev/null +++ b/plugins/OptionalManager/languages/es.json @@ -0,0 +1,7 @@ +{ + "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!" +} From 47d585768b2a7cd04b37886f9585cad0495b4eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=BBipumu?= Date: Mon, 12 Jun 2017 15:49:45 -0400 Subject: [PATCH 0062/2570] Add Spanish Translation First revision, orthography checked. --- plugins/Trayicon/languages/es.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 plugins/Trayicon/languages/es.json diff --git a/plugins/Trayicon/languages/es.json b/plugins/Trayicon/languages/es.json new file mode 100644 index 00000000..4cdc5d1f --- /dev/null +++ b/plugins/Trayicon/languages/es.json @@ -0,0 +1,14 @@ +{ + "ZeroNet Twitter": "ZeroNet Twitter", + "ZeroNet Reddit": "ZeroNet Reddit", + "ZeroNet Github": "ZeroNet Github", + "Report bug/request feature": "Reportar fallo/sugerir característica", + "!Open ZeroNet": "!Abrir ZeroNet", + "Quit": "Sair", + "(active)": "(activo)", + "(passive)": "(pasivo)", + "Connections: %s": "Conecciones: %s", + "Received: %.2f MB | Sent: %.2f MB": "Recibido: %.2f MB | Enviado: %.2f MB", + "Show console window": "Mostrar consola", + "Start ZeroNet when Windows starts": "Iniciar Zeronet cuando inicie Windows" +} From 67395b5e479240e7a178e7237eaa6af5c6afa454 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Tue, 13 Jun 2017 11:36:38 +0300 Subject: [PATCH 0063/2570] Allow fileRules for root content.json --- src/Content/ContentManager.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index f97eedd5..bb291b27 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -352,6 +352,12 @@ class ContentManager(object): if not file_info: return False # File not found inner_path = file_info["content_inner_path"] + + if inner_path == "content.json": # Root content.json + rules = {} + rules["signers"] = self.getValidSigners(inner_path, content) + return rules + dirs = inner_path.split("/") # Parent dirs of content.json inner_path_parts = [dirs.pop()] # Filename relative to content.json inner_path_parts.insert(0, dirs.pop()) # Dont check in self dir From a4a23f3ea0145219eb69063de792a0c037fca4a2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 13 Jun 2017 14:12:13 +0200 Subject: [PATCH 0064/2570] Fix site lock violation logging --- 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 1041a8ad..e01fc58d 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -60,7 +60,7 @@ class FileRequest(object): if params["site"] not in valid_sites: self.response({"error": "Invalid site"}) self.connection.log( - "%s site lock violation: %s not in %s, target onion: %s" % + "Site lock violation: %s not in %s, target onion: %s" % (params["site"], valid_sites, self.connection.target_onion) ) self.connection.badAction(5) From ae21f056d5bd8d878189259aa8c5843cf0bf9abb Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 13 Jun 2017 14:12:48 +0200 Subject: [PATCH 0065/2570] Error if no peer connection found --- src/Peer/Peer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index fdafcb50..17e6431e 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -127,6 +127,8 @@ class Peer(object): for retry in range(1, 4): # Retry 3 times try: + if not self.connection: + raise Exception("No connection found") res = self.connection.request(cmd, params, stream_to) if not res: raise Exception("Send error") From aa402ebaf35272679462821f464404aa83a7ba9e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 13 Jun 2017 14:13:09 +0200 Subject: [PATCH 0066/2570] Fix typo --- src/Test/TestNoparallel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test/TestNoparallel.py b/src/Test/TestNoparallel.py index 27528dae..596c6b23 100644 --- a/src/Test/TestNoparallel.py +++ b/src/Test/TestNoparallel.py @@ -77,7 +77,7 @@ class TestNoparallel: assert obj1.counted == 15 # Calls should be executed sequentially - def testIngoreClass(self): + def testIgnoreClass(self): obj1 = ExampleClass() obj2 = ExampleClass() From d55fbd1728c4c93f1161f7e82afe6aed5d6ab75c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 13 Jun 2017 14:18:17 +0200 Subject: [PATCH 0067/2570] Remove obsolete auth_key_sha512 --- src/Ui/UiWebsocket.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 0a96cdc3..13d96f33 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -273,7 +273,6 @@ class UiWebsocket(object): ret = { "auth_key": self.site.settings["auth_key"], # Obsolete, will be removed - "auth_key_sha512": hashlib.sha512(self.site.settings["auth_key"]).hexdigest()[0:64], # Obsolete, will be removed "auth_address": self.user.getAuthAddress(site.address, create=create_user), "cert_user_id": self.user.getCertUserId(site.address), "address": site.address, From 42874038e2986417cf1032c0c19300d888131db7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 13 Jun 2017 14:19:23 +0200 Subject: [PATCH 0068/2570] Only allow UI requests from safe hosts --- src/Ui/UiRequest.py | 26 +++++++++++++++++++++++++- src/Ui/UiServer.py | 2 ++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 61a09e56..fb5e0502 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -40,11 +40,35 @@ class UiRequest(object): self.start_response = start_response # Start response function self.user = None + def isHostAllowed(self, host): + if host in self.server.allowed_hosts: + return True + + if self.isProxyRequest(): # Support for chrome extension proxy + if self.server.site_manager.isDomain(host): + return True + else: + return False + + if config.ui_ip != "127.0.0.1" and self.server.learn_allowed_host: + # Learn the first request's host as allowed one + self.server.learn_allowed_host = False + self.server.allowed_hosts.add(host) + self.server.log.info("Added %s as allowed host" % host) + return True + + return False + # Call the request handler function base on path def route(self, path): - if config.ui_restrict and self.env['REMOTE_ADDR'] not in config.ui_restrict: # Restict Ui access by ip + # Restict Ui access by ip + if config.ui_restrict and self.env['REMOTE_ADDR'] not in config.ui_restrict: return self.error403(details=False) + # Check if host allowed to do request + if not self.isHostAllowed(self.env.get("HTTP_HOST")): + return self.error403("Invalid host", details=False) + path = re.sub("^http://zero[/]+", "/", path) # Remove begining http://zero/ for chrome extension path = re.sub("^http://", "/", path) # Remove begining http for chrome extension .bit access diff --git a/src/Ui/UiServer.py b/src/Ui/UiServer.py index 632936fc..5dab2bf7 100644 --- a/src/Ui/UiServer.py +++ b/src/Ui/UiServer.py @@ -58,6 +58,8 @@ class UiServer: self.port = config.ui_port if self.ip == "*": self.ip = "" # Bind all + self.allowed_hosts = set(["zero", "localhost:%s" % config.ui_port, "%s:%s" % (config.ui_ip, config.ui_port)]) + self.learn_allowed_host = True self.wrapper_nonces = [] self.site_manager = SiteManager.site_manager self.sites = SiteManager.site_manager.list() From bc02de4b182734d51c505beb8fe7d7b05269eae2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 13 Jun 2017 14:19:43 +0200 Subject: [PATCH 0069/2570] ZeroName only match .bit domains --- plugins/Zeroname/SiteManagerPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Zeroname/SiteManagerPlugin.py b/plugins/Zeroname/SiteManagerPlugin.py index 2d1979d8..0ddcbd4f 100644 --- a/plugins/Zeroname/SiteManagerPlugin.py +++ b/plugins/Zeroname/SiteManagerPlugin.py @@ -30,7 +30,7 @@ class SiteManagerPlugin(object): # Return: True if the address is domain def isDomain(self, address): - return re.match("(.*?)([A-Za-z0-9_-]+\.[A-Za-z0-9]+)$", address) + return re.match("(.*?)([A-Za-z0-9_-]+\.bit)$", address) # Resolve domain # Return: The address or None From eefe7e1b5a4a2e2a44451e9c21629c9d84ddeb58 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 13 Jun 2017 14:20:58 +0200 Subject: [PATCH 0070/2570] Don't add obsolete old signature format --- src/Content/ContentManager.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index bb291b27..cf241f35 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -611,10 +611,6 @@ class ContentManager(object): new_content["signs"] = {} new_content["signs"][privatekey_address] = sign - if inner_path == "content.json": # To root content.json add old format sign for backward compatibility - oldsign_content = json.dumps(new_content, sort_keys=True) - new_content["sign"] = CryptBitcoin.signOld(oldsign_content, privatekey) - if not self.verifyContent(inner_path, new_content): self.log.error("Sign failed: Invalid content") return False From 1f1cbf01d772f8b30badf3a33fcec88078da71de Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 13 Jun 2017 14:21:07 +0200 Subject: [PATCH 0071/2570] Rev2103 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 3a437d6e..f45cea41 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.5" - self.rev = 2099 + self.rev = 2103 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 1f83b6691b7864a35c1466ee23a14628f28ec974 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 13 Jun 2017 14:24:10 +0200 Subject: [PATCH 0072/2570] Fix CoffeeScript 1.12 support --- tools/coffee/coffee.wsf | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tools/coffee/coffee.wsf b/tools/coffee/coffee.wsf index c8909f7b..25d590c9 100644 --- a/tools/coffee/coffee.wsf +++ b/tools/coffee/coffee.wsf @@ -42,7 +42,7 @@ function convert(input, output) { } var coffee; - if (!input) { + if (!input) { // Read all input data from STDIN var chunks = []; while (!WScript.StdIn.AtEndOfStream) @@ -62,8 +62,7 @@ function convert(input, output) { return new f; } - var js = CoffeeScript.compile(coffee); - + var js = CoffeeScript.compile(coffee, {filename: "temp.coffee"}); if (!output) { WScript.StdOut.Write(js); } From 9ac5746e3c59c5a8eeaaaf030b48ff11a0986a1b Mon Sep 17 00:00:00 2001 From: MRoci Date: Thu, 23 Mar 2017 23:24:30 +0100 Subject: [PATCH 0073/2570] Changed this assignment to have the "signers" field in root's content.json as a list and not a dictionary to mantain uniformity with "includes" "signers" field and for not having a dict with empty values --- src/Content/ContentManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index be221765..912b4bab 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -631,7 +631,7 @@ class ContentManager(object): valid_signers = [] if inner_path == "content.json": # Root content.json if "content.json" in self.contents and "signers" in self.contents["content.json"]: - valid_signers += self.contents["content.json"]["signers"].keys() + valid_signers += self.contents["content.json"]["signers"][:] else: rules = self.getRules(inner_path, content) if rules and "signers" in rules: From 97b3563e6b4e88eba6ac338a52981806297823fc Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 15 Jun 2017 13:30:06 +0200 Subject: [PATCH 0074/2570] Rev2105, Fix database updating when path contains special characters --- src/Config.py | 2 +- src/Db/Db.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index f45cea41..e31b23bb 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.5" - self.rev = 2103 + self.rev = 2105 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Db/Db.py b/src/Db/Db.py index de711a21..026a36c3 100644 --- a/src/Db/Db.py +++ b/src/Db/Db.py @@ -225,7 +225,6 @@ class Db(object): def updateJson(self, file_path, file=None, cur=None): if not file_path.startswith(self.db_dir): return False # Not from the db dir: Skipping - relative_path = re.sub("^%s" % self.db_dir, "", file_path) # File path realative to db file # Check if filename matches any of mappings in schema matched_maps = [] for match, map_settings in self.schema["maps"].items(): From f506ac970190cbb6e6b53d294d618d44e55e2fed Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 15 Jun 2017 13:30:36 +0200 Subject: [PATCH 0075/2570] Faster getPath --- src/Site/SiteStorage.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index 103f184e..1101d56a 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -317,7 +317,10 @@ class SiteStorage(object): if path == self.directory: inner_path = "" else: - inner_path = re.sub("^%s/" % re.escape(self.directory), "", path) + if path.startswith(self.directory): + inner_path = path[len(self.directory)+1:] + else: + raise Exception(u"File not allowed: %s" % path) return inner_path # Verify all files sha512sum using content.json From 072546438865fc563fe017fc31f22dd6211c7403 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 15 Jun 2017 13:33:51 +0200 Subject: [PATCH 0076/2570] Fix database loading --- src/Db/Db.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Db/Db.py b/src/Db/Db.py index 026a36c3..df73bde7 100644 --- a/src/Db/Db.py +++ b/src/Db/Db.py @@ -225,6 +225,8 @@ class Db(object): def updateJson(self, file_path, file=None, cur=None): if not file_path.startswith(self.db_dir): return False # Not from the db dir: Skipping + relative_path = file_path[len(self.db_dir):] # File path realative to db file + # Check if filename matches any of mappings in schema matched_maps = [] for match, map_settings in self.schema["maps"].items(): From 2e04ba1880c08671bfc949f6f464e3fb608fefb9 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 15 Jun 2017 19:42:38 +0200 Subject: [PATCH 0077/2570] Bind ui server to ipv4 address --- src/Ui/UiServer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ui/UiServer.py b/src/Ui/UiServer.py index 5dab2bf7..6e6f9784 100644 --- a/src/Ui/UiServer.py +++ b/src/Ui/UiServer.py @@ -57,7 +57,7 @@ class UiServer: self.ip = config.ui_ip self.port = config.ui_port if self.ip == "*": - self.ip = "" # Bind all + self.ip = "0.0.0.0" # Bind all self.allowed_hosts = set(["zero", "localhost:%s" % config.ui_port, "%s:%s" % (config.ui_ip, config.ui_port)]) self.learn_allowed_host = True self.wrapper_nonces = [] From 0d3fa43f0055f9e831e4d080bb57cc24487f5f71 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 15 Jun 2017 19:43:23 +0200 Subject: [PATCH 0078/2570] Remove unnecessary ui ip replace --- src/Ui/UiServer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ui/UiServer.py b/src/Ui/UiServer.py index 6e6f9784..d9170cb1 100644 --- a/src/Ui/UiServer.py +++ b/src/Ui/UiServer.py @@ -128,7 +128,7 @@ class UiServer: url = "http://%s:%s/%s" % (config.ui_ip if config.ui_ip != "*" else "127.0.0.1", config.ui_port, config.homepage) gevent.spawn_later(0.3, browser.open, url, new=2) - self.server = WSGIServer((self.ip.replace("*", ""), self.port), handler, handler_class=UiWSGIHandler, log=self.log) + self.server = WSGIServer((self.ip, self.port), handler, handler_class=UiWSGIHandler, log=self.log) self.server.sockets = {} self.afterStarted() try: From c84fcf20343dbf1462ad666a430670161e3ab83b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 15 Jun 2017 19:48:01 +0200 Subject: [PATCH 0079/2570] Support manual ui_host command line parameter --- src/Config.py | 2 ++ src/Ui/UiRequest.py | 2 +- src/Ui/UiServer.py | 12 ++++++++++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Config.py b/src/Config.py index e31b23bb..434ce059 100644 --- a/src/Config.py +++ b/src/Config.py @@ -181,6 +181,8 @@ class Config(object): self.parser.add_argument('--ui_ip', help='Web interface bind address', default="127.0.0.1", metavar='ip') self.parser.add_argument('--ui_port', help='Web interface bind port', default=43110, type=int, metavar='port') self.parser.add_argument('--ui_restrict', help='Restrict web access', default=False, metavar='ip', nargs='*') + self.parser.add_argument('--ui_host', help='Allow access using this hosts', metavar='host', nargs='*') + 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', diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index fb5e0502..689a8bc2 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -50,7 +50,7 @@ class UiRequest(object): else: return False - if config.ui_ip != "127.0.0.1" and self.server.learn_allowed_host: + if self.server.learn_allowed_host: # Learn the first request's host as allowed one self.server.learn_allowed_host = False self.server.allowed_hosts.add(host) diff --git a/src/Ui/UiServer.py b/src/Ui/UiServer.py index d9170cb1..d623c2c3 100644 --- a/src/Ui/UiServer.py +++ b/src/Ui/UiServer.py @@ -58,8 +58,16 @@ class UiServer: self.port = config.ui_port if self.ip == "*": self.ip = "0.0.0.0" # Bind all - self.allowed_hosts = set(["zero", "localhost:%s" % config.ui_port, "%s:%s" % (config.ui_ip, config.ui_port)]) - self.learn_allowed_host = True + if config.ui_host: + self.allowed_hosts = set(config.ui_host) + self.learn_allowed_host = False + elif config.ui_ip == "127.0.0.1": + self.allowed_hosts = set(["zero", "localhost:%s" % config.ui_port, "127.0.0.1:%s" % config.ui_port]) + self.learn_allowed_host = False + else: + self.allowed_hosts = set([]) + self.learn_allowed_host = True # It will pin to the first http request's host + self.wrapper_nonces = [] self.site_manager = SiteManager.site_manager self.sites = SiteManager.site_manager.list() From 2f7bfa95b2359d39011f6be231fd68b5e4c35dee Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 15 Jun 2017 19:49:00 +0200 Subject: [PATCH 0080/2570] Fix multi value argument command line parsing --- src/Config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Config.py b/src/Config.py index 434ce059..883d772b 100644 --- a/src/Config.py +++ b/src/Config.py @@ -234,6 +234,7 @@ class Config(object): self.parser.add_argument('--tor_hs_limit', help='Maximum number of hidden services', metavar='limit', type=int, default=10) self.parser.add_argument('--version', action='version', version='ZeroNet %s r%s' % (self.version, self.rev)) + self.parser.add_argument('--end', help='Stop multi value argument parsing', action='store_true') return self.parser @@ -319,6 +320,7 @@ class Config(object): # Find out if action is specificed on start action = self.getAction(argv) if not action: + argv.append("--end") argv.append("main") action = "main" argv = self.moveUnknownToEnd(argv, action) From e9ce137e145324d9413c149ac4ae14658845e32b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 15 Jun 2017 19:49:51 +0200 Subject: [PATCH 0081/2570] Display invalid hosts --- src/Ui/UiRequest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 689a8bc2..ffa0b6de 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -67,7 +67,7 @@ class UiRequest(object): # Check if host allowed to do request if not self.isHostAllowed(self.env.get("HTTP_HOST")): - return self.error403("Invalid host", details=False) + return self.error403("Invalid host: %s" % self.env.get("HTTP_HOST"), details=False) path = re.sub("^http://zero[/]+", "/", path) # Remove begining http://zero/ for chrome extension path = re.sub("^http://", "/", path) # Remove begining http for chrome extension .bit access @@ -622,7 +622,7 @@ class UiRequest(object): return """

%s

%s

- """ % (title, message) + """ % (title, cgi.escape(message)) # - Reload for eaiser developing - From a89f66e8b5df8910f62bf49111d05052526cb791 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 15 Jun 2017 19:50:11 +0200 Subject: [PATCH 0082/2570] Rev2109 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 883d772b..df4956f2 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.5" - self.rev = 2105 + self.rev = 2109 self.argv = argv self.action = None self.config_file = "zeronet.conf" From f4a040064cdf0842e1dcb16b81976b7b6290394b Mon Sep 17 00:00:00 2001 From: Ivanq Date: Tue, 13 Jun 2017 11:47:15 +0300 Subject: [PATCH 0083/2570] Don't ask user for privatekey if file can be signed with auth_privatekey --- plugins/Sidebar/media/Sidebar.coffee | 21 +++++++------- plugins/Sidebar/media/all.js | 42 ++++++++++++++++------------ 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee index 954602de..acd78e91 100644 --- a/plugins/Sidebar/media/Sidebar.coffee +++ b/plugins/Sidebar/media/Sidebar.coffee @@ -354,17 +354,18 @@ class Sidebar extends Class @tag.find("#button-sign").off("click touchend").on "click touchend", => inner_path = @tag.find("#input-contents").val() - if wrapper.site_info.privatekey - # Privatekey stored in users.json - wrapper.ws.cmd "siteSign", {privatekey: "stored", inner_path: inner_path, update_changed_files: true}, (res) => - wrapper.notifications.add "sign", "done", "#{inner_path} Signed!", 5000 + wrapper.ws.cmd "fileRules", {inner_path: inner_path}, (res) => + if wrapper.site_info.privatekey or wrapper.site_info.auth_address in res.signers + # Privatekey stored in users.json + wrapper.ws.cmd "siteSign", {privatekey: "stored", inner_path: inner_path, update_changed_files: true}, (res) => + wrapper.notifications.add "sign", "done", "#{inner_path} Signed!", 5000 - else - # Ask the user for privatekey - wrapper.displayPrompt "Enter your private key:", "password", "Sign", "", (privatekey) => # Prompt the private key - wrapper.ws.cmd "siteSign", {privatekey: privatekey, inner_path: inner_path, update_changed_files: true}, (res) => - if res == "ok" - wrapper.notifications.add "sign", "done", "#{inner_path} Signed!", 5000 + else + # Ask the user for privatekey + wrapper.displayPrompt "Enter your private key:", "password", "Sign", "", (privatekey) => # Prompt the private key + wrapper.ws.cmd "siteSign", {privatekey: privatekey, inner_path: inner_path, update_changed_files: true}, (res) => + if res == "ok" + wrapper.notifications.add "sign", "done", "#{inner_path} Signed!", 5000 return false diff --git a/plugins/Sidebar/media/all.js b/plugins/Sidebar/media/all.js index 793d4408..22c1c4ff 100644 --- a/plugins/Sidebar/media/all.js +++ b/plugins/Sidebar/media/all.js @@ -189,7 +189,8 @@ window.initScrollable = function () { var Sidebar, 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; + hasProp = {}.hasOwnProperty, + 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; }; Sidebar = (function(superClass) { extend(Sidebar, superClass); @@ -600,27 +601,32 @@ window.initScrollable = function () { return function() { var inner_path; inner_path = _this.tag.find("#input-contents").val(); - if (wrapper.site_info.privatekey) { - wrapper.ws.cmd("siteSign", { - privatekey: "stored", - inner_path: inner_path, - update_changed_files: true - }, function(res) { - return wrapper.notifications.add("sign", "done", inner_path + " Signed!", 5000); - }); - } else { - wrapper.displayPrompt("Enter your private key:", "password", "Sign", "", function(privatekey) { + wrapper.ws.cmd("fileRules", { + inner_path: inner_path + }, function(res) { + var ref; + if (wrapper.site_info.privatekey || (ref = wrapper.site_info.auth_address, indexOf.call(res.signers, ref) >= 0)) { return wrapper.ws.cmd("siteSign", { - privatekey: privatekey, + privatekey: "stored", inner_path: inner_path, update_changed_files: true }, function(res) { - if (res === "ok") { - return wrapper.notifications.add("sign", "done", inner_path + " Signed!", 5000); - } + return wrapper.notifications.add("sign", "done", inner_path + " Signed!", 5000); }); - }); - } + } else { + return wrapper.displayPrompt("Enter your private key:", "password", "Sign", "", function(privatekey) { + return wrapper.ws.cmd("siteSign", { + privatekey: privatekey, + inner_path: inner_path, + update_changed_files: true + }, function(res) { + if (res === "ok") { + return wrapper.notifications.add("sign", "done", inner_path + " Signed!", 5000); + } + }); + }); + } + }); return false; }; })(this)); @@ -676,7 +682,7 @@ window.initScrollable = function () { return img.onload = (function(_this) { return function() { return wrapper.ws.cmd("sidebarGetPeers", [], function(globe_data) { - var e, ref, ref1; + var e, error, ref, ref1; if (_this.globe) { _this.globe.scene.remove(_this.globe.points); _this.globe.addData(globe_data, { From db8c85d2494c3d5fa27b0606976af2f292d6feff Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 15 Jun 2017 22:23:00 +0200 Subject: [PATCH 0084/2570] Version 0.5.6, Changelog --- CHANGELOG.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ src/Config.py | 2 +- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a7d04ce..2bab805b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,48 @@ +## ZeroNet 0.5.6 (2017-06-15) +### Added + - Callback for certSelect API command + - More compact list formatting in json + +### Changed + - Remove obsolete auth_key_sha512 and signature format + - Improved Spanish translation (Thanks to Pupiloho) + +### Fixed + - Opened port checking (Thanks l5h5t7 & saber28 for reporting) + - Standalone update.py argument parsing (Thanks Zalex for reporting) + - uPnP crash on startup (Thanks Vertux for reporting) + - CoffeeScript 1.12.6 compatibility (Thanks kavamaken & imachug) + - Multi value argument parsing + - Database error when running from directory that contains special characters (Thanks Pupiloho for reporting) + - Site lock violation logging + + +#### Proxy bypass during source upgrade [Reported by ZeroMux] + +In ZeroNet before 0.5.6 during the client's built-in source code upgrade mechanism, +ZeroNet did not respect Tor and/or proxy settings. + +Result: ZeroNet downloaded the update without using the Tor network and potentially leaked the connections. + +Fix: Removed the problematic code line from the updater that removed the proxy settings from the socket library. + +Affected versions: ZeroNet 0.5.5 and earlier, Fixed in: ZeroNet 0.5.6 + + +#### XSS vulnerability using DNS rebinding. [Reported by Beardog108] + +In ZeroNet before 0.5.6 the web interface did not validate the request's Host parameter. + +Result: An attacker using a specially crafted DNS entry could have bypassed the browser's cross-site-scripting protection +and potentially gained access to user's private data stored on site. + +Fix: By default ZeroNet only accept connections from 127.0.0.1 and localhost hosts. +If you bind the ui server to an external interface, then it also adds the first http request's host to the allowed host list +or you can define it manually using --ui_host. + +Affected versions: ZeroNet 0.5.5 and earlier, Fixed in: ZeroNet 0.5.6 + + ## ZeroNet 0.5.5 (2017-05-18) ### Added - Outgoing socket binding by --bind parameter diff --git a/src/Config.py b/src/Config.py index df4956f2..e4703771 100644 --- a/src/Config.py +++ b/src/Config.py @@ -9,7 +9,7 @@ import ConfigParser class Config(object): def __init__(self, argv): - self.version = "0.5.5" + self.version = "0.5.6" self.rev = 2109 self.argv = argv self.action = None From 4088c88a56693abcd9c445df84f56f4cee95808c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 15:32:58 +0200 Subject: [PATCH 0085/2570] Sidebar check siteSign result --- plugins/Sidebar/media/Sidebar.coffee | 3 ++- plugins/Sidebar/media/all.js | 20 ++++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee index acd78e91..e180c595 100644 --- a/plugins/Sidebar/media/Sidebar.coffee +++ b/plugins/Sidebar/media/Sidebar.coffee @@ -358,7 +358,8 @@ class Sidebar extends Class if wrapper.site_info.privatekey or wrapper.site_info.auth_address in res.signers # Privatekey stored in users.json wrapper.ws.cmd "siteSign", {privatekey: "stored", inner_path: inner_path, update_changed_files: true}, (res) => - wrapper.notifications.add "sign", "done", "#{inner_path} Signed!", 5000 + if res == "ok" + wrapper.notifications.add "sign", "done", "#{inner_path} Signed!", 5000 else # Ask the user for privatekey diff --git a/plugins/Sidebar/media/all.js b/plugins/Sidebar/media/all.js index 22c1c4ff..1acb2fdc 100644 --- a/plugins/Sidebar/media/all.js +++ b/plugins/Sidebar/media/all.js @@ -5,7 +5,7 @@ (function() { var Class, - __slice = [].slice; + slice = [].slice; Class = (function() { function Class() {} @@ -14,7 +14,7 @@ Class.prototype.log = function() { var args; - args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; if (!this.trace) { return; } @@ -28,23 +28,23 @@ Class.prototype.logStart = function() { var args, name; - name = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; + 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)"])); + 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) : []; + 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)"])); + this.log.apply(this, ["" + name].concat(slice.call(args), ["(Done in " + ms + "ms)"])); return this; }; @@ -57,6 +57,7 @@ }).call(this); + /* ---- plugins/Sidebar/media/RateLimit.coffee ---- */ @@ -86,6 +87,7 @@ }).call(this); + /* ---- plugins/Sidebar/media/Scrollable.js ---- */ @@ -611,7 +613,9 @@ window.initScrollable = function () { inner_path: inner_path, update_changed_files: true }, function(res) { - return wrapper.notifications.add("sign", "done", inner_path + " Signed!", 5000); + if (res === "ok") { + return wrapper.notifications.add("sign", "done", inner_path + " Signed!", 5000); + } }); } else { return wrapper.displayPrompt("Enter your private key:", "password", "Sign", "", function(privatekey) { @@ -682,7 +686,7 @@ window.initScrollable = function () { return img.onload = (function(_this) { return function() { return wrapper.ws.cmd("sidebarGetPeers", [], function(globe_data) { - var e, error, ref, ref1; + var e, ref, ref1; if (_this.globe) { _this.globe.scene.remove(_this.globe.points); _this.globe.addData(globe_data, { From d607696a7514fbd8d4f3e4be58ddbfe19950dea2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 15:33:23 +0200 Subject: [PATCH 0086/2570] Update websocket site_info on own setting change --- plugins/Sidebar/SidebarPlugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index c0a96494..41cbadc0 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -583,6 +583,7 @@ class UiWebsocketPlugin(object): return self.response(to, "You don't have permission to run this command") self.site.settings["own"] = bool(owned) + self.site.updateWebsocket(owned=owned) def actionSiteSetAutodownloadoptional(self, to, owned): permissions = self.getPermissions(to) From c5d90ddd1951e5db03860cce1ff66b0d0029b30e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 15:36:07 +0200 Subject: [PATCH 0087/2570] Start new workers if lots of tasks left --- src/Worker/WorkerManager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index c3b48d2e..633e5e1f 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -100,6 +100,9 @@ class WorkerManager(object): self.startWorkers() break # One reannounce per loop + if len(self.tasks) > len(self.workers) * 2 and len(self.workers) < self.getMaxWorkers(): + self.startWorkers() + self.log.debug("checkTasks stopped running") # Returns the next free or less worked task From c2b177434df8e3bcc7e41dc6ab2f2c66a9896cb2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 15:38:05 +0200 Subject: [PATCH 0088/2570] Drop error if want to publish non existent file --- src/Ui/UiWebsocket.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 13d96f33..f6fa21a9 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -405,6 +405,9 @@ class UiWebsocket(object): self.site.saveSettings() self.site.announce() + if not inner_path in self.site.content_manager.contents: + return self.response(to, {"error": "File %s not found" % inner_path}) + event_name = "publish %s %s" % (self.site.address, inner_path) called_instantly = RateLimit.isAllowed(event_name, 30) thread = RateLimit.callAsync(event_name, 30, self.doSitePublish, self.site, inner_path) # Only publish once in 30 seconds From 4b346243eb654757fc4846afbcc768a2d0081285 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 15:39:14 +0200 Subject: [PATCH 0089/2570] On clone sign content.json after all other files copied --- src/Site/Site.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 3e99b7fe..3ef0df50 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -576,7 +576,13 @@ class Site(object): # Copy files for content_inner_path, content in self.content_manager.contents.items(): - for file_relative_path in sorted(content["files"].keys()): + file_relative_paths = content.get("files", {}).keys() + + # Sign content.json at the end to make sure every file is included + file_relative_paths.sort() + file_relative_paths.sort(key=lambda key: key.replace("-default", "").endswith("content.json")) + + for file_relative_path in file_relative_paths: file_inner_path = helper.getDirname(content_inner_path) + file_relative_path # Relative to content.json file_inner_path = file_inner_path.strip("/") # Strip leading / if not file_inner_path.startswith(root_inner_path): @@ -599,7 +605,7 @@ class Site(object): dest_dir = os.path.dirname(file_path_dest) if not os.path.isdir(dest_dir): os.makedirs(dest_dir) - if file_inner_path_dest == "content.json-default": # Don't copy root content.json-default + if file_inner_path_dest.replace("-default", "") == "content.json": # Don't copy root content.json-default continue shutil.copy(file_path, file_path_dest) From fec3ff7d8ef33d81b5d089103e0b5dfc8876423c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 15:40:13 +0200 Subject: [PATCH 0090/2570] Remove bad files that has no info --- src/Site/Site.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 3ef0df50..cff97ece 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -377,13 +377,15 @@ class Site(object): # Remove files that no longer in content.json for bad_file in self.bad_files.keys(): - if bad_file.endswith("content.json"): - continue - file_info = self.content_manager.getFileInfo(bad_file) - if file_info is False or not file_info.get("size"): - del self.bad_files[bad_file] - self.log.debug("No info for file: %s, removing from bad_files" % bad_file) + if bad_file.endswith("content.json"): + if file_info is False: + del self.bad_files[bad_file] + self.log.debug("No info for file: %s, removing from bad_files" % bad_file) + else: + if file_info is False or not file_info.get("size"): + del self.bad_files[bad_file] + self.log.debug("No info for file: %s, removing from bad_files" % bad_file) if announce: self.announce() From 2a3b8a7692e039337474afade64785c9f4743935 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 15:47:03 +0200 Subject: [PATCH 0091/2570] Verify raise exception instead of return False --- src/Content/ContentManager.py | 110 +++++++++++++++------------------- 1 file changed, 48 insertions(+), 62 deletions(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index b06ec12f..2ffb7707 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -15,6 +15,10 @@ from Peer import PeerHashfield from ContentDbDict import ContentDbDict +class VerifyError(Exception): + pass + + class ContentManager(object): def __init__(self, site): @@ -611,9 +615,7 @@ class ContentManager(object): new_content["signs"] = {} new_content["signs"][privatekey_address] = sign - if not self.verifyContent(inner_path, new_content): - self.log.error("Sign failed: Invalid content") - return False + self.verifyContent(inner_path, new_content) if filewrite: self.log.info("Saving to %s..." % inner_path) @@ -655,18 +657,19 @@ class ContentManager(object): if not rules.get("cert_signers"): return True # Does not need cert + if not "cert_user_id" in content: + raise VerifyError("Missing cert_user_id") + name, domain = content["cert_user_id"].split("@") cert_address = rules["cert_signers"].get(domain) if not cert_address: # Cert signer not allowed - self.log.warning("Invalid cert signer: %s" % domain) - return False + raise VerifyError("Invalid cert signer: %s" % domain) try: cert_subject = "%s#%s/%s" % (rules["user_address"], content["cert_auth_type"], name) result = CryptBitcoin.verify(cert_subject, cert_address, content["cert_sign"]) except Exception, err: - self.log.warning("Certificate verify error: %s" % err) - result = False + raise VerifyError("Certificate verify error: %s" % err) return result # Checks if the content.json content is valid @@ -690,24 +693,21 @@ class ContentManager(object): # Check site address if content.get("address") and content["address"] != self.site.address: - self.log.warning("%s: Wrong site address: %s != %s" % (inner_path, content["address"], self.site.address)) - return False + raise VerifyError("Wrong site address: %s != %s" % (content["address"], self.site.address)) # Check file inner path if content.get("inner_path") and content["inner_path"] != inner_path: - self.log.warning("%s: Wrong inner_path: %s" % (inner_path, content["inner_path"])) - return False + raise VerifyError("Wrong inner_path: %s" % content["inner_path"]) # Check total site size limit if site_size > site_size_limit: - self.log.warning("%s: Site too large %s > %s, aborting task..." % (inner_path, site_size, site_size_limit)) if inner_path == "content.json" and self.site.settings["size"] == 0: # First content.json download, save site size to display warning self.site.settings["size"] = site_size task = self.site.worker_manager.findTask(inner_path) if task: # Dont try to download from other peers self.site.worker_manager.failTask(task) - return False + raise VerifyError("Site too large %s > %s, aborting task..." % (site_size, site_size_limit)) if inner_path == "content.json": self.site.settings["size"] = site_size @@ -717,39 +717,33 @@ class ContentManager(object): # Load include details rules = self.getRules(inner_path, content) if not rules: - self.log.warning("%s: No rules" % inner_path) - return False + raise VerifyError("No rules") # Check include size limit if rules.get("max_size") is not None: # Include size limit if content_size > rules["max_size"]: - self.log.warning("%s: Include too large %s > %s" % (inner_path, content_size, rules["max_size"])) - return False + raise VerifyError("Include too large %s > %s" % (content_size, rules["max_size"])) if rules.get("max_size_optional") is not None: # Include optional files limit if content_size_optional > rules["max_size_optional"]: - self.log.warning("%s: Include optional files too large %s > %s" % ( - inner_path, content_size_optional, rules["max_size_optional"]) + raise VerifyError("Include optional files too large %s > %s" % ( + content_size_optional, rules["max_size_optional"]) ) - return False # Filename limit if rules.get("files_allowed"): for file_inner_path in content["files"].keys(): if not re.match("^%s$" % rules["files_allowed"], file_inner_path): - self.log.warning("%s %s: File not allowed" % (inner_path, file_inner_path)) - return False + raise VerifyError("File not allowed: %s" % file_inner_path) if rules.get("files_allowed_optional"): for file_inner_path in content.get("files_optional", {}).keys(): if not re.match("^%s$" % rules["files_allowed_optional"], file_inner_path): - self.log.warning("%s %s: Optional file not allowed" % (inner_path, file_inner_path)) - return False + raise VerifyError("Optional file not allowed: %s" % file_inner_path) # Check if content includes allowed if rules.get("includes_allowed") is False and content.get("includes"): - self.log.warning("%s: Includes not allowed" % inner_path) - return False # Includes not allowed + raise VerifyError("Includes not allowed") self.site.settings["size"] = site_size self.site.settings["size_optional"] = site_size_optional @@ -772,20 +766,16 @@ class ContentManager(object): 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 - self.log.debug( - "We have newer %s (Our: %s, Sent: %s)" % - (inner_path, old_content["modified"], new_content["modified"]) + raise VerifyError( + "We have newer (Our: %s, Sent: %s)" % + (old_content["modified"], new_content["modified"]) ) - # gevent.spawn(self.site.publish, inner_path=inner_path) # Try to fix the broken peers - return False if new_content["modified"] > time.time() + 60 * 60 * 24: # Content modified in the far future (allow 1 day+) - self.log.warning("%s modify is in the future!" % inner_path) - return False + raise VerifyError("Modify timestamp is in the far future!") if self.isArchived(inner_path, new_content["modified"]): - self.log.warning("%s this file is archived!" % inner_path) if inner_path in self.site.bad_files: del self.site.bad_files[inner_path] - return False + raise VerifyError("This file is archived!") # Check sign sign = new_content.get("sign") signs = new_content.get("signs", {}) @@ -805,23 +795,19 @@ class ContentManager(object): '"modified": %s' % modified_fixed ) - if not self.verifyContent(inner_path, new_content): - return False # Content not valid (files too large, invalid files) + self.verifyContent(inner_path, new_content) 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 - if not CryptBitcoin.verify( - "%s:%s" % (signs_required, ",".join(valid_signers)), self.site.address, new_content["signers_sign"] - ): - self.log.warning("%s invalid signers_sign!" % inner_path) - return False + 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 - self.log.warning("%s invalid cert!" % inner_path) - return False + raise VerifyError("Invalid cert!") valid_signs = 0 for address in valid_signers: @@ -829,36 +815,36 @@ class ContentManager(object): valid_signs += CryptBitcoin.verify(sign_content, address, signs[address]) if valid_signs >= signs_required: break # Break if we has enough signs - if config.verbose: - self.log.debug("%s: Valid signs: %s/%s" % (inner_path, valid_signs, signs_required)) - return valid_signs >= signs_required + if valid_signs < signs_required: + raise VerifyError("Valid signs: %s/%s" % (valid_signs, signs_required)) + else: + return True else: # Old style signing - return CryptBitcoin.verify(sign_content, self.site.address, sign) + if CryptBitcoin.verify(sign_content, self.site.address, sign): + return True + else: + raise VerifyError("Invalid old-style sign") except Exception, err: self.log.warning("Verify sign error: %s" % Debug.formatException(err)) - return False + raise err else: # Check using sha512 hash file_info = self.getFileInfo(inner_path) if file_info: - if "sha512" in file_info: - hash_valid = CryptHash.sha512sum(file) == file_info["sha512"] - elif "sha1" in file_info: # Backward compatibility - hash_valid = CryptHash.sha1sum(file) == file_info["sha1"] - else: - hash_valid = False + if CryptHash.sha512sum(file) != file_info["sha512"]: + raise VerifyError("Invalid hash") + if file_info.get("size", 0) != file.tell(): - self.log.warning( - "%s file size does not match %s <> %s, Hash: %s" % - (inner_path, file.tell(), file_info.get("size", 0), hash_valid) + raise VerifyError( + "File size does not match %s <> %s" % + (inner_path, file.tell(), file_info.get("size", 0)) ) - return False - return hash_valid + + return True else: # File not in content.json - self.log.warning("File not in content.json: %s" % inner_path) - return False + raise VerifyError("File not in content.json") def optionalDownloaded(self, inner_path, hash, size=None, own=False): if size is None: From 94c7ce9f42f5b851c87c2786983271550349975b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 15:47:47 +0200 Subject: [PATCH 0092/2570] Sign raise error instead of return False --- src/Content/ContentManager.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 2ffb7707..4f79b024 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -19,6 +19,10 @@ class VerifyError(Exception): pass +class SignError(Exception): + pass + + class ContentManager(object): def __init__(self, site): @@ -587,7 +591,7 @@ class ContentManager(object): privatekey_address = CryptBitcoin.privatekeyToAddress(privatekey) valid_signers = self.getValidSigners(inner_path, new_content) if privatekey_address not in valid_signers: - return self.log.error( + raise SignError( "Private key invalid! Valid signers: %s, Private key address: %s" % (valid_signers, privatekey_address) ) From 0224863b1f0cbce8efcb05692f4b3f84a314107a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 15:48:33 +0200 Subject: [PATCH 0093/2570] Only use positive sizes to calculate size sum of content.json --- 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 4f79b024..3cdd2a16 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -679,7 +679,7 @@ class ContentManager(object): # Checks if the content.json content is valid # Return: True or False def verifyContent(self, inner_path, content): - content_size = len(json.dumps(content, indent=1)) + sum([file["size"] for file in content["files"].values()]) # Size of new content + content_size = len(json.dumps(content, indent=1)) + sum([file["size"] for file in content["files"].values() if file["size"] >= 0]) # Size of new content # Calculate old content size old_content = self.contents.get(inner_path) if old_content: @@ -689,7 +689,7 @@ class ContentManager(object): old_content_size = 0 old_content_size_optional = 0 - content_size_optional = sum([file["size"] for file in content.get("files_optional", {}).values()]) + content_size_optional = sum([file["size"] for file in content.get("files_optional", {}).values() if file["size"] >= 0]) site_size = self.site.settings["size"] - old_content_size + content_size # Site size without old content plus the new site_size_optional = self.site.settings["size_optional"] - old_content_size_optional + content_size_optional # Site size without old content plus the new From b0ee0dae935010e1c5b1ba2cd07d1a6c51fc6033 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 15:48:50 +0200 Subject: [PATCH 0094/2570] Remove old testing code --- src/Content/ContentManager.py | 52 ----------------------------------- 1 file changed, 52 deletions(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 3cdd2a16..2b41bbed 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -869,55 +869,3 @@ class ContentManager(object): done = self.hashfield.removeHash(hash) self.site.settings["optional_downloaded"] -= size return done - - -if __name__ == "__main__": - def testSign(): - global config - from Site import Site - site = Site("12Hw8rTgzrNo4DSh2AkqwPRqDyTticwJyH") - content_manager = ContentManager(site) - content_manager.sign( - "data/users/1KRxE1s3oDyNDawuYWpzbLUwNm8oDbeEp6/content.json", "5JCGE6UUruhfmAfcZ2GYjvrswkaiq7uLo6Gmtf2ep2Jh2jtNzWR" - ) - - def testVerify(): - from Site import Site - site = Site("12Hw8rTgzrNo4DSh2AkqwPRqDyTticwJyH") - - content_manager = ContentManager(site) - print "Loaded contents:", content_manager.contents.keys() - - file = open(site.storage.getPath("data/users/1KRxE1s3oDyNDawuYWpzbLUwNm8oDbeEp6/content.json")) - print "content.json valid:", content_manager.verifyFile( - "data/users/1KRxE1s3oDyNDawuYWpzbLUwNm8oDbeEp6/content.json", file, ignore_same=False - ) - - file = open(site.storage.getPath("data/users/1KRxE1s3oDyNDawuYWpzbLUwNm8oDbeEp6/messages.json")) - print "messages.json valid:", content_manager.verifyFile( - "data/users/1KRxE1s3oDyNDawuYWpzbLUwNm8oDbeEp6/messages.json", file, ignore_same=False - ) - - def testInfo(): - from Site import Site - site = Site("12Hw8rTgzrNo4DSh2AkqwPRqDyTticwJyH") - - content_manager = ContentManager(site) - print content_manager.contents.keys() - - print content_manager.getFileInfo("index.html") - print content_manager.getIncludeInfo("data/users/1KRxE1s3oDyNDawuYWpzbLUwNm8oDbeEp6/content.json") - print content_manager.getValidSigners("data/users/1KRxE1s3oDyNDawuYWpzbLUwNm8oDbeEp6/content.json") - print content_manager.getValidSigners("data/users/content.json") - print content_manager.getValidSigners("content.json") - - import sys - import logging - os.chdir("../..") - sys.path.insert(0, os.path.abspath(".")) - sys.path.insert(0, os.path.abspath("src")) - logging.basicConfig(level=logging.DEBUG) - - # testSign() - testVerify() - # testInfo() From 7653cba247d539b72d1ab373d6779585a4edf9e6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 15:49:39 +0200 Subject: [PATCH 0095/2570] SiteVerify command line action display verify error --- src/main.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main.py b/src/main.py index fa8ef59b..448e954f 100644 --- a/src/main.py +++ b/src/main.py @@ -240,13 +240,17 @@ class Actions(object): for content_inner_path in site.content_manager.contents: s = time.time() logging.info("Verifing %s signature..." % content_inner_path) - file_correct = site.content_manager.verifyFile( - content_inner_path, site.storage.open(content_inner_path, "rb"), ignore_same=False - ) + try: + file_correct = site.content_manager.verifyFile( + content_inner_path, site.storage.open(content_inner_path, "rb"), ignore_same=False + ) + except Exception, err: + file_correct = False + if file_correct is True: logging.info("[OK] %s (Done in %.3fs)" % (content_inner_path, time.time() - s)) else: - logging.error("[ERROR] %s: invalid file!" % content_inner_path) + logging.error("[ERROR] %s: invalid file: %s!" % (content_inner_path, err)) raw_input("Continue?") bad_files += content_inner_path From ff69b042168c4b6e3909047fb688e98faad75593 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 15:57:02 +0200 Subject: [PATCH 0096/2570] Catch verify exceptions on manual site files verification process --- src/Site/SiteStorage.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index 1101d56a..41c4a14e 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -351,11 +351,16 @@ class SiteStorage(object): if quick_check: ok = os.path.getsize(file_path) == content["files"][file_relative_path]["size"] + if not ok: + err = "Invalid size" else: - ok = self.site.content_manager.verifyFile(file_inner_path, open(file_path, "rb")) + try: + ok = self.site.content_manager.verifyFile(file_inner_path, open(file_path, "rb")) + except Exception, err: + ok = False if not ok: - self.log.debug("[CHANGED] %s" % file_inner_path) + self.log.debug("[INVALID] %s: %s" % (file_inner_path, err)) if add_changed or content.get("cert_user_id"): # If updating own site only add changed user files bad_files.append(file_inner_path) @@ -377,7 +382,10 @@ class SiteStorage(object): if quick_check: ok = os.path.getsize(file_path) == content["files_optional"][file_relative_path]["size"] else: - ok = self.site.content_manager.verifyFile(file_inner_path, open(file_path, "rb")) + try: + ok = self.site.content_manager.verifyFile(file_inner_path, open(file_path, "rb")) + except Exception, err: + ok = False if ok: if not self.site.content_manager.hashfield.hasHash(file_node["sha512"]): From c58d28861db686b089430c8299c492e0ab40b238 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 16:01:41 +0200 Subject: [PATCH 0097/2570] Raise RequestError instead of Exception of fileGet error --- src/File/FileRequest.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index e01fc58d..586d90ad 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -17,6 +17,10 @@ from Plugin import PluginManager FILE_BUFF = 1024 * 512 +class RequestError(Exception): + pass + + # Incoming requests @PluginManager.acceptPlugins class FileRequest(object): @@ -191,7 +195,7 @@ class FileRequest(object): file_size = os.fstat(file.fileno()).st_size if params["location"] > file_size: self.connection.badAction(5) - raise Exception("Bad file location") + raise RequestError("Bad file location") back = { "body": file, @@ -212,6 +216,9 @@ class FileRequest(object): return {"bytes_sent": bytes_sent, "file_size": file_size, "location": params["location"]} + except RequestError, err: + self.log.debug("GetFile request error: %s" % Debug.formatException(err)) + self.response({"error": "File read error: %s" % err}) except Exception, err: self.log.debug("GetFile read error: %s" % Debug.formatException(err)) self.response({"error": "File read error"}) @@ -232,7 +239,7 @@ class FileRequest(object): stream_bytes = min(FILE_BUFF, file_size - params["location"]) if stream_bytes < 0: self.connection.badAction(5) - raise Exception("Bad file location") + raise RequestError("Bad file location") back = { "size": file_size, From c7146613a1d44d6034e632c5d0fba1b51ba38238 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 16:02:52 +0200 Subject: [PATCH 0098/2570] Store inner path to variable on update request to save some complexity --- src/File/FileRequest.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index 586d90ad..d09b7959 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -107,7 +107,9 @@ class FileRequest(object): self.connection.badAction(1) return False - if not params["inner_path"].endswith("content.json"): + inner_path = params.get("inner_path", "") + + if not inner_path.endswith("content.json"): self.response({"error": "Only content.json update allowed"}) self.connection.badAction(5) return @@ -115,12 +117,12 @@ class FileRequest(object): try: content = json.loads(params["body"]) except Exception, err: - self.log.debug("Update for %s is invalid JSON: %s" % (params["inner_path"], err)) + self.log.debug("Update for %s is invalid JSON: %s" % (inner_path, err)) self.response({"error": "File invalid JSON"}) self.connection.badAction(5) return - file_uri = "%s/%s:%s" % (site.address, params["inner_path"], content["modified"]) + file_uri = "%s/%s:%s" % (site.address, inner_path, content["modified"]) if self.server.files_parsing.get(file_uri): # Check if we already working on it valid = None # Same file @@ -130,27 +132,27 @@ class FileRequest(object): if valid is True: # Valid and changed self.log.info("Update for %s/%s looks valid, saving..." % (params["site"], params["inner_path"])) self.server.files_parsing[file_uri] = True - site.storage.write(params["inner_path"], params["body"]) + site.storage.write(inner_path, params["body"]) del params["body"] - site.onFileDone(params["inner_path"]) # Trigger filedone + site.onFileDone(inner_path) # Trigger filedone - if params["inner_path"].endswith("content.json"): # Download every changed file from peer + if inner_path.endswith("content.json"): # Download every changed file from peer peer = site.addPeer(self.connection.ip, self.connection.port, return_peer=True) # Add or get peer # On complete publish to other peers diffs = params.get("diffs", {}) - site.onComplete.once(lambda: site.publish(inner_path=params["inner_path"], diffs=diffs, limit=2), "publish_%s" % params["inner_path"]) + site.onComplete.once(lambda: site.publish(inner_path=inner_path, diffs=diffs, limit=2), "publish_%s" % inner_path) # Load new content file and download changed files in new thread def downloader(): - site.downloadContent(params["inner_path"], peer=peer, diffs=params.get("diffs", {})) + site.downloadContent(inner_path, peer=peer, diffs=params.get("diffs", {})) del self.server.files_parsing[file_uri] gevent.spawn(downloader) else: del self.server.files_parsing[file_uri] - self.response({"ok": "Thanks, file %s updated!" % params["inner_path"]}) + self.response({"ok": "Thanks, file %s updated!" % inner_path}) self.connection.goodAction() elif valid is None: # Not changed From 25657ebdca57871d1e0f9cad9c1d566978718fe1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 16:04:17 +0200 Subject: [PATCH 0099/2570] Log to site instead of fileserver --- src/File/FileRequest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index d09b7959..c4f42350 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -130,7 +130,7 @@ class FileRequest(object): valid = site.content_manager.verifyFile(params["inner_path"], content) if valid is True: # Valid and changed - self.log.info("Update for %s/%s looks valid, saving..." % (params["site"], params["inner_path"])) + site.log.info("Update for %s looks valid, saving..." % inner_path) self.server.files_parsing[file_uri] = True site.storage.write(inner_path, params["body"]) del params["body"] @@ -163,8 +163,8 @@ class FileRequest(object): if peer: if not peer.connection: peer.connect(self.connection) # Assign current connection to peer - if params["inner_path"] in site.content_manager.contents: - peer.last_content_json_update = site.content_manager.contents[params["inner_path"]]["modified"] + if inner_path in site.content_manager.contents: + peer.last_content_json_update = site.content_manager.contents[inner_path]["modified"] if config.verbose: self.log.debug( "Same version, adding new peer for locked files: %s, tasks: %s" % From 9a9bd71634556442f08670b91b844cdcebc7b66d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 16:07:36 +0200 Subject: [PATCH 0100/2570] Catch file verification errors on update request --- src/File/FileRequest.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index c4f42350..ea114a1c 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -127,7 +127,11 @@ class FileRequest(object): if self.server.files_parsing.get(file_uri): # Check if we already working on it valid = None # Same file else: - valid = site.content_manager.verifyFile(params["inner_path"], content) + try: + valid = site.content_manager.verifyFile(inner_path, content) + except Exception, err: + self.log.debug("Update for %s is invalid" % (inner_path, err)) + valid = False if valid is True: # Valid and changed site.log.info("Update for %s looks valid, saving..." % inner_path) @@ -179,8 +183,7 @@ class FileRequest(object): self.connection.badAction() else: # Invalid sign or sha hash - self.log.debug("Update for %s is invalid" % params["inner_path"]) - self.response({"error": "File invalid"}) + self.response({"error": "File invalid: %s" % err}) self.connection.badAction(5) # Send file content request From f773bf333663d8efdce2bb34bf6cf6f255275dc2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 16:10:18 +0200 Subject: [PATCH 0101/2570] Add file_size to request to avoid unnecessary download --- src/File/FileRequest.py | 4 ++++ src/Peer/Peer.py | 4 ++-- src/Worker/Worker.py | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index ea114a1c..7b90b036 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -198,6 +198,10 @@ class FileRequest(object): file.seek(params["location"]) file.read_bytes = FILE_BUFF file_size = os.fstat(file.fileno()).st_size + if params.get("file_size") and params["file_size"] != file_size: + self.connection.badAction(5) + raise RequestError("File size does not match") + if params["location"] > file_size: self.connection.badAction(5) raise RequestError("Bad file location") diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index 17e6431e..ff38e153 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -155,7 +155,7 @@ class Peer(object): return None # Failed after 4 retry # Get a file content from peer - def getFile(self, site, inner_path): + def getFile(self, site, inner_path, file_size=None): # Use streamFile if client supports it if config.stream_downloads and self.connection and self.connection.handshake and self.connection.handshake["rev"] > 310: return self.streamFile(site, inner_path) @@ -168,7 +168,7 @@ class Peer(object): s = time.time() while True: # Read in 512k parts - res = self.request("getFile", {"site": site, "inner_path": inner_path, "location": location}) + res = self.request("getFile", {"site": site, "inner_path": inner_path, "location": location, "file_size": file_size}) if not res or "body" not in res: # Error return False diff --git a/src/Worker/Worker.py b/src/Worker/Worker.py index bdeb2431..a3592d7c 100644 --- a/src/Worker/Worker.py +++ b/src/Worker/Worker.py @@ -46,7 +46,7 @@ class Worker(object): site = task["site"] task["workers_num"] += 1 try: - buff = self.peer.getFile(site.address, task["inner_path"]) + buff = self.peer.getFile(site.address, task["inner_path"], task["size"]) except Exception, err: self.manager.log.debug("%s: getFile error: %s" % (self.key, err)) buff = None From 43c8dacd7074903323df2bb1dee1da588368bcab Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 16:10:49 +0200 Subject: [PATCH 0102/2570] Catch and log exact verification errors on worker download --- src/Worker/Worker.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Worker/Worker.py b/src/Worker/Worker.py index a3592d7c..71c8b552 100644 --- a/src/Worker/Worker.py +++ b/src/Worker/Worker.py @@ -56,11 +56,15 @@ class Worker(object): if task["done"] is True: # Task done, try to find new one continue if buff: # Download ok - correct = site.content_manager.verifyFile(task["inner_path"], buff) + try: + correct = site.content_manager.verifyFile(task["inner_path"], buff) + except Exception, err: + correct = False else: # Download error + err = "Download failed" correct = False - if correct is True or correct is None: # Hash ok or same file - self.manager.log.debug("%s: Hash correct: %s" % (self.key, task["inner_path"])) + if correct is True or correct is None: # Verify ok or same file + self.manager.log.debug("%s: Verify correct: %s" % (self.key, task["inner_path"])) if correct is True and task["done"] is False: # Save if changed and task not done yet buff.seek(0) site.storage.write(task["inner_path"], buff) @@ -68,10 +72,10 @@ class Worker(object): self.manager.doneTask(task) task["workers_num"] -= 1 self.task = None - else: # Hash failed + else: # Verify failed self.manager.log.debug( - "%s: Hash failed: %s, failed peers: %s" % - (self.key, task["inner_path"], len(task["failed"])) + "%s: Verify failed: %s, error: %s, failed peers: %s" % + (self.key, task["inner_path"], err, len(task["failed"])) ) task["failed"].append(self.peer) self.task = None From 79ca1069ec0c8e8e19409934f38b84ae887bcbff Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 16:11:47 +0200 Subject: [PATCH 0103/2570] Display exact sign error to UI on failure --- src/Ui/UiWebsocket.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index f6fa21a9..5abccc9c 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -377,10 +377,11 @@ class UiWebsocket(object): # Reload content.json, ignore errors to make it up-to-date site.content_manager.loadContent(inner_path, add_bad_files=False, force=True) # Sign using private key sent by user - signed = site.content_manager.sign(inner_path, privatekey, extend=extend, update_changed_files=update_changed_files, remove_missing_optional=remove_missing_optional) - if not signed: - self.cmd("notification", ["error", _["Content signing failed"]]) - self.response(to, {"error": "Site sign failed"}) + try: + signed = site.content_manager.sign(inner_path, privatekey, extend=extend, update_changed_files=update_changed_files, remove_missing_optional=remove_missing_optional) + except Exception, err: + self.cmd("notification", ["error", _["Content signing failed"] + "
%s" % err]) + self.response(to, {"error": "Site sign failed: %s" % err}) return site.content_manager.loadContent(inner_path, add_bad_files=False) # Load new content.json, ignore errors From 64199c7dedfc73112260710f03fcc8c9d690ae52 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 16:12:24 +0200 Subject: [PATCH 0104/2570] Reduce logging on tracker error --- 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 cff97ece..ff44368d 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -787,7 +787,7 @@ class Site(object): req.close() req = None if not response: - self.log.debug("Http tracker %s response error" % url) + self.log.debug("Http tracker %s response error" % tracker_address) return False # Decode peers peer_data = bencode.decode(response)["peers"] @@ -800,7 +800,7 @@ class Site(object): addr, port = struct.unpack('!LH', peer) peers.append({"addr": socket.inet_ntoa(struct.pack('!L', addr)), "port": port}) except Exception, err: - self.log.debug("Http tracker %s error: %s" % (url, err)) + self.log.debug("Http tracker %s error: %s" % (tracker_address, err)) if req: req.close() req = None From 066f54f521debef1ee660dbb27b746b59ec0c93d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 16:12:41 +0200 Subject: [PATCH 0105/2570] Only log listModification request in verbose mode --- src/Site/Site.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index ff44368d..e8e9445c 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -345,10 +345,12 @@ class Site(object): if since is None: # No since defined, download from last modification time-1day since = self.settings.get("modified", 60 * 60 * 24) - 60 * 60 * 24 - self.log.debug( - "Try to get listModifications from peers: %s, connected: %s, since: %s" % - (peers_try, peers_connected_num, since) - ) + + if config.verbose: + self.log.debug( + "Try to get listModifications from peers: %s, connected: %s, since: %s" % + (peers_try, peers_connected_num, since) + ) updaters = [] for i in range(3): From 916709a7e48a0be37d689beceb1a04d70a4b8cba Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 16:13:06 +0200 Subject: [PATCH 0106/2570] Allow less peer error if for popular sites --- src/Peer/Peer.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index ff38e153..3734ed85 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -329,7 +329,11 @@ class Peer(object): # On connection error def onConnectionError(self, reason="Unknown"): self.connection_error += 1 - if self.connection_error >= 6: # Dead peer + if len(self.site.peers) > 200: + limit = 3 + else: + limit = 6 + if self.connection_error >= limit: # Dead peer self.remove("Peer connection: %s" % reason) # Done working with peer From dca3c775d1d3e35274d3d6418164affe799d57c9 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 16:13:58 +0200 Subject: [PATCH 0107/2570] Test expect exact sign and verification errors --- src/Test/TestContent.py | 23 ++++++++++++---- src/Test/TestContentUser.py | 52 ++++++++++++++++++++++++------------- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/src/Test/TestContent.py b/src/Test/TestContent.py index 78065c00..bbe30097 100644 --- a/src/Test/TestContent.py +++ b/src/Test/TestContent.py @@ -5,6 +5,7 @@ from cStringIO import StringIO import pytest from Crypt import CryptBitcoin +from Content.ContentManager import VerifyError, SignError @pytest.mark.usefixtures("resetSettings") @@ -58,7 +59,10 @@ class TestContent: data_dict["files"]["data.json"]["size"] = 200000 # Emulate 2MB sized data.json data_dict["signs"] = {"1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict), privatekey)} data = StringIO(json.dumps(data_dict)) - assert not site.content_manager.verifyFile("data/test_include/content.json", data, ignore_same=False) + with pytest.raises(VerifyError) as err: + site.content_manager.verifyFile("data/test_include/content.json", data, ignore_same=False) + assert "Include too large" in str(err) + # Reset data_dict["files"]["data.json"]["size"] = 505 del data_dict["signs"] @@ -67,7 +71,10 @@ class TestContent: data_dict["files"]["notallowed.exe"] = data_dict["files"]["data.json"] data_dict["signs"] = {"1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict), privatekey)} data = StringIO(json.dumps(data_dict)) - assert not site.content_manager.verifyFile("data/test_include/content.json", data, ignore_same=False) + with pytest.raises(VerifyError) as err: + site.content_manager.verifyFile("data/test_include/content.json", data, ignore_same=False) + assert "File not allowed" in str(err) + # Reset del data_dict["files"]["notallowed.exe"] del data_dict["signs"] @@ -80,7 +87,9 @@ class TestContent: @pytest.mark.parametrize("inner_path", ["content.json", "data/test_include/content.json", "data/users/content.json"]) def testSign(self, site, inner_path): # Bad privatekey - assert not site.content_manager.sign(inner_path, privatekey="5aaa3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMnaa", filewrite=False) + with pytest.raises(SignError) as err: + site.content_manager.sign(inner_path, privatekey="5aaa3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMnaa", filewrite=False) + assert "Private key invalid" in str(err) # Good privatekey content = site.content_manager.sign(inner_path, privatekey="5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv", filewrite=False) @@ -154,7 +163,9 @@ class TestContent: "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), privatekey) } data = StringIO(json.dumps(data_dict)) - assert not site.content_manager.verifyFile(inner_path, data, ignore_same=False) + with pytest.raises(VerifyError) as err: + site.content_manager.verifyFile(inner_path, data, ignore_same=False) + assert "Wrong site address" in str(err) # Wrong inner_path data_dict["address"] = "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT" @@ -164,7 +175,9 @@ class TestContent: "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), privatekey) } data = StringIO(json.dumps(data_dict)) - assert not site.content_manager.verifyFile(inner_path, data, ignore_same=False) + with pytest.raises(VerifyError) as err: + site.content_manager.verifyFile(inner_path, data, ignore_same=False) + assert "Wrong inner_path" in str(err) # Everything right again data_dict["address"] = "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT" diff --git a/src/Test/TestContentUser.py b/src/Test/TestContentUser.py index 6655f5e2..95af8529 100644 --- a/src/Test/TestContentUser.py +++ b/src/Test/TestContentUser.py @@ -4,6 +4,7 @@ from cStringIO import StringIO import pytest from Crypt import CryptBitcoin +from Content.ContentManager import VerifyError, SignError @pytest.mark.usefixtures("resetSettings") @@ -81,7 +82,10 @@ class TestUserContent: rules = site.content_manager.getRules(user_inner_path, data_dict) assert rules["max_size"] == 0 data = StringIO(json.dumps(data_dict)) - assert not site.content_manager.verifyFile(user_inner_path, data, ignore_same=False) + + with pytest.raises(VerifyError) as err: + site.content_manager.verifyFile(user_inner_path, data, ignore_same=False) + assert "Include too large" in str(err) users_content["user_contents"]["permission_rules"][".*"]["max_size"] = 10000 # Reset # Test max optional size exception @@ -101,7 +105,9 @@ class TestUserContent: "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), privatekey) } data = StringIO(json.dumps(data_dict)) - assert not site.content_manager.verifyFile(user_inner_path, data, ignore_same=False) + with pytest.raises(VerifyError) as err: + site.content_manager.verifyFile(user_inner_path, data, ignore_same=False) + assert "Include optional files too large" in str(err) data_dict["files_optional"]["peanut-butter-jelly-time.gif"]["size"] = 1024 * 1024 # Reset # hello.exe = Not allowed @@ -111,7 +117,9 @@ class TestUserContent: "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), privatekey) } data = StringIO(json.dumps(data_dict)) - assert not site.content_manager.verifyFile(user_inner_path, data, ignore_same=False) + with pytest.raises(VerifyError) as err: + site.content_manager.verifyFile(user_inner_path, data, ignore_same=False) + assert "Optional file not allowed" in str(err) del data_dict["files_optional"]["hello.exe"] # Reset # Includes not allowed in user content @@ -121,7 +129,9 @@ class TestUserContent: "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), privatekey) } data = StringIO(json.dumps(data_dict)) - assert not site.content_manager.verifyFile(user_inner_path, data, ignore_same=False) + with pytest.raises(VerifyError) as err: + site.content_manager.verifyFile(user_inner_path, data, ignore_same=False) + assert "Includes not allowed" in err def testCert(self, site): @@ -174,10 +184,13 @@ class TestUserContent: # Test banned user cert_user_id = user_content["cert_user_id"] # My username site.content_manager.contents["data/users/content.json"]["user_contents"]["permissions"][cert_user_id] = False - assert not site.content_manager.verifyFile( - "data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/content.json", - StringIO(json.dumps(signed_content)), ignore_same=False - ) + with pytest.raises(VerifyError) as err: + site.content_manager.verifyFile( + "data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/content.json", + StringIO(json.dumps(signed_content)), ignore_same=False + ) + assert "Valid sings: 0" in str(err) + del site.content_manager.contents["data/users/content.json"]["user_contents"]["permissions"][cert_user_id] # Reset # Test invalid cert user_content["cert_sign"] = CryptBitcoin.sign( @@ -186,10 +199,12 @@ class TestUserContent: signed_content = site.content_manager.sign( "data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/content.json", user_priv, filewrite=False ) - assert not site.content_manager.verifyFile( - "data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/content.json", - StringIO(json.dumps(signed_content)), ignore_same=False - ) + with pytest.raises(VerifyError) as err: + site.content_manager.verifyFile( + "data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/content.json", + StringIO(json.dumps(signed_content)), ignore_same=False + ) + assert "Invalid cert" in str(err) # Test banned user, signed by the site owner user_content["cert_sign"] = CryptBitcoin.sign("1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C#%s/%s" % ( @@ -237,17 +252,18 @@ class TestUserContent: ) # Test removed cert - # user_content["cert_sign"] + del user_content["cert_user_id"] del user_content["cert_auth_type"] del user_content["signs"] # Remove signs before signing user_content["signs"] = { "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(user_content, sort_keys=True), user_priv) } - print "--- Signed content", user_content - assert not site.content_manager.verifyFile( - "data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/content.json", - StringIO(json.dumps(user_content)), ignore_same=False - ) + with pytest.raises(VerifyError) as err: + site.content_manager.verifyFile( + "data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/content.json", + StringIO(json.dumps(user_content)), ignore_same=False + ) + assert "Missing cert_user_id" in str(err) def testNewFile(self, site): privatekey = "5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv" # For 1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT From b7106995b773a2c2d8240b3ad9b6f1699ef3314f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 16:14:14 +0200 Subject: [PATCH 0108/2570] Test size errors on getFile --- src/Test/TestFileRequest.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Test/TestFileRequest.py b/src/Test/TestFileRequest.py index 9b42ffce..45d44b5a 100644 --- a/src/Test/TestFileRequest.py +++ b/src/Test/TestFileRequest.py @@ -18,9 +18,13 @@ class TestFileRequest: connection = client.getConnection("127.0.0.1", 1544) file_server.sites[site.address] = site + # Normal request response = connection.request("getFile", {"site": site.address, "inner_path": "content.json", "location": 0}) assert "sign" in response["body"] + response = connection.request("getFile", {"site": site.address, "inner_path": "content.json", "location": 0, "file_size": 4460}) + assert "sign" in response["body"] + # Invalid file response = connection.request("getFile", {"site": site.address, "inner_path": "invalid.file", "location": 0}) assert "File read error" in response["error"] @@ -40,6 +44,10 @@ class TestFileRequest: response = connection.request("getFile", {"site": ".", "inner_path": "users.json", "location": 0}) assert "Unknown site" in response["error"] + # Invalid size + response = connection.request("getFile", {"site": site.address, "inner_path": "content.json", "location": 0, "file_size": 1234}) + assert "File size does not match" in response["error"] + connection.close() client.stop() From 9f72fdeb41c3e36b345b7b117c4a0f22604c4cfa Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 16:14:36 +0200 Subject: [PATCH 0109/2570] Display sign error on command line siteSign command failure --- src/main.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index 448e954f..647681bd 100644 --- a/src/main.py +++ b/src/main.py @@ -222,7 +222,11 @@ class Actions(object): import getpass privatekey = getpass.getpass("Private key (input hidden):") diffs = site.content_manager.getDiffs(inner_path) - succ = site.content_manager.sign(inner_path=inner_path, privatekey=privatekey, update_changed_files=True, remove_missing_optional=remove_missing_optional) + try: + succ = site.content_manager.sign(inner_path=inner_path, privatekey=privatekey, update_changed_files=True, remove_missing_optional=remove_missing_optional) + except Exception, err: + logging.error("Sign error: %s" % err) + succ = False if succ and publish: self.sitePublish(address, inner_path=inner_path, diffs=diffs) From 7d9cb65ba16d76cbfd9b682b646b27b130bf02bf Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 16:16:13 +0200 Subject: [PATCH 0110/2570] Catch message type errors on connection parsing --- src/Connection/Connection.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 234053db..7134ded7 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -157,11 +157,14 @@ class Connection(object): self.unpacker.feed(buff) buff = None for message in self.unpacker: - self.incomplete_buff_recv = 0 - if "stream_bytes" in message: - self.handleStream(message) - else: - self.handleMessage(message) + try: + self.incomplete_buff_recv = 0 + if "stream_bytes" in message: + self.handleStream(message) + else: + self.handleMessage(message) + except TypeError: + raise Exception("Invalid message type: %s" % type(message)) message = None except Exception, err: From f1786c2ee6a5e248706ca09479f209691016590a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 16:16:43 +0200 Subject: [PATCH 0111/2570] Handle unknown message type --- src/Connection/Connection.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 7134ded7..a5c83a0b 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -235,8 +235,13 @@ class Connection(object): # Handle incoming message def handleMessage(self, message): + try: + cmd = message["cmd"] + except TypeError, AttributeError: + cmd = None + self.last_message_time = time.time() - if message.get("cmd") == "response": # New style response + if cmd == "response": # New style response if message["to"] in self.waiting_requests: if self.last_send_time and len(self.waiting_requests) == 1: ping = time.time() - self.last_send_time @@ -264,14 +269,13 @@ class Connection(object): self.setHandshake(message) else: self.log("Unknown response: %s" % message) - elif message.get("cmd"): # Handhsake request - if message["cmd"] == "handshake": + elif cmd: # Handhsake request + if cmd == "handshake": self.handleHandshake(message) else: self.server.handleRequest(self, message) else: # Old style response, no req_id defined - if config.debug_socket: - self.log("Unknown message: %s, waiting: %s" % (message, self.waiting_requests.keys())) + self.log("Unknown message, waiting: %s" % self.waiting_requests.keys()) if self.waiting_requests: last_req_id = min(self.waiting_requests.keys()) # Get the oldest waiting request and set it true self.waiting_requests[last_req_id].set(message) From 14cd9315c19e269027cd0e29ce52e37418d68f94 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 16:18:20 +0200 Subject: [PATCH 0112/2570] Don't try to send to closed socket --- src/Connection/Connection.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index a5c83a0b..c6743f5a 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -356,6 +356,11 @@ class Connection(object): message.get("params", {}).get("site"), message.get("params", {}).get("inner_path"), message.get("req_id")) ) + + if not self.sock: + self.log("Send error: missing socket") + return False + self.last_send_time = time.time() try: if streaming: From 15d85890699cb8f0998e5440512405da852de3de Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 16:18:37 +0200 Subject: [PATCH 0113/2570] Fix typo, less verbose socket logging --- 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 c6743f5a..baf50188 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -375,7 +375,7 @@ class Connection(object): self.server.bytes_sent += len(data) self.sock.sendall(data) except Exception, err: - self.close("Send errror: %s" % Debug.formatException(err)) + self.close("Send error: %s" % err) return False self.last_sent_time = time.time() return True From 67212ee29e3162cf23dabe2c57f103dcc0bc7476 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Jun 2017 16:19:04 +0200 Subject: [PATCH 0114/2570] Rev2121 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index e4703771..03782675 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.6" - self.rev = 2109 + self.rev = 2121 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 23db93d20f8c8b882319d42918bfd6e396846c04 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 20 Jun 2017 11:20:30 +0200 Subject: [PATCH 0115/2570] Fix invalid update logging --- 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 7b90b036..320e29cc 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -130,7 +130,7 @@ class FileRequest(object): try: valid = site.content_manager.verifyFile(inner_path, content) except Exception, err: - self.log.debug("Update for %s is invalid" % (inner_path, err)) + self.log.debug("Update for %s is invalid: %s" % (inner_path, err)) valid = False if valid is True: # Valid and changed From f08f354eb6f89eebfc092f7568849afcb5a16c2d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 20 Jun 2017 11:20:43 +0200 Subject: [PATCH 0116/2570] More detailed file size match error logging --- src/File/FileRequest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index 320e29cc..d3b9ebdc 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -200,7 +200,7 @@ class FileRequest(object): file_size = os.fstat(file.fileno()).st_size if params.get("file_size") and params["file_size"] != file_size: self.connection.badAction(5) - raise RequestError("File size does not match") + raise RequestError("File size does not match: %sB != %sB" % (params["file_size"], file_size)) if params["location"] > file_size: self.connection.badAction(5) @@ -226,7 +226,7 @@ class FileRequest(object): return {"bytes_sent": bytes_sent, "file_size": file_size, "location": params["location"]} except RequestError, err: - self.log.debug("GetFile request error: %s" % Debug.formatException(err)) + self.log.debug("GetFile %s %s request error: %s" % (self.connection, params["inner_path"], Debug.formatException(err))) self.response({"error": "File read error: %s" % err}) except Exception, err: self.log.debug("GetFile read error: %s" % Debug.formatException(err)) From e157894694bce1120785384feaceb67e6f3aeb85 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 20 Jun 2017 11:20:48 +0200 Subject: [PATCH 0117/2570] Rev2122 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 03782675..978d14db 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.6" - self.rev = 2121 + self.rev = 2122 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 71fbcee76c7677272a4df20feb858ecb2a0c8284 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 20 Jun 2017 20:28:09 +0200 Subject: [PATCH 0118/2570] Fix error on exiting when there is no stream_server started --- src/Connection/ConnectionServer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index 43439e68..987018fb 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -35,6 +35,7 @@ class ConnectionServer: self.ips = {} # Connection by ip self.has_internet = True # Internet outage detection + self.stream_server = None self.running = True self.thread_checker = gevent.spawn(self.checkConnections) @@ -74,7 +75,8 @@ class ConnectionServer: def stop(self): self.running = False - self.stream_server.stop() + if self.stream_server: + self.stream_server.stop() def handleIncomingConnection(self, sock, addr): ip, port = addr From a7b22e205538bc90f395f76aa0f49e604d4e8409 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 20 Jun 2017 20:28:35 +0200 Subject: [PATCH 0119/2570] Fix error on exit for peers without site --- 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 3734ed85..d2f0f8cd 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -329,7 +329,7 @@ class Peer(object): # On connection error def onConnectionError(self, reason="Unknown"): self.connection_error += 1 - if len(self.site.peers) > 200: + if self.site and len(self.site.peers) > 200: limit = 3 else: limit = 6 From 0d6d19502f46ac2f61138096982a0a3908bbe919 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 20 Jun 2017 20:29:11 +0200 Subject: [PATCH 0120/2570] Format json reply for peerCmd command line action --- src/main.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index 647681bd..77276a9c 100644 --- a/src/main.py +++ b/src/main.py @@ -471,7 +471,11 @@ class Actions(object): parameters = json.loads(parameters.replace("'", '"')) else: parameters = {} - logging.info("Response: %s" % peer.request(cmd, parameters)) + try: + res = peer.request(cmd, parameters) + print json.dumps(res, indent=2, ensure_ascii=False) + except Exception, err: + print "Unknown response (%s): %s" % (err, res) actions = Actions() From 66e2192e655605b8146283b8ff45b254b561e2cc Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 20 Jun 2017 20:32:51 +0200 Subject: [PATCH 0121/2570] Add --silent option to disable logging to terminal --- src/Config.py | 1 + src/main.py | 6 +++++- zeronet.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index 978d14db..65418ef5 100644 --- a/src/Config.py +++ b/src/Config.py @@ -168,6 +168,7 @@ class Config(object): # Config parameters self.parser.add_argument('--verbose', help='More detailed logging', action='store_true') self.parser.add_argument('--debug', help='Debug mode', action='store_true') + self.parser.add_argument('--silent', help='Disable logging to terminal output', action='store_true') self.parser.add_argument('--debug_socket', help='Debug socket connections', action='store_true') self.parser.add_argument('--debug_gevent', help='Debug gevent functions', action='store_true') diff --git a/src/main.py b/src/main.py index 77276a9c..d9426a28 100644 --- a/src/main.py +++ b/src/main.py @@ -76,9 +76,13 @@ if config.action == "main": ) else: log_file_path = "%s/cmd.log" % config.log_dir + if config.silent: + level = logging.ERROR + else: + level = logging.DEBUG logging.basicConfig( format='[%(asctime)s] %(levelname)-8s %(name)s %(message)s', - level=logging.DEBUG, stream=open(log_file_path, "w") + level=level, stream=open(log_file_path, "w") ) # Console logger diff --git a/zeronet.py b/zeronet.py index 5b68b426..d0503db0 100755 --- a/zeronet.py +++ b/zeronet.py @@ -6,7 +6,7 @@ import sys def main(): - print "- Starting ZeroNet..." + print "- Starting ZeroNet..." main = None try: From fd1f104f4e39255ff5a855393f76f793f5fd1602 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 20 Jun 2017 20:32:59 +0200 Subject: [PATCH 0122/2570] Rev2124 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 65418ef5..69ee2ed5 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.6" - self.rev = 2122 + self.rev = 2124 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 401d3ec1c944fdc5a72fc172c7f9d9159ef68b58 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 21 Jun 2017 00:03:26 +0200 Subject: [PATCH 0123/2570] Rev2125, Fix missing if line --- src/Config.py | 2 +- zeronet.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 69ee2ed5..e9567baa 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.6" - self.rev = 2124 + self.rev = 2125 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/zeronet.py b/zeronet.py index d0503db0..fa5d8f90 100755 --- a/zeronet.py +++ b/zeronet.py @@ -6,6 +6,7 @@ import sys def main(): + if "--silent" not in sys.argv: print "- Starting ZeroNet..." main = None From 03cabcb07cd8af8815368c7f3f85895e53b87f49 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 27 Jun 2017 18:08:28 +0200 Subject: [PATCH 0124/2570] Rev2127, Fix delete files without file_info --- src/Config.py | 2 +- src/Ui/UiWebsocket.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index e9567baa..ee9c5905 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.6" - self.rev = 2125 + self.rev = 2127 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 5abccc9c..3df7841d 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -533,7 +533,7 @@ class UiWebsocket(object): return self.response(to, {"error": "Forbidden, you can only modify your own files"}) file_info = self.site.content_manager.getFileInfo(inner_path) - if file_info.get("optional"): + if file_info and file_info.get("optional"): self.log.debug("Deleting optional file: %s" % inner_path) relative_path = file_info["relative_path"] content_json = self.site.storage.loadJson(file_info["content_inner_path"]) From ebbe19131b931f3dafe26afe191749990108706e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 30 Jun 2017 10:13:25 +0200 Subject: [PATCH 0125/2570] Rev2128, Update to OpenSSL v1.0.2l --- src/Config.py | 2 +- src/lib/opensslVerify/HashInfo.txt | Bin 2318 -> 2318 bytes src/lib/opensslVerify/ReadMe.txt | 4 ++-- src/lib/opensslVerify/libeay32.dll | Bin 1367552 -> 1369088 bytes src/lib/opensslVerify/openssl.exe | Bin 517632 -> 518656 bytes src/lib/opensslVerify/ssleay32.dll | Bin 337408 -> 337408 bytes 6 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Config.py b/src/Config.py index ee9c5905..ddcf8983 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.6" - self.rev = 2127 + self.rev = 2128 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/lib/opensslVerify/HashInfo.txt b/src/lib/opensslVerify/HashInfo.txt index 58c6257ff68c44050c964df27b683ed92e4c27d3..32739d5592ed74e6de99019e9307995e72a610ed 100644 GIT binary patch literal 2318 zcmb8w-KyJG5C!0UUC2A+34$fdwj4qr(2L&mrhNj-vLJ9s0=Y?FzU}(d_OUJP)-LUjwr^v5)7Gp#wfF5+`>Flfer|{M`q}d~|GRd|>XLC6 ziKz{dobztca?7}6eaqjJXYHqV?VK+wTfQ{^7Q0`v!otvwS*3Z1-nq*ScZnslLv&A> z?{ogxtsJsOX4k#A=DX&^C97k*=5NSziUdo)w_n=vesaNlBS$!k1D=HVF);~fW{UGl7v-)9^myJmOo ze)8y$7m8C>%X_&Sx_)!WOxZuiBY9U9b5_Mh^ELMQz2$_w?Xx1{Tc2H{3Cf4e#C?ba zi>jwisHgGn8!3A{rFF_E%Megysu9Yi>y&f5yu3MOO{R`{*6zVAzF&Kkknz52U-LZW zf;qdqlS?@1QdO$IpsymX+0$vP%KIh%$L!BJp(iVA z{!Y&R8+%vz0HA9P_Vga0t(;D(H}T5%Z006~HH%KWC;dN!KbC zQFG%n&;H#7xP%`vr($5Ng5;Gir;br^-If^-A-}~=p#>Kx@r@Q(@ey0-9$NG&Eaebp zv|l2jj-AA({NFO-OZa0xxm0Nh(o=|lMLY_ogmL_RM*QO?9@qd1aH~?eh-L7pE!@r3n6jK zlog!@54TTsiwckr42}b!0tbBg-#t)Vsv|4I(1#Trg_Q^^zii`1JXn!nWc7&M)bo#S zg7Qc9@v{6Fu_it$wu7S>@vFX6uF9zMdMn8C=hTa#~MX8~J^MlHym-gE~n z8LLwZ6>&niax!FJ^*d4bzPyvUR A`v3p{ literal 2318 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

{_XdiALWLpM7S&LM{ID? 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⪙iVYJjz1JczHca&P&OqSpFww|pnNEkoL=WR* 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 z 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�c& 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

)`dK&x%ONV!&D&r2>s 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

v!4D3W~R(+nBg!R=? zV{8NV0jw9h8C#A$hb_aZE7(=oZ0_(*2PX*V+f73P?cjrYsDu*8g6WU~3A;I<&iy=3 z3_ve*Ljc;q2lY?}MUW0EHVqP#ew2dXD0D#ww1SGMjo3}tV#o$Jq=1?~jf$(OU4QhX z_R)|wopW9ji_kZGrj5R2_^}-Ezr(Zq+O_jl))ybA_IvZh3Fv{N&<#hR3j)vqZO{sP zpaE+3>SsUGF8*l=x)@eL5fs8A$bt-TLpe+Z7oiOE_&@E{Cd>Z;S8iR+ delta 199269 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

p$ief1MZl0$oCIbkYE0Yw%lwwknLZ;p5 zK2K{PMYRzCu|ASijeJuBe7ocMX={!eNKpioB9NrmbGJ|&C`;DBN&q4Axd3b}y8y!G zJpj08mdCGRh!X>L>a>XY1Q@+ZHp71=dT+J)JN}IEa}}YrgoxevTZH=rp%{dr-_QAS z#@S67KQz_;Kw#nXwA-=5u5bN(1^F?Lu?=c)Y zj`;5CwKZn{sotr-ckzU}P&1<71 zNA@*YbN;A+H7URZ0Jes!-Pte*n_hl)Ywg~*7|Z8P;A4Wkihr7iAmDge-gM^Hu$%Vu z=qS5+b0dAZIal}hc?>PPzbEbz6jHiu_GA9$W;~hTH94FFe>s-RZ`s;2(a08V0$Y1B zYppWM*1ncoa@4m7@;w7wR#zIUx1;1zZF2vqKne3RC9N(GP?dS|46oN8d`CLBSaTdo zE`oleV$)laE2%=Qx2__QcD1m%>J%%{VibmLx&r0#wZbL)Y3wuS(jGimX^vHntg1{d zztl~=Occ;N&P4QDIxuFed}_b(CECP%gZ=>^n}WtC1+d!!3I!mxZS$rD#FAw)v8nG7JgtvwYGUAM4`8zZMHCc&_(O-YI)N!a}T|C@|N1XTL}CA*)<<1acDRGAvVQWIx6 zlFxZfCJD9cd~=f|$g--;Zk5kM30VMbBzx!5B!D&&z;Q_cZ6tthNdPr3fYv@Bii8%j zUv-XTL;iv?PG(1KPK{lGAA*p-{%O?fx85+Tz>A-pC?+9dHPW#@JjvJTYeVl&o2BFM$w47 z{Z1iIw=~oB3J=8F zNq~FYD$>;MUuWrB+Wi}v^>D!2{UcvGDZ9V0c~sfRxzx-w`#OV15DL>FV!mUv#xDk*s04YfM6Tieq{BGS+*@pg zW9ZD;c50EoSW*9>$(Y{O=nn~*k(V|J^P8Pon3#5HX(W6@2`V5h?vP2#YI%F8Somfr zd1m}SKPP5GSB!83q$F94G~*Iv;xO^fWlnfN*+bG@mebEf-EvIgKda=HkPtM3En$`U zbE$XUG^~W{ZM!tRQhHQf&)KDe>$;Yg=GA{+;}(G*F^foU?mjOs@!crjZDj|?56WFc z9-Q4o%w#o{Y?3AuAOFEN9MFR5Du1jUMQVShAAKFU2z5GzFft~7K}&0AgabL{z

xx(cG?=Oghjloc`UpsXbD^1 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 delta 118111 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 K%P5Fb7N$?;PalOI5gwL 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�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
@jP{DWW3}?}0WJ#5USyF9YmekyfIAH}fpeEFc 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 G=ll=OgNb+m From f30b2b6fc2b799d0bd3e1e81b68634bb1418af61 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 4 Jul 2017 01:12:58 +0200 Subject: [PATCH 0126/2570] Rev2130, Use SslPatch to load openssl library, Fix Android 6 openssl loading --- src/Config.py | 2 +- src/lib/opensslVerify/opensslVerify.py | 25 +++++--------- src/lib/pyelliptic/openssl.py | 18 ++-------- src/util/SslPatch.py | 47 +++++++++++++++++--------- 4 files changed, 43 insertions(+), 49 deletions(-) diff --git a/src/Config.py b/src/Config.py index ddcf8983..de045b3e 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.6" - self.rev = 2128 + self.rev = 2130 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/lib/opensslVerify/opensslVerify.py b/src/lib/opensslVerify/opensslVerify.py index d0eede54..524aeb4d 100644 --- a/src/lib/opensslVerify/opensslVerify.py +++ b/src/lib/opensslVerify/opensslVerify.py @@ -194,24 +194,15 @@ ssl = None def openLibrary(): global ssl - try: - if sys.platform.startswith("win"): - dll_path = os.path.dirname(os.path.abspath(__file__)) + "/" + "libeay32.dll" - elif sys.platform == "cygwin": - dll_path = "/bin/cygcrypto-1.0.0.dll" - elif os.path.isfile("../lib/libcrypto.so"): # ZeroBundle OSX - dll_path = "../lib/libcrypto.so" - elif os.path.isfile("/opt/lib/libcrypto.so.1.0.0"): # For optware and entware - dll_path = "/opt/lib/libcrypto.so.1.0.0" - else: - dll_path = "/usr/local/ssl/lib/libcrypto.so" - ssl = _OpenSSL(dll_path) - assert ssl - except Exception, err: - ssl = _OpenSSL(ctypes.util.find_library('ssl.so.1.0') or ctypes.util.find_library('ssl') or ctypes.util.find_library('crypto') or ctypes.util.find_library('libcrypto') or 'libeay32') + import util.SslPatch + ssl = _OpenSSL(util.SslPatch.getLibraryPath()) logging.debug("opensslVerify loaded: %s", ssl._lib) -openLibrary() +if __name__ == "__main__": + ssl = _OpenSSL(sys.argv[1]) +else: + openLibrary() + openssl_version = "%.9X" % ssl._lib.SSLeay() NID_secp256k1 = 714 @@ -461,4 +452,4 @@ if __name__ == "__main__": for i in range(1000): pubkey = getMessagePubkey("hello", sign) verified = btctools.pubkey_to_address(pubkey) == address - print "100x Verified", verified, time.time() - s + print "1000x Verified", verified, time.time() - s diff --git a/src/lib/pyelliptic/openssl.py b/src/lib/pyelliptic/openssl.py index ea8136dd..6043f404 100644 --- a/src/lib/pyelliptic/openssl.py +++ b/src/lib/pyelliptic/openssl.py @@ -496,21 +496,9 @@ class _OpenSSL: def loadOpenSSL(): import logging + import util.SslPatch global OpenSSL - try: - if sys.platform.startswith("win"): - dll_path = os.path.normpath(os.path.dirname(__file__) + "/../opensslVerify/" + "libeay32.dll") - elif sys.platform == "cygwin": - dll_path = "/bin/cygcrypto-1.0.0.dll" - elif os.path.isfile("../lib/libcrypto.so"): # ZeroBundle OSX - dll_path = "../lib/libcrypto.so" - else: - dll_path = "/usr/local/ssl/lib/libcrypto.so" - ssl = _OpenSSL(dll_path) - assert ssl - except Exception, err: - ssl = _OpenSSL(ctypes.util.find_library('ssl.so.1.0') or ctypes.util.find_library('ssl') or ctypes.util.find_library('crypto') or ctypes.util.find_library('libcrypto') or 'libeay32') - OpenSSL = ssl - logging.debug("pyelliptic loaded: %s", ssl._lib) + OpenSSL = _OpenSSL(util.SslPatch.getLibraryPath()) + logging.debug("pyelliptic loaded: %s", OpenSSL._lib) loadOpenSSL() diff --git a/src/util/SslPatch.py b/src/util/SslPatch.py index 29dba5ed..40e70820 100644 --- a/src/util/SslPatch.py +++ b/src/util/SslPatch.py @@ -4,31 +4,46 @@ import logging import os import sys +import ctypes +import ctypes.util from Config import config +def getLibraryPath(): + if sys.platform.startswith("win"): + lib_path = os.path.dirname(os.path.abspath(__file__)) + "/../lib/opensslVerify/libeay32.dll" + elif sys.platform == "cygwin": + lib_path = "/bin/cygcrypto-1.0.0.dll" + elif os.path.isfile("../lib/libcrypto.so"): # ZeroBundle OSX + lib_path = "../lib/libcrypto.so" + elif os.path.isfile("/opt/lib/libcrypto.so.1.0.0"): # For optware and entware + lib_path = "/opt/lib/libcrypto.so.1.0.0" + else: + lib_path = "/usr/local/ssl/lib/libcrypto.so" + + if os.path.isfile(lib_path): + return lib_path + + if "ANDROID_APP_PATH" in os.environ: + try: + lib_dir = os.environ["ANDROID_APP_PATH"] + "/../../lib" + return [lib for lib in os.listdir(lib_dir) if "crypto" in lib][0] + except Exception, err: + logging.debug("OpenSSL lib not found in: %s (%s)" % (lib_dir, err)) + + return (ctypes.util.find_library('ssl.so.1.0') or ctypes.util.find_library('ssl') or ctypes.util.find_library('crypto') or ctypes.util.find_library('libcrypto') or 'libeay32') + + + def openLibrary(): - import ctypes - import ctypes.util - try: - if sys.platform.startswith("win"): - dll_path = "src/lib/opensslVerify/libeay32.dll" - elif sys.platform == "cygwin": - dll_path = "/bin/cygcrypto-1.0.0.dll" - else: - dll_path = "/usr/local/ssl/lib/libcrypto.so" - ssl_lib = ctypes.CDLL(dll_path, ctypes.RTLD_GLOBAL) - assert ssl_lib - except: - dll_path = ctypes.util.find_library('ssl.so.1.0') or ctypes.util.find_library('ssl') or ctypes.util.find_library('crypto') or ctypes.util.find_library('libcrypto') - ssl_lib = ctypes.CDLL(dll_path or 'libeay32', ctypes.RTLD_GLOBAL) + lib_path = getLibraryPath() or "libeay32" + logging.debug("Opening %s..." % lib_path) + ssl_lib = ctypes.CDLL(lib_path, ctypes.RTLD_GLOBAL) return ssl_lib def disableSSLCompression(): - import ctypes - import ctypes.util try: openssl = openLibrary() openssl.SSL_COMP_get_compression_methods.restype = ctypes.c_void_p From 2a161f4421c9f5394d35b9ee4192be664d708af8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 6 Jul 2017 00:08:32 +0200 Subject: [PATCH 0127/2570] Never allow cross-origin file request --- src/Ui/UiRequest.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index ffa0b6de..336a55f3 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -170,8 +170,6 @@ class UiRequest(object): headers.append(("Version", "HTTP/1.1")) headers.append(("Connection", "Keep-Alive")) headers.append(("Keep-Alive", "max=25, timeout=30")) - if content_type != "text/html": - headers.append(("Access-Control-Allow-Origin", "*")) # Allow json access on non-html files headers.append(("X-Frame-Options", "SAMEORIGIN")) # headers.append(("Content-Security-Policy", "default-src 'self' data: 'unsafe-inline' ws://127.0.0.1:* http://127.0.0.1:* wss://tracker.webtorrent.io; sandbox allow-same-origin allow-top-navigation allow-scripts")) # Only local connections if self.env["REQUEST_METHOD"] == "OPTIONS": From 1d6168f457c7b51213518d1a8f19da150b289810 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 6 Jul 2017 00:09:05 +0200 Subject: [PATCH 0128/2570] Download, svg, xml, flash, pdf files instead of displaying to avoid js execution --- src/Ui/UiRequest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 336a55f3..b075d6a9 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -182,6 +182,10 @@ class UiRequest(object): if content_type == "text/plain": content_type = "text/plain; charset=utf-8" + # Download instead of display file types that can be dangerous + if re.findall("/svg|/xml|/x-shockwave-flash|/pdf", content_type): + headers.append(("Content-Disposition", "attachment")) + cacheable_type = ( content_type == "text/css" or content_type.startswith("image") or content_type.startswith("video") or self.env["REQUEST_METHOD"] == "OPTIONS" or content_type == "application/javascript" From 49735b7e554ab759ce47b990e94410edd0383eea Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 6 Jul 2017 00:09:35 +0200 Subject: [PATCH 0129/2570] Fix not internal error on request files from not seeded sites --- src/Ui/UiRequest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index b075d6a9..817557a4 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -409,7 +409,7 @@ class UiRequest(object): if config.debug 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) - if site.settings["own"]: + if site and site.settings["own"]: from Debug import DebugMedia DebugMedia.merge(file_path) if not address or address == ".": From 6c0062dbc18c7d1fc467be22781d9715d5b83240 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 6 Jul 2017 00:09:47 +0200 Subject: [PATCH 0130/2570] Rev2132 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index de045b3e..0ba948af 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.6" - self.rev = 2130 + self.rev = 2132 self.argv = argv self.action = None self.config_file = "zeronet.conf" From febdea6c64220e1da12f0e9f752e2c69fdf3ba93 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 9 Jul 2017 14:10:01 +0200 Subject: [PATCH 0131/2570] Serve files without wrapper if requested using /raw/ prefix --- plugins/FilePack/FilePackPlugin.py | 4 +-- plugins/TranslateSite/TranslateSitePlugin.py | 7 +++-- plugins/Zeroname/UiRequestPlugin.py | 2 +- src/Ui/UiRequest.py | 33 +++++++++++++++----- 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/plugins/FilePack/FilePackPlugin.py b/plugins/FilePack/FilePackPlugin.py index e80f9de5..0d413b60 100644 --- a/plugins/FilePack/FilePackPlugin.py +++ b/plugins/FilePack/FilePackPlugin.py @@ -38,7 +38,7 @@ def openArchive(archive_path, path_within): @PluginManager.registerTo("UiRequest") class UiRequestPlugin(object): - def actionSiteMedia(self, path, header_length=True): + def actionSiteMedia(self, path, **kwargs): if ".zip/" in path or ".tar.gz/" in path: path_parts = self.parsePath(path) file_path = u"%s/%s/%s" % (config.data_dir, path_parts["address"], path_parts["inner_path"].decode("utf8")) @@ -63,7 +63,7 @@ class UiRequestPlugin(object): self.log.debug("Error opening archive file: %s" % err) return self.error404(path) - return super(UiRequestPlugin, self).actionSiteMedia(path, header_length=header_length) + return super(UiRequestPlugin, self).actionSiteMedia(path, **kwargs) def streamFile(self, file): while 1: diff --git a/plugins/TranslateSite/TranslateSitePlugin.py b/plugins/TranslateSite/TranslateSitePlugin.py index f0112c11..a7bc65b0 100644 --- a/plugins/TranslateSite/TranslateSitePlugin.py +++ b/plugins/TranslateSite/TranslateSitePlugin.py @@ -6,14 +6,15 @@ from Translate import translate @PluginManager.registerTo("UiRequest") class UiRequestPlugin(object): - def actionSiteMedia(self, path, header_length=True): + def actionSiteMedia(self, path, **kwargs): file_name = path.split("/")[-1] if not file_name: # Path ends with / file_name = "index.html" extension = file_name.split(".")[-1] if translate.lang != "en" and extension in ["js", "html"]: path_parts = self.parsePath(path) - file_generator = super(UiRequestPlugin, self).actionSiteMedia(path, header_length=False) + kwargs["header_length"] = False + file_generator = super(UiRequestPlugin, self).actionSiteMedia(path, **kwargs) if "next" in dir(file_generator): # File found and generator returned site = self.server.sites.get(path_parts["address"]) return self.actionPatchFile(site, path_parts["inner_path"], file_generator) @@ -21,7 +22,7 @@ class UiRequestPlugin(object): return file_generator else: - return super(UiRequestPlugin, self).actionSiteMedia(path) + return super(UiRequestPlugin, self).actionSiteMedia(path, **kwargs) def actionUiMedia(self, path): file_generator = super(UiRequestPlugin, self).actionUiMedia(path) diff --git a/plugins/Zeroname/UiRequestPlugin.py b/plugins/Zeroname/UiRequestPlugin.py index ea9bfcbe..dca5ae47 100644 --- a/plugins/Zeroname/UiRequestPlugin.py +++ b/plugins/Zeroname/UiRequestPlugin.py @@ -12,7 +12,7 @@ class UiRequestPlugin(object): super(UiRequestPlugin, self).__init__(*args, **kwargs) # Media request - def actionSiteMedia(self, path, header_length=True): + def actionSiteMedia(self, path, **kwargs): match = re.match("/media/(?P
[A-Za-z0-9-]+\.[A-Za-z0-9\.-]+)(?P/.*|$)", path) if match: # Its a valid domain, resolve first domain = match.group("address") diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 817557a4..b97d867b 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -99,6 +99,9 @@ class UiRequest(object): return self.actionDebug() elif path == "/Console" and config.debug: return self.actionConsole() + # Wrapper-less static files + elif path.startswith("/raw/"): + return self.actionSiteMedia(path.replace("/raw", "/media", 1), header_noscript=True) # Site media wrapper else: if self.get.get("wrapper_nonce"): @@ -164,6 +167,22 @@ class UiRequest(object): self.user = UserManager.user_manager.create() return self.user + def getRequestUrl(self): + if self.isProxyRequest(): + if self.env["PATH_INFO"].startswith("http://zero"): + return self.env["PATH_INFO"] + else: # Add http://zero to direct domain access + return self.env["PATH_INFO"].replace("http://", "http://zero/", 1) + else: + return self.env["wsgi.url_scheme"] + "://" + self.env["HTTP_HOST"] + self.env["PATH_INFO"] + + def getReferer(self): + referer = self.env.get("HTTP_REFERER") + if referer and self.isProxyRequest() and not referer.startswith("http://zero"): + return referer.replace("http://", "http://zero/", 1) + else: + return referer + # Send response headers def sendHeader(self, status=200, content_type="text/html", extra_headers=[]): headers = [] @@ -383,7 +402,7 @@ class UiRequest(object): return None # Serve a media for site - def actionSiteMedia(self, path, header_length=True): + def actionSiteMedia(self, path, header_length=True, header_noscript=False): if ".." in path: # File not in allowed path return self.error403("Invalid file path") @@ -391,7 +410,7 @@ class UiRequest(object): # Check wrapper nonce content_type = self.getContentType(path_parts["inner_path"]) - if "htm" in content_type: # Valid nonce must present to render html files + if "htm" in content_type and not header_noscript: # Valid nonce must present to render html files wrapper_nonce = self.get.get("wrapper_nonce") if wrapper_nonce not in self.server.wrapper_nonces: return self.error403("Wrapper nonce error. Please reload the page.") @@ -415,7 +434,7 @@ class UiRequest(object): if not address or address == ".": return self.error403(path_parts["inner_path"]) if os.path.isfile(file_path): # File exists - return self.actionFile(file_path, header_length=header_length) + return self.actionFile(file_path, header_length=header_length, header_noscript=header_noscript) elif os.path.isdir(file_path): # If this is actually a folder, add "/" and redirect return self.actionRedirect("./{0}/".format(path_parts["inner_path"].split("/")[-1])) else: # File not exists, try to download @@ -429,7 +448,7 @@ class UiRequest(object): result = site.needFile(path_parts["inner_path"], priority=15) # Wait until file downloads if result: - return self.actionFile(file_path, header_length=header_length) + return self.actionFile(file_path, header_length=header_length, header_noscript=header_noscript) else: self.log.debug("File not found: %s" % path_parts["inner_path"]) # Site larger than allowed, re-add wrapper nonce to allow reload @@ -459,15 +478,13 @@ class UiRequest(object): return self.error400() # Stream a file to client - def actionFile(self, file_path, block_size=64 * 1024, send_header=True, header_length=True): + def actionFile(self, file_path, block_size=64 * 1024, send_header=True, header_length=True, header_noscript=False): if ".." in file_path: raise Exception("Invalid path") if os.path.isfile(file_path): # Try to figure out content type by extension content_type = self.getContentType(file_path) - # TODO: Dont allow external access: extra_headers= - # [("Content-Security-Policy", "default-src 'unsafe-inline' data: http://localhost:43110 ws://localhost:43110")] range = self.env.get("HTTP_RANGE") range_start = None if send_header: @@ -476,6 +493,8 @@ class UiRequest(object): extra_headers["Accept-Ranges"] = "bytes" if header_length: extra_headers["Content-Length"] = str(file_size) + if header_noscript: + extra_headers["Content-Security-Policy"] = "default-src 'none'; sandbox allow-top-navigation; img-src 'self'; style-src 'self' 'unsafe-inline';" if range: range_start = int(re.match(".*?([0-9]+)", range).group(1)) if re.match(".*?-([0-9]+)", range): From 434cfce32a0d4a5b98dcd872b06355032664353a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 9 Jul 2017 14:11:06 +0200 Subject: [PATCH 0132/2570] More simple same origin test for media files cross-site access --- src/Ui/UiRequest.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index b97d867b..534b5696 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -377,12 +377,12 @@ class UiRequest(object): self.server.wrapper_nonces.append(wrapper_nonce) return wrapper_nonce - # Returns if media request allowed from that referer - def isMediaRequestAllowed(self, site_address, referer): - if not re.sub("^http[s]{0,1}://", "", referer).startswith(self.env["HTTP_HOST"]): + def isSameOrigin(self, url_a, url_b): + if not url_a or not url_b: return False - referer_path = re.sub("http[s]{0,1}://.*?/", "/", referer).replace("/media", "") # Remove site address - return referer_path.startswith("/" + site_address) + origin_a = re.sub("(http[s]{0,1}://.*?/.*?/).*", "\\1", url_a) + origin_b = re.sub("(http[s]{0,1}://.*?/.*?/).*", "\\1", url_b) + return origin_a == origin_b # Return {address: 1Site.., inner_path: /data/users.json} from url path def parsePath(self, path): @@ -418,8 +418,8 @@ class UiRequest(object): else: referer = self.env.get("HTTP_REFERER") if referer and path_parts: # Only allow same site to receive media - if not self.isMediaRequestAllowed(path_parts["request_address"], referer): - self.log.error("Media referrer error: %s not allowed from %s" % (path_parts["address"], referer)) + if not self.isSameOrigin(self.getRequestUrl(), self.getReferer()): + self.log.error("Media referrer error: %s not allowed from %s" % (self.getRequestUrl(), self.getReferer())) return self.error403("Media referrer error") # Referrer not starts same address as requested path if path_parts: # Looks like a valid path From 5950b04c40e3ab46cad341f5bdf4f0d4b763d5e6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 9 Jul 2017 14:11:44 +0200 Subject: [PATCH 0133/2570] Add allow-origin header for media requests from same origin to fix css font support --- src/Ui/UiRequest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 534b5696..6402c7b8 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -190,7 +190,8 @@ class UiRequest(object): headers.append(("Connection", "Keep-Alive")) headers.append(("Keep-Alive", "max=25, timeout=30")) headers.append(("X-Frame-Options", "SAMEORIGIN")) - # headers.append(("Content-Security-Policy", "default-src 'self' data: 'unsafe-inline' ws://127.0.0.1:* http://127.0.0.1:* wss://tracker.webtorrent.io; sandbox allow-same-origin allow-top-navigation allow-scripts")) # Only local connections + if content_type != "text/html" and self.env.get("HTTP_REFERER") and self.isSameOrigin(self.getReferer(), self.getRequestUrl()): + headers.append(("Access-Control-Allow-Origin", "*")) # Allow load font files from css if self.env["REQUEST_METHOD"] == "OPTIONS": # Allow json access headers.append(("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Cookie")) From 426fe561c9416f93848bf5aa1883c3673d5a869f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 9 Jul 2017 14:12:13 +0200 Subject: [PATCH 0134/2570] Cleanup not used wrapper opener check --- src/Ui/template/wrapper.html | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Ui/template/wrapper.html b/src/Ui/template/wrapper.html index 7fd67873..6bf6e181 100644 --- a/src/Ui/template/wrapper.html +++ b/src/Ui/template/wrapper.html @@ -20,13 +20,6 @@ if (window.self !== window.top) window.open(window.location.toString(), "_top"); if (window.self !== window.top) window.stop(); if (window.self !== window.top && document.execCommand) document.execCommand("Stop", false) - -// Dont allow site to load in a popup -/* -if (window.opener) document.write("Opener not allowed") -if (window.opener && document.execCommand) document.execCommand("Stop", false) -if (window.opener && window.stop) window.stop() -*/
From 26a250d1df9da118b19714a877d4082ee26a0697 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 9 Jul 2017 14:12:53 +0200 Subject: [PATCH 0135/2570] Media isMediaRequestAllowed no longer required for origin checking --- plugins/Zeroname/UiRequestPlugin.py | 33 +---------------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/plugins/Zeroname/UiRequestPlugin.py b/plugins/Zeroname/UiRequestPlugin.py index dca5ae47..25e5bd96 100644 --- a/plugins/Zeroname/UiRequestPlugin.py +++ b/plugins/Zeroname/UiRequestPlugin.py @@ -19,38 +19,7 @@ class UiRequestPlugin(object): address = self.site_manager.resolveDomain(domain) if address: path = "/media/" + address + match.group("inner_path") - return super(UiRequestPlugin, self).actionSiteMedia(path, header_length=header_length) # 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("\?.*", "", referer_path) # Remove http params - - if not re.sub("^http[s]{0,1}://", "", referer).startswith(self.env["HTTP_HOST"]): # Different origin - return False - - if self.isProxyRequest(): # Match to site domain - referer = re.sub("^http://zero[/]+", "http://", referer) # Allow /zero access - match = re.match("http[s]{0,1}://(.*?)(/|$)", referer) - if match: - referer_site_address = match.group(1) - else: - referer_site_address = None - else: # Match to request path - match = re.match("/(?P
[A-Za-z0-9\.-]+)(?P/.*|$)", referer_path) - if match: - referer_site_address = match.group("address") - else: - referer_site_address = None - - if not referer_site_address: - return False - elif 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 + return super(UiRequestPlugin, self).actionSiteMedia(path, **kwargs) # Get the wrapper frame output @PluginManager.registerTo("ConfigPlugin") class ConfigPlugin(object): From 1384da469168bb2088aa4f9f14cece117db37eb6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 9 Jul 2017 14:54:20 +0200 Subject: [PATCH 0136/2570] Rev2137 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 0ba948af..1659b8dc 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.6" - self.rev = 2132 + self.rev = 2137 self.argv = argv self.action = None self.config_file = "zeronet.conf" From efbef25c7693b4e19164dc4b1640a43ca9cc9fff Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 10 Jul 2017 02:41:01 +0200 Subject: [PATCH 0137/2570] UserSetSettings, UserGetSettings Websocket API commands --- src/Ui/UiWebsocket.py | 9 ++++++++- src/User/User.py | 6 ++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 3df7841d..80e0718d 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -818,13 +818,20 @@ class UiWebsocket(object): self.cmd("notification", ["done", _["Site cloned"] + "" % new_address]) gevent.spawn(new_site.announce) - def actionSiteSetLimit(self, to, size_limit): self.site.settings["size_limit"] = int(size_limit) self.site.saveSettings() self.response(to, "ok") self.site.download(blind_includes=True) + def actionUserGetSettings(self, to): + settings = self.user.sites[self.site.address].get("settings", {}) + self.response(to, settings) + + def actionUserSetSettings(self, to, settings): + self.user.setSettings(self.site.address, settings) + self.response(to, "ok") + def actionServerUpdate(self, to): self.cmd("updating") sys.modules["main"].update_after_shutdown = True diff --git a/src/User/User.py b/src/User/User.py index 0bfe082e..2b10924f 100644 --- a/src/User/User.py +++ b/src/User/User.py @@ -67,6 +67,12 @@ class User(object): self.save() self.log.debug("Deleted site: %s" % address) + def setSettings(self, address, settings): + site_data = self.getSiteData(address) + site_data["settings"] = settings + self.save() + return site_data + # Get data for a new, unique site # Return: [site_address, bip32_index, {"auth_address": "xxx", "auth_privatekey": "xxx", "privatekey": "xxx"}] def getNewSiteData(self): From 7c6bea6ddd77e65fdacdcd3dcfeade6f6a59f6b7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 10 Jul 2017 02:42:28 +0200 Subject: [PATCH 0138/2570] Fix raw site access without / at the site address end --- src/Ui/UiRequest.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 6402c7b8..547bc3e3 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -394,10 +394,11 @@ class UiRequest(object): if ".." in path: raise Exception("Invalid path") - match = re.match("/media/(?P
[A-Za-z0-9\._-]+)/(?P.*)", path) + match = re.match("/media/(?P
[A-Za-z0-9\._-]+)(?P/.*|$)", path) if match: path_parts = match.groupdict() path_parts["request_address"] = path_parts["address"] # Original request address (for Merger sites) + path_parts["inner_path"] = path_parts["inner_path"].lstrip("/") return path_parts else: return None @@ -437,7 +438,10 @@ class UiRequest(object): if os.path.isfile(file_path): # File exists return self.actionFile(file_path, header_length=header_length, header_noscript=header_noscript) elif os.path.isdir(file_path): # If this is actually a folder, add "/" and redirect - return self.actionRedirect("./{0}/".format(path_parts["inner_path"].split("/")[-1])) + if path_parts["inner_path"]: + return self.actionRedirect("./%s/" % path_parts["inner_path"].split("/")[-1]) + else: + return self.actionRedirect("./%s/" % path_parts["address"]) else: # File not exists, try to download if address not in SiteManager.site_manager.sites: # Only in case if site already started downloading return self.error404(path_parts["inner_path"]) From f630e6c25e6578e2390df2cc38cc6869796f89f8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 10 Jul 2017 02:42:41 +0200 Subject: [PATCH 0139/2570] Test raw access security --- src/Test/TestWeb.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Test/TestWeb.py b/src/Test/TestWeb.py index 8cbce1cf..a6ad726c 100644 --- a/src/Test/TestWeb.py +++ b/src/Test/TestWeb.py @@ -30,11 +30,20 @@ def wget(url): @pytest.mark.webtest class TestWeb: def testFileSecurity(self, site_url): + assert "Not Found" in wget("%s/media/sites.json" % site_url) assert "Not Found" in wget("%s/media/./sites.json" % site_url) assert "Forbidden" in wget("%s/media/../config.py" % site_url) assert "Forbidden" in wget("%s/media/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr/../sites.json" % site_url) assert "Forbidden" in wget("%s/media/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr/..//sites.json" % site_url) assert "Forbidden" in wget("%s/media/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr/../../zeronet.py" % site_url) + + assert "Not Found" in wget("%s/raw/sites.json" % site_url) + assert "Forbidden" in wget("%s/raw/./sites.json" % site_url) + assert "Forbidden" in wget("%s/raw/../config.py" % site_url) + assert "Forbidden" in wget("%s/raw/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr/../sites.json" % site_url) + assert "Forbidden" in wget("%s/raw/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr/..//sites.json" % site_url) + assert "Forbidden" in wget("%s/raw/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr/../../zeronet.py" % site_url) + assert "Forbidden" in wget("%s/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr/../sites.json" % site_url) assert "Forbidden" in wget("%s/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr/..//sites.json" % site_url) assert "Forbidden" in wget("%s/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr/../../zeronet.py" % site_url) From 12ca870e38c0f01479c065ae32251c514a86f45f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 10 Jul 2017 02:42:50 +0200 Subject: [PATCH 0140/2570] Rev2141 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 1659b8dc..d23da438 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.6" - self.rev = 2137 + self.rev = 2141 self.argv = argv self.action = None self.config_file = "zeronet.conf" From c8f37674c6f3c324614e77f66227b65a36545f54 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 11 Jul 2017 21:03:24 +0200 Subject: [PATCH 0141/2570] Rev2142, Fix random wrong data dir path --- src/Config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index d23da438..1ee4a35e 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.6" - self.rev = 2141 + self.rev = 2142 self.argv = argv self.action = None self.config_file = "zeronet.conf" @@ -61,7 +61,7 @@ class Config(object): else: fix_float_decimals = False - this_file = os.path.abspath(__file__).replace("\\", "/") + this_file = os.path.abspath(__file__).replace("\\", "/").replace("Config.pyc", "Config.py") if this_file.endswith("/Contents/Resources/core/src/Config.py"): # Running as ZeroNet.app From e525ea2431ab25bcb251ce835133732e19e13242 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 11 Jul 2017 23:00:33 +0200 Subject: [PATCH 0142/2570] Rev2144, Fix CSP header in FilePack plugin, Allow media-src and font-src from self source --- plugins/FilePack/FilePackPlugin.py | 2 +- src/Config.py | 2 +- src/Ui/UiRequest.py | 10 ++++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/plugins/FilePack/FilePackPlugin.py b/plugins/FilePack/FilePackPlugin.py index 0d413b60..8db8429e 100644 --- a/plugins/FilePack/FilePackPlugin.py +++ b/plugins/FilePack/FilePackPlugin.py @@ -57,7 +57,7 @@ class UiRequestPlugin(object): try: file = openArchive(archive_path, path_within) content_type = self.getContentType(file_path) - self.sendHeader(200, content_type=content_type) + self.sendHeader(200, content_type=content_type, noscript=kwargs.get("header_noscript", False)) return self.streamFile(file) except Exception, err: self.log.debug("Error opening archive file: %s" % err) diff --git a/src/Config.py b/src/Config.py index 1ee4a35e..8c1159c6 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.6" - self.rev = 2142 + self.rev = 2144 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 547bc3e3..6edf82e7 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -184,7 +184,7 @@ class UiRequest(object): return referer # Send response headers - def sendHeader(self, status=200, content_type="text/html", extra_headers=[]): + def sendHeader(self, status=200, content_type="text/html", noscript=False, extra_headers=[]): headers = [] headers.append(("Version", "HTTP/1.1")) headers.append(("Connection", "Keep-Alive")) @@ -192,6 +192,10 @@ class UiRequest(object): headers.append(("X-Frame-Options", "SAMEORIGIN")) if content_type != "text/html" and self.env.get("HTTP_REFERER") and self.isSameOrigin(self.getReferer(), self.getRequestUrl()): headers.append(("Access-Control-Allow-Origin", "*")) # Allow load font files from css + + if noscript: + headers.append(("Content-Security-Policy", "default-src 'none'; sandbox allow-top-navigation; img-src 'self'; font-src 'self'; media-src 'self'; style-src 'self' 'unsafe-inline';")) + if self.env["REQUEST_METHOD"] == "OPTIONS": # Allow json access headers.append(("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Cookie")) @@ -498,8 +502,6 @@ class UiRequest(object): extra_headers["Accept-Ranges"] = "bytes" if header_length: extra_headers["Content-Length"] = str(file_size) - if header_noscript: - extra_headers["Content-Security-Policy"] = "default-src 'none'; sandbox allow-top-navigation; img-src 'self'; style-src 'self' 'unsafe-inline';" if range: range_start = int(re.match(".*?([0-9]+)", range).group(1)) if re.match(".*?-([0-9]+)", range): @@ -512,7 +514,7 @@ class UiRequest(object): status = 206 else: status = 200 - self.sendHeader(status, content_type=content_type, extra_headers=extra_headers.items()) + self.sendHeader(status, content_type=content_type, noscript=header_noscript, extra_headers=extra_headers.items()) if self.env["REQUEST_METHOD"] != "OPTIONS": file = open(file_path, "rb") if range_start: From 2777c4c5370963ad89221d12eaf53a9d5b28f0ce Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 12 Jul 2017 12:28:03 +0200 Subject: [PATCH 0143/2570] Read max 6MB from archive to protect against tar/zipbombs --- plugins/FilePack/FilePackPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/FilePack/FilePackPlugin.py b/plugins/FilePack/FilePackPlugin.py index 8db8429e..29cd7390 100644 --- a/plugins/FilePack/FilePackPlugin.py +++ b/plugins/FilePack/FilePackPlugin.py @@ -66,7 +66,7 @@ class UiRequestPlugin(object): return super(UiRequestPlugin, self).actionSiteMedia(path, **kwargs) def streamFile(self, file): - while 1: + for i in range(100): # Read max 6MB try: block = file.read(60 * 1024) if block: From 3cd7e4e48e024bd4e860fe9e9973ca6831657884 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 12 Jul 2017 12:28:21 +0200 Subject: [PATCH 0144/2570] Better way to strip pyc/pyd from config file path --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 8c1159c6..44308721 100644 --- a/src/Config.py +++ b/src/Config.py @@ -61,7 +61,7 @@ class Config(object): else: fix_float_decimals = False - this_file = os.path.abspath(__file__).replace("\\", "/").replace("Config.pyc", "Config.py") + this_file = os.path.abspath(__file__).replace("\\", "/").rstrip("cd") if this_file.endswith("/Contents/Resources/core/src/Config.py"): # Running as ZeroNet.app From 6bf3d34c6cbf41961a1435f2e3de759820f7b768 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 12 Jul 2017 12:28:28 +0200 Subject: [PATCH 0145/2570] Rev2145 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 44308721..62afab4b 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.6" - self.rev = 2144 + self.rev = 2145 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 1bb3140f5b8934a5d0bda058e5dec3358ab3f1b3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Jul 2017 14:58:16 +0200 Subject: [PATCH 0146/2570] Move file hashing to separate funcion to allow easier extension --- src/Content/ContentManager.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 2b41bbed..3423a5e3 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -464,6 +464,19 @@ class ContentManager(object): self.site.storage.delete(file_inner_path + "-old") return diffs + def hashFile(self, dir_inner_path, file_relative_path, optional=False): + back = {} + file_inner_path = dir_inner_path + "/" + file_relative_path + + file_path = self.site.storage.getPath(file_inner_path) + file_size = os.path.getsize(file_path) + sha512sum = CryptHash.sha512sum(file_path) # Calculate sha512 sum of file + if optional and not self.hashfield.hasHash(sha512sum): + self.optionalDownloaded(file_inner_path, sha512sum, file_size, own=True) + + back[file_relative_path] = {"sha512": sha512sum, "size": os.path.getsize(file_path)} + return back + # Hash files in directory def hashFiles(self, dir_inner_path, ignore_pattern=None, optional_pattern=None): files_node = {} @@ -491,18 +504,16 @@ class ContentManager(object): if ignored: # Ignore content.json, defined regexp and files starting with . self.log.info("- [SKIPPED] %s" % file_relative_path) else: - file_inner_path = dir_inner_path + "/" + file_relative_path - file_path = self.site.storage.getPath(file_inner_path) - sha512sum = CryptHash.sha512sum(file_path) # Calculate sha512 sum of file if optional: - self.log.info("- [OPTIONAL] %s (SHA512: %s)" % (file_relative_path, sha512sum)) - file_size = os.path.getsize(file_path) - files_optional_node[file_relative_path] = {"sha512": sha512sum, "size": file_size} - if not self.hashfield.hasHash(sha512sum): - self.optionalDownloaded(file_inner_path, sha512sum, file_size, own=True) + self.log.info("- [OPTIONAL] %s" % file_relative_path) + files_optional_node.update( + self.hashFile(dir_inner_path, file_relative_path, optional=True) + ) else: - self.log.info("- %s (SHA512: %s)" % (file_relative_path, sha512sum)) - files_node[file_relative_path] = {"sha512": sha512sum, "size": os.path.getsize(file_path)} + self.log.info("- %s" % file_relative_path) + files_node.update( + self.hashFile(dir_inner_path, file_relative_path) + ) return files_node, files_optional_node # Create and sign a content.json From 96a097e33df3d24aa261bd41cf4f3a8ec5cf95ae Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Jul 2017 14:58:52 +0200 Subject: [PATCH 0147/2570] Separate isValidRelativePath function --- src/Content/ContentManager.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 3423a5e3..b826ab92 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -477,11 +477,19 @@ class ContentManager(object): back[file_relative_path] = {"sha512": sha512sum, "size": os.path.getsize(file_path)} return back + def isValidRelativePath(self, relative_path): + if ".." in relative_path: + return False + elif len(relative_path) > 255: + return False + else: + return re.match("^[a-z\[\]\(\) A-Z0-9_@=\.\+-/]*$", relative_path) + # Hash files in directory def hashFiles(self, dir_inner_path, ignore_pattern=None, optional_pattern=None): files_node = {} files_optional_node = {} - if not re.match("^[a-zA-Z0-9_@=\.\+-/]*$", dir_inner_path): + if not isValidRelativePath(dir_inner_path): ignored = True self.log.error("- [ERROR] Only ascii encoded directories allowed: %s" % dir_inner_path) @@ -495,9 +503,9 @@ class ContentManager(object): ignored = True elif file_name.startswith(".") or file_name.endswith("-old") or file_name.endswith("-new"): ignored = True - elif not re.match("^[a-zA-Z0-9_@=\.\+\-/]+$", file_relative_path): + elif not self.isValidRelativePath(file_relative_path): ignored = True - self.log.error("- [ERROR] Only ascii encoded filenames allowed: %s" % file_relative_path) + self.log.error("- [ERROR] Invalid filename: %s" % file_relative_path) elif optional_pattern and re.match(optional_pattern, file_relative_path): optional = True From 7d3beeb9e09a73794f2b498445d1fee22974c01c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Jul 2017 14:59:17 +0200 Subject: [PATCH 0148/2570] Enforce valid relative paths on verification --- src/Content/ContentManager.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index b826ab92..6bfb7fda 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -753,6 +753,10 @@ class ContentManager(object): content_size_optional, rules["max_size_optional"]) ) + for file_relative_path in content.get("files", {}).keys() + content.get("files_optional", {}).keys(): + if not self.isValidRelativePath(file_relative_path): + raise VerifyError("Invalid relative path: %s" % file_relative_path) + # Filename limit if rules.get("files_allowed"): for file_inner_path in content["files"].keys(): From 096675c87eaf03a904eed8cdfbc0390908c13187 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Jul 2017 15:00:04 +0200 Subject: [PATCH 0149/2570] Add unit to verification error --- src/Content/ContentManager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 6bfb7fda..acbdff2f 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -730,7 +730,7 @@ class ContentManager(object): task = self.site.worker_manager.findTask(inner_path) if task: # Dont try to download from other peers self.site.worker_manager.failTask(task) - raise VerifyError("Site too large %s > %s, aborting task..." % (site_size, site_size_limit)) + raise VerifyError("Site too large %sB > %sB, aborting task..." % (site_size, site_size_limit)) if inner_path == "content.json": self.site.settings["size"] = site_size @@ -745,11 +745,11 @@ class ContentManager(object): # Check include size limit if rules.get("max_size") is not None: # Include size limit if content_size > rules["max_size"]: - raise VerifyError("Include too large %s > %s" % (content_size, rules["max_size"])) + raise VerifyError("Include too large %sB > %sB" % (content_size, rules["max_size"])) if rules.get("max_size_optional") is not None: # Include optional files limit if content_size_optional > rules["max_size_optional"]: - raise VerifyError("Include optional files too large %s > %s" % ( + raise VerifyError("Include optional files too large %sB > %sB" % ( content_size_optional, rules["max_size_optional"]) ) From 87910a236bb15f26d48398bacb71841146db8a69 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Jul 2017 15:00:23 +0200 Subject: [PATCH 0150/2570] Rev2147 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 62afab4b..12a32445 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.6" - self.rev = 2145 + self.rev = 2147 self.argv = argv self.action = None self.config_file = "zeronet.conf" From dd11f876737f84c64b836580f494b05ec208b132 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Jul 2017 15:03:04 +0200 Subject: [PATCH 0151/2570] Rev2148, Fix signing typo --- src/Config.py | 2 +- src/Content/ContentManager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index 12a32445..478a4abb 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.6" - self.rev = 2147 + self.rev = 2148 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index acbdff2f..14f435d0 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -489,7 +489,7 @@ class ContentManager(object): def hashFiles(self, dir_inner_path, ignore_pattern=None, optional_pattern=None): files_node = {} files_optional_node = {} - if not isValidRelativePath(dir_inner_path): + if not self.isValidRelativePath(dir_inner_path): ignored = True self.log.error("- [ERROR] Only ascii encoded directories allowed: %s" % dir_inner_path) From 13157eea1e87622c3ce299eab70deee4a82b6912 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Jul 2017 19:32:54 +0200 Subject: [PATCH 0152/2570] Move included content verification to separate function, fix root content.json file path verification --- src/Content/ContentManager.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 14f435d0..4ec9dd40 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -732,11 +732,24 @@ class ContentManager(object): self.site.worker_manager.failTask(task) raise VerifyError("Site too large %sB > %sB, aborting task..." % (site_size, site_size_limit)) + # Verify valid filenames + for file_relative_path in content.get("files", {}).keys() + content.get("files_optional", {}).keys(): + if not self.isValidRelativePath(file_relative_path): + raise VerifyError("Invalid relative path: %s" % file_relative_path) + if inner_path == "content.json": self.site.settings["size"] = site_size self.site.settings["size_optional"] = site_size_optional return True # Root content.json is passed + else: + if self.verifyContentInclude(inner_path, content, content_size, content_size_optional): + self.site.settings["size"] = site_size + self.site.settings["size_optional"] = site_size_optional + return True + else: + return False + def verifyContentInclude(self, inner_path, content, content_size, content_size_optional): # Load include details rules = self.getRules(inner_path, content) if not rules: @@ -753,10 +766,6 @@ class ContentManager(object): content_size_optional, rules["max_size_optional"]) ) - for file_relative_path in content.get("files", {}).keys() + content.get("files_optional", {}).keys(): - if not self.isValidRelativePath(file_relative_path): - raise VerifyError("Invalid relative path: %s" % file_relative_path) - # Filename limit if rules.get("files_allowed"): for file_inner_path in content["files"].keys(): @@ -772,9 +781,6 @@ class ContentManager(object): if rules.get("includes_allowed") is False and content.get("includes"): raise VerifyError("Includes not allowed") - self.site.settings["size"] = site_size - self.site.settings["size_optional"] = site_size_optional - return True # All good # Verify file validity From b60a1ec45544fc058584e04af303bbe96acb988d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Jul 2017 19:33:07 +0200 Subject: [PATCH 0153/2570] Test invalid filenames --- src/Test/TestContent.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/Test/TestContent.py b/src/Test/TestContent.py index bbe30097..1451e77f 100644 --- a/src/Test/TestContent.py +++ b/src/Test/TestContent.py @@ -188,3 +188,35 @@ class TestContent: } data = StringIO(json.dumps(data_dict)) assert site.content_manager.verifyFile(inner_path, data, ignore_same=False) + + def testVerifyInnerPath(self, site): + privatekey = "5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv" + inner_path = "content.json" + data_dict = site.storage.loadJson(inner_path) + + for good_relative_path in ["data.json", "out/data.json", "Any File [by none] (1).jpg"]: + data_dict["files"] = {good_relative_path: {"sha512": "369d4e780cc80504285f13774ca327fe725eed2d813aad229e62356b07365906", "size": 505}} + + if "sign" in data_dict: + del data_dict["sign"] + del data_dict["signs"] + data_dict["signs"] = { + "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), privatekey) + } + data = StringIO(json.dumps(data_dict)) + assert site.content_manager.verifyFile(inner_path, data, ignore_same=False) + + for bad_relative_path in ["../data.json", "data/" * 100, "invalid|file.jpg"]: + data_dict["files"] = {bad_relative_path: {"sha512": "369d4e780cc80504285f13774ca327fe725eed2d813aad229e62356b07365906", "size": 505}} + + if "sign" in data_dict: + del data_dict["sign"] + del data_dict["signs"] + data_dict["signs"] = { + "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), privatekey) + } + data = StringIO(json.dumps(data_dict)) + with pytest.raises(VerifyError) as err: + site.content_manager.verifyFile(inner_path, data, ignore_same=False) + assert "Invalid relative path" in str(err) + From 3f5a5b4f9bf362faff16f3bd6cf9f2258d5a41aa Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Jul 2017 19:33:16 +0200 Subject: [PATCH 0154/2570] Rev2151 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 478a4abb..9279f5ae 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.6" - self.rev = 2148 + self.rev = 2151 self.argv = argv self.action = None self.config_file = "zeronet.conf" From bf41c7b65100182b19b61a909da8381d4e52110f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 14 Jul 2017 10:31:42 +0200 Subject: [PATCH 0155/2570] Detect potentionally unsafe regex patterns --- src/util/SafeRe.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/util/SafeRe.py diff --git a/src/util/SafeRe.py b/src/util/SafeRe.py new file mode 100644 index 00000000..d5ddf3b6 --- /dev/null +++ b/src/util/SafeRe.py @@ -0,0 +1,20 @@ +import re + + +class UnsafePatternError(Exception): + pass + + +def isSafePattern(pattern): + if len(pattern) > 255: + raise UnsafePatternError("Pattern too long: %s characters" % len(pattern)) + + unsafe_pattern_match = re.search("[^\.][\*\{\+]", pattern) # Always should be "." before "*{+" characters to avoid ReDoS + if unsafe_pattern_match: + raise UnsafePatternError("Potentially unsafe part of the pattern: %s" % unsafe_pattern_match.group(0)) + return True + + +def match(pattern, *args, **kwargs): + if isSafePattern(pattern): + return re.match(pattern, *args, **kwargs) From c069d4f67c9f8f668ed6076af5f6bc18743e5bf8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 14 Jul 2017 10:34:18 +0200 Subject: [PATCH 0156/2570] Use SafeRe to match user defined patterns in content.json --- src/Content/ContentManager.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 4ec9dd40..e2347e32 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -11,6 +11,7 @@ from Crypt import CryptHash from Config import config from util import helper from util import Diff +from util import SafeRe from Peer import PeerHashfield from ContentDbDict import ContentDbDict @@ -408,7 +409,7 @@ class ContentManager(object): if "signers" in rules: rules["signers"] = rules["signers"][:] # Make copy of the signers for permission_pattern, permission_rules in user_contents["permission_rules"].items(): # Regexp rules - if not re.match(permission_pattern, user_urn): + if not SafeRe.match(permission_pattern, user_urn): continue # Rule is not valid for user # Update rules if its better than current recorded ones for key, val in permission_rules.iteritems(): @@ -483,13 +484,13 @@ class ContentManager(object): elif len(relative_path) > 255: return False else: - return re.match("^[a-z\[\]\(\) A-Z0-9_@=\.\+-/]*$", relative_path) + return re.match("^[a-z\[\]\(\) A-Z0-9_@=\.\+-/]+$", relative_path) # Hash files in directory def hashFiles(self, dir_inner_path, ignore_pattern=None, optional_pattern=None): files_node = {} files_optional_node = {} - if not self.isValidRelativePath(dir_inner_path): + if dir_inner_path and not self.isValidRelativePath(dir_inner_path): ignored = True self.log.error("- [ERROR] Only ascii encoded directories allowed: %s" % dir_inner_path) @@ -499,14 +500,14 @@ class ContentManager(object): ignored = optional = False if file_name == "content.json": ignored = True - elif ignore_pattern and re.match(ignore_pattern, file_relative_path): + elif ignore_pattern and SafeRe.match(ignore_pattern, file_relative_path): ignored = True elif file_name.startswith(".") or file_name.endswith("-old") or file_name.endswith("-new"): ignored = True elif not self.isValidRelativePath(file_relative_path): ignored = True self.log.error("- [ERROR] Invalid filename: %s" % file_relative_path) - elif optional_pattern and re.match(optional_pattern, file_relative_path): + elif optional_pattern and SafeRe.match(optional_pattern, file_relative_path): optional = True if ignored: # Ignore content.json, defined regexp and files starting with . @@ -769,12 +770,12 @@ class ContentManager(object): # Filename limit if rules.get("files_allowed"): for file_inner_path in content["files"].keys(): - if not re.match("^%s$" % rules["files_allowed"], file_inner_path): + if not SafeRe.match("^%s$" % rules["files_allowed"], file_inner_path): raise VerifyError("File not allowed: %s" % file_inner_path) if rules.get("files_allowed_optional"): for file_inner_path in content.get("files_optional", {}).keys(): - if not re.match("^%s$" % rules["files_allowed_optional"], file_inner_path): + if not SafeRe.match("^%s$" % rules["files_allowed_optional"], file_inner_path): raise VerifyError("Optional file not allowed: %s" % file_inner_path) # Check if content includes allowed From 74763465a8c1862315bff916e1872f080b74bc97 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 14 Jul 2017 10:34:57 +0200 Subject: [PATCH 0157/2570] Use SafeRe to match file patterns in dbschema.json --- src/Db/Db.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Db/Db.py b/src/Db/Db.py index df73bde7..c2de5509 100644 --- a/src/Db/Db.py +++ b/src/Db/Db.py @@ -8,6 +8,7 @@ import gevent from DbCursor import DbCursor from Config import config +from util import SafeRe opened_dbs = [] @@ -230,8 +231,11 @@ class Db(object): # Check if filename matches any of mappings in schema matched_maps = [] for match, map_settings in self.schema["maps"].items(): - if re.match(match, relative_path): - matched_maps.append(map_settings) + try: + if SafeRe.match(match, relative_path): + matched_maps.append(map_settings) + except SafeRe.UnsafePatternError as err: + self.log.error(err) # No match found for the file if not matched_maps: From 492408def7a76bb5fcf9eafc5338f69743226c5b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 14 Jul 2017 10:35:44 +0200 Subject: [PATCH 0158/2570] Use class variable to store test site privatekey --- src/Test/TestContent.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/Test/TestContent.py b/src/Test/TestContent.py index 1451e77f..0f2575ac 100644 --- a/src/Test/TestContent.py +++ b/src/Test/TestContent.py @@ -10,6 +10,8 @@ from Content.ContentManager import VerifyError, SignError @pytest.mark.usefixtures("resetSettings") class TestContent: + privatekey = "5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv" + def testInclude(self, site): # Rules defined in parent content.json rules = site.content_manager.getRules("data/test_include/content.json") @@ -36,7 +38,6 @@ class TestContent: assert site.content_manager.getValidSigners("content.json") == ["1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT"] def testInlcudeLimits(self, site): - privatekey = "5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv" # Data validation data_dict = { "files": { @@ -49,7 +50,7 @@ class TestContent: } # Normal data - data_dict["signs"] = {"1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict), privatekey)} + data_dict["signs"] = {"1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict), self.privatekey)} data = StringIO(json.dumps(data_dict)) assert site.content_manager.verifyFile("data/test_include/content.json", data, ignore_same=False) # Reset @@ -57,7 +58,7 @@ class TestContent: # Too large data_dict["files"]["data.json"]["size"] = 200000 # Emulate 2MB sized data.json - data_dict["signs"] = {"1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict), privatekey)} + data_dict["signs"] = {"1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict), self.privatekey)} data = StringIO(json.dumps(data_dict)) with pytest.raises(VerifyError) as err: site.content_manager.verifyFile("data/test_include/content.json", data, ignore_same=False) @@ -69,7 +70,7 @@ class TestContent: # Not allowed file data_dict["files"]["notallowed.exe"] = data_dict["files"]["data.json"] - data_dict["signs"] = {"1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict), privatekey)} + data_dict["signs"] = {"1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict), self.privatekey)} data = StringIO(json.dumps(data_dict)) with pytest.raises(VerifyError) as err: site.content_manager.verifyFile("data/test_include/content.json", data, ignore_same=False) @@ -80,7 +81,7 @@ class TestContent: del data_dict["signs"] # Should work again - data_dict["signs"] = {"1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict), privatekey)} + data_dict["signs"] = {"1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict), self.privatekey)} data = StringIO(json.dumps(data_dict)) assert site.content_manager.verifyFile("data/test_include/content.json", data, ignore_same=False) @@ -92,7 +93,7 @@ class TestContent: assert "Private key invalid" in str(err) # Good privatekey - content = site.content_manager.sign(inner_path, privatekey="5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv", filewrite=False) + content = site.content_manager.sign(inner_path, privatekey=self.privatekey, filewrite=False) content_old = site.content_manager.contents[inner_path] # Content before the sign assert not content_old == content # Timestamp changed assert site.address in content["signs"] # Used the site's private key to sign @@ -117,10 +118,10 @@ class TestContent: assert len(site.content_manager.hashfield) == 0 site.content_manager.contents["content.json"]["optional"] = "((data/img/zero.*))" - content_optional = site.content_manager.sign(privatekey="5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv", filewrite=False, remove_missing_optional=True) + content_optional = site.content_manager.sign(privatekey=self.privatekey, filewrite=False, remove_missing_optional=True) del site.content_manager.contents["content.json"]["optional"] - content_nooptional = site.content_manager.sign(privatekey="5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv", filewrite=False, remove_missing_optional=True) + content_nooptional = site.content_manager.sign(privatekey=self.privatekey, filewrite=False, remove_missing_optional=True) assert len(content_nooptional.get("files_optional", {})) == 0 # No optional files if no pattern assert len(content_optional["files_optional"]) > 0 @@ -145,14 +146,13 @@ class TestContent: assert file_info_optional["optional"] is True def testVerify(self, site): - privatekey = "5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv" inner_path = "data/test_include/content.json" data_dict = site.storage.loadJson(inner_path) data = StringIO(json.dumps(data_dict)) # Re-sign data_dict["signs"] = { - "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), privatekey) + "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), self.privatekey) } assert site.content_manager.verifyFile(inner_path, data, ignore_same=False) @@ -160,7 +160,7 @@ class TestContent: data_dict["address"] = "Othersite" del data_dict["signs"] data_dict["signs"] = { - "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), privatekey) + "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), self.privatekey) } data = StringIO(json.dumps(data_dict)) with pytest.raises(VerifyError) as err: @@ -172,7 +172,7 @@ class TestContent: data_dict["inner_path"] = "content.json" del data_dict["signs"] data_dict["signs"] = { - "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), privatekey) + "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), self.privatekey) } data = StringIO(json.dumps(data_dict)) with pytest.raises(VerifyError) as err: @@ -184,13 +184,12 @@ class TestContent: data_dict["inner_path"] = inner_path del data_dict["signs"] data_dict["signs"] = { - "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), privatekey) + "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), self.privatekey) } data = StringIO(json.dumps(data_dict)) assert site.content_manager.verifyFile(inner_path, data, ignore_same=False) def testVerifyInnerPath(self, site): - privatekey = "5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv" inner_path = "content.json" data_dict = site.storage.loadJson(inner_path) @@ -201,7 +200,7 @@ class TestContent: del data_dict["sign"] del data_dict["signs"] data_dict["signs"] = { - "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), privatekey) + "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), self.privatekey) } data = StringIO(json.dumps(data_dict)) assert site.content_manager.verifyFile(inner_path, data, ignore_same=False) @@ -213,7 +212,7 @@ class TestContent: del data_dict["sign"] del data_dict["signs"] data_dict["signs"] = { - "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), privatekey) + "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), self.privatekey) } data = StringIO(json.dumps(data_dict)) with pytest.raises(VerifyError) as err: From 1f4a5643dbe024831d0f7ccd2706716b9b505fdb Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 14 Jul 2017 10:36:18 +0200 Subject: [PATCH 0159/2570] Test unsafe regex patterns on sign and verify --- src/Test/TestContent.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/Test/TestContent.py b/src/Test/TestContent.py index 0f2575ac..7d0e0f63 100644 --- a/src/Test/TestContent.py +++ b/src/Test/TestContent.py @@ -6,6 +6,7 @@ import pytest from Crypt import CryptBitcoin from Content.ContentManager import VerifyError, SignError +from util.SafeRe import UnsafePatternError @pytest.mark.usefixtures("resetSettings") @@ -219,3 +220,23 @@ class TestContent: site.content_manager.verifyFile(inner_path, data, ignore_same=False) assert "Invalid relative path" in str(err) + @pytest.mark.parametrize("key", ["ignore", "optional"]) + def testSignUnsafePattern(self, site, key): + site.content_manager.contents["content.json"][key] = "([a-zA-Z]+)*" + with pytest.raises(UnsafePatternError) as err: + site.content_manager.sign("content.json", privatekey=self.privatekey, filewrite=False) + assert "Potentially unsafe" in str(err) + + + def testVerifyUnsafePattern(self, site): + site.content_manager.contents["content.json"]["includes"]["data/test_include/content.json"]["files_allowed"] = "([a-zA-Z]+)*" + with pytest.raises(UnsafePatternError) as err: + data = site.storage.open("data/test_include/content.json") + site.content_manager.verifyFile("data/test_include/content.json", data, ignore_same=False) + assert "Potentially unsafe" in str(err) + + site.content_manager.contents["data/users/content.json"]["user_contents"]["permission_rules"]["([a-zA-Z]+)*"] = {"max_size": 0} + with pytest.raises(UnsafePatternError) as err: + data = site.storage.open("data/users/1C5sgvWaSgfaTpV5kjBCnCiKtENNMYo69q/content.json") + site.content_manager.verifyFile("data/users/1C5sgvWaSgfaTpV5kjBCnCiKtENNMYo69q/content.json", data, ignore_same=False) + assert "Potentially unsafe" in str(err) From 699a8be7216830aa816a2651b753effea4be5ea1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 14 Jul 2017 10:36:41 +0200 Subject: [PATCH 0160/2570] Test unsafe patterns in dbschema --- src/Test/TestDb.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/Test/TestDb.py b/src/Test/TestDb.py index 9858563d..121c41aa 100644 --- a/src/Test/TestDb.py +++ b/src/Test/TestDb.py @@ -1,9 +1,5 @@ -import os import cStringIO as StringIO -from Config import config -from Db import Db - class TestDb: def testCheckTables(self, db): @@ -69,6 +65,21 @@ class TestDb: } """) f.seek(0) - assert db.updateJson(db.db_dir + "data.json", f) == True + assert db.updateJson(db.db_dir + "data.json", f) is True assert db.execute("SELECT COUNT(*) AS num FROM test_importfilter").fetchone()["num"] == 1 assert db.execute("SELECT COUNT(*) AS num FROM test").fetchone()["num"] == 1 + + def testUnsafePattern(self, db): + db.schema["maps"] = {"[A-Za-z.]*": db.schema["maps"]["data.json"]} # Only repetition of . supported + f = StringIO.StringIO() + f.write(""" + { + "test": [ + {"test_id": 1, "title": "Test 1 title", "extra col": "Ignore it"} + ] + } + """) + f.seek(0) + assert db.updateJson(db.db_dir + "data.json", f) is False + assert db.execute("SELECT COUNT(*) AS num FROM test_importfilter").fetchone()["num"] == 0 + assert db.execute("SELECT COUNT(*) AS num FROM test").fetchone()["num"] == 0 From 3459d35ed24fe0d0d867abc50c9ef355d7253661 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 14 Jul 2017 10:37:09 +0200 Subject: [PATCH 0161/2570] Test unsafe regex pattern recognization --- src/Test/TestSafeRe.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/Test/TestSafeRe.py diff --git a/src/Test/TestSafeRe.py b/src/Test/TestSafeRe.py new file mode 100644 index 00000000..670bb764 --- /dev/null +++ b/src/Test/TestSafeRe.py @@ -0,0 +1,18 @@ +from util import SafeRe + +import pytest + + +class TestSafeRe: + def testSafeMatch(self): + assert SafeRe.match( + "((js|css)/(?!all.(js|css))|data/users/.*db|data/users/.*/.*|data/archived|.*.py)", + "js/ZeroTalk.coffee" + ) + assert SafeRe.match(".+/data.json", "data/users/1J3rJ8ecnwH2EPYa6MrgZttBNc61ACFiCj/data.json") + + @pytest.mark.parametrize("pattern", ["([a-zA-Z]+)*", "(a|aa)+*", "(a|a?)+", "(.*a){10}"]) + def testUnsafeMatch(self, pattern): + with pytest.raises(SafeRe.UnsafePatternError) as err: + SafeRe.match(pattern, "aaaaaaaaaaaaaaaaaaaaaaaa!") + assert "Potentially unsafe" in str(err) From 5a42cb92cdd288654b24f5741f39d68ed1ed2263 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 14 Jul 2017 10:37:19 +0200 Subject: [PATCH 0162/2570] Rev2153 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 9279f5ae..5fdf9f87 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.6" - self.rev = 2151 + self.rev = 2153 self.argv = argv self.action = None self.config_file = "zeronet.conf" From d281f112d9f2ad19cb7369f5689439a45c1f27bb Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 14 Jul 2017 11:08:22 +0200 Subject: [PATCH 0163/2570] Rev2154, Fix same origin checking in proxy mode --- src/Config.py | 2 +- src/Ui/UiRequest.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Config.py b/src/Config.py index 5fdf9f87..8f615d5f 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.6" - self.rev = 2153 + self.rev = 2154 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 6edf82e7..d555d7ee 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -169,7 +169,7 @@ class UiRequest(object): def getRequestUrl(self): if self.isProxyRequest(): - if self.env["PATH_INFO"].startswith("http://zero"): + if self.env["PATH_INFO"].startswith("http://zero/"): return self.env["PATH_INFO"] else: # Add http://zero to direct domain access return self.env["PATH_INFO"].replace("http://", "http://zero/", 1) @@ -178,7 +178,7 @@ class UiRequest(object): def getReferer(self): referer = self.env.get("HTTP_REFERER") - if referer and self.isProxyRequest() and not referer.startswith("http://zero"): + if referer and self.isProxyRequest() and not referer.startswith("http://zero/"): return referer.replace("http://", "http://zero/", 1) else: return referer From 0e930efd95d6077e9cdaf32726dc64fe3e427910 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 15 Jul 2017 01:30:35 +0200 Subject: [PATCH 0164/2570] Cache SafeRe patterns --- src/util/SafeRe.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/util/SafeRe.py b/src/util/SafeRe.py index d5ddf3b6..4c104f4f 100644 --- a/src/util/SafeRe.py +++ b/src/util/SafeRe.py @@ -4,6 +4,8 @@ import re class UnsafePatternError(Exception): pass +cached_patterns = {} + def isSafePattern(pattern): if len(pattern) > 255: @@ -16,5 +18,10 @@ def isSafePattern(pattern): def match(pattern, *args, **kwargs): - if isSafePattern(pattern): - return re.match(pattern, *args, **kwargs) + 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) From ac1a03d17b87b7d5c66e2d7e957f226f8ea21c4e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 15 Jul 2017 01:30:53 +0200 Subject: [PATCH 0165/2570] Don't allow more than 10 repetitions in one pattern --- src/util/SafeRe.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/util/SafeRe.py b/src/util/SafeRe.py index 4c104f4f..1caa61a0 100644 --- a/src/util/SafeRe.py +++ b/src/util/SafeRe.py @@ -14,6 +14,11 @@ def isSafePattern(pattern): unsafe_pattern_match = re.search("[^\.][\*\{\+]", pattern) # Always should be "." before "*{+" characters to avoid ReDoS if unsafe_pattern_match: raise UnsafePatternError("Potentially unsafe part of the pattern: %s" % unsafe_pattern_match.group(0)) + + repetitions = re.findall("\.[\*\{\+]", pattern) + if len(repetitions) >= 10: + raise UnsafePatternError("More than 10 repetitions of %s" % repetitions[0]) + return True From 6a4882d81de9b5f2b96f775fab9d55d96da15813 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 15 Jul 2017 01:31:08 +0200 Subject: [PATCH 0166/2570] Test SafeRe repetition limit --- src/Test/TestSafeRe.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Test/TestSafeRe.py b/src/Test/TestSafeRe.py index 670bb764..111805fd 100644 --- a/src/Test/TestSafeRe.py +++ b/src/Test/TestSafeRe.py @@ -15,4 +15,10 @@ class TestSafeRe: def testUnsafeMatch(self, pattern): with pytest.raises(SafeRe.UnsafePatternError) as err: SafeRe.match(pattern, "aaaaaaaaaaaaaaaaaaaaaaaa!") - assert "Potentially unsafe" in str(err) + assert "Potentially unsafe" in str(err) + + @pytest.mark.parametrize("pattern", ["^(.*a)(.*a)(.*a)(.*a)(.*a)(.*a)(.*a)(.*a)(.*a)(.*a)(.*a)(.*a)(.*a)(.*a)(.*a)(.*a)(.*a)(.*a)(.*a)(.*a)(.*a)(.*a)(.*a)(.*a)$"]) + def testUnsafeRepetition(self, pattern): + with pytest.raises(SafeRe.UnsafePatternError) as err: + SafeRe.match(pattern, "aaaaaaaaaaaaaaaaaaaaaaaa!") + assert "More than" in str(err) From a0d85d7d8393805290a0dd203733c61471ea4a26 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 15 Jul 2017 01:32:15 +0200 Subject: [PATCH 0167/2570] Prompt new site addition in raw mode --- plugins/FilePack/FilePackPlugin.py | 2 +- src/Ui/UiRequest.py | 33 ++++++++++++++++++++++-- src/Ui/UiServer.py | 1 + src/Ui/template/site_add.html | 40 ++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 src/Ui/template/site_add.html diff --git a/plugins/FilePack/FilePackPlugin.py b/plugins/FilePack/FilePackPlugin.py index 29cd7390..7ba6b452 100644 --- a/plugins/FilePack/FilePackPlugin.py +++ b/plugins/FilePack/FilePackPlugin.py @@ -47,7 +47,7 @@ class UiRequestPlugin(object): if not os.path.isfile(archive_path): site = self.server.site_manager.get(path_parts["address"]) if not site: - self.error404(path) + return self.actionSiteAddPrompt(path) # Wait until file downloads result = site.needFile(site.storage.getInnerPath(archive_path), priority=10) # Send virutal file path download finished event to remove loading screen diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index d555d7ee..91ca261b 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -102,6 +102,9 @@ class UiRequest(object): # Wrapper-less static files elif path.startswith("/raw/"): return self.actionSiteMedia(path.replace("/raw", "/media", 1), header_noscript=True) + + elif path.startswith("/add/"): + return self.actionSiteAdd() # Site media wrapper else: if self.get.get("wrapper_nonce"): @@ -194,7 +197,7 @@ class UiRequest(object): headers.append(("Access-Control-Allow-Origin", "*")) # Allow load font files from css if noscript: - headers.append(("Content-Security-Policy", "default-src 'none'; sandbox allow-top-navigation; img-src 'self'; font-src 'self'; media-src 'self'; style-src 'self' 'unsafe-inline';")) + headers.append(("Content-Security-Policy", "default-src 'none'; sandbox allow-top-navigation allow-forms; img-src 'self'; font-src 'self'; media-src 'self'; style-src 'self' 'unsafe-inline';")) if self.env["REQUEST_METHOD"] == "OPTIONS": # Allow json access @@ -382,6 +385,12 @@ class UiRequest(object): self.server.wrapper_nonces.append(wrapper_nonce) return wrapper_nonce + # Create a new wrapper nonce that allows to get one site + def getAddNonce(self): + add_nonce = CryptHash.random() + self.server.add_nonces.append(add_nonce) + return add_nonce + def isSameOrigin(self, url_a, url_b): if not url_a or not url_b: return False @@ -448,7 +457,7 @@ class UiRequest(object): return self.actionRedirect("./%s/" % path_parts["address"]) else: # File not exists, try to download if address not in SiteManager.site_manager.sites: # Only in case if site already started downloading - return self.error404(path_parts["inner_path"]) + return self.actionSiteAddPrompt(path) site = SiteManager.site_manager.need(address) @@ -486,6 +495,26 @@ class UiRequest(object): else: # Bad url return self.error400() + def actionSiteAdd(self): + post = dict(cgi.parse_qsl(self.env["wsgi.input"].read())) + if post["add_nonce"] not in self.server.add_nonces: + return self.error403("Add nonce error.") + self.server.add_nonces.remove(post["add_nonce"]) + SiteManager.site_manager.need(post["address"]) + return self.actionRedirect(post["url"]) + + def actionSiteAddPrompt(self, path): + path_parts = self.parsePath(path) + if not path_parts or not self.server.site_manager.isAddress(path_parts["address"]): + return self.error404(path) + + self.sendHeader(200, "text/html", noscript=True) + template = open("src/Ui/template/site_add.html").read() + template = template.replace("{url}", cgi.escape(self.env["PATH_INFO"], True)) + template = template.replace("{address}", path_parts["address"]) + template = template.replace("{add_nonce}", self.getAddNonce()) + return template + # Stream a file to client def actionFile(self, file_path, block_size=64 * 1024, send_header=True, header_length=True, header_noscript=False): if ".." in file_path: diff --git a/src/Ui/UiServer.py b/src/Ui/UiServer.py index d623c2c3..926f1fea 100644 --- a/src/Ui/UiServer.py +++ b/src/Ui/UiServer.py @@ -69,6 +69,7 @@ class UiServer: self.learn_allowed_host = True # It will pin to the first http request's host self.wrapper_nonces = [] + self.add_nonces = [] self.site_manager = SiteManager.site_manager self.sites = SiteManager.site_manager.list() self.log = logging.getLogger(__name__) diff --git a/src/Ui/template/site_add.html b/src/Ui/template/site_add.html new file mode 100644 index 00000000..29481868 --- /dev/null +++ b/src/Ui/template/site_add.html @@ -0,0 +1,40 @@ + + +Add new site + + + + + +
+

Add new site

+

Please confirm before add new site to the client

+
+ + + + +
+
+ + + \ No newline at end of file From a6ce2a02531daff6d27b066cb9aa77923ddbb75e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 15 Jul 2017 01:32:39 +0200 Subject: [PATCH 0168/2570] Rev2156 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 8f615d5f..01cb978c 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.6" - self.rev = 2154 + self.rev = 2156 self.argv = argv self.action = None self.config_file = "zeronet.conf" From dc82f7ab3e387d4da622acdefaed4ed7696b8930 Mon Sep 17 00:00:00 2001 From: "Andrew (anoa)" Date: Sat, 15 Jul 2017 00:30:25 -0700 Subject: [PATCH 0169/2570] Small grammar fix --- src/Ui/template/site_add.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ui/template/site_add.html b/src/Ui/template/site_add.html index 29481868..605973bb 100644 --- a/src/Ui/template/site_add.html +++ b/src/Ui/template/site_add.html @@ -27,7 +27,7 @@

Add new site

-

Please confirm before add new site to the client

+

Please confirm before adding a new site to the client

@@ -37,4 +37,4 @@
- \ No newline at end of file + From b9e0275417a396bb86c9b6fdab4e87461575ccd2 Mon Sep 17 00:00:00 2001 From: cclauss Date: Sun, 16 Jul 2017 22:35:14 +0200 Subject: [PATCH 0170/2570] Update __init__.py --- plugins/disabled-StemPort/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/disabled-StemPort/__init__.py b/plugins/disabled-StemPort/__init__.py index 1ce4d973..71150ad6 100644 --- a/plugins/disabled-StemPort/__init__.py +++ b/plugins/disabled-StemPort/__init__.py @@ -1,7 +1,7 @@ try: from stem.control import Controller stem_found = True -except Exception, err: +except Exception as err: print "STEM NOT FOUND! %s" % err stem_found = False From c44cb11800916f2e0f986947ad782fb171351385 Mon Sep 17 00:00:00 2001 From: cclauss Date: Sun, 16 Jul 2017 22:38:47 +0200 Subject: [PATCH 0171/2570] Update CryptMessagePlugin.py --- plugins/CryptMessage/CryptMessagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/CryptMessage/CryptMessagePlugin.py b/plugins/CryptMessage/CryptMessagePlugin.py index 0302c83a..71499eca 100644 --- a/plugins/CryptMessage/CryptMessagePlugin.py +++ b/plugins/CryptMessage/CryptMessagePlugin.py @@ -53,7 +53,7 @@ class UiWebsocketPlugin(object): try: text = self.decrypt(encrypted_text.decode("base64"), privatekey) texts.append(text) - except Exception, err: + except Exception as err: texts.append(None) if type(param) == list: From b82cf8d08d9c1b4c8bc2f6d487a715f1c6d4c5a6 Mon Sep 17 00:00:00 2001 From: cclauss Date: Sun, 16 Jul 2017 22:39:42 +0200 Subject: [PATCH 0172/2570] Update FilePackPlugin.py --- plugins/FilePack/FilePackPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/FilePack/FilePackPlugin.py b/plugins/FilePack/FilePackPlugin.py index 7ba6b452..08725960 100644 --- a/plugins/FilePack/FilePackPlugin.py +++ b/plugins/FilePack/FilePackPlugin.py @@ -59,7 +59,7 @@ class UiRequestPlugin(object): content_type = self.getContentType(file_path) self.sendHeader(200, content_type=content_type, noscript=kwargs.get("header_noscript", False)) return self.streamFile(file) - except Exception, err: + except Exception as err: self.log.debug("Error opening archive file: %s" % err) return self.error404(path) From 688c240b3554e95e8e2b206276b023d362d8797d Mon Sep 17 00:00:00 2001 From: cclauss Date: Sun, 16 Jul 2017 22:42:29 +0200 Subject: [PATCH 0173/2570] Update MutePlugin.py --- plugins/Mute/MutePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Mute/MutePlugin.py b/plugins/Mute/MutePlugin.py index e1d6fb05..e30a6c38 100644 --- a/plugins/Mute/MutePlugin.py +++ b/plugins/Mute/MutePlugin.py @@ -14,7 +14,7 @@ if os.path.isfile("%s/mutes.json" % config.data_dir): data = json.load(open("%s/mutes.json" % config.data_dir)) mutes = data.get("mutes", {}) site_blacklist = data.get("site_blacklist", {}) - except Exception, err: + except Exception as err: mutes = {} site_blacklist = {} else: From d426b6bb020b75d74eaaa68a6a649c2b5850cc9a Mon Sep 17 00:00:00 2001 From: cclauss Date: Sun, 16 Jul 2017 22:43:57 +0200 Subject: [PATCH 0174/2570] Update NewsfeedPlugin.py --- plugins/Newsfeed/NewsfeedPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Newsfeed/NewsfeedPlugin.py b/plugins/Newsfeed/NewsfeedPlugin.py index feaf8542..949e62b6 100644 --- a/plugins/Newsfeed/NewsfeedPlugin.py +++ b/plugins/Newsfeed/NewsfeedPlugin.py @@ -60,7 +60,7 @@ class UiWebsocketPlugin(object): else: res = site.storage.query(query + " ORDER BY date_added DESC LIMIT %s" % limit) - except Exception, err: # Log error + except Exception as err: # Log error self.log.error("%s feed query %s error: %s" % (address, name, err)) continue From 08219a028539935e899a8236820dffeb55d93fd8 Mon Sep 17 00:00:00 2001 From: cclauss Date: Sun, 16 Jul 2017 22:46:13 +0200 Subject: [PATCH 0175/2570] Update ContentDbPlugin.py --- plugins/OptionalManager/ContentDbPlugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/OptionalManager/ContentDbPlugin.py b/plugins/OptionalManager/ContentDbPlugin.py index 9df52f00..d1a1dd29 100644 --- a/plugins/OptionalManager/ContentDbPlugin.py +++ b/plugins/OptionalManager/ContentDbPlugin.py @@ -132,7 +132,7 @@ class ContentDbPlugin(object): content = site.content_manager.contents[row["inner_path"]] try: num += self.setContentFilesOptional(site, row["inner_path"], content, cur=cur) - except Exception, err: + except Exception as err: self.log.error("Error loading %s into file_optional: %s" % (row["inner_path"], err)) cur.execute("COMMIT") cur.close() @@ -159,7 +159,7 @@ class ContentDbPlugin(object): cur = self try: cur.execute("BEGIN") - except Exception, err: + except Exception as err: self.log.warning("Transaction begin error %s %s: %s" % (site, content_inner_path, Debug.formatException(err))) num = 0 @@ -195,7 +195,7 @@ class ContentDbPlugin(object): if cur == self: try: cur.execute("END") - except Exception, err: + except Exception as err: self.log.warning("Transaction end error %s %s: %s" % (site, content_inner_path, Debug.formatException(err))) return num @@ -387,7 +387,7 @@ class ContentDbPlugin(object): site.content_manager.optionalRemove(row["inner_path"], row["hash_id"], row["size"]) site.storage.delete(row["inner_path"]) need_delete -= row["size"] - except Exception, err: + except Exception as err: site.log.error("Error deleting %s: %s" % (row["inner_path"], err)) if need_delete <= 0: From 21d0b829d3535ab2de9d33c3f571d4017eb9abb8 Mon Sep 17 00:00:00 2001 From: cclauss Date: Sun, 16 Jul 2017 22:47:22 +0200 Subject: [PATCH 0176/2570] Update UiWebsocketPlugin.py --- plugins/OptionalManager/UiWebsocketPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/OptionalManager/UiWebsocketPlugin.py b/plugins/OptionalManager/UiWebsocketPlugin.py index 91f7f6f8..b34b5b88 100644 --- a/plugins/OptionalManager/UiWebsocketPlugin.py +++ b/plugins/OptionalManager/UiWebsocketPlugin.py @@ -137,7 +137,7 @@ class UiWebsocketPlugin(object): try: site.storage.delete(inner_path) - except Exception, err: + except Exception as err: return self.response(to, {"error": "File delete error: %s" % err}) self.response(to, "ok") From 8db16a1421a674cb14872d87052aaa50e5d35c2e Mon Sep 17 00:00:00 2001 From: cclauss Date: Sun, 16 Jul 2017 22:48:35 +0200 Subject: [PATCH 0177/2570] Update PeerDbPlugin.py --- plugins/PeerDb/PeerDbPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/PeerDb/PeerDbPlugin.py b/plugins/PeerDb/PeerDbPlugin.py index 72e585ee..5e968b8b 100644 --- a/plugins/PeerDb/PeerDbPlugin.py +++ b/plugins/PeerDb/PeerDbPlugin.py @@ -79,7 +79,7 @@ class ContentDbPlugin(object): "INSERT INTO peer (site_id, address, port, hashfield, time_added) VALUES (?, ?, ?, ?, ?)", self.iteratePeers(site) ) - except Exception, err: + except Exception as err: site.log.error("Save peer error: %s" % err) finally: cur.execute("END") From 83e372ce4727ec65e04bb2c477efab1eb2a20d0c Mon Sep 17 00:00:00 2001 From: cclauss Date: Sun, 16 Jul 2017 22:50:03 +0200 Subject: [PATCH 0178/2570] Update SidebarPlugin.py --- plugins/Sidebar/SidebarPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index 41cbadc0..49fc5cfc 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -171,7 +171,7 @@ class UiWebsocketPlugin(object): size_total = size_other = site.settings["size"] # Bar - for extension, color in extensions: + for extension as color in extensions: if extension == "Total": continue if extension == "Other": From 7d7272a266c7f15a4341d6136acf95db957fd1d2 Mon Sep 17 00:00:00 2001 From: cclauss Date: Sun, 16 Jul 2017 22:51:01 +0200 Subject: [PATCH 0179/2570] Update SidebarPlugin.py --- plugins/Sidebar/SidebarPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index 49fc5cfc..99339fd0 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -502,7 +502,7 @@ class UiWebsocketPlugin(object): self.cmd("notification", ["geolite-done", _["GeoLite2 City database downloaded!"], 5000]) time.sleep(2) # Wait for notify animation return True - except Exception, err: + except Exception as err: self.log.error("Error downloading %s: %s" % (db_url, err)) pass self.cmd("notification", [ From 1f220093600e7786c68a8aac1e79e59924d50b92 Mon Sep 17 00:00:00 2001 From: cclauss Date: Sun, 16 Jul 2017 22:53:11 +0200 Subject: [PATCH 0180/2570] Update SiteManagerPlugin.py --- plugins/disabled-Dnschain/SiteManagerPlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/disabled-Dnschain/SiteManagerPlugin.py b/plugins/disabled-Dnschain/SiteManagerPlugin.py index 9121b425..1a41a9d7 100644 --- a/plugins/disabled-Dnschain/SiteManagerPlugin.py +++ b/plugins/disabled-Dnschain/SiteManagerPlugin.py @@ -60,7 +60,7 @@ class SiteManagerPlugin(object): return data["zeronet"].get(sub_domain) # Not found return address - except Exception, err: + except Exception as err: log.debug("Dnschain.net %s resolve error: %s" % (domain, Debug.formatException(err))) @@ -82,7 +82,7 @@ class SiteManagerPlugin(object): return data["zeronet"].get(sub_domain) # Not found return address - except Exception, err: + except Exception as err: log.debug("Dnschain.info %s resolve error: %s" % (domain, Debug.formatException(err))) From eaa344abca2b2e7544842b1411370519e0681acf Mon Sep 17 00:00:00 2001 From: cclauss Date: Sun, 16 Jul 2017 22:57:28 +0200 Subject: [PATCH 0181/2570] Update main.py --- src/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.py b/src/main.py index d9426a28..e74bc71c 100644 --- a/src/main.py +++ b/src/main.py @@ -32,14 +32,14 @@ if not os.path.isdir(config.log_dir): os.mkdir(config.log_dir) try: os.chmod(config.log_dir, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) - except Exception, err: + except Exception as err: print "Can't change permission of %s: %s" % (config.log_dir, err) if not os.path.isdir(config.data_dir): os.mkdir(config.data_dir) try: os.chmod(config.data_dir, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) - except Exception, err: + except Exception as err: print "Can't change permission of %s: %s" % (config.data_dir, err) if not os.path.isfile("%s/sites.json" % config.data_dir): From 6e51b5ba323cc8706460adf26ab761af1a702808 Mon Sep 17 00:00:00 2001 From: Jon P Date: Mon, 17 Jul 2017 16:08:18 +0200 Subject: [PATCH 0182/2570] Update Dockerfile --- Dockerfile | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/Dockerfile b/Dockerfile index 350fff9f..b0695b47 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,32 +1,28 @@ -FROM ubuntu:16.04 - -MAINTAINER Felix Imobersteg +FROM alpine:3.6 #Base settings -ENV DEBIAN_FRONTEND noninteractive ENV HOME /root #Install ZeroNet -RUN \ - apt-get update -y; \ - apt-get -y install msgpack-python python-gevent python-pip python-dev tor; \ - pip install msgpack-python --upgrade; \ - apt-get clean -y; \ - rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*; \ - echo "ControlPort 9051" >> /etc/tor/torrc; \ - echo "CookieAuthentication 1" >> /etc/tor/torrc - +RUN apk --update upgrade \ + && apk --no-cache --no-progress add musl-dev gcc python python-dev py2-pip tor \ + && pip install gevent msgpack-python \ + && apk del musl-dev gcc python-dev py2-pip \ + && rm -rf /var/cache/apk/* /tmp/* /var/tmp/* \ + && echo "ControlPort 9051" >> /etc/tor/torrc \ + && echo "CookieAuthentication 1" >> /etc/tor/torrc #Add Zeronet source -ADD . /root +COPY . /root VOLUME /root/data #Control if Tor proxy is started ENV ENABLE_TOR false +WORKDIR /root + #Set upstart command -CMD cd /root && (! ${ENABLE_TOR} || /etc/init.d/tor start) && python zeronet.py --ui_ip 0.0.0.0 +CMD (! ${ENABLE_TOR} || tor&) && python zeronet.py --ui_ip 0.0.0.0 #Expose ports -EXPOSE 43110 -EXPOSE 15441 +EXPOSE 43110 15441 From 551aa3ef677ea748e01e9d566f28d2e6fe77538f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 18 Jul 2017 20:53:52 +0200 Subject: [PATCH 0183/2570] Raise exception on unsupported dbschema version --- src/Db/DbCursor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Db/DbCursor.py b/src/Db/DbCursor.py index 728335d9..0b03a9f0 100644 --- a/src/Db/DbCursor.py +++ b/src/Db/DbCursor.py @@ -144,6 +144,8 @@ class DbCursor: self.execute("INSERT INTO json ?", {"site": site_address, "directory": directory, "file_name": file_name}) res = self.execute("SELECT * FROM json WHERE ? LIMIT 1", {"site": site_address, "directory": directory, "file_name": file_name}) row = res.fetchone() + else: + raise Exception("Dbschema version %s not supported" % self.db.schema.get("version")) return row def close(self): From bb27e3124ffc4d0d18747da0170a642f500cd2ab Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 18 Jul 2017 20:54:39 +0200 Subject: [PATCH 0184/2570] Remove files without info on retry bad files --- src/Site/Site.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Site/Site.py b/src/Site/Site.py index e8e9445c..94008a82 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -235,6 +235,19 @@ class Site(object): file_inner_paths = [] for bad_file, tries in self.bad_files.items(): if force or random.randint(0, min(40, tries)) < 4: # Larger number tries = less likely to check every 15min + # Skip files without info + file_info = self.content_manager.getFileInfo(bad_file) + if bad_file.endswith("content.json"): + if file_info is False: + del self.bad_files[bad_file] + self.log.debug("No info for file: %s, removing from bad_files" % bad_file) + continue + else: + if file_info is False or not file_info.get("size"): + del self.bad_files[bad_file] + self.log.debug("No info for file: %s, removing from bad_files" % bad_file) + continue + if bad_file.endswith("content.json"): content_inner_paths.append(bad_file) else: From bf672bdec337973841145fcd47b5f28370e39214 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 18 Jul 2017 20:55:04 +0200 Subject: [PATCH 0185/2570] Add new test to unsafe matches --- src/Test/TestSafeRe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test/TestSafeRe.py b/src/Test/TestSafeRe.py index 111805fd..ae2e9c15 100644 --- a/src/Test/TestSafeRe.py +++ b/src/Test/TestSafeRe.py @@ -11,7 +11,7 @@ class TestSafeRe: ) assert SafeRe.match(".+/data.json", "data/users/1J3rJ8ecnwH2EPYa6MrgZttBNc61ACFiCj/data.json") - @pytest.mark.parametrize("pattern", ["([a-zA-Z]+)*", "(a|aa)+*", "(a|a?)+", "(.*a){10}"]) + @pytest.mark.parametrize("pattern", ["([a-zA-Z]+)*", "(a|aa)+*", "(a|a?)+", "(.*a){10}", "((?!json).)*$", "(\w+\d+)+C"]) def testUnsafeMatch(self, pattern): with pytest.raises(SafeRe.UnsafePatternError) as err: SafeRe.match(pattern, "aaaaaaaaaaaaaaaaaaaaaaaa!") From 34ec05d4b4f1146b8ecf9be8f9452a46c4a54484 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 18 Jul 2017 20:55:53 +0200 Subject: [PATCH 0186/2570] Fix notification id collision --- src/Ui/media/Wrapper.coffee | 2 +- src/Ui/media/all.js | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index eae824be..3e3f79ea 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -49,7 +49,7 @@ class Wrapper @sendInner message # Pass message to inner frame else if cmd == "notification" # Display notification type = message.params[0] - id = "notification-#{message.id}" + id = "notification-ws-#{message.id}" if "-" in message.params[0] # - in first param: message id defined [id, type] = message.params[0].split("-") @notifications.add(id, type, message.params[1], message.params[2]) diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index 325379e5..2791c209 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -684,11 +684,11 @@ jQuery.extend( jQuery.easing, (function() { var Notifications, - __slice = [].slice; + slice = [].slice; Notifications = (function() { - function Notifications(_at_elem) { - this.elem = _at_elem; + function Notifications(elem1) { + this.elem = elem1; this; } @@ -707,14 +707,14 @@ jQuery.extend( jQuery.easing, }; Notifications.prototype.add = function(id, type, body, timeout) { - var elem, width, _i, _len, _ref; + var elem, i, len, ref, width; if (timeout == null) { timeout = 0; } id = id.replace(/[^A-Za-z0-9]/g, ""); - _ref = $(".notification-" + id); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - elem = _ref[_i]; + ref = $(".notification-" + id); + for (i = 0, len = ref.length; i < len; i++) { + elem = ref[i]; this.close($(elem)); } elem = $(".notification.template", this.elem).clone().removeClass("template"); @@ -791,8 +791,8 @@ jQuery.extend( jQuery.easing, Notifications.prototype.log = function() { var args; - args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; - return console.log.apply(console, ["[Notifications]"].concat(__slice.call(args))); + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return console.log.apply(console, ["[Notifications]"].concat(slice.call(args))); }; return Notifications; @@ -804,6 +804,7 @@ jQuery.extend( jQuery.easing, }).call(this); + /* ---- src/Ui/media/Wrapper.coffee ---- */ @@ -880,7 +881,7 @@ jQuery.extend( jQuery.easing, } } else if (cmd === "notification") { type = message.params[0]; - id = "notification-" + message.id; + id = "notification-ws-" + message.id; if (indexOf.call(message.params[0], "-") >= 0) { ref = message.params[0].split("-"), id = ref[0], type = ref[1]; } @@ -1550,4 +1551,4 @@ jQuery.extend( jQuery.easing, window.wrapper = new Wrapper(ws_url); -}).call(this); +}).call(this); \ No newline at end of file From 1f7b25b60c47af1c7dc5a6eb5b3e9c62269a43a3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 18 Jul 2017 20:57:33 +0200 Subject: [PATCH 0187/2570] Ignore http protocol in same origin comparison --- src/Ui/UiRequest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 91ca261b..30f3096c 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -394,8 +394,8 @@ class UiRequest(object): def isSameOrigin(self, url_a, url_b): if not url_a or not url_b: return False - origin_a = re.sub("(http[s]{0,1}://.*?/.*?/).*", "\\1", url_a) - origin_b = re.sub("(http[s]{0,1}://.*?/.*?/).*", "\\1", url_b) + origin_a = re.sub("http[s]{0,1}://(.*?/.*?/).*", "\\1", url_a) + origin_b = re.sub("http[s]{0,1}://(.*?/.*?/).*", "\\1", url_b) return origin_a == origin_b # Return {address: 1Site.., inner_path: /data/users.json} from url path From 56c9ac8e46562d1c2537074b0c027534303fd2a3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 18 Jul 2017 20:57:41 +0200 Subject: [PATCH 0188/2570] Rev2159 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 01cb978c..e830f397 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.6" - self.rev = 2156 + self.rev = 2159 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 961fcbf6608ab8951772a4710eced98b0218d4c3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 18 Jul 2017 22:39:48 +0200 Subject: [PATCH 0189/2570] Rev2160, Fix sidebar typo --- plugins/Sidebar/SidebarPlugin.py | 2 +- src/Config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index 99339fd0..68360f96 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -171,7 +171,7 @@ class UiWebsocketPlugin(object): size_total = size_other = site.settings["size"] # Bar - for extension as color in extensions: + for extension, color in extensions: if extension == "Total": continue if extension == "Other": diff --git a/src/Config.py b/src/Config.py index e830f397..a785b234 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.6" - self.rev = 2159 + self.rev = 2160 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 2aba9cc3c28a879d2ae9f5aa8d956b40be3bff7a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 19 Jul 2017 16:45:12 +0200 Subject: [PATCH 0190/2570] Only download content.json files as content --- src/Site/Site.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Site/Site.py b/src/Site/Site.py index 94008a82..20dfe3fe 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -131,6 +131,9 @@ class Site(object): if config.verbose: self.log.debug("Downloading %s..." % inner_path) + if not inner_path.endswith("content.json"): + return False + found = self.needFile(inner_path, update=self.bad_files.get(inner_path)) content_inner_dir = helper.getDirname(inner_path) if not found: From b88ee9a87a38b725bd94735a204675b5d1fa1b85 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 19 Jul 2017 16:45:28 +0200 Subject: [PATCH 0191/2570] Only allow to sign content.json files --- src/Content/ContentManager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index e2347e32..b2979b17 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -528,6 +528,9 @@ class ContentManager(object): # 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): + if not inner_path.endswith("content.json"): + raise SignError("Invalid file name, you can only sign content.json files") + if inner_path in self.contents: content = self.contents.get(inner_path) if content and content.get("cert_sign", False) is None and self.site.storage.isFile(inner_path): From 504d7812e5ed640a507b5d04b7ceb51c6081dfb9 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 19 Jul 2017 16:46:37 +0200 Subject: [PATCH 0192/2570] Fix exception if no path_parts returned --- src/Ui/UiRequest.py | 75 ++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 30f3096c..23353dce 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -423,6 +423,9 @@ class UiRequest(object): path_parts = self.parsePath(path) + if not path_parts: + return self.error404(path) + # Check wrapper nonce content_type = self.getContentType(path_parts["inner_path"]) if "htm" in content_type and not header_noscript: # Valid nonce must present to render html files @@ -437,45 +440,41 @@ class UiRequest(object): self.log.error("Media referrer error: %s not allowed from %s" % (self.getRequestUrl(), self.getReferer())) return self.error403("Media referrer error") # Referrer not starts same address as requested path - if path_parts: # Looks like a valid path - address = path_parts["address"] - file_path = "%s/%s/%s" % (config.data_dir, address, path_parts["inner_path"]) - if config.debug 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) - if site and site.settings["own"]: - from Debug import DebugMedia - DebugMedia.merge(file_path) - if not address or address == ".": - return self.error403(path_parts["inner_path"]) - if os.path.isfile(file_path): # File exists + address = path_parts["address"] + file_path = "%s/%s/%s" % (config.data_dir, address, path_parts["inner_path"]) + if config.debug 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) + if site and site.settings["own"]: + from Debug import DebugMedia + DebugMedia.merge(file_path) + if not address or address == ".": + return self.error403(path_parts["inner_path"]) + if os.path.isfile(file_path): # File exists + return self.actionFile(file_path, header_length=header_length, header_noscript=header_noscript) + elif os.path.isdir(file_path): # If this is actually a folder, add "/" and redirect + if path_parts["inner_path"]: + return self.actionRedirect("./%s/" % path_parts["inner_path"].split("/")[-1]) + else: + return self.actionRedirect("./%s/" % path_parts["address"]) + else: # File not exists, try to download + if address not in SiteManager.site_manager.sites: # Only in case if site already started downloading + return self.actionSiteAddPrompt(path) + + site = SiteManager.site_manager.need(address) + + if path_parts["inner_path"].endswith("favicon.ico"): # Default favicon for all sites + return self.actionFile("src/Ui/media/img/favicon.ico") + + result = site.needFile(path_parts["inner_path"], priority=15) # Wait until file downloads + if result: return self.actionFile(file_path, header_length=header_length, header_noscript=header_noscript) - elif os.path.isdir(file_path): # If this is actually a folder, add "/" and redirect - if path_parts["inner_path"]: - return self.actionRedirect("./%s/" % path_parts["inner_path"].split("/")[-1]) - else: - return self.actionRedirect("./%s/" % path_parts["address"]) - else: # File not exists, try to download - if address not in SiteManager.site_manager.sites: # Only in case if site already started downloading - return self.actionSiteAddPrompt(path) - - site = SiteManager.site_manager.need(address) - - if path_parts["inner_path"].endswith("favicon.ico"): # Default favicon for all sites - return self.actionFile("src/Ui/media/img/favicon.ico") - - result = site.needFile(path_parts["inner_path"], priority=15) # Wait until file downloads - if result: - return self.actionFile(file_path, header_length=header_length, header_noscript=header_noscript) - else: - self.log.debug("File not found: %s" % path_parts["inner_path"]) - # Site larger than allowed, re-add wrapper nonce to allow reload - if site.settings.get("size", 0) > site.getSizeLimit() * 1024 * 1024: - self.server.wrapper_nonces.append(self.get.get("wrapper_nonce")) - return self.error404(path_parts["inner_path"]) - - else: # Bad url - return self.error404(path) + else: + self.log.debug("File not found: %s" % path_parts["inner_path"]) + # Site larger than allowed, re-add wrapper nonce to allow reload + if site.settings.get("size", 0) > site.getSizeLimit() * 1024 * 1024: + self.server.wrapper_nonces.append(self.get.get("wrapper_nonce")) + return self.error404(path_parts["inner_path"]) # Serve a media for ui def actionUiMedia(self, path): From cbac57dc8824a9dff4a79dcfab008bebe5cddf6f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 19 Jul 2017 16:48:24 +0200 Subject: [PATCH 0193/2570] Plugin to request cross-site resource access --- plugins/Cors/CorsPlugin.py | 84 ++++++++++++++++++++++++++++++++++++++ plugins/Cors/__init__.py | 1 + 2 files changed, 85 insertions(+) create mode 100644 plugins/Cors/CorsPlugin.py create mode 100644 plugins/Cors/__init__.py diff --git a/plugins/Cors/CorsPlugin.py b/plugins/Cors/CorsPlugin.py new file mode 100644 index 00000000..5aa73ee1 --- /dev/null +++ b/plugins/Cors/CorsPlugin.py @@ -0,0 +1,84 @@ +import re +import cgi + +from Plugin import PluginManager +from Translate import Translate +from util import helper +from Debug import Debug +if "_" not in locals(): + _ = Translate("plugins/Cors/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): + # Add cors support for file commands + def corsFuncWrapper(self, func_name, to, inner_path, *args, **kwargs): + func = getattr(super(UiWebsocketPlugin, self), func_name) + if inner_path.startswith("cors-"): + cors_address, cors_inner_path = getCorsPath(self.site, inner_path) + + site_before = self.site # Save to be able to change it back after we ran the command + self.site = self.server.sites.get(cors_address) # Change the site to the merged one + try: + back = func(to, cors_inner_path, *args, **kwargs) + finally: + self.site = site_before # Change back to original site + return back + else: + 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 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): + site = self.server.sites.get(address) + if site: + site_name = site.content_manager.contents.get("content.json", {}).get("title") + button_title = _["Grant"] + else: + site_name = address + button_title = _["Grant & Add"] + + self.cmd( + "confirm", + [_["This site requests read permission to: %s"] % cgi.escape(site_name), button_title], + lambda (res): self.cbCorsPermission(to, address) + ) + + def cbCorsPermission(self, to, address): + self.actionPermissionAdd(to, "Cors:"+address) + site = self.server.sites.get(address) + if not site: + 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: + return None + return path_parts diff --git a/plugins/Cors/__init__.py b/plugins/Cors/__init__.py new file mode 100644 index 00000000..bca1ab3e --- /dev/null +++ b/plugins/Cors/__init__.py @@ -0,0 +1 @@ +import CorsPlugin \ No newline at end of file From bd9f88416c73abddd208243ff86d8cbf97ddd4ad Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 19 Jul 2017 16:48:40 +0200 Subject: [PATCH 0194/2570] Rev2162 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index a785b234..13432e7d 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.6" - self.rev = 2160 + self.rev = 2162 self.argv = argv self.action = None self.config_file = "zeronet.conf" From db8fe8d8903ea8450c5702efaf88784a7aaa5a2d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 19 Jul 2017 22:52:35 +0200 Subject: [PATCH 0195/2570] Version 0.5.7, Rev2163 --- src/Config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index 13432e7d..9c2b8c8d 100644 --- a/src/Config.py +++ b/src/Config.py @@ -9,8 +9,8 @@ import ConfigParser class Config(object): def __init__(self, argv): - self.version = "0.5.6" - self.rev = 2162 + self.version = "0.5.7" + self.rev = 2163 self.argv = argv self.action = None self.config_file = "zeronet.conf" From f45ecb6cf4bf72bd77351cf90bde176b0203e8c7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 27 Jul 2017 16:29:12 +0200 Subject: [PATCH 0196/2570] Log problematic pattern --- src/util/SafeRe.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/util/SafeRe.py b/src/util/SafeRe.py index 1caa61a0..f970a729 100644 --- a/src/util/SafeRe.py +++ b/src/util/SafeRe.py @@ -9,15 +9,15 @@ cached_patterns = {} def isSafePattern(pattern): if len(pattern) > 255: - raise UnsafePatternError("Pattern too long: %s characters" % len(pattern)) + raise UnsafePatternError("Pattern too long: %s characters in %s" % (len(pattern), pattern)) unsafe_pattern_match = re.search("[^\.][\*\{\+]", pattern) # Always should be "." before "*{+" characters to avoid ReDoS if unsafe_pattern_match: - raise UnsafePatternError("Potentially unsafe part of the pattern: %s" % unsafe_pattern_match.group(0)) + raise UnsafePatternError("Potentially unsafe part of the pattern: %s in %s" % (unsafe_pattern_match.group(0), pattern)) repetitions = re.findall("\.[\*\{\+]", pattern) if len(repetitions) >= 10: - raise UnsafePatternError("More than 10 repetitions of %s" % repetitions[0]) + raise UnsafePatternError("More than 10 repetitions of %s in %s" % (repetitions[0], pattern)) return True From 35efd6b107593fed292c017178a6e704268faaae Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 27 Jul 2017 16:29:39 +0200 Subject: [PATCH 0197/2570] ServerShowdirectory admin API command to show directory --- src/Ui/UiWebsocket.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 80e0718d..2d29652a 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -34,8 +34,8 @@ class UiWebsocket(object): self.send_queue = [] # Messages to send to client self.admin_commands = ( "sitePause", "siteResume", "siteDelete", "siteList", "siteSetLimit", "siteClone", - "channelJoinAllsite", "serverUpdate", "serverPortcheck", "serverShutdown", "certSet", "configSet", - "permissionAdd", "permissionRemove" + "channelJoinAllsite", "serverUpdate", "serverPortcheck", "serverShutdown", "serverShowdirectory", + "certSet", "configSet", "permissionAdd", "permissionRemove" ) self.async_commands = ("fileGet", "fileList", "dirList") @@ -848,6 +848,10 @@ class UiWebsocket(object): sys.modules["main"].file_server.stop() sys.modules["main"].ui_server.stop() + def actionServerShowdirectory(self, to, directory="backup"): + import webbrowser + webbrowser.open('file://' + os.path.abspath(config.data_dir)) + def actionConfigSet(self, to, key, value): if key not in ["tor", "language"]: self.response(to, {"error": "Forbidden"}) From eee073c103afbc079eb33eb66127b61464d1a2f4 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 27 Jul 2017 16:30:21 +0200 Subject: [PATCH 0198/2570] Publish incoming updates to more peers --- 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 d3b9ebdc..a8c09ace 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -145,7 +145,7 @@ class FileRequest(object): peer = site.addPeer(self.connection.ip, self.connection.port, return_peer=True) # 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=2), "publish_%s" % inner_path) + site.onComplete.once(lambda: site.publish(inner_path=inner_path, diffs=diffs, limit=3), "publish_%s" % inner_path) # Load new content file and download changed files in new thread def downloader(): From 5bdc3b4fb56053f37931d011c399011ea9340013 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 27 Jul 2017 16:30:37 +0200 Subject: [PATCH 0199/2570] Rev2165 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 9c2b8c8d..02be0530 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.7" - self.rev = 2163 + self.rev = 2165 self.argv = argv self.action = None self.config_file = "zeronet.conf" From fa96ab453f6b5b0a3a694cbc8803e65419d44408 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 28 Jul 2017 15:36:49 +0200 Subject: [PATCH 0200/2570] Rev2166, Mobile friendly notifications --- src/Config.py | 2 +- src/Ui/media/Notifications.coffee | 1 + src/Ui/media/Wrapper.css | 9 +++++++++ src/Ui/media/all.css | 9 +++++++++ src/Ui/media/all.js | 3 +++ 5 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 02be0530..c71f3369 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.7" - self.rev = 2165 + self.rev = 2166 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Ui/media/Notifications.coffee b/src/Ui/media/Notifications.coffee index 00c66761..9f38921b 100644 --- a/src/Ui/media/Notifications.coffee +++ b/src/Ui/media/Notifications.coffee @@ -57,6 +57,7 @@ class Notifications elem.css({"width": "50px", "transform": "scale(0.01)"}) elem.animate({"scale": 1}, 800, "easeOutElastic") elem.animate({"width": width}, 700, "easeInOutCubic") + $(".body", elem).css("width": (width - 80)) $(".body", elem).cssLater("box-shadow", "0px 0px 5px rgba(0,0,0,0.1)", 1000) # Close button or Confirm button diff --git a/src/Ui/media/Wrapper.css b/src/Ui/media/Wrapper.css index 4859112f..c4764395 100644 --- a/src/Ui/media/Wrapper.css +++ b/src/Ui/media/Wrapper.css @@ -174,4 +174,13 @@ a { color: black } @media print { #inner-iframe { position: fixed; } .progressbar, .fixbutton, .notifications, .loadingscreen { visibility: hidden; } +} + +/* Small screen */ +@media screen and (max-width: 600px) { + .notification .message { white-space: normal; } + .notification .buttons { padding-right: 22px; } + .notification .button { white-space: nowrap; } + .notification { margin: 0px } + .notifications { right: 0px } } \ No newline at end of file diff --git a/src/Ui/media/all.css b/src/Ui/media/all.css index ff64e90b..351a5adc 100644 --- a/src/Ui/media/all.css +++ b/src/Ui/media/all.css @@ -214,4 +214,13 @@ a { color: black } @media print { #inner-iframe { position: fixed; } .progressbar, .fixbutton, .notifications, .loadingscreen { visibility: hidden; } +} + +/* Small screen */ +@media screen and (max-width: 600px) { + .notification .message { white-space: normal; } + .notification .buttons { padding-right: 22px; } + .notification .button { white-space: nowrap; } + .notification { margin: 0px } + .notifications { right: 0px } } \ No newline at end of file diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index 2791c209..7f9cc03f 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -764,6 +764,9 @@ jQuery.extend( jQuery.easing, elem.animate({ "width": width }, 700, "easeInOutCubic"); + $(".body", elem).css({ + "width": width - 80 + }); $(".body", elem).cssLater("box-shadow", "0px 0px 5px rgba(0,0,0,0.1)", 1000); $(".close, .button", elem).on("click", (function(_this) { return function() { From 6610914fdb7400b17d5463887d84b786a2a4b19f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 28 Jul 2017 22:57:41 +0200 Subject: [PATCH 0201/2570] Rev2167, Skip downloading if no connection server specified --- src/Config.py | 2 +- src/Site/Site.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index c71f3369..5910757b 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.7" - self.rev = 2166 + self.rev = 2167 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Site/Site.py b/src/Site/Site.py index 20dfe3fe..10997b04 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -265,6 +265,10 @@ class Site(object): # Download all files of the site @util.Noparallel(blocking=False) def download(self, check_size=False, blind_includes=False): + if not self.connection_server: + self.log.debug("No connection server found, skipping download") + return False + self.log.debug( "Start downloading, bad_files: %s, check_size: %s, blind_includes: %s" % (self.bad_files, check_size, blind_includes) From 467cd480cff0c067ea338b40914c366aa23f901b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 29 Jul 2017 12:49:38 +0200 Subject: [PATCH 0202/2570] Rev2168, Only avoid reload site on every missing file --- src/Config.py | 2 +- src/Site/SiteManager.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Config.py b/src/Config.py index 5910757b..af0e32d9 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.7" - self.rev = 2167 + self.rev = 2168 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index 046254af..0a7e22db 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -136,9 +136,6 @@ class SiteManager(object): site.saveSettings() if all_file: # Also download user files on first sync site.download(check_size=True, blind_includes=True) - else: - if all_file: - site.download(check_size=True, blind_includes=True) return site From 527c2b4f54a14849d39f768494432ef2d50ceac4 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 29 Jul 2017 13:44:54 +0200 Subject: [PATCH 0203/2570] Sidebar close button for small screens --- plugins/Sidebar/SidebarPlugin.py | 1 + plugins/Sidebar/media/Sidebar.coffee | 6 ++++++ plugins/Sidebar/media/Sidebar.css | 8 +++++++- plugins/Sidebar/media/all.css | 8 +++++++- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index 68360f96..7eb39e74 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -440,6 +440,7 @@ class UiWebsocketPlugin(object): body = [] body.append("
") + body.append("×") body.append("

%s

" % cgi.escape(site.content_manager.contents.get("content.json", {}).get("title", ""), True)) body.append("
") diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee index e180c595..802d1cff 100644 --- a/plugins/Sidebar/media/Sidebar.coffee +++ b/plugins/Sidebar/media/Sidebar.coffee @@ -377,6 +377,12 @@ class Sidebar extends Class wrapper.ws.cmd "sitePublish", {"inner_path": inner_path, "sign": false}, => @tag.find("#button-publish").removeClass "loading" + # Close + @tag.find(".close").off("click touchend").on "click touchend", (e) => + @startDrag() + @stopDrag() + return false + @loadGlobe() diff --git a/plugins/Sidebar/media/Sidebar.css b/plugins/Sidebar/media/Sidebar.css index bf915baa..c76bde0c 100644 --- a/plugins/Sidebar/media/Sidebar.css +++ b/plugins/Sidebar/media/Sidebar.css @@ -14,6 +14,7 @@ .sidebar .content { margin: 30px; font-family: "Segoe UI Light", "Segoe UI", "Helvetica Neue"; color: white; width: 375px; height: 300px; font-weight: 200; transition: all 1s; opacity: 0 } .sidebar-container.loaded .content { opacity: 1; transform: none } .sidebar h1, .sidebar h2 { font-weight: lighter; } +.sidebar .close { color: #999; float: right; text-decoration: none; margin-top: -5px; padding: 0px 5px; font-size: 33px; margin-right: 20px; display: none } .sidebar .button { margin: 0px; display: inline-block; transition: all 0.3s; box-sizing: border-box; max-width: 260px } .sidebar .button.hidden { padding: 0px; max-width: 0px; opacity: 0; pointer-events: none } .sidebar #button-delete { background-color: transparent; border: 1px solid #333; color: #AAA; margin-left: 10px } @@ -121,4 +122,9 @@ /* Sign publish */ .contents { background-color: #3B3B3B; color: white; padding: 7px 10px; font-family: Consolas; font-size: 11px; display: inline-block; margin-bottom: 6px; } .contents a { color: white } -.contents a:active { background-color: #6B6B6B } \ No newline at end of file +.contents a:active { background-color: #6B6B6B } + +/* Small screen */ +@media screen and (max-width: 600px) { + .sidebar .close { display: block } +} \ No newline at end of file diff --git a/plugins/Sidebar/media/all.css b/plugins/Sidebar/media/all.css index f28bd0d4..baf9b94c 100644 --- a/plugins/Sidebar/media/all.css +++ b/plugins/Sidebar/media/all.css @@ -68,6 +68,7 @@ .sidebar .content { margin: 30px; font-family: "Segoe UI Light", "Segoe UI", "Helvetica Neue"; color: white; width: 375px; height: 300px; font-weight: 200; -webkit-transition: all 1s; -moz-transition: all 1s; -o-transition: all 1s; -ms-transition: all 1s; transition: all 1s ; opacity: 0 } .sidebar-container.loaded .content { opacity: 1; -webkit-transform: none ; -moz-transform: none ; -o-transform: none ; -ms-transform: none ; transform: none } .sidebar h1, .sidebar h2 { font-weight: lighter; } +.sidebar .close { color: #999; float: right; text-decoration: none; margin-top: -5px; padding: 0px 5px; font-size: 33px; margin-right: 20px; display: none } .sidebar .button { margin: 0px; display: inline-block; -webkit-transition: all 0.3s; -moz-transition: all 0.3s; -o-transition: all 0.3s; -ms-transition: all 0.3s; transition: all 0.3s ; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; max-width: 260px } .sidebar .button.hidden { padding: 0px; max-width: 0px; opacity: 0; pointer-events: none } .sidebar #button-delete { background-color: transparent; border: 1px solid #333; color: #AAA; margin-left: 10px } @@ -175,4 +176,9 @@ /* Sign publish */ .contents { background-color: #3B3B3B; color: white; padding: 7px 10px; font-family: Consolas; font-size: 11px; display: inline-block; margin-bottom: 6px; } .contents a { color: white } -.contents a:active { background-color: #6B6B6B } \ No newline at end of file +.contents a:active { background-color: #6B6B6B } + +/* Small screen */ +@media screen and (max-width: 600px) { + .sidebar .close { display: block } +} \ No newline at end of file From a69531c4c9438845cfb90a3d41ef9db2942a9a88 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 29 Jul 2017 13:45:57 +0200 Subject: [PATCH 0204/2570] Emulate short click for mobile --- plugins/Sidebar/media/Sidebar.coffee | 2 ++ plugins/Sidebar/media/all.js | 12 ++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee index 802d1cff..b5672687 100644 --- a/plugins/Sidebar/media/Sidebar.coffee +++ b/plugins/Sidebar/media/Sidebar.coffee @@ -55,6 +55,8 @@ class Sidebar extends Class @fixbutton_addx = @fixbutton.offset().left-mousex @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 diff --git a/plugins/Sidebar/media/all.js b/plugins/Sidebar/media/all.js index 1acb2fdc..8f36146e 100644 --- a/plugins/Sidebar/media/all.js +++ b/plugins/Sidebar/media/all.js @@ -57,7 +57,6 @@ }).call(this); - /* ---- plugins/Sidebar/media/RateLimit.coffee ---- */ @@ -87,7 +86,6 @@ }).call(this); - /* ---- plugins/Sidebar/media/Scrollable.js ---- */ @@ -260,6 +258,9 @@ window.initScrollable = function () { })(this)); this.fixbutton.parent().on("click touchend touchcancel", (function(_this) { return function(e) { + if ((+(new Date)) - _this.dragStarted < 100) { + window.top.location = _this.fixbutton.find(".fixbutton-bg").attr("href"); + } return _this.stopDrag(); }; })(this)); @@ -647,6 +648,13 @@ window.initScrollable = function () { }); }; })(this)); + this.tag.find(".close").off("click touchend").on("click touchend", (function(_this) { + return function(e) { + _this.startDrag(); + _this.stopDrag(); + return false; + }; + })(this)); return this.loadGlobe(); }; From eb97ea4f34a021813d87a4aab739a0d96c7c04a8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 29 Jul 2017 13:46:04 +0200 Subject: [PATCH 0205/2570] Rev2169 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index af0e32d9..f18ce94e 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.7" - self.rev = 2168 + self.rev = 2169 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 5b6d4f972edcbec3c4e6ddc93e1fc2c5018678d1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 30 Jul 2017 02:20:30 +0200 Subject: [PATCH 0206/2570] Version 0.5.7 Changelog --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bab805b..b9663a32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +## ZeroNet 0.5.7 (2017-07-19) +### Added + - New plugin: CORS to request read permission to other site's content + - New API command: userSetSettings/userGetSettings to store site's settings in users.json + - Avoid file download if the file size does not match with the requested one + - JavaScript and wrapper less file access using /raw/ prefix ([Example](http://127.0.0.1:43110/raw/1AsRLpuRxr3pb9p3TKoMXPSWHzh6i7fMGi/en.tar.gz/index.html)) + - --silent command line option to disable logging to stdout + + +### Changed + - Better error reporting on sign/verification errors + - More test for sign and verification process + - Update to OpenSSL v1.0.2l + - Limit compressed files to 6MB to avoid zip/tar.gz bomb + - Allow space, [], () characters in filenames + - Disable cross-site resource loading to improve privacy. [Reported by Beardog108] + - Download directly accessed Pdf/Svg/Swf files instead of displaying them to avoid wrapper escape using in JS in SVG file. [Reported by Beardog108] + - Disallow potentially unsafe regular expressions to avoid ReDoS [Reported by MuxZeroNet] + +### Fixed + - Detecting data directory when running Windows distribution exe [Reported by Plasmmer] + - OpenSSL loading under Android 6+ + - Error on exiting when no connection server started + + ## ZeroNet 0.5.6 (2017-06-15) ### Added - Callback for certSelect API command From b35bff4164ce62d8058494afdddd493e19cfac7d Mon Sep 17 00:00:00 2001 From: cclauss Date: Mon, 31 Jul 2017 11:55:03 +0200 Subject: [PATCH 0207/2570] from Debug import Debug Solves the unresolved name on lines 164 and 200 --- plugins/OptionalManager/ContentDbPlugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/OptionalManager/ContentDbPlugin.py b/plugins/OptionalManager/ContentDbPlugin.py index d1a1dd29..dcb0c5cd 100644 --- a/plugins/OptionalManager/ContentDbPlugin.py +++ b/plugins/OptionalManager/ContentDbPlugin.py @@ -8,6 +8,7 @@ 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 From 504b147cee4daecf54c5e26643c8051049fcfc59 Mon Sep 17 00:00:00 2001 From: cclauss Date: Mon, 31 Jul 2017 12:06:11 +0200 Subject: [PATCH 0208/2570] import math Resolves the undefined name on line 54 --- src/lib/BitcoinECC/newBitcoinECC.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/BitcoinECC/newBitcoinECC.py b/src/lib/BitcoinECC/newBitcoinECC.py index b09386bc..65b64880 100644 --- a/src/lib/BitcoinECC/newBitcoinECC.py +++ b/src/lib/BitcoinECC/newBitcoinECC.py @@ -1,6 +1,7 @@ import random import hashlib import base64 +import math class GaussInt: def __init__(self,x,y,n,p=0): From 6cb700b30290230c92d0460b51257cdc2621fdb7 Mon Sep 17 00:00:00 2001 From: cclauss Date: Mon, 31 Jul 2017 12:26:15 +0200 Subject: [PATCH 0209/2570] import threading and queue.Empty Fixes the undefined name on line 644 --- plugins/Trayicon/lib/notificationicon.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/Trayicon/lib/notificationicon.py b/plugins/Trayicon/lib/notificationicon.py index bc7e2175..128c3beb 100644 --- a/plugins/Trayicon/lib/notificationicon.py +++ b/plugins/Trayicon/lib/notificationicon.py @@ -8,6 +8,11 @@ import os import uuid import time import gevent +import threading +try: + from queue import Empty as queue_Empty # Python 3 +except ImportError: + from Queue import Empty as queue_Empty # Python 2 __all__ = ['NotificationIcon'] @@ -666,7 +671,7 @@ class NotificationIcon(object): while not self._pumpqueue.empty(): callable = self._pumpqueue.get(False) callable() - except Queue.Empty: + except queue_Empty: pass @@ -721,4 +726,4 @@ if __name__ == "__main__": def goodbye(): print "You are now leaving the Python sector." - ni._run() \ No newline at end of file + ni._run() From 231195ba0a6f0007496881f4d2c4ac5910053662 Mon Sep 17 00:00:00 2001 From: cclauss Date: Mon, 31 Jul 2017 12:28:55 +0200 Subject: [PATCH 0210/2570] import binascii Solves the undefined name on line 30 --- src/lib/subtl/subtl.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/subtl/subtl.py b/src/lib/subtl/subtl.py index bf6acad1..f53432cf 100644 --- a/src/lib/subtl/subtl.py +++ b/src/lib/subtl/subtl.py @@ -1,6 +1,7 @@ ''' Based on the specification at http://bittorrent.org/beps/bep_0015.html ''' +import binascii import random import struct import time From e1d3a31c6c0df371ee8fdb46e0c3d97fc99e7145 Mon Sep 17 00:00:00 2001 From: cclauss Date: Mon, 31 Jul 2017 13:08:22 +0200 Subject: [PATCH 0211/2570] data_unpack --> data_unpacked @shortcutme Is this the right way to fix these two undefined name errors? --- plugins/Stats/StatsPlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py index 2b2b7353..afba9086 100644 --- a/plugins/Stats/StatsPlugin.py +++ b/plugins/Stats/StatsPlugin.py @@ -526,7 +526,7 @@ class UiRequestPlugin(object): for y in range(1000): data_unpacked = msgpack.unpackb(data_packed) yield "." - assert data == data_unpacked, "%s != %s" % (data_unpack, data) + assert data == data_unpacked, "%s != %s" % (data_unpacked, data) with benchmark("streaming unpack 5K x 10 000", 1.4): for i in range(10): @@ -536,7 +536,7 @@ class UiRequestPlugin(object): for data_unpacked in unpacker: pass yield "." - assert data == data_unpacked, "%s != %s" % (data_unpack, data) + assert data == data_unpacked, "%s != %s" % (data_unpacked, data) # Db from Db import Db From b8d68e2589468fb6fe97d599b1a3a39e173a4214 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 31 Jul 2017 14:34:06 +0200 Subject: [PATCH 0212/2570] Fix undefined ssl error --- src/util/SslPatch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/util/SslPatch.py b/src/util/SslPatch.py index 40e70820..5cc8e1b6 100644 --- a/src/util/SslPatch.py +++ b/src/util/SslPatch.py @@ -99,9 +99,9 @@ def new_sslwrap( cert_reqs=__ssl__.CERT_NONE, ssl_version=__ssl__.PROTOCOL_SSLv23, ca_certs=None, ciphers=None ): - context = __ssl__.SSLContext(ssl.PROTOCOL_SSLv23) - context.options |= ssl.OP_NO_SSLv2 - context.options |= ssl.OP_NO_SSLv3 + context = __ssl__.SSLContext(__ssl__.PROTOCOL_SSLv23) + context.options |= __ssl__.OP_NO_SSLv2 + context.options |= __ssl__.OP_NO_SSLv3 context.verify_mode = cert_reqs or __ssl__.CERT_NONE if ca_certs: context.load_verify_locations(ca_certs) From 9e13994c54c65de050a55e28c11c29c1c1c3e3b1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 31 Jul 2017 14:34:18 +0200 Subject: [PATCH 0213/2570] Formatting SslPatch --- src/util/SslPatch.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/util/SslPatch.py b/src/util/SslPatch.py index 5cc8e1b6..a83f7fac 100644 --- a/src/util/SslPatch.py +++ b/src/util/SslPatch.py @@ -15,9 +15,9 @@ def getLibraryPath(): lib_path = os.path.dirname(os.path.abspath(__file__)) + "/../lib/opensslVerify/libeay32.dll" elif sys.platform == "cygwin": lib_path = "/bin/cygcrypto-1.0.0.dll" - elif os.path.isfile("../lib/libcrypto.so"): # ZeroBundle OSX + elif os.path.isfile("../lib/libcrypto.so"): # ZeroBundle OSX lib_path = "../lib/libcrypto.so" - elif os.path.isfile("/opt/lib/libcrypto.so.1.0.0"): # For optware and entware + elif os.path.isfile("/opt/lib/libcrypto.so.1.0.0"): # For optware and entware lib_path = "/opt/lib/libcrypto.so.1.0.0" else: lib_path = "/usr/local/ssl/lib/libcrypto.so" @@ -32,8 +32,10 @@ def getLibraryPath(): except Exception, err: logging.debug("OpenSSL lib not found in: %s (%s)" % (lib_dir, err)) - return (ctypes.util.find_library('ssl.so.1.0') or ctypes.util.find_library('ssl') or ctypes.util.find_library('crypto') or ctypes.util.find_library('libcrypto') or 'libeay32') - + return ( + ctypes.util.find_library('ssl.so.1.0') or ctypes.util.find_library('ssl') or + ctypes.util.find_library('crypto') or ctypes.util.find_library('libcrypto') or 'libeay32' + ) def openLibrary(): From c96dce3d0b4c4fec11accc1fb0e6b3b7823abf4b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 31 Jul 2017 14:35:01 +0200 Subject: [PATCH 0214/2570] Fix site cleanup from content.db --- src/Site/SiteManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index 0a7e22db..07180d2d 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -56,7 +56,7 @@ class SiteManager(object): # Remove orpan sites from contentdb content_db = ContentDb.getContentDb() - for row in content_db.execute("SELECT * FROM site"): + for row in content_db.execute("SELECT * FROM site").fetchall(): address = row["address"] if address not in self.sites: self.log.info("Deleting orphan site from content.db: %s" % address) From ac230219eed41102b8442288a9f945f937e3bd44 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 31 Jul 2017 14:35:48 +0200 Subject: [PATCH 0215/2570] DB cleanup error is not fatal --- src/Site/SiteManager.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index 07180d2d..dbdf244d 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -60,7 +60,12 @@ class SiteManager(object): address = row["address"] if address not in self.sites: self.log.info("Deleting orphan site from content.db: %s" % address) - content_db.execute("DELETE FROM site WHERE ?", {"address": address}) + + try: + content_db.execute("DELETE FROM site WHERE ?", {"address": address}) + except Exception as err: + self.log.error("Can't delete site %s from content_db: %s" % (address, err)) + if address in content_db.site_ids: del content_db.site_ids[address] if address in content_db.sites: From 4cd393e4d8f96f8fed0f4c629c55605d26b126e3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 31 Jul 2017 14:39:21 +0200 Subject: [PATCH 0216/2570] Auto download optional files command line argument --- src/Config.py | 1 + src/Site/Site.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/Config.py b/src/Config.py index f18ce94e..7ee7710e 100644 --- a/src/Config.py +++ b/src/Config.py @@ -224,6 +224,7 @@ class Config(object): self.parser.add_argument("--fix_float_decimals", help='Fix content.json modification date float precision on verification', type='bool', choices=[True, False], default=fix_float_decimals) self.parser.add_argument("--db_mode", choices=["speed", "security"], default="speed") + self.parser.add_argument("--download_optional", choices=["manual", "auto"], default="manual") self.parser.add_argument('--coffeescript_compiler', help='Coffeescript compiler for developing', default=coffeescript, metavar='executable_path') diff --git a/src/Site/Site.py b/src/Site/Site.py index 10997b04..8819ada5 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -96,6 +96,8 @@ class Site(object): "own": False, "serving": True, "permissions": [], "added": int(time.time()), "optional_downloaded": 0, "size_optional": 0 } # Default + if config.download_optional == "auto": + self.settings["autodownloadoptional"] = True # Add admin permissions to homepage if self.address == config.homepage and "ADMIN" not in self.settings["permissions"]: From bfd57561b78bebcba837719a49b0e07f2046abe8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 31 Jul 2017 14:39:27 +0200 Subject: [PATCH 0217/2570] Rev2170 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 7ee7710e..c14f2a58 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.7" - self.rev = 2169 + self.rev = 2170 self.argv = argv self.action = None self.config_file = "zeronet.conf" From c1aff56cf2ae7d30e4062f962b8a8bf841d0aff4 Mon Sep 17 00:00:00 2001 From: "Andrew (anoa)" Date: Fri, 4 Aug 2017 15:02:24 -0700 Subject: [PATCH 0218/2570] Small grammer fixes --- src/Translate/languages/da.json | 4 ++-- src/Translate/languages/de.json | 4 ++-- src/Translate/languages/es.json | 4 ++-- src/Translate/languages/fr.json | 4 ++-- src/Translate/languages/hu.json | 4 ++-- src/Translate/languages/it.json | 4 ++-- src/Translate/languages/nl.json | 4 ++-- src/Translate/languages/pl.json | 4 ++-- src/Translate/languages/pt-br.json | 4 ++-- src/Translate/languages/ru.json | 4 ++-- src/Translate/languages/tr.json | 4 ++-- src/Translate/languages/zh-tw.json | 4 ++-- src/Translate/languages/zh.json | 4 ++-- src/Ui/UiWebsocket.py | 4 ++-- 14 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/Translate/languages/da.json b/src/Translate/languages/da.json index 90509dd1..87ff8954 100644 --- a/src/Translate/languages/da.json +++ b/src/Translate/languages/da.json @@ -1,11 +1,11 @@ { - "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "Tillykke, din port ({0}) er åben.
Du er nu fuld klient på ZeroNet!", + "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "Tillykke, din port ({0}) er åben.
Du er nu fuld klient på ZeroNet!", "Tor mode active, every connection using Onion route.": "TOR er aktiv, alle forbindelser anvender Onions.", "Successfully started Tor onion hidden services.": "OK. Startede TOR skjult onion service.", "Unable to start hidden services, please check your config.": "Fejl. Kunne ikke starte TOR skjult onion service. Tjek din opsætning!", "For faster connections open {0} port on your router.": "Åben port {0} på din router for hurtigere forbindelse.", "Your connection is restricted. Please, open {0} port on your router": "Begrænset forbindelse. Åben venligst port {0} på din router", - "or configure Tor to become full member of ZeroNet network.": "eller opsæt TOR for fuld adgang til ZeroNet!", + "or configure Tor to become a full member of the ZeroNet network.": "eller opsæt TOR for fuld adgang til ZeroNet!", "Select account you want to use in this site:": "Vælg bruger til brug på denne side:", "currently selected": "nuværende bruger", diff --git a/src/Translate/languages/de.json b/src/Translate/languages/de.json index ceedab24..d234cdad 100644 --- a/src/Translate/languages/de.json +++ b/src/Translate/languages/de.json @@ -1,11 +1,11 @@ { - "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "Gratulation, dein Port {0} ist offen.
Du bist ein volles Mitglied des ZeroNet Netzwerks!", + "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "Gratulation, dein Port {0} ist offen.
Du bist ein volles Mitglied des ZeroNet Netzwerks!", "Tor mode active, every connection using Onion route.": "Tor modus aktiv, jede verbindung nutzt die Onion Route.", "Successfully started Tor onion hidden services.": "Tor versteckte Dienste erfolgreich gestartet.", "Unable to start hidden services, please check your config.": "Nicht möglich versteckte Dienste zu starten.", "For faster connections open {0} port on your router.": "Für schnellere verbindungen öffne Port {0} auf deinem Router.", "Your connection is restricted. Please, open {0} port on your router": "Deine Verbindung ist eingeschränkt. Bitte öffne Port {0} auf deinem Router", - "or configure Tor to become full member of ZeroNet network.": "oder konfiguriere Tor um ein volles Mitglied des ZeroNet Netzwerks zu werden.", + "or configure Tor to become a full member of the ZeroNet network.": "oder konfiguriere Tor um ein volles Mitglied des ZeroNet Netzwerks zu werden.", "Select account you want to use in this site:": "Wähle das Konto, dass du auf dieser Seite benutzen willst:", "currently selected": "aktuell ausgewählt", diff --git a/src/Translate/languages/es.json b/src/Translate/languages/es.json index 659dc0e9..cfb6e26e 100644 --- a/src/Translate/languages/es.json +++ b/src/Translate/languages/es.json @@ -1,11 +1,11 @@ { - "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "¡Felicidades! tu puerto {0} está abierto.
¡Eres un miembro completo de la red Zeronet!", + "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "¡Felicidades! tu puerto {0} está abierto.
¡Eres un miembro completo de la red Zeronet!", "Tor mode active, every connection using Onion route.": "Modo Tor activado, cada conexión usa una ruta Onion.", "Successfully started Tor onion hidden services.": "Tor ha iniciado satisfactoriamente la ocultación de los servicios onion.", "Unable to start hidden services, please check your config.": "No se puedo iniciar los servicios ocultos, por favor comprueba tu configuración.", "For faster connections open {0} port on your router.": "Para conexiones más rápidas abre el puerto {0} en tu router.", "Your connection is restricted. Please, open {0} port on your router": "Tu conexión está limitada. Por favor, abre el puerto {0} en tu router", - "or configure Tor to become full member of ZeroNet network.": "o configura Tor para convertirte en un miembro completo de la red ZeroNet.", + "or configure Tor to become a full member of the ZeroNet network.": "o configura Tor para convertirte en un miembro completo de la red ZeroNet.", "Select account you want to use in this site:": "Selecciona la cuenta que quieres utilizar en este sitio:", "currently selected": "actualmente seleccionada", diff --git a/src/Translate/languages/fr.json b/src/Translate/languages/fr.json index b6881cc4..dc0e5754 100644 --- a/src/Translate/languages/fr.json +++ b/src/Translate/languages/fr.json @@ -1,11 +1,11 @@ { - "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "Félicitations, le port ({0}) est ouvert.
Vous êtes maintenant membre de ZeroNet!!", + "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "Félicitations, le port ({0}) est ouvert.
Vous êtes maintenant membre de ZeroNet!!", "Tor mode active, every connection using Onion route.": "Tor actif, toutes les connections utilisent un routage Onion.", "Successfully started Tor onion hidden services.": "Tor activé avec succès.", "Unable to start hidden services, please check your config.": "Incapable d'activer Tor, veuillez vérifier votre configuration.", "For faster connections open {0} port on your router.": "Pour une meilleure connectivité, ouvrez le port {0} sur votre routeur.", "Your connection is restricted. Please, open {0} port on your router": "Connectivité limitée. Veuillez ouvrir le port {0} sur votre routeur", - "or configure Tor to become full member of ZeroNet network.": "ou configurez Tor afin d'avoir accès aux pairs ZeroNet Onion.", + "or configure Tor to become a full member of the ZeroNet network.": "ou configurez Tor afin d'avoir accès aux pairs ZeroNet Onion.", "Select account you want to use in this site:": "Sélectionnez le compte que vous voulez utiliser pour ce site:", "currently selected": "présentement sélectionné", diff --git a/src/Translate/languages/hu.json b/src/Translate/languages/hu.json index eb43615f..666c31c7 100644 --- a/src/Translate/languages/hu.json +++ b/src/Translate/languages/hu.json @@ -1,11 +1,11 @@ { - "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "Gratulálunk, a portod ({0}) nyitva van.
Teljes értékű tagja vagy a hálózatnak!", + "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "Gratulálunk, a portod ({0}) nyitva van.
Teljes értékű tagja vagy a hálózatnak!", "Tor mode active, every connection using Onion route.": "Tor mód aktív, minden kapcsolat az Onion hálózaton keresztül történik.", "Successfully started Tor onion hidden services.": "Sikeresen elindultak a Tor onion titkos szolgáltatások.", "Unable to start hidden services, please check your config.": "Nem sikerült elindítani a Tor onion szolgáltatásokat. Kérjük, ellenőrizd a beállításokat!", "For faster connections open {0} port on your router.": "A gyorsabb kapcsolatok érdekében nyisd ki a {0} portot a routereden.", "Your connection is restricted. Please, open {0} port on your router": "A kapcsolatod korlátozott. Kérjük, nyisd ki a {0} portot a routereden", - "or configure Tor to become full member of ZeroNet network.": "vagy állítsd be a Tor kliensed, hogy teljes értékű tagja legyél a hálózatnak!", + "or configure Tor to become a full member of the ZeroNet network.": "vagy állítsd be a Tor kliensed, hogy teljes értékű tagja legyél a hálózatnak!", "Select account you want to use in this site:": "Válaszd ki az oldalhoz használt felhasználónevet:", "currently selected": "jelenleg kijelölt", diff --git a/src/Translate/languages/it.json b/src/Translate/languages/it.json index 95c69de0..3d7651d7 100644 --- a/src/Translate/languages/it.json +++ b/src/Translate/languages/it.json @@ -1,11 +1,11 @@ { - "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "Congratulazioni, la tua porta ({0}) è aperta.
Sei ora pieno membro della rete ZeroNet!", + "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "Congratulazioni, la tua porta ({0}) è aperta.
Sei ora pieno membro della rete ZeroNet!", "Tor mode active, every connection using Onion route.": "Modalità Tor attiva, ogni connessione sta usando la rete Onion.", "Successfully started Tor onion hidden services.": "Tor onion hidden service avviati con successo.", "Unable to start hidden services, please check your config.": "Impossibile avviare gli hidden service. Si prega di controllare la propria configurazione!", "For faster connections open {0} port on your router.": "Per avere connessioni più veloci devi aprire la porta {0} sul tuo router.", "Your connection is restricted. Please, open {0} port on your router": "La tua connessione è limitata. Dovresti aprire la porta {0} sul tuo router", - "or configure Tor to become full member of ZeroNet network.": "o configurare Tor per diventare pieno membro della rete ZeroNet!", + "or configure Tor to become a full member of the ZeroNet network.": "o configurare Tor per diventare pieno membro della rete ZeroNet!", "Select account you want to use in this site:": "Seleziona l'account che vuoi utilizzare per questo sito:", "currently selected": "attualmente selezionato", diff --git a/src/Translate/languages/nl.json b/src/Translate/languages/nl.json index d2cd241c..6e7d364a 100644 --- a/src/Translate/languages/nl.json +++ b/src/Translate/languages/nl.json @@ -1,11 +1,11 @@ { - "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "Gefeliciteerd, je poort {0} is geopend.
Je bent een volledig lid van het ZeroNet netwerk!", + "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "Gefeliciteerd, je poort {0} is geopend.
Je bent een volledig lid van het ZeroNet netwerk!", "Tor mode active, every connection using Onion route.": "Tor modus actief, elke verbinding gebruikt een Onion route.", "Successfully started Tor onion hidden services.": "Tor onion verborgen diensten zijn met succes gestart.", "Unable to start hidden services, please check your config.": "Het was niet mogelijk om verborgen diensten te starten, controleer je configuratie.", "For faster connections open {0} port on your router.": "Voor snellere verbindingen open je de poort {0} op je router.", "Your connection is restricted. Please, open {0} port on your router": "Je verbinding is beperkt. Open altjeblieft poort {0} op je router", - "or configure Tor to become full member of ZeroNet network.": "of configureer Tor om een volledig lid van het ZeroNet netwerk te worden.", + "or configure Tor to become a full member of the ZeroNet network.": "of configureer Tor om een volledig lid van het ZeroNet netwerk te worden.", "Select account you want to use in this site:": "Selecteer het account die je wilt gebruiken binnen deze site:", "currently selected": "huidige selectie", diff --git a/src/Translate/languages/pl.json b/src/Translate/languages/pl.json index e3087c73..32f03bee 100644 --- a/src/Translate/languages/pl.json +++ b/src/Translate/languages/pl.json @@ -1,11 +1,11 @@ { - "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "Gratulacje, twój port {0} jest otwarty.
Jesteś pełnoprawnym użytkownikiem sieci ZeroNet!", + "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "Gratulacje, twój port {0} jest otwarty.
Jesteś pełnoprawnym użytkownikiem sieci ZeroNet!", "Tor mode active, every connection using Onion route.": "Tryb Tor aktywny, każde połączenie przy użyciu trasy Cebulowej.", "Successfully started Tor onion hidden services.": "Pomyślnie zainicjowano ukryte usługi cebulowe Tor.", "Unable to start hidden services, please check your config.": "Niezdolny do uruchomienia ukrytych usług, proszę sprawdź swoją konfigurację.", "For faster connections open {0} port on your router.": "Dla szybszego połączenia otwórz {0} port w swoim routerze.", "Your connection is restricted. Please, open {0} port on your router": "Połączenie jest ograniczone. Proszę, otwórz port {0} w swoim routerze", - "or configure Tor to become full member of ZeroNet network.": "bądź skonfiguruj Tora by stać się pełnoprawnym użytkownikiem sieci ZeroNet.", + "or configure Tor to become a full member of the ZeroNet network.": "bądź skonfiguruj Tora by stać się pełnoprawnym użytkownikiem sieci ZeroNet.", "Select account you want to use in this site:": "Wybierz konto którego chcesz użyć na tej stronie:", "currently selected": "aktualnie wybrany", diff --git a/src/Translate/languages/pt-br.json b/src/Translate/languages/pt-br.json index d0aaf541..e9700ed3 100644 --- a/src/Translate/languages/pt-br.json +++ b/src/Translate/languages/pt-br.json @@ -1,11 +1,11 @@ { - "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "Parabéns, a porta{0} está aberta.
Você é um membro completo da rede ZeroNet!", + "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "Parabéns, a porta{0} está aberta.
Você é um membro completo da rede ZeroNet!", "Tor mode active, every connection using Onion route.": "Modo Tor ativado, todas as conexões usam a rota Onion.", "Successfully started Tor onion hidden services.": "Os serviços ocultos Tor onion foram inciados com sucesso.", "Unable to start hidden services, please check your config.": "Não foi possível iniciar os serviços ocultos, por favor verifique suas configurações.", "For faster connections open {0} port on your router.": "Para conexões mais rápidas, abra a porta {0} em seu roteador.", "Your connection is restricted. Please, open {0} port on your router": "Sua conexão está restrita. Por favor, abra a porta {0} em seu roteador", - "or configure Tor to become full member of ZeroNet network.": "ou configure o Tor para se tornar um membro completo da rede ZeroNet.", + "or configure Tor to become a full member of the ZeroNet network.": "ou configure o Tor para se tornar um membro completo da rede ZeroNet.", "Select account you want to use in this site:": "Selecione a conta que deseja usar nesse site:", "currently selected": "atualmente selecionada", diff --git a/src/Translate/languages/ru.json b/src/Translate/languages/ru.json index 5bb1c4e4..5996cd19 100644 --- a/src/Translate/languages/ru.json +++ b/src/Translate/languages/ru.json @@ -1,11 +1,11 @@ { - "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "Поздравляем, ваш порт {0} открыт.
Вы полноценный участник сети ZeroNet!", + "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "Поздравляем, ваш порт {0} открыт.
Вы полноценный участник сети ZeroNet!", "Tor mode active, every connection using Onion route.": "Режим Tor включен, все соединения осуществляются через Tor.", "Successfully started Tor onion hidden services.": "Скрытый сервис Tor запущено успешно.", "Unable to start hidden services, please check your config.": "Ошибка при запуске скрытого сервиса, пожалуйста проверьте настройки", "For faster connections open {0} port on your router.": "Для более быстрой работы сети откройте {0} порт на вашем роутере.", "Your connection is restricted. Please, open {0} port on your router": "Подключение ограничено. Пожалуйста откройте {0} порт на вашем роутере", - "or configure Tor to become full member of ZeroNet network.": "или настройте Tor что бы стать полноценным участником сети ZeroNet.", + "or configure Tor to become a full member of the ZeroNet network.": "или настройте Tor что бы стать полноценным участником сети ZeroNet.", "Select account you want to use in this site:": "Выберите аккаунт для использования на этом сайте:", "currently selected": "сейчас выбран", diff --git a/src/Translate/languages/tr.json b/src/Translate/languages/tr.json index 0bdabd89..c27fc1f4 100644 --- a/src/Translate/languages/tr.json +++ b/src/Translate/languages/tr.json @@ -1,11 +1,11 @@ { - "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "Tebrikler, portunuz ({0}) açık.
Artık ZeroNet ağına katıldınız!", + "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "Tebrikler, portunuz ({0}) açık.
Artık ZeroNet ağına katıldınız!", "Tor mode active, every connection using Onion route.": "Tor aktif, tüm bağlantılar Onion yönlendircisini kullanıyor.", "Successfully started Tor onion hidden services.": "Gizli Tor hizmetleri başlatıldı.", "Unable to start hidden services, please check your config.": "Gizli hizmetler başlatılamadı, lütfen ayarlarınızı kontrol ediniz.", "For faster connections open {0} port on your router.": "Daha hızlı bağlantı için {0} nolu portu bilgisayarınıza yönlendirin.", "Your connection is restricted. Please, open {0} port on your router": "Sınırlı bağlantı. Lütfen, {0} nolu portu bilgisayarınıza yönlendirin", - "or configure Tor to become full member of ZeroNet network.": "ya da ZeroNet ağına tam olarak katılabilmek için Tor'u kullanın.", + "or configure Tor to become a full member of the ZeroNet network.": "ya da ZeroNet ağına tam olarak katılabilmek için Tor'u kullanın.", "Select account you want to use in this site:": "Bu sitede kullanmak için bir hesap seçiniz:", "currently selected": "kullanılan", diff --git a/src/Translate/languages/zh-tw.json b/src/Translate/languages/zh-tw.json index a30dd023..ea0130a0 100644 --- a/src/Translate/languages/zh-tw.json +++ b/src/Translate/languages/zh-tw.json @@ -1,11 +1,11 @@ { - "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "祝賀,你的埠 ({0}) 已經打開。
你已經是 ZeroNet 網路的正式成員了!", + "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "祝賀,你的埠 ({0}) 已經打開。
你已經是 ZeroNet 網路的正式成員了!", "Tor mode active, every connection using Onion route.": "Tor 模式啟用,每個連接正在使用洋蔥路由。", "Successfully started Tor onion hidden services.": "成功啟動 Tor 洋蔥隱藏服務。", "Unable to start hidden services, please check your config.": "無法打開隱藏服務,請檢查你的配置。", "For faster connections open {0} port on your router.": "為了更快的連接請在路由器上打開 {0} 埠。", "Your connection is restricted. Please, open {0} port on your router": "你的連接受限制。請在你的路由器上打開 {0} 埠", - "or configure Tor to become full member of ZeroNet network.": "或者配置你的 Tor 來成為 ZeroNet 的正式成員。", + "or configure Tor to become a full member of the ZeroNet network.": "或者配置你的 Tor 來成為 ZeroNet 的正式成員。", "Select account you want to use in this site:": "選擇你要在這個網站使用的帳戶:", "currently selected": "當前選擇", diff --git a/src/Translate/languages/zh.json b/src/Translate/languages/zh.json index e0b1232f..2d6c1f5d 100644 --- a/src/Translate/languages/zh.json +++ b/src/Translate/languages/zh.json @@ -1,11 +1,11 @@ { - "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "祝贺,你的端口 ({0}) 已经打开。
你已经是 ZeroNet 网络的正式成员了!", + "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "祝贺,你的端口 ({0}) 已经打开。
你已经是 ZeroNet 网络的正式成员了!", "Tor mode active, every connection using Onion route.": "Tor 模式启用,每个连接正在使用洋葱路由。", "Successfully started Tor onion hidden services.": "成功启动 Tor 洋葱隐藏服务。", "Unable to start hidden services, please check your config.": "无法打开隐藏服务,请检查你的配置。", "For faster connections open {0} port on your router.": "为了更快的连接请在路由器上打开 {0} 端口。", "Your connection is restricted. Please, open {0} port on your router": "你的连接受限制。请在你的路由器上打开 {0} 端口", - "or configure Tor to become full member of ZeroNet network.": "或者配置你的 Tor 来成为 ZeroNet 的正式成员。", + "or configure Tor to become a full member of the ZeroNet network.": "或者配置你的 Tor 来成为 ZeroNet 的正式成员。", "Select account you want to use in this site:": "选择你要在这个网站使用的帐户:", "currently selected": "当前选择", diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 2d29652a..ee0759d6 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -103,7 +103,7 @@ class UiWebsocket(object): if file_server.port_opened is True: self.site.notifications.append([ "done", - _["Congratulation, your port {0} is opened.
You are full member of ZeroNet network!"].format(config.fileserver_port), + _["Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!"].format(config.fileserver_port), 10000 ]) elif config.tor == "always" and file_server.tor_manager.start_onions: @@ -138,7 +138,7 @@ class UiWebsocket(object): "error", _(u""" {_[Your connection is restricted. Please, open {0} port on your router]}
- {_[or configure Tor to become full member of ZeroNet network.]} + {_[or configure Tor to become a full member of the ZeroNet network.]} """).format(config.fileserver_port), 0 ]) From 6a1c5d96acdea98af03a8cadbd1de0bed4f3059d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 9 Aug 2017 14:15:10 +0200 Subject: [PATCH 0219/2570] Disable bz2 support due incompatibility issues --- plugins/FilePack/FilePackPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/FilePack/FilePackPlugin.py b/plugins/FilePack/FilePackPlugin.py index 08725960..f7016ee5 100644 --- a/plugins/FilePack/FilePackPlugin.py +++ b/plugins/FilePack/FilePackPlugin.py @@ -81,7 +81,7 @@ class UiRequestPlugin(object): @PluginManager.registerTo("SiteStorage") class SiteStoragePlugin(object): def isFile(self, inner_path): - if ".zip/" in inner_path or ".tar.gz/" in inner_path or ".tar.bz2/" in inner_path: + if ".zip/" in inner_path or ".tar.gz/" in inner_path: match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))/(.*)", inner_path) inner_archive_path, path_within = match.groups() return super(SiteStoragePlugin, self).isFile(inner_archive_path) From bdb5e1597cabc4eca2f18f0a603df82734c9b6af Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 9 Aug 2017 14:15:44 +0200 Subject: [PATCH 0220/2570] Sign automatically on sidebar save button if possible --- plugins/Sidebar/media/Sidebar.coffee | 2 ++ plugins/Sidebar/media/all.js | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee index b5672687..9f891260 100644 --- a/plugins/Sidebar/media/Sidebar.coffee +++ b/plugins/Sidebar/media/Sidebar.coffee @@ -349,6 +349,8 @@ class Sidebar extends Class 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 diff --git a/plugins/Sidebar/media/all.js b/plugins/Sidebar/media/all.js index 8f36146e..0a54d46f 100644 --- a/plugins/Sidebar/media/all.js +++ b/plugins/Sidebar/media/all.js @@ -593,6 +593,13 @@ window.initScrollable = function () { return 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 + }); + } return _this.updateHtmlTag(); } }); From 4f729aa98f123ec309f827c770f003fd0f2c6cc1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 9 Aug 2017 14:17:58 +0200 Subject: [PATCH 0221/2570] Only use pure-python msgpack for socket streaming --- src/Config.py | 2 +- src/Connection/Connection.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Config.py b/src/Config.py index c14f2a58..4164a27a 100644 --- a/src/Config.py +++ b/src/Config.py @@ -220,7 +220,7 @@ class Config(object): self.parser.add_argument('--stream_downloads', help='Stream download directly to files (experimental)', type='bool', choices=[True, False], default=False) self.parser.add_argument("--msgpack_purepython", help='Use less memory, but a bit more CPU power', - type='bool', choices=[True, False], default=True) + type='bool', choices=[True, False], default=False) self.parser.add_argument("--fix_float_decimals", help='Fix content.json modification date float precision on verification', type='bool', choices=[True, False], default=fix_float_decimals) self.parser.add_argument("--db_mode", choices=["speed", "security"], default="speed") diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index baf50188..2cfdb27d 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -3,6 +3,7 @@ import time import gevent import msgpack +import msgpack.fallback from Config import config from Debug import Debug @@ -138,7 +139,7 @@ class Connection(object): self.connected = True buff_len = 0 - self.unpacker = msgpack.Unpacker() + self.unpacker = msgpack.fallback.Unpacker() # Due memory problems of C version try: while not self.closed: buff = self.sock.recv(16 * 1024) @@ -153,7 +154,7 @@ class Connection(object): self.server.bytes_recv += buff_len if not self.unpacker: - self.unpacker = msgpack.Unpacker() + self.unpacker = msgpack.fallback.Unpacker() self.unpacker.feed(buff) buff = None for message in self.unpacker: From f451ce6c910e0edd2f1ba9ed77216cf23628b2f6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 9 Aug 2017 14:19:09 +0200 Subject: [PATCH 0222/2570] 6MB LimitedGzip file helper to avoid gzip bombs --- src/util/helper.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/util/helper.py b/src/util/helper.py index ac3037af..89e41042 100644 --- a/src/util/helper.py +++ b/src/util/helper.py @@ -194,3 +194,11 @@ def socketBindMonkeyPatch(bind_ip, bind_port): socket.bind_addr = (bind_ip, int(bind_port)) socket.create_connection_original = socket.create_connection socket.create_connection = create_connection + + +def limitedGzipFile(*args, **kwargs): + import gzip + class LimitedGzipFile(gzip.GzipFile): + def read(self, size=-1): + return super(LimitedGzipFile, self).read(1024*1024*6) + return LimitedGzipFile(*args, **kwargs) From b503d59c49da148346aa9893d7287b8e9ccb46d2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 9 Aug 2017 14:19:39 +0200 Subject: [PATCH 0223/2570] Support gzip compressed database files --- src/Db/Db.py | 8 ++++++-- src/Site/SiteStorage.py | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Db/Db.py b/src/Db/Db.py index c2de5509..e4939508 100644 --- a/src/Db/Db.py +++ b/src/Db/Db.py @@ -9,6 +9,7 @@ import gevent from DbCursor import DbCursor from Config import config from util import SafeRe +from util import helper opened_dbs = [] @@ -244,12 +245,15 @@ class Db(object): # Load the json file try: if file is None: # Open file is not file object passed - file = open(file_path) + file = open(file_path, "rb") if file is False: # File deleted data = {} else: - data = json.load(file) + if file_path.endswith("json.gz"): + data = json.load(helper.limitedGzipFile(fileobj=file)) + else: + data = json.load(file) except Exception, err: self.log.debug("Json file %s load error: %s" % (file_path, err)) data = {} diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index 41c4a14e..bfdd7840 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -85,7 +85,7 @@ class SiteStorage(object): # 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 content.get("files", {}).keys() + content.get("files_optional", {}).keys(): - if not file_relative_path.endswith(".json"): + if not file_relative_path.endswith(".json") and not file_relative_path.endswith("json.gz"): 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 / @@ -239,7 +239,7 @@ class SiteStorage(object): if self.has_db: self.closeDb() self.openDb() - elif not config.disable_db and inner_path.endswith(".json") and self.has_db: # Load json file to db + elif not config.disable_db and (inner_path.endswith(".json") or inner_path.endswith(".json.gz")) 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)) try: From ef4a4acbc0342c5f815b3b9c5fc8ba67c5c1b656 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 9 Aug 2017 14:19:56 +0200 Subject: [PATCH 0224/2570] Only display read error in verbose mode --- src/File/FileRequest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index a8c09ace..df812d8c 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -229,7 +229,8 @@ class FileRequest(object): self.log.debug("GetFile %s %s request error: %s" % (self.connection, params["inner_path"], Debug.formatException(err))) self.response({"error": "File read error: %s" % err}) except Exception, err: - self.log.debug("GetFile read error: %s" % Debug.formatException(err)) + if config.verbose: + self.log.debug("GetFile read error: %s" % Debug.formatException(err)) self.response({"error": "File read error"}) return False From 24982aee42bcce09c75a43eb6a2f88e4b25849c8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 9 Aug 2017 14:20:20 +0200 Subject: [PATCH 0225/2570] Log invalid wrapper messages --- src/Ui/media/Wrapper.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index 3e3f79ea..2cbaf91f 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -88,6 +88,7 @@ class Wrapper message = e.data # Invalid message (probably not for us) if not message.cmd + @log "Invalid message:", message return false # Test nonce security to avoid third-party messages From d0937647318c005233e5faea2f8625965c6e34be Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 9 Aug 2017 14:20:49 +0200 Subject: [PATCH 0226/2570] New API command: fileNeed to request optional file --- src/Ui/UiWebsocket.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 2d29652a..5168ad9b 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -605,6 +605,14 @@ class UiWebsocket(object): body = base64.b64encode(body) return self.response(to, body) + def actionFileNeed(self, to, inner_path, timeout=300): + try: + with gevent.Timeout(timeout): + self.site.needFile(inner_path, priority=6) + except Exception, err: + return self.response(to, {"error": str(err)}) + return self.response(to, "ok") + def actionFileRules(self, to, inner_path): rules = self.site.content_manager.getRules(inner_path) if inner_path.endswith("content.json") and rules: From 65d68fb8acbc0752c8f78dc4a894d2f28c735fa6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 9 Aug 2017 14:21:09 +0200 Subject: [PATCH 0227/2570] Compile Ui js --- src/Ui/media/all.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index 7f9cc03f..800fc736 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -807,7 +807,6 @@ jQuery.extend( jQuery.easing, }).call(this); - /* ---- src/Ui/media/Wrapper.coffee ---- */ @@ -932,6 +931,7 @@ jQuery.extend( jQuery.easing, } message = e.data; if (!message.cmd) { + this.log("Invalid message:", message); return false; } if (window.postmessage_nonce_security && message.wrapper_nonce !== window.wrapper_nonce) { @@ -1554,4 +1554,4 @@ jQuery.extend( jQuery.easing, window.wrapper = new Wrapper(ws_url); -}).call(this); \ No newline at end of file +}).call(this); From 5a08545e2d1d2059644a8f57b1bb3e2f815de56a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 9 Aug 2017 14:21:44 +0200 Subject: [PATCH 0228/2570] Only recalculate site sizes on exit --- src/Site/SiteManager.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index dbdf244d..a0cb14a9 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -21,7 +21,7 @@ class SiteManager(object): self.sites = None self.loaded = False gevent.spawn(self.saveTimer) - atexit.register(self.save) + atexit.register(lambda: self.save(recalculate_size=True)) # Load all sites from data/sites.json def load(self, cleanup=True): @@ -75,7 +75,7 @@ class SiteManager(object): self.log.debug("SiteManager added %s sites" % added) self.loaded = True - def save(self): + def save(self, recalculate_size=False): if not self.sites: self.log.debug("Save skipped: No sites found") return @@ -85,27 +85,33 @@ class SiteManager(object): s = time.time() data = {} # Generate data file + s = time.time() for address, site in self.list().iteritems(): - site.settings["size"] = site.content_manager.getTotalSize() # Update site size + if recalculate_size: + site.settings["size"] = site.content_manager.getTotalSize() # Update site size data[address] = site.settings data[address]["cache"] = {} data[address]["cache"]["bad_files"] = site.bad_files data[address]["cache"]["hashfield"] = site.content_manager.hashfield.tostring().encode("base64") + time_generate = time.time() - s + s = time.time() if data: helper.atomicWrite("%s/sites.json" % config.data_dir, json.dumps(data, indent=2, sort_keys=True)) else: self.log.debug("Save error: No data") + time_write = time.time() - s + # Remove cache from site settings for address, site in self.list().iteritems(): site.settings["cache"] = {} - self.log.debug("Saved sites in %.2fs" % (time.time() - s)) + self.log.debug("Saved sites in %.2fs (generate: %.2fs, write: %.2fs)" % (time.time() - s, time_generate, time_write)) def saveTimer(self): while 1: time.sleep(60 * 10) - self.save() + self.save(recalculate_size=True) # Checks if its a valid address def isAddress(self, address): From 29aa1b7b9348e8333ac2a5bd8f49466b3f869781 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 9 Aug 2017 14:21:59 +0200 Subject: [PATCH 0229/2570] Log file diffing timings --- src/Site/Site.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 8819ada5..9e906658 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -165,14 +165,29 @@ class Site(object): diff_actions = diffs.get(file_relative_path) if diff_actions and self.bad_files.get(file_inner_path): try: + s = time.time() new_file = Diff.patch(self.storage.open(file_inner_path, "rb"), diff_actions) new_file.seek(0) + time_diff = time.time() - s + + s = time.time() diff_success = self.content_manager.verifyFile(file_inner_path, new_file) + time_verify = time.time() - s + if diff_success: - self.log.debug("Patched successfully: %s" % file_inner_path) + s = time.time() new_file.seek(0) self.storage.write(file_inner_path, new_file) + time_write = time.time() - s + + s = time.time() self.onFileDone(file_inner_path) + time_on_done = time.time() - s + + self.log.debug( + "Patched successfully: %s (diff: %.3fs, verify: %.3fs, write: %.3fs, on_done: %.3fs)" % + (file_inner_path, time_diff, time_verify, time_write, time_on_done) + ) except Exception, err: self.log.debug("Failed to patch %s: %s" % (file_inner_path, err)) diff_success = False From 0a380dc0ab3c36eb9a3ba8bee84acc3d9fff70d3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 9 Aug 2017 14:22:17 +0200 Subject: [PATCH 0230/2570] Rev2180 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 4164a27a..c1e43905 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.7" - self.rev = 2170 + self.rev = 2180 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 1db2327b3dc633be6ba05b00080c01d4cdc639b2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 15 Aug 2017 02:40:38 +0200 Subject: [PATCH 0231/2570] Rev2180, Allow ajax requests using ajax_key http get parameter --- src/Config.py | 2 +- src/Site/Site.py | 4 ++++ src/Ui/UiRequest.py | 24 ++++++++++++++++++++---- src/Ui/media/Wrapper.coffee | 2 ++ src/Ui/media/all.js | 6 ++++++ src/Ui/template/wrapper.html | 1 + 6 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/Config.py b/src/Config.py index c1e43905..d3f93846 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.7" - self.rev = 2180 + self.rev = 2184 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Site/Site.py b/src/Site/Site.py index 9e906658..ea4bdda7 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -68,6 +68,10 @@ class Site(object): self.settings["wrapper_key"] = CryptHash.random() self.log.debug("New wrapper key: %s" % self.settings["wrapper_key"]) + if not self.settings.get("ajax_key"): # To auth websocket permissions + self.settings["ajax_key"] = CryptHash.random() + self.log.debug("New ajax key: %s" % self.settings["ajax_key"]) + def __str__(self): return "Site %s" % self.address_short diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 23353dce..97d0d25d 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -77,7 +77,10 @@ class UiRequest(object): content_type = self.getContentType("index.html") else: content_type = self.getContentType(path) - self.sendHeader(content_type=content_type) + + extra_headers = [("Access-Control-Allow-Origin", "null")] + + self.sendHeader(content_type=content_type, extra_headers=extra_headers) return "" if path == "/": @@ -194,7 +197,7 @@ class UiRequest(object): headers.append(("Keep-Alive", "max=25, timeout=30")) headers.append(("X-Frame-Options", "SAMEORIGIN")) if content_type != "text/html" and self.env.get("HTTP_REFERER") and self.isSameOrigin(self.getReferer(), self.getRequestUrl()): - headers.append(("Access-Control-Allow-Origin", "*")) # Allow load font files from css + headers.append(("Access-Control-Allow-Origin", "*")) # Allow load font files from css if noscript: headers.append(("Content-Security-Policy", "default-src 'none'; sandbox allow-top-navigation allow-forms; img-src 'self'; font-src 'self'; media-src 'self'; style-src 'self' 'unsafe-inline';")) @@ -369,6 +372,7 @@ class UiRequest(object): meta_tags=meta_tags, query_string=re.escape(query_string), wrapper_key=site.settings["wrapper_key"], + ajax_key=site.settings["ajax_key"], wrapper_nonce=wrapper_nonce, postmessage_nonce_security=postmessage_nonce_security, permissions=json.dumps(site.settings["permissions"]), @@ -450,13 +454,23 @@ class UiRequest(object): DebugMedia.merge(file_path) if not address or address == ".": return self.error403(path_parts["inner_path"]) + if os.path.isfile(file_path): # File exists - return self.actionFile(file_path, header_length=header_length, header_noscript=header_noscript) + header_allow_ajax = False + if self.get.get("ajax_key"): + site = SiteManager.site_manager.get(path_parts["request_address"]) + if self.get["ajax_key"] == site.settings["ajax_key"]: + header_allow_ajax = True + else: + return self.error403("Invalid ajax_key") + return self.actionFile(file_path, header_length=header_length, header_noscript=header_noscript, header_allow_ajax=header_allow_ajax) + elif os.path.isdir(file_path): # If this is actually a folder, add "/" and redirect if path_parts["inner_path"]: return self.actionRedirect("./%s/" % path_parts["inner_path"].split("/")[-1]) else: return self.actionRedirect("./%s/" % path_parts["address"]) + else: # File not exists, try to download if address not in SiteManager.site_manager.sites: # Only in case if site already started downloading return self.actionSiteAddPrompt(path) @@ -515,7 +529,7 @@ class UiRequest(object): return template # Stream a file to client - def actionFile(self, file_path, block_size=64 * 1024, send_header=True, header_length=True, header_noscript=False): + def actionFile(self, file_path, block_size=64 * 1024, send_header=True, header_length=True, header_noscript=False, header_allow_ajax=False): if ".." in file_path: raise Exception("Invalid path") if os.path.isfile(file_path): @@ -542,6 +556,8 @@ class UiRequest(object): status = 206 else: status = 200 + if header_allow_ajax: + extra_headers["Access-Control-Allow-Origin"] = "null" self.sendHeader(status, content_type=content_type, noscript=header_noscript, extra_headers=extra_headers.items()) if self.env["REQUEST_METHOD"] != "OPTIONS": file = open(file_path, "rb") diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index 2cbaf91f..109ab57d 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -132,6 +132,8 @@ class Wrapper window.history.replaceState(message.params[0], message.params[1], query) else if cmd == "wrapperGetState" @sendInner {"cmd": "response", "to": message.id, "result": window.history.state} + else if cmd == "wrapperGetAjaxKey" + @sendInner {"cmd": "response", "to": message.id, "result": window.ajax_key} else if cmd == "wrapperOpenWindow" @actionOpenWindow(message.params) else if cmd == "wrapperPermissionAdd" diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index 800fc736..1f347a57 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -982,6 +982,12 @@ jQuery.extend( jQuery.easing, "to": message.id, "result": window.history.state }); + } else if (cmd === "wrapperGetAjaxKey") { + return this.sendInner({ + "cmd": "response", + "to": message.id, + "result": window.ajax_key + }); } else if (cmd === "wrapperOpenWindow") { return this.actionOpenWindow(message.params); } else if (cmd === "wrapperPermissionAdd") { diff --git a/src/Ui/template/wrapper.html b/src/Ui/template/wrapper.html index 6bf6e181..39915ce2 100644 --- a/src/Ui/template/wrapper.html +++ b/src/Ui/template/wrapper.html @@ -60,6 +60,7 @@ document.getElementById("inner-iframe").src = "{file_url}{query_string}" address = "{address}" wrapper_nonce = "{wrapper_nonce}" wrapper_key = "{wrapper_key}" +ajax_key = "{ajax_key}" postmessage_nonce_security = {postmessage_nonce_security} file_inner_path = "{file_inner_path}" permissions = {permissions} From 5c5e4b914c997fe1ba68512ff06d30700ab53527 Mon Sep 17 00:00:00 2001 From: grez911 Date: Tue, 15 Aug 2017 20:17:42 +0300 Subject: [PATCH 0232/2570] Added P2P portcheck --- src/File/FileRequest.py | 11 +++++++++ src/File/FileServer.py | 51 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index df812d8c..12f8ee4d 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -3,6 +3,7 @@ import os import time import json import itertools +import socket # Third party modules import gevent @@ -13,6 +14,7 @@ from util import RateLimit from util import StreamingMsgpack from util import helper from Plugin import PluginManager +from contextlib import closing FILE_BUFF = 1024 * 512 @@ -456,6 +458,15 @@ class FileRequest(object): def actionPing(self, params): self.response("Pong!") + # Check requested port of the other peer + def actionCheckport(self, params): + with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: + sock.settimeout(5) + if sock.connect_ex((self.connection.ip, params["port"])) == 0: + self.response("open %s" % self.connection.ip) + else: + self.response("closed %s" % self.connection.ip) + # Unknown command def actionUnknown(self, cmd, params): self.response({"error": "Unknown command: %s" % cmd}) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index 835c3c48..0dcc5658 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -2,13 +2,12 @@ import logging import urllib2 import re import time -import socket +import random import gevent import util from Config import config -from FileRequest import FileRequest from Site import SiteManager from Debug import Debug from Connection import ConnectionServer @@ -87,11 +86,57 @@ class FileServer(ConnectionServer): def testOpenport(self, port=None, use_alternative=True): if not port: port = self.port - back = self.testOpenportPortchecker(port) + back = self.testOpenportP2P(port) if back["result"] is not True and use_alternative: # If no success try alternative checker return self.testOpenportCanyouseeme(port) else: return back + return back + + def testOpenportP2P(self, port=None): + self.log.info("Checking port %s using P2P..." % port) + site = SiteManager.site_manager.get("1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D") + peers = [] + res = None + if not site: # First run, has no any peers + return self.testOpenportPortchecker(port) # Fallback to centralized service + for peer in site.peers.values(): # Get all non-onion peers + if peer.ip.endswith(".onion"): + peers.append(peer) + if len(peers) < 3: # Not enough peers + return self.testOpenportPortchecker(port) # Fallback to centralized service + for retry in range(1, 3): # Try 3 peers + random_peer = random.choice(peers) + with gevent.Timeout(10.0, False): # 10 sec timeout, don't raise exception + random_peer.connect() + res = random_peer.request("checkport", {"port": port}) + break # All fine, exit from for loop + if res is None: # Nobody answered + return self.testOpenportPortchecker(port) # Fallback to centralized service + message = res["body"] + if "open" not in message: + if config.tor != "always": + self.log.info("[BAD :(] Port closed %s" % message) + if port == self.port: + self.port_opened = False # Self port, update port_opened status + match = message.split(" ", 1)[1] # Try find my external ip in message + if match: # Found my ip in message + config.ip_external = match + SiteManager.peer_blacklist.append((config.ip_external, self.port)) # Add myself to peer blacklist + else: + config.ip_external = False + return {"result": False, "message": message} + else: + self.log.info("[OK :)] Port open: %s" % message) + if port == self.port: # Self port, update port_opened status + self.port_opened = True + match = message.split(" ", 1)[1] # Try find my external ip in message + if match: # Found my ip in message + config.ip_external = match + SiteManager.peer_blacklist.append((config.ip_external, self.port)) # Add myself to peer blacklist + else: + config.ip_external = False + return {"result": True, "message": message} def testOpenportPortchecker(self, port=None): self.log.info("Checking port %s using portchecker.co..." % port) From a4132c9cb11db661cff3924e85fc24ff58c81fef Mon Sep 17 00:00:00 2001 From: Sergei Bondarenko Date: Wed, 16 Aug 2017 09:40:54 +0300 Subject: [PATCH 0233/2570] Update FileServer.py --- src/File/FileServer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index 0dcc5658..0873acfa 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -8,6 +8,7 @@ import gevent import util from Config import config +from FileRequest import FileRequest from Site import SiteManager from Debug import Debug from Connection import ConnectionServer From c0a96983a7049567354466a9839f3c17efdfefa6 Mon Sep 17 00:00:00 2001 From: Sergei Bondarenko Date: Wed, 16 Aug 2017 14:32:47 +0300 Subject: [PATCH 0234/2570] Update FileServer.py --- src/File/FileServer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index 0873acfa..f0212606 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -2,6 +2,7 @@ import logging import urllib2 import re import time +import socket import random import gevent From 1fc2a917d4aeae32b945cf6c8573a71b80dd62c5 Mon Sep 17 00:00:00 2001 From: grez911 Date: Fri, 18 Aug 2017 11:29:41 +0300 Subject: [PATCH 0235/2570] Fix P2P port check --- src/File/FileRequest.py | 4 ++-- src/File/FileServer.py | 38 +++++++++++++++----------------------- 2 files changed, 17 insertions(+), 25 deletions(-) diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index 12f8ee4d..b458c6a2 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -463,9 +463,9 @@ class FileRequest(object): with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: sock.settimeout(5) if sock.connect_ex((self.connection.ip, params["port"])) == 0: - self.response("open %s" % self.connection.ip) + self.response({"status": "open", "ip_external": self.connection.ip}) else: - self.response("closed %s" % self.connection.ip) + self.response({"status": "closed", "ip_external": self.connection.ip}) # Unknown command def actionUnknown(self, cmd, params): diff --git a/src/File/FileServer.py b/src/File/FileServer.py index f0212606..80cdbd36 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -97,48 +97,40 @@ class FileServer(ConnectionServer): def testOpenportP2P(self, port=None): self.log.info("Checking port %s using P2P..." % port) - site = SiteManager.site_manager.get("1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D") + site = SiteManager.site_manager.get(config.homepage) peers = [] res = None if not site: # First run, has no any peers return self.testOpenportPortchecker(port) # Fallback to centralized service - for peer in site.peers.values(): # Get all non-onion peers - if peer.ip.endswith(".onion"): + for peer in site.peers.values(): + if not peer.ip.endswith(".onion"): # Get all non-onion peers peers.append(peer) if len(peers) < 3: # Not enough peers return self.testOpenportPortchecker(port) # Fallback to centralized service - for retry in range(1, 3): # Try 3 peers + for retry in range(0, 3): # Try 3 peers random_peer = random.choice(peers) with gevent.Timeout(10.0, False): # 10 sec timeout, don't raise exception random_peer.connect() res = random_peer.request("checkport", {"port": port}) - break # All fine, exit from for loop + if res is not None: + break # All fine, exit from for loop if res is None: # Nobody answered return self.testOpenportPortchecker(port) # Fallback to centralized service - message = res["body"] - if "open" not in message: + if res["status"] == "closed": if config.tor != "always": - self.log.info("[BAD :(] Port closed %s" % message) + self.log.info("[BAD :(] %s says that your port %s is closed" % (random_peer.ip, port)) if port == self.port: self.port_opened = False # Self port, update port_opened status - match = message.split(" ", 1)[1] # Try find my external ip in message - if match: # Found my ip in message - config.ip_external = match - SiteManager.peer_blacklist.append((config.ip_external, self.port)) # Add myself to peer blacklist - else: - config.ip_external = False - return {"result": False, "message": message} + config.ip_external = res["ip_external"] + SiteManager.peer_blacklist.append((config.ip_external, self.port)) # Add myself to peer blacklist + return {"result": False} else: - self.log.info("[OK :)] Port open: %s" % message) + self.log.info("[OK :)] %s says that your port %s is open" % (random_peer.ip, port)) if port == self.port: # Self port, update port_opened status self.port_opened = True - match = message.split(" ", 1)[1] # Try find my external ip in message - if match: # Found my ip in message - config.ip_external = match - SiteManager.peer_blacklist.append((config.ip_external, self.port)) # Add myself to peer blacklist - else: - config.ip_external = False - return {"result": True, "message": message} + config.ip_external = res["ip_external"] + SiteManager.peer_blacklist.append((config.ip_external, self.port)) # Add myself to peer blacklist + return {"result": True} def testOpenportPortchecker(self, port=None): self.log.info("Checking port %s using portchecker.co..." % port) From bf69d24566e5bc7f8a6e13de6273c80b1a3df240 Mon Sep 17 00:00:00 2001 From: grez911 Date: Fri, 18 Aug 2017 11:38:26 +0300 Subject: [PATCH 0236/2570] Deleted unnecessary import statement --- src/File/FileRequest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index b458c6a2..2fc17642 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -3,7 +3,6 @@ import os import time import json import itertools -import socket # Third party modules import gevent From 8487056edbaabe9771e390ff038588e1e1150a2c Mon Sep 17 00:00:00 2001 From: grez911 Date: Fri, 18 Aug 2017 11:43:06 +0300 Subject: [PATCH 0237/2570] Small fix in P2P portcheck --- src/File/FileServer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index 80cdbd36..d3aab8b3 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -93,7 +93,6 @@ class FileServer(ConnectionServer): return self.testOpenportCanyouseeme(port) else: return back - return back def testOpenportP2P(self, port=None): self.log.info("Checking port %s using P2P..." % port) From 4c7500e24804e75ac7c7cef6661ca3e6b5ea29ff Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 18 Aug 2017 14:37:56 +0200 Subject: [PATCH 0238/2570] Fix getDirname description --- src/util/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/helper.py b/src/util/helper.py index 89e41042..4fe6bf0f 100644 --- a/src/util/helper.py +++ b/src/util/helper.py @@ -110,7 +110,7 @@ def unpackOnionAddress(packed): # Get dir from file -# Return: data/site/content.json -> data/site +# Return: data/site/content.json -> data/site/ def getDirname(path): if "/" in path: return path[:path.rfind("/") + 1] From f1c320dd223b0de8184a835c02d2822f5d70cc4a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 18 Aug 2017 14:38:58 +0200 Subject: [PATCH 0239/2570] Log sign errors --- src/Ui/UiWebsocket.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 5168ad9b..8926aa7a 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -382,6 +382,7 @@ class UiWebsocket(object): except Exception, err: self.cmd("notification", ["error", _["Content signing failed"] + "
%s" % err]) self.response(to, {"error": "Site sign failed: %s" % err}) + self.log.error("Site sign failed: %s: %s" % (inner_path, Debug.formatException(err))) return site.content_manager.loadContent(inner_path, add_bad_files=False) # Load new content.json, ignore errors From 9f762a02306d0ffdfafede473086850980ae6718 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 18 Aug 2017 14:43:28 +0200 Subject: [PATCH 0240/2570] Bootstrapper use python date functions instead of sqlite --- plugins/disabled-Bootstrapper/BootstrapperDb.py | 8 +++++--- src/Site/SiteStorage.py | 9 +++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/plugins/disabled-Bootstrapper/BootstrapperDb.py b/plugins/disabled-Bootstrapper/BootstrapperDb.py index a3a91589..e9f690c6 100644 --- a/plugins/disabled-Bootstrapper/BootstrapperDb.py +++ b/plugins/disabled-Bootstrapper/BootstrapperDb.py @@ -20,8 +20,9 @@ class BootstrapperDb(Db): def cleanup(self): while 1: - self.execute("DELETE FROM peer WHERE date_announced < DATETIME('now', '-40 minute')") 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") @@ -96,14 +97,15 @@ class BootstrapperDb(Db): res = self.execute("SELECT * FROM peer WHERE ? LIMIT 1", {"ip4": ip4, "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 = DATETIME('now') WHERE ?", {"peer_id": peer_id}) + self.execute("UPDATE peer SET date_announced = ? WHERE peer_id = ?", (now, peer_id)) else: self.log.debug("New peer: %s %s signed: %s" % (ip4, onion, onion_signed)) if onion and not onion_signed: return len(hashes) - self.execute("INSERT INTO peer ?", {"ip4": ip4, "onion": onion, "port": port}) + self.execute("INSERT INTO peer ?", {"ip4": ip4, "onion": onion, "port": port, "date_announced": now}) peer_id = self.cur.cursor.lastrowid # Check user's hashes diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index bfdd7840..e1caabbc 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -163,8 +163,13 @@ class SiteStorage(object): return res # Open file object - def open(self, inner_path, mode="rb"): - return open(self.getPath(inner_path), mode) + def open(self, inner_path, mode="rb", create_dirs=False): + file_path = self.getPath(inner_path) + if create_dirs: + file_dir = os.path.dirname(file_path) + if not os.path.isdir(file_dir): + os.makedirs(file_dir) + return open(file_path, mode) # Open file object def read(self, inner_path, mode="r"): From 37ef2802de8f6d4f84f1bb70bb68cae6a0bcda65 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 18 Aug 2017 14:44:24 +0200 Subject: [PATCH 0241/2570] Log announce times --- .../disabled-Bootstrapper/BootstrapperPlugin.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/plugins/disabled-Bootstrapper/BootstrapperPlugin.py b/plugins/disabled-Bootstrapper/BootstrapperPlugin.py index 7577794c..c9786360 100644 --- a/plugins/disabled-Bootstrapper/BootstrapperPlugin.py +++ b/plugins/disabled-Bootstrapper/BootstrapperPlugin.py @@ -11,6 +11,8 @@ if "db" not in locals().keys(): # Share durin reloads @PluginManager.registerTo("FileRequest") class FileRequestPlugin(object): def actionAnnounce(self, params): + time_started = time.time() + s = time.time() hashes = params["hashes"] if "onion_signs" in params and len(params["onion_signs"]) == len(set(params["onions"])): @@ -35,11 +37,14 @@ class FileRequestPlugin(object): # Incorrect signs number all_onions_signed = False + time_onion_check = time.time() - s + if "ip4" in params["add"] and self.connection.ip != "127.0.0.1" and not self.connection.ip.endswith(".onion"): ip4 = self.connection.ip else: ip4 = None + s = time.time() # Separatley add onions to sites or at once if no onions present hashes_changed = 0 i = 0 @@ -51,7 +56,9 @@ class FileRequestPlugin(object): onion_signed=all_onions_signed ) i += 1 + time_db_onion = time.time() - s + s = time.time() # Announce all sites if ip4 defined if ip4: hashes_changed += db.peerAnnounce( @@ -60,7 +67,9 @@ class FileRequestPlugin(object): hashes=hashes, delete_missing_hashes=params.get("delete") ) + time_db_ip4 = time.time() - s + s = time.time() # Query sites back = {} peers = [] @@ -74,8 +83,14 @@ class FileRequestPlugin(object): limit=min(30, params["need_num"]), need_types=params["need_types"] ) 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_ip4: %.3fs, peerlist: %.3fs)" % + (len(hashes), len(onion_to_hash), time_onion_check, time_db_onion, time_db_ip4, time_peerlist) + ) self.response(back) From 32a0f96ecd86142a46ca5f5ae9f1d2fef64d90b7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 18 Aug 2017 14:45:06 +0200 Subject: [PATCH 0242/2570] Batch onion update --- .../disabled-Bootstrapper/BootstrapperPlugin.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/plugins/disabled-Bootstrapper/BootstrapperPlugin.py b/plugins/disabled-Bootstrapper/BootstrapperPlugin.py index c9786360..e6d42863 100644 --- a/plugins/disabled-Bootstrapper/BootstrapperPlugin.py +++ b/plugins/disabled-Bootstrapper/BootstrapperPlugin.py @@ -46,16 +46,24 @@ class FileRequestPlugin(object): s = time.time() # Separatley add onions to sites or at once if no onions present - hashes_changed = 0 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 + db.execute("BEGIN") + for onion, onion_hashes in onion_to_hash.iteritems(): hashes_changed += db.peerAnnounce( onion=onion, port=params["port"], - hashes=[hashes[i]], + hashes=onion_hashes, onion_signed=all_onions_signed ) - i += 1 + db.execute("END") time_db_onion = time.time() - s s = time.time() From b6c0c955c5cf2ef110b41748f1ad3013f7b099f8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 18 Aug 2017 14:45:48 +0200 Subject: [PATCH 0243/2570] Limit bootstrap queries to 1s --- plugins/disabled-Bootstrapper/BootstrapperPlugin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/disabled-Bootstrapper/BootstrapperPlugin.py b/plugins/disabled-Bootstrapper/BootstrapperPlugin.py index e6d42863..2d398d92 100644 --- a/plugins/disabled-Bootstrapper/BootstrapperPlugin.py +++ b/plugins/disabled-Bootstrapper/BootstrapperPlugin.py @@ -85,6 +85,10 @@ class FileRequestPlugin(object): back["onion_sign_this"] = "%.0f" % time.time() # Send back nonce for signing 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, ip4=self.connection.ip, onions=params.get("onions"), port=params["port"], From 2af08c0ba104e22384ff21216f1b377a10a8729e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 18 Aug 2017 14:46:06 +0200 Subject: [PATCH 0244/2570] Only return 5 peers if more than 500 site queired --- plugins/disabled-Bootstrapper/BootstrapperPlugin.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/plugins/disabled-Bootstrapper/BootstrapperPlugin.py b/plugins/disabled-Bootstrapper/BootstrapperPlugin.py index 2d398d92..d5af9bb9 100644 --- a/plugins/disabled-Bootstrapper/BootstrapperPlugin.py +++ b/plugins/disabled-Bootstrapper/BootstrapperPlugin.py @@ -84,6 +84,12 @@ class FileRequestPlugin(object): 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: + 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))) @@ -91,8 +97,8 @@ class FileRequestPlugin(object): hash_peers = db.peerList( hash, - ip4=self.connection.ip, onions=params.get("onions"), port=params["port"], - limit=min(30, params["need_num"]), need_types=params["need_types"] + ip4=self.connection.ip, onions=onion_to_hash.keys(), port=params["port"], + limit=min(limit, params["need_num"]), need_types=params["need_types"], order=order ) peers.append(hash_peers) time_peerlist = time.time() - s From a27422a0fb56a717f2df59bc62a6cc2ae6463b1a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 18 Aug 2017 14:46:35 +0200 Subject: [PATCH 0245/2570] Bootstrapper SQL query optimizations --- .../disabled-Bootstrapper/BootstrapperDb.py | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/plugins/disabled-Bootstrapper/BootstrapperDb.py b/plugins/disabled-Bootstrapper/BootstrapperDb.py index e9f690c6..a2385fd3 100644 --- a/plugins/disabled-Bootstrapper/BootstrapperDb.py +++ b/plugins/disabled-Bootstrapper/BootstrapperDb.py @@ -50,7 +50,7 @@ class BootstrapperDb(Db): peer_id INTEGER PRIMARY KEY ASC AUTOINCREMENT NOT NULL UNIQUE, port INTEGER NOT NULL, ip4 TEXT, - onion TEXT, + onion TEXT UNIQUE, date_added DATETIME DEFAULT (CURRENT_TIMESTAMP), date_announced DATETIME DEFAULT (CURRENT_TIMESTAMP) ); @@ -92,9 +92,9 @@ class BootstrapperDb(Db): # Check user if onion: - res = self.execute("SELECT * FROM peer WHERE ? LIMIT 1", {"onion": onion}) + res = self.execute("SELECT peer_id FROM peer WHERE ? LIMIT 1", {"onion": onion}) else: - res = self.execute("SELECT * FROM peer WHERE ? LIMIT 1", {"ip4": ip4, "port": port}) + res = self.execute("SELECT peer_id FROM peer WHERE ? LIMIT 1", {"ip4": ip4, "port": port}) user_row = res.fetchone() now = time.strftime("%Y-%m-%d %H:%M:%S") @@ -124,27 +124,31 @@ class BootstrapperDb(Db): else: return 0 - def peerList(self, hash, ip4=None, onions=[], port=None, limit=30, need_types=["ip4", "onion"]): + def peerList(self, hash, ip4=None, onions=[], port=None, limit=30, need_types=["ip4", "onion"], order=True): hash_peers = {"ip4": [], "onion": []} if limit == 0: return hash_peers hashid = self.getHashId(hash) - where = "hash_id = :hashid" + 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 += " AND (onion NOT IN (%s) OR onion IS NULL)" % ",".join(onions_escaped) + where_sql += " AND (onion NOT IN (%s) OR onion IS NULL)" % ",".join(onions_escaped) elif ip4: - where += " AND (NOT (ip4 = :ip4 AND port = :port) OR ip4 IS NULL)" + where_sql += " AND (NOT (ip4 = :ip4 AND port = :port) OR ip4 IS NULL)" query = """ SELECT ip4, port, onion FROM peer_to_hash LEFT JOIN peer USING (peer_id) WHERE %s - ORDER BY date_announced DESC + %s LIMIT :limit - """ % where + """ % (where_sql, order_sql) res = self.execute(query, {"hashid": hashid, "ip4": ip4, "onions": onions, "port": port, "limit": limit}) for row in res: @@ -156,4 +160,4 @@ class BootstrapperDb(Db): hash_peers["onion"].append( helper.packOnionAddress(row["onion"], row["port"]) ) - return hash_peers + return hash_peers \ No newline at end of file From 5e926ec98e4aac69f481d9a52d3838dd55020e56 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 18 Aug 2017 14:47:08 +0200 Subject: [PATCH 0246/2570] Rev2186 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index d3f93846..17730309 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.7" - self.rev = 2184 + self.rev = 2186 self.argv = argv self.action = None self.config_file = "zeronet.conf" From d591df39cc400489725328ac68954c48c5287b2a Mon Sep 17 00:00:00 2001 From: Sergei Bondarenko Date: Fri, 18 Aug 2017 22:42:14 +0300 Subject: [PATCH 0247/2570] Fix: ui_restrict now working with ui_password https://github.com/HelloZeroNet/ZeroNet/issues/207 --- plugins/disabled-UiPassword/UiPasswordPlugin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/disabled-UiPassword/UiPasswordPlugin.py b/plugins/disabled-UiPassword/UiPasswordPlugin.py index 7b4b2834..4ad179a0 100644 --- a/plugins/disabled-UiPassword/UiPasswordPlugin.py +++ b/plugins/disabled-UiPassword/UiPasswordPlugin.py @@ -25,6 +25,9 @@ class UiRequestPlugin(object): last_cleanup = time.time() 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: From b1989ef02e1c438794ad5bbfde06f9e4474b7394 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 19 Aug 2017 18:55:21 +0200 Subject: [PATCH 0248/2570] Limit optional file commands in multiuser mode --- plugins/disabled-Multiuser/MultiuserPlugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/disabled-Multiuser/MultiuserPlugin.py b/plugins/disabled-Multiuser/MultiuserPlugin.py index b2b6550a..75c340f1 100644 --- a/plugins/disabled-Multiuser/MultiuserPlugin.py +++ b/plugins/disabled-Multiuser/MultiuserPlugin.py @@ -12,6 +12,7 @@ try: except Exception, err: local_master_addresses = set() + @PluginManager.registerTo("UiRequest") class UiRequestPlugin(object): def __init__(self, *args, **kwargs): @@ -122,8 +123,9 @@ class UiWebsocketPlugin(object): def __init__(self, *args, **kwargs): self.multiuser_denied_cmds = ( "siteDelete", "configSet", "serverShutdown", "serverUpdate", "siteClone", - "siteSetOwned", "optionalLimitSet", "siteSetAutodownloadoptional", "dbReload", "dbRebuild", + "siteSetOwned", "siteSetAutodownloadoptional", "dbReload", "dbRebuild", "mergerSiteDelete", "siteSetLimit", + "optionalLimitSet", "optionalHelp", "optionalHelpRemove", "optionalHelpAll", "optionalFilePin", "optionalFileUnpin", "optionalFileDelete", "muteAdd", "muteRemove", "blacklistAdd", "blacklistRemove" ) if config.multiuser_no_new_sites: From ebdb1ed322ee1b3639d914e619a06b47201c71d4 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 19 Aug 2017 18:55:39 +0200 Subject: [PATCH 0249/2570] Fix permission limiting with uppercase function names --- plugins/disabled-Multiuser/MultiuserPlugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/disabled-Multiuser/MultiuserPlugin.py b/plugins/disabled-Multiuser/MultiuserPlugin.py index 75c340f1..33c86cf6 100644 --- a/plugins/disabled-Multiuser/MultiuserPlugin.py +++ b/plugins/disabled-Multiuser/MultiuserPlugin.py @@ -186,6 +186,7 @@ class UiWebsocketPlugin(object): self.actionUserLoginForm(0) def hasCmdPermission(self, cmd): + cmd = cmd[0].lower() + cmd[1:] if not config.multiuser_local and self.user.master_address not in local_master_addresses and cmd in self.multiuser_denied_cmds: self.cmd("notification", ["info", "This function is disabled on this proxy!"]) return False From 5ad614743b7a75ede7e86627f5e8a6cb1101485c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 19 Aug 2017 18:55:48 +0200 Subject: [PATCH 0250/2570] Rev2187 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 17730309..a7059a1f 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.7" - self.rev = 2186 + self.rev = 2187 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 905976a1b60cd5fa365fea364d758b6843d998d0 Mon Sep 17 00:00:00 2001 From: grez911 Date: Tue, 22 Aug 2017 17:43:55 +0300 Subject: [PATCH 0251/2570] Added exception handling for broken user.json --- src/Ui/UiRequest.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 97d0d25d..50acdff2 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -588,7 +588,11 @@ class UiRequest(object): site = site_check if site: # Correct wrapper key - user = self.getCurrentUser() + try: + user = self.getCurrentUser() + except Exception, err: + self.log.error("Error in data/user.json: %s" % err) + return self.error500() if not user: self.log.error("No user found") return self.error403() From 158f9e37e5244899f938c83c6f8c7ff04216565c Mon Sep 17 00:00:00 2001 From: grez911 Date: Fri, 25 Aug 2017 11:05:48 +0300 Subject: [PATCH 0252/2570] Added explanation of convertion actionWrapper response to list --- src/Ui/UiRequest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 50acdff2..57c631a6 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -282,8 +282,8 @@ class UiRequest(object): return False self.sendHeader(extra_headers=extra_headers[:]) - return iter([self.renderWrapper(site, path, inner_path, title, extra_headers)]) - # Dont know why wrapping with iter necessary, but without it around 100x slower + return [self.renderWrapper(site, path, inner_path, title, extra_headers)] + # Make response be sent at once (see https://github.com/HelloZeroNet/ZeroNet/issues/1092) else: # Bad url return False From 11463dbba5f4a16a5be8ed8717e2a60b910570a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=96=87=E5=B9=BF?= Date: Thu, 31 Aug 2017 11:45:47 +0800 Subject: [PATCH 0253/2570] Fix the import path --- zeronet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zeronet.py b/zeronet.py index fa5d8f90..6aa4bf25 100755 --- a/zeronet.py +++ b/zeronet.py @@ -60,7 +60,7 @@ def main(): except Exception, log_err: print "Failed to log error:", log_err traceback.print_exc() - from Config import config + from src.Config import config traceback.print_exc(file=open(config.log_dir + "/error.log", "a")) if main and main.update_after_shutdown: # Updater From b584c586ecee3df0f1bb6890406771fc4213d04e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 2 Sep 2017 14:26:39 +0200 Subject: [PATCH 0254/2570] Fix longer signers_sign signing --- src/Content/ContentManager.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index b2979b17..5d717adf 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -622,9 +622,8 @@ class ContentManager(object): if inner_path == "content.json" and privatekey_address == self.site.address: # If signing using the root key, then sign the valid signers - new_content["signers_sign"] = CryptBitcoin.sign( - "%s:%s" % (new_content["signs_required"], ",".join(valid_signers)), privatekey - ) + signers_data = "%s:%s" % (new_content["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") From bfd3d18a10e3cceddf88946dd4567e51acfd7200 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 2 Sep 2017 14:28:01 +0200 Subject: [PATCH 0255/2570] Fix multiuser plugin compatibility --- src/Ui/UiRequest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 57c631a6..d2e4e27f 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -282,7 +282,7 @@ class UiRequest(object): return False self.sendHeader(extra_headers=extra_headers[:]) - return [self.renderWrapper(site, path, inner_path, title, extra_headers)] + return iter([self.renderWrapper(site, path, inner_path, title, extra_headers)]) # Make response be sent at once (see https://github.com/HelloZeroNet/ZeroNet/issues/1092) else: # Bad url From 932db9bbe4d53aa344026ae42ed37cf06bf46603 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 2 Sep 2017 14:28:40 +0200 Subject: [PATCH 0256/2570] Rev2190 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index a7059a1f..1da9ca1a 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.7" - self.rev = 2187 + self.rev = 2190 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 38230c62e6ac90220a4d88758f05a5ca8deac806 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Sun, 10 Sep 2017 08:53:07 +0300 Subject: [PATCH 0257/2570] MergerSite: Add actionFileList and actionDirList proxy --- plugins/MergerSite/MergerSitePlugin.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/MergerSite/MergerSitePlugin.py b/plugins/MergerSite/MergerSitePlugin.py index 0fe83448..37c10598 100644 --- a/plugins/MergerSite/MergerSitePlugin.py +++ b/plugins/MergerSite/MergerSitePlugin.py @@ -141,6 +141,12 @@ class UiWebsocketPlugin(object): else: 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) From 4bbea46b908905d8eecadadb93ab3f4f8004db79 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Sun, 10 Sep 2017 09:46:21 +0300 Subject: [PATCH 0258/2570] Read files as binary in fileGet() --- 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 5168ad9b..ae05c256 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -596,7 +596,7 @@ class UiWebsocket(object): if required or inner_path in self.site.bad_files: with gevent.Timeout(timeout): self.site.needFile(inner_path, priority=6) - body = self.site.storage.read(inner_path) + body = self.site.storage.read(inner_path, "rb") except Exception, err: self.log.error("%s fileGet error: %s" % (inner_path, err)) body = None From db339eecd3034299cad90d95c934ea46fe00e83b Mon Sep 17 00:00:00 2001 From: Ivanq Date: Sat, 16 Sep 2017 21:57:20 +0300 Subject: [PATCH 0259/2570] Allow 'backup' or 'log' as directories --- src/Ui/UiWebsocket.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 0996bf13..8ecdf7aa 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -859,7 +859,13 @@ class UiWebsocket(object): def actionServerShowdirectory(self, to, directory="backup"): import webbrowser - webbrowser.open('file://' + os.path.abspath(config.data_dir)) + + if directory == "backup": + directory = os.path.abspath(config.data_dir) + elif directory == "log": + directory = os.path.abspath(config.log_dir) + + webbrowser.open('file://' + directory) def actionConfigSet(self, to, key, value): if key not in ["tor", "language"]: From d3d748923287d2b4bf53fb93959c04a5531f9795 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 20 Sep 2017 17:06:09 +0200 Subject: [PATCH 0260/2570] Rev2192, Fix incorrect site detection when using async commands with Cors or MergerSite plugin --- plugins/Cors/CorsPlugin.py | 14 +++++++------- plugins/MergerSite/MergerSitePlugin.py | 15 ++++++++------- src/Config.py | 2 +- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/plugins/Cors/CorsPlugin.py b/plugins/Cors/CorsPlugin.py index 5aa73ee1..a8651be2 100644 --- a/plugins/Cors/CorsPlugin.py +++ b/plugins/Cors/CorsPlugin.py @@ -1,5 +1,6 @@ import re import cgi +import copy from Plugin import PluginManager from Translate import Translate @@ -25,18 +26,17 @@ def getCorsPath(site, inner_path): class UiWebsocketPlugin(object): # Add cors support for file commands def corsFuncWrapper(self, func_name, to, inner_path, *args, **kwargs): - func = getattr(super(UiWebsocketPlugin, self), func_name) if inner_path.startswith("cors-"): cors_address, cors_inner_path = getCorsPath(self.site, inner_path) - site_before = self.site # Save to be able to change it back after we ran the command - self.site = self.server.sites.get(cors_address) # Change the site to the merged one - try: - back = func(to, cors_inner_path, *args, **kwargs) - finally: - self.site = site_before # Change back to original site + req_self = copy.copy(self) + req_self.site = self.server.sites.get(cors_address) # Change the site to the merged one + + 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): diff --git a/plugins/MergerSite/MergerSitePlugin.py b/plugins/MergerSite/MergerSitePlugin.py index 37c10598..49aeebe2 100644 --- a/plugins/MergerSite/MergerSitePlugin.py +++ b/plugins/MergerSite/MergerSitePlugin.py @@ -1,5 +1,6 @@ import re import time +import copy from Plugin import PluginManager from Translate import Translate @@ -122,7 +123,6 @@ class UiWebsocketPlugin(object): # Add support merger sites for file commands def mergerFuncWrapper(self, func_name, to, 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) @@ -131,14 +131,15 @@ class UiWebsocketPlugin(object): if merger_cert and self.user.getSiteData(merged_address).get("cert") != 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 = self.server.sites.get(merged_address) # Change the site to the merged one - try: - back = func(to, merged_inner_path, *args, **kwargs) - finally: - self.site = site_before # Change back to original site + 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) + back = func(to, merged_inner_path, *args, **kwargs) + return back else: + func = getattr(super(UiWebsocketPlugin, self), func_name) return func(to, inner_path, *args, **kwargs) def actionFileList(self, to, inner_path, *args, **kwargs): diff --git a/src/Config.py b/src/Config.py index 1da9ca1a..a73143db 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.5.7" - self.rev = 2190 + self.rev = 2192 self.argv = argv self.action = None self.config_file = "zeronet.conf" From f98f52a50eddf0410de1689340eefa624c0db3cf Mon Sep 17 00:00:00 2001 From: Ivanq Date: Fri, 22 Sep 2017 08:21:51 +0300 Subject: [PATCH 0261/2570] Allow siteClone not for admin sites --- src/Ui/UiWebsocket.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 8ecdf7aa..32675e70 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -33,7 +33,7 @@ class UiWebsocket(object): self.sending = False # Currently sending to client self.send_queue = [] # Messages to send to client self.admin_commands = ( - "sitePause", "siteResume", "siteDelete", "siteList", "siteSetLimit", "siteClone", + "sitePause", "siteResume", "siteDelete", "siteList", "siteSetLimit", "channelJoinAllsite", "serverUpdate", "serverPortcheck", "serverShutdown", "serverShowdirectory", "certSet", "configSet", "permissionAdd", "permissionRemove" ) @@ -809,7 +809,7 @@ class UiWebsocket(object): else: self.response(to, {"error": "Unknown site: %s" % address}) - def actionSiteClone(self, to, address, root_inner_path="", target_address=None): + def cbSiteClone(self, to, address, root_inner_path="", target_address=None): self.cmd("notification", ["info", _["Cloning site..."]]) site = self.server.sites.get(address) if target_address: @@ -827,6 +827,16 @@ class UiWebsocket(object): self.cmd("notification", ["done", _["Site cloned"] + "" % new_address]) gevent.spawn(new_site.announce) + def actionSiteClone(self, to, address, root_inner_path="", target_address=None): + if "ADMIN" in self.getPermissions(to): + self.cbSiteClone(to, address, root_inner_path, target_address) + else: + self.cmd( + "confirm", + [_["Clone site %s?"] % address, _["Clone"]], + lambda (res): self.cbSiteClone(to, address, root_inner_path, target_address) + ) + def actionSiteSetLimit(self, to, size_limit): self.site.settings["size_limit"] = int(size_limit) self.site.saveSettings() From 7fa019321c12bfbb806d88fde3a7cdd04d5e59f6 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Fri, 22 Sep 2017 08:28:39 +0300 Subject: [PATCH 0262/2570] Check site before cloning --- src/Ui/UiWebsocket.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 32675e70..07eadbd5 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -828,6 +828,14 @@ class UiWebsocket(object): gevent.spawn(new_site.announce) def actionSiteClone(self, to, address, root_inner_path="", target_address=None): + if not SiteManager.site_manager.isAddress(address): + self.response(to, {"error": "Not a site: %s" % address}) + return + + if not self.server.sites.get(address): + # Don't expose site existense + return + if "ADMIN" in self.getPermissions(to): self.cbSiteClone(to, address, root_inner_path, target_address) else: From 522e9ebf397910bfe15d0552a6230d68a8a29a43 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Mon, 25 Sep 2017 19:09:59 +0300 Subject: [PATCH 0263/2570] Show config via getConfig console command --- src/Config.py | 27 +++++++++++++++++++++++++++ src/main.py | 4 ++++ 2 files changed, 31 insertions(+) diff --git a/src/Config.py b/src/Config.py index a73143db..e811b512 100644 --- a/src/Config.py +++ b/src/Config.py @@ -165,6 +165,8 @@ class Config(object): action.add_argument('message', help='Message to sign') action.add_argument('privatekey', help='Private key') + action = self.subparsers.add_parser("getConfig", help='Return json-encoded info') + # Config parameters self.parser.add_argument('--verbose', help='More detailed logging', action='store_true') self.parser.add_argument('--debug', help='Debug mode', action='store_true') @@ -408,4 +410,29 @@ class Config(object): open(self.config_file, "w").write("\n".join(lines)) + def getServerInfo(self): + from Plugin import PluginManager + + info = { + "platform": sys.platform, + "fileserver_ip": self.fileserver_ip, + "fileserver_port": self.fileserver_port, + "ui_ip": self.ui_ip, + "ui_port": self.ui_port, + "version": self.version, + "rev": self.rev, + "language": self.language, + "debug": self.debug, + "plugins": PluginManager.plugin_manager.plugin_names + } + + try: + info["ip_external"] = sys.modules["main"].file_server.port_opened + info["tor_enabled"] = sys.modules["main"].file_server.tor_manager.enabled + info["tor_status"] = sys.modules["main"].file_server.tor_manager.status + except: + pass + + return info + config = Config(sys.argv) diff --git a/src/main.py b/src/main.py index e74bc71c..4014d19a 100644 --- a/src/main.py +++ b/src/main.py @@ -481,6 +481,10 @@ class Actions(object): except Exception, err: print "Unknown response (%s): %s" % (err, res) + def getConfig(self): + import json + print json.dumps(config.getServerInfo(), indent=2, ensure_ascii=False) + actions = Actions() # Starts here when running zeronet.py From f8f26f0a896cff63252b71749c35836be68c506c Mon Sep 17 00:00:00 2001 From: Ivanq Date: Fri, 22 Sep 2017 15:55:41 +0300 Subject: [PATCH 0264/2570] Return log_dir, data_dir, src_dir as result of getConfig --- src/Config.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index e811b512..77544870 100644 --- a/src/Config.py +++ b/src/Config.py @@ -423,7 +423,11 @@ class Config(object): "rev": self.rev, "language": self.language, "debug": self.debug, - "plugins": PluginManager.plugin_manager.plugin_names + "plugins": PluginManager.plugin_manager.plugin_names, + + "log_dir": os.path.abspath(self.log_dir), + "data_dir": os.path.abspath(self.data_dir), + "src_dir": os.path.dirname(os.path.abspath(__file__)) } try: From f4cdc3178894e0fd7b774aae9eea6d47e64d15f8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 14:27:29 +0200 Subject: [PATCH 0265/2570] Lock socket during sending data --- plugins/FilePack/FilePackPlugin.py- | 113 ++++++++++++++++++++++++++++ src/Connection/Connection.py | 22 ++++-- 2 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 plugins/FilePack/FilePackPlugin.py- diff --git a/plugins/FilePack/FilePackPlugin.py- b/plugins/FilePack/FilePackPlugin.py- new file mode 100644 index 00000000..8bcbeea2 --- /dev/null +++ b/plugins/FilePack/FilePackPlugin.py- @@ -0,0 +1,113 @@ +import os +import re +import itertools + +from Plugin import PluginManager +from Config import config +from util import helper + + +# 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): + if archive_path not in archive_cache: + if archive_path.endswith("tar.gz"): + import tarfile + archive_cache[archive_path] = tarfile.open(archive_path, "r:gz") + elif archive_path.endswith("tar.bz2"): + import tarfile + archive_cache[archive_path] = tarfile.open(archive_path, "r:bz2") + else: + import zipfile + archive_cache[archive_path] = zipfile.ZipFile(archive_path) + helper.timer(5, lambda: closeArchive(archive_path)) # Close after 5 sec + + archive = archive_cache[archive_path] + return archive + +def openArchiveFile(archive_path, path_within): + archive = openArchive(archive_path) + if archive_path.endswith(".zip"): + return archive.open(path_within) + else: + return archive.extractfile(path_within.encode("utf8")) + + +@PluginManager.registerTo("UiRequest") +class UiRequestPlugin(object): + def actionSiteMedia(self, path, **kwargs): + if ".zip/" in path or ".tar.gz/" in path: + path_parts = self.parsePath(path) + file_path = u"%s/%s/%s" % (config.data_dir, path_parts["address"], path_parts["inner_path"].decode("utf8")) + match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))/(.*)", file_path) + archive_path, path_within = match.groups() + if not os.path.isfile(archive_path): + site = self.server.site_manager.get(path_parts["address"]) + if not site: + self.error404(path) + # Wait until file downloads + result = site.needFile(site.storage.getInnerPath(archive_path), priority=10) + # Send virutal file path download finished event to remove loading screen + site.updateWebsocket(file_done=site.storage.getInnerPath(file_path)) + if not result: + return self.error404(path) + try: + file = openArchiveFile(archive_path, path_within) + content_type = self.getContentType(file_path) + self.sendHeader(200, content_type=content_type, noscript=kwargs.get("header_noscript", False)) + return self.streamFile(file) + except Exception, err: + self.log.debug("Error opening archive file: %s" % err) + return self.error404(path) + + return super(UiRequestPlugin, self).actionSiteMedia(path, **kwargs) + + def streamFile(self, file): + while 1: + 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 or ".tar.bz2/" in inner_path: + match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))/(.*)", inner_path) + inner_archive_path, path_within = match.groups() + return super(SiteStoragePlugin, self).isFile(inner_archive_path) + else: + return super(SiteStoragePlugin, self).isFile(inner_path) + + def getDbFiles(self): + for item in super(SiteStoragePlugin, self).getDbFiles(): + yield item + + # Search for archive files + for content_inner_path in self.site.content_manager.listContents(): + content = self.site.content_manager.contents[content_inner_path] + if not content: + merged_site.log.error("[MISSING] %s" % content_inner_path) + continue + + file_relative_paths = itertools.chain( + content.get("files", {}).iteritems(), + content.get("files_optional", {}).iteritems() + ) + + for file_relative_path, node in file_relative_paths: + if "zeronet-archive" in file_relative_path: + print node diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 2cfdb27d..57ccebec 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -4,6 +4,10 @@ import time import gevent import msgpack import msgpack.fallback +try: + from gevent.coros import RLock +except: + from gevent.lock import RLock from Config import config from Debug import Debug @@ -15,7 +19,7 @@ class Connection(object): __slots__ = ( "sock", "sock_wrapped", "ip", "port", "cert_pin", "target_onion", "id", "protocol", "type", "server", "unpacker", "req_id", "handshake", "crypt", "connected", "event_connected", "closed", "start_time", "last_recv_time", - "last_message_time", "last_send_time", "last_sent_time", "incomplete_buff_recv", "bytes_recv", "bytes_sent", "cpu_time", + "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", "bad_actions", "sites", "name", "updateName", "waiting_requests", "waiting_streams" ) @@ -58,6 +62,7 @@ class Connection(object): self.bad_actions = 0 self.sites = 0 self.cpu_time = 0.0 + self.send_lock = RLock() self.name = None self.updateName() @@ -351,6 +356,7 @@ class Connection(object): # Send data to connection def send(self, message, streaming=False): + 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" % ( message.get("cmd"), message.get("to"), streaming, @@ -362,10 +368,10 @@ class Connection(object): self.log("Send error: missing socket") return False - self.last_send_time = time.time() try: if streaming: - bytes_sent = StreamingMsgpack.stream(message, self.sock.sendall) + with self.send_lock: + bytes_sent = StreamingMsgpack.stream(message, self.sock.sendall) message = None self.bytes_sent += bytes_sent self.server.bytes_sent += bytes_sent @@ -374,7 +380,8 @@ class Connection(object): message = None self.bytes_sent += len(data) self.server.bytes_sent += len(data) - self.sock.sendall(data) + with self.send_lock: + self.sock.sendall(data) except Exception, err: self.close("Send error: %s" % err) return False @@ -387,9 +394,10 @@ class Connection(object): bytes_left = read_bytes while True: self.last_send_time = time.time() - self.sock.sendall( - file.read(min(bytes_left, buff)) - ) + with self.send_lock: + self.sock.sendall( + file.read(min(bytes_left, buff)) + ) bytes_left -= buff if bytes_left <= 0: break From d8453384abe6c2747edaf25b47ea1831a2982415 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 14:30:19 +0200 Subject: [PATCH 0266/2570] Separate socket creation and connection to be able to cancel it --- src/Connection/Connection.py | 3 ++- src/Tor/TorManager.py | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 57ccebec..db874839 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -104,7 +104,8 @@ class Connection(object): raise Exception("Can't connect to onion addresses, no Tor controller present") self.sock = self.server.tor_manager.createSocket(self.ip, self.port) else: - self.sock = socket.create_connection((self.ip, int(self.port))) + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.connect((self.ip, int(self.port))) # Implicit SSL if self.cert_pin: diff --git a/src/Tor/TorManager.py b/src/Tor/TorManager.py index 59c5989c..b9726683 100644 --- a/src/Tor/TorManager.py +++ b/src/Tor/TorManager.py @@ -304,9 +304,7 @@ class TorManager(object): self.log.debug("Creating new Tor socket to %s:%s" % (onion, port)) if config.tor == "always": # Every socket is proxied by default, in this mode sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.connect((onion, int(port))) else: sock = socks.socksocket() sock.set_proxy(socks.SOCKS5, self.proxy_ip, self.proxy_port) - sock.connect((onion, int(port))) return sock \ No newline at end of file From 5f37bf3eef58a8d63fc7844aae0f69ff36952cc9 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 14:31:16 +0200 Subject: [PATCH 0267/2570] Enable TCP_NODELAY if supported --- src/Connection/Connection.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index db874839..78b1ed1a 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -105,6 +105,10 @@ class Connection(object): self.sock = self.server.tor_manager.createSocket(self.ip, self.port) else: self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + if "TCP_NODELAY" in dir(socket): + self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + self.sock.connect((self.ip, int(self.port))) # Implicit SSL @@ -123,6 +127,10 @@ class Connection(object): # Handle incoming connection def handleIncomingConnection(self, sock): self.log("Incoming connection...") + + if "TCP_NODELAY" in dir(socket): + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + self.type = "in" if self.ip not in config.ip_local: # Clearnet: Check implicit SSL try: From bf0d35911644dd564d03378aa463f4e9c4d2fb75 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 14:31:39 +0200 Subject: [PATCH 0268/2570] Increase buffer size to 64k to better performance for big files --- 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 78b1ed1a..a0bd458b 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -156,7 +156,7 @@ class Connection(object): self.unpacker = msgpack.fallback.Unpacker() # Due memory problems of C version try: while not self.closed: - buff = self.sock.recv(16 * 1024) + buff = self.sock.recv(64 * 1024) if not buff: break # Connection closed buff_len = len(buff) From e76164c7a932ab36d9d571dc73e7302879f2c0fd Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 14:32:02 +0200 Subject: [PATCH 0269/2570] Change the posistion of handleStream function --- src/Connection/Connection.py | 86 ++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index a0bd458b..0fe24a66 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -187,6 +187,49 @@ class Connection(object): self.log("Socket error: %s" % Debug.formatException(err)) self.close("MessageLoop ended") # MessageLoop ended, close connection + # Stream socket directly to a file + def handleStream(self, message): + + read_bytes = message["stream_bytes"] # Bytes left we have to read from socket + try: + buff = self.unpacker.read_bytes(min(16 * 1024, read_bytes)) # Check if the unpacker has something left in buffer + except Exception, err: + buff = "" + file = self.waiting_streams[message["to"]] + if buff: + read_bytes -= len(buff) + file.write(buff) + + if config.debug_socket: + self.log("Starting stream %s: %s bytes (%s from unpacker)" % (message["to"], message["stream_bytes"], len(buff))) + + try: + while 1: + if read_bytes <= 0: + break + buff = self.sock.recv(16 * 1024) + if not buff: + break + buff_len = len(buff) + read_bytes -= buff_len + file.write(buff) + + # Statistics + self.last_recv_time = time.time() + self.incomplete_buff_recv += 1 + self.bytes_recv += buff_len + self.server.bytes_recv += buff_len + except Exception, err: + self.log("Stream read error: %s" % Debug.formatException(err)) + + if config.debug_socket: + self.log("End stream %s" % message["to"]) + + self.incomplete_buff_recv = 0 + self.waiting_requests[message["to"]].set(message) # Set the response to event + del self.waiting_streams[message["to"]] + del self.waiting_requests[message["to"]] + # My handshake info def getHandshakeInfo(self): # No TLS for onion connections @@ -320,49 +363,6 @@ class Connection(object): if not self.sock_wrapped and self.cert_pin: self.close("Crypt connection error: Socket not encrypted, but certificate pin present") - # Stream socket directly to a file - def handleStream(self, message): - - read_bytes = message["stream_bytes"] # Bytes left we have to read from socket - try: - buff = self.unpacker.read_bytes(min(16 * 1024, read_bytes)) # Check if the unpacker has something left in buffer - except Exception, err: - buff = "" - file = self.waiting_streams[message["to"]] - if buff: - read_bytes -= len(buff) - file.write(buff) - - if config.debug_socket: - self.log("Starting stream %s: %s bytes (%s from unpacker)" % (message["to"], message["stream_bytes"], len(buff))) - - try: - while 1: - if read_bytes <= 0: - break - buff = self.sock.recv(16 * 1024) - if not buff: - break - buff_len = len(buff) - read_bytes -= buff_len - file.write(buff) - - # Statistics - self.last_recv_time = time.time() - self.incomplete_buff_recv += 1 - self.bytes_recv += buff_len - self.server.bytes_recv += buff_len - except Exception, err: - self.log("Stream read error: %s" % Debug.formatException(err)) - - if config.debug_socket: - self.log("End stream %s" % message["to"]) - - self.incomplete_buff_recv = 0 - self.waiting_requests[message["to"]].set(message) # Set the response to event - del self.waiting_streams[message["to"]] - del self.waiting_requests[message["to"]] - # Send data to connection def send(self, message, streaming=False): self.last_send_time = time.time() From 2cf86642a246b904136ff7a3f57d555abb5ad4af Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 14:32:31 +0200 Subject: [PATCH 0270/2570] Run connection cleanup every 15 seconds --- src/Connection/ConnectionServer.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index 987018fb..7e14df32 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -170,10 +170,11 @@ class ConnectionServer: run_i = 0 while self.running: run_i += 1 - time.sleep(60) # Check every minute + time.sleep(15) # Check every minute self.ip_incoming = {} # Reset connected ips counter self.broken_ssl_peer_ids = {} # Reset broken ssl peerids count last_message_time = 0 + s = time.time() for connection in self.connections[:]: # Make a copy idle = time.time() - max(connection.last_recv_time, connection.start_time, connection.last_message_time) last_message_time = max(last_message_time, connection.last_message_time) @@ -220,7 +221,7 @@ class ConnectionServer: "[Cleanup] No site for connection" ) - elif run_i % 30 == 0: + elif run_i % 90 == 0: # Reset bad action counter every 30 min connection.bad_actions = 0 @@ -236,6 +237,9 @@ class ConnectionServer: self.has_internet = True self.onInternetOnline() + if time.time() - s > 0.01: + self.log.debug("Connection cleanup in %.3fs" % (time.time() - s)) + def onInternetOnline(self): self.log.info("Internet online") From 52a468d9fd3476c4f00ccf339c65c364fe3a27f1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 14:34:21 +0200 Subject: [PATCH 0271/2570] Allow 10 second for reponse --- src/Connection/ConnectionServer.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index 7e14df32..f3a97ca5 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -200,17 +200,17 @@ class ConnectionServer: # Incomplete data with more than 10 sec idle connection.close("[Cleanup] Connection buff stalled") - elif idle > 10 and connection.waiting_requests and time.time() - connection.last_send_time > 20: + elif idle > 10 and connection.protocol == "?": # No connection after 10 sec + connection.close( + "[Cleanup] Connect timeout: %.3fs" % idle + ) + + elif idle > 10 and connection.waiting_requests and time.time() - connection.last_send_time > 10: # Sent command and no response in 10 sec connection.close( "[Cleanup] Command %s timeout: %.3fs" % (connection.last_cmd, time.time() - connection.last_send_time) ) - elif idle > 30 and connection.protocol == "?": # No connection after 30 sec - connection.close( - "[Cleanup] Connect timeout: %.3fs" % idle - ) - elif idle < 60 and connection.bad_actions > 40: connection.close( "[Cleanup] Too many bad actions: %s" % connection.bad_actions From 7eca3f4bc54d2876d7b963b5758728da9e0c9ee6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 14:36:50 +0200 Subject: [PATCH 0272/2570] Display other peers optional files if we has not downloaded anything --- plugins/Stats/StatsPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py index afba9086..e9c420e6 100644 --- a/plugins/Stats/StatsPlugin.py +++ b/plugins/Stats/StatsPlugin.py @@ -165,7 +165,7 @@ class UiRequestPlugin(object): connection_id = peer.connection.id else: connection_id = None - if site.content_manager.hashfield: + if site.content_manager.has_optional_files: yield "Optional files: %4s " % len(peer.hashfield) time_added = (time.time() - peer.time_added) / (60 * 60 * 24) yield "(#%4s, err: %s, found: %3s min, add: %.1f day) %30s -
" % (connection_id, peer.connection_error, time_found, time_added, key) From bfb73921988cc5dfb109af67da7f5fef2128c178 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 14:37:15 +0200 Subject: [PATCH 0273/2570] Site has optional files if any of the peers has hashfield --- plugins/PeerDb/PeerDbPlugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/PeerDb/PeerDbPlugin.py b/plugins/PeerDb/PeerDbPlugin.py index 5e968b8b..e88e0cb3 100644 --- a/plugins/PeerDb/PeerDbPlugin.py +++ b/plugins/PeerDb/PeerDbPlugin.py @@ -50,6 +50,8 @@ class ContentDbPlugin(object): if row["address"].endswith(".onion"): peer.reputation = peer.reputation / 2 # 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): From 614a18913e6a5ec55f49bf74e346e6878ed73b68 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 14:37:50 +0200 Subject: [PATCH 0274/2570] Switch context during newsfeed queries to avoid blocking the client --- plugins/Newsfeed/NewsfeedPlugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/Newsfeed/NewsfeedPlugin.py b/plugins/Newsfeed/NewsfeedPlugin.py index 949e62b6..6cd3a467 100644 --- a/plugins/Newsfeed/NewsfeedPlugin.py +++ b/plugins/Newsfeed/NewsfeedPlugin.py @@ -74,6 +74,7 @@ class UiWebsocketPlugin(object): row["site"] = address row["feed_name"] = name rows.append(row) + time.sleep(0.0001) return self.response(to, rows) def actionFeedSearch(self, to, search): From 57c042ef79477e03f3a2c776dfe6466a2cf09382 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 14:38:33 +0200 Subject: [PATCH 0275/2570] Use recent peers for P2P port checking --- src/File/FileServer.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index d3aab8b3..0085c2b5 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -101,18 +101,19 @@ class FileServer(ConnectionServer): res = None if not site: # First run, has no any peers return self.testOpenportPortchecker(port) # Fallback to centralized service - for peer in site.peers.values(): - if not peer.ip.endswith(".onion"): # Get all non-onion peers - peers.append(peer) + peers = [peer for peer in site.getRecentPeers(10) if not peer.ip.endswith(".onion")] if len(peers) < 3: # Not enough peers return self.testOpenportPortchecker(port) # Fallback to centralized service for retry in range(0, 3): # Try 3 peers random_peer = random.choice(peers) with gevent.Timeout(10.0, False): # 10 sec timeout, don't raise exception - random_peer.connect() - res = random_peer.request("checkport", {"port": port}) - if res is not None: - break # All fine, exit from for loop + if not random_peer.connection: + random_peer.connect() + if random_peer.connection and random_peer.connection.handshake.get("rev") >= 2186: + res = random_peer.request("checkport", {"port": port}) + if res is not None: + break # All fine, exit from for loop + if res is None: # Nobody answered return self.testOpenportPortchecker(port) # Fallback to centralized service if res["status"] == "closed": From 02ad7542b393b987a214105072d9018017585392 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 14:39:04 +0200 Subject: [PATCH 0276/2570] Temporary disable P2P port checking until more client start supporting it --- 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 0085c2b5..72ed5f18 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -88,7 +88,7 @@ class FileServer(ConnectionServer): def testOpenport(self, port=None, use_alternative=True): if not port: port = self.port - back = self.testOpenportP2P(port) + back = self.testOpenportPortchecker(port) if back["result"] is not True and use_alternative: # If no success try alternative checker return self.testOpenportCanyouseeme(port) else: From 32ca6ac1a796e5331f504c645de9a55f15049971 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 14:40:13 +0200 Subject: [PATCH 0277/2570] Move site cache generation to separate function to allow plugins to extend it --- src/Site/Site.py | 6 ++++++ src/Site/SiteManager.py | 4 +--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index ea4bdda7..7d18b0f1 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -118,6 +118,12 @@ class Site(object): SiteManager.site_manager.load(False) SiteManager.site_manager.save() + def getSettingsCache(self): + back = {} + back["bad_files"] = self.bad_files + back["hashfield"] = self.content_manager.hashfield.tostring().encode("base64") + return back + # Max site size in MB def getSizeLimit(self): return self.settings.get("size_limit", int(config.size_limit)) diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index a0cb14a9..9a5ee1e1 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -90,9 +90,7 @@ class SiteManager(object): if recalculate_size: site.settings["size"] = site.content_manager.getTotalSize() # Update site size data[address] = site.settings - data[address]["cache"] = {} - data[address]["cache"]["bad_files"] = site.bad_files - data[address]["cache"]["hashfield"] = site.content_manager.hashfield.tostring().encode("base64") + data[address]["cache"] = site.getSettingsCache() time_generate = time.time() - s s = time.time() From 20e3dc5fa748c6f49e3e6a6514fea2bcecc2a2b3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 14:55:28 +0200 Subject: [PATCH 0278/2570] Only enable source code reloading for normal action --- src/Debug/DebugReloader.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Debug/DebugReloader.py b/src/Debug/DebugReloader.py index babcdca5..0e7eaf7b 100644 --- a/src/Debug/DebugReloader.py +++ b/src/Debug/DebugReloader.py @@ -23,10 +23,11 @@ class DebugReloader: if pyfilesystem: self.directory = directory self.callback = callback - logging.debug("Adding autoreload: %s, cb: %s" % (directory, callback)) - thread = threading.Thread(target=self.addWatcher) - thread.daemon = True - thread.start() + if config.action == "main": + logging.debug("Adding autoreload: %s, cb: %s" % (directory, callback)) + thread = threading.Thread(target=self.addWatcher) + thread.daemon = True + thread.start() def addWatcher(self, recursive=True): try: From fcfd428b549c5d61ab6315b9c31895fb93e98059 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 14:56:42 +0200 Subject: [PATCH 0279/2570] Enable plugins for Peer class --- src/Peer/Peer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index d2f0f8cd..a0fbff6e 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -9,12 +9,14 @@ from Debug import Debug from Config import config from util import helper from PeerHashfield import PeerHashfield +from Plugin import PluginManager if config.use_tempfiles: import tempfile # 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", From 71539829817584965f8feb46ec5e1af096d7b2dc Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 14:57:44 +0200 Subject: [PATCH 0280/2570] Allow to register callback after the pluginned classes got generated --- src/Plugin/PluginManager.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Plugin/PluginManager.py b/src/Plugin/PluginManager.py index 2a3c89f1..5769ac8f 100644 --- a/src/Plugin/PluginManager.py +++ b/src/Plugin/PluginManager.py @@ -15,6 +15,7 @@ class PluginManager: self.subclass_order = {} # Record the load order of the plugins, to keep it after reload self.pluggable = {} self.plugin_names = [] # Loaded plugin names + self.after_load = [] # Execute functions after loaded plugins sys.path.append(self.plugin_path) @@ -42,6 +43,9 @@ class PluginManager: if dir_name not in self.plugin_names: self.plugin_names.append(dir_name) + for func in self.after_load: + func() + # Reload all plugins def reloadPlugins(self): self.plugins_before = self.plugins @@ -141,6 +145,11 @@ def registerTo(class_name): return classDecorator +def afterLoad(func): + plugin_manager.after_load.append(func) + return func + + # - Example usage - if __name__ == "__main__": From 797abdea80fa7a0e25e22a76fbca0a32f9943d1f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 14:58:24 +0200 Subject: [PATCH 0281/2570] Load config class after it got pluginned --- plugins/OptionalManager/OptionalManagerPlugin.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/OptionalManager/OptionalManagerPlugin.py b/plugins/OptionalManager/OptionalManagerPlugin.py index 687c87af..75689190 100644 --- a/plugins/OptionalManager/OptionalManagerPlugin.py +++ b/plugins/OptionalManager/OptionalManagerPlugin.py @@ -6,6 +6,13 @@ from Plugin import PluginManager 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(): if access_log: content_db = ContentDbPlugin.content_db From d6d9e911fe18b7be70ec0622597f01531d3ed8ad Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 14:58:47 +0200 Subject: [PATCH 0282/2570] Load Peer class after it got pluginned --- plugins/AnnounceZero/AnnounceZeroPlugin.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/AnnounceZero/AnnounceZeroPlugin.py b/plugins/AnnounceZero/AnnounceZeroPlugin.py index 420ac182..cb0d251e 100644 --- a/plugins/AnnounceZero/AnnounceZeroPlugin.py +++ b/plugins/AnnounceZero/AnnounceZeroPlugin.py @@ -2,7 +2,6 @@ import hashlib import time from Plugin import PluginManager -from Peer import Peer from util import helper from Crypt import CryptRsa @@ -11,6 +10,13 @@ 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 importErrors(): + global Peer + from Peer import Peer + + # Process result got back from tracker def processPeerRes(site, peers): added = 0 From 8ae9b5261e2a9151d4cad8e9334652fc90c6208c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:03:56 +0200 Subject: [PATCH 0283/2570] Unify handling Stream and Get file requests --- src/File/FileRequest.py | 95 +++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 55 deletions(-) diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index 2fc17642..878b6f5f 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -187,34 +187,59 @@ class FileRequest(object): self.response({"error": "File invalid: %s" % err}) self.connection.badAction(5) + def isReadable(self, site, inner_path, file, pos): + return True + # Send file content request - def actionGetFile(self, params): + def handleGetFile(self, params, streaming=False): site = self.sites.get(params["site"]) if not site or not site.settings["serving"]: # Site unknown or not serving self.response({"error": "Unknown site"}) return False try: file_path = site.storage.getPath(params["inner_path"]) - with StreamingMsgpack.FilePart(file_path, "rb") as file: + if streaming: + file_obj = site.storage.open(params["inner_path"]) + else: + file_obj = StreamingMsgpack.FilePart(file_path, "rb") + + with file_obj as file: file.seek(params["location"]) - file.read_bytes = FILE_BUFF + read_bytes = params.get("read_bytes", FILE_BUFF) file_size = os.fstat(file.fileno()).st_size + + if file_size > read_bytes: # Check if file is readable at current position (for big files) + if not self.isReadable(site, params["inner_path"], file, params["location"]): + raise RequestError("File not readable at position: %s" % params["location"]) + + if not streaming: + file.read_bytes = read_bytes + if params.get("file_size") and params["file_size"] != file_size: - self.connection.badAction(5) + self.connection.badAction(2) raise RequestError("File size does not match: %sB != %sB" % (params["file_size"], file_size)) if params["location"] > file_size: self.connection.badAction(5) raise RequestError("Bad file location") - back = { - "body": file, - "size": file_size, - "location": min(file.tell() + FILE_BUFF, file_size) - } - self.response(back, streaming=True) + if streaming: + back = { + "size": file_size, + "location": min(file.tell() + read_bytes, file_size), + "stream_bytes": min(read_bytes, file_size - params["location"]) + } + self.response(back) + self.sendRawfile(file, read_bytes=read_bytes) + else: + back = { + "body": file, + "size": file_size, + "location": min(file.tell() + file.read_bytes, file_size) + } + self.response(back, streaming=True) - bytes_sent = min(FILE_BUFF, file_size - params["location"]) # Number of bytes we going to send + bytes_sent = min(read_bytes, file_size - params["location"]) # Number of bytes we going to send site.settings["bytes_sent"] = site.settings.get("bytes_sent", 0) + bytes_sent if config.debug_socket: self.log.debug("File %s at position %s sent %s bytes" % (file_path, params["location"], bytes_sent)) @@ -235,51 +260,11 @@ class FileRequest(object): self.response({"error": "File read error"}) return False - # New-style file streaming out of Msgpack context + def actionGetFile(self, params): + return self.handleGetFile(params) + def actionStreamFile(self, params): - site = self.sites.get(params["site"]) - if not site or not site.settings["serving"]: # Site unknown or not serving - self.response({"error": "Unknown site"}) - return False - try: - if config.debug_socket: - self.log.debug("Opening file: %s" % params["inner_path"]) - with site.storage.open(params["inner_path"]) as file: - file.seek(params["location"]) - file_size = os.fstat(file.fileno()).st_size - stream_bytes = min(FILE_BUFF, file_size - params["location"]) - if stream_bytes < 0: - self.connection.badAction(5) - raise RequestError("Bad file location") - - back = { - "size": file_size, - "location": min(file.tell() + FILE_BUFF, file_size), - "stream_bytes": stream_bytes - } - if config.debug_socket: - self.log.debug( - "Sending file %s from position %s to %s" % - (params["inner_path"], params["location"], back["location"]) - ) - self.response(back) - self.sendRawfile(file, read_bytes=FILE_BUFF) - - site.settings["bytes_sent"] = site.settings.get("bytes_sent", 0) + stream_bytes - if config.debug_socket: - self.log.debug("File %s at position %s sent %s bytes" % (params["inner_path"], params["location"], stream_bytes)) - - # Add peer to site if not added before - connected_peer = site.addPeer(self.connection.ip, self.connection.port) - if connected_peer: # Just added - connected_peer.connect(self.connection) # Assign current connection to peer - - return {"bytes_sent": stream_bytes, "file_size": file_size, "location": params["location"]} - - except Exception, err: - self.log.debug("GetFile read error: %s" % Debug.formatException(err)) - self.response({"error": "File read error"}) - return False + return self.handleGetFile(params, streaming=True) # Peer exchange request def actionPex(self, params): From 2eb6cc8f55e070c2e9128aada72e4855a49c29a0 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:07:26 +0200 Subject: [PATCH 0284/2570] Don't add myself to findhash result if ip detection failed --- src/File/FileRequest.py | 11 ++++++++--- src/Translate/languages/da.json | 4 ++-- src/Translate/languages/de.json | 4 ++-- src/Translate/languages/es.json | 4 ++-- src/Translate/languages/fr.json | 4 ++-- src/Translate/languages/hu.json | 4 ++-- src/Translate/languages/it.json | 4 ++-- src/Translate/languages/nl.json | 4 ++-- src/Translate/languages/pl.json | 4 ++-- src/Translate/languages/pt-br.json | 4 ++-- src/Translate/languages/ru.json | 4 ++-- src/Translate/languages/tr.json | 4 ++-- src/Translate/languages/zh-tw.json | 4 ++-- src/Translate/languages/zh.json | 4 ++-- 14 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index 878b6f5f..29d0a012 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -336,6 +336,7 @@ class FileRequest(object): self.response({"error": "Unknown site"}) return False + s = time.time() # 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 @@ -386,8 +387,11 @@ class FileRequest(object): elif config.ip_external: # External ip defined my_ip = helper.packAddress(config.ip_external, self.server.port) my_back = back_ip4 - else: # No external ip defined - my_ip = my_ip = helper.packAddress(self.server.ip, self.server.port) + elif self.server.ip and self.server.ip != "*": # No external ip defined + my_ip = helper.packAddress(self.server.ip, self.server.port) + my_back = back_ip4 + else: + my_ip = None my_back = back_ip4 my_hashfield_set = set(site.content_manager.hashfield) @@ -395,7 +399,8 @@ class FileRequest(object): if hash_id in my_hashfield_set: if hash_id not in my_back: my_back[hash_id] = [] - my_back[hash_id].append(my_ip) # Add myself + if my_ip: + my_back[hash_id].append(my_ip) # Add myself if config.verbose: self.log.debug( diff --git a/src/Translate/languages/da.json b/src/Translate/languages/da.json index 87ff8954..90509dd1 100644 --- a/src/Translate/languages/da.json +++ b/src/Translate/languages/da.json @@ -1,11 +1,11 @@ { - "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "Tillykke, din port ({0}) er åben.
Du er nu fuld klient på ZeroNet!", + "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "Tillykke, din port ({0}) er åben.
Du er nu fuld klient på ZeroNet!", "Tor mode active, every connection using Onion route.": "TOR er aktiv, alle forbindelser anvender Onions.", "Successfully started Tor onion hidden services.": "OK. Startede TOR skjult onion service.", "Unable to start hidden services, please check your config.": "Fejl. Kunne ikke starte TOR skjult onion service. Tjek din opsætning!", "For faster connections open {0} port on your router.": "Åben port {0} på din router for hurtigere forbindelse.", "Your connection is restricted. Please, open {0} port on your router": "Begrænset forbindelse. Åben venligst port {0} på din router", - "or configure Tor to become a full member of the ZeroNet network.": "eller opsæt TOR for fuld adgang til ZeroNet!", + "or configure Tor to become full member of ZeroNet network.": "eller opsæt TOR for fuld adgang til ZeroNet!", "Select account you want to use in this site:": "Vælg bruger til brug på denne side:", "currently selected": "nuværende bruger", diff --git a/src/Translate/languages/de.json b/src/Translate/languages/de.json index d234cdad..ceedab24 100644 --- a/src/Translate/languages/de.json +++ b/src/Translate/languages/de.json @@ -1,11 +1,11 @@ { - "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "Gratulation, dein Port {0} ist offen.
Du bist ein volles Mitglied des ZeroNet Netzwerks!", + "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "Gratulation, dein Port {0} ist offen.
Du bist ein volles Mitglied des ZeroNet Netzwerks!", "Tor mode active, every connection using Onion route.": "Tor modus aktiv, jede verbindung nutzt die Onion Route.", "Successfully started Tor onion hidden services.": "Tor versteckte Dienste erfolgreich gestartet.", "Unable to start hidden services, please check your config.": "Nicht möglich versteckte Dienste zu starten.", "For faster connections open {0} port on your router.": "Für schnellere verbindungen öffne Port {0} auf deinem Router.", "Your connection is restricted. Please, open {0} port on your router": "Deine Verbindung ist eingeschränkt. Bitte öffne Port {0} auf deinem Router", - "or configure Tor to become a full member of the ZeroNet network.": "oder konfiguriere Tor um ein volles Mitglied des ZeroNet Netzwerks zu werden.", + "or configure Tor to become full member of ZeroNet network.": "oder konfiguriere Tor um ein volles Mitglied des ZeroNet Netzwerks zu werden.", "Select account you want to use in this site:": "Wähle das Konto, dass du auf dieser Seite benutzen willst:", "currently selected": "aktuell ausgewählt", diff --git a/src/Translate/languages/es.json b/src/Translate/languages/es.json index cfb6e26e..659dc0e9 100644 --- a/src/Translate/languages/es.json +++ b/src/Translate/languages/es.json @@ -1,11 +1,11 @@ { - "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "¡Felicidades! tu puerto {0} está abierto.
¡Eres un miembro completo de la red Zeronet!", + "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "¡Felicidades! tu puerto {0} está abierto.
¡Eres un miembro completo de la red Zeronet!", "Tor mode active, every connection using Onion route.": "Modo Tor activado, cada conexión usa una ruta Onion.", "Successfully started Tor onion hidden services.": "Tor ha iniciado satisfactoriamente la ocultación de los servicios onion.", "Unable to start hidden services, please check your config.": "No se puedo iniciar los servicios ocultos, por favor comprueba tu configuración.", "For faster connections open {0} port on your router.": "Para conexiones más rápidas abre el puerto {0} en tu router.", "Your connection is restricted. Please, open {0} port on your router": "Tu conexión está limitada. Por favor, abre el puerto {0} en tu router", - "or configure Tor to become a full member of the ZeroNet network.": "o configura Tor para convertirte en un miembro completo de la red ZeroNet.", + "or configure Tor to become full member of ZeroNet network.": "o configura Tor para convertirte en un miembro completo de la red ZeroNet.", "Select account you want to use in this site:": "Selecciona la cuenta que quieres utilizar en este sitio:", "currently selected": "actualmente seleccionada", diff --git a/src/Translate/languages/fr.json b/src/Translate/languages/fr.json index dc0e5754..b6881cc4 100644 --- a/src/Translate/languages/fr.json +++ b/src/Translate/languages/fr.json @@ -1,11 +1,11 @@ { - "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "Félicitations, le port ({0}) est ouvert.
Vous êtes maintenant membre de ZeroNet!!", + "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "Félicitations, le port ({0}) est ouvert.
Vous êtes maintenant membre de ZeroNet!!", "Tor mode active, every connection using Onion route.": "Tor actif, toutes les connections utilisent un routage Onion.", "Successfully started Tor onion hidden services.": "Tor activé avec succès.", "Unable to start hidden services, please check your config.": "Incapable d'activer Tor, veuillez vérifier votre configuration.", "For faster connections open {0} port on your router.": "Pour une meilleure connectivité, ouvrez le port {0} sur votre routeur.", "Your connection is restricted. Please, open {0} port on your router": "Connectivité limitée. Veuillez ouvrir le port {0} sur votre routeur", - "or configure Tor to become a full member of the ZeroNet network.": "ou configurez Tor afin d'avoir accès aux pairs ZeroNet Onion.", + "or configure Tor to become full member of ZeroNet network.": "ou configurez Tor afin d'avoir accès aux pairs ZeroNet Onion.", "Select account you want to use in this site:": "Sélectionnez le compte que vous voulez utiliser pour ce site:", "currently selected": "présentement sélectionné", diff --git a/src/Translate/languages/hu.json b/src/Translate/languages/hu.json index 666c31c7..eb43615f 100644 --- a/src/Translate/languages/hu.json +++ b/src/Translate/languages/hu.json @@ -1,11 +1,11 @@ { - "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "Gratulálunk, a portod ({0}) nyitva van.
Teljes értékű tagja vagy a hálózatnak!", + "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "Gratulálunk, a portod ({0}) nyitva van.
Teljes értékű tagja vagy a hálózatnak!", "Tor mode active, every connection using Onion route.": "Tor mód aktív, minden kapcsolat az Onion hálózaton keresztül történik.", "Successfully started Tor onion hidden services.": "Sikeresen elindultak a Tor onion titkos szolgáltatások.", "Unable to start hidden services, please check your config.": "Nem sikerült elindítani a Tor onion szolgáltatásokat. Kérjük, ellenőrizd a beállításokat!", "For faster connections open {0} port on your router.": "A gyorsabb kapcsolatok érdekében nyisd ki a {0} portot a routereden.", "Your connection is restricted. Please, open {0} port on your router": "A kapcsolatod korlátozott. Kérjük, nyisd ki a {0} portot a routereden", - "or configure Tor to become a full member of the ZeroNet network.": "vagy állítsd be a Tor kliensed, hogy teljes értékű tagja legyél a hálózatnak!", + "or configure Tor to become full member of ZeroNet network.": "vagy állítsd be a Tor kliensed, hogy teljes értékű tagja legyél a hálózatnak!", "Select account you want to use in this site:": "Válaszd ki az oldalhoz használt felhasználónevet:", "currently selected": "jelenleg kijelölt", diff --git a/src/Translate/languages/it.json b/src/Translate/languages/it.json index 3d7651d7..95c69de0 100644 --- a/src/Translate/languages/it.json +++ b/src/Translate/languages/it.json @@ -1,11 +1,11 @@ { - "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "Congratulazioni, la tua porta ({0}) è aperta.
Sei ora pieno membro della rete ZeroNet!", + "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "Congratulazioni, la tua porta ({0}) è aperta.
Sei ora pieno membro della rete ZeroNet!", "Tor mode active, every connection using Onion route.": "Modalità Tor attiva, ogni connessione sta usando la rete Onion.", "Successfully started Tor onion hidden services.": "Tor onion hidden service avviati con successo.", "Unable to start hidden services, please check your config.": "Impossibile avviare gli hidden service. Si prega di controllare la propria configurazione!", "For faster connections open {0} port on your router.": "Per avere connessioni più veloci devi aprire la porta {0} sul tuo router.", "Your connection is restricted. Please, open {0} port on your router": "La tua connessione è limitata. Dovresti aprire la porta {0} sul tuo router", - "or configure Tor to become a full member of the ZeroNet network.": "o configurare Tor per diventare pieno membro della rete ZeroNet!", + "or configure Tor to become full member of ZeroNet network.": "o configurare Tor per diventare pieno membro della rete ZeroNet!", "Select account you want to use in this site:": "Seleziona l'account che vuoi utilizzare per questo sito:", "currently selected": "attualmente selezionato", diff --git a/src/Translate/languages/nl.json b/src/Translate/languages/nl.json index 6e7d364a..d2cd241c 100644 --- a/src/Translate/languages/nl.json +++ b/src/Translate/languages/nl.json @@ -1,11 +1,11 @@ { - "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "Gefeliciteerd, je poort {0} is geopend.
Je bent een volledig lid van het ZeroNet netwerk!", + "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "Gefeliciteerd, je poort {0} is geopend.
Je bent een volledig lid van het ZeroNet netwerk!", "Tor mode active, every connection using Onion route.": "Tor modus actief, elke verbinding gebruikt een Onion route.", "Successfully started Tor onion hidden services.": "Tor onion verborgen diensten zijn met succes gestart.", "Unable to start hidden services, please check your config.": "Het was niet mogelijk om verborgen diensten te starten, controleer je configuratie.", "For faster connections open {0} port on your router.": "Voor snellere verbindingen open je de poort {0} op je router.", "Your connection is restricted. Please, open {0} port on your router": "Je verbinding is beperkt. Open altjeblieft poort {0} op je router", - "or configure Tor to become a full member of the ZeroNet network.": "of configureer Tor om een volledig lid van het ZeroNet netwerk te worden.", + "or configure Tor to become full member of ZeroNet network.": "of configureer Tor om een volledig lid van het ZeroNet netwerk te worden.", "Select account you want to use in this site:": "Selecteer het account die je wilt gebruiken binnen deze site:", "currently selected": "huidige selectie", diff --git a/src/Translate/languages/pl.json b/src/Translate/languages/pl.json index 32f03bee..e3087c73 100644 --- a/src/Translate/languages/pl.json +++ b/src/Translate/languages/pl.json @@ -1,11 +1,11 @@ { - "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "Gratulacje, twój port {0} jest otwarty.
Jesteś pełnoprawnym użytkownikiem sieci ZeroNet!", + "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "Gratulacje, twój port {0} jest otwarty.
Jesteś pełnoprawnym użytkownikiem sieci ZeroNet!", "Tor mode active, every connection using Onion route.": "Tryb Tor aktywny, każde połączenie przy użyciu trasy Cebulowej.", "Successfully started Tor onion hidden services.": "Pomyślnie zainicjowano ukryte usługi cebulowe Tor.", "Unable to start hidden services, please check your config.": "Niezdolny do uruchomienia ukrytych usług, proszę sprawdź swoją konfigurację.", "For faster connections open {0} port on your router.": "Dla szybszego połączenia otwórz {0} port w swoim routerze.", "Your connection is restricted. Please, open {0} port on your router": "Połączenie jest ograniczone. Proszę, otwórz port {0} w swoim routerze", - "or configure Tor to become a full member of the ZeroNet network.": "bądź skonfiguruj Tora by stać się pełnoprawnym użytkownikiem sieci ZeroNet.", + "or configure Tor to become full member of ZeroNet network.": "bądź skonfiguruj Tora by stać się pełnoprawnym użytkownikiem sieci ZeroNet.", "Select account you want to use in this site:": "Wybierz konto którego chcesz użyć na tej stronie:", "currently selected": "aktualnie wybrany", diff --git a/src/Translate/languages/pt-br.json b/src/Translate/languages/pt-br.json index e9700ed3..d0aaf541 100644 --- a/src/Translate/languages/pt-br.json +++ b/src/Translate/languages/pt-br.json @@ -1,11 +1,11 @@ { - "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "Parabéns, a porta{0} está aberta.
Você é um membro completo da rede ZeroNet!", + "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "Parabéns, a porta{0} está aberta.
Você é um membro completo da rede ZeroNet!", "Tor mode active, every connection using Onion route.": "Modo Tor ativado, todas as conexões usam a rota Onion.", "Successfully started Tor onion hidden services.": "Os serviços ocultos Tor onion foram inciados com sucesso.", "Unable to start hidden services, please check your config.": "Não foi possível iniciar os serviços ocultos, por favor verifique suas configurações.", "For faster connections open {0} port on your router.": "Para conexões mais rápidas, abra a porta {0} em seu roteador.", "Your connection is restricted. Please, open {0} port on your router": "Sua conexão está restrita. Por favor, abra a porta {0} em seu roteador", - "or configure Tor to become a full member of the ZeroNet network.": "ou configure o Tor para se tornar um membro completo da rede ZeroNet.", + "or configure Tor to become full member of ZeroNet network.": "ou configure o Tor para se tornar um membro completo da rede ZeroNet.", "Select account you want to use in this site:": "Selecione a conta que deseja usar nesse site:", "currently selected": "atualmente selecionada", diff --git a/src/Translate/languages/ru.json b/src/Translate/languages/ru.json index 5996cd19..5bb1c4e4 100644 --- a/src/Translate/languages/ru.json +++ b/src/Translate/languages/ru.json @@ -1,11 +1,11 @@ { - "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "Поздравляем, ваш порт {0} открыт.
Вы полноценный участник сети ZeroNet!", + "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "Поздравляем, ваш порт {0} открыт.
Вы полноценный участник сети ZeroNet!", "Tor mode active, every connection using Onion route.": "Режим Tor включен, все соединения осуществляются через Tor.", "Successfully started Tor onion hidden services.": "Скрытый сервис Tor запущено успешно.", "Unable to start hidden services, please check your config.": "Ошибка при запуске скрытого сервиса, пожалуйста проверьте настройки", "For faster connections open {0} port on your router.": "Для более быстрой работы сети откройте {0} порт на вашем роутере.", "Your connection is restricted. Please, open {0} port on your router": "Подключение ограничено. Пожалуйста откройте {0} порт на вашем роутере", - "or configure Tor to become a full member of the ZeroNet network.": "или настройте Tor что бы стать полноценным участником сети ZeroNet.", + "or configure Tor to become full member of ZeroNet network.": "или настройте Tor что бы стать полноценным участником сети ZeroNet.", "Select account you want to use in this site:": "Выберите аккаунт для использования на этом сайте:", "currently selected": "сейчас выбран", diff --git a/src/Translate/languages/tr.json b/src/Translate/languages/tr.json index c27fc1f4..0bdabd89 100644 --- a/src/Translate/languages/tr.json +++ b/src/Translate/languages/tr.json @@ -1,11 +1,11 @@ { - "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "Tebrikler, portunuz ({0}) açık.
Artık ZeroNet ağına katıldınız!", + "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "Tebrikler, portunuz ({0}) açık.
Artık ZeroNet ağına katıldınız!", "Tor mode active, every connection using Onion route.": "Tor aktif, tüm bağlantılar Onion yönlendircisini kullanıyor.", "Successfully started Tor onion hidden services.": "Gizli Tor hizmetleri başlatıldı.", "Unable to start hidden services, please check your config.": "Gizli hizmetler başlatılamadı, lütfen ayarlarınızı kontrol ediniz.", "For faster connections open {0} port on your router.": "Daha hızlı bağlantı için {0} nolu portu bilgisayarınıza yönlendirin.", "Your connection is restricted. Please, open {0} port on your router": "Sınırlı bağlantı. Lütfen, {0} nolu portu bilgisayarınıza yönlendirin", - "or configure Tor to become a full member of the ZeroNet network.": "ya da ZeroNet ağına tam olarak katılabilmek için Tor'u kullanın.", + "or configure Tor to become full member of ZeroNet network.": "ya da ZeroNet ağına tam olarak katılabilmek için Tor'u kullanın.", "Select account you want to use in this site:": "Bu sitede kullanmak için bir hesap seçiniz:", "currently selected": "kullanılan", diff --git a/src/Translate/languages/zh-tw.json b/src/Translate/languages/zh-tw.json index ea0130a0..a30dd023 100644 --- a/src/Translate/languages/zh-tw.json +++ b/src/Translate/languages/zh-tw.json @@ -1,11 +1,11 @@ { - "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "祝賀,你的埠 ({0}) 已經打開。
你已經是 ZeroNet 網路的正式成員了!", + "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "祝賀,你的埠 ({0}) 已經打開。
你已經是 ZeroNet 網路的正式成員了!", "Tor mode active, every connection using Onion route.": "Tor 模式啟用,每個連接正在使用洋蔥路由。", "Successfully started Tor onion hidden services.": "成功啟動 Tor 洋蔥隱藏服務。", "Unable to start hidden services, please check your config.": "無法打開隱藏服務,請檢查你的配置。", "For faster connections open {0} port on your router.": "為了更快的連接請在路由器上打開 {0} 埠。", "Your connection is restricted. Please, open {0} port on your router": "你的連接受限制。請在你的路由器上打開 {0} 埠", - "or configure Tor to become a full member of the ZeroNet network.": "或者配置你的 Tor 來成為 ZeroNet 的正式成員。", + "or configure Tor to become full member of ZeroNet network.": "或者配置你的 Tor 來成為 ZeroNet 的正式成員。", "Select account you want to use in this site:": "選擇你要在這個網站使用的帳戶:", "currently selected": "當前選擇", diff --git a/src/Translate/languages/zh.json b/src/Translate/languages/zh.json index 2d6c1f5d..e0b1232f 100644 --- a/src/Translate/languages/zh.json +++ b/src/Translate/languages/zh.json @@ -1,11 +1,11 @@ { - "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "祝贺,你的端口 ({0}) 已经打开。
你已经是 ZeroNet 网络的正式成员了!", + "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "祝贺,你的端口 ({0}) 已经打开。
你已经是 ZeroNet 网络的正式成员了!", "Tor mode active, every connection using Onion route.": "Tor 模式启用,每个连接正在使用洋葱路由。", "Successfully started Tor onion hidden services.": "成功启动 Tor 洋葱隐藏服务。", "Unable to start hidden services, please check your config.": "无法打开隐藏服务,请检查你的配置。", "For faster connections open {0} port on your router.": "为了更快的连接请在路由器上打开 {0} 端口。", "Your connection is restricted. Please, open {0} port on your router": "你的连接受限制。请在你的路由器上打开 {0} 端口", - "or configure Tor to become a full member of the ZeroNet network.": "或者配置你的 Tor 来成为 ZeroNet 的正式成员。", + "or configure Tor to become full member of ZeroNet network.": "或者配置你的 Tor 来成为 ZeroNet 的正式成员。", "Select account you want to use in this site:": "选择你要在这个网站使用的帐户:", "currently selected": "当前选择", From 636813c603bfb1eb428401a15088d73ef5472ee0 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:19:09 +0200 Subject: [PATCH 0285/2570] Reduce connection cpu usage by socket send time --- src/File/FileRequest.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index 29d0a012..e86270c8 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -82,7 +82,6 @@ class FileRequest(object): func_name = "action" + cmd[0].upper() + cmd[1:] func = getattr(self, func_name, None) if cmd not in ["getFile", "streamFile"]: # Skip IO bound functions - s = time.time() if self.connection.cpu_time > 0.5: self.log.debug( "Delay %s %s, cpu_time used by connection: %.3fs" % @@ -91,6 +90,7 @@ class FileRequest(object): time.sleep(self.connection.cpu_time) if self.connection.cpu_time > 5: self.connection.close("Cpu time: %.3fs" % self.connection.cpu_time) + s = time.time() if func: func(params) else: @@ -98,7 +98,8 @@ class FileRequest(object): if cmd not in ["getFile", "streamFile"]: taken = time.time() - s - self.connection.cpu_time += taken + taken_sent = self.connection.last_sent_time - self.connection.last_send_time + self.connection.cpu_time += taken - taken_sent # Update a site file request def actionUpdate(self, params): From 628cc992e98d54f0c4d620df42b4dd77374c41cb Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:19:34 +0200 Subject: [PATCH 0286/2570] Don't send ok result when update is queued --- src/File/FileRequest.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index e86270c8..1e87271c 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -73,9 +73,6 @@ class FileRequest(object): if cmd == "update": event = "%s update %s %s" % (self.connection.id, params["site"], params["inner_path"]) - if not RateLimit.isAllowed(event): # There was already an update for this file in the last 10 second - time.sleep(5) - self.response({"ok": "File update queued"}) # If called more than once within 15 sec only keep the last update RateLimit.callAsync(event, max(self.connection.bad_actions, 15), self.actionUpdate, params) else: From 936371a7ec2aa26c245aa7e0aaef0585d2953efe Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:20:50 +0200 Subject: [PATCH 0287/2570] Support digest output for sha512sum --- src/Crypt/CryptHash.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Crypt/CryptHash.py b/src/Crypt/CryptHash.py index fb0c2dab..39459bcc 100644 --- a/src/Crypt/CryptHash.py +++ b/src/Crypt/CryptHash.py @@ -12,13 +12,19 @@ def sha1sum(file, blocksize=65536): return hash.hexdigest() -def sha512sum(file, blocksize=65536): +def sha512sum(file, blocksize=65536, format="hexdigest"): if hasattr(file, "endswith"): # Its a string open it file = open(file, "rb") hash = hashlib.sha512() for block in iter(lambda: file.read(blocksize), ""): hash.update(block) - return hash.hexdigest()[0:64] # Truncate to 256bits is good enough + + # Truncate to 256bits is good enough + if format == "hexdigest": + return hash.hexdigest()[0:64] + else: + return hash.digest()[0:32] + def sha256sum(file, blocksize=65536): From 8d26a572ddb08f48a460b9db1cd195113bf16a6f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:22:05 +0200 Subject: [PATCH 0288/2570] Hashlib-like truncated sha512/256 object --- src/Crypt/CryptHash.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/Crypt/CryptHash.py b/src/Crypt/CryptHash.py index 39459bcc..118053b6 100644 --- a/src/Crypt/CryptHash.py +++ b/src/Crypt/CryptHash.py @@ -44,20 +44,23 @@ def random(length=64, encoding="hex"): return hashlib.sha512(os.urandom(256)).hexdigest()[0:length] +# Sha512 truncated to 256bits +class Sha512t: + def __init__(self, data): + if data: + self.sha512 = hashlib.sha512(data) + else: + self.sha512 = hashlib.sha512() -if __name__ == "__main__": - import cStringIO as StringIO - a = StringIO.StringIO() - a.write("hello!") - a.seek(0) - print hashlib.sha1("hello!").hexdigest() - print sha1sum(a) + def hexdigest(self): + return self.sha512.hexdigest()[0:64] - import time - s = time.time() - print sha1sum(open("F:\\Temp\\bigfile")), - print time.time() - s + def digest(self): + return self.sha512.digest()[0:32] - s = time.time() - print sha512sum(open("F:\\Temp\\bigfile")), - print time.time() - s + def update(self, data): + return self.sha512.update(data) + + +def sha512t(data=None): + return Sha512t(data) From f9e64bc874be415eb28669bee558e7f4e7512306 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:23:03 +0200 Subject: [PATCH 0289/2570] Log inner_path and location for peer request in verbose mode --- 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 a0fbff6e..4018f6ee 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -125,7 +125,7 @@ class Peer(object): self.onConnectionError("Reconnect error") return None # Connection failed - self.log("Send request: %s %s" % (params.get("site", ""), cmd)) + self.log("Send request: %s %s %s %s" % (params.get("site", ""), cmd, params.get("inner_path", ""), params.get("location", ""))) for retry in range(1, 4): # Retry 3 times try: From 1c029981ae5d669324ce7b70f7cd7dc8d6827de1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:23:25 +0200 Subject: [PATCH 0290/2570] Raise exception when no response received --- src/Peer/Peer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index 4018f6ee..edbfcbc0 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -141,7 +141,10 @@ class Peer(object): else: # Successful request, reset connection error num self.connection_error = 0 self.time_response = time.time() - return res + if res: + return res + else: + raise Exception("Invalid response: %s" % res) except Exception, err: if type(err).__name__ == "Notify": # Greenlet killed by worker self.log("Peer worker got killed: %s, aborting cmd: %s" % (err.message, cmd)) From c0e69e91a12fd5ed66fd305b9f868f21b7063add Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:26:39 +0200 Subject: [PATCH 0291/2570] Always use streamFile for files bigger than 512k, increase file reads to 1MB for larger files --- src/Peer/Peer.py | 78 ++++++++++++++++++++++-------------------------- 1 file changed, 36 insertions(+), 42 deletions(-) diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index edbfcbc0..6d6fa40b 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -160,62 +160,56 @@ class Peer(object): return None # Failed after 4 retry # Get a file content from peer - def getFile(self, site, inner_path, file_size=None): - # Use streamFile if client supports it - if config.stream_downloads and self.connection and self.connection.handshake and self.connection.handshake["rev"] > 310: - return self.streamFile(site, inner_path) + def getFile(self, site, inner_path, file_size=None, pos_from=0, pos_to=None, streaming=False): + if file_size and file_size > 5 * 1024 * 1024: + max_read_size = 1024 * 1024 + else: + max_read_size = 512 * 1024 + + if pos_to: + read_bytes = min(max_read_size, pos_to - pos_from) + else: + read_bytes = max_read_size + + location = pos_from - location = 0 if config.use_tempfiles: buff = tempfile.SpooledTemporaryFile(max_size=16 * 1024, mode='w+b') else: buff = StringIO() s = time.time() - while True: # Read in 512k parts - res = self.request("getFile", {"site": site, "inner_path": inner_path, "location": location, "file_size": file_size}) + while True: # Read in smaller parts + if config.stream_downloads or read_bytes > 256 * 1024 or streaming: + res = self.request("streamFile", {"site": site, "inner_path": inner_path, "location": location, "read_bytes": read_bytes, "file_size": file_size}, stream_to=buff) + if not res or "location" not in res: # Error + return False + else: + self.log("Send: %s" % inner_path) + res = self.request("getFile", {"site": site, "inner_path": inner_path, "location": location, "read_bytes": read_bytes, "file_size": file_size}) + if not res or "location" not in res: # Error + return False + self.log("Recv: %s" % inner_path) + buff.write(res["body"]) + res["body"] = None # Save memory - if not res or "body" not in res: # Error - return False - - buff.write(res["body"]) - res["body"] = None # Save memory - if res["location"] == res["size"]: # End of file + if res["location"] == res["size"] or res["location"] == pos_to: # End of file break else: location = res["location"] + if pos_to: + read_bytes = min(max_read_size, pos_to - location) - self.download_bytes += res["location"] + if pos_to: + recv = pos_to - pos_from + else: + recv = res["location"] + + self.download_bytes += recv self.download_time += (time.time() - s) if self.site: - self.site.settings["bytes_recv"] = self.site.settings.get("bytes_recv", 0) + res["location"] - buff.seek(0) - return buff - - # Download file out of msgpack context to save memory and cpu - def streamFile(self, site, inner_path): - location = 0 - if config.use_tempfiles: - buff = tempfile.SpooledTemporaryFile(max_size=16 * 1024, mode='w+b') - else: - buff = StringIO() - - s = time.time() - while True: # Read in 512k parts - res = self.request("streamFile", {"site": site, "inner_path": inner_path, "location": location}, stream_to=buff) - - if not res or "location" not in res: # Error - self.log("Invalid response: %s" % res) - return False - - if res["location"] == res["size"]: # End of file - break - else: - location = res["location"] - - self.download_bytes += res["location"] - self.download_time += (time.time() - s) - self.site.settings["bytes_recv"] = self.site.settings.get("bytes_recv", 0) + res["location"] + self.site.settings["bytes_recv"] = self.site.settings.get("bytes_recv", 0) + recv + self.log("Downloaded: %s, pos: %s, read_bytes: %s" % (inner_path, buff.tell(), read_bytes)) buff.seek(0) return buff From c494b0143508e2774d9f2d5f2686f7fb490b0b10 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:26:53 +0200 Subject: [PATCH 0292/2570] Update hashfiled more often --- src/Peer/Peer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index 6d6fa40b..e8b60796 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -272,8 +272,8 @@ class Peer(object): return self.request("listModified", {"since": since, "site": self.site.address}) def updateHashfield(self, force=False): - # Don't update hashfield again in 15 min - if self.time_hashfield and time.time() - self.time_hashfield > 60 * 15 and not force: + # 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 self.time_hashfield = time.time() From 08b31416f376d361c5c0a977774d1932f635b5f4 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:28:37 +0200 Subject: [PATCH 0293/2570] Move needFileInfo to separate function --- src/Site/Site.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 7d18b0f1..1867e056 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -703,6 +703,17 @@ class Site(object): def pooledNeedFile(self, *args, **kwargs): return self.needFile(*args, **kwargs) + def needFileInfo(self, inner_path): + file_info = self.content_manager.getFileInfo(inner_path) + if not file_info: + # No info for file, download all content.json first + self.log.debug("No info for %s, waiting for all content.json" % inner_path) + success = self.downloadContent("content.json", download_files=False) + if not success: + return False + file_info = self.content_manager.getFileInfo(inner_path) + return file_info + # Check and download if file not exist def needFile(self, inner_path, update=False, blocking=True, peer=None, priority=0): if self.storage.isFile(inner_path) and not update: # File exist, no need to do anything @@ -721,17 +732,11 @@ class Site(object): if not self.content_manager.contents.get("content.json"): return False # Content.json download failed + file_info = None if not inner_path.endswith("content.json"): - file_info = self.content_manager.getFileInfo(inner_path) + file_info = self.needFileInfo(inner_path) if not file_info: - # No info for file, download all content.json first - self.log.debug("No info for %s, waiting for all content.json" % inner_path) - success = self.downloadContent("content.json", download_files=False) - if not success: - return False - file_info = self.content_manager.getFileInfo(inner_path) - if not file_info: - return False # Still no info for file + return False if "cert_signers" in file_info and not file_info["content_inner_path"] in self.content_manager.contents: self.log.debug("Missing content.json for requested user file: %s" % inner_path) if self.bad_files.get(file_info["content_inner_path"], 0) > 5: From ad969dcca7acb27fa417278330451d7edb9121aa Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:29:26 +0200 Subject: [PATCH 0294/2570] Move file download allowed checking to separate function --- src/Site/Site.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 1867e056..44f86eda 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -703,6 +703,16 @@ class Site(object): def pooledNeedFile(self, *args, **kwargs): return self.needFile(*args, **kwargs) + def isFileDownloadAllowed(self, inner_path, file_info): + if file_info.get("size", 0) > config.file_size_limit * 1024 * 1024: + self.log.debug( + "File size %s too large: %sMB > %sMB, skipping..." % + (inner_path, file_info.get("size", 0) / 1024 / 1024, config.file_size_limit) + ) + return False + else: + return True + def needFileInfo(self, inner_path): file_info = self.content_manager.getFileInfo(inner_path) if not file_info: @@ -745,11 +755,9 @@ class Site(object): )) return False self.downloadContent(file_info["content_inner_path"]) - if file_info.get("size", 0) > config.file_size_limit * 1024 * 1024: - self.log.debug( - "File size %s too large: %sMB > %sMB, skipping..." % - (inner_path, file_info.get("size", 0) / 1024 / 1024, config.file_size_limit) - ) + + if not self.isFileDownloadAllowed(inner_path, file_info): + self.log.debug("%s: Download not allowed" % inner_path) return False task = self.worker_manager.addTask(inner_path, peer, priority=priority) From 5ccdfbd40a4d8b6d1109f03c58172ecb3dc7af84 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:29:52 +0200 Subject: [PATCH 0295/2570] Update site optional size on save --- src/Site/SiteManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index 9a5ee1e1..556f7197 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -88,7 +88,7 @@ class SiteManager(object): s = time.time() for address, site in self.list().iteritems(): if recalculate_size: - site.settings["size"] = site.content_manager.getTotalSize() # Update site size + site.settings["size"], site.settings["size_optional"] = site.content_manager.getTotalSize() # Update site size data[address] = site.settings data[address]["cache"] = site.getSettingsCache() time_generate = time.time() - s From 468fe8f266ef337926fef0c207b74a0e5c8b3eab Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:30:10 +0200 Subject: [PATCH 0296/2570] Move json formatting to separate function --- src/Site/SiteStorage.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index e1caabbc..9bb2da44 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -258,8 +258,7 @@ class SiteStorage(object): with self.open(inner_path) as file: return json.load(file) - # Write formatted json file - def writeJson(self, inner_path, data): + def formatJson(self, data): content = json.dumps(data, indent=1, sort_keys=True) # Make it a little more compact by removing unnecessary white space @@ -282,9 +281,12 @@ class SiteStorage(object): # Remove end of line whitespace content = re.sub("(?m)[ ]+$", "", content) + return content + # Write formatted json file + def writeJson(self, inner_path, data): # Write to disk - self.write(inner_path, content) + self.write(inner_path, self.formatJson(data)) # Get file size def getSize(self, inner_path): From 9b8eeb6a7399e1a303852f8c525e61ace60d3e4e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:47:46 +0200 Subject: [PATCH 0297/2570] WorkerManager addTask returns task instead of event --- src/Site/Site.py | 4 ++-- src/Worker/WorkerManager.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 44f86eda..0382aa31 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -737,7 +737,7 @@ class Site(object): gevent.spawn(self.announce) if inner_path != "content.json": # Prevent double download task = self.worker_manager.addTask("content.json", peer) - task.get() + task["evt"].get() self.content_manager.loadContent() if not self.content_manager.contents.get("content.json"): return False # Content.json download failed @@ -762,7 +762,7 @@ class Site(object): task = self.worker_manager.addTask(inner_path, peer, priority=priority) if blocking: - return task.get() + return task["evt"].get() else: return task diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 633e5e1f..c17903ba 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -424,7 +424,7 @@ class WorkerManager(object): if priority: task["priority"] += priority # Boost on priority - return task["evt"] + return task else: # No task for that file yet evt = gevent.event.AsyncResult() if peer: @@ -470,7 +470,7 @@ class WorkerManager(object): else: self.startWorkers(peers) - return evt + return task # Find a task using inner_path def findTask(self, inner_path): From f45c0b2377c47b7963dfdc8cecd0cf4d588eaaf2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:48:36 +0200 Subject: [PATCH 0298/2570] Log time taken for optonal file download --- src/Worker/WorkerManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index c17903ba..d255930e 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -496,7 +496,7 @@ class WorkerManager(object): task["done"] = True self.tasks.remove(task) # Remove from queue if task["optional_hash_id"]: - self.log.debug("Downloaded optional file, adding to hashfield: %s" % task["inner_path"]) + self.log.debug("Downloaded optional file in %.3fs, adding to hashfield: %s" % (time.time() - task["time_started"], task["inner_path"])) self.site.content_manager.optionalDownloaded(task["inner_path"], task["optional_hash_id"], task["size"]) self.site.onFileDone(task["inner_path"]) task["evt"].set(True) From c91011e6ea73a03f87fbc07ba543affe3970be62 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:49:08 +0200 Subject: [PATCH 0299/2570] Pass file info for addTask call --- src/Site/Site.py | 4 ++-- src/Worker/WorkerManager.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 0382aa31..69ec2a57 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -760,11 +760,11 @@ class Site(object): self.log.debug("%s: Download not allowed" % inner_path) return False - task = self.worker_manager.addTask(inner_path, peer, priority=priority) + task = self.worker_manager.addTask(inner_path, peer, priority=priority, file_info=file_info) if blocking: return task["evt"].get() else: - return task + return task["evt"] # Add or update a peer to site # return_peer: Always return the peer even if it was already present diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index d255930e..53a85070 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -409,7 +409,7 @@ class WorkerManager(object): return 0 # Create new task and return asyncresult - def addTask(self, inner_path, peer=None, priority=0): + def addTask(self, inner_path, peer=None, priority=0, file_info=None): self.site.onFileStart(inner_path) # First task, trigger site download started task = self.findTask(inner_path) if task: # Already has task for that file @@ -431,7 +431,8 @@ class WorkerManager(object): peers = [peer] # Only download from this peer else: peers = None - file_info = self.site.content_manager.getFileInfo(inner_path) + if not file_info: + file_info = self.site.content_manager.getFileInfo(inner_path) if file_info and file_info["optional"]: optional_hash_id = helper.toHashId(file_info["sha512"]) else: From 43e6f374faabaa2f3083ee5507d29f7328cdba4e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:49:52 +0200 Subject: [PATCH 0300/2570] Don't increase priority when addTask called again --- src/Worker/WorkerManager.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 53a85070..81392197 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -413,6 +413,7 @@ class WorkerManager(object): self.site.onFileStart(inner_path) # First task, trigger site download started task = self.findTask(inner_path) if task: # Already has task for that file + task["priority"] = max(priority, task["priority"]) if peer and task["peers"]: # This peer also has new version, add it to task possible peers task["peers"].append(peer) self.log.debug("Added peer %s to %s" % (peer.key, task["inner_path"])) @@ -421,9 +422,6 @@ class WorkerManager(object): task["failed"].remove(peer) # New update arrived, remove the peer from failed peers self.log.debug("Removed peer %s from failed %s" % (peer.key, task["inner_path"])) self.startWorkers([peer]) - - if priority: - task["priority"] += priority # Boost on priority return task else: # No task for that file yet evt = gevent.event.AsyncResult() From 988d1696872ce16352f0369fe5ca8ee5093146e2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:51:17 +0200 Subject: [PATCH 0301/2570] Fix asked_peers ignore for optional files --- src/Worker/WorkerManager.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 81392197..e97037e6 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -299,16 +299,17 @@ class WorkerManager(object): # Try to query connected peers threads = [] - peers = [peer for peer in self.site.getConnectedPeers() if peer not in self.asked_peers] + peers = [peer for peer in self.site.getConnectedPeers() if peer.key not in self.asked_peers] if not peers: - peers = self.site.getConnectablePeers() + peers = self.site.getConnectablePeers(ignore=self.asked_peers) for peer in peers: threads.append(gevent.spawn(peer.findHashIds, list(optional_hash_ids))) - self.asked_peers.append(peer) + self.asked_peers.append(peer.key) for i in range(5): time.sleep(1) + thread_values = [thread.value for thread in threads if thread.value] if not thread_values: continue @@ -340,7 +341,7 @@ class WorkerManager(object): for peer in peers: threads.append(gevent.spawn(peer.findHashIds, list(optional_hash_ids))) - self.asked_peers.append(peer) + self.asked_peers.append(peer.key) gevent.joinall(threads, timeout=15) From e85efe2c64e681ad0cf17fcdfa5aa6fd0177cc8e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:52:04 +0200 Subject: [PATCH 0302/2570] Restart find optional files algorithm if new task started since running the function --- src/Worker/WorkerManager.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index e97037e6..2af1b511 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -356,6 +356,10 @@ class WorkerManager(object): if len(found) < len(optional_hash_ids): self.log.debug("No findhash result for optional files: %s" % (optional_hash_ids - set(found))) + if time_tasks != self.time_task_added: # New task added since start + self.log.debug("New task since start, restarting...") + gevent.spawn_later(0.1, self.startFindOptional) + # Stop all worker def stopWorkers(self): for worker in self.workers.values(): From 43bd8a2d6ca5a534ce3e86fd7efe3a2d4a8f7cef Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:52:37 +0200 Subject: [PATCH 0303/2570] Stop findhash if all task finished during the findhash call --- src/Worker/WorkerManager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 2af1b511..4f9949bd 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -296,6 +296,9 @@ class WorkerManager(object): if len(found) < len(optional_hash_ids) or find_more: self.log.debug("No connected hashtable result for optional files: %s" % (optional_hash_ids - set(found))) + if not self.tasks: + self.log.debug("No tasks, stopping finding optional peers") + return # Try to query connected peers threads = [] From c0b8e802ecc244bf2260b199b62e4910267608ec Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:53:00 +0200 Subject: [PATCH 0304/2570] Fix findhash initial delay --- src/Worker/WorkerManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 4f9949bd..04207ca8 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -247,7 +247,7 @@ class WorkerManager(object): # Wait for more file requests if len(self.tasks) < 20 or high_priority: time.sleep(0.01) - if len(self.tasks) > 90: + elif len(self.tasks) > 90: time.sleep(5) else: time.sleep(0.5) From 42ea01dde97bbf2e88d5ed2d1cba148b7ccfe25c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:53:59 +0200 Subject: [PATCH 0305/2570] No extra time for larger files task timeout --- src/Worker/WorkerManager.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 04207ca8..4b775934 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -50,8 +50,7 @@ class WorkerManager(object): tasks = self.tasks[:] # Copy it so removing elements wont cause any problem for task in tasks: - size_extra_time = task["size"] / (1024 * 100) # 1 second for every 100k - if task["time_started"] and time.time() >= task["time_started"] + 60 + size_extra_time: + if task["time_started"] and time.time() >= task["time_started"] + 60: self.log.debug("Timeout, Skipping: %s" % task) # Task taking too long time, skip it # Skip to next file workers workers = self.findWorkers(task) @@ -60,7 +59,7 @@ class WorkerManager(object): worker.skip() else: self.failTask(task) - elif time.time() >= task["time_added"] + 60 + size_extra_time and not self.workers: # No workers left + elif time.time() >= task["time_added"] + 60 and not self.workers: # No workers left self.log.debug("Timeout, Cleanup task: %s" % task) # Remove task self.failTask(task) @@ -69,9 +68,9 @@ class WorkerManager(object): # Find more workers: Task started more than 15 sec ago or no workers workers = self.findWorkers(task) self.log.debug( - "Slow task: %s 15+%ss, (workers: %s, optional_hash_id: %s, peers: %s, failed: %s, asked: %s)" % + "Slow task: %s, (workers: %s, optional_hash_id: %s, peers: %s, failed: %s, asked: %s)" % ( - task["inner_path"], size_extra_time, len(workers), task["optional_hash_id"], + task["inner_path"], len(workers), task["optional_hash_id"], len(task["peers"] or []), len(task["failed"]), len(self.asked_peers) ) ) From 6238bb7f2d0b4952c3ae6691a13eb52d487cebf0 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:55:07 +0200 Subject: [PATCH 0306/2570] Multiplexing support for peers (disabled by default) --- src/Worker/WorkerManager.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 4b775934..3fadcf4f 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -140,9 +140,13 @@ class WorkerManager(object): return config.workers # Add new worker - def addWorker(self, peer): + def addWorker(self, peer, multiplexing=False): key = peer.key - if key not in self.workers and len(self.workers) < self.getMaxWorkers(): + if len(self.workers) > self.getMaxWorkers(): + return False + if multiplexing: # Add even if we already have worker for this peer + key = "%s/%s" % (key, len(self.workers)) + if key not in self.workers: # We dont have worker for that peer and workers num less than max worker = Worker(self, peer) self.workers[key] = worker From 7c69118c2c678b56a78e2f0170cdb29c66134455 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:55:42 +0200 Subject: [PATCH 0307/2570] Move peer adding to separate function --- src/Worker/WorkerManager.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 3fadcf4f..ef3e648e 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -156,6 +156,16 @@ class WorkerManager(object): else: # We have woker for this peer or its over the limit return False + def taskAddPeer(self, task, peer): + if task["peers"] is None: + task["peers"] = [] + if peer in task["failed"]: + return False + + if peer not in task["peers"]: + task["peers"].append(peer) + return True + # Start workers to process tasks def startWorkers(self, peers=None): if not self.tasks: @@ -196,11 +206,8 @@ class WorkerManager(object): task["failed"] = [] if peer in task["failed"]: continue - found[optional_hash_id].append(peer) - if task["peers"] and peer not in task["peers"]: - task["peers"].append(peer) - else: - task["peers"] = [peer] + if self.taskAddPeer(task, peer): + found[optional_hash_id].append(peer) return found @@ -234,10 +241,7 @@ class WorkerManager(object): peer = self.site.addPeer(peer_ip[0], peer_ip[1], return_peer=True) if not peer: continue - if task["peers"] is None: - task["peers"] = [] - if peer not in task["peers"]: - task["peers"].append(peer) + if self.taskAddPeer(task, peer): found[hash_id].append(peer) if peer.hashfield.appendHashId(hash_id): # Peer has this file peer.time_hashfield = None # Peer hashfield probably outdated From 8ba420123717b8fbdb4b7716de215d313d5dfc3a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:56:58 +0200 Subject: [PATCH 0308/2570] More agressive optional file finding --- src/Worker/WorkerManager.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index ef3e648e..892e2edb 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -79,10 +79,8 @@ class WorkerManager(object): if self.workers: if not task["time_started"]: ask_limit = 20 - elif task["priority"] > 0: - ask_limit = max(10, time.time() - task["time_started"]) else: - ask_limit = max(10, (time.time() - task["time_started"]) / 2) + ask_limit = max(10, time.time() - task["time_started"]) if len(self.asked_peers) < ask_limit and len(task["peers"] or []) <= len(task["failed"]) * 2: # Re-search for high priority self.startFindOptional(find_more=True) @@ -90,13 +88,12 @@ class WorkerManager(object): peers_try = [peer for peer in task["peers"] if peer not in task["failed"]] if peers_try: self.startWorkers(peers_try) - else: - self.startFindOptional(find_more=True) + self.startFindOptional(find_more=True) else: if task["peers"]: # Release the peer lock self.log.debug("Task peer lock release: %s" % task["inner_path"]) task["peers"] = [] - self.startWorkers() + self.startWorkers() break # One reannounce per loop if len(self.tasks) > len(self.workers) * 2 and len(self.workers) < self.getMaxWorkers(): From 1036d0e4a0561d8c9e4e59dc291df0b840f6bb52 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:58:01 +0200 Subject: [PATCH 0309/2570] Allow second peer for same task only if priority is above 10 --- src/Worker/WorkerManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 892e2edb..71994bc2 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -104,7 +104,7 @@ class WorkerManager(object): # Returns the next free or less worked task def getTask(self, peer): # Sort tasks by priority and worker numbers - self.tasks.sort(key=lambda task: task["priority"] - task["workers_num"] * 5, reverse=True) + self.tasks.sort(key=lambda task: task["priority"] - task["workers_num"] * 10, reverse=True) for task in self.tasks: # Find a task if task["peers"] and peer not in task["peers"]: From 09d19da3bc10ef4173b86f3f6f3a09038a05046a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 15:58:51 +0200 Subject: [PATCH 0310/2570] Always update hashfield for peer during findHash --- src/Worker/WorkerManager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 71994bc2..987ad410 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -281,8 +281,7 @@ class WorkerManager(object): if not peers: peers = self.site.getConnectablePeers() for peer in peers: - if not peer.time_hashfield: - threads.append(gevent.spawn(peer.updateHashfield)) + threads.append(gevent.spawn(peer.updateHashfield, force=find_more)) gevent.joinall(threads, timeout=5) if time_tasks != self.time_task_added: # New task added since start From 59d45f445ae4974407157da46109de7d1155307d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 16:01:02 +0200 Subject: [PATCH 0311/2570] Always start findOptional if no worker running but there is tasks --- src/Worker/WorkerManager.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 987ad410..2939b256 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -389,11 +389,15 @@ class WorkerManager(object): del(self.workers[worker.key]) self.log.debug("Removed worker, workers: %s/%s" % (len(self.workers), self.getMaxWorkers())) if len(self.workers) <= self.getMaxWorkers() / 3 and len(self.asked_peers) < 10: - important_task = (task for task in self.tasks if task["priority"] > 0) - if next(important_task, None) or len(self.asked_peers) == 0: - self.startFindOptional(find_more=True) - else: - self.startFindOptional() + optional_task = next((task for task in self.tasks if task["optional_hash_id"]), None) + if optional_task: + if len(self.workers) == 0: + self.startFindOptional(find_more=True) + else: + self.startFindOptional() + elif self.tasks and not self.workers and worker.task: + self.log.debug("Starting new workers... (tasks: %s)" % len(self.tasks)) + self.startWorkers() # Tasks sorted by this From 6fe5e2b751a51278f4e50674c11df7d435e91661 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 16:02:49 +0200 Subject: [PATCH 0312/2570] Speed up task competition by wait 10*0.1 sec for task finish instead of 1x1sec before start wokring on it --- src/Worker/Worker.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Worker/Worker.py b/src/Worker/Worker.py index 71c8b552..de107709 100644 --- a/src/Worker/Worker.py +++ b/src/Worker/Worker.py @@ -36,10 +36,13 @@ class Worker(object): if task["workers_num"] > 0: # Wait a bit if someone already working on it if config.verbose: - self.manager.log.debug("%s: Someone already working on %s, sleeping 1 sec..." % (self.key, task["inner_path"])) - time.sleep(1) - if config.verbose: - self.manager.log.debug("%s: %s, task done after sleep: %s" % (self.key, task["inner_path"], task["done"])) + self.manager.log.debug("%s: Someone already working on %s (pri: %s), sleeping 1 sec..." % (self.key, task["inner_path"], task["priority"])) + for sleep_i in range(1,10): + time.sleep(0.1) + if task["done"] or task["workers_num"] == 0: + if config.verbose: + self.manager.log.debug("%s: %s, picked task free after %ss sleep. (done: %s)" % (self.key, task["inner_path"], 0.1 * sleep_i, task["done"])) + break if task["done"] is False: self.task = task From 49d863fa54dabc419e40ffb6cded51a0c8dafe9d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 16:04:03 +0200 Subject: [PATCH 0313/2570] Reduce indent by using continue instead of if --- src/Worker/Worker.py | 82 ++++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/src/Worker/Worker.py b/src/Worker/Worker.py index de107709..be04dca7 100644 --- a/src/Worker/Worker.py +++ b/src/Worker/Worker.py @@ -44,50 +44,50 @@ class Worker(object): self.manager.log.debug("%s: %s, picked task free after %ss sleep. (done: %s)" % (self.key, task["inner_path"], 0.1 * sleep_i, task["done"])) break - if task["done"] is False: - self.task = task - site = task["site"] - task["workers_num"] += 1 + if task["done"]: + continue + + self.task = task + site = task["site"] + task["workers_num"] += 1 + try: + buff = self.peer.getFile(site.address, task["inner_path"], task["size"]) + except Exception, err: + self.manager.log.debug("%s: getFile error: %s" % (self.key, err)) + buff = None + if self.running is False: # Worker no longer needed or got killed + self.manager.log.debug("%s: No longer needed, returning: %s" % (self.key, task["inner_path"])) + break + if task["done"] is True: # Task done, try to find new one + continue + if buff: # Download ok try: - buff = self.peer.getFile(site.address, task["inner_path"], task["size"]) + correct = site.content_manager.verifyFile(task["inner_path"], buff) except Exception, err: - self.manager.log.debug("%s: getFile error: %s" % (self.key, err)) - buff = None - if self.running is False: # Worker no longer needed or got killed - self.manager.log.debug("%s: No longer needed, returning: %s" % (self.key, task["inner_path"])) - break - if task["done"] is True: # Task done, try to find new one - continue - if buff: # Download ok - try: - correct = site.content_manager.verifyFile(task["inner_path"], buff) - except Exception, err: - correct = False - else: # Download error - err = "Download failed" correct = False - if correct is True or correct is None: # Verify ok or same file - self.manager.log.debug("%s: Verify correct: %s" % (self.key, task["inner_path"])) - if correct is True and task["done"] is False: # Save if changed and task not done yet - buff.seek(0) - site.storage.write(task["inner_path"], buff) - if task["done"] is False: - self.manager.doneTask(task) - task["workers_num"] -= 1 - self.task = None - else: # Verify failed - self.manager.log.debug( - "%s: Verify failed: %s, error: %s, failed peers: %s" % - (self.key, task["inner_path"], err, len(task["failed"])) - ) - task["failed"].append(self.peer) - self.task = None - self.peer.hash_failed += 1 - if self.peer.hash_failed >= max(len(self.manager.tasks), 3) or self.peer.connection_error > 10: - # Broken peer: More fails than tasks number but atleast 3 - break - task["workers_num"] -= 1 - time.sleep(1) + else: # Download error + err = "Download failed" + correct = False + if correct is True or correct is None: # Verify ok or same file + self.manager.log.debug("%s: Verify correct: %s" % (self.key, task["inner_path"])) + if correct is True and task["done"] is False: # Save if changed and task not done yet + buff.seek(0) + site.storage.write(task["inner_path"], buff) + if task["done"] is False: + self.manager.doneTask(task) + task["workers_num"] -= 1 + else: # Verify failed + task["workers_num"] -= 1 + self.manager.log.debug( + "%s: Verify failed: %s, error: %s, failed peers: %s, workers: %s" % + (self.key, task["inner_path"], err, len(task["failed"]), task["workers_num"]) + ) + task["failed"].append(self.peer) + self.peer.hash_failed += 1 + if self.peer.hash_failed >= max(len(self.manager.tasks), 3) or self.peer.connection_error > 10: + # Broken peer: More fails than tasks number but atleast 3 + break + time.sleep(1) self.peer.onWorkerDone() self.running = False self.manager.removeWorker(self) From d0f85f3d04b9b379ca4af4c88d4ba8b7ca6baa5a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Oct 2017 16:05:01 +0200 Subject: [PATCH 0314/2570] String / from left during dirname conversion --- src/util/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/helper.py b/src/util/helper.py index 4fe6bf0f..4a730ce5 100644 --- a/src/util/helper.py +++ b/src/util/helper.py @@ -113,7 +113,7 @@ def unpackOnionAddress(packed): # Return: data/site/content.json -> data/site/ def getDirname(path): if "/" in path: - return path[:path.rfind("/") + 1] + return path[:path.rfind("/") + 1].lstrip("/") else: return "" From fdb7b4cc0f11bde14e814ba70cc0e80f516907eb Mon Sep 17 00:00:00 2001 From: ZeroNet Date: Wed, 4 Oct 2017 09:23:21 +0200 Subject: [PATCH 0315/2570] Use Python 2.x for ZeroNet --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 05ebd010..7749f2ca 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ It downloads the latest version of ZeroNet then starts it automatically. * `wget https://github.com/HelloZeroNet/ZeroNet/archive/master.tar.gz` * `tar xvpfz master.tar.gz` * `cd ZeroNet-master` -* Start with `python zeronet.py` +* Start with `python2 zeronet.py` * Open http://127.0.0.1:43110/ in your browser ### [Arch Linux](https://www.archlinux.org) @@ -125,7 +125,7 @@ See `/usr/share/doc/zeronet-*/README.gentoo.bz2` for further assistance. * `vagrant up` * Access VM with `vagrant ssh` * `cd /vagrant` -* Run `python zeronet.py --ui_ip 0.0.0.0` +* Run `python2 zeronet.py --ui_ip 0.0.0.0` * Open http://127.0.0.1:43110/ in your browser ### [Docker](https://www.docker.com/) @@ -142,7 +142,7 @@ set `ENABLE_TOR` environment variable to `true` (Default: `false`). E.g.: * `virtualenv env` * `source env/bin/activate` * `pip install msgpack-python gevent` -* `python zeronet.py` +* `python2 zeronet.py` * Open http://127.0.0.1:43110/ in your browser ## Current limitations From 53da40fe5de90cbc7792f70d7641550ac85037be Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 12:31:57 +0200 Subject: [PATCH 0316/2570] Allow to show site directory in local file browser --- src/Ui/UiWebsocket.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index ee8cb630..0c2890da 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -875,15 +875,24 @@ class UiWebsocket(object): sys.modules["main"].file_server.stop() sys.modules["main"].ui_server.stop() - def actionServerShowdirectory(self, to, directory="backup"): + def actionServerShowdirectory(self, to, directory="backup", inner_path=""): + if self.request.env["REMOTE_ADDR"] != "127.0.0.1": + return self.response(to, {"error": "Only clients from 127.0.0.1 allowed to run this command"}) + import webbrowser - if directory == "backup": - directory = os.path.abspath(config.data_dir) + path = os.path.abspath(config.data_dir) elif directory == "log": - directory = os.path.abspath(config.log_dir) + path = os.path.abspath(config.log_dir) + elif directory == "site": + path = os.path.abspath(self.site.storage.getPath(helper.getDirname(inner_path))) - webbrowser.open('file://' + directory) + if os.path.isdir(path): + self.log.debug("Opening: %s" % path) + webbrowser.open('file://' + path) + return self.response(to, "ok") + else: + return self.response(to, {"error": "Not a directory"}) def actionConfigSet(self, to, key, value): if key not in ["tor", "language"]: From 903b62ba7c403085d3ab63f8aa6b26693e89e9b2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 12:32:40 +0200 Subject: [PATCH 0317/2570] Make fileNeed API command async --- src/Ui/UiWebsocket.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 0c2890da..8bb1bdde 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -14,6 +14,7 @@ from Debug import Debug from util import QueryJson, RateLimit from Plugin import PluginManager from Translate import translate as _ +from util import helper @PluginManager.acceptPlugins @@ -37,7 +38,7 @@ class UiWebsocket(object): "channelJoinAllsite", "serverUpdate", "serverPortcheck", "serverShutdown", "serverShowdirectory", "certSet", "configSet", "permissionAdd", "permissionRemove" ) - self.async_commands = ("fileGet", "fileList", "dirList") + self.async_commands = ("fileGet", "fileList", "dirList", "fileNeed") # Start listener loop def start(self): From 7dbc323f762bc5acddff0a0eb727c8736b530df6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 12:33:43 +0200 Subject: [PATCH 0318/2570] ZeroNet-internal virtual path on UiServer for internal commands --- src/Ui/UiRequest.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index d2e4e27f..dcc5497b 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -87,6 +87,14 @@ class UiRequest(object): return self.actionIndex() elif path == "/favicon.ico": return self.actionFile("src/Ui/media/img/favicon.ico") + # Internal functions + elif "/ZeroNet-Internal/" in path: + path = re.sub(".*?/ZeroNet-Internal/", "/", path) + func = getattr(self, "action" + path.lstrip("/"), None) # Check if we have action+request_path function + if func: + return func() + else: + return self.error404(path) # Media elif path.startswith("/uimedia/"): return self.actionUiMedia(path) From 3c46f60042210879eb1d5bc4b719f055790f1351 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 12:35:15 +0200 Subject: [PATCH 0319/2570] Show wrapper automatically if wrapper nonce is invalid --- src/Ui/UiRequest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index dcc5497b..700480f4 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -118,7 +118,8 @@ class UiRequest(object): return self.actionSiteAdd() # Site media wrapper else: - if self.get.get("wrapper_nonce"): + if self.get.get("wrapper_nonce") and self.get["wrapper_nonce"] in self.server.wrapper_nonces: + self.server.wrapper_nonces.remove(self.get["wrapper_nonce"]) return self.actionSiteMedia("/media" + path) # Only serve html files with frame else: body = self.actionWrapper(path) From 75b44f6980985b832778acf7a0a3b99e65aa775c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 12:37:22 +0200 Subject: [PATCH 0320/2570] Raise SecurityError on invalid path --- src/Ui/UiRequest.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 700480f4..42afbbf2 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -22,6 +22,10 @@ status_texts = { } +class SecurityError(Exception): + pass + + @PluginManager.acceptPlugins class UiRequest(object): @@ -417,8 +421,8 @@ class UiRequest(object): if path.endswith("/"): path = path + "index.html" - if ".." in path: - raise Exception("Invalid path") + if ".." in path or "./" in path: + raise SecurityError("Invalid path") match = re.match("/media/(?P
[A-Za-z0-9\._-]+)(?P/.*|$)", path) if match: From 698457590199b2fdb223370bdfebff4f4f18233a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 12:37:34 +0200 Subject: [PATCH 0321/2570] Remove dead reload code --- src/Ui/UiRequest.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 42afbbf2..d7a89838 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -713,12 +713,3 @@ class UiRequest(object):

%s

""" % (title, cgi.escape(message)) - -# - Reload for eaiser developing - -# def reload(): - # import imp, sys - # global UiWebsocket - # UiWebsocket = imp.load_source("UiWebsocket", "src/Ui/UiWebsocket.py").UiWebsocket - # reload(sys.modules["User.UserManager"]) - # UserManager.reloadModule() - # self.user = UserManager.user_manager.getCurrent() From 4d991cda6d0309f5612b9e357b05d6c8662f16f4 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 12:37:56 +0200 Subject: [PATCH 0322/2570] Also serve htm files with wrapper --- src/Ui/UiRequest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index d7a89838..48c56c61 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -267,7 +267,7 @@ class UiRequest(object): if match: address = match.group("address") inner_path = match.group("inner_path").lstrip("/") - if "." in inner_path and not inner_path.endswith(".html"): + if "." in inner_path and not inner_path.endswith(".html") and not inner_path.endswith(".htm"): return self.actionSiteMedia("/media" + path) # Only serve html files with frame if self.isAjaxRequest(): return self.error403("Ajax request not allowed to load wrapper") # No ajax allowed on wrapper From f53612bef74473ced98b01a058ccf6e06d594cb9 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 12:38:44 +0200 Subject: [PATCH 0323/2570] Media request should start with normal characters --- src/Ui/UiRequest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 48c56c61..efb29841 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -424,7 +424,7 @@ class UiRequest(object): if ".." in path or "./" in path: raise SecurityError("Invalid path") - match = re.match("/media/(?P
[A-Za-z0-9\._-]+)(?P/.*|$)", path) + match = re.match("/media/(?P
[A-Za-z0-9]+[A-Za-z0-9\._-]+)(?P/.*|$)", path) if match: path_parts = match.groupdict() path_parts["request_address"] = path_parts["address"] # Original request address (for Merger sites) From 74b2408668fedca818bac23c0a5ef4fbca30aec1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 12:39:08 +0200 Subject: [PATCH 0324/2570] Move path checking to parsePath --- src/Ui/UiRequest.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index efb29841..73fd5cda 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -435,10 +435,10 @@ class UiRequest(object): # Serve a media for site def actionSiteMedia(self, path, header_length=True, header_noscript=False): - if ".." in path: # File not in allowed path - return self.error403("Invalid file path") - - path_parts = self.parsePath(path) + try: + path_parts = self.parsePath(path) + except SecurityError as err: + return self.error403(err) if not path_parts: return self.error404(path) From 1f5db0aa242d4de26aeb79b78d146b7f2a5ba7f3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 12:41:14 +0200 Subject: [PATCH 0325/2570] Nonce checking moved to route command --- src/Ui/UiRequest.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 73fd5cda..87db17fc 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -445,11 +445,6 @@ class UiRequest(object): # Check wrapper nonce content_type = self.getContentType(path_parts["inner_path"]) - if "htm" in content_type and not header_noscript: # Valid nonce must present to render html files - wrapper_nonce = self.get.get("wrapper_nonce") - if wrapper_nonce not in self.server.wrapper_nonces: - return self.error403("Wrapper nonce error. Please reload the page.") - self.server.wrapper_nonces.remove(self.get["wrapper_nonce"]) else: referer = self.env.get("HTTP_REFERER") if referer and path_parts: # Only allow same site to receive media From edb9d3f7195c897cec6e7b8d1e6ce53c59f27c31 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 12:42:05 +0200 Subject: [PATCH 0326/2570] Media files requested from non-seeded site not wokring anymore --- src/Ui/UiRequest.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 87db17fc..f0b88817 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -445,12 +445,6 @@ class UiRequest(object): # Check wrapper nonce content_type = self.getContentType(path_parts["inner_path"]) - else: - referer = self.env.get("HTTP_REFERER") - if referer and path_parts: # Only allow same site to receive media - if not self.isSameOrigin(self.getRequestUrl(), self.getReferer()): - self.log.error("Media referrer error: %s not allowed from %s" % (self.getRequestUrl(), self.getReferer())) - return self.error403("Media referrer error") # Referrer not starts same address as requested path address = path_parts["address"] file_path = "%s/%s/%s" % (config.data_dir, address, path_parts["inner_path"]) From 0dd34403a237905c5660443cee910c95a0a7bc58 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 12:44:34 +0200 Subject: [PATCH 0327/2570] Combinate isfile and filesize query to one function --- src/Ui/UiRequest.py | 26 ++++++++++++++++---------- src/util/helper.py | 9 +++++++++ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index f0b88817..f105c10c 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -11,6 +11,7 @@ from User import UserManager from Plugin import PluginManager from Ui.UiWebsocket import UiWebsocket from Crypt import CryptHash +from util import helper status_texts = { 200: "200 OK", @@ -454,18 +455,22 @@ class UiRequest(object): if site and site.settings["own"]: from Debug import DebugMedia DebugMedia.merge(file_path) + if not address or address == ".": return self.error403(path_parts["inner_path"]) - if os.path.isfile(file_path): # File exists - header_allow_ajax = False - if self.get.get("ajax_key"): - site = SiteManager.site_manager.get(path_parts["request_address"]) - if self.get["ajax_key"] == site.settings["ajax_key"]: - header_allow_ajax = True - else: - return self.error403("Invalid ajax_key") - return self.actionFile(file_path, header_length=header_length, header_noscript=header_noscript, header_allow_ajax=header_allow_ajax) + header_allow_ajax = False + if self.get.get("ajax_key"): + site = SiteManager.site_manager.get(path_parts["request_address"]) + if self.get["ajax_key"] == site.settings["ajax_key"]: + header_allow_ajax = True + else: + return self.error403("Invalid ajax_key") + + file_size = helper.getFilesize(file_path) + + if file_size is not None: + return self.actionFile(file_path, header_length=header_length, header_noscript=header_noscript, header_allow_ajax=header_allow_ajax, file_size=file_size, path_parts=path_parts) elif os.path.isdir(file_path): # If this is actually a folder, add "/" and redirect if path_parts["inner_path"]: @@ -484,7 +489,8 @@ class UiRequest(object): result = site.needFile(path_parts["inner_path"], priority=15) # Wait until file downloads if result: - return self.actionFile(file_path, header_length=header_length, header_noscript=header_noscript) + file_size = helper.getFilesize(file_path) + return self.actionFile(file_path, header_length=header_length, header_noscript=header_noscript, header_allow_ajax=header_allow_ajax, file_size=file_size, path_parts=path_parts) else: self.log.debug("File not found: %s" % path_parts["inner_path"]) # Site larger than allowed, re-add wrapper nonce to allow reload diff --git a/src/util/helper.py b/src/util/helper.py index 4a730ce5..0da5bddd 100644 --- a/src/util/helper.py +++ b/src/util/helper.py @@ -123,6 +123,15 @@ def getDirname(path): def getFilename(path): return path[path.rfind("/") + 1:] +def getFilesize(path): + try: + s = os.stat(path) + except: + return None + if stat.S_ISREG(s.st_mode): # Test if it's file + return s.st_size + else: + return None # Convert hash to hashid for hashfield def toHashId(hash): From 4042de460e22d3b45703accaae37349464ee38c4 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 12:46:06 +0200 Subject: [PATCH 0328/2570] actionFile allows file details to be passed as parameter --- src/Ui/UiRequest.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index f105c10c..7afa34b3 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -537,10 +537,11 @@ class UiRequest(object): return template # Stream a file to client - def actionFile(self, file_path, block_size=64 * 1024, send_header=True, header_length=True, header_noscript=False, header_allow_ajax=False): - if ".." in file_path: - raise Exception("Invalid path") - if os.path.isfile(file_path): + def actionFile(self, file_path, block_size=64 * 1024, send_header=True, header_length=True, header_noscript=False, header_allow_ajax=False, file_size=None, file_obj=None, path_parts=None): + if file_size is None: + file_size = helper.getFilesize(file_path) + + if file_size is not None: # Try to figure out content type by extension content_type = self.getContentType(file_path) @@ -548,7 +549,6 @@ class UiRequest(object): range_start = None if send_header: extra_headers = {} - file_size = os.path.getsize(file_path) extra_headers["Accept-Ranges"] = "bytes" if header_length: extra_headers["Content-Length"] = str(file_size) @@ -568,18 +568,20 @@ class UiRequest(object): extra_headers["Access-Control-Allow-Origin"] = "null" self.sendHeader(status, content_type=content_type, noscript=header_noscript, extra_headers=extra_headers.items()) if self.env["REQUEST_METHOD"] != "OPTIONS": - file = open(file_path, "rb") + if not file_obj: + file_obj = open(file_path, "rb") + if range_start: - file.seek(range_start) + file_obj.seek(range_start) while 1: try: - block = file.read(block_size) + block = file_obj.read(block_size) if block: yield block else: raise StopIteration except StopIteration: - file.close() + file_obj.close() break else: # File not exists yield self.error404(file_path) From a2182e8a8d9d0dcd818be6d82f8f6f9029411ace Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 12:48:16 +0200 Subject: [PATCH 0329/2570] Invalid nonce requests automatically shows wrapper now --- src/Test/TestWeb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test/TestWeb.py b/src/Test/TestWeb.py index a6ad726c..8d2c295a 100644 --- a/src/Test/TestWeb.py +++ b/src/Test/TestWeb.py @@ -89,5 +89,5 @@ class TestWeb: with WaitForPageLoad(browser): browser.execute_script("window.top.location = window.location") assert "wrapper_nonce" in browser.current_url # We try to use nonce-ed html without iframe - assert "Forbidden" in browser.page_source # Only allow to use nonce once-time + assert " Date: Wed, 4 Oct 2017 12:48:48 +0200 Subject: [PATCH 0330/2570] request from directory . should drop forbidden error --- src/Test/TestWeb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test/TestWeb.py b/src/Test/TestWeb.py index 8d2c295a..3e321756 100644 --- a/src/Test/TestWeb.py +++ b/src/Test/TestWeb.py @@ -31,7 +31,7 @@ def wget(url): class TestWeb: def testFileSecurity(self, site_url): assert "Not Found" in wget("%s/media/sites.json" % site_url) - assert "Not Found" in wget("%s/media/./sites.json" % site_url) + assert "Forbidden" in wget("%s/media/./sites.json" % site_url) assert "Forbidden" in wget("%s/media/../config.py" % site_url) assert "Forbidden" in wget("%s/media/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr/../sites.json" % site_url) assert "Forbidden" in wget("%s/media/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr/..//sites.json" % site_url) From 8e3e96fe6544c1c2e0a38d609f96e68cf22ca97c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 12:50:24 +0200 Subject: [PATCH 0331/2570] Create storage object after site settings loaded --- 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 69ec2a57..ed263295 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -52,8 +52,8 @@ class Site(object): self.websockets = [] # Active site websocket connections self.connection_server = None - self.storage = SiteStorage(self, allow_create=allow_create) # Save and load site files 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 and "file_server" in dir(sys.modules["main"]): # Use global file server by default if possible From 36b74e1c6a6dbf191d1f0c1679f73e1d0180b16a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 12:51:24 +0200 Subject: [PATCH 0332/2570] ContentManager logging improvements --- src/Content/ContentManager.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 5d717adf..e1ecf2ec 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -559,8 +559,9 @@ class ContentManager(object): if extend: # Add extend keys if not exists for key, val in extend.items(): - if key not in content: + if not content.get(key): content[key] = val + self.log.info("Extending content.json with: %s" % key) directory = helper.getDirname(self.site.storage.getPath(inner_path)) inner_directory = helper.getDirname(inner_path) @@ -683,7 +684,7 @@ class ContentManager(object): if not rules.get("cert_signers"): return True # Does not need cert - if not "cert_user_id" in content: + if "cert_user_id" not in content: raise VerifyError("Missing cert_user_id") name, domain = content["cert_user_id"].split("@") @@ -733,7 +734,7 @@ class ContentManager(object): task = self.site.worker_manager.findTask(inner_path) if task: # Dont try to download from other peers self.site.worker_manager.failTask(task) - raise VerifyError("Site too large %sB > %sB, aborting task..." % (site_size, site_size_limit)) + raise VerifyError("Content too large %sB > %sB, aborting task..." % (site_size, site_size_limit)) # Verify valid filenames for file_relative_path in content.get("files", {}).keys() + content.get("files_optional", {}).keys(): @@ -862,7 +863,7 @@ class ContentManager(object): raise VerifyError("Invalid old-style sign") except Exception, err: - self.log.warning("Verify sign error: %s" % Debug.formatException(err)) + self.log.warning("%s: verify sign error: %s" % (inner_path, Debug.formatException(err))) raise err else: # Check using sha512 hash From ca473d6c3b2f537b44111084e5c9eb973b65efed Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 12:51:37 +0200 Subject: [PATCH 0333/2570] Allow plugins on ContentManager --- src/Content/ContentManager.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index e1ecf2ec..2d796a24 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -14,6 +14,7 @@ from util import Diff from util import SafeRe from Peer import PeerHashfield from ContentDbDict import ContentDbDict +from Plugin import PluginManager class VerifyError(Exception): @@ -24,6 +25,7 @@ class SignError(Exception): pass +@PluginManager.acceptPlugins class ContentManager(object): def __init__(self, site): From 38c663ee9559d9d01291b59b252b10282d7de930 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 13:10:26 +0200 Subject: [PATCH 0334/2570] Use WHERE 1 for queries without argument --- src/Db/DbCursor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Db/DbCursor.py b/src/Db/DbCursor.py index 0b03a9f0..cff075de 100644 --- a/src/Db/DbCursor.py +++ b/src/Db/DbCursor.py @@ -32,7 +32,9 @@ class DbCursor: query_wheres.append(key + " = ?") values.append(value) wheres = " AND ".join(query_wheres) - query = re.sub("(.*)[?]", "\\1%s" % wheres, query) # Replace the last ? + if wheres == "": + wheres = "1" + query = re.sub("(.*)[?]", "\\1 %s" % wheres, query) # Replace the last ? params = values else: # Convert param dict to INSERT INTO table (key, key2) VALUES (?, ?) format From 6e55c8b7c743dc2f37463d7cf7947b03507065da Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 13:10:41 +0200 Subject: [PATCH 0335/2570] Allow ranged SQL queries --- src/Db/DbCursor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Db/DbCursor.py b/src/Db/DbCursor.py index cff075de..e4d52254 100644 --- a/src/Db/DbCursor.py +++ b/src/Db/DbCursor.py @@ -28,6 +28,10 @@ class DbCursor: else: if key.startswith("not__"): query_wheres.append(key.replace("not__", "") + " != ?") + elif key.endswith(">"): + query_wheres.append(key.replace(">", "") + " > ?") + elif key.endswith("<"): + query_wheres.append(key.replace("<", "") + " < ?") else: query_wheres.append(key + " = ?") values.append(value) From cd025316db67e8ce6330ae6252d5c6904a7a5399 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 13:13:00 +0200 Subject: [PATCH 0336/2570] Auto-pin downloaded bigger files --- .../OptionalManager/OptionalManagerPlugin.py | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/plugins/OptionalManager/OptionalManagerPlugin.py b/plugins/OptionalManager/OptionalManagerPlugin.py index 75689190..ba2210aa 100644 --- a/plugins/OptionalManager/OptionalManagerPlugin.py +++ b/plugins/OptionalManager/OptionalManagerPlugin.py @@ -54,11 +54,22 @@ if "access_log" not in locals().keys(): # To keep between module reloads @PluginManager.registerTo("WorkerManager") class WorkerManagerPlugin(object): def doneTask(self, task): - if task["optional_hash_id"]: - content_db = self.site.content_manager.contents.db + content_db = self.site.content_manager.contents.db + if task["optional_hash_id"] and task["optional_hash_id"] not in self.site.content_manager.hashfield: + + inner_path = task["inner_path"] + is_pinned = 0 + if "|" in inner_path: # Big file piece + inner_path, file_range = inner_path.split("|") + file_info = self.site.content_manager.getFileInfo(inner_path) + # Auto-pin bigfiles + if config.pin_bigfile and file_info["size"] > 1024 * 1024 * config.pin_bigfile: + is_pinned = 1 + + content_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", - {"now": int(time.time()), "site_id": content_db.site_ids[self.site.address], "inner_path": task["inner_path"]} + "UPDATE file_optional SET time_downloaded = :now, is_downloaded = 1, peer = peer + 1, is_pinned = :is_pinned WHERE site_id = :site_id AND inner_path = :inner_path", + {"now": int(time.time()), "site_id": content_db.site_ids[self.site.address], "inner_path": inner_path, "is_pinned": is_pinned} ) super(WorkerManagerPlugin, self).doneTask(task) @@ -120,5 +131,6 @@ 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('--pin_bigfile', help='Automatically pin files larger than this limit', default=20, metavar="MB", type=int) return super(ConfigPlugin, self).createArguments() From e8a439cad3f47255468a726ceefc1d8ca2f8aedc Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 13:14:24 +0200 Subject: [PATCH 0337/2570] Mark my optional files using directory on signing instead of file path --- plugins/OptionalManager/ContentDbPlugin.py | 2 +- plugins/OptionalManager/UiWebsocketPlugin.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/OptionalManager/ContentDbPlugin.py b/plugins/OptionalManager/ContentDbPlugin.py index dcb0c5cd..5d7942e8 100644 --- a/plugins/OptionalManager/ContentDbPlugin.py +++ b/plugins/OptionalManager/ContentDbPlugin.py @@ -173,7 +173,7 @@ class ContentDbPlugin(object): is_downloaded = 1 else: is_downloaded = 0 - if site.address + "/" + file_inner_path in self.my_optional_files: + if site.address + "/" + content_inner_dir in self.my_optional_files: is_pinned = 1 else: is_pinned = 0 diff --git a/plugins/OptionalManager/UiWebsocketPlugin.py b/plugins/OptionalManager/UiWebsocketPlugin.py index b34b5b88..d69fccf0 100644 --- a/plugins/OptionalManager/UiWebsocketPlugin.py +++ b/plugins/OptionalManager/UiWebsocketPlugin.py @@ -18,12 +18,11 @@ class UiWebsocketPlugin(object): self.time_peer_numbers_updated = 0 super(UiWebsocketPlugin, self).__init__(*args, **kwargs) - def actionFileWrite(self, to, inner_path, *args, **kwargs): - super(UiWebsocketPlugin, self).actionFileWrite(to, inner_path, *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_db.my_optional_files[self.site.address + "/" + inner_path] = time.time() + 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( content_db.my_optional_files.iterkeys(), @@ -31,6 +30,8 @@ class UiWebsocketPlugin(object): ) del content_db.my_optional_files[oldest_key] + return super(UiWebsocketPlugin, self).actionSiteSign(to, privatekey, inner_path, *args, **kwargs) + def updatePeerNumbers(self): content_db = self.site.content_manager.contents.db content_db.updatePeerNumbers() From 282f06b65eb69e2f88b47faa89fdf1b8e580d6f0 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 13:15:44 +0200 Subject: [PATCH 0338/2570] Filter for OptionalFileList API call that allows to list non-downloaded files and only bigfiles --- plugins/OptionalManager/UiWebsocketPlugin.py | 49 +++++++++++++++++--- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/plugins/OptionalManager/UiWebsocketPlugin.py b/plugins/OptionalManager/UiWebsocketPlugin.py index d69fccf0..cf6a5195 100644 --- a/plugins/OptionalManager/UiWebsocketPlugin.py +++ b/plugins/OptionalManager/UiWebsocketPlugin.py @@ -39,7 +39,7 @@ class UiWebsocketPlugin(object): # Optional file functions - def actionOptionalFileList(self, to, address=None, orderby="time_downloaded DESC", limit=10): + def actionOptionalFileList(self, to, address=None, orderby="time_downloaded DESC", limit=10, filter="downloaded"): if not address: address = self.site.address @@ -50,6 +50,9 @@ class UiWebsocketPlugin(object): 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"}) @@ -61,10 +64,40 @@ class UiWebsocketPlugin(object): back = [] content_db = self.site.content_manager.contents.db - site_id = content_db.site_ids[address] - query = "SELECT * FROM file_optional WHERE site_id = %s AND is_downloaded = 1 ORDER BY %s LIMIT %s" % (site_id, orderby, limit) - for row in content_db.execute(query): - back.append(dict(row)) + + wheres = {} + if "bigfile" in filter: + wheres["size >"] = 1024 * 1024 * 10 + if "downloaded" in filter: + wheres["is_downloaded"] = 1 + + if address == "all": + join = "LEFT JOIN site USING (site_id)" + else: + wheres["site_id"] = content_db.site_ids[address] + join = "" + + query = "SELECT * FROM file_optional %s WHERE ? ORDER BY %s LIMIT %s" % (join, 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_info = self.addBigfileInfo(row) + else: + has_info = False + + if not has_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): @@ -81,7 +114,11 @@ class UiWebsocketPlugin(object): res = content_db.execute(query, {"site_id": site_id, "inner_path": inner_path}) row = next(res, None) if row: - self.response(to, dict(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) From e5b851b17146f8a4581310b55a12521504f40784 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 13:16:02 +0200 Subject: [PATCH 0339/2570] Add progress information to bigfiles --- plugins/OptionalManager/UiWebsocketPlugin.py | 42 ++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/plugins/OptionalManager/UiWebsocketPlugin.py b/plugins/OptionalManager/UiWebsocketPlugin.py index cf6a5195..2e690743 100644 --- a/plugins/OptionalManager/UiWebsocketPlugin.py +++ b/plugins/OptionalManager/UiWebsocketPlugin.py @@ -37,6 +37,48 @@ class UiWebsocketPlugin(object): content_db.updatePeerNumbers() self.site.updateWebsocket(peernumber_updated=True) + def addBigfileInfo(self, row): + content_db = self.site.content_manager.contents.db + site = content_db.sites[row["address"]] + if not site.settings.get("has_bigfile"): + return False + + file_info = site.content_manager.getFileInfo(row["inner_path"]) + if not file_info.get("piece_size"): + return False + + sha512 = file_info["sha512"] + piecefield = site.storage.piecefields[sha512].tostring() + + if not piecefield: + return False + + row["pieces"] = len(piecefield) + row["pieces_downloaded"] = piecefield.count("1") + row["downloaded_percent"] = 100 * row["pieces_downloaded"] / row["pieces"] + row["bytes_downloaded"] = row["pieces_downloaded"] * file_info["piece_size"] + row["is_downloading"] = bool(next((key for key in site.bad_files if key.startswith(row["inner_path"])), False)) + + # Add leech / seed stats + row["peer_seed"] = 0 + row["peer_leech"] = 0 + for peer in site.peers.itervalues(): + if not peer.time_piecefields_updated or sha512 not in peer.piecefields: + continue + peer_piecefield = peer.piecefields[sha512].tostring() + if peer_piecefield == "1" * row["pieces"]: + row["peer_seed"] += 1 + else: + row["peer_leech"] += 1 + + # Add myself + 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"): From e5963f8a76df1014790c38af361ac72849ed1df4 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 13:17:18 +0200 Subject: [PATCH 0340/2570] OptionalFileDelete API call only works on downloaded files --- plugins/OptionalManager/UiWebsocketPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/OptionalManager/UiWebsocketPlugin.py b/plugins/OptionalManager/UiWebsocketPlugin.py index 2e690743..7b4c25a1 100644 --- a/plugins/OptionalManager/UiWebsocketPlugin.py +++ b/plugins/OptionalManager/UiWebsocketPlugin.py @@ -203,7 +203,7 @@ class UiWebsocketPlugin(object): 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}) + 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: From aa9e8b067fa6708be69cbbdd55aaf947d265194c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 13:17:36 +0200 Subject: [PATCH 0341/2570] Send websocket event on optional file deletion --- plugins/OptionalManager/UiWebsocketPlugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/OptionalManager/UiWebsocketPlugin.py b/plugins/OptionalManager/UiWebsocketPlugin.py index 7b4c25a1..7fe4bd2d 100644 --- a/plugins/OptionalManager/UiWebsocketPlugin.py +++ b/plugins/OptionalManager/UiWebsocketPlugin.py @@ -219,6 +219,7 @@ class UiWebsocketPlugin(object): 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) self.response(to, "ok") From 96ceb253e8b40fb61ad68a08da030a7c84925271 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 13:19:19 +0200 Subject: [PATCH 0342/2570] GetTotal size also return optional sum of optional file sizes --- src/Content/ContentDb.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Content/ContentDb.py b/src/Content/ContentDb.py index 13a5002f..55984728 100644 --- a/src/Content/ContentDb.py +++ b/src/Content/ContentDb.py @@ -91,7 +91,7 @@ class ContentDb(Db): "size": size, "size_files": sum([val["size"] for key, val in content.get("files", {}).iteritems()]), "size_files_optional": sum([val["size"] for key, val in content.get("files_optional", {}).iteritems()]), - "modified": int(content["modified"]) + "modified": int(content.get("modified", 0)) }, { "site_id": self.site_ids.get(site.address, 0), "inner_path": inner_path @@ -116,15 +116,15 @@ class ContentDb(Db): params = {"site_id": self.site_ids.get(site.address, 0)} if ignore: params["not__inner_path"] = ignore - res = self.execute("SELECT SUM(size) + SUM(size_files) AS size FROM content WHERE ?", params) - return res.fetchone()["size"] + res = self.execute("SELECT SUM(size) + SUM(size_files) AS size, SUM(size_files_optional) AS size_optional FROM content WHERE ?", params) + row = dict(res.fetchone()) - def getOptionalSize(self, site): - res = self.execute( - "SELECT SUM(size_files_optional) AS size FROM content WHERE ?", - {"site_id": self.site_ids.get(site.address, 0)} - ) - return res.fetchone()["size"] + if not row["size"]: + row["size"] = 0 + if not row["size_optional"]: + row["size_optional"] = 0 + + return row["size"], row["size_optional"] def listModified(self, site, since): res = self.execute( From 74048ce53f59d6bf8d0ea167e7c03695df9ff608 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 13:20:39 +0200 Subject: [PATCH 0343/2570] Update site size on startup and file archival --- src/Content/ContentManager.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 2d796a24..2e48c459 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -40,13 +40,13 @@ class ContentManager(object): if len(self.contents) == 0: self.log.debug("ContentDb not initialized, load files from filesystem") self.loadContent(add_bad_files=False, delete_removed_files=False) - self.site.settings["size"] = self.getTotalSize() + self.site.settings["size"], self.site.settings["size_optional"] = self.getTotalSize() # Load hashfield cache if "hashfield" in self.site.settings.get("cache", {}): self.hashfield.fromstring(self.site.settings["cache"]["hashfield"].decode("base64")) del self.site.settings["cache"]["hashfield"] - elif self.contents.get("content.json") and self.getOptionalSize() > 0: + elif self.contents.get("content.json") and self.site.settings["size_optional"] > 0: self.site.storage.updateBadFiles() # No hashfield cache created yet self.has_optional_files = bool(self.hashfield) @@ -180,7 +180,7 @@ class ContentManager(object): archived_inner_path = content_inner_dir + archived_dirname + "/content.json" if self.contents.get(archived_inner_path, {}).get("modified", 0) < date_archived: self.removeContent(archived_inner_path) - self.site.settings["size"] = self.getTotalSize() + self.site.settings["size"], self.site.settings["size_optional"] = self.getTotalSize() # Load includes if load_includes and "includes" in new_content: @@ -273,18 +273,7 @@ class ContentManager(object): # Get total size of site # Return: 32819 (size of files in kb) def getTotalSize(self, ignore=None): - size = self.contents.db.getTotalSize(self.site, ignore) - if size: - return size - else: - return 0 - - def getOptionalSize(self): - size = self.contents.db.getOptionalSize(self.site) - if size: - return size - else: - return 0 + return self.contents.db.getTotalSize(self.site, ignore) def listModified(self, since): return self.contents.db.listModified(self.site, since) From 2ef537ee6ccfb93266ee62baf94adfd08c404492 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 13:24:22 +0200 Subject: [PATCH 0344/2570] Fox user_address detection for sub-directories in user directory --- src/Content/ContentManager.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 2e48c459..c8c11a0f 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -353,7 +353,7 @@ class ContentManager(object): return False # File not found inner_path = file_info["content_inner_path"] - if inner_path == "content.json": # Root content.json + if inner_path == "content.json": # Root content.json rules = {} rules["signers"] = self.getValidSigners(inner_path, content) return rules @@ -380,7 +380,13 @@ class ContentManager(object): # Return: The rules of the file or False if not allowed def getUserContentRules(self, parent_content, inner_path, content): user_contents = parent_content["user_contents"] - user_address = re.match(".*/([A-Za-z0-9]*?)/.*?$", inner_path).group(1) # Delivered for directory + + # Delivered for directory + if "inner_path" in parent_content: + parent_content_dir = helper.getDirname(parent_content["inner_path"]) + user_address = re.match("([A-Za-z0-9]*?)/", inner_path[len(parent_content_dir):]).group(1) + else: + user_address = re.match(".*/([A-Za-z0-9]*?)/.*?$", inner_path).group(1) try: if not content: From 5dbaf02e335610e223d66f2f7933318bce2d1552 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 13:24:44 +0200 Subject: [PATCH 0345/2570] Add content.json location to getFileInfo --- src/Content/ContentManager.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index c8c11a0f..4a5affc7 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -330,8 +330,13 @@ class ContentManager(object): # Return the rules if user dir if content and "user_contents" in content: back = content["user_contents"] - # Content.json is in the users dir - back["content_inner_path"] = re.sub("(.*)/.*?$", "\\1/content.json", inner_path) + content_inner_path_dir = helper.getDirname(content_inner_path) + relative_content_path = inner_path[len(content_inner_path_dir):] + if "/" in relative_content_path: + user_auth_address = re.match("([A-Za-z0-9]+)/.*", relative_content_path).group(1) + back["content_inner_path"] = "%s%s/content.json" % (content_inner_path_dir, user_auth_address) + else: + back["content_inner_path"] = content_inner_path_dir + "content.json" back["optional"] = None return back From f7ce40156475bb24ecb184ad992156fc8d892a1f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 13:25:07 +0200 Subject: [PATCH 0346/2570] Function to remove invalid characters from file path --- src/Content/ContentManager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 4a5affc7..61ae97b5 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -488,6 +488,9 @@ class ContentManager(object): else: return re.match("^[a-z\[\]\(\) A-Z0-9_@=\.\+-/]+$", relative_path) + def sanitizePath(self, inner_path): + return re.sub("[^a-z\[\]\(\) A-Z0-9_@=\.\+-/]", "", inner_path) + # Hash files in directory def hashFiles(self, dir_inner_path, ignore_pattern=None, optional_pattern=None): files_node = {} From cf1154f2c5fb75c4c0ab5998c03d528a6cb4ae42 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 13:28:59 +0200 Subject: [PATCH 0347/2570] Initial version of bigfile plugin --- plugins/BigFile/BigfilePiecefield.py | 158 +++++++ plugins/BigFile/BigfilePlugin.py | 678 +++++++++++++++++++++++++++ plugins/BigFile/Test/TestBigfile.py | 467 ++++++++++++++++++ plugins/BigFile/Test/conftest.py | 1 + plugins/BigFile/Test/pytest.ini | 5 + plugins/BigFile/__init__.py | 2 + 6 files changed, 1311 insertions(+) create mode 100644 plugins/BigFile/BigfilePiecefield.py create mode 100644 plugins/BigFile/BigfilePlugin.py create mode 100644 plugins/BigFile/Test/TestBigfile.py create mode 100644 plugins/BigFile/Test/conftest.py create mode 100644 plugins/BigFile/Test/pytest.ini create mode 100644 plugins/BigFile/__init__.py diff --git a/plugins/BigFile/BigfilePiecefield.py b/plugins/BigFile/BigfilePiecefield.py new file mode 100644 index 00000000..c7690279 --- /dev/null +++ b/plugins/BigFile/BigfilePiecefield.py @@ -0,0 +1,158 @@ +import array + + +def packPiecefield(data): + res = [] + if not data: + return array.array("H", "") + + if data[0] == "0": + res.append(0) + find = "1" + else: + find = "0" + last_pos = 0 + pos = 0 + while 1: + pos = data.find(find, pos) + if find == "0": + find = "1" + else: + find = "0" + 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 "" + + res = [] + char = "1" + for times in data: + if times > 10000: + return "" + res.append(char * times) + if char == "1": + char = "0" + else: + char = "1" + return "".join(res) + + +class BigfilePiecefield(object): + __slots__ = ["data"] + + def __init__(self): + self.data = "" + + def fromstring(self, s): + self.data = s + + def tostring(self): + return self.data + + def pack(self): + return packPiecefield(self.data).tostring() + + def unpack(self, s): + self.data = unpackPiecefield(array.array("H", s)) + + def __getitem__(self, key): + try: + return int(self.data[key]) + except IndexError: + return False + + def __setitem__(self, key, value): + data = self.data + if len(data) < key: + data = data.ljust(key+1, "0") + data = data[:key] + str(int(value)) + data[key + 1:] + self.data = data + + +class BigfilePiecefieldPacked(object): + __slots__ = ["data"] + + def __init__(self): + self.data = "" + + def fromstring(self, data): + self.data = packPiecefield(data).tostring() + + def tostring(self): + return unpackPiecefield(array.array("H", self.data)) + + def pack(self): + return array.array("H", self.data).tostring() + + def unpack(self, data): + self.data = data + + def __getitem__(self, key): + try: + return int(self.tostring()[key]) + except IndexError: + return False + + def __setitem__(self, key, value): + data = self.tostring() + if len(data) < key: + data = data.ljust(key+1, "0") + data = data[:key] + str(int(value)) + data[key + 1:] + self.fromstring(data) + + +if __name__ == "__main__": + import os + import psutil + import time + testdata = "1" * 100 + "0" * 900 + "1" * 4000 + "0" * 4999 + "1" + 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.fromstring(testdata[:i] + "0" + 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 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 piecefields.values(): + piecefield[1000] = True + + print "Change one x10000: +%sKB in %.3fs" % ((meminfo()[0] - m) / 1024, time.time() - s) + + m = meminfo()[0] + s = time.time() + for piecefield in 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 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 new file mode 100644 index 00000000..94e1ec7d --- /dev/null +++ b/plugins/BigFile/BigfilePlugin.py @@ -0,0 +1,678 @@ +import time +import os +import subprocess +import shutil +import collections +import gevent +import math + +import msgpack + +from Plugin import PluginManager +from Crypt import CryptHash +from lib import merkletools +from util import helper +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) + + 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 + + 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( + self.env['wsgi.input'], upload_info["size"], upload_info["piece_size"], out_file + ) + + if len(piecemap_info["sha512_pieces"]) == 1: # Small file, don't split + hash = piecemap_info["sha512_pieces"][0].encode("hex") + site.content_manager.optionalDownloaded(inner_path, hash, upload_info["size"], own=True) + + else: # Big file + file_name = helper.getFilename(inner_path) + msgpack.pack({file_name: piecemap_info}, site.storage.open(upload_info["piecemap"], "wb")) + + # Find piecemap and file relative path to content.json + file_info = site.content_manager.getFileInfo(inner_path) + 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 + } + + site.content_manager.optionalDownloaded(inner_path, merkle_root, upload_info["size"], own=True) + site.storage.writeJson(file_info["content_inner_path"], content) + + 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): + for i in range(100): + line = wsgi_input.readline() + if line == "\r\n": + break + 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"]) + kwargs["file_obj"] = site.storage.openBigfile(path_parts["inner_path"], prebuffer=2 * 1024 * 1024) + + return super(UiRequestPlugin, self).actionFile(file_path, *args, **kwargs) + + +@PluginManager.registerTo("UiWebsocket") +class UiWebsocketPlugin(object): + def actionBigfileUploadInit(self, to, inner_path, size): + 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) + + 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" + } + self.response(to, { + "url": "/ZeroNet-Internal/BigfileUpload?upload_nonce=" + nonce, + "pice_size": piece_size, + "inner_path": inner_path, + "file_relative_path": file_relative_path + }) + + +@PluginManager.registerTo("ContentManager") +class ContentManagerPlugin(object): + def getFileInfo(self, inner_path): + if "|" not in inner_path: + return super(ContentManagerPlugin, self).getFileInfo(inner_path) + + inner_path, file_range = inner_path.split("|") + pos_from, pos_to = map(int, file_range.split("-")) + file_info = super(ContentManagerPlugin, self).getFileInfo(inner_path) + return file_info + + def readFile(self, file_in, size, buff_size=1024 * 64): + part_num = 0 + recv_left = size + + while 1: + part_num += 1 + read_size = min(buff_size, recv_left) + part = file_in.read(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, file_in, 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(file_in, 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() + return mt.get_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"), 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" + + msgpack.pack({file_name: piecemap_info}, self.site.storage.open(piecemap_inner_path, "wb")) + back.update(super(ContentManagerPlugin, self).hashFile(dir_inner_path, piecemap_relative_path, True)) + + piece_num = int(math.ceil(float(file_size) / piece_size)) + + # Add the merkle root to hashfield + self.optionalDownloaded(inner_path, hash, file_size, own=True) + self.site.storage.piecefields[hash].fromstring("1" * 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))[helper.getFilename(inner_path)] + piecemap["piece_size"] = file_info["piece_size"] + return piecemap + + def verifyPiece(self, inner_path, pos, piece): + piecemap = self.getPiecemap(inner_path) + piece_i = 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, 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 = pos_from / file_info["piece_size"] + self.site.storage.piecefields[file_info["sha512"]][piece_i] = True + + # Only add to site size on first request + if hash in self.hashfield: + size = 0 + + return super(ContentManagerPlugin, self).optionalDownloaded(inner_path, hash, size, own) + + def optionalRemove(self, inner_path, hash, 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] + return super(ContentManagerPlugin, self).optionalRemove(inner_path, hash, 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").iteritems(): + self.piecefields[sha512].unpack(piecefield_packed.decode("base64")) + self.site.settings["cache"]["piecefields"] = {} + + def createSparseFile(self, inner_path, size, sha512=None): + file_path = self.getPath(inner_path) + f = open(file_path, 'wb') + f.truncate(size) + f.close() + if os.name == "nt": + subprocess.call(["fsutil", "sparse", "setflag", file_path]) + + 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 + file_dir = os.path.dirname(file_path) + if not os.path.isdir(file_dir): + os.makedirs(file_dir) + + 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 openBigfile(self, inner_path, prebuffer=0): + file_info = self.site.content_manager.getFileInfo(inner_path) + if "piecemap" not in file_info: # It's not a big file + return False + + self.site.needFile(inner_path, blocking=False) # Download piecemap + 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).read(128) == "\0" * 128: + piece_data = "0" + else: + piece_data = "1" + self.log.debug("%s: File exists, but not in piecefield. Filling piecefiled with %s * %s." % (inner_path, piece_num, piece_data)) + self.piecefields[sha512].fromstring(piece_data * piece_num) + else: + self.log.debug("Creating bigfile: %s" % inner_path) + self.createSparseFile(inner_path, file_info["size"], sha512) + self.piecefields[sha512].fromstring(piece_data * "0") + 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+") + + def read(self, buff=64 * 1024): + pos = self.f.tell() + read_until = pos + buff + requests = [] + # Request all required blocks + while 1: + piece_i = 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 = 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): + return self.f.seek(pos) + + def tell(self): + 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) + if "|" not in inner_path and self.site.isDownloadable(inner_path) and file_info["size"] / 1024 / 1024 <= config.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"] = 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"]].fromstring("0" * 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) == "\0" * 10: + # Looks empty, but makes sures we don't have that piece + file_info = site.content_manager.getFileInfo(inner_path) + piece_i = 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.settings["serving"]: # 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.iteritems()} + self.response({"piecefields_packed": piecefields_packed}) + + def actionSetPiecefields(self, params): + site = self.sites.get(params["site"]) + if not site or not site.settings["serving"]: # 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"].iteritems(): + 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) + for sha512, piecefield_packed in res["piecefields_packed"].iteritems(): + self.piecefields[sha512].unpack(piecefield_packed) + + 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_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: piecefield.pack().encode("base64") for sha512, piecefield in self.storage.piecefields.iteritems()} + return back + + def needFile(self, inner_path, *args, **kwargs): + if inner_path.endswith("|all"): + @util.Pooled(20) + def pooledNeedBigfile(*args, **kwargs): + return self.needFile(*args, **kwargs) + + inner_path = inner_path.replace("|all", "") + file_info = self.needFileInfo(inner_path) + 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]: + res = pooledNeedBigfile("%s|%s-%s" % (inner_path, piece_from, piece_to), 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 until this limit if help distribute option is checked', default=1, metavar="MB", type=int) + + return super(ConfigPlugin, self).createArguments() diff --git a/plugins/BigFile/Test/TestBigfile.py b/plugins/BigFile/Test/TestBigfile.py new file mode 100644 index 00000000..a934a67c --- /dev/null +++ b/plugins/BigFile/Test/TestBigfile.py @@ -0,0 +1,467 @@ +import time +import os +from cStringIO import StringIO + +import pytest +import msgpack +import mock +from lib import merkletools + +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 + + +@pytest.mark.usefixtures("resetSettings") +@pytest.mark.usefixtures("resetTempSettings") +class TestBigfile: + privatekey = "5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv" + + 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"))["optional.any.iso"] + assert len(piecemap["sha512_pieces"]) == 10 + assert piecemap["sha512_pieces"][0] != piecemap["sha512_pieces"][1] + assert piecemap["sha512_pieces"][0].encode("hex") == "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 = StringIO(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 = StringIO(f.read(1024 * 1024)) + f.close() + site.content_manager.verifyPiece(inner_path, i * 1024 * 1024, piece) + assert "Invalid hash" in str(err) + + 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), "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), "helloend" * 1024) + time_write_end = time.time() - s + + # Verify writes + f = site.storage.open(inner_path) + assert f.read(10) == "hellostart" + f.seek(99 * 1024 * 1024) + assert f.read(8) == "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("127.0.0.1", 1545) + client.sites[site_temp.address] = site_temp + site_temp.connection_server = client + connection = client.getConnection("127.0.0.1", 1544) + + # Add file_server as peer to client + peer_file_server = site_temp.addPeer("127.0.0.1", 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("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("127.0.0.1", 1545) + site_temp.connection_server = client + peer_client = site_temp.addPeer("127.0.0.1", 1544) + + # Download site + site_temp.download(blind_includes=True).join(timeout=5) + + bad_files = site_temp.storage.verifyFiles(quick_check=True) + 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) == "\0" * 10 + # Verify 5. and 10. block downloaded + f.seek(5 * 1024 * 1024) + assert f.read(7) == "Test524" + f.seek(9 * 1024 * 1024) + assert f.read(7) == "943---T" + + # Verify hashfield + assert set(site_temp.content_manager.hashfield) == set([18343, 30970]) # 18343: data/optional.any.iso, 30970: 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("127.0.0.1", 1545) + site_temp.connection_server = client + site_temp.addPeer("127.0.0.1", 1544) + + # Download site + site_temp.download(blind_includes=True).join(timeout=5) + + # 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) == "Test524" + + f.seek(9 * 1024 * 1024) + assert f.read(7) == "943---T" + + assert len(requests) == 4 # 1x peicemap + 1x getpiecefield + 2x for pieces + + assert set(site_temp.content_manager.hashfield) == set([18343, 30970]) + + 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) == "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("Test524") + assert data.endswith("Test838-") + assert "\0" not in data # No null bytes allowed + + assert len(requests) == 2 # Two block download + + + @pytest.mark.parametrize("piecefield_obj", [BigfilePiecefield, BigfilePiecefieldPacked]) + def testPiecefield(self, piecefield_obj, site): + testdatas = [ + "1" * 100 + "0" * 900 + "1" * 4000 + "0" * 4999 + "1", + "010101" * 10 + "01" * 90 + "10" * 400 + "0" * 4999, + "1" * 10000, + "0" * 10000 + ] + for testdata in testdatas: + piecefield = piecefield_obj() + + piecefield.fromstring(testdata) + assert piecefield.tostring() == testdata + assert piecefield[0] == int(testdata[0]) + assert piecefield[100] == int(testdata[100]) + assert piecefield[1000] == int(testdata[1000]) + assert piecefield[len(testdata)-1] == int(testdata[len(testdata)-1]) + + packed = piecefield.pack() + piecefield_new = piecefield_obj() + piecefield_new.unpack(packed) + assert piecefield.tostring() == piecefield_new.tostring() + assert piecefield_new.tostring() == 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("127.0.0.1", 1545) + site_temp.connection_server.sites[site_temp.address] = site_temp + site_temp.addPeer("127.0.0.1", 1544) + + # Download site + site_temp.download(blind_includes=True).join(timeout=5) + + # Download second block + with site_temp.storage.openBigfile(inner_path) as f: + f.seek(1024 * 1024) + assert f.read(1024)[0] != "\0" + + # Make sure first block not download + with site_temp.storage.open(inner_path) as f: + assert f.read(1024)[0] == "\0" + + peer2 = site.addPeer("127.0.0.1", 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("127.0.0.1", i) + print "%.3fs MEM: + %sKB" % (time.time() - s, (meminfo()[0] - mem_s) / 1024) # 0.082s MEM: + 6800KB + print 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("127.0.0.1", 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("127.0.0.1", 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("127.0.0.1", 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("127.0.0.1", 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("127.0.0.1", 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("127.0.0.1", 1544, site_temp, server2) + peer.piecefields[sha512][i] = "1" + 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("127.0.0.1", 1545) + site_temp.connection_server = client + site_temp.addPeer("127.0.0.1", 1544) + + # Download site + site_temp.download(blind_includes=True).join(timeout=5) + + # 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 not "\0" 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 not "\0" in f.read(1024) + assert site_temp.settings["optional_downloaded"] == size_piecemap + size_bigfile + + # Add second block + assert not "\0" 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("127.0.0.1", 1545) + site_temp.connection_server = client + site_temp.addPeer("127.0.0.1", 1544) + + # Download site + site_temp.download(blind_includes=True).join(timeout=5) + + # 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) == "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 "\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("127.0.0.1", 1545) + site_temp.connection_server = client + site_temp.addPeer("127.0.0.1", 1544) + + # Download site + site_temp.download(blind_includes=True).join(timeout=5) + + # 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 diff --git a/plugins/BigFile/Test/conftest.py b/plugins/BigFile/Test/conftest.py new file mode 100644 index 00000000..8c1df5b2 --- /dev/null +++ b/plugins/BigFile/Test/conftest.py @@ -0,0 +1 @@ +from src.Test.conftest import * \ No newline at end of file diff --git a/plugins/BigFile/Test/pytest.ini b/plugins/BigFile/Test/pytest.ini new file mode 100644 index 00000000..d09210d1 --- /dev/null +++ b/plugins/BigFile/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/BigFile/__init__.py b/plugins/BigFile/__init__.py new file mode 100644 index 00000000..005d6661 --- /dev/null +++ b/plugins/BigFile/__init__.py @@ -0,0 +1,2 @@ +import BigfilePlugin +from BigfilePiecefield import BigfilePiecefield, BigfilePiecefieldPacked \ No newline at end of file From 8e2be5cfe2894cf196722ec3190660df991c5ac7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 13:29:32 +0200 Subject: [PATCH 0348/2570] Add Merkletools package for bigfile plugin --- src/lib/merkletools/LICENSE | 21 ++++ src/lib/merkletools/README.md | 178 ++++++++++++++++++++++++++++++++ src/lib/merkletools/__init__.py | 138 +++++++++++++++++++++++++ src/lib/merkletools/setup.py | 29 ++++++ 4 files changed, 366 insertions(+) create mode 100644 src/lib/merkletools/LICENSE create mode 100644 src/lib/merkletools/README.md create mode 100644 src/lib/merkletools/__init__.py create mode 100644 src/lib/merkletools/setup.py diff --git a/src/lib/merkletools/LICENSE b/src/lib/merkletools/LICENSE new file mode 100644 index 00000000..aea5b275 --- /dev/null +++ b/src/lib/merkletools/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Tierion + +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/src/lib/merkletools/README.md b/src/lib/merkletools/README.md new file mode 100644 index 00000000..6a160ad4 --- /dev/null +++ b/src/lib/merkletools/README.md @@ -0,0 +1,178 @@ +# pymerkletools +[![PyPI version](https://badge.fury.io/py/merkletools.svg)](https://badge.fury.io/py/merkletools) [![Build Status](https://travis-ci.org/Tierion/pymerkletools.svg?branch=master)](https://travis-ci.org/Tierion/pymerkletools) + +This is a Python port of [merkle-tools](https://github.com/tierion/merkle-tools). + +Tools for creating Merkle trees, generating merkle proofs, and verification of merkle proofs. + +## Installation + +``` +pip install merkletools +``` + +### Create MerkleTools Object + +```python +import merkletools + +mt = MerkleTools(hash_type="md5") # default is sha256 +# valid hashTypes include all crypto hash algorithms +# such as 'MD5', 'SHA1', 'SHA224', 'SHA256', 'SHA384', 'SHA512' +# as well as the SHA3 family of algorithms +# including 'SHA3-224', 'SHA3-256', 'SHA3-384', and 'SHA3-512' +``` + +To use `sha3`, this module depends on [pysha3](https://pypi.python.org/pypi/pysha3). It will be installed as part of this module or you can install it manually with : +```bash +pip install pysha3==1.0b1 +``` + + +## Methods + +### add_leaf(value, do_hash) + +Adds a value as a leaf or a list of leafs to the tree. The value must be a hex string, otherwise set the optional `do_hash` to true to have your value hashed prior to being added to the tree. + +```python +hex_data = '05ae04314577b2783b4be98211d1b72476c59e9c413cfb2afa2f0c68e0d93911' +list_data = ['Some text data', 'perhaps'] + +mt.add_leaf(hexData) +mt.add_leaf(otherData, True) +``` + +### get_leaf_count() + +Returns the number of leaves that are currently added to the tree. + +```python +leaf_count = mt.get_leaf_count(); +``` + +### get_leaf(index) + +Returns the value of the leaf at the given index as a hex string. + +```python +leaf_value = mt.get_leaf(1) +``` + +### reset_tree() + +Removes all the leaves from the tree, prepararing to to begin creating a new tree. + +```python +mt.reset_tree() +``` + +### make_tree() + +Generates the merkle tree using the leaves that have been added. + +```python +mt.make_tree(); +``` + +### is_ready + +`.is_ready` is a boolean property indicating if the tree is built and ready to supply its root and proofs. The `is_ready` state is `True` only after calling 'make_tree()'. Adding leaves or resetting the tree will change the ready state to False. + +```python +is_ready = mt.is_ready +``` + +### get_merkle_root() + +Returns the merkle root of the tree as a hex string. If the tree is not ready, `None` is returned. + +```python +root_value = mt.get_merkle_root(); +``` + +### get_proof(index) + +Returns the proof as an array of hash objects for the leaf at the given index. If the tree is not ready or no leaf exists at the given index, null is returned. + +```python +proof = mt.get_proof(1) +``` + +The proof array contains a set of merkle sibling objects. Each object contains the sibling hash, with the key value of either right or left. The right or left value tells you where that sibling was in relation to the current hash being evaluated. This information is needed for proof validation, as explained in the following section. + +### validate_proof(proof, target_hash, merkle_root) + +Returns a boolean indicating whether or not the proof is valid and correctly connects the `target_hash` to the `merkle_root`. `proof` is a proof array as supplied by the `get_proof` method. The `target_hash` and `merkle_root` parameters must be a hex strings. + +```python +proof = [ + { right: '09096dbc49b7909917e13b795ebf289ace50b870440f10424af8845fb7761ea5' }, + { right: 'ed2456914e48c1e17b7bd922177291ef8b7f553edf1b1f66b6fc1a076524b22f' }, + { left: 'eac53dde9661daf47a428efea28c81a021c06d64f98eeabbdcff442d992153a8' }, +] +target_hash = '36e0fd847d927d68475f32a94efff30812ee3ce87c7752973f4dd7476aa2e97e' +merkle_root = 'b8b1f39aa2e3fc2dde37f3df04e829f514fb98369b522bfb35c663befa896766' + +is_valid = mt.validate_proof(proof, targetHash, merkleRoot) +``` + +The proof process uses all the proof objects in the array to attempt to prove a relationship between the `target_hash` and the `merkle_root` values. The steps to validate a proof are: + +1. Concatenate `target_hash` and the first hash in the proof array. The right or left designation specifies which side of the concatenation that the proof hash value should be on. +2. Hash the resulting value. +3. Concatenate the resulting hash with the next hash in the proof array, using the same left and right rules. +4. Hash that value and continue the process until you’ve gone through each item in the proof array. +5. The final hash value should equal the `merkle_root` value if the proof is valid, otherwise the proof is invalid. + +## Common Usage + +### Creating a tree and generating the proofs + +```python +mt = MerkleTools() + +mt.add_leaf("tierion", True) +mt.add_leaf(["bitcoin", "blockchain"], True) + +mt.make_tree() + +print "root:", mt.get_merkle_root() # root: '765f15d171871b00034ee55e48ffdf76afbc44ed0bcff5c82f31351d333c2ed1' + +print mt.get_proof(1) # [{left: '2da7240f6c88536be72abe9f04e454c6478ee29709fc3729ddfb942f804fbf08'}, + # {right: 'ef7797e13d3a75526946a3bcf00daec9fc9c9c4d51ddc7cc5df888f74dd434d1'}] + +print mt.validate_proof(mt.get_proof(1), mt.get_leaf(1), mt.get_merkle_root()) # True +``` + +## Notes + +### About tree generation + +1. Internally, leaves are stored as `bytearray`. When the tree is build, it is generated by hashing together the `bytearray` values. +2. Lonely leaf nodes are promoted to the next level up, as depicted below. + + ROOT=Hash(H+E) + / \ + / \ + H=Hash(F+G) E + / \ \ + / \ \ + F=Hash(A+B) G=Hash(C+D) E + / \ / \ \ + / \ / \ \ + A B C D E + + +### Development +This module uses Python's `hashlib` for hashing. Inside a `MerkleTools` object all +hashes are stored as Python `bytearray`. This way hashes can be concatenated simply with `+` and the result +used as input for the hash function. But for +simplicity and easy to use `MerkleTools` methods expect that both input and outputs are hex +strings. We can convert from one type to the other using default Python string methods. +For example: +```python +hash = hashlib.sha256('a').digest() # '\xca\x97\x81\x12\xca\x1b\xbd\xca\xfa\xc21\xb3\x9a#\xdcM\xa7\x86\xef\xf8\x14|Nr\xb9\x80w\x85\xaf\xeeH\xbb' +hex_string = hash.decode('hex') # 'ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb' +back_to_hash = hash_string.decode('hex') # '\xca\x97\x81\x12\xca\x1b\xbd\xca\xfa\xc21\xb3\x9a#\xdcM\xa7\x86\xef\xf8\x14|Nr\xb9\x80w\x85\xaf\xeeH\xbb' +``` diff --git a/src/lib/merkletools/__init__.py b/src/lib/merkletools/__init__.py new file mode 100644 index 00000000..ce5c9487 --- /dev/null +++ b/src/lib/merkletools/__init__.py @@ -0,0 +1,138 @@ +import hashlib +import binascii + +class MerkleTools(object): + def __init__(self, hash_type="sha256"): + hash_type = hash_type.lower() + if hash_type == 'sha256': + self.hash_function = hashlib.sha256 + elif hash_type == 'md5': + self.hash_function = hashlib.md5 + elif hash_type == 'sha224': + self.hash_function = hashlib.sha224 + elif hash_type == 'sha384': + self.hash_function = hashlib.sha384 + elif hash_type == 'sha512': + self.hash_function = hashlib.sha512 + elif hash_type == 'sha3_256': + self.hash_function = hashlib.sha3_256 + elif hash_type == 'sha3_224': + self.hash_function = hashlib.sha3_224 + elif hash_type == 'sha3_384': + self.hash_function = hashlib.sha3_384 + elif hash_type == 'sha3_512': + self.hash_function = hashlib.sha3_512 + else: + raise Exception('`hash_type` {} nor supported'.format(hash_type)) + + self.reset_tree() + + def _to_hex(self, x): + try: # python3 + return x.hex() + except: # python2 + return binascii.hexlify(x) + + def reset_tree(self): + self.leaves = list() + self.levels = None + self.is_ready = False + + def add_leaf(self, values, do_hash=False): + self.is_ready = False + # check if single leaf + if isinstance(values, tuple) or isinstance(values, list): + for v in values: + if do_hash: + v = v.encode('utf-8') + v = self.hash_function(v).hexdigest() + v = bytearray.fromhex(v) + else: + v = bytearray.fromhex(v) + self.leaves.append(v) + else: + if do_hash: + v = values.encode("utf-8") + v = self.hash_function(v).hexdigest() + v = bytearray.fromhex(v) + else: + v = bytearray.fromhex(values) + self.leaves.append(v) + + def get_leaf(self, index): + return self._to_hex(self.leaves[index]) + + def get_leaf_count(self): + return len(self.leaves) + + def get_tree_ready_state(self): + return self.is_ready + + def _calculate_next_level(self): + solo_leave = None + N = len(self.levels[0]) # number of leaves on the level + if N % 2 == 1: # if odd number of leaves on the level + solo_leave = self.levels[0][-1] + N -= 1 + + new_level = [] + for l, r in zip(self.levels[0][0:N:2], self.levels[0][1:N:2]): + new_level.append(self.hash_function(l+r).digest()) + if solo_leave is not None: + new_level.append(solo_leave) + self.levels = [new_level, ] + self.levels # prepend new level + + def make_tree(self): + self.is_ready = False + if self.get_leaf_count() > 0: + self.levels = [self.leaves, ] + while len(self.levels[0]) > 1: + self._calculate_next_level() + self.is_ready = True + + def get_merkle_root(self): + if self.is_ready: + if self.levels is not None: + return self._to_hex(self.levels[0][0]) + else: + return None + else: + return None + + def get_proof(self, index): + if self.levels is None: + return None + elif not self.is_ready or index > len(self.leaves)-1 or index < 0: + return None + else: + proof = [] + for x in range(len(self.levels) - 1, 0, -1): + level_len = len(self.levels[x]) + if (index == level_len - 1) and (level_len % 2 == 1): # skip if this is an odd end node + index = int(index / 2.) + continue + is_right_node = index % 2 + sibling_index = index - 1 if is_right_node else index + 1 + sibling_pos = "left" if is_right_node else "right" + sibling_value = self._to_hex(self.levels[x][sibling_index]) + proof.append({sibling_pos: sibling_value}) + index = int(index / 2.) + return proof + + def validate_proof(self, proof, target_hash, merkle_root): + merkle_root = bytearray.fromhex(merkle_root) + target_hash = bytearray.fromhex(target_hash) + if len(proof) == 0: + return target_hash == merkle_root + else: + proof_hash = target_hash + for p in proof: + try: + # the sibling is a left node + sibling = bytearray.fromhex(p['left']) + proof_hash = self.hash_function(sibling + proof_hash).digest() + except: + # the sibling is a right node + sibling = bytearray.fromhex(p['right']) + proof_hash = self.hash_function(proof_hash + sibling).digest() + return proof_hash == merkle_root diff --git a/src/lib/merkletools/setup.py b/src/lib/merkletools/setup.py new file mode 100644 index 00000000..51f2341b --- /dev/null +++ b/src/lib/merkletools/setup.py @@ -0,0 +1,29 @@ +import os + +from setuptools import find_packages +from setuptools import setup + +here = os.path.abspath(os.path.dirname(__file__)) +install_requires = [ + "pysha3==1.0b1" +] + +setup( + name='merkletools', + version='1.0.2', + description='Merkle Tools', + classifiers=[ + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 2.7", + ], + url='https://github.com/', + author='Eder Santana', + keywords='merkle tree, blockchain, tierion', + license="MIT", + packages=find_packages(), + include_package_data=False, + zip_safe=False, + install_requires=install_requires +) From be755fe25ee4ec7f7633263f54044918495aa332 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 13:30:17 +0200 Subject: [PATCH 0349/2570] Custom logging for tests to spot possible performance bottlenecks easier --- src/Test/conftest.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/Test/conftest.py b/src/Test/conftest.py index a1c20ae3..a2e26683 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -6,6 +6,7 @@ import logging import json import shutil import gc +import datetime import pytest import mock @@ -27,8 +28,32 @@ sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + "/..")) # Import from Config import config config.argv = ["none"] # Dont pass any argv to config parser config.parse(silent=True) # Plugins need to access the configuration +config.action = "test" + logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) +# Set custom formatter with realative time format (via: https://stackoverflow.com/questions/31521859/python-logging-module-time-since-last-log) +class TimeFilter(logging.Filter): + + def filter(self, record): + try: + last = self.last + except AttributeError: + last = record.relativeCreated + + delta = datetime.datetime.fromtimestamp(record.relativeCreated/1000.0) - datetime.datetime.fromtimestamp(last/1000.0) + + record.relative = '{0:.3f}'.format(delta.seconds + delta.microseconds/1000000.0) + + self.last = record.relativeCreated + return True + +log = logging.getLogger() +fmt = logging.Formatter(fmt='+%(relative)ss %(levelname)-8s %(name)s %(message)s') +[hndl.addFilter(TimeFilter()) for hndl in log.handlers] +[hndl.setFormatter(fmt) for hndl in log.handlers] + +# Load plugins from Plugin import PluginManager PluginManager.plugin_manager.loadPlugins() config.loadPlugins() From c82b19687a33e13c2146ea89aedcc862050c35df Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 13:31:49 +0200 Subject: [PATCH 0350/2570] Spy object also stores the called object --- src/Test/Spy.py | 31 +++++++++++++++++-------------- src/Test/TestSiteDownload.py | 6 +++--- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/Test/Spy.py b/src/Test/Spy.py index c017dea9..8d8f1800 100644 --- a/src/Test/Spy.py +++ b/src/Test/Spy.py @@ -1,17 +1,20 @@ class Spy: - def __init__(self, obj, func_name): - self.obj = obj - self.func_name = func_name - self.func_original = getattr(self.obj, func_name) - self.calls = [] + def __init__(self, obj, func_name): + self.obj = obj + self.func_name = func_name + self.func_original = getattr(self.obj, func_name) + self.calls = [] - def __enter__(self, *args, **kwargs): - def loggedFunc(cls, *args, **kwags): - print "Logging", self, args, kwargs - self.calls.append(args) - return self.func_original(cls, *args, **kwargs) - setattr(self.obj, self.func_name, loggedFunc) - return self.calls + def __enter__(self, *args, **kwargs): + def loggedFunc(cls, *args, **kwargs): + call = dict(enumerate(args, 1)) + call[0] = cls + call.update(kwargs) + print "Logging", call + self.calls.append(call) + return self.func_original(cls, *args, **kwargs) + setattr(self.obj, self.func_name, loggedFunc) + return self.calls - def __exit__(self, *args, **kwargs): - setattr(self.obj, self.func_name, self.func_original) \ No newline at end of file + def __exit__(self, *args, **kwargs): + setattr(self.obj, self.func_name, self.func_original) \ No newline at end of file diff --git a/src/Test/TestSiteDownload.py b/src/Test/TestSiteDownload.py index fcd2922c..ca155b65 100644 --- a/src/Test/TestSiteDownload.py +++ b/src/Test/TestSiteDownload.py @@ -39,7 +39,7 @@ class TestSiteDownload: site_temp.needFile("data/img/direct_domains.png", priority=15, blocking=False) site_temp.onFileDone.append(boostRequest) site_temp.download(blind_includes=True).join(timeout=5) - file_requests = [request[2]["inner_path"] for request in requests if request[0] in ("getFile", "streamFile")] + file_requests = [request[3]["inner_path"] for request in requests if request[1] in ("getFile", "streamFile")] # Test priority assert file_requests[0:2] == ["content.json", "index.html"] # Must-have files assert file_requests[2:4] == ["data/img/multiuser.png", "data/img/direct_domains.png"] # Directly requested files @@ -206,7 +206,7 @@ class TestSiteDownload: threads.append(site_temp.needFile("data/users/1CjfbrbwtP8Y2QjPy12vpTATkUT7oSiPQ9/peanut-butter-jelly-time.gif", blocking=False)) gevent.joinall(threads) - assert len([request for request in requests if request[0] == "findHashIds"]) == 1 # findHashids should call only once + assert len([request for request in requests if request[1] == "findHashIds"]) == 1 # findHashids should call only once assert site_temp.storage.isFile("data/optional.txt") assert site_temp.storage.isFile("data/users/1CjfbrbwtP8Y2QjPy12vpTATkUT7oSiPQ9/peanut-butter-jelly-time.gif") @@ -257,7 +257,7 @@ class TestSiteDownload: site.publish() time.sleep(0.1) site_temp.download(blind_includes=True).join(timeout=5) - assert len([request for request in requests if request[0] in ("getFile", "streamFile")]) == 1 + assert len([request for request in requests if request[1] in ("getFile", "streamFile")]) == 1 assert site_temp.storage.open("data/data.json").read() == data_new From b1bfe39cd56ae4cf2eadec303f1ddb1ff5c71534 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 13:32:39 +0200 Subject: [PATCH 0351/2570] Use merged getFile function call to download file using streaming --- src/Test/TestPeer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Test/TestPeer.py b/src/Test/TestPeer.py index f0d81033..cadc5eef 100644 --- a/src/Test/TestPeer.py +++ b/src/Test/TestPeer.py @@ -44,7 +44,7 @@ class TestPeer: peer_file_server = site_temp.addPeer("127.0.0.1", 1544) # Testing streamFile - buff = peer_file_server.streamFile(site_temp.address, "content.json") + buff = peer_file_server.getFile(site_temp.address, "content.json", streaming=True) assert "sign" in buff.getvalue() # Testing getFile @@ -159,4 +159,4 @@ class TestPeer: res = peer_file_server.findHashIds([1234, 1235]) assert res[1234] == [('1.2.3.4', 1544), ('1.2.3.5', 1545), ("127.0.0.1", 1544)] - assert res[1235] == [('1.2.3.5', 1545), ('1.2.3.6', 1546)] \ No newline at end of file + assert res[1235] == [('1.2.3.5', 1545), ('1.2.3.6', 1546)] From f0f9240fc81d4a7d71e2b87c226f33435720b40a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 13:33:00 +0200 Subject: [PATCH 0352/2570] getDirname strips the leading / chars --- src/Test/TestHelper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test/TestHelper.py b/src/Test/TestHelper.py index 28f7f6fb..3eb4c06d 100644 --- a/src/Test/TestHelper.py +++ b/src/Test/TestHelper.py @@ -27,7 +27,7 @@ class TestHelper: assert helper.getDirname("") == "" assert helper.getDirname("content.json") == "" assert helper.getDirname("data/users/") == "data/users/" - assert helper.getDirname("/data/users/content.json") == "/data/users/" + assert helper.getDirname("/data/users/content.json") == "data/users/" def testGetFilename(self): From 3d57fbb1b8a76ea69152947a0438e4797d18b418 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 13:33:26 +0200 Subject: [PATCH 0353/2570] Use the real size of content.json instead of predefine it --- src/Test/TestFileRequest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test/TestFileRequest.py b/src/Test/TestFileRequest.py index 45d44b5a..4daf842e 100644 --- a/src/Test/TestFileRequest.py +++ b/src/Test/TestFileRequest.py @@ -22,7 +22,7 @@ class TestFileRequest: response = connection.request("getFile", {"site": site.address, "inner_path": "content.json", "location": 0}) assert "sign" in response["body"] - response = connection.request("getFile", {"site": site.address, "inner_path": "content.json", "location": 0, "file_size": 4460}) + response = connection.request("getFile", {"site": site.address, "inner_path": "content.json", "location": 0, "file_size": site.storage.getSize("content.json")}) assert "sign" in response["body"] # Invalid file From ab9fa9ec0c2bf8488307d36766d85bbc44e17dff Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 13:34:14 +0200 Subject: [PATCH 0354/2570] Mark data/optional.* as optional for test site --- .../content.json | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/Test/testdata/1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT-original/content.json b/src/Test/testdata/1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT-original/content.json index 481e85c0..786db098 100644 --- a/src/Test/testdata/1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT-original/content.json +++ b/src/Test/testdata/1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT-original/content.json @@ -110,28 +110,24 @@ "files_allowed": "data.json", "includes_allowed": false, "max_size": 20000, - "signers": [ "15ik6LeBWnACWfaika1xqGapRZ1zh3JpCo" ], + "signers": ["15ik6LeBWnACWfaika1xqGapRZ1zh3JpCo"], "signers_required": 1, "user_id": 47, "user_name": "test" }, "data/users/content.json": { - "signers": [ "1LSxsKfC9S9TVXGGNSM3vPHjyW82jgCX5f" ], + "signers": ["1LSxsKfC9S9TVXGGNSM3vPHjyW82jgCX5f"], "signers_required": 1 } }, "inner_path": "content.json", - "modified": 1470340814.398, - "optional": "(data/img/zero.*|data/optional.txt)", - "sign": [ - 97109682715361435939224827180347249103292065360394744202228746983963408224367, - 11265525339134417763503386504484747758843583699493121728411011831763675881820 - ], + "modified": 1503257990, + "optional": "(data/img/zero.*|data/optional.*)", "signers_sign": "HDNmWJHM2diYln4pkdL+qYOvgE7MdwayzeG+xEUZBgp1HtOjBJS+knDEVQsBkjcOPicDG2it1r6R1eQrmogqSP0=", "signs": { - "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": "G9zhZM8oiMGFO3CqnU6QyzYKLZXnrOgr+BWOIqj+BWcrIvHBTbrtIkVchmq2VZo9JYTGO/loe0VuC+83BtqsMpM=" + "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": "G4Uq365UBliQG66ygip1jNGYqW6Eh9Mm7nLguDFqAgk/Hksq/ruqMf9rXv78mgUfPBvL2+XgDKYvFDtlykPFZxk=" }, "signs_required": 1, "title": "ZeroBlog", - "zeronet_version": "0.3.7" + "zeronet_version": "0.5.7" } \ No newline at end of file From d176150248d22bf65a29e5ceb2044617131140b6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 13:34:50 +0200 Subject: [PATCH 0355/2570] Test multiple length of message for signing --- src/Test/TestCryptBitcoin.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Test/TestCryptBitcoin.py b/src/Test/TestCryptBitcoin.py index bebf906e..a6009679 100644 --- a/src/Test/TestCryptBitcoin.py +++ b/src/Test/TestCryptBitcoin.py @@ -34,10 +34,12 @@ class TestCryptBitcoin: assert address_bad != "1MpDMxFeDUkiHohxx9tbGLeEGEuR4ZNsJz" # Text signing - sign = CryptBitcoin.sign("hello", privatekey) + for pad_len in range(0, 300, 10): + pad = pad_len * "!" + sign = CryptBitcoin.sign("hello" + pad, privatekey) - assert CryptBitcoin.verify("hello", address, sign) - assert not CryptBitcoin.verify("not hello", address, sign) + assert CryptBitcoin.verify("hello" + pad, address, sign) + assert not CryptBitcoin.verify("not hello" + pad, address, sign) # Signed by bad privatekey sign_bad = CryptBitcoin.sign("hello", privatekey_bad) From a71e82c3159cf69119513304031143fccd9e67ee Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 13:35:15 +0200 Subject: [PATCH 0356/2570] Test user sub-directories inner_path --- src/Test/TestContentUser.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Test/TestContentUser.py b/src/Test/TestContentUser.py index 95af8529..92a3e48f 100644 --- a/src/Test/TestContentUser.py +++ b/src/Test/TestContentUser.py @@ -13,6 +13,8 @@ class TestUserContent: # File info for not existing user file file_info = site.content_manager.getFileInfo("data/users/notexist/data.json") assert file_info["content_inner_path"] == "data/users/notexist/content.json" + file_info = site.content_manager.getFileInfo("data/users/notexist/a/b/data.json") + assert file_info["content_inner_path"] == "data/users/notexist/content.json" valid_signers = site.content_manager.getValidSigners("data/users/notexist/content.json") assert valid_signers == ["14wgQ4VDDZNoRMFF4yCDuTrBSHmYhL3bet", "notexist", "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT"] From e9beeb85fc97d6c78624522e88a4df8038e6bfbb Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 13:35:55 +0200 Subject: [PATCH 0357/2570] Use with to open files to avoid keeping them open in case of errors --- src/Test/TestContent.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Test/TestContent.py b/src/Test/TestContent.py index 7d0e0f63..82229623 100644 --- a/src/Test/TestContent.py +++ b/src/Test/TestContent.py @@ -231,12 +231,12 @@ class TestContent: def testVerifyUnsafePattern(self, site): site.content_manager.contents["content.json"]["includes"]["data/test_include/content.json"]["files_allowed"] = "([a-zA-Z]+)*" with pytest.raises(UnsafePatternError) as err: - data = site.storage.open("data/test_include/content.json") - site.content_manager.verifyFile("data/test_include/content.json", data, ignore_same=False) - assert "Potentially unsafe" in str(err) + with site.storage.open("data/test_include/content.json") as data: + site.content_manager.verifyFile("data/test_include/content.json", data, ignore_same=False) + assert "Potentially unsafe" in str(err) site.content_manager.contents["data/users/content.json"]["user_contents"]["permission_rules"]["([a-zA-Z]+)*"] = {"max_size": 0} with pytest.raises(UnsafePatternError) as err: - data = site.storage.open("data/users/1C5sgvWaSgfaTpV5kjBCnCiKtENNMYo69q/content.json") - site.content_manager.verifyFile("data/users/1C5sgvWaSgfaTpV5kjBCnCiKtENNMYo69q/content.json", data, ignore_same=False) - assert "Potentially unsafe" in str(err) + with site.storage.open("data/users/1C5sgvWaSgfaTpV5kjBCnCiKtENNMYo69q/content.json") as data: + site.content_manager.verifyFile("data/users/1C5sgvWaSgfaTpV5kjBCnCiKtENNMYo69q/content.json", data, ignore_same=False) + assert "Potentially unsafe" in str(err) From 0b9bc569593ecc4f889e1068569a8cc686985b73 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 13:36:14 +0200 Subject: [PATCH 0358/2570] Test sub-directory files in user diretories --- src/Test/TestContent.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Test/TestContent.py b/src/Test/TestContent.py index 82229623..7c46e022 100644 --- a/src/Test/TestContent.py +++ b/src/Test/TestContent.py @@ -131,6 +131,9 @@ class TestContent: def testFileInfo(self, site): assert "sha512" in site.content_manager.getFileInfo("index.html") + assert site.content_manager.getFileInfo("data/img/domain.png")["content_inner_path"] == "/content.json" + assert site.content_manager.getFileInfo("data/users/hello.png")["content_inner_path"] == "data/users/content.json" + assert site.content_manager.getFileInfo("data/users/content.json")["content_inner_path"] == "data/users/content.json" assert not site.content_manager.getFileInfo("notexist") # Optional file From e4fc4ca410eb230534c5067176104df25c4f2290 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 13:36:29 +0200 Subject: [PATCH 0359/2570] Version 0.6.0, Rev3091 --- src/Config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index 77544870..5707078c 100644 --- a/src/Config.py +++ b/src/Config.py @@ -9,8 +9,8 @@ import ConfigParser class Config(object): def __init__(self, argv): - self.version = "0.5.7" - self.rev = 2192 + self.version = "0.6.0" + self.rev = 3091 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 4c6e01d38d104f5b997718dc0b4de41285bea8e1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 17:24:49 +0200 Subject: [PATCH 0360/2570] Run Bigfile plugin tests --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 34f7706d..fa6d4289 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,7 @@ before_script: - openssl version -a script: - python -m pytest plugins/CryptMessage/Test + - python -m pytest plugins/Bigfile/Test - python -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini before_install: - pip install -U pytest mock pytest-cov selenium From b41570b6633b4c2b89184eddd1566432abdd6e3a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 17:25:14 +0200 Subject: [PATCH 0361/2570] Avoid console windows when setting sparse flag on Windows --- plugins/BigFile/BigfilePlugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/BigFile/BigfilePlugin.py b/plugins/BigFile/BigfilePlugin.py index 94e1ec7d..d79de803 100644 --- a/plugins/BigFile/BigfilePlugin.py +++ b/plugins/BigFile/BigfilePlugin.py @@ -340,7 +340,9 @@ class SiteStoragePlugin(object): f.truncate(size) f.close() if os.name == "nt": - subprocess.call(["fsutil", "sparse", "setflag", file_path]) + 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) From bc9bddf9186b722075d527a161c09d6d2b75ca50 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 17:25:33 +0200 Subject: [PATCH 0362/2570] Make sure the hashfield response is correct --- 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 e8b60796..5c0f6f63 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -278,7 +278,7 @@ class Peer(object): self.time_hashfield = time.time() res = self.request("getHashfield", {"site": self.site.address}) - if not res or "error" in res: + if not res or "error" in res or not "hashfield_raw" in res: return False self.hashfield.replaceFromString(res["hashfield_raw"]) From e2b6dd37b7895b0c3780d8d35a25e0fe00c42754 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 17:25:56 +0200 Subject: [PATCH 0363/2570] Only announce once per checkTask loop --- src/Worker/WorkerManager.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 2939b256..14ca2649 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -38,6 +38,7 @@ class WorkerManager(object): def checkTasks(self): while self.running: tasks = task = worker = workers = None # Cleanup local variables + announced = False time.sleep(15) # Check every 15 sec # Clean up workers @@ -74,7 +75,9 @@ class WorkerManager(object): len(task["peers"] or []), len(task["failed"]), len(self.asked_peers) ) ) - task["site"].announce(mode="more") # Find more peers + if not announced: + task["site"].announce(mode="more") # Find more peers + announced = True if task["optional_hash_id"]: if self.workers: if not task["time_started"]: From f997a69ebc35786382879a5d105cc920eda02c75 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 17:26:21 +0200 Subject: [PATCH 0364/2570] Run checkTask for every task --- src/Worker/WorkerManager.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 14ca2649..c97eda16 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -87,7 +87,7 @@ class WorkerManager(object): if len(self.asked_peers) < ask_limit and len(task["peers"] or []) <= len(task["failed"]) * 2: # Re-search for high priority self.startFindOptional(find_more=True) - elif task["peers"]: + if task["peers"]: peers_try = [peer for peer in task["peers"] if peer not in task["failed"]] if peers_try: self.startWorkers(peers_try) @@ -96,8 +96,7 @@ class WorkerManager(object): if task["peers"]: # Release the peer lock self.log.debug("Task peer lock release: %s" % task["inner_path"]) task["peers"] = [] - self.startWorkers() - break # One reannounce per loop + self.startWorkers() if len(self.tasks) > len(self.workers) * 2 and len(self.workers) < self.getMaxWorkers(): self.startWorkers() From 365ba9b5f4967438fc669f75a5bedf7f1259e635 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 17:26:43 +0200 Subject: [PATCH 0365/2570] Force start peers for optional files --- src/Worker/WorkerManager.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index c97eda16..9aafc39d 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -90,7 +90,7 @@ class WorkerManager(object): if task["peers"]: peers_try = [peer for peer in task["peers"] if peer not in task["failed"]] if peers_try: - self.startWorkers(peers_try) + self.startWorkers(peers_try, force_num=5) self.startFindOptional(find_more=True) else: if task["peers"]: # Release the peer lock @@ -139,9 +139,9 @@ class WorkerManager(object): return config.workers # Add new worker - def addWorker(self, peer, multiplexing=False): + def addWorker(self, peer, multiplexing=False, force=False): key = peer.key - if len(self.workers) > self.getMaxWorkers(): + if len(self.workers) > self.getMaxWorkers() and not force: return False if multiplexing: # Add even if we already have worker for this peer key = "%s/%s" % (key, len(self.workers)) @@ -166,7 +166,7 @@ class WorkerManager(object): return True # Start workers to process tasks - def startWorkers(self, peers=None): + def startWorkers(self, peers=None, force_num=0): if not self.tasks: return False # No task for workers if len(self.workers) >= self.getMaxWorkers() and not peers: @@ -185,7 +185,13 @@ class WorkerManager(object): for peer in peers: # One worker for every peer if peers and peer not in peers: continue # If peers defined and peer not valid - worker = self.addWorker(peer) + + if force_num: + worker = self.addWorker(peer, force=True) + force_num -= 1 + else: + worker = self.addWorker(peer) + if worker: self.log.debug("Added worker: %s, workers: %s/%s" % (peer.key, len(self.workers), self.getMaxWorkers())) @@ -272,7 +278,7 @@ class WorkerManager(object): if found: found_peers = set([peer for peers in found.values() for peer in peers]) - self.startWorkers(found_peers) + self.startWorkers(found_peers, force_num=3) if len(found) < len(optional_hash_ids) or find_more or (high_priority and any(len(peers) < 10 for peers in found.itervalues())): self.log.debug("No local result for optional files: %s" % (optional_hash_ids - set(found))) @@ -297,7 +303,7 @@ class WorkerManager(object): if found: found_peers = set([peer for hash_id_peers in found.values() for peer in hash_id_peers]) - self.startWorkers(found_peers) + self.startWorkers(found_peers, force_num=3) if len(found) < len(optional_hash_ids) or find_more: self.log.debug("No connected hashtable result for optional files: %s" % (optional_hash_ids - set(found))) @@ -330,7 +336,7 @@ class WorkerManager(object): if found: found_peers = set([peer for hash_id_peers in found.values() for peer in hash_id_peers]) - self.startWorkers(found_peers) + self.startWorkers(found_peers, force_num=3) if len(thread_values) == len(threads): # Got result from all started thread @@ -359,7 +365,7 @@ class WorkerManager(object): if found: found_peers = set([peer for hash_id_peers in found.values() for peer in hash_id_peers]) - self.startWorkers(found_peers) + self.startWorkers(found_peers, force_num=3) if len(found) < len(optional_hash_ids): self.log.debug("No findhash result for optional files: %s" % (optional_hash_ids - set(found))) From f1276beb975493610bfa32ef17ad5a795f0b1068 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 17:27:01 +0200 Subject: [PATCH 0366/2570] Bump prority for non-user json files --- src/Worker/WorkerManager.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 9aafc39d..47f62e08 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -417,16 +417,16 @@ class WorkerManager(object): if "-default" in inner_path: return -4 # Default files are cloning not important elif inner_path.endswith("all.css"): - return 13 # boost css files priority + return 14 # boost css files priority elif inner_path.endswith("all.js"): - return 12 # boost js files priority + return 13 # boost js files priority elif inner_path.endswith("dbschema.json"): - return 11 # boost database specification + return 12 # boost database specification elif inner_path.endswith("content.json"): return 1 # boost included content.json files priority a bit elif inner_path.endswith(".json"): if len(inner_path) < 50: # Boost non-user json files - return 10 + return 11 else: return 2 return 0 From 0e85beec30222e44ee839291fbbc632496609607 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Oct 2017 17:27:08 +0200 Subject: [PATCH 0367/2570] Rev3094 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 5707078c..ea0d2d41 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.0" - self.rev = 3091 + self.rev = 3094 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 4265c4126a670540d1fe830aa6f570ab07b14311 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 5 Oct 2017 13:26:40 +0200 Subject: [PATCH 0368/2570] Change Bigfile plugin directory case --- plugins/{BigFile => Bigfile}/BigfilePiecefield.py | 0 plugins/{BigFile => Bigfile}/BigfilePlugin.py | 0 plugins/{BigFile => Bigfile}/Test/TestBigfile.py | 0 plugins/{BigFile => Bigfile}/Test/conftest.py | 0 plugins/{BigFile => Bigfile}/Test/pytest.ini | 0 plugins/{BigFile => Bigfile}/__init__.py | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename plugins/{BigFile => Bigfile}/BigfilePiecefield.py (100%) rename plugins/{BigFile => Bigfile}/BigfilePlugin.py (100%) rename plugins/{BigFile => Bigfile}/Test/TestBigfile.py (100%) rename plugins/{BigFile => Bigfile}/Test/conftest.py (100%) rename plugins/{BigFile => Bigfile}/Test/pytest.ini (100%) rename plugins/{BigFile => Bigfile}/__init__.py (100%) diff --git a/plugins/BigFile/BigfilePiecefield.py b/plugins/Bigfile/BigfilePiecefield.py similarity index 100% rename from plugins/BigFile/BigfilePiecefield.py rename to plugins/Bigfile/BigfilePiecefield.py diff --git a/plugins/BigFile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py similarity index 100% rename from plugins/BigFile/BigfilePlugin.py rename to plugins/Bigfile/BigfilePlugin.py diff --git a/plugins/BigFile/Test/TestBigfile.py b/plugins/Bigfile/Test/TestBigfile.py similarity index 100% rename from plugins/BigFile/Test/TestBigfile.py rename to plugins/Bigfile/Test/TestBigfile.py diff --git a/plugins/BigFile/Test/conftest.py b/plugins/Bigfile/Test/conftest.py similarity index 100% rename from plugins/BigFile/Test/conftest.py rename to plugins/Bigfile/Test/conftest.py diff --git a/plugins/BigFile/Test/pytest.ini b/plugins/Bigfile/Test/pytest.ini similarity index 100% rename from plugins/BigFile/Test/pytest.ini rename to plugins/Bigfile/Test/pytest.ini diff --git a/plugins/BigFile/__init__.py b/plugins/Bigfile/__init__.py similarity index 100% rename from plugins/BigFile/__init__.py rename to plugins/Bigfile/__init__.py From 59a04f101db44ce3f36ae1874d02255a70a62a13 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 5 Oct 2017 14:05:48 +0200 Subject: [PATCH 0369/2570] Remove unnecessary file --- plugins/FilePack/FilePackPlugin.py- | 113 ---------------------------- 1 file changed, 113 deletions(-) delete mode 100644 plugins/FilePack/FilePackPlugin.py- diff --git a/plugins/FilePack/FilePackPlugin.py- b/plugins/FilePack/FilePackPlugin.py- deleted file mode 100644 index 8bcbeea2..00000000 --- a/plugins/FilePack/FilePackPlugin.py- +++ /dev/null @@ -1,113 +0,0 @@ -import os -import re -import itertools - -from Plugin import PluginManager -from Config import config -from util import helper - - -# 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): - if archive_path not in archive_cache: - if archive_path.endswith("tar.gz"): - import tarfile - archive_cache[archive_path] = tarfile.open(archive_path, "r:gz") - elif archive_path.endswith("tar.bz2"): - import tarfile - archive_cache[archive_path] = tarfile.open(archive_path, "r:bz2") - else: - import zipfile - archive_cache[archive_path] = zipfile.ZipFile(archive_path) - helper.timer(5, lambda: closeArchive(archive_path)) # Close after 5 sec - - archive = archive_cache[archive_path] - return archive - -def openArchiveFile(archive_path, path_within): - archive = openArchive(archive_path) - if archive_path.endswith(".zip"): - return archive.open(path_within) - else: - return archive.extractfile(path_within.encode("utf8")) - - -@PluginManager.registerTo("UiRequest") -class UiRequestPlugin(object): - def actionSiteMedia(self, path, **kwargs): - if ".zip/" in path or ".tar.gz/" in path: - path_parts = self.parsePath(path) - file_path = u"%s/%s/%s" % (config.data_dir, path_parts["address"], path_parts["inner_path"].decode("utf8")) - match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))/(.*)", file_path) - archive_path, path_within = match.groups() - if not os.path.isfile(archive_path): - site = self.server.site_manager.get(path_parts["address"]) - if not site: - self.error404(path) - # Wait until file downloads - result = site.needFile(site.storage.getInnerPath(archive_path), priority=10) - # Send virutal file path download finished event to remove loading screen - site.updateWebsocket(file_done=site.storage.getInnerPath(file_path)) - if not result: - return self.error404(path) - try: - file = openArchiveFile(archive_path, path_within) - content_type = self.getContentType(file_path) - self.sendHeader(200, content_type=content_type, noscript=kwargs.get("header_noscript", False)) - return self.streamFile(file) - except Exception, err: - self.log.debug("Error opening archive file: %s" % err) - return self.error404(path) - - return super(UiRequestPlugin, self).actionSiteMedia(path, **kwargs) - - def streamFile(self, file): - while 1: - 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 or ".tar.bz2/" in inner_path: - match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))/(.*)", inner_path) - inner_archive_path, path_within = match.groups() - return super(SiteStoragePlugin, self).isFile(inner_archive_path) - else: - return super(SiteStoragePlugin, self).isFile(inner_path) - - def getDbFiles(self): - for item in super(SiteStoragePlugin, self).getDbFiles(): - yield item - - # Search for archive files - for content_inner_path in self.site.content_manager.listContents(): - content = self.site.content_manager.contents[content_inner_path] - if not content: - merged_site.log.error("[MISSING] %s" % content_inner_path) - continue - - file_relative_paths = itertools.chain( - content.get("files", {}).iteritems(), - content.get("files_optional", {}).iteritems() - ) - - for file_relative_path, node in file_relative_paths: - if "zeronet-archive" in file_relative_path: - print node From 07caaa6b481fcc0276f5cb9cb1855d88a28af399 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 5 Oct 2017 19:24:39 +0200 Subject: [PATCH 0370/2570] Fix removed is_pinned flag on content.json update --- plugins/OptionalManager/ContentDbPlugin.py | 6 +++--- src/Config.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/OptionalManager/ContentDbPlugin.py b/plugins/OptionalManager/ContentDbPlugin.py index 5d7942e8..961a7390 100644 --- a/plugins/OptionalManager/ContentDbPlugin.py +++ b/plugins/OptionalManager/ContentDbPlugin.py @@ -179,8 +179,7 @@ class ContentDbPlugin(object): is_pinned = 0 cur.insertOrUpdate("file_optional", { "hash_id": hash_id, - "size": int(file["size"]), - "is_pinned": is_pinned + "size": int(file["size"]) }, { "site_id": site_id, "inner_path": file_inner_path @@ -188,7 +187,8 @@ class ContentDbPlugin(object): "time_added": int(time.time()), "time_downloaded": int(time.time()) if is_downloaded else 0, "is_downloaded": is_downloaded, - "peer": is_downloaded + "peer": is_downloaded, + "is_pinned": is_pinned }) self.optional_files[site_id][file_inner_path[-8:]] = 1 num += 1 diff --git a/src/Config.py b/src/Config.py index ea0d2d41..a9d016bf 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.0" - self.rev = 3094 + self.rev = 3095 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 7905e12fc36127a0e8df38ff9943ce0f2997af3c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 6 Oct 2017 00:42:19 +0200 Subject: [PATCH 0371/2570] Rev3097, Add peer info for bigfiles even if it's not downloaded --- plugins/OptionalManager/UiWebsocketPlugin.py | 27 ++++++++++---------- src/Config.py | 2 +- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/plugins/OptionalManager/UiWebsocketPlugin.py b/plugins/OptionalManager/UiWebsocketPlugin.py index 7fe4bd2d..89e51eac 100644 --- a/plugins/OptionalManager/UiWebsocketPlugin.py +++ b/plugins/OptionalManager/UiWebsocketPlugin.py @@ -50,14 +50,12 @@ class UiWebsocketPlugin(object): sha512 = file_info["sha512"] piecefield = site.storage.piecefields[sha512].tostring() - if not piecefield: - return False - - row["pieces"] = len(piecefield) - row["pieces_downloaded"] = piecefield.count("1") - row["downloaded_percent"] = 100 * row["pieces_downloaded"] / row["pieces"] - row["bytes_downloaded"] = row["pieces_downloaded"] * file_info["piece_size"] - row["is_downloading"] = bool(next((key for key in site.bad_files if key.startswith(row["inner_path"])), False)) + if piecefield: + row["pieces"] = len(piecefield) + row["pieces_downloaded"] = piecefield.count("1") + row["downloaded_percent"] = 100 * row["pieces_downloaded"] / row["pieces"] + row["bytes_downloaded"] = row["pieces_downloaded"] * file_info["piece_size"] + row["is_downloading"] = bool(next((task for task in site.worker_manager.tasks if task["inner_path"].startswith(row["inner_path"])), False)) # Add leech / seed stats row["peer_seed"] = 0 @@ -66,16 +64,19 @@ class UiWebsocketPlugin(object): if not peer.time_piecefields_updated or sha512 not in peer.piecefields: continue peer_piecefield = peer.piecefields[sha512].tostring() - if peer_piecefield == "1" * row["pieces"]: + if not peer_piecefield: + continue + if peer_piecefield == "1" * len(peer_piecefield): row["peer_seed"] += 1 else: row["peer_leech"] += 1 # Add myself - if row["pieces_downloaded"] == row["pieces"]: - row["peer_seed"] += 1 - else: - row["peer_leech"] += 1 + if piecefield: + if row["pieces_downloaded"] == row["pieces"]: + row["peer_seed"] += 1 + else: + row["peer_leech"] += 1 return True diff --git a/src/Config.py b/src/Config.py index a9d016bf..3008ebf6 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.0" - self.rev = 3095 + self.rev = 3097 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 136a0fbe28f8e5416beb5b50e19e94f1c4f78e54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=A0?= Date: Sat, 7 Oct 2017 20:16:20 +0200 Subject: [PATCH 0372/2570] Add Slovenian translate. --- src/Translate/languages/sl.json | 51 +++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/Translate/languages/sl.json diff --git a/src/Translate/languages/sl.json b/src/Translate/languages/sl.json new file mode 100644 index 00000000..fd6e6da3 --- /dev/null +++ b/src/Translate/languages/sl.json @@ -0,0 +1,51 @@ +{ + "Congratulation, your port {0} is opened.
You are full member of ZeroNet network!": "Čestitke, vaša vrata {0} so odprta.
Postali ste polnopravni član ZeroNet omrežja!", + "Tor mode active, every connection using Onion route.": "Način Tor aktiven.", + "Successfully started Tor onion hidden services.": "Storitve Tor uspešno zagnane.", + "Unable to start hidden services, please check your config.": "Ni bilo mogoče zagnati Tor storitev. Preverite nastavitve.", + "For faster connections open {0} port on your router.": "Za hitrejše povezave na svojem usmerjevalniku odprite vrata {0}.", + "Your connection is restricted. Please, open {0} port on your router": "Vaša povezava je omejena. Na svojem usmerjevalniku odprite vrata {0}", + "or configure Tor to become full member of ZeroNet network.": "ali nastavite Tor, da postanete polnopravni član ZeroNet omrežja.", + + "Select account you want to use in this site:": "Izberite račun, ki ga želite uporabiti na tem spletnem mestu:", + "currently selected": "trenutno izbrano", + "Unique to site": "Edinstven za spletno mesto", + + "Content signing failed": "Podpisovanje vsebine ni uspelo", + "Content publish queued for {0:.0f} seconds.": "Objava vsebine na čakanju za {0:.0f} sekund.", + "Content published to {0} peers.": "Vsebina objavljena na {0} povezavah.", + "No peers found, but your content is ready to access.": "Ni nobenih povezav, vendar je vaša vsebina pripravljena za dostop.", + "Your network connection is restricted. Please, open {0} port": "Vaša povezava je omejena. Prosimo, odprite vrata {0}", + "on your router to make your site accessible for everyone.": "na vašem usmerjevalniku, da bo vaše spletno mesto dostopno za vse.", + "Content publish failed.": "Objavljanje vsebine ni uspelo.", + "This file still in sync, if you write it now, then the previous content may be lost.": "Ta datoteka se še vedno sinhronizira. Če jo uredite zdaj, se lahko zgodi, da bo prejšnja vsebina izgubljena.", + "Write content anyway": "Vseeno uredi vsebino", + "New certificate added:": "Dodano novo potrdilo:", + "You current certificate:": "Trenutno potrdilo:", + "Change it to {auth_type}/{auth_user_name}@{domain}": "Spremenite ga na {auth_type}/{auth_user_name}@{domain}", + "Certificate changed to: {auth_type}/{auth_user_name}@{domain}.": "Potrdilo spremenjeno na: {auth_type}/{auth_user_name}@{domain}.", + "Site cloned": "Stran klonirana", + + "You have successfully changed the web interface's language!": "Uspešno ste spremenili jezik spletnega vmesnika!", + "Due to the browser's caching, the full transformation could take some minute.": "Zaradi predpomnjenja brskalnika lahko popolna preobrazba traja nekaj minut.", + + "Connection with UiServer Websocket was lost. Reconnecting...": "Povezava z UiServer Websocket je bila izgubljena. Ponovno povezovanje ...", + "Connection with UiServer Websocket recovered.": "Povezava z UiServer Websocket je vzpostavljena.", + "UiServer Websocket error, please reload the page.": "Napaka UiServer Websocket. Prosimo osvežite stran.", + "   Connecting...": "   Povezovanje ...", + "Site size: ": "Velikost strani: ", + "MB is larger than default allowed ": "MB je večja od dovoljenih", + "Open site and set size limit to \" + site_info.next_size_limit + \"MB": "Odpri to stran in nastavi omejitev na \" + site_info.next_size_limit + \"MB", + " files needs to be downloaded": " datotek mora biti prenešenih", + " downloaded": " preneseno", + " download failed": " prenos ni uspel", + "Peers found: ": "Najdene povezave: ", + "No peers found": "Ni najdenih povezav", + "Running out of size limit (": "Zmanjkuje dovoljenega prostora (", + "Set limit to \" + site_info.next_size_limit + \"MB": "Nastavi omejitev na \" + site_info.next_size_limit + \"MB", + "Site size limit changed to {0}MB": "Omejitev strani nastavljena na{0} MB", + " New version of this page has just released.
Reload to see the modified content.": " Ravnokar je bila objavljena nova različica te strani.
Osvežite jo, da boste videli novo vsebino.", + "This site requests permission:": "Ta stran zahteva dovoljenja:", + "Grant": "Dovoli" + +} From 7ba3d86af47f3b699e27fad1b98092485e2d5b47 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 10 Oct 2017 14:58:13 +0200 Subject: [PATCH 0373/2570] Ignore invalid updatePiecefields response --- plugins/Bigfile/BigfilePlugin.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index d79de803..4eebcb95 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -9,6 +9,7 @@ import math import msgpack from Plugin import PluginManager +from Debug import Debug from Crypt import CryptHash from lib import merkletools from util import helper @@ -602,8 +603,11 @@ class PeerPlugin(object): return False self.piecefields = collections.defaultdict(BigfilePiecefieldPacked) - for sha512, piecefield_packed in res["piecefields_packed"].iteritems(): - self.piecefields[sha512].unpack(piecefield_packed) + try: + for sha512, piecefield_packed in res["piecefields_packed"].iteritems(): + self.piecefields[sha512].unpack(piecefield_packed) + except Exception as err: + self.log("Invalid updatePiecefields response: %s" % Debug.formatException(err)) return self.piecefields From 3030e00b218af0ab5a32df912608b2949e7f2470 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 10 Oct 2017 14:58:43 +0200 Subject: [PATCH 0374/2570] Fix bigfile display if no piecemap info present --- plugins/Bigfile/BigfilePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index 4eebcb95..f9537cb5 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -379,7 +379,7 @@ class SiteStoragePlugin(object): def openBigfile(self, inner_path, prebuffer=0): file_info = self.site.content_manager.getFileInfo(inner_path) - if "piecemap" not in file_info: # It's not a big file + if file_info and "piecemap" not in file_info: # It's not a big file return False self.site.needFile(inner_path, blocking=False) # Download piecemap From de28643e20e3cc863a6db6b6950581d549f16fe7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 10 Oct 2017 14:58:52 +0200 Subject: [PATCH 0375/2570] Rev3098 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 3008ebf6..9c17c0d7 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.0" - self.rev = 3097 + self.rev = 3098 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 8b3ff6454fae08b12790a773e6c425b213622c18 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 11 Oct 2017 14:22:23 +0200 Subject: [PATCH 0376/2570] Limit connections to 500, backlog to 100 --- src/Connection/ConnectionServer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index f3a97ca5..f34c69dc 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -54,9 +54,9 @@ class ConnectionServer: sys.exit(0) if port: # Listen server on a port - self.pool = Pool(1000) # do not accept more than 1000 connections + self.pool = Pool(500) # do not accept more than 500 connections self.stream_server = StreamServer( - (ip.replace("*", "0.0.0.0"), port), self.handleIncomingConnection, spawn=self.pool, backlog=500 + (ip.replace("*", "0.0.0.0"), port), self.handleIncomingConnection, spawn=self.pool, backlog=100 ) if request_handler: self.handleRequest = request_handler From 68ea4d02cb6440d1afcf49a93638ca54a84ff980 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 11 Oct 2017 14:29:54 +0200 Subject: [PATCH 0377/2570] Don't reset bad file counter after restart, but limit to 20 --- 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 ed263295..a10f642d 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -92,9 +92,9 @@ class Site(object): settings["optional_downloaded"] = 0 self.bad_files = settings["cache"].get("bad_files", {}) settings["cache"]["bad_files"] = {} - # Reset tries + # Give it minimum 10 tries after restart for inner_path in self.bad_files: - self.bad_files[inner_path] = 1 + self.bad_files[inner_path] = min(self.bad_files[inner_path], 20) else: self.settings = { "own": False, "serving": True, "permissions": [], From 4a75d9d4f592802cdd311ed3b3ddba54faf8815f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 11 Oct 2017 14:30:12 +0200 Subject: [PATCH 0378/2570] Rev3099 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 9c17c0d7..f1267ad1 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.0" - self.rev = 3098 + self.rev = 3099 self.argv = argv self.action = None self.config_file = "zeronet.conf" From b322b750c282e7679e1e032c67f97c97b1f6fe23 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 13 Oct 2017 01:15:21 +0200 Subject: [PATCH 0379/2570] Don't load empty piecefields from sites.json --- plugins/Bigfile/BigfilePlugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index f9537cb5..47e192c6 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -332,7 +332,8 @@ class SiteStoragePlugin(object): self.piecefields = collections.defaultdict(BigfilePiecefield) if "piecefields" in self.site.settings.get("cache", {}): for sha512, piecefield_packed in self.site.settings["cache"].get("piecefields").iteritems(): - self.piecefields[sha512].unpack(piecefield_packed.decode("base64")) + if piecefield_packed: + self.piecefields[sha512].unpack(piecefield_packed.decode("base64")) self.site.settings["cache"]["piecefields"] = {} def createSparseFile(self, inner_path, size, sha512=None): From 046877599e756a18a76c86491ef230c0e775b25e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 13 Oct 2017 01:15:43 +0200 Subject: [PATCH 0380/2570] Don't create empty piecefield for every optional file --- plugins/OptionalManager/UiWebsocketPlugin.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/OptionalManager/UiWebsocketPlugin.py b/plugins/OptionalManager/UiWebsocketPlugin.py index 89e51eac..f57ed4bf 100644 --- a/plugins/OptionalManager/UiWebsocketPlugin.py +++ b/plugins/OptionalManager/UiWebsocketPlugin.py @@ -48,7 +48,10 @@ class UiWebsocketPlugin(object): return False sha512 = file_info["sha512"] - piecefield = site.storage.piecefields[sha512].tostring() + if sha512 in site.storage.piecefields: + piecefield = site.storage.piecefields[sha512].tostring() + else: + piecefield = None if piecefield: row["pieces"] = len(piecefield) From ef2f1d56d4f07af381f16d4172bb8253e504ad1a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 13 Oct 2017 01:16:11 +0200 Subject: [PATCH 0381/2570] Display short encryption for connections to reduce with --- plugins/Stats/StatsPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py index e9c420e6..c1108854 100644 --- a/plugins/Stats/StatsPlugin.py +++ b/plugins/Stats/StatsPlugin.py @@ -105,7 +105,7 @@ class UiRequestPlugin(object): ("%s", connection.type), ("%s:%s", (connection.ip, connection.port)), ("%s", connection.handshake.get("port_opened")), - ("%s", (connection.crypt, cipher)), + ("%s", (cipher, connection.crypt)), ("%6.3f", connection.last_ping_delay), ("%s", connection.incomplete_buff_recv), ("%s", connection.bad_actions), From 3c8867bf7225ad76cdc4b103a4d59e9b71e46c6b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 13 Oct 2017 01:16:44 +0200 Subject: [PATCH 0382/2570] Also display last received command for connection --- plugins/Stats/StatsPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py index c1108854..09f70335 100644 --- a/plugins/Stats/StatsPlugin.py +++ b/plugins/Stats/StatsPlugin.py @@ -115,7 +115,7 @@ class UiRequestPlugin(object): ("%.3f", connection.cpu_time), ("%.0fkB", connection.bytes_sent / 1024), ("%.0fkB", connection.bytes_recv / 1024), - ("%s", connection.last_cmd), + ("%s", (connection.last_cmd_recv, connection.last_cmd_sent)), ("%s", connection.waiting_requests.keys()), ("%s r%s", (connection.handshake.get("version"), connection.handshake.get("rev", "?"))), ("%s", connection.sites) From c8eb93db076a0d817db39841fdd7271a4f788212 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 13 Oct 2017 01:17:07 +0200 Subject: [PATCH 0383/2570] Bigfile stats --- plugins/Stats/StatsPlugin.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py index 09f70335..2908f464 100644 --- a/plugins/Stats/StatsPlugin.py +++ b/plugins/Stats/StatsPlugin.py @@ -172,6 +172,30 @@ class UiRequestPlugin(object): yield "
" yield "" + # Big files + yield "

Big files:
" + for site in self.server.sites.values(): + if not site.settings.get("has_bigfile"): + continue + bigfiles = {} + yield """%s
""" % (site.address, site.address) + for peer in site.peers.values(): + if not peer.time_piecefields_updated: + continue + for sha512, piecefield in peer.piecefields.iteritems(): + if sha512 not in bigfiles: + bigfiles[sha512] = [] + bigfiles[sha512].append(peer) + + yield "" + # No more if not in debug mode if not config.debug: raise StopIteration From 9484a278017049d8e6085c67dd07c6b828b77ef6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 13 Oct 2017 01:17:27 +0200 Subject: [PATCH 0384/2570] Sent and Received command statistics --- plugins/Stats/StatsPlugin.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py index 2908f464..0dfdfd10 100644 --- a/plugins/Stats/StatsPlugin.py +++ b/plugins/Stats/StatsPlugin.py @@ -196,6 +196,24 @@ class UiRequestPlugin(object): yield "" yield "
" + # Cmd stats + yield "
" + yield "

Sent commands:
" + yield "" + for stat_key, stat in sorted(main.file_server.stat_sent.items(), lambda a, b: cmp(a[1]["bytes"], b[1]["bytes"]), reverse=True): + yield "" % (stat_key, stat["num"], stat["bytes"] / 1024) + yield "
%sx %s =%.0fkB
" + yield "
" + + yield "
" + yield "

Received commands:
" + yield "" + for stat_key, stat in sorted(main.file_server.stat_recv.items(), lambda a, b: cmp(a[1]["bytes"], b[1]["bytes"]), reverse=True): + yield "" % (stat_key, stat["num"], stat["bytes"] / 1024) + yield "
%sx %s =%.0fkB
" + yield "
" + yield "
" + # No more if not in debug mode if not config.debug: raise StopIteration From 24c159604893889985e52a533c41d51b73435db9 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 13 Oct 2017 01:19:34 +0200 Subject: [PATCH 0385/2570] Keep track last received command for connection --- src/Connection/Connection.py | 8 +++++--- src/Connection/ConnectionServer.py | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 0fe24a66..456c36f0 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -20,7 +20,7 @@ class Connection(object): "sock", "sock_wrapped", "ip", "port", "cert_pin", "target_onion", "id", "protocol", "type", "server", "unpacker", "req_id", "handshake", "crypt", "connected", "event_connected", "closed", "start_time", "last_recv_time", "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", "bad_actions", "sites", "name", "updateName", "waiting_requests", "waiting_streams" + "last_ping_delay", "last_req_time", "last_cmd_sent", "last_cmd_recv", "bad_actions", "sites", "name", "updateName", "waiting_requests", "waiting_streams" ) def __init__(self, server, ip, port, sock=None, target_onion=None): @@ -58,7 +58,8 @@ class Connection(object): self.bytes_sent = 0 self.last_ping_delay = None self.last_req_time = 0 - self.last_cmd = None + self.last_cmd_sent = None + self.last_cmd_recv = None self.bad_actions = 0 self.sites = 0 self.cpu_time = 0.0 @@ -299,6 +300,7 @@ class Connection(object): cmd = None self.last_message_time = time.time() + self.last_cmd_recv = cmd if cmd == "response": # New style response if message["to"] in self.waiting_requests: if self.last_send_time and len(self.waiting_requests) == 1: @@ -422,7 +424,7 @@ class Connection(object): return False self.last_req_time = time.time() - self.last_cmd = cmd + self.last_cmd_sent = cmd self.req_id += 1 data = {"cmd": cmd, "req_id": self.req_id, "params": params} event = gevent.event.AsyncResult() # Create new event for response diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index f34c69dc..db921f8c 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -184,7 +184,7 @@ class ConnectionServer: del connection.unpacker connection.unpacker = None - elif connection.last_cmd == "announce" and idle > 20: # Bootstrapper connection close after 20 sec + elif connection.last_cmd_sent == "announce" and idle > 20: # Bootstrapper connection close after 20 sec connection.close("[Cleanup] Tracker connection: %s" % idle) if idle > 60 * 60: @@ -208,7 +208,7 @@ class ConnectionServer: elif idle > 10 and connection.waiting_requests and time.time() - connection.last_send_time > 10: # Sent command and no response in 10 sec connection.close( - "[Cleanup] Command %s timeout: %.3fs" % (connection.last_cmd, time.time() - connection.last_send_time) + "[Cleanup] Command %s timeout: %.3fs" % (connection.last_cmd_sent, time.time() - connection.last_send_time) ) elif idle < 60 and connection.bad_actions > 40: From de360a8585a3d438f21afcb4514faf76933fff68 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 13 Oct 2017 01:21:05 +0200 Subject: [PATCH 0386/2570] Received and sent connection command statistics --- src/Connection/Connection.py | 40 ++++++++++++++++++++++++------ src/Connection/ConnectionServer.py | 3 +++ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 456c36f0..c0b74c31 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -153,6 +153,7 @@ class Connection(object): self.updateName() self.connected = True buff_len = 0 + req_len = 0 self.unpacker = msgpack.fallback.Unpacker() # Due memory problems of C version try: @@ -167,6 +168,7 @@ class Connection(object): self.incomplete_buff_recv += 1 self.bytes_recv += buff_len self.server.bytes_recv += buff_len + req_len += buff_len if not self.unpacker: self.unpacker = msgpack.fallback.Unpacker() @@ -174,7 +176,19 @@ class Connection(object): buff = None for message in self.unpacker: try: + # Stats self.incomplete_buff_recv = 0 + stat_key = message.get("cmd", "unknown") + if stat_key == "response" and "to" in message: + cmd_sent = self.waiting_requests.get(message["to"], {"cmd": "unknown"})["cmd"] + stat_key = "response: %s" % cmd_sent + self.server.stat_recv[stat_key]["bytes"] += req_len + self.server.stat_recv[stat_key]["num"] += 1 + if "stream_bytes" in message: + self.server.stat_recv[stat_key]["bytes"] += message["stream_bytes"] + req_len = 0 + + # Handle message if "stream_bytes" in message: self.handleStream(message) else: @@ -227,7 +241,7 @@ class Connection(object): self.log("End stream %s" % message["to"]) self.incomplete_buff_recv = 0 - self.waiting_requests[message["to"]].set(message) # Set the response to event + self.waiting_requests[message["to"]]["evt"].set(message) # Set the response to event del self.waiting_streams[message["to"]] del self.waiting_requests[message["to"]] @@ -306,7 +320,7 @@ class Connection(object): if self.last_send_time and len(self.waiting_requests) == 1: ping = time.time() - self.last_send_time self.last_ping_delay = ping - self.waiting_requests[message["to"]].set(message) # Set the response to event + self.waiting_requests[message["to"]]["evt"].set(message) # Set the response to event del self.waiting_requests[message["to"]] elif message["to"] == 0: # Other peers handshake ping = time.time() - self.start_time @@ -329,7 +343,7 @@ class Connection(object): self.setHandshake(message) else: self.log("Unknown response: %s" % message) - elif cmd: # Handhsake request + elif cmd: if cmd == "handshake": self.handleHandshake(message) else: @@ -338,7 +352,7 @@ class Connection(object): self.log("Unknown message, waiting: %s" % self.waiting_requests.keys()) if self.waiting_requests: last_req_id = min(self.waiting_requests.keys()) # Get the oldest waiting request and set it true - self.waiting_requests[last_req_id].set(message) + self.waiting_requests[last_req_id]["evt"].set(message) del self.waiting_requests[last_req_id] # Remove from waiting request # Incoming handshake set request @@ -380,17 +394,25 @@ class Connection(object): return False try: + stat_key = message.get("cmd", "unknown") + if stat_key == "response": + stat_key = "response: %s" % self.last_cmd_recv + + self.server.stat_sent[stat_key]["num"] += 1 if streaming: with self.send_lock: bytes_sent = StreamingMsgpack.stream(message, self.sock.sendall) message = None self.bytes_sent += bytes_sent self.server.bytes_sent += bytes_sent + self.server.stat_sent[stat_key]["bytes"] += bytes_sent + message = None else: data = msgpack.packb(message) - message = None self.bytes_sent += len(data) self.server.bytes_sent += len(data) + self.server.stat_sent[stat_key]["bytes"] += len(data) + message = None with self.send_lock: self.sock.sendall(data) except Exception, err: @@ -414,13 +436,15 @@ class Connection(object): break self.bytes_sent += read_bytes self.server.bytes_sent += read_bytes + self.server.stat_sent["raw_file"]["num"] += 1 + self.server.stat_sent["raw_file"]["bytes"] += bytes_sent return True # Create and send a request to peer def request(self, cmd, params={}, stream_to=None): # Last command sent more than 10 sec ago, timeout if self.waiting_requests and self.protocol == "v2" and time.time() - max(self.last_req_time, self.last_recv_time) > 10: - self.close("Request %s timeout: %.3fs" % (self.last_cmd, time.time() - self.last_send_time)) + self.close("Request %s timeout: %.3fs" % (self.last_cmd_sent, time.time() - self.last_send_time)) return False self.last_req_time = time.time() @@ -428,7 +452,7 @@ class Connection(object): self.req_id += 1 data = {"cmd": cmd, "req_id": self.req_id, "params": params} event = gevent.event.AsyncResult() # Create new event for response - self.waiting_requests[self.req_id] = event + 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 @@ -463,7 +487,7 @@ class Connection(object): (reason, len(self.waiting_requests), self.sites, self.incomplete_buff_recv) ) for request in self.waiting_requests.values(): # Mark pending requests failed - request.set(False) + request["evt"].set(False) self.waiting_requests = {} self.waiting_streams = {} self.sites = 0 diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index db921f8c..fe81a5a9 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -6,6 +6,7 @@ import gevent import msgpack from gevent.server import StreamServer from gevent.pool import Pool +from collections import defaultdict from Debug import Debug from Connection import Connection @@ -39,6 +40,8 @@ class ConnectionServer: self.running = True self.thread_checker = gevent.spawn(self.checkConnections) + self.stat_recv = defaultdict(lambda: defaultdict(int)) + self.stat_sent = defaultdict(lambda: defaultdict(int)) self.bytes_recv = 0 self.bytes_sent = 0 From d32303de5775c3ad42cc32e214b82d7e0ee2cdad Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 13 Oct 2017 01:21:45 +0200 Subject: [PATCH 0387/2570] Increase buffer size for file streaming --- src/Connection/Connection.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index c0b74c31..75677c0b 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -222,7 +222,7 @@ class Connection(object): while 1: if read_bytes <= 0: break - buff = self.sock.recv(16 * 1024) + buff = self.sock.recv(64 * 1024) if not buff: break buff_len = len(buff) @@ -402,7 +402,6 @@ class Connection(object): if streaming: with self.send_lock: bytes_sent = StreamingMsgpack.stream(message, self.sock.sendall) - message = None self.bytes_sent += bytes_sent self.server.bytes_sent += bytes_sent self.server.stat_sent[stat_key]["bytes"] += bytes_sent From 19c335e39a8c8477eeddfa6e665bc9ef0fd4afd6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 13 Oct 2017 01:22:16 +0200 Subject: [PATCH 0388/2570] Fix bytes sent stats --- src/Connection/Connection.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 75677c0b..a0754d9f 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -420,21 +420,22 @@ class Connection(object): self.last_sent_time = time.time() return True - # Stream raw file to connection + # Stream file to connection without msgpacking def sendRawfile(self, file, read_bytes): buff = 64 * 1024 bytes_left = read_bytes + bytes_sent = 0 while True: self.last_send_time = time.time() + data = file.read(min(bytes_left, buff)) + bytes_sent += len(data) with self.send_lock: - self.sock.sendall( - file.read(min(bytes_left, buff)) - ) + self.sock.sendall(data) bytes_left -= buff if bytes_left <= 0: break - self.bytes_sent += read_bytes - self.server.bytes_sent += read_bytes + self.bytes_sent += bytes_sent + self.server.bytes_sent += bytes_sent self.server.stat_sent["raw_file"]["num"] += 1 self.server.stat_sent["raw_file"]["bytes"] += bytes_sent return True From 60af54a17ec7c7495176c59b0d4d2a48e569548c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 13 Oct 2017 01:23:09 +0200 Subject: [PATCH 0389/2570] Start find optional files if no peers to try --- src/Worker/WorkerManager.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 47f62e08..e78caffa 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -91,6 +91,9 @@ class WorkerManager(object): peers_try = [peer for peer in task["peers"] if peer not in task["failed"]] if peers_try: self.startWorkers(peers_try, force_num=5) + else: + self.startFindOptional(find_more=True) + else: self.startFindOptional(find_more=True) else: if task["peers"]: # Release the peer lock @@ -373,6 +376,8 @@ class WorkerManager(object): if time_tasks != self.time_task_added: # New task added since start self.log.debug("New task since start, restarting...") gevent.spawn_later(0.1, self.startFindOptional) + else: + self.log.debug("startFindOptional ended") # Stop all worker def stopWorkers(self): From 027ee83c6fd911779bea195746cb857faf8b63ce Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 13 Oct 2017 01:23:26 +0200 Subject: [PATCH 0390/2570] Wait 0.1s for new tasks before shut down worker --- src/Worker/Worker.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Worker/Worker.py b/src/Worker/Worker.py index be04dca7..d0f1b661 100644 --- a/src/Worker/Worker.py +++ b/src/Worker/Worker.py @@ -28,9 +28,12 @@ class Worker(object): while self.running: # Try to pickup free file download task task = self.manager.getTask(self.peer) - if not task: # Die, no more task - self.manager.log.debug("%s: No task found, stopping" % self.key) - break + if not task: # No more task + time.sleep(0.1) # Wait a bit for new tasks + task = self.manager.getTask(self.peer) + if not task: # Still no task, stop it + self.manager.log.debug("%s: No task found, stopping" % self.key) + break if not task["time_started"]: task["time_started"] = time.time() # Task started now From 65eff6b6d93622f95b136fcff772091712888dae Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 13 Oct 2017 01:23:43 +0200 Subject: [PATCH 0391/2570] Rev3102 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index f1267ad1..dc9cdf02 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.0" - self.rev = 3099 + self.rev = 3102 self.argv = argv self.action = None self.config_file = "zeronet.conf" From a66b71fb9c328977cff594abf015297c19866c68 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 13 Oct 2017 11:27:45 +0200 Subject: [PATCH 0392/2570] Rev3104, Fix bigfile signing via command line --- plugins/Bigfile/BigfilePlugin.py | 2 +- src/Config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index 47e192c6..f13b8fbe 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -242,7 +242,7 @@ class ContentManagerPlugin(object): 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"): + 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) diff --git a/src/Config.py b/src/Config.py index dc9cdf02..27e4834b 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.0" - self.rev = 3102 + self.rev = 3104 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 3f52f78af27d0298f80ced1c79d041c53a137e34 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 14 Oct 2017 12:51:31 +0200 Subject: [PATCH 0393/2570] Rev3105, Change tracker domain due GFW banned zeronet.io domain --- src/Config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index 27e4834b..ef56152e 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.0" - self.rev = 3104 + self.rev = 3105 self.argv = argv self.action = None self.config_file = "zeronet.conf" @@ -34,7 +34,7 @@ class Config(object): def createArguments(self): trackers = [ "zero://boot3rdez4rzn36x.onion:15441", - "zero://boot.zeronet.io#f36ca555bee6ba216b14d10f38c16f7769ff064e0e37d887603548cc2e64191d:15441", + "zero://zero.booth.moe#f36ca555bee6ba216b14d10f38c16f7769ff064e0e37d887603548cc2e64191d:15441", "udp://tracker.coppersurfer.tk:6969", "udp://tracker.leechers-paradise.org:6969", "udp://9.rarbg.com:2710", From 5a7a80f932bb02b75087b33783ca05d858dfff09 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Sat, 14 Oct 2017 19:40:44 -0700 Subject: [PATCH 0394/2570] Cross off big file support on README :) Got that one squared away. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7749f2ca..79780c7a 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ set `ENABLE_TOR` environment variable to `true` (Default: `false`). E.g.: ## Current limitations -* No torrent-like file splitting for big file support +* ~~No torrent-like file splitting for big file support~~ (big file support added) * ~~No more anonymous than Bittorrent~~ (built-in full Tor support added) * File transactions are not compressed ~~or encrypted yet~~ (TLS encryption added) * No private sites From 807dc866e5e460f7322355f9bd7e9f1defba5ace Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Oct 2017 01:43:51 +0200 Subject: [PATCH 0395/2570] Rev3106, Cancel other part downloads when delete optional file --- plugins/Bigfile/BigfilePlugin.py | 5 +++++ src/Config.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index f13b8fbe..87c94cf8 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -322,6 +322,11 @@ class ContentManagerPlugin(object): 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 self.site.bad_files.keys(): + if key.startswith(inner_path + "|"): + del self.site.bad_files[key] return super(ContentManagerPlugin, self).optionalRemove(inner_path, hash, size) diff --git a/src/Config.py b/src/Config.py index ef56152e..038d5424 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.0" - self.rev = 3105 + self.rev = 3106 self.argv = argv self.action = None self.config_file = "zeronet.conf" From c40f0c6919619249925dd631fec6fc95ad260f14 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Oct 2017 15:11:19 +0200 Subject: [PATCH 0396/2570] Fix uploading bigfile to non-user directory --- plugins/Bigfile/BigfilePlugin.py | 8 ++++---- src/Content/ContentManager.py | 12 ++++++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index 87c94cf8..60bdb4a8 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -126,7 +126,7 @@ class UiWebsocketPlugin(object): 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) + 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):] @@ -150,13 +150,13 @@ class UiWebsocketPlugin(object): @PluginManager.registerTo("ContentManager") class ContentManagerPlugin(object): - def getFileInfo(self, inner_path): + def getFileInfo(self, inner_path, *args, **kwargs): if "|" not in inner_path: - return super(ContentManagerPlugin, self).getFileInfo(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) + file_info = super(ContentManagerPlugin, self).getFileInfo(inner_path, *args, **kwargs) return file_info def readFile(self, file_in, size, buff_size=1024 * 64): diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 61ae97b5..0813e2af 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -302,7 +302,7 @@ class ContentManager(object): # Find the file info line from self.contents # Return: { "sha512": "c29d73d...21f518", "size": 41 , "content_inner_path": "content.json"} - def getFileInfo(self, inner_path): + def getFileInfo(self, inner_path, new_file=False): dirs = inner_path.split("/") # Parent dirs of content.json inner_path_parts = [dirs.pop()] # Filename relative to content.json while True: @@ -338,6 +338,14 @@ class ContentManager(object): else: back["content_inner_path"] = content_inner_path_dir + "content.json" back["optional"] = None + back["relative_path"] = "/".join(inner_path_parts) + return back + + if new_file and content: + back = {} + back["content_inner_path"] = content_inner_path + back["relative_path"] = "/".join(inner_path_parts) + back["optional"] = None return back # No inner path in this dir, lets try the parent dir @@ -874,7 +882,7 @@ class ContentManager(object): else: # Check using sha512 hash file_info = self.getFileInfo(inner_path) if file_info: - if CryptHash.sha512sum(file) != file_info["sha512"]: + if CryptHash.sha512sum(file) != file_info.get("sha512", ""): raise VerifyError("Invalid hash") if file_info.get("size", 0) != file.tell(): From 75cf8bbb0a0a6623d314f41280806060a984c5bb Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Oct 2017 15:11:43 +0200 Subject: [PATCH 0397/2570] Change arument order of siteSign API call --- 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 8bb1bdde..b97585e5 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -343,7 +343,7 @@ class UiWebsocket(object): self.response(to, ret) # Sign content.json - def actionSiteSign(self, to, privatekey=None, inner_path="content.json", response_ok=True, update_changed_files=False, remove_missing_optional=False): + def actionSiteSign(self, to, privatekey=None, inner_path="content.json", remove_missing_optional=False, update_changed_files=False, response_ok=True): self.log.debug("Signing: %s" % inner_path) site = self.site extend = {} # Extended info for signing From 95d9d4ee9aacaa045030b8f8f4411086082c0569 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Oct 2017 15:12:17 +0200 Subject: [PATCH 0398/2570] Add hashid to optional files stat --- plugins/Stats/StatsPlugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py index 0dfdfd10..9a2db5a5 100644 --- a/plugins/Stats/StatsPlugin.py +++ b/plugins/Stats/StatsPlugin.py @@ -189,7 +189,7 @@ class UiRequestPlugin(object): yield "" - yield "
" + yield "
" yield "

Received commands:
" yield "" for stat_key, stat in sorted(main.file_server.stat_recv.items(), lambda a, b: cmp(a[1]["bytes"], b[1]["bytes"]), reverse=True): - yield "" % (stat_key, stat["num"], stat["bytes"] / 1024) + yield "" % (stat_key, stat["num"], stat["bytes"] / 1024) yield "
%sx %s =%.0fkB
%sx %s =%.0fkB
" yield "
" yield "
" From b40ef0910807f0b531e0f14ec812f16d0500886a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Oct 2017 15:12:34 +0200 Subject: [PATCH 0399/2570] Rev3110 --- src/Config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 038d5424..5fd6cbb8 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,6 @@ class Config(object): def __init__(self, argv): self.version = "0.6.0" - self.rev = 3106 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 9551d11f5c3f1ea7a4efe1dcefca663be60d334d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Oct 2017 15:13:02 +0200 Subject: [PATCH 0400/2570] Rev3110 version update --- src/Config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Config.py b/src/Config.py index 5fd6cbb8..d8b30169 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,6 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.0" + self.rev = 3110 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 4efcfa8bf49a1d9ce9b6b8095b4cd92d30c70d74 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Oct 2017 20:26:41 +0200 Subject: [PATCH 0401/2570] Rev3112, Allow user rules based on auth address --- src/Config.py | 2 +- src/Content/ContentManager.py | 6 +++- src/Test/TestContentUser.py | 54 +++++++++++++++++++++++++++++++++-- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/src/Config.py b/src/Config.py index d8b30169..ad7d9a97 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.0" - self.rev = 3110 + self.rev = 3112 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 0813e2af..bde7756c 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -410,7 +410,11 @@ class ContentManager(object): user_urn = "n-a/n-a" cert_user_id = "n-a" - rules = copy.copy(user_contents["permissions"].get(cert_user_id, {})) # Default rules by username + if user_address in user_contents["permissions"]: + rules = copy.copy(user_contents["permissions"].get(user_address, {})) # Default rules based on address + else: + rules = copy.copy(user_contents["permissions"].get(cert_user_id, {})) # Default rules based on username + if rules is False: banned = True rules = {} diff --git a/src/Test/TestContentUser.py b/src/Test/TestContentUser.py index 92a3e48f..a53ebe17 100644 --- a/src/Test/TestContentUser.py +++ b/src/Test/TestContentUser.py @@ -8,7 +8,7 @@ from Content.ContentManager import VerifyError, SignError @pytest.mark.usefixtures("resetSettings") -class TestUserContent: +class TestContentUser: def testSigners(self, site): # File info for not existing user file file_info = site.content_manager.getFileInfo("data/users/notexist/data.json") @@ -66,6 +66,55 @@ class TestUserContent: rules = site.content_manager.getRules("data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/content.json", user_content) assert "1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C" not in rules["signers"] + def testRulesAddress(self, site): + user_inner_path = "data/users/1CjfbrbwtP8Y2QjPy12vpTATkUT7oSiPQ9/content.json" + user_content = site.storage.loadJson(user_inner_path) + + rules = site.content_manager.getRules(user_inner_path, user_content) + assert rules["max_size"] == 10000 + assert "1CjfbrbwtP8Y2QjPy12vpTATkUT7oSiPQ9" in rules["signers"] + + users_content = site.content_manager.contents["data/users/content.json"] + + # Ban user based on address + users_content["user_contents"]["permissions"]["1CjfbrbwtP8Y2QjPy12vpTATkUT7oSiPQ9"] = False + rules = site.content_manager.getRules(user_inner_path, user_content) + assert "1CjfbrbwtP8Y2QjPy12vpTATkUT7oSiPQ9" not in rules["signers"] + + # Change max allowed size + users_content["user_contents"]["permissions"]["1CjfbrbwtP8Y2QjPy12vpTATkUT7oSiPQ9"] = {"max_size": 20000} + rules = site.content_manager.getRules(user_inner_path, user_content) + assert rules["max_size"] == 20000 + + def testVerifyAddress(self, site): + privatekey = "5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv" # For 1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT + user_inner_path = "data/users/1CjfbrbwtP8Y2QjPy12vpTATkUT7oSiPQ9/content.json" + data_dict = site.storage.loadJson(user_inner_path) + users_content = site.content_manager.contents["data/users/content.json"] + + data = StringIO(json.dumps(data_dict)) + assert site.content_manager.verifyFile(user_inner_path, data, ignore_same=False) + + # Test error on 15k data.json + data_dict["files"]["data.json"]["size"] = 1024 * 15 + del data_dict["signs"] # Remove signs before signing + data_dict["signs"] = { + "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), privatekey) + } + data = StringIO(json.dumps(data_dict)) + with pytest.raises(VerifyError) as err: + site.content_manager.verifyFile(user_inner_path, data, ignore_same=False) + assert "Content too large" in str(err) + + # Give more space based on address + users_content["user_contents"]["permissions"]["1CjfbrbwtP8Y2QjPy12vpTATkUT7oSiPQ9"] = {"max_size": 20000} + del data_dict["signs"] # Remove signs before signing + data_dict["signs"] = { + "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), privatekey) + } + data = StringIO(json.dumps(data_dict)) + assert site.content_manager.verifyFile(user_inner_path, data, ignore_same=False) + def testVerify(self, site): privatekey = "5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv" # For 1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT user_inner_path = "data/users/1CjfbrbwtP8Y2QjPy12vpTATkUT7oSiPQ9/content.json" @@ -125,7 +174,7 @@ class TestUserContent: del data_dict["files_optional"]["hello.exe"] # Reset # Includes not allowed in user content - data_dict["includes"] = { "other.json": { } } + data_dict["includes"] = {"other.json": {}} del data_dict["signs"] # Remove signs before signing data_dict["signs"] = { "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT": CryptBitcoin.sign(json.dumps(data_dict, sort_keys=True), privatekey) @@ -135,7 +184,6 @@ class TestUserContent: site.content_manager.verifyFile(user_inner_path, data, ignore_same=False) assert "Includes not allowed" in err - def testCert(self, site): # user_addr = "1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C" user_priv = "5Kk7FSA63FC2ViKmKLuBxk9gQkaQ5713hKq8LmFAf4cVeXh6K6A" From e3931e8892b1de2fdd2bca67ffdd7923739a4439 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Oct 2017 10:52:08 +0200 Subject: [PATCH 0402/2570] Changelog for 0.6.0 --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9663a32..9cc9fc61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,35 @@ +## ZeroNet 0.6.0 (2017-10-17) + +### Added + - New plugin: Big file support + - Automatic pinning on Big file download + - Enable TCP_NODELAY for supporting sockets + - actionOptionalFileList API command arguments to list non-downloaded files or only big files + - serverShowdirectory API command arguments to allow to display site's directory in OS file browser + - fileNeed API command to initialize optional file downloading + - wrapperGetAjaxKey API command to request nonce for AJAX request + - Json.gz support for database files + - P2P port checking (Thanks for grez911) + - `--download_optional auto` argument to enable automatic optional file downloading for newly added site + - Statistics for big files and protocol command requests on /Stats + - Allow to set user limitation based on auth_address + +### Changed + - More aggressive and frequent connection timeout checking + - Use out of msgpack context file streaming for files larger than 512KB + - Allow optional files workers over the worker limit + - Automatic redirection to wrapper on nonce_error + - Send websocket event on optional file deletion + - Optimize sites.json saving + - Enable faster C-based msgpack packer by default + - Major optimization on Bootstrapper plugin SQL queries + - Don't reset bad file counter on restart, to allow easier give up on unreachable files + - Incoming connection limit changed from 1000 to 500 to avoid reaching socket limit on Windows + - Changed tracker boot.zeronet.io domain, because zeronet.io got banned in some countries + +#### Fixed + - Sub-directories in user directories + ## ZeroNet 0.5.7 (2017-07-19) ### Added - New plugin: CORS to request read permission to other site's content From d50603f7224d182d2c17bdf212ecb099f213f2a9 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Oct 2017 17:02:56 +0200 Subject: [PATCH 0403/2570] Rev3114, Add details to permission requests --- plugins/MergerSite/MergerSitePlugin.py | 20 ++++++++++++++++++++ src/Config.py | 2 +- src/Ui/UiWebsocket.py | 6 ++++++ src/Ui/media/Wrapper.coffee | 7 ++++--- src/Ui/media/all.js | 16 +++++++++------- 5 files changed, 40 insertions(+), 11 deletions(-) diff --git a/plugins/MergerSite/MergerSitePlugin.py b/plugins/MergerSite/MergerSitePlugin.py index 49aeebe2..f030ce07 100644 --- a/plugins/MergerSite/MergerSitePlugin.py +++ b/plugins/MergerSite/MergerSitePlugin.py @@ -199,6 +199,26 @@ class UiWebsocketPlugin(object): 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:", "") + merged_sites = [] + for address, merged_type in merged_db.iteritems(): + 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 as err: + 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): diff --git a/src/Config.py b/src/Config.py index ad7d9a97..1ffa7939 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.0" - self.rev = 3112 + self.rev = 3114 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index b97585e5..20005731 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -734,6 +734,12 @@ class UiWebsocket(object): self.site.updateWebsocket(permission_removed=permission) self.response(to, "ok") + def actionPermissionDetails(self, to, permission): + if permission == "ADMIN": + self.response(to, _["Modify your client's configuration and access all site"] + " " + _["(Dangerous!)"] + "") + else: + self.response(to, "") + # Set certificate that used for authenticate user for site def actionCertSet(self, to, domain): self.user.setCert(self.site.address, domain) diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index 109ab57d..fbbe1e8c 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -197,9 +197,10 @@ class Wrapper actionPermissionAdd: (message) -> permission = message.params - @displayConfirm "This site requests permission:" + " #{@toHtmlSafe(permission)}", "Grant", => - @ws.cmd "permissionAdd", permission, => - @sendInner {"cmd": "response", "to": message.id, "result": "Granted"} + @ws.cmd "permissionDetails", permission, (permission_details) => + @displayConfirm "This site requests permission:" + " #{@toHtmlSafe(permission)}" + "
#{permission_details}", "Grant", => + @ws.cmd "permissionAdd", permission, => + @sendInner {"cmd": "response", "to": message.id, "result": "Granted"} actionNotification: (message) -> message.params = @toHtmlSafe(message.params) # Escape html diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index 1f347a57..8a5fa48b 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -1076,13 +1076,15 @@ jQuery.extend( jQuery.easing, Wrapper.prototype.actionPermissionAdd = function(message) { var permission; permission = message.params; - return this.displayConfirm("This site requests permission:" + (" " + (this.toHtmlSafe(permission)) + ""), "Grant", (function(_this) { - return function() { - return _this.ws.cmd("permissionAdd", permission, function() { - return _this.sendInner({ - "cmd": "response", - "to": message.id, - "result": "Granted" + return this.ws.cmd("permissionDetails", permission, (function(_this) { + return function(permission_details) { + return _this.displayConfirm("This site requests permission:" + (" " + (_this.toHtmlSafe(permission)) + "") + ("
" + permission_details + ""), "Grant", function() { + return _this.ws.cmd("permissionAdd", permission, function() { + return _this.sendInner({ + "cmd": "response", + "to": message.id, + "result": "Granted" + }); }); }); }; From 0f7ed0e04df458e3e71f123a9247438b3b5f4cbe Mon Sep 17 00:00:00 2001 From: Fuqiao Xue Date: Wed, 18 Oct 2017 12:53:10 +0800 Subject: [PATCH 0404/2570] Update README-zh-cn.md --- README-zh-cn.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README-zh-cn.md b/README-zh-cn.md index 0abff7a8..9145e2b6 100644 --- a/README-zh-cn.md +++ b/README-zh-cn.md @@ -82,7 +82,7 @@ * `wget https://github.com/HelloZeroNet/ZeroNet/archive/master.tar.gz` * `tar xvpfz master.tar.gz` * `cd ZeroNet-master` -* 执行 `python zeronet.py` 来启动 +* 执行 `python2 zeronet.py` 来启动 * 在你的浏览器中打开 http://127.0.0.1:43110/ ### [FreeBSD](https://www.freebsd.org/) @@ -97,7 +97,7 @@ * `vagrant up` * 通过 `vagrant ssh` 连接到 VM * `cd /vagrant` -* 运行 `python zeronet.py --ui_ip 0.0.0.0` +* 运行 `python2 zeronet.py --ui_ip 0.0.0.0` * 在你的浏览器中打开 http://127.0.0.1:43110/ ### [Docker](https://www.docker.com/) @@ -113,12 +113,12 @@ * `virtualenv env` * `source env/bin/activate` * `pip install msgpack-python gevent` -* `python zeronet.py` +* `python2 zeronet.py` * 在你的浏览器中打开 http://127.0.0.1:43110/ ## 现有限制 -* 没有类似于 BitTorrent 的文件拆分来支持大文件 +* ~~没有类似于 BitTorrent 的文件拆分来支持大文件~~ (已添加大文件支持) * ~~没有比 BitTorrent 更好的匿名性~~ (已添加内置的完整 Tor 支持) * 传输文件时没有压缩~~和加密~~ (已添加 TLS 支持) * 不支持私有站点 From ed965041e63de6c64ed5ae069bacedea0460d00a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Oct 2017 11:01:57 +0200 Subject: [PATCH 0405/2570] Report progress on GeoLite2 download --- plugins/Sidebar/SidebarPlugin.py | 17 +++++++++++------ src/Ui/media/Notifications.coffee | 2 +- src/Ui/media/all.js | 5 +++-- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index 7eb39e74..bc0f22e2 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -477,7 +477,7 @@ class UiWebsocketPlugin(object): from util import helper self.log.info("Downloading GeoLite2 City database...") - self.cmd("notification", ["geolite-info", _["Downloading GeoLite2 City database (one time only, ~20MB)..."], 0]) + self.cmd("progress", ["geolite-info", _["Downloading GeoLite2 City database (one time only, ~20MB)..."], 0]) db_urls = [ "https://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz", "https://raw.githubusercontent.com/texnikru/GeoLite2-Database/master/GeoLite2-City.mmdb.gz" @@ -486,13 +486,18 @@ class UiWebsocketPlugin(object): try: # Download response = helper.httpRequest(db_url) - + data_size = response.getheader('content-length') + data_recv = 0 data = StringIO.StringIO() 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) @@ -500,16 +505,16 @@ class UiWebsocketPlugin(object): with gzip.GzipFile(fileobj=data) as gzip_file: shutil.copyfileobj(gzip_file, open(db_path, "wb")) - self.cmd("notification", ["geolite-done", _["GeoLite2 City database downloaded!"], 5000]) + self.cmd("progress", ["geolite-info", _["GeoLite2 City database downloaded!"], 100]) time.sleep(2) # Wait for notify animation return True except Exception as err: self.log.error("Error downloading %s: %s" % (db_url, err)) pass - self.cmd("notification", [ - "geolite-error", + self.cmd("progress", [ + "geolite-info", _["GeoLite2 City database download error: {}!
Please download manually and unpack to data dir:
{}"].format(err, db_urls[0]), - 0 + -100 ]) def actionSidebarGetPeers(self, to): diff --git a/src/Ui/media/Notifications.coffee b/src/Ui/media/Notifications.coffee index 9f38921b..c06a95dc 100644 --- a/src/Ui/media/Notifications.coffee +++ b/src/Ui/media/Notifications.coffee @@ -13,7 +13,7 @@ class Notifications add: (id, type, body, timeout=0) -> - id = id.replace /[^A-Za-z0-9]/g, "" + id = id.replace /[^A-Za-z0-9-]/g, "" # Close notifications with same id for elem in $(".notification-#{id}") @close $(elem) diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index 8a5fa48b..58eff9b6 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -711,7 +711,7 @@ jQuery.extend( jQuery.easing, if (timeout == null) { timeout = 0; } - id = id.replace(/[^A-Za-z0-9]/g, ""); + id = id.replace(/[^A-Za-z0-9-]/g, ""); ref = $(".notification-" + id); for (i = 0, len = ref.length; i < len; i++) { elem = ref[i]; @@ -807,6 +807,7 @@ jQuery.extend( jQuery.easing, }).call(this); + /* ---- src/Ui/media/Wrapper.coffee ---- */ @@ -1562,4 +1563,4 @@ jQuery.extend( jQuery.easing, window.wrapper = new Wrapper(ws_url); -}).call(this); +}).call(this); \ No newline at end of file From 940d583f9aba50148336bc99791cc569e0099cc3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Oct 2017 11:02:27 +0200 Subject: [PATCH 0406/2570] Fix site violation bug when using Tor --- src/Site/Site.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Site/Site.py b/src/Site/Site.py index a10f642d..67905e62 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -1027,11 +1027,14 @@ class Site(object): def getConnectedPeers(self): back = [] + tor_manager = self.connection_server.tor_manager for connection in self.connection_server.connections: if not connection.connected and time.time() - connection.start_time > 20: # Still not connected after 20s continue peer = self.peers.get("%s:%s" % (connection.ip, connection.port)) if peer: + if connection.target_onion and tor_manager.start_onions and tor_manager.getOnion(self.address) != connection.target_onion: + continue if not peer.connection: peer.connect(connection) back.append(peer) From e048fa6c6a2a06bab5ad28acdc128998b5a5ca48 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Oct 2017 11:02:40 +0200 Subject: [PATCH 0407/2570] Target 6 connections per site --- 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 67905e62..ad1621a3 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -965,7 +965,7 @@ class Site(object): self.announcePex() # Keep connections to get the updates - def needConnections(self, num=4, check_site_on_reconnect=False): + def needConnections(self, num=6, check_site_on_reconnect=False): need = min(len(self.peers), num, config.connected_limit) # Need 5 peer, but max total peers connected = len(self.getConnectedPeers()) From fa409e63f862f2d5b3df574a53c4ae1d26871236 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Oct 2017 11:03:09 +0200 Subject: [PATCH 0408/2570] Rev3120 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 1ffa7939..96ad112e 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.0" - self.rev = 3114 + self.rev = 3120 self.argv = argv self.action = None self.config_file = "zeronet.conf" From c519239d874119375dea89ccd0e3cec060e8f695 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 22 Oct 2017 11:11:26 +0200 Subject: [PATCH 0409/2570] Add more detail on cli signing error --- src/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index 4014d19a..8cd0b789 100644 --- a/src/main.py +++ b/src/main.py @@ -209,6 +209,7 @@ class Actions(object): def siteSign(self, address, privatekey=None, inner_path="content.json", publish=False, remove_missing_optional=False): from Site import Site from Site import SiteManager + from Debug import Debug SiteManager.site_manager.load() logging.info("Signing site: %s..." % address) site = Site(address, allow_create=False) @@ -229,7 +230,7 @@ class Actions(object): try: succ = site.content_manager.sign(inner_path=inner_path, privatekey=privatekey, update_changed_files=True, remove_missing_optional=remove_missing_optional) except Exception, err: - logging.error("Sign error: %s" % err) + logging.error("Sign error: %s" % Debug.formatException(err)) succ = False if succ and publish: self.sitePublish(address, inner_path=inner_path, diffs=diffs) From 9d511ba1655d8b2982b3d9810ba3901d0a5d1c09 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 22 Oct 2017 11:13:09 +0200 Subject: [PATCH 0410/2570] Rev3122, Auto ignore database file --- src/Config.py | 2 +- src/Content/ContentManager.py | 2 ++ src/Site/SiteStorage.py | 7 +++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 96ad112e..9a81ded5 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.0" - self.rev = 3120 + self.rev = 3122 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index bde7756c..1d4956e0 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -524,6 +524,8 @@ class ContentManager(object): elif not self.isValidRelativePath(file_relative_path): ignored = True self.log.error("- [ERROR] Invalid filename: %s" % file_relative_path) + elif dir_inner_path == "" and file_relative_path == self.site.storage.getDbFile(): + ignored = True elif optional_pattern and SafeRe.match(optional_pattern, file_relative_path): optional = True diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index 9bb2da44..1334912a 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -34,6 +34,13 @@ class SiteStorage(object): else: raise Exception("Directory not exists: %s" % self.directory) + def getDbFile(self): + if self.isFile("dbschema.json"): + schema = self.loadJson("dbschema.json") + return schema["db_file"] + else: + return False + # Load db from dbschema.json def openDb(self, check=True): try: From 9b83c683b545dbd729fb12c2bfb1769ec628bb83 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 26 Oct 2017 10:35:46 +0200 Subject: [PATCH 0411/2570] Reload content.json info after bigfile upload --- plugins/Bigfile/BigfilePlugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index 60bdb4a8..d1ad9df2 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -68,7 +68,7 @@ class UiRequestPlugin(object): msgpack.pack({file_name: piecemap_info}, site.storage.open(upload_info["piecemap"], "wb")) # Find piecemap and file relative path to content.json - file_info = site.content_manager.getFileInfo(inner_path) + 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):] @@ -91,6 +91,8 @@ class UiRequestPlugin(object): site.content_manager.optionalDownloaded(inner_path, merkle_root, 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"]), From 604792a4dd75a21bcb46dfeaf33b47c7fc802031 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 26 Oct 2017 10:40:02 +0200 Subject: [PATCH 0412/2570] Call response function for return values of UiWebsocket actions --- src/Ui/UiWebsocket.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 20005731..5a064d89 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -209,7 +209,9 @@ class UiWebsocket(object): def asyncWrapper(self, func): def asyncErrorWatcher(func, *args, **kwargs): try: - func(*args, **kwargs) + result = func(*args, **kwargs) + if result: + self.response(args[0], result) except Exception, err: if config.debug: # Allow websocket errors to appear on /Debug sys.modules["main"].DebugHook.handleError() @@ -245,13 +247,16 @@ class UiWebsocket(object): # Support calling as named, unnamed parameters and raw first argument too if type(params) is dict: - func(req["id"], **params) + result = func(req["id"], **params) elif type(params) is list: - func(req["id"], *params) + result = func(req["id"], *params) elif params: - func(req["id"], params) + result = func(req["id"], params) else: - func(req["id"]) + result = func(req["id"]) + + if result: + self.response(req["id"], result) # Format site info def formatSiteInfo(self, site, create_user=True): @@ -393,8 +398,8 @@ class UiWebsocket(object): if response_ok: self.response(to, "ok") - - return inner_path + else: + return inner_path # Sign and publish content.json def actionSitePublish(self, to, privatekey=None, inner_path="content.json", sign=True): From 53afd97346728a567292bd4cdec7b2116a822977 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 26 Oct 2017 10:40:41 +0200 Subject: [PATCH 0413/2570] Alway start workers for slow tasks --- src/Worker/WorkerManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index e78caffa..a0169083 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -99,7 +99,7 @@ class WorkerManager(object): if task["peers"]: # Release the peer lock self.log.debug("Task peer lock release: %s" % task["inner_path"]) task["peers"] = [] - self.startWorkers() + self.startWorkers() if len(self.tasks) > len(self.workers) * 2 and len(self.workers) < self.getMaxWorkers(): self.startWorkers() From e8f049a76560f4c190a8e8d999b5ab77636d76f1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 26 Oct 2017 10:43:10 +0200 Subject: [PATCH 0414/2570] Strip / from content.json location of file_info --- src/Content/ContentManager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 1d4956e0..732026c6 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -307,7 +307,8 @@ class ContentManager(object): inner_path_parts = [dirs.pop()] # Filename relative to content.json while True: content_inner_path = "%s/content.json" % "/".join(dirs) - content = self.contents.get(content_inner_path.strip("/")) + content_inner_path = content_inner_path.strip("/") + content = self.contents.get(content_inner_path) # Check in files if content and "files" in content: From 99e5af67b75f6543bcba7a2e49322a7699d1a7cb Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 26 Oct 2017 10:43:45 +0200 Subject: [PATCH 0415/2570] Make BigfileUploadInit merger sites compatible --- plugins/Bigfile/BigfilePlugin.py | 4 ++-- plugins/MergerSite/MergerSitePlugin.py | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index d1ad9df2..d2c9b359 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -142,12 +142,12 @@ class UiWebsocketPlugin(object): "piece_size": piece_size, "piecemap": inner_path + ".piecemap.msgpack" } - self.response(to, { + return { "url": "/ZeroNet-Internal/BigfileUpload?upload_nonce=" + nonce, "pice_size": piece_size, "inner_path": inner_path, "file_relative_path": file_relative_path - }) + } @PluginManager.registerTo("ContentManager") diff --git a/plugins/MergerSite/MergerSitePlugin.py b/plugins/MergerSite/MergerSitePlugin.py index f030ce07..287873af 100644 --- a/plugins/MergerSite/MergerSitePlugin.py +++ b/plugins/MergerSite/MergerSitePlugin.py @@ -135,9 +135,7 @@ class UiWebsocketPlugin(object): req_self.site = self.server.sites.get(merged_address) # Change the site to the merged one func = getattr(super(UiWebsocketPlugin, req_self), func_name) - back = func(to, merged_inner_path, *args, **kwargs) - - return back + return func(to, merged_inner_path, *args, **kwargs) else: func = getattr(super(UiWebsocketPlugin, self), func_name) return func(to, inner_path, *args, **kwargs) @@ -166,6 +164,13 @@ class UiWebsocketPlugin(object): 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) From 00b6842f355c4ac82fa99f1910533e7e49eb471d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 26 Oct 2017 10:43:51 +0200 Subject: [PATCH 0416/2570] Rev3125 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 9a81ded5..98052d39 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.0" - self.rev = 3122 + self.rev = 3125 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 9034449e96343ba8464f42bbd0fbf5714301df6a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 26 Oct 2017 10:50:56 +0200 Subject: [PATCH 0417/2570] Fix fileInfo test --- src/Test/TestContent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test/TestContent.py b/src/Test/TestContent.py index 7c46e022..d864db51 100644 --- a/src/Test/TestContent.py +++ b/src/Test/TestContent.py @@ -131,7 +131,7 @@ class TestContent: def testFileInfo(self, site): assert "sha512" in site.content_manager.getFileInfo("index.html") - assert site.content_manager.getFileInfo("data/img/domain.png")["content_inner_path"] == "/content.json" + assert site.content_manager.getFileInfo("data/img/domain.png")["content_inner_path"] == "content.json" assert site.content_manager.getFileInfo("data/users/hello.png")["content_inner_path"] == "data/users/content.json" assert site.content_manager.getFileInfo("data/users/content.json")["content_inner_path"] == "data/users/content.json" assert not site.content_manager.getFileInfo("notexist") From c64e97f0d6c2639bad1b73e68bca1a1cb377b843 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Thu, 26 Oct 2017 18:32:05 +0300 Subject: [PATCH 0418/2570] DbQuery: WHERE without AND is still WHERE --- src/Db/DbQuery.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Db/DbQuery.py b/src/Db/DbQuery.py index 10ce773b..a7730d5b 100644 --- a/src/Db/DbQuery.py +++ b/src/Db/DbQuery.py @@ -22,6 +22,8 @@ class DbQuery: def parseWheres(self, query_where): if " AND " in query_where: return query_where.split(" AND ") + elif query_where: + return [query_where] else: return [] From 818983831a840065401bb822ef81285c84639a9f Mon Sep 17 00:00:00 2001 From: Ivanq Date: Thu, 26 Oct 2017 18:34:53 +0300 Subject: [PATCH 0419/2570] Newsfeed: Add brackets around WHERE --- plugins/Newsfeed/NewsfeedPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Newsfeed/NewsfeedPlugin.py b/plugins/Newsfeed/NewsfeedPlugin.py index 6cd3a467..c73aad2b 100644 --- a/plugins/Newsfeed/NewsfeedPlugin.py +++ b/plugins/Newsfeed/NewsfeedPlugin.py @@ -105,7 +105,7 @@ class UiWebsocketPlugin(object): for name, query in feeds.iteritems(): try: db_query = DbQuery(query) - db_query.wheres.append("%s LIKE ? OR %s LIKE ?" % (db_query.fields["body"], db_query.fields["title"])) + db_query.wheres.append("(%s LIKE ? OR %s LIKE ?)" % (db_query.fields["body"], db_query.fields["title"])) db_query.parts["ORDER BY"] = "date_added DESC" db_query.parts["LIMIT"] = "30" From 2e74b73ba057a6fdf4beb70cb6c9643273607004 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 28 Oct 2017 02:39:49 +0200 Subject: [PATCH 0420/2570] Verify signatures using CLI --- src/Config.py | 6 ++++++ src/main.py | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/src/Config.py b/src/Config.py index 98052d39..4304c51e 100644 --- a/src/Config.py +++ b/src/Config.py @@ -165,6 +165,12 @@ class Config(object): action.add_argument('message', help='Message to sign') action.add_argument('privatekey', help='Private key') + # Crypt Verify + action = self.subparsers.add_parser("cryptVerify", help='Verify message using Bitcoin public address') + action.add_argument('message', help='Message to verify') + action.add_argument('sign', help='Signiture for message') + action.add_argument('address', help='Signer\'s address') + action = self.subparsers.add_parser("getConfig", help='Return json-encoded info') # Config parameters diff --git a/src/main.py b/src/main.py index 8cd0b789..8e98f486 100644 --- a/src/main.py +++ b/src/main.py @@ -416,6 +416,10 @@ class Actions(object): from Crypt import CryptBitcoin print CryptBitcoin.sign(message, privatekey) + def cryptVerify(self, message, sign, address): + from Crypt import CryptBitcoin + print CryptBitcoin.verify(message, address, sign) + # Peer def peerPing(self, peer_ip, peer_port=None): if not peer_port: From 09413f5fc794616ce61d357611141462d59ca04f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 28 Oct 2017 02:40:35 +0200 Subject: [PATCH 0421/2570] Only zoom sidebar globe if mouse button is pressed down --- plugins/Sidebar/media-globe/all.js | 5 ++--- plugins/Sidebar/media-globe/globe.js | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/Sidebar/media-globe/all.js b/plugins/Sidebar/media-globe/all.js index 6d41940f..5ddc0313 100644 --- a/plugins/Sidebar/media-globe/all.js +++ b/plugins/Sidebar/media-globe/all.js @@ -410,6 +410,7 @@ DAT.Globe = function(container, opts) { } function onMouseWheel(event) { + if (container.style.cursor != "move") return false; event.preventDefault(); if (overRenderer) { if (event.deltaY) { @@ -471,7 +472,6 @@ DAT.Globe = function(container, opts) { function unload() { running = false container.removeEventListener('mousedown', onMouseDown, false); - container.removeEventListener('mousewheel', onMouseWheel, false); if ('onwheel' in document) { container.removeEventListener('wheel', onMouseWheel, false); } else { @@ -526,7 +526,6 @@ DAT.Globe = function(container, opts) { - /* ---- plugins/Sidebar/media-globe/three.min.js ---- */ @@ -1343,4 +1342,4 @@ THREE.MorphBlendMesh.prototype.setAnimationDirectionForward=function(a){if(a=thi THREE.MorphBlendMesh.prototype.setAnimationDuration=function(a,b){var c=this.animationsMap[a];c&&(c.duration=b,c.fps=(c.end-c.start)/c.duration)};THREE.MorphBlendMesh.prototype.setAnimationWeight=function(a,b){var c=this.animationsMap[a];c&&(c.weight=b)};THREE.MorphBlendMesh.prototype.setAnimationTime=function(a,b){var c=this.animationsMap[a];c&&(c.time=b)};THREE.MorphBlendMesh.prototype.getAnimationTime=function(a){var b=0;if(a=this.animationsMap[a])b=a.time;return b}; THREE.MorphBlendMesh.prototype.getAnimationDuration=function(a){var b=-1;if(a=this.animationsMap[a])b=a.duration;return b};THREE.MorphBlendMesh.prototype.playAnimation=function(a){var b=this.animationsMap[a];b?(b.time=0,b.active=!0):console.warn("animation["+a+"] undefined")};THREE.MorphBlendMesh.prototype.stopAnimation=function(a){if(a=this.animationsMap[a])a.active=!1}; THREE.MorphBlendMesh.prototype.update=function(a){for(var b=0,c=this.animationsList.length;bd.duration||0>d.time)d.direction*=-1,d.time>d.duration&&(d.time=d.duration,d.directionBackwards=!0),0>d.time&&(d.time=0,d.directionBackwards=!1)}else d.time%=d.duration,0>d.time&&(d.time+=d.duration);var f=d.startFrame+THREE.Math.clamp(Math.floor(d.time/e),0,d.length-1),g=d.weight; -f!==d.currentFrame&&(this.morphTargetInfluences[d.lastFrame]=0,this.morphTargetInfluences[d.currentFrame]=1*g,this.morphTargetInfluences[f]=0,d.lastFrame=d.currentFrame,d.currentFrame=f);e=d.time%e/e;d.directionBackwards&&(e=1-e);this.morphTargetInfluences[d.currentFrame]=e*g;this.morphTargetInfluences[d.lastFrame]=(1-e)*g}}}; \ No newline at end of file +f!==d.currentFrame&&(this.morphTargetInfluences[d.lastFrame]=0,this.morphTargetInfluences[d.currentFrame]=1*g,this.morphTargetInfluences[f]=0,d.lastFrame=d.currentFrame,d.currentFrame=f);e=d.time%e/e;d.directionBackwards&&(e=1-e);this.morphTargetInfluences[d.currentFrame]=e*g;this.morphTargetInfluences[d.lastFrame]=(1-e)*g}}}; diff --git a/plugins/Sidebar/media-globe/globe.js b/plugins/Sidebar/media-globe/globe.js index eab71f9e..a5523796 100644 --- a/plugins/Sidebar/media-globe/globe.js +++ b/plugins/Sidebar/media-globe/globe.js @@ -321,6 +321,7 @@ DAT.Globe = function(container, opts) { } function onMouseWheel(event) { + if (container.style.cursor != "move") return false; event.preventDefault(); if (overRenderer) { if (event.deltaY) { From 46455737cc433ed451997cf932e0d8f4ba7893d6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 28 Oct 2017 02:40:42 +0200 Subject: [PATCH 0422/2570] Rev3126 --- src/Config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 4304c51e..2c8bc7a0 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.0" - self.rev = 3125 + self.rev = 3126 self.argv = argv self.action = None self.config_file = "zeronet.conf" @@ -172,6 +172,7 @@ class Config(object): action.add_argument('address', help='Signer\'s address') action = self.subparsers.add_parser("getConfig", help='Return json-encoded info') + action = self.subparsers.add_parser("testConnection", help='Testing') # Config parameters self.parser.add_argument('--verbose', help='More detailed logging', action='store_true') From 9d4515954b8cb7f8c0af31f30d83567a3486e624 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 29 Oct 2017 23:39:36 +0100 Subject: [PATCH 0423/2570] Rev3127, Fix UiWebsocket async message response skip --- src/Config.py | 2 +- src/Ui/UiWebsocket.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index 2c8bc7a0..084042db 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.0" - self.rev = 3126 + self.rev = 3127 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 5a064d89..88a2739f 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -187,9 +187,9 @@ class UiWebsocket(object): self.next_message_id += 1 if cb: # Callback after client responded self.waiting_cb[message["id"]] = cb + self.send_queue.append(message) if self.sending: return # Already sending - self.send_queue.append(message) try: while self.send_queue: self.sending = True From c3250378ee110721fc919e1ecfeb4c1040839f3f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 30 Oct 2017 15:15:40 +0100 Subject: [PATCH 0424/2570] Rev3128, Allow modals for sites --- src/Config.py | 2 +- src/Ui/UiRequest.py | 5 +---- src/Ui/template/wrapper.html | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Config.py b/src/Config.py index 084042db..5dddecc3 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.0" - self.rev = 3127 + self.rev = 3128 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 7afa34b3..884f4bef 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -366,10 +366,7 @@ class UiRequest(object): if content.get("postmessage_nonce_security"): postmessage_nonce_security = "true" - if site.settings.get("own"): - sandbox_permissions = "allow-modals" # For coffeescript compile errors - else: - sandbox_permissions = "" + sandbox_permissions = "" if show_loadingscreen is None: show_loadingscreen = not site.storage.isFile(file_inner_path) diff --git a/src/Ui/template/wrapper.html b/src/Ui/template/wrapper.html index 39915ce2..4e6ae06f 100644 --- a/src/Ui/template/wrapper.html +++ b/src/Ui/template/wrapper.html @@ -51,7 +51,7 @@ if (window.self !== window.top && document.execCommand) document.execCommand("St - + - - - """.replace("\t", "") - inject_html = inject_html.replace("{master_seed}", master_seed) # Set the master seed in the message - - return iter([re.sub("\s*\s*$", inject_html, back)]) # Replace the tags with the injection - elif loggedin: back = back_generator.next() inject_html = """ @@ -193,6 +166,30 @@ class UiWebsocketPlugin(object): else: return super(UiWebsocketPlugin, self).hasCmdPermission(cmd) + def actionCertAdd(self, *args, **kwargs): + super(UiWebsocketPlugin, self).actionCertAdd(*args, **kwargs) + master_seed = self.user.master_seed + # Inject the welcome message + inject_html = """ + + + + """.replace("\t", "") + inject_html = inject_html.replace("{master_seed}", master_seed) # Set the master seed in the message + self.cmd("injectHtml", inject_html) + @PluginManager.registerTo("ConfigPlugin") class ConfigPlugin(object): From 8b9a8997acdd89c8dedc846cc07b7a601683a43c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 29 Nov 2017 13:50:03 +0100 Subject: [PATCH 0462/2570] Rev3151 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 1d5d0eba..a0d6120e 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.0" - self.rev = 3148 + self.rev = 3151 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 6b92d011d24c708674891f174a8d7f9c5153d7ef Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 30 Nov 2017 19:38:56 +0100 Subject: [PATCH 0463/2570] Rev3153, Download missing sites from sites.json --- plugins/Zeroname/SiteManagerPlugin.py | 4 +-- src/Config.py | 2 +- src/Site/SiteManager.py | 38 ++++++++++++++++----------- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/plugins/Zeroname/SiteManagerPlugin.py b/plugins/Zeroname/SiteManagerPlugin.py index 0ddcbd4f..89ad9508 100644 --- a/plugins/Zeroname/SiteManagerPlugin.py +++ b/plugins/Zeroname/SiteManagerPlugin.py @@ -53,7 +53,7 @@ class SiteManagerPlugin(object): # Return or create site and start download site files # Return: Site or None if dns resolve failed - def need(self, address, all_file=True): + def need(self, address, *args, **kwargs): if self.isDomain(address): # Its looks like a domain address_resolved = self.resolveDomain(address) if address_resolved: @@ -61,7 +61,7 @@ class SiteManagerPlugin(object): else: return None - return super(SiteManagerPlugin, self).need(address, all_file) + return super(SiteManagerPlugin, self).need(address, *args, **kwargs) # Return: Site object or None if not found def get(self, address): diff --git a/src/Config.py b/src/Config.py index a0d6120e..af2fc917 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.0" - self.rev = 3151 + self.rev = 3153 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index 556f7197..b21ce625 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -24,7 +24,7 @@ class SiteManager(object): atexit.register(lambda: self.save(recalculate_size=True)) # Load all sites from data/sites.json - def load(self, cleanup=True): + def load(self, cleanup=True, startup=False): self.log.debug("Loading sites...") self.loaded = False from Site import Site @@ -34,17 +34,25 @@ class SiteManager(object): added = 0 # Load new adresses for address, settings in json.load(open("%s/sites.json" % config.data_dir)).iteritems(): - if address not in self.sites and os.path.isfile("%s/%s/content.json" % (config.data_dir, address)): - s = time.time() - try: - site = Site(address, settings=settings) - site.content_manager.contents.get("content.json") - except Exception, err: - self.log.debug("Error loading site %s: %s" % (address, err)) - continue - self.sites[address] = site - self.log.debug("Loaded site %s in %.3fs" % (address, time.time() - s)) - added += 1 + if address not in self.sites: + if os.path.isfile("%s/%s/content.json" % (config.data_dir, address)): + # Root content.json exists, try load site + s = time.time() + try: + site = Site(address, settings=settings) + site.content_manager.contents.get("content.json") + except Exception, err: + self.log.debug("Error loading site %s: %s" % (address, err)) + continue + self.sites[address] = site + self.log.debug("Loaded site %s in %.3fs" % (address, time.time() - s)) + added += 1 + elif startup: + # No site directory, start download + self.log.debug("Found new site in sites.json: %s" % address) + gevent.spawn(self.need, address, settings=settings) + added += 1 + address_found.append(address) # Remove deleted adresses @@ -126,7 +134,7 @@ class SiteManager(object): return self.sites.get(address) # Return or create site and start download site files - def need(self, address, all_file=True): + def need(self, address, all_file=True, settings=None): from Site import Site site = self.get(address) if not site: # Site not exist yet @@ -138,7 +146,7 @@ class SiteManager(object): if not self.isAddress(address): return False # Not address: %s % address self.log.debug("Added new site: %s" % address) - site = Site(address) + site = Site(address, settings=settings) self.sites[address] = site if not site.settings["serving"]: # Maybe it was deleted before site.settings["serving"] = True @@ -158,7 +166,7 @@ class SiteManager(object): def list(self): if self.sites is None: # Not loaded yet self.log.debug("Sites not loaded yet...") - self.load() + self.load(startup=True) return self.sites From 90ff9ac7fb2a0babf52aa72066e10217a5a7ffe8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 2 Dec 2017 02:38:17 +0100 Subject: [PATCH 0464/2570] Rev3155, Avoid UI hang during db rebuild --- src/Config.py | 2 +- src/Site/SiteStorage.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Config.py b/src/Config.py index af2fc917..91685fb0 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.0" - self.rev = 3153 + self.rev = 3155 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index 1334912a..7136cd0b 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -83,6 +83,7 @@ class SiteStorage(object): # Return possible db files for the site def getDbFiles(self): + found = 0 for content_inner_path, content in self.site.content_manager.contents.iteritems(): # content.json file itself if self.isFile(content_inner_path): @@ -100,6 +101,9 @@ class SiteStorage(object): yield file_inner_path, self.getPath(file_inner_path) else: self.log.error("[MISSING] %s" % file_inner_path) + found += 1 + if found % 100 == 0: + time.sleep(0.000001) # Context switch to avoid UI block # Rebuild sql cache def rebuildDb(self, delete_db=True): @@ -122,13 +126,14 @@ class SiteStorage(object): self.openDb(check=False) self.log.info("Creating tables...") self.db.checkTables() - self.log.info("Importing data...") cur = self.db.getCursor() cur.execute("BEGIN") cur.logging = False found = 0 s = time.time() + self.log.info("Getting db files...") db_files = list(self.getDbFiles()) + self.log.info("Importing data...") try: if len(db_files) > 100: self.site.messageWebsocket(_["Database rebuilding...
Imported {0} of {1} files..."].format("0000", len(db_files)), "rebuild", 0) @@ -144,6 +149,7 @@ class SiteStorage(object): "rebuild", int(float(found) / len(db_files) * 100) ) + time.sleep(0.000001) # Context switch to avoid UI block finally: cur.execute("END") @@ -471,7 +477,7 @@ class SiteStorage(object): os.unlink(path) break except Exception, err: - self.log.error("Error removing %s: %s, try #%s" % (path, err, retry)) + self.log.error(u"Error removing %s: %s, try #%s" % (inner_path, err, retry)) time.sleep(float(retry) / 10) self.onUpdated(inner_path, False) From 3de182a4edb25bf2665dd770eb6c0c220f9b1077 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 2 Dec 2017 15:31:36 +0100 Subject: [PATCH 0465/2570] Rev3157, Fix websocket utf8 error when requesting files from different sites at the same time --- src/Config.py | 2 +- src/Ui/UiWebsocket.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Config.py b/src/Config.py index 91685fb0..51f147b4 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.0" - self.rev = 3155 + self.rev = 3157 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 104c7380..bd6836e6 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -31,7 +31,7 @@ class UiWebsocket(object): self.next_message_id = 1 self.waiting_cb = {} # Waiting for callback. Key: message_id, Value: function pointer self.channels = [] # Channels joined to - self.sending = False # Currently sending to client + self.state = {"sending": False} # Currently sending to client self.send_queue = [] # Messages to send to client self.admin_commands = ( "sitePause", "siteResume", "siteDelete", "siteList", "siteSetLimit", @@ -191,14 +191,15 @@ class UiWebsocket(object): if cb: # Callback after client responded self.waiting_cb[message["id"]] = cb self.send_queue.append(message) - if self.sending: + if self.state["sending"]: return # Already sending try: while self.send_queue: - self.sending = True + s = time.time() + self.state["sending"] = True message = self.send_queue.pop(0) self.ws.send(json.dumps(message)) - self.sending = False + self.state["sending"] = False except Exception, err: self.log.debug("Websocket send error: %s" % Debug.formatException(err)) From 120829474764b1755721db7a4f3756f81c0e8580 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 2 Dec 2017 15:35:32 +0100 Subject: [PATCH 0466/2570] Remove unused variable, more clear comment on state object --- src/Ui/UiWebsocket.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index bd6836e6..f783d37e 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -31,7 +31,7 @@ class UiWebsocket(object): self.next_message_id = 1 self.waiting_cb = {} # Waiting for callback. Key: message_id, Value: function pointer self.channels = [] # Channels joined to - self.state = {"sending": False} # Currently sending to client + self.state = {"sending": False} # Shared state of websocket connection self.send_queue = [] # Messages to send to client self.admin_commands = ( "sitePause", "siteResume", "siteDelete", "siteList", "siteSetLimit", @@ -195,7 +195,6 @@ class UiWebsocket(object): return # Already sending try: while self.send_queue: - s = time.time() self.state["sending"] = True message = self.send_queue.pop(0) self.ws.send(json.dumps(message)) From 74e50e209a21e44172beeb3c112576092cd9aa6a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 4 Dec 2017 16:04:10 +0100 Subject: [PATCH 0467/2570] Rev3158, Fix site clone with sites larger that 10MB --- src/Config.py | 2 +- src/Site/Site.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index 51f147b4..f9a2d5f4 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.0" - self.rev = 3157 + self.rev = 3158 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Site/Site.py b/src/Site/Site.py index d991042f..430d371f 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -599,11 +599,16 @@ class Site(object): # Copy root content.json if not new_site.storage.isFile("content.json") and not overwrite: - # Content.json not exist yet, create a new one from source site + # New site: Content.json not exist yet, create a new one from source site + if "size_limit" in self.settings: + new_site.settings["size_limit"] = self.settings["size_limit"] + + # Use content.json-default is specified if self.storage.isFile(root_inner_path + "/content.json-default"): content_json = self.storage.loadJson(root_inner_path + "/content.json-default") else: content_json = self.storage.loadJson("content.json") + if "domain" in content_json: del content_json["domain"] content_json["title"] = "my" + content_json["title"] From 0009b1b7d1d3fc62a0b126b8ed1d66730e75242b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 7 Dec 2017 15:23:51 +0100 Subject: [PATCH 0468/2570] Allow site fullscreen functions --- src/Ui/template/wrapper.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ui/template/wrapper.html b/src/Ui/template/wrapper.html index 4e6ae06f..6f2c4741 100644 --- a/src/Ui/template/wrapper.html +++ b/src/Ui/template/wrapper.html @@ -51,7 +51,7 @@ if (window.self !== window.top && document.execCommand) document.execCommand("St - + - + From a1a2434d983bf12b45be80c9685be0b6adefed50 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 21 Feb 2018 03:05:02 +0100 Subject: [PATCH 0648/2570] Rename wrapper onLoad to onPageLoad --- src/Ui/media/Wrapper.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index b8c172e4..97b7dc8d 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -25,7 +25,7 @@ class Wrapper @address = null @opener_tested = false - window.onload = @onLoad # On iframe loaded + window.onload = @onPageLoad # On iframe loaded window.onhashchange = (e) => # On hash change @log "Hashchange", window.location.hash if window.location.hash @@ -387,7 +387,7 @@ class Wrapper # Iframe loaded - onLoad: (e) => + onPageLoad: (e) => @inner_loaded = true if not @inner_ready then @sendInner {"cmd": "wrapperReady"} # Inner frame loaded before wrapper #if not @site_error then @loading.hideScreen() # Hide loading screen From 61699c7477eb831662dbb476e565f4111d6b6322 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 21 Feb 2018 03:05:11 +0100 Subject: [PATCH 0649/2570] Allow small tag in messages --- src/Ui/media/Wrapper.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index 97b7dc8d..b25acdb6 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -498,7 +498,7 @@ class Wrapper value = @toHtmlSafe(value) else value = String(value).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') # Escape - value = value.replace(/<([\/]{0,1}(br|b|u|i))>/g, "<$1>") # Unescape b, i, u, br tags + value = value.replace(/<([\/]{0,1}(br|b|u|i|small))>/g, "<$1>") # Unescape b, i, u, br tags values[i] = value return values From 93162beaa023e1f1ae6b752d158e28abf4664203 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 21 Feb 2018 03:06:21 +0100 Subject: [PATCH 0650/2570] Allow to set get parameter to reload --- src/Ui/media/Wrapper.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index b25acdb6..50b2708b 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -322,7 +322,7 @@ class Wrapper $('').attr("content", @toHtmlSafe message.params).appendTo("head") actionReload: (message) -> - @reload() + @reload(message.params[0]) reload: (url_post="") -> if url_post From 5275988f3710179ef5a5e415f5d211a6107f6e3d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 21 Feb 2018 03:06:48 +0100 Subject: [PATCH 0651/2570] Move isProxyRequest and gotoSite to minimal ZeroFrame wrapper --- src/Ui/media/Wrapper.coffee | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index 50b2708b..bd524257 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -515,18 +515,6 @@ class Wrapper $("iframe").attr "src", src return false - - isProxyRequest: -> - return window.location.pathname == "/" - - - gotoSite: (elem) => - href = $(elem).attr("href") - if @isProxyRequest() # Fix for proxy request - $(elem).attr("href", "http://zero#{href}") - - - log: (args...) -> console.log "[Wrapper]", args... From 6b71f9177501e6fe057851853d5956e26fc81a52 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 21 Feb 2018 03:08:52 +0100 Subject: [PATCH 0652/2570] Verify mouse and keyboard events to avoid non-user verification on notification prompts --- src/Ui/media/Wrapper.coffee | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index bd524257..eb8b79b2 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -25,6 +25,8 @@ class Wrapper @address = null @opener_tested = false + @allowed_event_constructors = [MouseEvent, KeyboardEvent] # Allowed event constructors + window.onload = @onPageLoad # On iframe loaded window.onhashchange = (e) => # On hash change @log "Hashchange", window.location.hash @@ -37,6 +39,15 @@ class Wrapper $("#inner-iframe").focus() + verifyEvent: (allowed_target, e) => + if not e.originalEvent.isTrusted + throw "Event not trusted" + + if e.originalEvent.constructor not in @allowed_event_constructors + throw "Invalid event constructor: #{e.constructor} != #{allowed_event_constructor}" + + if e.originalEvent.currentTarget != allowed_target[0] + throw "Invalid event target: #{e.originalEvent.currentTarget} != #{allowed_target[0]}" # Incoming message from UiServer websocket onMessageWebsocket: (e) => @@ -216,9 +227,12 @@ class Wrapper if captions not instanceof Array then captions = [captions] # Convert to list if necessary for caption, i in captions button = $("#{caption}") # Add confirm button - button.on "click", (e) => - cb(parseInt(e.currentTarget.dataset.value)) - return false + ((button) => + button.on "click", (e) => + @verifyEvent button, e + cb(parseInt(e.currentTarget.dataset.value)) + return false + )(button) buttons.append(button) body.append(buttons) @notifications.add("notification-#{caption}", "ask", body) @@ -241,12 +255,14 @@ class Wrapper input = $("") # Add input input.on "keyup", (e) => # Send on enter + @verifyEvent input, e if e.keyCode == 13 - button.trigger "click" # Response to confirm + cb input.val() # Response to confirm body.append(input) button = $("#{caption}") # Add confirm button - button.on "click", => # Response on button click + button.on "click", (e) => # Response on button click + @verifyEvent button, e cb input.val() return false body.append(button) @@ -528,3 +544,4 @@ else ws_url = proto.ws + ":" + origin.replace(proto.http+":", "") + "/Websocket?wrapper_key=" + window.wrapper_key window.wrapper = new Wrapper(ws_url) + From 1833b3e89f05e6af9fbecc8ac53d6f805e74581d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 21 Feb 2018 03:09:01 +0100 Subject: [PATCH 0653/2570] Remove logging injected html --- src/Ui/media/Wrapper.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index eb8b79b2..0c0acd51 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -83,7 +83,6 @@ class Wrapper @ws.ws.close() @ws.onCloseWebsocket(null, 4000) else if cmd == "injectHtml" - console.log("inject", message) $("body").append(message.params) else @sendInner message # Pass message to inner frame From c0bf9a9a762b38a76613dde62c2e4127136b0509 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 21 Feb 2018 03:09:36 +0100 Subject: [PATCH 0654/2570] Add cmd function to Wrapper --- src/Ui/media/Wrapper.coffee | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index 0c0acd51..539a96fa 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -16,6 +16,8 @@ class Wrapper @ws.connect() @ws_error = null # Ws error message + @next_cmd_message_id = -1 + @site_info = null # Hold latest site info @event_site_info = $.Deferred() # Event when site_info received @inner_loaded = false # If iframe loaded or not @@ -109,6 +111,17 @@ class Wrapper @log "Message nonce error:", message.wrapper_nonce, '!=', window.wrapper_nonce return + cmd: (cmd, params={}, cb=null) => + message = {} + message.cmd = cmd + message.params = params + message.id = @next_cmd_message_id + if cb + @ws.waiting_cb[message.id] = cb + @next_cmd_message_id -= 1 + + @handleMessage(message) + cmd = message.cmd if cmd == "innerReady" @inner_ready = true From 99362c78bf93f5243f0b347087d3419c0aad24d3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 21 Feb 2018 03:09:48 +0100 Subject: [PATCH 0655/2570] Move handleMessage to separate function --- src/Ui/media/Wrapper.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index 539a96fa..87d1e04e 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -111,6 +111,8 @@ class Wrapper @log "Message nonce error:", message.wrapper_nonce, '!=', window.wrapper_nonce return + @handleMessage message + cmd: (cmd, params={}, cb=null) => message = {} message.cmd = cmd @@ -122,6 +124,7 @@ class Wrapper @handleMessage(message) + handleMessage: (message) => cmd = message.cmd if cmd == "innerReady" @inner_ready = true From 01ce86ce18fb09ceb02b4732c52b9a3aea38605f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 21 Feb 2018 03:10:23 +0100 Subject: [PATCH 0656/2570] Don't display permission request if site already has --- src/Ui/media/Wrapper.coffee | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index 87d1e04e..39e05dbb 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -226,10 +226,13 @@ class Wrapper actionPermissionAdd: (message) -> permission = message.params - @ws.cmd "permissionDetails", permission, (permission_details) => - @displayConfirm "This site requests permission:" + " #{@toHtmlSafe(permission)}" + "
#{permission_details}", "Grant", => - @ws.cmd "permissionAdd", permission, => - @sendInner {"cmd": "response", "to": message.id, "result": "Granted"} + $.when(@event_site_info).done => + if permission in @site_info.settings.permissions + return false + @ws.cmd "permissionDetails", permission, (permission_details) => + @displayConfirm "This site requests permission:" + " #{@toHtmlSafe(permission)}" + "
#{permission_details}", "Grant", => + @ws.cmd "permissionAdd", permission, (res) => + @sendInner {"cmd": "response", "to": message.id, "result": res} actionNotification: (message) -> message.params = @toHtmlSafe(message.params) # Escape html From aef3ecc3f251eda630bc6d5c9b28151ac1ce339f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 21 Feb 2018 03:11:50 +0100 Subject: [PATCH 0657/2570] Stop page load if opener is present --- src/Ui/template/wrapper.html | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Ui/template/wrapper.html b/src/Ui/template/wrapper.html index b5bf1551..b91f2454 100644 --- a/src/Ui/template/wrapper.html +++ b/src/Ui/template/wrapper.html @@ -17,9 +17,8 @@
From 6afe2dd7209d80c1a1630c8b3634510f1277e2b5 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 21 Feb 2018 03:12:51 +0100 Subject: [PATCH 0658/2570] New NOSANDBOX permission to remove sandboxed iframe restrictions --- src/Ui/UiRequest.py | 5 +++++ src/Ui/UiWebsocket.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 4f8e9b89..65cd3348 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -368,6 +368,10 @@ class UiRequest(object): sandbox_permissions = "" + if "NOSANDBOX" in site.settings["permissions"]: + sandbox_permissions += " allow-same-origin" + + if show_loadingscreen is None: show_loadingscreen = not site.storage.isFile(file_inner_path) @@ -448,6 +452,7 @@ class UiRequest(object): address = path_parts["address"] file_path = "%s/%s/%s" % (config.data_dir, address, path_parts["inner_path"]) + if config.debug 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) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 318dcf87..26a78129 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -760,6 +760,8 @@ class UiWebsocket(object): def actionPermissionDetails(self, to, permission): if permission == "ADMIN": self.response(to, _["Modify your client's configuration and access all site"] + " " + _["(Dangerous!)"] + "") + elif permission == "NOSANDBOX": + self.response(to, _["Full access to site data, cookie and local storage of all site."]) else: self.response(to, "") From b57a9f5c583aca80da17fc40c321e91f368c2ed7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 21 Feb 2018 03:13:27 +0100 Subject: [PATCH 0659/2570] Make cert selection compatible with wrapper modifications --- src/Ui/UiWebsocket.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 26a78129..1816df05 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -721,7 +721,7 @@ class UiWebsocket(object): body += "
" for domain in more_domains: body += _(u""" - + {_[Register]} »{domain} """) @@ -731,16 +731,14 @@ class UiWebsocket(object): - """ % to + """ % self.next_message_id # Send the notification - self.cmd("notification", ["ask", body]) + self.cmd("notification", ["ask", body], lambda domain: self.actionCertSet(to, domain)) # - Admin actions - From 2f2c49cc478eccc1cd7d21c5543533a51fd63169 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 21 Feb 2018 03:14:15 +0100 Subject: [PATCH 0660/2570] Make multiuser plugin compatible with wrapper modifications --- plugins/disabled-Multiuser/MultiuserPlugin.py | 34 ++++++------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/plugins/disabled-Multiuser/MultiuserPlugin.py b/plugins/disabled-Multiuser/MultiuserPlugin.py index bae735a2..16027c9f 100644 --- a/plugins/disabled-Multiuser/MultiuserPlugin.py +++ b/plugins/disabled-Multiuser/MultiuserPlugin.py @@ -64,7 +64,7 @@ class UiRequestPlugin(object): @@ -127,7 +127,7 @@ class UiWebsocketPlugin(object): def actionUserLogout(self, to): if "ADMIN" not in self.site.settings["permissions"]: return self.response(to, "Logout not allowed") - message = "You have been logged out. Login to another account" + message = "You have been logged out. Login to another account" message += "" self.cmd("notification", ["done", message, 1000000]) # 1000000 = Show ~forever :) # Delete from user_manager @@ -152,7 +152,7 @@ class UiWebsocketPlugin(object): if user.master_address: message = "Successfull login, reloading page..." message += "" % user.master_address - message += "" + message += "" self.cmd("notification", ["done", message]) else: self.cmd("notification", ["error", "Error: Invalid master seed"]) @@ -169,26 +169,14 @@ class UiWebsocketPlugin(object): def actionCertAdd(self, *args, **kwargs): super(UiWebsocketPlugin, self).actionCertAdd(*args, **kwargs) master_seed = self.user.master_seed - # Inject the welcome message - inject_html = """ - - - - """.replace("\t", "") - inject_html = inject_html.replace("{master_seed}", master_seed) # Set the master seed in the message - self.cmd("injectHtml", inject_html) + message = "" + message += "Hello, welcome to ZeroProxy!
A new, unique account created for you:
" + message += "
" + master_seed + "
" + message += "
This is your private key, save it, so you can login next time.
Without this key, your registered account will be lost forever!

" + message += "Ok, Saved it!

" + message += "This site allows you to browse ZeroNet content, but if you want to secure your account
" + message += "and help to make a better network, then please run your own ZeroNet client.
" + self.cmd("notification", ["info", message]) @PluginManager.registerTo("ConfigPlugin") From bee8aac0cc64d4ce6e0cf28179f879961783ce75 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 21 Feb 2018 03:14:43 +0100 Subject: [PATCH 0661/2570] Don't allow NOSANDBOX permission on a proxy as it can leak cookies --- plugins/disabled-Multiuser/MultiuserPlugin.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/disabled-Multiuser/MultiuserPlugin.py b/plugins/disabled-Multiuser/MultiuserPlugin.py index 16027c9f..65e1a6b2 100644 --- a/plugins/disabled-Multiuser/MultiuserPlugin.py +++ b/plugins/disabled-Multiuser/MultiuserPlugin.py @@ -178,6 +178,14 @@ class UiWebsocketPlugin(object): message += "and help to make a better network, then please run your own ZeroNet client." self.cmd("notification", ["info", message]) + def actionPermissionAdd(self, to, permission): + if permission == "NOSANDBOX": + 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): From 40693471e99d18073ce806ecdb0f16ec89e9e79c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 21 Feb 2018 03:15:04 +0100 Subject: [PATCH 0662/2570] Merge sidebar and wrapper js --- plugins/Sidebar/media/all.js | 120 ++++++++++----------- src/Ui/media/all.js | 202 ++++++++++++++++++++++++++--------- 2 files changed, 210 insertions(+), 112 deletions(-) diff --git a/plugins/Sidebar/media/all.js b/plugins/Sidebar/media/all.js index bf1f07d5..02e4f5fb 100644 --- a/plugins/Sidebar/media/all.js +++ b/plugins/Sidebar/media/all.js @@ -269,7 +269,7 @@ window.initScrollable = function () { (function() { - var Sidebar, + var Sidebar, wrapper, 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, @@ -278,7 +278,8 @@ window.initScrollable = function () { Sidebar = (function(superClass) { extend(Sidebar, superClass); - function Sidebar() { + function Sidebar(wrapper1) { + this.wrapper = wrapper1; this.unloadGlobe = bind(this.unloadGlobe, this); this.displayGlobe = bind(this.displayGlobe, this); this.loadGlobe = bind(this.loadGlobe, this); @@ -300,7 +301,7 @@ window.initScrollable = function () { this.dragStarted = 0; this.globe = null; this.preload_html = null; - this.original_set_site_info = wrapper.setSiteInfo; + this.original_set_site_info = this.wrapper.setSiteInfo; if (false) { this.startDrag(); this.moved(); @@ -310,15 +311,6 @@ window.initScrollable = function () { } Sidebar.prototype.initFixbutton = function() { - - /* - @fixbutton.on "mousedown touchstart", (e) => - if not @opened - @logStart("Preloading") - wrapper.ws.cmd "sidebarGetHtmlTag", {}, (res) => - @logEnd("Preloading") - @preload_html = res - */ this.fixbutton.on("mousedown touchstart", (function(_this) { return function(e) { if (e.button > 0) { @@ -413,10 +405,10 @@ window.initScrollable = function () { }; })(this)); $(window).trigger("resize"); - wrapper.setSiteInfo = (function(_this) { + this.wrapper.setSiteInfo = (function(_this) { return function(site_info) { _this.setSiteInfo(site_info); - return _this.original_set_site_info.apply(wrapper, arguments); + return _this.original_set_site_info.apply(_this.wrapper, arguments); }; })(this); img = new Image(); @@ -452,7 +444,7 @@ window.initScrollable = function () { this.setHtmlTag(this.preload_html); return this.preload_html = null; } else { - return wrapper.ws.cmd("sidebarGetHtmlTag", {}, this.setHtmlTag); + return this.wrapper.ws.cmd("sidebarGetHtmlTag", {}, this.setHtmlTag); } }; @@ -527,7 +519,9 @@ window.initScrollable = function () { this.opened = false; } else { targetx = this.width; - if (!this.opened) { + if (this.opened) { + onOpened(); + } else { this.when_loaded.done((function(_this) { return function() { return _this.onOpened(); @@ -570,9 +564,9 @@ window.initScrollable = function () { })(this)); this.tag.find("#button-sitelimit").off("click touchend").on("click touchend", (function(_this) { return function() { - wrapper.ws.cmd("siteSetLimit", $("#input-sitelimit").val(), function(res) { + _this.wrapper.ws.cmd("siteSetLimit", $("#input-sitelimit").val(), function(res) { if (res === "ok") { - wrapper.notifications.add("done-sitelimit", "done", "Site storage limit modified!", 5000); + _this.wrapper.notifications.add("done-sitelimit", "done", "Site storage limit modified!", 5000); } return _this.updateHtmlTag(); }); @@ -581,8 +575,8 @@ window.initScrollable = function () { })(this)); this.tag.find("#button-dbreload").off("click touchend").on("click touchend", (function(_this) { return function() { - wrapper.ws.cmd("dbReload", [], function() { - wrapper.notifications.add("done-dbreload", "done", "Database schema reloaded!", 5000); + _this.wrapper.ws.cmd("dbReload", [], function() { + _this.wrapper.notifications.add("done-dbreload", "done", "Database schema reloaded!", 5000); return _this.updateHtmlTag(); }); return false; @@ -590,9 +584,9 @@ window.initScrollable = function () { })(this)); this.tag.find("#button-dbrebuild").off("click touchend").on("click touchend", (function(_this) { return function() { - wrapper.notifications.add("done-dbrebuild", "info", "Database rebuilding...."); - wrapper.ws.cmd("dbRebuild", [], function() { - wrapper.notifications.add("done-dbrebuild", "done", "Database rebuilt!", 5000); + _this.wrapper.notifications.add("done-dbrebuild", "info", "Database rebuilding...."); + _this.wrapper.ws.cmd("dbRebuild", [], function() { + _this.wrapper.notifications.add("done-dbrebuild", "done", "Database rebuilt!", 5000); return _this.updateHtmlTag(); }); return false; @@ -601,8 +595,8 @@ window.initScrollable = function () { this.tag.find("#button-update").off("click touchend").on("click touchend", (function(_this) { return function() { _this.tag.find("#button-update").addClass("loading"); - wrapper.ws.cmd("siteUpdate", wrapper.site_info.address, function() { - wrapper.notifications.add("done-updated", "done", "Site updated!", 5000); + _this.wrapper.ws.cmd("siteUpdate", _this.wrapper.site_info.address, function() { + _this.wrapper.notifications.add("done-updated", "done", "Site updated!", 5000); return _this.tag.find("#button-update").removeClass("loading"); }); return false; @@ -611,30 +605,30 @@ window.initScrollable = function () { this.tag.find("#button-pause").off("click touchend").on("click touchend", (function(_this) { return function() { _this.tag.find("#button-pause").addClass("hidden"); - wrapper.ws.cmd("sitePause", wrapper.site_info.address); + _this.wrapper.ws.cmd("sitePause", _this.wrapper.site_info.address); return false; }; })(this)); this.tag.find("#button-resume").off("click touchend").on("click touchend", (function(_this) { return function() { _this.tag.find("#button-resume").addClass("hidden"); - wrapper.ws.cmd("siteResume", wrapper.site_info.address); + _this.wrapper.ws.cmd("siteResume", _this.wrapper.site_info.address); return false; }; })(this)); this.tag.find("#button-delete").off("click touchend").on("click touchend", (function(_this) { return function() { - wrapper.displayConfirm("Are you sure?", ["Delete this site", "Blacklist"], function(confirmed) { + _this.wrapper.displayConfirm("Are you sure?", ["Delete this site", "Blacklist"], function(confirmed) { if (confirmed === 1) { _this.tag.find("#button-delete").addClass("loading"); - return wrapper.ws.cmd("siteDelete", wrapper.site_info.address, function() { + return _this.wrapper.ws.cmd("siteDelete", _this.wrapper.site_info.address, function() { return document.location = $(".fixbutton-bg").attr("href"); }); } else if (confirmed === 2) { - return wrapper.displayPrompt("Blacklist this site", "text", "Delete and Blacklist", "Reason", function(reason) { + return _this.wrapper.displayPrompt("Blacklist this site", "text", "Delete and Blacklist", "Reason", function(reason) { _this.tag.find("#button-delete").addClass("loading"); - wrapper.ws.cmd("blacklistAdd", [wrapper.site_info.address, reason]); - return wrapper.ws.cmd("siteDelete", wrapper.site_info.address, function() { + _this.wrapper.ws.cmd("blacklistAdd", [_this.wrapper.site_info.address, reason]); + return _this.wrapper.ws.cmd("siteDelete", _this.wrapper.site_info.address, function() { return document.location = $(".fixbutton-bg").attr("href"); }); }); @@ -645,40 +639,40 @@ window.initScrollable = function () { })(this)); this.tag.find("#checkbox-owned").off("click touchend").on("click touchend", (function(_this) { return function() { - return wrapper.ws.cmd("siteSetOwned", [_this.tag.find("#checkbox-owned").is(":checked")]); + return _this.wrapper.ws.cmd("siteSetOwned", [_this.tag.find("#checkbox-owned").is(":checked")]); }; })(this)); this.tag.find("#checkbox-autodownloadoptional").off("click touchend").on("click touchend", (function(_this) { return function() { - return wrapper.ws.cmd("siteSetAutodownloadoptional", [_this.tag.find("#checkbox-autodownloadoptional").is(":checked")]); + return _this.wrapper.ws.cmd("siteSetAutodownloadoptional", [_this.tag.find("#checkbox-autodownloadoptional").is(":checked")]); }; })(this)); this.tag.find("#button-identity").off("click touchend").on("click touchend", (function(_this) { return function() { - wrapper.ws.cmd("certSelect"); + _this.wrapper.ws.cmd("certSelect"); return false; }; })(this)); this.tag.find("#checkbox-owned").off("click touchend").on("click touchend", (function(_this) { return function() { - return wrapper.ws.cmd("siteSetOwned", [_this.tag.find("#checkbox-owned").is(":checked")]); + return _this.wrapper.ws.cmd("siteSetOwned", [_this.tag.find("#checkbox-owned").is(":checked")]); }; })(this)); this.tag.find("#button-settings").off("click touchend").on("click touchend", (function(_this) { return function() { - wrapper.ws.cmd("fileGet", "content.json", function(res) { + _this.wrapper.ws.cmd("fileGet", "content.json", function(res) { var data, json_raw; data = JSON.parse(res); data["title"] = $("#settings-title").val(); data["description"] = $("#settings-description").val(); json_raw = unescape(encodeURIComponent(JSON.stringify(data, void 0, '\t'))); - return wrapper.ws.cmd("fileWrite", ["content.json", btoa(json_raw), true], function(res) { + return _this.wrapper.ws.cmd("fileWrite", ["content.json", btoa(json_raw), true], function(res) { if (res !== "ok") { - return wrapper.notifications.add("file-write", "error", "File write error: " + res); + return _this.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", { + _this.wrapper.notifications.add("file-write", "done", "Site settings saved!", 5000); + if (_this.wrapper.site_info.privatekey) { + _this.wrapper.ws.cmd("siteSign", { privatekey: "stored", inner_path: "content.json", update_changed_files: true @@ -703,29 +697,29 @@ window.initScrollable = function () { return function() { var inner_path; inner_path = _this.tag.find("#input-contents").val(); - wrapper.ws.cmd("fileRules", { + _this.wrapper.ws.cmd("fileRules", { inner_path: inner_path }, function(res) { var ref; - if (wrapper.site_info.privatekey || (ref = wrapper.site_info.auth_address, indexOf.call(res.signers, ref) >= 0)) { - return wrapper.ws.cmd("siteSign", { + if (_this.wrapper.site_info.privatekey || (ref = _this.wrapper.site_info.auth_address, indexOf.call(res.signers, ref) >= 0)) { + return _this.wrapper.ws.cmd("siteSign", { privatekey: "stored", inner_path: inner_path, update_changed_files: true }, function(res) { if (res === "ok") { - return wrapper.notifications.add("sign", "done", inner_path + " Signed!", 5000); + return _this.wrapper.notifications.add("sign", "done", inner_path + " Signed!", 5000); } }); } else { - return wrapper.displayPrompt("Enter your private key:", "password", "Sign", "", function(privatekey) { - return wrapper.ws.cmd("siteSign", { + return _this.wrapper.displayPrompt("Enter your private key:", "password", "Sign", "", function(privatekey) { + return _this.wrapper.ws.cmd("siteSign", { privatekey: privatekey, inner_path: inner_path, update_changed_files: true }, function(res) { if (res === "ok") { - return wrapper.notifications.add("sign", "done", inner_path + " Signed!", 5000); + return _this.wrapper.notifications.add("sign", "done", inner_path + " Signed!", 5000); } }); }); @@ -739,7 +733,7 @@ window.initScrollable = function () { return function() { var inner_path; inner_path = _this.tag.find("#input-contents").val(); - wrapper.ws.cmd("sitePublish", { + _this.wrapper.ws.cmd("sitePublish", { "inner_path": inner_path, "sign": false }); @@ -762,36 +756,38 @@ window.initScrollable = function () { })(this)); $("body").on("click", (function(_this) { return function() { - return _this.tag.find(".contents + .flex").removeClass("active"); + if (_this.tag) { + return _this.tag.find(".contents + .flex").removeClass("active"); + } }; })(this)); this.tag.find("#button-sign-publish").off("click touchend").on("click touchend", (function(_this) { return function() { var inner_path; inner_path = _this.tag.find("#input-contents").val(); - wrapper.ws.cmd("fileRules", { + _this.wrapper.ws.cmd("fileRules", { inner_path: inner_path }, function(res) { var ref; - if (wrapper.site_info.privatekey || (ref = wrapper.site_info.auth_address, indexOf.call(res.signers, ref) >= 0)) { - return wrapper.ws.cmd("sitePublish", { + if (_this.wrapper.site_info.privatekey || (ref = _this.wrapper.site_info.auth_address, indexOf.call(res.signers, ref) >= 0)) { + return _this.wrapper.ws.cmd("sitePublish", { privatekey: "stored", inner_path: inner_path, sign: true }, function(res) { if (res === "ok") { - return wrapper.notifications.add("sign", "done", inner_path + " Signed and published!", 5000); + return _this.wrapper.notifications.add("sign", "done", inner_path + " Signed and published!", 5000); } }); } else { - return wrapper.displayPrompt("Enter your private key:", "password", "Sign", "", function(privatekey) { - return wrapper.ws.cmd("sitePublish", { + return _this.wrapper.displayPrompt("Enter your private key:", "password", "Sign", "", function(privatekey) { + return _this.wrapper.ws.cmd("sitePublish", { privatekey: privatekey, inner_path: inner_path, sign: true }, function(res) { if (res === "ok") { - return wrapper.notifications.add("sign", "done", inner_path + " Signed and published!", 5000); + return _this.wrapper.notifications.add("sign", "done", inner_path + " Signed and published!", 5000); } }); }); @@ -821,7 +817,7 @@ window.initScrollable = function () { } }; })(this)); - return wrapper.setSiteInfo = this.original_set_site_info; + return this.wrapper.setSiteInfo = this.original_set_site_info; }; Sidebar.prototype.loadGlobe = function() { @@ -845,7 +841,7 @@ window.initScrollable = function () { img.src = "/uimedia/globe/world.jpg"; return img.onload = (function(_this) { return function() { - return wrapper.ws.cmd("sidebarGetPeers", [], function(globe_data) { + return _this.wrapper.ws.cmd("sidebarGetPeers", [], function(globe_data) { var e, ref, ref1; if (_this.globe) { _this.globe.scene.remove(_this.globe.points); @@ -892,8 +888,10 @@ window.initScrollable = function () { })(Class); + wrapper = window.wrapper; + setTimeout((function() { - return window.sidebar = new Sidebar(); + return window.sidebar = new Sidebar(wrapper); }), 500); window.transitionEnd = 'transitionend webkitTransitionEnd oTransitionEnd otransitionend'; diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index 6a1f8af1..128d46df 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -44,17 +44,18 @@ (function() { var ZeroWebsocket, - __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, - __slice = [].slice; + bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + slice = [].slice; ZeroWebsocket = (function() { function ZeroWebsocket(url) { - this.onCloseWebsocket = __bind(this.onCloseWebsocket, this); - this.onErrorWebsocket = __bind(this.onErrorWebsocket, this); - this.onOpenWebsocket = __bind(this.onOpenWebsocket, this); - this.log = __bind(this.log, this); - this.route = __bind(this.route, this); - this.onMessage = __bind(this.onMessage, this); + this.onCloseWebsocket = bind(this.onCloseWebsocket, this); + this.onErrorWebsocket = bind(this.onErrorWebsocket, this); + this.onOpenWebsocket = bind(this.onOpenWebsocket, this); + this.log = bind(this.log, this); + this.response = bind(this.response, this); + this.route = bind(this.route, this); + this.onMessage = bind(this.onMessage, this); this.url = url; this.next_message_id = 1; this.waiting_cb = {}; @@ -138,17 +139,17 @@ ZeroWebsocket.prototype.log = function() { var args; - args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; - return console.log.apply(console, ["[ZeroWebsocket]"].concat(__slice.call(args))); + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return console.log.apply(console, ["[ZeroWebsocket]"].concat(slice.call(args))); }; ZeroWebsocket.prototype.onOpenWebsocket = function(e) { - var message, _i, _len, _ref; + var i, len, message, ref; this.log("Open"); this.connected = true; - _ref = this.message_queue; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - message = _ref[_i]; + ref = this.message_queue; + for (i = 0, len = ref.length; i < len; i++) { + message = ref[i]; this.ws.send(JSON.stringify(message)); } this.message_queue = []; @@ -818,13 +819,16 @@ jQuery.extend( jQuery.easing, Wrapper = (function() { function Wrapper(ws_url) { - this.gotoSite = bind(this.gotoSite, this); this.setSizeLimit = bind(this.setSizeLimit, this); - this.onLoad = bind(this.onLoad, this); + this.onWrapperLoad = bind(this.onWrapperLoad, this); + this.onPageLoad = bind(this.onPageLoad, this); this.onCloseWebsocket = bind(this.onCloseWebsocket, this); this.onOpenWebsocket = bind(this.onOpenWebsocket, this); + this.handleMessage = bind(this.handleMessage, this); + this.cmd = bind(this.cmd, this); this.onMessageInner = bind(this.onMessageInner, this); this.onMessageWebsocket = bind(this.onMessageWebsocket, this); + this.verifyEvent = bind(this.verifyEvent, this); this.log("Created!"); this.loading = new Loading(); this.notifications = new Notifications($(".notifications")); @@ -838,6 +842,7 @@ jQuery.extend( jQuery.easing, this.ws.onMessage = this.onMessageWebsocket; this.ws.connect(); this.ws_error = null; + this.next_cmd_message_id = -1; this.site_info = null; this.event_site_info = $.Deferred(); this.inner_loaded = false; @@ -846,7 +851,8 @@ jQuery.extend( jQuery.easing, this.site_error = null; this.address = null; this.opener_tested = false; - window.onload = this.onLoad; + this.allowed_event_constructors = [MouseEvent, KeyboardEvent]; + window.onload = this.onPageLoad; window.onhashchange = (function(_this) { return function(e) { var src; @@ -871,6 +877,19 @@ jQuery.extend( jQuery.easing, $("#inner-iframe").focus(); } + Wrapper.prototype.verifyEvent = function(allowed_target, e) { + var ref; + if (!e.originalEvent.isTrusted) { + throw "Event not trusted"; + } + if (ref = e.originalEvent.constructor, indexOf.call(this.allowed_event_constructors, ref) < 0) { + throw "Invalid event constructor: " + e.constructor + " != " + allowed_event_constructor; + } + if (e.originalEvent.currentTarget !== allowed_target[0]) { + throw "Invalid event target: " + e.originalEvent.currentTarget + " != " + allowed_target[0]; + } + }; + Wrapper.prototype.onMessageWebsocket = function(e) { var cmd, id, message, ref, type; message = JSON.parse(e.data); @@ -914,7 +933,6 @@ jQuery.extend( jQuery.easing, this.ws.ws.close(); return this.ws.onCloseWebsocket(null, 4000); } else if (cmd === "injectHtml") { - console.log("inject", message); return $("body").append(message.params); } else { return this.sendInner(message); @@ -922,7 +940,7 @@ jQuery.extend( jQuery.easing, }; Wrapper.prototype.onMessageInner = function(e) { - var cmd, message, query; + var message; if (!window.postmessage_nonce_security && this.opener_tested === false) { if (window.opener && window.opener !== window) { this.log("Opener present", window.opener); @@ -941,6 +959,30 @@ jQuery.extend( jQuery.easing, this.log("Message nonce error:", message.wrapper_nonce, '!=', window.wrapper_nonce); return; } + return this.handleMessage(message); + }; + + Wrapper.prototype.cmd = function(cmd, params, cb) { + var message; + if (params == null) { + params = {}; + } + if (cb == null) { + cb = null; + } + message = {}; + message.cmd = cmd; + message.params = params; + message.id = this.next_cmd_message_id; + if (cb) { + this.ws.waiting_cb[message.id] = cb; + } + this.next_cmd_message_id -= 1; + return this.handleMessage(message); + }; + + Wrapper.prototype.handleMessage = function(message) { + var cmd, query; cmd = message.cmd; if (cmd === "innerReady") { this.inner_ready = true; @@ -1079,14 +1121,19 @@ jQuery.extend( jQuery.easing, Wrapper.prototype.actionPermissionAdd = function(message) { var permission; permission = message.params; - return this.ws.cmd("permissionDetails", permission, (function(_this) { - return function(permission_details) { - return _this.displayConfirm("This site requests permission:" + (" " + (_this.toHtmlSafe(permission)) + "") + ("
" + permission_details + ""), "Grant", function() { - return _this.ws.cmd("permissionAdd", permission, function() { - return _this.sendInner({ - "cmd": "response", - "to": message.id, - "result": "Granted" + return $.when(this.event_site_info).done((function(_this) { + return function() { + if (indexOf.call(_this.site_info.settings.permissions, permission) >= 0) { + return false; + } + return _this.ws.cmd("permissionDetails", permission, function(permission_details) { + return _this.displayConfirm("This site requests permission:" + (" " + (_this.toHtmlSafe(permission)) + "") + ("
" + permission_details + ""), "Grant", function() { + return _this.ws.cmd("permissionAdd", permission, function(res) { + return _this.sendInner({ + "cmd": "response", + "to": message.id, + "result": res + }); }); }); }); @@ -1102,21 +1149,25 @@ jQuery.extend( jQuery.easing, }; Wrapper.prototype.displayConfirm = function(message, captions, cb) { - var body, button, buttons, caption, i, j, len; + var body, button, buttons, caption, fn, i, j, len; body = $("" + message + ""); buttons = $(""); if (!(captions instanceof Array)) { captions = [captions]; } + fn = (function(_this) { + return function(button) { + return button.on("click", function(e) { + _this.verifyEvent(button, e); + cb(parseInt(e.currentTarget.dataset.value)); + return false; + }); + }; + })(this); for (i = j = 0, len = captions.length; j < len; i = ++j) { caption = captions[i]; button = $("" + caption + ""); - button.on("click", (function(_this) { - return function(e) { - cb(parseInt(e.currentTarget.dataset.value)); - return false; - }; - })(this)); + fn(button); buttons.append(button); } body.append(buttons); @@ -1157,15 +1208,17 @@ jQuery.extend( jQuery.easing, input = $(""); input.on("keyup", (function(_this) { return function(e) { + _this.verifyEvent(input, e); if (e.keyCode === 13) { - return button.trigger("click"); + return cb(input.val()); } }; })(this)); body.append(input); button = $("" + caption + ""); button.on("click", (function(_this) { - return function() { + return function(e) { + _this.verifyEvent(button, e); cb(input.val()); return false; }; @@ -1270,7 +1323,7 @@ jQuery.extend( jQuery.easing, }; Wrapper.prototype.actionReload = function(message) { - return this.reload(); + return this.reload(message.params[0]); }; Wrapper.prototype.reload = function(url_post) { @@ -1367,7 +1420,7 @@ jQuery.extend( jQuery.easing, })(this)), 1000); }; - Wrapper.prototype.onLoad = function(e) { + Wrapper.prototype.onPageLoad = function(e) { var ref; this.inner_loaded = true; if (!this.inner_ready) { @@ -1383,6 +1436,12 @@ jQuery.extend( jQuery.easing, } }; + Wrapper.prototype.onWrapperLoad = function() { + delete window.wrapper; + delete window.wrapper_key; + return $("#script_init").remove(); + }; + Wrapper.prototype.sendInner = function(message) { return this.inner.postMessage(message, '*'); }; @@ -1499,7 +1558,7 @@ jQuery.extend( jQuery.easing, value = this.toHtmlSafe(value); } else { value = String(value).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); - value = value.replace(/<([\/]{0,1}(br|b|u|i))>/g, "<$1>"); + value = value.replace(/<([\/]{0,1}(br|b|u|i|small))>/g, "<$1>"); } values[i] = value; } @@ -1528,18 +1587,6 @@ jQuery.extend( jQuery.easing, return false; }; - Wrapper.prototype.isProxyRequest = function() { - return window.location.pathname === "/"; - }; - - Wrapper.prototype.gotoSite = function(elem) { - var href; - href = $(elem).attr("href"); - if (this.isProxyRequest()) { - return $(elem).attr("href", "http://zero" + href); - } - }; - Wrapper.prototype.log = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; @@ -1569,3 +1616,56 @@ jQuery.extend( jQuery.easing, window.wrapper = new Wrapper(ws_url); }).call(this); + + + +/* ---- src/Ui/media/WrapperZeroFrame.coffee ---- */ + + +(function() { + var WrapperZeroFrame, + bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + WrapperZeroFrame = (function() { + function WrapperZeroFrame(wrapper) { + this.certSelectGotoSite = bind(this.certSelectGotoSite, this); + this.response = bind(this.response, this); + this.cmd = bind(this.cmd, this); + this.wrapperCmd = wrapper.cmd; + this.wrapperResponse = wrapper.ws.response; + console.log("WrapperZeroFrame", wrapper); + } + + WrapperZeroFrame.prototype.cmd = function(cmd, params, cb) { + if (params == null) { + params = {}; + } + if (cb == null) { + cb = null; + } + return this.wrapperCmd(cmd, params, cb); + }; + + WrapperZeroFrame.prototype.response = function(to, result) { + return this.wrapperResponse(to, result); + }; + + WrapperZeroFrame.prototype.isProxyRequest = function() { + return window.location.pathname === "/"; + }; + + WrapperZeroFrame.prototype.certSelectGotoSite = function(elem) { + var href; + href = $(elem).attr("href"); + if (this.isProxyRequest()) { + return $(elem).attr("href", "http://zero" + href); + } + }; + + return WrapperZeroFrame; + + })(); + + window.zeroframe = new WrapperZeroFrame(window.wrapper); + +}).call(this); \ No newline at end of file From dea669ac26eac0830a6ffa6da61ff922c32aeab3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 21 Feb 2018 03:15:16 +0100 Subject: [PATCH 0663/2570] Rev3335 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index d3d022c1..8085d5ce 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.2" - self.rev = 3329 + self.rev = 3335 self.argv = argv self.action = None self.config_file = "zeronet.conf" From bda31aea59fa5b694457d7abdb88ab3f9dfeb3bb Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 22 Feb 2018 23:34:18 +0100 Subject: [PATCH 0664/2570] Rev3337, Fix big site download button --- src/Config.py | 2 +- src/Ui/media/Loading.coffee | 7 ++++--- src/Ui/media/Wrapper.coffee | 2 +- src/Ui/media/all.js | 15 +++++++++------ 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/Config.py b/src/Config.py index 8085d5ce..9b650aae 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.2" - self.rev = 3335 + self.rev = 3337 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Ui/media/Loading.coffee b/src/Ui/media/Loading.coffee index d0604b6f..57b973bb 100644 --- a/src/Ui/media/Loading.coffee +++ b/src/Ui/media/Loading.coffee @@ -1,5 +1,5 @@ class Loading - constructor: -> + constructor: (@wrapper) -> if window.show_loadingscreen then @showScreen() @timer_hide = null @@ -27,7 +27,8 @@ class Loading if $(".console .button-setlimit").length == 0 # Not displaying it yet line = @printLine("Site size: #{parseInt(site_info.settings.size/1024/1024)}MB is larger than default allowed #{parseInt(site_info.size_limit)}MB", "warning") button = $("" + "Open site and set size limit to #{site_info.next_size_limit}MB" + "") - button.on "click", (-> return window.wrapper.setSizeLimit(site_info.next_size_limit) ) + button.on "click", => + return @wrapper.setSizeLimit(site_info.next_size_limit) line.after(button) setTimeout (=> @printLine('Ready.') @@ -67,4 +68,4 @@ class Loading -window.Loading = Loading \ No newline at end of file +window.Loading = Loading diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index 39e05dbb..584f1f0e 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -2,7 +2,7 @@ class Wrapper constructor: (ws_url) -> @log "Created!" - @loading = new Loading() + @loading = new Loading(@) @notifications = new Notifications($(".notifications")) @fixbutton = new Fixbutton() diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index 128d46df..08c21c28 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -574,7 +574,8 @@ jQuery.extend( jQuery.easing, var Loading; Loading = (function() { - function Loading() { + function Loading(wrapper) { + this.wrapper = wrapper; if (window.show_loadingscreen) { this.showScreen(); } @@ -610,9 +611,11 @@ jQuery.extend( jQuery.easing, if ($(".console .button-setlimit").length === 0) { line = this.printLine("Site size: " + (parseInt(site_info.settings.size / 1024 / 1024)) + "MB is larger than default allowed " + (parseInt(site_info.size_limit)) + "MB", "warning"); button = $("" + ("Open site and set size limit to " + site_info.next_size_limit + "MB") + ""); - button.on("click", (function() { - return window.wrapper.setSizeLimit(site_info.next_size_limit); - })); + button.on("click", (function(_this) { + return function() { + return _this.wrapper.setSizeLimit(site_info.next_size_limit); + }; + })(this)); line.after(button); return setTimeout(((function(_this) { return function() { @@ -680,6 +683,7 @@ jQuery.extend( jQuery.easing, }).call(this); + /* ---- src/Ui/media/Notifications.coffee ---- */ @@ -830,7 +834,7 @@ jQuery.extend( jQuery.easing, this.onMessageWebsocket = bind(this.onMessageWebsocket, this); this.verifyEvent = bind(this.verifyEvent, this); this.log("Created!"); - this.loading = new Loading(); + this.loading = new Loading(this); this.notifications = new Notifications($(".notifications")); this.fixbutton = new Fixbutton(); window.addEventListener("message", this.onMessageInner, false); @@ -1618,7 +1622,6 @@ jQuery.extend( jQuery.easing, }).call(this); - /* ---- src/Ui/media/WrapperZeroFrame.coffee ---- */ From 304d2f08748400ca37c1921f734ed25c03595a14 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 23 Feb 2018 02:25:50 +0100 Subject: [PATCH 0665/2570] Due to a bug it does not works on msgpack 0.5.5 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index eef988d0..a829cd1f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ gevent>=1.1.0 -msgpack-python>=0.4.4 +msgpack-python>=0.4.4,<0.5.5 From f8f47184748acc2de1147255b8911cc97cebb5f9 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 23 Feb 2018 15:43:21 +0100 Subject: [PATCH 0666/2570] List pip package versions after install --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 13ede246..26e70212 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ services: install: - pip install -U pip wheel - pip install -r requirements.txt + - pip list before_script: - openssl version -a script: From d7ae58347321a539b1637cd0c9b23422f05eddf9 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 23 Feb 2018 15:43:40 +0100 Subject: [PATCH 0667/2570] msgpack bug fixed in 0.5.6 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a829cd1f..eef988d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ gevent>=1.1.0 -msgpack-python>=0.4.4,<0.5.5 +msgpack-python>=0.4.4 From 357fd895bf2ac3ac6590dadddcb03b37a0f68838 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 23 Feb 2018 15:44:07 +0100 Subject: [PATCH 0668/2570] Add msgpack test --- src/Test/TestMsgpack.py | 46 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/Test/TestMsgpack.py diff --git a/src/Test/TestMsgpack.py b/src/Test/TestMsgpack.py new file mode 100644 index 00000000..3665a0a4 --- /dev/null +++ b/src/Test/TestMsgpack.py @@ -0,0 +1,46 @@ +import cStringIO as StringIO + +import msgpack +import pytest + +from Config import config +from util import StreamingMsgpack + + +class TestMsgpack: + test_data = {"cmd": "fileGet", "params": {"site": "1Site"}} + + def testUnpackinkg(self): + assert msgpack.unpackb(msgpack.packb(self.test_data)) == self.test_data + + @pytest.mark.parametrize("unpacker_class", [msgpack.Unpacker, msgpack.fallback.Unpacker]) + def testUnpacker(self, unpacker_class): + unpacker = unpacker_class() + + data = msgpack.packb(self.test_data) + data += msgpack.packb(self.test_data) + + messages = [] + for char in data: + unpacker.feed(char) + for message in unpacker: + messages.append(message) + + assert len(messages) == 2 + assert messages[0] == self.test_data + assert messages[0] == messages[1] + + def testStreaming(self): + f = StreamingMsgpack.FilePart("%s/users.json" % config.data_dir) + f.read_bytes = 10 + + data = {"cmd": "response", "params": f} + + out_buff = StringIO.StringIO() + StreamingMsgpack.stream(data, out_buff.write) + out_buff.seek(0) + + data_packb = {"cmd": "response", "params": open("%s/users.json" % config.data_dir).read(10)} + + out_buff.seek(0) + assert msgpack.unpackb(out_buff.read()) == data_packb From 9305612419ff1233e2062d844b9269c89702d94e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 23 Feb 2018 16:05:03 +0100 Subject: [PATCH 0669/2570] Use renamed msgpack pip package instead msgpack-python --- README.md | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bdbaae87..c36cc15a 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ set `ENABLE_TOR` environment variable to `true` (Default: `false`). E.g.: * `virtualenv env` * `source env/bin/activate` -* `pip install msgpack-python gevent` +* `pip install msgpack gevent` * `python2 zeronet.py` * Open http://127.0.0.1:43110/ in your browser diff --git a/requirements.txt b/requirements.txt index eef988d0..e5cfb71e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ gevent>=1.1.0 -msgpack-python>=0.4.4 +msgpack>=0.4.4 From 4b9455e84db1fe9ded78c5270549bae0f44a151a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 23 Feb 2018 16:26:27 +0100 Subject: [PATCH 0670/2570] Still not works on latest msgpack --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e5cfb71e..01e08f57 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ gevent>=1.1.0 -msgpack>=0.4.4 +msgpack>=0.4.4,<0.5.5 From f10233a0a600c187f053db8d29563de1034e8b10 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 27 Feb 2018 02:46:26 +0100 Subject: [PATCH 0671/2570] New msgpack compatible stream handling --- src/Connection/Connection.py | 126 ++++++++++++++++++++--------------- 1 file changed, 71 insertions(+), 55 deletions(-) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index e7c43de3..d6778690 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -163,7 +163,7 @@ class Connection(object): buff_len = 0 req_len = 0 - self.unpacker = msgpack.fallback.Unpacker() # Due memory problems of C version + unpacker_bytes = 0 try: while not self.closed: buff = self.sock.recv(64 * 1024) @@ -180,75 +180,86 @@ class Connection(object): if not self.unpacker: self.unpacker = msgpack.fallback.Unpacker() - self.unpacker.feed(buff) - buff = None - for message in self.unpacker: - try: - # Stats - self.incomplete_buff_recv = 0 - stat_key = message.get("cmd", "unknown") - if stat_key == "response" and "to" in message: - cmd_sent = self.waiting_requests.get(message["to"], {"cmd": "unknown"})["cmd"] - stat_key = "response: %s" % cmd_sent - self.server.stat_recv[stat_key]["bytes"] += req_len - self.server.stat_recv[stat_key]["num"] += 1 - if "stream_bytes" in message: - self.server.stat_recv[stat_key]["bytes"] += message["stream_bytes"] - req_len = 0 + unpacker_bytes = 0 - # Handle message - if "stream_bytes" in message: - self.handleStream(message) - else: - self.handleMessage(message) - except TypeError: - raise Exception("Invalid message type: %s" % type(message)) + self.unpacker.feed(buff) + unpacker_bytes += buff_len + + for message in self.unpacker: + if not type(message) is dict: + raise Exception( + "Invalid message type: %s, content: %r, buffer: %r" % + (type(message), message, buff[0:16]) + ) + + # Stats + self.incomplete_buff_recv = 0 + stat_key = message.get("cmd", "unknown") + if stat_key == "response" and "to" in message: + cmd_sent = self.waiting_requests.get(message["to"], {"cmd": "unknown"})["cmd"] + stat_key = "response: %s" % cmd_sent + if stat_key == "update": + stat_key = "update: %s" % message["params"]["site"] + self.server.stat_recv[stat_key]["bytes"] += req_len + self.server.stat_recv[stat_key]["num"] += 1 + if "stream_bytes" in message: + self.server.stat_recv[stat_key]["bytes"] += message["stream_bytes"] + req_len = 0 + + # Handle message + if "stream_bytes" in message: + buff_left = self.handleStream(message, self.unpacker, buff, unpacker_bytes) + self.unpacker = msgpack.fallback.Unpacker() + self.unpacker.feed(buff_left) + unpacker_bytes = len(buff_left) + if config.debug_socket: + self.log("Start new unpacker with buff_left: %r" % buff_left) + break + else: + self.handleMessage(message) message = None - except Exception, err: + except Exception as err: if not self.closed: self.log("Socket error: %s" % Debug.formatException(err)) - self.close("MessageLoop ended") # MessageLoop ended, close connection + self.server.stat_recv["error: %s" % err]["bytes"] += req_len + self.server.stat_recv["error: %s" % err]["num"] += 1 + self.close("MessageLoop ended (closed: %s)" % self.closed) # MessageLoop ended, close connection # Stream socket directly to a file - def handleStream(self, message): - - read_bytes = message["stream_bytes"] # Bytes left we have to read from socket - # Check if the unpacker has something left in buffer - if hasattr(self.unpacker, "_buffer"): # New version of msgpack - bytes_buffer_left = len(self.unpacker._buffer) - self.unpacker.tell() - else: - bytes_buffer_left = self.unpacker._fb_buf_n - self.unpacker._fb_buf_o - - extradata_len = min(bytes_buffer_left, read_bytes) - if extradata_len: - buff = self.unpacker.read_bytes(extradata_len) - # Get rid of extra data from buffer - if hasattr(self.unpacker, "_consume"): - self.unpacker._consume() - else: - self.unpacker._fb_consume() - else: - buff = "" - + def handleStream(self, message, unpacker, buff, unpacker_bytes): + stream_bytes_left = message["stream_bytes"] file = self.waiting_streams[message["to"]] - if buff: - read_bytes -= len(buff) - file.write(buff) + + if "tell" in dir(unpacker): + unpacker_unprocessed_bytes = unpacker_bytes - unpacker.tell() + else: + unpacker_unprocessed_bytes = unpacker._fb_buf_n - unpacker._fb_buf_o + + if unpacker_unprocessed_bytes: # Found stream bytes in unpacker + unpacker_stream_bytes = min(unpacker_unprocessed_bytes, stream_bytes_left) + buff_stream_start = len(buff) - unpacker_unprocessed_bytes + file.write(buff[buff_stream_start:buff_stream_start + unpacker_stream_bytes]) + stream_bytes_left -= unpacker_stream_bytes + else: + unpacker_stream_bytes = 0 if config.debug_socket: - self.log("Starting stream %s: %s bytes (%s from unpacker)" % (message["to"], message["stream_bytes"], len(buff))) + self.log( + "Starting stream %s: %s bytes (%s from unpacker, buff size: %s, unprocessed: %s)" % + (message["to"], message["stream_bytes"], unpacker_stream_bytes, len(buff), unpacker_unprocessed_bytes) + ) try: while 1: - if read_bytes <= 0: + if stream_bytes_left <= 0: break - buff = self.sock.recv(min(64 * 1024, read_bytes)) - if not buff: + stream_buff = self.sock.recv(min(64 * 1024, stream_bytes_left)) + if not stream_buff: break - buff_len = len(buff) - read_bytes -= buff_len - file.write(buff) + buff_len = len(stream_buff) + stream_bytes_left -= buff_len + file.write(stream_buff) # Statistics self.last_recv_time = time.time() @@ -266,6 +277,11 @@ class Connection(object): del self.waiting_streams[message["to"]] del self.waiting_requests[message["to"]] + if unpacker_stream_bytes: + return buff[buff_stream_start + unpacker_stream_bytes:] + else: + return "" + # My handshake info def getHandshakeInfo(self): # No TLS for onion connections From af57083afda5f1b87fa0c73e7afd312299c21fdd Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 27 Feb 2018 02:46:56 +0100 Subject: [PATCH 0672/2570] Handle ip change on onion connection correctly --- src/Connection/Connection.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index d6778690..6329892d 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -162,8 +162,8 @@ class Connection(object): self.connected = True buff_len = 0 req_len = 0 - unpacker_bytes = 0 + try: while not self.closed: buff = self.sock.recv(64 * 1024) @@ -330,7 +330,11 @@ class Connection(object): self.port = handshake["fileserver_port"] # Set peer fileserver port if handshake.get("onion") and not self.ip.endswith(".onion"): # Set incoming connection's onion address + if self.server.ips.get(self.ip) == self: + del self.server.ips[self.ip] self.ip = handshake["onion"] + ".onion" + self.log("Changing ip to %s" % self.ip) + self.server.ips[self.ip] = self self.updateName() # Check if we can encrypt the connection From bca5d8a6c53d00fa51044becd9125dcacc4382b5 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 27 Feb 2018 02:48:15 +0100 Subject: [PATCH 0673/2570] Use msgpack pip package everywhere --- Dockerfile | 2 +- README-ru.md | 2 +- README-zh-cn.md | 2 +- Vagrantfile | 2 +- src/Connection/ConnectionServer.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index b0695b47..bbfa08a0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ ENV HOME /root #Install ZeroNet RUN apk --update upgrade \ && apk --no-cache --no-progress add musl-dev gcc python python-dev py2-pip tor \ - && pip install gevent msgpack-python \ + && pip install gevent msgpack \ && apk del musl-dev gcc python-dev py2-pip \ && rm -rf /var/cache/apk/* /tmp/* /var/tmp/* \ && echo "ControlPort 9051" >> /etc/tor/torrc \ diff --git a/README-ru.md b/README-ru.md index ba5add9b..69c1c5b9 100644 --- a/README-ru.md +++ b/README-ru.md @@ -138,7 +138,7 @@ article](https://wiki.archlinux.org/index.php/ZeroNet) для дальнейше * `virtualenv env` * `source env/bin/activate` -* `pip install msgpack-python gevent` +* `pip install msgpack gevent` * `python2 zeronet.py` * Откройте http://127.0.0.1:43110/ в вашем браузере. diff --git a/README-zh-cn.md b/README-zh-cn.md index e7f142b7..bae145c8 100644 --- a/README-zh-cn.md +++ b/README-zh-cn.md @@ -112,7 +112,7 @@ * `virtualenv env` * `source env/bin/activate` -* `pip install msgpack-python gevent` +* `pip install msgpack gevent` * `python2 zeronet.py` * 在你的浏览器中打开 http://127.0.0.1:43110/ diff --git a/Vagrantfile b/Vagrantfile index 6c4da894..24fe0c45 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -40,6 +40,6 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| config.vm.provision "shell", inline: "sudo apt-get install msgpack-python python-gevent python-pip python-dev -y" config.vm.provision "shell", - inline: "sudo pip install msgpack-python --upgrade" + inline: "sudo pip install msgpack --upgrade" end diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index e27ccaef..2664ba6a 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -56,7 +56,7 @@ class ConnectionServer(object): # Check msgpack version if msgpack.version[0] == 0 and msgpack.version[1] < 4: self.log.error( - "Error: Unsupported msgpack version: %s (<0.4.0), please run `sudo apt-get install python-pip; sudo pip install msgpack-python --upgrade`" % + "Error: Unsupported msgpack version: %s (<0.4.0), please run `sudo apt-get install python-pip; sudo pip install msgpack --upgrade`" % str(msgpack.version) ) sys.exit(0) From 4859ffdf54aae3b2005e57de19c3cec4d01b09df Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 27 Feb 2018 02:48:34 +0100 Subject: [PATCH 0674/2570] We are compatible with every msgpack version again --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 01e08f57..e5cfb71e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ gevent>=1.1.0 -msgpack>=0.4.4,<0.5.5 +msgpack>=0.4.4 From 4c7013644f22b31ad6e7e387c306971008a8ef4d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 27 Feb 2018 02:49:04 +0100 Subject: [PATCH 0675/2570] Rev3340 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 9b650aae..c5a70cb9 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.2" - self.rev = 3337 + self.rev = 3340 self.argv = argv self.action = None self.config_file = "zeronet.conf" From b55832df34eb5d58021f8c6379d632266f7f777c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 6 Mar 2018 11:51:49 +0100 Subject: [PATCH 0676/2570] Fix local peer discovery SO_REUSEPORT exception on older kernels --- plugins/AnnounceLocal/BroadcastServer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/AnnounceLocal/BroadcastServer.py b/plugins/AnnounceLocal/BroadcastServer.py index 6f7db938..ec47a0d8 100644 --- a/plugins/AnnounceLocal/BroadcastServer.py +++ b/plugins/AnnounceLocal/BroadcastServer.py @@ -23,7 +23,10 @@ class BroadcastServer(object): sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) if hasattr(socket, 'SO_REUSEPORT'): - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + 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): From b0a8c4d27878d0a2d64b138777bbbda5f6ce0c6a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 6 Mar 2018 11:58:56 +0100 Subject: [PATCH 0677/2570] Change http headers from list to dict --- plugins/Bigfile/BigfilePlugin.py | 8 ++-- plugins/Mute/MutePlugin.py | 4 +- plugins/disabled-Multiuser/MultiuserPlugin.py | 4 +- src/Ui/UiRequest.py | 39 +++++++++---------- 4 files changed, 27 insertions(+), 28 deletions(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index de74d645..3d1382cc 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -44,10 +44,10 @@ class UiRequestPlugin(object): 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.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 diff --git a/plugins/Mute/MutePlugin.py b/plugins/Mute/MutePlugin.py index e30a6c38..dfae5dfa 100644 --- a/plugins/Mute/MutePlugin.py +++ b/plugins/Mute/MutePlugin.py @@ -143,8 +143,8 @@ class UiRequestPlugin(object): if address in site_blacklist: site = self.server.site_manager.get(config.homepage) if not extra_headers: - extra_headers = [] - self.sendHeader(extra_headers=extra_headers[:]) + extra_headers = {} + self.sendHeader(extra_headers=extra_headers) return iter([super(UiRequestPlugin, self).renderWrapper( site, path, "uimedia/plugins/mute/blacklisted.html?address=" + address, "Blacklisted site", extra_headers, show_loadingscreen=False diff --git a/plugins/disabled-Multiuser/MultiuserPlugin.py b/plugins/disabled-Multiuser/MultiuserPlugin.py index 65e1a6b2..e9fd4533 100644 --- a/plugins/disabled-Multiuser/MultiuserPlugin.py +++ b/plugins/disabled-Multiuser/MultiuserPlugin.py @@ -48,8 +48,8 @@ class UiRequestPlugin(object): if user_created: if not extra_headers: - extra_headers = [] - extra_headers.append(('Set-Cookie', "master_address=%s;path=/;max-age=2592000;" % user.master_address)) # = 30 days + extra_headers = {} + extra_headers['Set-Cookie'] = "master_address=%s;path=/;max-age=2592000;" % user.master_address # = 30 days loggedin = self.get.get("login") == "done" diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 65cd3348..161e4d78 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -83,7 +83,7 @@ class UiRequest(object): else: content_type = self.getContentType(path) - extra_headers = [("Access-Control-Allow-Origin", "null")] + extra_headers = {"Access-Control-Allow-Origin": "null"} self.sendHeader(content_type=content_type, extra_headers=extra_headers) return "" @@ -205,21 +205,21 @@ class UiRequest(object): # Send response headers def sendHeader(self, status=200, content_type="text/html", noscript=False, extra_headers=[]): - headers = [] - headers.append(("Version", "HTTP/1.1")) - headers.append(("Connection", "Keep-Alive")) - headers.append(("Keep-Alive", "max=25, timeout=30")) - headers.append(("X-Frame-Options", "SAMEORIGIN")) + headers = {} + headers["Version"] = "HTTP/1.1" + headers["Connection"] = "Keep-Alive" + headers["Keep-Alive"] = "max=25, timeout=30" + headers["X-Frame-Options"] = "SAMEORIGIN" if content_type != "text/html" and self.env.get("HTTP_REFERER") and self.isSameOrigin(self.getReferer(), self.getRequestUrl()): - headers.append(("Access-Control-Allow-Origin", "*")) # Allow load font files from css + headers["Access-Control-Allow-Origin"] = "*" # Allow load font files from css if noscript: - headers.append(("Content-Security-Policy", "default-src 'none'; sandbox allow-top-navigation allow-forms; img-src 'self'; font-src 'self'; media-src 'self'; style-src 'self' 'unsafe-inline';")) + headers["Content-Security-Policy"] = "default-src 'none'; sandbox allow-top-navigation allow-forms; img-src 'self'; font-src 'self'; media-src 'self'; style-src 'self' 'unsafe-inline';" if self.env["REQUEST_METHOD"] == "OPTIONS": # Allow json access - headers.append(("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Cookie, Range")) - headers.append(("Access-Control-Allow-Credentials", "true")) + headers["Access-Control-Allow-Headers"] = "Origin, X-Requested-With, Content-Type, Accept, Cookie, Range" + headers["Access-Control-Allow-Credentials"] = "true" if content_type == "text/html": content_type = "text/html; charset=utf-8" @@ -228,7 +228,7 @@ class UiRequest(object): # Download instead of display file types that can be dangerous if re.findall("/svg|/xml|/x-shockwave-flash|/pdf", content_type): - headers.append(("Content-Disposition", "attachment")) + headers["Content-Disposition"] = "attachment" cacheable_type = ( content_type == "text/css" or content_type.startswith("image") or content_type.startswith("video") or @@ -236,13 +236,12 @@ class UiRequest(object): ) if status in (200, 206) and cacheable_type: # Cache Css, Js, Image files for 10min - headers.append(("Cache-Control", "public, max-age=600")) # Cache 10 min + headers["Cache-Control"] = "public, max-age=600" # Cache 10 min else: - headers.append(("Cache-Control", "no-cache, no-store, private, must-revalidate, max-age=0")) # No caching at all - headers.append(("Content-Type", content_type)) - for extra_header in extra_headers: - headers.append(extra_header) - return self.start_response(status_texts[status], headers) + headers["Cache-Control"] = "no-cache, no-store, private, must-revalidate, max-age=0" # No caching at all + headers["Content-Type"] = content_type + headers.update(extra_headers) + return self.start_response(status_texts[status], headers.items()) # Renders a template def render(self, template_path, *args, **kwargs): @@ -262,7 +261,7 @@ class UiRequest(object): # Render a file from media with iframe site wrapper def actionWrapper(self, path, extra_headers=None): if not extra_headers: - extra_headers = [] + extra_headers = {} match = re.match("/(?P
[A-Za-z0-9\._-]+)(?P/.*|$)", path) if match: @@ -295,7 +294,7 @@ class UiRequest(object): if not site: return False - self.sendHeader(extra_headers=extra_headers[:]) + self.sendHeader(extra_headers=extra_headers) return iter([self.renderWrapper(site, path, inner_path, title, extra_headers)]) # Make response be sent at once (see https://github.com/HelloZeroNet/ZeroNet/issues/1092) @@ -570,7 +569,7 @@ class UiRequest(object): status = 200 if header_allow_ajax: extra_headers["Access-Control-Allow-Origin"] = "null" - self.sendHeader(status, content_type=content_type, noscript=header_noscript, extra_headers=extra_headers.items()) + self.sendHeader(status, content_type=content_type, noscript=header_noscript, extra_headers=extra_headers) if self.env["REQUEST_METHOD"] != "OPTIONS": if not file_obj: file_obj = open(file_path, "rb") From 3e970df09b1a469929c6db17bcbecc2a57c97461 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 6 Mar 2018 12:00:30 +0100 Subject: [PATCH 0678/2570] Simple replace wrapper template variables instead of python formatting to allow use braces --- src/Ui/UiRequest.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 161e4d78..a2f1be4c 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -245,8 +245,10 @@ class UiRequest(object): # Renders a template def render(self, template_path, *args, **kwargs): - template = open(template_path).read().decode("utf8") - return template.format(**kwargs).encode("utf8") + template = open(template_path).read() + for key, val in kwargs.items(): + template = template.replace("{%s}" % key, "%s" % val) + return template # - Actions - From e96dd14e0d0de7abe9ef87c48dac1c2366817be1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 6 Mar 2018 12:01:21 +0100 Subject: [PATCH 0679/2570] Display wrapper security errors and fix Firefox navigation blank pages --- src/Ui/template/wrapper.html | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Ui/template/wrapper.html b/src/Ui/template/wrapper.html index b91f2454..61efbd26 100644 --- a/src/Ui/template/wrapper.html +++ b/src/Ui/template/wrapper.html @@ -16,9 +16,19 @@
From 9dabd1f34402be6c5375e69210e684aa1e3d3f9d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 6 Mar 2018 12:01:39 +0100 Subject: [PATCH 0680/2570] Wrapper escape apos characters --- src/Ui/media/Wrapper.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index 584f1f0e..d2a7ac6a 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -531,7 +531,7 @@ class Wrapper if value instanceof Array value = @toHtmlSafe(value) else - value = String(value).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') # Escape + value = String(value).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''') # Escape dangerous characters value = value.replace(/<([\/]{0,1}(br|b|u|i|small))>/g, "<$1>") # Unescape b, i, u, br tags values[i] = value return values From e93b5c3c1c971c1bebfa5dcce994e7060fc0a88c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 6 Mar 2018 12:02:39 +0100 Subject: [PATCH 0681/2570] Create wrapper html tags based on attributes instead of raw html --- src/Ui/media/Wrapper.coffee | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index d2a7ac6a..5f0cfeb6 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -41,6 +41,7 @@ class Wrapper $("#inner-iframe").focus() + verifyEvent: (allowed_target, e) => if not e.originalEvent.isTrusted throw "Event not trusted" @@ -239,12 +240,13 @@ class Wrapper body = $(""+message.params[1]+"") @notifications.add("notification-#{message.id}", message.params[0], body, message.params[2]) - displayConfirm: (message, captions, cb) -> - body = $(""+message+"") + displayConfirm: (body, captions, cb) -> + body = $(""+body+"") buttons = $("") if captions not instanceof Array then captions = [captions] # Convert to list if necessary for caption, i in captions - button = $("#{caption}") # Add confirm button + button = $("", {href: "#" + caption, class: "button button-confirm button-#{caption} button-#{i+1}", "data-value": i + 1}) # Add confirm button + button.text(caption) ((button) => button.on "click", (e) => @verifyEvent button, e @@ -268,17 +270,17 @@ class Wrapper displayPrompt: (message, type, caption, placeholder, cb) -> - body = $(""+message+"") + body = $("").text(message) placeholder ?= "" - input = $("") # Add input + input = $("", {type: type, class: "input button-#{type}", placeholder: placeholder}) # Add input input.on "keyup", (e) => # Send on enter @verifyEvent input, e if e.keyCode == 13 cb input.val() # Response to confirm body.append(input) - button = $("#{caption}") # Add confirm button + button = $("", {href: "#" + caption, class: "button button-#{caption}"}).text(caption) # Add confirm button button.on "click", (e) => # Response on button click @verifyEvent button, e cb input.val() From 16efba2b911db9794dbb4fac3bc4bc3210b9abfc Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 6 Mar 2018 12:03:10 +0100 Subject: [PATCH 0682/2570] Merge wrapper js --- src/Ui/media/all.js | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index 08c21c28..aae99e56 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -683,7 +683,6 @@ jQuery.extend( jQuery.easing, }).call(this); - /* ---- src/Ui/media/Notifications.coffee ---- */ @@ -784,6 +783,13 @@ jQuery.extend( jQuery.easing, return _this.close(elem); }; })(this)); + $("input", elem).on("keyup", (function(_this) { + return function(e) { + if (e.keyCode === 13) { + return _this.close(elem); + } + }; + })(this)); return elem; }; @@ -1152,9 +1158,9 @@ jQuery.extend( jQuery.easing, return this.notifications.add("notification-" + message.id, message.params[0], body, message.params[2]); }; - Wrapper.prototype.displayConfirm = function(message, captions, cb) { - var body, button, buttons, caption, fn, i, j, len; - body = $("" + message + ""); + Wrapper.prototype.displayConfirm = function(body, captions, cb) { + var button, buttons, caption, fn, i, j, len; + body = $("" + body + ""); buttons = $(""); if (!(captions instanceof Array)) { captions = [captions]; @@ -1170,7 +1176,12 @@ jQuery.extend( jQuery.easing, })(this); for (i = j = 0, len = captions.length; j < len; i = ++j) { caption = captions[i]; - button = $("" + caption + ""); + button = $("", { + href: "#" + caption, + "class": "button button-confirm button-" + caption + " button-" + (i + 1), + "data-value": i + 1 + }); + button.text(caption); fn(button); buttons.append(button); } @@ -1205,11 +1216,15 @@ jQuery.extend( jQuery.easing, Wrapper.prototype.displayPrompt = function(message, type, caption, placeholder, cb) { var body, button, input; - body = $("" + message + ""); + body = $("").text(message); if (placeholder == null) { placeholder = ""; } - input = $(""); + input = $("", { + type: type, + "class": "input button-" + type, + placeholder: placeholder + }); input.on("keyup", (function(_this) { return function(e) { _this.verifyEvent(input, e); @@ -1219,7 +1234,10 @@ jQuery.extend( jQuery.easing, }; })(this)); body.append(input); - button = $("" + caption + ""); + button = $("", { + href: "#" + caption, + "class": "button button-" + caption + }).text(caption); button.on("click", (function(_this) { return function(e) { _this.verifyEvent(button, e); @@ -1561,7 +1579,7 @@ jQuery.extend( jQuery.easing, if (value instanceof Array) { value = this.toHtmlSafe(value); } else { - value = String(value).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + value = String(value).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); value = value.replace(/<([\/]{0,1}(br|b|u|i|small))>/g, "<$1>"); } values[i] = value; @@ -1622,6 +1640,7 @@ jQuery.extend( jQuery.easing, }).call(this); + /* ---- src/Ui/media/WrapperZeroFrame.coffee ---- */ From d2ea8a001f70fff585839b9438b415e7ecd0a264 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 6 Mar 2018 12:04:49 +0100 Subject: [PATCH 0683/2570] Remove unused private key input field from sidebar --- plugins/Sidebar/SidebarPlugin.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index ecd688dc..6e89f35b 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -387,7 +387,6 @@ class UiWebsocketPlugin(object): def sidebarRenderOwnSettings(self, body, site): title = cgi.escape(site.content_manager.contents.get("content.json", {}).get("title", ""), True) description = cgi.escape(site.content_manager.contents.get("content.json", {}).get("description", ""), True) - privatekey = cgi.escape(self.user.getSiteData(site.address, create=False).get("privatekey", "")) body.append(_(u"""
  • @@ -400,11 +399,6 @@ class UiWebsocketPlugin(object):
  • -
  • - - -
  • -
  • {_[Save site settings]}
  • From 666ef499248f37f7ec962c03664ecd74beb86b41 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 6 Mar 2018 12:05:08 +0100 Subject: [PATCH 0684/2570] sidebarGetHtmlTag is an admin command --- plugins/Sidebar/SidebarPlugin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index 6e89f35b..fe3bb720 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -429,6 +429,10 @@ class UiWebsocketPlugin(object): body.append("") def actionSidebarGetHtmlTag(self, to): + permissions = self.getPermissions(to) + if "ADMIN" not in permissions: + return self.response(to, "You don't have permission to run this command") + site = self.site body = [] From b9e79f8d98ae51d9240b4a9c6197d29f7d64370f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 6 Mar 2018 12:05:58 +0100 Subject: [PATCH 0685/2570] Fix sidebar navigation error --- plugins/Sidebar/media/Sidebar.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee index f78624bd..373b8d15 100644 --- a/plugins/Sidebar/media/Sidebar.coffee +++ b/plugins/Sidebar/media/Sidebar.coffee @@ -350,8 +350,8 @@ class Sidebar extends Class # 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("#button-sign-publish-menu").removeClass("visible") + @tag?.find(".contents + .flex").removeClass("sign-publish-flex") menu = new Menu(@tag.find("#menu-sign-publish")) menu.elem.css("margin-top", "-130px") # Open upwards From 0d1a5846a13b511d4af24e969c6f811f221ab67e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 6 Mar 2018 12:06:21 +0100 Subject: [PATCH 0686/2570] Merge sidebar js --- plugins/Sidebar/media/all.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/Sidebar/media/all.js b/plugins/Sidebar/media/all.js index 02e4f5fb..ceef0b25 100644 --- a/plugins/Sidebar/media/all.js +++ b/plugins/Sidebar/media/all.js @@ -687,8 +687,11 @@ window.initScrollable = function () { })(this)); $(document).on("click touchend", (function(_this) { return function() { - _this.tag.find("#button-sign-publish-menu").removeClass("visible"); - return _this.tag.find(".contents + .flex").removeClass("sign-publish-flex"); + var ref, ref1; + if ((ref = _this.tag) != null) { + ref.find("#button-sign-publish-menu").removeClass("visible"); + } + return (ref1 = _this.tag) != null ? ref1.find(".contents + .flex").removeClass("sign-publish-flex") : void 0; }; })(this)); menu = new Menu(this.tag.find("#menu-sign-publish")); From 982fb27f58404adcb27a9efaa2db02e1f8484dc3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 6 Mar 2018 12:07:07 +0100 Subject: [PATCH 0687/2570] Close notification on input enter --- src/Ui/media/Notifications.coffee | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Ui/media/Notifications.coffee b/src/Ui/media/Notifications.coffee index c06a95dc..393d5a44 100644 --- a/src/Ui/media/Notifications.coffee +++ b/src/Ui/media/Notifications.coffee @@ -69,6 +69,11 @@ class Notifications $(".select", elem).on "click", => @close elem + # Input enter + $("input", elem).on "keyup", (e) => + if e.keyCode == 13 + @close elem + return elem @@ -81,4 +86,4 @@ class Notifications console.log "[Notifications]", args... -window.Notifications = Notifications \ No newline at end of file +window.Notifications = Notifications From e87df8a57a90822c26db9e395aea5d542321c552 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 6 Mar 2018 12:09:39 +0100 Subject: [PATCH 0688/2570] NOSANDBOX is as dangerous as ADMIN command --- 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 1816df05..5e6a41b9 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -759,7 +759,7 @@ class UiWebsocket(object): if permission == "ADMIN": self.response(to, _["Modify your client's configuration and access all site"] + " " + _["(Dangerous!)"] + "") elif permission == "NOSANDBOX": - self.response(to, _["Full access to site data, cookie and local storage of all site."]) + self.response(to, _["Modify your client's configuration and access all site"] + " " + _["(Dangerous!)"] + "") else: self.response(to, "") From 7097859b795e4f99c7437867eb74d69d6dd89529 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 6 Mar 2018 12:09:54 +0100 Subject: [PATCH 0689/2570] Rev3350 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index c5a70cb9..0e059186 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.2" - self.rev = 3340 + self.rev = 3350 self.argv = argv self.action = None self.config_file = "zeronet.conf" From e03731fd24b0c44163353a76be3d40bc23f13b86 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 7 Mar 2018 03:10:13 +0100 Subject: [PATCH 0690/2570] Allow html formatting in wrapper prompt display --- src/Ui/media/Wrapper.coffee | 2 +- src/Ui/media/all.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index 5f0cfeb6..657d1887 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -270,7 +270,7 @@ class Wrapper displayPrompt: (message, type, caption, placeholder, cb) -> - body = $("").text(message) + body = $("").html(message) placeholder ?= "" input = $("", {type: type, class: "input button-#{type}", placeholder: placeholder}) # Add input diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index aae99e56..ead47793 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -1216,7 +1216,7 @@ jQuery.extend( jQuery.easing, Wrapper.prototype.displayPrompt = function(message, type, caption, placeholder, cb) { var body, button, input; - body = $("").text(message); + body = $("").html(message); if (placeholder == null) { placeholder = ""; } From 70489871573e1fbcd0c777c02488c4ea7ffd8988 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 7 Mar 2018 10:23:13 +0100 Subject: [PATCH 0691/2570] Rev3351, Fix sites with utf8 title --- src/Config.py | 2 +- src/Ui/UiRequest.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index 0e059186..70eb6d45 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.2" - self.rev = 3350 + self.rev = 3351 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index a2f1be4c..58ea6aca 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -248,7 +248,7 @@ class UiRequest(object): template = open(template_path).read() for key, val in kwargs.items(): template = template.replace("{%s}" % key, "%s" % val) - return template + return template.encode("utf8") # - Actions - From e8a0d56ff8cc291f3a78c755e747f90161255d8b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 8 Mar 2018 14:18:55 +0100 Subject: [PATCH 0692/2570] Support fileList command of archives --- plugins/FilePack/FilePackPlugin.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/plugins/FilePack/FilePackPlugin.py b/plugins/FilePack/FilePackPlugin.py index c8ac5b0a..b6d0e283 100644 --- a/plugins/FilePack/FilePackPlugin.py +++ b/plugins/FilePack/FilePackPlugin.py @@ -16,7 +16,7 @@ def closeArchive(archive_path): del archive_cache[archive_path] -def openArchive(archive_path, path_within): +def openArchive(archive_path): if archive_path not in archive_cache: if archive_path.endswith("tar.gz"): import tarfile @@ -30,7 +30,10 @@ def openArchive(archive_path, path_within): 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): + archive = openArchive(archive_path) if archive_path.endswith(".zip"): return archive.open(path_within) else: @@ -56,7 +59,7 @@ class UiRequestPlugin(object): if not result: return self.error404(path) try: - file = openArchive(archive_path, path_within) + file = openArchiveFile(archive_path, path_within) content_type = self.getContentType(file_path) self.sendHeader(200, content_type=content_type, noscript=kwargs.get("header_noscript", False)) return self.streamFile(file) @@ -84,7 +87,22 @@ class SiteStoragePlugin(object): def isFile(self, inner_path): if ".zip/" in inner_path or ".tar.gz/" in inner_path: match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))/(.*)", inner_path) - inner_archive_path, path_within = match.groups() - return super(SiteStoragePlugin, self).isFile(inner_archive_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 walk(self, inner_path): + if ".zip" in inner_path or ".tar.gz" in inner_path: + match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))(.*)", inner_path) + archive_inner_path, path_within = match.groups() + archive = openArchive(self.getPath(archive_inner_path)) + 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()] + return namelist + + else: + return super(SiteStoragePlugin, self).walk(inner_path) + From 9d1f491a899ff773f9e1ffed0be51e1d3f00de26 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 8 Mar 2018 14:19:42 +0100 Subject: [PATCH 0693/2570] More visible sidebar scrollbar --- plugins/Sidebar/media/Scrollbable.css | 4 ++-- plugins/Sidebar/media/all.css | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/Sidebar/media/Scrollbable.css b/plugins/Sidebar/media/Scrollbable.css index b11faea0..6e3e0b6a 100644 --- a/plugins/Sidebar/media/Scrollbable.css +++ b/plugins/Sidebar/media/Scrollbable.css @@ -25,7 +25,7 @@ position: absolute; width: 7px; border-radius: 5px; - background: #151515; + background: #3A3A3A; top: 0px; left: 395px; -webkit-transition: top .08s; @@ -41,4 +41,4 @@ -moz-user-select: none; -ms-user-select: none; user-select: none; -} \ No newline at end of file +} diff --git a/plugins/Sidebar/media/all.css b/plugins/Sidebar/media/all.css index 30ea5e60..eab5f992 100644 --- a/plugins/Sidebar/media/all.css +++ b/plugins/Sidebar/media/all.css @@ -54,7 +54,7 @@ position: absolute; width: 7px; -webkit-border-radius: 5px; -moz-border-radius: 5px; -o-border-radius: 5px; -ms-border-radius: 5px; border-radius: 5px ; - background: #151515; + background: #3A3A3A; top: 0px; left: 395px; -webkit-transition: top .08s; @@ -73,6 +73,7 @@ } + /* ---- plugins/Sidebar/media/Sidebar.css ---- */ From a51d794885b8d409ff3c5689344fc8f7c7bd1fe2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 8 Mar 2018 14:20:18 +0100 Subject: [PATCH 0694/2570] Rev3352 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 70eb6d45..536b61aa 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.2" - self.rev = 3351 + self.rev = 3352 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 645249afa9e8b796ae551ac623ad49d08f792031 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 8 Mar 2018 14:27:40 +0100 Subject: [PATCH 0695/2570] Rev3353, Pass arguments of storage walk, Email notify of build fail --- .travis.yml | 2 ++ plugins/FilePack/FilePackPlugin.py | 4 ++-- src/Config.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 26e70212..066b5531 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,4 +28,6 @@ cache: - $HOME/.cache/pip notifications: email: + recipients: + hello@zeronet.io on_success: change diff --git a/plugins/FilePack/FilePackPlugin.py b/plugins/FilePack/FilePackPlugin.py index b6d0e283..778aa39f 100644 --- a/plugins/FilePack/FilePackPlugin.py +++ b/plugins/FilePack/FilePackPlugin.py @@ -92,7 +92,7 @@ class SiteStoragePlugin(object): else: return super(SiteStoragePlugin, self).isFile(inner_path) - def walk(self, inner_path): + def walk(self, inner_path, *args, **kwags): if ".zip" in inner_path or ".tar.gz" in inner_path: match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))(.*)", inner_path) archive_inner_path, path_within = match.groups() @@ -104,5 +104,5 @@ class SiteStoragePlugin(object): return namelist else: - return super(SiteStoragePlugin, self).walk(inner_path) + return super(SiteStoragePlugin, self).walk(inner_path, *args, **kwags) diff --git a/src/Config.py b/src/Config.py index 536b61aa..9afdc4ba 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.2" - self.rev = 3352 + self.rev = 3353 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 738fd1a09b4d3a45f7514dede0720b533577027e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 9 Mar 2018 15:01:45 +0100 Subject: [PATCH 0696/2570] Rev3354, Fix ajax loading files from archives --- plugins/FilePack/FilePackPlugin.py | 10 +++++++++- src/Config.py | 2 +- src/Ui/UiRequest.py | 9 +++++---- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/plugins/FilePack/FilePackPlugin.py b/plugins/FilePack/FilePackPlugin.py index 778aa39f..a3cda1ab 100644 --- a/plugins/FilePack/FilePackPlugin.py +++ b/plugins/FilePack/FilePackPlugin.py @@ -58,10 +58,18 @@ class UiRequestPlugin(object): site.updateWebsocket(file_done=site.storage.getInnerPath(file_path)) if not result: return self.error404(path) + + 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) content_type = self.getContentType(file_path) - self.sendHeader(200, content_type=content_type, noscript=kwargs.get("header_noscript", False)) + 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" % err) diff --git a/src/Config.py b/src/Config.py index 9afdc4ba..0a58ac72 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.2" - self.rev = 3353 + self.rev = 3354 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 58ea6aca..991dd264 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -204,7 +204,7 @@ class UiRequest(object): return referer # Send response headers - def sendHeader(self, status=200, content_type="text/html", noscript=False, extra_headers=[]): + def sendHeader(self, status=200, content_type="text/html", noscript=False, allow_ajax=False, extra_headers=[]): headers = {} headers["Version"] = "HTTP/1.1" headers["Connection"] = "Keep-Alive" @@ -216,6 +216,9 @@ class UiRequest(object): if noscript: headers["Content-Security-Policy"] = "default-src 'none'; sandbox allow-top-navigation allow-forms; img-src 'self'; font-src 'self'; media-src 'self'; style-src 'self' 'unsafe-inline';" + if allow_ajax: + headers["Access-Control-Allow-Origin"] = "null" + if self.env["REQUEST_METHOD"] == "OPTIONS": # Allow json access headers["Access-Control-Allow-Headers"] = "Origin, X-Requested-With, Content-Type, Accept, Cookie, Range" @@ -569,9 +572,7 @@ class UiRequest(object): status = 206 else: status = 200 - if header_allow_ajax: - extra_headers["Access-Control-Allow-Origin"] = "null" - self.sendHeader(status, content_type=content_type, noscript=header_noscript, extra_headers=extra_headers) + self.sendHeader(status, content_type=content_type, noscript=header_noscript, allow_ajax=header_allow_ajax, extra_headers=extra_headers) if self.env["REQUEST_METHOD"] != "OPTIONS": if not file_obj: file_obj = open(file_path, "rb") From b1f16857fc6d6c0102d1f52f721611f2c44e4350 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 10 Mar 2018 01:59:55 +0100 Subject: [PATCH 0697/2570] Support per-site auto download bigfile size limit --- plugins/Bigfile/BigfilePlugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index 3d1382cc..5b40ece3 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -501,7 +501,8 @@ class WorkerManagerPlugin(object): if not self.site.storage.isFile(piecemap_inner_path): # Start download piecemap piecemap_task = super(WorkerManagerPlugin, self).addTask(piecemap_inner_path, priority=30) - if "|" not in inner_path and self.site.isDownloadable(inner_path) and file_info["size"] / 1024 / 1024 <= config.autodownload_bigfile_size_limit: + 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: From 96e9cc9c32fabf55c603270b795f14a422cf846f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 10 Mar 2018 02:00:12 +0100 Subject: [PATCH 0698/2570] Correct cli settings help --- plugins/Bigfile/BigfilePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index 5b40ece3..56321a27 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -694,6 +694,6 @@ class SitePlugin(object): 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 until this limit if help distribute option is checked', default=1, metavar="MB", type=int) + group.add_argument('--autodownload_bigfile_size_limit', help='Also download bigfiles smaller than this limit if help distribute option is checked', default=1, metavar="MB", type=int) return super(ConfigPlugin, self).createArguments() From bbb54d6a6ad4fdd30451f31c9098e01241f5d207 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 10 Mar 2018 02:00:51 +0100 Subject: [PATCH 0699/2570] Support per site max autodownload bigfile size limit changing via API --- plugins/Bigfile/BigfilePlugin.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index 56321a27..0fb3f08a 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -149,6 +149,14 @@ class UiWebsocketPlugin(object): "file_relative_path": file_relative_path } + 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") + @PluginManager.registerTo("ContentManager") class ContentManagerPlugin(object): From dd924f389f06b3b4f46b581f64acba781790c8b4 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 10 Mar 2018 02:01:49 +0100 Subject: [PATCH 0700/2570] Display and change per-site bigfile autodownload with sidebar --- plugins/Sidebar/media/Sidebar.coffee | 14 +++++++++----- plugins/Sidebar/media/Sidebar.css | 8 +++++++- plugins/Sidebar/media/all.css | 8 +++++++- plugins/Sidebar/media/all.js | 18 ++++++++++++------ 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee index 373b8d15..2f2b3747 100644 --- a/plugins/Sidebar/media/Sidebar.coffee +++ b/plugins/Sidebar/media/Sidebar.coffee @@ -249,7 +249,7 @@ class Sidebar extends Class @scrollable() # Re-calculate height when site admin opened or closed - @tag.find("#checkbox-owned").off("click touchend").on "click touchend", => + @tag.find("#checkbox-owned, #checkbox-autodownloadoptional").off("click touchend").on "click touchend", => setTimeout (=> @scrollable() ), 300 @@ -262,6 +262,14 @@ class Sidebar extends Class @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 + # Database reload @tag.find("#button-dbreload").off("click touchend").on "click touchend", => @wrapper.ws.cmd "dbReload", [], => @@ -327,10 +335,6 @@ class Sidebar extends Class @wrapper.ws.cmd "certSelect" return false - # Owned checkbox - @tag.find("#checkbox-owned").off("click touchend").on "click touchend", => - @wrapper.ws.cmd "siteSetOwned", [@tag.find("#checkbox-owned").is(":checked")] - # Save settings @tag.find("#button-settings").off("click touchend").on "click touchend", => @wrapper.ws.cmd "fileGet", "content.json", (res) => diff --git a/plugins/Sidebar/media/Sidebar.css b/plugins/Sidebar/media/Sidebar.css index 9f718b7c..895ba178 100644 --- a/plugins/Sidebar/media/Sidebar.css +++ b/plugins/Sidebar/media/Sidebar.css @@ -118,6 +118,12 @@ #checkbox-owned ~ .settings-owned { opacity: 0; max-height: 0px; transition: all 0.3s linear; overflow: hidden } #checkbox-owned:checked ~ .settings-owned { opacity: 1; max-height: 420px } +/* Settings autodownload */ + +.settings-autodownloadoptional { clear: both; box-sizing: border-box; padding-top: 0px; } +#checkbox-autodownloadoptional ~ .settings-autodownloadoptional { opacity: 0; max-height: 0px; transition: all 0.3s ease-in-out; overflow: hidden; } +#checkbox-autodownloadoptional:checked ~ .settings-autodownloadoptional { opacity: 1; max-height: 120px; padding-top: 30px; } + /* Globe */ .globe { width: 360px; height: 360px } .globe.loading { background: url(/uimedia/img/loading-circle.gif) center center no-repeat } @@ -154,4 +160,4 @@ /* Small screen */ @media screen and (max-width: 600px) { .sidebar .close { display: block } -} \ No newline at end of file +} diff --git a/plugins/Sidebar/media/all.css b/plugins/Sidebar/media/all.css index eab5f992..29398d36 100644 --- a/plugins/Sidebar/media/all.css +++ b/plugins/Sidebar/media/all.css @@ -197,6 +197,12 @@ #checkbox-owned ~ .settings-owned { opacity: 0; max-height: 0px; -webkit-transition: all 0.3s linear; -moz-transition: all 0.3s linear; -o-transition: all 0.3s linear; -ms-transition: all 0.3s linear; transition: all 0.3s linear ; overflow: hidden } #checkbox-owned:checked ~ .settings-owned { opacity: 1; max-height: 420px } +/* Settings autodownload */ + +.settings-autodownloadoptional { clear: both; -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-top: 0px; } +#checkbox-autodownloadoptional ~ .settings-autodownloadoptional { opacity: 0; max-height: 0px; -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 ; overflow: hidden; } +#checkbox-autodownloadoptional:checked ~ .settings-autodownloadoptional { opacity: 1; max-height: 120px; padding-top: 30px; } + /* Globe */ .globe { width: 360px; height: 360px } .globe.loading { background: url(/uimedia/img/loading-circle.gif) center center no-repeat } @@ -233,4 +239,4 @@ /* Small screen */ @media screen and (max-width: 600px) { .sidebar .close { display: block } -} \ No newline at end of file +} diff --git a/plugins/Sidebar/media/all.js b/plugins/Sidebar/media/all.js index ceef0b25..391a0a1d 100644 --- a/plugins/Sidebar/media/all.js +++ b/plugins/Sidebar/media/all.js @@ -555,7 +555,7 @@ window.initScrollable = function () { var menu; this.log("Opened"); this.scrollable(); - this.tag.find("#checkbox-owned").off("click touchend").on("click touchend", (function(_this) { + this.tag.find("#checkbox-owned, #checkbox-autodownloadoptional").off("click touchend").on("click touchend", (function(_this) { return function() { return setTimeout((function() { return _this.scrollable(); @@ -573,6 +573,17 @@ window.initScrollable = function () { return false; }; })(this)); + this.tag.find("#button-autodownload_bigfile_size_limit").off("click touchend").on("click touchend", (function(_this) { + return function() { + _this.wrapper.ws.cmd("siteSetAutodownloadBigfileLimit", $("#input-autodownload_bigfile_size_limit").val(), function(res) { + if (res === "ok") { + _this.wrapper.notifications.add("done-bigfilelimit", "done", "Site bigfile auto download limit modified!", 5000); + } + return _this.updateHtmlTag(); + }); + return false; + }; + })(this)); this.tag.find("#button-dbreload").off("click touchend").on("click touchend", (function(_this) { return function() { _this.wrapper.ws.cmd("dbReload", [], function() { @@ -653,11 +664,6 @@ window.initScrollable = function () { return false; }; })(this)); - this.tag.find("#checkbox-owned").off("click touchend").on("click touchend", (function(_this) { - return function() { - return _this.wrapper.ws.cmd("siteSetOwned", [_this.tag.find("#checkbox-owned").is(":checked")]); - }; - })(this)); this.tag.find("#button-settings").off("click touchend").on("click touchend", (function(_this) { return function() { _this.wrapper.ws.cmd("fileGet", "content.json", function(res) { From 8ca368f9c7b7421e6025f38c1644fa5204175e71 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 10 Mar 2018 02:02:58 +0100 Subject: [PATCH 0701/2570] Bigfile autodownload size limit field to sidebar --- plugins/Sidebar/SidebarPlugin.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index fe3bb720..71563e1d 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -270,9 +270,18 @@ class UiWebsocketPlugin(object):
  • -
  • """)) + autodownload_bigfile_size_limit = int(site.settings.get("autodownload_bigfile_size_limit", config.autodownload_bigfile_size_limit)) + body.append(_(u""" +
    + + MB + {_[Set]} +
    + """)) + body.append("") + def sidebarRenderBadFiles(self, body, site): body.append(_(u"""
  • From 05a37b3a9c026c0c0978388a0a4ed083c57eca45 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 10 Mar 2018 02:03:12 +0100 Subject: [PATCH 0702/2570] Display local peers number in sidebar --- plugins/Sidebar/SidebarPlugin.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index 71563e1d..93c80cc2 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -69,6 +69,7 @@ class UiWebsocketPlugin(object): connected = len([peer for peer in site.peers.values() if peer.connection and peer.connection.connected]) connectable = len([peer_id for peer_id in site.peers.keys() if not peer_id.endswith(":0")]) onion = len([peer_id for peer_id in site.peers.keys() if ".onion" in peer_id]) + local = len([peer for peer in site.peers.values() if helper.isPrivateIp(peer.ip)]) peers_total = len(site.peers) # Add myself @@ -86,6 +87,11 @@ class UiWebsocketPlugin(object): else: percent_connectable = percent_connected = percent_onion = 0 + if local: + local_html = _(u"
  • {_[Local]}:{local}
  • ") + else: + local_html = "" + body.append(_(u"""
  • @@ -99,6 +105,7 @@ class UiWebsocketPlugin(object):
  • {_[Connected]}:{connected}
  • {_[Connectable]}:{connectable}
  • {_[Onion]}:{onion}
  • + {local_html}
  • {_[Total]}:{peers_total}
  • From ad23021b29cfeecc5c42f35f20e6731d1bb91706 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 10 Mar 2018 02:03:30 +0100 Subject: [PATCH 0703/2570] More correct title for bad files on sidebar --- plugins/Sidebar/SidebarPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index 93c80cc2..df93f46d 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -292,7 +292,7 @@ class UiWebsocketPlugin(object): def sidebarRenderBadFiles(self, body, site): body.append(_(u"""
  • - +
      """)) From 511731f0b46aaa3a94591a672e44deef525a6637 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 10 Mar 2018 02:04:14 +0100 Subject: [PATCH 0704/2570] Add current timestamp to handshake for future time consensus feature --- src/Connection/Connection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 6329892d..13f5bfa4 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -309,7 +309,8 @@ class Connection(object): "target_ip": self.ip, "rev": config.rev, "crypt_supported": crypt_supported, - "crypt": self.crypt + "crypt": self.crypt, + "time": time.time() } if self.target_onion: handshake["onion"] = self.target_onion From 42fd7b216d374fefef92b1c5c14fc8380a3b00e1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 10 Mar 2018 02:05:03 +0100 Subject: [PATCH 0705/2570] Don't allow siteSetAutodownloadBigfileLimit for proxies --- plugins/disabled-Multiuser/MultiuserPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/disabled-Multiuser/MultiuserPlugin.py b/plugins/disabled-Multiuser/MultiuserPlugin.py index e9fd4533..f9f16f55 100644 --- a/plugins/disabled-Multiuser/MultiuserPlugin.py +++ b/plugins/disabled-Multiuser/MultiuserPlugin.py @@ -97,7 +97,7 @@ class UiWebsocketPlugin(object): self.multiuser_denied_cmds = ( "siteDelete", "configSet", "serverShutdown", "serverUpdate", "siteClone", "siteSetOwned", "siteSetAutodownloadoptional", "dbReload", "dbRebuild", - "mergerSiteDelete", "siteSetLimit", + "mergerSiteDelete", "siteSetLimit", "siteSetAutodownloadBigfileLimit", "optionalLimitSet", "optionalHelp", "optionalHelpRemove", "optionalHelpAll", "optionalFilePin", "optionalFileUnpin", "optionalFileDelete", "muteAdd", "muteRemove", "blacklistAdd", "blacklistRemove" ) From fbc10b8e327798fc1bb7e2f804bef95d68c915bb Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 10 Mar 2018 02:06:09 +0100 Subject: [PATCH 0706/2570] Rev3356 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 0a58ac72..2f805a6b 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.2" - self.rev = 3354 + self.rev = 3356 self.argv = argv self.action = None self.config_file = "zeronet.conf" From a88f56bec34f5afaaeb8c456c556e55ec082e128 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Sun, 11 Mar 2018 11:14:17 +0300 Subject: [PATCH 0707/2570] Add 'Open site directory' button --- plugins/Sidebar/SidebarPlugin.py | 4 ++++ plugins/Sidebar/media/Sidebar.coffee | 4 ++++ plugins/Sidebar/media/all.js | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index ecd688dc..56289453 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -408,6 +408,10 @@ class UiWebsocketPlugin(object):
    • {_[Save site settings]}
    • + +
    • + {_[Open site directory]} +
    • """)) def sidebarRenderContents(self, body, site): diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee index f78624bd..32b88eb1 100644 --- a/plugins/Sidebar/media/Sidebar.coffee +++ b/plugins/Sidebar/media/Sidebar.coffee @@ -348,6 +348,10 @@ class Sidebar extends Class @updateHtmlTag() return false + # Open site directory + @tag.find("#button-directory").off("click touchend").on "click touchend", => + @wrapper.ws.cmd "serverShowdirectory", ["site", @wrapper.site_info.address] + # Sign and publish content.json $(document).on "click touchend", => @tag.find("#button-sign-publish-menu").removeClass("visible") diff --git a/plugins/Sidebar/media/all.js b/plugins/Sidebar/media/all.js index 02e4f5fb..5b084cac 100644 --- a/plugins/Sidebar/media/all.js +++ b/plugins/Sidebar/media/all.js @@ -685,6 +685,11 @@ window.initScrollable = function () { return false; }; })(this)); + this.tag.find("#button-directory").off("click touchend").on("click touchend", (function(_this) { + return function() { + return _this.wrapper.ws.cmd("serverShowdirectory", ["site", _this.wrapper.site_info.address]); + }; + })(this)); $(document).on("click touchend", (function(_this) { return function() { _this.tag.find("#button-sign-publish-menu").removeClass("visible"); From 98dc46e01a08ac6a8c7b420b53f12153192a2841 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Sun, 11 Mar 2018 11:21:54 +0300 Subject: [PATCH 0708/2570] 'siteInfo' can be used as 'actionAs' argument for CORS sites --- plugins/Cors/CorsPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Cors/CorsPlugin.py b/plugins/Cors/CorsPlugin.py index 63baed30..dd51690d 100644 --- a/plugins/Cors/CorsPlugin.py +++ b/plugins/Cors/CorsPlugin.py @@ -27,7 +27,7 @@ class UiWebsocketPlugin(object): if super(UiWebsocketPlugin, self).hasSitePermission(address, cmd=cmd): return True - if not "Cors:%s" % address in self.site.settings["permissions"] or cmd not in ["dbQuery", "userGetSettings"]: + if not "Cors:%s" % address in self.site.settings["permissions"] or cmd not in ["dbQuery", "userGetSettings", "siteInfo"]: return False else: return True From e7744eace96bf0551532aa7c3e5965ef826ba53b Mon Sep 17 00:00:00 2001 From: tangdou1 <35254744+tangdou1@users.noreply.github.com> Date: Tue, 13 Mar 2018 15:05:44 +0800 Subject: [PATCH 0709/2570] update zh.json update zh.json for incoming "Open site directory" function --- plugins/Sidebar/languages/zh.json | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/Sidebar/languages/zh.json b/plugins/Sidebar/languages/zh.json index 55ad3acd..ba4e4daa 100644 --- a/plugins/Sidebar/languages/zh.json +++ b/plugins/Sidebar/languages/zh.json @@ -56,6 +56,7 @@ "Site title": "站点标题", "Site description": "站点描述", "Save site settings": "保存站点设置", + "Open site directory": "打开所在文件夹", "Content publishing": "内容发布", "Choose": "选择", From 4d717a5c19434e1b4c2cad2c1d9e9a9edce64077 Mon Sep 17 00:00:00 2001 From: tangdou1 <35254744+tangdou1@users.noreply.github.com> Date: Tue, 13 Mar 2018 15:17:58 +0800 Subject: [PATCH 0710/2570] Update zh.json for new functions Update zh.json for new functions --"Sign and publish" and incoming function "Open site directory" --- plugins/Sidebar/languages/zh.json | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/Sidebar/languages/zh.json b/plugins/Sidebar/languages/zh.json index ba4e4daa..505c65d3 100644 --- a/plugins/Sidebar/languages/zh.json +++ b/plugins/Sidebar/languages/zh.json @@ -62,6 +62,7 @@ "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 )...", From 27a3414a27e7180dd4feea5a5ac7b096f4aa3977 Mon Sep 17 00:00:00 2001 From: tangdou1 <35254744+tangdou1@users.noreply.github.com> Date: Tue, 13 Mar 2018 16:12:04 +0800 Subject: [PATCH 0711/2570] Update zh-tw.json for new function --- plugins/Sidebar/languages/zh-tw.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/Sidebar/languages/zh-tw.json b/plugins/Sidebar/languages/zh-tw.json index 724de508..4610ac48 100644 --- a/plugins/Sidebar/languages/zh-tw.json +++ b/plugins/Sidebar/languages/zh-tw.json @@ -56,11 +56,13 @@ "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 )...", From 1189c76691603da276f60d902e0a54e59dac634c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 14 Mar 2018 22:21:45 +0100 Subject: [PATCH 0712/2570] Add existing bigfiles to piecefield if they were downloaded outside of ZeroNet --- plugins/Bigfile/BigfilePlugin.py | 18 ++++++++++++++---- src/Test/TestConnectionServer.py | 10 ++++++++-- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index 0fb3f08a..1e8610ab 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -324,6 +324,12 @@ class ContentManagerPlugin(object): # Only add to site size on first request if hash in self.hashfield: size = 0 + elif size > 1024 * 1024: + file_info = self.getFileInfo(inner_path) + if 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, size, own) @@ -399,12 +405,10 @@ class SiteStoragePlugin(object): del content self.onUpdated(inner_path) - def openBigfile(self, inner_path, prebuffer=0): + 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.needFile(inner_path, blocking=False) # Download piecemap file_path = self.getPath(inner_path) sha512 = file_info["sha512"] piece_num = int(math.ceil(float(file_info["size"]) / file_info["piece_size"])) @@ -419,7 +423,13 @@ class SiteStoragePlugin(object): else: self.log.debug("Creating bigfile: %s" % inner_path) self.createSparseFile(inner_path, file_info["size"], sha512) - self.piecefields[sha512].fromstring(piece_data * "0") + self.piecefields[sha512].fromstring("0" * piece_num) + 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) diff --git a/src/Test/TestConnectionServer.py b/src/Test/TestConnectionServer.py index 96a5557f..2bbf5762 100644 --- a/src/Test/TestConnectionServer.py +++ b/src/Test/TestConnectionServer.py @@ -2,9 +2,11 @@ import time import gevent import pytest +import mock from Crypt import CryptConnection from Connection import ConnectionServer +from Config import config @pytest.mark.usefixtures("resetSettings") @@ -15,7 +17,9 @@ class TestConnection: assert file_server != client # Connect to myself - connection = client.getConnection("127.0.0.1", 1544) + with mock.patch('Config.config.ip_local', return_value=[]): # SSL not used for local ips + connection = client.getConnection("127.0.0.1", 1544) + assert len(file_server.connections) == 1 assert connection.handshake assert connection.crypt @@ -35,7 +39,9 @@ class TestConnection: crypt_supported_bk = CryptConnection.manager.crypt_supported CryptConnection.manager.crypt_supported = [] - connection = client.getConnection("127.0.0.1", 1544) + print "---" + with mock.patch('Config.config.ip_local', return_value=[]): # SSL not used for local ips + connection = client.getConnection("127.0.0.1", 1544) assert len(file_server.connections) == 1 assert not connection.crypt From 660fc982ca910b75dc6a88c98c1fd58fde3a5095 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 14 Mar 2018 22:22:04 +0100 Subject: [PATCH 0713/2570] Fix packed file request --- plugins/FilePack/FilePackPlugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/FilePack/FilePackPlugin.py b/plugins/FilePack/FilePackPlugin.py index a3cda1ab..8e3bc94e 100644 --- a/plugins/FilePack/FilePackPlugin.py +++ b/plugins/FilePack/FilePackPlugin.py @@ -59,6 +59,7 @@ class UiRequestPlugin(object): if not result: return self.error404(path) + 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"]: From feb00d5b8ad36dd27920c637c77200ad50d32f88 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 14 Mar 2018 22:22:45 +0100 Subject: [PATCH 0714/2570] Option to force encryption to all outgoing connection --- src/Config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Config.py b/src/Config.py index 2f805a6b..9780462e 100644 --- a/src/Config.py +++ b/src/Config.py @@ -220,6 +220,7 @@ class Config(object): type='bool', choices=[True, False], default=use_openssl) self.parser.add_argument('--disable_db', help='Disable database updating', action='store_true') self.parser.add_argument('--disable_encryption', help='Disable connection encryption', action='store_true') + self.parser.add_argument('--force_encryption', help="Enforce encryption to all peer connections", action='store_true') self.parser.add_argument('--disable_sslcompression', help='Disable SSL compression to save memory', type='bool', choices=[True, False], default=True) self.parser.add_argument('--keep_ssl_cert', help='Disable new SSL cert generation on startup', action='store_true') From 878777476483e536e78780d325023b30ac339e22 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 14 Mar 2018 22:24:06 +0100 Subject: [PATCH 0715/2570] Use implicit ssl for connections --- src/Connection/Connection.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 13f5bfa4..06041fcb 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -121,11 +121,26 @@ class Connection(object): self.sock.connect((self.ip, int(self.port))) # Implicit SSL + should_encrypt = not self.ip.endswith(".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 "tsl-rsa" in CryptConnection.manager.crypt_supported: + try: + self.sock = CryptConnection.manager.wrapSocket(self.sock, "tls-rsa") + self.sock.do_handshake() + self.crypt = "tls-rsa" + self.sock_wrapped = True + except Exception, err: + if not config.force_encryption: + self.log("Crypt connection error: %s, adding ip %s as broken ssl." % (err, self.ip)) + self.server.broken_ssl_ips[self.ip] = True + self.sock.close() + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.connect((self.ip, int(self.port))) + # Detect protocol self.send({"cmd": "handshake", "req_id": 0, "params": self.getHandshakeInfo()}) From 2204e0cf9c5a972e6905cb4a57ad646d4f28f9cb Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 14 Mar 2018 22:24:38 +0100 Subject: [PATCH 0716/2570] Fix message parsing after data left in unpacker --- src/Connection/Connection.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 06041fcb..f33c7b66 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -200,7 +200,11 @@ class Connection(object): self.unpacker.feed(buff) unpacker_bytes += buff_len - for message in self.unpacker: + while True: + try: + message = self.unpacker.next() + except StopIteration: + break if not type(message) is dict: raise Exception( "Invalid message type: %s, content: %r, buffer: %r" % @@ -229,7 +233,6 @@ class Connection(object): unpacker_bytes = len(buff_left) if config.debug_socket: self.log("Start new unpacker with buff_left: %r" % buff_left) - break else: self.handleMessage(message) @@ -285,7 +288,7 @@ class Connection(object): self.log("Stream read error: %s" % Debug.formatException(err)) if config.debug_socket: - self.log("End stream %s" % message["to"]) + self.log("End stream %s, file pos: %s" % (message["to"], file.tell())) self.incomplete_buff_recv = 0 self.waiting_requests[message["to"]]["evt"].set(message) # Set the response to event From 4586d3be78c94a7fca9c83e8ab915e31391918e3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 14 Mar 2018 22:25:27 +0100 Subject: [PATCH 0717/2570] Send my local time as int in the handshake --- 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 f33c7b66..d3b8dcfc 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -328,7 +328,7 @@ class Connection(object): "rev": config.rev, "crypt_supported": crypt_supported, "crypt": self.crypt, - "time": time.time() + "time": int(time.time()) } if self.target_onion: handshake["onion"] = self.target_onion From ba12489c3491245004ee2d4a5cae5b208a8bea26 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 14 Mar 2018 22:25:53 +0100 Subject: [PATCH 0718/2570] Log my and remote handshake --- src/Connection/Connection.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index d3b8dcfc..2ea3e3db 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -335,9 +335,15 @@ class Connection(object): elif self.ip.endswith(".onion"): handshake["onion"] = self.server.tor_manager.getOnion("global") + if config.debug_socket: + self.log("My Handshake: %s" % handshake) + return handshake def setHandshake(self, handshake): + if config.debug_socket: + self.log("Remote Handshake: %s" % handshake) + if handshake.get("peer_id") == self.server.peer_id: self.close("Same peer id, can't connect to myself") return False @@ -422,8 +428,6 @@ class Connection(object): # Incoming handshake set request def handleHandshake(self, message): - if config.debug_socket: - self.log("Handshake request: %s" % message) self.setHandshake(message["params"]) data = self.getHandshakeInfo() data["cmd"] = "response" From f1396b65b96621d1e8b41adb731565f8506f0a92 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 14 Mar 2018 22:26:08 +0100 Subject: [PATCH 0719/2570] Blacklist myself on peer id match --- src/Connection/Connection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 2ea3e3db..69ff1232 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -346,6 +346,7 @@ class Connection(object): if handshake.get("peer_id") == self.server.peer_id: self.close("Same peer id, can't connect to myself") + self.server.peer_blacklist.append((handshake["target_ip"], handshake["fileserver_port"])) return False self.handshake = handshake From 4f472982da1af24da7c2ee8222097be291047106 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 14 Mar 2018 22:26:59 +0100 Subject: [PATCH 0720/2570] Use broken ssl ip list instead of peer_id --- src/Connection/Connection.py | 5 +++-- src/Connection/ConnectionServer.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 69ff1232..39da0f24 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -442,8 +442,9 @@ class Connection(object): self.sock = CryptConnection.manager.wrapSocket(self.sock, self.crypt, server, cert_pin=self.cert_pin) self.sock_wrapped = True except Exception, err: - self.log("Crypt connection error: %s, adding peerid %s as broken ssl." % (err, message["params"]["peer_id"])) - self.server.broken_ssl_peer_ids[message["params"]["peer_id"]] = True + if not config.force_encryption: + self.log("Crypt connection error: %s, adding ip %s as broken ssl." % (err, self.ip)) + self.server.broken_ssl_ips[self.ip] = True self.close("Broken ssl") if not self.sock_wrapped and self.cert_pin: diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index 2664ba6a..0ab2e154 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -35,7 +35,7 @@ class ConnectionServer(object): self.connections = [] # Connections self.whitelist = config.ip_local # No flood protection on this ips self.ip_incoming = {} # Incoming connections from ip in the last minute to avoid connection flood - self.broken_ssl_peer_ids = {} # Peerids of broken ssl connections + self.broken_ssl_ips = {} # Peerids of broken ssl connections self.ips = {} # Connection by ip self.has_internet = True # Internet outage detection @@ -192,7 +192,7 @@ class ConnectionServer(object): run_i += 1 time.sleep(15) # Check every minute self.ip_incoming = {} # Reset connected ips counter - self.broken_ssl_peer_ids = {} # Reset broken ssl peerids count + self.broken_ssl_ips = {} # Reset broken ssl peerids count last_message_time = 0 s = time.time() for connection in self.connections[:]: # Make a copy From 1ad966bd808e3546693262998abc745016eb78e6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 14 Mar 2018 22:29:00 +0100 Subject: [PATCH 0721/2570] In passive mode if we have send onion address for every connection not just for tor network --- src/Connection/Connection.py | 21 +++++++++++---------- src/Connection/ConnectionServer.py | 9 ++++++--- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 39da0f24..5e465640 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -355,17 +355,9 @@ class Connection(object): else: self.port = handshake["fileserver_port"] # Set peer fileserver port - if handshake.get("onion") and not self.ip.endswith(".onion"): # Set incoming connection's onion address - if self.server.ips.get(self.ip) == self: - del self.server.ips[self.ip] - self.ip = handshake["onion"] + ".onion" - self.log("Changing ip to %s" % self.ip) - self.server.ips[self.ip] = self - self.updateName() - # Check if we can encrypt the connection - if handshake.get("crypt_supported") and handshake["peer_id"] not in self.server.broken_ssl_peer_ids: - if self.ip.endswith(".onion"): + if handshake.get("crypt_supported") and self.ip not in self.server.broken_ssl_ips: + if self.ip.endswith(".onion") or self.ip in config.ip_local: crypt = None elif handshake.get("crypt"): # Recommended crypt by server crypt = handshake["crypt"] @@ -374,6 +366,15 @@ class Connection(object): if crypt: self.crypt = crypt + + if self.type == "in" and handshake.get("onion") and not self.ip.endswith(".onion"): # Set incoming connection's onion address + if self.server.ips.get(self.ip) == self: + del self.server.ips[self.ip] + self.ip = handshake["onion"] + ".onion" + self.log("Changing ip to %s" % self.ip) + self.server.ips[self.ip] = self + self.updateName() + self.event_connected.set(True) # Mark handshake as done self.event_connected = None diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index 0ab2e154..e6c04dd9 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -110,8 +110,11 @@ class ConnectionServer(object): pass def getConnection(self, ip=None, port=None, peer_id=None, create=True, site=None): - if ip.endswith(".onion") and self.tor_manager.start_onions and site: # Site-unique connection for Tor - site_onion = self.tor_manager.getOnion(site.address) + if (ip.endswith(".onion") or self.port_opened == False) and self.tor_manager.start_onions and site: # Site-unique connection for Tor + if ip.endswith(".onion"): + site_onion = self.tor_manager.getOnion(site.address) + else: + site_onion = self.tor_manager.getOnion("global") key = ip + site_onion else: key = ip @@ -149,7 +152,7 @@ class ConnectionServer(object): raise Exception("This peer is blacklisted") try: - if ip.endswith(".onion") and self.tor_manager.start_onions and site: # Lock connection to site + if (ip.endswith(".onion") or self.port_opened == False) and self.tor_manager.start_onions and site: # Lock connection to site connection = Connection(self, ip, port, target_onion=site_onion) else: connection = Connection(self, ip, port) From 81e96d25ba0630829e116eff9c349b89ed409d57 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 14 Mar 2018 22:30:08 +0100 Subject: [PATCH 0722/2570] Handle request using global onion address --- 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 4b56175b..9f135c33 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -63,7 +63,7 @@ class FileRequest(object): # Don't allow other sites than locked if "site" in params and self.connection.target_onion: valid_sites = self.connection.getValidSites() - if params["site"] not in valid_sites: + if params["site"] not in valid_sites and valid_sites != ["global"]: self.response({"error": "Invalid site"}) self.connection.log( "Site lock violation: %s not in %s, target onion: %s" % From 3561ddf7d380be1933ff45c851c998c059bbe454 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 14 Mar 2018 22:31:21 +0100 Subject: [PATCH 0723/2570] Do check needConnection for evey site after startup to have up to date protected ip list --- src/File/FileServer.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index e9a29387..b76467c9 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -278,13 +278,12 @@ class FileServer(ConnectionServer): if site.bad_files: site.retryBadFiles() - if not startup: # Don't do it at start up because checkSite already has needConnections at start up. - 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) + 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) - 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 From c5f77a1c38b27579dc4e2123701fba62c1173960 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 14 Mar 2018 22:31:58 +0100 Subject: [PATCH 0724/2570] Handle global onion address correctly in needConnections --- src/Site/Site.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 43ecbb72..bff705f1 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -1078,8 +1078,11 @@ class Site(object): continue peer = self.peers.get("%s:%s" % (connection.ip, connection.port)) if peer: - if connection.target_onion and tor_manager.start_onions and tor_manager.getOnion(self.address) != connection.target_onion: - continue + if connection.ip.endswith(".onion") and connection.target_onion and tor_manager.start_onions: + # Check if the connection is made with the onion address created for the site + valid_target_onions = (tor_manager.getOnion(self.address), tor_manager.getOnion("global")) + if connection.target_onion not in valid_target_onions: + continue if not peer.connection: peer.connect(connection) back.append(peer) From 3d7dea52a07bd755e9e57ebcf14c66fa6fbc768a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 14 Mar 2018 22:32:15 +0100 Subject: [PATCH 0725/2570] Always load my sites from sites.json --- src/Site/SiteManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index 8d483f8b..18531ea5 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -46,7 +46,7 @@ class SiteManager(object): self.sites[address] = site self.log.debug("Loaded site %s in %.3fs" % (address, time.time() - s)) added += 1 - elif startup and settings.get("peers", 0) > 0: + elif startup and (settings.get("peers", 0) > 0 or settings.get("own")): # No site directory, start download self.log.debug("Found new site in sites.json: %s" % address) gevent.spawn(self.need, address, settings=settings) From 4083e552fd9e3f39889f5915a4df615342c9062b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 14 Mar 2018 22:32:28 +0100 Subject: [PATCH 0726/2570] Formatting --- src/Tor/TorManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tor/TorManager.py b/src/Tor/TorManager.py index baad17f0..b9499469 100644 --- a/src/Tor/TorManager.py +++ b/src/Tor/TorManager.py @@ -89,7 +89,7 @@ class TorManager(object): startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW self.tor_process = subprocess.Popen(r"%s -f torrc" % self.tor_exe, cwd=tor_dir, close_fds=True, startupinfo=startupinfo) - for wait in range(1,10): # Wait for startup + for wait in range(1, 10): # Wait for startup time.sleep(wait * 0.5) self.enabled = True if self.connect(): From 89beb12d59bfe8ddffa4b128f44c778c2c0afda0 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 14 Mar 2018 22:32:49 +0100 Subject: [PATCH 0727/2570] Set up HS for global at startup --- src/Tor/TorManager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Tor/TorManager.py b/src/Tor/TorManager.py index b9499469..33afc079 100644 --- a/src/Tor/TorManager.py +++ b/src/Tor/TorManager.py @@ -205,6 +205,7 @@ class TorManager(object): if self.enabled: self.log.debug("Start onions") self.start_onions = True + self.getOnion("global") # Get new exit node ip def resetCircuits(self): From 1549f2aa4dce515c48f744d25ee3af255560f668 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 14 Mar 2018 22:33:21 +0100 Subject: [PATCH 0728/2570] Don't share global HS with any site --- src/Tor/TorManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tor/TorManager.py b/src/Tor/TorManager.py index 33afc079..be3c94f6 100644 --- a/src/Tor/TorManager.py +++ b/src/Tor/TorManager.py @@ -216,7 +216,7 @@ class TorManager(object): def addOnion(self): if len(self.privatekeys) >= config.tor_hs_limit: - return random.choice(self.privatekeys.keys()) + return random.choice([key for key in self.privatekeys.keys() if key != self.site_onions.get("global")]) result = self.makeOnionAndKey() if result: From c401cabee79b5e97a6e28a4d76b4fb2e50b0a7ff Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 14 Mar 2018 22:33:41 +0100 Subject: [PATCH 0729/2570] Only start separate HS for every site in tor always mode --- src/Tor/TorManager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Tor/TorManager.py b/src/Tor/TorManager.py index be3c94f6..0d60e047 100644 --- a/src/Tor/TorManager.py +++ b/src/Tor/TorManager.py @@ -239,7 +239,6 @@ class TorManager(object): self.log.error("Tor addOnion error: %s" % res) return False - def delOnion(self, address): res = self.request("DEL_ONION %s" % address) if "250 OK" in res: @@ -291,7 +290,7 @@ class TorManager(object): with self.lock: if not self.enabled: return None - if self.start_onions: # Different onion for every site + if config.tor == "always": # Different onion for every site onion = self.site_onions.get(site_address) else: # Same onion for every site onion = self.site_onions.get("global") From 1ead9b59baf97d7db14bb170868f1b08978292b7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 14 Mar 2018 22:34:27 +0100 Subject: [PATCH 0730/2570] Rev3367 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 9780462e..42ca187e 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.2" - self.rev = 3356 + self.rev = 3367 self.argv = argv self.action = None self.config_file = "zeronet.conf" From ad1d81dccf95c3007aae8ce331f2a038499f76f6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 18 Mar 2018 21:25:28 +0100 Subject: [PATCH 0731/2570] Cache inner_path to sha512 hash for bigfiles to avoid loading content.json every time --- plugins/OptionalManager/UiWebsocketPlugin.py | 25 ++++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/plugins/OptionalManager/UiWebsocketPlugin.py b/plugins/OptionalManager/UiWebsocketPlugin.py index 2b000622..2f9d4e4b 100644 --- a/plugins/OptionalManager/UiWebsocketPlugin.py +++ b/plugins/OptionalManager/UiWebsocketPlugin.py @@ -12,6 +12,8 @@ from Translate import Translate if "_" not in locals(): _ = Translate("plugins/OptionalManager/languages/") +bigfile_sha512_cache = {} + @PluginManager.registerTo("UiWebsocket") class UiWebsocketPlugin(object): def __init__(self, *args, **kwargs): @@ -38,16 +40,23 @@ class UiWebsocketPlugin(object): 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_info = site.content_manager.getFileInfo(row["inner_path"]) - if not file_info or not file_info.get("piece_size"): - 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 - sha512 = file_info["sha512"] if sha512 in site.storage.piecefields: piecefield = site.storage.piecefields[sha512].tostring() else: @@ -57,7 +66,13 @@ class UiWebsocketPlugin(object): row["pieces"] = len(piecefield) row["pieces_downloaded"] = piecefield.count("1") row["downloaded_percent"] = 100 * row["pieces_downloaded"] / row["pieces"] - row["bytes_downloaded"] = row["pieces_downloaded"] * file_info["piece_size"] + if row["pieces_downloaded"]: + 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((task for task in site.worker_manager.tasks if task["inner_path"].startswith(row["inner_path"])), False)) # Add leech / seed stats From adf39b6f6acbf57a433a3fe188db7cf515bac4b5 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 18 Mar 2018 21:26:20 +0100 Subject: [PATCH 0732/2570] Avoid loading user content.json file on isArchived check --- src/Content/ContentManager.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 3ef6977c..0ff71291 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -290,11 +290,13 @@ class ContentManager(object): # Returns if file with the given modification date is archived or not def isArchived(self, inner_path, modified): - file_info = self.getFileInfo(inner_path) - match = re.match(".*/(.*?)/", inner_path) + match = re.match("(.*)/(.*?)/", inner_path) if not match: return False - relative_directory = match.group(1) + user_contents_inner_path = match.group(1) + "/content.json" + relative_directory = match.group(2) + + file_info = self.getFileInfo(user_contents_inner_path) if file_info and file_info.get("archived", {}).get(relative_directory) >= modified: return True else: From ccdfe7735607319d6be0c9d4fdc971a1790ea240 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 18 Mar 2018 21:27:04 +0100 Subject: [PATCH 0733/2570] Display more detailed info on content sign error --- src/Ui/UiWebsocket.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 5e6a41b9..e0dbf7df 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -16,7 +16,7 @@ from util import QueryJson, RateLimit from Plugin import PluginManager from Translate import translate as _ from util import helper - +from Content.ContentManager import VerifyError, SignError @PluginManager.acceptPlugins class UiWebsocket(object): @@ -403,11 +403,16 @@ class UiWebsocket(object): # Sign using private key sent by user try: signed = site.content_manager.sign(inner_path, privatekey, extend=extend, update_changed_files=update_changed_files, remove_missing_optional=remove_missing_optional) - except Exception, err: + except (VerifyError, SignError) as err: self.cmd("notification", ["error", _["Content signing failed"] + "
      %s" % err]) self.response(to, {"error": "Site sign failed: %s" % err}) self.log.error("Site sign failed: %s: %s" % (inner_path, Debug.formatException(err))) return + except Exception as err: + self.cmd("notification", ["error", _["Content signing error"] + "
      %s" % Debug.formatException(err)]) + self.response(to, {"error": "Site sign error: %s" % Debug.formatException(err)}) + self.log.error("Site sign error: %s: %s" % (inner_path, Debug.formatException(err))) + return site.content_manager.loadContent(inner_path, add_bad_files=False) # Load new content.json, ignore errors From e96435378aa6a63fd2dbbd9eca6ae43531543187 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 18 Mar 2018 21:27:26 +0100 Subject: [PATCH 0734/2570] Fix typo --- src/Worker/WorkerManager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 2b8ba664..bd45a12b 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -59,6 +59,7 @@ class WorkerManager(object): worker.skip() else: self.failTask(task) + elif time.time() >= task["time_added"] + 60 and not self.workers: # No workers left self.log.debug("Timeout, Cleanup task: %s" % task) # Remove task @@ -420,7 +421,6 @@ class WorkerManager(object): self.log.debug("Starting new workers... (tasks: %s)" % len(self.tasks)) self.startWorkers() - # Tasks sorted by this def getPriorityBoost(self, inner_path): if inner_path == "content.json": @@ -518,7 +518,7 @@ class WorkerManager(object): def checkComplete(self): time.sleep(0.1) if not self.tasks: - self.log.debug("Check compelte: No tasks") + self.log.debug("Check complete: No tasks") self.onComplete() def onComplete(self): From 75adfebf9f57cc635a91c26c9e6073e8379bbfa3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 18 Mar 2018 21:27:35 +0100 Subject: [PATCH 0735/2570] Rev3370 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 42ca187e..1b60eeb3 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.2" - self.rev = 3367 + self.rev = 3370 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 0533f29e7a0482db7eeb85003bfae0a128d64b77 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Mar 2018 02:57:54 +0100 Subject: [PATCH 0736/2570] Only recover file piecefield if we already hashed it --- plugins/Bigfile/BigfilePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index 1e8610ab..a90b747b 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -326,7 +326,7 @@ class ContentManagerPlugin(object): size = 0 elif size > 1024 * 1024: file_info = self.getFileInfo(inner_path) - if file_info: # We already have the file, but not in piecefield + 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) From ff3fdd4c72a79c97dea2bc54b4eb60ba5d459524 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Mar 2018 02:58:38 +0100 Subject: [PATCH 0737/2570] Get privatekey from master seed CLI action --- src/Config.py | 5 +++++ src/main.py | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/src/Config.py b/src/Config.py index 1b60eeb3..3928cdf2 100644 --- a/src/Config.py +++ b/src/Config.py @@ -171,6 +171,11 @@ class Config(object): action.add_argument('sign', help='Signiture for message') action.add_argument('address', help='Signer\'s address') + # Crypt GetPrivatekey + action = self.subparsers.add_parser("cryptGetPrivatekey", help='Generate a privatekey from master seed') + action.add_argument('master_seed', help='Source master seed') + action.add_argument('site_address_index', help='Site address index', type=int) + action = self.subparsers.add_parser("getConfig", help='Return json-encoded info') action = self.subparsers.add_parser("testConnection", help='Testing') action = self.subparsers.add_parser("testAnnounce", help='Testing') diff --git a/src/main.py b/src/main.py index 193c0f8a..7f393a49 100644 --- a/src/main.py +++ b/src/main.py @@ -428,6 +428,10 @@ class Actions(object): from Crypt import CryptBitcoin print CryptBitcoin.verify(message, address, sign) + def cryptGetPrivatekey(self, master_seed, site_address_index=None): + from Crypt import CryptBitcoin + print CryptBitcoin.hdPrivatekey(master_seed, site_address_index) + # Peer def peerPing(self, peer_ip, peer_port=None): if not peer_port: From b29d17d39a9d06ff0470a8d9c00f510586ab971d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Mar 2018 02:58:53 +0100 Subject: [PATCH 0738/2570] Rev3371 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 3928cdf2..1488122c 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.2" - self.rev = 3370 + self.rev = 3371 self.argv = argv self.action = None self.config_file = "zeronet.conf" From c3b146611b7b343cbdf56674d5e346060387ed20 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Mar 2018 16:05:14 +0100 Subject: [PATCH 0739/2570] Rev3372, Cosmetic changes on open site directory button --- plugins/Sidebar/SidebarPlugin.py | 6 +----- plugins/Sidebar/media/Sidebar.coffee | 4 +++- plugins/Sidebar/media/Sidebar.css | 3 +++ plugins/Sidebar/media/all.css | 3 +++ plugins/Sidebar/media/all.js | 5 +++-- src/Config.py | 2 +- 6 files changed, 14 insertions(+), 9 deletions(-) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index 1f87bfc4..4bf716f2 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -137,7 +137,7 @@ class UiWebsocketPlugin(object): """)) def sidebarRenderFileStats(self, body, site): - body.append(_(u"
      • ")) + body.append(_(u"
        • ")) extensions = ( ("html", "yellow"), @@ -418,10 +418,6 @@ class UiWebsocketPlugin(object):
        • {_[Save site settings]}
        • - -
        • - {_[Open site directory]} -
        • """)) def sidebarRenderContents(self, body, site): diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee index 6efa0928..e35f712b 100644 --- a/plugins/Sidebar/media/Sidebar.coffee +++ b/plugins/Sidebar/media/Sidebar.coffee @@ -352,9 +352,11 @@ class Sidebar extends Class @updateHtmlTag() return false + # Open site directory - @tag.find("#button-directory").off("click touchend").on "click touchend", => + @tag.find("#link-directory").off("click touchend").on "click touchend", => @wrapper.ws.cmd "serverShowdirectory", ["site", @wrapper.site_info.address] + return false # Sign and publish content.json $(document).on "click touchend", => diff --git a/plugins/Sidebar/media/Sidebar.css b/plugins/Sidebar/media/Sidebar.css index 895ba178..fa7ac7dc 100644 --- a/plugins/Sidebar/media/Sidebar.css +++ b/plugins/Sidebar/media/Sidebar.css @@ -11,6 +11,9 @@ #inner-iframe { transition: 0.3s ease-in-out; transform-origin: left; outline: 1px solid transparent; } .body-sidebar iframe { transform: rotateY(5deg); opacity: 0.8; pointer-events: none } /* translateX(-200px) scale(0.95)*/ +.sidebar .link-right { color: white; text-decoration: none; border-bottom: 1px solid #666; text-transform: uppercase; float: right; margin-right: 7px; margin-top: 1px; } +.sidebar .link-right:hover { border-color: #CCC; } +.sidebar .link-right:active { background-color: #444 } /* SIDEBAR */ .sidebar-container { width: 100%; height: 100%; overflow: hidden; position: fixed; } diff --git a/plugins/Sidebar/media/all.css b/plugins/Sidebar/media/all.css index 29398d36..fb793aa7 100644 --- a/plugins/Sidebar/media/all.css +++ b/plugins/Sidebar/media/all.css @@ -90,6 +90,9 @@ #inner-iframe { -webkit-transition: 0.3s ease-in-out; -moz-transition: 0.3s ease-in-out; -o-transition: 0.3s ease-in-out; -ms-transition: 0.3s ease-in-out; transition: 0.3s ease-in-out ; transform-origin: left; outline: 1px solid transparent; } .body-sidebar iframe { -webkit-transform: rotateY(5deg); -moz-transform: rotateY(5deg); -o-transform: rotateY(5deg); -ms-transform: rotateY(5deg); transform: rotateY(5deg) ; opacity: 0.8; pointer-events: none } /* translateX(-200px) scale(0.95)*/ +.sidebar .link-right { color: white; text-decoration: none; border-bottom: 1px solid #666; text-transform: uppercase; float: right; margin-right: 7px; margin-top: 1px; } +.sidebar .link-right:hover { border-color: #CCC; } +.sidebar .link-right:active { background-color: #444 } /* SIDEBAR */ .sidebar-container { width: 100%; height: 100%; overflow: hidden; position: fixed; } diff --git a/plugins/Sidebar/media/all.js b/plugins/Sidebar/media/all.js index 4486bfdf..db119ee7 100644 --- a/plugins/Sidebar/media/all.js +++ b/plugins/Sidebar/media/all.js @@ -691,9 +691,10 @@ window.initScrollable = function () { return false; }; })(this)); - this.tag.find("#button-directory").off("click touchend").on("click touchend", (function(_this) { + this.tag.find("#link-directory").off("click touchend").on("click touchend", (function(_this) { return function() { - return _this.wrapper.ws.cmd("serverShowdirectory", ["site", _this.wrapper.site_info.address]); + _this.wrapper.ws.cmd("serverShowdirectory", ["site", _this.wrapper.site_info.address]); + return false; }; })(this)); $(document).on("click touchend", (function(_this) { diff --git a/src/Config.py b/src/Config.py index 1488122c..d14f2738 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.2" - self.rev = 3371 + self.rev = 3372 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 9adec79401b6fb3c31719189e0d18437e581fa0b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 20 Mar 2018 21:53:39 +0100 Subject: [PATCH 0740/2570] Formatting --- src/Ui/UiWebsocket.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index e0dbf7df..98d72ed0 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -1,7 +1,6 @@ import json import time import sys -import hashlib import os import shutil import re @@ -18,6 +17,7 @@ from Translate import translate as _ from util import helper from Content.ContentManager import VerifyError, SignError + @PluginManager.acceptPlugins class UiWebsocket(object): @@ -97,8 +97,8 @@ class UiWebsocket(object): if ("0.0.0.0" == bind_ip or "*" == bind_ip) and (not whitelist): self.site.notifications.append([ "error", - _(u"You are not going to set up a public gateway. However, your Web UI is
          " + \ - "open to the whole Internet.
          " + \ + _(u"You are not going to set up a public gateway. However, your Web UI is
          " + + "open to the whole Internet.
          " + "Please check your configuration.") ]) @@ -436,7 +436,7 @@ class UiWebsocket(object): self.site.saveSettings() self.site.announce() - if not inner_path in self.site.content_manager.contents: + if inner_path not in self.site.content_manager.contents: return self.response(to, {"error": "File %s not found" % inner_path}) event_name = "publish %s %s" % (self.site.address, inner_path) @@ -941,10 +941,9 @@ class UiWebsocket(object): import Translate for translate in Translate.translates: translate.setLanguage(value) - self.cmd("notification", ["done", - _["You have successfully changed the web interface's language!"] + "
          " + - _["Due to the browser's caching, the full transformation could take some minute."] - , 10000]) + message = _["You have successfully changed the web interface's language!"] + "
          " + message += _["Due to the browser's caching, the full transformation could take some minute."] + self.cmd("notification", ["done", message, 10000]) config.language = value self.response(to, "ok") From 6b926b12a4815927f41877e45734920e87404aaf Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 20 Mar 2018 21:54:33 +0100 Subject: [PATCH 0741/2570] Don't try to delete optional file twice on fileDelete API command --- src/Ui/UiWebsocket.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 98d72ed0..14ff0e01 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -562,8 +562,10 @@ class UiWebsocket(object): self.log.error("File delete error: you don't own this site & you are not approved by the owner.") return self.response(to, {"error": "Forbidden, you can only modify your own files"}) + need_delete = True file_info = self.site.content_manager.getFileInfo(inner_path) if file_info and file_info.get("optional"): + # Non-existing optional files won't be removed from content.json, so we have to do it manually self.log.debug("Deleting optional file: %s" % inner_path) relative_path = file_info["relative_path"] content_json = self.site.storage.loadJson(file_info["content_inner_path"]) @@ -571,12 +573,14 @@ class UiWebsocket(object): 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) + need_delete = self.site.storage.isFile(inner_path) # File sill exists after removing from content.json (owned site) - try: - self.site.storage.delete(inner_path) - except Exception, err: - self.log.error("File delete error: Exception - %s" % err) - return self.response(to, {"error": "Delete error: %s" % err}) + if need_delete: + try: + self.site.storage.delete(inner_path) + except Exception, err: + self.log.error("File delete error: %s" % err) + return self.response(to, {"error": "Delete error: %s" % err}) self.response(to, "ok") From 11d8485399fbca4b69106eed9bb7bdb45221695a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 20 Mar 2018 21:55:12 +0100 Subject: [PATCH 0742/2570] Move file modification permission check to separate function --- src/Ui/UiWebsocket.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 14ff0e01..f1ffc494 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -165,6 +165,10 @@ class UiWebsocket(object): else: return True + def hasFilePermission(self, inner_path): + valid_signers = self.site.content_manager.getValidSigners(inner_path) + return self.site.settings["own"] or self.user.getAuthAddress(self.site.address) in valid_signers + # Event in a channel def event(self, channel, *params): if channel in self.channels: # We are joined to channel @@ -385,10 +389,7 @@ class UiWebsocket(object): extend["cert_user_id"] = self.user.getCertUserId(site.address) extend["cert_sign"] = cert["cert_sign"] - if ( - not site.settings["own"] and - self.user.getAuthAddress(self.site.address) not in self.site.content_manager.getValidSigners(inner_path) - ): + 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.") return self.response(to, {"error": "Forbidden, you can only modify your own sites"}) @@ -402,7 +403,7 @@ class UiWebsocket(object): site.content_manager.loadContent(inner_path, add_bad_files=False, force=True) # Sign using private key sent by user try: - signed = site.content_manager.sign(inner_path, privatekey, extend=extend, update_changed_files=update_changed_files, remove_missing_optional=remove_missing_optional) + site.content_manager.sign(inner_path, privatekey, extend=extend, update_changed_files=update_changed_files, remove_missing_optional=remove_missing_optional) except (VerifyError, SignError) as err: self.cmd("notification", ["error", _["Content signing failed"] + "
          %s" % err]) self.response(to, {"error": "Site sign failed: %s" % err}) @@ -507,7 +508,7 @@ class UiWebsocket(object): def actionFileWrite(self, to, inner_path, content_base64, ignore_bad_files=False): 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: + if not self.hasFilePermission(inner_path): 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"}) @@ -555,10 +556,7 @@ class UiWebsocket(object): ws.event("siteChanged", self.site, {"event": ["file_done", inner_path]}) def actionFileDelete(self, to, inner_path): - if ( - not self.site.settings["own"] and - self.user.getAuthAddress(self.site.address) not in self.site.content_manager.getValidSigners(inner_path) - ): + if not self.hasFilePermission(inner_path): self.log.error("File delete error: you don't own this site & you are not approved by the owner.") return self.response(to, {"error": "Forbidden, you can only modify your own files"}) From d275dfea2fb916492bee8a87934f167e583c2a3d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 20 Mar 2018 21:55:45 +0100 Subject: [PATCH 0743/2570] Log 403 error as error --- src/Ui/UiRequest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 991dd264..60b17f56 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -678,7 +678,7 @@ class UiRequest(object): # You are not allowed to access this def error403(self, message="", details=True): self.sendHeader(403) - self.log.debug("Error 403: %s" % message) + self.log.error("Error 403: %s" % message) return self.formatError("Forbidden", message, details=details) # Send file not found error From 9d3913ed7057ce432a3c085efb5d6b75bc9c6a40 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 20 Mar 2018 21:58:51 +0100 Subject: [PATCH 0744/2570] Also delete piecemap on bigfile deletion --- plugins/Bigfile/BigfilePlugin.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index a90b747b..337e7c42 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -157,6 +157,26 @@ class UiWebsocketPlugin(object): 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, 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): From 0cf6fb2c9d3db7c033227c8c481f004459dd487c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 20 Mar 2018 21:59:34 +0100 Subject: [PATCH 0745/2570] Rev3376 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index d14f2738..5fa1c88b 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.2" - self.rev = 3372 + self.rev = 3376 self.argv = argv self.action = None self.config_file = "zeronet.conf" From ee81aea2fa70c5a6f01240a7975a97c7e5ed687a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 20 Mar 2018 22:27:32 +0100 Subject: [PATCH 0746/2570] Wait until title in link security test --- src/Test/TestWeb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Test/TestWeb.py b/src/Test/TestWeb.py index 3e321756..7fba965c 100644 --- a/src/Test/TestWeb.py +++ b/src/Test/TestWeb.py @@ -4,7 +4,7 @@ import pytest try: from selenium.webdriver.support.ui import WebDriverWait - from selenium.webdriver.support.expected_conditions import staleness_of + from selenium.webdriver.support.expected_conditions import staleness_of, title_is from selenium.common.exceptions import NoSuchElementException except: pass @@ -55,7 +55,7 @@ class TestWeb: def testLinkSecurity(self, browser, site_url): browser.get("%s/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr/test/security.html" % site_url) - assert browser.title == "ZeroHello - ZeroNet" + WebDriverWait(browser, 5).until(title_is("ZeroHello - ZeroNet")) assert browser.current_url == "%s/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr/test/security.html" % site_url # Switch to inner frame From 76c4a6bb7cc49745e31787faf60d2eacc9298faa Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 21 Mar 2018 18:05:22 +0100 Subject: [PATCH 0747/2570] Rev3377, Fix sidebar animation --- plugins/Sidebar/media/Sidebar.coffee | 2 +- plugins/Sidebar/media/all.js | 2 +- src/Config.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee index e35f712b..bae1f3ed 100644 --- a/plugins/Sidebar/media/Sidebar.coffee +++ b/plugins/Sidebar/media/Sidebar.coffee @@ -221,7 +221,7 @@ class Sidebar extends Class # Opened targetx = @width if @opened - onOpened() + @onOpened() else @when_loaded.done => @onOpened() diff --git a/plugins/Sidebar/media/all.js b/plugins/Sidebar/media/all.js index db119ee7..0a9e4aaf 100644 --- a/plugins/Sidebar/media/all.js +++ b/plugins/Sidebar/media/all.js @@ -520,7 +520,7 @@ window.initScrollable = function () { } else { targetx = this.width; if (this.opened) { - onOpened(); + this.onOpened(); } else { this.when_loaded.done((function(_this) { return function() { diff --git a/src/Config.py b/src/Config.py index 5fa1c88b..f1f3edc2 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.2" - self.rev = 3376 + self.rev = 3377 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 077d9d391868c39f2e6b3b14364b6fbc3d2f94bf Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 21 Mar 2018 21:40:44 +0100 Subject: [PATCH 0748/2570] Increase selenium test timeouts --- src/Test/TestWeb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Test/TestWeb.py b/src/Test/TestWeb.py index 7fba965c..08b223d4 100644 --- a/src/Test/TestWeb.py +++ b/src/Test/TestWeb.py @@ -18,7 +18,7 @@ class WaitForPageLoad(object): self.old_page = self.browser.find_element_by_tag_name('html') def __exit__(self, *args): - WebDriverWait(self.browser, 5).until(staleness_of(self.old_page)) + WebDriverWait(self.browser, 10).until(staleness_of(self.old_page)) def wget(url): @@ -55,7 +55,7 @@ class TestWeb: def testLinkSecurity(self, browser, site_url): browser.get("%s/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr/test/security.html" % site_url) - WebDriverWait(browser, 5).until(title_is("ZeroHello - ZeroNet")) + WebDriverWait(browser, 10).until(title_is("ZeroHello - ZeroNet")) assert browser.current_url == "%s/1EU1tbG9oC1A8jz2ouVwGZyQ5asrNsE4Vr/test/security.html" % site_url # Switch to inner frame From 049d32683616d455101cdbef40a83abc6eeb9483 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 29 Mar 2018 02:39:51 +0200 Subject: [PATCH 0749/2570] Test optionalmanager with travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 066b5531..8fc7a92a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,7 @@ script: - python -m pytest -x plugins/CryptMessage/Test - python -m pytest -x plugins/Bigfile/Test - python -m pytest -x plugins/AnnounceLocal/Test + - python -m pytest -x plugins/OptionalManager/Test - python -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini before_install: - pip install -U pytest mock pytest-cov selenium From 0c985a5fdac4a734cd17e909031507ac62e39ee0 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 29 Mar 2018 02:41:30 +0200 Subject: [PATCH 0750/2570] Use current user cert to query file rules for non-existent files --- src/Ui/UiWebsocket.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index f1ffc494..c4f84eda 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -645,12 +645,23 @@ class UiWebsocket(object): return self.response(to, {"error": str(err)}) return self.response(to, "ok") - def actionFileRules(self, to, inner_path): - rules = self.site.content_manager.getRules(inner_path) - if inner_path.endswith("content.json") and rules: + def actionFileRules(self, to, inner_path, use_my_cert=False, content=None): + if not content: # No content defined by function call content = self.site.content_manager.contents.get(inner_path) + + if not content: # File not created yet + cert = self.user.getCert(self.site.address) + if cert and cert["auth_address"] in self.site.content_manager.getValidSigners(inner_path): + # Current selected cert if valid for this site, add it to query rules + content = {} + content["cert_auth_type"] = cert["auth_type"] + content["cert_user_id"] = self.user.getCertUserId(self.site.address) + content["cert_sign"] = cert["cert_sign"] + + rules = self.site.content_manager.getRules(inner_path, content) + if inner_path.endswith("content.json") and rules: if content: - rules["current_size"] = len(json.dumps(content)) + sum([file["size"] for file in content["files"].values()]) + rules["current_size"] = len(json.dumps(content)) + sum([file["size"] for file in content.get("files", {}).values()]) else: rules["current_size"] = 0 return self.response(to, rules) From 30ab2cf9a721a7fd9a96cbaa51f61765243a496a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 29 Mar 2018 02:42:57 +0200 Subject: [PATCH 0751/2570] Transform progressbar with scale instead of changing width --- src/Ui/media/Loading.coffee | 4 ++-- src/Ui/media/all.js | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Ui/media/Loading.coffee b/src/Ui/media/Loading.coffee index 57b973bb..b1ffba1f 100644 --- a/src/Ui/media/Loading.coffee +++ b/src/Ui/media/Loading.coffee @@ -8,12 +8,12 @@ class Loading if @timer_hide clearInterval @timer_hide RateLimit 200, -> - $(".progressbar").css("width", percent*100+"%").css("opacity", "1").css("display", "block") + $(".progressbar").css("transform": "scaleX(#{parseInt(percent*100)/100})").css("opacity", "1").css("display", "block") hideProgress: -> console.log "hideProgress" @timer_hide = setTimeout ( => - $(".progressbar").css("width", "100%").css("opacity", "0").hideLater(1000) + $(".progressbar").css("transform": "scaleX(1)").css("opacity", "0").hideLater(1000) ), 300 diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index ead47793..ade58213 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -587,7 +587,9 @@ jQuery.extend( jQuery.easing, clearInterval(this.timer_hide); } return RateLimit(200, function() { - return $(".progressbar").css("width", percent * 100 + "%").css("opacity", "1").css("display", "block"); + return $(".progressbar").css({ + "transform": "scaleX(" + (parseInt(percent * 100) / 100) + ")" + }).css("opacity", "1").css("display", "block"); }); }; @@ -595,7 +597,9 @@ jQuery.extend( jQuery.easing, console.log("hideProgress"); return this.timer_hide = setTimeout(((function(_this) { return function() { - return $(".progressbar").css("width", "100%").css("opacity", "0").hideLater(1000); + return $(".progressbar").css({ + "transform": "scaleX(1)" + }).css("opacity", "0").hideLater(1000); }; })(this)), 300); }; From b1240426cd0e6521c05f3ea3bd4d8ed0c7713722 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 29 Mar 2018 02:44:00 +0200 Subject: [PATCH 0752/2570] For 3d rending to progress bar --- src/Ui/media/Wrapper.css | 11 +++++++---- src/Ui/media/all.css | 11 +++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/Ui/media/Wrapper.css b/src/Ui/media/Wrapper.css index c4764395..2daccb35 100644 --- a/src/Ui/media/Wrapper.css +++ b/src/Ui/media/Wrapper.css @@ -5,7 +5,7 @@ a { color: black } .unsupported { text-align: center; z-index: 999; position: relative; margin: auto; width: 480px; background-color: white; padding: 20px; border-bottom: 2px solid #e74c3c; box-shadow: 0px 0px 15px #DDD; font-family: monospace; } .template { display: none !important } -#inner-iframe { width: 100%; height: 100%; position: absolute; border: 0 } /*; transition: all 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55), opacity 0.8s ease-in-out*/ +#inner-iframe { width: 100%; height: 100%; position: absolute; border: 0; } /*; transition: all 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55), opacity 0.8s ease-in-out*/ #inner-iframe.back { transform: scale(0.95) translate(-300px, 0); opacity: 0.4 } .button { padding: 5px 10px; margin-left: 10px; background-color: #FFF85F; border-bottom: 2px solid #CDBD1E; border-radius: 2px; text-decoration: none; transition: all 0.5s; background-position: left center; } @@ -125,7 +125,7 @@ a { color: black } .flipper { position: relative; display: block; height: inherit; width: inherit; animation: flip 1.2s infinite ease-in-out; -webkit-transform-style: preserve-3d; } .flipper .front, .flipper .back { position: absolute; top: 0; left: 0; backface-visibility: hidden; /*transform-style: preserve-3d;*/ display: block; - background-color: #d50000; height: 100%; width: 100%; /*outline: 1px solid transparent; /* FF AA fix */ + background-color: #d50000; height: 100%; width: 100%; /* outline: 1px solid transparent; /* FF AA fix */ } .flipper .back { background-color: white; z-index: 800; transform: rotateY(-180deg) } @@ -140,7 +140,10 @@ a { color: black } .loadingscreen.done .flipper-container { opacity: 0; transition: all 1.5s } -.progressbar { background: #26C281; position: fixed; z-index: 100; top: 0; left: 0; width: 0%; height: 2px; transition: width 0.5s, opacity 1s; display: none } +.progressbar { + background: #26C281; position: fixed; width: 100%; z-index: 100; top: 0; left: 0; transform: scaleX(0); transform-origin: 0% 0%; transform:translate3d(0,0,0); + height: 2px; transition: transform 0.5s, opacity 1s; display: none; backface-visibility: hidden; transform-style: preserve-3d; +} .progressbar .peg { display: block; position: absolute; right: 0; width: 100px; height: 100%; box-shadow: 0 0 10px #AF3BFF, 0 0 5px #29d; opacity: 1.0; transform: rotate(3deg) translate(0px, -4px); @@ -183,4 +186,4 @@ a { color: black } .notification .button { white-space: nowrap; } .notification { margin: 0px } .notifications { right: 0px } -} \ No newline at end of file +} diff --git a/src/Ui/media/all.css b/src/Ui/media/all.css index 351a5adc..047367df 100644 --- a/src/Ui/media/all.css +++ b/src/Ui/media/all.css @@ -10,7 +10,7 @@ a { color: black } .unsupported { text-align: center; z-index: 999; position: relative; margin: auto; width: 480px; background-color: white; padding: 20px; border-bottom: 2px solid #e74c3c; -webkit-box-shadow: 0px 0px 15px #DDD; -moz-box-shadow: 0px 0px 15px #DDD; -o-box-shadow: 0px 0px 15px #DDD; -ms-box-shadow: 0px 0px 15px #DDD; box-shadow: 0px 0px 15px #DDD ; font-family: monospace; } .template { display: none !important } -#inner-iframe { width: 100%; height: 100%; position: absolute; border: 0 } /*; transition: all 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55), opacity 0.8s ease-in-out*/ +#inner-iframe { width: 100%; height: 100%; position: absolute; border: 0; } /*; transition: all 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55), opacity 0.8s ease-in-out*/ #inner-iframe.back { -webkit-transform: scale(0.95) translate(-300px, 0); -moz-transform: scale(0.95) translate(-300px, 0); -o-transform: scale(0.95) translate(-300px, 0); -ms-transform: scale(0.95) translate(-300px, 0); transform: scale(0.95) translate(-300px, 0) ; opacity: 0.4 } .button { padding: 5px 10px; margin-left: 10px; background-color: #FFF85F; border-bottom: 2px solid #CDBD1E; -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; -moz-transition: all 0.5s; -o-transition: all 0.5s; -ms-transition: all 0.5s; transition: all 0.5s ; background-position: left center; } @@ -139,7 +139,7 @@ a { color: black } .flipper { position: relative; display: block; height: inherit; width: inherit; -webkit-animation: flip 1.2s infinite ease-in-out; -moz-animation: flip 1.2s infinite ease-in-out; -o-animation: flip 1.2s infinite ease-in-out; -ms-animation: flip 1.2s infinite ease-in-out; animation: flip 1.2s infinite ease-in-out ; -webkit-transform-style: preserve-3d; } .flipper .front, .flipper .back { position: absolute; top: 0; left: 0; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden ; /*transform-style: preserve-3d;*/ display: block; - background-color: #d50000; height: 100%; width: 100%; /*outline: 1px solid transparent; /* FF AA fix */ + background-color: #d50000; height: 100%; width: 100%; /* outline: 1px solid transparent; /* FF AA fix */ } .flipper .back { background-color: white; z-index: 800; -webkit-transform: rotateY(-180deg) ; -moz-transform: rotateY(-180deg) ; -o-transform: rotateY(-180deg) ; -ms-transform: rotateY(-180deg) ; transform: rotateY(-180deg) } @@ -154,7 +154,10 @@ a { color: black } .loadingscreen.done .flipper-container { opacity: 0; -webkit-transition: all 1.5s ; -moz-transition: all 1.5s ; -o-transition: all 1.5s ; -ms-transition: all 1.5s ; transition: all 1.5s } -.progressbar { background: #26C281; position: fixed; z-index: 100; top: 0; left: 0; width: 0%; height: 2px; -webkit-transition: width 0.5s, opacity 1s; -moz-transition: width 0.5s, opacity 1s; -o-transition: width 0.5s, opacity 1s; -ms-transition: width 0.5s, opacity 1s; transition: width 0.5s, opacity 1s ; display: none } +.progressbar { + background: #26C281; position: fixed; width: 100%; z-index: 100; top: 0; left: 0; -webkit-transform: scaleX(0); -moz-transform: scaleX(0); -o-transform: scaleX(0); -ms-transform: scaleX(0); transform: scaleX(0) ; transform-origin: 0% 0%; transform:translate3d(0,0,0); + height: 2px; -webkit-transition: transform 0.5s, opacity 1s; -moz-transition: transform 0.5s, opacity 1s; -o-transition: transform 0.5s, opacity 1s; -ms-transition: transform 0.5s, opacity 1s; transition: transform 0.5s, opacity 1s ; display: none; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden ; transform-style: preserve-3d; +} .progressbar .peg { display: block; position: absolute; right: 0; width: 100px; height: 100%; -webkit-box-shadow: 0 0 10px #AF3BFF, 0 0 5px #29d; -moz-box-shadow: 0 0 10px #AF3BFF, 0 0 5px #29d; -o-box-shadow: 0 0 10px #AF3BFF, 0 0 5px #29d; -ms-box-shadow: 0 0 10px #AF3BFF, 0 0 5px #29d; box-shadow: 0 0 10px #AF3BFF, 0 0 5px #29d ; opacity: 1.0; -webkit-transform: rotate(3deg) translate(0px, -4px); -moz-transform: rotate(3deg) translate(0px, -4px); -o-transform: rotate(3deg) translate(0px, -4px); -ms-transform: rotate(3deg) translate(0px, -4px); transform: rotate(3deg) translate(0px, -4px) ; @@ -223,4 +226,4 @@ a { color: black } .notification .button { white-space: nowrap; } .notification { margin: 0px } .notifications { right: 0px } -} \ No newline at end of file +} From b23a4c8288958175bdddfe2c7ae0e9ee75b221ca Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 29 Mar 2018 02:44:43 +0200 Subject: [PATCH 0753/2570] Only apply outline to iframe when sidebar opened --- plugins/Sidebar/media/Sidebar.css | 4 ++-- plugins/Sidebar/media/all.css | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/Sidebar/media/Sidebar.css b/plugins/Sidebar/media/Sidebar.css index fa7ac7dc..bef0a681 100644 --- a/plugins/Sidebar/media/Sidebar.css +++ b/plugins/Sidebar/media/Sidebar.css @@ -8,8 +8,8 @@ .body-sidebar { background-color: #666 !important; } -#inner-iframe { transition: 0.3s ease-in-out; transform-origin: left; outline: 1px solid transparent; } -.body-sidebar iframe { transform: rotateY(5deg); opacity: 0.8; pointer-events: none } /* translateX(-200px) scale(0.95)*/ +#inner-iframe { transition: 0.3s ease-in-out; transform-origin: left; } +.body-sidebar iframe { transform: rotateY(5deg); opacity: 0.8; pointer-events: none; outline: 1px solid transparent } /* translateX(-200px) scale(0.95)*/ .sidebar .link-right { color: white; text-decoration: none; border-bottom: 1px solid #666; text-transform: uppercase; float: right; margin-right: 7px; margin-top: 1px; } .sidebar .link-right:hover { border-color: #CCC; } diff --git a/plugins/Sidebar/media/all.css b/plugins/Sidebar/media/all.css index fb793aa7..d91287c3 100644 --- a/plugins/Sidebar/media/all.css +++ b/plugins/Sidebar/media/all.css @@ -87,8 +87,8 @@ .body-sidebar { background-color: #666 !important; } -#inner-iframe { -webkit-transition: 0.3s ease-in-out; -moz-transition: 0.3s ease-in-out; -o-transition: 0.3s ease-in-out; -ms-transition: 0.3s ease-in-out; transition: 0.3s ease-in-out ; transform-origin: left; outline: 1px solid transparent; } -.body-sidebar iframe { -webkit-transform: rotateY(5deg); -moz-transform: rotateY(5deg); -o-transform: rotateY(5deg); -ms-transform: rotateY(5deg); transform: rotateY(5deg) ; opacity: 0.8; pointer-events: none } /* translateX(-200px) scale(0.95)*/ +#inner-iframe { -webkit-transition: 0.3s ease-in-out; -moz-transition: 0.3s ease-in-out; -o-transition: 0.3s ease-in-out; -ms-transition: 0.3s ease-in-out; transition: 0.3s ease-in-out ; transform-origin: left; } +.body-sidebar iframe { -webkit-transform: rotateY(5deg); -moz-transform: rotateY(5deg); -o-transform: rotateY(5deg); -ms-transform: rotateY(5deg); transform: rotateY(5deg) ; opacity: 0.8; pointer-events: none; outline: 1px solid transparent } /* translateX(-200px) scale(0.95)*/ .sidebar .link-right { color: white; text-decoration: none; border-bottom: 1px solid #666; text-transform: uppercase; float: right; margin-right: 7px; margin-top: 1px; } .sidebar .link-right:hover { border-color: #CCC; } From 6daf583b4b7dea68e41d2d9acb9e7a9098c1ca1e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 29 Mar 2018 02:45:33 +0200 Subject: [PATCH 0754/2570] Only log detailed message type error with debug_socket argument --- src/Connection/Connection.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 5e465640..88f3ac4d 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -141,7 +141,6 @@ class Connection(object): self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect((self.ip, int(self.port))) - # Detect protocol self.send({"cmd": "handshake", "req_id": 0, "params": self.getHandshakeInfo()}) event_connected = self.event_connected @@ -206,10 +205,9 @@ class Connection(object): except StopIteration: break if not type(message) is dict: - raise Exception( - "Invalid message type: %s, content: %r, buffer: %r" % - (type(message), message, buff[0:16]) - ) + if config.debug_socket: + self.log("Invalid message type: %s, content: %r, buffer: %r" % (type(message), message, buff[0:16])) + raise Exception("Invalid message type: %s" % type(message)) # Stats self.incomplete_buff_recv = 0 From d61cd96d8ff028c1acb1caf5bfe8aad3d9b4121a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 29 Mar 2018 02:49:06 +0200 Subject: [PATCH 0755/2570] VerifyFiles call returns more detailed statistics --- plugins/Bigfile/Test/TestBigfile.py | 2 +- src/Site/SiteStorage.py | 15 +++++++++++++-- src/Test/TestSite.py | 2 +- src/Test/TestSiteDownload.py | 4 ++-- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/plugins/Bigfile/Test/TestBigfile.py b/plugins/Bigfile/Test/TestBigfile.py index e3e58cf2..9078b9bf 100644 --- a/plugins/Bigfile/Test/TestBigfile.py +++ b/plugins/Bigfile/Test/TestBigfile.py @@ -136,7 +136,7 @@ class TestBigfile: # Download site site_temp.download(blind_includes=True).join(timeout=5) - bad_files = site_temp.storage.verifyFiles(quick_check=True) + bad_files = site_temp.storage.verifyFiles(quick_check=True)["bad_files"] assert not bad_files # client_piecefield = peer_client.piecefields[file_info["sha512"]].tostring() diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index fc5d5252..37533d1a 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -4,6 +4,7 @@ import shutil import json import time import sys +from collections import defaultdict import sqlite3 import gevent.event @@ -376,25 +377,32 @@ class SiteStorage(object): # Verify all files sha512sum using content.json def verifyFiles(self, quick_check=False, add_optional=False, add_changed=True): bad_files = [] + back = defaultdict(int) + back["bad_files"] = bad_files i = 0 + self.log.debug("Verifing files...") 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.items(): + back["num_content"] += 1 i += 1 if i % 50 == 0: time.sleep(0.0001) # Context switch to avoid gevent hangs 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) bad_files.append(content_inner_path) for file_relative_path in content.get("files", {}).keys(): + 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 / file_path = self.getPath(file_inner_path) if not os.path.isfile(file_path): + back["num_file_missing"] += 1 self.log.debug("[MISSING] %s" % file_inner_path) bad_files.append(file_inner_path) continue @@ -410,6 +418,7 @@ class SiteStorage(object): ok = False if not ok: + back["num_file_invalid"] += 1 self.log.debug("[INVALID] %s: %s" % (file_inner_path, err)) if add_changed or content.get("cert_user_id"): # If updating own site only add changed user files bad_files.append(file_inner_path) @@ -418,6 +427,7 @@ class SiteStorage(object): optional_added = 0 optional_removed = 0 for file_relative_path in content.get("files_optional", {}).keys(): + 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 file_inner_path = file_inner_path.strip("/") # Strip leading / @@ -455,16 +465,17 @@ class SiteStorage(object): ) time.sleep(0.0001) # Context switch to avoid gevent hangs - return bad_files + return back # Check and try to fix site files integrity def updateBadFiles(self, quick_check=True): s = time.time() - bad_files = self.verifyFiles( + res = self.verifyFiles( quick_check, add_optional=self.site.isDownloadable(""), add_changed=not self.site.settings.get("own") # Don't overwrite changed files if site owned ) + bad_files = res["bad_files"] self.site.bad_files = {} if bad_files: for bad_file in bad_files: diff --git a/src/Test/TestSite.py b/src/Test/TestSite.py index 71d1706f..368c6529 100644 --- a/src/Test/TestSite.py +++ b/src/Test/TestSite.py @@ -26,7 +26,7 @@ class TestSite: assert new_site.storage.isFile("index.html") assert new_site.storage.isFile("data/users/content.json") assert new_site.storage.isFile("data/zeroblog.db") - assert new_site.storage.verifyFiles() == [] # No bad files allowed + assert new_site.storage.verifyFiles()["bad_files"] == [] # No bad files allowed assert new_site.storage.query("SELECT * FROM keyvalue WHERE key = 'title'").fetchone()["value"] == "MyZeroBlog" # Test re-cloning (updating) diff --git a/src/Test/TestSiteDownload.py b/src/Test/TestSiteDownload.py index 13bf85c7..3c5a7531 100644 --- a/src/Test/TestSiteDownload.py +++ b/src/Test/TestSiteDownload.py @@ -48,7 +48,7 @@ class TestSiteDownload: assert "-default" in file_requests[-1] # Put default files for cloning to the end # Check files - bad_files = site_temp.storage.verifyFiles(quick_check=True) + bad_files = site_temp.storage.verifyFiles(quick_check=True)["bad_files"] # -1 because data/users/1J6... user has invalid cert assert len(site_temp.content_manager.contents) == len(site.content_manager.contents) - 1 @@ -72,7 +72,7 @@ class TestSiteDownload: # Download normally site_temp.addPeer("127.0.0.1", 1544) site_temp.download(blind_includes=True).join(timeout=5) - bad_files = site_temp.storage.verifyFiles(quick_check=True) + bad_files = site_temp.storage.verifyFiles(quick_check=True)["bad_files"] assert not bad_files assert "data/users/1C5sgvWaSgfaTpV5kjBCnCiKtENNMYo69q/content.json" in site_temp.content_manager.contents From 17be72e55e3dab1f61a3e9b161c67c14cb193b5b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 29 Mar 2018 02:50:23 +0200 Subject: [PATCH 0756/2570] Process delayed queries every second --- src/Db/Db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Db/Db.py b/src/Db/Db.py index c4b65eb1..b30a46bf 100644 --- a/src/Db/Db.py +++ b/src/Db/Db.py @@ -87,7 +87,7 @@ class Db(object): def executeDelayed(self, *args, **kwargs): if not self.delayed_queue_thread: - self.delayed_queue_thread = gevent.spawn_later(10, self.processDelayed) + self.delayed_queue_thread = gevent.spawn_later(1, self.processDelayed) self.delayed_queue.append(("execute", (args, kwargs))) def insertOrUpdateDelayed(self, *args, **kwargs): From f162987a4f4b4e4ce48c8d4e9c034b0e500b4487 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 29 Mar 2018 02:54:52 +0200 Subject: [PATCH 0757/2570] Rename optionalRemove to optionalRemoved and accept hash only by id --- plugins/OptionalManager/UiWebsocketPlugin.py | 4 ++-- src/Content/ContentManager.py | 24 +++++++++----------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/plugins/OptionalManager/UiWebsocketPlugin.py b/plugins/OptionalManager/UiWebsocketPlugin.py index 2f9d4e4b..fa2cfee6 100644 --- a/plugins/OptionalManager/UiWebsocketPlugin.py +++ b/plugins/OptionalManager/UiWebsocketPlugin.py @@ -14,6 +14,7 @@ if "_" not in locals(): bigfile_sha512_cache = {} + @PluginManager.registerTo("UiWebsocket") class UiWebsocketPlugin(object): def __init__(self, *args, **kwargs): @@ -228,7 +229,7 @@ class UiWebsocketPlugin(object): if not row: return self.response(to, {"error": "Not found in content.db"}) - removed = site.content_manager.optionalRemove(inner_path, row["hash_id"], row["size"]) + 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"]}) @@ -242,7 +243,6 @@ class UiWebsocketPlugin(object): self.response(to, "ok") - # Limit functions def actionOptionalLimitStats(self, to): diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 0ff71291..32c709d3 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -111,7 +111,8 @@ class ContentManager(object): changed.append(file_inner_path) # Download new file elif old_hash != new_hash and self.hashfield.hasHash(old_hash) and not self.site.settings.get("own"): try: - self.optionalRemove(file_inner_path, old_hash, old_content["files_optional"][relative_path]["size"]) + old_hash_id = self.hashfield.getHashId(old_hash) + self.optionalRemoved(file_inner_path, old_hash_id, old_content["files_optional"][relative_path]["size"]) self.site.storage.delete(file_inner_path) self.log.debug("Deleted changed optional file: %s" % file_inner_path) except Exception, err: @@ -144,7 +145,8 @@ class ContentManager(object): if old_content.get("files_optional") and old_content["files_optional"].get(file_relative_path): old_hash = old_content["files_optional"][file_relative_path].get("sha512") if self.hashfield.hasHash(old_hash): - self.optionalRemove(file_inner_path, old_hash, old_content["files_optional"][file_relative_path]["size"]) + old_hash_id = self.hashField.getHashid(old_hash) + self.optionalRemoved(file_inner_path, old_hash_id, old_content["files_optional"][file_relative_path]["size"]) self.log.debug("Deleted file: %s" % file_inner_path) except Exception, err: @@ -490,7 +492,7 @@ class ContentManager(object): file_size = os.path.getsize(file_path) sha512sum = CryptHash.sha512sum(file_path) # Calculate sha512 sum of file if optional and not self.hashfield.hasHash(sha512sum): - self.optionalDownloaded(file_inner_path, sha512sum, file_size, own=True) + self.optionalDownloaded(file_inner_path, self.hashfield.getHashId(sha512sum), file_size, own=True) back[file_relative_path] = {"sha512": sha512sum, "size": os.path.getsize(file_path)} return back @@ -907,22 +909,18 @@ class ContentManager(object): else: # File not in content.json raise VerifyError("File not in content.json") - def optionalDownloaded(self, inner_path, hash, size=None, own=False): + def optionalDownloaded(self, inner_path, hash_id, size=None, own=False): if size is None: size = self.site.storage.getSize(inner_path) - if type(hash) is int: - done = self.hashfield.appendHashId(hash) - else: - done = self.hashfield.appendHash(hash) + + done = self.hashfield.appendHashId(hash_id) self.site.settings["optional_downloaded"] += size return done - def optionalRemove(self, inner_path, hash, size=None): + def optionalRemoved(self, inner_path, hash_id, size=None): if size is None: size = self.site.storage.getSize(inner_path) - if type(hash) is int: - done = self.hashfield.removeHashId(hash) - else: - done = self.hashfield.removeHash(hash) + done = self.hashfield.removeHashId(hash_id) + self.site.settings["optional_downloaded"] -= size return done From 508d2472e9bfb92316550644a59169783bae2f6e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 29 Mar 2018 02:55:53 +0200 Subject: [PATCH 0758/2570] Only verify content after valid signiture --- src/Content/ContentManager.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 32c709d3..d923471b 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -858,8 +858,6 @@ class ContentManager(object): '"modified": %s' % modified_fixed ) - self.verifyContent(inner_path, new_content) - if signs: # New style signing valid_signers = self.getValidSigners(inner_path, new_content) signs_required = self.getSignsRequired(inner_path, new_content) @@ -881,10 +879,10 @@ class ContentManager(object): if valid_signs < signs_required: raise VerifyError("Valid signs: %s/%s" % (valid_signs, signs_required)) else: - return True + return self.verifyContent(inner_path, new_content) else: # Old style signing if CryptBitcoin.verify(sign_content, self.site.address, sign): - return True + return self.verifyContent(inner_path, new_content) else: raise VerifyError("Invalid old-style sign") From 16a9f38844bda3b197cb972466698997b43e060a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 29 Mar 2018 02:57:06 +0200 Subject: [PATCH 0759/2570] Use renamed optionalRemoved function --- plugins/OptionalManager/ContentDbPlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/OptionalManager/ContentDbPlugin.py b/plugins/OptionalManager/ContentDbPlugin.py index f03ed4bb..997092d0 100644 --- a/plugins/OptionalManager/ContentDbPlugin.py +++ b/plugins/OptionalManager/ContentDbPlugin.py @@ -203,7 +203,7 @@ class ContentDbPlugin(object): 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: + 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: @@ -389,7 +389,7 @@ class ContentDbPlugin(object): 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.optionalRemove(row["inner_path"], row["hash_id"], row["size"]) + 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: From c1397bbaf727c270313489aec79881cc42fbaa36 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 29 Mar 2018 02:58:24 +0200 Subject: [PATCH 0760/2570] Attach optionalmanager events to ContentManager instead of WorkerManager --- .../OptionalManager/OptionalManagerPlugin.py | 54 ++++++++++++------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/plugins/OptionalManager/OptionalManagerPlugin.py b/plugins/OptionalManager/OptionalManagerPlugin.py index ba2210aa..e8955da9 100644 --- a/plugins/OptionalManager/OptionalManagerPlugin.py +++ b/plugins/OptionalManager/OptionalManagerPlugin.py @@ -51,31 +51,45 @@ if "access_log" not in locals().keys(): # To keep between module reloads helper.timer(60, processRequestLog) +@PluginManager.registerTo("ContentManager") +class ContentManagerPlugin(object): + def optionalDownloaded(self, inner_path, hash_id, size=None, own=False): + is_pinned = 0 + if "|" in inner_path: # Big file piece + file_inner_path, file_range = inner_path.split("|") + # Auto-pin bigfiles + if size and config.pin_bigfile and size > 1024 * 1024 * config.pin_bigfile: + is_pinned = 1 + else: + file_inner_path = inner_path + + self.contents.db.executeDelayed( + "UPDATE file_optional SET time_downloaded = :now, is_downloaded = 1, peer = peer + 1, is_pinned = :is_pinned 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, "is_pinned": is_pinned} + ) + + return super(ContentManagerPlugin, self).optionalDownloaded(inner_path, hash_id, size, own) + + def optionalRemoved(self, inner_path, hash_id, size=None): + self.contents.db.execute( + "UPDATE file_optional SET is_downloaded = 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} + ) + + print "Removed hash_id: %s" % hash_id, self.contents.db.cur.cursor.rowcount + if self.contents.db.cur.cursor.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) + @PluginManager.registerTo("WorkerManager") class WorkerManagerPlugin(object): def doneTask(self, task): - content_db = self.site.content_manager.contents.db - if task["optional_hash_id"] and task["optional_hash_id"] not in self.site.content_manager.hashfield: - - inner_path = task["inner_path"] - is_pinned = 0 - if "|" in inner_path: # Big file piece - inner_path, file_range = inner_path.split("|") - file_info = self.site.content_manager.getFileInfo(inner_path) - # Auto-pin bigfiles - if config.pin_bigfile and file_info["size"] > 1024 * 1024 * config.pin_bigfile: - is_pinned = 1 - - - content_db.executeDelayed( - "UPDATE file_optional SET time_downloaded = :now, is_downloaded = 1, peer = peer + 1, is_pinned = :is_pinned WHERE site_id = :site_id AND inner_path = :inner_path", - {"now": int(time.time()), "site_id": content_db.site_ids[self.site.address], "inner_path": inner_path, "is_pinned": is_pinned} - ) - super(WorkerManagerPlugin, self).doneTask(task) - if task["optional_hash_id"] and not self.tasks: - content_db.processDelayed() + if task["optional_hash_id"] and not self.tasks: # Execute delayed queries immedietly after tasks finished + ContentDbPlugin.content_db.processDelayed() @PluginManager.registerTo("UiRequest") From af7c7da7358cd05e9b421eee22b9f307d69e5581 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 29 Mar 2018 02:59:11 +0200 Subject: [PATCH 0761/2570] Optional file downloaded checking by separate function --- src/Content/ContentManager.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index d923471b..ff6d3594 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -304,6 +304,14 @@ class ContentManager(object): else: return False + def isDownloaded(self, inner_path, hash_id=None): + if not hash_id: + file_info = self.getFileInfo(inner_path) + if not file_info or "sha512" not in file_info: + return False + hash_id = self.hashfield.getHashId(file_info["sha512"]) + return hash_id in self.hashfield + # Find the file info line from self.contents # Return: { "sha512": "c29d73d...21f518", "size": 41 , "content_inner_path": "content.json"} def getFileInfo(self, inner_path, new_file=False): From 101d2ea9b6c48e223d42840a53fa29cc1b6277e3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 29 Mar 2018 02:59:59 +0200 Subject: [PATCH 0762/2570] Check optional file downloaded status from sql if optionalmanager is present --- .../OptionalManager/OptionalManagerPlugin.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/plugins/OptionalManager/OptionalManagerPlugin.py b/plugins/OptionalManager/OptionalManagerPlugin.py index e8955da9..ff62058b 100644 --- a/plugins/OptionalManager/OptionalManagerPlugin.py +++ b/plugins/OptionalManager/OptionalManagerPlugin.py @@ -83,6 +83,27 @@ class ContentManagerPlugin(object): if self.isDownloaded(hash_id=hash_id, force_check_db=True): self.hashfield.appendHashId(hash_id) + 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[0]: + return True + else: + return False + + @PluginManager.registerTo("WorkerManager") class WorkerManagerPlugin(object): def doneTask(self, task): From 7d3d0f7ceb7595b0f26c6ea352f2047deb3bae71 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 29 Mar 2018 03:13:37 +0200 Subject: [PATCH 0763/2570] Add test for different files with same hash_id --- .../Test/TestOptionalManager.py | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/plugins/OptionalManager/Test/TestOptionalManager.py b/plugins/OptionalManager/Test/TestOptionalManager.py index a61aa078..ab33dc42 100644 --- a/plugins/OptionalManager/Test/TestOptionalManager.py +++ b/plugins/OptionalManager/Test/TestOptionalManager.py @@ -40,3 +40,68 @@ class TestOptionalManager: 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("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("A" * 1234) + site.storage.open("testfile2", "wb").write("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 From 5c644d1b364c6c02bbd6f89d059c6d965c752bd7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 29 Mar 2018 03:15:49 +0200 Subject: [PATCH 0764/2570] Make Bigfile plugin compatible with optionalDownloaded changes --- plugins/Bigfile/BigfilePlugin.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index 337e7c42..1b2ffa59 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -61,7 +61,8 @@ class UiRequestPlugin(object): if len(piecemap_info["sha512_pieces"]) == 1: # Small file, don't split hash = piecemap_info["sha512_pieces"][0].encode("hex") - site.content_manager.optionalDownloaded(inner_path, hash, upload_info["size"], own=True) + hash_id = self.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) @@ -88,7 +89,8 @@ class UiRequestPlugin(object): "piece_size": piece_size } - site.content_manager.optionalDownloaded(inner_path, merkle_root, upload_info["size"], own=True) + merkle_root_hash_id = self.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 @@ -301,7 +303,8 @@ class ContentManagerPlugin(object): piece_num = int(math.ceil(float(file_size) / piece_size)) # Add the merkle root to hashfield - self.optionalDownloaded(inner_path, hash, file_size, own=True) + hash_id = self.site.content_manager.hashfield.getHashId(hash) + self.optionalDownloaded(inner_path, hash_id, file_size, own=True) self.site.storage.piecefields[hash].fromstring("1" * piece_num) back[file_relative_path] = {"sha512": hash, "size": file_size, "piecemap": piecemap_relative_path, "piece_size": piece_size} @@ -331,7 +334,7 @@ class ContentManagerPlugin(object): return self.verifyPiece(inner_path, pos_from, file) - def optionalDownloaded(self, inner_path, hash, size=None, own=False): + 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("-")) @@ -342,7 +345,7 @@ class ContentManagerPlugin(object): self.site.storage.piecefields[file_info["sha512"]][piece_i] = True # Only add to site size on first request - if hash in self.hashfield: + if hash_id in self.hashfield: size = 0 elif size > 1024 * 1024: file_info = self.getFileInfo(inner_path) @@ -351,9 +354,9 @@ class ContentManagerPlugin(object): if sha512 not in self.site.storage.piecefields: self.site.storage.checkBigfile(inner_path) - return super(ContentManagerPlugin, self).optionalDownloaded(inner_path, hash, size, own) + return super(ContentManagerPlugin, self).optionalDownloaded(inner_path, hash_id, size, own) - def optionalRemove(self, inner_path, hash, size=None): + 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"] @@ -364,7 +367,7 @@ class ContentManagerPlugin(object): for key in self.site.bad_files.keys(): if key.startswith(inner_path + "|"): del self.site.bad_files[key] - return super(ContentManagerPlugin, self).optionalRemove(inner_path, hash, size) + return super(ContentManagerPlugin, self).optionalRemoved(inner_path, hash_id, size) @PluginManager.registerTo("SiteStorage") From b8d83c7ec4f42e68b530bd128c1f96dc99ddde6e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 29 Mar 2018 03:16:47 +0200 Subject: [PATCH 0765/2570] Mark site with has_bigfile if new bigfile found --- plugins/Bigfile/BigfilePlugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index 1b2ffa59..4b808cf3 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -432,6 +432,8 @@ class SiteStoragePlugin(object): 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"])) From 547242b1cbe3425da0c7b533ee6300c7c0a4d887 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 29 Mar 2018 03:17:45 +0200 Subject: [PATCH 0766/2570] Make siteVerify CLI command compatible with new verifyFiles return value --- src/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index 7f393a49..d0fe9b47 100644 --- a/src/main.py +++ b/src/main.py @@ -267,7 +267,7 @@ class Actions(object): bad_files += content_inner_path logging.info("Verifying site files...") - bad_files += site.storage.verifyFiles() + bad_files += site.storage.verifyFiles()["bad_files"] if not bad_files: logging.info("[OK] All file sha512sum matches! (%.3fs)" % (time.time() - s)) else: From 0459c75dc0262a7bbb033326690cca643b9f5877 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 29 Mar 2018 03:19:26 +0200 Subject: [PATCH 0767/2570] Make file verification process handle correctly different files with same has_id by using new isDownloaded function --- src/Site/SiteStorage.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index 37533d1a..49297f79 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -432,9 +432,12 @@ class SiteStorage(object): file_inner_path = helper.getDirname(content_inner_path) + file_relative_path # Relative to site dir file_inner_path = file_inner_path.strip("/") # Strip leading / file_path = self.getPath(file_inner_path) + hash_id = self.site.content_manager.hashfield.getHashId(file_node["sha512"]) if not os.path.isfile(file_path): - if self.site.content_manager.hashfield.hasHash(file_node["sha512"]): - self.site.content_manager.optionalRemove(file_inner_path, file_node["sha512"], file_node["size"]) + if self.site.content_manager.isDownloaded(file_inner_path, hash_id): + back["num_optional_removed"] += 1 + self.log.debug("[OPTIONAL REMOVED] %s" % file_inner_path) + self.site.content_manager.optionalRemoved(file_inner_path, hash_id, file_node["size"]) if add_optional: bad_files.append(file_inner_path) continue @@ -448,12 +451,15 @@ class SiteStorage(object): ok = False if ok: - if not self.site.content_manager.hashfield.hasHash(file_node["sha512"]): - self.site.content_manager.optionalDownloaded(file_inner_path, file_node["sha512"], file_node["size"]) + if not self.site.content_manager.isDownloaded(file_inner_path, hash_id): + back["num_optional_added"] += 1 + self.site.content_manager.optionalDownloaded(file_inner_path, hash_id, file_node["size"]) optional_added += 1 + self.log.debug("[OPTIONAL FOUND] %s" % file_inner_path) else: - if self.site.content_manager.hashfield.hasHash(file_node["sha512"]): - self.site.content_manager.optionalRemove(file_inner_path, file_node["sha512"], file_node["size"]) + if self.site.content_manager.isDownloaded(file_inner_path, hash_id): + back["num_optional_removed"] += 1 + self.site.content_manager.optionalRemoved(file_inner_path, hash_id, file_node["size"]) optional_removed += 1 bad_files.append(file_inner_path) self.log.debug("[OPTIONAL CHANGED] %s" % file_inner_path) @@ -464,6 +470,7 @@ class SiteStorage(object): (content_inner_path, len(content["files"]), quick_check, optional_added, optional_removed) ) + self.site.content_manager.contents.db.processDelayed() time.sleep(0.0001) # Context switch to avoid gevent hangs return back From 0139d862b547e3e41079138c8c6dfc50dd5b356f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 29 Mar 2018 03:20:08 +0200 Subject: [PATCH 0768/2570] Rev3395 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index f1f3edc2..bd1db432 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.2" - self.rev = 3377 + self.rev = 3395 self.argv = argv self.action = None self.config_file = "zeronet.conf" From a69f898e12d2d0c321acd3ee0e99924531109725 Mon Sep 17 00:00:00 2001 From: dqwyy Date: Sun, 1 Apr 2018 00:21:28 +0800 Subject: [PATCH 0769/2570] Update zh-tw.json (#1360) * Update zh-tw.json * Update zh-tw.json --- plugins/Sidebar/languages/zh-tw.json | 44 ++++++++++++++-------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/plugins/Sidebar/languages/zh-tw.json b/plugins/Sidebar/languages/zh-tw.json index 4610ac48..9d4ea1be 100644 --- a/plugins/Sidebar/languages/zh-tw.json +++ b/plugins/Sidebar/languages/zh-tw.json @@ -5,37 +5,37 @@ "Connectable peers": "可連線節點", "Data transfer": "數據傳輸", - "Received": "已收到", - "Received bytes": "收到字節", + "Received": "已接收", + "Received bytes": "已接收位元組", "Sent": "已傳送", - "Sent bytes": "傳送字節", + "Sent bytes": "已傳送位元組", - "Files": "檔", + "Files": "檔案", "Total": "共計", "Image": "圖片", "Other": "其他", - "User data": "用戶數據", + "User data": "使用者數據", "Size limit": "大小限制", - "limit used": "限额", + "limit used": "已使用", "free space": "可用空間", - "Set": "設定", + "Set": "偏好設定", - "Optional files": "可選文件", + "Optional files": "可選檔案", "Downloaded": "已下載", - "Download and help distribute all files": "下載並幫助分發所有文件", + "Download and help distribute all files": "下載並幫助分發所有檔案", "Total size": "總大小", - "Downloaded files": "下載的文件", + "Downloaded files": "下載的檔案", "Database": "資料庫", "search feeds": "搜尋供稿", "{feeds} query": "{feeds} 查詢 ", - "Reload": "重載", + "Reload": "重新整理", "Rebuild": "重建", "No database found": "未找到資料庫", - "Identity address": "身份地址", - "Change": "改變", + "Identity address": "身分位址", + "Change": "變更", "Site control": "網站控制", "Update": "更新", @@ -44,24 +44,24 @@ "Delete": "刪除", "Are you sure?": "你確定?", - "Site address": "網站地址", + "Site address": "網站位址", "Donate": "捐贈", - "Missing files": "缺少的檔", + "Missing files": "缺少的檔案", "{} try": "{} 嘗試", "{} tries": "{} 已嘗試", "+ {num_bad_files} more": "+ {num_bad_files} 更多", "This is my site": "這是我的網站", "Site title": "網站標題", - "Site description": "網站描寫", + "Site description": "網站描述", "Save site settings": "存儲網站設定", - "Open site directory": "打開所在文件夾", + "Open site directory": "打開所在資料夾", - "Content publishing": "內容髮布", + "Content publishing": "內容發布", "Choose": "選擇", "Sign": "簽署", - "Publish": "發佈", + "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 地理位置資料庫下載錯誤:{}!
          請手動下載並解壓到數據目錄:
          {}", @@ -69,15 +69,15 @@ "GeoLite2 City database downloaded!": "GeoLite2 地理位置資料庫已下載!", "Are you sure?": "你確定?", - "Site storage limit modified!": "網站存儲限制已改變!", + "Site storage limit modified!": "網站存儲限制已變更!", "Database schema reloaded!": "資料庫架構重新加載!", "Database rebuilding....": "資料庫重建中...", "Database rebuilt!": "資料庫已重建!", "Site updated!": "網站已更新!", "Delete this site": "刪除此網站", - "File write error: ": "檔寫入錯誤:", + "File write error: ": "檔案寫入錯誤:", "Site settings saved!": "網站設置已保存!", "Enter your private key:": "輸入您的私鑰:", " Signed!": " 已簽署!", - "WebGL not supported": "不支持 WebGL" + "WebGL not supported": "不支援 WebGL" } From c2176ba27d699bcd221a927593d962147e0fb559 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Apr 2018 14:40:29 +0200 Subject: [PATCH 0770/2570] Add current fileserver port to stats page --- plugins/Stats/StatsPlugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py index 9a2db5a5..4b8cf5a3 100644 --- a/plugins/Stats/StatsPlugin.py +++ b/plugins/Stats/StatsPlugin.py @@ -68,6 +68,7 @@ class UiRequestPlugin(object): try: yield "rev%s | " % config.rev yield "%s | " % config.ip_external + yield "Port: %s | " % main.file_server.port yield "Opened: %s | " % main.file_server.port_opened yield "Crypt: %s | " % CryptConnection.manager.crypt_supported yield "In: %.2fMB, Out: %.2fMB | " % ( From 1b8b3cf1eecd095af951ce31b88d30ae1762039e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Apr 2018 14:40:47 +0200 Subject: [PATCH 0771/2570] Fix site loading with Zeroname plugin --- plugins/Zeroname/SiteManagerPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Zeroname/SiteManagerPlugin.py b/plugins/Zeroname/SiteManagerPlugin.py index 89ad9508..9691a328 100644 --- a/plugins/Zeroname/SiteManagerPlugin.py +++ b/plugins/Zeroname/SiteManagerPlugin.py @@ -65,7 +65,7 @@ class SiteManagerPlugin(object): # Return: Site object or None if not found def get(self, address): - if self.sites is None: # Not loaded yet + if not self.loaded: # Not loaded yet self.load() if self.isDomain(address): # Its looks like a domain address_resolved = self.resolveDomain(address) From a438803a5ab3659cbd4d11f1c883e49fcc456c7b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Apr 2018 14:42:16 +0200 Subject: [PATCH 0772/2570] Change zero.booth.moe tracker to https port --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index bd1db432..3e2313cd 100644 --- a/src/Config.py +++ b/src/Config.py @@ -34,7 +34,7 @@ class Config(object): def createArguments(self): trackers = [ "zero://boot3rdez4rzn36x.onion:15441", - "zero://zero.booth.moe#f36ca555bee6ba216b14d10f38c16f7769ff064e0e37d887603548cc2e64191d:15441", # US/NY + "zero://zero.booth.moe#f36ca555bee6ba216b14d10f38c16f7769ff064e0e37d887603548cc2e64191d:443", # US/NY "udp://tracker.coppersurfer.tk:6969", # DE "udp://tracker.leechers-paradise.org:6969", # NL "udp://9.rarbg.com:2710", # FR From b39da7e356e4d773f024db4b3290d48551df997d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Apr 2018 14:43:12 +0200 Subject: [PATCH 0773/2570] Change unreliable trackers --- src/Config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index 3e2313cd..4738a8be 100644 --- a/src/Config.py +++ b/src/Config.py @@ -37,8 +37,8 @@ class Config(object): "zero://zero.booth.moe#f36ca555bee6ba216b14d10f38c16f7769ff064e0e37d887603548cc2e64191d:443", # US/NY "udp://tracker.coppersurfer.tk:6969", # DE "udp://tracker.leechers-paradise.org:6969", # NL - "udp://9.rarbg.com:2710", # FR - "http://tracker.city9x.com:2710/announce", # US/LA + "udp://thetracker.org:80", # FR + "http://torrentsmd.eu:8080/announce", # US?/Cloudflare "http://0d.kebhana.mx:443/announce", # FR "http://retracker.spark-rostov.ru:80/announce" # RU ] From 270922b46057702db238d92f29339cd87c96e227 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Apr 2018 14:43:41 +0200 Subject: [PATCH 0774/2570] Fix implicit ssl connection typo --- 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 88f3ac4d..6658cf20 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -127,7 +127,7 @@ class Connection(object): self.sock.do_handshake() self.crypt = "tls-rsa" self.sock_wrapped = True - elif should_encrypt and "tsl-rsa" in CryptConnection.manager.crypt_supported: + elif should_encrypt and "tls-rsa" in CryptConnection.manager.crypt_supported: try: self.sock = CryptConnection.manager.wrapSocket(self.sock, "tls-rsa") self.sock.do_handshake() From 852aaae52e1070e45e86cc659b6c7083d5de7dba Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Apr 2018 14:45:04 +0200 Subject: [PATCH 0775/2570] Randomize fileserver port --- src/Config.py | 3 ++- src/Connection/ConnectionServer.py | 17 +++++++------ src/File/FileServer.py | 38 ++++++++++++++++++++++++++++-- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/src/Config.py b/src/Config.py index 4738a8be..c5ebf060 100644 --- a/src/Config.py +++ b/src/Config.py @@ -212,7 +212,8 @@ class Config(object): self.parser.add_argument('--workers', help='Download workers per site', default=5, type=int, metavar='workers') self.parser.add_argument('--fileserver_ip', help='FileServer bind address', default="*", metavar='ip') - self.parser.add_argument('--fileserver_port', help='FileServer bind port', default=15441, type=int, metavar='port') + 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') self.parser.add_argument('--ip_local', help='My local ips', default=ip_local, type=int, metavar='ip', nargs='*') self.parser.add_argument('--disable_udp', help='Disable UDP connections', action='store_true') diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index e6c04dd9..84cef96e 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -61,22 +61,25 @@ class ConnectionServer(object): ) sys.exit(0) - if port: # Listen server on a port - self.pool = Pool(500) # do not accept more than 500 connections - self.stream_server = StreamServer( - (ip.replace("*", "0.0.0.0"), port), self.handleIncomingConnection, spawn=self.pool, backlog=100 - ) - if request_handler: - self.handleRequest = request_handler + if request_handler: + self.handleRequest = request_handler def start(self): self.running = True CryptConnection.manager.loadCerts() + if not self.port: + self.log.info("No port found, not binding") + return False + self.log.debug("Binding to: %s:%s, (msgpack: %s), supported crypt: %s" % ( self.ip, self.port, ".".join(map(str, msgpack.version)), CryptConnection.manager.crypt_supported) ) try: + self.pool = Pool(500) # do not accept more than 500 connections + self.stream_server = StreamServer( + (self.ip, self.port), self.handleIncomingConnection, spawn=self.pool, backlog=100 + ) self.stream_server.serve_forever() # Start normal connection server except Exception, err: self.log.info("StreamServer bind error, must be running already: %s" % err) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index b76467c9..65882bbd 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -3,6 +3,7 @@ import urllib2 import re import time import random +import socket import gevent @@ -20,10 +21,21 @@ from Plugin import PluginManager class FileServer(ConnectionServer): def __init__(self, ip=config.fileserver_ip, port=config.fileserver_port): - ConnectionServer.__init__(self, ip, port, self.handleRequest) - self.site_manager = SiteManager.site_manager self.log = logging.getLogger("FileServer") + ip = ip.replace("*", "0.0.0.0") + + should_use_random_port = port == 0 or config.tor == "always" + if should_use_random_port: + port_range_from, port_range_to = map(int, config.fileserver_port_range.split("-")) + port = self.getRandomPort(ip, port_range_from, port_range_to) + config.fileserver_port = port + if not port: + raise Exception("Can't find bindable port") + if not config.tor == "always": + config.saveValue("fileserver_port", port) # Save random port value for next restart + + ConnectionServer.__init__(self, ip, port, self.handleRequest) if config.ip_external: # Ip external defined in arguments self.port_opened = True @@ -36,6 +48,28 @@ class FileServer(ConnectionServer): self.files_parsing = {} self.ui_server = None + 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)) + tried = [] + for bind_retry in range(100): + port = random.randint(port_range_from, port_range_to) + if port in tried: + continue + tried.append(port) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.bind((ip, port)) + success = True + except Exception as err: + self.log.warning("Error binding to port %s: %s" % (port, err)) + success = False + sock.close() + if success: + return port + else: + time.sleep(0.1) + return False + # Handle request to fileserver def handleRequest(self, connection, message): if config.verbose: From 09f1ad06250f60db81713b768dda03e6e479240b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Apr 2018 14:47:43 +0200 Subject: [PATCH 0776/2570] Add websocket client to allow API command requests from CLI --- src/lib/websocket/ChangeLog | 302 +++++++++++++++++++ src/lib/websocket/LICENSE | 135 +++++++++ src/lib/websocket/README.rst | 268 +++++++++++++++++ src/lib/websocket/__init__.py | 29 ++ src/lib/websocket/_abnf.py | 447 ++++++++++++++++++++++++++++ src/lib/websocket/_app.py | 320 ++++++++++++++++++++ src/lib/websocket/_cookiejar.py | 52 ++++ src/lib/websocket/_core.py | 495 +++++++++++++++++++++++++++++++ src/lib/websocket/_exceptions.py | 87 ++++++ src/lib/websocket/_handshake.py | 180 +++++++++++ src/lib/websocket/_http.py | 256 ++++++++++++++++ src/lib/websocket/_logging.py | 74 +++++ src/lib/websocket/_socket.py | 126 ++++++++ src/lib/websocket/_ssl_compat.py | 44 +++ src/lib/websocket/_url.py | 160 ++++++++++ src/lib/websocket/_utils.py | 105 +++++++ src/main.py | 19 ++ 17 files changed, 3099 insertions(+) create mode 100644 src/lib/websocket/ChangeLog create mode 100644 src/lib/websocket/LICENSE create mode 100644 src/lib/websocket/README.rst create mode 100644 src/lib/websocket/__init__.py create mode 100644 src/lib/websocket/_abnf.py create mode 100644 src/lib/websocket/_app.py create mode 100644 src/lib/websocket/_cookiejar.py create mode 100644 src/lib/websocket/_core.py create mode 100644 src/lib/websocket/_exceptions.py create mode 100644 src/lib/websocket/_handshake.py create mode 100644 src/lib/websocket/_http.py create mode 100644 src/lib/websocket/_logging.py create mode 100644 src/lib/websocket/_socket.py create mode 100644 src/lib/websocket/_ssl_compat.py create mode 100644 src/lib/websocket/_url.py create mode 100644 src/lib/websocket/_utils.py diff --git a/src/lib/websocket/ChangeLog b/src/lib/websocket/ChangeLog new file mode 100644 index 00000000..f4483d1e --- /dev/null +++ b/src/lib/websocket/ChangeLog @@ -0,0 +1,302 @@ +ChangeLog +============ + +- 0.47.0 + + - Fix socket constructor in _open_socket to use all relevant variables from getaddrinfo. (#383) + - .send() method is very slow (#340) + - cross-platform aync multi-client solution (#375) + - Fix detecting timeouts with SSL in recv (#387) + - Fix WebSocketApp does not poll for data correctly when using SSL (#384) + - Fix Infinite ping/pong timeouts in WebSocketApp.run_forever (#395) + - Added status message when HTTP can't be upgraded to WS (#399) + +- 0.46.0 + + - fixed OSError on windows (#370) + - fixed invalid character (#379) + +- 0.45.0 + + - change license to LGP v2.1 + - allow reuse of WebsocketApp.run_forever (#365) + - Update example for python3 (#360) + - add lock to recv function (#356) + - Parse close frame response correctly when reason present (#354) + - Fix SSL: SSLV3_ALERT_HANDSHAKE_FAILURE on Debian Stretch (#353) + - Wrap socket.gaierror with subclass of WebsocketException (#352) + - Resolve a proxy issue and a connection error (#345) + - Allow empty Host header value (#369) + - Fix undefined variable (#347) + - fix: getting a value with the key 'ca_certs' in sslopt dict (#326) + +- 0.44.0 + + -renames key in sslopt dict (#326) + +- 0.43.0 + + - Unkown kwarg 'ca_cert' when calling ssl wrap_socket() (#326) + - Race condition in WebSocket ping/pong (#327) + +- 0.42.0 + + - Implement simple cookie jar(#292) + - fix: when using pppoe redial will block.(#301) + - Fix insecure_pythons list in setup.py(#304) + - Support WEBSOCKET_CLIENT_CA_BUNDLE being directory(#307) + - WebSocketPayloadException under high traffic and limited network connection(#306) + - Not working --nocert parameter in wsdump.py(#315) + - Avoid the app to block on close on certain systems (#320) + - Fix warning is not defined. (#323) + +- 0.41.0 + + - move to repository to https://github.com/websocket-client/websocket-client.git + - _send_ping warning fails due to missing reference in _logging.__all__ (#294) + +- 0.40.0 + - Fix opcode -> op_code (#286) + +- 0.39.0 + - Shuffled around example code (#256) + - _send_ping graceful error handling (#262) + - Allow closing WebSocketApp with status/reason/timeout (#265) + - Support universal wheels (#267) + - _url: Added subnet IP address matching in no_proxy host detection (#270) + - fixed Incorrect encoding in continued messages python3 (#261) + - Pass headers for websocket handshake (#271) + - setup.py: Import `logging` before calling it. (#272) + - Implemented close code 1014 (#273) + - Support CA bundle specified by environment variable (#279) + - Response header values should not be converted to lower case (#264) + +- 0.38.0 + - Exclude port 443 from host http header (#248) + - Cleanup code (#249) + - Modify a code block directive in README (#250) + - fixed ping/pong timeouet (#253) + +- 0.37.0 + - fixed failure that `websocket.create_connection` does not accept `origin` as a parameter (#246 ) + +- 0.36.0 + - added support for using custom connection class (#235) + - use Named logger (#238) + - implement ping/pong timeout (#241) + - Corrects the syntax highlight code (#243) + - fixed failure to join thread before it is started (#242) + +- 0.35.0 + - Prints timings in console (#217) + - use inspect.getfullargspec with Python 3.x (#219) + - Check that exception message is actually a string before trying for substring check (#224) + - Use pre-initialized stream socket (#226) + - fixed TypeError: cafile, capath and cadata cannot be all omitted (#227) + +- 0.34.0 + + - Change import style (#203) + - fix attribute error on the older python. (#215) + +- 0.33.0 + + - fixed timeout+ssl error handling bug on python 2.7.10 (#190) + - add proxy support to wsdump.py (#194) + - use wsaccel if available (#193) + - add support for ssl cert chains to support client certs (#195) + - fix string formatting in exception (#196) + - fix typo in README.rst (#197) + - introduce on_data callback to pass data type. (#198) + - WebSocketBadStatusException for Handshake error (#199) + - set close timeout (#192) + - Map dict to headers list (#204) + - support client certification (#207) + - security improvement during handshake (#211) + - improve logging of error from callback (#212) + +- 0.32.0 + + - fix http proxy bug (#189) + +- 0.31.0 + + - Avoid deprecated BaseException.message (#180) + - Add travis builds (#182) + - fixed wsdump to work with piped input (#183) + - fixed output of wsdump.py with python3 (#185) + - add raw mode to wsdump.py (#186) + +- 0.30.0 + + - fixed if client is behind proxy (#169) + - support SNI for python 2.7.9+ and 3.2+ (#172) + - update Host HTTP header by user. (#171) + - fix typo for isEnabledFor (#173) + - can set verify_mode to CERT_NONE when check_hostname is enabled.(#175) + - make websockets iterable (#178) + +- 0.29.0 + + - fixed ssl socket bug + +- 0.28.0 + + - Fix erroneous argument shadowing(#168) + +- 0.27.0 + + - remove unittest2 requirements for python 2.6 (#156) + - fixed subprotocol case during header validation (#158) + - get response status and headers (#160) + - fix out-of-memory due to fragmentation when receiving a very large frame(#163) + - fix error if the payload data is nothing.(#166) + - refactoring. + +- 0.26.0 + + - all WebSocketException provide message string (#152) + - fixed tests fail when not connected to the network (#155) + - Add command line options and handle closed socket to wsdump.py (#153) + +- 0.25.0 + + - fixed for Python 2.6(#151) + +- 0.24.0 + + - Supporting http-basic auth in WebSocketApp (#143) + - fix failure of test.testInternalRecvStrict(#141) + - skip utf8 validation by skip_utf8_validation argument (#137) + - WebsocketProxyException will be raised if we got error about proxy.(#138) + +- 0.23.0 + + - Remove spurious print statement. (#135) + +- 0.22.0 + + - Fix not thread-safe of Websocket.close() (#120) + - Try to get proxy info from environment if not explicitly provided (#124) + - support proxy basic authentication. (#125) + - Fix NoneType exception at WebsocketApp.send (#126) + - not use proxy for localhost (#132) + +- 0.21.0 + + - Check for socket before attempting to close (#115) + - Enable turning off SSL verification in wsdump.py(#116) + - Enable to set subprotocol(#118) + - Better support for Autobahn test suite (http://autobahn.ws/testsuite) (#117) + +- v0.20.0 + + - fix typo. + +- v0.19.0 + + - suppress close event message(#107) + - detect socket connection state(#109) + - support for code and reason in on_close callback(#111) + - continuation frame handling seems suspicious(#113) + +- v0.18.0 + + - allow override of match_hostname usage on ssl (#105) + +- v0.17.0 + + - can't set timeout on a standing websocket connection (#102) + - fixed local variable 'error' referenced before assignment (#102, #98) + +- v0.16.0 + + - lock some method for multithread. (#92) + - disable cert verification. (#89) + +- v0.15.0 + + - fixed exception when send a large message (#84) + +- v0.14.1 + + - fixed to work on Python2.6 (#83) + +- v0.14.0 + + - Support python 3(#73) + - Support IPv6(#77) + - Support explicit web proxy(#57) + - specify cookie in connect method option(#82) + +- v0.13.0 + + - MemoryError when receiving large amount of data (~60 MB) at once(ISSUE#59) + - Controlling fragmentation(ISSUE#55) + - server certificate validation(ISSUE#56) + - PyPI tarball is missing test_websocket.py(ISSUE#65) + - Payload length encoding bug(ISSUE#58) + - disable Nagle algorithm by default(ISSUE#41) + - Better event loop in WebSocketApp(ISSUE#63) + - Skip tests that require Internet access by default(ISSUE#66) + +- v0.12.0 + + - support keep alive for WebSocketApp(ISSUE#34) + - fix some SSL bugs(ISSUE#35, #36) + - fix "Timing out leaves websocket library in bad state"(ISSUE#37) + - fix "WebSocketApp.run_with_no_err() silently eats all exceptions"(ISSUE#38) + - WebSocketTimeoutException will be raised for ws/wss timeout(ISSUE#40) + - improve wsdump message(ISSUE#42) + - support fragmentation message(ISSUE#43) + - fix some bugs + +- v0.11.0 + + - Only log non-normal close status(ISSUE#31) + - Fix default Origin isn't URI(ISSUE#32) + - fileno support(ISSUE#33) + +- v0.10.0 + + - allow to set HTTP Header to WebSocketApp(ISSUE#27) + - fix typo in pydoc(ISSUE#28) + - Passing a socketopt flag to the websocket constructor(ISSUE#29) + - websocket.send fails with long data(ISSUE#30) + + +- v0.9.0 + + - allow to set opcode in WebSocketApp.send(ISSUE#25) + - allow to modify Origin(ISSUE#26) + +- v0.8.0 + + - many bug fix + - some performance improvement + +- v0.7.0 + + - fixed problem to read long data.(ISSUE#12) + - fix buffer size boundary violation + +- v0.6.0 + + - Patches: UUID4, self.keep_running, mask_key (ISSUE#11) + - add wsdump.py tool + +- v0.5.2 + + - fix Echo App Demo Throw Error: 'NoneType' object has no attribute 'opcode (ISSUE#10) + +- v0.5.1 + + - delete invalid print statement. + +- v0.5.0 + + - support hybi-13 protocol. + +- v0.4.1 + + - fix incorrect custom header order(ISSUE#1) diff --git a/src/lib/websocket/LICENSE b/src/lib/websocket/LICENSE new file mode 100644 index 00000000..342ae716 --- /dev/null +++ b/src/lib/websocket/LICENSE @@ -0,0 +1,135 @@ +GNU LESSER GENERAL PUBLIC LICENSE +Version 2.1, February 1999 + +Copyright (C) 1991, 1999 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] +Preamble +The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. + +This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. + +When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. + +To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. + +We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. + +To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. + +Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. + +Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. + +When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. + +We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. + +For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. + +In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. + +Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. + +The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION +0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". + +A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. + +You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: + +a) The modified work must itself be a software library. +b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. +c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. +d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. +(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. + +3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. + +However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. + +When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. + +If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. + +6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: + +a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) +b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. +c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. +d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. +e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. +For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. + +It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: + +a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. +b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. +8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. + +This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. + +NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. diff --git a/src/lib/websocket/README.rst b/src/lib/websocket/README.rst new file mode 100644 index 00000000..7c303983 --- /dev/null +++ b/src/lib/websocket/README.rst @@ -0,0 +1,268 @@ +================= +websocket-client +================= + +websocket-client module is WebSocket client for python. This provide the low level APIs for WebSocket. All APIs are the synchronous functions. + +websocket-client supports only hybi-13. + + +License +============ + + - LGPL + +Installation +============= + +This module is tested on Python 2.7 and Python 3.x. + +Type "python setup.py install" or "pip install websocket-client" to install. + +.. CAUTION:: + + from v0.16.0, we can install by "pip install websocket-client" for python 3. + +This module depend on + + - six + - backports.ssl_match_hostname for Python 2.x + +performance +------------------ + + "send" method is too slow on pure python. If you want to get better performace, please install numpy or wsaccel. +You can get the best performance from numpy. + + +How about Python 3 +=========================== + +Now, we support python 3 on single source code from version 0.14.0. Thanks, @battlemidget and @ralphbean. + +HTTP Proxy +============= + +Support websocket access via http proxy. +The proxy server must allow "CONNECT" method to websocket port. +Default squid setting is "ALLOWED TO CONNECT ONLY HTTPS PORT". + +Current implementation of websocket-client is using "CONNECT" method via proxy. + + +example + +.. code:: python + + import websocket + ws = websocket.WebSocket() + ws.connect("ws://example.com/websocket", http_proxy_host="proxy_host_name", http_proxy_port=3128) + + + + +Examples +======== + +Long-lived connection +--------------------- +This example is similar to how WebSocket code looks in browsers using JavaScript. + +.. code:: python + + import websocket + try: + import thread + except ImportError: + import _thread as thread + import time + + def on_message(ws, message): + print(message) + + def on_error(ws, error): + print(error) + + def on_close(ws): + print("### closed ###") + + def on_open(ws): + def run(*args): + for i in range(3): + time.sleep(1) + ws.send("Hello %d" % i) + time.sleep(1) + ws.close() + print("thread terminating...") + thread.start_new_thread(run, ()) + + + if __name__ == "__main__": + websocket.enableTrace(True) + ws = websocket.WebSocketApp("ws://echo.websocket.org/", + on_message = on_message, + on_error = on_error, + on_close = on_close) + ws.on_open = on_open + ws.run_forever() + + +Short-lived one-off send-receive +-------------------------------- +This is if you want to communicate a short message and disconnect immediately when done. + +.. code:: python + + from websocket import create_connection + ws = create_connection("ws://echo.websocket.org/") + print("Sending 'Hello, World'...") + ws.send("Hello, World") + print("Sent") + print("Receiving...") + result = ws.recv() + print("Received '%s'" % result) + ws.close() + +If you want to customize socket options, set sockopt. + +sockopt example + +.. code:: python + + from websocket import create_connection + ws = create_connection("ws://echo.websocket.org/", + sockopt=((socket.IPPROTO_TCP, socket.TCP_NODELAY),)) + + +More advanced: Custom class +--------------------------- +You can also write your own class for the connection, if you want to handle the nitty-gritty details yourself. + +.. code:: python + + import socket + from websocket import create_connection, WebSocket + class MyWebSocket(WebSocket): + def recv_frame(self): + frame = super().recv_frame() + print('yay! I got this frame: ', frame) + return frame + + ws = create_connection("ws://echo.websocket.org/", + sockopt=((socket.IPPROTO_TCP, socket.TCP_NODELAY, 1),), class_=MyWebSocket) + + +FAQ +============ + +How to disable ssl cert verification? +---------------------------------------- + +Please set sslopt to {"cert_reqs": ssl.CERT_NONE}. + +WebSocketApp sample + +.. code:: python + + ws = websocket.WebSocketApp("wss://echo.websocket.org") + ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE}) + +create_connection sample + +.. code:: python + + ws = websocket.create_connection("wss://echo.websocket.org", + sslopt={"cert_reqs": ssl.CERT_NONE}) + +WebSocket sample + +.. code:: python + + ws = websocket.WebSocket(sslopt={"cert_reqs": ssl.CERT_NONE}) + ws.connect("wss://echo.websocket.org") + + +How to disable hostname verification. +---------------------------------------- + +Please set sslopt to {"check_hostname": False}. +(since v0.18.0) + +WebSocketApp sample + +.. code:: python + + ws = websocket.WebSocketApp("wss://echo.websocket.org") + ws.run_forever(sslopt={"check_hostname": False}) + +create_connection sample + +.. code:: python + + ws = websocket.create_connection("wss://echo.websocket.org", + sslopt={"check_hostname": False}) + +WebSocket sample + +.. code:: python + + ws = websocket.WebSocket(sslopt={"check_hostname": False}) + ws.connect("wss://echo.websocket.org") + + +How to enable `SNI `_? +--------------------------------------------------------------------------- + +SNI support is available for Python 2.7.9+ and 3.2+. It will be enabled automatically whenever possible. + + +Sub Protocols. +---------------------------------------- + +The server needs to support sub protocols, please set the subprotocol like this. + + +Subprotocol sample + +.. code:: python + + ws = websocket.create_connection("ws://example.com/websocket", subprotocols=["binary", "base64"]) + + + +wsdump.py +============ + +wsdump.py is simple WebSocket test(debug) tool. + +sample for echo.websocket.org:: + + $ wsdump.py ws://echo.websocket.org/ + Press Ctrl+C to quit + > Hello, WebSocket + < Hello, WebSocket + > How are you? + < How are you? + +Usage +--------- + +usage:: + + wsdump.py [-h] [-v [VERBOSE]] ws_url + +WebSocket Simple Dump Tool + +positional arguments: + ws_url websocket url. ex. ws://echo.websocket.org/ + +optional arguments: + -h, --help show this help message and exit +WebSocketApp + -v VERBOSE, --verbose VERBOSE set verbose mode. If set to 1, show opcode. If set to 2, enable to trace websocket module + +example:: + + $ wsdump.py ws://echo.websocket.org/ + $ wsdump.py ws://echo.websocket.org/ -v + $ wsdump.py ws://echo.websocket.org/ -vv diff --git a/src/lib/websocket/__init__.py b/src/lib/websocket/__init__.py new file mode 100644 index 00000000..b90e65ad --- /dev/null +++ b/src/lib/websocket/__init__.py @@ -0,0 +1,29 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +from ._abnf import * +from ._app import WebSocketApp +from ._core import * +from ._exceptions import * +from ._logging import * +from ._socket import * + +__version__ = "0.47.0" diff --git a/src/lib/websocket/_abnf.py b/src/lib/websocket/_abnf.py new file mode 100644 index 00000000..a0000fa1 --- /dev/null +++ b/src/lib/websocket/_abnf.py @@ -0,0 +1,447 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +import array +import os +import struct + +import six + +from ._exceptions import * +from ._utils import validate_utf8 +from threading import Lock + +try: + if six.PY3: + import numpy + else: + numpy = None +except ImportError: + numpy = None + +try: + # If wsaccel is available we use compiled routines to mask data. + if not numpy: + from wsaccel.xormask import XorMaskerSimple + + def _mask(_m, _d): + return XorMaskerSimple(_m).process(_d) +except ImportError: + # wsaccel is not available, we rely on python implementations. + def _mask(_m, _d): + for i in range(len(_d)): + _d[i] ^= _m[i % 4] + + if six.PY3: + return _d.tobytes() + else: + return _d.tostring() + + +__all__ = [ + 'ABNF', 'continuous_frame', 'frame_buffer', + 'STATUS_NORMAL', + 'STATUS_GOING_AWAY', + 'STATUS_PROTOCOL_ERROR', + 'STATUS_UNSUPPORTED_DATA_TYPE', + 'STATUS_STATUS_NOT_AVAILABLE', + 'STATUS_ABNORMAL_CLOSED', + 'STATUS_INVALID_PAYLOAD', + 'STATUS_POLICY_VIOLATION', + 'STATUS_MESSAGE_TOO_BIG', + 'STATUS_INVALID_EXTENSION', + 'STATUS_UNEXPECTED_CONDITION', + 'STATUS_BAD_GATEWAY', + 'STATUS_TLS_HANDSHAKE_ERROR', +] + +# closing frame status codes. +STATUS_NORMAL = 1000 +STATUS_GOING_AWAY = 1001 +STATUS_PROTOCOL_ERROR = 1002 +STATUS_UNSUPPORTED_DATA_TYPE = 1003 +STATUS_STATUS_NOT_AVAILABLE = 1005 +STATUS_ABNORMAL_CLOSED = 1006 +STATUS_INVALID_PAYLOAD = 1007 +STATUS_POLICY_VIOLATION = 1008 +STATUS_MESSAGE_TOO_BIG = 1009 +STATUS_INVALID_EXTENSION = 1010 +STATUS_UNEXPECTED_CONDITION = 1011 +STATUS_BAD_GATEWAY = 1014 +STATUS_TLS_HANDSHAKE_ERROR = 1015 + +VALID_CLOSE_STATUS = ( + STATUS_NORMAL, + STATUS_GOING_AWAY, + STATUS_PROTOCOL_ERROR, + STATUS_UNSUPPORTED_DATA_TYPE, + STATUS_INVALID_PAYLOAD, + STATUS_POLICY_VIOLATION, + STATUS_MESSAGE_TOO_BIG, + STATUS_INVALID_EXTENSION, + STATUS_UNEXPECTED_CONDITION, + STATUS_BAD_GATEWAY, +) + + +class ABNF(object): + """ + ABNF frame class. + see http://tools.ietf.org/html/rfc5234 + and http://tools.ietf.org/html/rfc6455#section-5.2 + """ + + # operation code values. + OPCODE_CONT = 0x0 + OPCODE_TEXT = 0x1 + OPCODE_BINARY = 0x2 + OPCODE_CLOSE = 0x8 + OPCODE_PING = 0x9 + OPCODE_PONG = 0xa + + # available operation code value tuple + OPCODES = (OPCODE_CONT, OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE, + OPCODE_PING, OPCODE_PONG) + + # opcode human readable string + OPCODE_MAP = { + OPCODE_CONT: "cont", + OPCODE_TEXT: "text", + OPCODE_BINARY: "binary", + OPCODE_CLOSE: "close", + OPCODE_PING: "ping", + OPCODE_PONG: "pong" + } + + # data length threshold. + LENGTH_7 = 0x7e + LENGTH_16 = 1 << 16 + LENGTH_63 = 1 << 63 + + def __init__(self, fin=0, rsv1=0, rsv2=0, rsv3=0, + opcode=OPCODE_TEXT, mask=1, data=""): + """ + Constructor for ABNF. + please check RFC for arguments. + """ + self.fin = fin + self.rsv1 = rsv1 + self.rsv2 = rsv2 + self.rsv3 = rsv3 + self.opcode = opcode + self.mask = mask + if data is None: + data = "" + self.data = data + self.get_mask_key = os.urandom + + def validate(self, skip_utf8_validation=False): + """ + validate the ABNF frame. + skip_utf8_validation: skip utf8 validation. + """ + if self.rsv1 or self.rsv2 or self.rsv3: + raise WebSocketProtocolException("rsv is not implemented, yet") + + if self.opcode not in ABNF.OPCODES: + raise WebSocketProtocolException("Invalid opcode %r", self.opcode) + + if self.opcode == ABNF.OPCODE_PING and not self.fin: + raise WebSocketProtocolException("Invalid ping frame.") + + if self.opcode == ABNF.OPCODE_CLOSE: + l = len(self.data) + if not l: + return + if l == 1 or l >= 126: + raise WebSocketProtocolException("Invalid close frame.") + if l > 2 and not skip_utf8_validation and not validate_utf8(self.data[2:]): + raise WebSocketProtocolException("Invalid close frame.") + + code = 256 * \ + six.byte2int(self.data[0:1]) + six.byte2int(self.data[1:2]) + if not self._is_valid_close_status(code): + raise WebSocketProtocolException("Invalid close opcode.") + + @staticmethod + def _is_valid_close_status(code): + return code in VALID_CLOSE_STATUS or (3000 <= code < 5000) + + def __str__(self): + return "fin=" + str(self.fin) \ + + " opcode=" + str(self.opcode) \ + + " data=" + str(self.data) + + @staticmethod + def create_frame(data, opcode, fin=1): + """ + create frame to send text, binary and other data. + + data: data to send. This is string value(byte array). + if opcode is OPCODE_TEXT and this value is unicode, + data value is converted into unicode string, automatically. + + opcode: operation code. please see OPCODE_XXX. + + fin: fin flag. if set to 0, create continue fragmentation. + """ + if opcode == ABNF.OPCODE_TEXT and isinstance(data, six.text_type): + data = data.encode("utf-8") + # mask must be set if send data from client + return ABNF(fin, 0, 0, 0, opcode, 1, data) + + def format(self): + """ + format this object to string(byte array) to send data to server. + """ + if any(x not in (0, 1) for x in [self.fin, self.rsv1, self.rsv2, self.rsv3]): + raise ValueError("not 0 or 1") + if self.opcode not in ABNF.OPCODES: + raise ValueError("Invalid OPCODE") + length = len(self.data) + if length >= ABNF.LENGTH_63: + raise ValueError("data is too long") + + frame_header = chr(self.fin << 7 + | self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4 + | self.opcode) + if length < ABNF.LENGTH_7: + frame_header += chr(self.mask << 7 | length) + frame_header = six.b(frame_header) + elif length < ABNF.LENGTH_16: + frame_header += chr(self.mask << 7 | 0x7e) + frame_header = six.b(frame_header) + frame_header += struct.pack("!H", length) + else: + frame_header += chr(self.mask << 7 | 0x7f) + frame_header = six.b(frame_header) + frame_header += struct.pack("!Q", length) + + if not self.mask: + return frame_header + self.data + else: + mask_key = self.get_mask_key(4) + return frame_header + self._get_masked(mask_key) + + def _get_masked(self, mask_key): + s = ABNF.mask(mask_key, self.data) + + if isinstance(mask_key, six.text_type): + mask_key = mask_key.encode('utf-8') + + return mask_key + s + + @staticmethod + def mask(mask_key, data): + """ + mask or unmask data. Just do xor for each byte + + mask_key: 4 byte string(byte). + + data: data to mask/unmask. + """ + if data is None: + data = "" + + if isinstance(mask_key, six.text_type): + mask_key = six.b(mask_key) + + if isinstance(data, six.text_type): + data = six.b(data) + + if numpy: + origlen = len(data) + _mask_key = mask_key[3] << 24 | mask_key[2] << 16 | mask_key[1] << 8 | mask_key[0] + + # We need data to be a multiple of four... + data += bytes(" " * (4 - (len(data) % 4)), "us-ascii") + a = numpy.frombuffer(data, dtype="uint32") + masked = numpy.bitwise_xor(a, [_mask_key]).astype("uint32") + if len(data) > origlen: + return masked.tobytes()[:origlen] + return masked.tobytes() + else: + _m = array.array("B", mask_key) + _d = array.array("B", data) + return _mask(_m, _d) + + +class frame_buffer(object): + _HEADER_MASK_INDEX = 5 + _HEADER_LENGTH_INDEX = 6 + + def __init__(self, recv_fn, skip_utf8_validation): + self.recv = recv_fn + self.skip_utf8_validation = skip_utf8_validation + # Buffers over the packets from the layer beneath until desired amount + # bytes of bytes are received. + self.recv_buffer = [] + self.clear() + self.lock = Lock() + + def clear(self): + self.header = None + self.length = None + self.mask = None + + def has_received_header(self): + return self.header is None + + def recv_header(self): + header = self.recv_strict(2) + b1 = header[0] + + if six.PY2: + b1 = ord(b1) + + fin = b1 >> 7 & 1 + rsv1 = b1 >> 6 & 1 + rsv2 = b1 >> 5 & 1 + rsv3 = b1 >> 4 & 1 + opcode = b1 & 0xf + b2 = header[1] + + if six.PY2: + b2 = ord(b2) + + has_mask = b2 >> 7 & 1 + length_bits = b2 & 0x7f + + self.header = (fin, rsv1, rsv2, rsv3, opcode, has_mask, length_bits) + + def has_mask(self): + if not self.header: + return False + return self.header[frame_buffer._HEADER_MASK_INDEX] + + def has_received_length(self): + return self.length is None + + def recv_length(self): + bits = self.header[frame_buffer._HEADER_LENGTH_INDEX] + length_bits = bits & 0x7f + if length_bits == 0x7e: + v = self.recv_strict(2) + self.length = struct.unpack("!H", v)[0] + elif length_bits == 0x7f: + v = self.recv_strict(8) + self.length = struct.unpack("!Q", v)[0] + else: + self.length = length_bits + + def has_received_mask(self): + return self.mask is None + + def recv_mask(self): + self.mask = self.recv_strict(4) if self.has_mask() else "" + + def recv_frame(self): + + with self.lock: + # Header + if self.has_received_header(): + self.recv_header() + (fin, rsv1, rsv2, rsv3, opcode, has_mask, _) = self.header + + # Frame length + if self.has_received_length(): + self.recv_length() + length = self.length + + # Mask + if self.has_received_mask(): + self.recv_mask() + mask = self.mask + + # Payload + payload = self.recv_strict(length) + if has_mask: + payload = ABNF.mask(mask, payload) + + # Reset for next frame + self.clear() + + frame = ABNF(fin, rsv1, rsv2, rsv3, opcode, has_mask, payload) + frame.validate(self.skip_utf8_validation) + + return frame + + def recv_strict(self, bufsize): + shortage = bufsize - sum(len(x) for x in self.recv_buffer) + while shortage > 0: + # Limit buffer size that we pass to socket.recv() to avoid + # fragmenting the heap -- the number of bytes recv() actually + # reads is limited by socket buffer and is relatively small, + # yet passing large numbers repeatedly causes lots of large + # buffers allocated and then shrunk, which results in + # fragmentation. + bytes_ = self.recv(min(16384, shortage)) + self.recv_buffer.append(bytes_) + shortage -= len(bytes_) + + unified = six.b("").join(self.recv_buffer) + + if shortage == 0: + self.recv_buffer = [] + return unified + else: + self.recv_buffer = [unified[bufsize:]] + return unified[:bufsize] + + +class continuous_frame(object): + + def __init__(self, fire_cont_frame, skip_utf8_validation): + self.fire_cont_frame = fire_cont_frame + self.skip_utf8_validation = skip_utf8_validation + self.cont_data = None + self.recving_frames = None + + def validate(self, frame): + if not self.recving_frames and frame.opcode == ABNF.OPCODE_CONT: + raise WebSocketProtocolException("Illegal frame") + if self.recving_frames and \ + frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY): + raise WebSocketProtocolException("Illegal frame") + + def add(self, frame): + if self.cont_data: + self.cont_data[1] += frame.data + else: + if frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY): + self.recving_frames = frame.opcode + self.cont_data = [frame.opcode, frame.data] + + if frame.fin: + self.recving_frames = None + + def is_fire(self, frame): + return frame.fin or self.fire_cont_frame + + def extract(self, frame): + data = self.cont_data + self.cont_data = None + frame.data = data[1] + if not self.fire_cont_frame and data[0] == ABNF.OPCODE_TEXT and not self.skip_utf8_validation and not validate_utf8(frame.data): + raise WebSocketPayloadException( + "cannot decode: " + repr(frame.data)) + + return [data[0], frame] diff --git a/src/lib/websocket/_app.py b/src/lib/websocket/_app.py new file mode 100644 index 00000000..74e90ae0 --- /dev/null +++ b/src/lib/websocket/_app.py @@ -0,0 +1,320 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" + +""" +WebSocketApp provides higher level APIs. +""" +import select +import sys +import threading +import time +import traceback + +import six + +from ._abnf import ABNF +from ._core import WebSocket, getdefaulttimeout +from ._exceptions import * +from . import _logging + + +__all__ = ["WebSocketApp"] + +class Dispatcher: + def __init__(self, app, ping_timeout): + self.app = app + self.ping_timeout = ping_timeout + + def read(self, sock, callback): + while self.app.sock.connected: + r, w, e = select.select( + (self.app.sock.sock, ), (), (), self.ping_timeout) # Use a 10 second timeout to avoid to wait forever on close + if r: + callback() + +class SSLDispacther: + def __init__(self, app, ping_timeout): + self.app = app + self.ping_timeout = ping_timeout + + def read(self, sock, callback): + while self.app.sock.connected: + r = self.select() + if r: + callback() + + def select(self): + sock = self.app.sock.sock + if sock.pending(): + return [sock,] + + r, w, e = select.select((sock, ), (), (), self.ping_timeout) + return r + +class WebSocketApp(object): + """ + Higher level of APIs are provided. + The interface is like JavaScript WebSocket object. + """ + + def __init__(self, url, header=None, + on_open=None, on_message=None, on_error=None, + on_close=None, on_ping=None, on_pong=None, + on_cont_message=None, + keep_running=True, get_mask_key=None, cookie=None, + subprotocols=None, + on_data=None): + """ + url: websocket url. + header: custom header for websocket handshake. + on_open: callable object which is called at opening websocket. + this function has one argument. The argument is this class object. + on_message: callable object which is called when received data. + on_message has 2 arguments. + The 1st argument is this class object. + The 2nd argument is utf-8 string which we get from the server. + on_error: callable object which is called when we get error. + on_error has 2 arguments. + The 1st argument is this class object. + The 2nd argument is exception object. + on_close: callable object which is called when closed the connection. + this function has one argument. The argument is this class object. + on_cont_message: callback object which is called when receive continued + frame data. + on_cont_message has 3 arguments. + The 1st argument is this class object. + The 2nd argument is utf-8 string which we get from the server. + The 3rd argument is continue flag. if 0, the data continue + to next frame data + on_data: callback object which is called when a message received. + This is called before on_message or on_cont_message, + and then on_message or on_cont_message is called. + on_data has 4 argument. + The 1st argument is this class object. + The 2nd argument is utf-8 string which we get from the server. + The 3rd argument is data type. ABNF.OPCODE_TEXT or ABNF.OPCODE_BINARY will be came. + The 4th argument is continue flag. if 0, the data continue + keep_running: this parameter is obosleted and ignored it. + get_mask_key: a callable to produce new mask keys, + see the WebSocket.set_mask_key's docstring for more information + subprotocols: array of available sub protocols. default is None. + """ + self.url = url + self.header = header if header is not None else [] + self.cookie = cookie + self.on_open = on_open + self.on_message = on_message + self.on_data = on_data + self.on_error = on_error + self.on_close = on_close + self.on_ping = on_ping + self.on_pong = on_pong + self.on_cont_message = on_cont_message + self.keep_running = False + self.get_mask_key = get_mask_key + self.sock = None + self.last_ping_tm = 0 + self.last_pong_tm = 0 + self.subprotocols = subprotocols + + def send(self, data, opcode=ABNF.OPCODE_TEXT): + """ + send message. + data: message to send. If you set opcode to OPCODE_TEXT, + data must be utf-8 string or unicode. + opcode: operation code of data. default is OPCODE_TEXT. + """ + + if not self.sock or self.sock.send(data, opcode) == 0: + raise WebSocketConnectionClosedException( + "Connection is already closed.") + + def close(self, **kwargs): + """ + close websocket connection. + """ + self.keep_running = False + if self.sock: + self.sock.close(**kwargs) + + def _send_ping(self, interval, event): + while not event.wait(interval): + self.last_ping_tm = time.time() + if self.sock: + try: + self.sock.ping() + except Exception as ex: + _logging.warning("send_ping routine terminated: {}".format(ex)) + break + + def run_forever(self, sockopt=None, sslopt=None, + ping_interval=0, ping_timeout=None, + http_proxy_host=None, http_proxy_port=None, + http_no_proxy=None, http_proxy_auth=None, + skip_utf8_validation=False, + host=None, origin=None, dispatcher=None): + """ + run event loop for WebSocket framework. + This loop is infinite loop and is alive during websocket is available. + sockopt: values for socket.setsockopt. + sockopt must be tuple + and each element is argument of sock.setsockopt. + sslopt: ssl socket optional dict. + ping_interval: automatically send "ping" command + every specified period(second) + if set to 0, not send automatically. + ping_timeout: timeout(second) if the pong message is not received. + http_proxy_host: http proxy host name. + http_proxy_port: http proxy port. If not set, set to 80. + http_no_proxy: host names, which doesn't use proxy. + skip_utf8_validation: skip utf8 validation. + host: update host header. + origin: update origin header. + """ + + if not ping_timeout or ping_timeout <= 0: + ping_timeout = None + if ping_timeout and ping_interval and ping_interval <= ping_timeout: + raise WebSocketException("Ensure ping_interval > ping_timeout") + if sockopt is None: + sockopt = [] + if sslopt is None: + sslopt = {} + if self.sock: + raise WebSocketException("socket is already opened") + thread = None + close_frame = None + self.keep_running = True + self.last_ping_tm = 0 + self.last_pong_tm = 0 + + def teardown(): + if not self.keep_running: + return + if thread and thread.isAlive(): + event.set() + thread.join() + self.keep_running = False + self.sock.close() + close_args = self._get_close_args( + close_frame.data if close_frame else None) + self._callback(self.on_close, *close_args) + self.sock = None + + try: + self.sock = WebSocket( + self.get_mask_key, sockopt=sockopt, sslopt=sslopt, + fire_cont_frame=self.on_cont_message and True or False, + skip_utf8_validation=skip_utf8_validation) + self.sock.settimeout(getdefaulttimeout()) + self.sock.connect( + self.url, header=self.header, cookie=self.cookie, + http_proxy_host=http_proxy_host, + http_proxy_port=http_proxy_port, http_no_proxy=http_no_proxy, + http_proxy_auth=http_proxy_auth, subprotocols=self.subprotocols, + host=host, origin=origin) + if not dispatcher: + dispatcher = self.create_dispatcher(ping_timeout) + + self._callback(self.on_open) + + if ping_interval: + event = threading.Event() + thread = threading.Thread( + target=self._send_ping, args=(ping_interval, event)) + thread.setDaemon(True) + thread.start() + + def read(): + if not self.keep_running: + return teardown() + + op_code, frame = self.sock.recv_data_frame(True) + if op_code == ABNF.OPCODE_CLOSE: + close_frame = frame + return teardown() + elif op_code == ABNF.OPCODE_PING: + self._callback(self.on_ping, frame.data) + elif op_code == ABNF.OPCODE_PONG: + self.last_pong_tm = time.time() + self._callback(self.on_pong, frame.data) + elif op_code == ABNF.OPCODE_CONT and self.on_cont_message: + self._callback(self.on_data, frame.data, + frame.opcode, frame.fin) + self._callback(self.on_cont_message, + frame.data, frame.fin) + else: + data = frame.data + if six.PY3 and op_code == ABNF.OPCODE_TEXT: + data = data.decode("utf-8") + self._callback(self.on_data, data, frame.opcode, True) + self._callback(self.on_message, data) + + if ping_timeout and self.last_ping_tm \ + and time.time() - self.last_ping_tm > ping_timeout \ + and self.last_ping_tm - self.last_pong_tm > ping_timeout: + raise WebSocketTimeoutException("ping/pong timed out") + return True + + dispatcher.read(self.sock.sock, read) + except (Exception, KeyboardInterrupt, SystemExit) as e: + self._callback(self.on_error, e) + if isinstance(e, SystemExit): + # propagate SystemExit further + raise + teardown() + + def create_dispatcher(self, ping_timeout): + timeout = ping_timeout or 10 + if self.sock.is_ssl(): + return SSLDispacther(self, timeout) + + return Dispatcher(self, timeout) + + def _get_close_args(self, data): + """ this functions extracts the code, reason from the close body + if they exists, and if the self.on_close except three arguments """ + import inspect + # if the on_close callback is "old", just return empty list + if sys.version_info < (3, 0): + if not self.on_close or len(inspect.getargspec(self.on_close).args) != 3: + return [] + else: + if not self.on_close or len(inspect.getfullargspec(self.on_close).args) != 3: + return [] + + if data and len(data) >= 2: + code = 256 * six.byte2int(data[0:1]) + six.byte2int(data[1:2]) + reason = data[2:].decode('utf-8') + return [code, reason] + + return [None, None] + + def _callback(self, callback, *args): + if callback: + try: + callback(self, *args) + except Exception as e: + _logging.error("error from callback {}: {}".format(callback, e)) + if _logging.isEnabledForDebug(): + _, _, tb = sys.exc_info() + traceback.print_tb(tb) diff --git a/src/lib/websocket/_cookiejar.py b/src/lib/websocket/_cookiejar.py new file mode 100644 index 00000000..3efeb0fd --- /dev/null +++ b/src/lib/websocket/_cookiejar.py @@ -0,0 +1,52 @@ +try: + import Cookie +except: + import http.cookies as Cookie + + +class SimpleCookieJar(object): + def __init__(self): + self.jar = dict() + + def add(self, set_cookie): + if set_cookie: + try: + simpleCookie = Cookie.SimpleCookie(set_cookie) + except: + simpleCookie = Cookie.SimpleCookie(set_cookie.encode('ascii', 'ignore')) + + for k, v in simpleCookie.items(): + domain = v.get("domain") + if domain: + if not domain.startswith("."): + domain = "." + domain + cookie = self.jar.get(domain) if self.jar.get(domain) else Cookie.SimpleCookie() + cookie.update(simpleCookie) + self.jar[domain.lower()] = cookie + + def set(self, set_cookie): + if set_cookie: + try: + simpleCookie = Cookie.SimpleCookie(set_cookie) + except: + simpleCookie = Cookie.SimpleCookie(set_cookie.encode('ascii', 'ignore')) + + for k, v in simpleCookie.items(): + domain = v.get("domain") + if domain: + if not domain.startswith("."): + domain = "." + domain + self.jar[domain.lower()] = simpleCookie + + def get(self, host): + if not host: + return "" + + cookies = [] + for domain, simpleCookie in self.jar.items(): + host = host.lower() + if host.endswith(domain) or host == domain[1:]: + cookies.append(self.jar.get(domain)) + + return "; ".join(filter(None, ["%s=%s" % (k, v.value) for cookie in filter(None, sorted(cookies)) for k, v in + sorted(cookie.items())])) diff --git a/src/lib/websocket/_core.py b/src/lib/websocket/_core.py new file mode 100644 index 00000000..2d009621 --- /dev/null +++ b/src/lib/websocket/_core.py @@ -0,0 +1,495 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +from __future__ import print_function + +import socket +import struct +import threading + +import six + +# websocket modules +from ._abnf import * +from ._exceptions import * +from ._handshake import * +from ._http import * +from ._logging import * +from ._socket import * +from ._ssl_compat import * +from ._utils import * + +__all__ = ['WebSocket', 'create_connection'] + +""" +websocket python client. +========================= + +This version support only hybi-13. +Please see http://tools.ietf.org/html/rfc6455 for protocol. +""" + + +class WebSocket(object): + """ + Low level WebSocket interface. + This class is based on + The WebSocket protocol draft-hixie-thewebsocketprotocol-76 + http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76 + + We can connect to the websocket server and send/receive data. + The following example is an echo client. + + >>> import websocket + >>> ws = websocket.WebSocket() + >>> ws.connect("ws://echo.websocket.org") + >>> ws.send("Hello, Server") + >>> ws.recv() + 'Hello, Server' + >>> ws.close() + + get_mask_key: a callable to produce new mask keys, see the set_mask_key + function's docstring for more details + sockopt: values for socket.setsockopt. + sockopt must be tuple and each element is argument of sock.setsockopt. + sslopt: dict object for ssl socket option. + fire_cont_frame: fire recv event for each cont frame. default is False + enable_multithread: if set to True, lock send method. + skip_utf8_validation: skip utf8 validation. + """ + + def __init__(self, get_mask_key=None, sockopt=None, sslopt=None, + fire_cont_frame=False, enable_multithread=False, + skip_utf8_validation=False, **_): + """ + Initialize WebSocket object. + """ + self.sock_opt = sock_opt(sockopt, sslopt) + self.handshake_response = None + self.sock = None + + self.connected = False + self.get_mask_key = get_mask_key + # These buffer over the build-up of a single frame. + self.frame_buffer = frame_buffer(self._recv, skip_utf8_validation) + self.cont_frame = continuous_frame( + fire_cont_frame, skip_utf8_validation) + + if enable_multithread: + self.lock = threading.Lock() + self.readlock = threading.Lock() + else: + self.lock = NoLock() + self.readlock = NoLock() + + def __iter__(self): + """ + Allow iteration over websocket, implying sequential `recv` executions. + """ + while True: + yield self.recv() + + def __next__(self): + return self.recv() + + def next(self): + return self.__next__() + + def fileno(self): + return self.sock.fileno() + + def set_mask_key(self, func): + """ + set function to create musk key. You can customize mask key generator. + Mainly, this is for testing purpose. + + func: callable object. the func takes 1 argument as integer. + The argument means length of mask key. + This func must return string(byte array), + which length is argument specified. + """ + self.get_mask_key = func + + def gettimeout(self): + """ + Get the websocket timeout(second). + """ + return self.sock_opt.timeout + + def settimeout(self, timeout): + """ + Set the timeout to the websocket. + + timeout: timeout time(second). + """ + self.sock_opt.timeout = timeout + if self.sock: + self.sock.settimeout(timeout) + + timeout = property(gettimeout, settimeout) + + def getsubprotocol(self): + """ + get subprotocol + """ + if self.handshake_response: + return self.handshake_response.subprotocol + else: + return None + + subprotocol = property(getsubprotocol) + + def getstatus(self): + """ + get handshake status + """ + if self.handshake_response: + return self.handshake_response.status + else: + return None + + status = property(getstatus) + + def getheaders(self): + """ + get handshake response header + """ + if self.handshake_response: + return self.handshake_response.headers + else: + return None + + def is_ssl(self): + return isinstance(self.sock, ssl.SSLSocket) + + headers = property(getheaders) + + def connect(self, url, **options): + """ + Connect to url. url is websocket url scheme. + ie. ws://host:port/resource + You can customize using 'options'. + If you set "header" list object, you can set your own custom header. + + >>> ws = WebSocket() + >>> ws.connect("ws://echo.websocket.org/", + ... header=["User-Agent: MyProgram", + ... "x-custom: header"]) + + timeout: socket timeout time. This value is integer. + if you set None for this value, + it means "use default_timeout value" + + options: "header" -> custom http header list or dict. + "cookie" -> cookie value. + "origin" -> custom origin url. + "host" -> custom host header string. + "http_proxy_host" - http proxy host name. + "http_proxy_port" - http proxy port. If not set, set to 80. + "http_no_proxy" - host names, which doesn't use proxy. + "http_proxy_auth" - http proxy auth information. + tuple of username and password. + default is None + "subprotocols" - array of available sub protocols. + default is None. + "socket" - pre-initialized stream socket. + + """ + self.sock, addrs = connect(url, self.sock_opt, proxy_info(**options), + options.pop('socket', None)) + + try: + self.handshake_response = handshake(self.sock, *addrs, **options) + self.connected = True + except: + if self.sock: + self.sock.close() + self.sock = None + raise + + def send(self, payload, opcode=ABNF.OPCODE_TEXT): + """ + Send the data as string. + + payload: Payload must be utf-8 string or unicode, + if the opcode is OPCODE_TEXT. + Otherwise, it must be string(byte array) + + opcode: operation code to send. Please see OPCODE_XXX. + """ + + frame = ABNF.create_frame(payload, opcode) + return self.send_frame(frame) + + def send_frame(self, frame): + """ + Send the data frame. + + frame: frame data created by ABNF.create_frame + + >>> ws = create_connection("ws://echo.websocket.org/") + >>> frame = ABNF.create_frame("Hello", ABNF.OPCODE_TEXT) + >>> ws.send_frame(frame) + >>> cont_frame = ABNF.create_frame("My name is ", ABNF.OPCODE_CONT, 0) + >>> ws.send_frame(frame) + >>> cont_frame = ABNF.create_frame("Foo Bar", ABNF.OPCODE_CONT, 1) + >>> ws.send_frame(frame) + + """ + if self.get_mask_key: + frame.get_mask_key = self.get_mask_key + data = frame.format() + length = len(data) + trace("send: " + repr(data)) + + with self.lock: + while data: + l = self._send(data) + data = data[l:] + + return length + + def send_binary(self, payload): + return self.send(payload, ABNF.OPCODE_BINARY) + + def ping(self, payload=""): + """ + send ping data. + + payload: data payload to send server. + """ + if isinstance(payload, six.text_type): + payload = payload.encode("utf-8") + self.send(payload, ABNF.OPCODE_PING) + + def pong(self, payload): + """ + send pong data. + + payload: data payload to send server. + """ + if isinstance(payload, six.text_type): + payload = payload.encode("utf-8") + self.send(payload, ABNF.OPCODE_PONG) + + def recv(self): + """ + Receive string data(byte array) from the server. + + return value: string(byte array) value. + """ + with self.readlock: + opcode, data = self.recv_data() + if six.PY3 and opcode == ABNF.OPCODE_TEXT: + return data.decode("utf-8") + elif opcode == ABNF.OPCODE_TEXT or opcode == ABNF.OPCODE_BINARY: + return data + else: + return '' + + def recv_data(self, control_frame=False): + """ + Receive data with operation code. + + control_frame: a boolean flag indicating whether to return control frame + data, defaults to False + + return value: tuple of operation code and string(byte array) value. + """ + opcode, frame = self.recv_data_frame(control_frame) + return opcode, frame.data + + def recv_data_frame(self, control_frame=False): + """ + Receive data with operation code. + + control_frame: a boolean flag indicating whether to return control frame + data, defaults to False + + return value: tuple of operation code and string(byte array) value. + """ + while True: + frame = self.recv_frame() + if not frame: + # handle error: + # 'NoneType' object has no attribute 'opcode' + raise WebSocketProtocolException( + "Not a valid frame %s" % frame) + elif frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY, ABNF.OPCODE_CONT): + self.cont_frame.validate(frame) + self.cont_frame.add(frame) + + if self.cont_frame.is_fire(frame): + return self.cont_frame.extract(frame) + + elif frame.opcode == ABNF.OPCODE_CLOSE: + self.send_close() + return frame.opcode, frame + elif frame.opcode == ABNF.OPCODE_PING: + if len(frame.data) < 126: + self.pong(frame.data) + else: + raise WebSocketProtocolException( + "Ping message is too long") + if control_frame: + return frame.opcode, frame + elif frame.opcode == ABNF.OPCODE_PONG: + if control_frame: + return frame.opcode, frame + + def recv_frame(self): + """ + receive data as frame from server. + + return value: ABNF frame object. + """ + return self.frame_buffer.recv_frame() + + def send_close(self, status=STATUS_NORMAL, reason=six.b("")): + """ + send close data to the server. + + status: status code to send. see STATUS_XXX. + + reason: the reason to close. This must be string or bytes. + """ + if status < 0 or status >= ABNF.LENGTH_16: + raise ValueError("code is invalid range") + self.connected = False + self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE) + + def close(self, status=STATUS_NORMAL, reason=six.b(""), timeout=3): + """ + Close Websocket object + + status: status code to send. see STATUS_XXX. + + reason: the reason to close. This must be string. + + timeout: timeout until receive a close frame. + If None, it will wait forever until receive a close frame. + """ + if self.connected: + if status < 0 or status >= ABNF.LENGTH_16: + raise ValueError("code is invalid range") + + try: + self.connected = False + self.send(struct.pack('!H', status) + + reason, ABNF.OPCODE_CLOSE) + sock_timeout = self.sock.gettimeout() + self.sock.settimeout(timeout) + try: + frame = self.recv_frame() + if isEnabledForError(): + recv_status = struct.unpack("!H", frame.data[0:2])[0] + if recv_status != STATUS_NORMAL: + error("close status: " + repr(recv_status)) + except: + pass + self.sock.settimeout(sock_timeout) + self.sock.shutdown(socket.SHUT_RDWR) + except: + pass + + self.shutdown() + + def abort(self): + """ + Low-level asynchronous abort, wakes up other threads that are waiting in recv_* + """ + if self.connected: + self.sock.shutdown(socket.SHUT_RDWR) + + def shutdown(self): + """close socket, immediately.""" + if self.sock: + self.sock.close() + self.sock = None + self.connected = False + + def _send(self, data): + return send(self.sock, data) + + def _recv(self, bufsize): + try: + return recv(self.sock, bufsize) + except WebSocketConnectionClosedException: + if self.sock: + self.sock.close() + self.sock = None + self.connected = False + raise + + +def create_connection(url, timeout=None, class_=WebSocket, **options): + """ + connect to url and return websocket object. + + Connect to url and return the WebSocket object. + Passing optional timeout parameter will set the timeout on the socket. + If no timeout is supplied, + the global default timeout setting returned by getdefauttimeout() is used. + You can customize using 'options'. + If you set "header" list object, you can set your own custom header. + + >>> conn = create_connection("ws://echo.websocket.org/", + ... header=["User-Agent: MyProgram", + ... "x-custom: header"]) + + + timeout: socket timeout time. This value is integer. + if you set None for this value, + it means "use default_timeout value" + + class_: class to instantiate when creating the connection. It has to implement + settimeout and connect. It's __init__ should be compatible with + WebSocket.__init__, i.e. accept all of it's kwargs. + options: "header" -> custom http header list or dict. + "cookie" -> cookie value. + "origin" -> custom origin url. + "host" -> custom host header string. + "http_proxy_host" - http proxy host name. + "http_proxy_port" - http proxy port. If not set, set to 80. + "http_no_proxy" - host names, which doesn't use proxy. + "http_proxy_auth" - http proxy auth information. + tuple of username and password. + default is None + "enable_multithread" -> enable lock for multithread. + "sockopt" -> socket options + "sslopt" -> ssl option + "subprotocols" - array of available sub protocols. + default is None. + "skip_utf8_validation" - skip utf8 validation. + "socket" - pre-initialized stream socket. + """ + sockopt = options.pop("sockopt", []) + sslopt = options.pop("sslopt", {}) + fire_cont_frame = options.pop("fire_cont_frame", False) + enable_multithread = options.pop("enable_multithread", False) + skip_utf8_validation = options.pop("skip_utf8_validation", False) + websock = class_(sockopt=sockopt, sslopt=sslopt, + fire_cont_frame=fire_cont_frame, + enable_multithread=enable_multithread, + skip_utf8_validation=skip_utf8_validation, **options) + websock.settimeout(timeout if timeout is not None else getdefaulttimeout()) + websock.connect(url, **options) + return websock diff --git a/src/lib/websocket/_exceptions.py b/src/lib/websocket/_exceptions.py new file mode 100644 index 00000000..24c85e0e --- /dev/null +++ b/src/lib/websocket/_exceptions.py @@ -0,0 +1,87 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" + + +""" +define websocket exceptions +""" + + +class WebSocketException(Exception): + """ + websocket exception class. + """ + pass + + +class WebSocketProtocolException(WebSocketException): + """ + If the websocket protocol is invalid, this exception will be raised. + """ + pass + + +class WebSocketPayloadException(WebSocketException): + """ + If the websocket payload is invalid, this exception will be raised. + """ + pass + + +class WebSocketConnectionClosedException(WebSocketException): + """ + If remote host closed the connection or some network error happened, + this exception will be raised. + """ + pass + + +class WebSocketTimeoutException(WebSocketException): + """ + WebSocketTimeoutException will be raised at socket timeout during read/write data. + """ + pass + + +class WebSocketProxyException(WebSocketException): + """ + WebSocketProxyException will be raised when proxy error occurred. + """ + pass + + +class WebSocketBadStatusException(WebSocketException): + """ + WebSocketBadStatusException will be raised when we get bad handshake status code. + """ + + def __init__(self, message, status_code, status_message=None): + msg = message % (status_code, status_message) if status_message is not None \ + else message % status_code + super(WebSocketBadStatusException, self).__init__(msg) + self.status_code = status_code + +class WebSocketAddressException(WebSocketException): + """ + If the websocket address info cannot be found, this exception will be raised. + """ + pass diff --git a/src/lib/websocket/_handshake.py b/src/lib/websocket/_handshake.py new file mode 100644 index 00000000..3fd5c9ee --- /dev/null +++ b/src/lib/websocket/_handshake.py @@ -0,0 +1,180 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +import hashlib +import hmac +import os + +import six + +from ._cookiejar import SimpleCookieJar +from ._exceptions import * +from ._http import * +from ._logging import * +from ._socket import * + +if six.PY3: + from base64 import encodebytes as base64encode +else: + from base64 import encodestring as base64encode + +__all__ = ["handshake_response", "handshake"] + +if hasattr(hmac, "compare_digest"): + compare_digest = hmac.compare_digest +else: + def compare_digest(s1, s2): + return s1 == s2 + +# websocket supported version. +VERSION = 13 + +CookieJar = SimpleCookieJar() + + +class handshake_response(object): + + def __init__(self, status, headers, subprotocol): + self.status = status + self.headers = headers + self.subprotocol = subprotocol + CookieJar.add(headers.get("set-cookie")) + + +def handshake(sock, hostname, port, resource, **options): + headers, key = _get_handshake_headers(resource, hostname, port, options) + + header_str = "\r\n".join(headers) + send(sock, header_str) + dump("request header", header_str) + + status, resp = _get_resp_headers(sock) + success, subproto = _validate(resp, key, options.get("subprotocols")) + if not success: + raise WebSocketException("Invalid WebSocket Header") + + return handshake_response(status, resp, subproto) + +def _pack_hostname(hostname): + # IPv6 address + if ':' in hostname: + return '[' + hostname + ']' + + return hostname + +def _get_handshake_headers(resource, host, port, options): + headers = [ + "GET %s HTTP/1.1" % resource, + "Upgrade: websocket", + "Connection: Upgrade" + ] + if port == 80 or port == 443: + hostport = _pack_hostname(host) + else: + hostport = "%s:%d" % (_pack_hostname(host), port) + + if "host" in options and options["host"] is not None: + headers.append("Host: %s" % options["host"]) + else: + headers.append("Host: %s" % hostport) + + if "origin" in options and options["origin"] is not None: + headers.append("Origin: %s" % options["origin"]) + else: + headers.append("Origin: http://%s" % hostport) + + key = _create_sec_websocket_key() + headers.append("Sec-WebSocket-Key: %s" % key) + headers.append("Sec-WebSocket-Version: %s" % VERSION) + + subprotocols = options.get("subprotocols") + if subprotocols: + headers.append("Sec-WebSocket-Protocol: %s" % ",".join(subprotocols)) + + if "header" in options: + header = options["header"] + if isinstance(header, dict): + header = map(": ".join, header.items()) + headers.extend(header) + + server_cookie = CookieJar.get(host) + client_cookie = options.get("cookie", None) + + cookie = "; ".join(filter(None, [server_cookie, client_cookie])) + + if cookie: + headers.append("Cookie: %s" % cookie) + + headers.append("") + headers.append("") + + return headers, key + + +def _get_resp_headers(sock, success_status=101): + status, resp_headers, status_message = read_headers(sock) + if status != success_status: + raise WebSocketBadStatusException("Handshake status %d %s", status, status_message) + return status, resp_headers + +_HEADERS_TO_CHECK = { + "upgrade": "websocket", + "connection": "upgrade", +} + + +def _validate(headers, key, subprotocols): + subproto = None + for k, v in _HEADERS_TO_CHECK.items(): + r = headers.get(k, None) + if not r: + return False, None + r = r.lower() + if v != r: + return False, None + + if subprotocols: + subproto = headers.get("sec-websocket-protocol", None).lower() + if not subproto or subproto not in [s.lower() for s in subprotocols]: + error("Invalid subprotocol: " + str(subprotocols)) + return False, None + + result = headers.get("sec-websocket-accept", None) + if not result: + return False, None + result = result.lower() + + if isinstance(result, six.text_type): + result = result.encode('utf-8') + + value = (key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode('utf-8') + hashed = base64encode(hashlib.sha1(value).digest()).strip().lower() + success = compare_digest(hashed, result) + + if success: + return True, subproto + else: + return False, None + + +def _create_sec_websocket_key(): + randomness = os.urandom(16) + return base64encode(randomness).decode('utf-8').strip() diff --git a/src/lib/websocket/_http.py b/src/lib/websocket/_http.py new file mode 100644 index 00000000..c3b53f56 --- /dev/null +++ b/src/lib/websocket/_http.py @@ -0,0 +1,256 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +import errno +import os +import socket +import sys + +import six + +from ._exceptions import * +from ._logging import * +from ._socket import* +from ._ssl_compat import * +from ._url import * + +if six.PY3: + from base64 import encodebytes as base64encode +else: + from base64 import encodestring as base64encode + +__all__ = ["proxy_info", "connect", "read_headers"] + + +class proxy_info(object): + + def __init__(self, **options): + self.host = options.get("http_proxy_host", None) + if self.host: + self.port = options.get("http_proxy_port", 0) + self.auth = options.get("http_proxy_auth", None) + else: + self.port = 0 + self.auth = None + self.no_proxy = options.get("http_no_proxy", None) + + +def connect(url, options, proxy, socket): + hostname, port, resource, is_secure = parse_url(url) + + if socket: + return socket, (hostname, port, resource) + + addrinfo_list, need_tunnel, auth = _get_addrinfo_list( + hostname, port, is_secure, proxy) + if not addrinfo_list: + raise WebSocketException( + "Host not found.: " + hostname + ":" + str(port)) + + sock = None + try: + sock = _open_socket(addrinfo_list, options.sockopt, options.timeout) + if need_tunnel: + sock = _tunnel(sock, hostname, port, auth) + + if is_secure: + if HAVE_SSL: + sock = _ssl_socket(sock, options.sslopt, hostname) + else: + raise WebSocketException("SSL not available.") + + return sock, (hostname, port, resource) + except: + if sock: + sock.close() + raise + + +def _get_addrinfo_list(hostname, port, is_secure, proxy): + phost, pport, pauth = get_proxy_info( + hostname, is_secure, proxy.host, proxy.port, proxy.auth, proxy.no_proxy) + try: + if not phost: + addrinfo_list = socket.getaddrinfo( + hostname, port, 0, 0, socket.SOL_TCP) + return addrinfo_list, False, None + else: + pport = pport and pport or 80 + addrinfo_list = socket.getaddrinfo(phost, pport, 0, 0, socket.SOL_TCP) + return addrinfo_list, True, pauth + except socket.gaierror as e: + raise WebSocketAddressException(e) + + +def _open_socket(addrinfo_list, sockopt, timeout): + err = None + for addrinfo in addrinfo_list: + family, socktype, proto = addrinfo[:3] + sock = socket.socket(family, socktype, proto) + sock.settimeout(timeout) + for opts in DEFAULT_SOCKET_OPTION: + sock.setsockopt(*opts) + for opts in sockopt: + sock.setsockopt(*opts) + + address = addrinfo[4] + try: + sock.connect(address) + except socket.error as error: + error.remote_ip = str(address[0]) + try: + eConnRefused = (errno.ECONNREFUSED, errno.WSAECONNREFUSED) + except: + eConnRefused = (errno.ECONNREFUSED, ) + if error.errno in eConnRefused: + err = error + continue + else: + raise + else: + break + else: + raise err + + return sock + + +def _can_use_sni(): + return six.PY2 and sys.version_info >= (2, 7, 9) or sys.version_info >= (3, 2) + + +def _wrap_sni_socket(sock, sslopt, hostname, check_hostname): + context = ssl.SSLContext(sslopt.get('ssl_version', ssl.PROTOCOL_SSLv23)) + + if sslopt.get('cert_reqs', ssl.CERT_NONE) != ssl.CERT_NONE: + context.load_verify_locations(cafile=sslopt.get('ca_certs', None), capath=sslopt.get('ca_cert_path', None)) + if sslopt.get('certfile', None): + context.load_cert_chain( + sslopt['certfile'], + sslopt.get('keyfile', None), + sslopt.get('password', None), + ) + # see + # https://github.com/liris/websocket-client/commit/b96a2e8fa765753e82eea531adb19716b52ca3ca#commitcomment-10803153 + context.verify_mode = sslopt['cert_reqs'] + if HAVE_CONTEXT_CHECK_HOSTNAME: + context.check_hostname = check_hostname + if 'ciphers' in sslopt: + context.set_ciphers(sslopt['ciphers']) + if 'cert_chain' in sslopt: + certfile, keyfile, password = sslopt['cert_chain'] + context.load_cert_chain(certfile, keyfile, password) + if 'ecdh_curve' in sslopt: + context.set_ecdh_curve(sslopt['ecdh_curve']) + + return context.wrap_socket( + sock, + do_handshake_on_connect=sslopt.get('do_handshake_on_connect', True), + suppress_ragged_eofs=sslopt.get('suppress_ragged_eofs', True), + server_hostname=hostname, + ) + + +def _ssl_socket(sock, user_sslopt, hostname): + sslopt = dict(cert_reqs=ssl.CERT_REQUIRED) + sslopt.update(user_sslopt) + + if os.environ.get('WEBSOCKET_CLIENT_CA_BUNDLE'): + certPath = os.environ.get('WEBSOCKET_CLIENT_CA_BUNDLE') + else: + certPath = os.path.join( + os.path.dirname(__file__), "cacert.pem") + if os.path.isfile(certPath) and user_sslopt.get('ca_certs', None) is None \ + and user_sslopt.get('ca_cert', None) is None: + sslopt['ca_certs'] = certPath + elif os.path.isdir(certPath) and user_sslopt.get('ca_cert_path', None) is None: + sslopt['ca_cert_path'] = certPath + + check_hostname = sslopt["cert_reqs"] != ssl.CERT_NONE and sslopt.pop( + 'check_hostname', True) + + if _can_use_sni(): + sock = _wrap_sni_socket(sock, sslopt, hostname, check_hostname) + else: + sslopt.pop('check_hostname', True) + sock = ssl.wrap_socket(sock, **sslopt) + + if not HAVE_CONTEXT_CHECK_HOSTNAME and check_hostname: + match_hostname(sock.getpeercert(), hostname) + + return sock + + +def _tunnel(sock, host, port, auth): + debug("Connecting proxy...") + connect_header = "CONNECT %s:%d HTTP/1.0\r\n" % (host, port) + # TODO: support digest auth. + if auth and auth[0]: + auth_str = auth[0] + if auth[1]: + auth_str += ":" + auth[1] + encoded_str = base64encode(auth_str.encode()).strip().decode() + connect_header += "Proxy-Authorization: Basic %s\r\n" % encoded_str + connect_header += "\r\n" + dump("request header", connect_header) + + send(sock, connect_header) + + try: + status, resp_headers, status_message = read_headers(sock) + except Exception as e: + raise WebSocketProxyException(str(e)) + + if status != 200: + raise WebSocketProxyException( + "failed CONNECT via proxy status: %r" % status) + + return sock + + +def read_headers(sock): + status = None + status_message = None + headers = {} + trace("--- response header ---") + + while True: + line = recv_line(sock) + line = line.decode('utf-8').strip() + if not line: + break + trace(line) + if not status: + + status_info = line.split(" ", 2) + status = int(status_info[1]) + status_message = status_info[2] + else: + kv = line.split(":", 1) + if len(kv) == 2: + key, value = kv + headers[key.lower()] = value.strip() + else: + raise WebSocketException("Invalid header") + + trace("-----------------------") + + return status, headers, status_message diff --git a/src/lib/websocket/_logging.py b/src/lib/websocket/_logging.py new file mode 100644 index 00000000..d406db6a --- /dev/null +++ b/src/lib/websocket/_logging.py @@ -0,0 +1,74 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +import logging + +_logger = logging.getLogger('websocket') +_traceEnabled = False + +__all__ = ["enableTrace", "dump", "error", "warning", "debug", "trace", + "isEnabledForError", "isEnabledForDebug"] + + +def enableTrace(traceable): + """ + turn on/off the traceability. + + traceable: boolean value. if set True, traceability is enabled. + """ + global _traceEnabled + _traceEnabled = traceable + if traceable: + if not _logger.handlers: + _logger.addHandler(logging.StreamHandler()) + _logger.setLevel(logging.DEBUG) + + +def dump(title, message): + if _traceEnabled: + _logger.debug("--- " + title + " ---") + _logger.debug(message) + _logger.debug("-----------------------") + + +def error(msg): + _logger.error(msg) + + +def warning(msg): + _logger.warning(msg) + + +def debug(msg): + _logger.debug(msg) + + +def trace(msg): + if _traceEnabled: + _logger.debug(msg) + + +def isEnabledForError(): + return _logger.isEnabledFor(logging.ERROR) + + +def isEnabledForDebug(): + return _logger.isEnabledFor(logging.DEBUG) diff --git a/src/lib/websocket/_socket.py b/src/lib/websocket/_socket.py new file mode 100644 index 00000000..c84fcf90 --- /dev/null +++ b/src/lib/websocket/_socket.py @@ -0,0 +1,126 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +import socket + +import six +import sys + +from ._exceptions import * +from ._ssl_compat import * +from ._utils import * + +DEFAULT_SOCKET_OPTION = [(socket.SOL_TCP, socket.TCP_NODELAY, 1)] +if hasattr(socket, "SO_KEEPALIVE"): + DEFAULT_SOCKET_OPTION.append((socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)) +if hasattr(socket, "TCP_KEEPIDLE"): + DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPIDLE, 30)) +if hasattr(socket, "TCP_KEEPINTVL"): + DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPINTVL, 10)) +if hasattr(socket, "TCP_KEEPCNT"): + DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPCNT, 3)) + +_default_timeout = None + +__all__ = ["DEFAULT_SOCKET_OPTION", "sock_opt", "setdefaulttimeout", "getdefaulttimeout", + "recv", "recv_line", "send"] + + +class sock_opt(object): + + def __init__(self, sockopt, sslopt): + if sockopt is None: + sockopt = [] + if sslopt is None: + sslopt = {} + self.sockopt = sockopt + self.sslopt = sslopt + self.timeout = None + + +def setdefaulttimeout(timeout): + """ + Set the global timeout setting to connect. + + timeout: default socket timeout time. This value is second. + """ + global _default_timeout + _default_timeout = timeout + + +def getdefaulttimeout(): + """ + Return the global timeout setting(second) to connect. + """ + return _default_timeout + + +def recv(sock, bufsize): + if not sock: + raise WebSocketConnectionClosedException("socket is already closed.") + + try: + bytes_ = sock.recv(bufsize) + except socket.timeout as e: + message = extract_err_message(e) + raise WebSocketTimeoutException(message) + except SSLError as e: + message = extract_err_message(e) + if isinstance(message, str) and 'timed out' in message: + raise WebSocketTimeoutException(message) + else: + raise + + if not bytes_: + raise WebSocketConnectionClosedException( + "Connection is already closed.") + + return bytes_ + + +def recv_line(sock): + line = [] + while True: + c = recv(sock, 1) + line.append(c) + if c == six.b("\n"): + break + return six.b("").join(line) + + +def send(sock, data): + if isinstance(data, six.text_type): + data = data.encode('utf-8') + + if not sock: + raise WebSocketConnectionClosedException("socket is already closed.") + + try: + return sock.send(data) + except socket.timeout as e: + message = extract_err_message(e) + raise WebSocketTimeoutException(message) + except Exception as e: + message = extract_err_message(e) + if isinstance(message, str) and "timed out" in message: + raise WebSocketTimeoutException(message) + else: + raise diff --git a/src/lib/websocket/_ssl_compat.py b/src/lib/websocket/_ssl_compat.py new file mode 100644 index 00000000..03048162 --- /dev/null +++ b/src/lib/websocket/_ssl_compat.py @@ -0,0 +1,44 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +__all__ = ["HAVE_SSL", "ssl", "SSLError"] + +try: + import ssl + from ssl import SSLError + if hasattr(ssl, 'SSLContext') and hasattr(ssl.SSLContext, 'check_hostname'): + HAVE_CONTEXT_CHECK_HOSTNAME = True + else: + HAVE_CONTEXT_CHECK_HOSTNAME = False + if hasattr(ssl, "match_hostname"): + from ssl import match_hostname + else: + from backports.ssl_match_hostname import match_hostname + __all__.append("match_hostname") + __all__.append("HAVE_CONTEXT_CHECK_HOSTNAME") + + HAVE_SSL = True +except ImportError: + # dummy class of SSLError for ssl none-support environment. + class SSLError(Exception): + pass + + HAVE_SSL = False diff --git a/src/lib/websocket/_url.py b/src/lib/websocket/_url.py new file mode 100644 index 00000000..f7bdf346 --- /dev/null +++ b/src/lib/websocket/_url.py @@ -0,0 +1,160 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" + +import os +import socket +import struct + +from six.moves.urllib.parse import urlparse + + +__all__ = ["parse_url", "get_proxy_info"] + + +def parse_url(url): + """ + parse url and the result is tuple of + (hostname, port, resource path and the flag of secure mode) + + url: url string. + """ + if ":" not in url: + raise ValueError("url is invalid") + + scheme, url = url.split(":", 1) + + parsed = urlparse(url, scheme="ws") + if parsed.hostname: + hostname = parsed.hostname + else: + raise ValueError("hostname is invalid") + port = 0 + if parsed.port: + port = parsed.port + + is_secure = False + if scheme == "ws": + if not port: + port = 80 + elif scheme == "wss": + is_secure = True + if not port: + port = 443 + else: + raise ValueError("scheme %s is invalid" % scheme) + + if parsed.path: + resource = parsed.path + else: + resource = "/" + + if parsed.query: + resource += "?" + parsed.query + + return hostname, port, resource, is_secure + + +DEFAULT_NO_PROXY_HOST = ["localhost", "127.0.0.1"] + + +def _is_ip_address(addr): + try: + socket.inet_aton(addr) + except socket.error: + return False + else: + return True + + +def _is_subnet_address(hostname): + try: + addr, netmask = hostname.split("/") + return _is_ip_address(addr) and 0 <= int(netmask) < 32 + except ValueError: + return False + + +def _is_address_in_network(ip, net): + ipaddr = struct.unpack('I', socket.inet_aton(ip))[0] + netaddr, bits = net.split('/') + netmask = struct.unpack('I', socket.inet_aton(netaddr))[0] & ((2 << int(bits) - 1) - 1) + return ipaddr & netmask == netmask + + +def _is_no_proxy_host(hostname, no_proxy): + if not no_proxy: + v = os.environ.get("no_proxy", "").replace(" ", "") + no_proxy = v.split(",") + if not no_proxy: + no_proxy = DEFAULT_NO_PROXY_HOST + + if hostname in no_proxy: + return True + elif _is_ip_address(hostname): + return any([_is_address_in_network(hostname, subnet) for subnet in no_proxy if _is_subnet_address(subnet)]) + + return False + + +def get_proxy_info( + hostname, is_secure, proxy_host=None, proxy_port=0, proxy_auth=None, + no_proxy=None): + """ + try to retrieve proxy host and port from environment + if not provided in options. + result is (proxy_host, proxy_port, proxy_auth). + proxy_auth is tuple of username and password + of proxy authentication information. + + hostname: websocket server name. + + is_secure: is the connection secure? (wss) + looks for "https_proxy" in env + before falling back to "http_proxy" + + options: "http_proxy_host" - http proxy host name. + "http_proxy_port" - http proxy port. + "http_no_proxy" - host names, which doesn't use proxy. + "http_proxy_auth" - http proxy auth information. + tuple of username and password. + default is None + """ + if _is_no_proxy_host(hostname, no_proxy): + return None, 0, None + + if proxy_host: + port = proxy_port + auth = proxy_auth + return proxy_host, port, auth + + env_keys = ["http_proxy"] + if is_secure: + env_keys.insert(0, "https_proxy") + + for key in env_keys: + value = os.environ.get(key, None) + if value: + proxy = urlparse(value) + auth = (proxy.username, proxy.password) if proxy.username else None + return proxy.hostname, proxy.port, auth + + return None, 0, None diff --git a/src/lib/websocket/_utils.py b/src/lib/websocket/_utils.py new file mode 100644 index 00000000..399fb89d --- /dev/null +++ b/src/lib/websocket/_utils.py @@ -0,0 +1,105 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +import six + +__all__ = ["NoLock", "validate_utf8", "extract_err_message"] + + +class NoLock(object): + + def __enter__(self): + pass + + def __exit__(self, exc_type, exc_value, traceback): + pass + +try: + # If wsaccel is available we use compiled routines to validate UTF-8 + # strings. + from wsaccel.utf8validator import Utf8Validator + + def _validate_utf8(utfbytes): + return Utf8Validator().validate(utfbytes)[0] + +except ImportError: + # UTF-8 validator + # python implementation of http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ + + _UTF8_ACCEPT = 0 + _UTF8_REJECT = 12 + + _UTF8D = [ + # The first part of the table maps bytes to character classes that + # to reduce the size of the transition table and create bitmasks. + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, + 10,3,3,3,3,3,3,3,3,3,3,3,3,4,3,3, 11,6,6,6,5,8,8,8,8,8,8,8,8,8,8,8, + + # The second part is a transition table that maps a combination + # of a state of the automaton and a character class to a state. + 0,12,24,36,60,96,84,12,12,12,48,72, 12,12,12,12,12,12,12,12,12,12,12,12, + 12, 0,12,12,12,12,12, 0,12, 0,12,12, 12,24,12,12,12,12,12,24,12,24,12,12, + 12,12,12,12,12,12,12,24,12,12,12,12, 12,24,12,12,12,12,12,12,12,24,12,12, + 12,12,12,12,12,12,12,36,12,36,12,12, 12,36,12,12,12,12,12,36,12,36,12,12, + 12,36,12,12,12,12,12,12,12,12,12,12, ] + + def _decode(state, codep, ch): + tp = _UTF8D[ch] + + codep = (ch & 0x3f) | (codep << 6) if ( + state != _UTF8_ACCEPT) else (0xff >> tp) & ch + state = _UTF8D[256 + state + tp] + + return state, codep + + def _validate_utf8(utfbytes): + state = _UTF8_ACCEPT + codep = 0 + for i in utfbytes: + if six.PY2: + i = ord(i) + state, codep = _decode(state, codep, i) + if state == _UTF8_REJECT: + return False + + return True + + +def validate_utf8(utfbytes): + """ + validate utf8 byte string. + utfbytes: utf byte string to check. + return value: if valid utf8 string, return true. Otherwise, return false. + """ + return _validate_utf8(utfbytes) + + +def extract_err_message(exception): + if exception.args: + return exception.args[0] + else: + return None diff --git a/src/main.py b/src/main.py index d0fe9b47..a85ff472 100644 --- a/src/main.py +++ b/src/main.py @@ -365,6 +365,25 @@ class Actions(object): print site.needFile(inner_path, update=True) def sitePublish(self, address, peer_ip=None, peer_port=15441, inner_path="content.json", diffs={}): + def siteCmd(self, address, cmd, parameters): + import json + from Site import SiteManager + + site = SiteManager.site_manager.get(address) + + ws = self.getWebsocket(site) + ws.send(json.dumps({"cmd": cmd, "params": parameters, "id": 1})) + res = json.loads(ws.recv()) + if "result" in res: + return res["result"] + else: + return res + + def getWebsocket(self, site): + from lib import websocket + ws = websocket.create_connection("ws://%s:%s/Websocket?wrapper_key=%s" % (config.ui_ip, config.ui_port, site.settings["wrapper_key"])) + return ws + global file_server from Site import Site from Site import SiteManager From 82e6bc5d3100b64ec0132c2016f4cde05ddd300c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Apr 2018 14:49:40 +0200 Subject: [PATCH 0777/2570] Use websocket connection instead of fileserver to execute commands from CLI --- src/File/FileRequest.py | 20 ----------------- src/Ui/UiWebsocket.py | 6 +++++ src/main.py | 49 +++++++++++++++++++---------------------- 3 files changed, 29 insertions(+), 46 deletions(-) diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index 9f135c33..62f144a7 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -419,26 +419,6 @@ class FileRequest(object): peer.hashfield.replaceFromString(params["hashfield_raw"]) self.response({"ok": "Updated"}) - def actionSiteReload(self, params): - if self.connection.ip not in config.ip_local and self.connection.ip != config.ip_external: - self.response({"error": "Only local host allowed"}) - - site = self.sites.get(params["site"]) - site.content_manager.loadContent(params["inner_path"], add_bad_files=False) - site.storage.verifyFiles(quick_check=True) - site.updateWebsocket() - - self.response({"ok": "Reloaded"}) - - def actionSitePublish(self, params): - if self.connection.ip not in config.ip_local and self.connection.ip != config.ip_external: - self.response({"error": "Only local host allowed"}) - - site = self.sites.get(params["site"]) - num = site.publish(limit=8, inner_path=params.get("inner_path", "content.json"), diffs=params.get("diffs", {})) - - self.response({"ok": "Successfuly published to %s peers" % num}) - # Send a simple Pong! answer def actionPing(self, params): self.response("Pong!") diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index c4f84eda..b698e2ff 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -504,6 +504,12 @@ class UiWebsocket(object): if notification: self.response(to, {"error": "Content publish failed."}) + def actionSiteReload(self, to, inner_path): + self.site.content_manager.loadContent(inner_path, add_bad_files=False) + self.site.storage.verifyFiles(quick_check=True) + self.site.updateWebsocket() + return "ok" + # Write a file to disk def actionFileWrite(self, to, inner_path, content_base64, ignore_bad_files=False): valid_signers = self.site.content_manager.getValidSigners(inner_path) diff --git a/src/main.py b/src/main.py index a85ff472..71109ec3 100644 --- a/src/main.py +++ b/src/main.py @@ -154,7 +154,9 @@ class Actions(object): logging.info("Version: %s r%s, Python %s, Gevent: %s" % (config.version, config.rev, sys.version, gevent.__version__)) func = getattr(self, function_name, None) - func(**kwargs) + back = func(**kwargs) + if back: + print back # Default action: Start serving UiServer and FileServer def main(self): @@ -229,14 +231,13 @@ class Actions(object): # Not found in users.json, ask from console import getpass privatekey = getpass.getpass("Private key (input hidden):") - diffs = site.content_manager.getDiffs(inner_path) try: succ = site.content_manager.sign(inner_path=inner_path, privatekey=privatekey, update_changed_files=True, remove_missing_optional=remove_missing_optional) except Exception, err: logging.error("Sign error: %s" % Debug.formatException(err)) succ = False if succ and publish: - self.sitePublish(address, inner_path=inner_path, diffs=diffs) + self.sitePublish(address, inner_path=inner_path) def siteVerify(self, address): import time @@ -364,7 +365,6 @@ class Actions(object): site.announce() print site.needFile(inner_path, update=True) - def sitePublish(self, address, peer_ip=None, peer_port=15441, inner_path="content.json", diffs={}): def siteCmd(self, address, cmd, parameters): import json from Site import SiteManager @@ -384,24 +384,33 @@ class Actions(object): ws = websocket.create_connection("ws://%s:%s/Websocket?wrapper_key=%s" % (config.ui_ip, config.ui_port, site.settings["wrapper_key"])) return ws + def sitePublish(self, address, peer_ip=None, peer_port=15441, inner_path="content.json"): global file_server from Site import Site from Site import SiteManager from File import FileServer # We need fileserver to handle incoming file requests from Peer import Peer - SiteManager.site_manager.load() - + site = SiteManager.site_manager.get(address) logging.info("Loading site...") - site = Site(address, allow_create=False) site.settings["serving"] = True # Serving the site even if its disabled - logging.info("Creating FileServer....") - file_server = FileServer() - site.connection_server = file_server - file_server_thread = gevent.spawn(file_server.start, check_sites=False) # Dont check every site integrity - time.sleep(0.001) + try: + ws = self.getWebsocket(site) + logging.info("Sending siteReload") + self.siteCmd(address, "siteReload", inner_path) + + logging.info("Sending sitePublish") + self.siteCmd(address, "sitePublish", {"inner_path": inner_path, "sign": False}) + logging.info("Done.") + + except Exception as err: + logging.info("Can't connect to local websocket client: %s" % err) + logging.info("Creating FileServer....") + file_server = FileServer() + site.connection_server = file_server + file_server_thread = gevent.spawn(file_server.start, check_sites=False) # Dont check every site integrity + time.sleep(0.001) - if not file_server_thread.ready(): # Started fileserver file_server.openport() if peer_ip: # Announce ip specificed @@ -409,7 +418,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, diffs=diffs) # Push to peers + published = site.publish(5, inner_path) # Push to peers if published > 0: time.sleep(3) logging.info("Serving files (max 60s)...") @@ -417,18 +426,6 @@ class Actions(object): logging.info("Done.") else: logging.info("No peers found, sitePublish command only works if you already have visitors serving your site") - else: - # Already running, notify local client on new content - logging.info("Sending siteReload") - if config.fileserver_ip == "*": - my_peer = Peer("127.0.0.1", config.fileserver_port) - else: - my_peer = Peer(config.fileserver_ip, config.fileserver_port) - - logging.info(my_peer.request("siteReload", {"site": site.address, "inner_path": inner_path})) - logging.info("Sending sitePublish") - logging.info(my_peer.request("sitePublish", {"site": site.address, "inner_path": inner_path, "diffs": diffs})) - logging.info("Done.") # Crypto commands def cryptPrivatekeyToAddress(self, privatekey=None): From a65d21d7e4aa4405eab97a7a429e1a49e1cecdd3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Apr 2018 14:50:09 +0200 Subject: [PATCH 0778/2570] Fix site cleanup after test --- src/Test/conftest.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Test/conftest.py b/src/Test/conftest.py index a2e26683..e95778ce 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -135,17 +135,22 @@ def site(request): RateLimit.called_db = {} site = Site("1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT") - site.announce = mock.MagicMock(return_value=True) # Don't try to find peers from the net # Always use original data assert "1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT" in site.storage.getPath("") # Make sure we dont delete everything shutil.rmtree(site.storage.getPath(""), True) shutil.copytree(site.storage.getPath("") + "-original", site.storage.getPath("")) + + # Add to site manager + SiteManager.site_manager.get("1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT") + site.announce = mock.MagicMock(return_value=True) # Don't try to find peers from the net + def cleanup(): site.storage.deleteFiles() site.content_manager.contents.db.deleteSite(site) del SiteManager.site_manager.sites["1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT"] site.content_manager.contents.db.close() + SiteManager.site_manager.sites.clear() db_path = "%s/content.db" % config.data_dir os.unlink(db_path) del ContentDb.content_dbs[db_path] From 8c0c0868e3217c90f44a24e889753863b4976602 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Apr 2018 14:50:58 +0200 Subject: [PATCH 0779/2570] Add SiteCmd CLI command for ZeroFrame API calls --- src/Config.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Config.py b/src/Config.py index c5ebf060..21b4a493 100644 --- a/src/Config.py +++ b/src/Config.py @@ -131,6 +131,12 @@ class Config(object): action = self.subparsers.add_parser("siteVerify", help='Verify site files using sha512: address') action.add_argument('address', help='Site to verify') + # SiteCmd + action = self.subparsers.add_parser("siteCmd", help='Execute a ZeroFrame API command on a site') + action.add_argument('address', help='Site address') + action.add_argument('cmd', help='API command name') + action.add_argument('parameters', help='Parameters of the command', nargs='?') + # dbRebuild action = self.subparsers.add_parser("dbRebuild", help='Rebuild site database cache') action.add_argument('address', help='Site to rebuild') From 223957084a37497cd11eb5cfb02fee5315c792ef Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Apr 2018 14:51:11 +0200 Subject: [PATCH 0780/2570] Rev3402 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 21b4a493..deb46c50 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.2" - self.rev = 3395 + self.rev = 3402 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 75861db84ba898153a94da103085ba686c719da6 Mon Sep 17 00:00:00 2001 From: tangdou1 <35254744+tangdou1@users.noreply.github.com> Date: Wed, 4 Apr 2018 09:50:04 +0800 Subject: [PATCH 0781/2570] Update Chinese translation (#1370) * Update zh.json * Update zh.json --- plugins/Sidebar/languages/zh.json | 11 +++++++++-- src/Translate/languages/zh.json | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/plugins/Sidebar/languages/zh.json b/plugins/Sidebar/languages/zh.json index 505c65d3..58c1d2a6 100644 --- a/plugins/Sidebar/languages/zh.json +++ b/plugins/Sidebar/languages/zh.json @@ -2,6 +2,8 @@ "Peers": "节点数", "Connected": "已连接", "Connectable": "可连接", + "Onion": "洋葱点", + "Local": "局域网", "Connectable peers": "可连接节点", "Data transfer": "数据传输", @@ -24,6 +26,7 @@ "Optional files": "可选文件", "Downloaded": "已下载", "Download and help distribute all files": "下载并帮助分发所有文件", + "Auto download big file size limit": "自动下载大文件大小限制", "Total size": "总大小", "Downloaded files": "已下载文件", @@ -47,7 +50,7 @@ "Site address": "站点地址", "Donate": "捐赠", - "Missing files": "丢失的文件", + "Needs to be updated": "需要更新", "{} try": "{} 尝试", "{} tries": "{} 已尝试", "+ {num_bad_files} more": "+ {num_bad_files} 更多", @@ -74,7 +77,11 @@ "Database rebuilding....": "数据库重建中...", "Database rebuilt!": "数据库已重建!", "Site updated!": "站点已更新!", - "Delete this site": "删除这个站点", + "Delete this site": "删除此站点", + "Blacklist": "黑名单", + "Blacklist this site": "拉黑此站点", + "Reason": "原因", + "Delete and Blacklist": "删除并拉黑", "File write error: ": "文件写入错误:", "Site settings saved!": "站点设置已保存!", "Enter your private key:": "输入你的私钥:", diff --git a/src/Translate/languages/zh.json b/src/Translate/languages/zh.json index e0b1232f..4bef7ed9 100644 --- a/src/Translate/languages/zh.json +++ b/src/Translate/languages/zh.json @@ -8,6 +8,7 @@ "or configure Tor to become full member of ZeroNet network.": "或者配置你的 Tor 来成为 ZeroNet 的正式成员。", "Select account you want to use in this site:": "选择你要在这个网站使用的帐户:", + "No certificate": "没有证书", "currently selected": "当前选择", "Unique to site": "网站独有身份", From c703458e00908208a2c3b1f877236bd7ee78d30f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Apr 2018 10:24:38 +0200 Subject: [PATCH 0782/2570] Rev3403, Fix uploading small files with bigfile plugin --- plugins/Bigfile/BigfilePlugin.py | 2 +- src/Config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index 4b808cf3..1fa49acf 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -61,7 +61,7 @@ class UiRequestPlugin(object): if len(piecemap_info["sha512_pieces"]) == 1: # Small file, don't split hash = piecemap_info["sha512_pieces"][0].encode("hex") - hash_id = self.site.content_manager.hashfield.getHashId(hash) + hash_id = site.content_manager.hashfield.getHashId(hash) site.content_manager.optionalDownloaded(inner_path, hash_id, upload_info["size"], own=True) else: # Big file diff --git a/src/Config.py b/src/Config.py index deb46c50..929f8174 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.2" - self.rev = 3402 + self.rev = 3403 self.argv = argv self.action = None self.config_file = "zeronet.conf" From abc481604ff7e2401a095d4d1e6308855ab3626f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Apr 2018 12:39:46 +0200 Subject: [PATCH 0783/2570] Rev3404, Fix bigfile upload --- plugins/Bigfile/BigfilePlugin.py | 2 +- src/Config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index 1fa49acf..b4b88c37 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -89,7 +89,7 @@ class UiRequestPlugin(object): "piece_size": piece_size } - merkle_root_hash_id = self.site.content_manager.hashfield.getHashId(merkle_root) + 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) diff --git a/src/Config.py b/src/Config.py index 929f8174..b80fef22 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.2" - self.rev = 3403 + self.rev = 3404 self.argv = argv self.action = None self.config_file = "zeronet.conf" From a877a9fd912e127453ce064652c466819cf0ab86 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Apr 2018 15:31:55 +0200 Subject: [PATCH 0784/2570] Proper cleanup local announcer listener after testcase --- plugins/AnnounceLocal/Test/TestAnnounce.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/AnnounceLocal/Test/TestAnnounce.py b/plugins/AnnounceLocal/Test/TestAnnounce.py index 416441f7..691ecc26 100644 --- a/plugins/AnnounceLocal/Test/TestAnnounce.py +++ b/plugins/AnnounceLocal/Test/TestAnnounce.py @@ -25,7 +25,7 @@ def announcer(file_server, site): return file_server.local_announcer @pytest.fixture -def announcer_remote(site_temp): +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) @@ -38,6 +38,12 @@ def announcer_remote(site_temp): 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") From d927e85ecadbe1226764377a388005f29ea4392f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Apr 2018 15:33:02 +0200 Subject: [PATCH 0785/2570] ConnectionServer is not running by default --- 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 84cef96e..668430f7 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -40,7 +40,7 @@ class ConnectionServer(object): self.has_internet = True # Internet outage detection self.stream_server = None - self.running = True + self.running = False self.thread_checker = gevent.spawn(self.checkConnections) self.stat_recv = defaultdict(lambda: defaultdict(int)) From 026b6a4b01777d8458f5cc4df4d7ce3a2e9263ef Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Apr 2018 15:33:25 +0200 Subject: [PATCH 0786/2570] Handle shutdown request properly if not file server is running --- src/Debug/DebugHook.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Debug/DebugHook.py b/src/Debug/DebugHook.py index f9b79f11..c3956eed 100644 --- a/src/Debug/DebugHook.py +++ b/src/Debug/DebugHook.py @@ -10,13 +10,16 @@ last_error = None def shutdown(): print "Shutting down..." - try: - if "file_server" in dir(sys.modules["main"]): - gevent.spawn(sys.modules["main"].file_server.stop) - if "ui_server" in dir(sys.modules["main"]): - gevent.spawn(sys.modules["main"].ui_server.stop) - except Exception, err: - print "Proper shutdown error: %s" % err + if "file_server" in dir(sys.modules["main"]) and sys.modules["main"].file_server.running: + try: + if "file_server" in dir(sys.modules["main"]): + gevent.spawn(sys.modules["main"].file_server.stop) + if "ui_server" in dir(sys.modules["main"]): + gevent.spawn(sys.modules["main"].ui_server.stop) + except Exception, err: + print "Proper shutdown error: %s" % err + sys.exit(0) + else: sys.exit(0) # Store last error, ignore notify, allow manual error logging @@ -94,4 +97,4 @@ if __name__ == "__main__": thread1.kill(exception=Debug.Notify("Worker stopped")) #thread2.throw(Debug.Notify("Throw")) print "killed" - gevent.joinall([thread1,thread2]) \ No newline at end of file + gevent.joinall([thread1,thread2]) From 7b1601c840ea0dca3462b7283d4bbdaff241be4e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Apr 2018 15:33:39 +0200 Subject: [PATCH 0787/2570] Ping port 15441 by default --- src/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index 71109ec3..a4c3730b 100644 --- a/src/main.py +++ b/src/main.py @@ -451,7 +451,7 @@ class Actions(object): # Peer def peerPing(self, peer_ip, peer_port=None): if not peer_port: - peer_port = config.fileserver_port + peer_port = 15441 logging.info("Opening a simple connection server") global file_server from Connection import ConnectionServer From 8998bf85bd2263e62dd17d10aa0e664584b14836 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Apr 2018 15:34:07 +0200 Subject: [PATCH 0788/2570] Handle connection errors when running peerPing command --- src/main.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main.py b/src/main.py index a4c3730b..bc9c9282 100644 --- a/src/main.py +++ b/src/main.py @@ -461,7 +461,15 @@ class Actions(object): from Peer import Peer logging.info("Pinging 5 times peer: %s:%s..." % (peer_ip, int(peer_port))) + s = time.time() peer = Peer(peer_ip, peer_port) + peer.connect() + + if not peer.connection: + print "Error: Can't connect to peer (connection error: %s)" % peer.connection_error + return False + print "Connection time: %.3fs (connection error: %s)" % (time.time() - s, peer.connection_error) + for i in range(5): print "Response time: %.3fs (crypt: %s)" % (peer.ping(), peer.connection.crypt) time.sleep(1) From e23dc5bda33a5a776f3dce7b34af781b1730c4ce Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Apr 2018 15:34:21 +0200 Subject: [PATCH 0789/2570] Rev3406 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index b80fef22..6b0d35e2 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.2" - self.rev = 3404 + self.rev = 3406 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 75879de47a000d6bc75e3bdb46724059e1af2905 Mon Sep 17 00:00:00 2001 From: rllola Date: Fri, 6 Apr 2018 00:25:53 +0200 Subject: [PATCH 0790/2570] Force the use of port 15441 inside the container --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index bbfa08a0..982426a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ ENV ENABLE_TOR false WORKDIR /root #Set upstart command -CMD (! ${ENABLE_TOR} || tor&) && python zeronet.py --ui_ip 0.0.0.0 +CMD (! ${ENABLE_TOR} || tor&) && python zeronet.py --ui_ip 0.0.0.0 --fileserver_port 15441 #Expose ports EXPOSE 43110 15441 From 5285699a21d3d4f6b2e7825e2727207d014d59de Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 6 Apr 2018 00:51:14 +0200 Subject: [PATCH 0791/2570] Rev3407, Fix connection timeout checker --- src/Config.py | 2 +- src/Connection/ConnectionServer.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index 6b0d35e2..a2a6b4f3 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.2" - self.rev = 3406 + self.rev = 3407 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index 668430f7..a973e4fc 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -41,7 +41,6 @@ class ConnectionServer(object): self.stream_server = None self.running = False - self.thread_checker = gevent.spawn(self.checkConnections) self.stat_recv = defaultdict(lambda: defaultdict(int)) self.stat_sent = defaultdict(lambda: defaultdict(int)) @@ -66,6 +65,7 @@ class ConnectionServer(object): def start(self): self.running = True + self.thread_checker = gevent.spawn(self.checkConnections) CryptConnection.manager.loadCerts() if not self.port: self.log.info("No port found, not binding") @@ -272,6 +272,7 @@ class ConnectionServer(object): if time.time() - s > 0.01: self.log.debug("Connection cleanup in %.3fs" % (time.time() - s)) + self.log("Checkconnections ended") @util.Noparallel(blocking=False) def checkMaxConnections(self): From 4fbfa1d57910b2af365bc27013666e1f735bc94b Mon Sep 17 00:00:00 2001 From: Ivanq Date: Fri, 6 Apr 2018 11:40:23 +0300 Subject: [PATCH 0792/2570] Fix mergerSiteAdd typo --- plugins/disabled-Multiuser/MultiuserPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/disabled-Multiuser/MultiuserPlugin.py b/plugins/disabled-Multiuser/MultiuserPlugin.py index f9f16f55..ad35bd40 100644 --- a/plugins/disabled-Multiuser/MultiuserPlugin.py +++ b/plugins/disabled-Multiuser/MultiuserPlugin.py @@ -102,7 +102,7 @@ class UiWebsocketPlugin(object): "muteAdd", "muteRemove", "blacklistAdd", "blacklistRemove" ) if config.multiuser_no_new_sites: - self.multiuser_denied_cmds += ("MergerSiteAdd", ) + self.multiuser_denied_cmds += ("mergerSiteAdd", ) super(UiWebsocketPlugin, self).__init__(*args, **kwargs) From d4a1764d209fa5e1817bff16ef060b532a1f2171 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 6 Apr 2018 18:18:10 +0200 Subject: [PATCH 0793/2570] Rev3408, Use fixed 15441 port in tor always mode --- src/Config.py | 5 +++-- src/File/FileServer.py | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Config.py b/src/Config.py index a2a6b4f3..1d68b753 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.2" - self.rev = 3407 + self.rev = 3408 self.argv = argv self.action = None self.config_file = "zeronet.conf" @@ -257,7 +257,8 @@ class Config(object): self.parser.add_argument('--tor_controller', help='Tor controller address', metavar='ip:port', default='127.0.0.1:9051') self.parser.add_argument('--tor_proxy', help='Tor proxy address', metavar='ip:port', default='127.0.0.1:9050') self.parser.add_argument('--tor_password', help='Tor controller password', metavar='password') - self.parser.add_argument('--tor_hs_limit', help='Maximum number of hidden services', metavar='limit', type=int, default=10) + self.parser.add_argument('--tor_hs_limit', help='Maximum number of hidden services in Tor always mode', metavar='limit', type=int, default=10) + self.parser.add_argument('--tor_hs_port', help='Hidden service port in Tor always mode', metavar='limit', type=int, default=15441) self.parser.add_argument('--version', action='version', version='ZeroNet %s r%s' % (self.version, self.rev)) self.parser.add_argument('--end', help='Stop multi value argument parsing', action='store_true') diff --git a/src/File/FileServer.py b/src/File/FileServer.py index 65882bbd..55c1a1b1 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -25,8 +25,10 @@ class FileServer(ConnectionServer): self.log = logging.getLogger("FileServer") ip = ip.replace("*", "0.0.0.0") - should_use_random_port = port == 0 or config.tor == "always" - if should_use_random_port: + if config.tor == "always": + port = config.tor_hs_port + config.fileserver_port = port + elif port == 0: # Use random port port_range_from, port_range_to = map(int, config.fileserver_port_range.split("-")) port = self.getRandomPort(ip, port_range_from, port_range_to) config.fileserver_port = port From 151a6ce9e3d57ac9928f8c1b06089a465f17c343 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 12 Apr 2018 19:22:06 +0200 Subject: [PATCH 0794/2570] UserManager exclusive logging --- src/User/UserManager.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/User/UserManager.py b/src/User/UserManager.py index dff7ece1..4b5275db 100644 --- a/src/User/UserManager.py +++ b/src/User/UserManager.py @@ -1,6 +1,7 @@ # Included modules import json import logging +import time # ZeroNet Modules from User import User @@ -12,6 +13,7 @@ from Config import config class UserManager(object): def __init__(self): self.users = {} + self.log = logging.getLogger("UserManager") # Load all user from data/users.json def load(self): @@ -20,6 +22,7 @@ class UserManager(object): user_found = [] added = 0 + s = time.time() # Load new users for master_address, data in json.load(open("%s/users.json" % config.data_dir)).items(): if master_address not in self.users: @@ -32,16 +35,16 @@ class UserManager(object): for master_address in self.users.keys(): if master_address not in user_found: del(self.users[master_address]) - logging.debug("Removed user: %s" % master_address) + self.log.debug("Removed user: %s" % master_address) if added: - logging.debug("UserManager added %s users" % added) + self.log.debug("Added %s users in %.3fs" % (added, time.time() - s)) # Create new user # Return: User def create(self, master_address=None, master_seed=None): user = User(master_address, master_seed) - logging.debug("Created user: %s" % user.master_address) + self.log.debug("Created user: %s" % user.master_address) if user.master_address: # If successfully created self.users[user.master_address] = user user.save() From 513de18af52f48ae070935d3c6214a28d32775f0 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 12 Apr 2018 19:22:38 +0200 Subject: [PATCH 0795/2570] Remove unused code --- src/User/UserManager.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/User/UserManager.py b/src/User/UserManager.py index 4b5275db..c90c7eea 100644 --- a/src/User/UserManager.py +++ b/src/User/UserManager.py @@ -68,18 +68,3 @@ class UserManager(object): user_manager = UserManager() # Singleton - - -# Debug: Reload User.py -def reloadModule(): - return "Not used" - - import imp - global User, UserManager, user_manager - User = imp.load_source("User", "src/User/User.py").User # Reload source - # module = imp.load_source("UserManager", "src/User/UserManager.py") # Reload module - # UserManager = module.UserManager - # user_manager = module.user_manager - # Reload users - user_manager = UserManager() - user_manager.load() From 05db08c8d29831e8ad5ff0b5a277991199c5a22c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 12 Apr 2018 19:24:21 +0200 Subject: [PATCH 0796/2570] Move new site auth address generation to separate function --- src/User/User.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/User/User.py b/src/User/User.py index 2b10924f..f14144af 100644 --- a/src/User/User.py +++ b/src/User/User.py @@ -2,6 +2,8 @@ import logging import json import time +import gevent + import util from Crypt import CryptBitcoin from Plugin import PluginManager @@ -44,21 +46,26 @@ class User(object): def getAddressAuthIndex(self, address): return int(address.encode("hex"), 16) + @util.Noparallel() + def generateAuthAddress(self, address): + 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] = { + "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] + # Get user site data # Return: {"auth_address": "xxx", "auth_privatekey": "xxx"} def getSiteData(self, address, create=True): if address not in self.sites: # Generate new BIP32 child key based on site address if not create: return {"auth_address": None, "auth_privatekey": None} # Dont create user yet - 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] = { - "auth_address": CryptBitcoin.privatekeyToAddress(auth_privatekey), - "auth_privatekey": auth_privatekey - } - self.save() - self.log.debug("Added new site: %s in %.3fs" % (address, time.time() - s)) + self.generateAuthAddress(address) return self.sites[address] def deleteSiteData(self, address): From 4eb843fd8cbc0d28b59a4e86a8d06810fd33fe85 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 12 Apr 2018 19:25:13 +0200 Subject: [PATCH 0797/2570] Delay saving users.json --- src/User/User.py | 16 +++++++++++----- src/User/UserManager.py | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/User/User.py b/src/User/User.py index f14144af..acaaf1d2 100644 --- a/src/User/User.py +++ b/src/User/User.py @@ -25,6 +25,7 @@ class User(object): self.master_address = CryptBitcoin.privatekeyToAddress(self.master_seed) self.sites = data.get("sites", {}) self.certs = data.get("certs", {}) + self.delayed_save_thread = None self.log = logging.getLogger("User:%s" % self.master_address) @@ -41,7 +42,12 @@ class User(object): user_data["sites"] = self.sites user_data["certs"] = self.certs helper.atomicWrite("%s/users.json" % config.data_dir, json.dumps(users, indent=2, sort_keys=True)) - self.log.debug("Saved in %.3fs" % (time.time()-s)) + self.log.debug("Saved in %.3fs" % (time.time() - s)) + self.delayed_save_thread = None + + def saveDelayed(self): + if not self.delayed_save_thread: + self.delayed_save_thread = gevent.spawn_later(5, self.save) def getAddressAuthIndex(self, address): return int(address.encode("hex"), 16) @@ -71,13 +77,13 @@ class User(object): def deleteSiteData(self, address): if address in self.sites: del(self.sites[address]) - self.save() + self.saveDelayed() self.log.debug("Deleted site: %s" % address) def setSettings(self, address, settings): site_data = self.getSiteData(address) site_data["settings"] = settings - self.save() + self.saveDelayed() return site_data # Get data for a new, unique site @@ -92,7 +98,7 @@ class User(object): # Save to sites self.getSiteData(site_address) self.sites[site_address]["privatekey"] = site_privatekey - self.save() + self.saveDelayed() return site_address, bip32_index, self.sites[site_address] # Get BIP32 address from site address @@ -145,7 +151,7 @@ class User(object): else: if "cert" in site_data: del site_data["cert"] - self.save() + self.saveDelayed() return site_data # Get cert for the site address diff --git a/src/User/UserManager.py b/src/User/UserManager.py index c90c7eea..e174f75e 100644 --- a/src/User/UserManager.py +++ b/src/User/UserManager.py @@ -47,7 +47,7 @@ class UserManager(object): self.log.debug("Created user: %s" % user.master_address) if user.master_address: # If successfully created self.users[user.master_address] = user - user.save() + user.saveDelayed() return user # List all users from data/users.json From e95bc31defc3af23cbced2e7b8c55494a97b31be Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 12 Apr 2018 19:25:50 +0200 Subject: [PATCH 0798/2570] Fix logging and shutdown --- 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 a973e4fc..0645fc65 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -272,7 +272,7 @@ class ConnectionServer(object): if time.time() - s > 0.01: self.log.debug("Connection cleanup in %.3fs" % (time.time() - s)) - self.log("Checkconnections ended") + self.log.debug("Checkconnections ended") @util.Noparallel(blocking=False) def checkMaxConnections(self): From 2e573e95e5d4998b28ffb4a7f0f0911e8513a0b8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 12 Apr 2018 19:26:44 +0200 Subject: [PATCH 0799/2570] Fix multiuser plugin deauth --- plugins/disabled-Multiuser/MultiuserPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/disabled-Multiuser/MultiuserPlugin.py b/plugins/disabled-Multiuser/MultiuserPlugin.py index ad35bd40..7fc1d8a5 100644 --- a/plugins/disabled-Multiuser/MultiuserPlugin.py +++ b/plugins/disabled-Multiuser/MultiuserPlugin.py @@ -128,7 +128,7 @@ class UiWebsocketPlugin(object): if "ADMIN" not in self.site.settings["permissions"]: return self.response(to, "Logout not allowed") message = "You have been logged out. Login to another account" - message += "" + message += "" self.cmd("notification", ["done", message, 1000000]) # 1000000 = Show ~forever :) # Delete from user_manager user_manager = sys.modules["User.UserManager"].user_manager From aa68e69a18508b12535f3a11b2d3e6d264857b5d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 12 Apr 2018 19:28:59 +0200 Subject: [PATCH 0800/2570] Rev3411 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 1d68b753..f4d3470a 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.2" - self.rev = 3408 + self.rev = 3411 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 9b046ecc757469e03f007bbd4202554807230322 Mon Sep 17 00:00:00 2001 From: rllola Date: Wed, 18 Apr 2018 04:10:11 +0200 Subject: [PATCH 0801/2570] Change to port 26552 --- Dockerfile | 4 ++-- README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 982426a2..a91d2bb6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ ENV ENABLE_TOR false WORKDIR /root #Set upstart command -CMD (! ${ENABLE_TOR} || tor&) && python zeronet.py --ui_ip 0.0.0.0 --fileserver_port 15441 +CMD (! ${ENABLE_TOR} || tor&) && python zeronet.py --ui_ip 0.0.0.0 --fileserver_port 26552 #Expose ports -EXPOSE 43110 15441 +EXPOSE 43110 26552 diff --git a/README.md b/README.md index c36cc15a..bd05efc4 100644 --- a/README.md +++ b/README.md @@ -130,12 +130,12 @@ See `/usr/share/doc/zeronet-*/README.gentoo.bz2` for further assistance. * Open http://127.0.0.1:43110/ in your browser ### [Docker](https://www.docker.com/) -* `docker run -d -v :/root/data -p 15441:15441 -p 127.0.0.1:43110:43110 nofish/zeronet` +* `docker run -d -v :/root/data -p 26552:26552 -p 127.0.0.1:43110:43110 nofish/zeronet` * This Docker image includes the Tor proxy, which is disabled by default. Beware that some hosting providers may not allow you running Tor in their servers. If you want to enable it, set `ENABLE_TOR` environment variable to `true` (Default: `false`). E.g.: - `docker run -d -e "ENABLE_TOR=true" -v :/root/data -p 15441:15441 -p 127.0.0.1:43110:43110 nofish/zeronet` + `docker run -d -e "ENABLE_TOR=true" -v :/root/data -p 26552:26552 -p 127.0.0.1:43110:43110 nofish/zeronet` * Open http://127.0.0.1:43110/ in your browser ### [Virtualenv](https://virtualenv.readthedocs.org/en/latest/) From e51788ac058f8bdded7325ca9967dd7a28cdd036 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 18 Apr 2018 13:11:50 +0200 Subject: [PATCH 0802/2570] Make sure that we close UDP sockets of local peer discovery --- plugins/AnnounceLocal/BroadcastServer.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/plugins/AnnounceLocal/BroadcastServer.py b/plugins/AnnounceLocal/BroadcastServer.py index ec47a0d8..5863ad05 100644 --- a/plugins/AnnounceLocal/BroadcastServer.py +++ b/plugins/AnnounceLocal/BroadcastServer.py @@ -1,6 +1,7 @@ import socket import logging import time +from contextlib import closing import msgpack @@ -90,8 +91,9 @@ class BroadcastServer(object): message_part["sender"] = self.sender_info self.log.debug("Send to %s: %s" % (addr, message_part["cmd"])) - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.sendto(msgpack.packb(message_part), addr) + with closing(socket.socket(socket.AF_INET, socket.SOCK_DGRAM)) as sock: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.sendto(msgpack.packb(message_part), addr) def getMyIps(self): return UpnpPunch._get_local_ips() @@ -108,10 +110,11 @@ class BroadcastServer(object): for my_ip in my_ips: try: - 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.packb(message), addr) + 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.packb(message), addr) except Exception as err: self.log.warning("Error sending broadcast using ip %s: %s" % (my_ip, err)) From fd842c43a6bd36321653906608b53d44d970e24e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 18 Apr 2018 13:13:20 +0200 Subject: [PATCH 0803/2570] Rev3412, Change no longer working trackers --- src/Config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Config.py b/src/Config.py index f4d3470a..1d35bc60 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.2" - self.rev = 3411 + self.rev = 3412 self.argv = argv self.action = None self.config_file = "zeronet.conf" @@ -37,10 +37,10 @@ class Config(object): "zero://zero.booth.moe#f36ca555bee6ba216b14d10f38c16f7769ff064e0e37d887603548cc2e64191d:443", # US/NY "udp://tracker.coppersurfer.tk:6969", # DE "udp://tracker.leechers-paradise.org:6969", # NL - "udp://thetracker.org:80", # FR - "http://torrentsmd.eu:8080/announce", # US?/Cloudflare + "udp://104.238.198.186:8000", # US/LA + "http://tracker.skyts.net:6969/announce", # CN "http://0d.kebhana.mx:443/announce", # FR - "http://retracker.spark-rostov.ru:80/announce" # RU + "http://retracker.mgts.by:80/announce" # BY ] # Platform specific if sys.platform.startswith("win"): From 3beab611a346a155a5efd0ab565e38ce634fe36f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 18 Apr 2018 21:40:29 +0200 Subject: [PATCH 0804/2570] Rev3415, Fix FireFox wrapper_nonce url injection --- src/Config.py | 2 +- src/Ui/template/wrapper.html | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Config.py b/src/Config.py index 1d35bc60..deaa2c9f 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.2" - self.rev = 3412 + self.rev = 3415 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/Ui/template/wrapper.html b/src/Ui/template/wrapper.html index 61efbd26..e6e6a1fb 100644 --- a/src/Ui/template/wrapper.html +++ b/src/Ui/template/wrapper.html @@ -17,7 +17,7 @@ + + + + diff --git a/plugins/ContentFilter/media/js/ZeroFrame.js b/plugins/ContentFilter/media/js/ZeroFrame.js new file mode 100644 index 00000000..d6facdbf --- /dev/null +++ b/plugins/ContentFilter/media/js/ZeroFrame.js @@ -0,0 +1,119 @@ +// 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 + } +} From c5ca4dd7b33802414264f97c5ad8ef71ade39f88 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 25 Jun 2018 14:40:51 +0200 Subject: [PATCH 0905/2570] Change dead tracker --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 5e77daaa..ac17523b 100644 --- a/src/Config.py +++ b/src/Config.py @@ -38,7 +38,7 @@ class Config(object): "udp://tracker.coppersurfer.tk:6969", # DE "udp://tracker.leechers-paradise.org:6969", # NL "udp://104.238.198.186:8000", # US/LA - "http://tracker.skyts.cn:6969/announce", # CN + "http://tracker.swateam.org.uk:2710/announce", # US/NY "http://open.acgnxtracker.com:80/announce", # DE "http://retracker.mgts.by:80/announce" # BY ] From fc46bb65f89b4d42a52a003ed39e57cab98b5ea0 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 25 Jun 2018 14:41:34 +0200 Subject: [PATCH 0906/2570] Version 0.6.3, Rev3495 --- src/Config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index ac17523b..494b6b94 100644 --- a/src/Config.py +++ b/src/Config.py @@ -9,8 +9,8 @@ import ConfigParser class Config(object): def __init__(self, argv): - self.version = "0.6.2" - self.rev = 3478 + self.version = "0.6.3" + self.rev = 3495 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 283231ac6ec2bf6d1831ed28fbf8b41eddbdfff8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 25 Jun 2018 15:13:56 +0200 Subject: [PATCH 0907/2570] Remove renamed Mute plugin --- plugins/Mute/MutePlugin.py | 160 ---------------------------- plugins/Mute/__init__.py | 1 - plugins/Mute/languages/es.json | 6 -- plugins/Mute/languages/hu.json | 6 -- plugins/Mute/languages/it.json | 6 -- plugins/Mute/languages/pt-br.json | 6 -- plugins/Mute/languages/zh-tw.json | 6 -- plugins/Mute/languages/zh.json | 6 -- plugins/Mute/media/blacklisted.html | 64 ----------- plugins/Mute/media/js/ZeroFrame.js | 93 ---------------- 10 files changed, 354 deletions(-) delete mode 100644 plugins/Mute/MutePlugin.py delete mode 100644 plugins/Mute/__init__.py delete mode 100644 plugins/Mute/languages/es.json delete mode 100644 plugins/Mute/languages/hu.json delete mode 100644 plugins/Mute/languages/it.json delete mode 100644 plugins/Mute/languages/pt-br.json delete mode 100644 plugins/Mute/languages/zh-tw.json delete mode 100644 plugins/Mute/languages/zh.json delete mode 100644 plugins/Mute/media/blacklisted.html delete mode 100644 plugins/Mute/media/js/ZeroFrame.js diff --git a/plugins/Mute/MutePlugin.py b/plugins/Mute/MutePlugin.py deleted file mode 100644 index dfae5dfa..00000000 --- a/plugins/Mute/MutePlugin.py +++ /dev/null @@ -1,160 +0,0 @@ -import time -import json -import os -import re - -from Plugin import PluginManager -from Translate import Translate -from Config import config -from util import helper - - -if os.path.isfile("%s/mutes.json" % config.data_dir): - try: - data = json.load(open("%s/mutes.json" % config.data_dir)) - mutes = data.get("mutes", {}) - site_blacklist = data.get("site_blacklist", {}) - except Exception as err: - mutes = {} - site_blacklist = {} -else: - open("%s/mutes.json" % config.data_dir, "w").write('{"mutes": {}, "site_blacklist": {}}') - mutes = {} - site_blacklist = {} - -if "_" not in locals(): - _ = Translate("plugins/Mute/languages/") - - -@PluginManager.registerTo("UiWebsocket") -class UiWebsocketPlugin(object): - # Search and remove or readd files of an user - def changeDb(self, auth_address, action): - self.log.debug("Mute action %s on user %s" % (action, auth_address)) - res = self.site.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.server.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) - - def cbMuteAdd(self, to, auth_address, cert_user_id, reason): - mutes[auth_address] = {"cert_user_id": cert_user_id, "reason": reason, "source": self.site.address, "date_added": time.time()} - self.saveMutes() - self.changeDb(auth_address, "remove") - self.response(to, "ok") - - 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?"] % cert_user_id, _["Mute"]], - lambda (res): self.cbMuteAdd(to, auth_address, cert_user_id, reason) - ) - - def cbMuteRemove(self, to, auth_address): - del mutes[auth_address] - self.saveMutes() - self.changeDb(auth_address, "load") - self.response(to, "ok") - - def actionMuteRemove(self, to, auth_address): - if "ADMIN" in self.getPermissions(to): - self.cbMuteRemove(to, auth_address) - else: - self.cmd( - "confirm", - [_["Unmute %s?"] % mutes[auth_address]["cert_user_id"], _["Unmute"]], - lambda (res): self.cbMuteRemove(to, auth_address) - ) - - def actionMuteList(self, to): - if "ADMIN" in self.getPermissions(to): - self.response(to, mutes) - else: - return self.response(to, {"error": "Only ADMIN sites can list mutes"}) - - # Blacklist - def actionBlacklistAdd(self, to, site_address, reason=None): - if "ADMIN" not in self.getPermissions(to): - return self.response(to, {"error": "Forbidden, only admin sites can add to blacklist"}) - site_blacklist[site_address] = {"date_added": time.time(), "reason": reason} - self.saveMutes() - self.response(to, "ok") - - def actionBlacklistRemove(self, to, site_address): - if "ADMIN" not in self.getPermissions(to): - return self.response(to, {"error": "Forbidden, only admin sites can remove from blacklist"}) - del site_blacklist[site_address] - self.saveMutes() - self.response(to, "ok") - - def actionBlacklistList(self, to): - if "ADMIN" in self.getPermissions(to): - self.response(to, site_blacklist) - else: - return self.response(to, {"error": "Only ADMIN sites can list blacklists"}) - - # Write mutes and blacklist to json file - def saveMutes(self): - helper.atomicWrite("%s/mutes.json" % config.data_dir, json.dumps({"mutes": mutes, "site_blacklist": site_blacklist}, indent=2, sort_keys=True)) - - -@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 auth_address in mutes: - 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) - - -@PluginManager.registerTo("UiRequest") -class UiRequestPlugin(object): - def actionWrapper(self, path, extra_headers=None): - match = re.match("/(?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.server.site_manager.isDomain(address): - address = self.server.site_manager.resolveDomain(address) - - if address in site_blacklist: - site = self.server.site_manager.get(config.homepage) - if not extra_headers: - extra_headers = {} - self.sendHeader(extra_headers=extra_headers) - return iter([super(UiRequestPlugin, self).renderWrapper( - site, path, "uimedia/plugins/mute/blacklisted.html?address=" + address, - "Blacklisted site", extra_headers, show_loadingscreen=False - )]) - else: - return super(UiRequestPlugin, self).actionWrapper(path, extra_headers) - - def actionUiMedia(self, path, *args, **kwargs): - if path.startswith("/uimedia/plugins/mute/"): - file_path = path.replace("/uimedia/plugins/mute/", "plugins/Mute/media/") - return self.actionFile(file_path) - else: - return super(UiRequestPlugin, self).actionUiMedia(path) diff --git a/plugins/Mute/__init__.py b/plugins/Mute/__init__.py deleted file mode 100644 index f9d1081c..00000000 --- a/plugins/Mute/__init__.py +++ /dev/null @@ -1 +0,0 @@ -import MutePlugin \ No newline at end of file diff --git a/plugins/Mute/languages/es.json b/plugins/Mute/languages/es.json deleted file mode 100644 index 732ccfd8..00000000 --- a/plugins/Mute/languages/es.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "Hide all content from %s?": "Esconder todo el contenido de %s?", - "Mute": "Silenciar", - "Unmute %s?": "Mostrar todo el contenido de %s?", - "Unmute": "Reactivar" -} diff --git a/plugins/Mute/languages/hu.json b/plugins/Mute/languages/hu.json deleted file mode 100644 index e3332db8..00000000 --- a/plugins/Mute/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/Mute/languages/it.json b/plugins/Mute/languages/it.json deleted file mode 100644 index b0246918..00000000 --- a/plugins/Mute/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/Mute/languages/pt-br.json b/plugins/Mute/languages/pt-br.json deleted file mode 100644 index fd858678..00000000 --- a/plugins/Mute/languages/pt-br.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "Hide all content from %s?": "Esconder todo conteúdo de %s?", - "Mute": "Ativar mudo", - "Unmute %s?": "Mostrar o contéudo de %s?", - "Unmute": "Desativar mudo" -} diff --git a/plugins/Mute/languages/zh-tw.json b/plugins/Mute/languages/zh-tw.json deleted file mode 100644 index 3bdb5fc0..00000000 --- a/plugins/Mute/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/Mute/languages/zh.json b/plugins/Mute/languages/zh.json deleted file mode 100644 index beea7e77..00000000 --- a/plugins/Mute/languages/zh.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "Hide all content from %s?": "屏蔽 %s 的所有内容?", - "Mute": "屏蔽", - "Unmute %s?": "对 %s 解除屏蔽?", - "Unmute": "解除屏蔽" -} diff --git a/plugins/Mute/media/blacklisted.html b/plugins/Mute/media/blacklisted.html deleted file mode 100644 index 5d2bf80c..00000000 --- a/plugins/Mute/media/blacklisted.html +++ /dev/null @@ -1,64 +0,0 @@ - - - - - -
          -

          Site blocked

          -

          This site is on your blacklist:

          -
          -
          Too much image
          -
          on 2015-01-25 12:32:11
          -
          - -
          - - - - - - \ No newline at end of file diff --git a/plugins/Mute/media/js/ZeroFrame.js b/plugins/Mute/media/js/ZeroFrame.js deleted file mode 100644 index f3e2214b..00000000 --- a/plugins/Mute/media/js/ZeroFrame.js +++ /dev/null @@ -1,93 +0,0 @@ -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) - } - - 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') - } -} - From bc227b5f561416f418014d9bccf95b405e34a14a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 28 Jun 2018 02:03:47 +0200 Subject: [PATCH 0908/2570] Rev3498, Go back to homepage and less inviting open site button on blocked site page --- plugins/ContentFilter/media/blocklisted.html | 17 ++++++++++++----- src/Config.py | 2 +- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/plugins/ContentFilter/media/blocklisted.html b/plugins/ContentFilter/media/blocklisted.html index 33930b26..21b1d187 100644 --- a/plugins/ContentFilter/media/blocklisted.html +++ b/plugins/ContentFilter/media/blocklisted.html @@ -20,6 +20,10 @@ .button:active { position: relative; top: 1px; } .button:focus { outline: none; } +.textbutton { color: #999; margin-top: 25px; display: inline-block; text-transform: none; font-family: Arial, Helvetica; text-decoration: none; padding: 5px 15px; } +.textbutton-main { background-color: #FFF; color: #333; border-radius: 5px; } +.textbutton:hover { text-decoration: underline; color: #333; transition: none !important; } +.textbutton:active { background-color: #fafbfc; }
          @@ -29,7 +33,10 @@
          Too much image
          on 2015-01-25 12:32:11
          - +
  • @@ -67,12 +74,12 @@ class Page extends ZeroFrame { document.getElementById("added").innerText = "at " + date.toLocaleDateString() + " " + date.toLocaleTimeString() if (block["include"]) { document.getElementById("added").innerText += " from a shared blocklist" - document.getElementById("button").innerText = "Ignore blocking and visit the site" + document.getElementById("visit").innerText = "Ignore blocking and visit the site" } document.getElementById("details").style.transform = "scale(1) rotateX(0deg)" - document.getElementById("button").style.transform = "translateY(0)" - document.getElementById("button").style.opacity = "1" - document.getElementById("button").onclick = () => { + document.getElementById("visit").style.transform = "translateY(0)" + document.getElementById("visit").style.opacity = "1" + document.getElementById("visit").onclick = () => { if (block["include"]) this.cmd("siteAdd", address, () => { this.cmd("wrapperReload") }) else diff --git a/src/Config.py b/src/Config.py index 494b6b94..979314bd 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3495 + self.rev = 3498 self.argv = argv self.action = None self.config_file = "zeronet.conf" From ff85241962b511e4dd7f18c3ca3ae3f2cbe5005c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 28 Jun 2018 02:39:20 +0200 Subject: [PATCH 0909/2570] More detailed not supported browser error message --- src/Ui/template/wrapper.html | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Ui/template/wrapper.html b/src/Ui/template/wrapper.html index e6e6a1fb..5071d8d9 100644 --- a/src/Ui/template/wrapper.html +++ b/src/Ui/template/wrapper.html @@ -75,8 +75,11 @@ permissions = {permissions} show_loadingscreen = {show_loadingscreen} server_url = '{server_url}' -if (typeof WebSocket === "undefined") - document.body.innerHTML += "
    Your browser is not supported please use Chrome or Firefox.
    "; +if (typeof WebSocket === "undefined") { + tag = document.createElement('div'); + tag.innerHTML += "
    Your browser does not WebSocket connections.
    Please use the latest Chrome or Firefox browser.
    "; + document.body.appendChild(tag) +} From 1ca2b40bf934bc5896051840c12f505e6786c5aa Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 28 Jun 2018 02:39:46 +0200 Subject: [PATCH 0910/2570] Fix IE Edge compatibility on notification button clicks --- src/Ui/media/Wrapper.coffee | 5 +++-- src/Ui/media/all.js | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index 39689626..764427fb 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -29,7 +29,7 @@ class Wrapper @opener_tested = false @announcer_line = null - @allowed_event_constructors = [MouseEvent, KeyboardEvent] # Allowed event constructors + @allowed_event_constructors = [window.MouseEvent, window.KeyboardEvent, window.PointerEvent] # Allowed event constructors window.onload = @onPageLoad # On iframe loaded window.onhashchange = (e) => # On hash change @@ -49,7 +49,8 @@ class Wrapper throw "Event not trusted" if e.originalEvent.constructor not in @allowed_event_constructors - throw "Invalid event constructor: #{e.constructor} != #{allowed_event_constructor}" + debugger + throw "Invalid event constructor: #{e.constructor} not in #{JSON.stringify(@allowed_event_constructors)}" if e.originalEvent.currentTarget != allowed_target[0] throw "Invalid event target: #{e.originalEvent.currentTarget} != #{allowed_target[0]}" diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index 1c600620..0cb9d81d 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -714,7 +714,6 @@ jQuery.extend( jQuery.easing, }).call(this); - /* ---- src/Ui/media/Notifications.coffee ---- */ @@ -896,7 +895,7 @@ jQuery.extend( jQuery.easing, this.address = null; this.opener_tested = false; this.announcer_line = null; - this.allowed_event_constructors = [MouseEvent, KeyboardEvent]; + this.allowed_event_constructors = [window.MouseEvent, window.KeyboardEvent, window.PointerEvent]; window.onload = this.onPageLoad; window.onhashchange = (function(_this) { return function(e) { @@ -928,7 +927,8 @@ jQuery.extend( jQuery.easing, throw "Event not trusted"; } if (ref = e.originalEvent.constructor, indexOf.call(this.allowed_event_constructors, ref) < 0) { - throw "Invalid event constructor: " + e.constructor + " != " + allowed_event_constructor; + debugger; + throw "Invalid event constructor: " + e.constructor + " not in " + (JSON.stringify(this.allowed_event_constructors)); } if (e.originalEvent.currentTarget !== allowed_target[0]) { throw "Invalid event target: " + e.originalEvent.currentTarget + " != " + allowed_target[0]; @@ -1731,6 +1731,7 @@ jQuery.extend( jQuery.easing, }).call(this); + /* ---- src/Ui/media/WrapperZeroFrame.coffee ---- */ From 5d0600a0af2a8f02467fe09e30ecb10e6f31027a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 28 Jun 2018 02:39:55 +0200 Subject: [PATCH 0911/2570] Rev3499 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 979314bd..bc5f9612 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3498 + self.rev = 3499 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 54b93febbafb33b008b25136978ee07e44277189 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 28 Jun 2018 14:05:57 +0200 Subject: [PATCH 0912/2570] Fix included file content leaking to filters.json --- plugins/ContentFilter/ContentFilterPlugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/ContentFilter/ContentFilterPlugin.py b/plugins/ContentFilter/ContentFilterPlugin.py index bde4ce01..25e69827 100644 --- a/plugins/ContentFilter/ContentFilterPlugin.py +++ b/plugins/ContentFilter/ContentFilterPlugin.py @@ -139,6 +139,7 @@ class UiWebsocketPlugin(object): if not all_sites and include["address"] != self.site.address: continue if filters: + include = dict(include) # Don't modify original file_content content = filter_storage.site_manager.get(include["address"]).storage.loadJson(include["inner_path"]) include["mutes"] = content.get("mutes", {}) include["siteblocks"] = content.get("siteblocks", {}) From 515057c7d0a7b0566c0080541d701a613dc153c8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 28 Jun 2018 14:06:28 +0200 Subject: [PATCH 0913/2570] Update multiuser plugin with new filter commands --- plugins/disabled-Multiuser/MultiuserPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/disabled-Multiuser/MultiuserPlugin.py b/plugins/disabled-Multiuser/MultiuserPlugin.py index 7fc1d8a5..f5adf077 100644 --- a/plugins/disabled-Multiuser/MultiuserPlugin.py +++ b/plugins/disabled-Multiuser/MultiuserPlugin.py @@ -99,7 +99,7 @@ class UiWebsocketPlugin(object): "siteSetOwned", "siteSetAutodownloadoptional", "dbReload", "dbRebuild", "mergerSiteDelete", "siteSetLimit", "siteSetAutodownloadBigfileLimit", "optionalLimitSet", "optionalHelp", "optionalHelpRemove", "optionalHelpAll", "optionalFilePin", "optionalFileUnpin", "optionalFileDelete", - "muteAdd", "muteRemove", "blacklistAdd", "blacklistRemove" + "muteAdd", "muteRemove", "siteblockAdd", "siteblockRemove", "filterIncludeAdd", "filterIncludeRemove" ) if config.multiuser_no_new_sites: self.multiuser_denied_cmds += ("mergerSiteAdd", ) From 822ea1b4500fdb00ba02919d66e502e6e34c9c42 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 28 Jun 2018 14:07:06 +0200 Subject: [PATCH 0914/2570] Fix site blacklisting using sidebar --- plugins/Sidebar/media/Sidebar.coffee | 2 +- plugins/Sidebar/media/all.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee index 037b921c..74601c1d 100644 --- a/plugins/Sidebar/media/Sidebar.coffee +++ b/plugins/Sidebar/media/Sidebar.coffee @@ -315,7 +315,7 @@ class Sidebar extends Class else if confirmed == 2 @wrapper.displayPrompt "Blacklist this site", "text", "Delete and Blacklist", "Reason", (reason) => @tag.find("#button-delete").addClass("loading") - @wrapper.ws.cmd "blacklistAdd", [@wrapper.site_info.address, reason] + @wrapper.ws.cmd "siteblockAdd", [@wrapper.site_info.address, reason] @wrapper.ws.cmd "siteDelete", @wrapper.site_info.address, -> document.location = $(".fixbutton-bg").attr("href") diff --git a/plugins/Sidebar/media/all.js b/plugins/Sidebar/media/all.js index eb878bae..50b1a02f 100644 --- a/plugins/Sidebar/media/all.js +++ b/plugins/Sidebar/media/all.js @@ -638,7 +638,7 @@ window.initScrollable = function () { } else if (confirmed === 2) { return _this.wrapper.displayPrompt("Blacklist this site", "text", "Delete and Blacklist", "Reason", function(reason) { _this.tag.find("#button-delete").addClass("loading"); - _this.wrapper.ws.cmd("blacklistAdd", [_this.wrapper.site_info.address, reason]); + _this.wrapper.ws.cmd("siteblockAdd", [_this.wrapper.site_info.address, reason]); return _this.wrapper.ws.cmd("siteDelete", _this.wrapper.site_info.address, function() { return document.location = $(".fixbutton-bg").attr("href"); }); From d3122020b9514d411c4f95db9a42324873f31dfe Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 28 Jun 2018 14:07:43 +0200 Subject: [PATCH 0915/2570] Rev3500 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index bc5f9612..8f8ff089 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3499 + self.rev = 3500 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 3aebdae30584f4789d555e6ab3b6db1733a3a0f8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 29 Jun 2018 14:18:09 +0200 Subject: [PATCH 0916/2570] Log last announce time on force announce --- src/Ui/UiRequest.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index ff1c4b4f..623fb7da 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -315,8 +315,9 @@ class UiRequest(object): self.sendHeader(extra_headers=extra_headers) - if time.time() - site.announcer.time_last_announce > 60 * 60 and site.settings["serving"]: - site.log.debug("Site requested, but not announced recently. Updating...") + min_last_announce = (time.time() - site.announcer.time_last_announce) / 60 + if min_last_announce > 60 and site.settings["serving"]: + site.log.debug("Site requested, but not announced recently (last %.0fmin ago). Updating..." % min_last_announce) gevent.spawn(site.update, announce=True) return iter([self.renderWrapper(site, path, inner_path, title, extra_headers)]) From 8775222a6182e6a3991bc3070091d0debbcf755e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 29 Jun 2018 14:18:27 +0200 Subject: [PATCH 0917/2570] Add missing word to websocket error message --- src/Ui/template/wrapper.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ui/template/wrapper.html b/src/Ui/template/wrapper.html index 5071d8d9..8faaa5e3 100644 --- a/src/Ui/template/wrapper.html +++ b/src/Ui/template/wrapper.html @@ -77,7 +77,7 @@ server_url = '{server_url}' if (typeof WebSocket === "undefined") { tag = document.createElement('div'); - tag.innerHTML += "
    Your browser does not WebSocket connections.
    Please use the latest Chrome or Firefox browser.
    "; + tag.innerHTML += "
    Your browser does not support WebSocket connections.
    Please use the latest Chrome or Firefox browser.
    "; document.body.appendChild(tag) } From 811c694f80260a3ebca3a1fb5ddc5ebcfd902bcb Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 29 Jun 2018 14:20:44 +0200 Subject: [PATCH 0918/2570] Ignore filter includes from removed sites --- plugins/ContentFilter/ContentFilterPlugin.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/ContentFilter/ContentFilterPlugin.py b/plugins/ContentFilter/ContentFilterPlugin.py index 25e69827..27d63bff 100644 --- a/plugins/ContentFilter/ContentFilterPlugin.py +++ b/plugins/ContentFilter/ContentFilterPlugin.py @@ -140,7 +140,10 @@ class UiWebsocketPlugin(object): continue if filters: include = dict(include) # Don't modify original file_content - content = filter_storage.site_manager.get(include["address"]).storage.loadJson(include["inner_path"]) + 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) From d5b31a5545a4d3702786bb42dbd302ba136dc782 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 29 Jun 2018 14:20:54 +0200 Subject: [PATCH 0919/2570] Rev3501 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 8f8ff089..d417482f 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3500 + self.rev = 3501 self.argv = argv self.action = None self.config_file = "zeronet.conf" From e08a702408256cd0570c89b4b7d9ca875d8f06c5 Mon Sep 17 00:00:00 2001 From: Biosias Date: Mon, 2 Jul 2018 01:00:42 +0200 Subject: [PATCH 0920/2570] Adding few translations --- src/Translate/languages/sk.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Translate/languages/sk.json b/src/Translate/languages/sk.json index 3f782a9b..90720961 100644 --- a/src/Translate/languages/sk.json +++ b/src/Translate/languages/sk.json @@ -46,6 +46,12 @@ "Site size limit changed to {0}MB": "Limit veľkosti pamäte nastavený na {0}MB", " New version of this page has just released.
    Reload to see the modified content.": " Bola vydaná nová verzia tejto stránky.
    Znovu načítajte túto stránku aby bolo vidieť zmeny.", "This site requests permission:": "Táto stránka vyžaduje povolenie:", - "Accept": "Udeliť" + "Accept": "Udeliť", + + "on": "", + "Oct": "Okt", + "May": "Máj", + "Jun": "Jún", + "Jul": "Júl", } From ec05e6864ab918794df12ea140f1da7c48e1d1fe Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 2 Jul 2018 15:42:16 +0200 Subject: [PATCH 0921/2570] Rev3502, Start fileserver to new greenlet to fix siteDownload and siteAnnounce CLI action --- src/Config.py | 2 +- src/main.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Config.py b/src/Config.py index d417482f..1edbe25a 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3501 + self.rev = 3502 self.argv = argv self.action = None self.config_file = "zeronet.conf" diff --git a/src/main.py b/src/main.py index 9334fc31..a424625e 100644 --- a/src/main.py +++ b/src/main.py @@ -325,7 +325,7 @@ class Actions(object): global file_server from File import FileServer file_server = FileServer("127.0.0.1", 1234) - file_server.start() + file_server_thread = gevent.spawn(file_server.start, check_sites=False) site = Site(address) @@ -342,7 +342,6 @@ class Actions(object): print "Downloading..." site.downloadContent("content.json", check_modifications=True) - print on_completed.get() print "Downloaded in %.3fs" % (time.time()-s) @@ -362,7 +361,7 @@ class Actions(object): global file_server from File import FileServer file_server = FileServer("127.0.0.1", 1234) - file_server.start() + file_server_thread = gevent.spawn(file_server.start, check_sites=False) site = Site(address) site.announce() From 718178c1b696cd1a5972a6879375f79f341f5cd7 Mon Sep 17 00:00:00 2001 From: Ruben Barkow Date: Tue, 3 Jul 2018 21:42:14 +0200 Subject: [PATCH 0922/2570] de.json - correct german translation --- src/Translate/languages/de.json | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Translate/languages/de.json b/src/Translate/languages/de.json index c2c7c3f9..2a211957 100644 --- a/src/Translate/languages/de.json +++ b/src/Translate/languages/de.json @@ -1,24 +1,24 @@ { "Congratulation, your port {0} is opened.
    You are full member of ZeroNet network!": "Gratulation, dein Port {0} ist offen.
    Du bist ein volles Mitglied des ZeroNet Netzwerks!", - "Tor mode active, every connection using Onion route.": "Tor modus aktiv, jede verbindung nutzt die Onion Route.", + "Tor mode active, every connection using Onion route.": "Tor Modus aktiv, jede Verbindung nutzt die Onion Route.", "Successfully started Tor onion hidden services.": "Tor versteckte Dienste erfolgreich gestartet.", "Unable to start hidden services, please check your config.": "Nicht möglich versteckte Dienste zu starten.", - "For faster connections open {0} port on your router.": "Für schnellere verbindungen öffne Port {0} auf deinem Router.", + "For faster connections open {0} port on your router.": "Für schnellere Verbindungen, öffne Port {0} auf deinem Router.", "Your connection is restricted. Please, open {0} port on your router": "Deine Verbindung ist eingeschränkt. Bitte öffne Port {0} auf deinem Router", "or configure Tor to become full member of ZeroNet network.": "oder konfiguriere Tor um ein volles Mitglied des ZeroNet Netzwerks zu werden.", - "Select account you want to use in this site:": "Wähle das Konto, dass du auf dieser Seite benutzen willst:", + "Select account you want to use in this site:": "Wähle das Konto, das du auf dieser Seite benutzen willst:", "currently selected": "aktuell ausgewählt", "Unique to site": "Eindeutig zur Seite", "Content signing failed": "Signierung des Inhalts fehlgeschlagen", "Content publish queued for {0:.0f} seconds.": "Veröffentlichung des Inhalts um {0:.0f} Sekunden verzögert.", "Content published to {0} peers.": "Inhalt zu {0} Peers veröffentlicht.", - "No peers found, but your content is ready to access.": "Keine Peers geufnden, aber dein Inhalt ist bereit zum zugriff.", - "Your network connection is restricted. Please, open {0} port": "Deine Netwerk Verbindung ist beschränkt. Bitte öffne Port {0}", - "on your router to make your site accessible for everyone.": "auf deinem router um deine Seite für jeden zugänglich zu machen.", + "No peers found, but your content is ready to access.": "Keine Peers gefunden, aber dein Inhalt ist bereit zum Zugriff.", + "Your network connection is restricted. Please, open {0} port": "Deine Netwerkverbindung ist beschränkt. Bitte öffne Port {0}", + "on your router to make your site accessible for everyone.": "auf deinem Router um deine Seite für Jeden zugänglich zu machen.", "Content publish failed.": "Inhalt konnte nicht veröffentlicht werden.", - "This file still in sync, if you write it now, then the previous content may be lost.": "Diese Datei ist immer noch am synchronisieren. Wenn jetzt geschrieben wird geht der vorherige Inhalt verloren.", + "This file still in sync, if you write it now, then the previous content may be lost.": "Diese Datei wird noch synchronisiert. Wenn jetzt geschrieben wird geht der vorherige Inhalt verloren.", "Write content anyway": "Inhalt trotzdem schreiben", "New certificate added:": "Neues Zertifikat hinzugefügt:", "You current certificate:": "Dein aktuelles Zertifikat:", @@ -27,23 +27,23 @@ "Site cloned": "Seite geklont", "You have successfully changed the web interface's language!": "Du hast die Sprache des Webinterface erfolgreich geändert!", - "Due to the browser's caching, the full transformation could take some minute.": "Wegen des Browser Cachings kann die volle Transformation Minuten dauern.", + "Due to the browser's caching, the full transformation could take some minute.": "Aufgrund des Browsercaches kann die volle Transformation Minuten dauern.", - "Connection with UiServer Websocket was lost. Reconnecting...": "Verbindung mit UiServer Websocket wurde verloren. Neu verbinden...", - "Connection with UiServer Websocket recovered.": "Verbindung mit UiServer Websocket wiederhergestellt.", + "Connection with UiServer Websocket was lost. Reconnecting...": "Die Verbindung mit UiServer Websocketist abgebrochen. Neu verbinden...", + "Connection with UiServer Websocket recovered.": "Die Verbindung mit UiServer Websocket wurde wiederhergestellt.", "UiServer Websocket error, please reload the page.": "UiServer Websocket Fehler, bitte Seite neu laden.", "   Connecting...": "   Verbinden...", - "Site size: ": "Seiten größe: ", - "MB is larger than default allowed ": "MB ist größer als standartmäßig erlaubt", - "Open site and set size limit to \" + site_info.next_size_limit + \"MB": "Öffne Seite und setze das limit auf \" + site_info.next_size_limit + \"MB", + "Site size: ": "Seitengröße: ", + "MB is larger than default allowed ": "MB ist größer als der erlaubte Standart", + "Open site and set size limit to \" + site_info.next_size_limit + \"MB": "Öffne Seite und setze das Limit auf \" + site_info.next_size_limit + \"MB", " files needs to be downloaded": " Dateien müssen noch heruntergeladen werden", " downloaded": " heruntergeladen", - " download failed": " herunterladen fehlgeschlagen", + " download failed": " Herunterladen fehlgeschlagen", "Peers found: ": "Peers gefunden: ", "No peers found": "Keine Peers gefunden", - "Running out of size limit (": "Das Speicher Limit wird knapp (", + "Running out of size limit (": "Das Speicherlimit ist bald ausgeschöpft (", "Set limit to \" + site_info.next_size_limit + \"MB": "Limit auf \" + site_info.next_size_limit + \"MB ändern", - "Site size limit changed to {0}MB": "Speicher Limit für diese Seite auf {0}MB geändert", + "Site size limit changed to {0}MB": "Speicherlimit für diese Seite auf {0}MB geändert", " New version of this page has just released.
    Reload to see the modified content.": " Neue version dieser Seite wurde gerade veröffentlicht.
    Lade die Seite neu um den geänderten Inhalt zu sehen.", "This site requests permission:": "Diese Seite fordert rechte:", "Accept": "Genehmigen" From e548a4b6aaa372c198cfbee0515a83fb00a3f6f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kr=C3=BCger?= Date: Tue, 3 Jul 2018 23:40:25 +0200 Subject: [PATCH 0923/2570] de.json: Fix typo Net{ => z}werkverbindung --- src/Translate/languages/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Translate/languages/de.json b/src/Translate/languages/de.json index 2a211957..aa8c3805 100644 --- a/src/Translate/languages/de.json +++ b/src/Translate/languages/de.json @@ -15,7 +15,7 @@ "Content publish queued for {0:.0f} seconds.": "Veröffentlichung des Inhalts um {0:.0f} Sekunden verzögert.", "Content published to {0} peers.": "Inhalt zu {0} Peers veröffentlicht.", "No peers found, but your content is ready to access.": "Keine Peers gefunden, aber dein Inhalt ist bereit zum Zugriff.", - "Your network connection is restricted. Please, open {0} port": "Deine Netwerkverbindung ist beschränkt. Bitte öffne Port {0}", + "Your network connection is restricted. Please, open {0} port": "Deine Netzwerkverbindung ist beschränkt. Bitte öffne Port {0}", "on your router to make your site accessible for everyone.": "auf deinem Router um deine Seite für Jeden zugänglich zu machen.", "Content publish failed.": "Inhalt konnte nicht veröffentlicht werden.", "This file still in sync, if you write it now, then the previous content may be lost.": "Diese Datei wird noch synchronisiert. Wenn jetzt geschrieben wird geht der vorherige Inhalt verloren.", From 8bdc61ddcc1cf1cca62e3342c8b728e5f2aa6345 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 5 Jul 2018 01:59:10 +0200 Subject: [PATCH 0924/2570] Rev3503, Ignore invalid site translation files --- plugins/TranslateSite/TranslateSitePlugin.py | 13 ++++++++----- src/Config.py | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/plugins/TranslateSite/TranslateSitePlugin.py b/plugins/TranslateSite/TranslateSitePlugin.py index a7bc65b0..6eefbb77 100644 --- a/plugins/TranslateSite/TranslateSitePlugin.py +++ b/plugins/TranslateSite/TranslateSitePlugin.py @@ -58,11 +58,14 @@ class UiRequestPlugin(object): # if site.content_manager.contents["content.json"]["files"].get(lang_file): site.needFile(lang_file, priority=10) - if inner_path.endswith("js"): - data = translate.translateData(data, site.storage.loadJson(lang_file), "js") - else: - data = translate.translateData(data, site.storage.loadJson(lang_file), "html") - data = data.replace("lang={lang}", "lang=" + str(translate.lang)) # lang get parameter to .js file to avoid cache + try: + if inner_path.endswith("js"): + data = translate.translateData(data, site.storage.loadJson(lang_file), "js") + else: + data = translate.translateData(data, site.storage.loadJson(lang_file), "html") + data = data.replace("lang={lang}", "lang=" + str(translate.lang)) # lang get parameter to .js file to avoid cache + except Exception as err: + site.log.error("Error loading translation file %s: %s" % (lang_file, err)) self.log.debug("Patched %s (%s bytes) in %.3fs" % (inner_path, len(data), time.time() - s)) yield data diff --git a/src/Config.py b/src/Config.py index 1edbe25a..419b1afe 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3502 + self.rev = 3503 self.argv = argv self.action = None self.config_file = "zeronet.conf" From dc2051fb59f8e5aa3335c49c5e51029c84fb8d50 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 6 Jul 2018 22:56:54 +0200 Subject: [PATCH 0925/2570] Skip invalid translate files --- src/Translate/Translate.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Translate/Translate.py b/src/Translate/Translate.py index 9c25eb33..a646ba8b 100644 --- a/src/Translate/Translate.py +++ b/src/Translate/Translate.py @@ -35,8 +35,12 @@ class Translate(dict): def load(self): if os.path.isfile(self.lang_file): - data = json.load(open(self.lang_file)) - logging.debug("Loaded translate file: %s (%s entries)" % (self.lang_file, len(data))) + try: + data = json.load(open(self.lang_file)) + logging.debug("Loaded translate file: %s (%s entries)" % (self.lang_file, len(data))) + except Exception as err: + logging.error("Error loading translate file %s: %s" % (self.lang_file, err)) + data = {} dict.__init__(self, data) else: data = {} From b6424b4596cd97846d9064852112653b7d8f158b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 6 Jul 2018 22:57:18 +0200 Subject: [PATCH 0926/2570] Fix sk translation --- src/Translate/languages/sk.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Translate/languages/sk.json b/src/Translate/languages/sk.json index 90720961..ad80dbc9 100644 --- a/src/Translate/languages/sk.json +++ b/src/Translate/languages/sk.json @@ -47,11 +47,11 @@ " New version of this page has just released.
    Reload to see the modified content.": " Bola vydaná nová verzia tejto stránky.
    Znovu načítajte túto stránku aby bolo vidieť zmeny.", "This site requests permission:": "Táto stránka vyžaduje povolenie:", "Accept": "Udeliť", - + "on": "", "Oct": "Okt", "May": "Máj", "Jun": "Jún", - "Jul": "Júl", + "Jul": "Júl" } From b6bf38b85cec018c76590e866445fed6b4d3fcd9 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 6 Jul 2018 22:57:27 +0200 Subject: [PATCH 0927/2570] Rev3504 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 419b1afe..36fa6b67 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3503 + self.rev = 3504 self.argv = argv self.action = None self.config_file = "zeronet.conf" From 26b0a7d75d7190f5c5dec32fcf784ef4dfb675d6 Mon Sep 17 00:00:00 2001 From: vitorio Date: Sat, 7 Jul 2018 01:15:40 -0500 Subject: [PATCH 0928/2570] Support removing donation link / custom donation test --- plugins/Sidebar/SidebarPlugin.py | 34 ++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index a40bc654..230be449 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -409,16 +409,30 @@ class UiWebsocketPlugin(object): """)) - site_address = self.site.address - body.append(_(u""" -
  • -
    -
    - {site_address} - {_[Donate]} -
    -
  • - """)) + donate_key = site.content_manager.contents.get("content.json", {}).get("donate", True) + if donate_key == False or donate_key == "": + pass + elif (type(donate_key) == str or type(donate_key) == unicode) and len(donate_key) > 0: + escaped_donate_key = cgi.escape(donate_key, True) + body.append(_(u""" +
  • +
    +
    + {escaped_donate_key} +
    +
  • + """)) + else: + site_address = self.site.address + body.append(_(u""" +
  • +
    +
    + {site_address} + {_[Donate]} +
    +
  • + """)) def sidebarRenderOwnedCheckbox(self, body, site): if self.site.settings["own"]: From b09da2641e6da078623552ed18b2774cffa45ef0 Mon Sep 17 00:00:00 2001 From: vitorio Date: Sat, 7 Jul 2018 10:55:20 -0500 Subject: [PATCH 0929/2570] Preserve site address --- plugins/Sidebar/SidebarPlugin.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index 230be449..d382c020 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -410,29 +410,33 @@ class UiWebsocketPlugin(object): """)) donate_key = site.content_manager.contents.get("content.json", {}).get("donate", True) + site_address = self.site.address + body.append(_(u""" +
  • +
    +
    + {site_address} + """)) if donate_key == False or donate_key == "": pass elif (type(donate_key) == str or type(donate_key) == unicode) and len(donate_key) > 0: escaped_donate_key = cgi.escape(donate_key, True) body.append(_(u""" -
  • -
    -
    - {escaped_donate_key} -
    -
  • +
    + +
  • +
    +
    + {escaped_donate_key} """)) else: - site_address = self.site.address body.append(_(u""" -
  • -
    -
    - {site_address} - {_[Donate]} -
    -
  • + {_[Donate]} """)) + body.append(_(u""" +
    + + """)) def sidebarRenderOwnedCheckbox(self, body, site): if self.site.settings["own"]: From 77aa23a375d0f2bfb14baa953a70d3ad9ce256b3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 10 Jul 2018 03:24:34 +0200 Subject: [PATCH 0930/2570] Make UiWebsocket admin_commands and async_commands class variables for easier extension --- src/Ui/UiWebsocket.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 15372d1a..097d336f 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -20,7 +20,12 @@ from Content.ContentManager import VerifyError, SignError @PluginManager.acceptPlugins class UiWebsocket(object): - + admin_commands = set([ + "sitePause", "siteResume", "siteDelete", "siteList", "siteSetLimit", "siteAdd", + "channelJoinAllsite", "serverUpdate", "serverPortcheck", "serverShutdown", "serverShowdirectory", "serverGetWrapperNonce", + "certSet", "configSet", "permissionAdd", "permissionRemove" + ]) + async_commands = set(["fileGet", "fileList", "dirList", "fileNeed"]) def __init__(self, ws, site, server, user, request): self.ws = ws self.site = site @@ -34,12 +39,6 @@ class UiWebsocket(object): self.channels = [] # Channels joined to self.state = {"sending": False} # Shared state of websocket connection self.send_queue = [] # Messages to send to client - self.admin_commands = ( - "sitePause", "siteResume", "siteDelete", "siteList", "siteSetLimit", "siteAdd", - "channelJoinAllsite", "serverUpdate", "serverPortcheck", "serverShutdown", "serverShowdirectory", "serverGetWrapperNonce", - "certSet", "configSet", "permissionAdd", "permissionRemove" - ) - self.async_commands = ("fileGet", "fileList", "dirList", "fileNeed") # Start listener loop def start(self): From 5aab10fab2adcc158c9b25addaebf323a499f058 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 10 Jul 2018 03:28:02 +0200 Subject: [PATCH 0931/2570] Support client restart without updating --- src/Ui/UiWebsocket.py | 4 +++- src/main.py | 2 ++ zeronet.py | 27 ++++++++++++--------------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 097d336f..3240b30f 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -960,7 +960,9 @@ class UiWebsocket(object): res = sys.modules["main"].file_server.openport() self.response(to, res) - def actionServerShutdown(self, to): + def actionServerShutdown(self, to, restart=False): + if restart: + sys.modules["main"].restart_after_shutdown = True sys.modules["main"].file_server.stop() sys.modules["main"].ui_server.stop() diff --git a/src/main.py b/src/main.py index a424625e..1c5f432f 100644 --- a/src/main.py +++ b/src/main.py @@ -20,6 +20,7 @@ else: # Old gevent # Not thread: pyfilesystem and systray icon, Not subprocess: Gevent 1.1+ update_after_shutdown = False # If set True then update and restart zeronet after main loop ended +restart_after_shutdown = False # If set True then restart zeronet after main loop ended # Load config from Config import config @@ -175,6 +176,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") # Site commands diff --git a/zeronet.py b/zeronet.py index 6aa4bf25..f75c9e4d 100755 --- a/zeronet.py +++ b/zeronet.py @@ -20,7 +20,6 @@ def main(): if main.update_after_shutdown: # Updater import gc import update - import atexit # Try cleanup openssl try: if "lib.opensslVerify" in sys.modules: @@ -42,16 +41,6 @@ def main(): except Exception, err: print "Update error: %s" % err - # Close log files - logger = sys.modules["main"].logging.getLogger() - - for handler in logger.handlers[:]: - handler.flush() - handler.close() - logger.removeHandler(handler) - - atexit._run_exitfuncs() - except Exception, err: # Prevent closing import traceback try: @@ -60,13 +49,21 @@ def main(): except Exception, log_err: print "Failed to log error:", log_err traceback.print_exc() - from src.Config import config + from Config import config traceback.print_exc(file=open(config.log_dir + "/error.log", "a")) - if main and main.update_after_shutdown: # Updater - # Restart - gc.collect() # Garbage collect + if main and (main.update_after_shutdown or main.restart_after_shutdown): # Updater + import atexit print "Restarting..." + # Close log files + logger = sys.modules["main"].logging.getLogger() + + for handler in logger.handlers[:]: + handler.flush() + handler.close() + logger.removeHandler(handler) + + atexit._run_exitfuncs() import time time.sleep(1) # Wait files to close args = sys.argv[:] From 10bab2b6e4d5f5cc04af153b2d37ef02b2b8bc8f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 10 Jul 2018 03:30:47 +0200 Subject: [PATCH 0932/2570] UiConfig plugin --- plugins/UiConfig/UiConfigPlugin.py | 53 + plugins/UiConfig/__init__.py | 1 + plugins/UiConfig/media/config.html | 20 + plugins/UiConfig/media/css/Config.css | 63 + plugins/UiConfig/media/css/all.css | 120 ++ plugins/UiConfig/media/css/button.css | 12 + plugins/UiConfig/media/css/fonts.css | 30 + plugins/UiConfig/media/img/loading.gif | Bin 0 -> 723 bytes .../UiConfig/media/js/ConfigStorage.coffee | 105 ++ plugins/UiConfig/media/js/UiConfig.coffee | 223 +++ plugins/UiConfig/media/js/all.js | 1650 +++++++++++++++++ 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 ++++++++ plugins/UiConfig/media/js/utils/Dollar.coffee | 3 + .../UiConfig/media/js/utils/ZeroFrame.coffee | 85 + 17 files changed, 3240 insertions(+) create mode 100644 plugins/UiConfig/UiConfigPlugin.py create mode 100644 plugins/UiConfig/__init__.py create mode 100644 plugins/UiConfig/media/config.html create mode 100644 plugins/UiConfig/media/css/Config.css create mode 100644 plugins/UiConfig/media/css/all.css create mode 100644 plugins/UiConfig/media/css/button.css create mode 100644 plugins/UiConfig/media/css/fonts.css create mode 100644 plugins/UiConfig/media/img/loading.gif create mode 100644 plugins/UiConfig/media/js/ConfigStorage.coffee create mode 100644 plugins/UiConfig/media/js/UiConfig.coffee create mode 100644 plugins/UiConfig/media/js/all.js create mode 100644 plugins/UiConfig/media/js/lib/Class.coffee create mode 100644 plugins/UiConfig/media/js/lib/Promise.coffee create mode 100644 plugins/UiConfig/media/js/lib/Prototypes.coffee create mode 100644 plugins/UiConfig/media/js/lib/maquette.js create mode 100644 plugins/UiConfig/media/js/utils/Dollar.coffee create mode 100644 plugins/UiConfig/media/js/utils/ZeroFrame.coffee diff --git a/plugins/UiConfig/UiConfigPlugin.py b/plugins/UiConfig/UiConfigPlugin.py new file mode 100644 index 00000000..34766bc4 --- /dev/null +++ b/plugins/UiConfig/UiConfigPlugin.py @@ -0,0 +1,53 @@ +from Plugin import PluginManager +from Config import config + + +@PluginManager.afterLoad +def importPluginnedClasses(): + from Ui import UiWebsocket + UiWebsocket.admin_commands.add("configList") + + +@PluginManager.registerTo("UiRequest") +class UiRequestPlugin(object): + def actionWrapper(self, path, extra_headers=None): + if path.strip("/") != "Config": + return super(UiRequestPlugin, self).actionWrapper(path, extra_headers) + + if not extra_headers: + extra_headers = {} + self.sendHeader(extra_headers=extra_headers) + site = self.server.site_manager.get(config.homepage) + return iter([super(UiRequestPlugin, self).renderWrapper( + site, path, "uimedia/plugins/uiconfig/config.html", + "Config", extra_headers, show_loadingscreen=False + )]) + + def actionUiMedia(self, path, *args, **kwargs): + if path.startswith("/uimedia/plugins/uiconfig/"): + file_path = path.replace("/uimedia/plugins/uiconfig/", "plugins/UiConfig/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) + return self.actionFile(file_path) + else: + return super(UiRequestPlugin, self).actionUiMedia(path) + + +@PluginManager.registerTo("UiWebsocket") +class UiWebsocketPlugin(object): + def actionConfigList(self, to): + back = {} + config_values = vars(config.arguments) + config_values.update(config.pending_changes) + for key, val in config_values.iteritems(): + if key not in config.keys_api_change_allowed: + continue + back[key] = { + "value": val, + "default": config.parser.get_default(key), + "pending": key in config.pending_changes + } + return back + diff --git a/plugins/UiConfig/__init__.py b/plugins/UiConfig/__init__.py new file mode 100644 index 00000000..3c48da61 --- /dev/null +++ b/plugins/UiConfig/__init__.py @@ -0,0 +1 @@ +import UiConfigPlugin diff --git a/plugins/UiConfig/media/config.html b/plugins/UiConfig/media/config.html new file mode 100644 index 00000000..407d2acd --- /dev/null +++ b/plugins/UiConfig/media/config.html @@ -0,0 +1,20 @@ + + + + + Settings - ZeroNet + + + + + + +

    ZeroNet config

    + +
    +
    +
    + + + + diff --git a/plugins/UiConfig/media/css/Config.css b/plugins/UiConfig/media/css/Config.css new file mode 100644 index 00000000..7f6d3d38 --- /dev/null +++ b/plugins/UiConfig/media/css/Config.css @@ -0,0 +1,63 @@ +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 } +h2 { margin-top: 10px; } +h3 { font-weight: normal } +h1 { background: linear-gradient(33deg,#af3bff,#0d99c9); color: white; padding: 16px 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; } +.section { margin: 0px 10%; } +.config-items { font-size: 19px; margin-top: 25px; margin-bottom: 75px; } +.config-item { position: relative; margin: 35px 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; + 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; } \ No newline at end of file diff --git a/plugins/UiConfig/media/css/all.css b/plugins/UiConfig/media/css/all.css new file mode 100644 index 00000000..34927064 --- /dev/null +++ b/plugins/UiConfig/media/css/all.css @@ -0,0 +1,120 @@ + + +/* ---- plugins/UiConfig/media/css/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 } +h2 { margin-top: 10px; } +h3 { font-weight: normal } +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; } +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 ; } +.section { margin: 0px 10%; } +.config-items { font-size: 19px; margin-top: 25px; margin-bottom: 75px; } +.config-item { position: relative; margin: 35px 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-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; } + + +/* ---- plugins/UiConfig/media/css/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 } + + +/* ---- plugins/UiConfig/media/css/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,d09GRgABAAAAAEbcABAAAAAAfQwAAQABAAAAAAAAAAAAAAAAAAAAAAAAAABHUE9TAAABbAAABOQAAAv2MtQEeUdTVUIAAAZQAAAAQQAAAFCyIrRQT1MvMgAABpQAAABXAAAAYLorAUBjbWFwAAAG7AAAAI8AAADEj/6wZGN2dCAAAAd8AAAAMAAAADAX3wLxZnBnbQAAB6wAAAE/AAABvC/mTqtnYXNwAAAI7AAAAAwAAAAMAAgAE2dseWYAAAj4AAA2eQAAYlxNsqlBaGVhZAAAP3QAAAA0AAAANve2KKdoaGVhAAA/qAAAAB8AAAAkDRcHFmhtdHgAAD/IAAACPAAAA3CPSUvWbG9jYQAAQgQAAAG6AAABusPVqwRtYXhwAABDwAAAACAAAAAgAwkC3m5hbWUAAEPgAAAAtAAAAU4XNjG1cG9zdAAARJQAAAF3AAACF7VLITZwcmVwAABGDAAAAM8AAAEuQJ9pDngBpJUDrCVbE0ZX9znX1ti2bdu2bU/w89nm1di2bdu2jXjqfWO7V1ajUru2Otk4QCD5qIRbqUqtRoT2aj+oDynwApjhwNN34fbsPKAPobrrDjggvbggAz21cOiHFyjoKeIpwkH3sHvRve4pxWVnojPdve7MdZY7e53zrq+bzL3r5nDzuTXcfm6iJ587Wa5U/lMuekp5hHv9Ge568okijyiFQ0F8CCSITGQhK9nITh7yUkDxQhSmKMUpQSlKU4bq1KExzWlBK9rwCZ/yGZ/zBV/yNd/wLd/xM7/yG7/zB3+SyFKWs4GNbGYLh/BSnBhKkI5SJCVR5iXs3j4iZGqZyX6nKNFUsq1UsSNUldVkDdnADtNIz8Z2mmZ2geZ2llbyE7X5VH4mP5dfyC/lCNUYKUfJ0XKMHCvHq8YEOVFOkpPlLNWeLefIuXKeXKg+FsnFcolcqr6Wy1XK36SxbpUOLWzxg/tsXJoSxlcWgw9FlVPcTlLCLlHKtpAovYruU/SyIptJlH6ay0K13Upva8e/rYNal2OcjWGB/Y2XYGIoR6SyjtOOaBQhXJEQRS4qEvag51P4ktuuUEzGyjgZLxNkAD4kI1AGk1Ets6lVSjaQjI1ys9wig6iicVaV1WQN2UiOlxPkRDlJTparpIfqRNGUGFpIH8IsgQiZWm6SW6VGpMxiMlbGyXiZID1ksBk0tasa+REcgrWbjua9k1ACbC+aMyG2RGONorqd1Ey3KvsMmr9WKUGrtEHZP2iV5miVZrPN5uFQXa21FgShu/bK9V7HCz4/+M4nBcnA9ltfW25z7ZKNs3G89bp3io+47JSdtbHvkX+Ct+dcfK7+Bdtpf+h+/o1trsvLQPQzsat2+pW5F3jvS5U0lhdi522PtbA9L6zn5efGkM/y3LsGAHbD/g22Tyv213N1GtoduwmSRzWG2go7BIS/cix/ameH20SbZFOJQFgyAFto4y3STgLhds2m2LIn+dtsB9i2JxWyA9hJ9fuNXeLF+uvtiB0DCWES6wxgl+WMN6zPWQDCnu6j/sUmGs+LuV1spo2wdRZrE4gkiiiLfNTvJRtgJ9RHpMZ/WqP4FIBQVAv5Qp3L2hFe3GM7/qa/5BWxg2/Iv/NsW7UG7Bzvdb0p326+Inb0PesfeLf56q+7BkDEK/LaAQBJXldHI9X96Q6+dVSX3m8mGhvy7ZdDbXSCE0YEqcn86BTP/eQUL0oxdIZTEp3iVKIyVahGTepRnwY0RCc6LWlF61ee4rHEEU8CiYxgJKMYzRjGMp4JTGQSk5nJLGYzh7nMYynLHp34m9CZz1YO4ZKfMOEQIRxSC4fMwiWL8JBVeMkmfMgtfMkj/Mgr/CkgvBQUARQVgRQTvhQXQZQQwZQUIZQSoZQWYVQS4VQWEVQRkVQTUdQU0WjmujcQMTQUETQWSWguktJSJKOVSEprkZyvhYdv+A4ffhZefuVP3WPRaUeiCGUEYwlnvIhkApOJYqaIZhbziGGpSMoyEcFykZRNwmGrcDgkfHDkP4WQhQ3EQBDE9pmZ+m/pK4ovGh2DLW8Y/0wRrZ3sTlWy/Ut6kPnlj7St3vzVJ3/zxZ878t9iVrSeNZdng1ty+3Z0tRvzw/zamDuNWXr9V2Q8vEZPedSbe/UNmH3D1uu4Sr5k7uHPvuMCT5oZE7a0fYJ4AWNgZGBg4GKQY9BhYHRx8wlh4GBgYQCC///BMow5memJQDEGCA8oxwKmOYBYCESDxa4xMDH4MDACoScANIcG1QAAAHgBY2BmWcj4hYGVgYF1FqsxAwOjPIRmvsiQxsTAwADEUPCAgel9AINCNJCpAOK75+enAyne/385kv5eZWDgSGLSVmBgnO/PyMDAYsW6gUEBCJkA3C8QGAB4AWNgYGACYmYgFgGSjGCahWEDkNZgUACyOBh4GeoYTjCcZPjPaMgYzHSM6RbTHQURBSkFOQUlBSsFF4UShTVKQv//A3XwAnUsAKo8BVQZBFUprCChIANUaYlQ+f/r/8f/DzEI/T/4f8L/gr///r7+++rBlgcbH2x4sPbB9Ad9D+IfaNw7DHQLkQAAN6c0ewAAKgDDAJIAmACHAGgAjACqAAAAFf5gABUEOgAVBbAAFQSNABADIQALBhgAFQAAAAB4AV2OBc4bMRCF7f4UlCoohmyFE1sRQ0WB3ZTbcDxlJlEPUOaGzvJWuBHmODlEaaFsGJ5PD0ydR7RnHM5X5PLv7/Eu40R3bt7Q4EoI+7EFfkvjkAKvSY0dJbrYKXYHJk9iJmZn781EVzy6fQ+7xcB7jfszagiwoXns2ZGRaFLqd3if6JTGro/ZDTAz8gBPAkDgg1Ljq8aeOi+wU+qZvsErK4WmRSkphY1Nz2BjpSSRxv5vjZ5//vh4qPZAYb+mEQkJQ4NmCoxmszDLS7yazVKzPP3ON//mLmf/F5p/F7BTtF3+qhd0XuVlyi/kZV56CsnSiKrzQ2N7EiVpxBSO2hpxhWOeSyinzD+J2dCsm2yX3XUj7NPIrNnRne1TSiHvwcUn9zD7XSMPkVRofnIFu2KcY8xKrdmxna1F+gexEIitAAABAAIACAAC//8AD3gBfFcFfBu5sx5pyWkuyW5iO0md15yzzboUqilQZmZmTCllZpcZjvnKTGs3x8x851duj5mZIcob2fGL3T/499uJZyWP5ht9+kYBCncDkB2SCQIoUAImdB5m0iJHkKa2GR5xRHRECzqy2aD5sCuOd4aHiEy19DKTFBWXEF1za7rXTXb8jB/ytfDCX/2+AsC4HcRUOkRuCCIkQUE0roChBGtdXAs6Fu4IqkljoU0ljDEVDBo1WZVzLpE2aCTlT3oD+xYNj90KQLwTc3ZALmyMxk7BcCmYcz0AzDmUnBLJNLmoum1y32Q6OqTQZP5CKQqKAl/UecXxy3CThM1kNWipf4OumRo2U1RTDZupqpkeNi2qmRs2bWFTUc2csGkPm0Q1s8MmVU0HT1oX9Azd64w8bsHNH5seedBm6PTEh72O9PqcSOU/E63PkT4f9DnaJ/xd+bt/9zqy+MPyD8ndrJLcfT8p20P2snH82cNeup9V0lJSBvghMLm2QDTke6AFTIsiTkKQSTHEeejkccTZeUkcYLYaFEg9nCTVvCHMrcptMCNuKI/j4tbFbbBZ/RCC8hguw/B6fH6v22a323SPoefJNqs9Ex2rrNh0r2H4/W6r3d3SJ7hnrz1//tVTe08889OcCZWVM7adf/Pcg3vOfi7Sb7ZNnb2MrBg8p7Dba2cOX7Jee6fhjy+tvHnmqCFVJb1ePn3qzYznns1497K0c1kVAEgwqfZraYv0AqSAA5qCHypgEZilRWZ5UT2PYsgNdAxLlEcNYjwKajQGgw8Es+JcAwHH5qETLIgby1WDHhpXgAyPz93SbkOsep7hjeL0eqNVIP9lTHKRzEmHdu0+dGjn7sPHunfq0LV7h47daMbhnXWvenbo0ql7x47dmLCSvrRSvDNw6uSa3oETJwLthg9r37v9iBHt/3lj9amTgT5rTpwMtBsxtGOfdiNGtPujmzivGwjQpvZr8WesjxPZUAYhMK1F/0qJXHRyLXWOAx0H50dxboQfxapphKtHGVUGHf1gc6PC6GkIo0NCsYGDIdUo5n9yHFb8Uz0qpyqHT8qpyOmZI4w2c1RTC1d7tc4anqdBGhkdmshNVo7GA2MF8+opFMrXcvAt55yfJNbVj8SKVhCJpBCfz+vGL5mK0yVjQRtLLX1+osicbALyzY/jkdK22by5e7c3z+x5acqYSaSkScEL3Xs8T9l3/Qc8NvUqY+SjNsv87OFG3YpXpZYUzytzDe7coy/ZsiQ4Yuzd/U688NSmCXd17sZub3v7oC2fjfhCGltW8VnjxjpZZy+dWjwpIJwormzTK79/iW/wBAAgqGEiyZKzQISGiQpWr1h4SISYUkm57FNqBQIBVkr3y8NAQ+3D36A4IWQV/JmZqJw2NT1T0Q3QAqTsQblg41NPbiqQH2Iv035kK206mGysZG3YMSs7xtrMDAyhTcjWSC4axqy4LiZRQdFdvnTNq1KX320HjVawZx6SCzc8/UKgUH6QtKPt2PKac4MDleRlMsxKBpFXpq4ZVBNmKyIxHbSvMAF1NBWyAQPW6z3nEIpfMhe2fL8kuIX8TClDEQQX6cwueUmTlNNpRPey/31uR/D0LuH14ccWkqFs//wTw9hv00gu+7IyEr8T3Cw2Ex+EZHAAktOEiPrIJO5s8hWcNqema06vU3PT02QFW/8NW0tWfSM432N9SfA9chuP5WOfkxnwHUgggyki+HwUXGw8M+65u8v3uexl0v7FyJpdaRIdRN8AAdJ5nYKQIGi4CB1U8zNNoUnPR3X1LjTb4EsQYnsMWACwJO6xk7e4bT/99GX0N7R2ndAo0jMzAOfHN02cnKkT94fv09bvr5QLAD8UpuJ51ev0rCK6SgOc3gCn19OKL9lADWokUbkS0ldBzwNNU8HdEjRXVGu0qPKIei288y5jBN59h9Cfl8yfv3jp/PmLaAn7hF0izUgO6U0cpAW7wD7NP3vy5Fk2o/rUyQeieM4C0DcRjwS+aHYSJiRhdokFkVRTjNUkvr1gffj25dM3f2ZXqEN85awnGncAgOhB3A1hQDSuhqG06+MGs+MEg0I21x4BImqiqcGk+kF0sY1xoc8M45pOL4mpgk13GVCnJSTTKXr+KSPXFgybNz6w4msqEctn537ZcSt7XKC7j1Bp9YE+E9bvXiU/S5K+eGzlJwfYcRkI9MM9smOuzWDV/+9pGmaYlnq9hLYFMjf0Fje13Izl5ntACdyDxkxTg0pcymnYlcImJDTWkK0ZcHQO3nrRBvWETcbdrEfVuA6VHa2IuhjrtnyGTjYeWzR1zsyJK7+iMpFevcjmTVuxkH176VX2rUy/Wls1d+3ilceELgtnTJs/d5R85OMrL40+Xdyiev7Ln15+Uh6/ZNmc5Qsj/CwFEIfj/jeANOgFJknoJonXwOrVZBeho02iBmkcTDlsEq4XIUsyjQo+3p84FpvOj7aLuIlTcynCvocf/qlml0xn/1WziWySrVR5nj1BOt4mXPlnKO1Lm0d5sxb3wsB8cmFylDcEVyexVFLRSeV8JAmXnJAllfClLUX8xpYRRhu0x6VoUYM5CS4WP7Qol4xGbc5ACRJ8Pr8v3WalWOW2FIsc2wbl3kECqXmlRfO5Xd/44pfPn2a/S/TjFRPnLl42d9J4O90m5J9jt9zYlFL2x6eX2A/nn5Us0xftWbf+UPvWQGEBYukSOQMu6B+nMDE0VnSsHA0kECeUCrz7ItigIy5ra0J7xQK3tGcqRoQsNh92U8w/JhEZmLktBoMe7bO7rLB0epebg632jH3uY/bP+ffYx6T9mVGBvNsWTF8WkF5wOh7Pcnz4lOJvxb4//z77iJSSLGJH3RhW06N96dRHXn5ww7qD0f3pDCC6cX9ugKIoomQEkXw9VczkxNMLnBCUCoruT0/3oxKL7r/NJmk/p7m+evWfGuE78Vt2lRns9N13kx40+4fnAD8CjMf6NcP6ZYKOq42NrmfDJWy4Xj1P+cEsSLLxkhUklCwkOAq4oqQVOOpuIs64nGxq0JVQz7ij5o27pAixmy+WM/67KC2ZsngH++XyNfbLtqVTF/36ykt/vrFletWG9bNnbDTmjRwzc/aYUbPF4lnHCwofXvLa5cuvLXm4qMWx2c+eP//PkRkbN1TNWrWa/j1u+eJJExcvjpzFAYg3s44vfRL+t0nkS3xjCynWFA5OSSRLynVkyecXVH67ol5PpINovJ8YLr/dnoHXLW8MFxXW7i3ZMSj8I0l96SOSyi5/3XNvxxtbB5aMDNy4dsmE9UtPPfNIx46difLpNfI/7DL7kp1g37C3GjV6NCeL/NStbO2ps2c2bD4CALW10f4qDgYDNPymcCtU8R4uYw/H8WnY1+/HcReOEKGKyJDmBj5OcRwItIUhwnqhFpJw9xFg6CkFlTYXTfVqZdf/tfIcAE0d79/dG2EECYYQQBQCAgoialiVLVpbFypuAUXFWRzUvVBcrQv3nv11zxCpv9pqh6DW0Up3ta4uW6uWCra1So7/3b3wfBfR//rVcsl7+ZL73nffffs7HTFBR5D3WpvCDmUdIQb1I01myQTjoQl2MRpRl/r3hG4oVpCF83Vw+kdwei2j93o4WagRrjD/Nw7YgU6IrsgAfQGRcYCTLxUZur5kPuL/lYuuNgU1XoSa+ueEfPon+J1yrD1J7UCC+5VG3BHBHVHcEcUdlSGKO3nPyzABMdyNFOv48MTEyEXCyPp9KK85NAqGGrz6I7y65gckiwz3dgAI+xivtAIDOA3LqyxbS9V3By2ZYgWxj1KxdrMPUEhIZKJWxzrtdWqXG6lJNABmTO6TO6EgZ/pvgvDn0c+vb5z6WEvxzh24q2xeXq9VAwomDR8q2098/X7JuWGdhg3GY64xvHvgZPkLaR2wgixCI1vHWKJpbdGx3G7mDCO77O7d6Eeg+9T6IJEoXP9qW0dDeSvNbVsrcjvaUN5aC9pa0c2ZWrhMKvyhjOgmkGUyEsFkpRLVKsh0dyc2B5YQICBgIe/NBCIEGNktqHxMBISRCV+50v3qzz2L/GNX5i4ra+5/7cXJK/oKktUtLnpWmZsBf4zfwZ/i9d7NYU+YMLgiIyLr7Gi8AA/zaQ6/hPNgCdx2D3ukdEseEwlhjDkuaOZ8eO9b/PGA3n2za6oggAlxCaLjSGGvi6/CKXAHfhxvwhtxbhtLaVQsrIM2+DLywL6O+mUrO6a7GfRIcPf8hNHZAIBE7VQd8ASDAWfec3ESdiGTC5nSGsiiwiLUtMnjuEOk1kzFcI9JHoR5kz0Y+SwCsXdhGH0VKhzHp/+FzFeRz9+O7fCtL2Q4AL8u2e72RcFosiLP9wIgHmY+hxmEgGJg84/lVDxnGtpH+FMziw5T/GGx/Sx9V+NPbS1/uvSGcm/t5vGnTEK3rUG9y6yEYO1+tfpYOon3TSpILhmHhztfw/bCn2qhobiwdDW+fQN/CjstfKZ4Dj4A9dOWrFx2S7KdOD56V0TLD0s++Qptwe2eLpq+6O1Jo56aACCYSGT3GbIfW4Kuj9KLgIabbN50LDdy1C0P5CSL2U+190OAThfGG/zHkIjP1Tfgj2ByPUSwrYiu7925+a0D27bugj/KF/F1OBh6QhP0gEPxrZ/ljc/fsONrFTee28R4g67DL2Qd3IERJIOHLwGln4cGSUJdTxdyhgDi1AKL4NMYAdkLvyXzDscv4Os/X3r77Nm3JRt+Ef9xEdfgl8Wb97668d7lQzcAZDjMIDh4glxAaHWfDV1JZj/rSS1tOuz1hHmUcIAjHG+MklgeL6F9LCbnn+jtWIJ+rI8SzjpaowWoDFuPSrZKXAiAE5+ZjCY9wHwiifwfvmXsI9wJMhnuBBn3B5CRXWYPc85tcJTWCd84gtBCVOTYSOfNYvNOJnxzgfBNCMgDJG7zSAeR2NXUTWzOuYmcC5VObFq7NxloMKYVZwDIYliIk59EGoTQ8FMi1WHihc7472r8D34dZmIIYUsBXXXbuXHroZP7iteG4MvI91jOCtgbusEO5K+347Q8e+MPb+JPbT/Gt4ZtDjppKBnYmi4D3IJyT8WxGL/UbqKsmPH2vW7kQdLd4LSKMre9bogIAvLe7u0GiyvOul0mNypGuE2h989SwFg6lJAPH3RNyQJYyWiVDLWO6XV1aHWtQn/HIrSI4vwGGfYxf74lFwHn0WS/ZYX76uoIKFu35IbrwlVyYQCxLpa96kTTx3OvJq5zuRfv5Pnw7hyqq8P1Z75rABK6Pm/yyAWS7d6fZ34//7k8f/ry4ka6xjKbeygnyTXR9CbFOhNBTIUiJtZlQleZiHWo4RgPKCvqPoxRivhqEFpQ55fr6lbBkzDE8TtKxt+gmY6VhGRb0QTHkw6dul8oThJo+wjtwodgwulWsMINaHf91LqjZPMpvyPTOJQPmKOhI8f8PFG13EQvVGfduUdgdUUc7AqJkgqDxNrKgaMhs+eobTNFT+700efrUV5FO30KebG5Uc8EWtlONUbCMKgzknfwPPyXDJ+HyXX+Mu77L9xf9q8jy7JPHHm3L/wDzYL3tomF0LEaU3YHPO9P/D/xPpFcNlR9sDfKQ0VIyDvYAkWjZCRQzAmOFb5urd0QeRq30fSlk1sX8kKZEurossFEhcHnyoTDl8u1YiS69x3B9zwSWwMExpGYerP/TAzKwmQIe+FjUFIzXI7/xHfxIdgdStAT9q2tfHHfu+/uf+kjNJB8sB+OIDdl6AFH4n34L3Twt98O4jvvXP/tEFB10nkWhzCCLoBffFVBMRMFCoqJUu7Jo9qcQ5WQhel6UVXuFrihDj12C/rgmlv4Xfj4imeeWYHfRW0c30q2f05/8nfluilTqH6k9PKT+hJ6GYEFpCu4GMj0BlevUyth7YJ7K4qXwVBu5hBhkW1IDMiHUy53QO1z+HbC7IyHkG/FrwOur4fAz/Q/oGEDoWEgCAODHkFDdtGcXDTnCMq5zh4tAL0r8H4kpavGhqLpIBNRJVTz83QOvA09Zkyd91RIxN025kVT8WEYuGH50hX4HMp1PC/ZLpyZ9q+OkeWL52TMDTFb1nadMXVp5dSnJy9Q9tJwohNfko6pURM+HNWSXLSkiJtbsnyG2TXfxfFwS0N5+AN5LeLfk+CaalbRx3ANsgkVK167jf+BYVf/gGESurZtzbKynQeu38YXb/6EX5bQb+9sXLEFzhw+vX3GF6/ZfsL4bXnqqum5OZM7pl96/eA3tz6Xly0pAhAEAyCWMjs8lpcL/M4jdosEtVlJxXhgirkUP1GHnxBHE/PJKN6sVGi0nNDoFpObCZzc5HQCL2Jc1JAPCxfF+1idfOgj3sJVDXfxqbrX12+xS7b6DrXYAcVbQnV9h+07dmwXqum83gBIErOT0h6ti1Svgj5NhjuVyQPgGCjm2X0hcx7M1kRooc4DKgqUA2AuFBx3fnH8AwW4oHC0GH+3L9MPbQCQf2TPuZTjaH4+bo9y+oEPGxL9IFfbfYkSzHAPk61ylpwjE4wKyA1qmgtMS6QQLWHPpkMRHYZTpdFCH61HFGtTIrRCc6KRuj30nxUBCMOOwggIr9bgFy/iizK+cAm/VAOXIklse+9LnYfY9m5f0XTvOnueTgCIvzM9MZCzvDVYu64bu9CRCx3brjqoeDokgUJH8jwTKfoEd3emyyzq/2glwTUEZ8DP8AVcRf5dgafIVSthCwp0tHeEojDHRXQJfU7X1YvgdY3g5QZ6cnhpZn/AMhdEigqdGRClC7oCqqHAaIAYNrITG6pOLWguHAm9sa4We0NvdANV1WdjiPTC83TuIWTuaYynHgfcdA+1JewiQCzqxW0bu7vEwj/M0IinwRkTnIPu3PsFfeeIFu4ePbpNHFi5Qdk/S/FhFCSvBTrQmuaUyJS8Jc8JFaXYgdrxKOiFF/B4uE2q/ueVI7rPld8ykZxQQWNOCMVqtyP5KmUV0w008gZRM18weD0Rhy865yaANFUl8m6WjsuY0hgTKbXQ00qBl16S195pf0QeDCCIR+eEeMWP421XpZaC+eZCZJgOCp/C6Ndg1Ccv6GU9Ooe+cbSFuxMSGC5CQ6awjXnnQZr99YDpJtEo17b6ScLmDz5g3+srHkZm6TgQWX5HiRfY3yJDRTCIBYg47TQ3EguI536ZvstWkibUTqdDOh28yXA/rXTQWwwWY0Uhj6GeaEHmKuxAUC8ehqKsxkeh2AeEgGiwWcE2gGAboOcEjmscwUumaSUSSa34wOusF7ELa7zgtAz3Eq8yr71eb3mJxRXZXiO8iEdB7xAOrvFq8ELFtgBOj9h9A2RmQvMxZC8X7WKJUKJJLHRs5YNnVN+bw2mwVVE5gqeXj9DpX4WvvH3n+yNj8nJG/QZ1dZVHfm3u67iSu9H/o4mz+7XtE9lr3Jvbdr81YuDIvunyouMfVuDgrHnJb+Ym75vQPe1JgMAiQpME2R/4gGAwUKMtfbWiT8+rG16i0GSJiTelgngLhgXJdNQ9YHkGH0Vr6nz8lGBEwsWThZs7+Z+p67Q67/TFuukL+xWFBE/OWVgM/7mJL/fPXi37O17q1oPIn/pXqp/IwJ0zu5dvpTzUj/hQf4p91JiJYsfrtbKdZ0SWuhGqaWbNl47lZtcYt9XsR7Q4IgYJjeapCp5GttOHzr2AJNzwdk1DQ01lnYguzsh/trj4jQnZ8rYLMO5G2HUY/+Nb8tD5J7aEbT9G+S2H0FbgacuI5qslp57XMbyF+N/R1mhgQUdaSBWpROetTo9c8c9zLp0csspad8Y/bkPBiUt1Ty/oPSk09Kke82eiZlCAqd27oJx/fl3eKxuG3thi75IKv03J+uxltleGEtreEbOBH8E9T4O73nV7BAEdZeygWHtZEPGuS4LKSMkHZ1u7BNV0LmSXQgEhNzCTBJTJoqM8wQKmAuEQs4Xmn/pexTXQ+8x31xx5SF41b9TqzD6pp/YPm94MwTcmmGDMjTY3YCLEf18ukxY/3yFmb0IPYV/ZZClgXCmAIAoAdF6OAWYwABCWeJDuRnJhdH0qSmjIJwC9ubggrebyI0KSVbDRzapJptHE5dkXXqi0hT0RE+DbMSg7+8IFYXnFwgNHPT0Oi/KwAQsr6udSGg/APUU3xr/RYAxwRc2F4HpyofdwXgSSi0CKp54PAwby4oU8RZsm2CVRiSCw7A2LuzXFOgN+OFmw0ep/CuOb2f/uEZeyvvfSudZVw078UDdrQZ9JltBJPRfMIVyEYFpOnzX3jn/2U0z4B8Fh02ZMycwi3LT5QGYqPJ+c9flLAAJilot6sg+MVD+rvgO/CzihojXInKuh50RKgiIQw3zY9lR82KkJO/Nf/6hu7Nju08Lr6oQ3ew0494OjCG1eVJwcV/8rmZ7x9ToA4BJywXI2Gq2nd/VxkMEmqbVesraew1m2uISWLYqdoftXAKAGG+4J15Lf9SZPmcFJI43RQ5aP2xlEDvmoczRX56C2taxZHx+WMFn77outO4c08+lkSut+k858b8WBSjf3o5Ju4DBxDkMDQLAYADGF4KGn/K5OzFVO6h8d63FDSqznvw/zwCtFtbWF0Ae2wjuJbXEVnsORsn/9UriHpBTszLZR6c3Hx3ybjo8RkrJ1YvkvIM8geyMcjNY8h15r53Kblhej/DZRLsLIRRgz4vk9E0xtHTPjKLMLX/nyPAbzveL3TZi4LaLT85P/daRuxIg+T/mjuoL8HuNakeVY03vAyJHDxl7+0TEdrVk5dUB3bz8PRxZas2zGY3H1V8XOynMtBED0FPvQvcA9F/covAK7n5yjFyIXDlRR5xHNbRa/v/CVI3WF47pPbU1w25WT98k5xxD04txx6Yn1NQwZRT/FEVx8QBhIcsFGTR5TDerHW7bBfD1eIpnfTJ15HWHaSFrPaCZsm0jj+ZEEIx1RQ0uX/3xt6bJlS3/5ddnSurTUJSXpGRnpi0vS01DkrZ07d+6oNd3eQXzEuj1jRo8es8e0c0xhYeEOhuMiPJLiqNWhbIk5TuCkhwdvrPxP7RPK1+Ym7ZO4S8dz11rrPvGP21jw8eXaBfN7TQwJmdhn/jz4zw18qUuGo046/0yvvrgSO178IrMzNj+W+u/NjL54pFDvxL3/o+S7qvI9XLj4kYir0pyg/hDln7/OGnSsrtMzg5ny7zEuNHR890bl3+fJJXcjkJyaRpX/weQkeCch9auXnXsPvUPw9gbdAC82VEWkd42p6g022CjAKkbAKTSA6g71itCIdMpo5y5DO8d3HxFYd8nQdvEAvwiDMEJMSXQYxM67c/J1EoDUThfOkvkjQZnGItW7xm8EFr+pGCpMEIjZPVNYTl6U6qGKF5sdbEbu6ZsFkRf7oGbEWTA1g9NYcIenqJmL9dhCq+1DQ4kTIoQaQ1Fe09EfZ12Ha/SHJYETrYxp0JWRS46euHr4+DUS+hk7dEju4GVnjt069sVtGf0gLsrNHwsjknoEtd1a+syHlevkrJHZjz2WFRi1femGg9+ulvMHPaHICnPDdbRAygRm0E/jU1M6qIUsetcINl/YRG1cN+6BaXWTL5V4PtRMUfjFrLgcVKv5wDePHu3cwTfCJzB4UPvl2154QcrE/1Q4Xs16TCfbfYy7X0aDKqBOwW8ekR8eYmcmy3iGVrU37zloTa6m9Hq4ExGrEzGqaYVQ666xb1bV5uYNmRVa9+WeQXmXfkMrHLPWFqenCM3uHQcQhAAg/EnwcAddeCnGMS/v4iESE0etEalOtqIslINICfNI5IwrKdEZK7zTXDZ+cw8v+gIvvAcnDxmCztw73ijHwwGQqsmFASzmrAiNNqUXTdsBD5j5Is07sMBWhiedOQvSvINEyw6IL27vRWtW8nRFOsLTQbp2OppBJ7ds0FkqxxAWInU0nW40G61ikvzKNfztiasI/nQCf3vtDfn7cpgEBXjvOPrRw8PRUuzs8IDobwCBBQDhJnkOT1DM8RgnXR8VT3LXeTir9kC1PZy65WPp4EuHAWSgnwjVdCSRpmgZ5h3sIQ+TJ8rMTzdSM0IQ6IjEj6EZvw7z8Y3PPsO/wXzy3hedgE87rjku0speFIbMCu0NuKdQT3A2gWGcVNVUOel5VtNwAhWxRkrug0pIkSz8KEjQdON5kfIBwU7W2GGJNN74i798E3rgjOhdZa26hbTw6qDvkh3QBs+C7tD+FLp9L3TaPr0biTgMSx4lxgBIdBYQqihv8nvkPxKbKiWFSetRqOOa0OPo0b3om6odCn2S8Da0Xk4FrUBbQMtjQCxNiWa70doHMnC1gmadmyKjnVH4eJaHZzLBpInSo4LKF0aMGjXihcoOo/oNGjx4UL9ReFviH6+dHj/dPn3i6ddqEldbXp5/evz+mNj9Y0/Pf9lC8XgT18KBD611htTiG/jSS7hWfl/BuwXBe4YG71axNj+Ctx/FmwxaWW3Xmf0Y3uYEBV+GPlspiq/VFKqg36IgZ2he3tCcgg5HX8wfMyb/xaPfUTwn7GsXvX8SxXN1Ys1rpyeShxh/+rU/EhU8ZsAl4gUhFgSARGAzECSaqly2GfjqJxb7JTdtAXRHKva7oocjFffQaU1csC0bvD4ncUj7lAGvvr5i0Na+CYNikweh37d+mdm9fbtxT/ht+SSra4eooh6Kv1KGV8JSsTPzV6IYFVUxpqc6EFC7nBb1y5oKa01zVSn1UvBKoQrC60puxFNokCJAGJio8cU4ueUaM/GkG5iObmz0uO+xEG2ivTBV0zGQjuUtm4isKF0/LLjCuoL4+MqTQ+deQsIH6z/+6PTpjz7ecVBAlxoDLNLiMy2v/xoMIz8Pq4ZtQq583/KbLVJjoAUS7QjEiSTfEwoKwH0R4JpG0O4m8ih2i8SqZC2x2gwVLZGw0AIbe4CvhX7s62otmglX0S1oJYwXSSgcyRsDZrIvf5FiotBX9REesbHSczvdf608+5OIrhcNHDTKHS5DQ4r7b+t89KhXef7cyt/P3jxnlycULpn5e6Wy3nkNP0vZ4i1WsdoeECXPB1Uj+QLUmAe1Z6QuUik9TYxMdNpbiWa6jZVEoi+xGZvHxxGTF4mpvQ+NKXyn5+I1Kzpak+LXrVnbw1Yw0t5z/dpN1iRr7Kq19bNrXnu1pubV12ompXbJTF267tleB0YVHsreuG59Ykpq0qb1W/v8e0xBec8169G8QxhDdOgdCBqUPRQIgPg+2ft+YKqyJn7kEfy4TGIzrUFJVYm3UYi2Az3d2OQ9DfWSwWZk7Gfk61bkaqYa6VjeTHPfw5k0sJiUf6SlTvkHLegpmAW98dPQF++Go/HuOrwTFpK/YDwNGoQOaJEjofLpyps3yYBOsbV4hsivIqW/ka4F4KuM7FDZezDWLsmAvpNiK7ylYAnRsnCy/ajF+8zPP/+Ma4UW9T8LH6O/AAK5uLW4mvCqldjWs1hni+qb0t80u4c5c5Kp2tywOVWtjHexYe0dwpSuLK5Nyt4ysQO9G0Z788hYHt1kpTJXru5s1yMjTW6KvHkbzgLTyntzAgUXVw/tn9UV1/zyA/6UGLmvzp27evl7tT8P7p/VBRqv/g71JMe5ekHp0rlVt392fBLVJzwxfv7R+MdDElOegSfyVkZ1Wlnw1vFT52U4d/Lo3r2HJWW8++aw1e06rSp45dPLJ+XC5YW9Bw2K63KonUdAM9PAzkOHJxpMnn4DH+tboOyT58WfhDnOtWnFMjCwmppROrVc1VtHDH5E+YHsUon8CXNqa3HQrVviT2fOnKEZi8GkruEHqQq0JPomHsxQ+DSGLEVMI2tayYWV7juLeJ/HYkjht6hR15ZISmox1u4ZaVFaRu0GT5G8KzeKfIWeqFkgkXaTskI9ZvO6+BTO6vtwpV2H9e4ISvKfjeIgJNp27ztyZN/uchFtGjYsv7Awf9hQhzcc/OdtOBi/cvsv/OpcuAe2gZFwDy7A5/G3eBQaIG/d/eVbs974eu9mOX/gymmzn342Z+QyfAdvhROgG9TBcXg7yVknQxvui4/hKtwH2mkfAqoQfFiNWTR4i1Zf30+dUJ4tkWnqhg4hZKCKCFSz9IemXlYvs4phfaz9sp4UZQXrY/WouCJdn61HJJdyRn9Bf0NfrxfzKjz1LfSImI/6gMZ0iforzMmMaFzfDPcPI6ojrkT8EUG+BSIMEWjaQeVamHaQXodECMWEvk1lVCKbzqigkW4egmVKn1mlrzz3bPJjXZ54Acqvrl6+W98Mr7BOav5Mj5zO6KgpNjA2de7EKbOtaZlxsV7yqNK1y/Fx65Co0s5hEzLaR8coteujwAxhlrAJRIDqvy4BHaiGXRsuAQhK4EzhqBAOJNCccm25IPBZQponO/qxY5mQBWdC8TX2W86+NCTTqlwgqnzrCcygE0gGa/jMNl9j4i1y/q5Jw4MB3ibW8BtbUR1wJYDk3FqYvFlzEVmlFiTdZg1oQS+tseX+mm+F+luVNmFbdDWpvKZNSJ1FbVhCw6dGDf8qpR9+TZV+RDZ2JQ12Zdm5WoaGh7fCgK1vpianJeo8drqLWb32lHXN71NQis7xPAtTXHj6DfyW0H9ZSfKw4KCneia1zTQZTP2iErp3XZ6a+ERnpq9WSM2FfCZPDLSLievSpGuS72iLvpGa76Gyp0SwoVXSMUb/ni60d1flz1l3wugfuJ91RySF6U52ByBD08vBtwwrkQRNF1HJzqJJ27dPKtq56sk4a/fu1rgnxXcm7907efKOHZPjuz+ekNCjB5OJIxquCXWSB8HLG3SluoWL4hHF0WQXpV3ycle0l82LU6Z8eyUkI9pFl+IbvAOO/QaG1x8RsoSVJ/AMuOoEXHT3chWl41NoJ/pKOgECwRjXrgKVMm8B2ssAYLGS1Z1C34XQevFAzV5H1do2A/SQTj6CFWyqy4CkjtBXjv2wY0Yba0JqxttIfn39qp0FsxcjmI92rocg4fG27ZJSOsjj1pfO6DdzwmQZQDAKlaHrJCcdBT7URBoJ7uUy0liItFCCjoHqA10OJE/wViD1UwLJAwXTyyl0KKNDOh1q6AfZdGhQgOkzk2+Uh2qkZFQosyiiyP6LgsUHY6PSo7KjBPKVKMJK3lHBUURmXo6qiSIC8gNyq7ytZlv6to2i3w00KAHtTk0QRY1SaRsB4+H+zNTMtPh0SqPSza93T328Z8XmFYdk9Ha31Ixe3bvNE5+O7xAZ3y5UHjV71uTE4QH+I7pOnT9nqhxtjYtJSlyi2HuzST7/cWc+n+rCdJHab3RooEO2SLP5IqULeVdBE/VE3rxFPxpBB286XCYf2cD9fD6gpQACaxQw05Q+9EK45oh0XMb1bM4NJDYczOIAOeAh4XMuDuDhEizjC328XZtzNEEopkJYjBguHVMweErLusu6mFk9U0dH1JJQyqaXZqemCM3vHR8Un9AiCKdJ5xWapAEgTGU1ia01cdQHGhUQUFxwstVCAW2vsvigBTnXsAMK1+DjyA0Kn52F0t2+7Df3of5wg9BFkVNC7H1yKXYO3FBbi/r/ocxfhDPhSQLpDTowf9pNZdipLAwgcnHCZqLWl3AyS6RiGibCNM+MQa/u1qX17NY/REjw7N937Jxn28W0ay2tUuYajLbDLUQmSqAH3wf8P9j3XHewTeC82LD4cLjlwxKYjrajki1mJudmEXuknbMeNQOQFeREsL3Eg9ojdAghA033uB7p8D89p2HW4T17jhzevffIW0MG9h8yNGfAYHHmpvfe2zR986FDmweOGzdwes748TlMR08EW4VVAjE8wGd+AOjAZ3Aqu28DQLpMdHUkOA+Gom3k9XPoD4heAt+gdwEABo5aBB/lOzKQqhhsOHBr/C75zjkhmn6Hr2pk3ykm39klnWDfOcu+840wi3XNfQsMaCf9juposO8ABEbimcIXYmfWA9YDEEl9v/NL///p/JJZl5eye6xO+zaOdYPRQ03Q6yh9ct9h40f3m45+E+CfH35xfcO0pGDS+oV2r5ubm/1sTsGkXNb6dZi0fnUcPhjuvsZsKqUnSReKIkBr9mRZ0APmAndwwEsSxWjySCqMRYWZCT+CwymMwRWmuwpTBV6BQylMM1niYUarMMfB6/ApCuMtu/yOlwozESyHecCbzEVhaCzIi4hiLe5lKuwxmAEPUFiTRGFNylEwzLdp+AsA3WDJxnLJW7iqz0c1PwiiMxRkHyHAPJdOFrsnkJ2+CSCtMNpQpw3wLrTAl2vINGVgL6LueAodcslAO+gF8o/aB0b2By0k/Dy4fqE39ngHXyJ2wRXHXB/U2vGTL9p69yac00JS2rmO4fHHcAIchxZAoOwbnEr7nghdIgDdN3PhkYZ6cp/197C1bqOsNahqXGuZ0V+F6a7CVIESZR0NsguMlwozEQxvXCPZZY0avqC9HGzOdsqcDUuUOSUJNf7eGwCghTqLCjMTJCn85abCNJwjMHMZXgpMVUOagpebrMK8T2A2MrwUmIkNgQpeDIbWKUmN/ABaKzWzTN7Nf8QpC3ZBAk4WuExYoOKscFkgWjZdoL1PAlXFArUjhGABFZcjQSP9q12LdCSuL4haW4GN1S5q05bRonZtERvxyPbt91u3WmEHa966BAW0/lU0Q23hQutxR9bChfswmit9D2yfdXTus98b95nOSSul/0CXSGA6Ofe9H5xGYYIkDx4mQYWZCT+BUylMsCtMrgpTRaT0ZArTSnaBma3CHAdfwMXsd1xhQlWYieANWEzXLoTC2EIMtpbOtYOgN/hauCEuB55ExgYQx8K/QoBG2lEismMPdGykUSsjhIkQmiHUQdgbpuCqTTAZpmzCVWzAx+BTsAvssgW/zwb8/haYiT+gcwgEn/2kP+N3EADCCRUH8B0HfPywPR/ADtWGjNqH0sBbcGh7+tJWeYlmN5XWDVbER+ND1LdjiWdqJEDiyJmhEum2EFMhEvppGjr6b0wftKk0bwztSih47cn+m5b0GVjfM8wiwzux07vtexdV+ptk7BOZH9/Y59G69YaLA26XKW0KJAp5acD3i/Dd7BWxUBjWpt1vB1OLomD9wRYtfjvE+IfVsbO1SHLyhlnZs0bJna2XCmNRYWbCT5U96+cK012FqSJ6dCiDkV1gvFSYieBNZc8yGJsfkZSqvGf10GzOFOec65Q5vSSFrwECmwjMQtaXZQLZfBU+Z5raIfBwRhrdPegOp64d5OpAbO6urpuPVWlfoQU7Rh+ntQ9X/FULvfGt2r/q6v5aQf6TbPjXusqqWvwleReOA1eNHb+G8e0z5Fl3ysEgEgzSSBxfrhrFtbVGLzUaB/4avgrxkZh7SZqqXZrrGt1dky8wcQVPccQMbvRf4Nzav069+t1M2PX8sf6vRHRsOy8tLx+/t3BE+vApYrcrd//9xrSzaV3xTysrKkKDjgW0yeneC5rWD/y8Z9+CTcuUtWB1v9IVshZdnbpkMQika9FODmBrocJcVmFmwiQQQGFiXWBkyQkjg6oUM4Vor1MgwH0YiwpzPC2K/coDMNJpFWaifwvKRR0oDD1eK6ZaO19vFadj4DMwjULGyxQy3mBLdsoZAcQ1XJeXin1Ae/AY6AJOc9XNmkO9Hl3qLLBSZ3s6CKYrlh5bUZJelk4rntOJ3shOH5GOpim3iitq0hvIC1GeTRc624PYiy2dO6GGapk2fLdtrOaSRKut1bTztDNfH/rwCB5LcPB1o5p4HmwsIRWvLj2Tlfz15opjt375NG9Q3qRrSK49Oem1pPSXx3x9wzFEEFevGrWw35OPnaqflrWh7ZmiucOFjPHTPRA8OM40NKfHqAM79rzeffi4YZnN5TWHumSkZ+G7P62Rl+xv3/6FmF6Hnux4ZFS3zGz0S9kMqdWEUrbG/XAqrU0ma/e4065JY3YNq6uVvif3n3Dy4hLQgnJIiFPfqTBXVJiZsLPCr2EuMLLMYBgvpvlTiFCdAgFUGOmMCjMxMIhyT2sKY2ttsFkUPmugzbeljB8/cto9Y4HE7B7VXgFlAKAC6ZQTRgYzW4hai4bZT4cJTJ70B4NR7B4LQAxKp9o9+wnMTOmgCjMRO4AMvBmMq92TQvi/j3QTWAhX7wSkxJivPAgOIiaNV5BOqc637/Uil4AOJq8ges8Um2EONsWa0k3ZphGmKaYSU5lpr+kt0wcmT+IaBpkoTEis3dcUwvReiIm+AF/K+zQS1lbD1AavtvRDczBLGepcm9r8CAv6Aqf3TjUjCTpLkYnxEVSi0fwbDceQK2fh/uJRk/CX3/+IL0GfSwO3xon6/hn4dp/vLL0jew7Y1uVsH9x8wfaw9eMWbtwq6SfgG/86ewcfhwHVP0BzepyUvztlS9E82aeVvsqY1X560b3U6n1LO2RUPDvnTbpOrL6QyZ9+ivwZyuSPWSeq66TU/TH+6u/kwT0Kf7WWFSgV5rIKMxMOVORhpAuMLDEYxoNDmTyMeGAu2aLCHB/O8Il8EJ/TKszEeCYP21AYWxuDLZxxhEDwfFVMFA+ynI8nSOXPaFOsVLGaNeOowQRAT5aiXs9U2vvvxgd1w6k1S/7ExHq9cBsvpqly9PiXH1y8d/simY/gNZPUHh7m7Cq+1oQZWa52lcDbVa14u4pdqXaVkTCMakpRHlKNLOtD7Koc6H41fnTME+vGDx+F//6lw7CoJ9aNHT2+rmUrGUb4x7cqWQDrA/1lfNm3fUBJCYqshfFGnw1f9LhWZrqNP/FutuFs9z+29FnUBqIhnl4nd3ad2RY67G5uJ/Yoa8FquthaDHHyxm5FFphkN7ZiKswpFWYmHACYNPB3hfmDwTDeGIIYhI5BaOc6qMJMjGOSgMHY/Gk9gfJbrN6HzZfrnM9fmS9QNjXaUitJLDDtv+tj+U/ViTbdx5Km1InWdVozvOkyUd07jje6dOfrRNXnY3TIVehwl9EhUEeejgZ0zYz/IZXBrBaEr6XWN11LXUpLxBU5WthwXdeDnYMVTmxOEgvlDxhRQ6KPbjD35jxE+wgj9SppROAseUfz8768ojfzRcP+XEUJX0Nssaj9zdSxUE/ckNRiVpqq0/WoX5y7OAvXEx8oEwrd1mYLs+lJHPRUjnsF1sKO8YUd9x6o8PCEPaEH7ADdYS+9eyUurMRWX6LykmS3Tyrxp1WfAra3CU0QsZdCQQdiMc3WnJb1yMYQ/ribBGCk+iCBGEoJZQkoj3tmwB8aF1FNlUqM5k7HatW4UVpgmjZoIBeSVG0aadjiM5mZJxb9iv8mEmHxycyMD6fxLTL3xs0vLSkpWVyyQLjT2C0zetjwUTCuzkSkQuHw4YXaphkUuff4CVJ7ffLkTjhG7Z/ZSfLsKcS3dAOhLMuO+Cz7QW9dsC5WJ+Qpx3GSbIOORGytQkpl2dqPoFuZWO+/alXgHwoflooDUIR0geXNOrL8lKCWDKcL2c7yXe/7kWAiAhovms6OUeKVzhs6eM6cwUPnTU6OjkpKiopOlvwGFBcPGFhUNDC6c1JMTDKEyUpPgfi10E/6GxhBAmAlU9qZ3KtpqMtLe8ugXngprh1kk6s1XQwHod/sYd1fsEYmLJk1LOlAXESSVD1i+dDMmLD8VUMz2jM59xIqEn8WOhJL8KvzIMeaweJIqEhy3rOBsWMzKH5dhL/hcCLDJGDQ1GL6siZQo1UwhXV5blbKRfEALMQ73iPw3YQ7MF8Lz/Yqg4fKCaf59AvSIPwczK0CgM2B78Lh0Is/C5WIi+E7F6Zc9MVXoTv0IPhRXNDz5LcjwEkmc0/CJwEARpceDp3q7xJc0FsM/hSDPwX7MXjed/RQbbsuDWa0HYYCiXCDO8WEfRbO0JbYCAc8NzXla9iNjk/iT2HkT+fIGHsBKP4pbEBdhTvAi3CmXfAQol0j+c/MLhw7Z/bYwjmCJX/O7BG9R86YOYLmJ8FWZBUOApl8L4Bsa39ahRoG46EVpvz9Er4CQ15CEXgaXG6Ey+k8Awh8CxVeovBGaIJhRuEeDMFXXvr7b+EgnmvEc2EZXEfgY0CRME2KBAJ9KhDLjqJLjITmV+lhzUXsEGb2/OmogzCIyGQP0Ayk8/H8+31HdllydzbjeAoaycJYVSmq9XIelUkrnSKhVfCJFNCXpaVV2CrCMyer5NvC7G0221Q0w3EAPonw2/SZehK/4AqZOxqUgvsh/wfKsaIjSTlWbDQ7EI2zs/T8YQOAnupMYMhR53bvSHqcDhlskbyrZ6omd+jR5y1cjWeLSa1CZ3KQGGTsLw5om+os9J+wC8ftWPbY1DjfpHlpN/F3G8h/MOxmyvQs34RpSUu3wzM4Dp6BJ9HUV318jnkbYIuPUOWiSv1x2NrgfcJgPFDcrHKRwj97UJHwvdDx4Wf9Ct/T/DYqqlLWyx8A0cz6CFuAyY/qJNS2HjWpPfzJhf9/oseQqvkjL7xw9ewTa3PD02Y/XjT2q6/QuLo60muYW/llcMuTphYFBbmk17DRDugNgBAuWAjPGUA3Dc81d00lIHeRsh2KLYfajLzBeVarnnGeN8950Gz1idShA8XFH+DRHvDFD/EY4bysh6Hr16+fjoKwLEET8mW0H9XwJ7outANRYIsmz95cSznFHnsw726PCmymSZE7s+FqplxJkudpE+aPzpTbHw+GeeStNg3/n82ew3OPzp4zmQTQV4QegaCPpmai+QNnHf+vqyMs/4fqiIfURgwGAG4hOEogRiPTmzd1zjOZnmuXVFO4LIGr5mQsak5mJpzXmKNT8jb/Bbts07oAAAB4AWNgZGAAYen931bF89t8ZZDkYACBIx8E9UD0OZEzun+E/l7lLOKoBHI5GZhAogBOMQvyeAFjYGRg4Ej6e5WBgdPoj9B/I44FQBFUcAcAiWcGPQB4AW2RUxidTQwG52Szv22ztm3btm3btm3btm3bvqvd03y1LuaZrPGGngCA+RkSkWEyhHR6jhTag4r+DBX8n6QKFSOdLKaNrOBb15rftSEZQrtIJGPILCkY6jIjNr+KMd/IZ+QxkhjtjAZGRqNsMCYRGSr/UFW/JbX2oq9Go427QIyP/yWbj8I3/h9G+5+o5tMxWscbE6xdmVp+DqMlJzO1Bclt3mgtwOiPxcbmGI2o7KObO5lzmD+huI7lb9+ATv4Hvv74B6KY4+kdvtQ1FJG4dHCF+dH8hatOQjcCJwPszsXs7l1oo/HJa86vKSgqu4lmdQGjpXxPH/k1PEfj0DaoP7ptc7vQKphrtAksG81RySdb+NnazfUr/vEPiGj+1/jGKCizSSLCLPPvPi8Nn/39X/TWlnbvheT1IympZ/gt9Igueo8S+hcTPspAYdeXBu4c5bQmrYO/f9Z3nM7uM1prdkq7stRw5Sknc2miy+mn35BK0jFGvqGmJLS5k2ls66t99AVzPqpkHKWehigT/PuH+Lhj+E6QRZDDSyRneH+Qg/moscqXIcLLDN5FM5DTN7facniTZzlsY4Bepkvw5x/io7UkeJaDZfAm8lt4kfxGb/MKY6wuI8UbGbxNX9JrV7Pl8BZBDoPpFjjY6+MFVPw4OfndJYbLPNq5I7TxnZn8UVtmhEaSzsgYWK4ZN8gox83b6SL1qCFVKeBGENNNJbXmJLu2Z5RO4RfXnZyuEuVcQZsTn8LB3z0FW2/CPAAAAAAAAAAAAAAALABaANQBSgHaAo4CqgLUAv4DLgNUA2gDgAOaA7IEAgQuBIQFAgVKBbAGGgZQBsgHMAdAB1AHgAeuB94IOgjuCTgJpgn8Cj4KhgrCCygLggueC9QMHgxCDKYM9A1GDYwN6A5MDrIO3g8aD1IPuhAGEEQQfhCkELwQ4BECER4RWBHiEkASkBLuE1IToBQUFFoUhhTKFRIVLhWaFeAWMhaQFuwXLBewGAAYRBh+GOIZPBmSGcwaEBooGmwashqyGtobRBuqHA4ccByaHT4dYB30Ho4emh60HrwfZh98H8ggCiBoIQYhQCGQIboh0CIGIjwihiKSIqwixiLgIzgjSiNcI24jgCOWI6wkIiQuJEAkUiRoJHokjCSeJLQlIiU0JUYlWCVqJXwlkiXEJkImVCZmJngmjiagJu4nVCdmJ3gniiecJ7AnxiiOKJoorCi+KNAo5Cj2KQgpGikwKcop3CnuKgAqEiokKjgqcCrqKvwrDisgKzQrRiukK7gr1CxeLPItGC1YLZQtni2oLcAt2i3uLgYuHi4+Llouci6KLp4u3C9eL3Yv2DAcMKQw9jEcMS4AAAABAAAA3ACXABYAXwAFAAEAAAAAAA4AAAIAAeYAAwABeAF9zANyI2AYBuBnt+YBMsqwjkfpsLY9qmL7Bj1Hb1pbP7+X6HOmy7/uAf8EeJn/GxV4mbvEjL/M3R88Pabfsr0Cbl7mUQdu7am4VNFUEbQp5VpOS8melIyWogt1yyoqMopSkn+kkmIiouKOpNQ15FSUBUWFREWe1ISoWcE378e+mU99WU1NVUlhYZ2nHXKh6sKVrJSQirqMsKKcKyllDSkNYRtWzVu0Zd+iGTEhkXtU0y0IeAFswQOWQgEAAMDZv7Zt27ZtZddTZ+4udYFmBEC5qKCaEjWBQK069Ro0atKsRas27Tp06tKtR68+/QYMGjJsxKgx4yZMmjJtxqw58xYsWrJsxao16zZs2rJtx649+w4cOnLsxKkz5y5cunLtxq079x48evLsxas37z58+vLtx68//0LCIqJi4hKSUtIyshWC4GErEAAAAOAs/3NtI+tluy7Ztm3zZZ6z69yMBuVixBqU50icNMkK1ap48kySXdGy3biVKl+CcYeuFalz786DMo1mTWvy2hsZ3po3Y86yBYuWHHtvzYpVzT64kmnTug0fnTqX6LNPvvjmq+9K/PDLT7/98c9f/wU4EShYkBBhQvUoFSFcpChnLvTZ0qLVtgM72rTr0m1Ch06T4g0ZNvDk+ZMXLo08efk4RnZGDkZOhlQWv1AfH/bSvEwDA0cXEG1kYG7C4lpalM+Rll9apFdcWsBZklGUmgpisZeU54Pp/DwwHwBPQXTqAHgBLc4lXMVQFIDxe5+/Ke4uCXd3KLhLWsWdhvWynugFl7ieRu+dnsb5flD+V44+W03Pqkm96nSsSX3pwfbG8hyVafqKLY53NhRyi8/1/P8l1md6//6SRzsznWXcUiuTXQ3F3NJTfU3V3NRrJp2WrjUzN3sl06/thr54PYV7+IYaQ1++jlly8+AO2iz5W4IT8OEJIqi29NXrGHhwB65DLfxAtSN5HvgQQgRjjiSfQJDDoBz5e4AA3BwJtOVAHgtBBGGeRNsK5DYGd8IvM61XFAA=) 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 new file mode 100644 index 00000000..9f46d478 --- /dev/null +++ b/plugins/UiConfig/media/css/button.css @@ -0,0 +1,12 @@ +/* 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/UiConfig/media/css/fonts.css b/plugins/UiConfig/media/css/fonts.css new file mode 100644 index 00000000..f5576c5a --- /dev/null +++ b/plugins/UiConfig/media/css/fonts.css @@ -0,0 +1,30 @@ +/* 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,d09GRgABAAAAAEbcABAAAAAAfQwAAQABAAAAAAAAAAAAAAAAAAAAAAAAAABHUE9TAAABbAAABOQAAAv2MtQEeUdTVUIAAAZQAAAAQQAAAFCyIrRQT1MvMgAABpQAAABXAAAAYLorAUBjbWFwAAAG7AAAAI8AAADEj/6wZGN2dCAAAAd8AAAAMAAAADAX3wLxZnBnbQAAB6wAAAE/AAABvC/mTqtnYXNwAAAI7AAAAAwAAAAMAAgAE2dseWYAAAj4AAA2eQAAYlxNsqlBaGVhZAAAP3QAAAA0AAAANve2KKdoaGVhAAA/qAAAAB8AAAAkDRcHFmhtdHgAAD/IAAACPAAAA3CPSUvWbG9jYQAAQgQAAAG6AAABusPVqwRtYXhwAABDwAAAACAAAAAgAwkC3m5hbWUAAEPgAAAAtAAAAU4XNjG1cG9zdAAARJQAAAF3AAACF7VLITZwcmVwAABGDAAAAM8AAAEuQJ9pDngBpJUDrCVbE0ZX9znX1ti2bdu2bU/w89nm1di2bdu2jXjqfWO7V1ajUru2Otk4QCD5qIRbqUqtRoT2aj+oDynwApjhwNN34fbsPKAPobrrDjggvbggAz21cOiHFyjoKeIpwkH3sHvRve4pxWVnojPdve7MdZY7e53zrq+bzL3r5nDzuTXcfm6iJ587Wa5U/lMuekp5hHv9Ge568okijyiFQ0F8CCSITGQhK9nITh7yUkDxQhSmKMUpQSlKU4bq1KExzWlBK9rwCZ/yGZ/zBV/yNd/wLd/xM7/yG7/zB3+SyFKWs4GNbGYLh/BSnBhKkI5SJCVR5iXs3j4iZGqZyX6nKNFUsq1UsSNUldVkDdnADtNIz8Z2mmZ2geZ2llbyE7X5VH4mP5dfyC/lCNUYKUfJ0XKMHCvHq8YEOVFOkpPlLNWeLefIuXKeXKg+FsnFcolcqr6Wy1XK36SxbpUOLWzxg/tsXJoSxlcWgw9FlVPcTlLCLlHKtpAovYruU/SyIptJlH6ay0K13Upva8e/rYNal2OcjWGB/Y2XYGIoR6SyjtOOaBQhXJEQRS4qEvag51P4ktuuUEzGyjgZLxNkAD4kI1AGk1Ets6lVSjaQjI1ys9wig6iicVaV1WQN2UiOlxPkRDlJTparpIfqRNGUGFpIH8IsgQiZWm6SW6VGpMxiMlbGyXiZID1ksBk0tasa+REcgrWbjua9k1ACbC+aMyG2RGONorqd1Ey3KvsMmr9WKUGrtEHZP2iV5miVZrPN5uFQXa21FgShu/bK9V7HCz4/+M4nBcnA9ltfW25z7ZKNs3G89bp3io+47JSdtbHvkX+Ct+dcfK7+Bdtpf+h+/o1trsvLQPQzsat2+pW5F3jvS5U0lhdi522PtbA9L6zn5efGkM/y3LsGAHbD/g22Tyv213N1GtoduwmSRzWG2go7BIS/cix/ameH20SbZFOJQFgyAFto4y3STgLhds2m2LIn+dtsB9i2JxWyA9hJ9fuNXeLF+uvtiB0DCWES6wxgl+WMN6zPWQDCnu6j/sUmGs+LuV1spo2wdRZrE4gkiiiLfNTvJRtgJ9RHpMZ/WqP4FIBQVAv5Qp3L2hFe3GM7/qa/5BWxg2/Iv/NsW7UG7Bzvdb0p326+Inb0PesfeLf56q+7BkDEK/LaAQBJXldHI9X96Q6+dVSX3m8mGhvy7ZdDbXSCE0YEqcn86BTP/eQUL0oxdIZTEp3iVKIyVahGTepRnwY0RCc6LWlF61ee4rHEEU8CiYxgJKMYzRjGMp4JTGQSk5nJLGYzh7nMYynLHp34m9CZz1YO4ZKfMOEQIRxSC4fMwiWL8JBVeMkmfMgtfMkj/Mgr/CkgvBQUARQVgRQTvhQXQZQQwZQUIZQSoZQWYVQS4VQWEVQRkVQTUdQU0WjmujcQMTQUETQWSWguktJSJKOVSEprkZyvhYdv+A4ffhZefuVP3WPRaUeiCGUEYwlnvIhkApOJYqaIZhbziGGpSMoyEcFykZRNwmGrcDgkfHDkP4WQhQ3EQBDE9pmZ+m/pK4ovGh2DLW8Y/0wRrZ3sTlWy/Ut6kPnlj7St3vzVJ3/zxZ878t9iVrSeNZdng1ty+3Z0tRvzw/zamDuNWXr9V2Q8vEZPedSbe/UNmH3D1uu4Sr5k7uHPvuMCT5oZE7a0fYJ4AWNgZGBg4GKQY9BhYHRx8wlh4GBgYQCC///BMow5memJQDEGCA8oxwKmOYBYCESDxa4xMDH4MDACoScANIcG1QAAAHgBY2BmWcj4hYGVgYF1FqsxAwOjPIRmvsiQxsTAwADEUPCAgel9AINCNJCpAOK75+enAyne/385kv5eZWDgSGLSVmBgnO/PyMDAYsW6gUEBCJkA3C8QGAB4AWNgYGACYmYgFgGSjGCahWEDkNZgUACyOBh4GeoYTjCcZPjPaMgYzHSM6RbTHQURBSkFOQUlBSsFF4UShTVKQv//A3XwAnUsAKo8BVQZBFUprCChIANUaYlQ+f/r/8f/DzEI/T/4f8L/gr///r7+++rBlgcbH2x4sPbB9Ad9D+IfaNw7DHQLkQAAN6c0ewAAKgDDAJIAmACHAGgAjACqAAAAFf5gABUEOgAVBbAAFQSNABADIQALBhgAFQAAAAB4AV2OBc4bMRCF7f4UlCoohmyFE1sRQ0WB3ZTbcDxlJlEPUOaGzvJWuBHmODlEaaFsGJ5PD0ydR7RnHM5X5PLv7/Eu40R3bt7Q4EoI+7EFfkvjkAKvSY0dJbrYKXYHJk9iJmZn781EVzy6fQ+7xcB7jfszagiwoXns2ZGRaFLqd3if6JTGro/ZDTAz8gBPAkDgg1Ljq8aeOi+wU+qZvsErK4WmRSkphY1Nz2BjpSSRxv5vjZ5//vh4qPZAYb+mEQkJQ4NmCoxmszDLS7yazVKzPP3ON//mLmf/F5p/F7BTtF3+qhd0XuVlyi/kZV56CsnSiKrzQ2N7EiVpxBSO2hpxhWOeSyinzD+J2dCsm2yX3XUj7NPIrNnRne1TSiHvwcUn9zD7XSMPkVRofnIFu2KcY8xKrdmxna1F+gexEIitAAABAAIACAAC//8AD3gBfFcFfBu5sx5pyWkuyW5iO0md15yzzboUqilQZmZmTCllZpcZjvnKTGs3x8x851duj5mZIcob2fGL3T/499uJZyWP5ht9+kYBCncDkB2SCQIoUAImdB5m0iJHkKa2GR5xRHRECzqy2aD5sCuOd4aHiEy19DKTFBWXEF1za7rXTXb8jB/ytfDCX/2+AsC4HcRUOkRuCCIkQUE0roChBGtdXAs6Fu4IqkljoU0ljDEVDBo1WZVzLpE2aCTlT3oD+xYNj90KQLwTc3ZALmyMxk7BcCmYcz0AzDmUnBLJNLmoum1y32Q6OqTQZP5CKQqKAl/UecXxy3CThM1kNWipf4OumRo2U1RTDZupqpkeNi2qmRs2bWFTUc2csGkPm0Q1s8MmVU0HT1oX9Azd64w8bsHNH5seedBm6PTEh72O9PqcSOU/E63PkT4f9DnaJ/xd+bt/9zqy+MPyD8ndrJLcfT8p20P2snH82cNeup9V0lJSBvghMLm2QDTke6AFTIsiTkKQSTHEeejkccTZeUkcYLYaFEg9nCTVvCHMrcptMCNuKI/j4tbFbbBZ/RCC8hguw/B6fH6v22a323SPoefJNqs9Ex2rrNh0r2H4/W6r3d3SJ7hnrz1//tVTe08889OcCZWVM7adf/Pcg3vOfi7Sb7ZNnb2MrBg8p7Dba2cOX7Jee6fhjy+tvHnmqCFVJb1ePn3qzYznns1497K0c1kVAEgwqfZraYv0AqSAA5qCHypgEZilRWZ5UT2PYsgNdAxLlEcNYjwKajQGgw8Es+JcAwHH5qETLIgby1WDHhpXgAyPz93SbkOsep7hjeL0eqNVIP9lTHKRzEmHdu0+dGjn7sPHunfq0LV7h47daMbhnXWvenbo0ql7x47dmLCSvrRSvDNw6uSa3oETJwLthg9r37v9iBHt/3lj9amTgT5rTpwMtBsxtGOfdiNGtPujmzivGwjQpvZr8WesjxPZUAYhMK1F/0qJXHRyLXWOAx0H50dxboQfxapphKtHGVUGHf1gc6PC6GkIo0NCsYGDIdUo5n9yHFb8Uz0qpyqHT8qpyOmZI4w2c1RTC1d7tc4anqdBGhkdmshNVo7GA2MF8+opFMrXcvAt55yfJNbVj8SKVhCJpBCfz+vGL5mK0yVjQRtLLX1+osicbALyzY/jkdK22by5e7c3z+x5acqYSaSkScEL3Xs8T9l3/Qc8NvUqY+SjNsv87OFG3YpXpZYUzytzDe7coy/ZsiQ4Yuzd/U688NSmCXd17sZub3v7oC2fjfhCGltW8VnjxjpZZy+dWjwpIJwormzTK79/iW/wBAAgqGEiyZKzQISGiQpWr1h4SISYUkm57FNqBQIBVkr3y8NAQ+3D36A4IWQV/JmZqJw2NT1T0Q3QAqTsQblg41NPbiqQH2Iv035kK206mGysZG3YMSs7xtrMDAyhTcjWSC4axqy4LiZRQdFdvnTNq1KX320HjVawZx6SCzc8/UKgUH6QtKPt2PKac4MDleRlMsxKBpFXpq4ZVBNmKyIxHbSvMAF1NBWyAQPW6z3nEIpfMhe2fL8kuIX8TClDEQQX6cwueUmTlNNpRPey/31uR/D0LuH14ccWkqFs//wTw9hv00gu+7IyEr8T3Cw2Ex+EZHAAktOEiPrIJO5s8hWcNqema06vU3PT02QFW/8NW0tWfSM432N9SfA9chuP5WOfkxnwHUgggyki+HwUXGw8M+65u8v3uexl0v7FyJpdaRIdRN8AAdJ5nYKQIGi4CB1U8zNNoUnPR3X1LjTb4EsQYnsMWACwJO6xk7e4bT/99GX0N7R2ndAo0jMzAOfHN02cnKkT94fv09bvr5QLAD8UpuJ51ev0rCK6SgOc3gCn19OKL9lADWokUbkS0ldBzwNNU8HdEjRXVGu0qPKIei288y5jBN59h9Cfl8yfv3jp/PmLaAn7hF0izUgO6U0cpAW7wD7NP3vy5Fk2o/rUyQeieM4C0DcRjwS+aHYSJiRhdokFkVRTjNUkvr1gffj25dM3f2ZXqEN85awnGncAgOhB3A1hQDSuhqG06+MGs+MEg0I21x4BImqiqcGk+kF0sY1xoc8M45pOL4mpgk13GVCnJSTTKXr+KSPXFgybNz6w4msqEctn537ZcSt7XKC7j1Bp9YE+E9bvXiU/S5K+eGzlJwfYcRkI9MM9smOuzWDV/+9pGmaYlnq9hLYFMjf0Fje13Izl5ntACdyDxkxTg0pcymnYlcImJDTWkK0ZcHQO3nrRBvWETcbdrEfVuA6VHa2IuhjrtnyGTjYeWzR1zsyJK7+iMpFevcjmTVuxkH176VX2rUy/Wls1d+3ilceELgtnTJs/d5R85OMrL40+Xdyiev7Ln15+Uh6/ZNmc5Qsj/CwFEIfj/jeANOgFJknoJonXwOrVZBeho02iBmkcTDlsEq4XIUsyjQo+3p84FpvOj7aLuIlTcynCvocf/qlml0xn/1WziWySrVR5nj1BOt4mXPlnKO1Lm0d5sxb3wsB8cmFylDcEVyexVFLRSeV8JAmXnJAllfClLUX8xpYRRhu0x6VoUYM5CS4WP7Qol4xGbc5ACRJ8Pr8v3WalWOW2FIsc2wbl3kECqXmlRfO5Xd/44pfPn2a/S/TjFRPnLl42d9J4O90m5J9jt9zYlFL2x6eX2A/nn5Us0xftWbf+UPvWQGEBYukSOQMu6B+nMDE0VnSsHA0kECeUCrz7ItigIy5ra0J7xQK3tGcqRoQsNh92U8w/JhEZmLktBoMe7bO7rLB0epebg632jH3uY/bP+ffYx6T9mVGBvNsWTF8WkF5wOh7Pcnz4lOJvxb4//z77iJSSLGJH3RhW06N96dRHXn5ww7qD0f3pDCC6cX9ugKIoomQEkXw9VczkxNMLnBCUCoruT0/3oxKL7r/NJmk/p7m+evWfGuE78Vt2lRns9N13kx40+4fnAD8CjMf6NcP6ZYKOq42NrmfDJWy4Xj1P+cEsSLLxkhUklCwkOAq4oqQVOOpuIs64nGxq0JVQz7ij5o27pAixmy+WM/67KC2ZsngH++XyNfbLtqVTF/36ykt/vrFletWG9bNnbDTmjRwzc/aYUbPF4lnHCwofXvLa5cuvLXm4qMWx2c+eP//PkRkbN1TNWrWa/j1u+eJJExcvjpzFAYg3s44vfRL+t0nkS3xjCynWFA5OSSRLynVkyecXVH67ol5PpINovJ8YLr/dnoHXLW8MFxXW7i3ZMSj8I0l96SOSyi5/3XNvxxtbB5aMDNy4dsmE9UtPPfNIx46difLpNfI/7DL7kp1g37C3GjV6NCeL/NStbO2ps2c2bD4CALW10f4qDgYDNPymcCtU8R4uYw/H8WnY1+/HcReOEKGKyJDmBj5OcRwItIUhwnqhFpJw9xFg6CkFlTYXTfVqZdf/tfIcAE0d79/dG2EECYYQQBQCAgoialiVLVpbFypuAUXFWRzUvVBcrQv3nv11zxCpv9pqh6DW0Up3ta4uW6uWCra1So7/3b3wfBfR//rVcsl7+ZL73nffffs7HTFBR5D3WpvCDmUdIQb1I01myQTjoQl2MRpRl/r3hG4oVpCF83Vw+kdwei2j93o4WagRrjD/Nw7YgU6IrsgAfQGRcYCTLxUZur5kPuL/lYuuNgU1XoSa+ueEfPon+J1yrD1J7UCC+5VG3BHBHVHcEcUdlSGKO3nPyzABMdyNFOv48MTEyEXCyPp9KK85NAqGGrz6I7y65gckiwz3dgAI+xivtAIDOA3LqyxbS9V3By2ZYgWxj1KxdrMPUEhIZKJWxzrtdWqXG6lJNABmTO6TO6EgZ/pvgvDn0c+vb5z6WEvxzh24q2xeXq9VAwomDR8q2098/X7JuWGdhg3GY64xvHvgZPkLaR2wgixCI1vHWKJpbdGx3G7mDCO77O7d6Eeg+9T6IJEoXP9qW0dDeSvNbVsrcjvaUN5aC9pa0c2ZWrhMKvyhjOgmkGUyEsFkpRLVKsh0dyc2B5YQICBgIe/NBCIEGNktqHxMBISRCV+50v3qzz2L/GNX5i4ra+5/7cXJK/oKktUtLnpWmZsBf4zfwZ/i9d7NYU+YMLgiIyLr7Gi8AA/zaQ6/hPNgCdx2D3ukdEseEwlhjDkuaOZ8eO9b/PGA3n2za6oggAlxCaLjSGGvi6/CKXAHfhxvwhtxbhtLaVQsrIM2+DLywL6O+mUrO6a7GfRIcPf8hNHZAIBE7VQd8ASDAWfec3ESdiGTC5nSGsiiwiLUtMnjuEOk1kzFcI9JHoR5kz0Y+SwCsXdhGH0VKhzHp/+FzFeRz9+O7fCtL2Q4AL8u2e72RcFosiLP9wIgHmY+hxmEgGJg84/lVDxnGtpH+FMziw5T/GGx/Sx9V+NPbS1/uvSGcm/t5vGnTEK3rUG9y6yEYO1+tfpYOon3TSpILhmHhztfw/bCn2qhobiwdDW+fQN/CjstfKZ4Dj4A9dOWrFx2S7KdOD56V0TLD0s++Qptwe2eLpq+6O1Jo56aACCYSGT3GbIfW4Kuj9KLgIabbN50LDdy1C0P5CSL2U+190OAThfGG/zHkIjP1Tfgj2ByPUSwrYiu7925+a0D27bugj/KF/F1OBh6QhP0gEPxrZ/ljc/fsONrFTee28R4g67DL2Qd3IERJIOHLwGln4cGSUJdTxdyhgDi1AKL4NMYAdkLvyXzDscv4Os/X3r77Nm3JRt+Ef9xEdfgl8Wb97668d7lQzcAZDjMIDh4glxAaHWfDV1JZj/rSS1tOuz1hHmUcIAjHG+MklgeL6F9LCbnn+jtWIJ+rI8SzjpaowWoDFuPSrZKXAiAE5+ZjCY9wHwiifwfvmXsI9wJMhnuBBn3B5CRXWYPc85tcJTWCd84gtBCVOTYSOfNYvNOJnxzgfBNCMgDJG7zSAeR2NXUTWzOuYmcC5VObFq7NxloMKYVZwDIYliIk59EGoTQ8FMi1WHihc7472r8D34dZmIIYUsBXXXbuXHroZP7iteG4MvI91jOCtgbusEO5K+347Q8e+MPb+JPbT/Gt4ZtDjppKBnYmi4D3IJyT8WxGL/UbqKsmPH2vW7kQdLd4LSKMre9bogIAvLe7u0GiyvOul0mNypGuE2h989SwFg6lJAPH3RNyQJYyWiVDLWO6XV1aHWtQn/HIrSI4vwGGfYxf74lFwHn0WS/ZYX76uoIKFu35IbrwlVyYQCxLpa96kTTx3OvJq5zuRfv5Pnw7hyqq8P1Z75rABK6Pm/yyAWS7d6fZ34//7k8f/ry4ka6xjKbeygnyTXR9CbFOhNBTIUiJtZlQleZiHWo4RgPKCvqPoxRivhqEFpQ55fr6lbBkzDE8TtKxt+gmY6VhGRb0QTHkw6dul8oThJo+wjtwodgwulWsMINaHf91LqjZPMpvyPTOJQPmKOhI8f8PFG13EQvVGfduUdgdUUc7AqJkgqDxNrKgaMhs+eobTNFT+700efrUV5FO30KebG5Uc8EWtlONUbCMKgzknfwPPyXDJ+HyXX+Mu77L9xf9q8jy7JPHHm3L/wDzYL3tomF0LEaU3YHPO9P/D/xPpFcNlR9sDfKQ0VIyDvYAkWjZCRQzAmOFb5urd0QeRq30fSlk1sX8kKZEurossFEhcHnyoTDl8u1YiS69x3B9zwSWwMExpGYerP/TAzKwmQIe+FjUFIzXI7/xHfxIdgdStAT9q2tfHHfu+/uf+kjNJB8sB+OIDdl6AFH4n34L3Twt98O4jvvXP/tEFB10nkWhzCCLoBffFVBMRMFCoqJUu7Jo9qcQ5WQhel6UVXuFrihDj12C/rgmlv4Xfj4imeeWYHfRW0c30q2f05/8nfluilTqH6k9PKT+hJ6GYEFpCu4GMj0BlevUyth7YJ7K4qXwVBu5hBhkW1IDMiHUy53QO1z+HbC7IyHkG/FrwOur4fAz/Q/oGEDoWEgCAODHkFDdtGcXDTnCMq5zh4tAL0r8H4kpavGhqLpIBNRJVTz83QOvA09Zkyd91RIxN025kVT8WEYuGH50hX4HMp1PC/ZLpyZ9q+OkeWL52TMDTFb1nadMXVp5dSnJy9Q9tJwohNfko6pURM+HNWSXLSkiJtbsnyG2TXfxfFwS0N5+AN5LeLfk+CaalbRx3ANsgkVK167jf+BYVf/gGESurZtzbKynQeu38YXb/6EX5bQb+9sXLEFzhw+vX3GF6/ZfsL4bXnqqum5OZM7pl96/eA3tz6Xly0pAhAEAyCWMjs8lpcL/M4jdosEtVlJxXhgirkUP1GHnxBHE/PJKN6sVGi0nNDoFpObCZzc5HQCL2Jc1JAPCxfF+1idfOgj3sJVDXfxqbrX12+xS7b6DrXYAcVbQnV9h+07dmwXqum83gBIErOT0h6ti1Svgj5NhjuVyQPgGCjm2X0hcx7M1kRooc4DKgqUA2AuFBx3fnH8AwW4oHC0GH+3L9MPbQCQf2TPuZTjaH4+bo9y+oEPGxL9IFfbfYkSzHAPk61ylpwjE4wKyA1qmgtMS6QQLWHPpkMRHYZTpdFCH61HFGtTIrRCc6KRuj30nxUBCMOOwggIr9bgFy/iizK+cAm/VAOXIklse+9LnYfY9m5f0XTvOnueTgCIvzM9MZCzvDVYu64bu9CRCx3brjqoeDokgUJH8jwTKfoEd3emyyzq/2glwTUEZ8DP8AVcRf5dgafIVSthCwp0tHeEojDHRXQJfU7X1YvgdY3g5QZ6cnhpZn/AMhdEigqdGRClC7oCqqHAaIAYNrITG6pOLWguHAm9sa4We0NvdANV1WdjiPTC83TuIWTuaYynHgfcdA+1JewiQCzqxW0bu7vEwj/M0IinwRkTnIPu3PsFfeeIFu4ePbpNHFi5Qdk/S/FhFCSvBTrQmuaUyJS8Jc8JFaXYgdrxKOiFF/B4uE2q/ueVI7rPld8ykZxQQWNOCMVqtyP5KmUV0w008gZRM18weD0Rhy865yaANFUl8m6WjsuY0hgTKbXQ00qBl16S195pf0QeDCCIR+eEeMWP421XpZaC+eZCZJgOCp/C6Ndg1Ccv6GU9Ooe+cbSFuxMSGC5CQ6awjXnnQZr99YDpJtEo17b6ScLmDz5g3+srHkZm6TgQWX5HiRfY3yJDRTCIBYg47TQ3EguI536ZvstWkibUTqdDOh28yXA/rXTQWwwWY0Uhj6GeaEHmKuxAUC8ehqKsxkeh2AeEgGiwWcE2gGAboOcEjmscwUumaSUSSa34wOusF7ELa7zgtAz3Eq8yr71eb3mJxRXZXiO8iEdB7xAOrvFq8ELFtgBOj9h9A2RmQvMxZC8X7WKJUKJJLHRs5YNnVN+bw2mwVVE5gqeXj9DpX4WvvH3n+yNj8nJG/QZ1dZVHfm3u67iSu9H/o4mz+7XtE9lr3Jvbdr81YuDIvunyouMfVuDgrHnJb+Ym75vQPe1JgMAiQpME2R/4gGAwUKMtfbWiT8+rG16i0GSJiTelgngLhgXJdNQ9YHkGH0Vr6nz8lGBEwsWThZs7+Z+p67Q67/TFuukL+xWFBE/OWVgM/7mJL/fPXi37O17q1oPIn/pXqp/IwJ0zu5dvpTzUj/hQf4p91JiJYsfrtbKdZ0SWuhGqaWbNl47lZtcYt9XsR7Q4IgYJjeapCp5GttOHzr2AJNzwdk1DQ01lnYguzsh/trj4jQnZ8rYLMO5G2HUY/+Nb8tD5J7aEbT9G+S2H0FbgacuI5qslp57XMbyF+N/R1mhgQUdaSBWpROetTo9c8c9zLp0csspad8Y/bkPBiUt1Ty/oPSk09Kke82eiZlCAqd27oJx/fl3eKxuG3thi75IKv03J+uxltleGEtreEbOBH8E9T4O73nV7BAEdZeygWHtZEPGuS4LKSMkHZ1u7BNV0LmSXQgEhNzCTBJTJoqM8wQKmAuEQs4Xmn/pexTXQ+8x31xx5SF41b9TqzD6pp/YPm94MwTcmmGDMjTY3YCLEf18ukxY/3yFmb0IPYV/ZZClgXCmAIAoAdF6OAWYwABCWeJDuRnJhdH0qSmjIJwC9ubggrebyI0KSVbDRzapJptHE5dkXXqi0hT0RE+DbMSg7+8IFYXnFwgNHPT0Oi/KwAQsr6udSGg/APUU3xr/RYAxwRc2F4HpyofdwXgSSi0CKp54PAwby4oU8RZsm2CVRiSCw7A2LuzXFOgN+OFmw0ep/CuOb2f/uEZeyvvfSudZVw078UDdrQZ9JltBJPRfMIVyEYFpOnzX3jn/2U0z4B8Fh02ZMycwi3LT5QGYqPJ+c9flLAAJilot6sg+MVD+rvgO/CzihojXInKuh50RKgiIQw3zY9lR82KkJO/Nf/6hu7Nju08Lr6oQ3ew0494OjCG1eVJwcV/8rmZ7x9ToA4BJywXI2Gq2nd/VxkMEmqbVesraew1m2uISWLYqdoftXAKAGG+4J15Lf9SZPmcFJI43RQ5aP2xlEDvmoczRX56C2taxZHx+WMFn77outO4c08+lkSut+k858b8WBSjf3o5Ju4DBxDkMDQLAYADGF4KGn/K5OzFVO6h8d63FDSqznvw/zwCtFtbWF0Ae2wjuJbXEVnsORsn/9UriHpBTszLZR6c3Hx3ybjo8RkrJ1YvkvIM8geyMcjNY8h15r53Kblhej/DZRLsLIRRgz4vk9E0xtHTPjKLMLX/nyPAbzveL3TZi4LaLT85P/daRuxIg+T/mjuoL8HuNakeVY03vAyJHDxl7+0TEdrVk5dUB3bz8PRxZas2zGY3H1V8XOynMtBED0FPvQvcA9F/covAK7n5yjFyIXDlRR5xHNbRa/v/CVI3WF47pPbU1w25WT98k5xxD04txx6Yn1NQwZRT/FEVx8QBhIcsFGTR5TDerHW7bBfD1eIpnfTJ15HWHaSFrPaCZsm0jj+ZEEIx1RQ0uX/3xt6bJlS3/5ddnSurTUJSXpGRnpi0vS01DkrZ07d+6oNd3eQXzEuj1jRo8es8e0c0xhYeEOhuMiPJLiqNWhbIk5TuCkhwdvrPxP7RPK1+Ym7ZO4S8dz11rrPvGP21jw8eXaBfN7TQwJmdhn/jz4zw18qUuGo046/0yvvrgSO178IrMzNj+W+u/NjL54pFDvxL3/o+S7qvI9XLj4kYir0pyg/hDln7/OGnSsrtMzg5ny7zEuNHR890bl3+fJJXcjkJyaRpX/weQkeCch9auXnXsPvUPw9gbdAC82VEWkd42p6g022CjAKkbAKTSA6g71itCIdMpo5y5DO8d3HxFYd8nQdvEAvwiDMEJMSXQYxM67c/J1EoDUThfOkvkjQZnGItW7xm8EFr+pGCpMEIjZPVNYTl6U6qGKF5sdbEbu6ZsFkRf7oGbEWTA1g9NYcIenqJmL9dhCq+1DQ4kTIoQaQ1Fe09EfZ12Ha/SHJYETrYxp0JWRS46euHr4+DUS+hk7dEju4GVnjt069sVtGf0gLsrNHwsjknoEtd1a+syHlevkrJHZjz2WFRi1femGg9+ulvMHPaHICnPDdbRAygRm0E/jU1M6qIUsetcINl/YRG1cN+6BaXWTL5V4PtRMUfjFrLgcVKv5wDePHu3cwTfCJzB4UPvl2154QcrE/1Q4Xs16TCfbfYy7X0aDKqBOwW8ekR8eYmcmy3iGVrU37zloTa6m9Hq4ExGrEzGqaYVQ666xb1bV5uYNmRVa9+WeQXmXfkMrHLPWFqenCM3uHQcQhAAg/EnwcAddeCnGMS/v4iESE0etEalOtqIslINICfNI5IwrKdEZK7zTXDZ+cw8v+gIvvAcnDxmCztw73ijHwwGQqsmFASzmrAiNNqUXTdsBD5j5Is07sMBWhiedOQvSvINEyw6IL27vRWtW8nRFOsLTQbp2OppBJ7ds0FkqxxAWInU0nW40G61ikvzKNfztiasI/nQCf3vtDfn7cpgEBXjvOPrRw8PRUuzs8IDobwCBBQDhJnkOT1DM8RgnXR8VT3LXeTir9kC1PZy65WPp4EuHAWSgnwjVdCSRpmgZ5h3sIQ+TJ8rMTzdSM0IQ6IjEj6EZvw7z8Y3PPsO/wXzy3hedgE87rjku0speFIbMCu0NuKdQT3A2gWGcVNVUOel5VtNwAhWxRkrug0pIkSz8KEjQdON5kfIBwU7W2GGJNN74i798E3rgjOhdZa26hbTw6qDvkh3QBs+C7tD+FLp9L3TaPr0biTgMSx4lxgBIdBYQqihv8nvkPxKbKiWFSetRqOOa0OPo0b3om6odCn2S8Da0Xk4FrUBbQMtjQCxNiWa70doHMnC1gmadmyKjnVH4eJaHZzLBpInSo4LKF0aMGjXihcoOo/oNGjx4UL9ReFviH6+dHj/dPn3i6ddqEldbXp5/evz+mNj9Y0/Pf9lC8XgT18KBD611htTiG/jSS7hWfl/BuwXBe4YG71axNj+Ctx/FmwxaWW3Xmf0Y3uYEBV+GPlspiq/VFKqg36IgZ2he3tCcgg5HX8wfMyb/xaPfUTwn7GsXvX8SxXN1Ys1rpyeShxh/+rU/EhU8ZsAl4gUhFgSARGAzECSaqly2GfjqJxb7JTdtAXRHKva7oocjFffQaU1csC0bvD4ncUj7lAGvvr5i0Na+CYNikweh37d+mdm9fbtxT/ht+SSra4eooh6Kv1KGV8JSsTPzV6IYFVUxpqc6EFC7nBb1y5oKa01zVSn1UvBKoQrC60puxFNokCJAGJio8cU4ueUaM/GkG5iObmz0uO+xEG2ivTBV0zGQjuUtm4isKF0/LLjCuoL4+MqTQ+deQsIH6z/+6PTpjz7ecVBAlxoDLNLiMy2v/xoMIz8Pq4ZtQq583/KbLVJjoAUS7QjEiSTfEwoKwH0R4JpG0O4m8ih2i8SqZC2x2gwVLZGw0AIbe4CvhX7s62otmglX0S1oJYwXSSgcyRsDZrIvf5FiotBX9REesbHSczvdf608+5OIrhcNHDTKHS5DQ4r7b+t89KhXef7cyt/P3jxnlycULpn5e6Wy3nkNP0vZ4i1WsdoeECXPB1Uj+QLUmAe1Z6QuUik9TYxMdNpbiWa6jZVEoi+xGZvHxxGTF4mpvQ+NKXyn5+I1Kzpak+LXrVnbw1Yw0t5z/dpN1iRr7Kq19bNrXnu1pubV12ompXbJTF267tleB0YVHsreuG59Ykpq0qb1W/v8e0xBec8169G8QxhDdOgdCBqUPRQIgPg+2ft+YKqyJn7kEfy4TGIzrUFJVYm3UYi2Az3d2OQ9DfWSwWZk7Gfk61bkaqYa6VjeTHPfw5k0sJiUf6SlTvkHLegpmAW98dPQF++Go/HuOrwTFpK/YDwNGoQOaJEjofLpyps3yYBOsbV4hsivIqW/ka4F4KuM7FDZezDWLsmAvpNiK7ylYAnRsnCy/ajF+8zPP/+Ma4UW9T8LH6O/AAK5uLW4mvCqldjWs1hni+qb0t80u4c5c5Kp2tywOVWtjHexYe0dwpSuLK5Nyt4ysQO9G0Z788hYHt1kpTJXru5s1yMjTW6KvHkbzgLTyntzAgUXVw/tn9UV1/zyA/6UGLmvzp27evl7tT8P7p/VBRqv/g71JMe5ekHp0rlVt392fBLVJzwxfv7R+MdDElOegSfyVkZ1Wlnw1vFT52U4d/Lo3r2HJWW8++aw1e06rSp45dPLJ+XC5YW9Bw2K63KonUdAM9PAzkOHJxpMnn4DH+tboOyT58WfhDnOtWnFMjCwmppROrVc1VtHDH5E+YHsUon8CXNqa3HQrVviT2fOnKEZi8GkruEHqQq0JPomHsxQ+DSGLEVMI2tayYWV7juLeJ/HYkjht6hR15ZISmox1u4ZaVFaRu0GT5G8KzeKfIWeqFkgkXaTskI9ZvO6+BTO6vtwpV2H9e4ISvKfjeIgJNp27ztyZN/uchFtGjYsv7Awf9hQhzcc/OdtOBi/cvsv/OpcuAe2gZFwDy7A5/G3eBQaIG/d/eVbs974eu9mOX/gymmzn342Z+QyfAdvhROgG9TBcXg7yVknQxvui4/hKtwH2mkfAqoQfFiNWTR4i1Zf30+dUJ4tkWnqhg4hZKCKCFSz9IemXlYvs4phfaz9sp4UZQXrY/WouCJdn61HJJdyRn9Bf0NfrxfzKjz1LfSImI/6gMZ0iforzMmMaFzfDPcPI6ojrkT8EUG+BSIMEWjaQeVamHaQXodECMWEvk1lVCKbzqigkW4egmVKn1mlrzz3bPJjXZ54Acqvrl6+W98Mr7BOav5Mj5zO6KgpNjA2de7EKbOtaZlxsV7yqNK1y/Fx65Co0s5hEzLaR8coteujwAxhlrAJRIDqvy4BHaiGXRsuAQhK4EzhqBAOJNCccm25IPBZQponO/qxY5mQBWdC8TX2W86+NCTTqlwgqnzrCcygE0gGa/jMNl9j4i1y/q5Jw4MB3ibW8BtbUR1wJYDk3FqYvFlzEVmlFiTdZg1oQS+tseX+mm+F+luVNmFbdDWpvKZNSJ1FbVhCw6dGDf8qpR9+TZV+RDZ2JQ12Zdm5WoaGh7fCgK1vpianJeo8drqLWb32lHXN71NQis7xPAtTXHj6DfyW0H9ZSfKw4KCneia1zTQZTP2iErp3XZ6a+ERnpq9WSM2FfCZPDLSLievSpGuS72iLvpGa76Gyp0SwoVXSMUb/ni60d1flz1l3wugfuJ91RySF6U52ByBD08vBtwwrkQRNF1HJzqJJ27dPKtq56sk4a/fu1rgnxXcm7907efKOHZPjuz+ekNCjB5OJIxquCXWSB8HLG3SluoWL4hHF0WQXpV3ycle0l82LU6Z8eyUkI9pFl+IbvAOO/QaG1x8RsoSVJ/AMuOoEXHT3chWl41NoJ/pKOgECwRjXrgKVMm8B2ssAYLGS1Z1C34XQevFAzV5H1do2A/SQTj6CFWyqy4CkjtBXjv2wY0Yba0JqxttIfn39qp0FsxcjmI92rocg4fG27ZJSOsjj1pfO6DdzwmQZQDAKlaHrJCcdBT7URBoJ7uUy0liItFCCjoHqA10OJE/wViD1UwLJAwXTyyl0KKNDOh1q6AfZdGhQgOkzk2+Uh2qkZFQosyiiyP6LgsUHY6PSo7KjBPKVKMJK3lHBUURmXo6qiSIC8gNyq7ytZlv6to2i3w00KAHtTk0QRY1SaRsB4+H+zNTMtPh0SqPSza93T328Z8XmFYdk9Ha31Ixe3bvNE5+O7xAZ3y5UHjV71uTE4QH+I7pOnT9nqhxtjYtJSlyi2HuzST7/cWc+n+rCdJHab3RooEO2SLP5IqULeVdBE/VE3rxFPxpBB286XCYf2cD9fD6gpQACaxQw05Q+9EK45oh0XMb1bM4NJDYczOIAOeAh4XMuDuDhEizjC328XZtzNEEopkJYjBguHVMweErLusu6mFk9U0dH1JJQyqaXZqemCM3vHR8Un9AiCKdJ5xWapAEgTGU1ia01cdQHGhUQUFxwstVCAW2vsvigBTnXsAMK1+DjyA0Kn52F0t2+7Df3of5wg9BFkVNC7H1yKXYO3FBbi/r/ocxfhDPhSQLpDTowf9pNZdipLAwgcnHCZqLWl3AyS6RiGibCNM+MQa/u1qX17NY/REjw7N937Jxn28W0ay2tUuYajLbDLUQmSqAH3wf8P9j3XHewTeC82LD4cLjlwxKYjrajki1mJudmEXuknbMeNQOQFeREsL3Eg9ojdAghA033uB7p8D89p2HW4T17jhzevffIW0MG9h8yNGfAYHHmpvfe2zR986FDmweOGzdwes748TlMR08EW4VVAjE8wGd+AOjAZ3Aqu28DQLpMdHUkOA+Gom3k9XPoD4heAt+gdwEABo5aBB/lOzKQqhhsOHBr/C75zjkhmn6Hr2pk3ykm39klnWDfOcu+840wi3XNfQsMaCf9juposO8ABEbimcIXYmfWA9YDEEl9v/NL///p/JJZl5eye6xO+zaOdYPRQ03Q6yh9ct9h40f3m45+E+CfH35xfcO0pGDS+oV2r5ubm/1sTsGkXNb6dZi0fnUcPhjuvsZsKqUnSReKIkBr9mRZ0APmAndwwEsSxWjySCqMRYWZCT+CwymMwRWmuwpTBV6BQylMM1niYUarMMfB6/ApCuMtu/yOlwozESyHecCbzEVhaCzIi4hiLe5lKuwxmAEPUFiTRGFNylEwzLdp+AsA3WDJxnLJW7iqz0c1PwiiMxRkHyHAPJdOFrsnkJ2+CSCtMNpQpw3wLrTAl2vINGVgL6LueAodcslAO+gF8o/aB0b2By0k/Dy4fqE39ngHXyJ2wRXHXB/U2vGTL9p69yac00JS2rmO4fHHcAIchxZAoOwbnEr7nghdIgDdN3PhkYZ6cp/197C1bqOsNahqXGuZ0V+F6a7CVIESZR0NsguMlwozEQxvXCPZZY0avqC9HGzOdsqcDUuUOSUJNf7eGwCghTqLCjMTJCn85abCNJwjMHMZXgpMVUOagpebrMK8T2A2MrwUmIkNgQpeDIbWKUmN/ABaKzWzTN7Nf8QpC3ZBAk4WuExYoOKscFkgWjZdoL1PAlXFArUjhGABFZcjQSP9q12LdCSuL4haW4GN1S5q05bRonZtERvxyPbt91u3WmEHa966BAW0/lU0Q23hQutxR9bChfswmit9D2yfdXTus98b95nOSSul/0CXSGA6Ofe9H5xGYYIkDx4mQYWZCT+BUylMsCtMrgpTRaT0ZArTSnaBma3CHAdfwMXsd1xhQlWYieANWEzXLoTC2EIMtpbOtYOgN/hauCEuB55ExgYQx8K/QoBG2lEismMPdGykUSsjhIkQmiHUQdgbpuCqTTAZpmzCVWzAx+BTsAvssgW/zwb8/haYiT+gcwgEn/2kP+N3EADCCRUH8B0HfPywPR/ADtWGjNqH0sBbcGh7+tJWeYlmN5XWDVbER+ND1LdjiWdqJEDiyJmhEum2EFMhEvppGjr6b0wftKk0bwztSih47cn+m5b0GVjfM8wiwzux07vtexdV+ptk7BOZH9/Y59G69YaLA26XKW0KJAp5acD3i/Dd7BWxUBjWpt1vB1OLomD9wRYtfjvE+IfVsbO1SHLyhlnZs0bJna2XCmNRYWbCT5U96+cK012FqSJ6dCiDkV1gvFSYieBNZc8yGJsfkZSqvGf10GzOFOec65Q5vSSFrwECmwjMQtaXZQLZfBU+Z5raIfBwRhrdPegOp64d5OpAbO6urpuPVWlfoQU7Rh+ntQ9X/FULvfGt2r/q6v5aQf6TbPjXusqqWvwleReOA1eNHb+G8e0z5Fl3ysEgEgzSSBxfrhrFtbVGLzUaB/4avgrxkZh7SZqqXZrrGt1dky8wcQVPccQMbvRf4Nzav069+t1M2PX8sf6vRHRsOy8tLx+/t3BE+vApYrcrd//9xrSzaV3xTysrKkKDjgW0yeneC5rWD/y8Z9+CTcuUtWB1v9IVshZdnbpkMQika9FODmBrocJcVmFmwiQQQGFiXWBkyQkjg6oUM4Vor1MgwH0YiwpzPC2K/coDMNJpFWaifwvKRR0oDD1eK6ZaO19vFadj4DMwjULGyxQy3mBLdsoZAcQ1XJeXin1Ae/AY6AJOc9XNmkO9Hl3qLLBSZ3s6CKYrlh5bUZJelk4rntOJ3shOH5GOpim3iitq0hvIC1GeTRc624PYiy2dO6GGapk2fLdtrOaSRKut1bTztDNfH/rwCB5LcPB1o5p4HmwsIRWvLj2Tlfz15opjt375NG9Q3qRrSK49Oem1pPSXx3x9wzFEEFevGrWw35OPnaqflrWh7ZmiucOFjPHTPRA8OM40NKfHqAM79rzeffi4YZnN5TWHumSkZ+G7P62Rl+xv3/6FmF6Hnux4ZFS3zGz0S9kMqdWEUrbG/XAqrU0ma/e4065JY3YNq6uVvif3n3Dy4hLQgnJIiFPfqTBXVJiZsLPCr2EuMLLMYBgvpvlTiFCdAgFUGOmMCjMxMIhyT2sKY2ttsFkUPmugzbeljB8/cto9Y4HE7B7VXgFlAKAC6ZQTRgYzW4hai4bZT4cJTJ70B4NR7B4LQAxKp9o9+wnMTOmgCjMRO4AMvBmMq92TQvi/j3QTWAhX7wSkxJivPAgOIiaNV5BOqc637/Uil4AOJq8ges8Um2EONsWa0k3ZphGmKaYSU5lpr+kt0wcmT+IaBpkoTEis3dcUwvReiIm+AF/K+zQS1lbD1AavtvRDczBLGepcm9r8CAv6Aqf3TjUjCTpLkYnxEVSi0fwbDceQK2fh/uJRk/CX3/+IL0GfSwO3xon6/hn4dp/vLL0jew7Y1uVsH9x8wfaw9eMWbtwq6SfgG/86ewcfhwHVP0BzepyUvztlS9E82aeVvsqY1X560b3U6n1LO2RUPDvnTbpOrL6QyZ9+ivwZyuSPWSeq66TU/TH+6u/kwT0Kf7WWFSgV5rIKMxMOVORhpAuMLDEYxoNDmTyMeGAu2aLCHB/O8Il8EJ/TKszEeCYP21AYWxuDLZxxhEDwfFVMFA+ynI8nSOXPaFOsVLGaNeOowQRAT5aiXs9U2vvvxgd1w6k1S/7ExHq9cBsvpqly9PiXH1y8d/simY/gNZPUHh7m7Cq+1oQZWa52lcDbVa14u4pdqXaVkTCMakpRHlKNLOtD7Koc6H41fnTME+vGDx+F//6lw7CoJ9aNHT2+rmUrGUb4x7cqWQDrA/1lfNm3fUBJCYqshfFGnw1f9LhWZrqNP/FutuFs9z+29FnUBqIhnl4nd3ad2RY67G5uJ/Yoa8FquthaDHHyxm5FFphkN7ZiKswpFWYmHACYNPB3hfmDwTDeGIIYhI5BaOc6qMJMjGOSgMHY/Gk9gfJbrN6HzZfrnM9fmS9QNjXaUitJLDDtv+tj+U/ViTbdx5Km1InWdVozvOkyUd07jje6dOfrRNXnY3TIVehwl9EhUEeejgZ0zYz/IZXBrBaEr6XWN11LXUpLxBU5WthwXdeDnYMVTmxOEgvlDxhRQ6KPbjD35jxE+wgj9SppROAseUfz8768ojfzRcP+XEUJX0Nssaj9zdSxUE/ckNRiVpqq0/WoX5y7OAvXEx8oEwrd1mYLs+lJHPRUjnsF1sKO8YUd9x6o8PCEPaEH7ADdYS+9eyUurMRWX6LykmS3Tyrxp1WfAra3CU0QsZdCQQdiMc3WnJb1yMYQ/ribBGCk+iCBGEoJZQkoj3tmwB8aF1FNlUqM5k7HatW4UVpgmjZoIBeSVG0aadjiM5mZJxb9iv8mEmHxycyMD6fxLTL3xs0vLSkpWVyyQLjT2C0zetjwUTCuzkSkQuHw4YXaphkUuff4CVJ7ffLkTjhG7Z/ZSfLsKcS3dAOhLMuO+Cz7QW9dsC5WJ+Qpx3GSbIOORGytQkpl2dqPoFuZWO+/alXgHwoflooDUIR0geXNOrL8lKCWDKcL2c7yXe/7kWAiAhovms6OUeKVzhs6eM6cwUPnTU6OjkpKiopOlvwGFBcPGFhUNDC6c1JMTDKEyUpPgfi10E/6GxhBAmAlU9qZ3KtpqMtLe8ugXngprh1kk6s1XQwHod/sYd1fsEYmLJk1LOlAXESSVD1i+dDMmLD8VUMz2jM59xIqEn8WOhJL8KvzIMeaweJIqEhy3rOBsWMzKH5dhL/hcCLDJGDQ1GL6siZQo1UwhXV5blbKRfEALMQ73iPw3YQ7MF8Lz/Yqg4fKCaf59AvSIPwczK0CgM2B78Lh0Is/C5WIi+E7F6Zc9MVXoTv0IPhRXNDz5LcjwEkmc0/CJwEARpceDp3q7xJc0FsM/hSDPwX7MXjed/RQbbsuDWa0HYYCiXCDO8WEfRbO0JbYCAc8NzXla9iNjk/iT2HkT+fIGHsBKP4pbEBdhTvAi3CmXfAQol0j+c/MLhw7Z/bYwjmCJX/O7BG9R86YOYLmJ8FWZBUOApl8L4Bsa39ahRoG46EVpvz9Er4CQ15CEXgaXG6Ey+k8Awh8CxVeovBGaIJhRuEeDMFXXvr7b+EgnmvEc2EZXEfgY0CRME2KBAJ9KhDLjqJLjITmV+lhzUXsEGb2/OmogzCIyGQP0Ayk8/H8+31HdllydzbjeAoaycJYVSmq9XIelUkrnSKhVfCJFNCXpaVV2CrCMyer5NvC7G0221Q0w3EAPonw2/SZehK/4AqZOxqUgvsh/wfKsaIjSTlWbDQ7EI2zs/T8YQOAnupMYMhR53bvSHqcDhlskbyrZ6omd+jR5y1cjWeLSa1CZ3KQGGTsLw5om+os9J+wC8ftWPbY1DjfpHlpN/F3G8h/MOxmyvQs34RpSUu3wzM4Dp6BJ9HUV318jnkbYIuPUOWiSv1x2NrgfcJgPFDcrHKRwj97UJHwvdDx4Wf9Ct/T/DYqqlLWyx8A0cz6CFuAyY/qJNS2HjWpPfzJhf9/oseQqvkjL7xw9ewTa3PD02Y/XjT2q6/QuLo60muYW/llcMuTphYFBbmk17DRDugNgBAuWAjPGUA3Dc81d00lIHeRsh2KLYfajLzBeVarnnGeN8950Gz1idShA8XFH+DRHvDFD/EY4bysh6Hr16+fjoKwLEET8mW0H9XwJ7outANRYIsmz95cSznFHnsw726PCmymSZE7s+FqplxJkudpE+aPzpTbHw+GeeStNg3/n82ew3OPzp4zmQTQV4QegaCPpmai+QNnHf+vqyMs/4fqiIfURgwGAG4hOEogRiPTmzd1zjOZnmuXVFO4LIGr5mQsak5mJpzXmKNT8jb/Bbts07oAAAB4AWNgZGAAYen931bF89t8ZZDkYACBIx8E9UD0OZEzun+E/l7lLOKoBHI5GZhAogBOMQvyeAFjYGRg4Ej6e5WBgdPoj9B/I44FQBFUcAcAiWcGPQB4AW2RUxidTQwG52Szv22ztm3btm3btm3btm3bvqvd03y1LuaZrPGGngCA+RkSkWEyhHR6jhTag4r+DBX8n6QKFSOdLKaNrOBb15rftSEZQrtIJGPILCkY6jIjNr+KMd/IZ+QxkhjtjAZGRqNsMCYRGSr/UFW/JbX2oq9Go427QIyP/yWbj8I3/h9G+5+o5tMxWscbE6xdmVp+DqMlJzO1Bclt3mgtwOiPxcbmGI2o7KObO5lzmD+huI7lb9+ATv4Hvv74B6KY4+kdvtQ1FJG4dHCF+dH8hatOQjcCJwPszsXs7l1oo/HJa86vKSgqu4lmdQGjpXxPH/k1PEfj0DaoP7ptc7vQKphrtAksG81RySdb+NnazfUr/vEPiGj+1/jGKCizSSLCLPPvPi8Nn/39X/TWlnbvheT1IympZ/gt9Igueo8S+hcTPspAYdeXBu4c5bQmrYO/f9Z3nM7uM1prdkq7stRw5Sknc2miy+mn35BK0jFGvqGmJLS5k2ls66t99AVzPqpkHKWehigT/PuH+Lhj+E6QRZDDSyRneH+Qg/moscqXIcLLDN5FM5DTN7facniTZzlsY4Bepkvw5x/io7UkeJaDZfAm8lt4kfxGb/MKY6wuI8UbGbxNX9JrV7Pl8BZBDoPpFjjY6+MFVPw4OfndJYbLPNq5I7TxnZn8UVtmhEaSzsgYWK4ZN8gox83b6SL1qCFVKeBGENNNJbXmJLu2Z5RO4RfXnZyuEuVcQZsTn8LB3z0FW2/CPAAAAAAAAAAAAAAALABaANQBSgHaAo4CqgLUAv4DLgNUA2gDgAOaA7IEAgQuBIQFAgVKBbAGGgZQBsgHMAdAB1AHgAeuB94IOgjuCTgJpgn8Cj4KhgrCCygLggueC9QMHgxCDKYM9A1GDYwN6A5MDrIO3g8aD1IPuhAGEEQQfhCkELwQ4BECER4RWBHiEkASkBLuE1IToBQUFFoUhhTKFRIVLhWaFeAWMhaQFuwXLBewGAAYRBh+GOIZPBmSGcwaEBooGmwashqyGtobRBuqHA4ccByaHT4dYB30Ho4emh60HrwfZh98H8ggCiBoIQYhQCGQIboh0CIGIjwihiKSIqwixiLgIzgjSiNcI24jgCOWI6wkIiQuJEAkUiRoJHokjCSeJLQlIiU0JUYlWCVqJXwlkiXEJkImVCZmJngmjiagJu4nVCdmJ3gniiecJ7AnxiiOKJoorCi+KNAo5Cj2KQgpGikwKcop3CnuKgAqEiokKjgqcCrqKvwrDisgKzQrRiukK7gr1CxeLPItGC1YLZQtni2oLcAt2i3uLgYuHi4+Llouci6KLp4u3C9eL3Yv2DAcMKQw9jEcMS4AAAABAAAA3ACXABYAXwAFAAEAAAAAAA4AAAIAAeYAAwABeAF9zANyI2AYBuBnt+YBMsqwjkfpsLY9qmL7Bj1Hb1pbP7+X6HOmy7/uAf8EeJn/GxV4mbvEjL/M3R88Pabfsr0Cbl7mUQdu7am4VNFUEbQp5VpOS8melIyWogt1yyoqMopSkn+kkmIiouKOpNQ15FSUBUWFREWe1ISoWcE378e+mU99WU1NVUlhYZ2nHXKh6sKVrJSQirqMsKKcKyllDSkNYRtWzVu0Zd+iGTEhkXtU0y0IeAFswQOWQgEAAMDZv7Zt27ZtZddTZ+4udYFmBEC5qKCaEjWBQK069Ro0atKsRas27Tp06tKtR68+/QYMGjJsxKgx4yZMmjJtxqw58xYsWrJsxao16zZs2rJtx649+w4cOnLsxKkz5y5cunLtxq079x48evLsxas37z58+vLtx68//0LCIqJi4hKSUtIyshWC4GErEAAAAOAs/3NtI+tluy7Ztm3zZZ6z69yMBuVixBqU50icNMkK1ap48kySXdGy3biVKl+CcYeuFalz786DMo1mTWvy2hsZ3po3Y86yBYuWHHtvzYpVzT64kmnTug0fnTqX6LNPvvjmq+9K/PDLT7/98c9f/wU4EShYkBBhQvUoFSFcpChnLvTZ0qLVtgM72rTr0m1Ch06T4g0ZNvDk+ZMXLo08efk4RnZGDkZOhlQWv1AfH/bSvEwDA0cXEG1kYG7C4lpalM+Rll9apFdcWsBZklGUmgpisZeU54Pp/DwwHwBPQXTqAHgBLc4lXMVQFIDxe5+/Ke4uCXd3KLhLWsWdhvWynugFl7ieRu+dnsb5flD+V44+W03Pqkm96nSsSX3pwfbG8hyVafqKLY53NhRyi8/1/P8l1md6//6SRzsznWXcUiuTXQ3F3NJTfU3V3NRrJp2WrjUzN3sl06/thr54PYV7+IYaQ1++jlly8+AO2iz5W4IT8OEJIqi29NXrGHhwB65DLfxAtSN5HvgQQgRjjiSfQJDDoBz5e4AA3BwJtOVAHgtBBGGeRNsK5DYGd8IvM61XFAA=) 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 new file mode 100644 index 0000000000000000000000000000000000000000..27d0aa8108b0800f9cddcf613f787347d9981e05 GIT binary patch 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 + + 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" + return value.split("\n") + if type == "boolean" and not value + return false + else + return value + + createSections: -> + section = @createSection("Web Interface") + + # 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: "fileserver_port" + title: "File server port" + type: "text" + restrict: "number" + description: "Other peers will use this port to reach your served sites. (default: 15441)" + + section.items.push + title: "Tor" + key: "tor" + type: "select" + options: [ + {title: "Disable", value: "disable"} + {title: "Enable", value: "enable"} + {title: "Always", value: "always"} + ] + value: "Enable" + 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)" + + 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: "text" + description: "Load additional list of torrent trackers dynamically, from a file" + placeholder: "Eg.: data/trackers.json" + value_pos: "fullwidth" + + section.items.push + title: "Proxy for tracker connections" + key: "trackers_proxy" + type: "select" + options: [ + {title: "Disable", value: "disable"} + {title: "Tor", value: "tor"} + ] + + createSection: (title) => + section = {} + section.title = title + section.items = [] + @items.push(section) + return section + +window.ConfigStorage = ConfigStorage diff --git a/plugins/UiConfig/media/js/UiConfig.coffee b/plugins/UiConfig/media/js/UiConfig.coffee new file mode 100644 index 00000000..372cf812 --- /dev/null +++ b/plugins/UiConfig/media/js/UiConfig.coffee @@ -0,0 +1,223 @@ +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 + window.onbeforeunload = => + if @getValuesChanged().length > 0 + return true + else + return null + + onOpenWebsocket: => + @cmd("wrapperSetTitle", "Config - ZeroNet") + @updateConfig() + + updateConfig: (cb) => + @cmd "configList", [], (res) => + @config = res + @values = {} + @config_storage = new ConfigStorage(@config) + for key, item of res + @values[key] = @config_storage.formatValue(item.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)) + value_same_as_default = JSON.stringify(@config[item.key].default) == JSON.stringify(value) + 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?() + + 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)" + + h("div.config-item", [ + 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) => + @log "autosize", arguments + 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 + }) + + 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 == @values[item.key], value: option.value}, option.title) + ) + + render: => + if not @config + return h("div.content") + + h("div.content", [ + @config_storage.items.map @renderSection + ]) + + handleSaveClick: => + @save_loading = true + @logStart "Save" + @saveValues => + @save_loading = false + @logEnd "Save" + @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 changes 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 new file mode 100644 index 00000000..b99c17d8 --- /dev/null +++ b/plugins/UiConfig/media/js/all.js @@ -0,0 +1,1650 @@ + + +/* ---- plugins/UiConfig/media/js/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); + + +/* ---- plugins/UiConfig/media/js/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); + + +/* ---- plugins/UiConfig/media/js/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); + + +/* ---- plugins/UiConfig/media/js/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/Dollar.coffee b/plugins/UiConfig/media/js/utils/Dollar.coffee new file mode 100644 index 00000000..7f19f551 --- /dev/null +++ b/plugins/UiConfig/media/js/utils/Dollar.coffee @@ -0,0 +1,3 @@ +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 new file mode 100644 index 00000000..11512d16 --- /dev/null +++ b/plugins/UiConfig/media/js/utils/ZeroFrame.coffee @@ -0,0 +1,85 @@ +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 From b87ad9cd2f00c6219ddc9abd811e68957e22ef24 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 10 Jul 2018 03:31:39 +0200 Subject: [PATCH 0933/2570] Fix disabled tor startup --- src/Connection/ConnectionServer.py | 3 ++- src/Tor/TorManager.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index f657f000..49130c2f 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -64,7 +64,7 @@ class ConnectionServer(object): if check_connections: self.thread_checker = gevent.spawn(self.checkConnections) CryptConnection.manager.loadCerts() - if config.tor != "disabled": + if config.tor != "disable": self.tor_manager.start() if not self.port: self.log.info("No port found, not binding") @@ -89,6 +89,7 @@ class ConnectionServer(object): self.log.info("StreamServer listen error: %s" % err) def stop(self): + self.log.debug("Stopping") self.running = False if self.stream_server: self.stream_server.stop() diff --git a/src/Tor/TorManager.py b/src/Tor/TorManager.py index 60fc4395..b0001529 100644 --- a/src/Tor/TorManager.py +++ b/src/Tor/TorManager.py @@ -59,6 +59,7 @@ class TorManager(object): self.proxy_port = int(self.proxy_port) def start(self): + self.log.debug("Starting (Tor: %s)" % config.tor) self.starting = True try: if not self.connect(): From 7e8a93e974a35e824b4e8b5669f4bebd695236d4 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 10 Jul 2018 03:32:37 +0200 Subject: [PATCH 0934/2570] Load trackers files in announceSites loop --- src/File/FileServer.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index 354fba77..eaeca68e 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -328,18 +328,11 @@ class FileServer(ConnectionServer): startup = False time.sleep(60 * 20) - def trackersFileReloader(self): - while 1: - config.loadTrackersFile() - time.sleep(60) - # Announce sites every 20 min def announceSites(self): - if config.trackers_file: - gevent.spawn(self.trackersFileReloader) - time.sleep(5 * 60) # Sites already announced on startup while 1: + config.loadTrackersFile() s = time.time() for address, site in self.sites.items(): if not site.settings["serving"]: From 71806e6f95a3ac03cc9c1ad0a4640defd993b25b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 10 Jul 2018 03:33:32 +0200 Subject: [PATCH 0935/2570] Trackers loaded from trackers_file will be appended to trackers instead of replacing it --- src/Config.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Config.py b/src/Config.py index 36fa6b67..bc5d82a1 100644 --- a/src/Config.py +++ b/src/Config.py @@ -269,10 +269,19 @@ class Config(object): return self.parser def loadTrackersFile(self): - self.trackers = [] - for tracker in open(self.trackers_file): - if "://" in tracker: - self.trackers.append(tracker.strip()) + if not self.trackers_file: + return None + + self.trackers = self.arguments.trackers[:] + + try: + for line in open(self.trackers_file): + tracker = line.strip() + if "://" in tracker and tracker not in self.trackers: + self.trackers.append(tracker) + except Exception as err: + print "Error loading trackers files: %s" % err + # Find arguments specified for current action def getActionArguments(self): From 86e4679e046bdcf3606f973c5cded641c720b1ed Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 10 Jul 2018 03:34:06 +0200 Subject: [PATCH 0936/2570] Skip tracker announce on incalid url pattern --- src/Site/SiteAnnouncer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index 6dd9efd5..97788428 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -144,7 +144,10 @@ class SiteAnnouncer(object): def announceTracker(self, tracker, mode="start", num_want=10): s = time.time() - protocol, address = tracker.split("://") + if "://" not in tracker: + self.site.log.warning("Tracker %s error: Invalid address" % tracker) + return False + protocol, address = tracker.split("://", 1) if tracker not in self.stats: self.stats[tracker] = {"status": "", "num_request": 0, "num_success": 0, "num_error": 0, "time_request": 0} From 6c4ce03d59de9bd2ca5ff08ec2735a989d135509 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 10 Jul 2018 03:34:23 +0200 Subject: [PATCH 0937/2570] Reload trackers file before start downloading new site --- src/Site/SiteManager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index 4c324bc5..26f9c54c 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -146,6 +146,7 @@ class SiteManager(object): if not self.isAddress(address): return False # Not address: %s % address self.log.debug("Added new site: %s" % address) + config.loadTrackersFile() site = Site(address, settings=settings) self.sites[address] = site if not site.settings["serving"]: # Maybe it was deleted before From 68eb9a37cacbec3e96620047fc36d1c121c54715 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 10 Jul 2018 03:36:09 +0200 Subject: [PATCH 0938/2570] Strip ending / character when using url as function name --- src/Ui/UiRequest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 623fb7da..9807e602 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -105,7 +105,7 @@ class UiRequest(object): # Internal functions elif "/ZeroNet-Internal/" in path: path = re.sub(".*?/ZeroNet-Internal/", "/", path) - func = getattr(self, "action" + path.lstrip("/"), None) # Check if we have action+request_path function + func = getattr(self, "action" + path.strip("/"), None) # Check if we have action+request_path function if func: return func() else: @@ -145,7 +145,7 @@ class UiRequest(object): if body: return body else: - func = getattr(self, "action" + path.lstrip("/"), None) # Check if we have action+request_path function + func = getattr(self, "action" + path.strip("/"), None) # Check if we have action+request_path function if func: return func() else: From 656f48b35486ddf5298e26091bdc5bc4aad841d7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 10 Jul 2018 03:37:38 +0200 Subject: [PATCH 0939/2570] Support open_browser disable even if it's started with start.py --- src/Config.py | 11 +++++++++-- src/Ui/UiServer.py | 17 ++++++++++------- src/main.py | 15 +++++++++------ 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/Config.py b/src/Config.py index bc5d82a1..32d48d65 100644 --- a/src/Config.py +++ b/src/Config.py @@ -385,10 +385,17 @@ class Config(object): for key, val in config.items(section): if section != "global": # If not global prefix key with section key = section + "_" + key + + to_end = key == "open_browser" # Prefer config value over argument + argv_extend = ["--%s" % key] if val: for line in val.strip().split("\n"): # Allow multi-line values - argv.insert(1, line) - argv.insert(1, "--%s" % key) + argv_extend.append(line) + + if to_end: + argv = argv[:-2] + argv_extend + argv[-2:] + else: + argv = argv[:1] + argv_extend + argv[1:] return argv # Expose arguments as class attributes diff --git a/src/Ui/UiServer.py b/src/Ui/UiServer.py index ecd53a0a..9026c4e8 100644 --- a/src/Ui/UiServer.py +++ b/src/Ui/UiServer.py @@ -137,15 +137,18 @@ class UiServer: self.log.info("Web interface: http://%s:%s/" % (config.ui_ip, config.ui_port)) self.log.info("--------------------------------------") - if config.open_browser: + if config.open_browser and config.open_browser != "False": logging.info("Opening browser: %s...", config.open_browser) import webbrowser - if config.open_browser == "default_browser": - browser = webbrowser.get() - else: - browser = webbrowser.get(config.open_browser) - url = "http://%s:%s/%s" % (config.ui_ip if config.ui_ip != "*" else "127.0.0.1", config.ui_port, config.homepage) - gevent.spawn_later(0.3, browser.open, url, new=2) + try: + if config.open_browser == "default_browser": + browser = webbrowser.get() + else: + browser = webbrowser.get(config.open_browser) + url = "http://%s:%s/%s" % (config.ui_ip if config.ui_ip != "*" else "127.0.0.1", config.ui_port, config.homepage) + gevent.spawn_later(0.3, browser.open, url, new=2) + except Exception as err: + print "Error starting browser: %s" % err self.server = WSGIServer((self.ip, self.port), handler, handler_class=UiWSGIHandler, log=self.log) self.server.sockets = {} diff --git a/src/main.py b/src/main.py index 1c5f432f..469e27fc 100644 --- a/src/main.py +++ b/src/main.py @@ -59,14 +59,17 @@ if config.action == "main": lock.write("%s" % os.getpid()) except IOError as err: print "Can't open lock file, your ZeroNet client is probably already running, exiting... (%s)" % err - if config.open_browser: + if config.open_browser and config.open_browser != "False": print "Opening browser: %s...", config.open_browser import webbrowser - if config.open_browser == "default_browser": - browser = webbrowser.get() - else: - browser = webbrowser.get(config.open_browser) - browser.open("http://%s:%s/%s" % (config.ui_ip if config.ui_ip != "*" else "127.0.0.1", config.ui_port, config.homepage), new=2) + try: + if config.open_browser == "default_browser": + browser = webbrowser.get() + else: + browser = webbrowser.get(config.open_browser) + browser.open("http://%s:%s/%s" % (config.ui_ip if config.ui_ip != "*" else "127.0.0.1", config.ui_port, config.homepage), new=2) + except Exception as err: + print "Error starting browser: %s" % err sys.exit() if os.path.isfile("%s/debug.log" % config.log_dir): # Simple logrotate From 4208885e368286930f8c8762d4b7032b14f79e66 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 10 Jul 2018 03:39:33 +0200 Subject: [PATCH 0940/2570] Store pending configuration values --- src/Config.py | 4 ++++ src/Ui/UiWebsocket.py | 13 ++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Config.py b/src/Config.py index 32d48d65..cb4ae2a7 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,6 +13,10 @@ class Config(object): self.rev = 3504 self.argv = argv self.action = None + self.pending_changes = {} + self.need_restart = False + self.keys_restart_need = set(["tor", "fileserver_port"]) + self.config_file = "zeronet.conf" self.createParser() self.createArguments() diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 3240b30f..24f5faee 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -994,9 +994,16 @@ class UiWebsocket(object): config.saveValue(key, value) - instant_change_keys = ["language", "tor_use_bridges", "trackers_proxy"] - if key in instant_change_keys: - setattr(config, key, value) + if key not in config.keys_restart_need: + if value is None: # Default value + setattr(config, key, config.parser.get_default(key)) + setattr(config.arguments, key, config.parser.get_default(key)) + else: + setattr(config, key, value) + setattr(config.arguments, key, value) + else: + config.need_restart = True + config.pending_changes[key] = value if key == "language": import Translate From 30134dbbc92cd2b5622d36e2d14db129c15d56e7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 10 Jul 2018 03:40:56 +0200 Subject: [PATCH 0941/2570] Store API changeable configuration keys in Config.py --- src/Config.py | 1 + src/Ui/UiWebsocket.py | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Config.py b/src/Config.py index cb4ae2a7..48b3e89a 100644 --- a/src/Config.py +++ b/src/Config.py @@ -15,6 +15,7 @@ class Config(object): self.action = None self.pending_changes = {} self.need_restart = False + self.keys_api_change_allowed = set(["tor", "fileserver_port", "language", "tor_use_bridges", "trackers_proxy", "trackers", "trackers_file", "open_browser"]) self.keys_restart_need = set(["tor", "fileserver_port"]) self.config_file = "zeronet.conf" diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 24f5faee..e9a13403 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -986,10 +986,8 @@ class UiWebsocket(object): return self.response(to, {"error": "Not a directory"}) def actionConfigSet(self, to, key, value): - allowed_keys = ["tor", "language", "tor_use_bridges", "trackers_proxy"] - - if key not in allowed_keys: - self.response(to, {"error": "Forbidden"}) + if key not in config.keys_api_change_allowed: + self.response(to, {"error": "Forbidden you cannot set this config key"}) return config.saveValue(key, value) From 3041064d21cc11bffd3ad80e71e73b43cd43ae6a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 10 Jul 2018 03:43:31 +0200 Subject: [PATCH 0942/2570] Reload trackers file on value change --- src/Ui/UiWebsocket.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index e9a13403..ff4843bf 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -1019,4 +1019,7 @@ class UiWebsocket(object): tor_manager = sys.modules["main"].file_server.tor_manager tor_manager.request("SETCONF UseBridges=%i" % value) + if key == "trackers_file": + config.loadTrackersFile() + self.response(to, "ok") From e001448adb5cd95c39a0c85064ef897ba5ab4d83 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 10 Jul 2018 03:43:57 +0200 Subject: [PATCH 0943/2570] Add config link to loading screen --- src/Ui/media/Wrapper.css | 7 +++++++ src/Ui/media/all.css | 7 +++++++ src/Ui/template/wrapper.html | 1 + 3 files changed, 15 insertions(+) diff --git a/src/Ui/media/Wrapper.css b/src/Ui/media/Wrapper.css index 96b4639e..9c74be6e 100644 --- a/src/Ui/media/Wrapper.css +++ b/src/Ui/media/Wrapper.css @@ -108,6 +108,13 @@ a { color: black } .loadingscreen { width: 100%; height: 100%; position: absolute; background-color: #EEE; z-index: 1; overflow: hidden; display: none } .loading-text { text-align: center; vertical-align: middle; top: 50%; position: absolute; margin-top: 39px; width: 100% } +.loading-config { + margin: 20px; display: inline-block; text-transform: uppercase; font-family: Consolas, monospace; position: relative;; + text-decoration: none; letter-spacing: 1px; font-size: 12px; border-bottom: 1px solid #999; top: -60px; transition: all 1s cubic-bezier(1, 0, 0, 1); transition-delay: 0.3s; +} +.loading-config:hover { border-bottom-color: #000; transition: none; } +.loadingscreen.ready .loading-config { top: 0px; } + /* Console */ .console { line-height: 24px; font-family: monospace; font-size: 14px; color: #ADADAD; text-transform: uppercase; opacity: 0; transform: translateY(-20px); } diff --git a/src/Ui/media/all.css b/src/Ui/media/all.css index 5ef1e6c8..f24f83a6 100644 --- a/src/Ui/media/all.css +++ b/src/Ui/media/all.css @@ -122,6 +122,13 @@ a { color: black } .loadingscreen { width: 100%; height: 100%; position: absolute; background-color: #EEE; z-index: 1; overflow: hidden; display: none } .loading-text { text-align: center; vertical-align: middle; top: 50%; position: absolute; margin-top: 39px; width: 100% } +.loading-config { + margin: 20px; display: inline-block; text-transform: uppercase; font-family: Consolas, monospace; position: relative;; + text-decoration: none; letter-spacing: 1px; font-size: 12px; border-bottom: 1px solid #999; top: -60px; -webkit-transition: all 1s cubic-bezier(1, 0, 0, 1); -moz-transition: all 1s cubic-bezier(1, 0, 0, 1); -o-transition: all 1s cubic-bezier(1, 0, 0, 1); -ms-transition: all 1s cubic-bezier(1, 0, 0, 1); transition: all 1s cubic-bezier(1, 0, 0, 1) ; transition-delay: 0.3s; +} +.loading-config:hover { border-bottom-color: #000; -webkit-transition: none; -moz-transition: none; -o-transition: none; -ms-transition: none; transition: none ; } +.loadingscreen.ready .loading-config { top: 0px; } + /* Console */ .console { line-height: 24px; font-family: monospace; font-size: 14px; color: #ADADAD; text-transform: uppercase; opacity: 0; -webkit-transform: translateY(-20px); -moz-transform: translateY(-20px); -o-transform: translateY(-20px); -ms-transform: translateY(-20px); transform: translateY(-20px) ; } diff --git a/src/Ui/template/wrapper.html b/src/Ui/template/wrapper.html index 8faaa5e3..89840b2c 100644 --- a/src/Ui/template/wrapper.html +++ b/src/Ui/template/wrapper.html @@ -51,6 +51,7 @@ else if (window.opener && window.opener.location.toString()) {
    + Config
    From 731b79fc6ce2ec7dd8faebe74d90cf9e297b1d43 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 10 Jul 2018 03:45:33 +0200 Subject: [PATCH 0944/2570] Store copy of variables of list config items to be able to read the original value --- src/Config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 48b3e89a..aace6599 100644 --- a/src/Config.py +++ b/src/Config.py @@ -287,7 +287,6 @@ class Config(object): except Exception as err: print "Error loading trackers files: %s" % err - # Find arguments specified for current action def getActionArguments(self): back = {} @@ -409,6 +408,8 @@ class Config(object): if self.arguments: args = vars(self.arguments) for key, val in args.items(): + if type(val) is list: + val = val[:] setattr(self, key, val) def loadPlugins(self): From 71ad1d9906be4e206d4998230a694546732fc3f2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 10 Jul 2018 03:45:57 +0200 Subject: [PATCH 0945/2570] Properly handle saving on multiline config settings --- src/Config.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index aace6599..94f10666 100644 --- a/src/Config.py +++ b/src/Config.py @@ -443,11 +443,23 @@ class Config(object): key_line_i = i i += 1 + if key_line_i: + while True: # Delete previous multiline values + is_value_line = lines[key_line_i + 1].startswith(" ") or lines[key_line_i + 1].startswith("\t") + if not is_value_line: + break + del lines[key_line_i + 1] + if value is None: # Delete line if key_line_i: del lines[key_line_i] + else: # Add / update - new_line = "%s = %s" % (key, str(value).replace("\n", "").replace("\r", "")) + if type(value) is list: + value_lines = [""] + [str(line).replace("\n", "").replace("\r", "") for line in value] + else: + value_lines = [str(value).replace("\n", "").replace("\r", "")] + new_line = "%s = %s" % (key, "\n ".join(value_lines)) if key_line_i: # Already in the config, change the line lines[key_line_i] = new_line elif global_line_i is None: # No global section yet, append to end of file From d677eb1698d55f878801bc2751bb6fa93473e08c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 10 Jul 2018 03:46:05 +0200 Subject: [PATCH 0946/2570] Rev3523 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 94f10666..4fad1b72 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3504 + self.rev = 3523 self.argv = argv self.action = None self.pending_changes = {} From b1575a979ec67392d97f4a53b278fd0702fdefa9 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 10 Jul 2018 04:00:16 +0200 Subject: [PATCH 0947/2570] Rev3524, Fix saving for single line config values --- src/Config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index 4fad1b72..9dfcda27 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3523 + self.rev = 3524 self.argv = argv self.action = None self.pending_changes = {} @@ -443,7 +443,7 @@ class Config(object): key_line_i = i i += 1 - if key_line_i: + if key_line_i and len(lines) > key_line_i + 1: while True: # Delete previous multiline values is_value_line = lines[key_line_i + 1].startswith(" ") or lines[key_line_i + 1].startswith("\t") if not is_value_line: From 1296ab9b600f5c4b329f27144e237f4c102da617 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Jul 2018 01:34:21 +0200 Subject: [PATCH 0948/2570] Only pre-allocate up to 5MB from big files --- plugins/Bigfile/BigfilePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index b4b88c37..29970836 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -389,7 +389,7 @@ class SiteStoragePlugin(object): os.makedirs(file_dir) f = open(file_path, 'wb') - f.truncate(size) + f.truncate(min(1024 * 1024 * 5, size)) # Only pre-allocate up to 5MB f.close() if os.name == "nt": startupinfo = subprocess.STARTUPINFO() From dae1197ce4ddf863da06425d4762129dea4082e1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Jul 2018 01:34:48 +0200 Subject: [PATCH 0949/2570] Pass the real file size on bigfile request as the size on the disk can be smaller --- plugins/Bigfile/BigfilePlugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index 29970836..7a01df6f 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -113,7 +113,9 @@ class UiRequestPlugin(object): 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"]) - kwargs["file_obj"] = site.storage.openBigfile(path_parts["inner_path"], prebuffer=2 * 1024 * 1024) + big_file = site.storage.openBigfile(path_parts["inner_path"], prebuffer=2 * 1024 * 1024) + kwargs["file_obj"] = big_file + kwargs["file_size"] = big_file.size return super(UiRequestPlugin, self).actionFile(file_path, *args, **kwargs) From 0f567385a8266431813ea6b1f0f4a071a70ed998 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Jul 2018 01:35:35 +0200 Subject: [PATCH 0950/2570] Only check size on file request for smaller files --- src/File/FileRequest.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index 62f144a7..be880149 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -207,13 +207,14 @@ class FileRequest(object): if file_size > read_bytes: # Check if file is readable at current position (for big files) if not self.isReadable(site, params["inner_path"], file, params["location"]): raise RequestError("File not readable at position: %s" % params["location"]) + else: + if params.get("file_size") and params["file_size"] != file_size: + self.connection.badAction(2) + raise RequestError("File size does not match: %sB != %sB" % (params["file_size"], file_size)) if not streaming: file.read_bytes = read_bytes - if params.get("file_size") and params["file_size"] != file_size: - self.connection.badAction(2) - raise RequestError("File size does not match: %sB != %sB" % (params["file_size"], file_size)) if params["location"] > file_size: self.connection.badAction(5) From ec8b53263cfcc9eaf071e40c87a0e3dd86ebf2fb Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Jul 2018 01:36:23 +0200 Subject: [PATCH 0951/2570] Store last announce error time --- src/Site/SiteAnnouncer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index 97788428..917a9051 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -149,7 +149,7 @@ class SiteAnnouncer(object): return False protocol, address = tracker.split("://", 1) if tracker not in self.stats: - self.stats[tracker] = {"status": "", "num_request": 0, "num_success": 0, "num_error": 0, "time_request": 0} + self.stats[tracker] = {"status": "", "num_request": 0, "num_success": 0, "num_error": 0, "time_request": 0, "time_last_error": 0} self.stats[tracker]["status"] = "announcing" self.stats[tracker]["time_request"] = time.time() @@ -176,6 +176,7 @@ class SiteAnnouncer(object): self.stats[tracker]["status"] = "error" self.stats[tracker]["time_status"] = time.time() self.stats[tracker]["last_error"] = str(err).decode("utf8", "ignore") + self.stats[tracker]["time_last_error"] = time.time() self.stats[tracker]["num_error"] += 1 self.updateWebsocket(tracker="error") return False From 7475fa69fabfe584a5cb2e44699a6d4b30b3f34e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Jul 2018 01:47:49 +0200 Subject: [PATCH 0952/2570] Support custom proxy for tracker connections --- src/Config.py | 2 +- src/Connection/Connection.py | 10 ++++++++-- src/Site/SiteAnnouncer.py | 7 ++++++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Config.py b/src/Config.py index 9dfcda27..60b74f78 100644 --- a/src/Config.py +++ b/src/Config.py @@ -234,7 +234,7 @@ class Config(object): self.parser.add_argument('--ip_external', help='Set reported external ip (tested on start if None)', metavar='ip') self.parser.add_argument('--trackers', help='Bootstraping torrent trackers', default=trackers, metavar='protocol://address', nargs='*') self.parser.add_argument('--trackers_file', help='Load torrent trackers dynamically from a file', default=False, metavar='path') - self.parser.add_argument('--trackers_proxy', help='Force use proxy to connect to trackers', default="disable", choices=["disable", "tor"]) + self.parser.add_argument('--trackers_proxy', help='Force use proxy to connect to trackers (disable, tor, ip:port)', default="disable") self.parser.add_argument('--use_openssl', help='Use OpenSSL liblary for speedup', type='bool', choices=[True, False], default=use_openssl) self.parser.add_argument('--disable_db', help='Disable database updating', action='store_true') diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 5037b766..4071c773 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -112,8 +112,14 @@ class Connection(object): 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 == "tor" and self.cert_pin and "zero://%s#%s:%s" % (self.ip, self.cert_pin, self.port) in config.trackers: - self.sock = self.server.tor_manager.createSocket(self.ip, self.port) + elif config.trackers_proxy != "disable" and self.cert_pin and "zero://%s#%s:%s" % (self.ip, self.cert_pin, self.port) in config.trackers: + if config.trackers_proxy == "tor": + self.sock = self.server.tor_manager.createSocket(self.ip, self.port) + else: + from lib.PySocks import socks + self.sock = socks.socksocket() + proxy_ip, proxy_port = config.trackers_proxy.split(":") + self.sock.set_proxy(socks.PROXY_TYPE_SOCKS5, proxy_ip, int(proxy_port)) else: self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index 917a9051..090e0afc 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -241,8 +241,13 @@ class SiteAnnouncer(object): handler = sockshandler.SocksiPyHandler(socks.SOCKS5, tor_manager.proxy_ip, tor_manager.proxy_port) opener = urllib2.build_opener(handler) return opener.open(url, timeout=50) - else: + elif config.trackers_proxy == "disable": return urllib2.urlopen(url, timeout=25) + else: + proxy_ip, proxy_port = config.trackers_proxy.split(":") + handler = sockshandler.SocksiPyHandler(socks.SOCKS5, proxy_ip, int(proxy_port)) + opener = urllib2.build_opener(handler) + return opener.open(url, timeout=50) def announceTrackerHttp(self, tracker_address, mode="start", num_want=10): if "ip4" in self.getOpenedServiceTypes(): From d772280147f0c4f1dd7a80f7219cea8989642db4 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Jul 2018 01:48:27 +0200 Subject: [PATCH 0953/2570] Global announcer statistics admin API call --- src/Ui/UiWebsocket.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index ff4843bf..ac87146c 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -23,9 +23,10 @@ class UiWebsocket(object): admin_commands = set([ "sitePause", "siteResume", "siteDelete", "siteList", "siteSetLimit", "siteAdd", "channelJoinAllsite", "serverUpdate", "serverPortcheck", "serverShutdown", "serverShowdirectory", "serverGetWrapperNonce", - "certSet", "configSet", "permissionAdd", "permissionRemove" + "certSet", "configSet", "permissionAdd", "permissionRemove", "announcerStats" ]) async_commands = set(["fileGet", "fileList", "dirList", "fileNeed"]) + def __init__(self, ws, site, server, user, request): self.ws = ws self.site = site @@ -187,7 +188,6 @@ class UiWebsocket(object): announcer_info.update(params[1]) self.cmd("setAnnouncerInfo", announcer_info) - # Send response to client (to = message.id) def response(self, to, result): self.send({"cmd": "response", "to": to, "result": result}) @@ -378,8 +378,8 @@ class UiWebsocket(object): # Server variables def actionServerInfo(self, to): - ret = self.formatServerInfo() - self.response(to, ret) + back = self.formatServerInfo() + self.response(to, back) # Create a new wrapper nonce that allows to load html file def actionServerGetWrapperNonce(self, to): @@ -387,8 +387,23 @@ class UiWebsocket(object): self.response(to, wrapper_nonce) def actionAnnouncerInfo(self, to): - ret = self.formatAnnouncerInfo(self.site) - self.response(to, ret) + back = self.formatAnnouncerInfo(self.site) + self.response(to, back) + + def actionAnnouncerStats(self, to): + back = {} + for site in self.server.sites.values(): + for tracker, stats in site.announcer.stats.iteritems(): + if tracker not in back: + back[tracker] = {} + is_latest_data = stats["time_request"] > back[tracker].get("time_request", 0) + for key, val in stats.iteritems(): + if key.startswith("num_"): + back[tracker][key] = back[tracker].get(key, 0) + val + elif is_latest_data: + back[tracker][key] = val + + return back # Sign content.json def actionSiteSign(self, to, privatekey=None, inner_path="content.json", remove_missing_optional=False, update_changed_files=False, response_ok=True): From 30940fad2e99860cb4f35aa5ee946b167cb62f83 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Jul 2018 01:49:12 +0200 Subject: [PATCH 0954/2570] Set value to default one on pending reset --- plugins/UiConfig/UiConfigPlugin.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/UiConfig/UiConfigPlugin.py b/plugins/UiConfig/UiConfigPlugin.py index 34766bc4..558a21fa 100644 --- a/plugins/UiConfig/UiConfigPlugin.py +++ b/plugins/UiConfig/UiConfigPlugin.py @@ -44,10 +44,13 @@ class UiWebsocketPlugin(object): for key, val in config_values.iteritems(): if key not in config.keys_api_change_allowed: continue + is_pending = key in config.pending_changes + if val == None and is_pending: + val = config.parser.get_default(key) back[key] = { "value": val, "default": config.parser.get_default(key), - "pending": key in config.pending_changes + "pending": is_pending } return back From 272e30d76165500e5a59dffb882ee763ecb074b2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Jul 2018 01:51:18 +0200 Subject: [PATCH 0955/2570] Separate config renderer related functions to separate file --- plugins/UiConfig/media/js/ConfigView.coffee | 123 +++++++++++++++++++ plugins/UiConfig/media/js/UiConfig.coffee | 125 ++------------------ 2 files changed, 131 insertions(+), 117 deletions(-) create mode 100644 plugins/UiConfig/media/js/ConfigView.coffee diff --git a/plugins/UiConfig/media/js/ConfigView.coffee b/plugins/UiConfig/media/js/ConfigView.coffee new file mode 100644 index 00000000..77a63c64 --- /dev/null +++ b/plugins/UiConfig/media/js/ConfigView.coffee @@ -0,0 +1,123 @@ +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 + }) + + 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 == @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 index 372cf812..eb9bb91f 100644 --- a/plugins/UiConfig/media/js/UiConfig.coffee +++ b/plugins/UiConfig/media/js/UiConfig.coffee @@ -5,6 +5,7 @@ class UiConfig extends ZeroFrame @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 @@ -20,8 +21,11 @@ class UiConfig extends ZeroFrame @config = res @values = {} @config_storage = new ConfigStorage(@config) + @config_view.values = @values + @config_view.config_storage = @config_storage for key, item of res - @values[key] = @config_storage.formatValue(item.value) + value = item.value + @values[key] = @config_storage.formatValue(value) @projector.scheduleRender() cb?() @@ -34,7 +38,7 @@ class UiConfig extends ZeroFrame getValuesChanged: => values_changed = [] for key, value of @values - if @config_storage.formatValue(value) != @config_storage.formatValue(@config[key].value) + if @config_storage.formatValue(value) != @config_storage.formatValue(@config[key]?.value) values_changed.push({key: key, value: value}) return values_changed @@ -65,127 +69,14 @@ class UiConfig extends ZeroFrame Page.cmd "configSet", [key, value], (res) => if res != "ok" Page.cmd "wrapperNotification", ["error", res.error] - cb?() - - 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)" - - h("div.config-item", [ - 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) => - @log "autosize", arguments - 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 - }) - - 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 == @values[item.key], value: option.value}, option.title) - ) + cb?(true) render: => if not @config return h("div.content") h("div.content", [ - @config_storage.items.map @renderSection + @config_view.render() ]) handleSaveClick: => From efe3680cd72575e60e3a0bd04d4b575e1907c127 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Jul 2018 01:51:32 +0200 Subject: [PATCH 0956/2570] Fix typo --- plugins/UiConfig/media/js/UiConfig.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/UiConfig/media/js/UiConfig.coffee b/plugins/UiConfig/media/js/UiConfig.coffee index eb9bb91f..68218df6 100644 --- a/plugins/UiConfig/media/js/UiConfig.coffee +++ b/plugins/UiConfig/media/js/UiConfig.coffee @@ -106,7 +106,7 @@ class UiConfig extends ZeroFrame 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 changes settings requires restart"), + 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") ])) From fcda2e8706e3bc353b316b2cef166904543f21b0 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Jul 2018 01:52:10 +0200 Subject: [PATCH 0957/2570] Reset restart loading status on weboscket connection --- plugins/UiConfig/media/js/UiConfig.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/UiConfig/media/js/UiConfig.coffee b/plugins/UiConfig/media/js/UiConfig.coffee index 68218df6..2b619288 100644 --- a/plugins/UiConfig/media/js/UiConfig.coffee +++ b/plugins/UiConfig/media/js/UiConfig.coffee @@ -14,6 +14,7 @@ class UiConfig extends ZeroFrame onOpenWebsocket: => @cmd("wrapperSetTitle", "Config - ZeroNet") + @restart_loading = false @updateConfig() updateConfig: (cb) => From a42bcfceb6eef3c37f5b966676e6279abb5e2408 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Jul 2018 01:52:27 +0200 Subject: [PATCH 0958/2570] Validate config values before saving --- plugins/UiConfig/media/js/UiConfig.coffee | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/UiConfig/media/js/UiConfig.coffee b/plugins/UiConfig/media/js/UiConfig.coffee index 2b619288..3b12d7e6 100644 --- a/plugins/UiConfig/media/js/UiConfig.coffee +++ b/plugins/UiConfig/media/js/UiConfig.coffee @@ -58,6 +58,15 @@ class UiConfig extends ZeroFrame value_same_as_default = JSON.stringify(@config[item.key].default) == JSON.stringify(value) if value_same_as_default value = null + + if @config[item.key].item.valid_pattern and value + 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 + @saveValue(item.key, value, if last then cb else null) saveValue: (key, value, cb) => From e8895a4d7635072d621afbd5125319072b46236f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Jul 2018 01:52:51 +0200 Subject: [PATCH 0959/2570] Only save if values validated --- plugins/UiConfig/media/js/UiConfig.coffee | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/UiConfig/media/js/UiConfig.coffee b/plugins/UiConfig/media/js/UiConfig.coffee index 3b12d7e6..d0fbffd6 100644 --- a/plugins/UiConfig/media/js/UiConfig.coffee +++ b/plugins/UiConfig/media/js/UiConfig.coffee @@ -92,10 +92,11 @@ class UiConfig extends ZeroFrame handleSaveClick: => @save_loading = true @logStart "Save" - @saveValues => + @saveValues (success) => @save_loading = false @logEnd "Save" - @updateConfig() + if success + @updateConfig() Page.projector.scheduleRender() return false From 7c0541034a0458161dcc379920780a0633298041 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Jul 2018 01:53:34 +0200 Subject: [PATCH 0960/2570] Restrict port to numbers --- plugins/UiConfig/media/js/ConfigStorage.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/UiConfig/media/js/ConfigStorage.coffee b/plugins/UiConfig/media/js/ConfigStorage.coffee index 90e433f8..a153432f 100644 --- a/plugins/UiConfig/media/js/ConfigStorage.coffee +++ b/plugins/UiConfig/media/js/ConfigStorage.coffee @@ -47,7 +47,7 @@ class ConfigStorage extends Class key: "fileserver_port" title: "File server port" type: "text" - restrict: "number" + valid_pattern: /[0-9]*/ description: "Other peers will use this port to reach your served sites. (default: 15441)" section.items.push From f1f0332d22ecdec4994a7d710caebdc4992b17e9 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Jul 2018 01:54:07 +0200 Subject: [PATCH 0961/2570] Config filed for custom socks proxy for trackers --- plugins/UiConfig/media/js/ConfigStorage.coffee | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/plugins/UiConfig/media/js/ConfigStorage.coffee b/plugins/UiConfig/media/js/ConfigStorage.coffee index a153432f..59111e6e 100644 --- a/plugins/UiConfig/media/js/ConfigStorage.coffee +++ b/plugins/UiConfig/media/js/ConfigStorage.coffee @@ -59,7 +59,6 @@ class ConfigStorage extends Class {title: "Enable", value: "enable"} {title: "Always", value: "always"} ] - value: "Enable" description: [ "Disable: Don't connect to peers on Tor network", h("br"), "Enable: Only use Tor for Tor network peers", h("br"), @@ -91,10 +90,21 @@ class ConfigStorage extends Class key: "trackers_proxy" type: "select" options: [ - {title: "Disable", value: "disable"} + {title: "Custom", value: ""} {title: "Tor", value: "tor"} + {title: "Disable", value: "disable"} ] + 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"] + createSection: (title) => section = {} section.title = title From 8f427b727cd9324f7f6355ccee31b9574b4e2909 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Jul 2018 01:54:30 +0200 Subject: [PATCH 0962/2570] Save config storage item for value --- plugins/UiConfig/media/js/ConfigStorage.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/UiConfig/media/js/ConfigStorage.coffee b/plugins/UiConfig/media/js/ConfigStorage.coffee index 59111e6e..a6da910c 100644 --- a/plugins/UiConfig/media/js/ConfigStorage.coffee +++ b/plugins/UiConfig/media/js/ConfigStorage.coffee @@ -12,6 +12,7 @@ class ConfigStorage extends Class 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 From 8d2da6f30bd586067c22e49186ff7c9c1067261a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Jul 2018 01:54:39 +0200 Subject: [PATCH 0963/2570] Add animation helper for config --- .../UiConfig/media/js/utils/Animation.coffee | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 plugins/UiConfig/media/js/utils/Animation.coffee diff --git a/plugins/UiConfig/media/js/utils/Animation.coffee b/plugins/UiConfig/media/js/utils/Animation.coffee new file mode 100644 index 00000000..271b88c1 --- /dev/null +++ b/plugins/UiConfig/media/js/utils/Animation.coffee @@ -0,0 +1,138 @@ +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 From 4fedace1796efc110628ecd1fedd71f7813a019d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Jul 2018 01:55:09 +0200 Subject: [PATCH 0964/2570] Css for custom tracker proxy field --- plugins/UiConfig/media/css/Config.css | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/plugins/UiConfig/media/css/Config.css b/plugins/UiConfig/media/css/Config.css index 7f6d3d38..98291d33 100644 --- a/plugins/UiConfig/media/css/Config.css +++ b/plugins/UiConfig/media/css/Config.css @@ -9,10 +9,11 @@ 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; } +.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 { position: relative; margin: 35px 0px; } +.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; } @@ -53,11 +54,15 @@ a:hover { text-decoration: none } .bottom { width: 100%; text-align: center; background-color: #ffffffde; padding: 25px; bottom: -120px; - transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1);; position: fixed; backface-visibility: hidden; box-sizing: border-box; + 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; } \ No newline at end of file +.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 From 0de16855b08c1479020ed4aedc37d56534d646a3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Jul 2018 01:55:15 +0200 Subject: [PATCH 0965/2570] Merge config js, css --- plugins/UiConfig/media/css/all.css | 11 +- plugins/UiConfig/media/js/all.js | 606 ++++++++++++++++++++--------- 2 files changed, 429 insertions(+), 188 deletions(-) diff --git a/plugins/UiConfig/media/css/all.css b/plugins/UiConfig/media/css/all.css index 34927064..7bb0087a 100644 --- a/plugins/UiConfig/media/css/all.css +++ b/plugins/UiConfig/media/css/all.css @@ -14,10 +14,11 @@ 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 ; } +.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 { position: relative; margin: 35px 0px; } +.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; } @@ -58,7 +59,7 @@ a:hover { text-decoration: none } .bottom { width: 100%; text-align: center; background-color: #ffffffde; padding: 25px; bottom: -120px; - -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 ; + -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; } @@ -67,6 +68,10 @@ a:hover { text-decoration: none } .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 ; } + /* ---- plugins/UiConfig/media/css/button.css ---- */ diff --git a/plugins/UiConfig/media/js/all.js b/plugins/UiConfig/media/js/all.js index b99c17d8..5f6db411 100644 --- a/plugins/UiConfig/media/js/all.js +++ b/plugins/UiConfig/media/js/all.js @@ -964,6 +964,173 @@ +/* ---- plugins/UiConfig/media/js/utils/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); + + /* ---- plugins/UiConfig/media/js/utils/Dollar.coffee ---- */ @@ -1146,7 +1313,8 @@ } item.value = this.formatValue(values[item.key].value); item["default"] = this.formatValue(values[item.key]["default"]); - results1.push(item.pending = values[item.key].pending); + item.pending = values[item.key].pending; + results1.push(values[item.key].item = item); } return results1; }).call(this)); @@ -1190,7 +1358,7 @@ key: "fileserver_port", title: "File server port", type: "text", - restrict: "number", + valid_pattern: /[0-9]*/, description: "Other peers will use this port to reach your served sites. (default: 15441)" }); section.items.push({ @@ -1209,7 +1377,6 @@ value: "always" } ], - value: "Enable", 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({ @@ -1232,20 +1399,37 @@ placeholder: "Eg.: data/trackers.json", value_pos: "fullwidth" }); - return section.items.push({ + section.items.push({ title: "Proxy for tracker connections", key: "trackers_proxy", type: "select", options: [ { - title: "Disable", - value: "disable" + title: "Custom", + value: "" }, { title: "Tor", value: "tor" + }, { + title: "Disable", + value: "disable" } ] }); + return 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: (function(_this) { + return function() { + var ref; + return (ref = Page.values["trackers_proxy"]) === "tor" || ref === "disable"; + }; + })(this) + }); }; ConfigStorage.prototype.createSection = function(title) { @@ -1266,6 +1450,210 @@ }).call(this); + +/* ---- plugins/UiConfig/media/js/ConfigView.coffee ---- */ + + +(function() { + var ConfigView, + 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; + + ConfigView = (function(superClass) { + extend(ConfigView, superClass); + + function ConfigView() { + this.renderValueSelect = bind(this.renderValueSelect, this); + this.renderValueCheckbox = bind(this.renderValueCheckbox, this); + this.renderValueTextarea = bind(this.renderValueTextarea, this); + this.autosizeTextarea = bind(this.autosizeTextarea, this); + this.renderValueText = bind(this.renderValueText, this); + this.handleCheckboxChange = bind(this.handleCheckboxChange, this); + this.handleInputChange = bind(this.handleInputChange, this); + this.renderSectionItem = bind(this.renderSectionItem, this); + this.handleResetClick = bind(this.handleResetClick, this); + this.renderSection = bind(this.renderSection, this); + this; + } + + ConfigView.prototype.render = function() { + return this.config_storage.items.map(this.renderSection); + }; + + ConfigView.prototype.renderSection = function(section) { + return h("div.section", { + key: section.title + }, [h("h2", section.title), h("div.config-items", section.items.map(this.renderSectionItem))]); + }; + + ConfigView.prototype.handleResetClick = function(e) { + var config_key, default_value, node, ref; + node = e.currentTarget; + config_key = node.attributes.config_key.value; + default_value = (ref = node.attributes.default_value) != null ? ref.value : void 0; + return Page.cmd("wrapperConfirm", ["Reset " + config_key + " value?", "Reset to default"], (function(_this) { + return function(res) { + if (res) { + _this.values[config_key] = default_value; + } + return Page.projector.scheduleRender(); + }; + })(this)); + }; + + ConfigView.prototype.renderSectionItem = function(item) { + var marker_title, ref, value_changed, value_default, value_pos; + value_pos = item.value_pos; + if (item.type === "textarea") { + if (value_pos == null) { + value_pos = "fullwidth"; + } + } else { + if (value_pos == null) { + value_pos = "right"; + } + } + value_changed = this.config_storage.formatValue(this.values[item.key]) !== item.value; + value_default = this.config_storage.formatValue(this.values[item.key]) === item["default"]; + if ((ref = item.key) === "open_browser" || ref === "fileserver_port") { + value_default = true; + } + marker_title = "Changed from default value: " + item["default"] + " -> " + this.values[item.key]; + if (item.pending) { + marker_title += " (change pending until client restart)"; + } + if (typeof item.isHidden === "function" ? item.isHidden() : void 0) { + return null; + } + return 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, item.type === "select" ? this.renderValueSelect(item) : item.type === "checkbox" ? this.renderValueCheckbox(item) : item.type === "textarea" ? this.renderValueTextarea(item) : this.renderValueText(item), h("a.marker", { + href: "#Reset", + title: marker_title, + onclick: this.handleResetClick, + config_key: item.key, + default_value: item["default"], + classes: { + "default": value_default, + changed: value_changed, + visible: !value_default || value_changed || item.pending, + pending: item.pending + } + }, "\u2022")) + ]); + }; + + ConfigView.prototype.handleInputChange = function(e) { + var config_key, node; + node = e.target; + config_key = node.attributes.config_key.value; + this.values[config_key] = node.value; + return Page.projector.scheduleRender(); + }; + + ConfigView.prototype.handleCheckboxChange = function(e) { + var config_key, node, value; + node = e.currentTarget; + config_key = node.attributes.config_key.value; + value = !node.classList.contains("checked"); + this.values[config_key] = value; + return Page.projector.scheduleRender(); + }; + + ConfigView.prototype.renderValueText = function(item) { + var value; + value = this.values[item.key]; + if (!value) { + value = ""; + } + return h("input.input-" + item.type, { + type: item.type, + config_key: item.key, + value: value, + placeholder: item.placeholder, + oninput: this.handleInputChange + }); + }; + + ConfigView.prototype.autosizeTextarea = function(e) { + var h, height_before, node, scrollh; + if (e.currentTarget) { + 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) { + return node.style.height = scrollh + "px"; + } else { + return node.style.height = height_before; + } + }; + + ConfigView.prototype.renderValueTextarea = function(item) { + var value; + value = this.values[item.key]; + if (!value) { + value = ""; + } + return h("textarea.input-" + item.type + ".input-text", { + type: item.type, + config_key: item.key, + oninput: this.handleInputChange, + afterCreate: this.autosizeTextarea, + updateAnimation: this.autosizeTextarea, + value: value + }); + }; + + ConfigView.prototype.renderValueCheckbox = function(item) { + var checked; + if (this.values[item.key] && this.values[item.key] !== "False") { + checked = true; + } else { + checked = false; + } + return h("div.checkbox", { + onclick: this.handleCheckboxChange, + config_key: item.key, + classes: { + checked: checked + } + }, h("div.checkbox-skin")); + }; + + ConfigView.prototype.renderValueSelect = function(item) { + return h("select.input-select", { + config_key: item.key, + oninput: this.handleInputChange + }, item.options.map((function(_this) { + return function(option) { + return h("option", { + selected: option.value === _this.values[item.key], + value: option.value + }, option.title); + }; + })(this))); + }; + + return ConfigView; + + })(Class); + + window.ConfigView = ConfigView; + +}).call(this); + + /* ---- plugins/UiConfig/media/js/UiConfig.coffee ---- */ @@ -1286,16 +1674,6 @@ this.renderBottomSave = bind(this.renderBottomSave, this); this.handleSaveClick = bind(this.handleSaveClick, this); this.render = bind(this.render, this); - this.renderValueSelect = bind(this.renderValueSelect, this); - this.renderValueCheckbox = bind(this.renderValueCheckbox, this); - this.renderValueTextarea = bind(this.renderValueTextarea, this); - this.autosizeTextarea = bind(this.autosizeTextarea, this); - this.renderValueText = bind(this.renderValueText, this); - this.handleCheckboxChange = bind(this.handleCheckboxChange, this); - this.handleInputChange = bind(this.handleInputChange, this); - this.renderSectionItem = bind(this.renderSectionItem, this); - this.handleResetClick = bind(this.handleResetClick, this); - this.renderSection = bind(this.renderSection, this); this.saveValue = bind(this.saveValue, this); this.saveValues = bind(this.saveValues, this); this.getValuesPending = bind(this.getValuesPending, this); @@ -1310,6 +1688,7 @@ this.save_visible = true; this.config = null; this.values = null; + this.config_view = new ConfigView(); return window.onbeforeunload = (function(_this) { return function() { if (_this.getValuesChanged().length > 0) { @@ -1323,19 +1702,23 @@ UiConfig.prototype.onOpenWebsocket = function() { this.cmd("wrapperSetTitle", "Config - ZeroNet"); + this.restart_loading = false; return this.updateConfig(); }; UiConfig.prototype.updateConfig = function(cb) { return this.cmd("configList", [], (function(_this) { return function(res) { - var item, key; + var item, key, value; _this.config = res; _this.values = {}; _this.config_storage = new ConfigStorage(_this.config); + _this.config_view.values = _this.values; + _this.config_view.config_storage = _this.config_storage; for (key in res) { item = res[key]; - _this.values[key] = _this.config_storage.formatValue(item.value); + value = item.value; + _this.values[key] = _this.config_storage.formatValue(value); } _this.projector.scheduleRender(); return typeof cb === "function" ? cb() : void 0; @@ -1351,12 +1734,12 @@ }; UiConfig.prototype.getValuesChanged = function() { - var key, ref, value, values_changed; + var key, ref, ref1, value, values_changed; values_changed = []; ref = this.values; for (key in ref) { value = ref[key]; - if (this.config_storage.formatValue(value) !== this.config_storage.formatValue(this.config[key].value)) { + if (this.config_storage.formatValue(value) !== this.config_storage.formatValue((ref1 = this.config[key]) != null ? ref1.value : void 0)) { values_changed.push({ key: key, value: value @@ -1380,7 +1763,7 @@ }; UiConfig.prototype.saveValues = function(cb) { - var changed_values, i, item, j, last, len, results, value, value_same_as_default; + var changed_values, i, item, j, last, len, match, message, results, value, value_same_as_default; changed_values = this.getValuesChanged(); results = []; for (i = j = 0, len = changed_values.length; j < len; i = ++j) { @@ -1391,6 +1774,15 @@ if (value_same_as_default) { value = null; } + if (this.config[item.key].item.valid_pattern && value) { + match = value.match(this.config[item.key].item.valid_pattern); + if (!match || match[0] !== value) { + message = "Invalid value of " + this.config[item.key].item.title + ": " + value + " (does not matches " + this.config[item.key].item.valid_pattern + ")"; + Page.cmd("wrapperNotification", ["error", message]); + cb(false); + break; + } + } results.push(this.saveValue(item.key, value, last ? cb : null)); } return results; @@ -1409,184 +1801,28 @@ if (res !== "ok") { Page.cmd("wrapperNotification", ["error", res.error]); } - return typeof cb === "function" ? cb() : void 0; + return typeof cb === "function" ? cb(true) : void 0; }; })(this)); }; - UiConfig.prototype.renderSection = function(section) { - return h("div.section", { - key: section.title - }, [h("h2", section.title), h("div.config-items", section.items.map(this.renderSectionItem))]); - }; - - UiConfig.prototype.handleResetClick = function(e) { - var config_key, default_value, node, ref; - node = e.currentTarget; - config_key = node.attributes.config_key.value; - default_value = (ref = node.attributes.default_value) != null ? ref.value : void 0; - return Page.cmd("wrapperConfirm", ["Reset " + config_key + " value?", "Reset to default"], (function(_this) { - return function(res) { - if (res) { - _this.values[config_key] = default_value; - } - return Page.projector.scheduleRender(); - }; - })(this)); - }; - - UiConfig.prototype.renderSectionItem = function(item) { - var marker_title, ref, value_changed, value_default, value_pos; - value_pos = item.value_pos; - if (item.type === "textarea") { - if (value_pos == null) { - value_pos = "fullwidth"; - } - } else { - if (value_pos == null) { - value_pos = "right"; - } - } - value_changed = this.config_storage.formatValue(this.values[item.key]) !== item.value; - value_default = this.config_storage.formatValue(this.values[item.key]) === item["default"]; - if ((ref = item.key) === "open_browser" || ref === "fileserver_port") { - value_default = true; - } - marker_title = "Changed from default value: " + item["default"] + " -> " + this.values[item.key]; - if (item.pending) { - marker_title += " (change pending until client restart)"; - } - return h("div.config-item", [ - h("div.title", [h("h3", item.title), h("div.description", item.description)]), h("div.value.value-" + value_pos, item.type === "select" ? this.renderValueSelect(item) : item.type === "checkbox" ? this.renderValueCheckbox(item) : item.type === "textarea" ? this.renderValueTextarea(item) : this.renderValueText(item), h("a.marker", { - href: "#Reset", - title: marker_title, - onclick: this.handleResetClick, - config_key: item.key, - default_value: item["default"], - classes: { - "default": value_default, - changed: value_changed, - visible: !value_default || value_changed || item.pending, - pending: item.pending - } - }, "\u2022")) - ]); - }; - - UiConfig.prototype.handleInputChange = function(e) { - var config_key, node; - node = e.target; - config_key = node.attributes.config_key.value; - this.values[config_key] = node.value; - return Page.projector.scheduleRender(); - }; - - UiConfig.prototype.handleCheckboxChange = function(e) { - var config_key, node, value; - node = e.currentTarget; - config_key = node.attributes.config_key.value; - value = !node.classList.contains("checked"); - this.values[config_key] = value; - return Page.projector.scheduleRender(); - }; - - UiConfig.prototype.renderValueText = function(item) { - var value; - value = this.values[item.key]; - if (!value) { - value = ""; - } - return h("input.input-" + item.type, { - type: item.type, - config_key: item.key, - value: value, - placeholder: item.placeholder, - oninput: this.handleInputChange - }); - }; - - UiConfig.prototype.autosizeTextarea = function(e) { - var h, height_before, node, scrollh; - this.log("autosize", arguments); - if (e.currentTarget) { - 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) { - return node.style.height = scrollh + "px"; - } else { - return node.style.height = height_before; - } - }; - - UiConfig.prototype.renderValueTextarea = function(item) { - var value; - value = this.values[item.key]; - if (!value) { - value = ""; - } - return h("textarea.input-" + item.type + ".input-text", { - type: item.type, - config_key: item.key, - oninput: this.handleInputChange, - afterCreate: this.autosizeTextarea, - updateAnimation: this.autosizeTextarea, - value: value - }); - }; - - UiConfig.prototype.renderValueCheckbox = function(item) { - var checked; - if (this.values[item.key] && this.values[item.key] !== "False") { - checked = true; - } else { - checked = false; - } - return h("div.checkbox", { - onclick: this.handleCheckboxChange, - config_key: item.key, - classes: { - checked: checked - } - }, h("div.checkbox-skin")); - }; - - UiConfig.prototype.renderValueSelect = function(item) { - return h("select.input-select", { - config_key: item.key, - oninput: this.handleInputChange - }, item.options.map((function(_this) { - return function(option) { - return h("option", { - selected: option.value === _this.values[item.key], - value: option.value - }, option.title); - }; - })(this))); - }; - UiConfig.prototype.render = function() { if (!this.config) { return h("div.content"); } - return h("div.content", [this.config_storage.items.map(this.renderSection)]); + return h("div.content", [this.config_view.render()]); }; UiConfig.prototype.handleSaveClick = function() { this.save_loading = true; this.logStart("Save"); this.saveValues((function(_this) { - return function() { + return function(success) { _this.save_loading = false; _this.logEnd("Save"); - _this.updateConfig(); + if (success) { + _this.updateConfig(); + } return Page.projector.scheduleRender(); }; })(this)); @@ -1629,7 +1865,7 @@ visible: values_pending.length && !values_changed.length } }, h("div.bottom-content", [ - h("div.title", "Some changes settings requires restart"), h("a.button.button-submit.button-restart", { + h("div.title", "Some changed settings requires restart"), h("a.button.button-submit.button-restart", { href: "#Restart", classes: { loading: this.restart_loading @@ -1647,4 +1883,4 @@ window.Page.createProjector(); -}).call(this); +}).call(this); \ No newline at end of file From 2f57e9cbee974bc6859cbc283fa318ff94d4e940 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Jul 2018 01:55:41 +0200 Subject: [PATCH 0966/2570] Bigfile test formatting fix --- plugins/Bigfile/Test/TestBigfile.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/plugins/Bigfile/Test/TestBigfile.py b/plugins/Bigfile/Test/TestBigfile.py index 9078b9bf..d253e8be 100644 --- a/plugins/Bigfile/Test/TestBigfile.py +++ b/plugins/Bigfile/Test/TestBigfile.py @@ -1,11 +1,9 @@ import time -import os from cStringIO import StringIO import pytest import msgpack import mock -from lib import merkletools from Connection import ConnectionServer from Content.ContentManager import VerifyError @@ -218,8 +216,6 @@ class TestBigfile: data = f.read(1024 * 1024 * 30) assert len(data) == 0 - - @pytest.mark.parametrize("piecefield_obj", [BigfilePiecefield, BigfilePiecefieldPacked]) def testPiecefield(self, piecefield_obj, site): testdatas = [ @@ -236,7 +232,7 @@ class TestBigfile: assert piecefield[0] == int(testdata[0]) assert piecefield[100] == int(testdata[100]) assert piecefield[1000] == int(testdata[1000]) - assert piecefield[len(testdata)-1] == int(testdata[len(testdata)-1]) + assert piecefield[len(testdata) - 1] == int(testdata[len(testdata) - 1]) packed = piecefield.pack() piecefield_new = piecefield_obj() @@ -244,7 +240,6 @@ class TestBigfile: assert piecefield.tostring() == piecefield_new.tostring() assert piecefield_new.tostring() == testdata - def testFileGet(self, file_server, site, site_temp): inner_path = self.createBigfile(site) @@ -277,7 +272,6 @@ class TestBigfile: # 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 @@ -293,7 +287,6 @@ class TestBigfile: print "%.3fs MEM: + %sKB" % (time.time() - s, (meminfo()[0] - mem_s) / 1024) # 0.082s MEM: + 6800KB print site.peers.values()[0].piecefields - def testUpdatePiecefield(self, file_server, site, site_temp): inner_path = self.createBigfile(site) @@ -339,7 +332,6 @@ class TestBigfile: # 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) @@ -398,19 +390,18 @@ class TestBigfile: size_bigfile = site_temp.content_manager.getFileInfo(inner_path)["size"] with site_temp.storage.openBigfile(inner_path) as f: - assert not "\0" in f.read(1024) + assert "\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 not "\0" in f.read(1024) + assert "\0" not in f.read(1024) assert site_temp.settings["optional_downloaded"] == size_piecemap + size_bigfile # Add second block - assert not "\0" in f.read(1024 * 1024) + assert "\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) From e9c7d6a0cc2f05266e5ee62fdd60bafe5959c291 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Jul 2018 01:55:58 +0200 Subject: [PATCH 0967/2570] Add bigfile test for dynamic file allocation --- plugins/Bigfile/Test/TestBigfile.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/plugins/Bigfile/Test/TestBigfile.py b/plugins/Bigfile/Test/TestBigfile.py index d253e8be..74a9a58d 100644 --- a/plugins/Bigfile/Test/TestBigfile.py +++ b/plugins/Bigfile/Test/TestBigfile.py @@ -466,3 +466,28 @@ class TestBigfile: 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("127.0.0.1", 1545) + site_temp.connection_server = client + site_temp.addPeer("127.0.0.1", 1544) + + # Download site + site_temp.download(blind_includes=True).join(timeout=5) + + # 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) From ea619ce99a4e058ebd3d3dfff499e49addd40ab3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Jul 2018 01:56:19 +0200 Subject: [PATCH 0968/2570] Fix tor disabling for tests --- src/Test/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test/conftest.py b/src/Test/conftest.py index 59baddcc..b38c5f7e 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -62,7 +62,7 @@ config.parse() # Parse again to add plugin configuration options config.data_dir = "src/Test/testdata" # Use test data for unittests config.debug_socket = True # Use test data for unittests config.verbose = True # Use test data for unittests -config.tor = "disabled" # Don't start Tor client +config.tor = "disable" # Don't start Tor client config.trackers = [] os.chdir(os.path.abspath(os.path.dirname(__file__) + "/../..")) # Set working dir From 113e7e0d3d4a83207c503ebb9a4db27d79bf7be1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 16 Jul 2018 01:56:37 +0200 Subject: [PATCH 0969/2570] Rev3535 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 60b74f78..687e91c7 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3524 + self.rev = 3535 self.argv = argv self.action = None self.pending_changes = {} From 8e1c0a7fa498d039762173a8cf02f39aca610fb2 Mon Sep 17 00:00:00 2001 From: rllola Date: Mon, 16 Jul 2018 17:32:18 +0200 Subject: [PATCH 0970/2570] Fix rev 3535 big javascript file --- plugins/Bigfile/BigfilePlugin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index 7a01df6f..ff005396 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -114,8 +114,9 @@ class UiRequestPlugin(object): 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) - kwargs["file_obj"] = big_file - kwargs["file_size"] = big_file.size + if big_file: + kwargs["file_obj"] = big_file + kwargs["file_size"] = big_file.size return super(UiRequestPlugin, self).actionFile(file_path, *args, **kwargs) From 6e1f4fada9dea43ea0d4f39d23094b6eb84c92f4 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Jul 2018 02:09:14 +0200 Subject: [PATCH 0971/2570] Rev3537, Allow to add peers to site with get request --- src/Config.py | 2 +- src/Ui/UiRequest.py | 27 ++++++++++++++++++++++----- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/Config.py b/src/Config.py index 687e91c7..b60d5f95 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3535 + self.rev = 3537 self.argv = argv self.action = None self.pending_changes = {} diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 9807e602..eb6e800c 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -332,6 +332,21 @@ class UiRequest(object): else: return "/" + address + def processQueryString(self, site, query_string): + match = re.search("zeronet_peers=(.*?)(&|$)", query_string) + if match: + query_string = query_string.replace(match.group(0), "") + num_added = 0 + for peer in match.group(1).split(","): + if not re.match(".*?:[0-9]+$", peer): + continue + ip, port = peer.split(":") + if site.addPeer(ip, int(port), source="query_string"): + num_added += 1 + site.log.debug("%s peers added by query string" % num_added) + + return query_string + def renderWrapper(self, site, path, inner_path, title, extra_headers, show_loadingscreen=None): file_inner_path = inner_path if not file_inner_path: @@ -360,13 +375,15 @@ class UiRequest(object): postmessage_nonce_security = "false" wrapper_nonce = self.getWrapperNonce() + inner_query_string = self.processQueryString(site, self.env.get("QUERY_STRING", "")) - if self.env.get("QUERY_STRING"): - query_string = "?%s&wrapper_nonce=%s" % (self.env["QUERY_STRING"], wrapper_nonce) + if inner_query_string: + inner_query_string = "?%s&wrapper_nonce=%s" % (inner_query_string, wrapper_nonce) elif "?" in inner_path: - query_string = "&wrapper_nonce=%s" % wrapper_nonce + inner_query_string = "&wrapper_nonce=%s" % wrapper_nonce else: - query_string = "?wrapper_nonce=%s" % wrapper_nonce + inner_query_string = "?wrapper_nonce=%s" % wrapper_nonce + if self.isProxyRequest(): # Its a remote proxy request if self.env["REMOTE_ADDR"] == "127.0.0.1": # Local client, the server address also should be 127.0.0.1 @@ -409,7 +426,7 @@ class UiRequest(object): title=cgi.escape(title, True), body_style=body_style, meta_tags=meta_tags, - query_string=re.escape(query_string), + query_string=re.escape(inner_query_string), wrapper_key=site.settings["wrapper_key"], ajax_key=site.settings["ajax_key"], wrapper_nonce=wrapper_nonce, From 405394707fd9243e77ce899059d2d2f5d2ca85f8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 19 Jul 2018 20:40:55 +0200 Subject: [PATCH 0972/2570] Shorter download site button title --- plugins/Sidebar/SidebarPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index d382c020..bb4fd1bf 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -165,7 +165,7 @@ class UiWebsocketPlugin(object):
      """)) From a07a31a2bbe56a463edf57f5e911390fc183f1fe Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 19 Jul 2018 20:42:04 +0200 Subject: [PATCH 0973/2570] Copy site url with peers link to sidebar --- plugins/Sidebar/SidebarPlugin.py | 10 +++++++++- plugins/Sidebar/media/Sidebar.coffee | 13 +++++++++++++ plugins/Sidebar/media/all.js | 15 +++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index bb4fd1bf..6ef218b5 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -115,9 +115,17 @@ class UiWebsocketPlugin(object): else: local_html = "" + copy_link = "http://127.0.0.1:43110/%s/?zeronet_peers=%s" % ( + site.content_manager.contents["content.json"].get("domain", site.address), + ",".join([peer.key for peer in site.getConnectablePeers(20, allow_private=False)]) + ) + body.append(_(u"""
    • - +
      • diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee index 74601c1d..e3bc6f0c 100644 --- a/plugins/Sidebar/media/Sidebar.coffee +++ b/plugins/Sidebar/media/Sidebar.coffee @@ -358,6 +358,19 @@ class Sidebar extends Class @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") diff --git a/plugins/Sidebar/media/all.js b/plugins/Sidebar/media/all.js index 50b1a02f..6e9b85b1 100644 --- a/plugins/Sidebar/media/all.js +++ b/plugins/Sidebar/media/all.js @@ -697,6 +697,21 @@ window.initScrollable = function () { return false; }; })(this)); + this.tag.find("#link-copypeers").off("click touchend").on("click touchend", (function(_this) { + return function(e) { + var copy_text, handler; + copy_text = e.currentTarget.href; + handler = function(e) { + e.clipboardData.setData('text/plain', copy_text); + e.preventDefault(); + _this.wrapper.notifications.add("copy", "done", "Site address with peers copied to your clipboard", 5000); + return document.removeEventListener('copy', handler, true); + }; + document.addEventListener('copy', handler, true); + document.execCommand('copy'); + return false; + }; + })(this)); $(document).on("click touchend", (function(_this) { return function() { var ref, ref1; From 7954caf95730361a2a919c80ae82696da35c1863 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 19 Jul 2018 20:42:53 +0200 Subject: [PATCH 0974/2570] Force reannounce not yet download site on refresh --- src/Ui/UiRequest.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index eb6e800c..5cf6efbe 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -308,7 +308,11 @@ class UiRequest(object): title = site.content_manager.contents["content.json"]["title"] else: title = "Loading %s..." % address - site = SiteManager.site_manager.need(address) # Start download site + site = SiteManager.site_manager.get(address) + if site: # Already added, but not downloaded + gevent.spawn(site.update, announce=True) + else: # If not added yet + site = SiteManager.site_manager.need(address) if not site: return False From f143000f4adfabbdd0ca90b8da936f75775d797d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 19 Jul 2018 20:43:14 +0200 Subject: [PATCH 0975/2570] Fix parsing udp trackers with request path --- src/Site/SiteAnnouncer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index 090e0afc..5b7818c5 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -216,7 +216,7 @@ class SiteAnnouncer(object): if config.trackers_proxy != "disable": raise AnnounceError("Udp trackers not available with proxies") - ip, port = tracker_address.split(":") + ip, port = tracker_address.split("/")[0].split(":") tracker = UdpTrackerClient(ip, int(port)) if "ip4" in self.getOpenedServiceTypes(): tracker.peer_port = self.fileserver_port From 8d3b1d10d2c56707e531bbb2e0e556c884d5bfdd Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 19 Jul 2018 20:43:33 +0200 Subject: [PATCH 0976/2570] Fix typo --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index b60d5f95..2b46da56 100644 --- a/src/Config.py +++ b/src/Config.py @@ -285,7 +285,7 @@ class Config(object): if "://" in tracker and tracker not in self.trackers: self.trackers.append(tracker) except Exception as err: - print "Error loading trackers files: %s" % err + print "Error loading trackers file: %s" % err # Find arguments specified for current action def getActionArguments(self): From f7099b2bc75fe8d06c82b89e8ff4366fb623f0fa Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 19 Jul 2018 20:43:46 +0200 Subject: [PATCH 0977/2570] Load trackers file on startup --- src/Config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Config.py b/src/Config.py index 2b46da56..4cfb5718 100644 --- a/src/Config.py +++ b/src/Config.py @@ -358,6 +358,8 @@ class Config(object): self.parser._print_message = original_print_message self.parser.exit = original_exit + self.loadTrackersFile() + # Parse command line arguments def parseCommandline(self, argv, silent=False): # Find out if action is specificed on start From a357b021ba355a07de8311c4aff5495f24fbcd50 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 19 Jul 2018 20:44:25 +0200 Subject: [PATCH 0978/2570] Rev3540 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 4cfb5718..61dc412b 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3537 + self.rev = 3540 self.argv = argv self.action = None self.pending_changes = {} From debe9959e4cc5dde8a91744a67efc62046c4ecb6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Jul 2018 04:24:35 +0200 Subject: [PATCH 0979/2570] Fix saving browser open setting --- plugins/UiConfig/media/js/ConfigStorage.coffee | 2 +- plugins/UiConfig/media/js/all.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/UiConfig/media/js/ConfigStorage.coffee b/plugins/UiConfig/media/js/ConfigStorage.coffee index a6da910c..fda06864 100644 --- a/plugins/UiConfig/media/js/ConfigStorage.coffee +++ b/plugins/UiConfig/media/js/ConfigStorage.coffee @@ -25,7 +25,7 @@ class ConfigStorage extends Class return value deformatValue: (value, type) -> - if type == "object" + if type == "object" and typeof(value) == "string" return value.split("\n") if type == "boolean" and not value return false diff --git a/plugins/UiConfig/media/js/all.js b/plugins/UiConfig/media/js/all.js index 5f6db411..a587e5a4 100644 --- a/plugins/UiConfig/media/js/all.js +++ b/plugins/UiConfig/media/js/all.js @@ -1335,7 +1335,7 @@ }; ConfigStorage.prototype.deformatValue = function(value, type) { - if (type === "object") { + if (type === "object" && typeof value === "string") { return value.split("\n"); } if (type === "boolean" && !value) { From f33350a4ef8ad176a53f128f5d9fe80643a08aa8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Jul 2018 04:25:40 +0200 Subject: [PATCH 0980/2570] Rev3542, Trackers file path always relative to executable --- src/Config.py | 71 +++++++++++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/src/Config.py b/src/Config.py index 61dc412b..17161d52 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,13 +10,14 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3540 + self.rev = 3542 self.argv = argv self.action = None self.pending_changes = {} self.need_restart = False self.keys_api_change_allowed = set(["tor", "fileserver_port", "language", "tor_use_bridges", "trackers_proxy", "trackers", "trackers_file", "open_browser"]) self.keys_restart_need = set(["tor", "fileserver_port"]) + self.start_dir = self.getStartDir() self.config_file = "zeronet.conf" self.createParser() @@ -35,6 +36,40 @@ class Config(object): def strToBool(self, v): return v.lower() in ("yes", "true", "t", "1") + def getStartDir(self): + this_file = os.path.abspath(__file__).replace("\\", "/").rstrip("cd") + + if this_file.endswith("/Contents/Resources/core/src/Config.py"): + # Running as ZeroNet.app + if this_file.startswith("/Application") or this_file.startswith("/private") or this_file.startswith(os.path.expanduser("~/Library")): + # Runnig from non-writeable directory, put data to Application Support + start_dir = os.path.expanduser("~/Library/Application Support/ZeroNet").decode(sys.getfilesystemencoding()) + else: + # Running from writeable directory put data next to .app + start_dir = re.sub("/[^/]+/Contents/Resources/core/src/Config.py", "", this_file).decode(sys.getfilesystemencoding()) + config_file = start_dir + "/zeronet.conf" + data_dir = start_dir + "/data" + log_dir = start_dir + "/log" + elif this_file.endswith("/core/src/Config.py"): + # Running as exe or source is at Application Support directory, put var files to outside of core dir + start_dir = this_file.replace("/core/src/Config.py", "").decode(sys.getfilesystemencoding()) + config_file = start_dir + "/zeronet.conf" + data_dir = start_dir + "/data" + log_dir = start_dir + "/log" + elif this_file.endswith("usr/share/zeronet/src/Config.py"): + # Running from non-writeable location, e.g., AppImage + start_dir = os.path.expanduser("~/ZeroNet").decode(sys.getfilesystemencoding()) + config_file = start_dir + "/zeronet.conf" + data_dir = start_dir + "/data" + log_dir = start_dir + "/log" + else: + start_dir = "." + config_file = "zeronet.conf" + data_dir = "data" + log_dir = "log" + + return start_dir + # Create command line arguments def createArguments(self): trackers = [ @@ -66,35 +101,9 @@ class Config(object): else: fix_float_decimals = False - this_file = os.path.abspath(__file__).replace("\\", "/").rstrip("cd") - - if this_file.endswith("/Contents/Resources/core/src/Config.py"): - # Running as ZeroNet.app - if this_file.startswith("/Application") or this_file.startswith("/private") or this_file.startswith(os.path.expanduser("~/Library")): - # Runnig from non-writeable directory, put data to Application Support - start_dir = os.path.expanduser("~/Library/Application Support/ZeroNet").decode(sys.getfilesystemencoding()) - else: - # Running from writeable directory put data next to .app - start_dir = re.sub("/[^/]+/Contents/Resources/core/src/Config.py", "", this_file).decode(sys.getfilesystemencoding()) - config_file = start_dir + "/zeronet.conf" - data_dir = start_dir + "/data" - log_dir = start_dir + "/log" - elif this_file.endswith("/core/src/Config.py"): - # Running as exe or source is at Application Support directory, put var files to outside of core dir - start_dir = this_file.replace("/core/src/Config.py", "").decode(sys.getfilesystemencoding()) - config_file = start_dir + "/zeronet.conf" - data_dir = start_dir + "/data" - log_dir = start_dir + "/log" - elif this_file.endswith("usr/share/zeronet/src/Config.py"): - # Running from non-writeable location, e.g., AppImage - start_dir = os.path.expanduser("~/ZeroNet").decode(sys.getfilesystemencoding()) - config_file = start_dir + "/zeronet.conf" - data_dir = start_dir + "/data" - log_dir = start_dir + "/log" - else: - config_file = "zeronet.conf" - data_dir = "data" - log_dir = "log" + config_file = self.start_dir + "/zeronet.conf" + data_dir = self.start_dir + "/data" + log_dir = self.start_dir + "/log" ip_local = ["127.0.0.1"] @@ -280,7 +289,7 @@ class Config(object): self.trackers = self.arguments.trackers[:] try: - for line in open(self.trackers_file): + for line in open(self.start_dir + "/" + self.trackers_file): tracker = line.strip() if "://" in tracker and tracker not in self.trackers: self.trackers.append(tracker) From c9da6d2ee2e411f18165f521b475873bc1b804d2 Mon Sep 17 00:00:00 2001 From: tangdou1 <35254744+tangdou1@users.noreply.github.com> Date: Wed, 1 Aug 2018 18:05:25 +0800 Subject: [PATCH 0981/2570] small update in zh.json small update in zh.json --- plugins/Sidebar/languages/zh.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/Sidebar/languages/zh.json b/plugins/Sidebar/languages/zh.json index 58c1d2a6..a9df8913 100644 --- a/plugins/Sidebar/languages/zh.json +++ b/plugins/Sidebar/languages/zh.json @@ -1,4 +1,5 @@ { + "Copy to clipboard": "复制到剪切板", "Peers": "节点数", "Connected": "已连接", "Connectable": "可连接", @@ -13,6 +14,7 @@ "Sent bytes": "已发送字节", "Files": "文件", + "Save as .zip": "打包成zip文件", "Total": "总计", "Image": "图像", "Other": "其他", From e6f0a86c5a9c32e084c16e0b32f603354afd3007 Mon Sep 17 00:00:00 2001 From: redfish Date: Thu, 2 Aug 2018 23:40:44 +0000 Subject: [PATCH 0982/2570] main: logging: do not override the warn level This fixes the very annoying problem where the log messages with level WARNING are not printed, while log messages with lower level INFO are printed. This is very confusing behavior and should be avoided. This was due to this override. If there is too much logging output in debug mode, then let's deescalate the level of the frequently-printed log messages down to DEBUG level. --- src/main.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main.py b/src/main.py index 469e27fc..b0f1a326 100644 --- a/src/main.py +++ b/src/main.py @@ -48,9 +48,6 @@ if not os.path.isfile("%s/sites.json" % config.data_dir): if not os.path.isfile("%s/users.json" % config.data_dir): open("%s/users.json" % config.data_dir, "w").write("{}") -# Setup logging -logging.WARNING = 15 # Don't display warnings if not in debug mode -logging.addLevelName(15, "WARNING") if config.action == "main": from util import helper log_file_path = "%s/debug.log" % config.log_dir From 4c62840cefeb76eee4ef1a5ed3ad59c30820af14 Mon Sep 17 00:00:00 2001 From: redfish Date: Thu, 2 Aug 2018 23:42:50 +0000 Subject: [PATCH 0983/2570] TorManager: escalate log error for Tor auth This errorr is fatal for Tor usage (on Linux). Let's make escalate it. Also see the previous commit, without which, this error doesn't even get printed at all. --- src/Tor/TorManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tor/TorManager.py b/src/Tor/TorManager.py index b0001529..4794fb32 100644 --- a/src/Tor/TorManager.py +++ b/src/Tor/TorManager.py @@ -213,7 +213,7 @@ class TorManager(object): except Exception, err: self.conn = None self.setStatus(u"Error (%s)" % str(err).decode("utf8", "ignore")) - self.log.warning(u"Tor controller connect error: %s" % Debug.formatException(str(err).decode("utf8", "ignore"))) + self.log.error(u"Tor controller connect error: %s" % Debug.formatException(str(err).decode("utf8", "ignore"))) self.enabled = False return self.conn From 9bd10d5102d9bf1c778a74bf8d81e7ba4df336bb Mon Sep 17 00:00:00 2001 From: redfish Date: Thu, 2 Aug 2018 23:47:24 +0000 Subject: [PATCH 0984/2570] TorManager: correct the Tor error log message On Linux, there is no self-bundled Tor, so the message was not correct because it was stating that self-bundled Tor was being started. --- src/Tor/TorManager.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Tor/TorManager.py b/src/Tor/TorManager.py index 4794fb32..c629b7b3 100644 --- a/src/Tor/TorManager.py +++ b/src/Tor/TorManager.py @@ -66,7 +66,10 @@ class TorManager(object): raise Exception("No connection") self.log.debug("Tor proxy port %s check ok" % config.tor_proxy) except Exception, err: - self.log.info(u"Starting self-bundled Tor, due to Tor proxy port %s check error: %s" % (config.tor_proxy, err)) + if sys.platform.startswith("win"): + self.log.info(u"Starting self-bundled Tor, due to Tor proxy port %s check error: %s" % (config.tor_proxy, err)) + else: + self.log.info(u"Disabling Tor, because error while accessing Tor proxy at port %s: %s" % (config.tor_proxy, err)) self.enabled = False # Change to self-bundled Tor ports from lib.PySocks import socks From 84b36e72e473833807d677631544776fd9941a7c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 13 Aug 2018 02:43:23 +0200 Subject: [PATCH 0985/2570] Prefer non-onion peers when sidebar copy peers to clipboard --- plugins/Sidebar/SidebarPlugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index 6ef218b5..e77056ab 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -115,9 +115,11 @@ class UiWebsocketPlugin(object): 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["content.json"].get("domain", site.address), - ",".join([peer.key for peer in site.getConnectablePeers(20, allow_private=False)]) + ",".join(peer_ips) ) body.append(_(u""" From c89170a22d873434e86d9bd210f5f3098c6aecf0 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 13 Aug 2018 02:44:50 +0200 Subject: [PATCH 0986/2570] Allow first local ip find method to fail --- src/util/UpnpPunch.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/util/UpnpPunch.py b/src/util/UpnpPunch.py index 8eb5ba5e..7f14198d 100644 --- a/src/util/UpnpPunch.py +++ b/src/util/UpnpPunch.py @@ -129,14 +129,17 @@ def _parse_igd_profile(profile_xml): def _get_local_ips(): local_ips = [] - # get local ip using UDP and a broadcast address - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) - # Not using because gevents getaddrinfo doesn't like that - # using port 1 as per hobbldygoop's comment about port 0 not working on osx: - # https://github.com/sirMackk/ZeroNet/commit/fdcd15cf8df0008a2070647d4d28ffedb503fba2#commitcomment-9863928 - s.connect(('239.255.255.250', 1)) - local_ips.append(s.getsockname()[0]) + try: + # get local ip using UDP and a broadcast address + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + # Not using because gevents getaddrinfo doesn't like that + # using port 1 as per hobbldygoop's comment about port 0 not working on osx: + # https://github.com/sirMackk/ZeroNet/commit/fdcd15cf8df0008a2070647d4d28ffedb503fba2#commitcomment-9863928 + s.connect(('239.255.255.250', 1)) + local_ips.append(s.getsockname()[0]) + except: + pass # Get ip by using UDP and a normal address (google dns ip) try: From f5ab2f63c0ceb511fe937180665377285d1e637b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 13 Aug 2018 02:45:02 +0200 Subject: [PATCH 0987/2570] Rev3546 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 17161d52..25a269a2 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3542 + self.rev = 3546 self.argv = argv self.action = None self.pending_changes = {} From 3ae761dd9d2c90001747b6eec13f2bf9f41f0ee8 Mon Sep 17 00:00:00 2001 From: eduaddad Date: Tue, 14 Aug 2018 16:59:33 -0300 Subject: [PATCH 0988/2570] Create pt-br.json translation into brazilian portuguese --- plugins/ContentFilter/languages/pt-br.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 plugins/ContentFilter/languages/pt-br.json diff --git a/plugins/ContentFilter/languages/pt-br.json b/plugins/ContentFilter/languages/pt-br.json new file mode 100644 index 00000000..3c6bfbdc --- /dev/null +++ b/plugins/ContentFilter/languages/pt-br.json @@ -0,0 +1,6 @@ +{ + "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" +} From 879eb6295d69377d683101e49a582de1b5d1d97d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 16 Aug 2018 16:20:11 +0200 Subject: [PATCH 0989/2570] BigFile lock seek and read until previous request finished, fix seek relative to bigfile end --- plugins/Bigfile/BigfilePlugin.py | 75 +++++++++++++++++--------------- src/Config.py | 2 +- 2 files changed, 42 insertions(+), 35 deletions(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index ff005396..827541a6 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -3,10 +3,11 @@ import os import subprocess import shutil import collections -import gevent import math import msgpack +import gevent +import gevent.lock from Plugin import PluginManager from Debug import Debug @@ -475,52 +476,58 @@ class BigFile(object): 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): - pos = self.f.tell() - read_until = min(self.size, pos + buff) - requests = [] - # Request all required blocks - while 1: - piece_i = 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 + with self.read_lock: + pos = self.f.tell() + read_until = min(self.size, pos + buff) + requests = [] + # Request all required blocks while 1: piece_i = pos / self.piece_size - if piece_i * self.piece_size >= prebuffer_until: + 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]: - self.site.needFile("%s|%s-%s" % (self.inner_path, pos_from, pos_to), blocking=False, update=True, priority=max(0, priority)) - priority -= 1 + 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 - gevent.joinall(requests) - self.read_bytes += buff + if not all(requests): + return None - # 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 + # Request prebuffer + if self.prebuffer: + prebuffer_until = min(self.size, read_until + self.prebuffer) + priority = 3 + while 1: + piece_i = 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 - return self.f.read(buff) + gevent.joinall(requests) + self.read_bytes += buff - def seek(self, pos): - return self.f.seek(pos) + # 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 tell(self): self.f.tell() diff --git a/src/Config.py b/src/Config.py index 25a269a2..0205d54f 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3546 + self.rev = 3549 self.argv = argv self.action = None self.pending_changes = {} From 5854beebc6b2250f083b259b5ce99fa250a2b928 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 16 Aug 2018 16:20:21 +0200 Subject: [PATCH 0990/2570] Fix Bigfile tell --- plugins/Bigfile/BigfilePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index 827541a6..d3ac37b1 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -530,7 +530,7 @@ class BigFile(object): return self.f.seek(pos, whence) def tell(self): - self.f.tell() + return self.f.tell() def close(self): self.f.close() From cfce0577835905000c18a7e02604cc0bccdfb905 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 16 Aug 2018 16:21:21 +0200 Subject: [PATCH 0991/2570] Support streaming bigfile for zip file listing and reading --- plugins/FilePack/FilePackPlugin.py | 52 +++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/plugins/FilePack/FilePackPlugin.py b/plugins/FilePack/FilePackPlugin.py index 8e3bc94e..20c290f3 100644 --- a/plugins/FilePack/FilePackPlugin.py +++ b/plugins/FilePack/FilePackPlugin.py @@ -5,6 +5,7 @@ import gevent from Plugin import PluginManager from Config import config +from Debug import Debug # Keep archive open for faster reponse times for large sites @@ -16,24 +17,25 @@ def closeArchive(archive_path): del archive_cache[archive_path] -def openArchive(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, "r:gz") + archive_cache[archive_path] = tarfile.open(file_obj or archive_path, "r:gz") elif archive_path.endswith("tar.bz2"): import tarfile - archive_cache[archive_path] = tarfile.open(archive_path, "r:bz2") + archive_cache[archive_path] = tarfile.open(file_obj or archive_path, "r:bz2") else: import zipfile - archive_cache[archive_path] = zipfile.ZipFile(archive_path) + 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): - archive = openArchive(archive_path) + +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: @@ -44,20 +46,24 @@ def openArchiveFile(archive_path, path_within): 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 = u"%s/%s/%s" % (config.data_dir, path_parts["address"], path_parts["inner_path"].decode("utf8")) match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))/(.*)", file_path) archive_path, path_within = match.groups() - if not os.path.isfile(archive_path): + if archive_path not in archive_cache: site = self.server.site_manager.get(path_parts["address"]) if not site: return self.actionSiteAddPrompt(path) - # Wait until file downloads - result = site.needFile(site.storage.getInnerPath(archive_path), priority=10) - # Send virutal file path download finished event to remove loading screen - site.updateWebsocket(file_done=site.storage.getInnerPath(file_path)) - if not result: - return self.error404(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) header_allow_ajax = False if self.get.get("ajax_key"): @@ -68,12 +74,12 @@ class UiRequestPlugin(object): return self.error403("Invalid ajax_key") try: - file = openArchiveFile(archive_path, path_within) + 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" % err) + self.log.debug("Error opening archive file: %s" % Debug.formatException(err)) return self.error404(path) return super(UiRequestPlugin, self).actionSiteMedia(path, **kwargs) @@ -105,7 +111,21 @@ class SiteStoragePlugin(object): if ".zip" in inner_path or ".tar.gz" in inner_path: match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))(.*)", inner_path) archive_inner_path, path_within = match.groups() - archive = openArchive(self.getPath(archive_inner_path)) + archive_path = self.getPath(archive_inner_path) + file_obj = None + if archive_path not in archive_cache: + if not os.path.isfile(archive_path): + result = self.site.needFile(archive_inner_path, priority=10) + self.site.updateWebsocket(file_done=archive_inner_path) + if not result: + raise Exception("Unable to download file") + file_obj = self.site.storage.openBigfile(archive_inner_path) + + try: + archive = openArchive(archive_path, file_obj=file_obj) + except Exception as err: + raise Exception("Unable to download file: %s" % err) + if archive_inner_path.endswith(".zip"): namelist = [name for name in archive.namelist() if not name.endswith("/")] else: From b2ea4c758dbd7b54667e198bd9d07c9f583a05cf Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 16 Aug 2018 16:22:17 +0200 Subject: [PATCH 0992/2570] Handle error on file listing API calls --- src/Ui/UiWebsocket.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index ac87146c..40df83ff 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -642,11 +642,17 @@ class UiWebsocket(object): # List files in directory def actionFileList(self, to, inner_path): - return self.response(to, list(self.site.storage.walk(inner_path))) + try: + return list(self.site.storage.walk(inner_path)) + except Exception as err: + return {"error": str(err)} # List directories in a directory def actionDirList(self, to, inner_path): - return self.response(to, list(self.site.storage.list(inner_path))) + try: + return list(self.site.storage.list(inner_path)) + except Exception as err: + return {"error": str(err)} # Sql query def actionDbQuery(self, to, query, params=None, wait_for=None): From efd1efad4dded5aff6e8cca78b91931f2c9e9537 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 16 Aug 2018 16:23:04 +0200 Subject: [PATCH 0993/2570] Rev3550 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 0205d54f..8d8cc568 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3549 + self.rev = 3550 self.argv = argv self.action = None self.pending_changes = {} From 27f2c445324039c6d3632c01ffba131ce98e7f43 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 02:45:37 +0200 Subject: [PATCH 0994/2570] AnnounceShare plugin --- plugins/AnnounceShare/AnnounceSharePlugin.py | 153 ++++++++++++++++++ .../AnnounceShare/Test/TestAnnounceShare.py | 29 ++++ plugins/AnnounceShare/Test/conftest.py | 3 + plugins/AnnounceShare/Test/pytest.ini | 5 + plugins/AnnounceShare/__init__.py | 1 + 5 files changed, 191 insertions(+) create mode 100644 plugins/AnnounceShare/AnnounceSharePlugin.py create mode 100644 plugins/AnnounceShare/Test/TestAnnounceShare.py create mode 100644 plugins/AnnounceShare/Test/conftest.py create mode 100644 plugins/AnnounceShare/Test/pytest.ini create mode 100644 plugins/AnnounceShare/__init__.py diff --git a/plugins/AnnounceShare/AnnounceSharePlugin.py b/plugins/AnnounceShare/AnnounceSharePlugin.py new file mode 100644 index 00000000..ffbed91b --- /dev/null +++ b/plugins/AnnounceShare/AnnounceSharePlugin.py @@ -0,0 +1,153 @@ +import time +import os +import logging +import json +import atexit + +import gevent + +from Config import config +from Plugin import PluginManager +from util import helper +import util + + +class TrackerStorage(object): + def __init__(self): + self.log = logging.getLogger("TrackerStorage") + self.file_path = "%s/trackers.json" % config.data_dir + self.file_content = 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): + trackers = self.getTrackers() + if tracker_address not in trackers: + trackers[tracker_address] = { + "time_added": time.time(), + "time_success": 0, + "latency": 99.0, + "num_error": 0, + "my": False + } + trackers[tracker_address]["time_found"] = time.time() + trackers[tracker_address]["my"] = my + self.log.debug("New tracker found: %s" % tracker_address) + + 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 trackers[tracker_address]["num_error"] > 30 and trackers[tracker_address]["time_success"] < time.time() - 60 * 60 * 24: + 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): + trackers = { + key: tracker for key, tracker in self.getTrackers(type).iteritems() + if tracker["time_success"] > time.time() - 60 * 60 * 24 + } + return trackers + + def load(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 save(self): + s = time.time() + helper.atomicWrite(self.file_path, json.dumps(self.file_content, indent=2, sort_keys=True)) + self.log.debug("Saved in %.3fs" % (time.time() - s)) + + def discoverTrackers(self, 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 + for tracker_address in res["trackers"]: + self.onTrackerFound(tracker_address) + + if not num_success and len(peers) < 20: + self.time_discover = 0.0 + + if num_success: + self.save() + + if config.verbose: + self.log.debug("Trackers discovered from %s/%s peers in %.3fs" % (num_success, len(peers), time.time() - s)) + + +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 = 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) + else: + tracker_storage.onTrackerError(tracker) + + return res + + +@PluginManager.registerTo("FileRequest") +class FileRequestPlugin(object): + def actionGetTrackers(self, params): + shared_trackers = tracker_storage.getWorkingTrackers("shared").keys() + self.response({"trackers": shared_trackers}) + + +@PluginManager.registerTo("FileServer") +class FileServerPlugin(object): + def openport(self, *args, **kwargs): + res = super(FileServerPlugin, self).openport(*args, **kwargs) + if res and not config.tor == "always" and "Bootstrapper" in PluginManager.plugin_manager.plugin_names: + my_tracker_address = "zero://%s:%s" % (config.ip_external, config.fileserver_port) + tracker_storage.onTrackerFound(my_tracker_address, my=True) + return res diff --git a/plugins/AnnounceShare/Test/TestAnnounceShare.py b/plugins/AnnounceShare/Test/TestAnnounceShare.py new file mode 100644 index 00000000..060e3cdb --- /dev/null +++ b/plugins/AnnounceShare/Test/TestAnnounceShare.py @@ -0,0 +1,29 @@ +import time +import copy + +import gevent +import pytest +import mock + +from AnnounceShare import AnnounceSharePlugin +from File import FileServer +from Peer import Peer +from Test import Spy + + +@pytest.mark.usefixtures("resetSettings") +@pytest.mark.usefixtures("resetTempSettings") +class TestAnnounceShare: + def testAnnounceList(self, file_server): + peer = Peer("127.0.0.1", 1544, connection_server=file_server) + assert peer.request("getTrackers")["trackers"] == [] + + tracker_storage = AnnounceSharePlugin.tracker_storage + tracker_storage.onTrackerFound("zero://127.0.0.1:15441") + assert peer.request("getTrackers")["trackers"] == [] + + # It needs to have at least one successfull announce to be shared to other peers + tracker_storage.onTrackerSuccess("zero://127.0.0.1:15441", 1.0) + assert peer.request("getTrackers")["trackers"] == ["zero://127.0.0.1:15441"] + + diff --git a/plugins/AnnounceShare/Test/conftest.py b/plugins/AnnounceShare/Test/conftest.py new file mode 100644 index 00000000..5abd4dd6 --- /dev/null +++ b/plugins/AnnounceShare/Test/conftest.py @@ -0,0 +1,3 @@ +from src.Test.conftest import * + +from Config import config diff --git a/plugins/AnnounceShare/Test/pytest.ini b/plugins/AnnounceShare/Test/pytest.ini new file mode 100644 index 00000000..d09210d1 --- /dev/null +++ b/plugins/AnnounceShare/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/AnnounceShare/__init__.py b/plugins/AnnounceShare/__init__.py new file mode 100644 index 00000000..f55cb2c6 --- /dev/null +++ b/plugins/AnnounceShare/__init__.py @@ -0,0 +1 @@ +import AnnounceSharePlugin From b7a66295f6ea77041cfaff79aff70ee6a6f870d6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 02:46:12 +0200 Subject: [PATCH 0995/2570] Start local peer discovery async to avoid double announce --- plugins/AnnounceLocal/AnnounceLocalPlugin.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/AnnounceLocal/AnnounceLocalPlugin.py b/plugins/AnnounceLocal/AnnounceLocalPlugin.py index de98e6ea..27b4d38a 100644 --- a/plugins/AnnounceLocal/AnnounceLocalPlugin.py +++ b/plugins/AnnounceLocal/AnnounceLocalPlugin.py @@ -12,10 +12,15 @@ 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): - local_announcer.discover(force=force) + thread = gevent.spawn(local_announcer.discover, force=force) + back = super(SiteAnnouncerPlugin, self).announce(force=force, *args, **kwargs) - return super(SiteAnnouncerPlugin, self).announce(force=force, *args, **kwargs) + if thread: + thread.join() + + return back class LocalAnnouncer(BroadcastServer.BroadcastServer): From a7adb517ca3d2b2e6c382d9c31d66bb507ed2b7b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 02:47:43 +0200 Subject: [PATCH 0996/2570] Only return small number of peers if no new site added by the peer --- plugins/disabled-Bootstrapper/BootstrapperPlugin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/disabled-Bootstrapper/BootstrapperPlugin.py b/plugins/disabled-Bootstrapper/BootstrapperPlugin.py index d5af9bb9..753bc390 100644 --- a/plugins/disabled-Bootstrapper/BootstrapperPlugin.py +++ b/plugins/disabled-Bootstrapper/BootstrapperPlugin.py @@ -84,7 +84,7 @@ class FileRequestPlugin(object): 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: + if len(hashes) > 500 or not hashes_changed: limit = 5 order = False else: @@ -103,7 +103,6 @@ class FileRequestPlugin(object): 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_ip4: %.3fs, peerlist: %.3fs)" % From 6783fb901012e9e6de6cc405d104376927e24f07 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 02:47:58 +0200 Subject: [PATCH 0997/2570] Formatting --- plugins/disabled-Bootstrapper/BootstrapperPlugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/disabled-Bootstrapper/BootstrapperPlugin.py b/plugins/disabled-Bootstrapper/BootstrapperPlugin.py index 753bc390..0286ae35 100644 --- a/plugins/disabled-Bootstrapper/BootstrapperPlugin.py +++ b/plugins/disabled-Bootstrapper/BootstrapperPlugin.py @@ -4,7 +4,7 @@ from Plugin import PluginManager from BootstrapperDb import BootstrapperDb from Crypt import CryptRsa -if "db" not in locals().keys(): # Share durin reloads +if "db" not in locals().keys(): # Share during reloads db = BootstrapperDb() @@ -17,7 +17,7 @@ class FileRequestPlugin(object): if "onion_signs" in params and len(params["onion_signs"]) == len(set(params["onions"])): # Check if all sign is correct - if time.time() - float(params["onion_sign_this"]) < 3*60: # Peer has 3 minute to sign the message + if time.time() - float(params["onion_sign_this"]) < 3 * 60: # Peer has 3 minute to sign the message onions_signed = [] # Check onion signs for onion_publickey, onion_sign in params["onion_signs"].items(): @@ -105,8 +105,8 @@ class FileRequestPlugin(object): back["peers"] = peers self.connection.log( - "Announce %s sites (onions: %s, onion_check: %.3fs, db_onion: %.3fs, db_ip4: %.3fs, peerlist: %.3fs)" % - (len(hashes), len(onion_to_hash), time_onion_check, time_db_onion, time_db_ip4, time_peerlist) + "Announce %s sites (onions: %s, onion_check: %.3fs, db_onion: %.3fs, db_ip4: %.3fs, peerlist: %.3fs, limit: %s)" % + (len(hashes), len(onion_to_hash), time_onion_check, time_db_onion, time_db_ip4, time_peerlist, limit) ) self.response(back) From 53a8c2d574d8221cd9a93bf14e0a67dbe167b21f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 02:48:36 +0200 Subject: [PATCH 0998/2570] Fix trayicon compatibility with latest gevent --- plugins/Trayicon/TrayiconPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Trayicon/TrayiconPlugin.py b/plugins/Trayicon/TrayiconPlugin.py index dc5da3f6..3eed2f26 100644 --- a/plugins/Trayicon/TrayiconPlugin.py +++ b/plugins/Trayicon/TrayiconPlugin.py @@ -64,7 +64,7 @@ class ActionsPlugin(object): icon.clicked = lambda: self.opensite("http://%s:%s/%s" % (ui_ip, config.ui_port, config.homepage)) self.quit_servers_event = gevent.threadpool.ThreadResult( - lambda res: gevent.spawn_later(0.1, self.quitServers) + lambda res: gevent.spawn_later(0.1, self.quitServers), gevent.threadpool.get_hub(), lambda: True ) # Fix gevent thread switch error gevent.threadpool.start_new_thread(icon._run, ()) # Start in real thread (not gevent compatible) super(ActionsPlugin, self).main() From e05c432d149cdfc2414133f3a888ba8e446a6074 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 02:52:23 +0200 Subject: [PATCH 0999/2570] Store if it's tracker connection on connect --- plugins/AnnounceZero/AnnounceZeroPlugin.py | 16 +++++++++------- src/Connection/Connection.py | 7 ++++--- src/Connection/ConnectionServer.py | 6 +++--- src/Peer/Peer.py | 12 +++++++----- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/plugins/AnnounceZero/AnnounceZeroPlugin.py b/plugins/AnnounceZero/AnnounceZeroPlugin.py index 87d35ccc..42df0915 100644 --- a/plugins/AnnounceZero/AnnounceZeroPlugin.py +++ b/plugins/AnnounceZero/AnnounceZeroPlugin.py @@ -84,12 +84,14 @@ class SiteAnnouncerPlugin(object): request["delete"] = True # Sent request to tracker - tracker = connection_pool.get(tracker_address) # Re-use tracker connection if possible - if not tracker: + tracker_peer = connection_pool.get(tracker_address) # Re-use tracker connection if possible + if not tracker_peer: tracker_ip, tracker_port = tracker_address.split(":") - tracker = Peer(tracker_ip, tracker_port, connection_server=self.site.connection_server) - connection_pool[tracker_address] = tracker - res = tracker.request("announce", request) + tracker_peer = Peer(tracker_ip, 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: @@ -116,14 +118,14 @@ class SiteAnnouncerPlugin(object): if publickey not in request["onion_signs"]: sign = CryptRsa.sign(res["onion_sign_this"], self.site.connection_server.tor_manager.getPrivatekey(onion)) request["onion_signs"][publickey] = sign - res = tracker.request("announce", request) + 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.remove() # Close connection, we don't need it in next 5 minute + 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) in %.3fs" % diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 4071c773..2e298578 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -19,12 +19,12 @@ from util import helper class Connection(object): __slots__ = ( "sock", "sock_wrapped", "ip", "port", "cert_pin", "target_onion", "id", "protocol", "type", "server", "unpacker", "req_id", - "handshake", "crypt", "connected", "event_connected", "closed", "start_time", "last_recv_time", "is_private_ip", + "handshake", "crypt", "connected", "event_connected", "closed", "start_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", "updateName", "waiting_requests", "waiting_streams" ) - def __init__(self, server, ip, port, sock=None, target_onion=None): + def __init__(self, server, ip, port, sock=None, target_onion=None, is_tracker_connection=False): self.sock = sock self.ip = ip self.port = port @@ -41,6 +41,7 @@ class Connection(object): self.is_private_ip = True else: self.is_private_ip = False + self.is_tracker_connection = is_tracker_connection self.server = server self.unpacker = None # Stream incoming socket messages here @@ -112,7 +113,7 @@ class Connection(object): 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 self.cert_pin and "zero://%s#%s:%s" % (self.ip, self.cert_pin, self.port) in config.trackers: + elif config.trackers_proxy != "disable" and self.is_tracker_connection: if config.trackers_proxy == "tor": self.sock = self.server.tor_manager.createSocket(self.ip, self.port) else: diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index 49130c2f..99dd5319 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -117,7 +117,7 @@ class ConnectionServer(object): def handleMessage(self, *args, **kwargs): pass - def getConnection(self, ip=None, port=None, peer_id=None, create=True, site=None): + def getConnection(self, ip=None, port=None, peer_id=None, create=True, site=None, is_tracker_connection=False): if (ip.endswith(".onion") or self.port_opened == False) and self.tor_manager.start_onions and site: # Site-unique connection for Tor if ip.endswith(".onion"): site_onion = self.tor_manager.getOnion(site.address) @@ -161,9 +161,9 @@ class ConnectionServer(object): try: if (ip.endswith(".onion") or self.port_opened == False) and self.tor_manager.start_onions and site: # Lock connection to site - connection = Connection(self, ip, port, target_onion=site_onion) + connection = Connection(self, ip, port, target_onion=site_onion, is_tracker_connection=is_tracker_connection) else: - connection = Connection(self, ip, port) + connection = Connection(self, ip, port, is_tracker_connection=is_tracker_connection) self.ips[key] = connection self.connections.append(connection) succ = connection.connect() diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index 99b01852..d7632081 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -19,7 +19,7 @@ if config.use_tempfiles: @PluginManager.acceptPlugins class Peer(object): __slots__ = ( - "ip", "port", "site", "key", "connection", "connection_server", "time_found", "time_response", "time_hashfield", "time_added", "has_hashfield", + "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" ) @@ -38,6 +38,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.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 @@ -79,18 +80,19 @@ class Peer(object): try: if self.connection_server: - self.connection = self.connection_server.getConnection(self.ip, self.port, site=self.site) + connection_server = self.connection_server elif self.site: - self.connection = self.site.connection_server.getConnection(self.ip, self.port, site=self.site) + connection_server = self.site.connection_server else: - self.connection = sys.modules["main"].file_server.getConnection(self.ip, self.port, site=self.site) + connection_server = sys.modules["main"].file_server + self.connection = connection_server.getConnection(self.ip, self.port, site=self.site, is_tracker_connection=self.is_tracker_connection) self.connection.sites += 1 - except Exception, 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)) self.connection = None + return self.connection # Check if we have connection to peer def findConnection(self): From ca3b01f898a2c67494ea02cc6b5e8a0297fb43a3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 02:52:42 +0200 Subject: [PATCH 1000/2570] Allow tracker connection to loopback to same client --- src/Connection/Connection.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 2e298578..5a051a54 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -342,6 +342,9 @@ class Connection(object): elif self.ip.endswith(".onion"): handshake["onion"] = self.server.tor_manager.getOnion("global") + if self.is_tracker_connection: + handshake["tracker_connection"] = True + if config.debug_socket: self.log("My Handshake: %s" % handshake) @@ -351,7 +354,7 @@ class Connection(object): if config.debug_socket: self.log("Remote Handshake: %s" % handshake) - if handshake.get("peer_id") == self.server.peer_id: + if handshake.get("peer_id") == self.server.peer_id and not handshake.get("tracker_connection") and not self.is_tracker_connection: self.close("Same peer id, can't connect to myself") self.server.peer_blacklist.append((handshake["target_ip"], handshake["fileserver_port"])) return False From 7738e6a3815d28fbf367cfc783c3ad88d9d0c289 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 02:53:15 +0200 Subject: [PATCH 1001/2570] Fix tracker annonce sleeping formatting --- 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 eaeca68e..0e4a8a79 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -347,7 +347,7 @@ class FileServer(ConnectionServer): taken = time.time() - s sleep = max(0, 60 * 20 / len(config.trackers) - taken) # Query all trackers one-by-one in 20 minutes evenly distributed - self.log.debug("Site announce tracker done in %.3fs, sleeping for %ss..." % (taken, sleep)) + self.log.debug("Site announce tracker done in %.3fs, sleeping for %.3fs..." % (taken, sleep)) time.sleep(sleep) # Detects if computer back from wakeup From 65f48800b969dca69d664ccefb224c92eaa64a41 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 02:53:39 +0200 Subject: [PATCH 1002/2570] Allow tracker list to be extended by plugins --- src/Site/SiteAnnouncer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index 5b7818c5..8e04d0d7 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -32,8 +32,11 @@ class SiteAnnouncer(object): self.last_tracker_id = random.randint(0, 10) self.time_last_announce = 0 + def getTrackers(self): + return config.trackers + def getSupportedTrackers(self): - trackers = config.trackers + trackers = self.getTrackers() if config.disable_udp or config.trackers_proxy != "disable": trackers = [tracker for tracker in trackers if not tracker.startswith("udp://")] From 0e3698fa2fe17da4e90f2598d289c3c9619f7c4b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 02:55:08 +0200 Subject: [PATCH 1003/2570] Announce to next tracker on fail --- src/Site/SiteAnnouncer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index 8e04d0d7..8d30536d 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -123,7 +123,10 @@ class SiteAnnouncer(object): ) else: if len(threads) > 1: - self.site.log.error("Announce to %s trackers in %.3fs, failed" % (num_announced, time.time() - s)) + self.site.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...") + gevent.spawn_later(1.0, self.announce, force=force, mode=mode, pex=pex) self.updateWebsocket(trackers="announced") From 33877c73bb331452006568f7599c47b371acf605 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 02:56:24 +0200 Subject: [PATCH 1004/2570] Test tracker address validity --- src/Site/SiteAnnouncer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index 8d30536d..4fe62708 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -5,6 +5,7 @@ import urllib import urllib2 import struct import socket +import re from lib import bencode from lib.subtl.subtl import UdpTrackerClient @@ -150,8 +151,8 @@ class SiteAnnouncer(object): def announceTracker(self, tracker, mode="start", num_want=10): s = time.time() - if "://" not in tracker: - self.site.log.warning("Tracker %s error: Invalid address" % tracker) + if "://" not in tracker or not re.match("^[A-Za-z0-9:/\\.#-]+$", tracker): + self.site.log.warning("Tracker %s error: Invalid address" % tracker.decode("utf8", "ignore")) return False protocol, address = tracker.split("://", 1) if tracker not in self.stats: From 8738817eab7f2b6e4ed1515292070a79b618b90f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 02:56:41 +0200 Subject: [PATCH 1005/2570] Fix tor logging on message fail --- src/Tor/TorManager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Tor/TorManager.py b/src/Tor/TorManager.py index c629b7b3..22b11b40 100644 --- a/src/Tor/TorManager.py +++ b/src/Tor/TorManager.py @@ -300,7 +300,8 @@ class TorManager(object): time.sleep(1) self.connect() back = None - self.log.debug("< %s" % back.strip()) + if back: + self.log.debug("< %s" % back.strip()) return back def getPrivatekey(self, address): From c5c3f7a667f9a028e19a1d8c8c5022966f19f33f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 02:57:02 +0200 Subject: [PATCH 1006/2570] Don't reannounce site if just added --- src/Ui/UiRequest.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 5cf6efbe..77703658 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -283,6 +283,7 @@ class UiRequest(object): extra_headers = {} match = re.match("/(?P
        [A-Za-z0-9\._-]+)(?P/.*|$)", path) + just_added = False if match: address = match.group("address") inner_path = match.group("inner_path").lstrip("/") @@ -310,9 +311,12 @@ class UiRequest(object): title = "Loading %s..." % address site = SiteManager.site_manager.get(address) if site: # Already added, but not downloaded - gevent.spawn(site.update, announce=True) + if time.time() - site.announcer.time_last_announce > 5: + site.log.debug("Reannouncing site...") + gevent.spawn(site.update, announce=True) else: # If not added yet site = SiteManager.site_manager.need(address) + just_added = True if not site: return False @@ -320,7 +324,7 @@ class UiRequest(object): self.sendHeader(extra_headers=extra_headers) min_last_announce = (time.time() - site.announcer.time_last_announce) / 60 - if min_last_announce > 60 and site.settings["serving"]: + if min_last_announce > 60 and site.settings["serving"] and not just_added: site.log.debug("Site requested, but not announced recently (last %.0fmin ago). Updating..." % min_last_announce) gevent.spawn(site.update, announce=True) From 73078531aa81fcee7c423699e255574bb2a26fad Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 02:58:05 +0200 Subject: [PATCH 1007/2570] Rev3565 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 8d8cc568..477afc2c 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3550 + self.rev = 3565 self.argv = argv self.action = None self.pending_changes = {} From 839b63e389228f5075f20421a5744c9f1deccbc2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 22:44:31 +0200 Subject: [PATCH 1008/2570] Tracker stat skip data with no status information --- 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 40df83ff..1abf1bfa 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -396,7 +396,7 @@ class UiWebsocket(object): for tracker, stats in site.announcer.stats.iteritems(): if tracker not in back: back[tracker] = {} - is_latest_data = stats["time_request"] > back[tracker].get("time_request", 0) + is_latest_data = stats["time_request"] > back[tracker].get("time_request", 0) and stats["status"] for key, val in stats.iteritems(): if key.startswith("num_"): back[tracker][key] = back[tracker].get(key, 0) + val From ecbf230293f87645efd178d8674ce8d18e668776 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 22:44:55 +0200 Subject: [PATCH 1009/2570] Fix site update on non existent sites --- src/Ui/UiWebsocket.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 1abf1bfa..b6e76151 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -864,10 +864,11 @@ class UiWebsocket(object): self.response(to, "Updated") site = self.server.sites.get(address) - if not site.settings["serving"]: - site.settings["serving"] = True - site.saveSettings() if site and (site.address == self.site.address or "ADMIN" in self.site.settings["permissions"]): + if not site.settings["serving"]: + site.settings["serving"] = True + site.saveSettings() + gevent.spawn(updateThread) else: self.response(to, {"error": "Unknown site: %s" % address}) From 767ec1795a6ad80047a7fe26b2df461655bb70e9 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 22:45:44 +0200 Subject: [PATCH 1010/2570] Reset shared tracker error number on startup --- plugins/AnnounceShare/AnnounceSharePlugin.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/AnnounceShare/AnnounceSharePlugin.py b/plugins/AnnounceShare/AnnounceSharePlugin.py index ffbed91b..3ea99f0f 100644 --- a/plugins/AnnounceShare/AnnounceSharePlugin.py +++ b/plugins/AnnounceShare/AnnounceSharePlugin.py @@ -18,6 +18,11 @@ class TrackerStorage(object): self.file_path = "%s/trackers.json" % config.data_dir self.file_content = self.load() + trackers = self.getTrackers() + self.log.debug("Loaded %s shared trackers" % len(trackers)) + for tracker in trackers.values(): + tracker["num_error"] = 0 + self.time_discover = 0.0 atexit.register(self.save) From 7a3dbf626edede44c67cab13bd567073d23f1118 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 22:50:46 +0200 Subject: [PATCH 1011/2570] Only share trackers with success in last hour --- plugins/AnnounceShare/AnnounceSharePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/AnnounceShare/AnnounceSharePlugin.py b/plugins/AnnounceShare/AnnounceSharePlugin.py index 3ea99f0f..d4c44144 100644 --- a/plugins/AnnounceShare/AnnounceSharePlugin.py +++ b/plugins/AnnounceShare/AnnounceSharePlugin.py @@ -60,7 +60,7 @@ class TrackerStorage(object): trackers[tracker_address]["time_error"] = time.time() trackers[tracker_address]["num_error"] += 1 - if trackers[tracker_address]["num_error"] > 30 and trackers[tracker_address]["time_success"] < time.time() - 60 * 60 * 24: + if trackers[tracker_address]["num_error"] > 30 and trackers[tracker_address]["time_success"] < time.time() - 60 * 60: self.log.debug("Tracker %s looks down, removing." % tracker_address) del trackers[tracker_address] @@ -70,7 +70,7 @@ class TrackerStorage(object): def getWorkingTrackers(self, type): trackers = { key: tracker for key, tracker in self.getTrackers(type).iteritems() - if tracker["time_success"] > time.time() - 60 * 60 * 24 + if tracker["time_success"] > time.time() - 60 * 60 } return trackers From 0b38d73cc5c337a622f482ef92ef01805a35c76e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 22:52:21 +0200 Subject: [PATCH 1012/2570] AnnounceZero return None on skipped announce due batching --- plugins/AnnounceShare/AnnounceSharePlugin.py | 2 +- plugins/AnnounceZero/AnnounceZeroPlugin.py | 4 ++-- src/Site/SiteAnnouncer.py | 8 +++++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/plugins/AnnounceShare/AnnounceSharePlugin.py b/plugins/AnnounceShare/AnnounceSharePlugin.py index d4c44144..7fac3e64 100644 --- a/plugins/AnnounceShare/AnnounceSharePlugin.py +++ b/plugins/AnnounceShare/AnnounceSharePlugin.py @@ -135,7 +135,7 @@ class SiteAnnouncerPlugin(object): if res: latency = res tracker_storage.onTrackerSuccess(tracker, latency) - else: + elif res is False: tracker_storage.onTrackerError(tracker) return res diff --git a/plugins/AnnounceZero/AnnounceZeroPlugin.py b/plugins/AnnounceZero/AnnounceZeroPlugin.py index 42df0915..b86f6ae7 100644 --- a/plugins/AnnounceZero/AnnounceZeroPlugin.py +++ b/plugins/AnnounceZero/AnnounceZeroPlugin.py @@ -63,7 +63,7 @@ class SiteAnnouncerPlugin(object): else: # Multi: Announce all currently serving site full_announce = True if time.time() - time_full_announced.get(tracker_address, 0) < 60 * 5: # No reannounce all sites within 5 minute - return [] + 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.settings["serving"]] @@ -132,4 +132,4 @@ class SiteAnnouncerPlugin(object): (tracker_address, site_index, peers_added, time.time() - s) ) - return None + return True diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index 4fe62708..f4314dd5 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -158,6 +158,7 @@ class SiteAnnouncer(object): if tracker not in self.stats: self.stats[tracker] = {"status": "", "num_request": 0, "num_success": 0, "num_error": 0, "time_request": 0, "time_last_error": 0} + last_status = self.stats[tracker]["status"] self.stats[tracker]["status"] = "announcing" self.stats[tracker]["time_request"] = time.time() self.stats[tracker]["num_request"] += 1 @@ -188,11 +189,16 @@ class SiteAnnouncer(object): self.updateWebsocket(tracker="error") return False + if peers is None: # Announce skipped + self.stats[tracker]["time_status"] = time.time() + self.stats[tracker]["status"] = last_status + return None + self.stats[tracker]["status"] = "announced" self.stats[tracker]["time_status"] = time.time() self.stats[tracker]["num_success"] += 1 - if peers is None: # No peers returned + if peers is True: # Announce success, but no peers returned return time.time() - s # Adding peers From 9bdf3ad0c82f7d426f1eea52848df7929138c95e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 22:52:30 +0200 Subject: [PATCH 1013/2570] Only log new tacker discovery if it was not added before --- plugins/AnnounceShare/AnnounceSharePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/AnnounceShare/AnnounceSharePlugin.py b/plugins/AnnounceShare/AnnounceSharePlugin.py index 7fac3e64..c0fe81c3 100644 --- a/plugins/AnnounceShare/AnnounceSharePlugin.py +++ b/plugins/AnnounceShare/AnnounceSharePlugin.py @@ -39,9 +39,9 @@ class TrackerStorage(object): "num_error": 0, "my": False } + self.log.debug("New tracker found: %s" % tracker_address) trackers[tracker_address]["time_found"] = time.time() trackers[tracker_address]["my"] = my - self.log.debug("New tracker found: %s" % tracker_address) def onTrackerSuccess(self, tracker_address, latency): trackers = self.getTrackers() From 81d50ed3bba854de02b49b0c41da2a142269e07b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 22:53:01 +0200 Subject: [PATCH 1014/2570] Add trackers to stats page --- plugins/Stats/StatsPlugin.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py index 4b8cf5a3..22e106ca 100644 --- a/plugins/Stats/StatsPlugin.py +++ b/plugins/Stats/StatsPlugin.py @@ -123,6 +123,36 @@ class UiRequestPlugin(object): ]) yield "" + # Trackers + yield "

        Trackers:
        " + yield "" + for tracker_address, tracker_stat in sys.modules["Site.SiteAnnouncer"].global_stats.iteritems(): + yield self.formatTableRow([ + ("%s", tracker_address), + ("%s", tracker_stat["num_request"]), + ("%s", tracker_stat["num_error"]), + ("%.0f min ago", min(999, (time.time() - tracker_stat["time_request"]) / 60)) + ]) + yield "
        address request successive errors last_request
        " + + + if "AnnounceShare" in PluginManager.plugin_manager.plugin_names: + yield "

        Shared trackers:
        " + yield "" + from AnnounceShare import AnnounceSharePlugin + for tracker_address, tracker_stat in AnnounceSharePlugin.tracker_storage.getTrackers().iteritems(): + yield self.formatTableRow([ + ("%s", tracker_address), + ("%.0f min ago", min(999, (time.time() - tracker_stat["time_added"]) / 60)), + ("%.3fs", tracker_stat["latency"]), + ("%s", tracker_stat["num_error"]), + ("%.0f min ago", min(999, (time.time() - tracker_stat["time_success"]) / 60)), + ]) + yield "
        address added latency successive errors last_success
        " + + + + # Tor hidden services yield "

        Tor hidden services (status: %s):
        " % main.file_server.tor_manager.status for site_address, onion in main.file_server.tor_manager.site_onions.items(): From 5887f902027b25f53876d7864af7d1798a992c3e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 22:54:45 +0200 Subject: [PATCH 1015/2570] Wait handshake done before sending other request to connections --- src/Connection/Connection.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 5a051a54..5305b130 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -475,6 +475,10 @@ class Connection(object): self.log("Send error: missing socket") return False + if not self.connected and message.get("cmd") != "handshake": + self.log("Wait for handshake before send request") + self.event_connected.get() + try: stat_key = message.get("cmd", "unknown") if stat_key == "response": @@ -499,7 +503,7 @@ class Connection(object): with self.send_lock: self.sock.sendall(data) except Exception, err: - self.close("Send error: %s" % err) + self.close("Send error: %s (cmd: %s)" % (err, stat_key)) return False self.last_sent_time = time.time() return True From 669572b0e64a4aa36ea0f2e9d43edc8295890a9d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 22:55:31 +0200 Subject: [PATCH 1016/2570] Fix udp tracker lib error reporting --- src/lib/subtl/subtl.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/subtl/subtl.py b/src/lib/subtl/subtl.py index f53432cf..cfbe6770 100644 --- a/src/lib/subtl/subtl.py +++ b/src/lib/subtl/subtl.py @@ -112,7 +112,7 @@ class UdpTrackerClient: return trans def error(self, message): - print('error: {}'.format(message)) + raise Exception('error: {}'.format(message)) def _send(self, action, payload=None): if not payload: @@ -140,7 +140,7 @@ class UdpTrackerClient: elif action == SCRAPE: return self._process_scrape(payload, trans) elif action == ERROR: - return self._proecss_error(payload, trans) + return self._process_error(payload, trans) else: raise UdpTrackerClientException( 'Unknown action response: {}'.format(action)) @@ -202,7 +202,7 @@ class UdpTrackerClient: it here for the possibility. ''' self.error(payload) - return payload + return False def _generate_peer_id(self): '''http://www.bittorrent.org/beps/bep_0020.html''' From b6d11d6be300b073e8d41b341732a2264ad2b647 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 22:56:20 +0200 Subject: [PATCH 1017/2570] Fix request number counting with zero trackers --- src/Site/SiteAnnouncer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index f4314dd5..0fbe28b2 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -186,6 +186,7 @@ class SiteAnnouncer(object): self.stats[tracker]["last_error"] = str(err).decode("utf8", "ignore") self.stats[tracker]["time_last_error"] = time.time() self.stats[tracker]["num_error"] += 1 + self.stats[tracker]["num_request"] += 1 self.updateWebsocket(tracker="error") return False @@ -197,6 +198,7 @@ class SiteAnnouncer(object): self.stats[tracker]["status"] = "announced" self.stats[tracker]["time_status"] = time.time() self.stats[tracker]["num_success"] += 1 + self.stats[tracker]["num_request"] += 1 if peers is True: # Announce success, but no peers returned return time.time() - s From 92db3878cf9300b4992876f1df8c4820f03c8b9e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 22:56:46 +0200 Subject: [PATCH 1018/2570] Don't include skipped trackers to report --- src/Site/SiteAnnouncer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index 0fbe28b2..d754ed59 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -99,6 +99,8 @@ class SiteAnnouncer(object): 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)) From b43a05b662a59c16c5e41b8b91edfe7336b67f19 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 22:57:09 +0200 Subject: [PATCH 1019/2570] Don't increase request tracker number before actually sending it --- src/Site/SiteAnnouncer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index d754ed59..c0512c82 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -163,7 +163,6 @@ class SiteAnnouncer(object): last_status = self.stats[tracker]["status"] self.stats[tracker]["status"] = "announcing" self.stats[tracker]["time_request"] = time.time() - self.stats[tracker]["num_request"] += 1 if config.verbose: self.site.log.debug("Tracker announcing to %s (mode: %s)" % (tracker, mode)) if mode == "update": From 867cf478b13e7bc70b4e7bbacc3ddd55931be251 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 22:57:34 +0200 Subject: [PATCH 1020/2570] Log tracker request mode on error --- src/Site/SiteAnnouncer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index c0512c82..7c81548e 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -178,7 +178,7 @@ class SiteAnnouncer(object): else: raise AnnounceError("Unknown protocol: %s" % protocol) except Exception, err: - self.site.log.warning("Tracker %s announce failed: %s" % (tracker, str(err).decode("utf8", "ignore"))) + self.site.log.warning("Tracker %s announce failed: %s in mode %s" % (tracker, str(err).decode("utf8", "ignore"), mode)) error = err if error: From 71d05a6fc215ad962508f13573b8025028dbd1fb Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 22:58:27 +0200 Subject: [PATCH 1021/2570] Global stats to reduce tracker request for unreliable trackers --- src/Site/SiteAnnouncer.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index 7c81548e..959dc5ac 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -6,6 +6,7 @@ import urllib2 import struct import socket import re +import collections from lib import bencode from lib.subtl.subtl import UdpTrackerClient @@ -16,12 +17,13 @@ import gevent from Plugin import PluginManager from Config import config import util -from Debug import Debug class AnnounceError(Exception): pass +global_stats = collections.defaultdict(lambda: collections.defaultdict(int)) + @PluginManager.acceptPlugins class SiteAnnouncer(object): @@ -79,6 +81,15 @@ class SiteAnnouncer(object): trackers = self.getAnnouncingTrackers(mode) + if len(trackers) == 1: + tracker = trackers[0] + tracker_stats = global_stats[tracker] + # Reduce the announce time for trackers that looks unreliable + if tracker_stats["num_error"] > 5 and tracker_stats["time_request"] > time.time() - 60 * min(30, tracker_stats["num_error"]): + if config.verbose: + self.site.log.debug("Tracker %s looks unreliable, announce skipped (error: %s)" % (tracker, tracker_stats["num_error"])) + return + if config.verbose: self.site.log.debug("Tracker announcing, trackers: %s" % trackers) @@ -163,6 +174,7 @@ class SiteAnnouncer(object): last_status = self.stats[tracker]["status"] self.stats[tracker]["status"] = "announcing" 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)) if mode == "update": @@ -188,6 +200,8 @@ class SiteAnnouncer(object): self.stats[tracker]["time_last_error"] = time.time() self.stats[tracker]["num_error"] += 1 self.stats[tracker]["num_request"] += 1 + global_stats[tracker]["num_request"] += 1 + global_stats[tracker]["num_error"] += 1 self.updateWebsocket(tracker="error") return False @@ -200,6 +214,8 @@ class SiteAnnouncer(object): self.stats[tracker]["time_status"] = time.time() self.stats[tracker]["num_success"] += 1 self.stats[tracker]["num_request"] += 1 + global_stats[tracker]["num_request"] += 1 + global_stats[tracker]["num_error"] = 0 if peers is True: # Announce success, but no peers returned return time.time() - s @@ -239,7 +255,8 @@ class SiteAnnouncer(object): else: tracker.peer_port = 0 tracker.connect() - tracker.poll_once() + if not tracker.poll_once(): + raise AnnounceError("Could not connect") tracker.announce(info_hash=hashlib.sha1(self.site.address).hexdigest(), num_want=num_want, left=431102370) back = tracker.poll_once() if not back: From 030ad0a8de20443eabbb454ffba3b02d620af93f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 26 Aug 2018 22:58:56 +0200 Subject: [PATCH 1022/2570] Rev3568 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 477afc2c..325ae98f 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3565 + self.rev = 3568 self.argv = argv self.action = None self.pending_changes = {} From 5505a8609dad185759e9b7e2d3b9bcc457b8323f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 27 Aug 2018 11:48:11 +0200 Subject: [PATCH 1023/2570] Fix AnnounceShare source code reloading --- plugins/AnnounceShare/AnnounceSharePlugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/AnnounceShare/AnnounceSharePlugin.py b/plugins/AnnounceShare/AnnounceSharePlugin.py index c0fe81c3..5e06686c 100644 --- a/plugins/AnnounceShare/AnnounceSharePlugin.py +++ b/plugins/AnnounceShare/AnnounceSharePlugin.py @@ -114,7 +114,8 @@ class TrackerStorage(object): self.log.debug("Trackers discovered from %s/%s peers in %.3fs" % (num_success, len(peers), time.time() - s)) -tracker_storage = TrackerStorage() +if "tracker_storage" not in locals(): + tracker_storage = TrackerStorage() @PluginManager.registerTo("SiteAnnouncer") From b4b4694eb5c041dceb9350fca58408a48a08d30a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 27 Aug 2018 11:48:25 +0200 Subject: [PATCH 1024/2570] Only add one new tracker from one source --- plugins/AnnounceShare/AnnounceSharePlugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/AnnounceShare/AnnounceSharePlugin.py b/plugins/AnnounceShare/AnnounceSharePlugin.py index 5e06686c..7dbb4db7 100644 --- a/plugins/AnnounceShare/AnnounceSharePlugin.py +++ b/plugins/AnnounceShare/AnnounceSharePlugin.py @@ -102,7 +102,9 @@ class TrackerStorage(object): num_success += 1 for tracker_address in res["trackers"]: - self.onTrackerFound(tracker_address) + 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 From 426309a8f5369f88a472bf6c96de3c75c69affd2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 27 Aug 2018 11:48:39 +0200 Subject: [PATCH 1025/2570] Stop discovering new trackers after 5 working one --- plugins/AnnounceShare/AnnounceSharePlugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/AnnounceShare/AnnounceSharePlugin.py b/plugins/AnnounceShare/AnnounceSharePlugin.py index 7dbb4db7..51bb11df 100644 --- a/plugins/AnnounceShare/AnnounceSharePlugin.py +++ b/plugins/AnnounceShare/AnnounceSharePlugin.py @@ -90,6 +90,8 @@ class TrackerStorage(object): self.log.debug("Saved in %.3fs" % (time.time() - s)) def discoverTrackers(self, peers): + if len(self.getWorkingTrackers()) > 5: + return False s = time.time() num_success = 0 for peer in peers: From 984f50b792ba46d52f64a62b74971e49e59f642a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 27 Aug 2018 11:48:52 +0200 Subject: [PATCH 1026/2570] Working tracker list default type --- plugins/AnnounceShare/AnnounceSharePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/AnnounceShare/AnnounceSharePlugin.py b/plugins/AnnounceShare/AnnounceSharePlugin.py index 51bb11df..22409eb5 100644 --- a/plugins/AnnounceShare/AnnounceSharePlugin.py +++ b/plugins/AnnounceShare/AnnounceSharePlugin.py @@ -67,7 +67,7 @@ class TrackerStorage(object): def getTrackers(self, type="shared"): return self.file_content.setdefault(type, {}) - def getWorkingTrackers(self, type): + def getWorkingTrackers(self, type="shared"): trackers = { key: tracker for key, tracker in self.getTrackers(type).iteritems() if tracker["time_success"] > time.time() - 60 * 60 From 4f2352355f7077b33ea696dba814fbbf46b05694 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 27 Aug 2018 11:49:05 +0200 Subject: [PATCH 1027/2570] Return True when adding new tracker --- plugins/AnnounceShare/AnnounceSharePlugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/AnnounceShare/AnnounceSharePlugin.py b/plugins/AnnounceShare/AnnounceSharePlugin.py index 22409eb5..b9a1cc12 100644 --- a/plugins/AnnounceShare/AnnounceSharePlugin.py +++ b/plugins/AnnounceShare/AnnounceSharePlugin.py @@ -40,6 +40,8 @@ class TrackerStorage(object): "my": False } self.log.debug("New tracker found: %s" % tracker_address) + return True + trackers[tracker_address]["time_found"] = time.time() trackers[tracker_address]["my"] = my From 069c59d3d70001e564c5dcc5acc83eb5bbe78916 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 27 Aug 2018 11:49:24 +0200 Subject: [PATCH 1028/2570] Only allow zero trackers to be shared --- plugins/AnnounceShare/AnnounceSharePlugin.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/AnnounceShare/AnnounceSharePlugin.py b/plugins/AnnounceShare/AnnounceSharePlugin.py index b9a1cc12..93c4e6c9 100644 --- a/plugins/AnnounceShare/AnnounceSharePlugin.py +++ b/plugins/AnnounceShare/AnnounceSharePlugin.py @@ -20,8 +20,10 @@ class TrackerStorage(object): trackers = self.getTrackers() self.log.debug("Loaded %s shared trackers" % len(trackers)) - for tracker in trackers.values(): + for address, tracker in trackers.items(): tracker["num_error"] = 0 + if not address.startswith("zero://"): + del trackers[address] self.time_discover = 0.0 atexit.register(self.save) @@ -30,6 +32,9 @@ class TrackerStorage(object): return {"shared": {}} def onTrackerFound(self, tracker_address, type="shared", my=False): + if not tracker_address.startswith("zero://"): + return False + trackers = self.getTrackers() if tracker_address not in trackers: trackers[tracker_address] = { From c9c6a4b8678689a86739b58755296d802fa52b51 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 27 Aug 2018 11:50:13 +0200 Subject: [PATCH 1029/2570] Less frequent reannounce to zero trackers --- plugins/AnnounceZero/AnnounceZeroPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/AnnounceZero/AnnounceZeroPlugin.py b/plugins/AnnounceZero/AnnounceZeroPlugin.py index b86f6ae7..2c3b50d5 100644 --- a/plugins/AnnounceZero/AnnounceZeroPlugin.py +++ b/plugins/AnnounceZero/AnnounceZeroPlugin.py @@ -62,7 +62,7 @@ class SiteAnnouncerPlugin(object): full_announce = False else: # Multi: Announce all currently serving site full_announce = True - if time.time() - time_full_announced.get(tracker_address, 0) < 60 * 5: # No reannounce all sites within 5 minute + if time.time() - time_full_announced.get(tracker_address, 0) < 60 * 15: # No reannounce all sites within 5 minute return None time_full_announced[tracker_address] = time.time() from Site import SiteManager From 23284e1f351ee1c29484d8054b39aa80db0d344b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 27 Aug 2018 11:50:53 +0200 Subject: [PATCH 1030/2570] Reduce the reannounce to unreliable trackers not just single announces --- src/Site/SiteAnnouncer.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index 959dc5ac..819e3ffa 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -81,15 +81,6 @@ class SiteAnnouncer(object): trackers = self.getAnnouncingTrackers(mode) - if len(trackers) == 1: - tracker = trackers[0] - tracker_stats = global_stats[tracker] - # Reduce the announce time for trackers that looks unreliable - if tracker_stats["num_error"] > 5 and tracker_stats["time_request"] > time.time() - 60 * min(30, tracker_stats["num_error"]): - if config.verbose: - self.site.log.debug("Tracker %s looks unreliable, announce skipped (error: %s)" % (tracker, tracker_stats["num_error"])) - return - if config.verbose: self.site.log.debug("Tracker announcing, trackers: %s" % trackers) @@ -100,6 +91,12 @@ class SiteAnnouncer(object): num_announced = 0 for tracker in trackers: # Start announce threads + tracker_stats = global_stats[tracker] + # Reduce the announce time for trackers that looks unreliable + if tracker_stats["num_error"] > 5 and tracker_stats["time_request"] > time.time() - 60 * min(30, tracker_stats["num_error"]): + if config.verbose: + self.site.log.debug("Tracker %s looks unreliable, announce skipped (error: %s)" % (tracker, tracker_stats["num_error"])) + continue thread = gevent.spawn(self.announceTracker, tracker, mode=mode) threads.append(thread) thread.tracker = tracker From b45bbff97c023abed98bec972f0208d3bb8f3b5a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 27 Aug 2018 11:51:07 +0200 Subject: [PATCH 1031/2570] Rev3571 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 325ae98f..6aea575d 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3568 + self.rev = 3571 self.argv = argv self.action = None self.pending_changes = {} From f3b97dedc81ae386a1492de35c4ea1cf160ccfab Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 29 Aug 2018 19:51:26 +0200 Subject: [PATCH 1032/2570] Fix time_found record for tracker stats --- plugins/AnnounceShare/AnnounceSharePlugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/AnnounceShare/AnnounceSharePlugin.py b/plugins/AnnounceShare/AnnounceSharePlugin.py index 93c4e6c9..2e6fc282 100644 --- a/plugins/AnnounceShare/AnnounceSharePlugin.py +++ b/plugins/AnnounceShare/AnnounceSharePlugin.py @@ -36,6 +36,7 @@ class TrackerStorage(object): return False trackers = self.getTrackers() + added = False if tracker_address not in trackers: trackers[tracker_address] = { "time_added": time.time(), @@ -45,10 +46,11 @@ class TrackerStorage(object): "my": False } self.log.debug("New tracker found: %s" % tracker_address) - return True + 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() From d8654edd2ccdfc937fbd9dc5f4c0908db45f5ee6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 29 Aug 2018 19:52:10 +0200 Subject: [PATCH 1033/2570] Fix comment --- plugins/AnnounceZero/AnnounceZeroPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/AnnounceZero/AnnounceZeroPlugin.py b/plugins/AnnounceZero/AnnounceZeroPlugin.py index 2c3b50d5..6ac6e532 100644 --- a/plugins/AnnounceZero/AnnounceZeroPlugin.py +++ b/plugins/AnnounceZero/AnnounceZeroPlugin.py @@ -62,7 +62,7 @@ class SiteAnnouncerPlugin(object): 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 5 minute + 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 From 1cf50e5fcd81e279e77eec4d8f59a2bd94795830 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 29 Aug 2018 19:52:24 +0200 Subject: [PATCH 1034/2570] Fix chart collection when there is no site --- plugins/Chart/ChartCollector.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/Chart/ChartCollector.py b/plugins/Chart/ChartCollector.py index d3096975..471c4b91 100644 --- a/plugins/Chart/ChartCollector.py +++ b/plugins/Chart/ChartCollector.py @@ -27,6 +27,8 @@ class ChartCollector(object): collectors = {} file_server = sys.modules["main"].file_server sites = file_server.sites + if not sites: + return collectors content_db = sites.values()[0].content_manager.contents.db # Connection stats From 1cce926ef8e4058ef317939234175ddac5440e8d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 29 Aug 2018 19:52:55 +0200 Subject: [PATCH 1035/2570] Record handshake time for connections --- src/Connection/Connection.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 5305b130..10a0111f 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -19,7 +19,7 @@ from util import helper class Connection(object): __slots__ = ( "sock", "sock_wrapped", "ip", "port", "cert_pin", "target_onion", "id", "protocol", "type", "server", "unpacker", "req_id", - "handshake", "crypt", "connected", "event_connected", "closed", "start_time", "last_recv_time", "is_private_ip", "is_tracker_connection", + "handshake", "crypt", "connected", "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", "updateName", "waiting_requests", "waiting_streams" ) @@ -56,6 +56,7 @@ class Connection(object): # Stats self.start_time = time.time() + self.handshake_time = 0 self.last_recv_time = 0 self.last_message_time = 0 self.last_send_time = 0 @@ -387,6 +388,7 @@ class Connection(object): self.event_connected.set(True) # Mark handshake as done self.event_connected = None + self.handshake_time = time.time() # Handle incoming message def handleMessage(self, message): From 16f7dafcdff747929289bcf61d69e60d9c958a1b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 29 Aug 2018 19:53:24 +0200 Subject: [PATCH 1036/2570] Only record offline internet if there was message received before --- 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 99dd5319..7abd76d7 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -266,7 +266,7 @@ class ConnectionServer(object): # 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: + if self.has_internet and last_message_time: self.has_internet = False self.onInternetOffline() else: From ee97eb6d276cd9cd2d16eadf6060152bb0bebbfd Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 29 Aug 2018 19:53:44 +0200 Subject: [PATCH 1037/2570] Calculate media time correction from connections --- src/Connection/ConnectionServer.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index 7abd76d7..9959abff 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -306,3 +306,15 @@ class ConnectionServer(object): def onInternetOffline(self): self.log.info("Internet offline") + + def getTimeCorrection(self): + corrections = sorted([ + connection.handshake.get("time") - connection.handshake_time + connection.last_ping_delay + for connection in self.connections + if connection.handshake.get("time") and connection.last_ping_delay + ]) + if len(corrections) < 6: + return 0.0 + mid = len(corrections) / 2 - 1 + median = (corrections[mid - 1] + corrections[mid] + corrections[mid + 1]) / 3 + return median From 3a9ded9315f6e3ec4543ce6c464c38172dcbecba Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 29 Aug 2018 19:54:31 +0200 Subject: [PATCH 1038/2570] On startup don't run more than 5 checkSite at the same time --- 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 0e4a8a79..a0b27c31 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -6,6 +6,7 @@ import random import socket import gevent +import gevent.pool import util from Config import config @@ -263,6 +264,7 @@ class FileServer(ConnectionServer): @util.Noparallel() def checkSites(self, check_files=False, force_port_check=False): self.log.debug("Checking sites...") + s = time.time() sites_checking = False if self.port_opened is None 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 @@ -277,11 +279,13 @@ class FileServer(ConnectionServer): self.tor_manager.startOnions() if not sites_checking: + check_pool = gevent.pool.Pool(5) for site in sorted(self.sites.values(), key=lambda site: site.settings.get("modified", 0), reverse=True): # Check sites integrity - check_thread = gevent.spawn(self.checkSite, site, check_files) # Check in new thread + 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)) def cleanupSites(self): import gc From b96559b76f63d13d25b3dad47f65c029e71b0d39 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 29 Aug 2018 19:55:04 +0200 Subject: [PATCH 1039/2570] Reduce connection requirements on startup --- src/File/FileServer.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index a0b27c31..7e6bf1ef 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -257,8 +257,6 @@ class FileServer(ConnectionServer): site.update(check_files=check_files) # Update site's content.json and download changed files site.sendMyHashfield() site.updateHashfield() - if len(site.peers) > 5: # Keep active connections if site having 5 or more peers - site.needConnections() # Check sites integrity @util.Noparallel() From 6edabeeecc1d40394e4faad7b59e49b958077ce5 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 29 Aug 2018 19:55:50 +0200 Subject: [PATCH 1040/2570] Log hashfield query time --- src/Site/Site.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index a3950279..f6b0da01 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -947,6 +947,7 @@ class Site(object): if not self.content_manager.hashfield and not self.content_manager.contents.get("content.json", {}).get("files_optional"): return False + s = time.time() queried = 0 connected_peers = self.getConnectedPeers() for peer in connected_peers: @@ -957,7 +958,7 @@ class Site(object): if queried >= limit: break if queried: - self.log.debug("Queried hashfield from %s peers" % queried) + self.log.debug("Queried hashfield from %s peers in %.3fs" % (queried, time.time() - s)) return queried # Returns if the optional file is need to be downloaded or not From 0efa6a58166108323e42f32b24e0ca375c231677 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 29 Aug 2018 19:56:27 +0200 Subject: [PATCH 1041/2570] Time correction to stats page --- plugins/Stats/StatsPlugin.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py index 22e106ca..6957e76b 100644 --- a/plugins/Stats/StatsPlugin.py +++ b/plugins/Stats/StatsPlugin.py @@ -65,17 +65,19 @@ class UiRequestPlugin(object): """ # Memory + yield "rev%s | " % config.rev + yield "%s | " % config.ip_external + yield "Port: %s | " % main.file_server.port + yield "Opened: %s | " % main.file_server.port_opened + yield "Crypt: %s | " % CryptConnection.manager.crypt_supported + yield "In: %.2fMB, Out: %.2fMB | " % ( + float(main.file_server.bytes_recv) / 1024 / 1024, + float(main.file_server.bytes_sent) / 1024 / 1024 + ) + yield "Peerid: %s | " % main.file_server.peer_id + yield "Time correction: %.2fs" % main.file_server.getTimeCorrection() + try: - yield "rev%s | " % config.rev - yield "%s | " % config.ip_external - yield "Port: %s | " % main.file_server.port - yield "Opened: %s | " % main.file_server.port_opened - yield "Crypt: %s | " % CryptConnection.manager.crypt_supported - yield "In: %.2fMB, Out: %.2fMB | " % ( - float(main.file_server.bytes_recv) / 1024 / 1024, - float(main.file_server.bytes_sent) / 1024 / 1024 - ) - yield "Peerid: %s | " % main.file_server.peer_id import psutil process = psutil.Process(os.getpid()) mem = process.get_memory_info()[0] / float(2 ** 20) From ad02f384d211a56ca7f436bb95ee9f89f19f2045 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 29 Aug 2018 19:57:38 +0200 Subject: [PATCH 1042/2570] Connection time correction on stats page --- plugins/Stats/StatsPlugin.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py index 6957e76b..27719417 100644 --- a/plugins/Stats/StatsPlugin.py +++ b/plugins/Stats/StatsPlugin.py @@ -97,12 +97,16 @@ class UiRequestPlugin(object): ) yield "" yield "" - yield "" + yield "" for connection in main.file_server.connections: if "cipher" in dir(connection.sock): cipher = connection.sock.cipher()[0] else: cipher = connection.crypt + if "time" in connection.handshake and connection.last_ping_delay: + time_correction = connection.handshake["time"] - connection.handshake_time - connection.last_ping_delay + else: + time_correction = 0.0 yield self.formatTableRow([ ("%3d", connection.id), ("%s", connection.type), @@ -114,13 +118,14 @@ class UiRequestPlugin(object): ("%s", connection.bad_actions), ("since", max(connection.last_send_time, connection.last_recv_time)), ("since", connection.start_time), - ("%.3f", connection.last_sent_time - connection.last_send_time), + ("%.3f", max(-1, connection.last_sent_time - connection.last_send_time)), ("%.3f", connection.cpu_time), ("%.0fkB", connection.bytes_sent / 1024), ("%.0fkB", connection.bytes_recv / 1024), ("%s", (connection.last_cmd_recv, connection.last_cmd_sent)), ("%s", connection.waiting_requests.keys()), ("%s r%s", (connection.handshake.get("version"), connection.handshake.get("rev", "?"))), + ("%.2fs", time_correction), ("%s", connection.sites) ]) yield "
        id type ip open crypt pingbuff bad idle open delay cpu out in last sentwait version sites
        wait version time sites
        " From 70c1e57ff0a70044ff60ea30fc7dc06ca7c258a2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 29 Aug 2018 19:57:54 +0200 Subject: [PATCH 1043/2570] Shared tracker found time to stats page --- plugins/Stats/StatsPlugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py index 27719417..dfa6a7d7 100644 --- a/plugins/Stats/StatsPlugin.py +++ b/plugins/Stats/StatsPlugin.py @@ -145,12 +145,13 @@ class UiRequestPlugin(object): if "AnnounceShare" in PluginManager.plugin_manager.plugin_names: yield "

        Shared trackers:
        " - yield "" + yield "
        address added latency successive errors last_success
        " from AnnounceShare import AnnounceSharePlugin for tracker_address, tracker_stat in AnnounceSharePlugin.tracker_storage.getTrackers().iteritems(): yield self.formatTableRow([ ("%s", tracker_address), ("%.0f min ago", min(999, (time.time() - tracker_stat["time_added"]) / 60)), + ("%.0f min ago", min(999, (time.time() - tracker_stat.get("time_found", 0)) / 60)), ("%.3fs", tracker_stat["latency"]), ("%s", tracker_stat["num_error"]), ("%.0f min ago", min(999, (time.time() - tracker_stat["time_success"]) / 60)), From 3b6b79229f40643f43efe780c43e03fc9b78fb55 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 29 Aug 2018 19:58:18 +0200 Subject: [PATCH 1044/2570] Db table stats --- plugins/Stats/StatsPlugin.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py index dfa6a7d7..793bf15b 100644 --- a/plugins/Stats/StatsPlugin.py +++ b/plugins/Stats/StatsPlugin.py @@ -1,6 +1,7 @@ import time import cgi import os +import json from Plugin import PluginManager from Config import config @@ -169,7 +170,15 @@ class UiRequestPlugin(object): # Db yield "

        Db:
        " for db in sys.modules["Db.Db"].opened_dbs: - yield "- %.3fs: %s
        " % (time.time() - db.last_query_time, db.db_path.encode("utf8")) + tables = [row["name"] for row in db.execute("SELECT name FROM sqlite_master WHERE type = 'table'").fetchall()] + table_rows = {} + for table in tables: + table_rows[table] = db.execute("SELECT COUNT(*) AS c FROM %s" % table).fetchone()["c"] + db_size = os.path.getsize(db.db_path) / 1024.0 / 1024.0 + yield "- %.3fs: %s %.3fMB, table rows: %s
        " % ( + time.time() - db.last_query_time, db.db_path.encode("utf8"), db_size, json.dumps(table_rows, sort_keys=True) + ) + # Sites yield "

        Sites:" From e102d256b2584fdf3c716a82d8b8b0d2c0497638 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 29 Aug 2018 19:58:23 +0200 Subject: [PATCH 1045/2570] Formatting --- plugins/Stats/StatsPlugin.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py index 793bf15b..e165bf62 100644 --- a/plugins/Stats/StatsPlugin.py +++ b/plugins/Stats/StatsPlugin.py @@ -143,7 +143,6 @@ class UiRequestPlugin(object): ]) yield "
        address added found latency successive errors last_success
        " - if "AnnounceShare" in PluginManager.plugin_manager.plugin_names: yield "

        Shared trackers:
        " yield "" @@ -159,9 +158,6 @@ class UiRequestPlugin(object): ]) yield "
        address added found latency successive errors last_success
        " - - - # Tor hidden services yield "

        Tor hidden services (status: %s):
        " % main.file_server.tor_manager.status for site_address, onion in main.file_server.tor_manager.site_onions.items(): From 1ce87ecfa85f870df75699a84e3cbb892c47dd6c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 29 Aug 2018 19:58:33 +0200 Subject: [PATCH 1046/2570] Rev3576 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 6aea575d..2c2afb14 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3571 + self.rev = 3576 self.argv = argv self.action = None self.pending_changes = {} From 3f1a103e821fc00b017b6716a67ae4d7e82aceb7 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Fri, 31 Aug 2018 19:46:45 +0300 Subject: [PATCH 1047/2570] Allow 'fileList' command on CORS --- plugins/Cors/CorsPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Cors/CorsPlugin.py b/plugins/Cors/CorsPlugin.py index a157bd41..8d758988 100644 --- a/plugins/Cors/CorsPlugin.py +++ b/plugins/Cors/CorsPlugin.py @@ -27,7 +27,7 @@ class UiWebsocketPlugin(object): if super(UiWebsocketPlugin, self).hasSitePermission(address, cmd=cmd): return True - if not "Cors:%s" % address in self.site.settings["permissions"] or cmd not in ["fileQuery", "dbQuery", "userGetSettings", "siteInfo"]: + if not "Cors:%s" % address in self.site.settings["permissions"] or cmd not in ["fileGet", "fileList", "dirList", "fileRules", "optionalFileInfo", "fileQuery", "dbQuery", "userGetSettings", "siteInfo"]: return False else: return True From 710a85429e9fdc19575f137c49771bf9fa2fb9a8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 2 Sep 2018 02:17:18 +0200 Subject: [PATCH 1048/2570] Remove unreliable shared trackers earlier if we have atleast 4 working one --- plugins/AnnounceShare/AnnounceSharePlugin.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/AnnounceShare/AnnounceSharePlugin.py b/plugins/AnnounceShare/AnnounceSharePlugin.py index 2e6fc282..80e71a69 100644 --- a/plugins/AnnounceShare/AnnounceSharePlugin.py +++ b/plugins/AnnounceShare/AnnounceSharePlugin.py @@ -69,7 +69,13 @@ class TrackerStorage(object): trackers[tracker_address]["time_error"] = time.time() trackers[tracker_address]["num_error"] += 1 - if trackers[tracker_address]["num_error"] > 30 and trackers[tracker_address]["time_success"] < time.time() - 60 * 60: + if len(self.getWorkingTrackers()) > 4: + error_limit = 15 + 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] From 2affb9b8635811256fbf031483d6c12428dd8137 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 2 Sep 2018 02:17:42 +0200 Subject: [PATCH 1049/2570] Configurable wokring shared trackers limit --- plugins/AnnounceShare/AnnounceSharePlugin.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/plugins/AnnounceShare/AnnounceSharePlugin.py b/plugins/AnnounceShare/AnnounceSharePlugin.py index 80e71a69..9c27f7e4 100644 --- a/plugins/AnnounceShare/AnnounceSharePlugin.py +++ b/plugins/AnnounceShare/AnnounceSharePlugin.py @@ -105,7 +105,7 @@ class TrackerStorage(object): self.log.debug("Saved in %.3fs" % (time.time() - s)) def discoverTrackers(self, peers): - if len(self.getWorkingTrackers()) > 5: + if len(self.getWorkingTrackers()) > config.working_shared_trackers_limit: return False s = time.time() num_success = 0 @@ -176,3 +176,11 @@ class FileServerPlugin(object): my_tracker_address = "zero://%s:%s" % (config.ip_external, 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() From 12c95e6fca73714af7441b0547bd528fec19e8f2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 2 Sep 2018 02:18:11 +0200 Subject: [PATCH 1050/2570] Fix marking peers from zero trackers --- plugins/AnnounceZero/AnnounceZeroPlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/AnnounceZero/AnnounceZeroPlugin.py b/plugins/AnnounceZero/AnnounceZeroPlugin.py index 6ac6e532..7cb1e48d 100644 --- a/plugins/AnnounceZero/AnnounceZeroPlugin.py +++ b/plugins/AnnounceZero/AnnounceZeroPlugin.py @@ -25,14 +25,14 @@ def processPeerRes(tracker_address, site, peers): for packed_address in peers["ip4"]: found_ip4 += 1 peer_ip, peer_port = helper.unpackAddress(packed_address) - if site.addPeer(peer_ip, peer_port): + if site.addPeer(peer_ip, peer_port, source="tracker"): added += 1 # 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): + if site.addPeer(peer_onion, peer_port, source="tracker"): added += 1 if added: From 89c871f171a7b82f5c92b1c7e19ef83b0c94d8e6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 2 Sep 2018 02:18:57 +0200 Subject: [PATCH 1051/2570] Fix newsfeed listing during new site added --- plugins/Newsfeed/NewsfeedPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Newsfeed/NewsfeedPlugin.py b/plugins/Newsfeed/NewsfeedPlugin.py index 3d7434bf..b2fc78b0 100644 --- a/plugins/Newsfeed/NewsfeedPlugin.py +++ b/plugins/Newsfeed/NewsfeedPlugin.py @@ -37,7 +37,7 @@ class UiWebsocketPlugin(object): total_s = time.time() num_sites = 0 - for address, site_data in self.user.sites.iteritems(): + for address, site_data in self.user.sites.items(): feeds = site_data.get("follow") if not feeds: continue From 3869cb9bf36b4efba90461f4f7a57dd4eb763aa2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 2 Sep 2018 02:19:51 +0200 Subject: [PATCH 1052/2570] Show incoming and outgoing connection separatley in stats page --- plugins/Stats/StatsPlugin.py | 4 ++-- src/Connection/ConnectionServer.py | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py index e165bf62..1f3ae99a 100644 --- a/plugins/Stats/StatsPlugin.py +++ b/plugins/Stats/StatsPlugin.py @@ -93,8 +93,8 @@ class UiRequestPlugin(object): yield "
        " # Connections - yield "Connections (%s, total made: %s):
        " % ( - len(main.file_server.connections), main.file_server.last_connection_id + yield "Connections (%s, total made: %s, in: %s, out: %s):
        " % ( + len(main.file_server.connections), main.file_server.last_connection_id, main.file_server.num_incoming, main.file_server.num_outgoing ) yield "" yield "" diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index 9959abff..1852437f 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -45,6 +45,9 @@ class ConnectionServer(object): self.num_recv = 0 self.num_sent = 0 + self.num_incoming = 0 + self.num_outgoing = 0 + # Bittorrent style peerid self.peer_id = "-UT3530-%s" % CryptHash.random(12, "base64") @@ -96,6 +99,7 @@ class ConnectionServer(object): def handleIncomingConnection(self, sock, addr): ip, port = addr + self.num_incoming += 1 # Connection flood protection if ip in self.ip_incoming and ip not in self.whitelist: @@ -164,6 +168,7 @@ class ConnectionServer(object): connection = Connection(self, ip, port, target_onion=site_onion, is_tracker_connection=is_tracker_connection) else: connection = Connection(self, ip, port, is_tracker_connection=is_tracker_connection) + self.num_outgoing += 1 self.ips[key] = connection self.connections.append(connection) succ = connection.connect() From 6c2eae29f5e37ab85732e8573d51cf997dc6ee71 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 2 Sep 2018 02:20:02 +0200 Subject: [PATCH 1053/2570] Sort trackers in stats page --- plugins/Stats/StatsPlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py index 1f3ae99a..38920de8 100644 --- a/plugins/Stats/StatsPlugin.py +++ b/plugins/Stats/StatsPlugin.py @@ -134,7 +134,7 @@ class UiRequestPlugin(object): # Trackers yield "

        Trackers:
        " yield "
        id type ip open crypt pingbuff bad idle open delay cpu out in last sent
        " - for tracker_address, tracker_stat in sys.modules["Site.SiteAnnouncer"].global_stats.iteritems(): + for tracker_address, tracker_stat in sorted(sys.modules["Site.SiteAnnouncer"].global_stats.iteritems()): yield self.formatTableRow([ ("%s", tracker_address), ("%s", tracker_stat["num_request"]), @@ -147,7 +147,7 @@ class UiRequestPlugin(object): yield "

        Shared trackers:
        " yield "
        address request successive errors last_request
        " from AnnounceShare import AnnounceSharePlugin - for tracker_address, tracker_stat in AnnounceSharePlugin.tracker_storage.getTrackers().iteritems(): + for tracker_address, tracker_stat in sorted(AnnounceSharePlugin.tracker_storage.getTrackers().iteritems()): yield self.formatTableRow([ ("%s", tracker_address), ("%.0f min ago", min(999, (time.time() - tracker_stat["time_added"]) / 60)), From 47fcee4df86316bc34c06f0e6c00f3b9a8863615 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 2 Sep 2018 02:21:38 +0200 Subject: [PATCH 1054/2570] Limit peer reputation --- src/Peer/Peer.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index d7632081..0a56d158 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -65,6 +65,11 @@ class Peer(object): # Connect to host def connect(self, connection=None): + if self.reputation < -10: + self.reputation = -10 + if self.reputation > 10: + self.reputation = 10 + if self.connection: self.log("Getting connection (Closing %s)..." % self.connection) self.connection.close("Connection change") From 4241b6276083ac4bfd92f4295552d933c1d43007 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 2 Sep 2018 02:21:48 +0200 Subject: [PATCH 1055/2570] Log peer reputation on connect --- 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 0a56d158..907b8057 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -74,7 +74,7 @@ class Peer(object): self.log("Getting connection (Closing %s)..." % self.connection) self.connection.close("Connection change") else: - self.log("Getting connection...") + self.log("Getting connection (reputation: %s)..." % self.reputation) if connection: # Connection specified self.log("Assigning connection %s" % connection) From 49562d43c3b2b7da4b5e0d2fff530f35d1bed782 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 2 Sep 2018 02:22:21 +0200 Subject: [PATCH 1056/2570] Change peer reputation on connection success and error --- src/Peer/Peer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index 907b8057..1146b63d 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -91,6 +91,7 @@ class Peer(object): else: connection_server = sys.modules["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 except Exception, err: self.onConnectionError("Getting connection error") @@ -350,6 +351,7 @@ class Peer(object): limit = 3 else: limit = 6 + self.reputation -= 1 if self.connection_error >= limit: # Dead peer self.remove("Peer connection: %s" % reason) From 658aace8f1fdb932112e01a3699fe72b3c1d1ea0 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 2 Sep 2018 02:22:35 +0200 Subject: [PATCH 1057/2570] Increase peer reputation when found by tracker --- src/Peer/Peer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index 1146b63d..3f06f305 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -125,9 +125,9 @@ class Peer(object): # Found a peer from a source def found(self, source="other"): if source == "tracker": - self.reputation += 10 + self.reputation += 1 elif source == "local": - self.reputation += 30 + self.reputation += 3 if source in ("tracker", "local"): self.site.peers_recent.appendleft(self) self.time_found = time.time() From 313d9a4ef996f2235078b15751cd1abde1e6fa4d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 2 Sep 2018 02:23:06 +0200 Subject: [PATCH 1058/2570] Need less peer sites with less peers --- src/Site/Site.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index f6b0da01..7a22e6ba 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -777,7 +777,12 @@ class Site(object): self.announcer.announce(*args, **kwargs) # Keep connections to get the updates - def needConnections(self, num=6, check_site_on_reconnect=False): + 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 connected = len(self.getConnectedPeers()) From a5fcc7d65f97b9af38ec32101b85d18e6b5af174 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 2 Sep 2018 02:23:19 +0200 Subject: [PATCH 1059/2570] Sort recent peers by reputation --- src/Site/Site.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 7a22e6ba..2c42b801 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -845,7 +845,11 @@ class Site(object): self.log.debug("Recent peers %s of %s (need: %s)" % (len(found), len(self.peers_recent), need_num)) if len(found) >= need_num or len(found) >= len(self.peers): - return found[0:need_num] + return sorted( + found, + key=lambda peer: peer.reputation, + reverse=True + )[0:need_num] # Add random peers need_more = need_num - len(found) From b84abea6700e2e9d2f8e0154b6d385e91961c243 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 2 Sep 2018 02:23:46 +0200 Subject: [PATCH 1060/2570] Order more peers for connected peers simply by reputation --- src/Site/Site.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 2c42b801..1560eadd 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -855,10 +855,9 @@ class Site(object): need_more = need_num - len(found) found_more = sorted( self.peers.values()[0:need_more * 50], - key=lambda peer: peer.time_found + peer.reputation * 60, + key=lambda peer: peer.reputation, reverse=True )[0:need_more * 2] - random.shuffle(found_more) found += found_more From 4fe33268ac10c816a134abfb2d0b04ca1c2f076c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 2 Sep 2018 02:24:07 +0200 Subject: [PATCH 1061/2570] Don't allow parallel rebuild --- 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 4d08908e..41699818 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -114,6 +114,7 @@ class SiteStorage(object): time.sleep(0.000001) # Context switch to avoid UI block # Rebuild sql cache + @util.Noparallel() def rebuildDb(self, delete_db=True): self.has_db = self.isFile("dbschema.json") if not self.has_db: @@ -369,7 +370,7 @@ class SiteStorage(object): inner_path = "" else: if path.startswith(self.directory): - inner_path = path[len(self.directory)+1:] + inner_path = path[len(self.directory) + 1:] else: raise Exception(u"File not allowed: %s" % path) return inner_path From 7dcde3e5852a12b245e213ae6b2902182e612080 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 2 Sep 2018 02:26:17 +0200 Subject: [PATCH 1062/2570] Store peer reputation, time_found in peer db --- plugins/PeerDb/PeerDbPlugin.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/plugins/PeerDb/PeerDbPlugin.py b/plugins/PeerDb/PeerDbPlugin.py index 88a36f69..d06da1e1 100644 --- a/plugins/PeerDb/PeerDbPlugin.py +++ b/plugins/PeerDb/PeerDbPlugin.py @@ -22,12 +22,14 @@ class ContentDbPlugin(object): ["address", "TEXT NOT NULL"], ["port", "INTEGER NOT NULL"], ["hashfield", "BLOB"], - ["time_added", "INTEGER NOT NULL"] + ["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": 1 + "schema_changed": 2 } return schema @@ -46,7 +48,8 @@ class ContentDbPlugin(object): peer.hashfield.replaceFromString(row["hashfield"]) num_hashfield += 1 peer.time_added = row["time_added"] - peer.reputation = int((time.time() - peer.time_added) / (60 * 60 * 24)) # Boost reputation for older peers (1 point for every day) + peer.time_found = row["time_found"] + peer.reputation = row["reputation"] if row["address"].endswith(".onion"): peer.reputation = peer.reputation / 2 # Onion peers less likely working num += 1 @@ -62,7 +65,7 @@ class ContentDbPlugin(object): hashfield = sqlite3.Binary(peer.hashfield.tostring()) else: hashfield = "" - yield (site_id, address, port, hashfield, int(peer.time_added)) + yield (site_id, address, port, hashfield, peer.reputation, int(peer.time_added), int(peer.time_found)) def savePeers(self, site, spawn=False): if spawn: @@ -78,7 +81,7 @@ class ContentDbPlugin(object): try: self.execute("DELETE FROM peer WHERE site_id = :site_id", {"site_id": site_id}) self.cur.cursor.executemany( - "INSERT INTO peer (site_id, address, port, hashfield, time_added) VALUES (?, ?, ?, ?, ?)", + "INSERT INTO peer (site_id, address, port, hashfield, reputation, time_added, time_found) VALUES (?, ?, ?, ?, ?, ?, ?)", self.iteratePeers(site) ) except Exception as err: From 1b0055ac6123d5560d8a7fdbb34b2fd0ad1365ce Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 2 Sep 2018 02:26:42 +0200 Subject: [PATCH 1063/2570] Less reputation for onion peers --- plugins/PeerDb/PeerDbPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/PeerDb/PeerDbPlugin.py b/plugins/PeerDb/PeerDbPlugin.py index d06da1e1..f9be1b06 100644 --- a/plugins/PeerDb/PeerDbPlugin.py +++ b/plugins/PeerDb/PeerDbPlugin.py @@ -51,7 +51,7 @@ class ContentDbPlugin(object): peer.time_found = row["time_found"] peer.reputation = row["reputation"] if row["address"].endswith(".onion"): - peer.reputation = peer.reputation / 2 # Onion peers less likely working + peer.reputation = peer.reputation / 2 - 1 # Onion peers less likely working num += 1 if num_hashfield: site.content_manager.has_optional_files = True From a4224555d221835c363f8b5cfd2ff835f92aad10 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 2 Sep 2018 02:33:44 +0200 Subject: [PATCH 1064/2570] Increase gzipped json file size limit --- src/util/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/helper.py b/src/util/helper.py index 7818fc92..8f8b29f4 100644 --- a/src/util/helper.py +++ b/src/util/helper.py @@ -209,7 +209,7 @@ def limitedGzipFile(*args, **kwargs): import gzip class LimitedGzipFile(gzip.GzipFile): def read(self, size=-1): - return super(LimitedGzipFile, self).read(1024*1024*6) + return super(LimitedGzipFile, self).read(1024*1024*25) return LimitedGzipFile(*args, **kwargs) def avg(items): From a97f5a8488433d027f4c24c937e4ead9fd1afc50 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 2 Sep 2018 02:34:47 +0200 Subject: [PATCH 1065/2570] Rev3582 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 2c2afb14..1fae50e4 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3576 + self.rev = 3582 self.argv = argv self.action = None self.pending_changes = {} From 12d494bf38ab3c9d73411d0d90b649a921f699a0 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 4 Sep 2018 15:43:20 +0200 Subject: [PATCH 1066/2570] Limit peer reputation increase from tracker --- src/Peer/Peer.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index 3f06f305..c56fb63e 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -124,10 +124,12 @@ class Peer(object): # Found a peer from a source def found(self, source="other"): - if source == "tracker": - self.reputation += 1 - elif source == "local": - self.reputation += 3 + if self.reputation < 5: + if source == "tracker": + self.reputation += 1 + elif source == "local": + self.reputation += 3 + if source in ("tracker", "local"): self.site.peers_recent.appendleft(self) self.time_found = time.time() From a5c25ce438a08fce1a6dd2d7991b2f5cf0b4d258 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 4 Sep 2018 15:43:52 +0200 Subject: [PATCH 1067/2570] Dont add removed trackers to announcer stats --- src/Ui/UiWebsocket.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index b6e76151..bdd89223 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -392,11 +392,14 @@ class UiWebsocket(object): def actionAnnouncerStats(self, to): back = {} + trackers = self.site.announcer.getTrackers() for site in self.server.sites.values(): for tracker, stats in site.announcer.stats.iteritems(): + if tracker not in trackers: + continue if tracker not in back: back[tracker] = {} - is_latest_data = stats["time_request"] > back[tracker].get("time_request", 0) and stats["status"] + is_latest_data = bool(stats["time_request"] > back[tracker].get("time_request", 0) and stats["status"]) for key, val in stats.iteritems(): if key.startswith("num_"): back[tracker][key] = back[tracker].get(key, 0) + val From 8692ea1a748df9f8b8c80c02263bc45c953bba94 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 4 Sep 2018 15:44:23 +0200 Subject: [PATCH 1068/2570] Rev3585 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 1fae50e4..b00580b0 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3582 + self.rev = 3585 self.argv = argv self.action = None self.pending_changes = {} From f1c89f0917b32056bdf78740553cc40a9b2e290f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 5 Sep 2018 14:31:26 +0200 Subject: [PATCH 1069/2570] Stats page show peer reputation --- plugins/Stats/StatsPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py index 38920de8..017d0504 100644 --- a/plugins/Stats/StatsPlugin.py +++ b/plugins/Stats/StatsPlugin.py @@ -212,7 +212,7 @@ class UiRequestPlugin(object): if site.content_manager.has_optional_files: yield "Optional files: %4s " % len(peer.hashfield) time_added = (time.time() - peer.time_added) / (60 * 60 * 24) - yield "(#%4s, err: %s, found: %3s min, add: %.1f day) %30s -
        " % (connection_id, peer.connection_error, time_found, time_added, key) + yield "(#%4s, rep: %2s, err: %s, found: %3s min, add: %.1f day) %30s -
        " % (connection_id, peer.reputation, peer.connection_error, time_found, time_added, key) yield "
        " yield "
        address added found latency successive errors last_success
        " From 3b7ec3d9130030a9506e7d1ef31d303d8c9f5562 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 5 Sep 2018 14:32:01 +0200 Subject: [PATCH 1070/2570] Lowercased Timecorrection function --- plugins/Stats/StatsPlugin.py | 2 +- src/Connection/ConnectionServer.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py index 017d0504..f675b37a 100644 --- a/plugins/Stats/StatsPlugin.py +++ b/plugins/Stats/StatsPlugin.py @@ -76,7 +76,7 @@ class UiRequestPlugin(object): float(main.file_server.bytes_sent) / 1024 / 1024 ) yield "Peerid: %s | " % main.file_server.peer_id - yield "Time correction: %.2fs" % main.file_server.getTimeCorrection() + yield "Time correction: %.2fs" % main.file_server.getTimecorrection() try: import psutil diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index 1852437f..ab1a06ee 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -312,7 +312,7 @@ class ConnectionServer(object): def onInternetOffline(self): self.log.info("Internet offline") - def getTimeCorrection(self): + def getTimecorrection(self): corrections = sorted([ connection.handshake.get("time") - connection.handshake_time + connection.last_ping_delay for connection in self.connections From 21a80650b07f26f054a59b8d2bb76bfe05cb8ef7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 5 Sep 2018 14:32:23 +0200 Subject: [PATCH 1071/2570] Cache timecorrection value --- src/Connection/ConnectionServer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index ab1a06ee..2a60af64 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -48,6 +48,8 @@ class ConnectionServer(object): self.num_incoming = 0 self.num_outgoing = 0 + self.timecorrection = 0.0 + # Bittorrent style peerid self.peer_id = "-UT3530-%s" % CryptHash.random(12, "base64") @@ -280,6 +282,8 @@ class ConnectionServer(object): self.has_internet = True self.onInternetOnline() + self.timecorrection = self.getTimecorrection() + if time.time() - s > 0.01: self.log.debug("Connection cleanup in %.3fs" % (time.time() - s)) self.log.debug("Checkconnections ended") From 5f1e7ffd0cd19e6a342733de7f00c49378fa463f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 5 Sep 2018 14:32:43 +0200 Subject: [PATCH 1072/2570] Correct sent time with timecorrection value --- 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 10a0111f..04cdc7ea 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -336,7 +336,7 @@ class Connection(object): "rev": config.rev, "crypt_supported": crypt_supported, "crypt": self.crypt, - "time": int(time.time()) + "time": int(time.time() + self.server.timecorrection) } if self.target_onion: handshake["onion"] = self.target_onion From dc52a8a08bf1ad6c2e44acff3ce8c42f075da949 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 5 Sep 2018 14:33:21 +0200 Subject: [PATCH 1073/2570] Use shared file_server object when generating server info for websocket --- src/Ui/UiWebsocket.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index bdd89223..b7057a5b 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -315,14 +315,15 @@ class UiWebsocket(object): return ret def formatServerInfo(self): + file_server = sys.modules["main"].file_server return { - "ip_external": sys.modules["main"].file_server.port_opened, + "ip_external": file_server.port_opened, "platform": sys.platform, "fileserver_ip": config.fileserver_ip, "fileserver_port": config.fileserver_port, - "tor_enabled": sys.modules["main"].file_server.tor_manager.enabled, - "tor_status": sys.modules["main"].file_server.tor_manager.status, - "tor_has_meek_bridges": sys.modules["main"].file_server.tor_manager.has_meek_bridges, + "tor_enabled": file_server.tor_manager.enabled, + "tor_status": file_server.tor_manager.status, + "tor_has_meek_bridges": file_server.tor_manager.has_meek_bridges, "tor_use_bridges": config.tor_use_bridges, "ui_ip": config.ui_ip, "ui_port": config.ui_port, From b06ba06f40ae964248215af9c7edbac88bc5d086 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 5 Sep 2018 14:33:31 +0200 Subject: [PATCH 1074/2570] Add timecorrection to server info --- src/Ui/UiWebsocket.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index b7057a5b..45f4a2b6 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -329,6 +329,7 @@ class UiWebsocket(object): "ui_port": config.ui_port, "version": config.version, "rev": config.rev, + "timecorrection": file_server.timecorrection, "language": config.language, "debug": config.debug, "plugins": PluginManager.plugin_manager.plugin_names From 317bd2fcec1e7cb7b7d55367fa5bac9eabc4811f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 5 Sep 2018 14:34:00 +0200 Subject: [PATCH 1075/2570] Rev3588 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index b00580b0..f74a2f86 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3585 + self.rev = 3588 self.argv = argv self.action = None self.pending_changes = {} From c8669328619f46f82ef46419b8f1566db05390d6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 6 Sep 2018 00:54:57 +0200 Subject: [PATCH 1076/2570] Full fileList/fileGet/dirList support for packed files --- plugins/FilePack/FilePackPlugin.py | 87 ++++++++++++++++++++++++------ 1 file changed, 72 insertions(+), 15 deletions(-) diff --git a/plugins/FilePack/FilePackPlugin.py b/plugins/FilePack/FilePackPlugin.py index 20c290f3..8d662bba 100644 --- a/plugins/FilePack/FilePackPlugin.py +++ b/plugins/FilePack/FilePackPlugin.py @@ -107,31 +107,88 @@ class SiteStoragePlugin(object): 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) + + try: + archive = openArchive(archive_path, file_obj=file_obj) + except Exception as err: + raise Exception("Unable to download file: %s" % 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|tar.bz2|zip))(.*)", inner_path) archive_inner_path, path_within = match.groups() - archive_path = self.getPath(archive_inner_path) - file_obj = None - if archive_path not in archive_cache: - if not os.path.isfile(archive_path): - result = self.site.needFile(archive_inner_path, priority=10) - self.site.updateWebsocket(file_done=archive_inner_path) - if not result: - raise Exception("Unable to download file") - file_obj = self.site.storage.openBigfile(archive_inner_path) - - try: - archive = openArchive(archive_path, file_obj=file_obj) - except Exception as err: - raise Exception("Unable to download file: %s" % err) + 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()] - return namelist + + 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|tar.bz2|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="r"): + if ".zip/" in inner_path or ".tar.gz/" in inner_path: + match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))(.*)", inner_path) + archive_inner_path, path_within = match.groups() + archive = self.openArchive(archive_inner_path) + path_within = path_within.lstrip("/") + print archive, archive_inner_path + + if archive_inner_path.endswith(".zip"): + return archive.open(path_within).read() + else: + return archive.extractfile(path_within.encode("utf8")).read() + + else: + return super(SiteStoragePlugin, self).read(inner_path, mode) + From c7d94548be0ec23111a02b98e843f08d79f7636e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 6 Sep 2018 00:55:54 +0200 Subject: [PATCH 1077/2570] Support returning empty list as reponse --- src/Ui/UiWebsocket.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 45f4a2b6..902715b9 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -226,7 +226,7 @@ class UiWebsocket(object): def asyncErrorWatcher(func, *args, **kwargs): try: result = func(*args, **kwargs) - if result: + if result is not None: self.response(args[0], result) except Exception, err: if config.debug: # Allow websocket errors to appear on /Debug @@ -270,7 +270,7 @@ class UiWebsocket(object): else: result = func(req["id"]) - if result: + if result is not None: self.response(req["id"], result) # Format site info From d3499e71ef709540edce9f06866c0e60df948421 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 6 Sep 2018 00:56:36 +0200 Subject: [PATCH 1078/2570] FileGet use return function instead of calling 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 902715b9..a2eca267 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -691,7 +691,7 @@ class UiWebsocket(object): if body and format == "base64": import base64 body = base64.b64encode(body) - return self.response(to, body) + return body def actionFileNeed(self, to, inner_path, timeout=300): try: From b88b422e2f6a6a90ffeda3790403d5be853a1bdd Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 6 Sep 2018 00:57:21 +0200 Subject: [PATCH 1079/2570] Rev3591 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index f74a2f86..e7fb68d9 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3588 + self.rev = 3591 self.argv = argv self.action = None self.pending_changes = {} From 52081f4a2aab2747e3b37bfe2ed6b67f12488b2b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 7 Sep 2018 11:32:44 +0200 Subject: [PATCH 1080/2570] Make Accept a problematic string as it brokes jquery script loading --- src/Translate/languages/da.json | 2 +- src/Translate/languages/de.json | 2 +- src/Translate/languages/es.json | 2 +- src/Translate/languages/fr.json | 2 +- src/Translate/languages/hu.json | 2 +- src/Translate/languages/it.json | 2 +- src/Translate/languages/nl.json | 2 +- src/Translate/languages/pl.json | 2 +- src/Translate/languages/pt-br.json | 2 +- src/Translate/languages/ru.json | 2 +- src/Translate/languages/sk.json | 2 +- src/Translate/languages/sl.json | 2 +- src/Translate/languages/tr.json | 2 +- src/Translate/languages/zh-tw.json | 2 +- src/Translate/languages/zh.json | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Translate/languages/da.json b/src/Translate/languages/da.json index a1628f69..3d009257 100644 --- a/src/Translate/languages/da.json +++ b/src/Translate/languages/da.json @@ -46,6 +46,6 @@ "Site size limit changed to {0}MB": "Max side størrelse ændret til {0}MB", " New version of this page has just released.
        Reload to see the modified content.": " Ny version af denne side er blevet offentliggjort.
        Genindlæs venligst siden (F5) for at se nyt indhold!", "This site requests permission:": "Denne side betyder om tilladdelse:", - "Accept": "Tillad" + "_(Accept)": "Tillad" } diff --git a/src/Translate/languages/de.json b/src/Translate/languages/de.json index aa8c3805..ecff4e61 100644 --- a/src/Translate/languages/de.json +++ b/src/Translate/languages/de.json @@ -46,6 +46,6 @@ "Site size limit changed to {0}MB": "Speicherlimit für diese Seite auf {0}MB geändert", " New version of this page has just released.
        Reload to see the modified content.": " Neue version dieser Seite wurde gerade veröffentlicht.
        Lade die Seite neu um den geänderten Inhalt zu sehen.", "This site requests permission:": "Diese Seite fordert rechte:", - "Accept": "Genehmigen" + "_(Accept)": "Genehmigen" } diff --git a/src/Translate/languages/es.json b/src/Translate/languages/es.json index 22ce8a50..ccd0754b 100644 --- a/src/Translate/languages/es.json +++ b/src/Translate/languages/es.json @@ -46,6 +46,6 @@ "Site size limit changed to {0}MB": "Límite de tamaño del sitio cambiado a {0}MBs", " New version of this page has just released.
        Reload to see the modified content.": " Se ha publicado una nueva versión de esta página .
        Recarga para ver el contenido modificado.", "This site requests permission:": "Este sitio solicita permiso:", - "Accept": "Conceder" + "_(Accept)": "Conceder" } diff --git a/src/Translate/languages/fr.json b/src/Translate/languages/fr.json index 76e8fc7a..e7b1e19d 100644 --- a/src/Translate/languages/fr.json +++ b/src/Translate/languages/fr.json @@ -46,6 +46,6 @@ "Site size limit changed to {0}MB": "Taille maximale du site changée à {0}MB", " New version of this page has just released.
        Reload to see the modified content.": " Une nouvelle version de cette page vient d'être publiée.
        Rechargez pour voir les modifications.", "This site requests permission:": "Ce site requiert une permission:", - "Accept": "Autoriser" + "_(Accept)": "Autoriser" } diff --git a/src/Translate/languages/hu.json b/src/Translate/languages/hu.json index 8ec57ec7..af1dca4b 100644 --- a/src/Translate/languages/hu.json +++ b/src/Translate/languages/hu.json @@ -46,6 +46,6 @@ "Site size limit changed to {0}MB": "A méretkorlát módosítva {0}MB-ra", " New version of this page has just released.
        Reload to see the modified content.": "Az oldal épp most módosult
        A megváltozott tartalomért töltsd újra!", "This site requests permission:": "Az oldal megtekintéséhez szükséges jog:", - "Accept": "Engedélyezés" + "_(Accept)": "Engedélyezés" } diff --git a/src/Translate/languages/it.json b/src/Translate/languages/it.json index e6186d43..f4c770f0 100644 --- a/src/Translate/languages/it.json +++ b/src/Translate/languages/it.json @@ -46,6 +46,6 @@ "Site size limit changed to {0}MB": "Limite di spazio cambiato a {0}MB", " New version of this page has just released.
        Reload to see the modified content.": "E' stata rilasciata una nuova versione di questa pagina
        Ricaricare per vedere il contenuto modificato!", "This site requests permission:": "Questo sito richiede permessi:", - "Accept": "Concedere" + "_(Accept)": "Concedere" } diff --git a/src/Translate/languages/nl.json b/src/Translate/languages/nl.json index 23a21397..87d66050 100644 --- a/src/Translate/languages/nl.json +++ b/src/Translate/languages/nl.json @@ -46,6 +46,6 @@ "Site size limit changed to {0}MB": "Site limiet op grootte is veranderd naar {0}MB", " New version of this page has just released.
        Reload to see the modified content.": " Een nieuwe versie van deze pagina is zojuist uitgekomen.
        Herlaad de pagina om de bijgewerkte inhoud te zien.", "This site requests permission:": "Deze site vraagt om permissie:", - "Accept": "Toekennen" + "_(Accept)": "Toekennen" } diff --git a/src/Translate/languages/pl.json b/src/Translate/languages/pl.json index e4678444..160c257c 100644 --- a/src/Translate/languages/pl.json +++ b/src/Translate/languages/pl.json @@ -46,6 +46,6 @@ "Site size limit changed to {0}MB": "Rozmiar limitu strony zmieniony na {0}MBów", " New version of this page has just released.
        Reload to see the modified content.": "Nowa wersja tej strony właśnie została wydana.
        Odśwież by zobaczyć nową, zmodyfikowaną treść strony.", "This site requests permission:": "Ta strona wymaga uprawnień:", - "Accept": "Przyznaj uprawnienia" + "_(Accept)": "Przyznaj uprawnienia" } diff --git a/src/Translate/languages/pt-br.json b/src/Translate/languages/pt-br.json index 3aa7eeb0..e201e682 100644 --- a/src/Translate/languages/pt-br.json +++ b/src/Translate/languages/pt-br.json @@ -46,6 +46,6 @@ "Site size limit changed to {0}MB": "Limite de tamanho do site alterado para {0}MBs", " New version of this page has just released.
        Reload to see the modified content.": " Uma nova versão desse site acaba de ser publicada.
        Atualize para ver o conteúdo modificado.", "This site requests permission:": "Esse site solicita permissão:", - "Accept": "Conceder" + "_(Accept)": "Conceder" } diff --git a/src/Translate/languages/ru.json b/src/Translate/languages/ru.json index 51bcd5e2..e9a0e826 100644 --- a/src/Translate/languages/ru.json +++ b/src/Translate/languages/ru.json @@ -46,6 +46,6 @@ "Site size limit changed to {0}MB": "Лимит памяти на диске изменен на {0}MB", " New version of this page has just released.
        Reload to see the modified content.": "Доступна новая версия данной страницы
        Обновите страницу, что бы увидеть изменения!", "This site requests permission:": "Данный сайт запрашивает разрешения:", - "Accept": "Предоставить" + "_(Accept)": "Предоставить" } diff --git a/src/Translate/languages/sk.json b/src/Translate/languages/sk.json index ad80dbc9..263f7155 100644 --- a/src/Translate/languages/sk.json +++ b/src/Translate/languages/sk.json @@ -46,7 +46,7 @@ "Site size limit changed to {0}MB": "Limit veľkosti pamäte nastavený na {0}MB", " New version of this page has just released.
        Reload to see the modified content.": " Bola vydaná nová verzia tejto stránky.
        Znovu načítajte túto stránku aby bolo vidieť zmeny.", "This site requests permission:": "Táto stránka vyžaduje povolenie:", - "Accept": "Udeliť", + "_(Accept)": "Udeliť", "on": "", "Oct": "Okt", diff --git a/src/Translate/languages/sl.json b/src/Translate/languages/sl.json index 3bf4cb0e..d5e852c7 100644 --- a/src/Translate/languages/sl.json +++ b/src/Translate/languages/sl.json @@ -46,6 +46,6 @@ "Site size limit changed to {0}MB": "Omejitev strani nastavljena na{0} MB", " New version of this page has just released.
        Reload to see the modified content.": " Ravnokar je bila objavljena nova različica te strani.
        Osvežite jo, da boste videli novo vsebino.", "This site requests permission:": "Ta stran zahteva dovoljenja:", - "Accept": "Dovoli" + "_(Accept)": "Dovoli" } diff --git a/src/Translate/languages/tr.json b/src/Translate/languages/tr.json index 68f6748d..97e6c0ba 100644 --- a/src/Translate/languages/tr.json +++ b/src/Translate/languages/tr.json @@ -46,6 +46,6 @@ "Site size limit changed to {0}MB": "Site boyut sınırlaması {0}MB olarak ayarlandı", " New version of this page has just released.
        Reload to see the modified content.": " Bu sayfanın yeni versiyonu yayımlandı.
        Değişen içeriği görmek için yeniden yükleyiniz.", "This site requests permission:": "Bu site bir izin istiyor:", - "Accept": "İzin ver" + "_(Accept)": "İzin ver" } diff --git a/src/Translate/languages/zh-tw.json b/src/Translate/languages/zh-tw.json index 0636a20b..e9935f08 100644 --- a/src/Translate/languages/zh-tw.json +++ b/src/Translate/languages/zh-tw.json @@ -49,6 +49,6 @@ "Site size limit changed to {0}MB": "網站大小限制已改變到 {0}MB", " New version of this page has just released.
        Reload to see the modified content.": " 本頁面的新版本已經發佈。
        重新載入來查看更改後的內容。", "This site requests permission:": "這個網站的請求許可權:", - "Accept": "授權" + "_(Accept)": "授權" } diff --git a/src/Translate/languages/zh.json b/src/Translate/languages/zh.json index 0d901ce6..0bc7c068 100644 --- a/src/Translate/languages/zh.json +++ b/src/Translate/languages/zh.json @@ -50,6 +50,6 @@ "Site size limit changed to {0}MB": "站点大小限制已更改到 {0}MB", " New version of this page has just released.
        Reload to see the modified content.": " 本页面的新版本已经发布。
        重新加载来查看更改后的内容。", "This site requests permission:": "这个站点的请求权限:", - "Accept": "授权" + "_(Accept)": "授权" } From 714aea2e27f743fb5a6fe6e7d4fc7528e7167df5 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 7 Sep 2018 11:33:40 +0200 Subject: [PATCH 1081/2570] RequestFullscreen API does not requires confirmation anymore as you can call it directly --- src/Ui/media/Wrapper.coffee | 17 +++----------- src/Ui/media/all.js | 37 +++++++++++++------------------ src/Ui/media/lib/Translate.coffee | 1 + 3 files changed, 19 insertions(+), 36 deletions(-) create mode 100644 src/Ui/media/lib/Translate.coffee diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index 764427fb..9c98d127 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -218,20 +218,9 @@ class Wrapper w.location = params[0] actionRequestFullscreen: -> - if "Fullscreen" in @site_info.settings.permissions - elem = document.getElementById("inner-iframe") - request_fullscreen = elem.requestFullScreen || elem.webkitRequestFullscreen || elem.mozRequestFullScreen || elem.msRequestFullScreen - request_fullscreen.call(elem) - setTimeout ( => - if window.innerHeight != screen.height # Fullscreen failed, probably only allowed on click - @displayConfirm "This site requests permission:" + " Fullscreen", "Accept", => - request_fullscreen.call(elem) - ), 100 - else - @displayConfirm "This site requests permission:" + " Fullscreen", "Accept", => - @site_info.settings.permissions.push("Fullscreen") - @actionRequestFullscreen() - @ws.cmd "permissionAdd", "Fullscreen" + elem = document.getElementById("inner-iframe") + request_fullscreen = elem.requestFullScreen || elem.webkitRequestFullscreen || elem.mozRequestFullScreen || elem.msRequestFullScreen + request_fullscreen.call(elem) actionPermissionAdd: (message) -> permission = message.params diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index 0cb9d81d..da231c38 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -39,6 +39,18 @@ }).call(this); +/* ---- src/Ui/media/lib/Translate.coffee ---- */ + + +(function() { + window._ = function(s) { + return s; + }; + +}).call(this); + + + /* ---- src/Ui/media/lib/ZeroWebsocket.coffee ---- */ @@ -1145,28 +1157,9 @@ jQuery.extend( jQuery.easing, Wrapper.prototype.actionRequestFullscreen = function() { var elem, request_fullscreen; - if (indexOf.call(this.site_info.settings.permissions, "Fullscreen") >= 0) { - elem = document.getElementById("inner-iframe"); - request_fullscreen = elem.requestFullScreen || elem.webkitRequestFullscreen || elem.mozRequestFullScreen || elem.msRequestFullScreen; - request_fullscreen.call(elem); - return setTimeout(((function(_this) { - return function() { - if (window.innerHeight !== screen.height) { - return _this.displayConfirm("This site requests permission:" + " Fullscreen", "Accept", function() { - return request_fullscreen.call(elem); - }); - } - }; - })(this)), 100); - } else { - return this.displayConfirm("This site requests permission:" + " Fullscreen", "Accept", (function(_this) { - return function() { - _this.site_info.settings.permissions.push("Fullscreen"); - _this.actionRequestFullscreen(); - return _this.ws.cmd("permissionAdd", "Fullscreen"); - }; - })(this)); - } + elem = document.getElementById("inner-iframe"); + request_fullscreen = elem.requestFullScreen || elem.webkitRequestFullscreen || elem.mozRequestFullScreen || elem.msRequestFullScreen; + return request_fullscreen.call(elem); }; Wrapper.prototype.actionPermissionAdd = function(message) { diff --git a/src/Ui/media/lib/Translate.coffee b/src/Ui/media/lib/Translate.coffee new file mode 100644 index 00000000..2b323494 --- /dev/null +++ b/src/Ui/media/lib/Translate.coffee @@ -0,0 +1 @@ +window._ = (s) -> return s \ No newline at end of file From 57f3cee390d6b4000b2801a4d400077127857788 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 7 Sep 2018 11:33:57 +0200 Subject: [PATCH 1082/2570] Fix fileGet return when file is not existent --- 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 a2eca267..17d22152 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -691,7 +691,7 @@ class UiWebsocket(object): if body and format == "base64": import base64 body = base64.b64encode(body) - return body + self.response(to, body) def actionFileNeed(self, to, inner_path, timeout=300): try: From 84ab076fe8abfb163f58c96589bdbadd884ce847 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 7 Sep 2018 11:34:11 +0200 Subject: [PATCH 1083/2570] Rev3594 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index e7fb68d9..586e618e 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3591 + self.rev = 3594 self.argv = argv self.action = None self.pending_changes = {} From 87536619e362ed5ff942c1e754ca706483ecdc2a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Sep 2018 03:11:24 +0200 Subject: [PATCH 1084/2570] Translate support for configuration page --- plugins/UiConfig/UiConfigPlugin.py | 19 ++++++++++++++++--- plugins/UiConfig/media/config.html | 2 +- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/plugins/UiConfig/UiConfigPlugin.py b/plugins/UiConfig/UiConfigPlugin.py index 558a21fa..6c6a7982 100644 --- a/plugins/UiConfig/UiConfigPlugin.py +++ b/plugins/UiConfig/UiConfigPlugin.py @@ -1,5 +1,11 @@ from Plugin import PluginManager from Config import config +from Translate import Translate +from cStringIO import StringIO + + +if "_" not in locals(): + _ = Translate("plugins/UiConfig/languages/") @PluginManager.afterLoad @@ -30,7 +36,15 @@ class UiRequestPlugin(object): # If debugging merge *.css to all.css and *.js to all.js from Debug import DebugMedia DebugMedia.merge(file_path) - return self.actionFile(file_path) + + if file_path.endswith("js"): + data = _.translateData(open(file_path).read(), mode="js") + elif file_path.endswith("html"): + data = _.translateData(open(file_path).read(), mode="html") + else: + data = open(file_path).read() + + return self.actionFile(file_path, file_obj=StringIO(data), file_size=len(data)) else: return super(UiRequestPlugin, self).actionUiMedia(path) @@ -45,7 +59,7 @@ class UiWebsocketPlugin(object): if key not in config.keys_api_change_allowed: continue is_pending = key in config.pending_changes - if val == None and is_pending: + if val is None and is_pending: val = config.parser.get_default(key) back[key] = { "value": val, @@ -53,4 +67,3 @@ class UiWebsocketPlugin(object): "pending": is_pending } return back - diff --git a/plugins/UiConfig/media/config.html b/plugins/UiConfig/media/config.html index 407d2acd..752dccf0 100644 --- a/plugins/UiConfig/media/config.html +++ b/plugins/UiConfig/media/config.html @@ -15,6 +15,6 @@
        - + From 238667d989e8bff275865b73118d4985cc7534d4 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Sep 2018 03:11:35 +0200 Subject: [PATCH 1085/2570] Hungarian translate for configuration page --- plugins/UiConfig/languages/hu.json | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 plugins/UiConfig/languages/hu.json diff --git a/plugins/UiConfig/languages/hu.json b/plugins/UiConfig/languages/hu.json new file mode 100644 index 00000000..efaf2ea6 --- /dev/null +++ b/plugins/UiConfig/languages/hu.json @@ -0,0 +1,33 @@ +{ + "ZeroNet config": "ZeroNet beállítások", + "Web Interface": "Web felület", + "Open web browser on ZeroNet startup": "Bögésző megnyitása a ZeroNet indulásakor", + + "Network": "Hálózat", + "File server port": "FIle szerver port", + "Other peers will use this port to reach your served sites. (default: 15441)": "Más kliensek ezen a porton tudják elérni a kiszolgált oldalaidat (alapbeállítás: 15441)", + + "Disable: Don't connect to peers on Tor network": "Kikapcsolás: Ne csatlakozzon a Tor hálózatra", + "Enable: Only use Tor for Tor network peers": "Bekapcsolás: Csak a Tor kliensekhez használja a Tor hálózatot", + "Always: Use Tor for every connections to hide your IP address (slower)": "Mindig: Minden kapcsolatot a Tor hálózaton keresztül hozza létre az IP cím elrejtéséhez (lassabb)", + + "Disable": "Kikapcsolás", + "Enable": "Bekapcsolás", + "Always": "Mindig", + + "Use Tor bridges": "Tor bridge-ek használata", + "Use obfuscated bridge relays to avoid network level Tor block (even slower)": "Tor elrejtő bridge-ek használata a hálózat szintű Tor tiltás megkerüléséhez (még lassabb)", + + "Discover new peers using these adresses": "Új kapcsolat felfedezése ezen címek használatával", + + "Trackers files": "Tracker file-ok", + "Load additional list of torrent trackers dynamically, from a file": "További trackerek felfedezése dinamikusan, ezen file használatával", + "Eg.: data/trackers.json": "Pl.: data/trackers.json", + + "Proxy for tracker connections": "Proxy tracker kapcsolatohoz", + + " configuration item value changed": " beállítás megváltoztatva", + "Save settings": "Beállítások mentése", + "Some changed settings requires restart": "A beállítások érvényesítéséhez a kliens újraindítása szükséges", + "Restart ZeroNet client": "ZeroNet kliens újraindítása" +} \ No newline at end of file From 8594fa819996e36b730c2661cd662211f8c21a14 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Sep 2018 03:12:02 +0200 Subject: [PATCH 1086/2570] Don't try to load english translation files --- src/Translate/Translate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Translate/Translate.py b/src/Translate/Translate.py index a646ba8b..21e6e774 100644 --- a/src/Translate/Translate.py +++ b/src/Translate/Translate.py @@ -34,7 +34,11 @@ class Translate(dict): return "" % self.lang def load(self): - if os.path.isfile(self.lang_file): + if self.lang == "en": + data = {} + dict.__init__(self, data) + self.clear() + elif os.path.isfile(self.lang_file): try: data = json.load(open(self.lang_file)) logging.debug("Loaded translate file: %s (%s entries)" % (self.lang_file, len(data))) From 54efe5a351e3f6153251da28d8e181fccf9194c7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Sep 2018 03:12:29 +0200 Subject: [PATCH 1087/2570] Rev3597 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 586e618e..2e28c54a 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3594 + self.rev = 3597 self.argv = argv self.action = None self.pending_changes = {} From b05d125db2b4bb2a0d18fdba98d57b49c7286872 Mon Sep 17 00:00:00 2001 From: Daniell Mesquita Date: Wed, 12 Sep 2018 22:43:37 -0300 Subject: [PATCH 1088/2570] Added brazillian portuguese translation for Config --- plugins/UiConfig/language/pt-br.json | 33 ++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 plugins/UiConfig/language/pt-br.json diff --git a/plugins/UiConfig/language/pt-br.json b/plugins/UiConfig/language/pt-br.json new file mode 100644 index 00000000..7595c2f7 --- /dev/null +++ b/plugins/UiConfig/language/pt-br.json @@ -0,0 +1,33 @@ +{ + "ZeroNet config": "Configurações da ZeroNet", + "Web Interface": "Interface Web", + "Open web browser on ZeroNet startup": "Abrir navegador da web quando a ZeroNet iniciar", + + "Network": "Rede", + "File server port": "Porta de servidor de arquivos", + "Other peers will use this port to reach your served sites. (default: 15441)": "Outros peers usarão esta porta para alcançar seus sites servidos. (padrão: 15441)", + + "Disable: Don't connect to peers on Tor network": "Desativar: Não conectar à peers na rede Tor", + "Enable: Only use Tor for Tor network peers": "Ativar: Somente usar Tor para os peers da rede Tor", + "Always: Use Tor for every connections to hide your IP address (slower)": "Sempre: Usar o Tor para todas as conexões, para esconder seu endereço IP (mais lento)", + + "Disable": "Desativar", + "Enable": "Ativar", + "Sempre": "Mindig", + + "Use Tor bridges": "Usar pontes do Tor", + "Use obfuscated bridge relays to avoid network level Tor block (even slower)": "Usar relays de ponte ofuscados para evitar o bloqueio de Tor de nível de rede (ainda mais lento)", + + "Discover new peers using these adresses": "Descobrir novos peers usando estes endereços", + + "Trackers files": "Arquivos de trackers", + "Load additional list of torrent trackers dynamically, from a file": "Carregar lista adicional de trackers de torrent dinamicamente, à partir de um arquivo", + "Eg.: data/trackers.json": "Exemplo: data/trackers.json", + + "Proxy for tracker connections": "Proxy para conexões de tracker", + + " configuration item value changed": " valor do item de configuração foi alterado", + "Save settings": "Salvar configurações", + "Some changed settings requires restart": "Algumas configurações alteradas requrem reinicialização", + "Restart ZeroNet client": "Reiniciar cliente da ZeroNet" +} From a1abbd406d21697ad67e8d5e502edb1e03377878 Mon Sep 17 00:00:00 2001 From: tangdou1 <35254744+tangdou1@users.noreply.github.com> Date: Thu, 13 Sep 2018 11:35:47 +0800 Subject: [PATCH 1089/2570] Create zh.json --- plugins/UiConfig/languages/zh.json | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 plugins/UiConfig/languages/zh.json diff --git a/plugins/UiConfig/languages/zh.json b/plugins/UiConfig/languages/zh.json new file mode 100644 index 00000000..7281319f --- /dev/null +++ b/plugins/UiConfig/languages/zh.json @@ -0,0 +1,33 @@ +{ + "ZeroNet config": "ZeroNet配置", + "Web Interface": "网页界面", + "Open web browser on ZeroNet startup": "ZeroNet启动时,打开浏览器", + + "Network": "网络", + "File server port": "文件服务器端口", + "Other peers will use this port to reach your served sites. (default: 15441)": "其他节点将通过该端口访问您的服务站点. (默认: 15441)", + + "Disable: Don't connect to peers on Tor network": "关闭: 不连接洋葱网络中的节点", + "Enable: Only use Tor for Tor network peers": "开启: Tor仅用于连接洋葱网络中的节点", + "Always: Use Tor for every connections to hide your IP address (slower)": "总是: 将Tor用于每个网络连接,从而隐藏您的IP地址 (很慢)", + + "Disable": "关闭", + "Enable": "开启", + "Always": "总是", + + "Use Tor bridges": "使用Tor网桥", + "Use obfuscated bridge relays to avoid network level Tor block (even slower)": "使用混淆网桥中继,从而避免网络层Tor阻碍 (超级慢)", + + "Discover new peers using these adresses": "使用这些地址发现新节点", + + "Trackers files": "Trackers文件", + "Load additional list of torrent trackers dynamically, from a file": "从一个文件中,动态加载额外的种子Trackers列表", + "Eg.: data/trackers.json": "例如: data/trackers.json", + + "Proxy for tracker connections": "Tracker连接代理", + + " configuration item value changed": " 个配置值已经改变", + "Save settings": "保存配置", + "Some changed settings requires restart": "一些配置改变,需要重启才能生效", + "Restart ZeroNet client": "重启ZeroNet客户端" +} From d7912c6973be0ef13414d391e998287738ec5744 Mon Sep 17 00:00:00 2001 From: Daniell Mesquita Date: Fri, 14 Sep 2018 16:58:59 -0300 Subject: [PATCH 1090/2570] fix pull request #1597 Signed-off-by: Daniell Mesquita --- plugins/UiConfig/{language => languages}/pt-br.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename plugins/UiConfig/{language => languages}/pt-br.json (100%) diff --git a/plugins/UiConfig/language/pt-br.json b/plugins/UiConfig/languages/pt-br.json similarity index 100% rename from plugins/UiConfig/language/pt-br.json rename to plugins/UiConfig/languages/pt-br.json From 66c03c6798348a51d63ed0de14fe2d717d8e3094 Mon Sep 17 00:00:00 2001 From: Patrick Schleizer Date: Sat, 15 Sep 2018 13:01:59 -0400 Subject: [PATCH 1091/2570] add Whonix instructions --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index bd05efc4..85f733a8 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,10 @@ It downloads the latest version of ZeroNet then starts it automatically. * Start with `python2 zeronet.py` * Open http://127.0.0.1:43110/ in your browser +### [Whonix](https://www.whonix.org) + +* [Instructions](https://www.whonix.org/wiki/ZeroNet) + ### [Arch Linux](https://www.archlinux.org) * `git clone https://aur.archlinux.org/zeronet.git` From 0766205cb0e4a07144fd5bc2d393158089f4fee0 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 17 Sep 2018 15:18:45 +0200 Subject: [PATCH 1092/2570] Missing changelog for 0.6.3 --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45817ce4..1acf28cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,37 @@ +## ZeroNet 0.6.3 (2018-06-26) +### Added + - New plugin: ContentFilter that allows to have shared site and user block list. + - Support Tor meek proxies to avoid tracker blocking of GFW + - Detect network level tracker blocking and easy setting meek proxy for tracker connections. + - Support downloading 2GB+ sites as .zip (Thx to Radtoo) + - Support ZeroNet as a transparent proxy (Thx to JeremyRand) + - Allow fileQuery as CORS command (Thx to imachug) + - Windows distribution includes Tor and meek client by default + - Download sites as zip link to sidebar + - File server port randomization + - Implicit SSL for all connection + - fileList API command for zip files + - Auto download bigfiles size limit on sidebar + - Local peer number to the sidebar + - Open site directory button in sidebar + +## Changed + - Switched to Azure Tor meek proxy as Amazon one became unavailable + - Refactored/rewritten tracker connection manager + - Improved peer discovery for optional files without opened port + - Also delete Bigfile's piecemap on deletion + +## Fixed + - Important security issue: Iframe sandbox escape [Reported by Ivanq / gitcenter] + - Local peer discovery when running multiple clients on the same machine + - Uploading small files with Bigfile plugin + - Ctrl-c shutdown when running CLI commands + - High CPU/IO usage when Multiuser plugin enabled + - Firefox back button + - Peer discovery on older Linux kernels + - Optional file handling when multiple files have the same hash_id (first 4 chars of the hash) + - Msgpack 0.5.5 and 0.5.6 compatibility + ## ZeroNet 0.6.2 (2018-02-18) ### Added From 5f33516054e961220242570adc5532503b907d6b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 17 Sep 2018 15:20:02 +0200 Subject: [PATCH 1093/2570] Cleanup unreliable trackers easier if reached the shared tracker limit --- plugins/AnnounceShare/AnnounceSharePlugin.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plugins/AnnounceShare/AnnounceSharePlugin.py b/plugins/AnnounceShare/AnnounceSharePlugin.py index 9c27f7e4..83415b56 100644 --- a/plugins/AnnounceShare/AnnounceSharePlugin.py +++ b/plugins/AnnounceShare/AnnounceSharePlugin.py @@ -69,8 +69,8 @@ class TrackerStorage(object): trackers[tracker_address]["time_error"] = time.time() trackers[tracker_address]["num_error"] += 1 - if len(self.getWorkingTrackers()) > 4: - error_limit = 15 + if len(self.getWorkingTrackers()) >= config.working_shared_trackers_limit: + error_limit = 5 else: error_limit = 30 error_limit @@ -129,8 +129,7 @@ class TrackerStorage(object): if num_success: self.save() - if config.verbose: - self.log.debug("Trackers discovered from %s/%s peers in %.3fs" % (num_success, len(peers), time.time() - s)) + self.log.debug("Trackers discovered from %s/%s peers in %.3fs" % (num_success, len(peers), time.time() - s)) if "tracker_storage" not in locals(): From 1eacc26ef9d35988b6e45650bf54abd5bc11d603 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 17 Sep 2018 15:20:31 +0200 Subject: [PATCH 1094/2570] Always call addPeer command with strip ip address and int port --- plugins/AnnounceZero/AnnounceZeroPlugin.py | 2 +- plugins/PeerDb/PeerDbPlugin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/AnnounceZero/AnnounceZeroPlugin.py b/plugins/AnnounceZero/AnnounceZeroPlugin.py index 7cb1e48d..9cf20b95 100644 --- a/plugins/AnnounceZero/AnnounceZeroPlugin.py +++ b/plugins/AnnounceZero/AnnounceZeroPlugin.py @@ -87,7 +87,7 @@ class SiteAnnouncerPlugin(object): tracker_peer = connection_pool.get(tracker_address) # Re-use tracker connection if possible if not tracker_peer: tracker_ip, tracker_port = tracker_address.split(":") - tracker_peer = Peer(tracker_ip, tracker_port, connection_server=self.site.connection_server) + 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 diff --git a/plugins/PeerDb/PeerDbPlugin.py b/plugins/PeerDb/PeerDbPlugin.py index f9be1b06..e2b1e129 100644 --- a/plugins/PeerDb/PeerDbPlugin.py +++ b/plugins/PeerDb/PeerDbPlugin.py @@ -41,7 +41,7 @@ class ContentDbPlugin(object): num = 0 num_hashfield = 0 for row in res: - peer = site.addPeer(row["address"], row["port"]) + peer = site.addPeer(str(row["address"]), row["port"]) if not peer: # Already exist continue if row["hashfield"]: From 6c610c509f4abcd758f12a0f908a7440460624cd Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 17 Sep 2018 15:24:44 +0200 Subject: [PATCH 1095/2570] Rename RemoveGoodFileTasks to RemoveSolvedFileTasks and add result argument --- plugins/Sidebar/SidebarPlugin.py | 2 +- src/Worker/WorkerManager.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index e77056ab..473b9f9e 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -717,7 +717,7 @@ class UiWebsocketPlugin(object): self.site.settings["autodownloadoptional"] = bool(owned) self.site.bad_files = {} gevent.spawn(self.site.update, check_files=True) - self.site.worker_manager.removeGoodFileTasks() + self.site.worker_manager.removeSolvedFileTasks() def actionDbReload(self, to): permissions = self.getPermissions(to) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index bd45a12b..e3cbfde7 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -120,12 +120,12 @@ class WorkerManager(object): continue # No peers found yet for the optional task return task - def removeGoodFileTasks(self): + def removeSolvedFileTasks(self, mark_as_good=True): for task in self.tasks[:]: if task["inner_path"] not in self.site.bad_files: - self.log.debug("No longer in bad_files, marking as good: %s" % task["inner_path"]) + self.log.debug("No longer in bad_files, marking as %s: %s" % (mark_as_good, task["inner_path"])) task["done"] = True - task["evt"].set(True) + task["evt"].set(mark_as_good) self.tasks.remove(task) if not self.tasks: self.started_task_num = 0 From 577761a6bb5c3ebe5148f640314fa99472c98a35 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 17 Sep 2018 15:25:17 +0200 Subject: [PATCH 1096/2570] Add cache to default settings --- 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 1560eadd..cf0beddb 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -98,7 +98,7 @@ class Site(object): self.bad_files[inner_path] = min(self.bad_files[inner_path], 20) else: self.settings = { - "own": False, "serving": True, "permissions": [], + "own": False, "serving": True, "permissions": [], "cache": {"bad_files": {}}, "size_files_optional": 0, "added": int(time.time()), "optional_downloaded": 0, "size_optional": 0 } # Default if config.download_optional == "auto": From 9658c2d5530bd543aca819b00e778bdc464235f0 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 17 Sep 2018 15:27:11 +0200 Subject: [PATCH 1097/2570] Stop downloadcontent pool if reached 95% of site limit --- src/Site/Site.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index cf0beddb..b5caebf2 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -317,24 +317,38 @@ class Site(object): return valid def pooledDownloadContent(self, inner_paths, pool_size=100, only_if_bad=False): - self.log.debug("New downloadContent pool: len: %s" % len(inner_paths)) + self.log.debug("New downloadContent pool: len: %s, only if bad: %s" % (len(inner_paths), only_if_bad)) self.worker_manager.started_task_num += len(inner_paths) pool = gevent.pool.Pool(pool_size) + num_skipped = 0 + site_size_limit = self.getSizeLimit() * 1024 * 1024 for inner_path in inner_paths: if not only_if_bad or inner_path in self.bad_files: pool.spawn(self.downloadContent, inner_path) + else: + num_skipped += 1 self.worker_manager.started_task_num -= 1 - self.log.debug("Ended downloadContent pool len: %s" % len(inner_paths)) + if self.settings["size"] > site_size_limit * 0.95: + self.log.warning("Site size limit almost reached, aborting downloadContent pool") + for aborted_inner_path in inner_paths: + if aborted_inner_path in self.bad_files: + del self.bad_files[aborted_inner_path] + self.worker_manager.removeSolvedFileTasks(mark_as_good=False) + break + self.log.debug("Ended downloadContent pool len: %s, skipped: %s" % (len(inner_paths), num_skipped)) def pooledDownloadFile(self, inner_paths, pool_size=100, only_if_bad=False): - self.log.debug("New downloadFile pool: len: %s" % len(inner_paths)) + self.log.debug("New downloadFile pool: len: %s, only if bad: %s" % (len(inner_paths), only_if_bad)) self.worker_manager.started_task_num += len(inner_paths) pool = gevent.pool.Pool(pool_size) + num_skipped = 0 for inner_path in inner_paths: if not only_if_bad or inner_path in self.bad_files: pool.spawn(self.needFile, inner_path, update=True) + else: + num_skipped += 1 self.worker_manager.started_task_num -= 1 - self.log.debug("Ended downloadFile pool len: %s" % len(inner_paths)) + 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): @@ -369,7 +383,7 @@ class Site(object): if modified_contents: self.log.debug("%s new modified file from %s" % (len(modified_contents), peer)) modified_contents.sort(key=lambda inner_path: 0 - res["modified_files"][inner_path]) # Download newest first - gevent.spawn(self.pooledDownloadContent, modified_contents) + gevent.spawn(self.pooledDownloadContent, modified_contents, only_if_bad=True) # Check modified content.json files from peers and add modified files to bad_files # Return: Successfully queried peers [Peer, Peer...] @@ -415,7 +429,7 @@ class Site(object): if queried: break - self.log.debug("Queried listModifications from: %s in %.3fs" % (queried, time.time() - s)) + self.log.debug("Queried listModifications from: %s in %.3fs since %s" % (queried, time.time() - s, since)) time.sleep(0.1) return queried @@ -757,6 +771,7 @@ class Site(object): def addPeer(self, ip, port, return_peer=False, connection=None, source="other"): if not ip or ip == "0.0.0.0": return False + key = "%s:%s" % (ip, port) peer = self.peers.get(key) if peer: # Already has this ip From 6fc66b7b13122692a8a5f166903f7397714736d7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 17 Sep 2018 15:27:29 +0200 Subject: [PATCH 1098/2570] Move file forgot to separate function --- src/Site/Site.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index b5caebf2..346dffa9 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -1058,5 +1058,8 @@ class Site(object): self.updateWebsocket(file_failed=inner_path) if self.bad_files.get(inner_path, 0) > 30: - self.log.debug("Giving up on %s" % inner_path) - del self.bad_files[inner_path] # Give up after 30 tries + self.fileForgot(inner_path) + + def fileForgot(self, inner_path): + self.log.debug("Giving up on %s" % inner_path) + del self.bad_files[inner_path] # Give up after 30 tries From 00c9c14efb0e8523ba672b4bef10d0d52225e0cd Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 17 Sep 2018 15:28:53 +0200 Subject: [PATCH 1099/2570] Support listmodified before and after argument --- src/Content/ContentDb.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Content/ContentDb.py b/src/Content/ContentDb.py index 55984728..650a1ba9 100644 --- a/src/Content/ContentDb.py +++ b/src/Content/ContentDb.py @@ -126,11 +126,13 @@ class ContentDb(Db): return row["size"], row["size_optional"] - def listModified(self, site, since): - res = self.execute( - "SELECT inner_path, modified FROM content WHERE site_id = :site_id AND modified > :since", - {"site_id": self.site_ids.get(site.address, 0), "since": since} - ) + def listModified(self, site, after=None, before=None): + params = {"site_id": self.site_ids.get(site.address, 0)} + if after: + params["modified>"] = after + if before: + params["modified<"] = before + res = self.execute("SELECT inner_path, modified FROM content WHERE ?", params) return {row["inner_path"]: row["modified"] for row in res} content_dbs = {} From d124e2ed15c5a69dbae6e3fe27fc4a667cf5ad2d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 17 Sep 2018 15:30:02 +0200 Subject: [PATCH 1100/2570] Support deletion of of all user submitted content that is older than specified date --- src/Content/ContentManager.py | 43 +++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 1d8617fc..feecca28 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -182,8 +182,37 @@ class ContentManager(object): archived_inner_path = content_inner_dir + archived_dirname + "/content.json" if self.contents.get(archived_inner_path, {}).get("modified", 0) < date_archived: self.removeContent(archived_inner_path) + deleted += archived_inner_path self.site.settings["size"], self.site.settings["size_optional"] = self.getTotalSize() + # Check archived before + if old_content and "user_contents" in new_content and "archived_before" in new_content["user_contents"]: + old_archived_before = old_content.get("user_contents", {}).get("archived_before", 0) + new_archived_before = new_content.get("user_contents", {}).get("archived_before", 0) + if old_archived_before != new_archived_before: + self.log.debug("Archived before changed: %s -> %s" % (old_archived_before, new_archived_before)) + + # Remove downloaded archived files + num_removed_contents = 0 + for archived_inner_path in self.listModified(before=new_archived_before): + if archived_inner_path.startswith(content_inner_dir) and archived_inner_path != content_inner_path: + self.removeContent(archived_inner_path) + num_removed_contents += 1 + self.site.settings["size"], self.site.settings["size_optional"] = self.getTotalSize() + + # Remove archived files from download queue + num_removed_bad_files = 0 + for bad_file in self.site.bad_files.keys(): + if bad_file.endswith("content.json"): + del self.site.bad_files[bad_file] + num_removed_bad_files += 1 + + if num_removed_bad_files > 0: + self.site.worker_manager.removeSolvedFileTasks(mark_as_good=False) + gevent.spawn(self.site.update, since=0) + + self.log.debug("Archived removed contents: %s, removed bad files: %s" % (num_removed_contents, num_removed_bad_files)) + # Load includes if load_includes and "includes" in new_content: for relative_path, info in new_content["includes"].items(): @@ -235,6 +264,7 @@ class ContentManager(object): for inner_path in deleted: if inner_path in self.site.bad_files: del self.site.bad_files[inner_path] + self.site.worker_manager.removeSolvedFileTasks() if new_content.get("modified", 0) > self.site.settings.get("modified", 0): # Dont store modifications in the far future (more than 10 minute) @@ -277,8 +307,8 @@ class ContentManager(object): def getTotalSize(self, ignore=None): return self.contents.db.getTotalSize(self.site, ignore) - def listModified(self, since): - return self.contents.db.listModified(self.site, since) + def listModified(self, after=None, before=None): + return self.contents.db.listModified(self.site, after=after, before=before) def listContents(self, inner_path="content.json", user_files=False): if inner_path not in self.contents: @@ -299,8 +329,13 @@ class ContentManager(object): relative_directory = match.group(2) file_info = self.getFileInfo(user_contents_inner_path) - if file_info and file_info.get("archived", {}).get(relative_directory) >= modified: - return True + if file_info: + time_archived_before = file_info.get("archived_before", 0) + time_directory_archived = file_info.get("archived", {}).get(relative_directory) + if modified <= time_archived_before or modified <= time_directory_archived: + return True + else: + return False else: return False From e4af2d5e86f7d3f5b1f09c2e7b23893267ab5c43 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 17 Sep 2018 15:30:48 +0200 Subject: [PATCH 1101/2570] Pick up task that is taken by idle peer --- src/Worker/Worker.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Worker/Worker.py b/src/Worker/Worker.py index 5c5bfaf9..9d3bb616 100644 --- a/src/Worker/Worker.py +++ b/src/Worker/Worker.py @@ -60,6 +60,18 @@ class Worker(object): )) break + if sleep_i % 10 == 0: + workers = self.manager.findWorkers(task) + if not workers or not workers[0].peer.connection: + break + worker_idle = time.time() - workers[0].peer.connection.last_recv_time + if worker_idle > 1: + if config.verbose: + self.manager.log.debug("%s: %s, worker %s seems idle, picked up task after %ss sleep. (done: %s)" % ( + self.key, task["inner_path"], workers[0].key, 0.1 * sleep_i, task["done"] + )) + break + if task["done"]: continue From 86d1d4898a6a086e72de7a670cf45abeaf757cb3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 17 Sep 2018 15:31:10 +0200 Subject: [PATCH 1102/2570] Remove task on file write error --- src/Worker/Worker.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Worker/Worker.py b/src/Worker/Worker.py index 9d3bb616..ea4b8fce 100644 --- a/src/Worker/Worker.py +++ b/src/Worker/Worker.py @@ -98,11 +98,20 @@ class Worker(object): correct = False if correct is True or correct is None: # Verify ok or same file self.manager.log.debug("%s: Verify correct: %s" % (self.key, task["inner_path"])) + write_error = None if correct is True and task["done"] is False: # Save if changed and task not done yet buff.seek(0) - site.storage.write(task["inner_path"], buff) + try: + site.storage.write(task["inner_path"], buff) + write_error = False + except Exception as err: + self.manager.log.error("%s: Error writing: %s (%s)" % (self.key, task["inner_path"], err)) + write_error = err if task["done"] is False: - self.manager.doneTask(task) + if write_error: + self.manager.failTask(task) + else: + self.manager.doneTask(task) task["workers_num"] -= 1 else: # Verify failed task["workers_num"] -= 1 From 53a40fa9148dae4aa8ebc89f1954bd8b2697d1ff Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 17 Sep 2018 15:32:32 +0200 Subject: [PATCH 1103/2570] Stop worker if the file is not required anymore --- src/Worker/Worker.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Worker/Worker.py b/src/Worker/Worker.py index ea4b8fce..399e1f32 100644 --- a/src/Worker/Worker.py +++ b/src/Worker/Worker.py @@ -124,6 +124,9 @@ class Worker(object): if self.peer.hash_failed >= max(len(self.manager.tasks), 3) or self.peer.connection_error > 10: # Broken peer: More fails than tasks number but atleast 3 break + if task["inner_path"] not in site.bad_files: + # Don't need this file anymore + break time.sleep(1) self.peer.onWorkerDone() self.running = False From d44a4e8ae2c0bc8d0df6619dfc63a369a6d297c1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 17 Sep 2018 15:33:45 +0200 Subject: [PATCH 1104/2570] Sidebar optimization and dummy static page for uncoming internal page --- plugins/Sidebar/media/Internals.coffee | 60 ++++++ plugins/Sidebar/media/Internals.css | 17 ++ plugins/Sidebar/media/Sidebar.coffee | 178 ++++++++++----- plugins/Sidebar/media/Sidebar.css | 7 +- plugins/Sidebar/media/all.css | 29 ++- plugins/Sidebar/media/all.js | 287 ++++++++++++++++++++----- 6 files changed, 464 insertions(+), 114 deletions(-) create mode 100644 plugins/Sidebar/media/Internals.coffee create mode 100644 plugins/Sidebar/media/Internals.css diff --git a/plugins/Sidebar/media/Internals.coffee b/plugins/Sidebar/media/Internals.coffee new file mode 100644 index 00000000..484ecdb7 --- /dev/null +++ b/plugins/Sidebar/media/Internals.coffee @@ -0,0 +1,60 @@ +class Internals extends Class + constructor: (@sidebar) -> + @tag = null + @opened = false + if window.top.location.hash == "#internals" + setTimeout (=> @open()), 10 + + createHtmltag: -> + @when_loaded = $.Deferred() + if not @container + @container = $(""" +
        +
        +
    + """) + @container.appendTo(document.body) + @tag = @container.find(".internals") + + open: => + @createHtmltag() + @sidebar.fixbutton_targety = @sidebar.page_height + @stopDragY() + + onOpened: => + @sidebar.onClosed() + @log "onOpened" + + onClosed: => + $(document.body).removeClass("body-internals") + + 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 + @log "cleanup" + # Revert body transformations + @log "stopdrag", "opened:", @opened, targety + if not @opened + @onClosed() + +window.Internals = Internals \ No newline at end of file diff --git a/plugins/Sidebar/media/Internals.css b/plugins/Sidebar/media/Internals.css new file mode 100644 index 00000000..36b2489e --- /dev/null +++ b/plugins/Sidebar/media/Internals.css @@ -0,0 +1,17 @@ +.internals-container { width: 100%; z-index: 998; position: absolute; top: -100vh; } +.internals { background-color: #EEE; height: 100vh; transform: translateY(0px); } +.internals-middle {height: 0px; top: 50%; position: absolute; width: 100%; left: 50%; } + +.internals .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; +} +.internals .peers { width: 0px; height: 0px; position: absolute; left: -20px; top: -20px; text-align: center; } +.internals .peer { left: 0px; top: 0px; position: absolute; } +.internals .peer .icon { width: 20px; height: 20px; padding: 10px; display: inline-block; text-decoration: none; left: 200px; position: absolute; color: #666; } +.internals .peer .icon:before { content: "\25BC"; position: absolute; margin-top: 3px; margin-left: -1px; opacity: 0; transition: all 0.3s } +.internals .peer .icon:hover:before { opacity: 1; transition: none } +.internals .peer .line { + width: 187px; border-top: 1px solid #CCC; position: absolute; top: 20px; left: 20px; + transform: rotateZ(334deg); transform-origin: bottom left; +} \ No newline at end of file diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee index e3bc6f0c..7f5c8f1d 100644 --- a/plugins/Sidebar/media/Sidebar.coffee +++ b/plugins/Sidebar/media/Sidebar.coffee @@ -4,11 +4,16 @@ class Sidebar extends Class @container = null @opened = false @width = 410 + @internals = new Internals(@) @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 @@ -41,13 +46,16 @@ class Sidebar extends Class @dragStarted = (+ new Date) @fixbutton.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_addx = @fixbutton.offset().left - mousex + @fixbutton_addy = @fixbutton.offset().top - mousey @startDrag() @fixbutton.parent().on "click touchend touchcancel", (e) => - if (+ new Date)-@dragStarted < 100 + if (+ new Date) - @dragStarted < 100 window.top.location = @fixbutton.find(".fixbutton-bg").attr("href") @stopDrag() @resized() @@ -55,6 +63,7 @@ class Sidebar extends Class resized: => @page_width = $(window).width() + @page_height = $(window).height() @fixbutton_initx = @page_width - 75 # Initial x position if @opened @fixbutton.css @@ -65,6 +74,7 @@ class Sidebar extends Class # Start dragging the fixbutton startDrag: -> + @move_lock = "x" # Temporary until internals not finished @log "startDrag" @fixbutton_targetx = @fixbutton_initx # Fallback x position @@ -81,7 +91,9 @@ class Sidebar extends Class @fixbutton.one "click", (e) => @stopDrag() @fixbutton.removeClass("dragging") - if Math.abs(@fixbutton.offset().left - @fixbutton_initx) > 5 + 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() @@ -97,20 +109,45 @@ class Sidebar extends Class # Wait for moving the fixbutton waitMove: (e) => - if Math.abs(@fixbutton.offset().left - @fixbutton_targetx) > 10 and (+ new Date)-@dragStarted > 100 - @moved() + 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 - moved: -> - @log "Moved" + 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-internals") + return @internals.createHtmltag() @createHtmltag() - $(document.body).css("perspective", "1000px").addClass("body-sidebar") + $(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() - $(window).trigger "resize" # Override setsiteinfo to catch changes @wrapper.setSiteInfo = (site_info) => @@ -157,7 +194,6 @@ class Sidebar extends Class @when_loaded.resolve() else # Not first update, patch the html to keep unchanged dom elements - @log "Patching content" 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 @@ -169,19 +205,38 @@ class Sidebar extends Class animDrag: (e) => mousex = e.pageX - if not mousex + 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 + overdrag = @fixbutton_initx - @width - mousex if overdrag > 0 # Overdragged - overdrag_percent = 1+overdrag/300 + overdrag_percent = 1 + overdrag/300 mousex = (mousex + (@fixbutton_initx-@width)*overdrag_percent)/(1+overdrag_percent) - targetx = @fixbutton_initx-mousex-@fixbutton_addx + targetx = @fixbutton_initx - mousex - @fixbutton_addx + targety = @fixbutton_inity - mousey - @fixbutton_addy - @fixbutton[0].style.left = (mousex+@fixbutton_addx)+"px" + if @move_lock == "x" + targety = @fixbutton_inity + else if @move_lock == "y" + targetx = @fixbutton_initx - if @tag - @tag[0].style.transform = "translateX(#{0-targetx}px)" + 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 @internals.tag + @internals.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) @@ -189,6 +244,11 @@ class Sidebar extends Class else @fixbutton_targetx = @fixbutton_initx + if (not @internals.opened and 0 - targety > @page_height/10) or (@internals.opened and 0 - targety > @page_height*0.95) + @fixbutton_targety = @page_height - @fixbutton_inity - 50 + else + @fixbutton_targety = @fixbutton_inity + # Stop dragging the fixbutton stopDrag: -> @@ -203,45 +263,56 @@ class Sidebar extends Class # Move back to initial position if @fixbutton_targetx != @fixbutton.offset().left # Animate fixbutton - @fixbutton.stop().animate {"left": @fixbutton_targetx}, 500, "easeOutBack", => + 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", @fixbutton_targetx) + @fixbutton.css("left", left) $(".fixbutton-bg").trigger "mouseout" # Switch fixbutton back to normal status - # Animate sidebar and iframe - if @fixbutton_targetx == @fixbutton_initx - # Closed - targetx = 0 - @opened = false + @stopDragX() + @internals.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 - # Opened - targetx = @width - if @opened + @when_loaded.done => @onOpened() - else - @when_loaded.done => - @onOpened() - @opened = true + @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 - @tag.remove() - @tag = null + # 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 + @tag.remove() + @tag = null - # Revert body transformations - @log "stopdrag", "opened:", @opened - if not @opened - @onClosed() + # Revert body transformations + @log "stopdrag", "opened:", @opened + if not @opened + @onClosed() onOpened: -> @@ -448,19 +519,23 @@ class Sidebar extends Class # Close @tag.find(".close").off("click touchend").on "click touchend", (e) => - @startDrag() - @stopDrag() + @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 - $(document.body).css("height", "auto").css("perspective", "").css("transition", "").off transitionEnd + if e.target == document.body and not $(document.body).hasClass("body-sidebar") and not $(document.body).hasClass("body-internals") + $(document.body).css("height", "auto").css("perspective", "").css("will-change", "").css("transition", "").off transitionEnd @unloadGlobe() # We dont need site info anymore @@ -468,7 +543,7 @@ class Sidebar extends Class loadGlobe: => - console.log "loadGlobe", @tag.find(".globe").hasClass("loading") + console.log "loadGlobe", @tag.find(".globe")[0], @tag.find(".globe").hasClass("loading") if @tag.find(".globe").hasClass("loading") setTimeout (=> if typeof(DAT) == "undefined" # Globe script not loaded, do it first @@ -487,6 +562,7 @@ class Sidebar extends Class @globe.scene.remove(@globe.points) @globe.addData( globe_data, {format: 'magnitude', name: "hello", animated: false} ) @globe.createPoints() + @tag?.find(".globe").removeClass("loading") else if typeof(DAT) != "undefined" try @globe = new DAT.Globe( @tag.find(".globe")[0], {"imgDir": "/uimedia/globe/"} ) @@ -496,8 +572,8 @@ class Sidebar extends Class catch e console.log "WebGL error", e @tag?.find(".globe").addClass("error").text("WebGL not supported") + @tag?.find(".globe").removeClass("loading") - @tag?.find(".globe").removeClass("loading") unloadGlobe: => diff --git a/plugins/Sidebar/media/Sidebar.css b/plugins/Sidebar/media/Sidebar.css index bef0a681..660b1bab 100644 --- a/plugins/Sidebar/media/Sidebar.css +++ b/plugins/Sidebar/media/Sidebar.css @@ -7,9 +7,10 @@ .fixbutton-bg:active { cursor: -webkit-grabbing; } -.body-sidebar { background-color: #666 !important; } -#inner-iframe { transition: 0.3s ease-in-out; transform-origin: left; } -.body-sidebar iframe { transform: rotateY(5deg); opacity: 0.8; pointer-events: none; outline: 1px solid transparent } /* translateX(-200px) scale(0.95)*/ +.body-sidebar, .body-internals { background-color: #666 !important; } +#inner-iframe { transition: 0.3s ease-in-out; transform-origin: left bottom; } +.body-sidebar iframe { transform: rotateY(5deg); opacity: 0.8; pointer-events: none; outline: 1px solid transparent } +.body-internals iframe { transform: rotateX(5deg); opacity: 0.8; pointer-events: none; outline: 1px solid transparent } .sidebar .link-right { color: white; text-decoration: none; border-bottom: 1px solid #666; text-transform: uppercase; float: right; margin-right: 7px; margin-top: 1px; } .sidebar .link-right:hover { border-color: #CCC; } diff --git a/plugins/Sidebar/media/all.css b/plugins/Sidebar/media/all.css index d91287c3..fcf00618 100644 --- a/plugins/Sidebar/media/all.css +++ b/plugins/Sidebar/media/all.css @@ -1,5 +1,27 @@ +/* ---- plugins/Sidebar/media/Internals.css ---- */ + + +.internals-container { width: 100%; z-index: 998; position: absolute; top: -100vh; } +.internals { background-color: #EEE; height: 100vh; -webkit-transform: translateY(0px); -moz-transform: translateY(0px); -o-transform: translateY(0px); -ms-transform: translateY(0px); transform: translateY(0px) ; } +.internals-middle {height: 0px; top: 50%; position: absolute; width: 100%; left: 50%; } + +.internals .mynode { + border: 0.5px solid #aaa; width: 50px; height: 50px; -webkit-transform: rotateZ(45deg); -moz-transform: rotateZ(45deg); -o-transform: rotateZ(45deg); -ms-transform: rotateZ(45deg); 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; +} +.internals .peers { width: 0px; height: 0px; position: absolute; left: -20px; top: -20px; text-align: center; } +.internals .peer { left: 0px; top: 0px; position: absolute; } +.internals .peer .icon { width: 20px; height: 20px; padding: 10px; display: inline-block; text-decoration: none; left: 200px; position: absolute; color: #666; } +.internals .peer .icon:before { content: "\25BC"; position: absolute; margin-top: 3px; margin-left: -1px; opacity: 0; -webkit-transition: all 0.3s ; -moz-transition: all 0.3s ; -o-transition: all 0.3s ; -ms-transition: all 0.3s ; transition: all 0.3s } +.internals .peer .icon:hover:before { opacity: 1; -webkit-transition: none ; -moz-transition: none ; -o-transition: none ; -ms-transition: none ; transition: none } +.internals .peer .line { + width: 187px; border-top: 1px solid #CCC; position: absolute; top: 20px; left: 20px; + -webkit-transform: rotateZ(334deg); -moz-transform: rotateZ(334deg); -o-transform: rotateZ(334deg); -ms-transform: rotateZ(334deg); transform: rotateZ(334deg) ; transform-origin: bottom left; +} + + /* ---- plugins/Sidebar/media/Menu.css ---- */ @@ -86,9 +108,10 @@ .fixbutton-bg:active { cursor: -webkit-grabbing; } -.body-sidebar { background-color: #666 !important; } -#inner-iframe { -webkit-transition: 0.3s ease-in-out; -moz-transition: 0.3s ease-in-out; -o-transition: 0.3s ease-in-out; -ms-transition: 0.3s ease-in-out; transition: 0.3s ease-in-out ; transform-origin: left; } -.body-sidebar iframe { -webkit-transform: rotateY(5deg); -moz-transform: rotateY(5deg); -o-transform: rotateY(5deg); -ms-transform: rotateY(5deg); transform: rotateY(5deg) ; opacity: 0.8; pointer-events: none; outline: 1px solid transparent } /* translateX(-200px) scale(0.95)*/ +.body-sidebar, .body-internals { background-color: #666 !important; } +#inner-iframe { -webkit-transition: 0.3s ease-in-out; -moz-transition: 0.3s ease-in-out; -o-transition: 0.3s ease-in-out; -ms-transition: 0.3s ease-in-out; transition: 0.3s ease-in-out ; transform-origin: left bottom; } +.body-sidebar iframe { -webkit-transform: rotateY(5deg); -moz-transform: rotateY(5deg); -o-transform: rotateY(5deg); -ms-transform: rotateY(5deg); transform: rotateY(5deg) ; opacity: 0.8; pointer-events: none; outline: 1px solid transparent } +.body-internals iframe { -webkit-transform: rotateX(5deg); -moz-transform: rotateX(5deg); -o-transform: rotateX(5deg); -ms-transform: rotateX(5deg); transform: rotateX(5deg) ; opacity: 0.8; pointer-events: none; outline: 1px solid transparent } .sidebar .link-right { color: white; text-decoration: none; border-bottom: 1px solid #666; text-transform: uppercase; float: right; margin-right: 7px; margin-top: 1px; } .sidebar .link-right:hover { border-color: #CCC; } diff --git a/plugins/Sidebar/media/all.js b/plugins/Sidebar/media/all.js index 6e9b85b1..79124e3a 100644 --- a/plugins/Sidebar/media/all.js +++ b/plugins/Sidebar/media/all.js @@ -57,6 +57,95 @@ }).call(this); +/* ---- plugins/Sidebar/media/Internals.coffee ---- */ + + +(function() { + var Internals, + 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; + + Internals = (function(superClass) { + extend(Internals, superClass); + + function Internals(sidebar) { + this.sidebar = sidebar; + this.stopDragY = bind(this.stopDragY, this); + this.onClosed = bind(this.onClosed, this); + this.onOpened = bind(this.onOpened, this); + this.open = bind(this.open, this); + this.tag = null; + this.opened = false; + if (window.top.location.hash === "#internals") { + setTimeout(((function(_this) { + return function() { + return _this.open(); + }; + })(this)), 10); + } + } + + Internals.prototype.createHtmltag = function() { + this.when_loaded = $.Deferred(); + if (!this.container) { + this.container = $("
    \n
    "); + this.container.appendTo(document.body); + return this.tag = this.container.find(".internals"); + } + }; + + Internals.prototype.open = function() { + this.createHtmltag(); + this.sidebar.fixbutton_targety = this.sidebar.page_height; + return this.stopDragY(); + }; + + Internals.prototype.onOpened = function() { + this.sidebar.onClosed(); + return this.log("onOpened"); + }; + + Internals.prototype.onClosed = function() { + return $(document.body).removeClass("body-internals"); + }; + + Internals.prototype.stopDragY = function() { + var targety; + if (this.sidebar.fixbutton_targety === this.sidebar.fixbutton_inity) { + targety = 0; + this.opened = false; + } else { + targety = this.sidebar.fixbutton_targety - this.sidebar.fixbutton_inity; + this.onOpened(); + this.opened = true; + } + if (this.tag) { + this.tag.css("transition", "0.5s ease-out"); + this.tag.css("transform", "translateY(" + targety + "px)").one(transitionEnd, (function(_this) { + return function() { + _this.tag.css("transition", ""); + if (!_this.opened) { + return _this.log("cleanup"); + } + }; + })(this)); + } + this.log("stopdrag", "opened:", this.opened, targety); + if (!this.opened) { + return this.onClosed(); + } + }; + + return Internals; + + })(Class); + + window.Internals = Internals; + +}).call(this); + + /* ---- plugins/Sidebar/media/Menu.coffee ---- */ @@ -291,11 +380,16 @@ window.initScrollable = function () { this.container = null; this.opened = false; this.width = 410; + this.internals = new Internals(this); this.fixbutton = $(".fixbutton"); this.fixbutton_addx = 0; + this.fixbutton_addy = 0; this.fixbutton_initx = 0; + this.fixbutton_inity = 15; this.fixbutton_targetx = 0; + this.move_lock = null; this.page_width = $(window).width(); + this.page_height = $(window).height(); this.frame = $("#inner-iframe"); this.initFixbutton(); this.dragStarted = 0; @@ -321,12 +415,15 @@ window.initScrollable = function () { _this.fixbutton.off("mousemove touchmove"); _this.dragStarted = +(new Date); return _this.fixbutton.one("mousemove touchmove", function(e) { - var mousex; + var mousex, mousey; mousex = e.pageX; + mousey = e.pageY; if (!mousex) { mousex = e.originalEvent.touches[0].pageX; + mousey = e.originalEvent.touches[0].pageY; } _this.fixbutton_addx = _this.fixbutton.offset().left - mousex; + _this.fixbutton_addy = _this.fixbutton.offset().top - mousey; return _this.startDrag(); }); }; @@ -345,6 +442,7 @@ window.initScrollable = function () { Sidebar.prototype.resized = function() { this.page_width = $(window).width(); + this.page_height = $(window).height(); this.fixbutton_initx = this.page_width - 75; if (this.opened) { return this.fixbutton.css({ @@ -358,6 +456,7 @@ window.initScrollable = function () { }; Sidebar.prototype.startDrag = function() { + this.move_lock = "x"; this.log("startDrag"); this.fixbutton_targetx = this.fixbutton_initx; this.fixbutton.addClass("dragging"); @@ -367,9 +466,12 @@ window.initScrollable = function () { } this.fixbutton.one("click", (function(_this) { return function(e) { + var moved_x, moved_y; _this.stopDrag(); _this.fixbutton.removeClass("dragging"); - if (Math.abs(_this.fixbutton.offset().left - _this.fixbutton_initx) > 5) { + moved_x = Math.abs(_this.fixbutton.offset().left - _this.fixbutton_initx); + moved_y = Math.abs(_this.fixbutton.offset().top - _this.fixbutton_inity); + if (moved_x > 5 || moved_y > 10) { return e.preventDefault(); } }; @@ -385,17 +487,47 @@ window.initScrollable = function () { }; Sidebar.prototype.waitMove = function(e) { - if (Math.abs(this.fixbutton.offset().left - this.fixbutton_targetx) > 10 && (+(new Date)) - this.dragStarted > 100) { - this.moved(); + var moved_x, moved_y; + document.body.style.perspective = "1000px"; + document.body.style.height = "100%"; + document.body.style.willChange = "perspective"; + document.documentElement.style.height = "100%"; + moved_x = Math.abs(parseInt(this.fixbutton[0].style.left) - this.fixbutton_targetx); + moved_y = Math.abs(parseInt(this.fixbutton[0].style.top) - this.fixbutton_targety); + if (moved_x > 5 && (+(new Date)) - this.dragStarted + moved_x > 50) { + this.moved("x"); + this.fixbutton.stop().animate({ + "top": this.fixbutton_inity + }, 1000); + return this.fixbutton.parents().off("mousemove touchmove", this.waitMove); + } else if (moved_y > 5 && (+(new Date)) - this.dragStarted + moved_y > 50) { + this.moved("y"); return this.fixbutton.parents().off("mousemove touchmove", this.waitMove); } }; - Sidebar.prototype.moved = function() { + Sidebar.prototype.moved = function(direction) { var img; - this.log("Moved"); + this.log("Moved", direction); + this.move_lock = direction; + if (direction === "y") { + $(document.body).addClass("body-internals"); + return this.internals.createHtmltag(); + } this.createHtmltag(); - $(document.body).css("perspective", "1000px").addClass("body-sidebar"); + $(document.body).addClass("body-sidebar"); + this.container.on("mousedown touchend touchcancel", (function(_this) { + return function(e) { + if (e.target !== e.currentTarget) { + return true; + } + _this.log("closing"); + if ($(document.body).hasClass("body-sidebar")) { + _this.close(); + return true; + } + }; + })(this)); $(window).off("resize"); $(window).on("resize", (function(_this) { return function() { @@ -404,7 +536,6 @@ window.initScrollable = function () { return _this.resized(); }; })(this)); - $(window).trigger("resize"); this.wrapper.setSiteInfo = (function(_this) { return function(site_info) { _this.setSiteInfo(site_info); @@ -455,7 +586,6 @@ window.initScrollable = function () { morphdom(this.tag.find(".content")[0], '
    ' + res + '
    '); return this.when_loaded.resolve(); } else { - this.log("Patching content"); return morphdom(this.tag.find(".content")[0], '
    ' + res + '
    ', { onBeforeMorphEl: function(from_el, to_el) { if (from_el.className === "globe" || from_el.className.indexOf("noupdate") >= 0) { @@ -469,10 +599,12 @@ window.initScrollable = function () { }; Sidebar.prototype.animDrag = function(e) { - var mousex, overdrag, overdrag_percent, targetx; + var mousex, mousey, overdrag, overdrag_percent, targetx, targety; mousex = e.pageX; - if (!mousex) { + mousey = e.pageY; + if (!mousex && e.originalEvent.touches) { mousex = e.originalEvent.touches[0].pageX; + mousey = e.originalEvent.touches[0].pageY; } overdrag = this.fixbutton_initx - this.width - mousex; if (overdrag > 0) { @@ -480,19 +612,38 @@ window.initScrollable = function () { mousex = (mousex + (this.fixbutton_initx - this.width) * overdrag_percent) / (1 + overdrag_percent); } targetx = this.fixbutton_initx - mousex - this.fixbutton_addx; - this.fixbutton[0].style.left = (mousex + this.fixbutton_addx) + "px"; - if (this.tag) { - this.tag[0].style.transform = "translateX(" + (0 - targetx) + "px)"; + targety = this.fixbutton_inity - mousey - this.fixbutton_addy; + if (this.move_lock === "x") { + targety = this.fixbutton_inity; + } else if (this.move_lock === "y") { + targetx = this.fixbutton_initx; + } + if (!this.move_lock || this.move_lock === "x") { + this.fixbutton[0].style.left = (mousex + this.fixbutton_addx) + "px"; + if (this.tag) { + this.tag[0].style.transform = "translateX(" + (0 - targetx) + "px)"; + } + } + if (!this.move_lock || this.move_lock === "y") { + this.fixbutton[0].style.top = (mousey + this.fixbutton_addy) + "px"; + if (this.internals.tag) { + this.internals.tag[0].style.transform = "translateY(" + (0 - targety) + "px)"; + } } if ((!this.opened && targetx > this.width / 3) || (this.opened && targetx > this.width * 0.9)) { - return this.fixbutton_targetx = this.fixbutton_initx - this.width; + this.fixbutton_targetx = this.fixbutton_initx - this.width; } else { - return this.fixbutton_targetx = this.fixbutton_initx; + this.fixbutton_targetx = this.fixbutton_initx; + } + if ((!this.internals.opened && 0 - targety > this.page_height / 10) || (this.internals.opened && 0 - targety > this.page_height * 0.95)) { + return this.fixbutton_targety = this.page_height - this.fixbutton_inity - 50; + } else { + return this.fixbutton_targety = this.fixbutton_inity; } }; Sidebar.prototype.stopDrag = function() { - var targetx; + var left, top; this.fixbutton.parents().off("mousemove touchmove"); this.fixbutton.off("mousemove touchmove"); this.fixbutton.css("pointer-events", ""); @@ -502,52 +653,68 @@ window.initScrollable = function () { } this.fixbutton.removeClass("dragging"); if (this.fixbutton_targetx !== this.fixbutton.offset().left) { + if (this.move_lock === "y") { + top = this.fixbutton_targety; + left = this.fixbutton_initx; + } + if (this.move_lock === "x") { + top = this.fixbutton_inity; + left = this.fixbutton_targetx; + } this.fixbutton.stop().animate({ - "left": this.fixbutton_targetx + "left": left, + "top": top }, 500, "easeOutBack", (function(_this) { return function() { if (_this.fixbutton_targetx === _this.fixbutton_initx) { _this.fixbutton.css("left", "auto"); } else { - _this.fixbutton.css("left", _this.fixbutton_targetx); + _this.fixbutton.css("left", left); } return $(".fixbutton-bg").trigger("mouseout"); }; })(this)); - if (this.fixbutton_targetx === this.fixbutton_initx) { - targetx = 0; - this.opened = false; + this.stopDragX(); + this.internals.stopDragY(); + } + return this.move_lock = null; + }; + + Sidebar.prototype.stopDragX = function() { + var targetx; + if (this.fixbutton_targetx === this.fixbutton_initx || this.move_lock === "y") { + targetx = 0; + this.opened = false; + } else { + targetx = this.width; + if (this.opened) { + this.onOpened(); } else { - targetx = this.width; - if (this.opened) { - this.onOpened(); - } else { - this.when_loaded.done((function(_this) { - return function() { - return _this.onOpened(); - }; - })(this)); - } - this.opened = true; - } - if (this.tag) { - this.tag.css("transition", "0.4s ease-out"); - this.tag.css("transform", "translateX(-" + targetx + "px)").one(transitionEnd, (function(_this) { + this.when_loaded.done((function(_this) { return function() { - _this.tag.css("transition", ""); - if (!_this.opened) { - _this.container.remove(); - _this.container = null; - _this.tag.remove(); - return _this.tag = null; - } + return _this.onOpened(); }; })(this)); } - this.log("stopdrag", "opened:", this.opened); - if (!this.opened) { - return this.onClosed(); - } + this.opened = true; + } + if (this.tag) { + this.tag.css("transition", "0.4s ease-out"); + this.tag.css("transform", "translateX(-" + targetx + "px)").one(transitionEnd, (function(_this) { + return function() { + _this.tag.css("transition", ""); + if (!_this.opened) { + _this.container.remove(); + _this.container = null; + _this.tag.remove(); + return _this.tag = null; + } + }; + })(this)); + } + this.log("stopdrag", "opened:", this.opened); + if (!this.opened) { + return this.onClosed(); } }; @@ -851,21 +1018,26 @@ window.initScrollable = function () { })(this)); this.tag.find(".close").off("click touchend").on("click touchend", (function(_this) { return function(e) { - _this.startDrag(); - _this.stopDrag(); + _this.close(); return false; }; })(this)); return this.loadGlobe(); }; + Sidebar.prototype.close = function() { + this.move_lock = "x"; + this.startDrag(); + return this.stopDrag(); + }; + Sidebar.prototype.onClosed = function() { $(window).off("resize"); $(window).on("resize", this.resized); $(document.body).css("transition", "0.6s ease-in-out").removeClass("body-sidebar").on(transitionEnd, (function(_this) { return function(e) { - if (e.target === document.body) { - $(document.body).css("height", "auto").css("perspective", "").css("transition", "").off(transitionEnd); + if (e.target === document.body && !$(document.body).hasClass("body-sidebar") && !$(document.body).hasClass("body-internals")) { + $(document.body).css("height", "auto").css("perspective", "").css("will-change", "").css("transition", "").off(transitionEnd); return _this.unloadGlobe(); } }; @@ -874,7 +1046,7 @@ window.initScrollable = function () { }; Sidebar.prototype.loadGlobe = function() { - console.log("loadGlobe", this.tag.find(".globe").hasClass("loading")); + console.log("loadGlobe", this.tag.find(".globe")[0], this.tag.find(".globe").hasClass("loading")); if (this.tag.find(".globe").hasClass("loading")) { return setTimeout(((function(_this) { return function() { @@ -895,7 +1067,7 @@ window.initScrollable = function () { return img.onload = (function(_this) { return function() { return _this.wrapper.ws.cmd("sidebarGetPeers", [], function(globe_data) { - var e, ref, ref1; + var e, ref, ref1, ref2; if (_this.globe) { _this.globe.scene.remove(_this.globe.points); _this.globe.addData(globe_data, { @@ -904,6 +1076,7 @@ window.initScrollable = function () { animated: false }); _this.globe.createPoints(); + return (ref = _this.tag) != null ? ref.find(".globe").removeClass("loading") : void 0; } else if (typeof DAT !== "undefined") { try { _this.globe = new DAT.Globe(_this.tag.find(".globe")[0], { @@ -918,12 +1091,12 @@ window.initScrollable = function () { } catch (error) { e = error; console.log("WebGL error", e); - if ((ref = _this.tag) != null) { - ref.find(".globe").addClass("error").text("WebGL not supported"); + if ((ref1 = _this.tag) != null) { + ref1.find(".globe").addClass("error").text("WebGL not supported"); } } + return (ref2 = _this.tag) != null ? ref2.find(".globe").removeClass("loading") : void 0; } - return (ref1 = _this.tag) != null ? ref1.find(".globe").removeClass("loading") : void 0; }); }; })(this); From 2772e705aea75182664550e19872f32a82f5c747 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 17 Sep 2018 15:34:03 +0200 Subject: [PATCH 1105/2570] Rev3608 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 2e28c54a..fc92f3e8 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3597 + self.rev = 3608 self.argv = argv self.action = None self.pending_changes = {} From d04b7599139ff974ddfbc9fee1a852a26b6a36bb Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 17 Sep 2018 15:40:44 +0200 Subject: [PATCH 1106/2570] Allow more time inaccuracy for noparallel test --- src/Test/TestNoparallel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test/TestNoparallel.py b/src/Test/TestNoparallel.py index 1f1d0140..67ef0caf 100644 --- a/src/Test/TestNoparallel.py +++ b/src/Test/TestNoparallel.py @@ -119,4 +119,4 @@ class TestNoparallel: assert obj1.counted + obj2.counted == 10 taken = time.time() - s - assert 0.11 > taken >= 0.1 # 2 * 0.05s count = ~0.1s + assert 0.12 > taken >= 0.1 # 2 * 0.05s count = ~0.1s From 2a1849d027a7dee06808b2ac3bb88ba7652a11e3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 17 Sep 2018 17:48:15 +0200 Subject: [PATCH 1107/2570] Rev3609, Don't validate valid pattern in UiConfig if the field is hidden --- plugins/UiConfig/media/js/UiConfig.coffee | 2 +- plugins/UiConfig/media/js/all.js | 5 ++--- src/Config.py | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/UiConfig/media/js/UiConfig.coffee b/plugins/UiConfig/media/js/UiConfig.coffee index d0fbffd6..4c11d428 100644 --- a/plugins/UiConfig/media/js/UiConfig.coffee +++ b/plugins/UiConfig/media/js/UiConfig.coffee @@ -59,7 +59,7 @@ class UiConfig extends ZeroFrame if value_same_as_default value = null - if @config[item.key].item.valid_pattern and 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})" diff --git a/plugins/UiConfig/media/js/all.js b/plugins/UiConfig/media/js/all.js index a587e5a4..8e24b298 100644 --- a/plugins/UiConfig/media/js/all.js +++ b/plugins/UiConfig/media/js/all.js @@ -1450,7 +1450,6 @@ }).call(this); - /* ---- plugins/UiConfig/media/js/ConfigView.coffee ---- */ @@ -1774,7 +1773,7 @@ if (value_same_as_default) { value = null; } - if (this.config[item.key].item.valid_pattern && value) { + if (this.config[item.key].item.valid_pattern && !this.config[item.key].item.isHidden()) { match = value.match(this.config[item.key].item.valid_pattern); if (!match || match[0] !== value) { message = "Invalid value of " + this.config[item.key].item.title + ": " + value + " (does not matches " + this.config[item.key].item.valid_pattern + ")"; @@ -1883,4 +1882,4 @@ window.Page.createProjector(); -}).call(this); \ No newline at end of file +}).call(this); diff --git a/src/Config.py b/src/Config.py index fc92f3e8..0992b1e3 100644 --- a/src/Config.py +++ b/src/Config.py @@ -10,7 +10,7 @@ class Config(object): def __init__(self, argv): self.version = "0.6.3" - self.rev = 3608 + self.rev = 3609 self.argv = argv self.action = None self.pending_changes = {} From 8fd2af1870aa37eba3a20403d456a6e2650363ca Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 21 Sep 2018 02:17:53 +0200 Subject: [PATCH 1108/2570] Private key save and fogot to sidebar --- plugins/Sidebar/SidebarPlugin.py | 23 ++++++++++++++++++++--- plugins/Sidebar/media/Sidebar.coffee | 13 +++++++++++++ plugins/Sidebar/media/Sidebar.css | 3 ++- plugins/Sidebar/media/all.css | 3 ++- plugins/Sidebar/media/all.js | 22 ++++++++++++++++++++-- 5 files changed, 57 insertions(+), 7 deletions(-) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index 473b9f9e..f3d97f3d 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -126,7 +126,7 @@ class UiWebsocketPlugin(object):
  • {_[Copy to clipboard]} + {_[Copy to clipboard]}
  • """)) body.append("") @@ -754,8 +755,6 @@ class UiWebsocketPlugin(object): @flag.no_multiuser def actionSiteSetAutodownloadoptional(self, to, owned): self.site.settings["autodownloadoptional"] = bool(owned) - self.site.bad_files = {} - gevent.spawn(self.site.update, check_files=True) self.site.worker_manager.removeSolvedFileTasks() @flag.no_multiuser diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee index b65912d4..47c6e7f8 100644 --- a/plugins/Sidebar/media/Sidebar.coffee +++ b/plugins/Sidebar/media/Sidebar.coffee @@ -372,6 +372,14 @@ class Sidebar extends Class @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", [], => From 95bf4ecb429889e3ef52c1f204c74c4741f1bf53 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 7 Feb 2020 16:40:27 +0100 Subject: [PATCH 2319/2570] Read 5MB of logs for non-default console tabs --- plugins/Sidebar/media/Console.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Sidebar/media/Console.coffee b/plugins/Sidebar/media/Console.coffee index 4ef9a4fd..724ebb94 100644 --- a/plugins/Sidebar/media/Console.coffee +++ b/plugins/Sidebar/media/Console.coffee @@ -183,7 +183,7 @@ class Console extends Class if @filter == "" @read_size = 32 * 1024 else - @read_size = 1024 * 1024 + @read_size = 5 * 1024 * 1024 @loadConsoleText() handleTabClick: (e) => From a3546d56b0ec56d43d8168fbc703e1d8ae286e31 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 7 Feb 2020 16:42:26 +0100 Subject: [PATCH 2320/2570] Merge js --- plugins/Sidebar/media/all.js | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/plugins/Sidebar/media/all.js b/plugins/Sidebar/media/all.js index 3480ecfe..f83a5c5c 100644 --- a/plugins/Sidebar/media/all.js +++ b/plugins/Sidebar/media/all.js @@ -55,6 +55,7 @@ }).call(this); + /* ---- Console.coffee ---- */ @@ -330,7 +331,7 @@ if (this.filter === "") { this.read_size = 32 * 1024; } else { - this.read_size = 1024 * 1024; + this.read_size = 5 * 1024 * 1024; } return this.loadConsoleText(); }; @@ -436,6 +437,7 @@ }).call(this); + /* ---- RateLimit.coffee ---- */ @@ -464,6 +466,7 @@ }).call(this); + /* ---- Scrollable.js ---- */ @@ -601,9 +604,9 @@ window.initScrollable = function () { this.globe = null; this.preload_html = null; this.original_set_site_info = this.wrapper.setSiteInfo; - if (false) { + if (window.top.location.hash === "#ZeroNet:OpenSidebar") { this.startDrag(); - this.moved(); + this.moved("x"); this.fixbutton_targetx = this.fixbutton_initx - this.width; this.stopDrag(); } @@ -811,9 +814,9 @@ window.initScrollable = function () { return false; }; })(this)); - return this.tag.find("#privatekey-forgot").off("click, touchend").on("click touchend", (function(_this) { + return this.tag.find("#privatekey-forget").off("click, touchend").on("click touchend", (function(_this) { return function(e) { - _this.wrapper.displayConfirm("Remove saved private key for this site?", "Forgot", function(res) { + _this.wrapper.displayConfirm("Remove saved private key for this site?", "Forget", function(res) { if (!res) { return false; } @@ -1013,6 +1016,18 @@ window.initScrollable = function () { return false; }; })(this)); + this.tag.find("#button-autodownload_previous").off("click touchend").on("click touchend", (function(_this) { + return function() { + _this.wrapper.ws.cmd("siteUpdate", { + "address": _this.wrapper.site_info.address, + "check_files": true + }, function() { + return _this.wrapper.notifications.add("done-download_optional", "done", "Optional files downloaded", 5000); + }); + _this.wrapper.notifications.add("start-download_optional", "info", "Optional files download started", 5000); + return false; + }; + })(this)); this.tag.find("#button-dbreload").off("click touchend").on("click touchend", (function(_this) { return function() { _this.wrapper.ws.cmd("dbReload", [], function() { @@ -1345,6 +1360,7 @@ window.initScrollable = function () { }).call(this); + /* ---- morphdom.js ---- */ From 037f0a3ff43cafb39270c45e0773068a45eca63a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 7 Feb 2020 16:43:23 +0100 Subject: [PATCH 2321/2570] Rev4404 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 3bc695b5..7623e575 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4401 + self.rev = 4404 self.argv = argv self.action = None self.test_parser = None From 28ce08de8ef1455f7f4d23382d74fe5204e0900d Mon Sep 17 00:00:00 2001 From: tangdou1 <35254744+tangdou1@users.noreply.github.com> Date: Tue, 11 Feb 2020 23:12:06 +0800 Subject: [PATCH 2322/2570] Update zh.json (#2413) * Update zh.json * Update zh.json * Update zh.json --- plugins/Sidebar/languages/zh.json | 5 ++++- plugins/UiConfig/languages/zh.json | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/plugins/Sidebar/languages/zh.json b/plugins/Sidebar/languages/zh.json index 696084cf..639ac7f6 100644 --- a/plugins/Sidebar/languages/zh.json +++ b/plugins/Sidebar/languages/zh.json @@ -27,8 +27,11 @@ "Optional files": "可选文件", "Downloaded": "已下载", - "Download and help distribute all files": "下载并帮助分发所有文件", + "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": "已下载文件", diff --git a/plugins/UiConfig/languages/zh.json b/plugins/UiConfig/languages/zh.json index 6d6e9f80..9240b249 100644 --- a/plugins/UiConfig/languages/zh.json +++ b/plugins/UiConfig/languages/zh.json @@ -41,6 +41,19 @@ "Everything": "记录全部", "Only important messages": "仅记录重要信息", "Only errors": "仅记录错误", + "Threads for async file system reads": "文件系统异步读取线程", + "Threads for async file system writes": "文件系统异步写入线程", + "Threads for cryptographic functions": "密码函数线程", + "Threads for database operations": "数据库操作线程", + "Sync read": "同步读取", + "Sync write": "同步写入", + "Sync execution": "同步执行", + "1 thread": "1个线程", + "2 threads": "2个线程", + "3 threads": "3个线程", + "4 threads": "4个线程", + "5 threads": "5个线程", + "10 threads": "10个线程", " configuration item value changed": " 个配置值已经改变", "Save settings": "保存配置", From fefd2474b108eb4b8a38f5c4a58777501552067e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Feb 2020 17:22:09 +0100 Subject: [PATCH 2323/2570] Don't reload sites on listing --- src/Ui/UiWebsocket.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index d8441af6..e4819644 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -877,7 +877,6 @@ class UiWebsocket(object): @flag.admin def actionSiteList(self, to, connecting_sites=False): ret = [] - SiteManager.site_manager.load() # Reload sites for site in list(self.server.sites.values()): if not site.content_manager.contents.get("content.json") and not connecting_sites: continue # Incomplete site From 113b57415f2dee9227c50811e70e52671d215c11 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Feb 2020 17:22:37 +0100 Subject: [PATCH 2324/2570] More detailed info on origin error --- src/Ui/UiRequest.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index a549d42a..7c019a64 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -779,8 +779,9 @@ class UiRequest(object): if origin: origin_host = origin.split("://", 1)[-1] if origin_host != host and origin_host not in self.server.allowed_ws_origins: - ws.send(json.dumps({"error": "Invalid origin: %s" % origin})) - return self.error403("Invalid origin: %s" % origin) + error_message = "Invalid origin: %s (host: %s, allowed: %s)" % (origin, host, self.server.allowed_ws_origins) + ws.send(json.dumps({"error": error_message})) + return self.error403(error_message) # Find site by wrapper_key wrapper_key = self.get["wrapper_key"] From d36324e0d357f5286eaff78136ef11a5991fff5e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Feb 2020 17:23:00 +0100 Subject: [PATCH 2325/2570] More detailed info on http host error --- src/Ui/UiRequest.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 7c019a64..27fbfd72 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -112,8 +112,14 @@ class UiRequest(object): self_host = self.env["HTTP_HOST"].split(":")[0] self_ip = self.env["HTTP_HOST"].replace(self_host, socket.gethostbyname(self_host)) link = "http://{0}{1}".format(self_ip, http_get) - ret_link = """

    Access via ip: {0}""".format(html.escape(link)).encode("utf8") - return iter([ret_error, ret_link]) + ret_body = """ +

    Start the client with --ui_host "{host}" argument

    +

    or access via ip: {link}

    + """.format( + host=html.escape(self.env["HTTP_HOST"]), + link=html.escape(link) + ).encode("utf8") + return iter([ret_error, ret_body]) # Prepend .bit host for transparent proxy if self.isDomain(self.env.get("HTTP_HOST")): @@ -907,7 +913,8 @@ class UiRequest(object): else: return """

    %s

    %s

    From d2627f36d5cce66f6bfdcf22843554b50b9a68fa Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Feb 2020 17:23:37 +0100 Subject: [PATCH 2326/2570] Pass all arguments on site need --- src/Site/SiteManager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index a6ff4fee..73696688 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -165,7 +165,7 @@ class SiteManager(object): return site - def add(self, address, all_file=False, settings=None): + def add(self, address, all_file=False, settings=None, **kwargs): from .Site import Site self.sites_changed = int(time.time()) # Try to find site with differect case @@ -187,7 +187,7 @@ class SiteManager(object): return site # Return or create site and start download site files - def need(self, address, all_file=True, settings=None): + def need(self, address, *args, **kwargs): if self.isDomainCached(address): address_resolved = self.resolveDomainCached(address) if address_resolved: @@ -195,7 +195,7 @@ class SiteManager(object): site = self.get(address) if not site: # Site not exist yet - site = self.add(address, all_file=all_file, settings=settings) + site = self.add(address, *args, **kwargs) return site def delete(self, address): From 61ac6a30d306efeaa99659398e3832a2c1160ffe Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Feb 2020 17:24:22 +0100 Subject: [PATCH 2327/2570] Fix loading blocked raw sites --- plugins/TranslateSite/TranslateSitePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/TranslateSite/TranslateSitePlugin.py b/plugins/TranslateSite/TranslateSitePlugin.py index 01521921..d82d0ebe 100644 --- a/plugins/TranslateSite/TranslateSitePlugin.py +++ b/plugins/TranslateSite/TranslateSitePlugin.py @@ -25,6 +25,8 @@ class UiRequestPlugin(object): file_generator = super(UiRequestPlugin, self).actionSiteMedia(path, **kwargs) if "__next__" in dir(file_generator): # File found and generator returned site = self.server.sites.get(path_parts["address"]) + if not site or not site.content_manager.contents.get("content.json"): + return file_generator return self.actionPatchFile(site, path_parts["inner_path"], file_generator) else: return file_generator @@ -45,8 +47,6 @@ class UiRequestPlugin(object): def actionPatchFile(self, site, inner_path, file_generator): content_json = site.content_manager.contents.get("content.json") - if not content_json: - return file_generator lang_file = "languages/%s.json" % translate.lang lang_file_exist = False if site.settings.get("own"): # My site, check if the file is exist (allow to add new lang without signing) From 70cc982e2e925c356111eb7dbbe1b086a9ec2852 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Feb 2020 17:24:59 +0100 Subject: [PATCH 2328/2570] Log actual disabled function for multiuser plugin --- plugins/disabled-Multiuser/MultiuserPlugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/disabled-Multiuser/MultiuserPlugin.py b/plugins/disabled-Multiuser/MultiuserPlugin.py index e7eabdf1..799c3337 100644 --- a/plugins/disabled-Multiuser/MultiuserPlugin.py +++ b/plugins/disabled-Multiuser/MultiuserPlugin.py @@ -7,6 +7,7 @@ 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 @@ -212,7 +213,7 @@ class UiWebsocketPlugin(object): 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 is disabled on this proxy!"]) + self.cmd("notification", ["info", _("This function ({cmd}) is disabled on this proxy!")]) return False else: return super(UiWebsocketPlugin, self).hasCmdPermission(cmd) From bc76bf291a48411bdaa1071883d216bbf02b10cc Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Feb 2020 17:26:15 +0100 Subject: [PATCH 2329/2570] Fix site blocklist with address hash based blocking and move checking to server-side --- plugins/ContentFilter/ContentFilterPlugin.py | 47 ++++++++++++++++--- plugins/ContentFilter/ContentFilterStorage.py | 24 ++++++++-- plugins/ContentFilter/media/blocklisted.html | 22 +-------- 3 files changed, 63 insertions(+), 30 deletions(-) diff --git a/plugins/ContentFilter/ContentFilterPlugin.py b/plugins/ContentFilter/ContentFilterPlugin.py index 4d5d1b4c..f2f84b49 100644 --- a/plugins/ContentFilter/ContentFilterPlugin.py +++ b/plugins/ContentFilter/ContentFilterPlugin.py @@ -1,7 +1,6 @@ import time import re import html -import hashlib import os from Plugin import PluginManager @@ -26,9 +25,20 @@ class SiteManagerPlugin(object): filter_storage = ContentFilterStorage(site_manager=self) def add(self, address, *args, **kwargs): - if filter_storage.isSiteblocked(address): - details = filter_storage.getSiteblockDetails(address) - raise Exception("Site blocked: %s" % html.escape(details.get("reason", "unknown reason"))) + 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) @@ -79,6 +89,17 @@ class UiWebsocketPlugin(object): 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): @@ -97,6 +118,18 @@ class UiWebsocketPlugin(object): 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): @@ -202,11 +235,11 @@ class UiRequestPlugin(object): address = self.resolveDomain(address) if address: - address_sha256 = "0x" + hashlib.sha256(address.encode("utf8")).hexdigest() + address_hashed = filter_storage.getSiteAddressHashed(address) else: - address_sha256 = None + address_hashed = None - if filter_storage.isSiteblocked(address) or filter_storage.isSiteblocked(address_sha256): + 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 = {} diff --git a/plugins/ContentFilter/ContentFilterStorage.py b/plugins/ContentFilter/ContentFilterStorage.py index 2215ccca..70409d6b 100644 --- a/plugins/ContentFilter/ContentFilterStorage.py +++ b/plugins/ContentFilter/ContentFilterStorage.py @@ -3,12 +3,14 @@ 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") @@ -114,16 +116,32 @@ class ContentFilterStorage(object): 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 - else: - return False + return False def getSiteblockDetails(self, address): details = self.file_content["siteblocks"].get(address) if not details: - details = self.include_filters["siteblocks"].get(address) + 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 diff --git a/plugins/ContentFilter/media/blocklisted.html b/plugins/ContentFilter/media/blocklisted.html index 9a287b72..c9d201a9 100644 --- a/plugins/ContentFilter/media/blocklisted.html +++ b/plugins/ContentFilter/media/blocklisted.html @@ -62,25 +62,7 @@ class Page extends ZeroFrame { } async updateSiteblockDetails(address) { - var address_sha256 = await sha256hex(address) - var blocks = await this.cmdp("siteblockList") - if (blocks[address] || blocks[address_sha256]) { - block = blocks[address] - } else { - var includes = await this.cmdp("filterIncludeList", {all_sites: true, filters: true}) - for (let include of includes) { - if (include["siteblocks"][address]) { - var block = include["siteblocks"][address] - block["include"] = include - } - if (include["siteblocks"][address_sha256]) { - var block = include["siteblocks"][address_sha256] - block["include"] = include - } - } - } - - this.blocks = blocks + var block = await this.cmdp("siteblockGet", address) var reason = block["reason"] if (!reason) reason = "Unknown reason" var date = new Date(block["date_added"] * 1000) @@ -95,7 +77,7 @@ class Page extends ZeroFrame { document.getElementById("visit").style.opacity = "1" document.getElementById("visit").onclick = () => { if (block["include"]) - this.cmd("siteAdd", address, () => { this.cmd("wrapperReload") }) + this.cmd("siteblockIgnoreAddSite", address, () => { this.cmd("wrapperReload") }) else this.cmd("siteblockRemove", address, () => { this.cmd("wrapperReload") }) } From 8aa4e27938a08c2ac66d659f56835f81590a194e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Feb 2020 17:26:29 +0100 Subject: [PATCH 2330/2570] Rev4411 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 7623e575..aaaae028 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4404 + self.rev = 4411 self.argv = argv self.action = None self.test_parser = None From 64e5e0c80ebf38ec4971271a2db0ee986445d5b4 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 18 Feb 2020 15:28:14 +0100 Subject: [PATCH 2331/2570] Rev445, Fix and test random fail in CryptMessage decrypt --- plugins/CryptMessage/CryptMessage.py | 25 +++++++++++++++++++++---- plugins/CryptMessage/Test/TestCrypt.py | 19 +++++++++++++++++-- src/Config.py | 2 +- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/plugins/CryptMessage/CryptMessage.py b/plugins/CryptMessage/CryptMessage.py index b6c65673..38abbe5d 100644 --- a/plugins/CryptMessage/CryptMessage.py +++ b/plugins/CryptMessage/CryptMessage.py @@ -1,13 +1,13 @@ import hashlib import base64 -import binascii +import struct import lib.pybitcointools as btctools -from util import ThreadPool from Crypt import Crypt ecc_cache = {} + def eciesEncrypt(data, pubkey, ephemcurve=None, ciphername='aes-256-cbc'): from lib import pyelliptic pubkey_openssl = toOpensslPublickey(base64.b64decode(pubkey)) @@ -32,7 +32,7 @@ def eciesDecryptMulti(encrypted_datas, privatekey): try: text = eciesDecrypt(encrypted_data, privatekey).decode("utf8") texts.append(text) - except: + except Exception: texts.append(None) return texts @@ -41,9 +41,26 @@ def eciesDecrypt(encrypted_data, privatekey): ecc_key = getEcc(privatekey) return ecc_key.decrypt(base64.b64decode(encrypted_data)) + +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] - ciphertext = encrypted[16 + 70:-32] + curve, pubkey_x, pubkey_y, i = decodePubkey(encrypted[16:]) + ciphertext = encrypted[16 + i:-32] return iv, ciphertext diff --git a/plugins/CryptMessage/Test/TestCrypt.py b/plugins/CryptMessage/Test/TestCrypt.py index 05cc6e44..96f73761 100644 --- a/plugins/CryptMessage/Test/TestCrypt.py +++ b/plugins/CryptMessage/Test/TestCrypt.py @@ -57,12 +57,12 @@ class TestCrypt: assert decrypted != "hello" # Decrypt using correct privatekey - decrypted = ui_websocket.testAction("EciesDecrypt", encrypted) + decrypted = ui_websocket.testAction("EciesDecrypt", encrypted) assert decrypted == "hello" # Decrypt incorrect text decrypted = ui_websocket.testAction("EciesDecrypt", "baad") - assert decrypted == None + assert decrypted is None # Decrypt batch decrypted = ui_websocket.testAction("EciesDecrypt", [encrypted, "baad", encrypted]) @@ -90,6 +90,21 @@ class TestCrypt: 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() diff --git a/src/Config.py b/src/Config.py index aaaae028..eddfefb2 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4411 + self.rev = 4445 self.argv = argv self.action = None self.test_parser = None From 8facd9ff843d98e6a000876db4073c38ecfeb8e9 Mon Sep 17 00:00:00 2001 From: canewsin Date: Tue, 18 Feb 2020 23:09:16 +0530 Subject: [PATCH 2332/2570] Added Custom Openssl Path for Native Clients and start_dir config This Parameter helpful where openssl path is not fixed always, we can also use this to reduce code verbosity by providing other like these and provide them as parameter if sys.platform.startswith("win"): self.openssl_bin = "tools\\openssl\\openssl.exe" elif config.dist_type.startswith("bundle_linux"): self.openssl_bin = "../runtime/bin/openssl" else: self.openssl_bin = "openssl" Also Added Custom start_dir config option since android path issue of not valid "./" path, where files via provided path are not loading on some systems like Android client. for more detailed conversation see pull request [#2422](https://github.com/HelloZeroNet/ZeroNet/pull/2422) --- src/Config.py | 9 +++++++-- src/Crypt/CryptConnection.py | 13 ++++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Config.py b/src/Config.py index eddfefb2..d482d892 100644 --- a/src/Config.py +++ b/src/Config.py @@ -29,6 +29,7 @@ class Config(object): ]) self.start_dir = self.getStartDir() + self.openssl_path = "default" self.config_file = self.start_dir + "/zeronet.conf" self.data_dir = self.start_dir + "/data" self.log_dir = self.start_dir + "/log" @@ -106,6 +107,8 @@ class Config(object): else: fix_float_decimals = False + openssl_path = "default" + start_dir = self.start_dir config_file = self.start_dir + "/zeronet.conf" data_dir = self.start_dir + "/data" log_dir = self.start_dir + "/log" @@ -220,6 +223,7 @@ class Config(object): self.parser.add_argument('--batch', help="Batch mode (No interactive input for commands)", action='store_true') + self.parser.add_argument('--start_dir', help='Start Directory of ZeroNet(Usually dir where zeronet.py Exists)',default=start_dir, metavar="path") self.parser.add_argument('--config_file', help='Path of config file', default=config_file, metavar="path") self.parser.add_argument('--data_dir', help='Path of data directory', default=data_dir, metavar="path") @@ -259,6 +263,7 @@ class Config(object): self.parser.add_argument('--ip_external', help='Set reported external ip (tested on start if None)', metavar='ip', nargs='*') self.parser.add_argument('--offline', help='Disable network communication', action='store_true') + self.parser.add_argument('--openssl_path', help='Custom Path to OpenSSL Binary', default=openssl_path, metavar="path") self.parser.add_argument('--disable_udp', help='Disable UDP connections', action='store_true') self.parser.add_argument('--proxy', help='Socks proxy address', metavar='ip:port') self.parser.add_argument('--bind', help='Bind outgoing sockets to this address', metavar='ip') @@ -479,7 +484,7 @@ class Config(object): for key, val in args.items(): if type(val) is list: val = val[:] - if key in ("data_dir", "log_dir"): + if key in ("start_dir", "data_dir", "log_dir", "openssl_path"): val = val.replace("\\", "/") setattr(self, key, val) @@ -560,7 +565,7 @@ class Config(object): "language": self.language, "debug": self.debug, "plugins": PluginManager.plugin_manager.plugin_names, - + "openssl_path": os.path.abspath(self.openssl_path), "log_dir": os.path.abspath(self.log_dir), "data_dir": os.path.abspath(self.data_dir), "src_dir": os.path.dirname(os.path.abspath(__file__)) diff --git a/src/Crypt/CryptConnection.py b/src/Crypt/CryptConnection.py index 689357fa..3f6279fb 100644 --- a/src/Crypt/CryptConnection.py +++ b/src/Crypt/CryptConnection.py @@ -11,12 +11,15 @@ from util import helper class CryptConnectionManager: def __init__(self): - if sys.platform.startswith("win"): - self.openssl_bin = "tools\\openssl\\openssl.exe" - elif config.dist_type.startswith("bundle_linux"): - self.openssl_bin = "../runtime/bin/openssl" + if config.openssl_path != "default": + self.openssl_bin = config.openssl_path else: - self.openssl_bin = "openssl" + if sys.platform.startswith("win"): + self.openssl_bin = "tools\\openssl\\openssl.exe" + elif config.dist_type.startswith("bundle_linux"): + self.openssl_bin = "../runtime/bin/openssl" + else: + self.openssl_bin = "openssl" self.context_client = None self.context_server = None From 2c826eba2d3aaaeaba397de9f90ce42e1decef07 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 19 Feb 2020 16:48:14 +0100 Subject: [PATCH 2333/2570] Rev4447, Fix Msgpack 1.0.0 compatibility --- src/Config.py | 2 +- src/util/Msgpack.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Config.py b/src/Config.py index eddfefb2..caac7f2d 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4445 + self.rev = 4447 self.argv = argv self.action = None self.test_parser = None diff --git a/src/util/Msgpack.py b/src/util/Msgpack.py index f87b95b0..1033f92e 100644 --- a/src/util/Msgpack.py +++ b/src/util/Msgpack.py @@ -78,10 +78,14 @@ def getUnpacker(fallback=False, decode=True): else: unpacker = msgpack.Unpacker + extra_kwargs = {"max_buffer_size": 5 * 1024 * 1024} + if msgpack.version[0] >= 1: + extra_kwargs["strict_map_key"] = False + if decode: # Workaround for backward compatibility: Try to decode bin to str - unpacker = unpacker(raw=True, object_pairs_hook=objectDecoderHook, max_buffer_size=5 * 1024 * 1024) + unpacker = unpacker(raw=True, object_pairs_hook=objectDecoderHook, **extra_kwargs) else: - unpacker = unpacker(raw=False, max_buffer_size=5 * 1024 * 1024) + unpacker = unpacker(raw=False, **extra_kwargs) return unpacker From fca1033f83e9b82e94733e1e7452bf88f61cc377 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 20 Feb 2020 17:18:59 +0100 Subject: [PATCH 2334/2570] Fix trayicon auto start script write/read with utf8 path --- plugins/Trayicon/TrayiconPlugin.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/Trayicon/TrayiconPlugin.py b/plugins/Trayicon/TrayiconPlugin.py index e9b12a26..622f742a 100644 --- a/plugins/Trayicon/TrayiconPlugin.py +++ b/plugins/Trayicon/TrayiconPlugin.py @@ -140,17 +140,17 @@ class ActionsPlugin(object): cmd = cmd.replace("start.py", "zeronet.py").replace('"--open_browser"', "").replace('"default_browser"', "").strip() cmd += ' --open_browser ""' - return """ - @echo off - chcp 65001 > nul - set PYTHONIOENCODING=utf-8 - cd /D \"%s\" - start "" %s - """ % (cwd, cmd) + return "\r\n".join([ + '@echo off', + 'chcp 65001 > nul', + 'set PYTHONIOENCODING=utf-8', + 'cd /D \"%s\"' % cwd, + 'start "" %s' % cmd + ]) def isAutorunEnabled(self): path = self.getAutorunPath() - return os.path.isfile(path) and open(path).read() == self.formatAutorun() + return os.path.isfile(path) and open(path, "rb").read().decode("utf8") == self.formatAutorun() def titleAutorun(self): translate = _["Start ZeroNet when Windows starts"] @@ -163,4 +163,4 @@ class ActionsPlugin(object): if self.isAutorunEnabled(): os.unlink(self.getAutorunPath()) else: - open(self.getAutorunPath(), "w").write(self.formatAutorun()) + open(self.getAutorunPath(), "wb").write(self.formatAutorun().encode("utf8")) From b1819ff71d2b66b25db095d10284b8ac2b5f3724 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 20 Feb 2020 17:19:16 +0100 Subject: [PATCH 2335/2570] Fix trayicon autostart script duplicated arguments --- plugins/Trayicon/TrayiconPlugin.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/Trayicon/TrayiconPlugin.py b/plugins/Trayicon/TrayiconPlugin.py index 622f742a..2dc3a5c6 100644 --- a/plugins/Trayicon/TrayiconPlugin.py +++ b/plugins/Trayicon/TrayiconPlugin.py @@ -132,12 +132,17 @@ class ActionsPlugin(object): else: cwd = os.path.dirname(sys.executable) + ignored_args = [ + "--open_browser", "default_browser", + "--dist_type", "bundle_win64" + ] + if sys.platform == 'win32': - args = ['"%s"' % arg for arg in args if arg] + args = ['"%s"' % arg for arg in args if arg and arg not in ignored_args] cmd = " ".join(args) # Dont open browser on autorun - cmd = cmd.replace("start.py", "zeronet.py").replace('"--open_browser"', "").replace('"default_browser"', "").strip() + cmd = cmd.replace("start.py", "zeronet.py").strip() cmd += ' --open_browser ""' return "\r\n".join([ From 1cc0ec3f31b3b8db4a534ec64ebe82629b81fa2e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 20 Feb 2020 17:23:00 +0100 Subject: [PATCH 2336/2570] Indepently configurable OpenSSL lib/bin file --- src/Config.py | 13 ++++++++----- src/Crypt/CryptConnection.py | 15 +++++++-------- src/util/OpensslFindPatch.py | 4 ++++ 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/Config.py b/src/Config.py index 6645baea..ba9139cc 100644 --- a/src/Config.py +++ b/src/Config.py @@ -33,6 +33,8 @@ class Config(object): self.config_file = self.start_dir + "/zeronet.conf" self.data_dir = self.start_dir + "/data" self.log_dir = self.start_dir + "/log" + self.openssl_lib_file = None + self.openssl_bin_file = None self.trackers_file = False self.createParser() @@ -107,7 +109,6 @@ class Config(object): else: fix_float_decimals = False - openssl_path = "default" start_dir = self.start_dir config_file = self.start_dir + "/zeronet.conf" data_dir = self.start_dir + "/data" @@ -263,7 +264,6 @@ class Config(object): self.parser.add_argument('--ip_external', help='Set reported external ip (tested on start if None)', metavar='ip', nargs='*') self.parser.add_argument('--offline', help='Disable network communication', action='store_true') - self.parser.add_argument('--openssl_path', help='Custom Path to OpenSSL Binary', default=openssl_path, metavar="path") self.parser.add_argument('--disable_udp', help='Disable UDP connections', action='store_true') self.parser.add_argument('--proxy', help='Socks proxy address', metavar='ip:port') self.parser.add_argument('--bind', help='Bind outgoing sockets to this address', metavar='ip') @@ -272,6 +272,8 @@ class Config(object): self.parser.add_argument('--trackers_proxy', help='Force use proxy to connect to trackers (disable, tor, ip:port)', default="disable") self.parser.add_argument('--use_libsecp256k1', help='Use Libsecp256k1 liblary for speedup', type='bool', choices=[True, False], default=True) self.parser.add_argument('--use_openssl', help='Use OpenSSL liblary for speedup', type='bool', choices=[True, False], default=True) + self.parser.add_argument('--openssl_lib_file', help='Path for OpenSSL library file (default: detect)', default=argparse.SUPPRESS, metavar="path") + self.parser.add_argument('--openssl_bin_file', help='Path for OpenSSL binary file (default: detect)', default=argparse.SUPPRESS, metavar="path") self.parser.add_argument('--disable_db', help='Disable database updating', action='store_true') self.parser.add_argument('--disable_encryption', help='Disable connection encryption', action='store_true') self.parser.add_argument('--force_encryption', help="Enforce encryption to all peer connections", action='store_true') @@ -484,8 +486,9 @@ class Config(object): for key, val in args.items(): if type(val) is list: val = val[:] - if key in ("start_dir", "data_dir", "log_dir", "openssl_path"): - val = val.replace("\\", "/") + if key in ("data_dir", "log_dir", "start_dir", "openssl_bin_file", "openssl_lib_file"): + if val: + val = val.replace("\\", "/") setattr(self, key, val) def loadPlugins(self): @@ -565,7 +568,7 @@ class Config(object): "language": self.language, "debug": self.debug, "plugins": PluginManager.plugin_manager.plugin_names, - "openssl_path": os.path.abspath(self.openssl_path), + "log_dir": os.path.abspath(self.log_dir), "data_dir": os.path.abspath(self.data_dir), "src_dir": os.path.dirname(os.path.abspath(__file__)) diff --git a/src/Crypt/CryptConnection.py b/src/Crypt/CryptConnection.py index 3f6279fb..ebbc6295 100644 --- a/src/Crypt/CryptConnection.py +++ b/src/Crypt/CryptConnection.py @@ -11,15 +11,14 @@ from util import helper class CryptConnectionManager: def __init__(self): - if config.openssl_path != "default": - self.openssl_bin = config.openssl_path + if config.openssl_bin_file: + self.openssl_bin = config.openssl_bin_file + elif sys.platform.startswith("win"): + self.openssl_bin = "tools\\openssl\\openssl.exe" + elif config.dist_type.startswith("bundle_linux"): + self.openssl_bin = "../runtime/bin/openssl" else: - if sys.platform.startswith("win"): - self.openssl_bin = "tools\\openssl\\openssl.exe" - elif config.dist_type.startswith("bundle_linux"): - self.openssl_bin = "../runtime/bin/openssl" - else: - self.openssl_bin = "openssl" + self.openssl_bin = "openssl" self.context_client = None self.context_server = None diff --git a/src/util/OpensslFindPatch.py b/src/util/OpensslFindPatch.py index d6851e32..fd09ec6b 100644 --- a/src/util/OpensslFindPatch.py +++ b/src/util/OpensslFindPatch.py @@ -4,11 +4,15 @@ import sys import ctypes import ctypes.util +from Config import config find_library_original = ctypes.util.find_library def getOpensslPath(): + if config.openssl_lib_file: + return config.openssl_lib_file + if sys.platform.startswith("win"): lib_paths = [ os.path.join(os.getcwd(), "tools/openssl/libeay32.dll"), # ZeroBundle Windows From a9c75a3146c05dce260165a190b6bc5b7dd3edb4 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 20 Feb 2020 17:25:06 +0100 Subject: [PATCH 2337/2570] Fix start dir parsing for command line and better description --- src/Config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Config.py b/src/Config.py index ba9139cc..d0e4cd27 100644 --- a/src/Config.py +++ b/src/Config.py @@ -29,7 +29,6 @@ class Config(object): ]) self.start_dir = self.getStartDir() - self.openssl_path = "default" self.config_file = self.start_dir + "/zeronet.conf" self.data_dir = self.start_dir + "/data" self.log_dir = self.start_dir + "/log" @@ -56,7 +55,9 @@ class Config(object): def getStartDir(self): this_file = os.path.abspath(__file__).replace("\\", "/").rstrip("cd") - if this_file.endswith("/Contents/Resources/core/src/Config.py"): + if "--start_dir" in self.argv: + start_dir = self.argv[self.argv.index("--start_dir") + 1] + elif this_file.endswith("/Contents/Resources/core/src/Config.py"): # Running as ZeroNet.app if this_file.startswith("/Application") or this_file.startswith("/private") or this_file.startswith(os.path.expanduser("~/Library")): # Runnig from non-writeable directory, put data to Application Support @@ -109,7 +110,6 @@ class Config(object): else: fix_float_decimals = False - start_dir = self.start_dir config_file = self.start_dir + "/zeronet.conf" data_dir = self.start_dir + "/data" log_dir = self.start_dir + "/log" @@ -224,7 +224,7 @@ class Config(object): self.parser.add_argument('--batch', help="Batch mode (No interactive input for commands)", action='store_true') - self.parser.add_argument('--start_dir', help='Start Directory of ZeroNet(Usually dir where zeronet.py Exists)',default=start_dir, metavar="path") + self.parser.add_argument('--start_dir', help='Path of working dir for variable content (data, log, .conf)', default=self.start_dir, metavar="path") self.parser.add_argument('--config_file', help='Path of config file', default=config_file, metavar="path") self.parser.add_argument('--data_dir', help='Path of data directory', default=data_dir, metavar="path") From 9b85d8638d30205c72ca8c8c162bcd01022f5405 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 20 Feb 2020 17:25:56 +0100 Subject: [PATCH 2338/2570] Don't allow run site api calls when site is deleting --- src/Site/Site.py | 1 + src/Ui/UiWebsocket.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/Site/Site.py b/src/Site/Site.py index 09ff03c9..32f10abe 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -1059,6 +1059,7 @@ class Site(object): self.log.info("Deleting site...") s = time.time() self.settings["serving"] = False + self.settings["deleting"] = True self.saveSettings() num_greenlets = self.greenlet_manager.stopGreenlets("Site %s deleted" % self.address) self.worker_manager.running = False diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index e4819644..7fce398b 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -216,6 +216,9 @@ class UiWebsocket(object): else: # Normal command func_name = self.getCmdFuncName(cmd) func = getattr(self, func_name, None) + if self.site.settings.get("deleting"): + return self.response(req["id"], {"error": "Site is deleting"}) + if not func: # Unknown command return self.response(req["id"], {"error": "Unknown command: %s" % cmd}) From ae9a76a6c93d9bea6fb9e6561161b21014b60ad6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 20 Feb 2020 17:27:31 +0100 Subject: [PATCH 2339/2570] Fix double sites.json loading on startup when adding missing sites --- src/Site/SiteManager.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index 73696688..e1a7c7c3 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -44,6 +44,8 @@ class SiteManager(object): except Exception as err: raise Exception("Unable to load %s: %s" % (json_path, err)) + sites_need = [] + for address, settings in data.items(): if address not in self.sites: if os.path.isfile("%s/%s/content.json" % (config.data_dir, address)): @@ -61,7 +63,7 @@ class SiteManager(object): elif startup: # No site directory, start download self.log.debug("Found new site in sites.json: %s" % address) - gevent.spawn(self.need, address, settings=settings) + sites_need.append([address, settings]) added += 1 address_found.append(address) @@ -90,9 +92,11 @@ class SiteManager(object): if address in content_db.sites: del content_db.sites[address] + self.loaded = True + for address, settings in sites_need: + gevent.spawn(self.need, address, settings=settings) if added: self.log.info("Added %s sites in %.3fs" % (added, time.time() - load_s)) - self.loaded = True def saveDelayed(self): RateLimit.callAsync("Save sites.json", allowed_again=5, func=self.save) From 8b994e42c23a49225833357240c73d51896da6b2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 20 Feb 2020 17:27:50 +0100 Subject: [PATCH 2340/2570] Rev4452 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index d0e4cd27..c2e6985a 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4447 + self.rev = 4452 self.argv = argv self.action = None self.test_parser = None From f0a706f6ab847ba22798a8107b4b1f15d79de6b7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 21 Feb 2020 13:58:11 +0100 Subject: [PATCH 2341/2570] Rev4455, Fix new sites file downloading --- src/Config.py | 2 +- src/Site/SiteManager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index c2e6985a..facf575e 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4452 + self.rev = 4455 self.argv = argv self.action = None self.test_parser = None diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index e1a7c7c3..44773b0d 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -169,7 +169,7 @@ class SiteManager(object): return site - def add(self, address, all_file=False, settings=None, **kwargs): + def add(self, address, all_file=True, settings=None, **kwargs): from .Site import Site self.sites_changed = int(time.time()) # Try to find site with differect case From f8e2cbe4295101605bead86f5479f2e617002fd6 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Mon, 24 Feb 2020 15:46:01 +0300 Subject: [PATCH 2342/2570] Allow uploading files via websocket (#2437) * Allow uploading files via websocket * Fix --- plugins/Bigfile/BigfilePlugin.py | 85 +++++++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 8 deletions(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index 053098a8..6bffb69e 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -61,13 +61,44 @@ class UiRequestPlugin(object): }) 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( - self.env['wsgi.input'], upload_info["size"], upload_info["piece_size"], out_file + read, upload_info["size"], upload_info["piece_size"], out_file ) if len(piecemap_info["sha512_pieces"]) == 1: # Small file, don't split @@ -106,12 +137,12 @@ class UiRequestPlugin(object): site.content_manager.contents.loadItem(file_info["content_inner_path"]) # reload cache - return json.dumps({ + 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 @@ -169,6 +200,44 @@ class UiWebsocketPlugin(object): "file_relative_path": file_relative_path } + def actionBigfileUploadInitWebsocket(self, to, inner_path, size): + 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" + } + + 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 + } + @flag.no_multiuser def actionSiteSetAutodownloadBigfileLimit(self, to, limit): permissions = self.getPermissions(to) @@ -210,14 +279,14 @@ class ContentManagerPlugin(object): file_info = super(ContentManagerPlugin, self).getFileInfo(inner_path, *args, **kwargs) return file_info - def readFile(self, file_in, size, buff_size=1024 * 64): + 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 = file_in.read(read_size) + part = read_func(read_size) if not part: break @@ -230,7 +299,7 @@ class ContentManagerPlugin(object): if recv_left <= 0: break - def hashBigfile(self, file_in, size, piece_size=1024 * 1024, file_out=None): + def hashBigfile(self, read_func, size, piece_size=1024 * 1024, file_out=None): self.site.settings["has_bigfile"] = True recv = 0 @@ -243,7 +312,7 @@ class ContentManagerPlugin(object): mt.hash_function = CryptHash.sha512t part = "" - for part in self.readFile(file_in, size): + for part in self.readFile(read_func, size): if file_out: file_out.write(part) @@ -309,7 +378,7 @@ class ContentManagerPlugin(object): 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"), file_size) + merkle_root, piece_size, piecemap_info = self.hashBigfile(self.site.storage.open(inner_path, "rb").read, file_size) if not hash: hash = merkle_root From 17f65a51796023f5ecacadb79596636521518dba Mon Sep 17 00:00:00 2001 From: Ivanq Date: Mon, 24 Feb 2020 19:19:35 +0300 Subject: [PATCH 2343/2570] Avoid code duplication --- plugins/Bigfile/BigfilePlugin.py | 65 +++++++++++--------------------- 1 file changed, 22 insertions(+), 43 deletions(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index 6bffb69e..41506f13 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -169,38 +169,7 @@ class UiRequestPlugin(object): @PluginManager.registerTo("UiWebsocket") class UiWebsocketPlugin(object): - def actionBigfileUploadInit(self, to, inner_path, size): - 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" - } - return { - "url": "/ZeroNet-Internal/BigfileUpload?upload_nonce=" + nonce, - "piece_size": piece_size, - "inner_path": inner_path, - "file_relative_path": file_relative_path - } - - def actionBigfileUploadInitWebsocket(self, to, inner_path, size): + 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: @@ -225,18 +194,28 @@ class UiWebsocketPlugin(object): "piecemap": inner_path + ".piecemap.msgpack" } - server_url = self.request.getWsServerUrl() - if server_url: - proto, host = server_url.split("://") - origin = proto.replace("http", "ws") + "://" + host + 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: - 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 - } + return {"error": "Unknown protocol"} @flag.no_multiuser def actionSiteSetAutodownloadBigfileLimit(self, to, limit): From 6a1235bd456e69ed44359af62ba20acd6971a1e8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 25 Feb 2020 16:45:55 +0100 Subject: [PATCH 2344/2570] Remove old Gevent RLock support --- src/Tor/TorManager.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Tor/TorManager.py b/src/Tor/TorManager.py index 7c3d7277..48db2752 100644 --- a/src/Tor/TorManager.py +++ b/src/Tor/TorManager.py @@ -15,11 +15,7 @@ from Config import config from Crypt import CryptRsa from Site import SiteManager import socks -try: - from gevent.coros import RLock -except: - from gevent.lock import RLock -from util import helper +from gevent.lock import RLock from Debug import Debug from Plugin import PluginManager From b85477787dddaf1e485ac7511059b578ae155b32 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 25 Feb 2020 16:46:21 +0100 Subject: [PATCH 2345/2570] Workaround for Tor utf8 cookie file path encoding bug on Windows --- src/Tor/TorManager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Tor/TorManager.py b/src/Tor/TorManager.py index 48db2752..7e5c8bb0 100644 --- a/src/Tor/TorManager.py +++ b/src/Tor/TorManager.py @@ -152,6 +152,9 @@ class TorManager(object): res_auth = self.send('AUTHENTICATE "%s"' % config.tor_password, conn) elif cookie_match: cookie_file = cookie_match.group(1).encode("ascii").decode("unicode_escape") + if not os.path.isfile(cookie_file) and self.tor_process: + # Workaround for tor client cookie auth file utf8 encoding bug (https://github.com/torproject/stem/issues/57) + cookie_file = os.path.dirname(self.tor_exe) + "\\data\\control_auth_cookie" auth_hex = binascii.b2a_hex(open(cookie_file, "rb").read()) res_auth = self.send("AUTHENTICATE %s" % auth_hex.decode("utf8"), conn) else: From 58f03e21ef381701a6115cce50a721d5d940378c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 25 Feb 2020 16:47:04 +0100 Subject: [PATCH 2346/2570] Change unreliable trackers --- src/Config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index facf575e..4b6832c2 100644 --- a/src/Config.py +++ b/src/Config.py @@ -82,9 +82,9 @@ class Config(object): "zero://boot3rdez4rzn36x.onion:15441", "zero://zero.booth.moe#f36ca555bee6ba216b14d10f38c16f7769ff064e0e37d887603548cc2e64191d:443", # US/NY "udp://tracker.coppersurfer.tk:6969", # DE - "udp://tracker.zum.bi:6969", # US/NY "udp://104.238.198.186:8000", # US/LA - "http://tracker01.loveapp.com:6789/announce", # Google + "udp://retracker.akado-ural.ru:80", # RU + "http://h4.trakx.nibba.trade:80/announce", # US/VA "http://open.acgnxtracker.com:80/announce", # DE "http://tracker.bt4g.com:2095/announce", # Cloudflare "zero://2602:ffc5::c5b2:5360:26312" # US/ATL From 6218a92895bc4c16829ac3e347fb7bb300a4311d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 25 Feb 2020 16:47:28 +0100 Subject: [PATCH 2347/2570] Rev4458 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 4b6832c2..6dfd25b3 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4455 + self.rev = 4458 self.argv = argv self.action = None self.test_parser = None From 2862587c152bb02c8671409eae0a5f307c279228 Mon Sep 17 00:00:00 2001 From: krzotr Date: Thu, 27 Feb 2020 00:48:26 +0100 Subject: [PATCH 2348/2570] Fixed "LookupError: 'hex' is not a text encoding" on /StatsBootstrapper page (#2442) * Fixed "LookupError: 'hex' is not a text encoding" * Fixed KeyError: 'ip4' --- plugins/disabled-Bootstrapper/BootstrapperPlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/disabled-Bootstrapper/BootstrapperPlugin.py b/plugins/disabled-Bootstrapper/BootstrapperPlugin.py index 474f79c1..59e7af7b 100644 --- a/plugins/disabled-Bootstrapper/BootstrapperPlugin.py +++ b/plugins/disabled-Bootstrapper/BootstrapperPlugin.py @@ -150,7 +150,7 @@ class UiRequestPlugin(object): ).fetchall() yield "
    %s (added: %s, peers: %s)
    " % ( - str(hash_row["hash"]).encode("hex"), hash_row["date_added"], len(peer_rows) + 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)) + yield " - {type} {address}:{port} added: {date_added}, announced: {date_announced}
    ".format(**dict(peer_row)) From 219b90668f673ee0c9cf54b633397641b2a985f6 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Fri, 28 Feb 2020 03:20:04 +0300 Subject: [PATCH 2349/2570] Switch from gevent-websocket to gevent-ws (#2439) * Switch from gevent-websocket to gevent-ws * Return error handling, add gevent_ws source to lib --- requirements.txt | 2 +- src/Config.py | 1 - src/Ui/UiRequest.py | 2 +- src/Ui/UiServer.py | 31 ++-- src/lib/gevent_ws/__init__.py | 256 ++++++++++++++++++++++++++++++++++ 5 files changed, 268 insertions(+), 24 deletions(-) create mode 100644 src/lib/gevent_ws/__init__.py diff --git a/requirements.txt b/requirements.txt index 3a542131..c2893480 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ rsa PySocks>=1.6.8 pyasn1 websocket_client -gevent-websocket +gevent-ws coincurve python-bitcoinlib maxminddb diff --git a/src/Config.py b/src/Config.py index 6dfd25b3..29e05739 100644 --- a/src/Config.py +++ b/src/Config.py @@ -646,7 +646,6 @@ class Config(object): logging.addLevelName(15, "WARNING") logging.getLogger('').name = "-" # Remove root prefix - logging.getLogger("geventwebsocket.handler").setLevel(logging.WARNING) # Don't log ws debug messages if console_logging: self.initConsoleLogger() diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 27fbfd72..075462e7 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -814,7 +814,7 @@ class UiRequest(object): # 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) - return "Bye." + return [b"Bye."] else: # No site found by wrapper key ws.send(json.dumps({"error": "Wrapper key not found: %s" % wrapper_key})) return self.error403("Wrapper key not found: %s" % wrapper_key) diff --git a/src/Ui/UiServer.py b/src/Ui/UiServer.py index 7f6f35b7..188ff811 100644 --- a/src/Ui/UiServer.py +++ b/src/Ui/UiServer.py @@ -5,8 +5,7 @@ import socket import gevent from gevent.pywsgi import WSGIServer -from gevent.pywsgi import WSGIHandler -from geventwebsocket.handler import WebSocketHandler +from lib.gevent_ws import WebSocketHandler from .UiRequest import UiRequest from Site import SiteManager @@ -27,7 +26,7 @@ class LogDb(logging.StreamHandler): # Skip websocket handler if not necessary -class UiWSGIHandler(WSGIHandler): +class UiWSGIHandler(WebSocketHandler): def __init__(self, *args, **kwargs): self.server = args[2] @@ -46,24 +45,14 @@ class UiWSGIHandler(WSGIHandler): self.write(block) def run_application(self): - if "HTTP_UPGRADE" in self.environ: # Websocket request - try: - ws_handler = WebSocketHandler(*self.args, **self.kwargs) - ws_handler.__dict__ = self.__dict__ # Match class variables - ws_handler.run_application() - except (ConnectionAbortedError, ConnectionResetError) as err: - logging.warning("UiWSGIHandler websocket connection error: %s" % err) - except Exception as err: - logging.error("UiWSGIHandler websocket error: %s" % Debug.formatException(err)) - self.handleError(err) - else: # Standard HTTP request - try: - super(UiWSGIHandler, self).run_application() - except (ConnectionAbortedError, ConnectionResetError) as err: - logging.warning("UiWSGIHandler connection error: %s" % err) - except Exception as err: - logging.error("UiWSGIHandler error: %s" % Debug.formatException(err)) - self.handleError(err) + err_name = "UiWSGIHandler websocket" if "HTTP_UPGRADE" in self.environ else "UiWSGIHandler" + try: + super(UiWSGIHandler, self).run_application() + except (ConnectionAbortedError, ConnectionResetError) as err: + logging.warning("%s connection error: %s" % (err_name, err)) + except Exception as err: + logging.warning("%s error: %s" % (err_name, Debug.formatException(err))) + self.handleError(err) def handle(self): # Save socket to be able to close them properly on exit diff --git a/src/lib/gevent_ws/__init__.py b/src/lib/gevent_ws/__init__.py new file mode 100644 index 00000000..8ad74155 --- /dev/null +++ b/src/lib/gevent_ws/__init__.py @@ -0,0 +1,256 @@ +from gevent.pywsgi import WSGIHandler, _InvalidClientInput +from gevent.queue import Queue +import gevent +import hashlib +import base64 +import struct +import socket +import time +import sys + + +SEND_PACKET_SIZE = 1300 +OPCODE_TEXT = 1 +OPCODE_BINARY = 2 +OPCODE_CLOSE = 8 +OPCODE_PING = 9 +OPCODE_PONG = 10 +STATUS_OK = 1000 +STATUS_PROTOCOL_ERROR = 1002 +STATUS_DATA_ERROR = 1007 +STATUS_POLICY_VIOLATION = 1008 +STATUS_TOO_LONG = 1009 + + +class WebSocket: + def __init__(self, socket): + self.socket = socket + self.closed = False + self.status = None + self._receive_error = None + self._queue = Queue() + self.max_length = 10 * 1024 * 1024 + gevent.spawn(self._listen) + + + def set_max_message_length(self, length): + self.max_length = length + + + def _listen(self): + try: + while True: + fin = False + message = bytearray() + is_first_message = True + start_opcode = None + while not fin: + payload, opcode, fin = self._get_frame(max_length=self.max_length - len(message)) + # Make sure continuation frames have correct information + if not is_first_message and opcode != 0: + self._error(STATUS_PROTOCOL_ERROR) + if is_first_message: + if opcode not in (OPCODE_TEXT, OPCODE_BINARY): + self._error(STATUS_PROTOCOL_ERROR) + # Save opcode + start_opcode = opcode + message += payload + is_first_message = False + message = bytes(message) + if start_opcode == OPCODE_TEXT: # UTF-8 text + try: + message = message.decode() + except UnicodeDecodeError: + self._error(STATUS_DATA_ERROR) + self._queue.put(message) + except Exception as e: + self.closed = True + self._receive_error = e + self._queue.put(None) # To make sure the error is read + + + def receive(self): + if not self._queue.empty(): + return self.receive_nowait() + if isinstance(self._receive_error, EOFError): + return None + if self._receive_error: + raise self._receive_error + self._queue.peek() + return self.receive_nowait() + + + def receive_nowait(self): + ret = self._queue.get_nowait() + if self._receive_error and not isinstance(self._receive_error, EOFError): + raise self._receive_error + return ret + + + def send(self, data): + if self.closed: + raise EOFError() + if isinstance(data, str): + self._send_frame(OPCODE_TEXT, data.encode()) + elif isinstance(data, bytes): + self._send_frame(OPCODE_BINARY, data) + else: + raise TypeError("Expected str or bytes, got " + repr(type(data))) + + + # Reads a frame from the socket. Pings, pongs and close packets are handled + # automatically + def _get_frame(self, max_length): + while True: + payload, opcode, fin = self._read_frame(max_length=max_length) + if opcode == OPCODE_PING: + self._send_frame(OPCODE_PONG, payload) + elif opcode == OPCODE_PONG: + pass + elif opcode == OPCODE_CLOSE: + if len(payload) >= 2: + self.status = struct.unpack("!H", payload[:2])[0] + was_closed = self.closed + self.closed = True + if not was_closed: + # Send a close frame in response + self.close(STATUS_OK) + raise EOFError() + else: + return payload, opcode, fin + + + # Low-level function, use _get_frame instead + def _read_frame(self, max_length): + header = self._recv_exactly(2) + + if not (header[1] & 0x80): + self._error(STATUS_POLICY_VIOLATION) + + opcode = header[0] & 0xf + fin = bool(header[0] & 0x80) + + payload_length = header[1] & 0x7f + if payload_length == 126: + payload_length = struct.unpack("!H", self._recv_exactly(2))[0] + elif payload_length == 127: + payload_length = struct.unpack("!Q", self._recv_exactly(8))[0] + + # Control frames are handled in a special way + if opcode in (OPCODE_PING, OPCODE_PONG): + max_length = 125 + + if payload_length > max_length: + self._error(STATUS_TOO_LONG) + + mask = self._recv_exactly(4) + payload = self._recv_exactly(payload_length) + payload = self._unmask(payload, mask) + + return payload, opcode, fin + + + def _recv_exactly(self, length): + buf = bytearray() + while len(buf) < length: + block = self.socket.recv(min(4096, length - len(buf))) + if block == b"": + raise EOFError() + buf += block + return bytes(buf) + + + def _unmask(self, payload, mask): + def gen(c): + return bytes([x ^ c for x in range(256)]) + + + payload = bytearray(payload) + payload[0::4] = payload[0::4].translate(gen(mask[0])) + payload[1::4] = payload[1::4].translate(gen(mask[1])) + payload[2::4] = payload[2::4].translate(gen(mask[2])) + payload[3::4] = payload[3::4].translate(gen(mask[3])) + return bytes(payload) + + + def _send_frame(self, opcode, data): + for i in range(0, len(data), SEND_PACKET_SIZE): + part = data[i:i + SEND_PACKET_SIZE] + fin = int(i == (len(data) - 1) // SEND_PACKET_SIZE * SEND_PACKET_SIZE) + header = bytes( + [ + (opcode if i == 0 else 0) | (fin << 7), + min(len(part), 126) + ] + ) + if len(part) >= 126: + header += struct.pack("!H", len(part)) + self.socket.sendall(header + part) + + + def _error(self, status): + self.close(status) + raise EOFError() + + + def close(self, status=STATUS_OK): + self.closed = True + self._send_frame(OPCODE_CLOSE, struct.pack("!H", status)) + self.socket.close() + + +class WebSocketHandler(WSGIHandler): + def handle_one_response(self): + self.time_start = time.time() + self.status = None + self.headers_sent = False + + self.result = None + self.response_use_chunked = False + self.response_length = 0 + + + http_connection = [s.strip() for s in self.environ.get("HTTP_CONNECTION", "").split(",")] + if "Upgrade" not in http_connection or self.environ.get("HTTP_UPGRADE", "") != "websocket": + # Not my problem + return super(WebSocketHandler, self).handle_one_response() + + if "HTTP_SEC_WEBSOCKET_KEY" not in self.environ: + self.start_response("400 Bad Request", []) + return + + # Generate Sec-Websocket-Accept header + accept = self.environ["HTTP_SEC_WEBSOCKET_KEY"].encode() + accept += b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + accept = base64.b64encode(hashlib.sha1(accept).digest()).decode() + + # Accept + self.start_response("101 Switching Protocols", [ + ("Upgrade", "websocket"), + ("Connection", "Upgrade"), + ("Sec-Websocket-Accept", accept) + ])(b"") + + self.environ["wsgi.websocket"] = WebSocket(self.socket) + + # Can't call super because it sets invalid flags like "status" + try: + try: + self.run_application() + finally: + try: + self.wsgi_input._discard() + except (socket.error, IOError): + pass + except _InvalidClientInput: + self._send_error_response_if_possible(400) + except socket.error as ex: + if ex.args[0] in self.ignored_socket_errors: + self.close_connection = True + else: + self.handle_error(*sys.exc_info()) + except: # pylint:disable=bare-except + self.handle_error(*sys.exc_info()) + finally: + self.time_finish = time.time() + self.log_request() From b790bcac9b1fdb7fc4289164d6e6491449bf7957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Otr=C4=99ba?= Date: Fri, 28 Feb 2020 01:24:44 +0100 Subject: [PATCH 2350/2570] Polish translation --- plugins/Trayicon/languages/pl.json | 14 +++++++ plugins/UiConfig/languages/pl.json | 62 ++++++++++++++++++++++++++++++ src/Translate/languages/pl.json | 13 ++++--- 3 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 plugins/Trayicon/languages/pl.json create mode 100644 plugins/UiConfig/languages/pl.json diff --git a/plugins/Trayicon/languages/pl.json b/plugins/Trayicon/languages/pl.json new file mode 100644 index 00000000..84c14796 --- /dev/null +++ b/plugins/Trayicon/languages/pl.json @@ -0,0 +1,14 @@ +{ + "ZeroNet Twitter": "ZeroNet Twitter", + "ZeroNet Reddit": "ZeroNet Reddit", + "ZeroNet Github": "ZeroNet Github", + "Report bug/request feature": "Zgłoś błąd / propozycję", + "!Open ZeroNet": "!Otwórz ZeroNet", + "Quit": "Zamknij", + "(active)": "(aktywny)", + "(passive)": "(pasywny)", + "Connections: %s": "Połączenia: %s", + "Received: %.2f MB | Sent: %.2f MB": "Odebrano: %.2f MB | Wysłano: %.2f MB", + "Show console window": "Pokaż okno konsoli", + "Start ZeroNet when Windows starts": "Uruchom ZeroNet podczas startu Windows" +} diff --git a/plugins/UiConfig/languages/pl.json b/plugins/UiConfig/languages/pl.json new file mode 100644 index 00000000..daeba3d8 --- /dev/null +++ b/plugins/UiConfig/languages/pl.json @@ -0,0 +1,62 @@ +{ + "ZeroNet config": "Konfiguracja ZeroNet", + "Web Interface": "Interfejs webowy", + "Open web browser on ZeroNet startup": "Otwórz przeglądarkę podczas uruchomienia ZeroNet", + + "Network": "Sieć", + "Offline mode": "Tryb offline", + "Disable network communication.": "Wyłącz komunikacje sieciową", + "File server network": "Sieć serwera plików", + "Accept incoming peers using IPv4 or IPv6 address. (default: dual)": "Akceptuj połączenia przychodzące używając IPv4 i IPv6. (domyślnie: oba)", + "Dual (IPv4 & IPv6)": "Oba (IPv4 i IPv6)", + "File server port": "Port serwera plików", + "Other peers will use this port to reach your served sites. (default: 15441)": "Inni użytkownicy będą używać tego portu do połączenia się z Tobą. (domyślnie 15441)", + "File server external ip": "Zewnętrzny adres IP serwera plików", + "Detect automatically": "Wykryj automatycznie", + "Your file server is accessible on these ips. (default: detect automatically)": "Twój serwer plików będzie dostępny na tych adresach IP. (domyślnie: wykryj automatycznie)", + + "Disable: Don't connect to peers on Tor network": "Wyłącz: Nie łącz się do użytkowników sieci Tor", + "Enable: Only use Tor for Tor network peers": "Włącz: Łącz się do użytkowników sieci Tor", + "Always: Use Tor for every connections to hide your IP address (slower)": "Zawsze Tor: Użyj Tor dla wszystkich połączeń w celu ukrycia Twojego adresu IP (wolne działanie)", + + "Disable": "Wyłącz", + "Enable": "Włącz", + "Always": "Zawsze Tor", + + "Use Tor bridges": "Użyj Tor bridges", + "Use obfuscated bridge relays to avoid network level Tor block (even slower)": "Użyj obfuskacji, aby uniknąć blokowania Tor na poziomie sieci (jeszcze wolniejsze działanie)", + "Trackers": "Trackery", + "Discover new peers using these adresses": "Wykryj użytkowników korzystając z tych adresów trackerów", + + "Trackers files": "Pliki trackerów", + "Load additional list of torrent trackers dynamically, from a file": "Dynamicznie wczytaj dodatkową listę trackerów z pliku .json", + "Eg.: data/trackers.json": "Np.: data/trackers.json", + + "Proxy for tracker connections": "Serwer proxy dla trackerów", + "Custom": "Własny", + "Custom socks proxy address for trackers": "Adres serwera proxy do łączenia z trackerami", + "Eg.: 127.0.0.1:1080": "Np.: 127.0.0.1:1080", + "Performance": "Wydajność", + "Level of logging to file": "Poziom logowania do pliku", + "Everything": "Wszystko", + "Only important messages": "Tylko ważne wiadomości", + "Only errors": "Tylko błędy", + "Threads for async file system reads": "Wątki asynchroniczne dla odczytu", + "Threads for async file system writes": "Wątki asynchroniczne dla zapisu", + "Threads for cryptographic functions": "Wątki dla funkcji kryptograficznych", + "Threads for database operations": "Wątki dla operacji na bazie danych", + "Sync read": "Synchroniczny odczyt", + "Sync write": "Synchroniczny zapis", + "Sync execution": "Synchroniczne wykonanie", + "1 thread": "1 wątek", + "2 threads": "2 wątki", + "3 threads": "3 wątki", + "4 threads": "4 wątki", + "5 threads": "5 wątków", + "10 threads": "10 wątków", + + " configuration item value changed": " obiekt konfiguracji zmieniony", + "Save settings": "Zapisz ustawienia", + "Some changed settings requires restart": "Niektóre zmiany ustawień wymagają ponownego uruchomienia ZeroNet", + "Restart ZeroNet client": "Uruchom ponownie ZeroNet" +} diff --git a/src/Translate/languages/pl.json b/src/Translate/languages/pl.json index 75caeceb..679e909d 100644 --- a/src/Translate/languages/pl.json +++ b/src/Translate/languages/pl.json @@ -13,8 +13,8 @@ "Content signing failed": "Podpisanie treści zawiodło", "Content publish queued for {0:.0f} seconds.": "Publikacja treści wstrzymana na {0:.0f} sekund(y).", - "Content published to {0} peers.": "Treść opublikowana do {0} uzytkowników równorzednych.", - "No peers found, but your content is ready to access.": "Nie odnaleziono użytkowników równorzędnych, ale twoja treść jest dostępna.", + "Content published to {0} peers.": "Treść opublikowana do {0} uzytkowników.", + "No peers found, but your content is ready to access.": "Nie odnaleziono użytkowników, ale twoja treść jest dostępna.", "Your network connection is restricted. Please, open {0} port": "Twoje połączenie sieciowe jest ograniczone. Proszę, otwórz port {0}", "on your router to make your site accessible for everyone.": "w swoim routerze, by twoja strona mogłabyć dostępna dla wszystkich.", "Content publish failed.": "Publikacja treści zawiodła.", @@ -39,13 +39,16 @@ " files needs to be downloaded": " pliki muszą zostać ściągnięte", " downloaded": " ściągnięte", " download failed": " ściąganie nie powiodło się", - "Peers found: ": "Odnaleziono użytkowników równorzednych: ", - "No peers found": "Nie odnaleziono użytkowników równorzędnych", + "Peers found: ": "Odnaleziono użytkowników: ", + "No peers found": "Nie odnaleziono użytkowników", "Running out of size limit (": "Limit rozmiaru na wyczerpaniu (", "Set limit to \" + site_info.next_size_limit + \"MB": "Ustaw limit na \" + site_info.next_size_limit + \"MBów", "Site size limit changed to {0}MB": "Rozmiar limitu strony zmieniony na {0}MBów", " New version of this page has just released.
    Reload to see the modified content.": "Nowa wersja tej strony właśnie została wydana.
    Odśwież by zobaczyć nową, zmodyfikowaną treść strony.", "This site requests permission:": "Ta strona wymaga uprawnień:", - "_(Accept)": "Przyznaj uprawnienia" + "_(Accept)": "Przyznaj uprawnienia", + "Sign and publish": "Podpisz i opublikuj", + "Restart ZeroNet client?": "Uruchomić ponownie klienta ZeroNet?", + "Restart": "Uruchom ponownie" } From 5baacf963d71889e07a2e0aba70094b7cbc35510 Mon Sep 17 00:00:00 2001 From: krzotr Date: Sat, 29 Feb 2020 00:51:41 +0100 Subject: [PATCH 2351/2570] Fixed `Cache-Control` for .js and .css files --- src/Ui/UiRequest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 075462e7..51730954 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -299,9 +299,6 @@ class UiRequest(object): headers["Access-Control-Allow-Headers"] = "Origin, X-Requested-With, Content-Type, Accept, Cookie, Range" headers["Access-Control-Allow-Credentials"] = "true" - if content_type in ("text/plain", "text/html", "text/css", "application/javascript", "application/json", "application/manifest+json"): - content_type += "; charset=utf-8" - # Download instead of display file types that can be dangerous if re.findall("/svg|/xml|/x-shockwave-flash|/pdf", content_type): headers["Content-Disposition"] = "attachment" @@ -312,6 +309,9 @@ class UiRequest(object): content_type in ("application/javascript", "text/css") ) + if content_type in ("text/plain", "text/html", "text/css", "application/javascript", "application/json", "application/manifest+json"): + content_type += "; charset=utf-8" + if status in (200, 206) and cacheable_type: # Cache Css, Js, Image files for 10min headers["Cache-Control"] = "public, max-age=600" # Cache 10 min else: From 1fc67a3d716871b2963fe6a9589854a6c2d5209c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 2 Mar 2020 16:44:34 +0100 Subject: [PATCH 2352/2570] Rev4460, Fix mergersite update on slow storage --- plugins/MergerSite/MergerSitePlugin.py | 24 +++++++++++++++--------- src/Config.py | 2 +- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/plugins/MergerSite/MergerSitePlugin.py b/plugins/MergerSite/MergerSitePlugin.py index ca2ba31e..6ea94be4 100644 --- a/plugins/MergerSite/MergerSitePlugin.py +++ b/plugins/MergerSite/MergerSitePlugin.py @@ -345,9 +345,9 @@ class SiteManagerPlugin(object): def updateMergerSites(self): global merger_db, merged_db, merged_to_merger, site_manager s = time.time() - merger_db = {} - merged_db = {} - merged_to_merger = {} + merger_db_new = {} + merged_db_new = {} + merged_to_merger_new = {} site_manager = self if not self.sites: return @@ -359,7 +359,7 @@ class SiteManagerPlugin(object): self.log.error("Error loading site %s: %s" % (site.address, Debug.formatException(err))) continue if merged_type: - merged_db[site.address] = merged_type + merged_db_new[site.address] = merged_type # Update merger sites for permission in site.settings["permissions"]: @@ -373,9 +373,9 @@ class SiteManagerPlugin(object): site.settings["permissions"].remove(permission) continue merger_type = permission.replace("Merger:", "") - if site.address not in merger_db: - merger_db[site.address] = [] - merger_db[site.address].append(merger_type) + 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 @@ -383,8 +383,14 @@ class SiteManagerPlugin(object): for merger_site in self.sites.values(): if "Merger:" + merged_type in merger_site.settings["permissions"]: if site.address not in merged_to_merger: - merged_to_merger[site.address] = [] - merged_to_merger[site.address].append(merger_site) + 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): diff --git a/src/Config.py b/src/Config.py index 29e05739..8b555658 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4458 + self.rev = 4460 self.argv = argv self.action = None self.test_parser = None From e0bf4dc9ecc52252102e330d0e86f63dde2e3c91 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 2 Mar 2020 17:08:43 +0100 Subject: [PATCH 2353/2570] Skip announcing to trackers with unsupported address --- src/Site/SiteAnnouncer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index cfa16ab2..2fd63e82 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -39,6 +39,8 @@ class SiteAnnouncer(object): if not self.site.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"] From 27761c5045116c546cf1157c7bc1ff2902e812c1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 2 Mar 2020 17:09:13 +0100 Subject: [PATCH 2354/2570] Fix merger site updating --- plugins/MergerSite/MergerSitePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/MergerSite/MergerSitePlugin.py b/plugins/MergerSite/MergerSitePlugin.py index 6ea94be4..d2c24398 100644 --- a/plugins/MergerSite/MergerSitePlugin.py +++ b/plugins/MergerSite/MergerSitePlugin.py @@ -382,7 +382,7 @@ class SiteManagerPlugin(object): 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: + if site.address not in merged_to_merger_new: merged_to_merger_new[site.address] = [] merged_to_merger_new[site.address].append(merger_site) From f46b945cdce30ac2e8a00296c3536bcc45e74a55 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 2 Mar 2020 17:09:21 +0100 Subject: [PATCH 2355/2570] Rev4461 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 8b555658..d14f2f43 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4460 + self.rev = 4461 self.argv = argv self.action = None self.test_parser = None From aaabcb6b1a45e917d4b95fe1a242e3050144aef8 Mon Sep 17 00:00:00 2001 From: zyw271828 Date: Tue, 3 Mar 2020 12:48:18 +0800 Subject: [PATCH 2356/2570] Update "How to join" section of README-zh-cn.md --- README-zh-cn.md | 81 +++++++++++++++++-------------------------------- 1 file changed, 28 insertions(+), 53 deletions(-) diff --git a/README-zh-cn.md b/README-zh-cn.md index 103194ea..f5e0ebf6 100644 --- a/README-zh-cn.md +++ b/README-zh-cn.md @@ -56,65 +56,40 @@ #### [在 ZeroNet 文档里查看更多的屏幕截图 »](https://zeronet.io/docs/using_zeronet/sample_sites/) -## 如何加入 ? +## 如何加入 -* 下载 ZeroBundle 文件包: - * [Microsoft Windows](https://github.com/HelloZeroNet/ZeroNet-win/archive/dist/ZeroNet-win.zip) - * [Apple macOS](https://github.com/HelloZeroNet/ZeroNet-mac/archive/dist/ZeroNet-mac.zip) - * [Linux 64bit](https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux64.tar.gz) - * [Linux 32bit](https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux32.tar.gz) -* 解压缩 -* 运行 `ZeroNet.exe` (win), `ZeroNet(.app)` (osx), `ZeroNet.sh` (linux) +### Windows -### Linux 命令行 + - 下载 [ZeroNet-py3-win64.zip](https://github.com/HelloZeroNet/ZeroNet-win/archive/dist-win64/ZeroNet-py3-win64.zip) (18MB) + - 在任意位置解压缩 + - 运行 `ZeroNet.exe` + +### macOS -* `wget https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux64.tar.gz` -* `tar xvpfz ZeroBundle-linux64.tar.gz` -* `cd ZeroBundle` -* 执行 `./ZeroNet.sh` 来启动 + - 下载 [ZeroNet-dist-mac.zip](https://github.com/HelloZeroNet/ZeroNet-dist/archive/mac/ZeroNet-dist-mac.zip) (13.2MB) + - 在任意位置解压缩 + - 运行 `ZeroNet.app` + +### Linux (x86-64bit) -在你打开时他将会自动下载最新版本的 ZeroNet 。 + - `wget https://github.com/HelloZeroNet/ZeroNet-linux/archive/dist-linux64/ZeroNet-py3-linux64.tar.gz` + - `tar xvpfz ZeroNet-py3-linux64.tar.gz` + - `cd ZeroNet-linux-dist-linux64/` + - 使用以下命令启动 `./ZeroNet.sh` + - 在浏览器打开 http://127.0.0.1:43110/ 即可访问 ZeroHello 页面 + + __提示:__ 若要允许在 Web 界面上的远程连接,使用以下命令启动 `./ZeroNet.sh --ui_ip '*' --ui_restrict your.ip.address` -#### 在 Debian Linux 中手动安装 +### 从源代码安装 -* `sudo apt-get update` -* `sudo apt-get install msgpack-python python-gevent` -* `wget https://github.com/HelloZeroNet/ZeroNet/archive/master.tar.gz` -* `tar xvpfz master.tar.gz` -* `cd ZeroNet-master` -* 执行 `python2 zeronet.py` 来启动 -* 在你的浏览器中打开 http://127.0.0.1:43110/ - -### [FreeBSD](https://www.freebsd.org/) - -* `pkg install zeronet` 或者 `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` -* 通过 `vagrant ssh` 连接到 VM -* `cd /vagrant` -* 运行 `python2 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 26552:26552 -p 43110:43110 nofish/zeronet` -* 这个 Docker 镜像包含了 Tor ,但默认是禁用的,因为一些托管商不允许你在他们的服务器上运行 Tor。如果你希望启用它, -设置 `ENABLE_TOR` 环境变量为 `true` (默认: `false`). E.g.: - - `docker run -d -e "ENABLE_TOR=true" -v :/root/data -p 26552:26552 -p 43110:43110 nofish/zeronet` -* 在你的浏览器中打开 http://127.0.0.1:43110/ - -### [Virtualenv](https://virtualenv.readthedocs.org/en/latest/) - -* `virtualenv env` -* `source env/bin/activate` -* `pip install msgpack gevent` -* `python2 zeronet.py` -* 在你的浏览器中打开 http://127.0.0.1:43110/ + - `wget https://github.com/HelloZeroNet/ZeroNet/archive/py3/ZeroNet-py3.tar.gz` + - `tar xvpfz ZeroNet-py3.tar.gz` + - `cd ZeroNet-py3` + - `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/ 即可访问 ZeroHello 页面 ## 现有限制 From e2a582d8929815ffee94233c8102a392e9cf67bf Mon Sep 17 00:00:00 2001 From: zyw271828 Date: Tue, 3 Mar 2020 12:54:41 +0800 Subject: [PATCH 2357/2570] Update "How can I create a ZeroNet site" section of README-zh-cn.md --- README-zh-cn.md | 50 ++++++------------------------------------------- 1 file changed, 6 insertions(+), 44 deletions(-) diff --git a/README-zh-cn.md b/README-zh-cn.md index f5e0ebf6..939eca86 100644 --- a/README-zh-cn.md +++ b/README-zh-cn.md @@ -99,52 +99,14 @@ * 不支持私有站点 -## 如何创建一个 ZeroNet 站点? +## 如何创建一个 ZeroNet 站点? + * 点击 [ZeroHello](http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D) 站点的 **⋮** > **「新建空站点」** 菜单项 + * 您将被**重定向**到一个全新的站点,该站点只能由您修改 + * 您可以在 **data/[您的站点地址]** 目录中找到并修改网站的内容 + * 修改后打开您的网站,将右上角的「0」按钮拖到左侧,然后点击底部的**签名**并**发布**按钮 -如果 zeronet 在运行,把它关掉 -执行: -```bash -$ zeronet.py siteCreate -... -- Site private key: 23DKQpzxhbVBrAtvLEc2uvk7DZweh4qL3fn3jpM3LgHDczMK2TtYUq -- Site address: 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2 -... -- Site created! -$ zeronet.py -... -``` - -你已经完成了! 现在任何人都可以通过 -`http://localhost:43110/13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2` -来访问你的站点 - -下一步: [ZeroNet 开发者文档](https://zeronet.io/docs/site_development/getting_started/) - - -## 我要如何修改 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://zeronet.io/docs/site_development/getting_started/) ## 帮助这个项目 From 6df3036f11fd188aaaa1c25a8c9c8cdc9156409c Mon Sep 17 00:00:00 2001 From: zyw271828 Date: Tue, 3 Mar 2020 13:12:54 +0800 Subject: [PATCH 2358/2570] Improve README-zh-cn.md according to latest README.md --- README-zh-cn.md | 66 ++++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/README-zh-cn.md b/README-zh-cn.md index 939eca86..fabdb0e5 100644 --- a/README-zh-cn.md +++ b/README-zh-cn.md @@ -1,51 +1,49 @@ -# ZeroNet [![Build Status](https://travis-ci.org/HelloZeroNet/ZeroNet.svg?branch=master)](https://travis-ci.org/HelloZeroNet/ZeroNet) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://zeronet.io/docs/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://zeronet.io/docs/help_zeronet/donate/) +# ZeroNet [![Build Status](https://travis-ci.org/HelloZeroNet/ZeroNet.svg?branch=py3)](https://travis-ci.org/HelloZeroNet/ZeroNet) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://zeronet.io/docs/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://zeronet.io/docs/help_zeronet/donate/) [English](./README.md) 使用 Bitcoin 加密和 BitTorrent 网络的去中心化网络 - https://zeronet.io -## 为什么? +## 为什么? -* 我们相信开放,自由,无审查的网络 +* 我们相信开放,自由,无审查的网络和通讯 * 不会受单点故障影响:只要有在线的节点,站点就会保持在线 -* 无托管费用: 站点由访问者托管 -* 无法关闭: 因为节点无处不在 -* 快速并可离线运行: 即使没有互联网连接也可以使用 +* 无托管费用:站点由访问者托管 +* 无法关闭:因为节点无处不在 +* 快速并可离线运行:即使没有互联网连接也可以使用 ## 功能 * 实时站点更新 * 支持 Namecoin 的 .bit 域名 - * 安装方便: 只需解压并运行 + * 安装方便:只需解压并运行 * 一键克隆存在的站点 - * 无需密码、基于 [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) 的认证:用与比特币钱包相同的加密方法用来保护你的账户 -你的账户被使用和比特币钱包相同的加密方法 - * 内建 SQL 服务器和 P2P 数据同步: 让开发更简单并提升加载速度 - * 匿名性: 完整的 Tor 网络支持,支持通过 .onion 隐藏服务相互连接而不是通过IPv4地址连接 + * 无需密码、基于 [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) + 的认证:您的账户被与比特币钱包相同的加密方法保护 + * 内建 SQL 服务器和 P2P 数据同步:让开发更简单并提升加载速度 + * 匿名性:完整的 Tor 网络支持,支持通过 .onion 隐藏服务相互连接而不是通过 IPv4 地址连接 * TLS 加密连接 * 自动打开 uPnP 端口 - * 插件和多用户 (开放式代理) 支持 - * 全平台兼容 + * 多用户(openproxy)支持的插件 + * 适用于任何浏览器 / 操作系统 ## 原理 -* 在你运行`zeronet.py`后你将可以通过`http://127.0.0.1:43110/{zeronet_address}` (比如. -`http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D`)。访问 zeronet 中的站点。 +* 在运行 `zeronet.py` 后,您将可以通过 + `http://127.0.0.1:43110/{zeronet_address}`(例如: + `http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D`)访问 zeronet 中的站点 +* 在您浏览 zeronet 站点时,客户端会尝试通过 BitTorrent 网络来寻找可用的节点,从而下载需要的文件(html,css,js...) +* 您将会储存每一个浏览过的站点 +* 每个站点都包含一个名为 `content.json` 的文件,它储存了其他所有文件的 sha512 散列值以及一个通过站点私钥生成的签名 +* 如果站点的所有者(拥有站点地址的私钥)修改了站点,并且他 / 她签名了新的 `content.json` 然后推送至其他节点, + 那么这些节点将会在使用签名验证 `content.json` 的真实性后,下载修改后的文件并将新内容推送至另外的节点 -* 在你浏览 zeronet 站点时,客户端会尝试通过 BitTorrent 网络来寻找可用的节点,从而下载需要的文件 (html, css, js...) - -* 你将会储存每一个浏览过的站点 -* 每个站点都包含一个名为 `content.json` ,它储存了其他所有文件的 sha512 hash 值 - 和一个通过站点私钥建立的签名 -* 如果站点的所有者 (拥有私钥的那个人) 修改了站点, 并且他/她签名了新的 `content.json` 然后推送至其他节点, -那么所有节点将会在验证 `content.json` 的真实性 (使用签名)后, 下载修改后的文件并推送至其他节点。 - -#### [有关于 ZeroNet 加密, 站点更新, 多用户站点的幻灯片 »](https://docs.google.com/presentation/d/1qBxkroB_iiX2zHEn0dt-N-qRZgyEzui46XS2hEa3AA4/pub?start=false&loop=false&delayms=3000) +#### [关于 ZeroNet 加密,站点更新,多用户站点的幻灯片 »](https://docs.google.com/presentation/d/1_2qK1IuOKJ51pgBvllZ9Yu7Au2l551t3XBgyTSvilew/pub?start=false&loop=false&delayms=3000) #### [常见问题 »](https://zeronet.io/docs/faq/) -#### [ZeroNet开发者文档 »](https://zeronet.io/docs/site_development/getting_started/) +#### [ZeroNet 开发者文档 »](https://zeronet.io/docs/site_development/getting_started/) ## 屏幕截图 @@ -53,7 +51,7 @@ ![Screenshot](https://i.imgur.com/H60OAHY.png) ![ZeroTalk](https://zeronet.io/docs/img/zerotalk.png) -#### [在 ZeroNet 文档里查看更多的屏幕截图 »](https://zeronet.io/docs/using_zeronet/sample_sites/) +#### [ZeroNet 文档中的更多屏幕截图 »](https://zeronet.io/docs/using_zeronet/sample_sites/) ## 如何加入 @@ -93,9 +91,9 @@ ## 现有限制 -* ~~没有类似于 BitTorrent 的文件拆分来支持大文件~~ (已添加大文件支持) -* ~~没有比 BitTorrent 更好的匿名性~~ (已添加内置的完整 Tor 支持) -* 传输文件时没有压缩~~和加密~~ (已添加 TLS 支持) +* ~~没有类似于 torrent 的文件拆分来支持大文件~~ (已添加大文件支持) +* ~~没有比 BitTorrent 更好的匿名性~~ (已添加内置的完整 Tor 支持) +* 传输文件时没有压缩~~和加密~~ (已添加 TLS 支持) * 不支持私有站点 @@ -115,11 +113,11 @@ ### 赞助商 -* 在 OSX/Safari 下 [BrowserStack.com](https://www.browserstack.com) 带来更好的兼容性 +* [BrowserStack.com](https://www.browserstack.com) 使更好的 macOS/Safari 兼容性成为可能 -#### 感谢! +#### 感谢您! -* 更多信息, 帮助, 变更记录和 zeronet 站点: https://www.reddit.com/r/zeronet/ -* 在: [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) 和我们聊天,或者使用 [gitter](https://gitter.im/HelloZeroNet/ZeroNet) +* 更多信息,帮助,变更记录和 zeronet 站点:https://www.reddit.com/r/zeronet/ +* 前往 [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) 或 [gitter](https://gitter.im/HelloZeroNet/ZeroNet) 和我们聊天 * [这里](https://gitter.im/ZeroNet-zh/Lobby)是一个 gitter 上的中文聊天室 -* Email: hello@noloop.me +* Email: hello@zeronet.io (PGP: [960F FF2D 6C14 5AA6 13E8 491B 5B63 BAE6 CB96 13AE](https://zeronet.io/files/tamas@zeronet.io_pub.asc)) From c4f65a5d7b48ad43646c9a57b3c1161042b3dd52 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Mar 2020 21:50:28 +0100 Subject: [PATCH 2359/2570] Rev4462, Experimental fix for segfault on shutdown --- src/Config.py | 2 +- src/util/ThreadPool.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Config.py b/src/Config.py index d14f2f43..101f1277 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4461 + self.rev = 4462 self.argv = argv self.action = None self.test_parser = None diff --git a/src/util/ThreadPool.py b/src/util/ThreadPool.py index 2c78a2ad..5bb3c0d6 100644 --- a/src/util/ThreadPool.py +++ b/src/util/ThreadPool.py @@ -55,9 +55,6 @@ class ThreadPool: del self.pool self.pool = None - def __del__(self): - self.kill() - lock_pool = gevent.threadpool.ThreadPool(50) main_thread_id = threading.current_thread().ident From 09e65e1d95ebb61f43261dc01697747a48e9eee5 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Wed, 4 Mar 2020 22:11:59 +0300 Subject: [PATCH 2360/2570] Make ThreadPool a context manager to prevent memory leaks --- src/Test/TestNoparallel.py | 32 ++++---- src/Test/TestThreadPool.py | 160 ++++++++++++++++++------------------- src/util/ThreadPool.py | 6 ++ 3 files changed, 99 insertions(+), 99 deletions(-) diff --git a/src/Test/TestNoparallel.py b/src/Test/TestNoparallel.py index d80cc5fb..6fc4f57d 100644 --- a/src/Test/TestNoparallel.py +++ b/src/Test/TestNoparallel.py @@ -149,21 +149,19 @@ class TestNoparallel: def testMultithreadMix(self, queue_spawn): obj1 = ExampleClass() - thread_pool = ThreadPool.ThreadPool(10) + with ThreadPool.ThreadPool(10) as thread_pool: + s = time.time() + t1 = queue_spawn(obj1.countBlocking, 5) + time.sleep(0.01) + t2 = thread_pool.spawn(obj1.countBlocking, 5) + time.sleep(0.01) + t3 = thread_pool.spawn(obj1.countBlocking, 5) + time.sleep(0.3) + t4 = gevent.spawn(obj1.countBlocking, 5) + threads = [t1, t2, t3, t4] + for thread in threads: + assert thread.get() == "counted:5" - s = time.time() - t1 = queue_spawn(obj1.countBlocking, 5) - time.sleep(0.01) - t2 = thread_pool.spawn(obj1.countBlocking, 5) - time.sleep(0.01) - t3 = thread_pool.spawn(obj1.countBlocking, 5) - time.sleep(0.3) - t4 = gevent.spawn(obj1.countBlocking, 5) - threads = [t1, t2, t3, t4] - for thread in threads: - assert thread.get() == "counted:5" - - time_taken = time.time() - s - assert obj1.counted == 5 - assert 0.5 < time_taken < 0.7 - thread_pool.kill() + time_taken = time.time() - s + assert obj1.counted == 5 + assert 0.5 < time_taken < 0.7 diff --git a/src/Test/TestThreadPool.py b/src/Test/TestThreadPool.py index 6c7f35e7..5e95005e 100644 --- a/src/Test/TestThreadPool.py +++ b/src/Test/TestThreadPool.py @@ -9,31 +9,29 @@ from util import ThreadPool class TestThreadPool: def testExecutionOrder(self): - pool = ThreadPool.ThreadPool(4) + with ThreadPool.ThreadPool(4) as pool: + events = [] - events = [] + @pool.wrap + def blocker(): + events.append("S") + out = 0 + for i in range(10000000): + if i == 3000000: + events.append("M") + out += 1 + events.append("D") + return out - @pool.wrap - def blocker(): - events.append("S") - out = 0 - for i in range(10000000): - if i == 3000000: - events.append("M") - out += 1 - events.append("D") - return out + threads = [] + for i in range(3): + threads.append(gevent.spawn(blocker)) + gevent.joinall(threads) - threads = [] - for i in range(3): - threads.append(gevent.spawn(blocker)) - gevent.joinall(threads) + assert events == ["S"] * 3 + ["M"] * 3 + ["D"] * 3 - assert events == ["S"] * 3 + ["M"] * 3 + ["D"] * 3 - - res = blocker() - assert res == 10000000 - pool.kill() + res = blocker() + assert res == 10000000 def testLockBlockingSameThread(self): lock = ThreadPool.Lock() @@ -60,89 +58,88 @@ class TestThreadPool: time.sleep(0.5) lock.release() - pool = ThreadPool.ThreadPool(10) - threads = [ - pool.spawn(locker), - pool.spawn(locker), - gevent.spawn(locker), - pool.spawn(locker) - ] - time.sleep(0.1) + with ThreadPool.ThreadPool(10) as pool: + threads = [ + pool.spawn(locker), + pool.spawn(locker), + gevent.spawn(locker), + pool.spawn(locker) + ] + time.sleep(0.1) - s = time.time() + s = time.time() - lock.acquire(True, 5.0) + lock.acquire(True, 5.0) - unlock_taken = time.time() - s + unlock_taken = time.time() - s - assert 1.8 < unlock_taken < 2.2 + assert 1.8 < unlock_taken < 2.2 - gevent.joinall(threads) + gevent.joinall(threads) def testMainLoopCallerThreadId(self): main_thread_id = threading.current_thread().ident - pool = ThreadPool.ThreadPool(5) + with ThreadPool.ThreadPool(5) as pool: + def getThreadId(*args, **kwargs): + return threading.current_thread().ident - def getThreadId(*args, **kwargs): - return threading.current_thread().ident + t = pool.spawn(getThreadId) + assert t.get() != main_thread_id - t = pool.spawn(getThreadId) - assert t.get() != main_thread_id - - t = pool.spawn(lambda: ThreadPool.main_loop.call(getThreadId)) - assert t.get() == main_thread_id + t = pool.spawn(lambda: ThreadPool.main_loop.call(getThreadId)) + assert t.get() == main_thread_id def testMainLoopCallerGeventSpawn(self): main_thread_id = threading.current_thread().ident - pool = ThreadPool.ThreadPool(5) - def waiter(): - time.sleep(1) - return threading.current_thread().ident + with ThreadPool.ThreadPool(5) as pool: + def waiter(): + time.sleep(1) + return threading.current_thread().ident - def geventSpawner(): - event = ThreadPool.main_loop.call(gevent.spawn, waiter) + def geventSpawner(): + event = ThreadPool.main_loop.call(gevent.spawn, waiter) - with pytest.raises(Exception) as greenlet_err: - event.get() - assert str(greenlet_err.value) == "cannot switch to a different thread" + with pytest.raises(Exception) as greenlet_err: + event.get() + assert str(greenlet_err.value) == "cannot switch to a different thread" - waiter_thread_id = ThreadPool.main_loop.call(event.get) - return waiter_thread_id + waiter_thread_id = ThreadPool.main_loop.call(event.get) + return waiter_thread_id - s = time.time() - waiter_thread_id = pool.apply(geventSpawner) - assert main_thread_id == waiter_thread_id - time_taken = time.time() - s - assert 0.9 < time_taken < 1.2 + s = time.time() + waiter_thread_id = pool.apply(geventSpawner) + assert main_thread_id == waiter_thread_id + time_taken = time.time() - s + assert 0.9 < time_taken < 1.2 def testEvent(self): - pool = ThreadPool.ThreadPool(5) - event = ThreadPool.Event() + with ThreadPool.ThreadPool(5) as pool: + event = ThreadPool.Event() - def setter(): - time.sleep(1) - event.set("done!") + def setter(): + time.sleep(1) + event.set("done!") - def getter(): - return event.get() + def getter(): + return event.get() - pool.spawn(setter) - t_gevent = gevent.spawn(getter) - t_pool = pool.spawn(getter) - s = time.time() - assert event.get() == "done!" - time_taken = time.time() - s - gevent.joinall([t_gevent, t_pool]) + pool.spawn(setter) + t_gevent = gevent.spawn(getter) + t_pool = pool.spawn(getter) + s = time.time() + assert event.get() == "done!" + time_taken = time.time() - s + gevent.joinall([t_gevent, t_pool]) - assert t_gevent.get() == "done!" - assert t_pool.get() == "done!" + assert t_gevent.get() == "done!" + assert t_pool.get() == "done!" - assert 0.9 < time_taken < 1.2 + assert 0.9 < time_taken < 1.2 - with pytest.raises(Exception) as err: - event.set("another result") + with pytest.raises(Exception) as err: + event.set("another result") - assert "Event already has value" in str(err.value) + assert "Event already has value" in str(err.value) def testMemoryLeak(self): import gc @@ -153,10 +150,9 @@ class TestThreadPool: return "ok" def poolTest(): - pool = ThreadPool.ThreadPool(5) - for i in range(20): - pool.spawn(worker) - pool.kill() + with ThreadPool.ThreadPool(5) as pool: + for i in range(20): + pool.spawn(worker) for i in range(5): poolTest() diff --git a/src/util/ThreadPool.py b/src/util/ThreadPool.py index 5bb3c0d6..8c759039 100644 --- a/src/util/ThreadPool.py +++ b/src/util/ThreadPool.py @@ -55,6 +55,12 @@ class ThreadPool: del self.pool self.pool = None + def __enter__(self): + return self + + def __exit__(self, *args): + self.kill() + lock_pool = gevent.threadpool.ThreadPool(50) main_thread_id = threading.current_thread().ident From 296e4aab57e0226f9875ba4e40269864641a168e Mon Sep 17 00:00:00 2001 From: ZeroNet Date: Thu, 5 Mar 2020 17:54:46 +0100 Subject: [PATCH 2361/2570] Fix sslcrypto thread safety (#2454) * Use sslcrypto instead of pyelliptic and pybitcointools * Fix CryptMessage * Support Python 3.4 * Fix user creation * Get rid of pyelliptic and pybitcointools * Fix typo * Delete test file * Add sslcrypto to tree * Update sslcrypto * Add pyaes to src/lib * Fix typo in tests * Update sslcrypto version * Use privatekey_bin instead of privatekey for bytes objects * Fix sslcrypto * Fix Benchmark plugin * Don't calculate the same thing twice * Only import sslcrypto once * Handle fallback sslcrypto implementation during tests * Fix sslcrypto fallback implementation selection * Fix thread safety * Add derivation * Bring split back * Fix typo * v3.3 * Fix custom OpenSSL discovery --- .gitlab-ci.yml | 2 +- .travis.yml | 5 +- plugins/Benchmark/BenchmarkPlugin.py | 4 +- plugins/CryptMessage/CryptMessage.py | 61 +- plugins/CryptMessage/CryptMessagePlugin.py | 56 +- plugins/CryptMessage/Test/TestCrypt.py | 7 +- requirements.txt | 1 - src/Crypt/CryptBitcoin.py | 87 +- src/Test/conftest.py | 2 +- .../libsecp256k1message.py | 43 +- .../LICENSE => pyaes/LICENSE.txt} | 9 +- src/lib/pyaes/README.md | 363 +++ src/lib/pyaes/__init__.py | 53 + src/lib/pyaes/aes.py | 589 +++++ src/lib/pyaes/blockfeeder.py | 227 ++ src/lib/pyaes/util.py | 60 + src/lib/pybitcointools/__init__.py | 10 - src/lib/pybitcointools/bci.py | 528 ----- src/lib/pybitcointools/blocks.py | 50 - src/lib/pybitcointools/composite.py | 128 -- src/lib/pybitcointools/deterministic.py | 199 -- src/lib/pybitcointools/english.txt | 2048 ----------------- src/lib/pybitcointools/main.py | 581 ----- src/lib/pybitcointools/mnemonic.py | 127 - src/lib/pybitcointools/py2specials.py | 98 - src/lib/pybitcointools/py3specials.py | 123 - src/lib/pybitcointools/ripemd.py | 414 ---- src/lib/pybitcointools/stealth.py | 100 - src/lib/pybitcointools/transaction.py | 514 ----- src/lib/pyelliptic/LICENSE | 674 ------ src/lib/pyelliptic/README.md | 96 - src/lib/pyelliptic/__init__.py | 19 - src/lib/pyelliptic/arithmetic.py | 144 -- src/lib/pyelliptic/cipher.py | 84 - src/lib/pyelliptic/ecc.py | 505 ---- src/lib/pyelliptic/hash.py | 69 - src/lib/pyelliptic/openssl.py | 553 ----- src/lib/pyelliptic/setup.py | 23 - src/lib/sslcrypto/LICENSE | 27 + src/lib/sslcrypto/__init__.py | 6 + src/lib/sslcrypto/_aes.py | 53 + src/lib/sslcrypto/_ecc.py | 334 +++ src/lib/sslcrypto/_ripemd.py | 375 +++ src/lib/sslcrypto/fallback/__init__.py | 3 + src/lib/sslcrypto/fallback/_jacobian.py | 159 ++ src/lib/sslcrypto/fallback/_util.py | 79 + src/lib/sslcrypto/fallback/aes.py | 101 + src/lib/sslcrypto/fallback/ecc.py | 371 +++ src/lib/sslcrypto/fallback/rsa.py | 8 + src/lib/sslcrypto/openssl/__init__.py | 3 + src/lib/sslcrypto/openssl/aes.py | 156 ++ src/lib/sslcrypto/openssl/discovery.py | 3 + src/lib/sslcrypto/openssl/ecc.py | 575 +++++ src/lib/sslcrypto/openssl/library.py | 98 + src/lib/sslcrypto/openssl/rsa.py | 11 + src/util/Electrum.py | 39 + src/util/OpensslFindPatch.py | 30 +- 57 files changed, 3781 insertions(+), 7306 deletions(-) rename src/lib/{pybitcointools/LICENSE => pyaes/LICENSE.txt} (80%) create mode 100644 src/lib/pyaes/README.md create mode 100644 src/lib/pyaes/__init__.py create mode 100644 src/lib/pyaes/aes.py create mode 100644 src/lib/pyaes/blockfeeder.py create mode 100644 src/lib/pyaes/util.py delete mode 100644 src/lib/pybitcointools/__init__.py delete mode 100644 src/lib/pybitcointools/bci.py delete mode 100644 src/lib/pybitcointools/blocks.py delete mode 100644 src/lib/pybitcointools/composite.py delete mode 100644 src/lib/pybitcointools/deterministic.py delete mode 100644 src/lib/pybitcointools/english.txt delete mode 100644 src/lib/pybitcointools/main.py delete mode 100644 src/lib/pybitcointools/mnemonic.py delete mode 100644 src/lib/pybitcointools/py2specials.py delete mode 100644 src/lib/pybitcointools/py3specials.py delete mode 100644 src/lib/pybitcointools/ripemd.py delete mode 100644 src/lib/pybitcointools/stealth.py delete mode 100644 src/lib/pybitcointools/transaction.py delete mode 100644 src/lib/pyelliptic/LICENSE delete mode 100644 src/lib/pyelliptic/README.md delete mode 100644 src/lib/pyelliptic/__init__.py delete mode 100644 src/lib/pyelliptic/arithmetic.py delete mode 100644 src/lib/pyelliptic/cipher.py delete mode 100644 src/lib/pyelliptic/ecc.py delete mode 100644 src/lib/pyelliptic/hash.py delete mode 100644 src/lib/pyelliptic/openssl.py delete mode 100644 src/lib/pyelliptic/setup.py create mode 100644 src/lib/sslcrypto/LICENSE create mode 100644 src/lib/sslcrypto/__init__.py create mode 100644 src/lib/sslcrypto/_aes.py create mode 100644 src/lib/sslcrypto/_ecc.py create mode 100644 src/lib/sslcrypto/_ripemd.py create mode 100644 src/lib/sslcrypto/fallback/__init__.py create mode 100644 src/lib/sslcrypto/fallback/_jacobian.py create mode 100644 src/lib/sslcrypto/fallback/_util.py create mode 100644 src/lib/sslcrypto/fallback/aes.py create mode 100644 src/lib/sslcrypto/fallback/ecc.py create mode 100644 src/lib/sslcrypto/fallback/rsa.py create mode 100644 src/lib/sslcrypto/openssl/__init__.py create mode 100644 src/lib/sslcrypto/openssl/aes.py create mode 100644 src/lib/sslcrypto/openssl/discovery.py create mode 100644 src/lib/sslcrypto/openssl/ecc.py create mode 100644 src/lib/sslcrypto/openssl/library.py create mode 100644 src/lib/sslcrypto/openssl/rsa.py create mode 100644 src/util/Electrum.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b62c7b0c..f3e1ed29 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,7 +21,7 @@ stages: - python -m pytest -x plugins/Multiuser/Test --color=yes - mv plugins/disabled-Bootstrapper plugins/Bootstrapper - python -m pytest -x plugins/Bootstrapper/Test --color=yes - - flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pybitcointools/ + - flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pyaes/ test:py3.4: image: python:3.4.3 diff --git a/.travis.yml b/.travis.yml index 148bd402..bdaafa22 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,10 +33,13 @@ script: - 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/pybitcointools/ + - flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pyaes/ after_failure: - zip -r log.zip log/ - curl --upload-file ./log.zip https://transfer.sh/log.zip +after_success: + - codecov + - coveralls --rcfile=src/Test/coverage.ini notifications: email: recipients: diff --git a/plugins/Benchmark/BenchmarkPlugin.py b/plugins/Benchmark/BenchmarkPlugin.py index f22e6a26..8af140d8 100644 --- a/plugins/Benchmark/BenchmarkPlugin.py +++ b/plugins/Benchmark/BenchmarkPlugin.py @@ -104,8 +104,8 @@ class ActionsPlugin: 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": "btctools"}, "num": 20, "time_standard": 0.38}, - {"func": self.testVerify, "kwargs": {"lib_verify": "openssl"}, "num": 200, "time_standard": 0.30}, + {"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}, diff --git a/plugins/CryptMessage/CryptMessage.py b/plugins/CryptMessage/CryptMessage.py index 38abbe5d..74659404 100644 --- a/plugins/CryptMessage/CryptMessage.py +++ b/plugins/CryptMessage/CryptMessage.py @@ -1,28 +1,22 @@ import hashlib import base64 import struct - -import lib.pybitcointools as btctools +from lib import sslcrypto from Crypt import Crypt -ecc_cache = {} + +curve = sslcrypto.ecc.get_curve("secp256k1") -def eciesEncrypt(data, pubkey, ephemcurve=None, ciphername='aes-256-cbc'): - from lib import pyelliptic - pubkey_openssl = toOpensslPublickey(base64.b64decode(pubkey)) - curve, pubkey_x, pubkey_y, i = pyelliptic.ECC._decode_pubkey(pubkey_openssl) - if ephemcurve is None: - ephemcurve = curve - ephem = pyelliptic.ECC(curve=ephemcurve) - key = hashlib.sha512(ephem.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest() - key_e, key_m = key[:32], key[32:] - pubkey = ephem.get_pubkey() - iv = pyelliptic.OpenSSL.rand(pyelliptic.OpenSSL.get_cipher(ciphername).get_blocksize()) - ctx = pyelliptic.Cipher(key_e, iv, 1, ciphername) - ciphertext = iv + pubkey + ctx.ciphering(data) - mac = pyelliptic.hmac_sha256(key_m, ciphertext) - return key_e, ciphertext + mac +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 @@ -37,9 +31,8 @@ def eciesDecryptMulti(encrypted_datas, privatekey): return texts -def eciesDecrypt(encrypted_data, privatekey): - ecc_key = getEcc(privatekey) - return ecc_key.decrypt(base64.b64decode(encrypted_data)) +def eciesDecrypt(ciphertext, privatekey): + return curve.decrypt(base64.b64decode(ciphertext), curve.wif_to_private(privatekey), derivation="sha512") def decodePubkey(pubkey): @@ -63,29 +56,3 @@ def split(encrypted): ciphertext = encrypted[16 + i:-32] return iv, ciphertext - - -def getEcc(privatekey=None): - from lib import pyelliptic - global ecc_cache - if privatekey not in ecc_cache: - if privatekey: - publickey_bin = btctools.encode_pubkey(btctools.privtopub(privatekey), "bin") - publickey_openssl = toOpensslPublickey(publickey_bin) - privatekey_openssl = toOpensslPrivatekey(privatekey) - ecc_cache[privatekey] = pyelliptic.ECC(curve='secp256k1', privkey=privatekey_openssl, pubkey=publickey_openssl) - else: - ecc_cache[None] = pyelliptic.ECC() - return ecc_cache[privatekey] - - -def toOpensslPrivatekey(privatekey): - privatekey_bin = btctools.encode_privkey(privatekey, "bin") - return b'\x02\xca\x00\x20' + privatekey_bin - - -def toOpensslPublickey(publickey): - publickey_bin = btctools.encode_pubkey(publickey, "bin") - publickey_bin = publickey_bin[1:] - publickey_openssl = b'\x02\xca\x00 ' + publickey_bin[:32] + b'\x00 ' + publickey_bin[32:] - return publickey_openssl diff --git a/plugins/CryptMessage/CryptMessagePlugin.py b/plugins/CryptMessage/CryptMessagePlugin.py index 45afe184..150bf8be 100644 --- a/plugins/CryptMessage/CryptMessagePlugin.py +++ b/plugins/CryptMessage/CryptMessagePlugin.py @@ -5,24 +5,22 @@ import gevent from Plugin import PluginManager from Crypt import CryptBitcoin, CryptHash -import lib.pybitcointools as btctools from Config import config +import sslcrypto + from . import CryptMessage +curve = sslcrypto.ecc.get_curve("secp256k1") + @PluginManager.registerTo("UiWebsocket") class UiWebsocketPlugin(object): - def eciesDecrypt(self, encrypted, privatekey): - back = CryptMessage.getEcc(privatekey).decrypt(encrypted) - return back.decode("utf8") - # - Actions - # Returns user's public key unique to site # Return: Public key def actionUserPublickey(self, to, index=0): - publickey = self.user.getEncryptPublickey(self.site.address, index) - self.response(to, publickey) + 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 @@ -55,23 +53,16 @@ class UiWebsocketPlugin(object): # Encrypt a text using AES # Return: Iv, AES key, Encrypted text - def actionAesEncrypt(self, to, text, key=None, iv=None): - from lib import pyelliptic - + def actionAesEncrypt(self, to, text, key=None): if key: key = base64.b64decode(key) else: - key = os.urandom(32) - - if iv: # Generate new AES key if not definied - iv = base64.b64decode(iv) - else: - iv = pyelliptic.Cipher.gen_IV('aes-256-cbc') + key = sslcrypto.aes.new_key() if text: - encrypted = pyelliptic.Cipher(key, iv, 1, ciphername='aes-256-cbc').ciphering(text.encode("utf8")) + encrypted, iv = sslcrypto.aes.encrypt(text.encode("utf8"), key) else: - encrypted = b"" + encrypted, iv = b"", b"" res = [base64.b64encode(item).decode("utf8") for item in [key, iv, encrypted]] self.response(to, res) @@ -79,8 +70,6 @@ class UiWebsocketPlugin(object): # Decrypt a text using AES # Return: Decrypted text def actionAesDecrypt(self, to, *args): - from lib import pyelliptic - if len(args) == 3: # Single decrypt encrypted_texts = [(args[0], args[1])] keys = [args[2]] @@ -93,9 +82,8 @@ class UiWebsocketPlugin(object): iv = base64.b64decode(iv) text = None for key in keys: - ctx = pyelliptic.Cipher(base64.b64decode(key), iv, 0, ciphername='aes-256-cbc') try: - decrypted = ctx.ciphering(encrypted_text) + 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: @@ -122,12 +110,11 @@ class UiWebsocketPlugin(object): # Gets the publickey of a given privatekey def actionEccPrivToPub(self, to, privatekey): - self.response(to, btctools.privtopub(privatekey)) + self.response(to, curve.private_to_public(curve.wif_to_private(privatekey))) # Gets the address of a given publickey def actionEccPubToAddr(self, to, publickey): - address = btctools.pubtoaddr(btctools.decode_pubkey(publickey)) - self.response(to, address) + self.response(to, curve.public_to_address(bytes.fromhex(publickey))) @PluginManager.registerTo("User") @@ -163,7 +150,7 @@ class UserPlugin(object): if "encrypt_publickey_%s" % index not in site_data: privatekey = self.getEncryptPrivatekey(address, param_index) - publickey = btctools.encode_pubkey(btctools.privtopub(privatekey), "bin_compressed") + publickey = curve.private_to_public(curve.wif_to_private(privatekey)) site_data["encrypt_publickey_%s" % index] = base64.b64encode(publickey).decode("utf8") return site_data["encrypt_publickey_%s" % index] @@ -200,8 +187,8 @@ class ActionsPlugin: aes_key, encrypted = CryptMessage.eciesEncrypt(self.utf8_text.encode("utf8"), self.publickey) for i in range(num_run): assert len(aes_key) == 32 - ecc = CryptMessage.getEcc(self.privatekey) - assert ecc.decrypt(encrypted) == self.utf8_text.encode("utf8"), "%s != %s" % (ecc.decrypt(encrypted), self.utf8_text.encode("utf8")) + 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): @@ -223,23 +210,16 @@ class ActionsPlugin: gevent.joinall(threads) def testCryptAesEncrypt(self, num_run=1): - from lib import pyelliptic - for i in range(num_run): key = os.urandom(32) - iv = pyelliptic.Cipher.gen_IV('aes-256-cbc') - encrypted = pyelliptic.Cipher(key, iv, 1, ciphername='aes-256-cbc').ciphering(self.utf8_text.encode("utf8")) + encrypted = sslcrypto.aes.encrypt(self.utf8_text.encode("utf8"), key) yield "." def testCryptAesDecrypt(self, num_run=1): - from lib import pyelliptic - key = os.urandom(32) - iv = pyelliptic.Cipher.gen_IV('aes-256-cbc') - encrypted_text = pyelliptic.Cipher(key, iv, 1, ciphername='aes-256-cbc').ciphering(self.utf8_text.encode("utf8")) + encrypted_text, iv = sslcrypto.aes.encrypt(self.utf8_text.encode("utf8"), key) for i in range(num_run): - ctx = pyelliptic.Cipher(key, iv, 0, ciphername='aes-256-cbc') - decrypted = ctx.ciphering(encrypted_text).decode("utf8") + 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 index 96f73761..25a077d8 100644 --- a/plugins/CryptMessage/Test/TestCrypt.py +++ b/plugins/CryptMessage/Test/TestCrypt.py @@ -18,13 +18,10 @@ class TestCrypt: assert len(aes_key) == 32 # assert len(encrypted) == 134 + int(len(text) / 16) * 16 # Not always true - ecc = CryptMessage.getEcc(self.privatekey) - assert ecc.decrypt(encrypted) == text_repeated + assert CryptMessage.eciesDecrypt(base64.b64encode(encrypted), self.privatekey) == text_repeated def testDecryptEcies(self, user): - encrypted = base64.b64decode(self.ecies_encrypted_text) - ecc = CryptMessage.getEcc(self.privatekey) - assert ecc.decrypt(encrypted) == b"hello" + assert CryptMessage.eciesDecrypt(self.ecies_encrypted_text, self.privatekey) == b"hello" def testPublickey(self, ui_websocket): pub = ui_websocket.testAction("UserPublickey", 0) diff --git a/requirements.txt b/requirements.txt index c2893480..83f3c3ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,5 +9,4 @@ pyasn1 websocket_client gevent-ws coincurve -python-bitcoinlib maxminddb diff --git a/src/Crypt/CryptBitcoin.py b/src/Crypt/CryptBitcoin.py index b6bfaa77..f54015dc 100644 --- a/src/Crypt/CryptBitcoin.py +++ b/src/Crypt/CryptBitcoin.py @@ -1,16 +1,21 @@ import logging import base64 +import binascii import time +import hashlib -from util import OpensslFindPatch -from lib import pybitcointools as btctools +from util.Electrum import dbl_format from Config import config -lib_verify_best = "btctools" +lib_verify_best = "sslcrypto" +import sslcrypto +sslcurve_native = sslcrypto.ecc.get_curve("secp256k1") +sslcurve_fallback = sslcrypto.fallback.ecc.get_curve("secp256k1") +sslcurve = sslcurve_native def loadLib(lib_name, silent=False): - global bitcoin, libsecp256k1message, lib_verify_best + global sslcurve, libsecp256k1message, lib_verify_best if lib_name == "libsecp256k1": s = time.time() from lib import libsecp256k1message @@ -21,24 +26,10 @@ def loadLib(lib_name, silent=False): "Libsecpk256k1 loaded: %s in %.3fs" % (type(coincurve._libsecp256k1.lib).__name__, time.time() - s) ) - elif lib_name == "openssl": - s = time.time() - import bitcoin.signmessage - import bitcoin.core.key - import bitcoin.wallet - - try: - # OpenSSL 1.1.0 - ssl_version = bitcoin.core.key._ssl.SSLeay() - except AttributeError: - # OpenSSL 1.1.1+ - ssl_version = bitcoin.core.key._ssl.OpenSSL_version_num() - - if not silent: - logging.info( - "OpenSSL loaded: %s, version: %.9X in %.3fs" % - (bitcoin.core.key._ssl, ssl_version, time.time() - s) - ) + elif lib_name == "sslcrypto": + sslcurve = sslcurve_native + elif lib_name == "sslcrypto_fallback": + sslcurve = sslcurve_fallback try: if not config.use_libsecp256k1: @@ -46,35 +37,30 @@ try: loadLib("libsecp256k1") lib_verify_best = "libsecp256k1" except Exception as err: - logging.info("Libsecp256k1 load failed: %s, try to load OpenSSL" % err) - try: - if not config.use_openssl: - raise Exception("Disabled by config") - loadLib("openssl") - lib_verify_best = "openssl" - except Exception as err: - logging.info("OpenSSL load failed: %s, falling back to slow bitcoin verify" % err) + logging.info("Libsecp256k1 load failed: %s" % err) -def newPrivatekey(uncompressed=True): # Return new private key - privatekey = btctools.encode_privkey(btctools.random_key(), "wif") - return privatekey +def newPrivatekey(): # Return new private key + return sslcurve.private_to_wif(sslcurve.new_private_key()).decode() def newSeed(): - return btctools.random_key() + return binascii.hexlify(sslcurve.new_private_key()).decode() def hdPrivatekey(seed, child): - masterkey = btctools.bip32_master_key(bytes(seed, "ascii")) - childkey = btctools.bip32_ckd(masterkey, child % 100000000) # Too large child id could cause problems - key = btctools.bip32_extract_key(childkey) - return btctools.encode_privkey(key, "wif") + # Too large child id could cause problems + privatekey_bin = sslcurve.derive_child(seed.encode(), child % 100000000) + return sslcurve.private_to_wif(privatekey_bin).decode() def privatekeyToAddress(privatekey): # Return address from private key try: - return btctools.privkey_to_address(privatekey) + if len(privatekey) == 64: + privatekey_bin = bytes.fromhex(privatekey) + else: + privatekey_bin = sslcurve.wif_to_private(privatekey.encode()) + return sslcurve.private_to_address(privatekey_bin, is_compressed=False).decode() except Exception: # Invalid privatekey return False @@ -82,8 +68,13 @@ def privatekeyToAddress(privatekey): # Return address from private key def sign(data, privatekey): # Return sign to data using private key if privatekey.startswith("23") and len(privatekey) > 52: return None # Old style private key not supported - sign = btctools.ecdsa_sign(data, privatekey) - return sign + return base64.b64encode(sslcurve.sign( + data.encode(), + sslcurve.wif_to_private(privatekey.encode()), + is_compressed=False, + recoverable=True, + hash=dbl_format + )).decode() def verify(data, valid_address, sign, lib_verify=None): # Verify data using address and sign @@ -95,17 +86,9 @@ def verify(data, valid_address, sign, lib_verify=None): # Verify data using add if lib_verify == "libsecp256k1": sign_address = libsecp256k1message.recover_address(data.encode("utf8"), sign).decode("utf8") - elif lib_verify == "openssl": - sig = base64.b64decode(sign) - message = bitcoin.signmessage.BitcoinMessage(data) - hash = message.GetHash() - - pubkey = bitcoin.core.key.CPubKey.recover_compact(hash, sig) - - sign_address = str(bitcoin.wallet.P2PKHBitcoinAddress.from_pubkey(pubkey)) - elif lib_verify == "btctools": # Use pure-python - pub = btctools.ecdsa_recover(data, sign) - sign_address = btctools.pubtoaddr(pub) + elif lib_verify in ("sslcrypto", "sslcrypto_fallback"): + publickey = sslcurve.recover(base64.b64decode(sign), data.encode(), hash=dbl_format) + sign_address = sslcurve.public_to_address(publickey).decode() else: raise Exception("No library enabled for signature verification") diff --git a/src/Test/conftest.py b/src/Test/conftest.py index cc4d5faf..8f9dc3a5 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -436,7 +436,7 @@ def db(request): return db -@pytest.fixture(params=["btctools", "openssl", "libsecp256k1"]) +@pytest.fixture(params=["sslcrypto", "sslcrypto_fallback", "libsecp256k1"]) def crypt_bitcoin_lib(request, monkeypatch): monkeypatch.setattr(CryptBitcoin, "lib_verify_best", request.param) CryptBitcoin.loadLib(request.param) diff --git a/src/lib/libsecp256k1message/libsecp256k1message.py b/src/lib/libsecp256k1message/libsecp256k1message.py index 92802e1c..59768b88 100644 --- a/src/lib/libsecp256k1message/libsecp256k1message.py +++ b/src/lib/libsecp256k1message/libsecp256k1message.py @@ -1,9 +1,9 @@ import hashlib -import struct import base64 from coincurve import PrivateKey, PublicKey from base58 import b58encode_check, b58decode_check from hmac import compare_digest +from util.Electrum import format as zero_format RECID_MIN = 0 RECID_MAX = 3 @@ -97,7 +97,7 @@ def sign_data(secretkey, byte_string): """Sign [byte_string] with [secretkey]. Return serialized signature compatible with Electrum (ZeroNet).""" # encode the message - encoded = _zero_format(byte_string) + encoded = zero_format(byte_string) # sign the message and get a coincurve signature signature = secretkey.sign_recoverable(encoded) # reserialize signature and return it @@ -110,7 +110,7 @@ def verify_data(key_digest, electrum_signature, byte_string): # reserialize signature signature = coincurve_sig(electrum_signature) # encode the message - encoded = _zero_format(byte_string) + encoded = zero_format(byte_string) # recover full public key from signature # "which guarantees a correct signature" publickey = recover_public_key(signature, encoded) @@ -130,45 +130,10 @@ def verify_sig(publickey, signature, byte_string): def verify_key(publickey, key_digest): return compare_digest(key_digest, public_digest(publickey)) - -# Electrum, the heck?! - -def bchr(i): - return struct.pack('B', i) - -def _zero_encode(val, base, minlen=0): - base, minlen = int(base), int(minlen) - code_string = b''.join([bchr(x) for x in range(256)]) - result = b'' - while val > 0: - index = val % base - result = code_string[index:index + 1] + result - val //= base - return code_string[0:1] * max(minlen - len(result), 0) + result - -def _zero_insane_int(x): - x = int(x) - if x < 253: - return bchr(x) - elif x < 65536: - return bchr(253) + _zero_encode(x, 256, 2)[::-1] - elif x < 4294967296: - return bchr(254) + _zero_encode(x, 256, 4)[::-1] - else: - return bchr(255) + _zero_encode(x, 256, 8)[::-1] - - -def _zero_magic(message): - return b'\x18Bitcoin Signed Message:\n' + _zero_insane_int(len(message)) + message - -def _zero_format(message): - padded = _zero_magic(message) - return hashlib.sha256(padded).digest() - def recover_address(data, sign): sign_bytes = base64.b64decode(sign) is_compressed = ((sign_bytes[0] - 27) & 4) != 0 - publickey = recover_public_key(coincurve_sig(sign_bytes), _zero_format(data)) + publickey = recover_public_key(coincurve_sig(sign_bytes), zero_format(data)) return compute_public_address(publickey, compressed=is_compressed) __all__ = [ diff --git a/src/lib/pybitcointools/LICENSE b/src/lib/pyaes/LICENSE.txt similarity index 80% rename from src/lib/pybitcointools/LICENSE rename to src/lib/pyaes/LICENSE.txt index c47d4ad0..0417a6c2 100644 --- a/src/lib/pybitcointools/LICENSE +++ b/src/lib/pyaes/LICENSE.txt @@ -1,12 +1,6 @@ -This code is public domain. Everyone has the right to do whatever they want -with it for any purpose. - -In case your jurisdiction does not consider the above disclaimer valid or -enforceable, here's an MIT license for you: - The MIT License (MIT) -Copyright (c) 2013 Vitalik Buterin +Copyright (c) 2014 Richard Moore Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -25,3 +19,4 @@ 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/src/lib/pyaes/README.md b/src/lib/pyaes/README.md new file mode 100644 index 00000000..26e3b2ba --- /dev/null +++ b/src/lib/pyaes/README.md @@ -0,0 +1,363 @@ +pyaes +===== + +A pure-Python implementation of the AES block cipher algorithm and the common modes of operation (CBC, CFB, CTR, ECB and OFB). + + +Features +-------- + +* Supports all AES key sizes +* Supports all AES common modes +* Pure-Python (no external dependencies) +* BlockFeeder API allows streams to easily be encrypted and decrypted +* Python 2.x and 3.x support (make sure you pass in bytes(), not strings for Python 3) + + +API +--- + +All keys may be 128 bits (16 bytes), 192 bits (24 bytes) or 256 bits (32 bytes) long. + +To generate a random key use: +```python +import os + +# 128 bit, 192 bit and 256 bit keys +key_128 = os.urandom(16) +key_192 = os.urandom(24) +key_256 = os.urandom(32) +``` + +To generate keys from simple-to-remember passwords, consider using a _password-based key-derivation function_ such as [scrypt](https://github.com/ricmoo/pyscrypt). + + +### Common Modes of Operation + +There are many modes of operations, each with various pros and cons. In general though, the **CBC** and **CTR** modes are recommended. The **ECB is NOT recommended.**, and is included primarily for completeness. + +Each of the following examples assumes the following key: +```python +import pyaes + +# A 256 bit (32 byte) key +key = "This_key_for_demo_purposes_only!" + +# For some modes of operation we need a random initialization vector +# of 16 bytes +iv = "InitializationVe" +``` + + +#### Counter Mode of Operation (recommended) + +```python +aes = pyaes.AESModeOfOperationCTR(key) +plaintext = "Text may be any length you wish, no padding is required" +ciphertext = aes.encrypt(plaintext) + +# '''\xb6\x99\x10=\xa4\x96\x88\xd1\x89\x1co\xe6\x1d\xef;\x11\x03\xe3\xee +# \xa9V?wY\xbfe\xcdO\xe3\xdf\x9dV\x19\xe5\x8dk\x9fh\xb87>\xdb\xa3\xd6 +# \x86\xf4\xbd\xb0\x97\xf1\t\x02\xe9 \xed''' +print repr(ciphertext) + +# The counter mode of operation maintains state, so decryption requires +# a new instance be created +aes = pyaes.AESModeOfOperationCTR(key) +decrypted = aes.decrypt(ciphertext) + +# True +print decrypted == plaintext + +# To use a custom initial value +counter = pyaes.Counter(initial_value = 100) +aes = pyaes.AESModeOfOperationCTR(key, counter = counter) +ciphertext = aes.encrypt(plaintext) + +# '''WZ\x844\x02\xbfoY\x1f\x12\xa6\xce\x03\x82Ei)\xf6\x97mX\x86\xe3\x9d +# _1\xdd\xbd\x87\xb5\xccEM_4\x01$\xa6\x81\x0b\xd5\x04\xd7Al\x07\xe5 +# \xb2\x0e\\\x0f\x00\x13,\x07''' +print repr(ciphertext) +``` + + +#### Cipher-Block Chaining (recommended) + +```python +aes = pyaes.AESModeOfOperationCBC(key, iv = iv) +plaintext = "TextMustBe16Byte" +ciphertext = aes.encrypt(plaintext) + +# '\xd6:\x18\xe6\xb1\xb3\xc3\xdc\x87\xdf\xa7|\x08{k\xb6' +print repr(ciphertext) + + +# The cipher-block chaining mode of operation maintains state, so +# decryption requires a new instance be created +aes = pyaes.AESModeOfOperationCBC(key, iv = iv) +decrypted = aes.decrypt(ciphertext) + +# True +print decrypted == plaintext +``` + + +#### Cipher Feedback + +```python +# Each block into the mode of operation must be a multiple of the segment +# size. For this example we choose 8 bytes. +aes = pyaes.AESModeOfOperationCFB(key, iv = iv, segment_size = 8) +plaintext = "TextMustBeAMultipleOfSegmentSize" +ciphertext = aes.encrypt(plaintext) + +# '''v\xa9\xc1w"\x8aL\x93\xcb\xdf\xa0/\xf8Y\x0b\x8d\x88i\xcb\x85rmp +# \x85\xfe\xafM\x0c)\xd5\xeb\xaf''' +print repr(ciphertext) + + +# The cipher-block chaining mode of operation maintains state, so +# decryption requires a new instance be created +aes = pyaes.AESModeOfOperationCFB(key, iv = iv, segment_size = 8) +decrypted = aes.decrypt(ciphertext) + +# True +print decrypted == plaintext +``` + + +#### Output Feedback Mode of Operation + +```python +aes = pyaes.AESModeOfOperationOFB(key, iv = iv) +plaintext = "Text may be any length you wish, no padding is required" +ciphertext = aes.encrypt(plaintext) + +# '''v\xa9\xc1wO\x92^\x9e\rR\x1e\xf7\xb1\xa2\x9d"l1\xc7\xe7\x9d\x87(\xc26s +# \xdd8\xc8@\xb6\xd9!\xf5\x0cM\xaa\x9b\xc4\xedLD\xe4\xb9\xd8\xdf\x9e\xac +# \xa1\xb8\xea\x0f\x8ev\xb5''' +print repr(ciphertext) + +# The counter mode of operation maintains state, so decryption requires +# a new instance be created +aes = pyaes.AESModeOfOperationOFB(key, iv = iv) +decrypted = aes.decrypt(ciphertext) + +# True +print decrypted == plaintext +``` + + +#### Electronic Codebook (NOT recommended) + +```python +aes = pyaes.AESModeOfOperationECB(key) +plaintext = "TextMustBe16Byte" +ciphertext = aes.encrypt(plaintext) + +# 'L6\x95\x85\xe4\xd9\xf1\x8a\xfb\xe5\x94X\x80|\x19\xc3' +print repr(ciphertext) + +# Since there is no state stored in this mode of operation, it +# is not necessary to create a new aes object for decryption. +#aes = pyaes.AESModeOfOperationECB(key) +decrypted = aes.decrypt(ciphertext) + +# True +print decrypted == plaintext +``` + + +### BlockFeeder + +Since most of the modes of operations require data in specific block-sized or segment-sized blocks, it can be difficult when working with large arbitrary streams or strings of data. + +The BlockFeeder class is meant to make life easier for you, by buffering bytes across multiple calls and returning bytes as they are available, as well as padding or stripping the output when finished, if necessary. + +```python +import pyaes + +# Any mode of operation can be used; for this example CBC +key = "This_key_for_demo_purposes_only!" +iv = "InitializationVe" + +ciphertext = '' + +# We can encrypt one line at a time, regardles of length +encrypter = pyaes.Encrypter(pyaes.AESModeOfOperationCBC(key, iv)) +for line in file('/etc/passwd'): + ciphertext += encrypter.feed(line) + +# Make a final call to flush any remaining bytes and add paddin +ciphertext += encrypter.feed() + +# We can decrypt the cipher text in chunks (here we split it in half) +decrypter = pyaes.Decrypter(pyaes.AESModeOfOperationCBC(key, iv)) +decrypted = decrypter.feed(ciphertext[:len(ciphertext) / 2]) +decrypted += decrypter.feed(ciphertext[len(ciphertext) / 2:]) + +# Again, make a final call to flush any remaining bytes and strip padding +decrypted += decrypter.feed() + +print file('/etc/passwd').read() == decrypted +``` + +### Stream Feeder + +This is meant to make it even easier to encrypt and decrypt streams and large files. + +```python +import pyaes + +# Any mode of operation can be used; for this example CTR +key = "This_key_for_demo_purposes_only!" + +# Create the mode of operation to encrypt with +mode = pyaes.AESModeOfOperationCTR(key) + +# The input and output files +file_in = file('/etc/passwd') +file_out = file('/tmp/encrypted.bin', 'wb') + +# Encrypt the data as a stream, the file is read in 8kb chunks, be default +pyaes.encrypt_stream(mode, file_in, file_out) + +# Close the files +file_in.close() +file_out.close() +``` + +Decrypting is identical, except you would use `pyaes.decrypt_stream`, and the encrypted file would be the `file_in` and target for decryption the `file_out`. + +### AES block cipher + +Generally you should use one of the modes of operation above. This may however be useful for experimenting with a custom mode of operation or dealing with encrypted blocks. + +The block cipher requires exactly one block of data to encrypt or decrypt, and each block should be an array with each element an integer representation of a byte. + +```python +import pyaes + +# 16 byte block of plain text +plaintext = "Hello World!!!!!" +plaintext_bytes = [ ord(c) for c in plaintext ] + +# 32 byte key (256 bit) +key = "This_key_for_demo_purposes_only!" + +# Our AES instance +aes = pyaes.AES(key) + +# Encrypt! +ciphertext = aes.encrypt(plaintext_bytes) + +# [55, 250, 182, 25, 185, 208, 186, 95, 206, 115, 50, 115, 108, 58, 174, 115] +print repr(ciphertext) + +# Decrypt! +decrypted = aes.decrypt(ciphertext) + +# True +print decrypted == plaintext_bytes +``` + +What is a key? +-------------- + +This seems to be a point of confusion for many people new to using encryption. You can think of the key as the *"password"*. However, these algorithms require the *"password"* to be a specific length. + +With AES, there are three possible key lengths, 16-bytes, 24-bytes or 32-bytes. When you create an AES object, the key size is automatically detected, so it is important to pass in a key of the correct length. + +Often, you wish to provide a password of arbitrary length, for example, something easy to remember or write down. In these cases, you must come up with a way to transform the password into a key, of a specific length. A **Password-Based Key Derivation Function** (PBKDF) is an algorithm designed for this exact purpose. + +Here is an example, using the popular (possibly obsolete?) *crypt* PBKDF: + +``` +# See: https://www.dlitz.net/software/python-pbkdf2/ +import pbkdf2 + +password = "HelloWorld" + +# The crypt PBKDF returns a 48-byte string +key = pbkdf2.crypt(password) + +# A 16-byte, 24-byte and 32-byte key, respectively +key_16 = key[:16] +key_24 = key[:24] +key_32 = key[:32] +``` + +The [scrypt](https://github.com/ricmoo/pyscrypt) PBKDF is intentionally slow, to make it more difficult to brute-force guess a password: + +``` +# See: https://github.com/ricmoo/pyscrypt +import pyscrypt + +password = "HelloWorld" + +# Salt is required, and prevents Rainbow Table attacks +salt = "SeaSalt" + +# N, r, and p are parameters to specify how difficult it should be to +# generate a key; bigger numbers take longer and more memory +N = 1024 +r = 1 +p = 1 + +# A 16-byte, 24-byte and 32-byte key, respectively; the scrypt algorithm takes +# a 6-th parameter, indicating key length +key_16 = pyscrypt.hash(password, salt, N, r, p, 16) +key_24 = pyscrypt.hash(password, salt, N, r, p, 24) +key_32 = pyscrypt.hash(password, salt, N, r, p, 32) +``` + +Another possibility, is to use a hashing function, such as SHA256 to hash the password, but this method may be vulnerable to [Rainbow Attacks](http://en.wikipedia.org/wiki/Rainbow_table), unless you use a [salt](http://en.wikipedia.org/wiki/Salt_(cryptography)). + +```python +import hashlib + +password = "HelloWorld" + +# The SHA256 hash algorithm returns a 32-byte string +hashed = hashlib.sha256(password).digest() + +# A 16-byte, 24-byte and 32-byte key, respectively +key_16 = hashed[:16] +key_24 = hashed[:24] +key_32 = hashed +``` + + + + +Performance +----------- + +There is a test case provided in _/tests/test-aes.py_ which does some basic performance testing (its primary purpose is moreso as a regression test). + +Based on that test, in **CPython**, this library is about 30x slower than [PyCrypto](https://www.dlitz.net/software/pycrypto/) for CBC, ECB and OFB; about 80x slower for CFB; and 300x slower for CTR. + +Based on that same test, in **Pypy**, this library is about 4x slower than [PyCrypto](https://www.dlitz.net/software/pycrypto/) for CBC, ECB and OFB; about 12x slower for CFB; and 19x slower for CTR. + +The PyCrypto documentation makes reference to the counter call being responsible for the speed problems of the counter (CTR) mode of operation, which is why they use a specially optimized counter. I will investigate this problem further in the future. + + +FAQ +--- + +#### Why do this? + +The short answer, *why not?* + +The longer answer, is for my [pyscrypt](https://github.com/ricmoo/pyscrypt) library. I required a pure-Python AES implementation that supported 256-bit keys with the counter (CTR) mode of operation. After searching, I found several implementations, but all were missing CTR or only supported 128 bit keys. After all the work of learning AES inside and out to implement the library, it was only a marginal amount of extra work to library-ify a more general solution. So, *why not?* + +#### How do I get a question I have added? + +E-mail me at pyaes@ricmoo.com with any questions, suggestions, comments, et cetera. + + +#### Can I give you my money? + +Umm... Ok? :-) + +_Bitcoin_ - `18UDs4qV1shu2CgTS2tKojhCtM69kpnWg9` diff --git a/src/lib/pyaes/__init__.py b/src/lib/pyaes/__init__.py new file mode 100644 index 00000000..5712f794 --- /dev/null +++ b/src/lib/pyaes/__init__.py @@ -0,0 +1,53 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 Richard Moore +# +# 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. + +# This is a pure-Python implementation of the AES algorithm and AES common +# modes of operation. + +# See: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard +# See: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation + + +# Supported key sizes: +# 128-bit +# 192-bit +# 256-bit + + +# Supported modes of operation: +# ECB - Electronic Codebook +# CBC - Cipher-Block Chaining +# CFB - Cipher Feedback +# OFB - Output Feedback +# CTR - Counter + +# See the README.md for API details and general information. + +# Also useful, PyCrypto, a crypto library implemented in C with Python bindings: +# https://www.dlitz.net/software/pycrypto/ + + +VERSION = [1, 3, 0] + +from .aes import AES, AESModeOfOperationCTR, AESModeOfOperationCBC, AESModeOfOperationCFB, AESModeOfOperationECB, AESModeOfOperationOFB, AESModesOfOperation, Counter +from .blockfeeder import decrypt_stream, Decrypter, encrypt_stream, Encrypter +from .blockfeeder import PADDING_NONE, PADDING_DEFAULT diff --git a/src/lib/pyaes/aes.py b/src/lib/pyaes/aes.py new file mode 100644 index 00000000..c6e8bc02 --- /dev/null +++ b/src/lib/pyaes/aes.py @@ -0,0 +1,589 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 Richard Moore +# +# 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. + +# This is a pure-Python implementation of the AES algorithm and AES common +# modes of operation. + +# See: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard + +# Honestly, the best description of the modes of operations are the wonderful +# diagrams on Wikipedia. They explain in moments what my words could never +# achieve. Hence the inline documentation here is sparer than I'd prefer. +# See: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation + +# Also useful, PyCrypto, a crypto library implemented in C with Python bindings: +# https://www.dlitz.net/software/pycrypto/ + + +# Supported key sizes: +# 128-bit +# 192-bit +# 256-bit + + +# Supported modes of operation: +# ECB - Electronic Codebook +# CBC - Cipher-Block Chaining +# CFB - Cipher Feedback +# OFB - Output Feedback +# CTR - Counter + + +# See the README.md for API details and general information. + + +import copy +import struct + +__all__ = ["AES", "AESModeOfOperationCTR", "AESModeOfOperationCBC", "AESModeOfOperationCFB", + "AESModeOfOperationECB", "AESModeOfOperationOFB", "AESModesOfOperation", "Counter"] + + +def _compact_word(word): + return (word[0] << 24) | (word[1] << 16) | (word[2] << 8) | word[3] + +def _string_to_bytes(text): + return list(ord(c) for c in text) + +def _bytes_to_string(binary): + return "".join(chr(b) for b in binary) + +def _concat_list(a, b): + return a + b + + +# Python 3 compatibility +try: + xrange +except Exception: + xrange = range + + # Python 3 supports bytes, which is already an array of integers + def _string_to_bytes(text): + if isinstance(text, bytes): + return text + return [ord(c) for c in text] + + # In Python 3, we return bytes + def _bytes_to_string(binary): + return bytes(binary) + + # Python 3 cannot concatenate a list onto a bytes, so we bytes-ify it first + def _concat_list(a, b): + return a + bytes(b) + + +# Based *largely* on the Rijndael implementation +# See: http://csrc.nist.gov/publications/fips/fips197/fips-197.pdf +class AES(object): + '''Encapsulates the AES block cipher. + + You generally should not need this. Use the AESModeOfOperation classes + below instead.''' + + # Number of rounds by keysize + number_of_rounds = {16: 10, 24: 12, 32: 14} + + # Round constant words + rcon = [ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91 ] + + # S-box and Inverse S-box (S is for Substitution) + S = [ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 ] + Si =[ 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d ] + + # Transformations for encryption + T1 = [ 0xc66363a5, 0xf87c7c84, 0xee777799, 0xf67b7b8d, 0xfff2f20d, 0xd66b6bbd, 0xde6f6fb1, 0x91c5c554, 0x60303050, 0x02010103, 0xce6767a9, 0x562b2b7d, 0xe7fefe19, 0xb5d7d762, 0x4dababe6, 0xec76769a, 0x8fcaca45, 0x1f82829d, 0x89c9c940, 0xfa7d7d87, 0xeffafa15, 0xb25959eb, 0x8e4747c9, 0xfbf0f00b, 0x41adadec, 0xb3d4d467, 0x5fa2a2fd, 0x45afafea, 0x239c9cbf, 0x53a4a4f7, 0xe4727296, 0x9bc0c05b, 0x75b7b7c2, 0xe1fdfd1c, 0x3d9393ae, 0x4c26266a, 0x6c36365a, 0x7e3f3f41, 0xf5f7f702, 0x83cccc4f, 0x6834345c, 0x51a5a5f4, 0xd1e5e534, 0xf9f1f108, 0xe2717193, 0xabd8d873, 0x62313153, 0x2a15153f, 0x0804040c, 0x95c7c752, 0x46232365, 0x9dc3c35e, 0x30181828, 0x379696a1, 0x0a05050f, 0x2f9a9ab5, 0x0e070709, 0x24121236, 0x1b80809b, 0xdfe2e23d, 0xcdebeb26, 0x4e272769, 0x7fb2b2cd, 0xea75759f, 0x1209091b, 0x1d83839e, 0x582c2c74, 0x341a1a2e, 0x361b1b2d, 0xdc6e6eb2, 0xb45a5aee, 0x5ba0a0fb, 0xa45252f6, 0x763b3b4d, 0xb7d6d661, 0x7db3b3ce, 0x5229297b, 0xdde3e33e, 0x5e2f2f71, 0x13848497, 0xa65353f5, 0xb9d1d168, 0x00000000, 0xc1eded2c, 0x40202060, 0xe3fcfc1f, 0x79b1b1c8, 0xb65b5bed, 0xd46a6abe, 0x8dcbcb46, 0x67bebed9, 0x7239394b, 0x944a4ade, 0x984c4cd4, 0xb05858e8, 0x85cfcf4a, 0xbbd0d06b, 0xc5efef2a, 0x4faaaae5, 0xedfbfb16, 0x864343c5, 0x9a4d4dd7, 0x66333355, 0x11858594, 0x8a4545cf, 0xe9f9f910, 0x04020206, 0xfe7f7f81, 0xa05050f0, 0x783c3c44, 0x259f9fba, 0x4ba8a8e3, 0xa25151f3, 0x5da3a3fe, 0x804040c0, 0x058f8f8a, 0x3f9292ad, 0x219d9dbc, 0x70383848, 0xf1f5f504, 0x63bcbcdf, 0x77b6b6c1, 0xafdada75, 0x42212163, 0x20101030, 0xe5ffff1a, 0xfdf3f30e, 0xbfd2d26d, 0x81cdcd4c, 0x180c0c14, 0x26131335, 0xc3ecec2f, 0xbe5f5fe1, 0x359797a2, 0x884444cc, 0x2e171739, 0x93c4c457, 0x55a7a7f2, 0xfc7e7e82, 0x7a3d3d47, 0xc86464ac, 0xba5d5de7, 0x3219192b, 0xe6737395, 0xc06060a0, 0x19818198, 0x9e4f4fd1, 0xa3dcdc7f, 0x44222266, 0x542a2a7e, 0x3b9090ab, 0x0b888883, 0x8c4646ca, 0xc7eeee29, 0x6bb8b8d3, 0x2814143c, 0xa7dede79, 0xbc5e5ee2, 0x160b0b1d, 0xaddbdb76, 0xdbe0e03b, 0x64323256, 0x743a3a4e, 0x140a0a1e, 0x924949db, 0x0c06060a, 0x4824246c, 0xb85c5ce4, 0x9fc2c25d, 0xbdd3d36e, 0x43acacef, 0xc46262a6, 0x399191a8, 0x319595a4, 0xd3e4e437, 0xf279798b, 0xd5e7e732, 0x8bc8c843, 0x6e373759, 0xda6d6db7, 0x018d8d8c, 0xb1d5d564, 0x9c4e4ed2, 0x49a9a9e0, 0xd86c6cb4, 0xac5656fa, 0xf3f4f407, 0xcfeaea25, 0xca6565af, 0xf47a7a8e, 0x47aeaee9, 0x10080818, 0x6fbabad5, 0xf0787888, 0x4a25256f, 0x5c2e2e72, 0x381c1c24, 0x57a6a6f1, 0x73b4b4c7, 0x97c6c651, 0xcbe8e823, 0xa1dddd7c, 0xe874749c, 0x3e1f1f21, 0x964b4bdd, 0x61bdbddc, 0x0d8b8b86, 0x0f8a8a85, 0xe0707090, 0x7c3e3e42, 0x71b5b5c4, 0xcc6666aa, 0x904848d8, 0x06030305, 0xf7f6f601, 0x1c0e0e12, 0xc26161a3, 0x6a35355f, 0xae5757f9, 0x69b9b9d0, 0x17868691, 0x99c1c158, 0x3a1d1d27, 0x279e9eb9, 0xd9e1e138, 0xebf8f813, 0x2b9898b3, 0x22111133, 0xd26969bb, 0xa9d9d970, 0x078e8e89, 0x339494a7, 0x2d9b9bb6, 0x3c1e1e22, 0x15878792, 0xc9e9e920, 0x87cece49, 0xaa5555ff, 0x50282878, 0xa5dfdf7a, 0x038c8c8f, 0x59a1a1f8, 0x09898980, 0x1a0d0d17, 0x65bfbfda, 0xd7e6e631, 0x844242c6, 0xd06868b8, 0x824141c3, 0x299999b0, 0x5a2d2d77, 0x1e0f0f11, 0x7bb0b0cb, 0xa85454fc, 0x6dbbbbd6, 0x2c16163a ] + T2 = [ 0xa5c66363, 0x84f87c7c, 0x99ee7777, 0x8df67b7b, 0x0dfff2f2, 0xbdd66b6b, 0xb1de6f6f, 0x5491c5c5, 0x50603030, 0x03020101, 0xa9ce6767, 0x7d562b2b, 0x19e7fefe, 0x62b5d7d7, 0xe64dabab, 0x9aec7676, 0x458fcaca, 0x9d1f8282, 0x4089c9c9, 0x87fa7d7d, 0x15effafa, 0xebb25959, 0xc98e4747, 0x0bfbf0f0, 0xec41adad, 0x67b3d4d4, 0xfd5fa2a2, 0xea45afaf, 0xbf239c9c, 0xf753a4a4, 0x96e47272, 0x5b9bc0c0, 0xc275b7b7, 0x1ce1fdfd, 0xae3d9393, 0x6a4c2626, 0x5a6c3636, 0x417e3f3f, 0x02f5f7f7, 0x4f83cccc, 0x5c683434, 0xf451a5a5, 0x34d1e5e5, 0x08f9f1f1, 0x93e27171, 0x73abd8d8, 0x53623131, 0x3f2a1515, 0x0c080404, 0x5295c7c7, 0x65462323, 0x5e9dc3c3, 0x28301818, 0xa1379696, 0x0f0a0505, 0xb52f9a9a, 0x090e0707, 0x36241212, 0x9b1b8080, 0x3ddfe2e2, 0x26cdebeb, 0x694e2727, 0xcd7fb2b2, 0x9fea7575, 0x1b120909, 0x9e1d8383, 0x74582c2c, 0x2e341a1a, 0x2d361b1b, 0xb2dc6e6e, 0xeeb45a5a, 0xfb5ba0a0, 0xf6a45252, 0x4d763b3b, 0x61b7d6d6, 0xce7db3b3, 0x7b522929, 0x3edde3e3, 0x715e2f2f, 0x97138484, 0xf5a65353, 0x68b9d1d1, 0x00000000, 0x2cc1eded, 0x60402020, 0x1fe3fcfc, 0xc879b1b1, 0xedb65b5b, 0xbed46a6a, 0x468dcbcb, 0xd967bebe, 0x4b723939, 0xde944a4a, 0xd4984c4c, 0xe8b05858, 0x4a85cfcf, 0x6bbbd0d0, 0x2ac5efef, 0xe54faaaa, 0x16edfbfb, 0xc5864343, 0xd79a4d4d, 0x55663333, 0x94118585, 0xcf8a4545, 0x10e9f9f9, 0x06040202, 0x81fe7f7f, 0xf0a05050, 0x44783c3c, 0xba259f9f, 0xe34ba8a8, 0xf3a25151, 0xfe5da3a3, 0xc0804040, 0x8a058f8f, 0xad3f9292, 0xbc219d9d, 0x48703838, 0x04f1f5f5, 0xdf63bcbc, 0xc177b6b6, 0x75afdada, 0x63422121, 0x30201010, 0x1ae5ffff, 0x0efdf3f3, 0x6dbfd2d2, 0x4c81cdcd, 0x14180c0c, 0x35261313, 0x2fc3ecec, 0xe1be5f5f, 0xa2359797, 0xcc884444, 0x392e1717, 0x5793c4c4, 0xf255a7a7, 0x82fc7e7e, 0x477a3d3d, 0xacc86464, 0xe7ba5d5d, 0x2b321919, 0x95e67373, 0xa0c06060, 0x98198181, 0xd19e4f4f, 0x7fa3dcdc, 0x66442222, 0x7e542a2a, 0xab3b9090, 0x830b8888, 0xca8c4646, 0x29c7eeee, 0xd36bb8b8, 0x3c281414, 0x79a7dede, 0xe2bc5e5e, 0x1d160b0b, 0x76addbdb, 0x3bdbe0e0, 0x56643232, 0x4e743a3a, 0x1e140a0a, 0xdb924949, 0x0a0c0606, 0x6c482424, 0xe4b85c5c, 0x5d9fc2c2, 0x6ebdd3d3, 0xef43acac, 0xa6c46262, 0xa8399191, 0xa4319595, 0x37d3e4e4, 0x8bf27979, 0x32d5e7e7, 0x438bc8c8, 0x596e3737, 0xb7da6d6d, 0x8c018d8d, 0x64b1d5d5, 0xd29c4e4e, 0xe049a9a9, 0xb4d86c6c, 0xfaac5656, 0x07f3f4f4, 0x25cfeaea, 0xafca6565, 0x8ef47a7a, 0xe947aeae, 0x18100808, 0xd56fbaba, 0x88f07878, 0x6f4a2525, 0x725c2e2e, 0x24381c1c, 0xf157a6a6, 0xc773b4b4, 0x5197c6c6, 0x23cbe8e8, 0x7ca1dddd, 0x9ce87474, 0x213e1f1f, 0xdd964b4b, 0xdc61bdbd, 0x860d8b8b, 0x850f8a8a, 0x90e07070, 0x427c3e3e, 0xc471b5b5, 0xaacc6666, 0xd8904848, 0x05060303, 0x01f7f6f6, 0x121c0e0e, 0xa3c26161, 0x5f6a3535, 0xf9ae5757, 0xd069b9b9, 0x91178686, 0x5899c1c1, 0x273a1d1d, 0xb9279e9e, 0x38d9e1e1, 0x13ebf8f8, 0xb32b9898, 0x33221111, 0xbbd26969, 0x70a9d9d9, 0x89078e8e, 0xa7339494, 0xb62d9b9b, 0x223c1e1e, 0x92158787, 0x20c9e9e9, 0x4987cece, 0xffaa5555, 0x78502828, 0x7aa5dfdf, 0x8f038c8c, 0xf859a1a1, 0x80098989, 0x171a0d0d, 0xda65bfbf, 0x31d7e6e6, 0xc6844242, 0xb8d06868, 0xc3824141, 0xb0299999, 0x775a2d2d, 0x111e0f0f, 0xcb7bb0b0, 0xfca85454, 0xd66dbbbb, 0x3a2c1616 ] + T3 = [ 0x63a5c663, 0x7c84f87c, 0x7799ee77, 0x7b8df67b, 0xf20dfff2, 0x6bbdd66b, 0x6fb1de6f, 0xc55491c5, 0x30506030, 0x01030201, 0x67a9ce67, 0x2b7d562b, 0xfe19e7fe, 0xd762b5d7, 0xabe64dab, 0x769aec76, 0xca458fca, 0x829d1f82, 0xc94089c9, 0x7d87fa7d, 0xfa15effa, 0x59ebb259, 0x47c98e47, 0xf00bfbf0, 0xadec41ad, 0xd467b3d4, 0xa2fd5fa2, 0xafea45af, 0x9cbf239c, 0xa4f753a4, 0x7296e472, 0xc05b9bc0, 0xb7c275b7, 0xfd1ce1fd, 0x93ae3d93, 0x266a4c26, 0x365a6c36, 0x3f417e3f, 0xf702f5f7, 0xcc4f83cc, 0x345c6834, 0xa5f451a5, 0xe534d1e5, 0xf108f9f1, 0x7193e271, 0xd873abd8, 0x31536231, 0x153f2a15, 0x040c0804, 0xc75295c7, 0x23654623, 0xc35e9dc3, 0x18283018, 0x96a13796, 0x050f0a05, 0x9ab52f9a, 0x07090e07, 0x12362412, 0x809b1b80, 0xe23ddfe2, 0xeb26cdeb, 0x27694e27, 0xb2cd7fb2, 0x759fea75, 0x091b1209, 0x839e1d83, 0x2c74582c, 0x1a2e341a, 0x1b2d361b, 0x6eb2dc6e, 0x5aeeb45a, 0xa0fb5ba0, 0x52f6a452, 0x3b4d763b, 0xd661b7d6, 0xb3ce7db3, 0x297b5229, 0xe33edde3, 0x2f715e2f, 0x84971384, 0x53f5a653, 0xd168b9d1, 0x00000000, 0xed2cc1ed, 0x20604020, 0xfc1fe3fc, 0xb1c879b1, 0x5bedb65b, 0x6abed46a, 0xcb468dcb, 0xbed967be, 0x394b7239, 0x4ade944a, 0x4cd4984c, 0x58e8b058, 0xcf4a85cf, 0xd06bbbd0, 0xef2ac5ef, 0xaae54faa, 0xfb16edfb, 0x43c58643, 0x4dd79a4d, 0x33556633, 0x85941185, 0x45cf8a45, 0xf910e9f9, 0x02060402, 0x7f81fe7f, 0x50f0a050, 0x3c44783c, 0x9fba259f, 0xa8e34ba8, 0x51f3a251, 0xa3fe5da3, 0x40c08040, 0x8f8a058f, 0x92ad3f92, 0x9dbc219d, 0x38487038, 0xf504f1f5, 0xbcdf63bc, 0xb6c177b6, 0xda75afda, 0x21634221, 0x10302010, 0xff1ae5ff, 0xf30efdf3, 0xd26dbfd2, 0xcd4c81cd, 0x0c14180c, 0x13352613, 0xec2fc3ec, 0x5fe1be5f, 0x97a23597, 0x44cc8844, 0x17392e17, 0xc45793c4, 0xa7f255a7, 0x7e82fc7e, 0x3d477a3d, 0x64acc864, 0x5de7ba5d, 0x192b3219, 0x7395e673, 0x60a0c060, 0x81981981, 0x4fd19e4f, 0xdc7fa3dc, 0x22664422, 0x2a7e542a, 0x90ab3b90, 0x88830b88, 0x46ca8c46, 0xee29c7ee, 0xb8d36bb8, 0x143c2814, 0xde79a7de, 0x5ee2bc5e, 0x0b1d160b, 0xdb76addb, 0xe03bdbe0, 0x32566432, 0x3a4e743a, 0x0a1e140a, 0x49db9249, 0x060a0c06, 0x246c4824, 0x5ce4b85c, 0xc25d9fc2, 0xd36ebdd3, 0xacef43ac, 0x62a6c462, 0x91a83991, 0x95a43195, 0xe437d3e4, 0x798bf279, 0xe732d5e7, 0xc8438bc8, 0x37596e37, 0x6db7da6d, 0x8d8c018d, 0xd564b1d5, 0x4ed29c4e, 0xa9e049a9, 0x6cb4d86c, 0x56faac56, 0xf407f3f4, 0xea25cfea, 0x65afca65, 0x7a8ef47a, 0xaee947ae, 0x08181008, 0xbad56fba, 0x7888f078, 0x256f4a25, 0x2e725c2e, 0x1c24381c, 0xa6f157a6, 0xb4c773b4, 0xc65197c6, 0xe823cbe8, 0xdd7ca1dd, 0x749ce874, 0x1f213e1f, 0x4bdd964b, 0xbddc61bd, 0x8b860d8b, 0x8a850f8a, 0x7090e070, 0x3e427c3e, 0xb5c471b5, 0x66aacc66, 0x48d89048, 0x03050603, 0xf601f7f6, 0x0e121c0e, 0x61a3c261, 0x355f6a35, 0x57f9ae57, 0xb9d069b9, 0x86911786, 0xc15899c1, 0x1d273a1d, 0x9eb9279e, 0xe138d9e1, 0xf813ebf8, 0x98b32b98, 0x11332211, 0x69bbd269, 0xd970a9d9, 0x8e89078e, 0x94a73394, 0x9bb62d9b, 0x1e223c1e, 0x87921587, 0xe920c9e9, 0xce4987ce, 0x55ffaa55, 0x28785028, 0xdf7aa5df, 0x8c8f038c, 0xa1f859a1, 0x89800989, 0x0d171a0d, 0xbfda65bf, 0xe631d7e6, 0x42c68442, 0x68b8d068, 0x41c38241, 0x99b02999, 0x2d775a2d, 0x0f111e0f, 0xb0cb7bb0, 0x54fca854, 0xbbd66dbb, 0x163a2c16 ] + T4 = [ 0x6363a5c6, 0x7c7c84f8, 0x777799ee, 0x7b7b8df6, 0xf2f20dff, 0x6b6bbdd6, 0x6f6fb1de, 0xc5c55491, 0x30305060, 0x01010302, 0x6767a9ce, 0x2b2b7d56, 0xfefe19e7, 0xd7d762b5, 0xababe64d, 0x76769aec, 0xcaca458f, 0x82829d1f, 0xc9c94089, 0x7d7d87fa, 0xfafa15ef, 0x5959ebb2, 0x4747c98e, 0xf0f00bfb, 0xadadec41, 0xd4d467b3, 0xa2a2fd5f, 0xafafea45, 0x9c9cbf23, 0xa4a4f753, 0x727296e4, 0xc0c05b9b, 0xb7b7c275, 0xfdfd1ce1, 0x9393ae3d, 0x26266a4c, 0x36365a6c, 0x3f3f417e, 0xf7f702f5, 0xcccc4f83, 0x34345c68, 0xa5a5f451, 0xe5e534d1, 0xf1f108f9, 0x717193e2, 0xd8d873ab, 0x31315362, 0x15153f2a, 0x04040c08, 0xc7c75295, 0x23236546, 0xc3c35e9d, 0x18182830, 0x9696a137, 0x05050f0a, 0x9a9ab52f, 0x0707090e, 0x12123624, 0x80809b1b, 0xe2e23ddf, 0xebeb26cd, 0x2727694e, 0xb2b2cd7f, 0x75759fea, 0x09091b12, 0x83839e1d, 0x2c2c7458, 0x1a1a2e34, 0x1b1b2d36, 0x6e6eb2dc, 0x5a5aeeb4, 0xa0a0fb5b, 0x5252f6a4, 0x3b3b4d76, 0xd6d661b7, 0xb3b3ce7d, 0x29297b52, 0xe3e33edd, 0x2f2f715e, 0x84849713, 0x5353f5a6, 0xd1d168b9, 0x00000000, 0xeded2cc1, 0x20206040, 0xfcfc1fe3, 0xb1b1c879, 0x5b5bedb6, 0x6a6abed4, 0xcbcb468d, 0xbebed967, 0x39394b72, 0x4a4ade94, 0x4c4cd498, 0x5858e8b0, 0xcfcf4a85, 0xd0d06bbb, 0xefef2ac5, 0xaaaae54f, 0xfbfb16ed, 0x4343c586, 0x4d4dd79a, 0x33335566, 0x85859411, 0x4545cf8a, 0xf9f910e9, 0x02020604, 0x7f7f81fe, 0x5050f0a0, 0x3c3c4478, 0x9f9fba25, 0xa8a8e34b, 0x5151f3a2, 0xa3a3fe5d, 0x4040c080, 0x8f8f8a05, 0x9292ad3f, 0x9d9dbc21, 0x38384870, 0xf5f504f1, 0xbcbcdf63, 0xb6b6c177, 0xdada75af, 0x21216342, 0x10103020, 0xffff1ae5, 0xf3f30efd, 0xd2d26dbf, 0xcdcd4c81, 0x0c0c1418, 0x13133526, 0xecec2fc3, 0x5f5fe1be, 0x9797a235, 0x4444cc88, 0x1717392e, 0xc4c45793, 0xa7a7f255, 0x7e7e82fc, 0x3d3d477a, 0x6464acc8, 0x5d5de7ba, 0x19192b32, 0x737395e6, 0x6060a0c0, 0x81819819, 0x4f4fd19e, 0xdcdc7fa3, 0x22226644, 0x2a2a7e54, 0x9090ab3b, 0x8888830b, 0x4646ca8c, 0xeeee29c7, 0xb8b8d36b, 0x14143c28, 0xdede79a7, 0x5e5ee2bc, 0x0b0b1d16, 0xdbdb76ad, 0xe0e03bdb, 0x32325664, 0x3a3a4e74, 0x0a0a1e14, 0x4949db92, 0x06060a0c, 0x24246c48, 0x5c5ce4b8, 0xc2c25d9f, 0xd3d36ebd, 0xacacef43, 0x6262a6c4, 0x9191a839, 0x9595a431, 0xe4e437d3, 0x79798bf2, 0xe7e732d5, 0xc8c8438b, 0x3737596e, 0x6d6db7da, 0x8d8d8c01, 0xd5d564b1, 0x4e4ed29c, 0xa9a9e049, 0x6c6cb4d8, 0x5656faac, 0xf4f407f3, 0xeaea25cf, 0x6565afca, 0x7a7a8ef4, 0xaeaee947, 0x08081810, 0xbabad56f, 0x787888f0, 0x25256f4a, 0x2e2e725c, 0x1c1c2438, 0xa6a6f157, 0xb4b4c773, 0xc6c65197, 0xe8e823cb, 0xdddd7ca1, 0x74749ce8, 0x1f1f213e, 0x4b4bdd96, 0xbdbddc61, 0x8b8b860d, 0x8a8a850f, 0x707090e0, 0x3e3e427c, 0xb5b5c471, 0x6666aacc, 0x4848d890, 0x03030506, 0xf6f601f7, 0x0e0e121c, 0x6161a3c2, 0x35355f6a, 0x5757f9ae, 0xb9b9d069, 0x86869117, 0xc1c15899, 0x1d1d273a, 0x9e9eb927, 0xe1e138d9, 0xf8f813eb, 0x9898b32b, 0x11113322, 0x6969bbd2, 0xd9d970a9, 0x8e8e8907, 0x9494a733, 0x9b9bb62d, 0x1e1e223c, 0x87879215, 0xe9e920c9, 0xcece4987, 0x5555ffaa, 0x28287850, 0xdfdf7aa5, 0x8c8c8f03, 0xa1a1f859, 0x89898009, 0x0d0d171a, 0xbfbfda65, 0xe6e631d7, 0x4242c684, 0x6868b8d0, 0x4141c382, 0x9999b029, 0x2d2d775a, 0x0f0f111e, 0xb0b0cb7b, 0x5454fca8, 0xbbbbd66d, 0x16163a2c ] + + # Transformations for decryption + T5 = [ 0x51f4a750, 0x7e416553, 0x1a17a4c3, 0x3a275e96, 0x3bab6bcb, 0x1f9d45f1, 0xacfa58ab, 0x4be30393, 0x2030fa55, 0xad766df6, 0x88cc7691, 0xf5024c25, 0x4fe5d7fc, 0xc52acbd7, 0x26354480, 0xb562a38f, 0xdeb15a49, 0x25ba1b67, 0x45ea0e98, 0x5dfec0e1, 0xc32f7502, 0x814cf012, 0x8d4697a3, 0x6bd3f9c6, 0x038f5fe7, 0x15929c95, 0xbf6d7aeb, 0x955259da, 0xd4be832d, 0x587421d3, 0x49e06929, 0x8ec9c844, 0x75c2896a, 0xf48e7978, 0x99583e6b, 0x27b971dd, 0xbee14fb6, 0xf088ad17, 0xc920ac66, 0x7dce3ab4, 0x63df4a18, 0xe51a3182, 0x97513360, 0x62537f45, 0xb16477e0, 0xbb6bae84, 0xfe81a01c, 0xf9082b94, 0x70486858, 0x8f45fd19, 0x94de6c87, 0x527bf8b7, 0xab73d323, 0x724b02e2, 0xe31f8f57, 0x6655ab2a, 0xb2eb2807, 0x2fb5c203, 0x86c57b9a, 0xd33708a5, 0x302887f2, 0x23bfa5b2, 0x02036aba, 0xed16825c, 0x8acf1c2b, 0xa779b492, 0xf307f2f0, 0x4e69e2a1, 0x65daf4cd, 0x0605bed5, 0xd134621f, 0xc4a6fe8a, 0x342e539d, 0xa2f355a0, 0x058ae132, 0xa4f6eb75, 0x0b83ec39, 0x4060efaa, 0x5e719f06, 0xbd6e1051, 0x3e218af9, 0x96dd063d, 0xdd3e05ae, 0x4de6bd46, 0x91548db5, 0x71c45d05, 0x0406d46f, 0x605015ff, 0x1998fb24, 0xd6bde997, 0x894043cc, 0x67d99e77, 0xb0e842bd, 0x07898b88, 0xe7195b38, 0x79c8eedb, 0xa17c0a47, 0x7c420fe9, 0xf8841ec9, 0x00000000, 0x09808683, 0x322bed48, 0x1e1170ac, 0x6c5a724e, 0xfd0efffb, 0x0f853856, 0x3daed51e, 0x362d3927, 0x0a0fd964, 0x685ca621, 0x9b5b54d1, 0x24362e3a, 0x0c0a67b1, 0x9357e70f, 0xb4ee96d2, 0x1b9b919e, 0x80c0c54f, 0x61dc20a2, 0x5a774b69, 0x1c121a16, 0xe293ba0a, 0xc0a02ae5, 0x3c22e043, 0x121b171d, 0x0e090d0b, 0xf28bc7ad, 0x2db6a8b9, 0x141ea9c8, 0x57f11985, 0xaf75074c, 0xee99ddbb, 0xa37f60fd, 0xf701269f, 0x5c72f5bc, 0x44663bc5, 0x5bfb7e34, 0x8b432976, 0xcb23c6dc, 0xb6edfc68, 0xb8e4f163, 0xd731dcca, 0x42638510, 0x13972240, 0x84c61120, 0x854a247d, 0xd2bb3df8, 0xaef93211, 0xc729a16d, 0x1d9e2f4b, 0xdcb230f3, 0x0d8652ec, 0x77c1e3d0, 0x2bb3166c, 0xa970b999, 0x119448fa, 0x47e96422, 0xa8fc8cc4, 0xa0f03f1a, 0x567d2cd8, 0x223390ef, 0x87494ec7, 0xd938d1c1, 0x8ccaa2fe, 0x98d40b36, 0xa6f581cf, 0xa57ade28, 0xdab78e26, 0x3fadbfa4, 0x2c3a9de4, 0x5078920d, 0x6a5fcc9b, 0x547e4662, 0xf68d13c2, 0x90d8b8e8, 0x2e39f75e, 0x82c3aff5, 0x9f5d80be, 0x69d0937c, 0x6fd52da9, 0xcf2512b3, 0xc8ac993b, 0x10187da7, 0xe89c636e, 0xdb3bbb7b, 0xcd267809, 0x6e5918f4, 0xec9ab701, 0x834f9aa8, 0xe6956e65, 0xaaffe67e, 0x21bccf08, 0xef15e8e6, 0xbae79bd9, 0x4a6f36ce, 0xea9f09d4, 0x29b07cd6, 0x31a4b2af, 0x2a3f2331, 0xc6a59430, 0x35a266c0, 0x744ebc37, 0xfc82caa6, 0xe090d0b0, 0x33a7d815, 0xf104984a, 0x41ecdaf7, 0x7fcd500e, 0x1791f62f, 0x764dd68d, 0x43efb04d, 0xccaa4d54, 0xe49604df, 0x9ed1b5e3, 0x4c6a881b, 0xc12c1fb8, 0x4665517f, 0x9d5eea04, 0x018c355d, 0xfa877473, 0xfb0b412e, 0xb3671d5a, 0x92dbd252, 0xe9105633, 0x6dd64713, 0x9ad7618c, 0x37a10c7a, 0x59f8148e, 0xeb133c89, 0xcea927ee, 0xb761c935, 0xe11ce5ed, 0x7a47b13c, 0x9cd2df59, 0x55f2733f, 0x1814ce79, 0x73c737bf, 0x53f7cdea, 0x5ffdaa5b, 0xdf3d6f14, 0x7844db86, 0xcaaff381, 0xb968c43e, 0x3824342c, 0xc2a3405f, 0x161dc372, 0xbce2250c, 0x283c498b, 0xff0d9541, 0x39a80171, 0x080cb3de, 0xd8b4e49c, 0x6456c190, 0x7bcb8461, 0xd532b670, 0x486c5c74, 0xd0b85742 ] + T6 = [ 0x5051f4a7, 0x537e4165, 0xc31a17a4, 0x963a275e, 0xcb3bab6b, 0xf11f9d45, 0xabacfa58, 0x934be303, 0x552030fa, 0xf6ad766d, 0x9188cc76, 0x25f5024c, 0xfc4fe5d7, 0xd7c52acb, 0x80263544, 0x8fb562a3, 0x49deb15a, 0x6725ba1b, 0x9845ea0e, 0xe15dfec0, 0x02c32f75, 0x12814cf0, 0xa38d4697, 0xc66bd3f9, 0xe7038f5f, 0x9515929c, 0xebbf6d7a, 0xda955259, 0x2dd4be83, 0xd3587421, 0x2949e069, 0x448ec9c8, 0x6a75c289, 0x78f48e79, 0x6b99583e, 0xdd27b971, 0xb6bee14f, 0x17f088ad, 0x66c920ac, 0xb47dce3a, 0x1863df4a, 0x82e51a31, 0x60975133, 0x4562537f, 0xe0b16477, 0x84bb6bae, 0x1cfe81a0, 0x94f9082b, 0x58704868, 0x198f45fd, 0x8794de6c, 0xb7527bf8, 0x23ab73d3, 0xe2724b02, 0x57e31f8f, 0x2a6655ab, 0x07b2eb28, 0x032fb5c2, 0x9a86c57b, 0xa5d33708, 0xf2302887, 0xb223bfa5, 0xba02036a, 0x5ced1682, 0x2b8acf1c, 0x92a779b4, 0xf0f307f2, 0xa14e69e2, 0xcd65daf4, 0xd50605be, 0x1fd13462, 0x8ac4a6fe, 0x9d342e53, 0xa0a2f355, 0x32058ae1, 0x75a4f6eb, 0x390b83ec, 0xaa4060ef, 0x065e719f, 0x51bd6e10, 0xf93e218a, 0x3d96dd06, 0xaedd3e05, 0x464de6bd, 0xb591548d, 0x0571c45d, 0x6f0406d4, 0xff605015, 0x241998fb, 0x97d6bde9, 0xcc894043, 0x7767d99e, 0xbdb0e842, 0x8807898b, 0x38e7195b, 0xdb79c8ee, 0x47a17c0a, 0xe97c420f, 0xc9f8841e, 0x00000000, 0x83098086, 0x48322bed, 0xac1e1170, 0x4e6c5a72, 0xfbfd0eff, 0x560f8538, 0x1e3daed5, 0x27362d39, 0x640a0fd9, 0x21685ca6, 0xd19b5b54, 0x3a24362e, 0xb10c0a67, 0x0f9357e7, 0xd2b4ee96, 0x9e1b9b91, 0x4f80c0c5, 0xa261dc20, 0x695a774b, 0x161c121a, 0x0ae293ba, 0xe5c0a02a, 0x433c22e0, 0x1d121b17, 0x0b0e090d, 0xadf28bc7, 0xb92db6a8, 0xc8141ea9, 0x8557f119, 0x4caf7507, 0xbbee99dd, 0xfda37f60, 0x9ff70126, 0xbc5c72f5, 0xc544663b, 0x345bfb7e, 0x768b4329, 0xdccb23c6, 0x68b6edfc, 0x63b8e4f1, 0xcad731dc, 0x10426385, 0x40139722, 0x2084c611, 0x7d854a24, 0xf8d2bb3d, 0x11aef932, 0x6dc729a1, 0x4b1d9e2f, 0xf3dcb230, 0xec0d8652, 0xd077c1e3, 0x6c2bb316, 0x99a970b9, 0xfa119448, 0x2247e964, 0xc4a8fc8c, 0x1aa0f03f, 0xd8567d2c, 0xef223390, 0xc787494e, 0xc1d938d1, 0xfe8ccaa2, 0x3698d40b, 0xcfa6f581, 0x28a57ade, 0x26dab78e, 0xa43fadbf, 0xe42c3a9d, 0x0d507892, 0x9b6a5fcc, 0x62547e46, 0xc2f68d13, 0xe890d8b8, 0x5e2e39f7, 0xf582c3af, 0xbe9f5d80, 0x7c69d093, 0xa96fd52d, 0xb3cf2512, 0x3bc8ac99, 0xa710187d, 0x6ee89c63, 0x7bdb3bbb, 0x09cd2678, 0xf46e5918, 0x01ec9ab7, 0xa8834f9a, 0x65e6956e, 0x7eaaffe6, 0x0821bccf, 0xe6ef15e8, 0xd9bae79b, 0xce4a6f36, 0xd4ea9f09, 0xd629b07c, 0xaf31a4b2, 0x312a3f23, 0x30c6a594, 0xc035a266, 0x37744ebc, 0xa6fc82ca, 0xb0e090d0, 0x1533a7d8, 0x4af10498, 0xf741ecda, 0x0e7fcd50, 0x2f1791f6, 0x8d764dd6, 0x4d43efb0, 0x54ccaa4d, 0xdfe49604, 0xe39ed1b5, 0x1b4c6a88, 0xb8c12c1f, 0x7f466551, 0x049d5eea, 0x5d018c35, 0x73fa8774, 0x2efb0b41, 0x5ab3671d, 0x5292dbd2, 0x33e91056, 0x136dd647, 0x8c9ad761, 0x7a37a10c, 0x8e59f814, 0x89eb133c, 0xeecea927, 0x35b761c9, 0xede11ce5, 0x3c7a47b1, 0x599cd2df, 0x3f55f273, 0x791814ce, 0xbf73c737, 0xea53f7cd, 0x5b5ffdaa, 0x14df3d6f, 0x867844db, 0x81caaff3, 0x3eb968c4, 0x2c382434, 0x5fc2a340, 0x72161dc3, 0x0cbce225, 0x8b283c49, 0x41ff0d95, 0x7139a801, 0xde080cb3, 0x9cd8b4e4, 0x906456c1, 0x617bcb84, 0x70d532b6, 0x74486c5c, 0x42d0b857 ] + T7 = [ 0xa75051f4, 0x65537e41, 0xa4c31a17, 0x5e963a27, 0x6bcb3bab, 0x45f11f9d, 0x58abacfa, 0x03934be3, 0xfa552030, 0x6df6ad76, 0x769188cc, 0x4c25f502, 0xd7fc4fe5, 0xcbd7c52a, 0x44802635, 0xa38fb562, 0x5a49deb1, 0x1b6725ba, 0x0e9845ea, 0xc0e15dfe, 0x7502c32f, 0xf012814c, 0x97a38d46, 0xf9c66bd3, 0x5fe7038f, 0x9c951592, 0x7aebbf6d, 0x59da9552, 0x832dd4be, 0x21d35874, 0x692949e0, 0xc8448ec9, 0x896a75c2, 0x7978f48e, 0x3e6b9958, 0x71dd27b9, 0x4fb6bee1, 0xad17f088, 0xac66c920, 0x3ab47dce, 0x4a1863df, 0x3182e51a, 0x33609751, 0x7f456253, 0x77e0b164, 0xae84bb6b, 0xa01cfe81, 0x2b94f908, 0x68587048, 0xfd198f45, 0x6c8794de, 0xf8b7527b, 0xd323ab73, 0x02e2724b, 0x8f57e31f, 0xab2a6655, 0x2807b2eb, 0xc2032fb5, 0x7b9a86c5, 0x08a5d337, 0x87f23028, 0xa5b223bf, 0x6aba0203, 0x825ced16, 0x1c2b8acf, 0xb492a779, 0xf2f0f307, 0xe2a14e69, 0xf4cd65da, 0xbed50605, 0x621fd134, 0xfe8ac4a6, 0x539d342e, 0x55a0a2f3, 0xe132058a, 0xeb75a4f6, 0xec390b83, 0xefaa4060, 0x9f065e71, 0x1051bd6e, 0x8af93e21, 0x063d96dd, 0x05aedd3e, 0xbd464de6, 0x8db59154, 0x5d0571c4, 0xd46f0406, 0x15ff6050, 0xfb241998, 0xe997d6bd, 0x43cc8940, 0x9e7767d9, 0x42bdb0e8, 0x8b880789, 0x5b38e719, 0xeedb79c8, 0x0a47a17c, 0x0fe97c42, 0x1ec9f884, 0x00000000, 0x86830980, 0xed48322b, 0x70ac1e11, 0x724e6c5a, 0xfffbfd0e, 0x38560f85, 0xd51e3dae, 0x3927362d, 0xd9640a0f, 0xa621685c, 0x54d19b5b, 0x2e3a2436, 0x67b10c0a, 0xe70f9357, 0x96d2b4ee, 0x919e1b9b, 0xc54f80c0, 0x20a261dc, 0x4b695a77, 0x1a161c12, 0xba0ae293, 0x2ae5c0a0, 0xe0433c22, 0x171d121b, 0x0d0b0e09, 0xc7adf28b, 0xa8b92db6, 0xa9c8141e, 0x198557f1, 0x074caf75, 0xddbbee99, 0x60fda37f, 0x269ff701, 0xf5bc5c72, 0x3bc54466, 0x7e345bfb, 0x29768b43, 0xc6dccb23, 0xfc68b6ed, 0xf163b8e4, 0xdccad731, 0x85104263, 0x22401397, 0x112084c6, 0x247d854a, 0x3df8d2bb, 0x3211aef9, 0xa16dc729, 0x2f4b1d9e, 0x30f3dcb2, 0x52ec0d86, 0xe3d077c1, 0x166c2bb3, 0xb999a970, 0x48fa1194, 0x642247e9, 0x8cc4a8fc, 0x3f1aa0f0, 0x2cd8567d, 0x90ef2233, 0x4ec78749, 0xd1c1d938, 0xa2fe8cca, 0x0b3698d4, 0x81cfa6f5, 0xde28a57a, 0x8e26dab7, 0xbfa43fad, 0x9de42c3a, 0x920d5078, 0xcc9b6a5f, 0x4662547e, 0x13c2f68d, 0xb8e890d8, 0xf75e2e39, 0xaff582c3, 0x80be9f5d, 0x937c69d0, 0x2da96fd5, 0x12b3cf25, 0x993bc8ac, 0x7da71018, 0x636ee89c, 0xbb7bdb3b, 0x7809cd26, 0x18f46e59, 0xb701ec9a, 0x9aa8834f, 0x6e65e695, 0xe67eaaff, 0xcf0821bc, 0xe8e6ef15, 0x9bd9bae7, 0x36ce4a6f, 0x09d4ea9f, 0x7cd629b0, 0xb2af31a4, 0x23312a3f, 0x9430c6a5, 0x66c035a2, 0xbc37744e, 0xcaa6fc82, 0xd0b0e090, 0xd81533a7, 0x984af104, 0xdaf741ec, 0x500e7fcd, 0xf62f1791, 0xd68d764d, 0xb04d43ef, 0x4d54ccaa, 0x04dfe496, 0xb5e39ed1, 0x881b4c6a, 0x1fb8c12c, 0x517f4665, 0xea049d5e, 0x355d018c, 0x7473fa87, 0x412efb0b, 0x1d5ab367, 0xd25292db, 0x5633e910, 0x47136dd6, 0x618c9ad7, 0x0c7a37a1, 0x148e59f8, 0x3c89eb13, 0x27eecea9, 0xc935b761, 0xe5ede11c, 0xb13c7a47, 0xdf599cd2, 0x733f55f2, 0xce791814, 0x37bf73c7, 0xcdea53f7, 0xaa5b5ffd, 0x6f14df3d, 0xdb867844, 0xf381caaf, 0xc43eb968, 0x342c3824, 0x405fc2a3, 0xc372161d, 0x250cbce2, 0x498b283c, 0x9541ff0d, 0x017139a8, 0xb3de080c, 0xe49cd8b4, 0xc1906456, 0x84617bcb, 0xb670d532, 0x5c74486c, 0x5742d0b8 ] + T8 = [ 0xf4a75051, 0x4165537e, 0x17a4c31a, 0x275e963a, 0xab6bcb3b, 0x9d45f11f, 0xfa58abac, 0xe303934b, 0x30fa5520, 0x766df6ad, 0xcc769188, 0x024c25f5, 0xe5d7fc4f, 0x2acbd7c5, 0x35448026, 0x62a38fb5, 0xb15a49de, 0xba1b6725, 0xea0e9845, 0xfec0e15d, 0x2f7502c3, 0x4cf01281, 0x4697a38d, 0xd3f9c66b, 0x8f5fe703, 0x929c9515, 0x6d7aebbf, 0x5259da95, 0xbe832dd4, 0x7421d358, 0xe0692949, 0xc9c8448e, 0xc2896a75, 0x8e7978f4, 0x583e6b99, 0xb971dd27, 0xe14fb6be, 0x88ad17f0, 0x20ac66c9, 0xce3ab47d, 0xdf4a1863, 0x1a3182e5, 0x51336097, 0x537f4562, 0x6477e0b1, 0x6bae84bb, 0x81a01cfe, 0x082b94f9, 0x48685870, 0x45fd198f, 0xde6c8794, 0x7bf8b752, 0x73d323ab, 0x4b02e272, 0x1f8f57e3, 0x55ab2a66, 0xeb2807b2, 0xb5c2032f, 0xc57b9a86, 0x3708a5d3, 0x2887f230, 0xbfa5b223, 0x036aba02, 0x16825ced, 0xcf1c2b8a, 0x79b492a7, 0x07f2f0f3, 0x69e2a14e, 0xdaf4cd65, 0x05bed506, 0x34621fd1, 0xa6fe8ac4, 0x2e539d34, 0xf355a0a2, 0x8ae13205, 0xf6eb75a4, 0x83ec390b, 0x60efaa40, 0x719f065e, 0x6e1051bd, 0x218af93e, 0xdd063d96, 0x3e05aedd, 0xe6bd464d, 0x548db591, 0xc45d0571, 0x06d46f04, 0x5015ff60, 0x98fb2419, 0xbde997d6, 0x4043cc89, 0xd99e7767, 0xe842bdb0, 0x898b8807, 0x195b38e7, 0xc8eedb79, 0x7c0a47a1, 0x420fe97c, 0x841ec9f8, 0x00000000, 0x80868309, 0x2bed4832, 0x1170ac1e, 0x5a724e6c, 0x0efffbfd, 0x8538560f, 0xaed51e3d, 0x2d392736, 0x0fd9640a, 0x5ca62168, 0x5b54d19b, 0x362e3a24, 0x0a67b10c, 0x57e70f93, 0xee96d2b4, 0x9b919e1b, 0xc0c54f80, 0xdc20a261, 0x774b695a, 0x121a161c, 0x93ba0ae2, 0xa02ae5c0, 0x22e0433c, 0x1b171d12, 0x090d0b0e, 0x8bc7adf2, 0xb6a8b92d, 0x1ea9c814, 0xf1198557, 0x75074caf, 0x99ddbbee, 0x7f60fda3, 0x01269ff7, 0x72f5bc5c, 0x663bc544, 0xfb7e345b, 0x4329768b, 0x23c6dccb, 0xedfc68b6, 0xe4f163b8, 0x31dccad7, 0x63851042, 0x97224013, 0xc6112084, 0x4a247d85, 0xbb3df8d2, 0xf93211ae, 0x29a16dc7, 0x9e2f4b1d, 0xb230f3dc, 0x8652ec0d, 0xc1e3d077, 0xb3166c2b, 0x70b999a9, 0x9448fa11, 0xe9642247, 0xfc8cc4a8, 0xf03f1aa0, 0x7d2cd856, 0x3390ef22, 0x494ec787, 0x38d1c1d9, 0xcaa2fe8c, 0xd40b3698, 0xf581cfa6, 0x7ade28a5, 0xb78e26da, 0xadbfa43f, 0x3a9de42c, 0x78920d50, 0x5fcc9b6a, 0x7e466254, 0x8d13c2f6, 0xd8b8e890, 0x39f75e2e, 0xc3aff582, 0x5d80be9f, 0xd0937c69, 0xd52da96f, 0x2512b3cf, 0xac993bc8, 0x187da710, 0x9c636ee8, 0x3bbb7bdb, 0x267809cd, 0x5918f46e, 0x9ab701ec, 0x4f9aa883, 0x956e65e6, 0xffe67eaa, 0xbccf0821, 0x15e8e6ef, 0xe79bd9ba, 0x6f36ce4a, 0x9f09d4ea, 0xb07cd629, 0xa4b2af31, 0x3f23312a, 0xa59430c6, 0xa266c035, 0x4ebc3774, 0x82caa6fc, 0x90d0b0e0, 0xa7d81533, 0x04984af1, 0xecdaf741, 0xcd500e7f, 0x91f62f17, 0x4dd68d76, 0xefb04d43, 0xaa4d54cc, 0x9604dfe4, 0xd1b5e39e, 0x6a881b4c, 0x2c1fb8c1, 0x65517f46, 0x5eea049d, 0x8c355d01, 0x877473fa, 0x0b412efb, 0x671d5ab3, 0xdbd25292, 0x105633e9, 0xd647136d, 0xd7618c9a, 0xa10c7a37, 0xf8148e59, 0x133c89eb, 0xa927eece, 0x61c935b7, 0x1ce5ede1, 0x47b13c7a, 0xd2df599c, 0xf2733f55, 0x14ce7918, 0xc737bf73, 0xf7cdea53, 0xfdaa5b5f, 0x3d6f14df, 0x44db8678, 0xaff381ca, 0x68c43eb9, 0x24342c38, 0xa3405fc2, 0x1dc37216, 0xe2250cbc, 0x3c498b28, 0x0d9541ff, 0xa8017139, 0x0cb3de08, 0xb4e49cd8, 0x56c19064, 0xcb84617b, 0x32b670d5, 0x6c5c7448, 0xb85742d0 ] + + # Transformations for decryption key expansion + U1 = [ 0x00000000, 0x0e090d0b, 0x1c121a16, 0x121b171d, 0x3824342c, 0x362d3927, 0x24362e3a, 0x2a3f2331, 0x70486858, 0x7e416553, 0x6c5a724e, 0x62537f45, 0x486c5c74, 0x4665517f, 0x547e4662, 0x5a774b69, 0xe090d0b0, 0xee99ddbb, 0xfc82caa6, 0xf28bc7ad, 0xd8b4e49c, 0xd6bde997, 0xc4a6fe8a, 0xcaaff381, 0x90d8b8e8, 0x9ed1b5e3, 0x8ccaa2fe, 0x82c3aff5, 0xa8fc8cc4, 0xa6f581cf, 0xb4ee96d2, 0xbae79bd9, 0xdb3bbb7b, 0xd532b670, 0xc729a16d, 0xc920ac66, 0xe31f8f57, 0xed16825c, 0xff0d9541, 0xf104984a, 0xab73d323, 0xa57ade28, 0xb761c935, 0xb968c43e, 0x9357e70f, 0x9d5eea04, 0x8f45fd19, 0x814cf012, 0x3bab6bcb, 0x35a266c0, 0x27b971dd, 0x29b07cd6, 0x038f5fe7, 0x0d8652ec, 0x1f9d45f1, 0x119448fa, 0x4be30393, 0x45ea0e98, 0x57f11985, 0x59f8148e, 0x73c737bf, 0x7dce3ab4, 0x6fd52da9, 0x61dc20a2, 0xad766df6, 0xa37f60fd, 0xb16477e0, 0xbf6d7aeb, 0x955259da, 0x9b5b54d1, 0x894043cc, 0x87494ec7, 0xdd3e05ae, 0xd33708a5, 0xc12c1fb8, 0xcf2512b3, 0xe51a3182, 0xeb133c89, 0xf9082b94, 0xf701269f, 0x4de6bd46, 0x43efb04d, 0x51f4a750, 0x5ffdaa5b, 0x75c2896a, 0x7bcb8461, 0x69d0937c, 0x67d99e77, 0x3daed51e, 0x33a7d815, 0x21bccf08, 0x2fb5c203, 0x058ae132, 0x0b83ec39, 0x1998fb24, 0x1791f62f, 0x764dd68d, 0x7844db86, 0x6a5fcc9b, 0x6456c190, 0x4e69e2a1, 0x4060efaa, 0x527bf8b7, 0x5c72f5bc, 0x0605bed5, 0x080cb3de, 0x1a17a4c3, 0x141ea9c8, 0x3e218af9, 0x302887f2, 0x223390ef, 0x2c3a9de4, 0x96dd063d, 0x98d40b36, 0x8acf1c2b, 0x84c61120, 0xaef93211, 0xa0f03f1a, 0xb2eb2807, 0xbce2250c, 0xe6956e65, 0xe89c636e, 0xfa877473, 0xf48e7978, 0xdeb15a49, 0xd0b85742, 0xc2a3405f, 0xccaa4d54, 0x41ecdaf7, 0x4fe5d7fc, 0x5dfec0e1, 0x53f7cdea, 0x79c8eedb, 0x77c1e3d0, 0x65daf4cd, 0x6bd3f9c6, 0x31a4b2af, 0x3fadbfa4, 0x2db6a8b9, 0x23bfa5b2, 0x09808683, 0x07898b88, 0x15929c95, 0x1b9b919e, 0xa17c0a47, 0xaf75074c, 0xbd6e1051, 0xb3671d5a, 0x99583e6b, 0x97513360, 0x854a247d, 0x8b432976, 0xd134621f, 0xdf3d6f14, 0xcd267809, 0xc32f7502, 0xe9105633, 0xe7195b38, 0xf5024c25, 0xfb0b412e, 0x9ad7618c, 0x94de6c87, 0x86c57b9a, 0x88cc7691, 0xa2f355a0, 0xacfa58ab, 0xbee14fb6, 0xb0e842bd, 0xea9f09d4, 0xe49604df, 0xf68d13c2, 0xf8841ec9, 0xd2bb3df8, 0xdcb230f3, 0xcea927ee, 0xc0a02ae5, 0x7a47b13c, 0x744ebc37, 0x6655ab2a, 0x685ca621, 0x42638510, 0x4c6a881b, 0x5e719f06, 0x5078920d, 0x0a0fd964, 0x0406d46f, 0x161dc372, 0x1814ce79, 0x322bed48, 0x3c22e043, 0x2e39f75e, 0x2030fa55, 0xec9ab701, 0xe293ba0a, 0xf088ad17, 0xfe81a01c, 0xd4be832d, 0xdab78e26, 0xc8ac993b, 0xc6a59430, 0x9cd2df59, 0x92dbd252, 0x80c0c54f, 0x8ec9c844, 0xa4f6eb75, 0xaaffe67e, 0xb8e4f163, 0xb6edfc68, 0x0c0a67b1, 0x02036aba, 0x10187da7, 0x1e1170ac, 0x342e539d, 0x3a275e96, 0x283c498b, 0x26354480, 0x7c420fe9, 0x724b02e2, 0x605015ff, 0x6e5918f4, 0x44663bc5, 0x4a6f36ce, 0x587421d3, 0x567d2cd8, 0x37a10c7a, 0x39a80171, 0x2bb3166c, 0x25ba1b67, 0x0f853856, 0x018c355d, 0x13972240, 0x1d9e2f4b, 0x47e96422, 0x49e06929, 0x5bfb7e34, 0x55f2733f, 0x7fcd500e, 0x71c45d05, 0x63df4a18, 0x6dd64713, 0xd731dcca, 0xd938d1c1, 0xcb23c6dc, 0xc52acbd7, 0xef15e8e6, 0xe11ce5ed, 0xf307f2f0, 0xfd0efffb, 0xa779b492, 0xa970b999, 0xbb6bae84, 0xb562a38f, 0x9f5d80be, 0x91548db5, 0x834f9aa8, 0x8d4697a3 ] + U2 = [ 0x00000000, 0x0b0e090d, 0x161c121a, 0x1d121b17, 0x2c382434, 0x27362d39, 0x3a24362e, 0x312a3f23, 0x58704868, 0x537e4165, 0x4e6c5a72, 0x4562537f, 0x74486c5c, 0x7f466551, 0x62547e46, 0x695a774b, 0xb0e090d0, 0xbbee99dd, 0xa6fc82ca, 0xadf28bc7, 0x9cd8b4e4, 0x97d6bde9, 0x8ac4a6fe, 0x81caaff3, 0xe890d8b8, 0xe39ed1b5, 0xfe8ccaa2, 0xf582c3af, 0xc4a8fc8c, 0xcfa6f581, 0xd2b4ee96, 0xd9bae79b, 0x7bdb3bbb, 0x70d532b6, 0x6dc729a1, 0x66c920ac, 0x57e31f8f, 0x5ced1682, 0x41ff0d95, 0x4af10498, 0x23ab73d3, 0x28a57ade, 0x35b761c9, 0x3eb968c4, 0x0f9357e7, 0x049d5eea, 0x198f45fd, 0x12814cf0, 0xcb3bab6b, 0xc035a266, 0xdd27b971, 0xd629b07c, 0xe7038f5f, 0xec0d8652, 0xf11f9d45, 0xfa119448, 0x934be303, 0x9845ea0e, 0x8557f119, 0x8e59f814, 0xbf73c737, 0xb47dce3a, 0xa96fd52d, 0xa261dc20, 0xf6ad766d, 0xfda37f60, 0xe0b16477, 0xebbf6d7a, 0xda955259, 0xd19b5b54, 0xcc894043, 0xc787494e, 0xaedd3e05, 0xa5d33708, 0xb8c12c1f, 0xb3cf2512, 0x82e51a31, 0x89eb133c, 0x94f9082b, 0x9ff70126, 0x464de6bd, 0x4d43efb0, 0x5051f4a7, 0x5b5ffdaa, 0x6a75c289, 0x617bcb84, 0x7c69d093, 0x7767d99e, 0x1e3daed5, 0x1533a7d8, 0x0821bccf, 0x032fb5c2, 0x32058ae1, 0x390b83ec, 0x241998fb, 0x2f1791f6, 0x8d764dd6, 0x867844db, 0x9b6a5fcc, 0x906456c1, 0xa14e69e2, 0xaa4060ef, 0xb7527bf8, 0xbc5c72f5, 0xd50605be, 0xde080cb3, 0xc31a17a4, 0xc8141ea9, 0xf93e218a, 0xf2302887, 0xef223390, 0xe42c3a9d, 0x3d96dd06, 0x3698d40b, 0x2b8acf1c, 0x2084c611, 0x11aef932, 0x1aa0f03f, 0x07b2eb28, 0x0cbce225, 0x65e6956e, 0x6ee89c63, 0x73fa8774, 0x78f48e79, 0x49deb15a, 0x42d0b857, 0x5fc2a340, 0x54ccaa4d, 0xf741ecda, 0xfc4fe5d7, 0xe15dfec0, 0xea53f7cd, 0xdb79c8ee, 0xd077c1e3, 0xcd65daf4, 0xc66bd3f9, 0xaf31a4b2, 0xa43fadbf, 0xb92db6a8, 0xb223bfa5, 0x83098086, 0x8807898b, 0x9515929c, 0x9e1b9b91, 0x47a17c0a, 0x4caf7507, 0x51bd6e10, 0x5ab3671d, 0x6b99583e, 0x60975133, 0x7d854a24, 0x768b4329, 0x1fd13462, 0x14df3d6f, 0x09cd2678, 0x02c32f75, 0x33e91056, 0x38e7195b, 0x25f5024c, 0x2efb0b41, 0x8c9ad761, 0x8794de6c, 0x9a86c57b, 0x9188cc76, 0xa0a2f355, 0xabacfa58, 0xb6bee14f, 0xbdb0e842, 0xd4ea9f09, 0xdfe49604, 0xc2f68d13, 0xc9f8841e, 0xf8d2bb3d, 0xf3dcb230, 0xeecea927, 0xe5c0a02a, 0x3c7a47b1, 0x37744ebc, 0x2a6655ab, 0x21685ca6, 0x10426385, 0x1b4c6a88, 0x065e719f, 0x0d507892, 0x640a0fd9, 0x6f0406d4, 0x72161dc3, 0x791814ce, 0x48322bed, 0x433c22e0, 0x5e2e39f7, 0x552030fa, 0x01ec9ab7, 0x0ae293ba, 0x17f088ad, 0x1cfe81a0, 0x2dd4be83, 0x26dab78e, 0x3bc8ac99, 0x30c6a594, 0x599cd2df, 0x5292dbd2, 0x4f80c0c5, 0x448ec9c8, 0x75a4f6eb, 0x7eaaffe6, 0x63b8e4f1, 0x68b6edfc, 0xb10c0a67, 0xba02036a, 0xa710187d, 0xac1e1170, 0x9d342e53, 0x963a275e, 0x8b283c49, 0x80263544, 0xe97c420f, 0xe2724b02, 0xff605015, 0xf46e5918, 0xc544663b, 0xce4a6f36, 0xd3587421, 0xd8567d2c, 0x7a37a10c, 0x7139a801, 0x6c2bb316, 0x6725ba1b, 0x560f8538, 0x5d018c35, 0x40139722, 0x4b1d9e2f, 0x2247e964, 0x2949e069, 0x345bfb7e, 0x3f55f273, 0x0e7fcd50, 0x0571c45d, 0x1863df4a, 0x136dd647, 0xcad731dc, 0xc1d938d1, 0xdccb23c6, 0xd7c52acb, 0xe6ef15e8, 0xede11ce5, 0xf0f307f2, 0xfbfd0eff, 0x92a779b4, 0x99a970b9, 0x84bb6bae, 0x8fb562a3, 0xbe9f5d80, 0xb591548d, 0xa8834f9a, 0xa38d4697 ] + U3 = [ 0x00000000, 0x0d0b0e09, 0x1a161c12, 0x171d121b, 0x342c3824, 0x3927362d, 0x2e3a2436, 0x23312a3f, 0x68587048, 0x65537e41, 0x724e6c5a, 0x7f456253, 0x5c74486c, 0x517f4665, 0x4662547e, 0x4b695a77, 0xd0b0e090, 0xddbbee99, 0xcaa6fc82, 0xc7adf28b, 0xe49cd8b4, 0xe997d6bd, 0xfe8ac4a6, 0xf381caaf, 0xb8e890d8, 0xb5e39ed1, 0xa2fe8cca, 0xaff582c3, 0x8cc4a8fc, 0x81cfa6f5, 0x96d2b4ee, 0x9bd9bae7, 0xbb7bdb3b, 0xb670d532, 0xa16dc729, 0xac66c920, 0x8f57e31f, 0x825ced16, 0x9541ff0d, 0x984af104, 0xd323ab73, 0xde28a57a, 0xc935b761, 0xc43eb968, 0xe70f9357, 0xea049d5e, 0xfd198f45, 0xf012814c, 0x6bcb3bab, 0x66c035a2, 0x71dd27b9, 0x7cd629b0, 0x5fe7038f, 0x52ec0d86, 0x45f11f9d, 0x48fa1194, 0x03934be3, 0x0e9845ea, 0x198557f1, 0x148e59f8, 0x37bf73c7, 0x3ab47dce, 0x2da96fd5, 0x20a261dc, 0x6df6ad76, 0x60fda37f, 0x77e0b164, 0x7aebbf6d, 0x59da9552, 0x54d19b5b, 0x43cc8940, 0x4ec78749, 0x05aedd3e, 0x08a5d337, 0x1fb8c12c, 0x12b3cf25, 0x3182e51a, 0x3c89eb13, 0x2b94f908, 0x269ff701, 0xbd464de6, 0xb04d43ef, 0xa75051f4, 0xaa5b5ffd, 0x896a75c2, 0x84617bcb, 0x937c69d0, 0x9e7767d9, 0xd51e3dae, 0xd81533a7, 0xcf0821bc, 0xc2032fb5, 0xe132058a, 0xec390b83, 0xfb241998, 0xf62f1791, 0xd68d764d, 0xdb867844, 0xcc9b6a5f, 0xc1906456, 0xe2a14e69, 0xefaa4060, 0xf8b7527b, 0xf5bc5c72, 0xbed50605, 0xb3de080c, 0xa4c31a17, 0xa9c8141e, 0x8af93e21, 0x87f23028, 0x90ef2233, 0x9de42c3a, 0x063d96dd, 0x0b3698d4, 0x1c2b8acf, 0x112084c6, 0x3211aef9, 0x3f1aa0f0, 0x2807b2eb, 0x250cbce2, 0x6e65e695, 0x636ee89c, 0x7473fa87, 0x7978f48e, 0x5a49deb1, 0x5742d0b8, 0x405fc2a3, 0x4d54ccaa, 0xdaf741ec, 0xd7fc4fe5, 0xc0e15dfe, 0xcdea53f7, 0xeedb79c8, 0xe3d077c1, 0xf4cd65da, 0xf9c66bd3, 0xb2af31a4, 0xbfa43fad, 0xa8b92db6, 0xa5b223bf, 0x86830980, 0x8b880789, 0x9c951592, 0x919e1b9b, 0x0a47a17c, 0x074caf75, 0x1051bd6e, 0x1d5ab367, 0x3e6b9958, 0x33609751, 0x247d854a, 0x29768b43, 0x621fd134, 0x6f14df3d, 0x7809cd26, 0x7502c32f, 0x5633e910, 0x5b38e719, 0x4c25f502, 0x412efb0b, 0x618c9ad7, 0x6c8794de, 0x7b9a86c5, 0x769188cc, 0x55a0a2f3, 0x58abacfa, 0x4fb6bee1, 0x42bdb0e8, 0x09d4ea9f, 0x04dfe496, 0x13c2f68d, 0x1ec9f884, 0x3df8d2bb, 0x30f3dcb2, 0x27eecea9, 0x2ae5c0a0, 0xb13c7a47, 0xbc37744e, 0xab2a6655, 0xa621685c, 0x85104263, 0x881b4c6a, 0x9f065e71, 0x920d5078, 0xd9640a0f, 0xd46f0406, 0xc372161d, 0xce791814, 0xed48322b, 0xe0433c22, 0xf75e2e39, 0xfa552030, 0xb701ec9a, 0xba0ae293, 0xad17f088, 0xa01cfe81, 0x832dd4be, 0x8e26dab7, 0x993bc8ac, 0x9430c6a5, 0xdf599cd2, 0xd25292db, 0xc54f80c0, 0xc8448ec9, 0xeb75a4f6, 0xe67eaaff, 0xf163b8e4, 0xfc68b6ed, 0x67b10c0a, 0x6aba0203, 0x7da71018, 0x70ac1e11, 0x539d342e, 0x5e963a27, 0x498b283c, 0x44802635, 0x0fe97c42, 0x02e2724b, 0x15ff6050, 0x18f46e59, 0x3bc54466, 0x36ce4a6f, 0x21d35874, 0x2cd8567d, 0x0c7a37a1, 0x017139a8, 0x166c2bb3, 0x1b6725ba, 0x38560f85, 0x355d018c, 0x22401397, 0x2f4b1d9e, 0x642247e9, 0x692949e0, 0x7e345bfb, 0x733f55f2, 0x500e7fcd, 0x5d0571c4, 0x4a1863df, 0x47136dd6, 0xdccad731, 0xd1c1d938, 0xc6dccb23, 0xcbd7c52a, 0xe8e6ef15, 0xe5ede11c, 0xf2f0f307, 0xfffbfd0e, 0xb492a779, 0xb999a970, 0xae84bb6b, 0xa38fb562, 0x80be9f5d, 0x8db59154, 0x9aa8834f, 0x97a38d46 ] + U4 = [ 0x00000000, 0x090d0b0e, 0x121a161c, 0x1b171d12, 0x24342c38, 0x2d392736, 0x362e3a24, 0x3f23312a, 0x48685870, 0x4165537e, 0x5a724e6c, 0x537f4562, 0x6c5c7448, 0x65517f46, 0x7e466254, 0x774b695a, 0x90d0b0e0, 0x99ddbbee, 0x82caa6fc, 0x8bc7adf2, 0xb4e49cd8, 0xbde997d6, 0xa6fe8ac4, 0xaff381ca, 0xd8b8e890, 0xd1b5e39e, 0xcaa2fe8c, 0xc3aff582, 0xfc8cc4a8, 0xf581cfa6, 0xee96d2b4, 0xe79bd9ba, 0x3bbb7bdb, 0x32b670d5, 0x29a16dc7, 0x20ac66c9, 0x1f8f57e3, 0x16825ced, 0x0d9541ff, 0x04984af1, 0x73d323ab, 0x7ade28a5, 0x61c935b7, 0x68c43eb9, 0x57e70f93, 0x5eea049d, 0x45fd198f, 0x4cf01281, 0xab6bcb3b, 0xa266c035, 0xb971dd27, 0xb07cd629, 0x8f5fe703, 0x8652ec0d, 0x9d45f11f, 0x9448fa11, 0xe303934b, 0xea0e9845, 0xf1198557, 0xf8148e59, 0xc737bf73, 0xce3ab47d, 0xd52da96f, 0xdc20a261, 0x766df6ad, 0x7f60fda3, 0x6477e0b1, 0x6d7aebbf, 0x5259da95, 0x5b54d19b, 0x4043cc89, 0x494ec787, 0x3e05aedd, 0x3708a5d3, 0x2c1fb8c1, 0x2512b3cf, 0x1a3182e5, 0x133c89eb, 0x082b94f9, 0x01269ff7, 0xe6bd464d, 0xefb04d43, 0xf4a75051, 0xfdaa5b5f, 0xc2896a75, 0xcb84617b, 0xd0937c69, 0xd99e7767, 0xaed51e3d, 0xa7d81533, 0xbccf0821, 0xb5c2032f, 0x8ae13205, 0x83ec390b, 0x98fb2419, 0x91f62f17, 0x4dd68d76, 0x44db8678, 0x5fcc9b6a, 0x56c19064, 0x69e2a14e, 0x60efaa40, 0x7bf8b752, 0x72f5bc5c, 0x05bed506, 0x0cb3de08, 0x17a4c31a, 0x1ea9c814, 0x218af93e, 0x2887f230, 0x3390ef22, 0x3a9de42c, 0xdd063d96, 0xd40b3698, 0xcf1c2b8a, 0xc6112084, 0xf93211ae, 0xf03f1aa0, 0xeb2807b2, 0xe2250cbc, 0x956e65e6, 0x9c636ee8, 0x877473fa, 0x8e7978f4, 0xb15a49de, 0xb85742d0, 0xa3405fc2, 0xaa4d54cc, 0xecdaf741, 0xe5d7fc4f, 0xfec0e15d, 0xf7cdea53, 0xc8eedb79, 0xc1e3d077, 0xdaf4cd65, 0xd3f9c66b, 0xa4b2af31, 0xadbfa43f, 0xb6a8b92d, 0xbfa5b223, 0x80868309, 0x898b8807, 0x929c9515, 0x9b919e1b, 0x7c0a47a1, 0x75074caf, 0x6e1051bd, 0x671d5ab3, 0x583e6b99, 0x51336097, 0x4a247d85, 0x4329768b, 0x34621fd1, 0x3d6f14df, 0x267809cd, 0x2f7502c3, 0x105633e9, 0x195b38e7, 0x024c25f5, 0x0b412efb, 0xd7618c9a, 0xde6c8794, 0xc57b9a86, 0xcc769188, 0xf355a0a2, 0xfa58abac, 0xe14fb6be, 0xe842bdb0, 0x9f09d4ea, 0x9604dfe4, 0x8d13c2f6, 0x841ec9f8, 0xbb3df8d2, 0xb230f3dc, 0xa927eece, 0xa02ae5c0, 0x47b13c7a, 0x4ebc3774, 0x55ab2a66, 0x5ca62168, 0x63851042, 0x6a881b4c, 0x719f065e, 0x78920d50, 0x0fd9640a, 0x06d46f04, 0x1dc37216, 0x14ce7918, 0x2bed4832, 0x22e0433c, 0x39f75e2e, 0x30fa5520, 0x9ab701ec, 0x93ba0ae2, 0x88ad17f0, 0x81a01cfe, 0xbe832dd4, 0xb78e26da, 0xac993bc8, 0xa59430c6, 0xd2df599c, 0xdbd25292, 0xc0c54f80, 0xc9c8448e, 0xf6eb75a4, 0xffe67eaa, 0xe4f163b8, 0xedfc68b6, 0x0a67b10c, 0x036aba02, 0x187da710, 0x1170ac1e, 0x2e539d34, 0x275e963a, 0x3c498b28, 0x35448026, 0x420fe97c, 0x4b02e272, 0x5015ff60, 0x5918f46e, 0x663bc544, 0x6f36ce4a, 0x7421d358, 0x7d2cd856, 0xa10c7a37, 0xa8017139, 0xb3166c2b, 0xba1b6725, 0x8538560f, 0x8c355d01, 0x97224013, 0x9e2f4b1d, 0xe9642247, 0xe0692949, 0xfb7e345b, 0xf2733f55, 0xcd500e7f, 0xc45d0571, 0xdf4a1863, 0xd647136d, 0x31dccad7, 0x38d1c1d9, 0x23c6dccb, 0x2acbd7c5, 0x15e8e6ef, 0x1ce5ede1, 0x07f2f0f3, 0x0efffbfd, 0x79b492a7, 0x70b999a9, 0x6bae84bb, 0x62a38fb5, 0x5d80be9f, 0x548db591, 0x4f9aa883, 0x4697a38d ] + + def __init__(self, key): + + if len(key) not in (16, 24, 32): + raise ValueError('Invalid key size') + + rounds = self.number_of_rounds[len(key)] + + # Encryption round keys + self._Ke = [[0] * 4 for i in xrange(rounds + 1)] + + # Decryption round keys + self._Kd = [[0] * 4 for i in xrange(rounds + 1)] + + round_key_count = (rounds + 1) * 4 + KC = len(key) // 4 + + # Convert the key into ints + tk = [ struct.unpack('>i', key[i:i + 4])[0] for i in xrange(0, len(key), 4) ] + + # Copy values into round key arrays + for i in xrange(0, KC): + self._Ke[i // 4][i % 4] = tk[i] + self._Kd[rounds - (i // 4)][i % 4] = tk[i] + + # Key expansion (fips-197 section 5.2) + rconpointer = 0 + t = KC + while t < round_key_count: + + tt = tk[KC - 1] + tk[0] ^= ((self.S[(tt >> 16) & 0xFF] << 24) ^ + (self.S[(tt >> 8) & 0xFF] << 16) ^ + (self.S[ tt & 0xFF] << 8) ^ + self.S[(tt >> 24) & 0xFF] ^ + (self.rcon[rconpointer] << 24)) + rconpointer += 1 + + if KC != 8: + for i in xrange(1, KC): + tk[i] ^= tk[i - 1] + + # Key expansion for 256-bit keys is "slightly different" (fips-197) + else: + for i in xrange(1, KC // 2): + tk[i] ^= tk[i - 1] + tt = tk[KC // 2 - 1] + + tk[KC // 2] ^= (self.S[ tt & 0xFF] ^ + (self.S[(tt >> 8) & 0xFF] << 8) ^ + (self.S[(tt >> 16) & 0xFF] << 16) ^ + (self.S[(tt >> 24) & 0xFF] << 24)) + + for i in xrange(KC // 2 + 1, KC): + tk[i] ^= tk[i - 1] + + # Copy values into round key arrays + j = 0 + while j < KC and t < round_key_count: + self._Ke[t // 4][t % 4] = tk[j] + self._Kd[rounds - (t // 4)][t % 4] = tk[j] + j += 1 + t += 1 + + # Inverse-Cipher-ify the decryption round key (fips-197 section 5.3) + for r in xrange(1, rounds): + for j in xrange(0, 4): + tt = self._Kd[r][j] + self._Kd[r][j] = (self.U1[(tt >> 24) & 0xFF] ^ + self.U2[(tt >> 16) & 0xFF] ^ + self.U3[(tt >> 8) & 0xFF] ^ + self.U4[ tt & 0xFF]) + + def encrypt(self, plaintext): + 'Encrypt a block of plain text using the AES block cipher.' + + if len(plaintext) != 16: + raise ValueError('wrong block length') + + rounds = len(self._Ke) - 1 + (s1, s2, s3) = [1, 2, 3] + a = [0, 0, 0, 0] + + # Convert plaintext to (ints ^ key) + t = [(_compact_word(plaintext[4 * i:4 * i + 4]) ^ self._Ke[0][i]) for i in xrange(0, 4)] + + # Apply round transforms + for r in xrange(1, rounds): + for i in xrange(0, 4): + a[i] = (self.T1[(t[ i ] >> 24) & 0xFF] ^ + self.T2[(t[(i + s1) % 4] >> 16) & 0xFF] ^ + self.T3[(t[(i + s2) % 4] >> 8) & 0xFF] ^ + self.T4[ t[(i + s3) % 4] & 0xFF] ^ + self._Ke[r][i]) + t = copy.copy(a) + + # The last round is special + result = [ ] + for i in xrange(0, 4): + tt = self._Ke[rounds][i] + result.append((self.S[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) + result.append((self.S[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) + result.append((self.S[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) + result.append((self.S[ t[(i + s3) % 4] & 0xFF] ^ tt ) & 0xFF) + + return result + + def decrypt(self, ciphertext): + 'Decrypt a block of cipher text using the AES block cipher.' + + if len(ciphertext) != 16: + raise ValueError('wrong block length') + + rounds = len(self._Kd) - 1 + (s1, s2, s3) = [3, 2, 1] + a = [0, 0, 0, 0] + + # Convert ciphertext to (ints ^ key) + t = [(_compact_word(ciphertext[4 * i:4 * i + 4]) ^ self._Kd[0][i]) for i in xrange(0, 4)] + + # Apply round transforms + for r in xrange(1, rounds): + for i in xrange(0, 4): + a[i] = (self.T5[(t[ i ] >> 24) & 0xFF] ^ + self.T6[(t[(i + s1) % 4] >> 16) & 0xFF] ^ + self.T7[(t[(i + s2) % 4] >> 8) & 0xFF] ^ + self.T8[ t[(i + s3) % 4] & 0xFF] ^ + self._Kd[r][i]) + t = copy.copy(a) + + # The last round is special + result = [ ] + for i in xrange(0, 4): + tt = self._Kd[rounds][i] + result.append((self.Si[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) + result.append((self.Si[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) + result.append((self.Si[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) + result.append((self.Si[ t[(i + s3) % 4] & 0xFF] ^ tt ) & 0xFF) + + return result + + +class Counter(object): + '''A counter object for the Counter (CTR) mode of operation. + + To create a custom counter, you can usually just override the + increment method.''' + + def __init__(self, initial_value = 1): + + # Convert the value into an array of bytes long + self._counter = [ ((initial_value >> i) % 256) for i in xrange(128 - 8, -1, -8) ] + + value = property(lambda s: s._counter) + + def increment(self): + '''Increment the counter (overflow rolls back to 0).''' + + for i in xrange(len(self._counter) - 1, -1, -1): + self._counter[i] += 1 + + if self._counter[i] < 256: break + + # Carry the one + self._counter[i] = 0 + + # Overflow + else: + self._counter = [ 0 ] * len(self._counter) + + +class AESBlockModeOfOperation(object): + '''Super-class for AES modes of operation that require blocks.''' + def __init__(self, key): + self._aes = AES(key) + + def decrypt(self, ciphertext): + raise Exception('not implemented') + + def encrypt(self, plaintext): + raise Exception('not implemented') + + +class AESStreamModeOfOperation(AESBlockModeOfOperation): + '''Super-class for AES modes of operation that are stream-ciphers.''' + +class AESSegmentModeOfOperation(AESStreamModeOfOperation): + '''Super-class for AES modes of operation that segment data.''' + + segment_bytes = 16 + + + +class AESModeOfOperationECB(AESBlockModeOfOperation): + '''AES Electronic Codebook Mode of Operation. + + o Block-cipher, so data must be padded to 16 byte boundaries + + Security Notes: + o This mode is not recommended + o Any two identical blocks produce identical encrypted values, + exposing data patterns. (See the image of Tux on wikipedia) + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Electronic_codebook_.28ECB.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.1''' + + + name = "Electronic Codebook (ECB)" + + def encrypt(self, plaintext): + if len(plaintext) != 16: + raise ValueError('plaintext block must be 16 bytes') + + plaintext = _string_to_bytes(plaintext) + return _bytes_to_string(self._aes.encrypt(plaintext)) + + def decrypt(self, ciphertext): + if len(ciphertext) != 16: + raise ValueError('ciphertext block must be 16 bytes') + + ciphertext = _string_to_bytes(ciphertext) + return _bytes_to_string(self._aes.decrypt(ciphertext)) + + + +class AESModeOfOperationCBC(AESBlockModeOfOperation): + '''AES Cipher-Block Chaining Mode of Operation. + + o The Initialization Vector (IV) + o Block-cipher, so data must be padded to 16 byte boundaries + o An incorrect initialization vector will only cause the first + block to be corrupt; all other blocks will be intact + o A corrupt bit in the cipher text will cause a block to be + corrupted, and the next block to be inverted, but all other + blocks will be intact. + + Security Notes: + o This method (and CTR) ARE recommended. + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher-block_chaining_.28CBC.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.2''' + + + name = "Cipher-Block Chaining (CBC)" + + def __init__(self, key, iv = None): + if iv is None: + self._last_cipherblock = [ 0 ] * 16 + elif len(iv) != 16: + raise ValueError('initialization vector must be 16 bytes') + else: + self._last_cipherblock = _string_to_bytes(iv) + + AESBlockModeOfOperation.__init__(self, key) + + def encrypt(self, plaintext): + if len(plaintext) != 16: + raise ValueError('plaintext block must be 16 bytes') + + plaintext = _string_to_bytes(plaintext) + precipherblock = [ (p ^ l) for (p, l) in zip(plaintext, self._last_cipherblock) ] + self._last_cipherblock = self._aes.encrypt(precipherblock) + + return _bytes_to_string(self._last_cipherblock) + + def decrypt(self, ciphertext): + if len(ciphertext) != 16: + raise ValueError('ciphertext block must be 16 bytes') + + cipherblock = _string_to_bytes(ciphertext) + plaintext = [ (p ^ l) for (p, l) in zip(self._aes.decrypt(cipherblock), self._last_cipherblock) ] + self._last_cipherblock = cipherblock + + return _bytes_to_string(plaintext) + + + +class AESModeOfOperationCFB(AESSegmentModeOfOperation): + '''AES Cipher Feedback Mode of Operation. + + o A stream-cipher, so input does not need to be padded to blocks, + but does need to be padded to segment_size + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_feedback_.28CFB.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.3''' + + + name = "Cipher Feedback (CFB)" + + def __init__(self, key, iv, segment_size = 1): + if segment_size == 0: segment_size = 1 + + if iv is None: + self._shift_register = [ 0 ] * 16 + elif len(iv) != 16: + raise ValueError('initialization vector must be 16 bytes') + else: + self._shift_register = _string_to_bytes(iv) + + self._segment_bytes = segment_size + + AESBlockModeOfOperation.__init__(self, key) + + segment_bytes = property(lambda s: s._segment_bytes) + + def encrypt(self, plaintext): + if len(plaintext) % self._segment_bytes != 0: + raise ValueError('plaintext block must be a multiple of segment_size') + + plaintext = _string_to_bytes(plaintext) + + # Break block into segments + encrypted = [ ] + for i in xrange(0, len(plaintext), self._segment_bytes): + plaintext_segment = plaintext[i: i + self._segment_bytes] + xor_segment = self._aes.encrypt(self._shift_register)[:len(plaintext_segment)] + cipher_segment = [ (p ^ x) for (p, x) in zip(plaintext_segment, xor_segment) ] + + # Shift the top bits out and the ciphertext in + self._shift_register = _concat_list(self._shift_register[len(cipher_segment):], cipher_segment) + + encrypted.extend(cipher_segment) + + return _bytes_to_string(encrypted) + + def decrypt(self, ciphertext): + if len(ciphertext) % self._segment_bytes != 0: + raise ValueError('ciphertext block must be a multiple of segment_size') + + ciphertext = _string_to_bytes(ciphertext) + + # Break block into segments + decrypted = [ ] + for i in xrange(0, len(ciphertext), self._segment_bytes): + cipher_segment = ciphertext[i: i + self._segment_bytes] + xor_segment = self._aes.encrypt(self._shift_register)[:len(cipher_segment)] + plaintext_segment = [ (p ^ x) for (p, x) in zip(cipher_segment, xor_segment) ] + + # Shift the top bits out and the ciphertext in + self._shift_register = _concat_list(self._shift_register[len(cipher_segment):], cipher_segment) + + decrypted.extend(plaintext_segment) + + return _bytes_to_string(decrypted) + + + +class AESModeOfOperationOFB(AESStreamModeOfOperation): + '''AES Output Feedback Mode of Operation. + + o A stream-cipher, so input does not need to be padded to blocks, + allowing arbitrary length data. + o A bit twiddled in the cipher text, twiddles the same bit in the + same bit in the plain text, which can be useful for error + correction techniques. + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Output_feedback_.28OFB.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.4''' + + + name = "Output Feedback (OFB)" + + def __init__(self, key, iv = None): + if iv is None: + self._last_precipherblock = [ 0 ] * 16 + elif len(iv) != 16: + raise ValueError('initialization vector must be 16 bytes') + else: + self._last_precipherblock = _string_to_bytes(iv) + + self._remaining_block = [ ] + + AESBlockModeOfOperation.__init__(self, key) + + def encrypt(self, plaintext): + encrypted = [ ] + for p in _string_to_bytes(plaintext): + if len(self._remaining_block) == 0: + self._remaining_block = self._aes.encrypt(self._last_precipherblock) + self._last_precipherblock = [ ] + precipherbyte = self._remaining_block.pop(0) + self._last_precipherblock.append(precipherbyte) + cipherbyte = p ^ precipherbyte + encrypted.append(cipherbyte) + + return _bytes_to_string(encrypted) + + def decrypt(self, ciphertext): + # AES-OFB is symetric + return self.encrypt(ciphertext) + + + +class AESModeOfOperationCTR(AESStreamModeOfOperation): + '''AES Counter Mode of Operation. + + o A stream-cipher, so input does not need to be padded to blocks, + allowing arbitrary length data. + o The counter must be the same size as the key size (ie. len(key)) + o Each block independant of the other, so a corrupt byte will not + damage future blocks. + o Each block has a uniue counter value associated with it, which + contributes to the encrypted value, so no data patterns are + leaked. + o Also known as: Counter Mode (CM), Integer Counter Mode (ICM) and + Segmented Integer Counter (SIC + + Security Notes: + o This method (and CBC) ARE recommended. + o Each message block is associated with a counter value which must be + unique for ALL messages with the same key. Otherwise security may be + compromised. + + Also see: + + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_.28CTR.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.5 + and Appendix B for managing the initial counter''' + + + name = "Counter (CTR)" + + def __init__(self, key, counter = None): + AESBlockModeOfOperation.__init__(self, key) + + if counter is None: + counter = Counter() + + self._counter = counter + self._remaining_counter = [ ] + + def encrypt(self, plaintext): + while len(self._remaining_counter) < len(plaintext): + self._remaining_counter += self._aes.encrypt(self._counter.value) + self._counter.increment() + + plaintext = _string_to_bytes(plaintext) + + encrypted = [ (p ^ c) for (p, c) in zip(plaintext, self._remaining_counter) ] + self._remaining_counter = self._remaining_counter[len(encrypted):] + + return _bytes_to_string(encrypted) + + def decrypt(self, crypttext): + # AES-CTR is symetric + return self.encrypt(crypttext) + + +# Simple lookup table for each mode +AESModesOfOperation = dict( + ctr = AESModeOfOperationCTR, + cbc = AESModeOfOperationCBC, + cfb = AESModeOfOperationCFB, + ecb = AESModeOfOperationECB, + ofb = AESModeOfOperationOFB, +) diff --git a/src/lib/pyaes/blockfeeder.py b/src/lib/pyaes/blockfeeder.py new file mode 100644 index 00000000..b9a904d2 --- /dev/null +++ b/src/lib/pyaes/blockfeeder.py @@ -0,0 +1,227 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 Richard Moore +# +# 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. + + +from .aes import AESBlockModeOfOperation, AESSegmentModeOfOperation, AESStreamModeOfOperation +from .util import append_PKCS7_padding, strip_PKCS7_padding, to_bufferable + + +# First we inject three functions to each of the modes of operations +# +# _can_consume(size) +# - Given a size, determine how many bytes could be consumed in +# a single call to either the decrypt or encrypt method +# +# _final_encrypt(data, padding = PADDING_DEFAULT) +# - call and return encrypt on this (last) chunk of data, +# padding as necessary; this will always be at least 16 +# bytes unless the total incoming input was less than 16 +# bytes +# +# _final_decrypt(data, padding = PADDING_DEFAULT) +# - same as _final_encrypt except for decrypt, for +# stripping off padding +# + +PADDING_NONE = 'none' +PADDING_DEFAULT = 'default' + +# @TODO: Ciphertext stealing and explicit PKCS#7 +# PADDING_CIPHERTEXT_STEALING +# PADDING_PKCS7 + +# ECB and CBC are block-only ciphers + +def _block_can_consume(self, size): + if size >= 16: return 16 + return 0 + +# After padding, we may have more than one block +def _block_final_encrypt(self, data, padding = PADDING_DEFAULT): + if padding == PADDING_DEFAULT: + data = append_PKCS7_padding(data) + + elif padding == PADDING_NONE: + if len(data) != 16: + raise Exception('invalid data length for final block') + else: + raise Exception('invalid padding option') + + if len(data) == 32: + return self.encrypt(data[:16]) + self.encrypt(data[16:]) + + return self.encrypt(data) + + +def _block_final_decrypt(self, data, padding = PADDING_DEFAULT): + if padding == PADDING_DEFAULT: + return strip_PKCS7_padding(self.decrypt(data)) + + if padding == PADDING_NONE: + if len(data) != 16: + raise Exception('invalid data length for final block') + return self.decrypt(data) + + raise Exception('invalid padding option') + +AESBlockModeOfOperation._can_consume = _block_can_consume +AESBlockModeOfOperation._final_encrypt = _block_final_encrypt +AESBlockModeOfOperation._final_decrypt = _block_final_decrypt + + + +# CFB is a segment cipher + +def _segment_can_consume(self, size): + return self.segment_bytes * int(size // self.segment_bytes) + +# CFB can handle a non-segment-sized block at the end using the remaining cipherblock +def _segment_final_encrypt(self, data, padding = PADDING_DEFAULT): + if padding != PADDING_DEFAULT: + raise Exception('invalid padding option') + + faux_padding = (chr(0) * (self.segment_bytes - (len(data) % self.segment_bytes))) + padded = data + to_bufferable(faux_padding) + return self.encrypt(padded)[:len(data)] + +# CFB can handle a non-segment-sized block at the end using the remaining cipherblock +def _segment_final_decrypt(self, data, padding = PADDING_DEFAULT): + if padding != PADDING_DEFAULT: + raise Exception('invalid padding option') + + faux_padding = (chr(0) * (self.segment_bytes - (len(data) % self.segment_bytes))) + padded = data + to_bufferable(faux_padding) + return self.decrypt(padded)[:len(data)] + +AESSegmentModeOfOperation._can_consume = _segment_can_consume +AESSegmentModeOfOperation._final_encrypt = _segment_final_encrypt +AESSegmentModeOfOperation._final_decrypt = _segment_final_decrypt + + + +# OFB and CTR are stream ciphers + +def _stream_can_consume(self, size): + return size + +def _stream_final_encrypt(self, data, padding = PADDING_DEFAULT): + if padding not in [PADDING_NONE, PADDING_DEFAULT]: + raise Exception('invalid padding option') + + return self.encrypt(data) + +def _stream_final_decrypt(self, data, padding = PADDING_DEFAULT): + if padding not in [PADDING_NONE, PADDING_DEFAULT]: + raise Exception('invalid padding option') + + return self.decrypt(data) + +AESStreamModeOfOperation._can_consume = _stream_can_consume +AESStreamModeOfOperation._final_encrypt = _stream_final_encrypt +AESStreamModeOfOperation._final_decrypt = _stream_final_decrypt + + + +class BlockFeeder(object): + '''The super-class for objects to handle chunking a stream of bytes + into the appropriate block size for the underlying mode of operation + and applying (or stripping) padding, as necessary.''' + + def __init__(self, mode, feed, final, padding = PADDING_DEFAULT): + self._mode = mode + self._feed = feed + self._final = final + self._buffer = to_bufferable("") + self._padding = padding + + def feed(self, data = None): + '''Provide bytes to encrypt (or decrypt), returning any bytes + possible from this or any previous calls to feed. + + Call with None or an empty string to flush the mode of + operation and return any final bytes; no further calls to + feed may be made.''' + + if self._buffer is None: + raise ValueError('already finished feeder') + + # Finalize; process the spare bytes we were keeping + if data is None: + result = self._final(self._buffer, self._padding) + self._buffer = None + return result + + self._buffer += to_bufferable(data) + + # We keep 16 bytes around so we can determine padding + result = to_bufferable('') + while len(self._buffer) > 16: + can_consume = self._mode._can_consume(len(self._buffer) - 16) + if can_consume == 0: break + result += self._feed(self._buffer[:can_consume]) + self._buffer = self._buffer[can_consume:] + + return result + + +class Encrypter(BlockFeeder): + 'Accepts bytes of plaintext and returns encrypted ciphertext.' + + def __init__(self, mode, padding = PADDING_DEFAULT): + BlockFeeder.__init__(self, mode, mode.encrypt, mode._final_encrypt, padding) + + +class Decrypter(BlockFeeder): + 'Accepts bytes of ciphertext and returns decrypted plaintext.' + + def __init__(self, mode, padding = PADDING_DEFAULT): + BlockFeeder.__init__(self, mode, mode.decrypt, mode._final_decrypt, padding) + + +# 8kb blocks +BLOCK_SIZE = (1 << 13) + +def _feed_stream(feeder, in_stream, out_stream, block_size = BLOCK_SIZE): + 'Uses feeder to read and convert from in_stream and write to out_stream.' + + while True: + chunk = in_stream.read(block_size) + if not chunk: + break + converted = feeder.feed(chunk) + out_stream.write(converted) + converted = feeder.feed() + out_stream.write(converted) + + +def encrypt_stream(mode, in_stream, out_stream, block_size = BLOCK_SIZE, padding = PADDING_DEFAULT): + 'Encrypts a stream of bytes from in_stream to out_stream using mode.' + + encrypter = Encrypter(mode, padding = padding) + _feed_stream(encrypter, in_stream, out_stream, block_size) + + +def decrypt_stream(mode, in_stream, out_stream, block_size = BLOCK_SIZE, padding = PADDING_DEFAULT): + 'Decrypts a stream of bytes from in_stream to out_stream using mode.' + + decrypter = Decrypter(mode, padding = padding) + _feed_stream(decrypter, in_stream, out_stream, block_size) diff --git a/src/lib/pyaes/util.py b/src/lib/pyaes/util.py new file mode 100644 index 00000000..081a3759 --- /dev/null +++ b/src/lib/pyaes/util.py @@ -0,0 +1,60 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 Richard Moore +# +# 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. + +# Why to_bufferable? +# Python 3 is very different from Python 2.x when it comes to strings of text +# and strings of bytes; in Python 3, strings of bytes do not exist, instead to +# represent arbitrary binary data, we must use the "bytes" object. This method +# ensures the object behaves as we need it to. + +def to_bufferable(binary): + return binary + +def _get_byte(c): + return ord(c) + +try: + xrange +except: + + def to_bufferable(binary): + if isinstance(binary, bytes): + return binary + return bytes(ord(b) for b in binary) + + def _get_byte(c): + return c + +def append_PKCS7_padding(data): + pad = 16 - (len(data) % 16) + return data + to_bufferable(chr(pad) * pad) + +def strip_PKCS7_padding(data): + if len(data) % 16 != 0: + raise ValueError("invalid length") + + pad = _get_byte(data[-1]) + + if pad > 16: + raise ValueError("invalid padding byte") + + return data[:-pad] diff --git a/src/lib/pybitcointools/__init__.py b/src/lib/pybitcointools/__init__.py deleted file mode 100644 index 7d529abc..00000000 --- a/src/lib/pybitcointools/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .py2specials import * -from .py3specials import * -from .main import * -from .transaction import * -from .deterministic import * -from .bci import * -from .composite import * -from .stealth import * -from .blocks import * -from .mnemonic import * diff --git a/src/lib/pybitcointools/bci.py b/src/lib/pybitcointools/bci.py deleted file mode 100644 index 79a2c401..00000000 --- a/src/lib/pybitcointools/bci.py +++ /dev/null @@ -1,528 +0,0 @@ -#!/usr/bin/python -import json, re -import random -import sys -try: - from urllib.request import build_opener -except: - from urllib2 import build_opener - - -# Makes a request to a given URL (first arg) and optional params (second arg) -def make_request(*args): - opener = build_opener() - opener.addheaders = [('User-agent', - 'Mozilla/5.0'+str(random.randrange(1000000)))] - try: - return opener.open(*args).read().strip() - except Exception as e: - try: - p = e.read().strip() - except: - p = e - raise Exception(p) - - -def is_testnet(inp): - '''Checks if inp is a testnet address or if UTXO is a known testnet TxID''' - if isinstance(inp, (list, tuple)) and len(inp) >= 1: - return any([is_testnet(x) for x in inp]) - elif not isinstance(inp, basestring): # sanity check - raise TypeError("Input must be str/unicode, not type %s" % str(type(inp))) - - if not inp or (inp.lower() in ("btc", "testnet")): - pass - - ## ADDRESSES - if inp[0] in "123mn": - if re.match("^[2mn][a-km-zA-HJ-NP-Z0-9]{26,33}$", inp): - return True - elif re.match("^[13][a-km-zA-HJ-NP-Z0-9]{26,33}$", inp): - return False - else: - #sys.stderr.write("Bad address format %s") - return None - - ## TXID - elif re.match('^[0-9a-fA-F]{64}$', inp): - base_url = "http://api.blockcypher.com/v1/btc/{network}/txs/{txid}?includesHex=false" - try: - # try testnet fetchtx - make_request(base_url.format(network="test3", txid=inp.lower())) - return True - except: - # try mainnet fetchtx - make_request(base_url.format(network="main", txid=inp.lower())) - return False - sys.stderr.write("TxID %s has no match for testnet or mainnet (Bad TxID)") - return None - else: - raise TypeError("{0} is unknown input".format(inp)) - - -def set_network(*args): - '''Decides if args for unspent/fetchtx/pushtx are mainnet or testnet''' - r = [] - for arg in args: - if not arg: - pass - if isinstance(arg, basestring): - r.append(is_testnet(arg)) - elif isinstance(arg, (list, tuple)): - return set_network(*arg) - if any(r) and not all(r): - raise Exception("Mixed Testnet/Mainnet queries") - return "testnet" if any(r) else "btc" - - -def parse_addr_args(*args): - # Valid input formats: unspent([addr1, addr2, addr3]) - # unspent([addr1, addr2, addr3], network) - # unspent(addr1, addr2, addr3) - # unspent(addr1, addr2, addr3, network) - addr_args = args - network = "btc" - if len(args) == 0: - return [], 'btc' - if len(args) >= 1 and args[-1] in ('testnet', 'btc'): - network = args[-1] - addr_args = args[:-1] - if len(addr_args) == 1 and isinstance(addr_args, list): - network = set_network(*addr_args[0]) - addr_args = addr_args[0] - if addr_args and isinstance(addr_args, tuple) and isinstance(addr_args[0], list): - addr_args = addr_args[0] - network = set_network(addr_args) - return network, addr_args - - -# Gets the unspent outputs of one or more addresses -def bci_unspent(*args): - network, addrs = parse_addr_args(*args) - u = [] - for a in addrs: - try: - data = make_request('https://blockchain.info/unspent?active='+a) - except Exception as e: - if str(e) == 'No free outputs to spend': - continue - else: - raise Exception(e) - try: - jsonobj = json.loads(data.decode("utf-8")) - for o in jsonobj["unspent_outputs"]: - h = o['tx_hash'].decode('hex')[::-1].encode('hex') - u.append({ - "output": h+':'+str(o['tx_output_n']), - "value": o['value'] - }) - except: - raise Exception("Failed to decode data: "+data) - return u - - -def blockr_unspent(*args): - # Valid input formats: blockr_unspent([addr1, addr2,addr3]) - # blockr_unspent(addr1, addr2, addr3) - # blockr_unspent([addr1, addr2, addr3], network) - # blockr_unspent(addr1, addr2, addr3, network) - # Where network is 'btc' or 'testnet' - network, addr_args = parse_addr_args(*args) - - if network == 'testnet': - blockr_url = 'http://tbtc.blockr.io/api/v1/address/unspent/' - elif network == 'btc': - blockr_url = 'http://btc.blockr.io/api/v1/address/unspent/' - else: - raise Exception( - 'Unsupported network {0} for blockr_unspent'.format(network)) - - if len(addr_args) == 0: - return [] - elif isinstance(addr_args[0], list): - addrs = addr_args[0] - else: - addrs = addr_args - res = make_request(blockr_url+','.join(addrs)) - data = json.loads(res.decode("utf-8"))['data'] - o = [] - if 'unspent' in data: - data = [data] - for dat in data: - for u in dat['unspent']: - o.append({ - "output": u['tx']+':'+str(u['n']), - "value": int(u['amount'].replace('.', '')) - }) - return o - - -def helloblock_unspent(*args): - addrs, network = parse_addr_args(*args) - if network == 'testnet': - url = 'https://testnet.helloblock.io/v1/addresses/%s/unspents?limit=500&offset=%s' - elif network == 'btc': - url = 'https://mainnet.helloblock.io/v1/addresses/%s/unspents?limit=500&offset=%s' - o = [] - for addr in addrs: - for offset in xrange(0, 10**9, 500): - res = make_request(url % (addr, offset)) - data = json.loads(res.decode("utf-8"))["data"] - if not len(data["unspents"]): - break - elif offset: - sys.stderr.write("Getting more unspents: %d\n" % offset) - for dat in data["unspents"]: - o.append({ - "output": dat["txHash"]+':'+str(dat["index"]), - "value": dat["value"], - }) - return o - - -unspent_getters = { - 'bci': bci_unspent, - 'blockr': blockr_unspent, - 'helloblock': helloblock_unspent -} - - -def unspent(*args, **kwargs): - f = unspent_getters.get(kwargs.get('source', ''), bci_unspent) - return f(*args) - - -# Gets the transaction output history of a given set of addresses, -# including whether or not they have been spent -def history(*args): - # Valid input formats: history([addr1, addr2,addr3]) - # history(addr1, addr2, addr3) - if len(args) == 0: - return [] - elif isinstance(args[0], list): - addrs = args[0] - else: - addrs = args - - txs = [] - for addr in addrs: - offset = 0 - while 1: - gathered = False - while not gathered: - try: - data = make_request( - 'https://blockchain.info/address/%s?format=json&offset=%s' % - (addr, offset)) - gathered = True - except Exception as e: - try: - sys.stderr.write(e.read().strip()) - except: - sys.stderr.write(str(e)) - gathered = False - try: - jsonobj = json.loads(data.decode("utf-8")) - except: - raise Exception("Failed to decode data: "+data) - txs.extend(jsonobj["txs"]) - if len(jsonobj["txs"]) < 50: - break - offset += 50 - sys.stderr.write("Fetching more transactions... "+str(offset)+'\n') - outs = {} - for tx in txs: - for o in tx["out"]: - if o.get('addr', None) in addrs: - key = str(tx["tx_index"])+':'+str(o["n"]) - outs[key] = { - "address": o["addr"], - "value": o["value"], - "output": tx["hash"]+':'+str(o["n"]), - "block_height": tx.get("block_height", None) - } - for tx in txs: - for i, inp in enumerate(tx["inputs"]): - if "prev_out" in inp: - if inp["prev_out"].get("addr", None) in addrs: - key = str(inp["prev_out"]["tx_index"]) + \ - ':'+str(inp["prev_out"]["n"]) - if outs.get(key): - outs[key]["spend"] = tx["hash"]+':'+str(i) - return [outs[k] for k in outs] - - -# Pushes a transaction to the network using https://blockchain.info/pushtx -def bci_pushtx(tx): - if not re.match('^[0-9a-fA-F]*$', tx): - tx = tx.encode('hex') - return make_request('https://blockchain.info/pushtx', 'tx='+tx) - - -def eligius_pushtx(tx): - if not re.match('^[0-9a-fA-F]*$', tx): - tx = tx.encode('hex') - s = make_request( - 'http://eligius.st/~wizkid057/newstats/pushtxn.php', - 'transaction='+tx+'&send=Push') - strings = re.findall('string[^"]*"[^"]*"', s) - for string in strings: - quote = re.findall('"[^"]*"', string)[0] - if len(quote) >= 5: - return quote[1:-1] - - -def blockr_pushtx(tx, network='btc'): - if network == 'testnet': - blockr_url = 'http://tbtc.blockr.io/api/v1/tx/push' - elif network == 'btc': - blockr_url = 'http://btc.blockr.io/api/v1/tx/push' - else: - raise Exception( - 'Unsupported network {0} for blockr_pushtx'.format(network)) - - if not re.match('^[0-9a-fA-F]*$', tx): - tx = tx.encode('hex') - return make_request(blockr_url, '{"hex":"%s"}' % tx) - - -def helloblock_pushtx(tx): - if not re.match('^[0-9a-fA-F]*$', tx): - tx = tx.encode('hex') - return make_request('https://mainnet.helloblock.io/v1/transactions', - 'rawTxHex='+tx) - -pushtx_getters = { - 'bci': bci_pushtx, - 'blockr': blockr_pushtx, - 'helloblock': helloblock_pushtx -} - - -def pushtx(*args, **kwargs): - f = pushtx_getters.get(kwargs.get('source', ''), bci_pushtx) - return f(*args) - - -def last_block_height(network='btc'): - if network == 'testnet': - data = make_request('http://tbtc.blockr.io/api/v1/block/info/last') - jsonobj = json.loads(data.decode("utf-8")) - return jsonobj["data"]["nb"] - - data = make_request('https://blockchain.info/latestblock') - jsonobj = json.loads(data.decode("utf-8")) - return jsonobj["height"] - - -# Gets a specific transaction -def bci_fetchtx(txhash): - if isinstance(txhash, list): - return [bci_fetchtx(h) for h in txhash] - if not re.match('^[0-9a-fA-F]*$', txhash): - txhash = txhash.encode('hex') - data = make_request('https://blockchain.info/rawtx/'+txhash+'?format=hex') - return data - - -def blockr_fetchtx(txhash, network='btc'): - if network == 'testnet': - blockr_url = 'http://tbtc.blockr.io/api/v1/tx/raw/' - elif network == 'btc': - blockr_url = 'http://btc.blockr.io/api/v1/tx/raw/' - else: - raise Exception( - 'Unsupported network {0} for blockr_fetchtx'.format(network)) - if isinstance(txhash, list): - txhash = ','.join([x.encode('hex') if not re.match('^[0-9a-fA-F]*$', x) - else x for x in txhash]) - jsondata = json.loads(make_request(blockr_url+txhash).decode("utf-8")) - return [d['tx']['hex'] for d in jsondata['data']] - else: - if not re.match('^[0-9a-fA-F]*$', txhash): - txhash = txhash.encode('hex') - jsondata = json.loads(make_request(blockr_url+txhash).decode("utf-8")) - return jsondata['data']['tx']['hex'] - - -def helloblock_fetchtx(txhash, network='btc'): - if isinstance(txhash, list): - return [helloblock_fetchtx(h) for h in txhash] - if not re.match('^[0-9a-fA-F]*$', txhash): - txhash = txhash.encode('hex') - if network == 'testnet': - url = 'https://testnet.helloblock.io/v1/transactions/' - elif network == 'btc': - url = 'https://mainnet.helloblock.io/v1/transactions/' - else: - raise Exception( - 'Unsupported network {0} for helloblock_fetchtx'.format(network)) - data = json.loads(make_request(url + txhash).decode("utf-8"))["data"]["transaction"] - o = { - "locktime": data["locktime"], - "version": data["version"], - "ins": [], - "outs": [] - } - for inp in data["inputs"]: - o["ins"].append({ - "script": inp["scriptSig"], - "outpoint": { - "index": inp["prevTxoutIndex"], - "hash": inp["prevTxHash"], - }, - "sequence": 4294967295 - }) - for outp in data["outputs"]: - o["outs"].append({ - "value": outp["value"], - "script": outp["scriptPubKey"] - }) - from .transaction import serialize - from .transaction import txhash as TXHASH - tx = serialize(o) - assert TXHASH(tx) == txhash - return tx - - -fetchtx_getters = { - 'bci': bci_fetchtx, - 'blockr': blockr_fetchtx, - 'helloblock': helloblock_fetchtx -} - - -def fetchtx(*args, **kwargs): - f = fetchtx_getters.get(kwargs.get('source', ''), bci_fetchtx) - return f(*args) - - -def firstbits(address): - if len(address) >= 25: - return make_request('https://blockchain.info/q/getfirstbits/'+address) - else: - return make_request( - 'https://blockchain.info/q/resolvefirstbits/'+address) - - -def get_block_at_height(height): - j = json.loads(make_request("https://blockchain.info/block-height/" + - str(height)+"?format=json").decode("utf-8")) - for b in j['blocks']: - if b['main_chain'] is True: - return b - raise Exception("Block at this height not found") - - -def _get_block(inp): - if len(str(inp)) < 64: - return get_block_at_height(inp) - else: - return json.loads(make_request( - 'https://blockchain.info/rawblock/'+inp).decode("utf-8")) - - -def bci_get_block_header_data(inp): - j = _get_block(inp) - return { - 'version': j['ver'], - 'hash': j['hash'], - 'prevhash': j['prev_block'], - 'timestamp': j['time'], - 'merkle_root': j['mrkl_root'], - 'bits': j['bits'], - 'nonce': j['nonce'], - } - -def blockr_get_block_header_data(height, network='btc'): - if network == 'testnet': - blockr_url = "http://tbtc.blockr.io/api/v1/block/raw/" - elif network == 'btc': - blockr_url = "http://btc.blockr.io/api/v1/block/raw/" - else: - raise Exception( - 'Unsupported network {0} for blockr_get_block_header_data'.format(network)) - - k = json.loads(make_request(blockr_url + str(height)).decode("utf-8")) - j = k['data'] - return { - 'version': j['version'], - 'hash': j['hash'], - 'prevhash': j['previousblockhash'], - 'timestamp': j['time'], - 'merkle_root': j['merkleroot'], - 'bits': int(j['bits'], 16), - 'nonce': j['nonce'], - } - - -def get_block_timestamp(height, network='btc'): - if network == 'testnet': - blockr_url = "http://tbtc.blockr.io/api/v1/block/info/" - elif network == 'btc': - blockr_url = "http://btc.blockr.io/api/v1/block/info/" - else: - raise Exception( - 'Unsupported network {0} for get_block_timestamp'.format(network)) - - import time, calendar - if isinstance(height, list): - k = json.loads(make_request(blockr_url + ','.join([str(x) for x in height])).decode("utf-8")) - o = {x['nb']: calendar.timegm(time.strptime(x['time_utc'], - "%Y-%m-%dT%H:%M:%SZ")) for x in k['data']} - return [o[x] for x in height] - else: - k = json.loads(make_request(blockr_url + str(height)).decode("utf-8")) - j = k['data']['time_utc'] - return calendar.timegm(time.strptime(j, "%Y-%m-%dT%H:%M:%SZ")) - - -block_header_data_getters = { - 'bci': bci_get_block_header_data, - 'blockr': blockr_get_block_header_data -} - - -def get_block_header_data(inp, **kwargs): - f = block_header_data_getters.get(kwargs.get('source', ''), - bci_get_block_header_data) - return f(inp, **kwargs) - - -def get_txs_in_block(inp): - j = _get_block(inp) - hashes = [t['hash'] for t in j['tx']] - return hashes - - -def get_block_height(txhash): - j = json.loads(make_request('https://blockchain.info/rawtx/'+txhash).decode("utf-8")) - return j['block_height'] - -# fromAddr, toAddr, 12345, changeAddress -def get_tx_composite(inputs, outputs, output_value, change_address=None, network=None): - """mktx using blockcypher API""" - inputs = [inputs] if not isinstance(inputs, list) else inputs - outputs = [outputs] if not isinstance(outputs, list) else outputs - network = set_network(change_address or inputs) if not network else network.lower() - url = "http://api.blockcypher.com/v1/btc/{network}/txs/new?includeToSignTx=true".format( - network=('test3' if network=='testnet' else 'main')) - is_address = lambda a: bool(re.match("^[123mn][a-km-zA-HJ-NP-Z0-9]{26,33}$", a)) - if any([is_address(x) for x in inputs]): - inputs_type = 'addresses' # also accepts UTXOs, only addresses supported presently - if any([is_address(x) for x in outputs]): - outputs_type = 'addresses' # TODO: add UTXO support - data = { - 'inputs': [{inputs_type: inputs}], - 'confirmations': 0, - 'preference': 'high', - 'outputs': [{outputs_type: outputs, "value": output_value}] - } - if change_address: - data["change_address"] = change_address # - jdata = json.loads(make_request(url, data)) - hash, txh = jdata.get("tosign")[0], jdata.get("tosign_tx")[0] - assert bin_dbl_sha256(txh.decode('hex')).encode('hex') == hash, "checksum mismatch %s" % hash - return txh.encode("utf-8") - -blockcypher_mktx = get_tx_composite diff --git a/src/lib/pybitcointools/blocks.py b/src/lib/pybitcointools/blocks.py deleted file mode 100644 index 9df6b35c..00000000 --- a/src/lib/pybitcointools/blocks.py +++ /dev/null @@ -1,50 +0,0 @@ -from .main import * - - -def serialize_header(inp): - o = encode(inp['version'], 256, 4)[::-1] + \ - inp['prevhash'].decode('hex')[::-1] + \ - inp['merkle_root'].decode('hex')[::-1] + \ - encode(inp['timestamp'], 256, 4)[::-1] + \ - encode(inp['bits'], 256, 4)[::-1] + \ - encode(inp['nonce'], 256, 4)[::-1] - h = bin_sha256(bin_sha256(o))[::-1].encode('hex') - assert h == inp['hash'], (sha256(o), inp['hash']) - return o.encode('hex') - - -def deserialize_header(inp): - inp = inp.decode('hex') - return { - "version": decode(inp[:4][::-1], 256), - "prevhash": inp[4:36][::-1].encode('hex'), - "merkle_root": inp[36:68][::-1].encode('hex'), - "timestamp": decode(inp[68:72][::-1], 256), - "bits": decode(inp[72:76][::-1], 256), - "nonce": decode(inp[76:80][::-1], 256), - "hash": bin_sha256(bin_sha256(inp))[::-1].encode('hex') - } - - -def mk_merkle_proof(header, hashes, index): - nodes = [h.decode('hex')[::-1] for h in hashes] - if len(nodes) % 2 and len(nodes) > 2: - nodes.append(nodes[-1]) - layers = [nodes] - while len(nodes) > 1: - newnodes = [] - for i in range(0, len(nodes) - 1, 2): - newnodes.append(bin_sha256(bin_sha256(nodes[i] + nodes[i+1]))) - if len(newnodes) % 2 and len(newnodes) > 2: - newnodes.append(newnodes[-1]) - nodes = newnodes - layers.append(nodes) - # Sanity check, make sure merkle root is valid - assert nodes[0][::-1].encode('hex') == header['merkle_root'] - merkle_siblings = \ - [layers[i][(index >> i) ^ 1] for i in range(len(layers)-1)] - return { - "hash": hashes[index], - "siblings": [x[::-1].encode('hex') for x in merkle_siblings], - "header": header - } diff --git a/src/lib/pybitcointools/composite.py b/src/lib/pybitcointools/composite.py deleted file mode 100644 index e5d50492..00000000 --- a/src/lib/pybitcointools/composite.py +++ /dev/null @@ -1,128 +0,0 @@ -from .main import * -from .transaction import * -from .bci import * -from .deterministic import * -from .blocks import * - - -# Takes privkey, address, value (satoshis), fee (satoshis) -def send(frm, to, value, fee=10000, **kwargs): - return sendmultitx(frm, to + ":" + str(value), fee, **kwargs) - - -# Takes privkey, "address1:value1,address2:value2" (satoshis), fee (satoshis) -def sendmultitx(frm, *args, **kwargs): - tv, fee = args[:-1], int(args[-1]) - outs = [] - outvalue = 0 - for a in tv: - outs.append(a) - outvalue += int(a.split(":")[1]) - - u = unspent(privtoaddr(frm), **kwargs) - u2 = select(u, int(outvalue)+int(fee)) - argz = u2 + outs + [privtoaddr(frm), fee] - tx = mksend(*argz) - tx2 = signall(tx, frm) - return pushtx(tx2, **kwargs) - - -# Takes address, address, value (satoshis), fee(satoshis) -def preparetx(frm, to, value, fee=10000, **kwargs): - tovalues = to + ":" + str(value) - return preparemultitx(frm, tovalues, fee, **kwargs) - - -# Takes address, address:value, address:value ... (satoshis), fee(satoshis) -def preparemultitx(frm, *args, **kwargs): - tv, fee = args[:-1], int(args[-1]) - outs = [] - outvalue = 0 - for a in tv: - outs.append(a) - outvalue += int(a.split(":")[1]) - - u = unspent(frm, **kwargs) - u2 = select(u, int(outvalue)+int(fee)) - argz = u2 + outs + [frm, fee] - return mksend(*argz) - - -# BIP32 hierarchical deterministic multisig script -def bip32_hdm_script(*args): - if len(args) == 3: - keys, req, path = args - else: - i, keys, path = 0, [], [] - while len(args[i]) > 40: - keys.append(args[i]) - i += 1 - req = int(args[i]) - path = map(int, args[i+1:]) - pubs = sorted(map(lambda x: bip32_descend(x, path), keys)) - return mk_multisig_script(pubs, req) - - -# BIP32 hierarchical deterministic multisig address -def bip32_hdm_addr(*args): - return scriptaddr(bip32_hdm_script(*args)) - - -# Setup a coinvault transaction -def setup_coinvault_tx(tx, script): - txobj = deserialize(tx) - N = deserialize_script(script)[-2] - for inp in txobj["ins"]: - inp["script"] = serialize_script([None] * (N+1) + [script]) - return serialize(txobj) - - -# Sign a coinvault transaction -def sign_coinvault_tx(tx, priv): - pub = privtopub(priv) - txobj = deserialize(tx) - subscript = deserialize_script(txobj['ins'][0]['script']) - oscript = deserialize_script(subscript[-1]) - k, pubs = oscript[0], oscript[1:-2] - for j in range(len(txobj['ins'])): - scr = deserialize_script(txobj['ins'][j]['script']) - for i, p in enumerate(pubs): - if p == pub: - scr[i+1] = multisign(tx, j, subscript[-1], priv) - if len(filter(lambda x: x, scr[1:-1])) >= k: - scr = [None] + filter(lambda x: x, scr[1:-1])[:k] + [scr[-1]] - txobj['ins'][j]['script'] = serialize_script(scr) - return serialize(txobj) - - -# Inspects a transaction -def inspect(tx, **kwargs): - d = deserialize(tx) - isum = 0 - ins = {} - for _in in d['ins']: - h = _in['outpoint']['hash'] - i = _in['outpoint']['index'] - prevout = deserialize(fetchtx(h, **kwargs))['outs'][i] - isum += prevout['value'] - a = script_to_address(prevout['script']) - ins[a] = ins.get(a, 0) + prevout['value'] - outs = [] - osum = 0 - for _out in d['outs']: - outs.append({'address': script_to_address(_out['script']), - 'value': _out['value']}) - osum += _out['value'] - return { - 'fee': isum - osum, - 'outs': outs, - 'ins': ins - } - - -def merkle_prove(txhash): - blocknum = str(get_block_height(txhash)) - header = get_block_header_data(blocknum) - hashes = get_txs_in_block(blocknum) - i = hashes.index(txhash) - return mk_merkle_proof(header, hashes, i) diff --git a/src/lib/pybitcointools/deterministic.py b/src/lib/pybitcointools/deterministic.py deleted file mode 100644 index b2bdbbc6..00000000 --- a/src/lib/pybitcointools/deterministic.py +++ /dev/null @@ -1,199 +0,0 @@ -from .main import * -import hmac -import hashlib -from binascii import hexlify -# Electrum wallets - - -def electrum_stretch(seed): - return slowsha(seed) - -# Accepts seed or stretched seed, returns master public key - - -def electrum_mpk(seed): - if len(seed) == 32: - seed = electrum_stretch(seed) - return privkey_to_pubkey(seed)[2:] - -# Accepts (seed or stretched seed), index and secondary index -# (conventionally 0 for ordinary addresses, 1 for change) , returns privkey - - -def electrum_privkey(seed, n, for_change=0): - if len(seed) == 32: - seed = electrum_stretch(seed) - mpk = electrum_mpk(seed) - offset = dbl_sha256(from_int_representation_to_bytes(n)+b':'+from_int_representation_to_bytes(for_change)+b':'+binascii.unhexlify(mpk)) - return add_privkeys(seed, offset) - -# Accepts (seed or stretched seed or master pubkey), index and secondary index -# (conventionally 0 for ordinary addresses, 1 for change) , returns pubkey - - -def electrum_pubkey(masterkey, n, for_change=0): - if len(masterkey) == 32: - mpk = electrum_mpk(electrum_stretch(masterkey)) - elif len(masterkey) == 64: - mpk = electrum_mpk(masterkey) - else: - mpk = masterkey - bin_mpk = encode_pubkey(mpk, 'bin_electrum') - offset = bin_dbl_sha256(from_int_representation_to_bytes(n)+b':'+from_int_representation_to_bytes(for_change)+b':'+bin_mpk) - return add_pubkeys('04'+mpk, privtopub(offset)) - -# seed/stretched seed/pubkey -> address (convenience method) - - -def electrum_address(masterkey, n, for_change=0, version=0): - return pubkey_to_address(electrum_pubkey(masterkey, n, for_change), version) - -# Given a master public key, a private key from that wallet and its index, -# cracks the secret exponent which can be used to generate all other private -# keys in the wallet - - -def crack_electrum_wallet(mpk, pk, n, for_change=0): - bin_mpk = encode_pubkey(mpk, 'bin_electrum') - offset = dbl_sha256(str(n)+':'+str(for_change)+':'+bin_mpk) - return subtract_privkeys(pk, offset) - -# Below code ASSUMES binary inputs and compressed pubkeys -MAINNET_PRIVATE = b'\x04\x88\xAD\xE4' -MAINNET_PUBLIC = b'\x04\x88\xB2\x1E' -TESTNET_PRIVATE = b'\x04\x35\x83\x94' -TESTNET_PUBLIC = b'\x04\x35\x87\xCF' -PRIVATE = [MAINNET_PRIVATE, TESTNET_PRIVATE] -PUBLIC = [MAINNET_PUBLIC, TESTNET_PUBLIC] - -# BIP32 child key derivation - - -def raw_bip32_ckd(rawtuple, i): - vbytes, depth, fingerprint, oldi, chaincode, key = rawtuple - i = int(i) - - if vbytes in PRIVATE: - priv = key - pub = privtopub(key) - else: - pub = key - - if i >= 2**31: - if vbytes in PUBLIC: - raise Exception("Can't do private derivation on public key!") - I = hmac.new(chaincode, b'\x00'+priv[:32]+encode(i, 256, 4), hashlib.sha512).digest() - else: - I = hmac.new(chaincode, pub+encode(i, 256, 4), hashlib.sha512).digest() - - if vbytes in PRIVATE: - newkey = add_privkeys(I[:32]+B'\x01', priv) - fingerprint = bin_hash160(privtopub(key))[:4] - if vbytes in PUBLIC: - newkey = add_pubkeys(compress(privtopub(I[:32])), key) - fingerprint = bin_hash160(key)[:4] - - return (vbytes, depth + 1, fingerprint, i, I[32:], newkey) - - -def bip32_serialize(rawtuple): - vbytes, depth, fingerprint, i, chaincode, key = rawtuple - i = encode(i, 256, 4) - chaincode = encode(hash_to_int(chaincode), 256, 32) - keydata = b'\x00'+key[:-1] if vbytes in PRIVATE else key - bindata = vbytes + from_int_to_byte(depth % 256) + fingerprint + i + chaincode + keydata - return changebase(bindata+bin_dbl_sha256(bindata)[:4], 256, 58) - - -def bip32_deserialize(data): - dbin = changebase(data, 58, 256) - if bin_dbl_sha256(dbin[:-4])[:4] != dbin[-4:]: - raise Exception("Invalid checksum") - vbytes = dbin[0:4] - depth = from_byte_to_int(dbin[4]) - fingerprint = dbin[5:9] - i = decode(dbin[9:13], 256) - chaincode = dbin[13:45] - key = dbin[46:78]+b'\x01' if vbytes in PRIVATE else dbin[45:78] - return (vbytes, depth, fingerprint, i, chaincode, key) - - -def raw_bip32_privtopub(rawtuple): - vbytes, depth, fingerprint, i, chaincode, key = rawtuple - newvbytes = MAINNET_PUBLIC if vbytes == MAINNET_PRIVATE else TESTNET_PUBLIC - return (newvbytes, depth, fingerprint, i, chaincode, privtopub(key)) - - -def bip32_privtopub(data): - return bip32_serialize(raw_bip32_privtopub(bip32_deserialize(data))) - - -def bip32_ckd(data, i): - return bip32_serialize(raw_bip32_ckd(bip32_deserialize(data), i)) - - -def bip32_master_key(seed, vbytes=MAINNET_PRIVATE): - I = hmac.new(from_string_to_bytes("Bitcoin seed"), seed, hashlib.sha512).digest() - return bip32_serialize((vbytes, 0, b'\x00'*4, 0, I[32:], I[:32]+b'\x01')) - - -def bip32_bin_extract_key(data): - return bip32_deserialize(data)[-1] - - -def bip32_extract_key(data): - return safe_hexlify(bip32_deserialize(data)[-1]) - -# Exploits the same vulnerability as above in Electrum wallets -# Takes a BIP32 pubkey and one of the child privkeys of its corresponding -# privkey and returns the BIP32 privkey associated with that pubkey - - -def raw_crack_bip32_privkey(parent_pub, priv): - vbytes, depth, fingerprint, i, chaincode, key = priv - pvbytes, pdepth, pfingerprint, pi, pchaincode, pkey = parent_pub - i = int(i) - - if i >= 2**31: - raise Exception("Can't crack private derivation!") - - I = hmac.new(pchaincode, pkey+encode(i, 256, 4), hashlib.sha512).digest() - - pprivkey = subtract_privkeys(key, I[:32]+b'\x01') - - newvbytes = MAINNET_PRIVATE if vbytes == MAINNET_PUBLIC else TESTNET_PRIVATE - return (newvbytes, pdepth, pfingerprint, pi, pchaincode, pprivkey) - - -def crack_bip32_privkey(parent_pub, priv): - dsppub = bip32_deserialize(parent_pub) - dspriv = bip32_deserialize(priv) - return bip32_serialize(raw_crack_bip32_privkey(dsppub, dspriv)) - - -def coinvault_pub_to_bip32(*args): - if len(args) == 1: - args = args[0].split(' ') - vals = map(int, args[34:]) - I1 = ''.join(map(chr, vals[:33])) - I2 = ''.join(map(chr, vals[35:67])) - return bip32_serialize((MAINNET_PUBLIC, 0, b'\x00'*4, 0, I2, I1)) - - -def coinvault_priv_to_bip32(*args): - if len(args) == 1: - args = args[0].split(' ') - vals = map(int, args[34:]) - I2 = ''.join(map(chr, vals[35:67])) - I3 = ''.join(map(chr, vals[72:104])) - return bip32_serialize((MAINNET_PRIVATE, 0, b'\x00'*4, 0, I2, I3+b'\x01')) - - -def bip32_descend(*args): - if len(args) == 2 and isinstance(args[1], list): - key, path = args - else: - key, path = args[0], map(int, args[1:]) - for p in path: - key = bip32_ckd(key, p) - return bip32_extract_key(key) diff --git a/src/lib/pybitcointools/english.txt b/src/lib/pybitcointools/english.txt deleted file mode 100644 index 942040ed..00000000 --- a/src/lib/pybitcointools/english.txt +++ /dev/null @@ -1,2048 +0,0 @@ -abandon -ability -able -about -above -absent -absorb -abstract -absurd -abuse -access -accident -account -accuse -achieve -acid -acoustic -acquire -across -act -action -actor -actress -actual -adapt -add -addict -address -adjust -admit -adult -advance -advice -aerobic -affair -afford -afraid -again -age -agent -agree -ahead -aim -air -airport -aisle -alarm -album -alcohol -alert -alien -all -alley -allow -almost -alone -alpha -already -also -alter -always -amateur -amazing -among -amount -amused -analyst -anchor -ancient -anger -angle -angry -animal -ankle -announce -annual -another -answer -antenna -antique -anxiety -any -apart -apology -appear -apple -approve -april -arch -arctic -area -arena -argue -arm -armed -armor -army -around -arrange -arrest -arrive -arrow -art -artefact -artist -artwork -ask -aspect -assault -asset -assist -assume -asthma -athlete -atom -attack -attend -attitude -attract -auction -audit -august -aunt -author -auto -autumn -average -avocado -avoid -awake -aware -away -awesome -awful -awkward -axis -baby -bachelor -bacon -badge -bag -balance -balcony -ball -bamboo -banana -banner -bar -barely -bargain -barrel -base -basic -basket -battle -beach -bean -beauty -because -become -beef -before -begin -behave -behind -believe -below -belt -bench -benefit -best -betray -better -between -beyond -bicycle -bid -bike -bind -biology -bird -birth -bitter -black -blade -blame -blanket -blast -bleak -bless -blind -blood -blossom -blouse -blue -blur -blush -board -boat -body -boil -bomb -bone -bonus -book -boost -border -boring -borrow -boss -bottom -bounce -box -boy -bracket -brain -brand -brass -brave -bread -breeze -brick -bridge -brief -bright -bring -brisk -broccoli -broken -bronze -broom -brother -brown -brush -bubble -buddy -budget -buffalo -build -bulb -bulk -bullet -bundle -bunker -burden -burger -burst -bus -business -busy -butter -buyer -buzz -cabbage -cabin -cable -cactus -cage -cake -call -calm -camera -camp -can -canal -cancel -candy -cannon -canoe -canvas -canyon -capable -capital -captain -car -carbon -card -cargo -carpet -carry -cart -case -cash -casino -castle -casual -cat -catalog -catch -category -cattle -caught -cause -caution -cave -ceiling -celery -cement -census -century -cereal -certain -chair -chalk -champion -change -chaos -chapter -charge -chase -chat -cheap -check -cheese -chef -cherry -chest -chicken -chief -child -chimney -choice -choose -chronic -chuckle -chunk -churn -cigar -cinnamon -circle -citizen -city -civil -claim -clap -clarify -claw -clay -clean -clerk -clever -click -client -cliff -climb -clinic -clip -clock -clog -close -cloth -cloud -clown -club -clump -cluster -clutch -coach -coast -coconut -code -coffee -coil -coin -collect -color -column -combine -come -comfort -comic -common -company -concert -conduct -confirm -congress -connect -consider -control -convince -cook -cool -copper -copy -coral -core -corn -correct -cost -cotton -couch -country -couple -course -cousin -cover -coyote -crack -cradle -craft -cram -crane -crash -crater -crawl -crazy -cream -credit -creek -crew -cricket -crime -crisp -critic -crop -cross -crouch -crowd -crucial -cruel -cruise -crumble -crunch -crush -cry -crystal -cube -culture -cup -cupboard -curious -current -curtain -curve -cushion -custom -cute -cycle -dad -damage -damp -dance -danger -daring -dash -daughter -dawn -day -deal -debate -debris -decade -december -decide -decline -decorate -decrease -deer -defense -define -defy -degree -delay -deliver -demand -demise -denial -dentist -deny -depart -depend -deposit -depth -deputy -derive -describe -desert -design -desk -despair -destroy -detail -detect -develop -device -devote -diagram -dial -diamond -diary -dice -diesel -diet -differ -digital -dignity -dilemma -dinner -dinosaur -direct -dirt -disagree -discover -disease -dish -dismiss -disorder -display -distance -divert -divide -divorce -dizzy -doctor -document -dog -doll -dolphin -domain -donate -donkey -donor -door -dose -double -dove -draft -dragon -drama -drastic -draw -dream -dress -drift -drill -drink -drip -drive -drop -drum -dry -duck -dumb -dune -during -dust -dutch -duty -dwarf -dynamic -eager -eagle -early -earn -earth -easily -east -easy -echo -ecology -economy -edge -edit -educate -effort -egg -eight -either -elbow -elder -electric -elegant -element -elephant -elevator -elite -else -embark -embody -embrace -emerge -emotion -employ -empower -empty -enable -enact -end -endless -endorse -enemy -energy -enforce -engage -engine -enhance -enjoy -enlist -enough -enrich -enroll -ensure -enter -entire -entry -envelope -episode -equal -equip -era -erase -erode -erosion -error -erupt -escape -essay -essence -estate -eternal -ethics -evidence -evil -evoke -evolve -exact -example -excess -exchange -excite -exclude -excuse -execute -exercise -exhaust -exhibit -exile -exist -exit -exotic -expand -expect -expire -explain -expose -express -extend -extra -eye -eyebrow -fabric -face -faculty -fade -faint -faith -fall -false -fame -family -famous -fan -fancy -fantasy -farm -fashion -fat -fatal -father -fatigue -fault -favorite -feature -february -federal -fee -feed -feel -female -fence -festival -fetch -fever -few -fiber -fiction -field -figure -file -film -filter -final -find -fine -finger -finish -fire -firm -first -fiscal -fish -fit -fitness -fix -flag -flame -flash -flat -flavor -flee -flight -flip -float -flock -floor -flower -fluid -flush -fly -foam -focus -fog -foil -fold -follow -food -foot -force -forest -forget -fork -fortune -forum -forward -fossil -foster -found -fox -fragile -frame -frequent -fresh -friend -fringe -frog -front -frost -frown -frozen -fruit -fuel -fun -funny -furnace -fury -future -gadget -gain -galaxy -gallery -game -gap -garage -garbage -garden -garlic -garment -gas -gasp -gate -gather -gauge -gaze -general -genius -genre -gentle -genuine -gesture -ghost -giant -gift -giggle -ginger -giraffe -girl -give -glad -glance -glare -glass -glide -glimpse -globe -gloom -glory -glove -glow -glue -goat -goddess -gold -good -goose -gorilla -gospel -gossip -govern -gown -grab -grace -grain -grant -grape -grass -gravity -great -green -grid -grief -grit -grocery -group -grow -grunt -guard -guess -guide -guilt -guitar -gun -gym -habit -hair -half -hammer -hamster -hand -happy -harbor -hard -harsh -harvest -hat -have -hawk -hazard -head -health -heart -heavy -hedgehog -height -hello -helmet -help -hen -hero -hidden -high -hill -hint -hip -hire -history -hobby -hockey -hold -hole -holiday -hollow -home -honey -hood -hope -horn -horror -horse -hospital -host -hotel -hour -hover -hub -huge -human -humble -humor -hundred -hungry -hunt -hurdle -hurry -hurt -husband -hybrid -ice -icon -idea -identify -idle -ignore -ill -illegal -illness -image -imitate -immense -immune -impact -impose -improve -impulse -inch -include -income -increase -index -indicate -indoor -industry -infant -inflict -inform -inhale -inherit -initial -inject -injury -inmate -inner -innocent -input -inquiry -insane -insect -inside -inspire -install -intact -interest -into -invest -invite -involve -iron -island -isolate -issue -item -ivory -jacket -jaguar -jar -jazz -jealous -jeans -jelly -jewel -job -join -joke -journey -joy -judge -juice -jump -jungle -junior -junk -just -kangaroo -keen -keep -ketchup -key -kick -kid -kidney -kind -kingdom -kiss -kit -kitchen -kite -kitten -kiwi -knee -knife -knock -know -lab -label -labor -ladder -lady -lake -lamp -language -laptop -large -later -latin -laugh -laundry -lava -law -lawn -lawsuit -layer -lazy -leader -leaf -learn -leave -lecture -left -leg -legal -legend -leisure -lemon -lend -length -lens -leopard -lesson -letter -level -liar -liberty -library -license -life -lift -light -like -limb -limit -link -lion -liquid -list -little -live -lizard -load -loan -lobster -local -lock -logic -lonely -long -loop -lottery -loud -lounge -love -loyal -lucky -luggage -lumber -lunar -lunch -luxury -lyrics -machine -mad -magic -magnet -maid -mail -main -major -make -mammal -man -manage -mandate -mango -mansion -manual -maple -marble -march -margin -marine -market -marriage -mask -mass -master -match -material -math -matrix -matter -maximum -maze -meadow -mean -measure -meat -mechanic -medal -media -melody -melt -member -memory -mention -menu -mercy -merge -merit -merry -mesh -message -metal -method -middle -midnight -milk -million -mimic -mind -minimum -minor -minute -miracle -mirror -misery -miss -mistake -mix -mixed -mixture -mobile -model -modify -mom -moment -monitor -monkey -monster -month -moon -moral -more -morning -mosquito -mother -motion -motor -mountain -mouse -move -movie -much -muffin -mule -multiply -muscle -museum -mushroom -music -must -mutual -myself -mystery -myth -naive -name -napkin -narrow -nasty -nation -nature -near -neck -need -negative -neglect -neither -nephew -nerve -nest -net -network -neutral -never -news -next -nice -night -noble -noise -nominee -noodle -normal -north -nose -notable -note -nothing -notice -novel -now -nuclear -number -nurse -nut -oak -obey -object -oblige -obscure -observe -obtain -obvious -occur -ocean -october -odor -off -offer -office -often -oil -okay -old -olive -olympic -omit -once -one -onion -online -only -open -opera -opinion -oppose -option -orange -orbit -orchard -order -ordinary -organ -orient -original -orphan -ostrich -other -outdoor -outer -output -outside -oval -oven -over -own -owner -oxygen -oyster -ozone -pact -paddle -page -pair -palace -palm -panda -panel -panic -panther -paper -parade -parent -park -parrot -party -pass -patch -path -patient -patrol -pattern -pause -pave -payment -peace -peanut -pear -peasant -pelican -pen -penalty -pencil -people -pepper -perfect -permit -person -pet -phone -photo -phrase -physical -piano -picnic -picture -piece -pig -pigeon -pill -pilot -pink -pioneer -pipe -pistol -pitch -pizza -place -planet -plastic -plate -play -please -pledge -pluck -plug -plunge -poem -poet -point -polar -pole -police -pond -pony -pool -popular -portion -position -possible -post -potato -pottery -poverty -powder -power -practice -praise -predict -prefer -prepare -present -pretty -prevent -price -pride -primary -print -priority -prison -private -prize -problem -process -produce -profit -program -project -promote -proof -property -prosper -protect -proud -provide -public -pudding -pull -pulp -pulse -pumpkin -punch -pupil -puppy -purchase -purity -purpose -purse -push -put -puzzle -pyramid -quality -quantum -quarter -question -quick -quit -quiz -quote -rabbit -raccoon -race -rack -radar -radio -rail -rain -raise -rally -ramp -ranch -random -range -rapid -rare -rate -rather -raven -raw -razor -ready -real -reason -rebel -rebuild -recall -receive -recipe -record -recycle -reduce -reflect -reform -refuse -region -regret -regular -reject -relax -release -relief -rely -remain -remember -remind -remove -render -renew -rent -reopen -repair -repeat -replace -report -require -rescue -resemble -resist -resource -response -result -retire -retreat -return -reunion -reveal -review -reward -rhythm -rib -ribbon -rice -rich -ride -ridge -rifle -right -rigid -ring -riot -ripple -risk -ritual -rival -river -road -roast -robot -robust -rocket -romance -roof -rookie -room -rose -rotate -rough -round -route -royal -rubber -rude -rug -rule -run -runway -rural -sad -saddle -sadness -safe -sail -salad -salmon -salon -salt -salute -same -sample -sand -satisfy -satoshi -sauce -sausage -save -say -scale -scan -scare -scatter -scene -scheme -school -science -scissors -scorpion -scout -scrap -screen -script -scrub -sea -search -season -seat -second -secret -section -security -seed -seek -segment -select -sell -seminar -senior -sense -sentence -series -service -session -settle -setup -seven -shadow -shaft -shallow -share -shed -shell -sheriff -shield -shift -shine -ship -shiver -shock -shoe -shoot -shop -short -shoulder -shove -shrimp -shrug -shuffle -shy -sibling -sick -side -siege -sight -sign -silent -silk -silly -silver -similar -simple -since -sing -siren -sister -situate -six -size -skate -sketch -ski -skill -skin -skirt -skull -slab -slam -sleep -slender -slice -slide -slight -slim -slogan -slot -slow -slush -small -smart -smile -smoke -smooth -snack -snake -snap -sniff -snow -soap -soccer -social -sock -soda -soft -solar -soldier -solid -solution -solve -someone -song -soon -sorry -sort -soul -sound -soup -source -south -space -spare -spatial -spawn -speak -special -speed -spell -spend -sphere -spice -spider -spike -spin -spirit -split -spoil -sponsor -spoon -sport -spot -spray -spread -spring -spy -square -squeeze -squirrel -stable -stadium -staff -stage -stairs -stamp -stand -start -state -stay -steak -steel -stem -step -stereo -stick -still -sting -stock -stomach -stone -stool -story -stove -strategy -street -strike -strong -struggle -student -stuff -stumble -style -subject -submit -subway -success -such -sudden -suffer -sugar -suggest -suit -summer -sun -sunny -sunset -super -supply -supreme -sure -surface -surge -surprise -surround -survey -suspect -sustain -swallow -swamp -swap -swarm -swear -sweet -swift -swim -swing -switch -sword -symbol -symptom -syrup -system -table -tackle -tag -tail -talent -talk -tank -tape -target -task -taste -tattoo -taxi -teach -team -tell -ten -tenant -tennis -tent -term -test -text -thank -that -theme -then -theory -there -they -thing -this -thought -three -thrive -throw -thumb -thunder -ticket -tide -tiger -tilt -timber -time -tiny -tip -tired -tissue -title -toast -tobacco -today -toddler -toe -together -toilet -token -tomato -tomorrow -tone -tongue -tonight -tool -tooth -top -topic -topple -torch -tornado -tortoise -toss -total -tourist -toward -tower -town -toy -track -trade -traffic -tragic -train -transfer -trap -trash -travel -tray -treat -tree -trend -trial -tribe -trick -trigger -trim -trip -trophy -trouble -truck -true -truly -trumpet -trust -truth -try -tube -tuition -tumble -tuna -tunnel -turkey -turn -turtle -twelve -twenty -twice -twin -twist -two -type -typical -ugly -umbrella -unable -unaware -uncle -uncover -under -undo -unfair -unfold -unhappy -uniform -unique -unit -universe -unknown -unlock -until -unusual -unveil -update -upgrade -uphold -upon -upper -upset -urban -urge -usage -use -used -useful -useless -usual -utility -vacant -vacuum -vague -valid -valley -valve -van -vanish -vapor -various -vast -vault -vehicle -velvet -vendor -venture -venue -verb -verify -version -very -vessel -veteran -viable -vibrant -vicious -victory -video -view -village -vintage -violin -virtual -virus -visa -visit -visual -vital -vivid -vocal -voice -void -volcano -volume -vote -voyage -wage -wagon -wait -walk -wall -walnut -want -warfare -warm -warrior -wash -wasp -waste -water -wave -way -wealth -weapon -wear -weasel -weather -web -wedding -weekend -weird -welcome -west -wet -whale -what -wheat -wheel -when -where -whip -whisper -wide -width -wife -wild -will -win -window -wine -wing -wink -winner -winter -wire -wisdom -wise -wish -witness -wolf -woman -wonder -wood -wool -word -work -world -worry -worth -wrap -wreck -wrestle -wrist -write -wrong -yard -year -yellow -you -young -youth -zebra -zero -zone -zoo diff --git a/src/lib/pybitcointools/main.py b/src/lib/pybitcointools/main.py deleted file mode 100644 index 8cf3a9f7..00000000 --- a/src/lib/pybitcointools/main.py +++ /dev/null @@ -1,581 +0,0 @@ -#!/usr/bin/python -from .py2specials import * -from .py3specials import * -import binascii -import hashlib -import re -import sys -import os -import base64 -import time -import random -import hmac -from .ripemd import * - -# Elliptic curve parameters (secp256k1) - -P = 2**256 - 2**32 - 977 -N = 115792089237316195423570985008687907852837564279074904382605163141518161494337 -A = 0 -B = 7 -Gx = 55066263022277343669578718895168534326250603453777594175500187360389116729240 -Gy = 32670510020758816978083085130507043184471273380659243275938904335757337482424 -G = (Gx, Gy) - - -def change_curve(p, n, a, b, gx, gy): - global P, N, A, B, Gx, Gy, G - P, N, A, B, Gx, Gy = p, n, a, b, gx, gy - G = (Gx, Gy) - - -def getG(): - return G - -# Extended Euclidean Algorithm - - -def inv(a, n): - if a == 0: - return 0 - lm, hm = 1, 0 - low, high = a % n, n - while low > 1: - r = high//low - nm, new = hm-lm*r, high-low*r - lm, low, hm, high = nm, new, lm, low - return lm % n - - - -# JSON access (for pybtctool convenience) - - -def access(obj, prop): - if isinstance(obj, dict): - if prop in obj: - return obj[prop] - elif '.' in prop: - return obj[float(prop)] - else: - return obj[int(prop)] - else: - return obj[int(prop)] - - -def multiaccess(obj, prop): - return [access(o, prop) for o in obj] - - -def slice(obj, start=0, end=2**200): - return obj[int(start):int(end)] - - -def count(obj): - return len(obj) - -_sum = sum - - -def sum(obj): - return _sum(obj) - - -def isinf(p): - return p[0] == 0 and p[1] == 0 - - -def to_jacobian(p): - o = (p[0], p[1], 1) - return o - - -def jacobian_double(p): - if not p[1]: - return (0, 0, 0) - ysq = (p[1] ** 2) % P - S = (4 * p[0] * ysq) % P - M = (3 * p[0] ** 2 + A * p[2] ** 4) % P - nx = (M**2 - 2 * S) % P - ny = (M * (S - nx) - 8 * ysq ** 2) % P - nz = (2 * p[1] * p[2]) % P - return (nx, ny, nz) - - -def jacobian_add(p, q): - if not p[1]: - return q - if not q[1]: - return p - U1 = (p[0] * q[2] ** 2) % P - U2 = (q[0] * p[2] ** 2) % P - S1 = (p[1] * q[2] ** 3) % P - S2 = (q[1] * p[2] ** 3) % P - if U1 == U2: - if S1 != S2: - return (0, 0, 1) - return jacobian_double(p) - H = U2 - U1 - R = S2 - S1 - H2 = (H * H) % P - H3 = (H * H2) % P - U1H2 = (U1 * H2) % P - nx = (R ** 2 - H3 - 2 * U1H2) % P - ny = (R * (U1H2 - nx) - S1 * H3) % P - nz = (H * p[2] * q[2]) % P - return (nx, ny, nz) - - -def from_jacobian(p): - z = inv(p[2], P) - return ((p[0] * z**2) % P, (p[1] * z**3) % P) - - -def jacobian_multiply(a, n): - if a[1] == 0 or n == 0: - return (0, 0, 1) - if n == 1: - return a - if n < 0 or n >= N: - return jacobian_multiply(a, n % N) - if (n % 2) == 0: - return jacobian_double(jacobian_multiply(a, n//2)) - if (n % 2) == 1: - return jacobian_add(jacobian_double(jacobian_multiply(a, n//2)), a) - - -def fast_multiply(a, n): - return from_jacobian(jacobian_multiply(to_jacobian(a), n)) - - -def fast_add(a, b): - return from_jacobian(jacobian_add(to_jacobian(a), to_jacobian(b))) - -# Functions for handling pubkey and privkey formats - - -def get_pubkey_format(pub): - if is_python2: - two = '\x02' - three = '\x03' - four = '\x04' - else: - two = 2 - three = 3 - four = 4 - - if isinstance(pub, (tuple, list)): return 'decimal' - elif len(pub) == 65 and pub[0] == four: return 'bin' - elif len(pub) == 130 and pub[0:2] == '04': return 'hex' - elif len(pub) == 33 and pub[0] in [two, three]: return 'bin_compressed' - elif len(pub) == 66 and pub[0:2] in ['02', '03']: return 'hex_compressed' - elif len(pub) == 64: return 'bin_electrum' - elif len(pub) == 128: return 'hex_electrum' - else: raise Exception("Pubkey not in recognized format") - - -def encode_pubkey(pub, formt): - if not isinstance(pub, (tuple, list)): - pub = decode_pubkey(pub) - if formt == 'decimal': return pub - elif formt == 'bin': return b'\x04' + encode(pub[0], 256, 32) + encode(pub[1], 256, 32) - elif formt == 'bin_compressed': - return from_int_to_byte(2+(pub[1] % 2)) + encode(pub[0], 256, 32) - elif formt == 'hex': return '04' + encode(pub[0], 16, 64) + encode(pub[1], 16, 64) - elif formt == 'hex_compressed': - return '0'+str(2+(pub[1] % 2)) + encode(pub[0], 16, 64) - elif formt == 'bin_electrum': return encode(pub[0], 256, 32) + encode(pub[1], 256, 32) - elif formt == 'hex_electrum': return encode(pub[0], 16, 64) + encode(pub[1], 16, 64) - else: raise Exception("Invalid format!") - - -def decode_pubkey(pub, formt=None): - if not formt: formt = get_pubkey_format(pub) - if formt == 'decimal': return pub - elif formt == 'bin': return (decode(pub[1:33], 256), decode(pub[33:65], 256)) - elif formt == 'bin_compressed': - x = decode(pub[1:33], 256) - beta = pow(int(x*x*x+A*x+B), int((P+1)//4), int(P)) - y = (P-beta) if ((beta + from_byte_to_int(pub[0])) % 2) else beta - return (x, y) - elif formt == 'hex': return (decode(pub[2:66], 16), decode(pub[66:130], 16)) - elif formt == 'hex_compressed': - return decode_pubkey(safe_from_hex(pub), 'bin_compressed') - elif formt == 'bin_electrum': - return (decode(pub[:32], 256), decode(pub[32:64], 256)) - elif formt == 'hex_electrum': - return (decode(pub[:64], 16), decode(pub[64:128], 16)) - else: raise Exception("Invalid format!") - -def get_privkey_format(priv): - if isinstance(priv, int_types): return 'decimal' - elif len(priv) == 32: return 'bin' - elif len(priv) == 33: return 'bin_compressed' - elif len(priv) == 64: return 'hex' - elif len(priv) == 66: return 'hex_compressed' - else: - bin_p = b58check_to_bin(priv) - if len(bin_p) == 32: return 'wif' - elif len(bin_p) == 33: return 'wif_compressed' - else: raise Exception("WIF does not represent privkey") - -def encode_privkey(priv, formt, vbyte=0): - if not isinstance(priv, int_types): - return encode_privkey(decode_privkey(priv), formt, vbyte) - if formt == 'decimal': return priv - elif formt == 'bin': return encode(priv, 256, 32) - elif formt == 'bin_compressed': return encode(priv, 256, 32)+b'\x01' - elif formt == 'hex': return encode(priv, 16, 64) - elif formt == 'hex_compressed': return encode(priv, 16, 64)+'01' - elif formt == 'wif': - return bin_to_b58check(encode(priv, 256, 32), 128+int(vbyte)) - elif formt == 'wif_compressed': - return bin_to_b58check(encode(priv, 256, 32)+b'\x01', 128+int(vbyte)) - else: raise Exception("Invalid format!") - -def decode_privkey(priv,formt=None): - if not formt: formt = get_privkey_format(priv) - if formt == 'decimal': return priv - elif formt == 'bin': return decode(priv, 256) - elif formt == 'bin_compressed': return decode(priv[:32], 256) - elif formt == 'hex': return decode(priv, 16) - elif formt == 'hex_compressed': return decode(priv[:64], 16) - elif formt == 'wif': return decode(b58check_to_bin(priv),256) - elif formt == 'wif_compressed': - return decode(b58check_to_bin(priv)[:32],256) - else: raise Exception("WIF does not represent privkey") - -def add_pubkeys(p1, p2): - f1, f2 = get_pubkey_format(p1), get_pubkey_format(p2) - return encode_pubkey(fast_add(decode_pubkey(p1, f1), decode_pubkey(p2, f2)), f1) - -def add_privkeys(p1, p2): - f1, f2 = get_privkey_format(p1), get_privkey_format(p2) - return encode_privkey((decode_privkey(p1, f1) + decode_privkey(p2, f2)) % N, f1) - -def mul_privkeys(p1, p2): - f1, f2 = get_privkey_format(p1), get_privkey_format(p2) - return encode_privkey((decode_privkey(p1, f1) * decode_privkey(p2, f2)) % N, f1) - -def multiply(pubkey, privkey): - f1, f2 = get_pubkey_format(pubkey), get_privkey_format(privkey) - pubkey, privkey = decode_pubkey(pubkey, f1), decode_privkey(privkey, f2) - # http://safecurves.cr.yp.to/twist.html - if not isinf(pubkey) and (pubkey[0]**3+B-pubkey[1]*pubkey[1]) % P != 0: - raise Exception("Point not on curve") - return encode_pubkey(fast_multiply(pubkey, privkey), f1) - - -def divide(pubkey, privkey): - factor = inv(decode_privkey(privkey), N) - return multiply(pubkey, factor) - - -def compress(pubkey): - f = get_pubkey_format(pubkey) - if 'compressed' in f: return pubkey - elif f == 'bin': return encode_pubkey(decode_pubkey(pubkey, f), 'bin_compressed') - elif f == 'hex' or f == 'decimal': - return encode_pubkey(decode_pubkey(pubkey, f), 'hex_compressed') - - -def decompress(pubkey): - f = get_pubkey_format(pubkey) - if 'compressed' not in f: return pubkey - elif f == 'bin_compressed': return encode_pubkey(decode_pubkey(pubkey, f), 'bin') - elif f == 'hex_compressed' or f == 'decimal': - return encode_pubkey(decode_pubkey(pubkey, f), 'hex') - - -def privkey_to_pubkey(privkey): - f = get_privkey_format(privkey) - privkey = decode_privkey(privkey, f) - if privkey >= N: - raise Exception("Invalid privkey") - if f in ['bin', 'bin_compressed', 'hex', 'hex_compressed', 'decimal']: - return encode_pubkey(fast_multiply(G, privkey), f) - else: - return encode_pubkey(fast_multiply(G, privkey), f.replace('wif', 'hex')) - -privtopub = privkey_to_pubkey - - -def privkey_to_address(priv, magicbyte=0): - return pubkey_to_address(privkey_to_pubkey(priv), magicbyte) -privtoaddr = privkey_to_address - - -def neg_pubkey(pubkey): - f = get_pubkey_format(pubkey) - pubkey = decode_pubkey(pubkey, f) - return encode_pubkey((pubkey[0], (P-pubkey[1]) % P), f) - - -def neg_privkey(privkey): - f = get_privkey_format(privkey) - privkey = decode_privkey(privkey, f) - return encode_privkey((N - privkey) % N, f) - -def subtract_pubkeys(p1, p2): - f1, f2 = get_pubkey_format(p1), get_pubkey_format(p2) - k2 = decode_pubkey(p2, f2) - return encode_pubkey(fast_add(decode_pubkey(p1, f1), (k2[0], (P - k2[1]) % P)), f1) - - -def subtract_privkeys(p1, p2): - f1, f2 = get_privkey_format(p1), get_privkey_format(p2) - k2 = decode_privkey(p2, f2) - return encode_privkey((decode_privkey(p1, f1) - k2) % N, f1) - -# Hashes - - -def bin_hash160(string): - intermed = hashlib.sha256(string).digest() - digest = '' - try: - digest = hashlib.new('ripemd160', intermed).digest() - except: - digest = RIPEMD160(intermed).digest() - return digest - - -def hash160(string): - return safe_hexlify(bin_hash160(string)) - - -def bin_sha256(string): - binary_data = string if isinstance(string, bytes) else bytes(string, 'utf-8') - return hashlib.sha256(binary_data).digest() - -def sha256(string): - return bytes_to_hex_string(bin_sha256(string)) - - -def bin_ripemd160(string): - try: - digest = hashlib.new('ripemd160', string).digest() - except: - digest = RIPEMD160(string).digest() - return digest - - -def ripemd160(string): - return safe_hexlify(bin_ripemd160(string)) - - -def bin_dbl_sha256(s): - bytes_to_hash = from_string_to_bytes(s) - return hashlib.sha256(hashlib.sha256(bytes_to_hash).digest()).digest() - - -def dbl_sha256(string): - return safe_hexlify(bin_dbl_sha256(string)) - - -def bin_slowsha(string): - string = from_string_to_bytes(string) - orig_input = string - for i in range(100000): - string = hashlib.sha256(string + orig_input).digest() - return string - - -def slowsha(string): - return safe_hexlify(bin_slowsha(string)) - - -def hash_to_int(x): - if len(x) in [40, 64]: - return decode(x, 16) - return decode(x, 256) - - -def num_to_var_int(x): - x = int(x) - if x < 253: return from_int_to_byte(x) - elif x < 65536: return from_int_to_byte(253)+encode(x, 256, 2)[::-1] - elif x < 4294967296: return from_int_to_byte(254) + encode(x, 256, 4)[::-1] - else: return from_int_to_byte(255) + encode(x, 256, 8)[::-1] - - -# WTF, Electrum? -def electrum_sig_hash(message): - padded = b"\x18Bitcoin Signed Message:\n" + num_to_var_int(len(message)) + from_string_to_bytes(message) - return bin_dbl_sha256(padded) - - -def random_key(): - # Gotta be secure after that java.SecureRandom fiasco... - entropy = random_string(32) \ - + str(random.randrange(2**256)) \ - + str(int(time.time() * 1000000)) - return sha256(entropy) - - -def random_electrum_seed(): - entropy = os.urandom(32) \ - + str(random.randrange(2**256)) \ - + str(int(time.time() * 1000000)) - return sha256(entropy)[:32] - -# Encodings - -def b58check_to_bin(inp): - leadingzbytes = len(re.match('^1*', inp).group(0)) - data = b'\x00' * leadingzbytes + changebase(inp, 58, 256) - assert bin_dbl_sha256(data[:-4])[:4] == data[-4:] - return data[1:-4] - - -def get_version_byte(inp): - leadingzbytes = len(re.match('^1*', inp).group(0)) - data = b'\x00' * leadingzbytes + changebase(inp, 58, 256) - assert bin_dbl_sha256(data[:-4])[:4] == data[-4:] - return ord(data[0]) - - -def hex_to_b58check(inp, magicbyte=0): - return bin_to_b58check(binascii.unhexlify(inp), magicbyte) - - -def b58check_to_hex(inp): - return safe_hexlify(b58check_to_bin(inp)) - - -def pubkey_to_address(pubkey, magicbyte=0): - if isinstance(pubkey, (list, tuple)): - pubkey = encode_pubkey(pubkey, 'bin') - if len(pubkey) in [66, 130]: - return bin_to_b58check( - bin_hash160(binascii.unhexlify(pubkey)), magicbyte) - return bin_to_b58check(bin_hash160(pubkey), magicbyte) - -pubtoaddr = pubkey_to_address - - -def is_privkey(priv): - try: - get_privkey_format(priv) - return True - except: - return False - -def is_pubkey(pubkey): - try: - get_pubkey_format(pubkey) - return True - except: - return False - -def is_address(addr): - ADDR_RE = re.compile("^[123mn][a-km-zA-HJ-NP-Z0-9]{26,33}$") - return bool(ADDR_RE.match(addr)) - - -# EDCSA - - -def encode_sig(v, r, s): - vb, rb, sb = from_int_to_byte(v), encode(r, 256), encode(s, 256) - - result = base64.b64encode(vb+b'\x00'*(32-len(rb))+rb+b'\x00'*(32-len(sb))+sb) - return result if is_python2 else str(result, 'utf-8') - - -def decode_sig(sig): - bytez = base64.b64decode(sig) - return from_byte_to_int(bytez[0]), decode(bytez[1:33], 256), decode(bytez[33:], 256) - -# https://tools.ietf.org/html/rfc6979#section-3.2 - - -def deterministic_generate_k(msghash, priv): - v = b'\x01' * 32 - k = b'\x00' * 32 - priv = encode_privkey(priv, 'bin') - msghash = encode(hash_to_int(msghash), 256, 32) - k = hmac.new(k, v+b'\x00'+priv+msghash, hashlib.sha256).digest() - v = hmac.new(k, v, hashlib.sha256).digest() - k = hmac.new(k, v+b'\x01'+priv+msghash, hashlib.sha256).digest() - v = hmac.new(k, v, hashlib.sha256).digest() - return decode(hmac.new(k, v, hashlib.sha256).digest(), 256) - - -def ecdsa_raw_sign(msghash, priv): - - z = hash_to_int(msghash) - k = deterministic_generate_k(msghash, priv) - - r, y = fast_multiply(G, k) - s = inv(k, N) * (z + r*decode_privkey(priv)) % N - - v, r, s = 27+((y % 2) ^ (0 if s * 2 < N else 1)), r, s if s * 2 < N else N - s - if 'compressed' in get_privkey_format(priv): - v += 4 - return v, r, s - - -def ecdsa_sign(msg, priv): - v, r, s = ecdsa_raw_sign(electrum_sig_hash(msg), priv) - sig = encode_sig(v, r, s) - assert ecdsa_verify(msg, sig, - privtopub(priv)), "Bad Sig!\t %s\nv = %d\n,r = %d\ns = %d" % (sig, v, r, s) - return sig - - -def ecdsa_raw_verify(msghash, vrs, pub): - v, r, s = vrs - if not (27 <= v <= 34): - return False - - w = inv(s, N) - z = hash_to_int(msghash) - - u1, u2 = z*w % N, r*w % N - x, y = fast_add(fast_multiply(G, u1), fast_multiply(decode_pubkey(pub), u2)) - return bool(r == x and (r % N) and (s % N)) - - -# For BitcoinCore, (msg = addr or msg = "") be default -def ecdsa_verify_addr(msg, sig, addr): - assert is_address(addr) - Q = ecdsa_recover(msg, sig) - magic = get_version_byte(addr) - return (addr == pubtoaddr(Q, int(magic))) or (addr == pubtoaddr(compress(Q), int(magic))) - - -def ecdsa_verify(msg, sig, pub): - if is_address(pub): - return ecdsa_verify_addr(msg, sig, pub) - return ecdsa_raw_verify(electrum_sig_hash(msg), decode_sig(sig), pub) - - -def ecdsa_raw_recover(msghash, vrs): - v, r, s = vrs - if not (27 <= v <= 34): - raise ValueError("%d must in range 27-31" % v) - x = r - xcubedaxb = (x*x*x+A*x+B) % P - beta = pow(xcubedaxb, (P+1)//4, P) - y = beta if v % 2 ^ beta % 2 else (P - beta) - # If xcubedaxb is not a quadratic residue, then r cannot be the x coord - # for a point on the curve, and so the sig is invalid - if (xcubedaxb - y*y) % P != 0 or not (r % N) or not (s % N): - return False - z = hash_to_int(msghash) - Gz = jacobian_multiply((Gx, Gy, 1), (N - z) % N) - XY = jacobian_multiply((x, y, 1), s) - Qr = jacobian_add(Gz, XY) - Q = jacobian_multiply(Qr, inv(r, N)) - Q = from_jacobian(Q) - - # if ecdsa_raw_verify(msghash, vrs, Q): - return Q - # return False - - -def ecdsa_recover(msg, sig): - v,r,s = decode_sig(sig) - Q = ecdsa_raw_recover(electrum_sig_hash(msg), (v,r,s)) - return encode_pubkey(Q, 'hex_compressed') if v >= 31 else encode_pubkey(Q, 'hex') diff --git a/src/lib/pybitcointools/mnemonic.py b/src/lib/pybitcointools/mnemonic.py deleted file mode 100644 index a9df3617..00000000 --- a/src/lib/pybitcointools/mnemonic.py +++ /dev/null @@ -1,127 +0,0 @@ -import hashlib -import os.path -import binascii -import random -from bisect import bisect_left - -wordlist_english=list(open(os.path.join(os.path.dirname(os.path.realpath(__file__)),'english.txt'),'r')) - -def eint_to_bytes(entint,entbits): - a=hex(entint)[2:].rstrip('L').zfill(32) - print(a) - return binascii.unhexlify(a) - -def mnemonic_int_to_words(mint,mint_num_words,wordlist=wordlist_english): - backwords=[wordlist[(mint >> (11*x)) & 0x7FF].strip() for x in range(mint_num_words)] - return backwords[::-1] - -def entropy_cs(entbytes): - entropy_size=8*len(entbytes) - checksum_size=entropy_size//32 - hd=hashlib.sha256(entbytes).hexdigest() - csint=int(hd,16) >> (256-checksum_size) - return csint,checksum_size - -def entropy_to_words(entbytes,wordlist=wordlist_english): - if(len(entbytes) < 4 or len(entbytes) % 4 != 0): - raise ValueError("The size of the entropy must be a multiple of 4 bytes (multiple of 32 bits)") - entropy_size=8*len(entbytes) - csint,checksum_size = entropy_cs(entbytes) - entint=int(binascii.hexlify(entbytes),16) - mint=(entint << checksum_size) | csint - mint_num_words=(entropy_size+checksum_size)//11 - - return mnemonic_int_to_words(mint,mint_num_words,wordlist) - -def words_bisect(word,wordlist=wordlist_english): - lo=bisect_left(wordlist,word) - hi=len(wordlist)-bisect_left(wordlist[:lo:-1],word) - - return lo,hi - -def words_split(wordstr,wordlist=wordlist_english): - def popword(wordstr,wordlist): - for fwl in range(1,9): - w=wordstr[:fwl].strip() - lo,hi=words_bisect(w,wordlist) - if(hi-lo == 1): - return w,wordstr[fwl:].lstrip() - wordlist=wordlist[lo:hi] - raise Exception("Wordstr %s not found in list" %(w)) - - words=[] - tail=wordstr - while(len(tail)): - head,tail=popword(tail,wordlist) - words.append(head) - return words - -def words_to_mnemonic_int(words,wordlist=wordlist_english): - if(isinstance(words,str)): - words=words_split(words,wordlist) - return sum([wordlist.index(w) << (11*x) for x,w in enumerate(words[::-1])]) - -def words_verify(words,wordlist=wordlist_english): - if(isinstance(words,str)): - words=words_split(words,wordlist) - - mint = words_to_mnemonic_int(words,wordlist) - mint_bits=len(words)*11 - cs_bits=mint_bits//32 - entropy_bits=mint_bits-cs_bits - eint=mint >> cs_bits - csint=mint & ((1 << cs_bits)-1) - ebytes=_eint_to_bytes(eint,entropy_bits) - return csint == entropy_cs(ebytes) - -def mnemonic_to_seed(mnemonic_phrase,passphrase=b''): - try: - from hashlib import pbkdf2_hmac - def pbkdf2_hmac_sha256(password,salt,iters=2048): - return pbkdf2_hmac(hash_name='sha512',password=password,salt=salt,iterations=iters) - except: - try: - from Crypto.Protocol.KDF import PBKDF2 - from Crypto.Hash import SHA512,HMAC - - def pbkdf2_hmac_sha256(password,salt,iters=2048): - return PBKDF2(password=password,salt=salt,dkLen=64,count=iters,prf=lambda p,s: HMAC.new(p,s,SHA512).digest()) - except: - try: - - from pbkdf2 import PBKDF2 - import hmac - def pbkdf2_hmac_sha256(password,salt,iters=2048): - return PBKDF2(password,salt, iterations=iters, macmodule=hmac, digestmodule=hashlib.sha512).read(64) - except: - raise RuntimeError("No implementation of pbkdf2 was found!") - - return pbkdf2_hmac_sha256(password=mnemonic_phrase,salt=b'mnemonic'+passphrase) - -def words_mine(prefix,entbits,satisfunction,wordlist=wordlist_english,randombits=random.getrandbits): - prefix_bits=len(prefix)*11 - mine_bits=entbits-prefix_bits - pint=words_to_mnemonic_int(prefix,wordlist) - pint<<=mine_bits - dint=randombits(mine_bits) - count=0 - while(not satisfunction(entropy_to_words(eint_to_bytes(pint+dint,entbits)))): - dint=randombits(mine_bits) - if((count & 0xFFFF) == 0): - print("Searched %f percent of the space" % (float(count)/float(1 << mine_bits))) - - return entropy_to_words(eint_to_bytes(pint+dint,entbits)) - -if __name__=="__main__": - import json - testvectors=json.load(open('vectors.json','r')) - passed=True - for v in testvectors['english']: - ebytes=binascii.unhexlify(v[0]) - w=' '.join(entropy_to_words(ebytes)) - seed=mnemonic_to_seed(w,passphrase='TREZOR') - passed = passed and w==v[1] - passed = passed and binascii.hexlify(seed)==v[2] - print("Tests %s." % ("Passed" if passed else "Failed")) - - diff --git a/src/lib/pybitcointools/py2specials.py b/src/lib/pybitcointools/py2specials.py deleted file mode 100644 index 337154f3..00000000 --- a/src/lib/pybitcointools/py2specials.py +++ /dev/null @@ -1,98 +0,0 @@ -import sys, re -import binascii -import os -import hashlib - - -if sys.version_info.major == 2: - string_types = (str, unicode) - string_or_bytes_types = string_types - int_types = (int, float, long) - - # Base switching - code_strings = { - 2: '01', - 10: '0123456789', - 16: '0123456789abcdef', - 32: 'abcdefghijklmnopqrstuvwxyz234567', - 58: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz', - 256: ''.join([chr(x) for x in range(256)]) - } - - def bin_dbl_sha256(s): - bytes_to_hash = from_string_to_bytes(s) - return hashlib.sha256(hashlib.sha256(bytes_to_hash).digest()).digest() - - def lpad(msg, symbol, length): - if len(msg) >= length: - return msg - return symbol * (length - len(msg)) + msg - - def get_code_string(base): - if base in code_strings: - return code_strings[base] - else: - raise ValueError("Invalid base!") - - def changebase(string, frm, to, minlen=0): - if frm == to: - return lpad(string, get_code_string(frm)[0], minlen) - return encode(decode(string, frm), to, minlen) - - def bin_to_b58check(inp, magicbyte=0): - if magicbyte == 0: - inp = '\x00' + inp - while magicbyte > 0: - inp = chr(int(magicbyte % 256)) + inp - magicbyte //= 256 - leadingzbytes = len(re.match('^\x00*', inp).group(0)) - checksum = bin_dbl_sha256(inp)[:4] - return '1' * leadingzbytes + changebase(inp+checksum, 256, 58) - - def bytes_to_hex_string(b): - return b.encode('hex') - - def safe_from_hex(s): - return s.decode('hex') - - def from_int_representation_to_bytes(a): - return str(a) - - def from_int_to_byte(a): - return chr(a) - - def from_byte_to_int(a): - return ord(a) - - def from_bytes_to_string(s): - return s - - def from_string_to_bytes(a): - return a - - def safe_hexlify(a): - return binascii.hexlify(a) - - def encode(val, base, minlen=0): - base, minlen = int(base), int(minlen) - code_string = get_code_string(base) - result = "" - while val > 0: - result = code_string[val % base] + result - val //= base - return code_string[0] * max(minlen - len(result), 0) + result - - def decode(string, base): - base = int(base) - code_string = get_code_string(base) - result = 0 - if base == 16: - string = string.lower() - while len(string) > 0: - result *= base - result += code_string.find(string[0]) - string = string[1:] - return result - - def random_string(x): - return os.urandom(x) diff --git a/src/lib/pybitcointools/py3specials.py b/src/lib/pybitcointools/py3specials.py deleted file mode 100644 index 7593b9a6..00000000 --- a/src/lib/pybitcointools/py3specials.py +++ /dev/null @@ -1,123 +0,0 @@ -import sys, os -import binascii -import hashlib - - -if sys.version_info.major == 3: - string_types = (str) - string_or_bytes_types = (str, bytes) - int_types = (int, float) - # Base switching - code_strings = { - 2: '01', - 10: '0123456789', - 16: '0123456789abcdef', - 32: 'abcdefghijklmnopqrstuvwxyz234567', - 58: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz', - 256: ''.join([chr(x) for x in range(256)]) - } - - def bin_dbl_sha256(s): - bytes_to_hash = from_string_to_bytes(s) - return hashlib.sha256(hashlib.sha256(bytes_to_hash).digest()).digest() - - def lpad(msg, symbol, length): - if len(msg) >= length: - return msg - return symbol * (length - len(msg)) + msg - - def get_code_string(base): - if base in code_strings: - return code_strings[base] - else: - raise ValueError("Invalid base!") - - def changebase(string, frm, to, minlen=0): - if frm == to: - return lpad(string, get_code_string(frm)[0], minlen) - return encode(decode(string, frm), to, minlen) - - def bin_to_b58check(inp, magicbyte=0): - if magicbyte == 0: - inp = from_int_to_byte(0) + inp - while magicbyte > 0: - inp = from_int_to_byte(magicbyte % 256) + inp - magicbyte //= 256 - - leadingzbytes = 0 - for x in inp: - if x != 0: - break - leadingzbytes += 1 - - checksum = bin_dbl_sha256(inp)[:4] - return '1' * leadingzbytes + changebase(inp+checksum, 256, 58) - - def bytes_to_hex_string(b): - if isinstance(b, str): - return b - - return ''.join('{:02x}'.format(y) for y in b) - - def safe_from_hex(s): - return bytes.fromhex(s) - - def from_int_representation_to_bytes(a): - return bytes(str(a), 'utf-8') - - def from_int_to_byte(a): - return bytes([a]) - - def from_byte_to_int(a): - return a - - def from_string_to_bytes(a): - return a if isinstance(a, bytes) else bytes(a, 'utf-8') - - def safe_hexlify(a): - return str(binascii.hexlify(a), 'utf-8') - - def encode(val, base, minlen=0): - base, minlen = int(base), int(minlen) - code_string = get_code_string(base) - result_bytes = bytes() - while val > 0: - curcode = code_string[val % base] - result_bytes = bytes([ord(curcode)]) + result_bytes - val //= base - - pad_size = minlen - len(result_bytes) - - padding_element = b'\x00' if base == 256 else b'1' \ - if base == 58 else b'0' - if (pad_size > 0): - result_bytes = padding_element*pad_size + result_bytes - - result_string = ''.join([chr(y) for y in result_bytes]) - result = result_bytes if base == 256 else result_string - - return result - - def decode(string, base): - if base == 256 and isinstance(string, str): - string = bytes(bytearray.fromhex(string)) - base = int(base) - code_string = get_code_string(base) - result = 0 - if base == 256: - def extract(d, cs): - return d - else: - def extract(d, cs): - return cs.find(d if isinstance(d, str) else chr(d)) - - if base == 16: - string = string.lower() - while len(string) > 0: - result *= base - result += extract(string[0], code_string) - string = string[1:] - return result - - def random_string(x): - return str(os.urandom(x)) diff --git a/src/lib/pybitcointools/ripemd.py b/src/lib/pybitcointools/ripemd.py deleted file mode 100644 index 4b0c6045..00000000 --- a/src/lib/pybitcointools/ripemd.py +++ /dev/null @@ -1,414 +0,0 @@ -## ripemd.py - pure Python implementation of the RIPEMD-160 algorithm. -## Bjorn Edstrom 16 december 2007. -## -## Copyrights -## ========== -## -## This code is a derived from an implementation by Markus Friedl which is -## subject to the following license. This Python implementation is not -## subject to any other license. -## -##/* -## * Copyright (c) 2001 Markus Friedl. All rights reserved. -## * -## * Redistribution and use in source and binary forms, with or without -## * modification, are permitted provided that the following conditions -## * are met: -## * 1. Redistributions of source code must retain the above copyright -## * notice, this list of conditions and the following disclaimer. -## * 2. Redistributions in binary form must reproduce the above copyright -## * notice, this list of conditions and the following disclaimer in the -## * documentation and/or other materials provided with the distribution. -## * -## * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR -## * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -## * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -## * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, -## * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -## * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -## * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -## * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -## * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -## * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -## */ -##/* -## * Preneel, Bosselaers, Dobbertin, "The Cryptographic Hash Function RIPEMD-160", -## * RSA Laboratories, CryptoBytes, Volume 3, Number 2, Autumn 1997, -## * ftp://ftp.rsasecurity.com/pub/cryptobytes/crypto3n2.pdf -## */ - -try: - import psyco - psyco.full() -except ImportError: - pass - -import sys - -is_python2 = sys.version_info.major == 2 -#block_size = 1 -digest_size = 20 -digestsize = 20 - -try: - range = xrange -except: - pass - -class RIPEMD160: - """Return a new RIPEMD160 object. An optional string argument - may be provided; if present, this string will be automatically - hashed.""" - - def __init__(self, arg=None): - self.ctx = RMDContext() - if arg: - self.update(arg) - self.dig = None - - def update(self, arg): - """update(arg)""" - RMD160Update(self.ctx, arg, len(arg)) - self.dig = None - - def digest(self): - """digest()""" - if self.dig: - return self.dig - ctx = self.ctx.copy() - self.dig = RMD160Final(self.ctx) - self.ctx = ctx - return self.dig - - def hexdigest(self): - """hexdigest()""" - dig = self.digest() - hex_digest = '' - for d in dig: - if (is_python2): - hex_digest += '%02x' % ord(d) - else: - hex_digest += '%02x' % d - return hex_digest - - def copy(self): - """copy()""" - import copy - return copy.deepcopy(self) - - - -def new(arg=None): - """Return a new RIPEMD160 object. An optional string argument - may be provided; if present, this string will be automatically - hashed.""" - return RIPEMD160(arg) - - - -# -# Private. -# - -class RMDContext: - def __init__(self): - self.state = [0x67452301, 0xEFCDAB89, 0x98BADCFE, - 0x10325476, 0xC3D2E1F0] # uint32 - self.count = 0 # uint64 - self.buffer = [0]*64 # uchar - def copy(self): - ctx = RMDContext() - ctx.state = self.state[:] - ctx.count = self.count - ctx.buffer = self.buffer[:] - return ctx - -K0 = 0x00000000 -K1 = 0x5A827999 -K2 = 0x6ED9EBA1 -K3 = 0x8F1BBCDC -K4 = 0xA953FD4E - -KK0 = 0x50A28BE6 -KK1 = 0x5C4DD124 -KK2 = 0x6D703EF3 -KK3 = 0x7A6D76E9 -KK4 = 0x00000000 - -def ROL(n, x): - return ((x << n) & 0xffffffff) | (x >> (32 - n)) - -def F0(x, y, z): - return x ^ y ^ z - -def F1(x, y, z): - return (x & y) | (((~x) % 0x100000000) & z) - -def F2(x, y, z): - return (x | ((~y) % 0x100000000)) ^ z - -def F3(x, y, z): - return (x & z) | (((~z) % 0x100000000) & y) - -def F4(x, y, z): - return x ^ (y | ((~z) % 0x100000000)) - -def R(a, b, c, d, e, Fj, Kj, sj, rj, X): - a = ROL(sj, (a + Fj(b, c, d) + X[rj] + Kj) % 0x100000000) + e - c = ROL(10, c) - return a % 0x100000000, c - -PADDING = [0x80] + [0]*63 - -import sys -import struct - -def RMD160Transform(state, block): #uint32 state[5], uchar block[64] - x = [0]*16 - if sys.byteorder == 'little': - if is_python2: - x = struct.unpack('<16L', ''.join([chr(x) for x in block[0:64]])) - else: - x = struct.unpack('<16L', bytes(block[0:64])) - else: - raise "Error!!" - a = state[0] - b = state[1] - c = state[2] - d = state[3] - e = state[4] - - #/* Round 1 */ - a, c = R(a, b, c, d, e, F0, K0, 11, 0, x); - e, b = R(e, a, b, c, d, F0, K0, 14, 1, x); - d, a = R(d, e, a, b, c, F0, K0, 15, 2, x); - c, e = R(c, d, e, a, b, F0, K0, 12, 3, x); - b, d = R(b, c, d, e, a, F0, K0, 5, 4, x); - a, c = R(a, b, c, d, e, F0, K0, 8, 5, x); - e, b = R(e, a, b, c, d, F0, K0, 7, 6, x); - d, a = R(d, e, a, b, c, F0, K0, 9, 7, x); - c, e = R(c, d, e, a, b, F0, K0, 11, 8, x); - b, d = R(b, c, d, e, a, F0, K0, 13, 9, x); - a, c = R(a, b, c, d, e, F0, K0, 14, 10, x); - e, b = R(e, a, b, c, d, F0, K0, 15, 11, x); - d, a = R(d, e, a, b, c, F0, K0, 6, 12, x); - c, e = R(c, d, e, a, b, F0, K0, 7, 13, x); - b, d = R(b, c, d, e, a, F0, K0, 9, 14, x); - a, c = R(a, b, c, d, e, F0, K0, 8, 15, x); #/* #15 */ - #/* Round 2 */ - e, b = R(e, a, b, c, d, F1, K1, 7, 7, x); - d, a = R(d, e, a, b, c, F1, K1, 6, 4, x); - c, e = R(c, d, e, a, b, F1, K1, 8, 13, x); - b, d = R(b, c, d, e, a, F1, K1, 13, 1, x); - a, c = R(a, b, c, d, e, F1, K1, 11, 10, x); - e, b = R(e, a, b, c, d, F1, K1, 9, 6, x); - d, a = R(d, e, a, b, c, F1, K1, 7, 15, x); - c, e = R(c, d, e, a, b, F1, K1, 15, 3, x); - b, d = R(b, c, d, e, a, F1, K1, 7, 12, x); - a, c = R(a, b, c, d, e, F1, K1, 12, 0, x); - e, b = R(e, a, b, c, d, F1, K1, 15, 9, x); - d, a = R(d, e, a, b, c, F1, K1, 9, 5, x); - c, e = R(c, d, e, a, b, F1, K1, 11, 2, x); - b, d = R(b, c, d, e, a, F1, K1, 7, 14, x); - a, c = R(a, b, c, d, e, F1, K1, 13, 11, x); - e, b = R(e, a, b, c, d, F1, K1, 12, 8, x); #/* #31 */ - #/* Round 3 */ - d, a = R(d, e, a, b, c, F2, K2, 11, 3, x); - c, e = R(c, d, e, a, b, F2, K2, 13, 10, x); - b, d = R(b, c, d, e, a, F2, K2, 6, 14, x); - a, c = R(a, b, c, d, e, F2, K2, 7, 4, x); - e, b = R(e, a, b, c, d, F2, K2, 14, 9, x); - d, a = R(d, e, a, b, c, F2, K2, 9, 15, x); - c, e = R(c, d, e, a, b, F2, K2, 13, 8, x); - b, d = R(b, c, d, e, a, F2, K2, 15, 1, x); - a, c = R(a, b, c, d, e, F2, K2, 14, 2, x); - e, b = R(e, a, b, c, d, F2, K2, 8, 7, x); - d, a = R(d, e, a, b, c, F2, K2, 13, 0, x); - c, e = R(c, d, e, a, b, F2, K2, 6, 6, x); - b, d = R(b, c, d, e, a, F2, K2, 5, 13, x); - a, c = R(a, b, c, d, e, F2, K2, 12, 11, x); - e, b = R(e, a, b, c, d, F2, K2, 7, 5, x); - d, a = R(d, e, a, b, c, F2, K2, 5, 12, x); #/* #47 */ - #/* Round 4 */ - c, e = R(c, d, e, a, b, F3, K3, 11, 1, x); - b, d = R(b, c, d, e, a, F3, K3, 12, 9, x); - a, c = R(a, b, c, d, e, F3, K3, 14, 11, x); - e, b = R(e, a, b, c, d, F3, K3, 15, 10, x); - d, a = R(d, e, a, b, c, F3, K3, 14, 0, x); - c, e = R(c, d, e, a, b, F3, K3, 15, 8, x); - b, d = R(b, c, d, e, a, F3, K3, 9, 12, x); - a, c = R(a, b, c, d, e, F3, K3, 8, 4, x); - e, b = R(e, a, b, c, d, F3, K3, 9, 13, x); - d, a = R(d, e, a, b, c, F3, K3, 14, 3, x); - c, e = R(c, d, e, a, b, F3, K3, 5, 7, x); - b, d = R(b, c, d, e, a, F3, K3, 6, 15, x); - a, c = R(a, b, c, d, e, F3, K3, 8, 14, x); - e, b = R(e, a, b, c, d, F3, K3, 6, 5, x); - d, a = R(d, e, a, b, c, F3, K3, 5, 6, x); - c, e = R(c, d, e, a, b, F3, K3, 12, 2, x); #/* #63 */ - #/* Round 5 */ - b, d = R(b, c, d, e, a, F4, K4, 9, 4, x); - a, c = R(a, b, c, d, e, F4, K4, 15, 0, x); - e, b = R(e, a, b, c, d, F4, K4, 5, 5, x); - d, a = R(d, e, a, b, c, F4, K4, 11, 9, x); - c, e = R(c, d, e, a, b, F4, K4, 6, 7, x); - b, d = R(b, c, d, e, a, F4, K4, 8, 12, x); - a, c = R(a, b, c, d, e, F4, K4, 13, 2, x); - e, b = R(e, a, b, c, d, F4, K4, 12, 10, x); - d, a = R(d, e, a, b, c, F4, K4, 5, 14, x); - c, e = R(c, d, e, a, b, F4, K4, 12, 1, x); - b, d = R(b, c, d, e, a, F4, K4, 13, 3, x); - a, c = R(a, b, c, d, e, F4, K4, 14, 8, x); - e, b = R(e, a, b, c, d, F4, K4, 11, 11, x); - d, a = R(d, e, a, b, c, F4, K4, 8, 6, x); - c, e = R(c, d, e, a, b, F4, K4, 5, 15, x); - b, d = R(b, c, d, e, a, F4, K4, 6, 13, x); #/* #79 */ - - aa = a; - bb = b; - cc = c; - dd = d; - ee = e; - - a = state[0] - b = state[1] - c = state[2] - d = state[3] - e = state[4] - - #/* Parallel round 1 */ - a, c = R(a, b, c, d, e, F4, KK0, 8, 5, x) - e, b = R(e, a, b, c, d, F4, KK0, 9, 14, x) - d, a = R(d, e, a, b, c, F4, KK0, 9, 7, x) - c, e = R(c, d, e, a, b, F4, KK0, 11, 0, x) - b, d = R(b, c, d, e, a, F4, KK0, 13, 9, x) - a, c = R(a, b, c, d, e, F4, KK0, 15, 2, x) - e, b = R(e, a, b, c, d, F4, KK0, 15, 11, x) - d, a = R(d, e, a, b, c, F4, KK0, 5, 4, x) - c, e = R(c, d, e, a, b, F4, KK0, 7, 13, x) - b, d = R(b, c, d, e, a, F4, KK0, 7, 6, x) - a, c = R(a, b, c, d, e, F4, KK0, 8, 15, x) - e, b = R(e, a, b, c, d, F4, KK0, 11, 8, x) - d, a = R(d, e, a, b, c, F4, KK0, 14, 1, x) - c, e = R(c, d, e, a, b, F4, KK0, 14, 10, x) - b, d = R(b, c, d, e, a, F4, KK0, 12, 3, x) - a, c = R(a, b, c, d, e, F4, KK0, 6, 12, x) #/* #15 */ - #/* Parallel round 2 */ - e, b = R(e, a, b, c, d, F3, KK1, 9, 6, x) - d, a = R(d, e, a, b, c, F3, KK1, 13, 11, x) - c, e = R(c, d, e, a, b, F3, KK1, 15, 3, x) - b, d = R(b, c, d, e, a, F3, KK1, 7, 7, x) - a, c = R(a, b, c, d, e, F3, KK1, 12, 0, x) - e, b = R(e, a, b, c, d, F3, KK1, 8, 13, x) - d, a = R(d, e, a, b, c, F3, KK1, 9, 5, x) - c, e = R(c, d, e, a, b, F3, KK1, 11, 10, x) - b, d = R(b, c, d, e, a, F3, KK1, 7, 14, x) - a, c = R(a, b, c, d, e, F3, KK1, 7, 15, x) - e, b = R(e, a, b, c, d, F3, KK1, 12, 8, x) - d, a = R(d, e, a, b, c, F3, KK1, 7, 12, x) - c, e = R(c, d, e, a, b, F3, KK1, 6, 4, x) - b, d = R(b, c, d, e, a, F3, KK1, 15, 9, x) - a, c = R(a, b, c, d, e, F3, KK1, 13, 1, x) - e, b = R(e, a, b, c, d, F3, KK1, 11, 2, x) #/* #31 */ - #/* Parallel round 3 */ - d, a = R(d, e, a, b, c, F2, KK2, 9, 15, x) - c, e = R(c, d, e, a, b, F2, KK2, 7, 5, x) - b, d = R(b, c, d, e, a, F2, KK2, 15, 1, x) - a, c = R(a, b, c, d, e, F2, KK2, 11, 3, x) - e, b = R(e, a, b, c, d, F2, KK2, 8, 7, x) - d, a = R(d, e, a, b, c, F2, KK2, 6, 14, x) - c, e = R(c, d, e, a, b, F2, KK2, 6, 6, x) - b, d = R(b, c, d, e, a, F2, KK2, 14, 9, x) - a, c = R(a, b, c, d, e, F2, KK2, 12, 11, x) - e, b = R(e, a, b, c, d, F2, KK2, 13, 8, x) - d, a = R(d, e, a, b, c, F2, KK2, 5, 12, x) - c, e = R(c, d, e, a, b, F2, KK2, 14, 2, x) - b, d = R(b, c, d, e, a, F2, KK2, 13, 10, x) - a, c = R(a, b, c, d, e, F2, KK2, 13, 0, x) - e, b = R(e, a, b, c, d, F2, KK2, 7, 4, x) - d, a = R(d, e, a, b, c, F2, KK2, 5, 13, x) #/* #47 */ - #/* Parallel round 4 */ - c, e = R(c, d, e, a, b, F1, KK3, 15, 8, x) - b, d = R(b, c, d, e, a, F1, KK3, 5, 6, x) - a, c = R(a, b, c, d, e, F1, KK3, 8, 4, x) - e, b = R(e, a, b, c, d, F1, KK3, 11, 1, x) - d, a = R(d, e, a, b, c, F1, KK3, 14, 3, x) - c, e = R(c, d, e, a, b, F1, KK3, 14, 11, x) - b, d = R(b, c, d, e, a, F1, KK3, 6, 15, x) - a, c = R(a, b, c, d, e, F1, KK3, 14, 0, x) - e, b = R(e, a, b, c, d, F1, KK3, 6, 5, x) - d, a = R(d, e, a, b, c, F1, KK3, 9, 12, x) - c, e = R(c, d, e, a, b, F1, KK3, 12, 2, x) - b, d = R(b, c, d, e, a, F1, KK3, 9, 13, x) - a, c = R(a, b, c, d, e, F1, KK3, 12, 9, x) - e, b = R(e, a, b, c, d, F1, KK3, 5, 7, x) - d, a = R(d, e, a, b, c, F1, KK3, 15, 10, x) - c, e = R(c, d, e, a, b, F1, KK3, 8, 14, x) #/* #63 */ - #/* Parallel round 5 */ - b, d = R(b, c, d, e, a, F0, KK4, 8, 12, x) - a, c = R(a, b, c, d, e, F0, KK4, 5, 15, x) - e, b = R(e, a, b, c, d, F0, KK4, 12, 10, x) - d, a = R(d, e, a, b, c, F0, KK4, 9, 4, x) - c, e = R(c, d, e, a, b, F0, KK4, 12, 1, x) - b, d = R(b, c, d, e, a, F0, KK4, 5, 5, x) - a, c = R(a, b, c, d, e, F0, KK4, 14, 8, x) - e, b = R(e, a, b, c, d, F0, KK4, 6, 7, x) - d, a = R(d, e, a, b, c, F0, KK4, 8, 6, x) - c, e = R(c, d, e, a, b, F0, KK4, 13, 2, x) - b, d = R(b, c, d, e, a, F0, KK4, 6, 13, x) - a, c = R(a, b, c, d, e, F0, KK4, 5, 14, x) - e, b = R(e, a, b, c, d, F0, KK4, 15, 0, x) - d, a = R(d, e, a, b, c, F0, KK4, 13, 3, x) - c, e = R(c, d, e, a, b, F0, KK4, 11, 9, x) - b, d = R(b, c, d, e, a, F0, KK4, 11, 11, x) #/* #79 */ - - t = (state[1] + cc + d) % 0x100000000; - state[1] = (state[2] + dd + e) % 0x100000000; - state[2] = (state[3] + ee + a) % 0x100000000; - state[3] = (state[4] + aa + b) % 0x100000000; - state[4] = (state[0] + bb + c) % 0x100000000; - state[0] = t % 0x100000000; - - pass - - -def RMD160Update(ctx, inp, inplen): - if type(inp) == str: - inp = [ord(i)&0xff for i in inp] - - have = int((ctx.count // 8) % 64) - inplen = int(inplen) - need = 64 - have - ctx.count += 8 * inplen - off = 0 - if inplen >= need: - if have: - for i in range(need): - ctx.buffer[have+i] = inp[i] - RMD160Transform(ctx.state, ctx.buffer) - off = need - have = 0 - while off + 64 <= inplen: - RMD160Transform(ctx.state, inp[off:]) #<--- - off += 64 - if off < inplen: - # memcpy(ctx->buffer + have, input+off, len-off); - for i in range(inplen - off): - ctx.buffer[have+i] = inp[off+i] - -def RMD160Final(ctx): - size = struct.pack(" 73: return False - if (sig[0] != 0x30): return False - if (sig[1] != len(sig)-3): return False - rlen = sig[3] - if (5+rlen >= len(sig)): return False - slen = sig[5+rlen] - if (rlen + slen + 7 != len(sig)): return False - if (sig[2] != 0x02): return False - if (rlen == 0): return False - if (sig[4] & 0x80): return False - if (rlen > 1 and (sig[4] == 0x00) and not (sig[5] & 0x80)): return False - if (sig[4+rlen] != 0x02): return False - if (slen == 0): return False - if (sig[rlen+6] & 0x80): return False - if (slen > 1 and (sig[6+rlen] == 0x00) and not (sig[7+rlen] & 0x80)): - return False - return True - -def txhash(tx, hashcode=None): - if isinstance(tx, str) and re.match('^[0-9a-fA-F]*$', tx): - tx = changebase(tx, 16, 256) - if hashcode: - return dbl_sha256(from_string_to_bytes(tx) + encode(int(hashcode), 256, 4)[::-1]) - else: - return safe_hexlify(bin_dbl_sha256(tx)[::-1]) - - -def bin_txhash(tx, hashcode=None): - return binascii.unhexlify(txhash(tx, hashcode)) - - -def ecdsa_tx_sign(tx, priv, hashcode=SIGHASH_ALL): - rawsig = ecdsa_raw_sign(bin_txhash(tx, hashcode), priv) - return der_encode_sig(*rawsig)+encode(hashcode, 16, 2) - - -def ecdsa_tx_verify(tx, sig, pub, hashcode=SIGHASH_ALL): - return ecdsa_raw_verify(bin_txhash(tx, hashcode), der_decode_sig(sig), pub) - - -def ecdsa_tx_recover(tx, sig, hashcode=SIGHASH_ALL): - z = bin_txhash(tx, hashcode) - _, r, s = der_decode_sig(sig) - left = ecdsa_raw_recover(z, (0, r, s)) - right = ecdsa_raw_recover(z, (1, r, s)) - return (encode_pubkey(left, 'hex'), encode_pubkey(right, 'hex')) - -# Scripts - - -def mk_pubkey_script(addr): - # Keep the auxiliary functions around for altcoins' sake - return '76a914' + b58check_to_hex(addr) + '88ac' - - -def mk_scripthash_script(addr): - return 'a914' + b58check_to_hex(addr) + '87' - -# Address representation to output script - - -def address_to_script(addr): - if addr[0] == '3' or addr[0] == '2': - return mk_scripthash_script(addr) - else: - return mk_pubkey_script(addr) - -# Output script to address representation - - -def script_to_address(script, vbyte=0): - if re.match('^[0-9a-fA-F]*$', script): - script = binascii.unhexlify(script) - if script[:3] == b'\x76\xa9\x14' and script[-2:] == b'\x88\xac' and len(script) == 25: - return bin_to_b58check(script[3:-2], vbyte) # pubkey hash addresses - else: - if vbyte in [111, 196]: - # Testnet - scripthash_byte = 196 - elif vbyte == 0: - # Mainnet - scripthash_byte = 5 - else: - scripthash_byte = vbyte - # BIP0016 scripthash addresses - return bin_to_b58check(script[2:-1], scripthash_byte) - - -def p2sh_scriptaddr(script, magicbyte=5): - if re.match('^[0-9a-fA-F]*$', script): - script = binascii.unhexlify(script) - return hex_to_b58check(hash160(script), magicbyte) -scriptaddr = p2sh_scriptaddr - - -def deserialize_script(script): - if isinstance(script, str) and re.match('^[0-9a-fA-F]*$', script): - return json_changebase(deserialize_script(binascii.unhexlify(script)), - lambda x: safe_hexlify(x)) - out, pos = [], 0 - while pos < len(script): - code = from_byte_to_int(script[pos]) - if code == 0: - out.append(None) - pos += 1 - elif code <= 75: - out.append(script[pos+1:pos+1+code]) - pos += 1 + code - elif code <= 78: - szsz = pow(2, code - 76) - sz = decode(script[pos+szsz: pos:-1], 256) - out.append(script[pos + 1 + szsz:pos + 1 + szsz + sz]) - pos += 1 + szsz + sz - elif code <= 96: - out.append(code - 80) - pos += 1 - else: - out.append(code) - pos += 1 - return out - - -def serialize_script_unit(unit): - if isinstance(unit, int): - if unit < 16: - return from_int_to_byte(unit + 80) - else: - return from_int_to_byte(unit) - elif unit is None: - return b'\x00' - else: - if len(unit) <= 75: - return from_int_to_byte(len(unit))+unit - elif len(unit) < 256: - return from_int_to_byte(76)+from_int_to_byte(len(unit))+unit - elif len(unit) < 65536: - return from_int_to_byte(77)+encode(len(unit), 256, 2)[::-1]+unit - else: - return from_int_to_byte(78)+encode(len(unit), 256, 4)[::-1]+unit - - -if is_python2: - def serialize_script(script): - if json_is_base(script, 16): - return binascii.hexlify(serialize_script(json_changebase(script, - lambda x: binascii.unhexlify(x)))) - return ''.join(map(serialize_script_unit, script)) -else: - def serialize_script(script): - if json_is_base(script, 16): - return safe_hexlify(serialize_script(json_changebase(script, - lambda x: binascii.unhexlify(x)))) - - result = bytes() - for b in map(serialize_script_unit, script): - result += b if isinstance(b, bytes) else bytes(b, 'utf-8') - return result - - -def mk_multisig_script(*args): # [pubs],k or pub1,pub2...pub[n],k - if isinstance(args[0], list): - pubs, k = args[0], int(args[1]) - else: - pubs = list(filter(lambda x: len(str(x)) >= 32, args)) - k = int(args[len(pubs)]) - return serialize_script([k]+pubs+[len(pubs)]+[0xae]) - -# Signing and verifying - - -def verify_tx_input(tx, i, script, sig, pub): - if re.match('^[0-9a-fA-F]*$', tx): - tx = binascii.unhexlify(tx) - if re.match('^[0-9a-fA-F]*$', script): - script = binascii.unhexlify(script) - if not re.match('^[0-9a-fA-F]*$', sig): - sig = safe_hexlify(sig) - hashcode = decode(sig[-2:], 16) - modtx = signature_form(tx, int(i), script, hashcode) - return ecdsa_tx_verify(modtx, sig, pub, hashcode) - - -def sign(tx, i, priv, hashcode=SIGHASH_ALL): - i = int(i) - if (not is_python2 and isinstance(re, bytes)) or not re.match('^[0-9a-fA-F]*$', tx): - return binascii.unhexlify(sign(safe_hexlify(tx), i, priv)) - if len(priv) <= 33: - priv = safe_hexlify(priv) - pub = privkey_to_pubkey(priv) - address = pubkey_to_address(pub) - signing_tx = signature_form(tx, i, mk_pubkey_script(address), hashcode) - sig = ecdsa_tx_sign(signing_tx, priv, hashcode) - txobj = deserialize(tx) - txobj["ins"][i]["script"] = serialize_script([sig, pub]) - return serialize(txobj) - - -def signall(tx, priv): - # if priv is a dictionary, assume format is - # { 'txinhash:txinidx' : privkey } - if isinstance(priv, dict): - for e, i in enumerate(deserialize(tx)["ins"]): - k = priv["%s:%d" % (i["outpoint"]["hash"], i["outpoint"]["index"])] - tx = sign(tx, e, k) - else: - for i in range(len(deserialize(tx)["ins"])): - tx = sign(tx, i, priv) - return tx - - -def multisign(tx, i, script, pk, hashcode=SIGHASH_ALL): - if re.match('^[0-9a-fA-F]*$', tx): - tx = binascii.unhexlify(tx) - if re.match('^[0-9a-fA-F]*$', script): - script = binascii.unhexlify(script) - modtx = signature_form(tx, i, script, hashcode) - return ecdsa_tx_sign(modtx, pk, hashcode) - - -def apply_multisignatures(*args): - # tx,i,script,sigs OR tx,i,script,sig1,sig2...,sig[n] - tx, i, script = args[0], int(args[1]), args[2] - sigs = args[3] if isinstance(args[3], list) else list(args[3:]) - - if isinstance(script, str) and re.match('^[0-9a-fA-F]*$', script): - script = binascii.unhexlify(script) - sigs = [binascii.unhexlify(x) if x[:2] == '30' else x for x in sigs] - if isinstance(tx, str) and re.match('^[0-9a-fA-F]*$', tx): - return safe_hexlify(apply_multisignatures(binascii.unhexlify(tx), i, script, sigs)) - - # Not pushing empty elements on the top of the stack if passing no - # script (in case of bare multisig inputs there is no script) - script_blob = [] if script.__len__() == 0 else [script] - - txobj = deserialize(tx) - txobj["ins"][i]["script"] = serialize_script([None]+sigs+script_blob) - return serialize(txobj) - - -def is_inp(arg): - return len(arg) > 64 or "output" in arg or "outpoint" in arg - - -def mktx(*args): - # [in0, in1...],[out0, out1...] or in0, in1 ... out0 out1 ... - ins, outs = [], [] - for arg in args: - if isinstance(arg, list): - for a in arg: (ins if is_inp(a) else outs).append(a) - else: - (ins if is_inp(arg) else outs).append(arg) - - txobj = {"locktime": 0, "version": 1, "ins": [], "outs": []} - for i in ins: - if isinstance(i, dict) and "outpoint" in i: - txobj["ins"].append(i) - else: - if isinstance(i, dict) and "output" in i: - i = i["output"] - txobj["ins"].append({ - "outpoint": {"hash": i[:64], "index": int(i[65:])}, - "script": "", - "sequence": 4294967295 - }) - for o in outs: - if isinstance(o, string_or_bytes_types): - addr = o[:o.find(':')] - val = int(o[o.find(':')+1:]) - o = {} - if re.match('^[0-9a-fA-F]*$', addr): - o["script"] = addr - else: - o["address"] = addr - o["value"] = val - - outobj = {} - if "address" in o: - outobj["script"] = address_to_script(o["address"]) - elif "script" in o: - outobj["script"] = o["script"] - else: - raise Exception("Could not find 'address' or 'script' in output.") - outobj["value"] = o["value"] - txobj["outs"].append(outobj) - - return serialize(txobj) - - -def select(unspent, value): - value = int(value) - high = [u for u in unspent if u["value"] >= value] - high.sort(key=lambda u: u["value"]) - low = [u for u in unspent if u["value"] < value] - low.sort(key=lambda u: -u["value"]) - if len(high): - return [high[0]] - i, tv = 0, 0 - while tv < value and i < len(low): - tv += low[i]["value"] - i += 1 - if tv < value: - raise Exception("Not enough funds") - return low[:i] - -# Only takes inputs of the form { "output": blah, "value": foo } - - -def mksend(*args): - argz, change, fee = args[:-2], args[-2], int(args[-1]) - ins, outs = [], [] - for arg in argz: - if isinstance(arg, list): - for a in arg: - (ins if is_inp(a) else outs).append(a) - else: - (ins if is_inp(arg) else outs).append(arg) - - isum = sum([i["value"] for i in ins]) - osum, outputs2 = 0, [] - for o in outs: - if isinstance(o, string_types): - o2 = { - "address": o[:o.find(':')], - "value": int(o[o.find(':')+1:]) - } - else: - o2 = o - outputs2.append(o2) - osum += o2["value"] - - if isum < osum+fee: - raise Exception("Not enough money") - elif isum > osum+fee+5430: - outputs2 += [{"address": change, "value": isum-osum-fee}] - - return mktx(ins, outputs2) diff --git a/src/lib/pyelliptic/LICENSE b/src/lib/pyelliptic/LICENSE deleted file mode 100644 index 94a9ed02..00000000 --- a/src/lib/pyelliptic/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/src/lib/pyelliptic/README.md b/src/lib/pyelliptic/README.md deleted file mode 100644 index 3acf819c..00000000 --- a/src/lib/pyelliptic/README.md +++ /dev/null @@ -1,96 +0,0 @@ -# PyElliptic - -PyElliptic is a high level wrapper for the cryptographic library : OpenSSL. -Under the GNU General Public License - -Python3 compatible. For GNU/Linux and Windows. -Require OpenSSL - -## Version - -The [upstream pyelliptic](https://github.com/yann2192/pyelliptic) has been -deprecated by the author at 1.5.8 and ECC API has been removed. - -This version is a fork of the pyelliptic extracted from the [BitMessage source -tree](https://github.com/Bitmessage/PyBitmessage), and does contain the ECC -API. To minimize confusion but to avoid renaming the module, major version has -been bumped. - -BitMessage is actively maintained, and this fork of pyelliptic will track and -incorporate any changes to pyelliptic from BitMessage. Ideally, in the future, -BitMessage would import this module as a dependency instead of maintaining a -copy of the source in its repository. - -The BitMessage fork forked from v1.3 of upstream pyelliptic. The commits in -this repository are the commits extracted from the BitMessage repository and -applied to pyelliptic v1.3 upstream repository (i.e. to the base of the fork), -so history with athorship is preserved. - -Some of the changes in upstream pyelliptic between 1.3 and 1.5.8 came from -BitMessage, those changes are present in this fork. Other changes do not exist -in this fork (they may be added in the future). - -Also, a few minor changes exist in this fork but is not (yet) present in -BitMessage source. See: - - git log 1.3-PyBitmessage-37489cf7feff8d5047f24baa8f6d27f353a6d6ac..HEAD - -## Features - -### Asymmetric cryptography using Elliptic Curve Cryptography (ECC) - -* Key agreement : ECDH -* Digital signatures : ECDSA -* Hybrid encryption : ECIES (like RSA) - -### Symmetric cryptography - -* AES-128 (CBC, OFB, CFB, CTR) -* AES-256 (CBC, OFB, CFB, CTR) -* Blowfish (CFB and CBC) -* RC4 - -### Other - -* CSPRNG -* HMAC (using SHA512) -* PBKDF2 (SHA256 and SHA512) - -## Example - -```python -#!/usr/bin/python - -import pyelliptic - -# Symmetric encryption -iv = pyelliptic.Cipher.gen_IV('aes-256-cfb') -ctx = pyelliptic.Cipher("secretkey", iv, 1, ciphername='aes-256-cfb') - -ciphertext = ctx.update('test1') -ciphertext += ctx.update('test2') -ciphertext += ctx.final() - -ctx2 = pyelliptic.Cipher("secretkey", iv, 0, ciphername='aes-256-cfb') -print ctx2.ciphering(ciphertext) - -# Asymmetric encryption -alice = pyelliptic.ECC() # default curve: sect283r1 -bob = pyelliptic.ECC(curve='sect571r1') - -ciphertext = alice.encrypt("Hello Bob", bob.get_pubkey()) -print bob.decrypt(ciphertext) - -signature = bob.sign("Hello Alice") -# alice's job : -print pyelliptic.ECC(pubkey=bob.get_pubkey()).verify(signature, "Hello Alice") - -# ERROR !!! -try: - key = alice.get_ecdh_key(bob.get_pubkey()) -except: print("For ECDH key agreement, the keys must be defined on the same curve !") - -alice = pyelliptic.ECC(curve='sect571r1') -print alice.get_ecdh_key(bob.get_pubkey()).encode('hex') -print bob.get_ecdh_key(alice.get_pubkey()).encode('hex') -``` diff --git a/src/lib/pyelliptic/__init__.py b/src/lib/pyelliptic/__init__.py deleted file mode 100644 index 761d08af..00000000 --- a/src/lib/pyelliptic/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (C) 2010 -# Author: Yann GUIBET -# Contact: - -__version__ = '1.3' - -__all__ = [ - 'OpenSSL', - 'ECC', - 'Cipher', - 'hmac_sha256', - 'hmac_sha512', - 'pbkdf2' -] - -from .openssl import OpenSSL -from .ecc import ECC -from .cipher import Cipher -from .hash import hmac_sha256, hmac_sha512, pbkdf2 diff --git a/src/lib/pyelliptic/arithmetic.py b/src/lib/pyelliptic/arithmetic.py deleted file mode 100644 index 95c85b93..00000000 --- a/src/lib/pyelliptic/arithmetic.py +++ /dev/null @@ -1,144 +0,0 @@ -# pylint: disable=missing-docstring,too-many-function-args - -import hashlib -import re - -P = 2**256 - 2**32 - 2**9 - 2**8 - 2**7 - 2**6 - 2**4 - 1 -A = 0 -Gx = 55066263022277343669578718895168534326250603453777594175500187360389116729240 -Gy = 32670510020758816978083085130507043184471273380659243275938904335757337482424 -G = (Gx, Gy) - - -def inv(a, n): - lm, hm = 1, 0 - low, high = a % n, n - while low > 1: - r = high / low - nm, new = hm - lm * r, high - low * r - lm, low, hm, high = nm, new, lm, low - return lm % n - - -def get_code_string(base): - if base == 2: - return '01' - elif base == 10: - return '0123456789' - elif base == 16: - return "0123456789abcdef" - elif base == 58: - return "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" - elif base == 256: - return ''.join([chr(x) for x in range(256)]) - else: - raise ValueError("Invalid base!") - - -def encode(val, base, minlen=0): - code_string = get_code_string(base) - result = "" - while val > 0: - result = code_string[val % base] + result - val /= base - if len(result) < minlen: - result = code_string[0] * (minlen - len(result)) + result - return result - - -def decode(string, base): - code_string = get_code_string(base) - result = 0 - if base == 16: - string = string.lower() - while string: - result *= base - result += code_string.find(string[0]) - string = string[1:] - return result - - -def changebase(string, frm, to, minlen=0): - return encode(decode(string, frm), to, minlen) - - -def base10_add(a, b): - if a is None: - return b[0], b[1] - if b is None: - return a[0], a[1] - if a[0] == b[0]: - if a[1] == b[1]: - return base10_double(a[0], a[1]) - return None - m = ((b[1] - a[1]) * inv(b[0] - a[0], P)) % P - x = (m * m - a[0] - b[0]) % P - y = (m * (a[0] - x) - a[1]) % P - return (x, y) - - -def base10_double(a): - if a is None: - return None - m = ((3 * a[0] * a[0] + A) * inv(2 * a[1], P)) % P - x = (m * m - 2 * a[0]) % P - y = (m * (a[0] - x) - a[1]) % P - return (x, y) - - -def base10_multiply(a, n): - if n == 0: - return G - if n == 1: - return a - if (n % 2) == 0: - return base10_double(base10_multiply(a, n / 2)) - if (n % 2) == 1: - return base10_add(base10_double(base10_multiply(a, n / 2)), a) - return None - - -def hex_to_point(h): - return (decode(h[2:66], 16), decode(h[66:], 16)) - - -def point_to_hex(p): - return '04' + encode(p[0], 16, 64) + encode(p[1], 16, 64) - - -def multiply(privkey, pubkey): - return point_to_hex(base10_multiply(hex_to_point(pubkey), decode(privkey, 16))) - - -def privtopub(privkey): - return point_to_hex(base10_multiply(G, decode(privkey, 16))) - - -def add(p1, p2): - if len(p1) == 32: - return encode(decode(p1, 16) + decode(p2, 16) % P, 16, 32) - return point_to_hex(base10_add(hex_to_point(p1), hex_to_point(p2))) - - -def hash_160(string): - intermed = hashlib.sha256(string).digest() - ripemd160 = hashlib.new('ripemd160') - ripemd160.update(intermed) - return ripemd160.digest() - - -def dbl_sha256(string): - return hashlib.sha256(hashlib.sha256(string).digest()).digest() - - -def bin_to_b58check(inp): - inp_fmtd = '\x00' + inp - leadingzbytes = len(re.match('^\x00*', inp_fmtd).group(0)) - checksum = dbl_sha256(inp_fmtd)[:4] - return '1' * leadingzbytes + changebase(inp_fmtd + checksum, 256, 58) - -# Convert a public key (in hex) to a Bitcoin address - - -def pubkey_to_address(pubkey): - return bin_to_b58check(hash_160(changebase(pubkey, 16, 256))) diff --git a/src/lib/pyelliptic/cipher.py b/src/lib/pyelliptic/cipher.py deleted file mode 100644 index 54ae7a09..00000000 --- a/src/lib/pyelliptic/cipher.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright (C) 2011 Yann GUIBET -# See LICENSE for details. - -from .openssl import OpenSSL - - -class Cipher: - """ - Symmetric encryption - - import pyelliptic - iv = pyelliptic.Cipher.gen_IV('aes-256-cfb') - ctx = pyelliptic.Cipher("secretkey", iv, 1, ciphername='aes-256-cfb') - ciphertext = ctx.update('test1') - ciphertext += ctx.update('test2') - ciphertext += ctx.final() - - ctx2 = pyelliptic.Cipher("secretkey", iv, 0, ciphername='aes-256-cfb') - print ctx2.ciphering(ciphertext) - """ - def __init__(self, key, iv, do, ciphername='aes-256-cbc'): - """ - do == 1 => Encrypt; do == 0 => Decrypt - """ - self.cipher = OpenSSL.get_cipher(ciphername) - self.ctx = OpenSSL.EVP_CIPHER_CTX_new() - if do == 1 or do == 0: - k = OpenSSL.malloc(key, len(key)) - IV = OpenSSL.malloc(iv, len(iv)) - OpenSSL.EVP_CipherInit_ex( - self.ctx, self.cipher.get_pointer(), 0, k, IV, do) - else: - raise Exception("RTFM ...") - - @staticmethod - def get_all_cipher(): - """ - static method, returns all ciphers available - """ - return OpenSSL.cipher_algo.keys() - - @staticmethod - def get_blocksize(ciphername): - cipher = OpenSSL.get_cipher(ciphername) - return cipher.get_blocksize() - - @staticmethod - def gen_IV(ciphername): - cipher = OpenSSL.get_cipher(ciphername) - return OpenSSL.rand(cipher.get_blocksize()) - - def update(self, input): - i = OpenSSL.c_int(0) - buffer = OpenSSL.malloc(b"", len(input) + self.cipher.get_blocksize()) - inp = OpenSSL.malloc(input, len(input)) - if OpenSSL.EVP_CipherUpdate(self.ctx, OpenSSL.byref(buffer), - OpenSSL.byref(i), inp, len(input)) == 0: - raise Exception("[OpenSSL] EVP_CipherUpdate FAIL ...") - return buffer.raw[0:i.value] - - def final(self): - i = OpenSSL.c_int(0) - buffer = OpenSSL.malloc(b"", self.cipher.get_blocksize()) - if (OpenSSL.EVP_CipherFinal_ex(self.ctx, OpenSSL.byref(buffer), - OpenSSL.byref(i))) == 0: - raise Exception("[OpenSSL] EVP_CipherFinal_ex FAIL ...") - return buffer.raw[0:i.value] - - def ciphering(self, input): - """ - Do update and final in one method - """ - buff = self.update(input) - return buff + self.final() - - def __del__(self): - if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: - OpenSSL.EVP_CIPHER_CTX_reset(self.ctx) - else: - OpenSSL.EVP_CIPHER_CTX_cleanup(self.ctx) - OpenSSL.EVP_CIPHER_CTX_free(self.ctx) diff --git a/src/lib/pyelliptic/ecc.py b/src/lib/pyelliptic/ecc.py deleted file mode 100644 index a45f8d78..00000000 --- a/src/lib/pyelliptic/ecc.py +++ /dev/null @@ -1,505 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -pyelliptic/ecc.py -===================== -""" -# pylint: disable=protected-access - -# Copyright (C) 2011 Yann GUIBET -# See LICENSE for details. - -from hashlib import sha512 -from struct import pack, unpack - -from .cipher import Cipher -from .hash import equals, hmac_sha256 -from .openssl import OpenSSL - - -class ECC(object): - """ - Asymmetric encryption with Elliptic Curve Cryptography (ECC) - ECDH, ECDSA and ECIES - - >>> import pyelliptic - - >>> alice = pyelliptic.ECC() # default curve: sect283r1 - >>> bob = pyelliptic.ECC(curve='sect571r1') - - >>> ciphertext = alice.encrypt("Hello Bob", bob.get_pubkey()) - >>> print bob.decrypt(ciphertext) - - >>> signature = bob.sign("Hello Alice") - >>> # alice's job : - >>> print pyelliptic.ECC( - >>> pubkey=bob.get_pubkey()).verify(signature, "Hello Alice") - - >>> # ERROR !!! - >>> try: - >>> key = alice.get_ecdh_key(bob.get_pubkey()) - >>> except: - >>> print("For ECDH key agreement, the keys must be defined on the same curve !") - - >>> alice = pyelliptic.ECC(curve='sect571r1') - >>> print alice.get_ecdh_key(bob.get_pubkey()).encode('hex') - >>> print bob.get_ecdh_key(alice.get_pubkey()).encode('hex') - - """ - - def __init__( - self, - pubkey=None, - privkey=None, - pubkey_x=None, - pubkey_y=None, - raw_privkey=None, - curve='sect283r1', - ): # pylint: disable=too-many-arguments - """ - For a normal and High level use, specifie pubkey, - privkey (if you need) and the curve - """ - if isinstance(curve, str): - self.curve = OpenSSL.get_curve(curve) - else: - self.curve = curve - - if pubkey_x is not None and pubkey_y is not None: - self._set_keys(pubkey_x, pubkey_y, raw_privkey) - elif pubkey is not None: - curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) - if privkey is not None: - curve2, raw_privkey, _ = ECC._decode_privkey(privkey) - if curve != curve2: - raise Exception("Bad ECC keys ...") - self.curve = curve - self._set_keys(pubkey_x, pubkey_y, raw_privkey) - else: - self.privkey, self.pubkey_x, self.pubkey_y = self._generate() - - def _set_keys(self, pubkey_x, pubkey_y, privkey): - if self.raw_check_key(privkey, pubkey_x, pubkey_y) < 0: - self.pubkey_x = None - self.pubkey_y = None - self.privkey = None - raise Exception("Bad ECC keys ...") - else: - self.pubkey_x = pubkey_x - self.pubkey_y = pubkey_y - self.privkey = privkey - - @staticmethod - def get_curves(): - """ - static method, returns the list of all the curves available - """ - return OpenSSL.curves.keys() - - def get_curve(self): - """Encryption object from curve name""" - return OpenSSL.get_curve_by_id(self.curve) - - def get_curve_id(self): - """Currently used curve""" - return self.curve - - def get_pubkey(self): - """ - High level function which returns : - curve(2) + len_of_pubkeyX(2) + pubkeyX + len_of_pubkeyY + pubkeyY - """ - return b''.join(( - pack('!H', self.curve), - pack('!H', len(self.pubkey_x)), - self.pubkey_x, - pack('!H', len(self.pubkey_y)), - self.pubkey_y, - )) - - def get_privkey(self): - """ - High level function which returns - curve(2) + len_of_privkey(2) + privkey - """ - return b''.join(( - pack('!H', self.curve), - pack('!H', len(self.privkey)), - self.privkey, - )) - - @staticmethod - def _decode_pubkey(pubkey): - i = 0 - curve = unpack('!H', pubkey[i:i + 2])[0] - i += 2 - tmplen = unpack('!H', pubkey[i:i + 2])[0] - i += 2 - pubkey_x = pubkey[i:i + tmplen] - i += tmplen - tmplen = unpack('!H', pubkey[i:i + 2])[0] - i += 2 - pubkey_y = pubkey[i:i + tmplen] - i += tmplen - return curve, pubkey_x, pubkey_y, i - - @staticmethod - def _decode_privkey(privkey): - i = 0 - curve = unpack('!H', privkey[i:i + 2])[0] - i += 2 - tmplen = unpack('!H', privkey[i:i + 2])[0] - i += 2 - privkey = privkey[i:i + tmplen] - i += tmplen - return curve, privkey, i - - def _generate(self): - try: - pub_key_x = OpenSSL.BN_new() - pub_key_y = OpenSSL.BN_new() - - key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) - if key == 0: - raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") - if (OpenSSL.EC_KEY_generate_key(key)) == 0: - raise Exception("[OpenSSL] EC_KEY_generate_key FAIL ...") - if (OpenSSL.EC_KEY_check_key(key)) == 0: - raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") - priv_key = OpenSSL.EC_KEY_get0_private_key(key) - - group = OpenSSL.EC_KEY_get0_group(key) - pub_key = OpenSSL.EC_KEY_get0_public_key(key) - - if OpenSSL.EC_POINT_get_affine_coordinates_GFp( - group, pub_key, pub_key_x, pub_key_y, 0) == 0: - raise Exception("[OpenSSL] EC_POINT_get_affine_coordinates_GFp FAIL ...") - - privkey = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(priv_key)) - pubkeyx = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(pub_key_x)) - pubkeyy = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(pub_key_y)) - OpenSSL.BN_bn2bin(priv_key, privkey) - privkey = privkey.raw - OpenSSL.BN_bn2bin(pub_key_x, pubkeyx) - pubkeyx = pubkeyx.raw - OpenSSL.BN_bn2bin(pub_key_y, pubkeyy) - pubkeyy = pubkeyy.raw - self.raw_check_key(privkey, pubkeyx, pubkeyy) - - return privkey, pubkeyx, pubkeyy - - finally: - OpenSSL.EC_KEY_free(key) - OpenSSL.BN_free(pub_key_x) - OpenSSL.BN_free(pub_key_y) - - def get_ecdh_key(self, pubkey): - """ - High level function. Compute public key with the local private key - and returns a 512bits shared key - """ - curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) - if curve != self.curve: - raise Exception("ECC keys must be from the same curve !") - return sha512(self.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest() - - def raw_get_ecdh_key(self, pubkey_x, pubkey_y): - """ECDH key as binary data""" - try: - ecdh_keybuffer = OpenSSL.malloc(0, 32) - - other_key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) - if other_key == 0: - raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") - - other_pub_key_x = OpenSSL.BN_bin2bn(pubkey_x, len(pubkey_x), 0) - other_pub_key_y = OpenSSL.BN_bin2bn(pubkey_y, len(pubkey_y), 0) - - other_group = OpenSSL.EC_KEY_get0_group(other_key) - other_pub_key = OpenSSL.EC_POINT_new(other_group) - - if (OpenSSL.EC_POINT_set_affine_coordinates_GFp(other_group, - other_pub_key, - other_pub_key_x, - other_pub_key_y, - 0)) == 0: - raise Exception( - "[OpenSSL] EC_POINT_set_affine_coordinates_GFp FAIL ...") - if (OpenSSL.EC_KEY_set_public_key(other_key, other_pub_key)) == 0: - raise Exception("[OpenSSL] EC_KEY_set_public_key FAIL ...") - if (OpenSSL.EC_KEY_check_key(other_key)) == 0: - raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") - - own_key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) - if own_key == 0: - raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") - own_priv_key = OpenSSL.BN_bin2bn( - self.privkey, len(self.privkey), 0) - - if (OpenSSL.EC_KEY_set_private_key(own_key, own_priv_key)) == 0: - raise Exception("[OpenSSL] EC_KEY_set_private_key FAIL ...") - - if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: - OpenSSL.EC_KEY_set_method(own_key, OpenSSL.EC_KEY_OpenSSL()) - else: - OpenSSL.ECDH_set_method(own_key, OpenSSL.ECDH_OpenSSL()) - ecdh_keylen = OpenSSL.ECDH_compute_key( - ecdh_keybuffer, 32, other_pub_key, own_key, 0) - - if ecdh_keylen != 32: - raise Exception("[OpenSSL] ECDH keylen FAIL ...") - - return ecdh_keybuffer.raw - - finally: - OpenSSL.EC_KEY_free(other_key) - OpenSSL.BN_free(other_pub_key_x) - OpenSSL.BN_free(other_pub_key_y) - OpenSSL.EC_POINT_free(other_pub_key) - OpenSSL.EC_KEY_free(own_key) - OpenSSL.BN_free(own_priv_key) - - def check_key(self, privkey, pubkey): - """ - Check the public key and the private key. - The private key is optional (replace by None) - """ - curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) - if privkey is None: - raw_privkey = None - curve2 = curve - else: - curve2, raw_privkey, _ = ECC._decode_privkey(privkey) - if curve != curve2: - raise Exception("Bad public and private key") - return self.raw_check_key(raw_privkey, pubkey_x, pubkey_y, curve) - - def raw_check_key(self, privkey, pubkey_x, pubkey_y, curve=None): - """Check key validity, key is supplied as binary data""" - # pylint: disable=too-many-branches - if curve is None: - curve = self.curve - elif isinstance(curve, str): - curve = OpenSSL.get_curve(curve) - else: - curve = curve - try: - key = OpenSSL.EC_KEY_new_by_curve_name(curve) - if key == 0: - raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") - if privkey is not None: - priv_key = OpenSSL.BN_bin2bn(privkey, len(privkey), 0) - pub_key_x = OpenSSL.BN_bin2bn(pubkey_x, len(pubkey_x), 0) - pub_key_y = OpenSSL.BN_bin2bn(pubkey_y, len(pubkey_y), 0) - - if privkey is not None: - if (OpenSSL.EC_KEY_set_private_key(key, priv_key)) == 0: - raise Exception( - "[OpenSSL] EC_KEY_set_private_key FAIL ...") - - group = OpenSSL.EC_KEY_get0_group(key) - pub_key = OpenSSL.EC_POINT_new(group) - - if (OpenSSL.EC_POINT_set_affine_coordinates_GFp(group, pub_key, - pub_key_x, - pub_key_y, - 0)) == 0: - raise Exception( - "[OpenSSL] EC_POINT_set_affine_coordinates_GFp FAIL ...") - if (OpenSSL.EC_KEY_set_public_key(key, pub_key)) == 0: - raise Exception("[OpenSSL] EC_KEY_set_public_key FAIL ...") - if (OpenSSL.EC_KEY_check_key(key)) == 0: - raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") - return 0 - - finally: - OpenSSL.EC_KEY_free(key) - OpenSSL.BN_free(pub_key_x) - OpenSSL.BN_free(pub_key_y) - OpenSSL.EC_POINT_free(pub_key) - if privkey is not None: - OpenSSL.BN_free(priv_key) - - def sign(self, inputb, digest_alg=OpenSSL.digest_ecdsa_sha1): - """ - Sign the input with ECDSA method and returns the signature - """ - # pylint: disable=too-many-branches,too-many-locals - try: - size = len(inputb) - buff = OpenSSL.malloc(inputb, size) - digest = OpenSSL.malloc(0, 64) - if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: - md_ctx = OpenSSL.EVP_MD_CTX_new() - else: - md_ctx = OpenSSL.EVP_MD_CTX_create() - dgst_len = OpenSSL.pointer(OpenSSL.c_int(0)) - siglen = OpenSSL.pointer(OpenSSL.c_int(0)) - sig = OpenSSL.malloc(0, 151) - - key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) - if key == 0: - raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") - - priv_key = OpenSSL.BN_bin2bn(self.privkey, len(self.privkey), 0) - pub_key_x = OpenSSL.BN_bin2bn(self.pubkey_x, len(self.pubkey_x), 0) - pub_key_y = OpenSSL.BN_bin2bn(self.pubkey_y, len(self.pubkey_y), 0) - - if (OpenSSL.EC_KEY_set_private_key(key, priv_key)) == 0: - raise Exception("[OpenSSL] EC_KEY_set_private_key FAIL ...") - - group = OpenSSL.EC_KEY_get0_group(key) - pub_key = OpenSSL.EC_POINT_new(group) - - if (OpenSSL.EC_POINT_set_affine_coordinates_GFp(group, pub_key, - pub_key_x, - pub_key_y, - 0)) == 0: - raise Exception( - "[OpenSSL] EC_POINT_set_affine_coordinates_GFp FAIL ...") - if (OpenSSL.EC_KEY_set_public_key(key, pub_key)) == 0: - raise Exception("[OpenSSL] EC_KEY_set_public_key FAIL ...") - if (OpenSSL.EC_KEY_check_key(key)) == 0: - raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") - - if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: - OpenSSL.EVP_MD_CTX_new(md_ctx) - else: - OpenSSL.EVP_MD_CTX_init(md_ctx) - OpenSSL.EVP_DigestInit_ex(md_ctx, digest_alg(), None) - - if (OpenSSL.EVP_DigestUpdate(md_ctx, buff, size)) == 0: - raise Exception("[OpenSSL] EVP_DigestUpdate FAIL ...") - OpenSSL.EVP_DigestFinal_ex(md_ctx, digest, dgst_len) - OpenSSL.ECDSA_sign(0, digest, dgst_len.contents, sig, siglen, key) - if (OpenSSL.ECDSA_verify(0, digest, dgst_len.contents, sig, - siglen.contents, key)) != 1: - raise Exception("[OpenSSL] ECDSA_verify FAIL ...") - - return sig.raw[:siglen.contents.value] - - finally: - OpenSSL.EC_KEY_free(key) - OpenSSL.BN_free(pub_key_x) - OpenSSL.BN_free(pub_key_y) - OpenSSL.BN_free(priv_key) - OpenSSL.EC_POINT_free(pub_key) - if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: - OpenSSL.EVP_MD_CTX_free(md_ctx) - else: - OpenSSL.EVP_MD_CTX_destroy(md_ctx) - - def verify(self, sig, inputb, digest_alg=OpenSSL.digest_ecdsa_sha1): - """ - Verify the signature with the input and the local public key. - Returns a boolean - """ - # pylint: disable=too-many-branches - try: - bsig = OpenSSL.malloc(sig, len(sig)) - binputb = OpenSSL.malloc(inputb, len(inputb)) - digest = OpenSSL.malloc(0, 64) - dgst_len = OpenSSL.pointer(OpenSSL.c_int(0)) - if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: - md_ctx = OpenSSL.EVP_MD_CTX_new() - else: - md_ctx = OpenSSL.EVP_MD_CTX_create() - key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) - - if key == 0: - raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") - - pub_key_x = OpenSSL.BN_bin2bn(self.pubkey_x, len(self.pubkey_x), 0) - pub_key_y = OpenSSL.BN_bin2bn(self.pubkey_y, len(self.pubkey_y), 0) - group = OpenSSL.EC_KEY_get0_group(key) - pub_key = OpenSSL.EC_POINT_new(group) - - if (OpenSSL.EC_POINT_set_affine_coordinates_GFp(group, pub_key, - pub_key_x, - pub_key_y, - 0)) == 0: - raise Exception( - "[OpenSSL] EC_POINT_set_affine_coordinates_GFp FAIL ...") - if (OpenSSL.EC_KEY_set_public_key(key, pub_key)) == 0: - raise Exception("[OpenSSL] EC_KEY_set_public_key FAIL ...") - if (OpenSSL.EC_KEY_check_key(key)) == 0: - raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") - if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: - OpenSSL.EVP_MD_CTX_new(md_ctx) - else: - OpenSSL.EVP_MD_CTX_init(md_ctx) - OpenSSL.EVP_DigestInit_ex(md_ctx, digest_alg(), None) - if (OpenSSL.EVP_DigestUpdate(md_ctx, binputb, len(inputb))) == 0: - raise Exception("[OpenSSL] EVP_DigestUpdate FAIL ...") - - OpenSSL.EVP_DigestFinal_ex(md_ctx, digest, dgst_len) - ret = OpenSSL.ECDSA_verify( - 0, digest, dgst_len.contents, bsig, len(sig), key) - - if ret == -1: - return False # Fail to Check - if ret == 0: - return False # Bad signature ! - return True # Good - - finally: - OpenSSL.EC_KEY_free(key) - OpenSSL.BN_free(pub_key_x) - OpenSSL.BN_free(pub_key_y) - OpenSSL.EC_POINT_free(pub_key) - if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: - OpenSSL.EVP_MD_CTX_free(md_ctx) - else: - OpenSSL.EVP_MD_CTX_destroy(md_ctx) - - @staticmethod - def encrypt(data, pubkey, ephemcurve=None, ciphername='aes-256-cbc'): - """ - Encrypt data with ECIES method using the public key of the recipient. - """ - curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) - return ECC.raw_encrypt(data, pubkey_x, pubkey_y, curve=curve, - ephemcurve=ephemcurve, ciphername=ciphername) - - @staticmethod - def raw_encrypt( - data, - pubkey_x, - pubkey_y, - curve='sect283r1', - ephemcurve=None, - ciphername='aes-256-cbc', - ): # pylint: disable=too-many-arguments - """ECHD encryption, keys supplied in binary data format""" - - if ephemcurve is None: - ephemcurve = curve - ephem = ECC(curve=ephemcurve) - key = sha512(ephem.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest() - key_e, key_m = key[:32], key[32:] - pubkey = ephem.get_pubkey() - iv = OpenSSL.rand(OpenSSL.get_cipher(ciphername).get_blocksize()) - ctx = Cipher(key_e, iv, 1, ciphername) - ciphertext = iv + pubkey + ctx.ciphering(data) - mac = hmac_sha256(key_m, ciphertext) - return ciphertext + mac - - def decrypt(self, data, ciphername='aes-256-cbc'): - """ - Decrypt data with ECIES method using the local private key - """ - # pylint: disable=too-many-locals - blocksize = OpenSSL.get_cipher(ciphername).get_blocksize() - iv = data[:blocksize] - i = blocksize - _, pubkey_x, pubkey_y, i2 = ECC._decode_pubkey(data[i:]) - i += i2 - ciphertext = data[i:len(data) - 32] - i += len(ciphertext) - mac = data[i:] - key = sha512(self.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest() - key_e, key_m = key[:32], key[32:] - if not equals(hmac_sha256(key_m, data[:len(data) - 32]), mac): - raise RuntimeError("Fail to verify data") - ctx = Cipher(key_e, iv, 0, ciphername) - return ctx.ciphering(ciphertext) diff --git a/src/lib/pyelliptic/hash.py b/src/lib/pyelliptic/hash.py deleted file mode 100644 index d6a15811..00000000 --- a/src/lib/pyelliptic/hash.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright (C) 2011 Yann GUIBET -# See LICENSE for details. - -from .openssl import OpenSSL - - -# For python3 -def _equals_bytes(a, b): - if len(a) != len(b): - return False - result = 0 - for x, y in zip(a, b): - result |= x ^ y - return result == 0 - - -def _equals_str(a, b): - if len(a) != len(b): - return False - result = 0 - for x, y in zip(a, b): - result |= ord(x) ^ ord(y) - return result == 0 - - -def equals(a, b): - if isinstance(a, str): - return _equals_str(a, b) - else: - return _equals_bytes(a, b) - - -def hmac_sha256(k, m): - """ - Compute the key and the message with HMAC SHA5256 - """ - key = OpenSSL.malloc(k, len(k)) - d = OpenSSL.malloc(m, len(m)) - md = OpenSSL.malloc(0, 32) - i = OpenSSL.pointer(OpenSSL.c_int(0)) - OpenSSL.HMAC(OpenSSL.EVP_sha256(), key, len(k), d, len(m), md, i) - return md.raw - - -def hmac_sha512(k, m): - """ - Compute the key and the message with HMAC SHA512 - """ - key = OpenSSL.malloc(k, len(k)) - d = OpenSSL.malloc(m, len(m)) - md = OpenSSL.malloc(0, 64) - i = OpenSSL.pointer(OpenSSL.c_int(0)) - OpenSSL.HMAC(OpenSSL.EVP_sha512(), key, len(k), d, len(m), md, i) - return md.raw - - -def pbkdf2(password, salt=None, i=10000, keylen=64): - if salt is None: - salt = OpenSSL.rand(8) - p_password = OpenSSL.malloc(password, len(password)) - p_salt = OpenSSL.malloc(salt, len(salt)) - output = OpenSSL.malloc(0, keylen) - OpenSSL.PKCS5_PBKDF2_HMAC(p_password, len(password), p_salt, - len(p_salt), i, OpenSSL.EVP_sha256(), - keylen, output) - return salt, output.raw diff --git a/src/lib/pyelliptic/openssl.py b/src/lib/pyelliptic/openssl.py deleted file mode 100644 index bc4fe6a6..00000000 --- a/src/lib/pyelliptic/openssl.py +++ /dev/null @@ -1,553 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright (C) 2011 Yann GUIBET -# See LICENSE for details. -# -# Software slightly changed by Jonathan Warren - -import sys -import ctypes - -OpenSSL = None - - -class CipherName: - def __init__(self, name, pointer, blocksize): - self._name = name - self._pointer = pointer - self._blocksize = blocksize - - def __str__(self): - return "Cipher : " + self._name + " | Blocksize : " + str(self._blocksize) + " | Fonction pointer : " + str(self._pointer) - - def get_pointer(self): - return self._pointer() - - def get_name(self): - return self._name - - def get_blocksize(self): - return self._blocksize - - -def get_version(library): - version = None - hexversion = None - cflags = None - try: - #OpenSSL 1.1 - OPENSSL_VERSION = 0 - OPENSSL_CFLAGS = 1 - library.OpenSSL_version.argtypes = [ctypes.c_int] - library.OpenSSL_version.restype = ctypes.c_char_p - version = library.OpenSSL_version(OPENSSL_VERSION) - cflags = library.OpenSSL_version(OPENSSL_CFLAGS) - library.OpenSSL_version_num.restype = ctypes.c_long - hexversion = library.OpenSSL_version_num() - except AttributeError: - try: - #OpenSSL 1.0 - SSLEAY_VERSION = 0 - SSLEAY_CFLAGS = 2 - library.SSLeay.restype = ctypes.c_long - library.SSLeay_version.restype = ctypes.c_char_p - library.SSLeay_version.argtypes = [ctypes.c_int] - version = library.SSLeay_version(SSLEAY_VERSION) - cflags = library.SSLeay_version(SSLEAY_CFLAGS) - hexversion = library.SSLeay() - except AttributeError: - #raise NotImplementedError('Cannot determine version of this OpenSSL library.') - pass - return (version, hexversion, cflags) - - -class _OpenSSL: - """ - Wrapper for OpenSSL using ctypes - """ - def __init__(self, library): - """ - Build the wrapper - """ - self._lib = ctypes.CDLL(library) - self._version, self._hexversion, self._cflags = get_version(self._lib) - self._libreSSL = self._version.startswith(b"LibreSSL") - - self.pointer = ctypes.pointer - self.c_int = ctypes.c_int - self.byref = ctypes.byref - self.create_string_buffer = ctypes.create_string_buffer - - self.BN_new = self._lib.BN_new - self.BN_new.restype = ctypes.c_void_p - self.BN_new.argtypes = [] - - self.BN_free = self._lib.BN_free - self.BN_free.restype = None - self.BN_free.argtypes = [ctypes.c_void_p] - - self.BN_num_bits = self._lib.BN_num_bits - self.BN_num_bits.restype = ctypes.c_int - self.BN_num_bits.argtypes = [ctypes.c_void_p] - - self.BN_bn2bin = self._lib.BN_bn2bin - self.BN_bn2bin.restype = ctypes.c_int - self.BN_bn2bin.argtypes = [ctypes.c_void_p, ctypes.c_void_p] - - self.BN_bin2bn = self._lib.BN_bin2bn - self.BN_bin2bn.restype = ctypes.c_void_p - self.BN_bin2bn.argtypes = [ctypes.c_void_p, ctypes.c_int, - ctypes.c_void_p] - - self.EC_KEY_free = self._lib.EC_KEY_free - self.EC_KEY_free.restype = None - self.EC_KEY_free.argtypes = [ctypes.c_void_p] - - self.EC_KEY_new_by_curve_name = self._lib.EC_KEY_new_by_curve_name - self.EC_KEY_new_by_curve_name.restype = ctypes.c_void_p - self.EC_KEY_new_by_curve_name.argtypes = [ctypes.c_int] - - self.EC_KEY_generate_key = self._lib.EC_KEY_generate_key - self.EC_KEY_generate_key.restype = ctypes.c_int - self.EC_KEY_generate_key.argtypes = [ctypes.c_void_p] - - self.EC_KEY_check_key = self._lib.EC_KEY_check_key - self.EC_KEY_check_key.restype = ctypes.c_int - self.EC_KEY_check_key.argtypes = [ctypes.c_void_p] - - self.EC_KEY_get0_private_key = self._lib.EC_KEY_get0_private_key - self.EC_KEY_get0_private_key.restype = ctypes.c_void_p - self.EC_KEY_get0_private_key.argtypes = [ctypes.c_void_p] - - self.EC_KEY_get0_public_key = self._lib.EC_KEY_get0_public_key - self.EC_KEY_get0_public_key.restype = ctypes.c_void_p - self.EC_KEY_get0_public_key.argtypes = [ctypes.c_void_p] - - self.EC_KEY_get0_group = self._lib.EC_KEY_get0_group - self.EC_KEY_get0_group.restype = ctypes.c_void_p - self.EC_KEY_get0_group.argtypes = [ctypes.c_void_p] - - self.EC_POINT_get_affine_coordinates_GFp = self._lib.EC_POINT_get_affine_coordinates_GFp - self.EC_POINT_get_affine_coordinates_GFp.restype = ctypes.c_int - self.EC_POINT_get_affine_coordinates_GFp.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] - - self.EC_KEY_set_private_key = self._lib.EC_KEY_set_private_key - self.EC_KEY_set_private_key.restype = ctypes.c_int - self.EC_KEY_set_private_key.argtypes = [ctypes.c_void_p, - ctypes.c_void_p] - - self.EC_KEY_set_public_key = self._lib.EC_KEY_set_public_key - self.EC_KEY_set_public_key.restype = ctypes.c_int - self.EC_KEY_set_public_key.argtypes = [ctypes.c_void_p, - ctypes.c_void_p] - - self.EC_KEY_set_group = self._lib.EC_KEY_set_group - self.EC_KEY_set_group.restype = ctypes.c_int - self.EC_KEY_set_group.argtypes = [ctypes.c_void_p, ctypes.c_void_p] - - self.EC_POINT_set_affine_coordinates_GFp = self._lib.EC_POINT_set_affine_coordinates_GFp - self.EC_POINT_set_affine_coordinates_GFp.restype = ctypes.c_int - self.EC_POINT_set_affine_coordinates_GFp.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] - - self.EC_POINT_new = self._lib.EC_POINT_new - self.EC_POINT_new.restype = ctypes.c_void_p - self.EC_POINT_new.argtypes = [ctypes.c_void_p] - - self.EC_POINT_free = self._lib.EC_POINT_free - self.EC_POINT_free.restype = None - self.EC_POINT_free.argtypes = [ctypes.c_void_p] - - self.BN_CTX_free = self._lib.BN_CTX_free - self.BN_CTX_free.restype = None - self.BN_CTX_free.argtypes = [ctypes.c_void_p] - - self.EC_POINT_mul = self._lib.EC_POINT_mul - self.EC_POINT_mul.restype = ctypes.c_int - self.EC_POINT_mul.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] - - self.EC_KEY_set_private_key = self._lib.EC_KEY_set_private_key - self.EC_KEY_set_private_key.restype = ctypes.c_int - self.EC_KEY_set_private_key.argtypes = [ctypes.c_void_p, - ctypes.c_void_p] - - if self._hexversion >= 0x10100000 and not self._libreSSL: - self.EC_KEY_OpenSSL = self._lib.EC_KEY_OpenSSL - self._lib.EC_KEY_OpenSSL.restype = ctypes.c_void_p - self._lib.EC_KEY_OpenSSL.argtypes = [] - - self.EC_KEY_set_method = self._lib.EC_KEY_set_method - self._lib.EC_KEY_set_method.restype = ctypes.c_int - self._lib.EC_KEY_set_method.argtypes = [ctypes.c_void_p, ctypes.c_void_p] - else: - self.ECDH_OpenSSL = self._lib.ECDH_OpenSSL - self._lib.ECDH_OpenSSL.restype = ctypes.c_void_p - self._lib.ECDH_OpenSSL.argtypes = [] - - self.ECDH_set_method = self._lib.ECDH_set_method - self._lib.ECDH_set_method.restype = ctypes.c_int - self._lib.ECDH_set_method.argtypes = [ctypes.c_void_p, ctypes.c_void_p] - - self.BN_CTX_new = self._lib.BN_CTX_new - self._lib.BN_CTX_new.restype = ctypes.c_void_p - self._lib.BN_CTX_new.argtypes = [] - - self.ECDH_compute_key = self._lib.ECDH_compute_key - self.ECDH_compute_key.restype = ctypes.c_int - self.ECDH_compute_key.argtypes = [ctypes.c_void_p, - ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p] - - self.EVP_CipherInit_ex = self._lib.EVP_CipherInit_ex - self.EVP_CipherInit_ex.restype = ctypes.c_int - self.EVP_CipherInit_ex.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_void_p] - - self.EVP_CIPHER_CTX_new = self._lib.EVP_CIPHER_CTX_new - self.EVP_CIPHER_CTX_new.restype = ctypes.c_void_p - self.EVP_CIPHER_CTX_new.argtypes = [] - - # Cipher - self.EVP_aes_128_cfb128 = self._lib.EVP_aes_128_cfb128 - self.EVP_aes_128_cfb128.restype = ctypes.c_void_p - self.EVP_aes_128_cfb128.argtypes = [] - - self.EVP_aes_256_cfb128 = self._lib.EVP_aes_256_cfb128 - self.EVP_aes_256_cfb128.restype = ctypes.c_void_p - self.EVP_aes_256_cfb128.argtypes = [] - - self.EVP_aes_128_cbc = self._lib.EVP_aes_128_cbc - self.EVP_aes_128_cbc.restype = ctypes.c_void_p - self.EVP_aes_128_cbc.argtypes = [] - - self.EVP_aes_256_cbc = self._lib.EVP_aes_256_cbc - self.EVP_aes_256_cbc.restype = ctypes.c_void_p - self.EVP_aes_256_cbc.argtypes = [] - - #self.EVP_aes_128_ctr = self._lib.EVP_aes_128_ctr - #self.EVP_aes_128_ctr.restype = ctypes.c_void_p - #self.EVP_aes_128_ctr.argtypes = [] - - #self.EVP_aes_256_ctr = self._lib.EVP_aes_256_ctr - #self.EVP_aes_256_ctr.restype = ctypes.c_void_p - #self.EVP_aes_256_ctr.argtypes = [] - - self.EVP_aes_128_ofb = self._lib.EVP_aes_128_ofb - self.EVP_aes_128_ofb.restype = ctypes.c_void_p - self.EVP_aes_128_ofb.argtypes = [] - - self.EVP_aes_256_ofb = self._lib.EVP_aes_256_ofb - self.EVP_aes_256_ofb.restype = ctypes.c_void_p - self.EVP_aes_256_ofb.argtypes = [] - - self.EVP_bf_cbc = self._lib.EVP_bf_cbc - self.EVP_bf_cbc.restype = ctypes.c_void_p - self.EVP_bf_cbc.argtypes = [] - - self.EVP_bf_cfb64 = self._lib.EVP_bf_cfb64 - self.EVP_bf_cfb64.restype = ctypes.c_void_p - self.EVP_bf_cfb64.argtypes = [] - - self.EVP_rc4 = self._lib.EVP_rc4 - self.EVP_rc4.restype = ctypes.c_void_p - self.EVP_rc4.argtypes = [] - - if self._hexversion >= 0x10100000 and not self._libreSSL: - self.EVP_CIPHER_CTX_reset = self._lib.EVP_CIPHER_CTX_reset - self.EVP_CIPHER_CTX_reset.restype = ctypes.c_int - self.EVP_CIPHER_CTX_reset.argtypes = [ctypes.c_void_p] - else: - self.EVP_CIPHER_CTX_cleanup = self._lib.EVP_CIPHER_CTX_cleanup - self.EVP_CIPHER_CTX_cleanup.restype = ctypes.c_int - self.EVP_CIPHER_CTX_cleanup.argtypes = [ctypes.c_void_p] - - self.EVP_CIPHER_CTX_free = self._lib.EVP_CIPHER_CTX_free - self.EVP_CIPHER_CTX_free.restype = None - self.EVP_CIPHER_CTX_free.argtypes = [ctypes.c_void_p] - - self.EVP_CipherUpdate = self._lib.EVP_CipherUpdate - self.EVP_CipherUpdate.restype = ctypes.c_int - self.EVP_CipherUpdate.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int] - - self.EVP_CipherFinal_ex = self._lib.EVP_CipherFinal_ex - self.EVP_CipherFinal_ex.restype = ctypes.c_int - self.EVP_CipherFinal_ex.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_void_p] - - self.EVP_DigestInit = self._lib.EVP_DigestInit - self.EVP_DigestInit.restype = ctypes.c_int - self._lib.EVP_DigestInit.argtypes = [ctypes.c_void_p, ctypes.c_void_p] - - self.EVP_DigestInit_ex = self._lib.EVP_DigestInit_ex - self.EVP_DigestInit_ex.restype = ctypes.c_int - self._lib.EVP_DigestInit_ex.argtypes = 3 * [ctypes.c_void_p] - - self.EVP_DigestUpdate = self._lib.EVP_DigestUpdate - self.EVP_DigestUpdate.restype = ctypes.c_int - self.EVP_DigestUpdate.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_int] - - self.EVP_DigestFinal = self._lib.EVP_DigestFinal - self.EVP_DigestFinal.restype = ctypes.c_int - self.EVP_DigestFinal.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_void_p] - - self.EVP_DigestFinal_ex = self._lib.EVP_DigestFinal_ex - self.EVP_DigestFinal_ex.restype = ctypes.c_int - self.EVP_DigestFinal_ex.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_void_p] - - self.ECDSA_sign = self._lib.ECDSA_sign - self.ECDSA_sign.restype = ctypes.c_int - self.ECDSA_sign.argtypes = [ctypes.c_int, ctypes.c_void_p, - ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] - - self.ECDSA_verify = self._lib.ECDSA_verify - self.ECDSA_verify.restype = ctypes.c_int - self.ECDSA_verify.argtypes = [ctypes.c_int, ctypes.c_void_p, - ctypes.c_int, ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p] - - if self._hexversion >= 0x10100000 and not self._libreSSL: - self.EVP_MD_CTX_new = self._lib.EVP_MD_CTX_new - self.EVP_MD_CTX_new.restype = ctypes.c_void_p - self.EVP_MD_CTX_new.argtypes = [] - - self.EVP_MD_CTX_reset = self._lib.EVP_MD_CTX_reset - self.EVP_MD_CTX_reset.restype = None - self.EVP_MD_CTX_reset.argtypes = [ctypes.c_void_p] - - self.EVP_MD_CTX_free = self._lib.EVP_MD_CTX_free - self.EVP_MD_CTX_free.restype = None - self.EVP_MD_CTX_free.argtypes = [ctypes.c_void_p] - - self.EVP_sha1 = self._lib.EVP_sha1 - self.EVP_sha1.restype = ctypes.c_void_p - self.EVP_sha1.argtypes = [] - - self.digest_ecdsa_sha1 = self.EVP_sha1 - else: - self.EVP_MD_CTX_create = self._lib.EVP_MD_CTX_create - self.EVP_MD_CTX_create.restype = ctypes.c_void_p - self.EVP_MD_CTX_create.argtypes = [] - - self.EVP_MD_CTX_init = self._lib.EVP_MD_CTX_init - self.EVP_MD_CTX_init.restype = None - self.EVP_MD_CTX_init.argtypes = [ctypes.c_void_p] - - self.EVP_MD_CTX_destroy = self._lib.EVP_MD_CTX_destroy - self.EVP_MD_CTX_destroy.restype = None - self.EVP_MD_CTX_destroy.argtypes = [ctypes.c_void_p] - - self.EVP_ecdsa = self._lib.EVP_ecdsa - self._lib.EVP_ecdsa.restype = ctypes.c_void_p - self._lib.EVP_ecdsa.argtypes = [] - - self.digest_ecdsa_sha1 = self.EVP_ecdsa - - self.RAND_bytes = self._lib.RAND_bytes - self.RAND_bytes.restype = ctypes.c_int - self.RAND_bytes.argtypes = [ctypes.c_void_p, ctypes.c_int] - - self.EVP_sha256 = self._lib.EVP_sha256 - self.EVP_sha256.restype = ctypes.c_void_p - self.EVP_sha256.argtypes = [] - - self.i2o_ECPublicKey = self._lib.i2o_ECPublicKey - self.i2o_ECPublicKey.restype = ctypes.c_int - self.i2o_ECPublicKey.argtypes = [ctypes.c_void_p, ctypes.c_void_p] - - self.EVP_sha512 = self._lib.EVP_sha512 - self.EVP_sha512.restype = ctypes.c_void_p - self.EVP_sha512.argtypes = [] - - self.HMAC = self._lib.HMAC - self.HMAC.restype = ctypes.c_void_p - self.HMAC.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, - ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p] - - try: - self.PKCS5_PBKDF2_HMAC = self._lib.PKCS5_PBKDF2_HMAC - except: - # The above is not compatible with all versions of OSX. - self.PKCS5_PBKDF2_HMAC = self._lib.PKCS5_PBKDF2_HMAC_SHA1 - - self.PKCS5_PBKDF2_HMAC.restype = ctypes.c_int - self.PKCS5_PBKDF2_HMAC.argtypes = [ctypes.c_void_p, ctypes.c_int, - ctypes.c_void_p, ctypes.c_int, - ctypes.c_int, ctypes.c_void_p, - ctypes.c_int, ctypes.c_void_p] - - self._set_ciphers() - self._set_curves() - - def _set_ciphers(self): - self.cipher_algo = { - 'aes-128-cbc': CipherName('aes-128-cbc', self.EVP_aes_128_cbc, 16), - 'aes-256-cbc': CipherName('aes-256-cbc', self.EVP_aes_256_cbc, 16), - 'aes-128-cfb': CipherName('aes-128-cfb', self.EVP_aes_128_cfb128, 16), - 'aes-256-cfb': CipherName('aes-256-cfb', self.EVP_aes_256_cfb128, 16), - 'aes-128-ofb': CipherName('aes-128-ofb', self._lib.EVP_aes_128_ofb, 16), - 'aes-256-ofb': CipherName('aes-256-ofb', self._lib.EVP_aes_256_ofb, 16), - #'aes-128-ctr': CipherName('aes-128-ctr', self._lib.EVP_aes_128_ctr, 16), - #'aes-256-ctr': CipherName('aes-256-ctr', self._lib.EVP_aes_256_ctr, 16), - 'bf-cfb': CipherName('bf-cfb', self.EVP_bf_cfb64, 8), - 'bf-cbc': CipherName('bf-cbc', self.EVP_bf_cbc, 8), - 'rc4': CipherName('rc4', self.EVP_rc4, 128), # 128 is the initialisation size not block size - } - - def _set_curves(self): - self.curves = { - 'secp112r1': 704, - 'secp112r2': 705, - 'secp128r1': 706, - 'secp128r2': 707, - 'secp160k1': 708, - 'secp160r1': 709, - 'secp160r2': 710, - 'secp192k1': 711, - 'secp224k1': 712, - 'secp224r1': 713, - 'secp256k1': 714, - 'secp384r1': 715, - 'secp521r1': 716, - 'sect113r1': 717, - 'sect113r2': 718, - 'sect131r1': 719, - 'sect131r2': 720, - 'sect163k1': 721, - 'sect163r1': 722, - 'sect163r2': 723, - 'sect193r1': 724, - 'sect193r2': 725, - 'sect233k1': 726, - 'sect233r1': 727, - 'sect239k1': 728, - 'sect283k1': 729, - 'sect283r1': 730, - 'sect409k1': 731, - 'sect409r1': 732, - 'sect571k1': 733, - 'sect571r1': 734, - } - - def BN_num_bytes(self, x): - """ - returns the length of a BN (OpenSSl API) - """ - return int((self.BN_num_bits(x) + 7) / 8) - - def get_cipher(self, name): - """ - returns the OpenSSL cipher instance - """ - if name not in self.cipher_algo: - raise Exception("Unknown cipher") - return self.cipher_algo[name] - - def get_curve(self, name): - """ - returns the id of a elliptic curve - """ - if name not in self.curves: - raise Exception("Unknown curve") - return self.curves[name] - - def get_curve_by_id(self, id): - """ - returns the name of a elliptic curve with his id - """ - res = None - for i in self.curves: - if self.curves[i] == id: - res = i - break - if res is None: - raise Exception("Unknown curve") - return res - - def rand(self, size): - """ - OpenSSL random function - """ - buffer = self.malloc(0, size) - # This pyelliptic library, by default, didn't check the return value of RAND_bytes. It is - # evidently possible that it returned an error and not-actually-random data. However, in - # tests on various operating systems, while generating hundreds of gigabytes of random - # strings of various sizes I could not get an error to occur. Also Bitcoin doesn't check - # the return value of RAND_bytes either. - # Fixed in Bitmessage version 0.4.2 (in source code on 2013-10-13) - while self.RAND_bytes(buffer, size) != 1: - import time - time.sleep(1) - return buffer.raw - - def malloc(self, data, size): - """ - returns a create_string_buffer (ctypes) - """ - buffer = None - if data != 0: - if sys.version_info.major == 3 and isinstance(data, type('')): - data = data.encode() - buffer = self.create_string_buffer(data, size) - else: - buffer = self.create_string_buffer(size) - return buffer - -def loadOpenSSL(): - global OpenSSL - from os import path, environ - from ctypes.util import find_library - - libdir = [] - - if 'linux' in sys.platform or 'darwin' in sys.platform or 'bsd' in sys.platform: - libdir.append(find_library('ssl')) - elif 'win32' in sys.platform or 'win64' in sys.platform: - libdir.append(find_library('libeay32')) - - if getattr(sys,'frozen', None): - if 'darwin' in sys.platform: - libdir.extend([ - path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.dylib'), - path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.1.0.dylib'), - path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.0.2.dylib'), - path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.0.1.dylib'), - path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.0.0.dylib'), - path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.0.9.8.dylib'), - ]) - elif 'win32' in sys.platform or 'win64' in sys.platform: - libdir.append(path.join(sys._MEIPASS, 'libeay32.dll')) - else: - libdir.extend([ - path.join(sys._MEIPASS, 'libcrypto.so'), - path.join(sys._MEIPASS, 'libssl.so'), - path.join(sys._MEIPASS, 'libcrypto.so.1.1.0'), - path.join(sys._MEIPASS, 'libssl.so.1.1.0'), - path.join(sys._MEIPASS, 'libcrypto.so.1.0.2'), - path.join(sys._MEIPASS, 'libssl.so.1.0.2'), - path.join(sys._MEIPASS, 'libcrypto.so.1.0.1'), - path.join(sys._MEIPASS, 'libssl.so.1.0.1'), - path.join(sys._MEIPASS, 'libcrypto.so.1.0.0'), - path.join(sys._MEIPASS, 'libssl.so.1.0.0'), - path.join(sys._MEIPASS, 'libcrypto.so.0.9.8'), - path.join(sys._MEIPASS, 'libssl.so.0.9.8'), - ]) - if 'darwin' in sys.platform: - libdir.extend(['libcrypto.dylib', '/usr/local/opt/openssl/lib/libcrypto.dylib']) - elif 'win32' in sys.platform or 'win64' in sys.platform: - libdir.append('libeay32.dll') - else: - libdir.append('libcrypto.so') - libdir.append('libssl.so') - libdir.append('libcrypto.so.1.0.0') - libdir.append('libssl.so.1.0.0') - for library in libdir: - try: - OpenSSL = _OpenSSL(library) - return - except: - pass - raise Exception("Failed to load OpenSSL library, searched for: " + " ".join(libdir)) - -loadOpenSSL() diff --git a/src/lib/pyelliptic/setup.py b/src/lib/pyelliptic/setup.py deleted file mode 100644 index cc9c0a21..00000000 --- a/src/lib/pyelliptic/setup.py +++ /dev/null @@ -1,23 +0,0 @@ -from setuptools import setup, find_packages - -setup( - name="pyelliptic", - version='2.0.1', - url='https://github.com/radfish/pyelliptic', - license='GPL', - description="Python OpenSSL wrapper for ECC (ECDSA, ECIES), AES, HMAC, Blowfish, ...", - author='Yann GUIBET', - author_email='yannguibet@gmail.com', - maintainer="redfish", - maintainer_email='redfish@galactica.pw', - packages=find_packages(), - classifiers=[ - 'Operating System :: Unix', - 'Operating System :: Microsoft :: Windows', - 'Environment :: MacOS X', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.7', - 'Topic :: Security :: Cryptography', - ], -) diff --git a/src/lib/sslcrypto/LICENSE b/src/lib/sslcrypto/LICENSE new file mode 100644 index 00000000..2feefc45 --- /dev/null +++ b/src/lib/sslcrypto/LICENSE @@ -0,0 +1,27 @@ +MIT License + +Copyright (c) 2019 Ivan Machugovskiy + +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. + + +Additionally, the following licenses must be preserved: + +- ripemd implementation is licensed under BSD-3 by Markus Friedl, see `_ripemd.py`; +- jacobian curve implementation is dual-licensed under MIT or public domain license, see `_jacobian.py`. diff --git a/src/lib/sslcrypto/__init__.py b/src/lib/sslcrypto/__init__.py new file mode 100644 index 00000000..77f9b3f3 --- /dev/null +++ b/src/lib/sslcrypto/__init__.py @@ -0,0 +1,6 @@ +__all__ = ["aes", "ecc", "rsa"] + +try: + from .openssl import aes, ecc, rsa +except OSError: + from .fallback import aes, ecc, rsa diff --git a/src/lib/sslcrypto/_aes.py b/src/lib/sslcrypto/_aes.py new file mode 100644 index 00000000..4f8d4ec2 --- /dev/null +++ b/src/lib/sslcrypto/_aes.py @@ -0,0 +1,53 @@ +# pylint: disable=import-outside-toplevel + +class AES: + def __init__(self, backend, fallback=None): + self._backend = backend + self._fallback = fallback + + + def get_algo_key_length(self, algo): + if algo.count("-") != 2: + raise ValueError("Invalid algorithm name") + try: + return int(algo.split("-")[1]) // 8 + except ValueError: + raise ValueError("Invalid algorithm name") from None + + + def new_key(self, algo="aes-256-cbc"): + if not self._backend.is_algo_supported(algo): + if self._fallback is None: + raise ValueError("This algorithm is not supported") + return self._fallback.new_key(algo) + return self._backend.random(self.get_algo_key_length(algo)) + + + def encrypt(self, data, key, algo="aes-256-cbc"): + if not self._backend.is_algo_supported(algo): + if self._fallback is None: + raise ValueError("This algorithm is not supported") + return self._fallback.encrypt(data, key, algo) + + key_length = self.get_algo_key_length(algo) + if len(key) != key_length: + raise ValueError("Expected key to be {} bytes, got {} bytes".format(key_length, len(key))) + + return self._backend.encrypt(data, key, algo) + + + def decrypt(self, ciphertext, iv, key, algo="aes-256-cbc"): + if not self._backend.is_algo_supported(algo): + if self._fallback is None: + raise ValueError("This algorithm is not supported") + return self._fallback.decrypt(ciphertext, iv, key, algo) + + key_length = self.get_algo_key_length(algo) + if len(key) != key_length: + raise ValueError("Expected key to be {} bytes, got {} bytes".format(key_length, len(key))) + + return self._backend.decrypt(ciphertext, iv, key, algo) + + + def get_backend(self): + return self._backend.get_backend() diff --git a/src/lib/sslcrypto/_ecc.py b/src/lib/sslcrypto/_ecc.py new file mode 100644 index 00000000..9831d688 --- /dev/null +++ b/src/lib/sslcrypto/_ecc.py @@ -0,0 +1,334 @@ +import hashlib +import struct +import hmac +import base58 + + +try: + hashlib.new("ripemd160") +except ValueError: + # No native implementation + from . import _ripemd + def ripemd160(*args): + return _ripemd.new(*args) +else: + # Use OpenSSL + def ripemd160(*args): + return hashlib.new("ripemd160", *args) + + +class ECC: + CURVES = { + "secp112r1": 704, + "secp112r2": 705, + "secp128r1": 706, + "secp128r2": 707, + "secp160k1": 708, + "secp160r1": 709, + "secp160r2": 710, + "secp192k1": 711, + "prime192v1": 409, + "secp224k1": 712, + "secp224r1": 713, + "secp256k1": 714, + "prime256v1": 415, + "secp384r1": 715, + "secp521r1": 716 + } + + def __init__(self, backend, aes): + self._backend = backend + self._aes = aes + + + def get_curve(self, name): + if name not in self.CURVES: + raise ValueError("Unknown curve {}".format(name)) + nid = self.CURVES[name] + return EllipticCurve(self._backend(nid), self._aes, nid) + + + def get_backend(self): + return self._backend.get_backend() + + +class EllipticCurve: + def __init__(self, backend, aes, nid): + self._backend = backend + self._aes = aes + self.nid = nid + + + def _encode_public_key(self, x, y, is_compressed=True, raw=True): + if raw: + if is_compressed: + return bytes([0x02 + (y[-1] % 2)]) + x + else: + return bytes([0x04]) + x + y + else: + return struct.pack("!HH", self.nid, len(x)) + x + struct.pack("!H", len(y)) + y + + + def _decode_public_key(self, public_key, partial=False): + if not public_key: + raise ValueError("No public key") + + if public_key[0] == 0x04: + # Uncompressed + expected_length = 1 + 2 * self._backend.public_key_length + if partial: + if len(public_key) < expected_length: + raise ValueError("Invalid uncompressed public key length") + else: + if len(public_key) != expected_length: + raise ValueError("Invalid uncompressed public key length") + x = public_key[1:1 + self._backend.public_key_length] + y = public_key[1 + self._backend.public_key_length:expected_length] + if partial: + return (x, y), expected_length + else: + return x, y + elif public_key[0] in (0x02, 0x03): + # Compressed + expected_length = 1 + self._backend.public_key_length + if partial: + if len(public_key) < expected_length: + raise ValueError("Invalid compressed public key length") + else: + if len(public_key) != expected_length: + raise ValueError("Invalid compressed public key length") + + x, y = self._backend.decompress_point(public_key[:expected_length]) + # Sanity check + if x != public_key[1:expected_length]: + raise ValueError("Incorrect compressed public key") + if partial: + return (x, y), expected_length + else: + return x, y + else: + raise ValueError("Invalid public key prefix") + + + def _decode_public_key_openssl(self, public_key, partial=False): + if not public_key: + raise ValueError("No public key") + + i = 0 + + nid, = struct.unpack("!H", public_key[i:i + 2]) + i += 2 + if nid != self.nid: + raise ValueError("Wrong curve") + + xlen, = struct.unpack("!H", public_key[i:i + 2]) + i += 2 + if len(public_key) - i < xlen: + raise ValueError("Too short public key") + x = public_key[i:i + xlen] + i += xlen + + ylen, = struct.unpack("!H", public_key[i:i + 2]) + i += 2 + if len(public_key) - i < ylen: + raise ValueError("Too short public key") + y = public_key[i:i + ylen] + i += ylen + + if partial: + return (x, y), i + else: + if i < len(public_key): + raise ValueError("Too long public key") + return x, y + + + def new_private_key(self): + return self._backend.new_private_key() + + + def private_to_public(self, private_key, is_compressed=True): + x, y = self._backend.private_to_public(private_key) + return self._encode_public_key(x, y, is_compressed=is_compressed) + + + def private_to_wif(self, private_key): + return base58.b58encode_check(b"\x80" + private_key) + + + def wif_to_private(self, wif): + dec = base58.b58decode_check(wif) + if dec[0] != 0x80: + raise ValueError("Invalid network (expected mainnet)") + return dec[1:] + + + def public_to_address(self, public_key): + h = hashlib.sha256(public_key).digest() + hash160 = ripemd160(h).digest() + return base58.b58encode_check(b"\x00" + hash160) + + + def private_to_address(self, private_key, is_compressed=True): + # Kinda useless but left for quick migration from pybitcointools + return self.public_to_address(self.private_to_public(private_key, is_compressed=is_compressed)) + + + def derive(self, private_key, public_key): + if not isinstance(public_key, tuple): + public_key = self._decode_public_key(public_key) + return self._backend.ecdh(private_key, public_key) + + + def _digest(self, data, hash): + if hash is None: + return data + elif callable(hash): + return hash(data) + elif hash == "sha1": + return hashlib.sha1(data).digest() + elif hash == "sha256": + return hashlib.sha256(data).digest() + elif hash == "sha512": + return hashlib.sha512(data).digest() + else: + raise ValueError("Unknown hash/derivation method") + + + # High-level functions + def encrypt(self, data, public_key, algo="aes-256-cbc", derivation="sha256", mac="hmac-sha256", return_aes_key=False): + # Generate ephemeral private key + private_key = self.new_private_key() + + # Derive key + ecdh = self.derive(private_key, public_key) + key = self._digest(ecdh, derivation) + k_enc_len = self._aes.get_algo_key_length(algo) + if len(key) < k_enc_len: + raise ValueError("Too short digest") + k_enc, k_mac = key[:k_enc_len], key[k_enc_len:] + + # Encrypt + ciphertext, iv = self._aes.encrypt(data, k_enc, algo=algo) + ephem_public_key = self.private_to_public(private_key) + ephem_public_key = self._decode_public_key(ephem_public_key) + ephem_public_key = self._encode_public_key(*ephem_public_key, raw=False) + ciphertext = iv + ephem_public_key + ciphertext + + # Add MAC tag + if callable(mac): + tag = mac(k_mac, ciphertext) + elif mac == "hmac-sha256": + h = hmac.new(k_mac, digestmod="sha256") + h.update(ciphertext) + tag = h.digest() + elif mac == "hmac-sha512": + h = hmac.new(k_mac, digestmod="sha512") + h.update(ciphertext) + tag = h.digest() + elif mac is None: + tag = b"" + else: + raise ValueError("Unsupported MAC") + + if return_aes_key: + return ciphertext + tag, k_enc + else: + return ciphertext + tag + + + def decrypt(self, ciphertext, private_key, algo="aes-256-cbc", derivation="sha256", mac="hmac-sha256"): + # Get MAC tag + if callable(mac): + tag_length = mac.digest_size + elif mac == "hmac-sha256": + tag_length = hmac.new(b"", digestmod="sha256").digest_size + elif mac == "hmac-sha512": + tag_length = hmac.new(b"", digestmod="sha512").digest_size + elif mac is None: + tag_length = 0 + else: + raise ValueError("Unsupported MAC") + + if len(ciphertext) < tag_length: + raise ValueError("Ciphertext is too small to contain MAC tag") + if tag_length == 0: + tag = b"" + else: + ciphertext, tag = ciphertext[:-tag_length], ciphertext[-tag_length:] + + orig_ciphertext = ciphertext + + if len(ciphertext) < 16: + raise ValueError("Ciphertext is too small to contain IV") + iv, ciphertext = ciphertext[:16], ciphertext[16:] + + public_key, pos = self._decode_public_key_openssl(ciphertext, partial=True) + ciphertext = ciphertext[pos:] + + # Derive key + ecdh = self.derive(private_key, public_key) + key = self._digest(ecdh, derivation) + k_enc_len = self._aes.get_algo_key_length(algo) + if len(key) < k_enc_len: + raise ValueError("Too short digest") + k_enc, k_mac = key[:k_enc_len], key[k_enc_len:] + + # Verify MAC tag + if callable(mac): + expected_tag = mac(k_mac, orig_ciphertext) + elif mac == "hmac-sha256": + h = hmac.new(k_mac, digestmod="sha256") + h.update(orig_ciphertext) + expected_tag = h.digest() + elif mac == "hmac-sha512": + h = hmac.new(k_mac, digestmod="sha512") + h.update(orig_ciphertext) + expected_tag = h.digest() + elif mac is None: + expected_tag = b"" + + if not hmac.compare_digest(tag, expected_tag): + raise ValueError("Invalid MAC tag") + + return self._aes.decrypt(ciphertext, iv, k_enc, algo=algo) + + + def sign(self, data, private_key, hash="sha256", recoverable=False, is_compressed=True, entropy=None): + data = self._digest(data, hash) + if not entropy: + v = b"\x01" * len(data) + k = b"\x00" * len(data) + k = hmac.new(k, v + b"\x00" + private_key + data, "sha256").digest() + v = hmac.new(k, v, "sha256").digest() + k = hmac.new(k, v + b"\x01" + private_key + data, "sha256").digest() + v = hmac.new(k, v, "sha256").digest() + entropy = hmac.new(k, v, "sha256").digest() + return self._backend.sign(data, private_key, recoverable, is_compressed, entropy=entropy) + + + def recover(self, signature, data, hash="sha256"): + # Sanity check: is this signature recoverable? + if len(signature) != 1 + 2 * self._backend.public_key_length: + raise ValueError("Cannot recover an unrecoverable signature") + x, y = self._backend.recover(signature, self._digest(data, hash)) + is_compressed = signature[0] >= 31 + return self._encode_public_key(x, y, is_compressed=is_compressed) + + + def verify(self, signature, data, public_key, hash="sha256"): + if len(signature) == 1 + 2 * self._backend.public_key_length: + # Recoverable signature + signature = signature[1:] + if len(signature) != 2 * self._backend.public_key_length: + raise ValueError("Invalid signature format") + if not isinstance(public_key, tuple): + public_key = self._decode_public_key(public_key) + return self._backend.verify(signature, self._digest(data, hash), public_key) + + + def derive_child(self, seed, child): + # Based on BIP32 + if not 0 <= child < 2 ** 31: + raise ValueError("Invalid child index") + return self._backend.derive_child(seed, child) diff --git a/src/lib/sslcrypto/_ripemd.py b/src/lib/sslcrypto/_ripemd.py new file mode 100644 index 00000000..89377cc2 --- /dev/null +++ b/src/lib/sslcrypto/_ripemd.py @@ -0,0 +1,375 @@ +# Copyright (c) 2001 Markus Friedl. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# pylint: skip-file + +import sys + +digest_size = 20 +digestsize = 20 + +class RIPEMD160: + """ + Return a new RIPEMD160 object. An optional string argument + may be provided; if present, this string will be automatically + hashed. + """ + + def __init__(self, arg=None): + self.ctx = RMDContext() + if arg: + self.update(arg) + self.dig = None + + def update(self, arg): + RMD160Update(self.ctx, arg, len(arg)) + self.dig = None + + def digest(self): + if self.dig: + return self.dig + ctx = self.ctx.copy() + self.dig = RMD160Final(self.ctx) + self.ctx = ctx + return self.dig + + def hexdigest(self): + dig = self.digest() + hex_digest = "" + for d in dig: + hex_digest += "%02x" % d + return hex_digest + + def copy(self): + import copy + return copy.deepcopy(self) + + + +def new(arg=None): + """ + Return a new RIPEMD160 object. An optional string argument + may be provided; if present, this string will be automatically + hashed. + """ + return RIPEMD160(arg) + + + +# +# Private. +# + +class RMDContext: + def __init__(self): + self.state = [0x67452301, 0xEFCDAB89, 0x98BADCFE, + 0x10325476, 0xC3D2E1F0] # uint32 + self.count = 0 # uint64 + self.buffer = [0] * 64 # uchar + def copy(self): + ctx = RMDContext() + ctx.state = self.state[:] + ctx.count = self.count + ctx.buffer = self.buffer[:] + return ctx + +K0 = 0x00000000 +K1 = 0x5A827999 +K2 = 0x6ED9EBA1 +K3 = 0x8F1BBCDC +K4 = 0xA953FD4E + +KK0 = 0x50A28BE6 +KK1 = 0x5C4DD124 +KK2 = 0x6D703EF3 +KK3 = 0x7A6D76E9 +KK4 = 0x00000000 + +def ROL(n, x): + return ((x << n) & 0xffffffff) | (x >> (32 - n)) + +def F0(x, y, z): + return x ^ y ^ z + +def F1(x, y, z): + return (x & y) | (((~x) % 0x100000000) & z) + +def F2(x, y, z): + return (x | ((~y) % 0x100000000)) ^ z + +def F3(x, y, z): + return (x & z) | (((~z) % 0x100000000) & y) + +def F4(x, y, z): + return x ^ (y | ((~z) % 0x100000000)) + +def R(a, b, c, d, e, Fj, Kj, sj, rj, X): + a = ROL(sj, (a + Fj(b, c, d) + X[rj] + Kj) % 0x100000000) + e + c = ROL(10, c) + return a % 0x100000000, c + +PADDING = [0x80] + [0] * 63 + +import sys +import struct + +def RMD160Transform(state, block): # uint32 state[5], uchar block[64] + x = [0] * 16 + if sys.byteorder == "little": + x = struct.unpack("<16L", bytes(block[0:64])) + else: + raise ValueError("Big-endian platforms are not supported") + a = state[0] + b = state[1] + c = state[2] + d = state[3] + e = state[4] + + # Round 1 + a, c = R(a, b, c, d, e, F0, K0, 11, 0, x) + e, b = R(e, a, b, c, d, F0, K0, 14, 1, x) + d, a = R(d, e, a, b, c, F0, K0, 15, 2, x) + c, e = R(c, d, e, a, b, F0, K0, 12, 3, x) + b, d = R(b, c, d, e, a, F0, K0, 5, 4, x) + a, c = R(a, b, c, d, e, F0, K0, 8, 5, x) + e, b = R(e, a, b, c, d, F0, K0, 7, 6, x) + d, a = R(d, e, a, b, c, F0, K0, 9, 7, x) + c, e = R(c, d, e, a, b, F0, K0, 11, 8, x) + b, d = R(b, c, d, e, a, F0, K0, 13, 9, x) + a, c = R(a, b, c, d, e, F0, K0, 14, 10, x) + e, b = R(e, a, b, c, d, F0, K0, 15, 11, x) + d, a = R(d, e, a, b, c, F0, K0, 6, 12, x) + c, e = R(c, d, e, a, b, F0, K0, 7, 13, x) + b, d = R(b, c, d, e, a, F0, K0, 9, 14, x) + a, c = R(a, b, c, d, e, F0, K0, 8, 15, x) # #15 + # Round 2 + e, b = R(e, a, b, c, d, F1, K1, 7, 7, x) + d, a = R(d, e, a, b, c, F1, K1, 6, 4, x) + c, e = R(c, d, e, a, b, F1, K1, 8, 13, x) + b, d = R(b, c, d, e, a, F1, K1, 13, 1, x) + a, c = R(a, b, c, d, e, F1, K1, 11, 10, x) + e, b = R(e, a, b, c, d, F1, K1, 9, 6, x) + d, a = R(d, e, a, b, c, F1, K1, 7, 15, x) + c, e = R(c, d, e, a, b, F1, K1, 15, 3, x) + b, d = R(b, c, d, e, a, F1, K1, 7, 12, x) + a, c = R(a, b, c, d, e, F1, K1, 12, 0, x) + e, b = R(e, a, b, c, d, F1, K1, 15, 9, x) + d, a = R(d, e, a, b, c, F1, K1, 9, 5, x) + c, e = R(c, d, e, a, b, F1, K1, 11, 2, x) + b, d = R(b, c, d, e, a, F1, K1, 7, 14, x) + a, c = R(a, b, c, d, e, F1, K1, 13, 11, x) + e, b = R(e, a, b, c, d, F1, K1, 12, 8, x) # #31 + # Round 3 + d, a = R(d, e, a, b, c, F2, K2, 11, 3, x) + c, e = R(c, d, e, a, b, F2, K2, 13, 10, x) + b, d = R(b, c, d, e, a, F2, K2, 6, 14, x) + a, c = R(a, b, c, d, e, F2, K2, 7, 4, x) + e, b = R(e, a, b, c, d, F2, K2, 14, 9, x) + d, a = R(d, e, a, b, c, F2, K2, 9, 15, x) + c, e = R(c, d, e, a, b, F2, K2, 13, 8, x) + b, d = R(b, c, d, e, a, F2, K2, 15, 1, x) + a, c = R(a, b, c, d, e, F2, K2, 14, 2, x) + e, b = R(e, a, b, c, d, F2, K2, 8, 7, x) + d, a = R(d, e, a, b, c, F2, K2, 13, 0, x) + c, e = R(c, d, e, a, b, F2, K2, 6, 6, x) + b, d = R(b, c, d, e, a, F2, K2, 5, 13, x) + a, c = R(a, b, c, d, e, F2, K2, 12, 11, x) + e, b = R(e, a, b, c, d, F2, K2, 7, 5, x) + d, a = R(d, e, a, b, c, F2, K2, 5, 12, x) # #47 + # Round 4 + c, e = R(c, d, e, a, b, F3, K3, 11, 1, x) + b, d = R(b, c, d, e, a, F3, K3, 12, 9, x) + a, c = R(a, b, c, d, e, F3, K3, 14, 11, x) + e, b = R(e, a, b, c, d, F3, K3, 15, 10, x) + d, a = R(d, e, a, b, c, F3, K3, 14, 0, x) + c, e = R(c, d, e, a, b, F3, K3, 15, 8, x) + b, d = R(b, c, d, e, a, F3, K3, 9, 12, x) + a, c = R(a, b, c, d, e, F3, K3, 8, 4, x) + e, b = R(e, a, b, c, d, F3, K3, 9, 13, x) + d, a = R(d, e, a, b, c, F3, K3, 14, 3, x) + c, e = R(c, d, e, a, b, F3, K3, 5, 7, x) + b, d = R(b, c, d, e, a, F3, K3, 6, 15, x) + a, c = R(a, b, c, d, e, F3, K3, 8, 14, x) + e, b = R(e, a, b, c, d, F3, K3, 6, 5, x) + d, a = R(d, e, a, b, c, F3, K3, 5, 6, x) + c, e = R(c, d, e, a, b, F3, K3, 12, 2, x) # #63 + # Round 5 + b, d = R(b, c, d, e, a, F4, K4, 9, 4, x) + a, c = R(a, b, c, d, e, F4, K4, 15, 0, x) + e, b = R(e, a, b, c, d, F4, K4, 5, 5, x) + d, a = R(d, e, a, b, c, F4, K4, 11, 9, x) + c, e = R(c, d, e, a, b, F4, K4, 6, 7, x) + b, d = R(b, c, d, e, a, F4, K4, 8, 12, x) + a, c = R(a, b, c, d, e, F4, K4, 13, 2, x) + e, b = R(e, a, b, c, d, F4, K4, 12, 10, x) + d, a = R(d, e, a, b, c, F4, K4, 5, 14, x) + c, e = R(c, d, e, a, b, F4, K4, 12, 1, x) + b, d = R(b, c, d, e, a, F4, K4, 13, 3, x) + a, c = R(a, b, c, d, e, F4, K4, 14, 8, x) + e, b = R(e, a, b, c, d, F4, K4, 11, 11, x) + d, a = R(d, e, a, b, c, F4, K4, 8, 6, x) + c, e = R(c, d, e, a, b, F4, K4, 5, 15, x) + b, d = R(b, c, d, e, a, F4, K4, 6, 13, x) # #79 + + aa = a + bb = b + cc = c + dd = d + ee = e + + a = state[0] + b = state[1] + c = state[2] + d = state[3] + e = state[4] + + # Parallel round 1 + a, c = R(a, b, c, d, e, F4, KK0, 8, 5, x) + e, b = R(e, a, b, c, d, F4, KK0, 9, 14, x) + d, a = R(d, e, a, b, c, F4, KK0, 9, 7, x) + c, e = R(c, d, e, a, b, F4, KK0, 11, 0, x) + b, d = R(b, c, d, e, a, F4, KK0, 13, 9, x) + a, c = R(a, b, c, d, e, F4, KK0, 15, 2, x) + e, b = R(e, a, b, c, d, F4, KK0, 15, 11, x) + d, a = R(d, e, a, b, c, F4, KK0, 5, 4, x) + c, e = R(c, d, e, a, b, F4, KK0, 7, 13, x) + b, d = R(b, c, d, e, a, F4, KK0, 7, 6, x) + a, c = R(a, b, c, d, e, F4, KK0, 8, 15, x) + e, b = R(e, a, b, c, d, F4, KK0, 11, 8, x) + d, a = R(d, e, a, b, c, F4, KK0, 14, 1, x) + c, e = R(c, d, e, a, b, F4, KK0, 14, 10, x) + b, d = R(b, c, d, e, a, F4, KK0, 12, 3, x) + a, c = R(a, b, c, d, e, F4, KK0, 6, 12, x) # #15 + # Parallel round 2 + e, b = R(e, a, b, c, d, F3, KK1, 9, 6, x) + d, a = R(d, e, a, b, c, F3, KK1, 13, 11, x) + c, e = R(c, d, e, a, b, F3, KK1, 15, 3, x) + b, d = R(b, c, d, e, a, F3, KK1, 7, 7, x) + a, c = R(a, b, c, d, e, F3, KK1, 12, 0, x) + e, b = R(e, a, b, c, d, F3, KK1, 8, 13, x) + d, a = R(d, e, a, b, c, F3, KK1, 9, 5, x) + c, e = R(c, d, e, a, b, F3, KK1, 11, 10, x) + b, d = R(b, c, d, e, a, F3, KK1, 7, 14, x) + a, c = R(a, b, c, d, e, F3, KK1, 7, 15, x) + e, b = R(e, a, b, c, d, F3, KK1, 12, 8, x) + d, a = R(d, e, a, b, c, F3, KK1, 7, 12, x) + c, e = R(c, d, e, a, b, F3, KK1, 6, 4, x) + b, d = R(b, c, d, e, a, F3, KK1, 15, 9, x) + a, c = R(a, b, c, d, e, F3, KK1, 13, 1, x) + e, b = R(e, a, b, c, d, F3, KK1, 11, 2, x) # #31 + # Parallel round 3 + d, a = R(d, e, a, b, c, F2, KK2, 9, 15, x) + c, e = R(c, d, e, a, b, F2, KK2, 7, 5, x) + b, d = R(b, c, d, e, a, F2, KK2, 15, 1, x) + a, c = R(a, b, c, d, e, F2, KK2, 11, 3, x) + e, b = R(e, a, b, c, d, F2, KK2, 8, 7, x) + d, a = R(d, e, a, b, c, F2, KK2, 6, 14, x) + c, e = R(c, d, e, a, b, F2, KK2, 6, 6, x) + b, d = R(b, c, d, e, a, F2, KK2, 14, 9, x) + a, c = R(a, b, c, d, e, F2, KK2, 12, 11, x) + e, b = R(e, a, b, c, d, F2, KK2, 13, 8, x) + d, a = R(d, e, a, b, c, F2, KK2, 5, 12, x) + c, e = R(c, d, e, a, b, F2, KK2, 14, 2, x) + b, d = R(b, c, d, e, a, F2, KK2, 13, 10, x) + a, c = R(a, b, c, d, e, F2, KK2, 13, 0, x) + e, b = R(e, a, b, c, d, F2, KK2, 7, 4, x) + d, a = R(d, e, a, b, c, F2, KK2, 5, 13, x) # #47 + # Parallel round 4 + c, e = R(c, d, e, a, b, F1, KK3, 15, 8, x) + b, d = R(b, c, d, e, a, F1, KK3, 5, 6, x) + a, c = R(a, b, c, d, e, F1, KK3, 8, 4, x) + e, b = R(e, a, b, c, d, F1, KK3, 11, 1, x) + d, a = R(d, e, a, b, c, F1, KK3, 14, 3, x) + c, e = R(c, d, e, a, b, F1, KK3, 14, 11, x) + b, d = R(b, c, d, e, a, F1, KK3, 6, 15, x) + a, c = R(a, b, c, d, e, F1, KK3, 14, 0, x) + e, b = R(e, a, b, c, d, F1, KK3, 6, 5, x) + d, a = R(d, e, a, b, c, F1, KK3, 9, 12, x) + c, e = R(c, d, e, a, b, F1, KK3, 12, 2, x) + b, d = R(b, c, d, e, a, F1, KK3, 9, 13, x) + a, c = R(a, b, c, d, e, F1, KK3, 12, 9, x) + e, b = R(e, a, b, c, d, F1, KK3, 5, 7, x) + d, a = R(d, e, a, b, c, F1, KK3, 15, 10, x) + c, e = R(c, d, e, a, b, F1, KK3, 8, 14, x) # #63 + # Parallel round 5 + b, d = R(b, c, d, e, a, F0, KK4, 8, 12, x) + a, c = R(a, b, c, d, e, F0, KK4, 5, 15, x) + e, b = R(e, a, b, c, d, F0, KK4, 12, 10, x) + d, a = R(d, e, a, b, c, F0, KK4, 9, 4, x) + c, e = R(c, d, e, a, b, F0, KK4, 12, 1, x) + b, d = R(b, c, d, e, a, F0, KK4, 5, 5, x) + a, c = R(a, b, c, d, e, F0, KK4, 14, 8, x) + e, b = R(e, a, b, c, d, F0, KK4, 6, 7, x) + d, a = R(d, e, a, b, c, F0, KK4, 8, 6, x) + c, e = R(c, d, e, a, b, F0, KK4, 13, 2, x) + b, d = R(b, c, d, e, a, F0, KK4, 6, 13, x) + a, c = R(a, b, c, d, e, F0, KK4, 5, 14, x) + e, b = R(e, a, b, c, d, F0, KK4, 15, 0, x) + d, a = R(d, e, a, b, c, F0, KK4, 13, 3, x) + c, e = R(c, d, e, a, b, F0, KK4, 11, 9, x) + b, d = R(b, c, d, e, a, F0, KK4, 11, 11, x) # #79 + + t = (state[1] + cc + d) % 0x100000000 + state[1] = (state[2] + dd + e) % 0x100000000 + state[2] = (state[3] + ee + a) % 0x100000000 + state[3] = (state[4] + aa + b) % 0x100000000 + state[4] = (state[0] + bb + c) % 0x100000000 + state[0] = t % 0x100000000 + + +def RMD160Update(ctx, inp, inplen): + if type(inp) == str: + inp = [ord(i)&0xff for i in inp] + + have = int((ctx.count // 8) % 64) + inplen = int(inplen) + need = 64 - have + ctx.count += 8 * inplen + off = 0 + if inplen >= need: + if have: + for i in range(need): + ctx.buffer[have + i] = inp[i] + RMD160Transform(ctx.state, ctx.buffer) + off = need + have = 0 + while off + 64 <= inplen: + RMD160Transform(ctx.state, inp[off:]) #<--- + off += 64 + if off < inplen: + # memcpy(ctx->buffer + have, input+off, len-off) + for i in range(inplen - off): + ctx.buffer[have + i] = inp[off + i] + +def RMD160Final(ctx): + size = struct.pack("= self.n: + return self.jacobian_multiply(a, n % self.n, secret) + half = self.jacobian_multiply(a, n // 2, secret) + half_sq = self.jacobian_double(half) + if secret: + # A constant-time implementation + half_sq_a = self.jacobian_add(half_sq, a) + if n % 2 == 0: + result = half_sq + if n % 2 == 1: + result = half_sq_a + return result + else: + if n % 2 == 0: + return half_sq + return self.jacobian_add(half_sq, a) + + + def jacobian_shamir(self, a, n, b, m): + ab = self.jacobian_add(a, b) + if n < 0 or n >= self.n: + n %= self.n + if m < 0 or m >= self.n: + m %= self.n + res = 0, 0, 1 # point on infinity + for i in range(self.n_length - 1, -1, -1): + res = self.jacobian_double(res) + has_n = n & (1 << i) + has_m = m & (1 << i) + if has_n: + if has_m == 0: + res = self.jacobian_add(res, a) + if has_m != 0: + res = self.jacobian_add(res, ab) + else: + if has_m == 0: + res = self.jacobian_add(res, (0, 0, 1)) # Try not to leak + if has_m != 0: + res = self.jacobian_add(res, b) + return res + + + def fast_multiply(self, a, n, secret=False): + return self.from_jacobian(self.jacobian_multiply(self.to_jacobian(a), n, secret)) + + + def fast_add(self, a, b): + return self.from_jacobian(self.jacobian_add(self.to_jacobian(a), self.to_jacobian(b))) + + + def fast_shamir(self, a, n, b, m): + return self.from_jacobian(self.jacobian_shamir(self.to_jacobian(a), n, self.to_jacobian(b), m)) + + + def is_on_curve(self, a): + x, y = a + # Simple arithmetic check + if (pow(x, 3, self.p) + self.a * x + self.b) % self.p != y * y % self.p: + return False + # nP = point-at-infinity + return self.isinf(self.jacobian_multiply(self.to_jacobian(a), self.n)) diff --git a/src/lib/sslcrypto/fallback/_util.py b/src/lib/sslcrypto/fallback/_util.py new file mode 100644 index 00000000..2236ebee --- /dev/null +++ b/src/lib/sslcrypto/fallback/_util.py @@ -0,0 +1,79 @@ +def int_to_bytes(raw, length): + data = [] + for _ in range(length): + data.append(raw % 256) + raw //= 256 + return bytes(data[::-1]) + + +def bytes_to_int(data): + raw = 0 + for byte in data: + raw = raw * 256 + byte + return raw + + +def legendre(a, p): + res = pow(a, (p - 1) // 2, p) + if res == p - 1: + return -1 + else: + return res + + +def inverse(a, n): + if a == 0: + return 0 + lm, hm = 1, 0 + low, high = a % n, n + while low > 1: + r = high // low + nm, new = hm - lm * r, high - low * r + lm, low, hm, high = nm, new, lm, low + return lm % n + + +def square_root_mod_prime(n, p): + if n == 0: + return 0 + if p == 2: + return n # We should never get here but it might be useful + if legendre(n, p) != 1: + raise ValueError("No square root") + # Optimizations + if p % 4 == 3: + return pow(n, (p + 1) // 4, p) + # 1. By factoring out powers of 2, find Q and S such that p - 1 = + # Q * 2 ** S with Q odd + q = p - 1 + s = 0 + while q % 2 == 0: + q //= 2 + s += 1 + # 2. Search for z in Z/pZ which is a quadratic non-residue + z = 1 + while legendre(z, p) != -1: + z += 1 + m, c, t, r = s, pow(z, q, p), pow(n, q, p), pow(n, (q + 1) // 2, p) + while True: + if t == 0: + return 0 + elif t == 1: + return r + # Use repeated squaring to find the least i, 0 < i < M, such + # that t ** (2 ** i) = 1 + t_sq = t + i = 0 + for i in range(1, m): + t_sq = t_sq * t_sq % p + if t_sq == 1: + break + else: + raise ValueError("Should never get here") + # Let b = c ** (2 ** (m - i - 1)) + b = pow(c, 2 ** (m - i - 1), p) + m = i + c = b * b % p + t = t * b * b % p + r = r * b % p + return r diff --git a/src/lib/sslcrypto/fallback/aes.py b/src/lib/sslcrypto/fallback/aes.py new file mode 100644 index 00000000..e168bf34 --- /dev/null +++ b/src/lib/sslcrypto/fallback/aes.py @@ -0,0 +1,101 @@ +import os +import pyaes +from .._aes import AES + + +__all__ = ["aes"] + +class AESBackend: + def _get_algo_cipher_type(self, algo): + if not algo.startswith("aes-") or algo.count("-") != 2: + raise ValueError("Unknown cipher algorithm {}".format(algo)) + key_length, cipher_type = algo[4:].split("-") + if key_length not in ("128", "192", "256"): + raise ValueError("Unknown cipher algorithm {}".format(algo)) + if cipher_type not in ("cbc", "ctr", "cfb", "ofb"): + raise ValueError("Unknown cipher algorithm {}".format(algo)) + return cipher_type + + + def is_algo_supported(self, algo): + try: + self._get_algo_cipher_type(algo) + return True + except ValueError: + return False + + + def random(self, length): + return os.urandom(length) + + + def encrypt(self, data, key, algo="aes-256-cbc"): + cipher_type = self._get_algo_cipher_type(algo) + + # Generate random IV + iv = os.urandom(16) + + if cipher_type == "cbc": + cipher = pyaes.AESModeOfOperationCBC(key, iv=iv) + elif cipher_type == "ctr": + # The IV is actually a counter, not an IV but it does almost the + # same. Notice: pyaes always uses 1 as initial counter! Make sure + # not to call pyaes directly. + + # We kinda do two conversions here: from byte array to int here, and + # from int to byte array in pyaes internals. It's possible to fix that + # but I didn't notice any performance changes so I'm keeping clean code. + iv_int = 0 + for byte in iv: + iv_int = (iv_int * 256) + byte + counter = pyaes.Counter(iv_int) + cipher = pyaes.AESModeOfOperationCTR(key, counter=counter) + elif cipher_type == "cfb": + # Change segment size from default 8 bytes to 16 bytes for OpenSSL + # compatibility + cipher = pyaes.AESModeOfOperationCFB(key, iv, segment_size=16) + elif cipher_type == "ofb": + cipher = pyaes.AESModeOfOperationOFB(key, iv) + + encrypter = pyaes.Encrypter(cipher) + ciphertext = encrypter.feed(data) + ciphertext += encrypter.feed() + return ciphertext, iv + + + def decrypt(self, ciphertext, iv, key, algo="aes-256-cbc"): + cipher_type = self._get_algo_cipher_type(algo) + + if cipher_type == "cbc": + cipher = pyaes.AESModeOfOperationCBC(key, iv=iv) + elif cipher_type == "ctr": + # The IV is actually a counter, not an IV but it does almost the + # same. Notice: pyaes always uses 1 as initial counter! Make sure + # not to call pyaes directly. + + # We kinda do two conversions here: from byte array to int here, and + # from int to byte array in pyaes internals. It's possible to fix that + # but I didn't notice any performance changes so I'm keeping clean code. + iv_int = 0 + for byte in iv: + iv_int = (iv_int * 256) + byte + counter = pyaes.Counter(iv_int) + cipher = pyaes.AESModeOfOperationCTR(key, counter=counter) + elif cipher_type == "cfb": + # Change segment size from default 8 bytes to 16 bytes for OpenSSL + # compatibility + cipher = pyaes.AESModeOfOperationCFB(key, iv, segment_size=16) + elif cipher_type == "ofb": + cipher = pyaes.AESModeOfOperationOFB(key, iv) + + decrypter = pyaes.Decrypter(cipher) + data = decrypter.feed(ciphertext) + data += decrypter.feed() + return data + + + def get_backend(self): + return "fallback" + + +aes = AES(AESBackend()) diff --git a/src/lib/sslcrypto/fallback/ecc.py b/src/lib/sslcrypto/fallback/ecc.py new file mode 100644 index 00000000..3a438d4d --- /dev/null +++ b/src/lib/sslcrypto/fallback/ecc.py @@ -0,0 +1,371 @@ +import hmac +import os +from ._jacobian import JacobianCurve +from .._ecc import ECC +from .aes import aes +from ._util import int_to_bytes, bytes_to_int, inverse, square_root_mod_prime + + +# pylint: disable=line-too-long +CURVES = { + # nid: (p, n, a, b, (Gx, Gy)), + 704: ( + # secp112r1 + 0xDB7C2ABF62E35E668076BEAD208B, + 0xDB7C2ABF62E35E7628DFAC6561C5, + 0xDB7C2ABF62E35E668076BEAD2088, + 0x659EF8BA043916EEDE8911702B22, + ( + 0x09487239995A5EE76B55F9C2F098, + 0xA89CE5AF8724C0A23E0E0FF77500 + ) + ), + 705: ( + # secp112r2 + 0xDB7C2ABF62E35E668076BEAD208B, + 0x36DF0AAFD8B8D7597CA10520D04B, + 0x6127C24C05F38A0AAAF65C0EF02C, + 0x51DEF1815DB5ED74FCC34C85D709, + ( + 0x4BA30AB5E892B4E1649DD0928643, + 0xADCD46F5882E3747DEF36E956E97 + ) + ), + 706: ( + # secp128r1 + 0xFFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFF, + 0xFFFFFFFE0000000075A30D1B9038A115, + 0xFFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFC, + 0xE87579C11079F43DD824993C2CEE5ED3, + ( + 0x161FF7528B899B2D0C28607CA52C5B86, + 0xCF5AC8395BAFEB13C02DA292DDED7A83 + ) + ), + 707: ( + # secp128r2 + 0xFFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFF, + 0x3FFFFFFF7FFFFFFFBE0024720613B5A3, + 0xD6031998D1B3BBFEBF59CC9BBFF9AEE1, + 0x5EEEFCA380D02919DC2C6558BB6D8A5D, + ( + 0x7B6AA5D85E572983E6FB32A7CDEBC140, + 0x27B6916A894D3AEE7106FE805FC34B44 + ) + ), + 708: ( + # secp160k1 + 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFAC73, + 0x0100000000000000000001B8FA16DFAB9ACA16B6B3, + 0, + 7, + ( + 0x3B4C382CE37AA192A4019E763036F4F5DD4D7EBB, + 0x938CF935318FDCED6BC28286531733C3F03C4FEE + ) + ), + 709: ( + # secp160r1 + 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFF, + 0x0100000000000000000001F4C8F927AED3CA752257, + 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFC, + 0x001C97BEFC54BD7A8B65ACF89F81D4D4ADC565FA45, + ( + 0x4A96B5688EF573284664698968C38BB913CBFC82, + 0x23A628553168947D59DCC912042351377AC5FB32 + ) + ), + 710: ( + # secp160r2 + 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFAC73, + 0x0100000000000000000000351EE786A818F3A1A16B, + 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFAC70, + 0x00B4E134D3FB59EB8BAB57274904664D5AF50388BA, + ( + 0x52DCB034293A117E1F4FF11B30F7199D3144CE6D, + 0xFEAFFEF2E331F296E071FA0DF9982CFEA7D43F2E + ) + ), + 711: ( + # secp192k1 + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFEE37, + 0xFFFFFFFFFFFFFFFFFFFFFFFE26F2FC170F69466A74DEFD8D, + 0, + 3, + ( + 0xDB4FF10EC057E9AE26B07D0280B7F4341DA5D1B1EAE06C7D, + 0x9B2F2F6D9C5628A7844163D015BE86344082AA88D95E2F9D + ) + ), + 409: ( + # prime192v1 + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFF, + 0xFFFFFFFFFFFFFFFFFFFFFFFF99DEF836146BC9B1B4D22831, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFC, + 0x64210519E59C80E70FA7E9AB72243049FEB8DEECC146B9B1, + ( + 0x188DA80EB03090F67CBF20EB43A18800F4FF0AFD82FF1012, + 0x07192B95FFC8DA78631011ED6B24CDD573F977A11E794811 + ) + ), + 712: ( + # secp224k1 + 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFE56D, + 0x010000000000000000000000000001DCE8D2EC6184CAF0A971769FB1F7, + 0, + 5, + ( + 0xA1455B334DF099DF30FC28A169A467E9E47075A90F7E650EB6B7A45C, + 0x7E089FED7FBA344282CAFBD6F7E319F7C0B0BD59E2CA4BDB556D61A5 + ) + ), + 713: ( + # secp224r1 + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000001, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF16A2E0B8F03E13DD29455C5C2A3D, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFE, + 0xB4050A850C04B3ABF54132565044B0B7D7BFD8BA270B39432355FFB4, + ( + 0xB70E0CBD6BB4BF7F321390B94A03C1D356C21122343280D6115C1D21, + 0xBD376388B5F723FB4C22DFE6CD4375A05A07476444D5819985007E34 + ) + ), + 714: ( + # secp256k1 + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141, + 0, + 7, + ( + 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, + 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8 + ) + ), + 415: ( + # prime256v1 + 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF, + 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551, + 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC, + 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B, + ( + 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296, + 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5 + ) + ), + 715: ( + # secp384r1 + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC, + 0xB3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF, + ( + 0xAA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7, + 0x3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F + ) + ), + 716: ( + # secp521r1 + 0x01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, + 0x01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409, + 0x01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC, + 0x0051953EB9618E1C9A1F929A21A0B68540EEA2DA725B99B315F3B8B489918EF109E156193951EC7E937B1652C0BD3BB1BF073573DF883D2C34F1EF451FD46B503F00, + ( + 0x00C6858E06B70404E9CD9E3ECB662395B4429C648139053FB521F828AF606B4D3DBAA14B5E77EFE75928FE1DC127A2FFA8DE3348B3C1856A429BF97E7E31C2E5BD66, + 0x011839296A789A3BC0045C8A5FB42C7D1BD998F54449579B446817AFBD17273E662C97EE72995EF42640C550B9013FAD0761353C7086A272C24088BE94769FD16650 + ) + ) +} +# pylint: enable=line-too-long + + +class EllipticCurveBackend: + def __init__(self, nid): + self.p, self.n, self.a, self.b, self.g = CURVES[nid] + self.jacobian = JacobianCurve(*CURVES[nid]) + + self.public_key_length = (len(bin(self.p).replace("0b", "")) + 7) // 8 + self.order_bitlength = len(bin(self.n).replace("0b", "")) + + + def _int_to_bytes(self, raw, len=None): + return int_to_bytes(raw, len or self.public_key_length) + + + def decompress_point(self, public_key): + # Parse & load data + x = bytes_to_int(public_key[1:]) + # Calculate Y + y_square = (pow(x, 3, self.p) + self.a * x + self.b) % self.p + try: + y = square_root_mod_prime(y_square, self.p) + except Exception: + raise ValueError("Invalid public key") from None + if y % 2 != public_key[0] - 0x02: + y = self.p - y + return self._int_to_bytes(x), self._int_to_bytes(y) + + + def new_private_key(self): + while True: + private_key = os.urandom(self.public_key_length) + if bytes_to_int(private_key) >= self.n: + continue + return private_key + + + def private_to_public(self, private_key): + raw = bytes_to_int(private_key) + x, y = self.jacobian.fast_multiply(self.g, raw) + return self._int_to_bytes(x), self._int_to_bytes(y) + + + def ecdh(self, private_key, public_key): + x, y = public_key + x, y = bytes_to_int(x), bytes_to_int(y) + private_key = bytes_to_int(private_key) + x, _ = self.jacobian.fast_multiply((x, y), private_key, secret=True) + return self._int_to_bytes(x) + + + def _subject_to_int(self, subject): + return bytes_to_int(subject[:(self.order_bitlength + 7) // 8]) + + + def sign(self, subject, raw_private_key, recoverable, is_compressed, entropy): + z = self._subject_to_int(subject) + private_key = bytes_to_int(raw_private_key) + k = bytes_to_int(entropy) + + # Fix k length to prevent Minerva. Increasing multiplier by a + # multiple of order doesn't break anything. This fix was ported + # from python-ecdsa + ks = k + self.n + kt = ks + self.n + ks_len = len(bin(ks).replace("0b", "")) // 8 + kt_len = len(bin(kt).replace("0b", "")) // 8 + if ks_len == kt_len: + k = kt + else: + k = ks + px, py = self.jacobian.fast_multiply(self.g, k, secret=True) + + r = px % self.n + if r == 0: + # Invalid k + raise ValueError("Invalid k") + + s = (inverse(k, self.n) * (z + (private_key * r))) % self.n + if s == 0: + # Invalid k + raise ValueError("Invalid k") + + inverted = False + if s * 2 >= self.n: + s = self.n - s + inverted = True + rs_buf = self._int_to_bytes(r) + self._int_to_bytes(s) + + if recoverable: + recid = (py % 2) ^ inverted + recid += 2 * int(px // self.n) + if is_compressed: + return bytes([31 + recid]) + rs_buf + else: + if recid >= 4: + raise ValueError("Too big recovery ID, use compressed address instead") + return bytes([27 + recid]) + rs_buf + else: + return rs_buf + + + def recover(self, signature, subject): + z = self._subject_to_int(subject) + + recid = signature[0] - 27 if signature[0] < 31 else signature[0] - 31 + r = bytes_to_int(signature[1:self.public_key_length + 1]) + s = bytes_to_int(signature[self.public_key_length + 1:]) + + # Verify bounds + if not 0 <= recid < 2 * (self.p // self.n + 1): + raise ValueError("Invalid recovery ID") + if r >= self.n: + raise ValueError("r is out of bounds") + if s >= self.n: + raise ValueError("s is out of bounds") + + rinv = inverse(r, self.n) + u1 = (-z * rinv) % self.n + u2 = (s * rinv) % self.n + + # Recover R + rx = r + (recid // 2) * self.n + if rx >= self.p: + raise ValueError("Rx is out of bounds") + + # Almost copied from decompress_point + ry_square = (pow(rx, 3, self.p) + self.a * rx + self.b) % self.p + try: + ry = square_root_mod_prime(ry_square, self.p) + except Exception: + raise ValueError("Invalid recovered public key") from None + + # Ensure the point is correct + if ry % 2 != recid % 2: + # Fix Ry sign + ry = self.p - ry + + x, y = self.jacobian.fast_shamir(self.g, u1, (rx, ry), u2) + return self._int_to_bytes(x), self._int_to_bytes(y) + + + def verify(self, signature, subject, public_key): + z = self._subject_to_int(subject) + + r = bytes_to_int(signature[:self.public_key_length]) + s = bytes_to_int(signature[self.public_key_length:]) + + # Verify bounds + if r >= self.n: + raise ValueError("r is out of bounds") + if s >= self.n: + raise ValueError("s is out of bounds") + + public_key = [bytes_to_int(c) for c in public_key] + + # Ensure that the public key is correct + if not self.jacobian.is_on_curve(public_key): + raise ValueError("Public key is not on curve") + + sinv = inverse(s, self.n) + u1 = (z * sinv) % self.n + u2 = (r * sinv) % self.n + + x1, _ = self.jacobian.fast_shamir(self.g, u1, public_key, u2) + if r != x1 % self.n: + raise ValueError("Invalid signature") + + return True + + + def derive_child(self, seed, child): + # Round 1 + h = hmac.new(key=b"Bitcoin seed", msg=seed, digestmod="sha512").digest() + private_key1 = h[:32] + x, y = self.private_to_public(private_key1) + public_key1 = bytes([0x02 + (y[-1] % 2)]) + x + private_key1 = bytes_to_int(private_key1) + + # Round 2 + msg = public_key1 + self._int_to_bytes(child, 4) + h = hmac.new(key=h[32:], msg=msg, digestmod="sha512").digest() + private_key2 = bytes_to_int(h[:32]) + + return self._int_to_bytes((private_key1 + private_key2) % self.n) + + + @classmethod + def get_backend(cls): + return "fallback" + + +ecc = ECC(EllipticCurveBackend, aes) diff --git a/src/lib/sslcrypto/fallback/rsa.py b/src/lib/sslcrypto/fallback/rsa.py new file mode 100644 index 00000000..54b8d2cb --- /dev/null +++ b/src/lib/sslcrypto/fallback/rsa.py @@ -0,0 +1,8 @@ +# pylint: disable=too-few-public-methods + +class RSA: + def get_backend(self): + return "fallback" + + +rsa = RSA() diff --git a/src/lib/sslcrypto/openssl/__init__.py b/src/lib/sslcrypto/openssl/__init__.py new file mode 100644 index 00000000..a32ae692 --- /dev/null +++ b/src/lib/sslcrypto/openssl/__init__.py @@ -0,0 +1,3 @@ +from .aes import aes +from .ecc import ecc +from .rsa import rsa diff --git a/src/lib/sslcrypto/openssl/aes.py b/src/lib/sslcrypto/openssl/aes.py new file mode 100644 index 00000000..c58451d5 --- /dev/null +++ b/src/lib/sslcrypto/openssl/aes.py @@ -0,0 +1,156 @@ +import ctypes +import threading +from .._aes import AES +from ..fallback.aes import aes as fallback_aes +from .library import lib, openssl_backend + + +# Initialize functions +try: + lib.EVP_CIPHER_CTX_new.restype = ctypes.POINTER(ctypes.c_char) +except AttributeError: + pass +lib.EVP_get_cipherbyname.restype = ctypes.POINTER(ctypes.c_char) + + +thread_local = threading.local() + + +class Context: + def __init__(self, ptr, do_free): + self.lib = lib + self.ptr = ptr + self.do_free = do_free + + + def __del__(self): + if self.do_free: + self.lib.EVP_CIPHER_CTX_free(self.ptr) + + +class AESBackend: + ALGOS = ( + "aes-128-cbc", "aes-192-cbc", "aes-256-cbc", + "aes-128-ctr", "aes-192-ctr", "aes-256-ctr", + "aes-128-cfb", "aes-192-cfb", "aes-256-cfb", + "aes-128-ofb", "aes-192-ofb", "aes-256-ofb" + ) + + def __init__(self): + self.is_supported_ctx_new = hasattr(lib, "EVP_CIPHER_CTX_new") + self.is_supported_ctx_reset = hasattr(lib, "EVP_CIPHER_CTX_reset") + + + def _get_ctx(self): + if not hasattr(thread_local, "ctx"): + if self.is_supported_ctx_new: + thread_local.ctx = Context(lib.EVP_CIPHER_CTX_new(), True) + else: + # 1 KiB ought to be enough for everybody. We don't know the real + # size of the context buffer because we are unsure about padding and + # pointer size + thread_local.ctx = Context(ctypes.create_string_buffer(1024), False) + return thread_local.ctx.ptr + + + def get_backend(self): + return openssl_backend + + + def _get_cipher(self, algo): + if algo not in self.ALGOS: + raise ValueError("Unknown cipher algorithm {}".format(algo)) + cipher = lib.EVP_get_cipherbyname(algo.encode()) + if not cipher: + raise ValueError("Unknown cipher algorithm {}".format(algo)) + return cipher + + + def is_algo_supported(self, algo): + try: + self._get_cipher(algo) + return True + except ValueError: + return False + + + def random(self, length): + entropy = ctypes.create_string_buffer(length) + lib.RAND_bytes(entropy, length) + return bytes(entropy) + + + def encrypt(self, data, key, algo="aes-256-cbc"): + # Initialize context + ctx = self._get_ctx() + if not self.is_supported_ctx_new: + lib.EVP_CIPHER_CTX_init(ctx) + try: + lib.EVP_EncryptInit_ex(ctx, self._get_cipher(algo), None, None, None) + + # Generate random IV + iv_length = 16 + iv = self.random(iv_length) + + # Set key and IV + lib.EVP_EncryptInit_ex(ctx, None, None, key, iv) + + # Actually encrypt + block_size = 16 + output = ctypes.create_string_buffer((len(data) // block_size + 1) * block_size) + output_len = ctypes.c_int() + + if not lib.EVP_CipherUpdate(ctx, output, ctypes.byref(output_len), data, len(data)): + raise ValueError("Could not feed cipher with data") + + new_output = ctypes.byref(output, output_len.value) + output_len2 = ctypes.c_int() + if not lib.EVP_CipherFinal_ex(ctx, new_output, ctypes.byref(output_len2)): + raise ValueError("Could not finalize cipher") + + ciphertext = output[:output_len.value + output_len2.value] + return ciphertext, iv + finally: + if self.is_supported_ctx_reset: + lib.EVP_CIPHER_CTX_reset(ctx) + else: + lib.EVP_CIPHER_CTX_cleanup(ctx) + + + def decrypt(self, ciphertext, iv, key, algo="aes-256-cbc"): + # Initialize context + ctx = self._get_ctx() + if not self.is_supported_ctx_new: + lib.EVP_CIPHER_CTX_init(ctx) + try: + lib.EVP_DecryptInit_ex(ctx, self._get_cipher(algo), None, None, None) + + # Make sure IV length is correct + iv_length = 16 + if len(iv) != iv_length: + raise ValueError("Expected IV to be {} bytes, got {} bytes".format(iv_length, len(iv))) + + # Set key and IV + lib.EVP_DecryptInit_ex(ctx, None, None, key, iv) + + # Actually decrypt + output = ctypes.create_string_buffer(len(ciphertext)) + output_len = ctypes.c_int() + + if not lib.EVP_DecryptUpdate(ctx, output, ctypes.byref(output_len), ciphertext, len(ciphertext)): + raise ValueError("Could not feed decipher with ciphertext") + + new_output = ctypes.byref(output, output_len.value) + output_len2 = ctypes.c_int() + if not lib.EVP_DecryptFinal_ex(ctx, new_output, ctypes.byref(output_len2)): + raise ValueError("Could not finalize decipher") + + return output[:output_len.value + output_len2.value] + finally: + if self.is_supported_ctx_reset: + lib.EVP_CIPHER_CTX_reset(ctx) + else: + lib.EVP_CIPHER_CTX_cleanup(ctx) + + +aes = AES(AESBackend(), fallback_aes) diff --git a/src/lib/sslcrypto/openssl/discovery.py b/src/lib/sslcrypto/openssl/discovery.py new file mode 100644 index 00000000..0ebb0299 --- /dev/null +++ b/src/lib/sslcrypto/openssl/discovery.py @@ -0,0 +1,3 @@ +# Can be redefined by user +def discover(): + pass \ No newline at end of file diff --git a/src/lib/sslcrypto/openssl/ecc.py b/src/lib/sslcrypto/openssl/ecc.py new file mode 100644 index 00000000..5b5f0bde --- /dev/null +++ b/src/lib/sslcrypto/openssl/ecc.py @@ -0,0 +1,575 @@ +import ctypes +import hmac +import threading +from .._ecc import ECC +from .aes import aes +from .library import lib, openssl_backend + + +# Initialize functions +lib.BN_new.restype = ctypes.POINTER(ctypes.c_char) +lib.BN_bin2bn.restype = ctypes.POINTER(ctypes.c_char) +lib.BN_CTX_new.restype = ctypes.POINTER(ctypes.c_char) +lib.EC_GROUP_new_by_curve_name.restype = ctypes.POINTER(ctypes.c_char) +lib.EC_KEY_new.restype = ctypes.POINTER(ctypes.c_char) +lib.EC_POINT_new.restype = ctypes.POINTER(ctypes.c_char) +lib.EC_KEY_get0_private_key.restype = ctypes.POINTER(ctypes.c_char) +lib.EVP_PKEY_new.restype = ctypes.POINTER(ctypes.c_char) +try: + lib.EVP_PKEY_CTX_new.restype = ctypes.POINTER(ctypes.c_char) +except AttributeError: + pass + + +thread_local = threading.local() + + +# This lock is required to keep ECC thread-safe. Old OpenSSL versions (before +# 1.1.0) use global objects so they aren't thread safe. Fortunately we can check +# the code to find out which functions are thread safe. +# +# For example, EC_GROUP_new_by_curve_name checks global error code to initialize +# the group, so if two errors happen at once or two threads read the error code, +# or the codes are read in the wrong order, the group is initialized in a wrong +# way. +# +# EC_KEY_new_by_curve_name calls EC_GROUP_new_by_curve_name so it's not thread +# safe. We can't use the lock because it would be too slow; instead, we use +# EC_KEY_new and then EC_KEY_set_group which calls EC_GROUP_copy instead which +# is thread safe. +lock = threading.Lock() + + +class BN: + # BN_CTX + class Context: + def __init__(self): + self.ptr = lib.BN_CTX_new() + self.lib = lib # For finalizer + + + def __del__(self): + self.lib.BN_CTX_free(self.ptr) + + + @classmethod + def get(cls): + # Get thread-safe contexf + if not hasattr(thread_local, "bn_ctx"): + thread_local.bn_ctx = cls() + return thread_local.bn_ctx.ptr + + + def __init__(self, value=None, link_only=False): + if link_only: + self.bn = value + self._free = False + else: + if value is None: + self.bn = lib.BN_new() + self._free = True + elif isinstance(value, bytes): + self.bn = lib.BN_bin2bn(value, len(value), None) + self._free = True + else: + self.bn = lib.BN_new() + lib.BN_clear(self.bn) + lib.BN_add_word(self.bn, value) + self._free = True + + + def __del__(self): + if self._free: + lib.BN_free(self.bn) + + + def bytes(self, length=None): + buf = ctypes.create_string_buffer((len(self) + 7) // 8) + lib.BN_bn2bin(self.bn, buf) + buf = bytes(buf) + if length is None: + return buf + else: + if length < len(buf): + raise ValueError("Too little space for BN") + return b"\x00" * (length - len(buf)) + buf + + def __int__(self): + value = 0 + for byte in self.bytes(): + value = value * 256 + byte + return value + + def __len__(self): + return lib.BN_num_bits(self.bn) + + + def inverse(self, modulo): + result = BN() + if not lib.BN_mod_inverse(result.bn, self.bn, modulo.bn, BN.Context.get()): + raise ValueError("Could not compute inverse") + return result + + + def __floordiv__(self, other): + if not isinstance(other, BN): + raise TypeError("Can only divide BN by BN, not {}".format(other)) + result = BN() + if not lib.BN_div(result.bn, None, self.bn, other.bn, BN.Context.get()): + raise ZeroDivisionError("Division by zero") + return result + + def __mod__(self, other): + if not isinstance(other, BN): + raise TypeError("Can only divide BN by BN, not {}".format(other)) + result = BN() + if not lib.BN_div(None, result.bn, self.bn, other.bn, BN.Context.get()): + raise ZeroDivisionError("Division by zero") + return result + + def __add__(self, other): + if not isinstance(other, BN): + raise TypeError("Can only sum BN's, not BN and {}".format(other)) + result = BN() + if not lib.BN_add(result.bn, self.bn, other.bn): + raise ValueError("Could not sum two BN's") + return result + + def __sub__(self, other): + if not isinstance(other, BN): + raise TypeError("Can only subtract BN's, not BN and {}".format(other)) + result = BN() + if not lib.BN_sub(result.bn, self.bn, other.bn): + raise ValueError("Could not subtract BN from BN") + return result + + def __mul__(self, other): + if not isinstance(other, BN): + raise TypeError("Can only multiply BN by BN, not {}".format(other)) + result = BN() + if not lib.BN_mul(result.bn, self.bn, other.bn, BN.Context.get()): + raise ValueError("Could not multiply two BN's") + return result + + def __neg__(self): + return BN(0) - self + + + # A dirty but nice way to update current BN and free old BN at the same time + def __imod__(self, other): + res = self % other + self.bn, res.bn = res.bn, self.bn + return self + def __iadd__(self, other): + res = self + other + self.bn, res.bn = res.bn, self.bn + return self + def __isub__(self, other): + res = self - other + self.bn, res.bn = res.bn, self.bn + return self + def __imul__(self, other): + res = self * other + self.bn, res.bn = res.bn, self.bn + return self + + + def cmp(self, other): + if not isinstance(other, BN): + raise TypeError("Can only compare BN with BN, not {}".format(other)) + return lib.BN_cmp(self.bn, other.bn) + + def __eq__(self, other): + return self.cmp(other) == 0 + def __lt__(self, other): + return self.cmp(other) < 0 + def __gt__(self, other): + return self.cmp(other) > 0 + def __ne__(self, other): + return self.cmp(other) != 0 + def __le__(self, other): + return self.cmp(other) <= 0 + def __ge__(self, other): + return self.cmp(other) >= 0 + + + def __repr__(self): + return "".format(int(self)) + + def __str__(self): + return str(int(self)) + + +class EllipticCurveBackend: + def __init__(self, nid): + self.lib = lib # For finalizer + self.nid = nid + with lock: + # Thread-safety + self.group = lib.EC_GROUP_new_by_curve_name(self.nid) + if not self.group: + raise ValueError("The curve is not supported by OpenSSL") + + self.order = BN() + self.p = BN() + bn_ctx = BN.Context.get() + lib.EC_GROUP_get_order(self.group, self.order.bn, bn_ctx) + lib.EC_GROUP_get_curve_GFp(self.group, self.p.bn, None, None, bn_ctx) + + self.public_key_length = (len(self.p) + 7) // 8 + + self.is_supported_evp_pkey_ctx = hasattr(lib, "EVP_PKEY_CTX_new") + + + def __del__(self): + self.lib.EC_GROUP_free(self.group) + + + def _private_key_to_ec_key(self, private_key): + # Thread-safety + eckey = lib.EC_KEY_new() + lib.EC_KEY_set_group(eckey, self.group) + if not eckey: + raise ValueError("Failed to allocate EC_KEY") + private_key = BN(private_key) + if not lib.EC_KEY_set_private_key(eckey, private_key.bn): + lib.EC_KEY_free(eckey) + raise ValueError("Invalid private key") + return eckey, private_key + + + def _public_key_to_point(self, public_key): + x = BN(public_key[0]) + y = BN(public_key[1]) + # EC_KEY_set_public_key_affine_coordinates is not supported by + # OpenSSL 1.0.0 so we can't use it + point = lib.EC_POINT_new(self.group) + if not lib.EC_POINT_set_affine_coordinates_GFp(self.group, point, x.bn, y.bn, BN.Context.get()): + raise ValueError("Could not set public key affine coordinates") + return point + + + def _public_key_to_ec_key(self, public_key): + # Thread-safety + eckey = lib.EC_KEY_new() + lib.EC_KEY_set_group(eckey, self.group) + if not eckey: + raise ValueError("Failed to allocate EC_KEY") + try: + # EC_KEY_set_public_key_affine_coordinates is not supported by + # OpenSSL 1.0.0 so we can't use it + point = self._public_key_to_point(public_key) + if not lib.EC_KEY_set_public_key(eckey, point): + raise ValueError("Could not set point") + lib.EC_POINT_free(point) + return eckey + except Exception as e: + lib.EC_KEY_free(eckey) + raise e from None + + + def _point_to_affine(self, point): + # Convert to affine coordinates + x = BN() + y = BN() + if lib.EC_POINT_get_affine_coordinates_GFp(self.group, point, x.bn, y.bn, BN.Context.get()) != 1: + raise ValueError("Failed to convert public key to affine coordinates") + # Convert to binary + if (len(x) + 7) // 8 > self.public_key_length: + raise ValueError("Public key X coordinate is too large") + if (len(y) + 7) // 8 > self.public_key_length: + raise ValueError("Public key Y coordinate is too large") + return x.bytes(self.public_key_length), y.bytes(self.public_key_length) + + + def decompress_point(self, public_key): + point = lib.EC_POINT_new(self.group) + if not point: + raise ValueError("Could not create point") + try: + if not lib.EC_POINT_oct2point(self.group, point, public_key, len(public_key), BN.Context.get()): + raise ValueError("Invalid compressed public key") + return self._point_to_affine(point) + finally: + lib.EC_POINT_free(point) + + + def new_private_key(self): + # Create random key + # Thread-safety + eckey = lib.EC_KEY_new() + lib.EC_KEY_set_group(eckey, self.group) + lib.EC_KEY_generate_key(eckey) + # To big integer + private_key = BN(lib.EC_KEY_get0_private_key(eckey), link_only=True) + # To binary + private_key_buf = private_key.bytes() + # Cleanup + lib.EC_KEY_free(eckey) + return private_key_buf + + + def private_to_public(self, private_key): + eckey, private_key = self._private_key_to_ec_key(private_key) + try: + # Derive public key + point = lib.EC_POINT_new(self.group) + try: + if not lib.EC_POINT_mul(self.group, point, private_key.bn, None, None, BN.Context.get()): + raise ValueError("Failed to derive public key") + return self._point_to_affine(point) + finally: + lib.EC_POINT_free(point) + finally: + lib.EC_KEY_free(eckey) + + + def ecdh(self, private_key, public_key): + if not self.is_supported_evp_pkey_ctx: + # Use ECDH_compute_key instead + # Create EC_KEY from private key + eckey, _ = self._private_key_to_ec_key(private_key) + try: + # Create EC_POINT from public key + point = self._public_key_to_point(public_key) + try: + key = ctypes.create_string_buffer(self.public_key_length) + if lib.ECDH_compute_key(key, self.public_key_length, point, eckey, None) == -1: + raise ValueError("Could not compute shared secret") + return bytes(key) + finally: + lib.EC_POINT_free(point) + finally: + lib.EC_KEY_free(eckey) + + # Private key: + # Create EC_KEY + eckey, _ = self._private_key_to_ec_key(private_key) + try: + # Convert to EVP_PKEY + pkey = lib.EVP_PKEY_new() + if not pkey: + raise ValueError("Could not create private key object") + try: + lib.EVP_PKEY_set1_EC_KEY(pkey, eckey) + + # Public key: + # Create EC_KEY + peer_eckey = self._public_key_to_ec_key(public_key) + try: + # Convert to EVP_PKEY + peer_pkey = lib.EVP_PKEY_new() + if not peer_pkey: + raise ValueError("Could not create public key object") + try: + lib.EVP_PKEY_set1_EC_KEY(peer_pkey, peer_eckey) + + # Create context + ctx = lib.EVP_PKEY_CTX_new(pkey, None) + if not ctx: + raise ValueError("Could not create EVP context") + try: + if lib.EVP_PKEY_derive_init(ctx) != 1: + raise ValueError("Could not initialize key derivation") + if not lib.EVP_PKEY_derive_set_peer(ctx, peer_pkey): + raise ValueError("Could not set peer") + + # Actually derive + key_len = ctypes.c_int(0) + lib.EVP_PKEY_derive(ctx, None, ctypes.byref(key_len)) + key = ctypes.create_string_buffer(key_len.value) + lib.EVP_PKEY_derive(ctx, key, ctypes.byref(key_len)) + + return bytes(key) + finally: + lib.EVP_PKEY_CTX_free(ctx) + finally: + lib.EVP_PKEY_free(peer_pkey) + finally: + lib.EC_KEY_free(peer_eckey) + finally: + lib.EVP_PKEY_free(pkey) + finally: + lib.EC_KEY_free(eckey) + + + def _subject_to_bn(self, subject): + return BN(subject[:(len(self.order) + 7) // 8]) + + + def sign(self, subject, private_key, recoverable, is_compressed, entropy): + z = self._subject_to_bn(subject) + private_key = BN(private_key) + k = BN(entropy) + + rp = lib.EC_POINT_new(self.group) + bn_ctx = BN.Context.get() + try: + # Fix Minerva + k1 = k + self.order + k2 = k1 + self.order + if len(k1) == len(k2): + k = k2 + else: + k = k1 + if not lib.EC_POINT_mul(self.group, rp, k.bn, None, None, bn_ctx): + raise ValueError("Could not generate R") + # Convert to affine coordinates + rx = BN() + ry = BN() + if lib.EC_POINT_get_affine_coordinates_GFp(self.group, rp, rx.bn, ry.bn, bn_ctx) != 1: + raise ValueError("Failed to convert R to affine coordinates") + r = rx % self.order + if r == BN(0): + raise ValueError("Invalid k") + # Calculate s = k^-1 * (z + r * private_key) mod n + s = (k.inverse(self.order) * (z + r * private_key)) % self.order + if s == BN(0): + raise ValueError("Invalid k") + + inverted = False + if s * BN(2) >= self.order: + s = self.order - s + inverted = True + + r_buf = r.bytes(self.public_key_length) + s_buf = s.bytes(self.public_key_length) + if recoverable: + # Generate recid + recid = int(ry % BN(2)) ^ inverted + # The line below is highly unlikely to matter in case of + # secp256k1 but might make sense for other curves + recid += 2 * int(rx // self.order) + if is_compressed: + return bytes([31 + recid]) + r_buf + s_buf + else: + if recid >= 4: + raise ValueError("Too big recovery ID, use compressed address instead") + return bytes([27 + recid]) + r_buf + s_buf + else: + return r_buf + s_buf + finally: + lib.EC_POINT_free(rp) + + + def recover(self, signature, subject): + recid = signature[0] - 27 if signature[0] < 31 else signature[0] - 31 + r = BN(signature[1:self.public_key_length + 1]) + s = BN(signature[self.public_key_length + 1:]) + + # Verify bounds + if r >= self.order: + raise ValueError("r is out of bounds") + if s >= self.order: + raise ValueError("s is out of bounds") + + bn_ctx = BN.Context.get() + + z = self._subject_to_bn(subject) + + rinv = r.inverse(self.order) + u1 = (-z * rinv) % self.order + u2 = (s * rinv) % self.order + + # Recover R + rx = r + BN(recid // 2) * self.order + if rx >= self.p: + raise ValueError("Rx is out of bounds") + rp = lib.EC_POINT_new(self.group) + if not rp: + raise ValueError("Could not create R") + try: + init_buf = b"\x02" + rx.bytes(self.public_key_length) + if not lib.EC_POINT_oct2point(self.group, rp, init_buf, len(init_buf), bn_ctx): + raise ValueError("Could not use Rx to initialize point") + ry = BN() + if lib.EC_POINT_get_affine_coordinates_GFp(self.group, rp, None, ry.bn, bn_ctx) != 1: + raise ValueError("Failed to convert R to affine coordinates") + if int(ry % BN(2)) != recid % 2: + # Fix Ry sign + ry = self.p - ry + if lib.EC_POINT_set_affine_coordinates_GFp(self.group, rp, rx.bn, ry.bn, bn_ctx) != 1: + raise ValueError("Failed to update R coordinates") + + # Recover public key + result = lib.EC_POINT_new(self.group) + if not result: + raise ValueError("Could not create point") + try: + if not lib.EC_POINT_mul(self.group, result, u1.bn, rp, u2.bn, bn_ctx): + raise ValueError("Could not recover public key") + return self._point_to_affine(result) + finally: + lib.EC_POINT_free(result) + finally: + lib.EC_POINT_free(rp) + + + def verify(self, signature, subject, public_key): + r_raw = signature[:self.public_key_length] + r = BN(r_raw) + s = BN(signature[self.public_key_length:]) + if r >= self.order: + raise ValueError("r is out of bounds") + if s >= self.order: + raise ValueError("s is out of bounds") + + bn_ctx = BN.Context.get() + + z = self._subject_to_bn(subject) + + pub_p = lib.EC_POINT_new(self.group) + if not pub_p: + raise ValueError("Could not create public key point") + try: + init_buf = b"\x04" + public_key[0] + public_key[1] + if not lib.EC_POINT_oct2point(self.group, pub_p, init_buf, len(init_buf), bn_ctx): + raise ValueError("Could initialize point") + + sinv = s.inverse(self.order) + u1 = (z * sinv) % self.order + u2 = (r * sinv) % self.order + + # Recover public key + result = lib.EC_POINT_new(self.group) + if not result: + raise ValueError("Could not create point") + try: + if not lib.EC_POINT_mul(self.group, result, u1.bn, pub_p, u2.bn, bn_ctx): + raise ValueError("Could not recover public key") + if BN(self._point_to_affine(result)[0]) % self.order != r: + raise ValueError("Invalid signature") + return True + finally: + lib.EC_POINT_free(result) + finally: + lib.EC_POINT_free(pub_p) + + + def derive_child(self, seed, child): + # Round 1 + h = hmac.new(key=b"Bitcoin seed", msg=seed, digestmod="sha512").digest() + private_key1 = h[:32] + x, y = self.private_to_public(private_key1) + public_key1 = bytes([0x02 + (y[-1] % 2)]) + x + private_key1 = BN(private_key1) + + # Round 2 + child_bytes = [] + for _ in range(4): + child_bytes.append(child & 255) + child >>= 8 + child_bytes = bytes(child_bytes[::-1]) + msg = public_key1 + child_bytes + h = hmac.new(key=h[32:], msg=msg, digestmod="sha512").digest() + private_key2 = BN(h[:32]) + + return ((private_key1 + private_key2) % self.order).bytes(self.public_key_length) + + + @classmethod + def get_backend(cls): + return openssl_backend + + +ecc = ECC(EllipticCurveBackend, aes) diff --git a/src/lib/sslcrypto/openssl/library.py b/src/lib/sslcrypto/openssl/library.py new file mode 100644 index 00000000..47bedc3a --- /dev/null +++ b/src/lib/sslcrypto/openssl/library.py @@ -0,0 +1,98 @@ +import os +import sys +import ctypes +import ctypes.util +from .discovery import discover as user_discover + + +# Disable false-positive _MEIPASS +# pylint: disable=no-member,protected-access + +# Discover OpenSSL library +def discover_paths(): + # Search local files first + if "win" in sys.platform: + # Windows + names = [ + "libeay32.dll" + ] + openssl_paths = [os.path.abspath(path) for path in names] + if hasattr(sys, "_MEIPASS"): + openssl_paths += [os.path.join(sys._MEIPASS, path) for path in openssl_paths] + openssl_paths.append(ctypes.util.find_library("libeay32")) + elif "darwin" in sys.platform: + # Mac OS + names = [ + "libcrypto.dylib", + "libcrypto.1.1.0.dylib", + "libcrypto.1.0.2.dylib", + "libcrypto.1.0.1.dylib", + "libcrypto.1.0.0.dylib", + "libcrypto.0.9.8.dylib" + ] + openssl_paths = [os.path.abspath(path) for path in names] + openssl_paths += names + openssl_paths += [ + "/usr/local/opt/openssl/lib/libcrypto.dylib" + ] + if hasattr(sys, "_MEIPASS") and "RESOURCEPATH" in os.environ: + openssl_paths += [ + os.path.join(os.environ["RESOURCEPATH"], "..", "Frameworks", name) + for name in names + ] + openssl_paths.append(ctypes.util.find_library("ssl")) + else: + # Linux, BSD and such + names = [ + "libcrypto.so", + "libssl.so", + "libcrypto.so.1.1.0", + "libssl.so.1.1.0", + "libcrypto.so.1.0.2", + "libssl.so.1.0.2", + "libcrypto.so.1.0.1", + "libssl.so.1.0.1", + "libcrypto.so.1.0.0", + "libssl.so.1.0.0", + "libcrypto.so.0.9.8", + "libssl.so.0.9.8" + ] + openssl_paths = [os.path.abspath(path) for path in names] + openssl_paths += names + if hasattr(sys, "_MEIPASS"): + openssl_paths += [os.path.join(sys._MEIPASS, path) for path in names] + openssl_paths.append(ctypes.util.find_library("ssl")) + lst = user_discover() + if isinstance(lst, str): + lst = [lst] + elif not lst: + lst = [] + return lst + openssl_paths + + +def discover_library(): + for path in discover_paths(): + if path: + try: + return ctypes.CDLL(path) + except OSError: + pass + raise OSError("OpenSSL is unavailable") + + +lib = discover_library() + +# Initialize internal state +try: + lib.OPENSSL_add_all_algorithms_conf() +except AttributeError: + pass + +try: + lib.OpenSSL_version.restype = ctypes.c_char_p + openssl_backend = lib.OpenSSL_version(0).decode() +except AttributeError: + lib.SSLeay_version.restype = ctypes.c_char_p + openssl_backend = lib.SSLeay_version(0).decode() + +openssl_backend += " at " + lib._name diff --git a/src/lib/sslcrypto/openssl/rsa.py b/src/lib/sslcrypto/openssl/rsa.py new file mode 100644 index 00000000..afd8b51c --- /dev/null +++ b/src/lib/sslcrypto/openssl/rsa.py @@ -0,0 +1,11 @@ +# pylint: disable=too-few-public-methods + +from .library import openssl_backend + + +class RSA: + def get_backend(self): + return openssl_backend + + +rsa = RSA() diff --git a/src/util/Electrum.py b/src/util/Electrum.py new file mode 100644 index 00000000..112151aa --- /dev/null +++ b/src/util/Electrum.py @@ -0,0 +1,39 @@ +import hashlib +import struct + + +# Electrum, the heck?! + +def bchr(i): + return struct.pack("B", i) + +def encode(val, base, minlen=0): + base, minlen = int(base), int(minlen) + code_string = b"".join([bchr(x) for x in range(256)]) + result = b"" + while val > 0: + index = val % base + result = code_string[index:index + 1] + result + val //= base + return code_string[0:1] * max(minlen - len(result), 0) + result + +def insane_int(x): + x = int(x) + if x < 253: + return bchr(x) + elif x < 65536: + return bchr(253) + encode(x, 256, 2)[::-1] + elif x < 4294967296: + return bchr(254) + encode(x, 256, 4)[::-1] + else: + return bchr(255) + encode(x, 256, 8)[::-1] + + +def magic(message): + return b"\x18Bitcoin Signed Message:\n" + insane_int(len(message)) + message + +def format(message): + return hashlib.sha256(magic(message)).digest() + +def dbl_format(message): + return hashlib.sha256(format(message)).digest() diff --git a/src/util/OpensslFindPatch.py b/src/util/OpensslFindPatch.py index fd09ec6b..5e218d7e 100644 --- a/src/util/OpensslFindPatch.py +++ b/src/util/OpensslFindPatch.py @@ -1,13 +1,11 @@ import logging import os import sys -import ctypes -import ctypes.util +from ctypes.util import find_library +from lib.sslcrypto.openssl import discovery from Config import config -find_library_original = ctypes.util.find_library - def getOpensslPath(): if config.openssl_lib_file: @@ -49,29 +47,11 @@ def getOpensslPath(): logging.debug("OpenSSL lib not found in: %s (%s)" % (path, err)) lib_path = ( - find_library_original('ssl.so') or find_library_original('ssl') or - find_library_original('crypto') or find_library_original('libcrypto') or 'libeay32' + find_library('ssl.so') or find_library('ssl') or + find_library('crypto') or find_library('libcrypto') or 'libeay32' ) return lib_path -def patchCtypesOpensslFindLibrary(): - def findLibraryPatched(name): - if name in ("ssl", "crypto", "libeay32"): - lib_path = getOpensslPath() - return lib_path - else: - return find_library_original(name) - - ctypes.util.find_library = findLibraryPatched - - -patchCtypesOpensslFindLibrary() - - -def openLibrary(): - lib_path = getOpensslPath() - logging.debug("Opening %s..." % lib_path) - ssl_lib = ctypes.CDLL(lib_path, ctypes.RTLD_GLOBAL) - return ssl_lib +discovery.discover = getOpensslPath From 02fd1dc4d04bff0c643e391df2a059c57217c003 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Thu, 5 Mar 2020 22:59:07 +0300 Subject: [PATCH 2362/2570] Add GitHub Actions workflow --- .github/workflows/tests.yml | 49 +++++++++++++++++++++++++++++++++++++ .gitignore | 1 + 2 files changed, 50 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..6f2a748e --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,49 @@ +name: tests + +on: [push] + +jobs: + test: + + runs-on: ubuntu-16.04 + strategy: + max-parallel: 16 + matrix: + python-version: [3.5, 3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v1 + + - 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: 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: 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/ diff --git a/.gitignore b/.gitignore index 451a67be..38dd3a34 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ __pycache__/ # Hidden files .* +!/.github !/.gitignore !/.travis.yml !/.gitlab-ci.yml From 193632c3f96e764c1df614b4582c9c6248608b80 Mon Sep 17 00:00:00 2001 From: canewsin Date: Sat, 7 Mar 2020 17:30:27 +0530 Subject: [PATCH 2363/2570] Added Github Action Test Badge to ReadMe --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f48d7c32..a4ade769 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ZeroNet [![Build Status](https://travis-ci.org/HelloZeroNet/ZeroNet.svg?branch=py3)](https://travis-ci.org/HelloZeroNet/ZeroNet) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://zeronet.io/docs/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://zeronet.io/docs/help_zeronet/donate/) +# ZeroNet [![Build Status](https://travis-ci.org/HelloZeroNet/ZeroNet.svg?branch=py3)](https://travis-ci.org/HelloZeroNet/ZeroNet) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://zeronet.io/docs/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://zeronet.io/docs/help_zeronet/donate/) ![tests](https://github.com/HelloZeroNet/ZeroNet/workflows/tests/badge.svg) Decentralized websites using Bitcoin crypto and the BitTorrent network - https://zeronet.io From a2457b2488b5bca50bfa11d1ec80386e33a3a005 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Sun, 8 Mar 2020 23:49:46 +0300 Subject: [PATCH 2364/2570] Forgot that Upgrade is case-insensitive --- src/lib/gevent_ws/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/gevent_ws/__init__.py b/src/lib/gevent_ws/__init__.py index 8ad74155..7309ad1c 100644 --- a/src/lib/gevent_ws/__init__.py +++ b/src/lib/gevent_ws/__init__.py @@ -210,8 +210,8 @@ class WebSocketHandler(WSGIHandler): self.response_length = 0 - http_connection = [s.strip() for s in self.environ.get("HTTP_CONNECTION", "").split(",")] - if "Upgrade" not in http_connection or self.environ.get("HTTP_UPGRADE", "") != "websocket": + http_connection = [s.strip().lower() for s in self.environ.get("HTTP_CONNECTION", "").split(",")] + if "upgrade" not in http_connection or self.environ.get("HTTP_UPGRADE", "") != "websocket": # Not my problem return super(WebSocketHandler, self).handle_one_response() From 33af83b2cd477e9af84139fe90e087d820dba935 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Tue, 10 Mar 2020 22:31:26 +0300 Subject: [PATCH 2365/2570] Search for any OpenSSL version in LD_LIBRARY_PATH --- src/util/OpensslFindPatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/OpensslFindPatch.py b/src/util/OpensslFindPatch.py index 5e218d7e..80987876 100644 --- a/src/util/OpensslFindPatch.py +++ b/src/util/OpensslFindPatch.py @@ -42,7 +42,7 @@ def getOpensslPath(): lib_dir_paths = os.environ["LD_LIBRARY_PATH"].split(":") for path in lib_dir_paths: try: - return [lib for lib in os.listdir(path) if "libcrypto.so.1.0" in lib][0] + return [lib for lib in os.listdir(path) if "libcrypto.so" in lib][0] except Exception as err: logging.debug("OpenSSL lib not found in: %s (%s)" % (path, err)) From 19f003141b75940ecfbe3039ca4203f6b37c8c75 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Sat, 14 Mar 2020 07:27:19 +0300 Subject: [PATCH 2366/2570] Disable process_result on websocket requests --- src/lib/gevent_ws/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/gevent_ws/__init__.py b/src/lib/gevent_ws/__init__.py index 7309ad1c..e295b41e 100644 --- a/src/lib/gevent_ws/__init__.py +++ b/src/lib/gevent_ws/__init__.py @@ -254,3 +254,8 @@ class WebSocketHandler(WSGIHandler): finally: self.time_finish = time.time() self.log_request() + + + def process_result(self): + if "wsgi.websocket" not in self.environ: + super(WebSocketHandler, self).process_result() From 7e17a4e96700bf48984fbbeb724bd6cd47bf887c Mon Sep 17 00:00:00 2001 From: Ivanq Date: Sun, 15 Mar 2020 20:18:04 +0300 Subject: [PATCH 2367/2570] Switch to sslcrypto v4.0 to support OpenSSL without builtin curves --- src/lib/sslcrypto/_ecc.py | 187 +++++++++++++++++++++++++++--- src/lib/sslcrypto/fallback/ecc.py | 182 +---------------------------- src/lib/sslcrypto/openssl/ecc.py | 40 ++++--- 3 files changed, 199 insertions(+), 210 deletions(-) diff --git a/src/lib/sslcrypto/_ecc.py b/src/lib/sslcrypto/_ecc.py index 9831d688..3dbc56cd 100644 --- a/src/lib/sslcrypto/_ecc.py +++ b/src/lib/sslcrypto/_ecc.py @@ -18,23 +18,176 @@ else: class ECC: + # pylint: disable=line-too-long + # name: (nid, p, n, a, b, (Gx, Gy)), CURVES = { - "secp112r1": 704, - "secp112r2": 705, - "secp128r1": 706, - "secp128r2": 707, - "secp160k1": 708, - "secp160r1": 709, - "secp160r2": 710, - "secp192k1": 711, - "prime192v1": 409, - "secp224k1": 712, - "secp224r1": 713, - "secp256k1": 714, - "prime256v1": 415, - "secp384r1": 715, - "secp521r1": 716 + "secp112r1": ( + 704, + 0xDB7C2ABF62E35E668076BEAD208B, + 0xDB7C2ABF62E35E7628DFAC6561C5, + 0xDB7C2ABF62E35E668076BEAD2088, + 0x659EF8BA043916EEDE8911702B22, + ( + 0x09487239995A5EE76B55F9C2F098, + 0xA89CE5AF8724C0A23E0E0FF77500 + ) + ), + "secp112r2": ( + 705, + 0xDB7C2ABF62E35E668076BEAD208B, + 0x36DF0AAFD8B8D7597CA10520D04B, + 0x6127C24C05F38A0AAAF65C0EF02C, + 0x51DEF1815DB5ED74FCC34C85D709, + ( + 0x4BA30AB5E892B4E1649DD0928643, + 0xADCD46F5882E3747DEF36E956E97 + ) + ), + "secp128r1": ( + 706, + 0xFFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFF, + 0xFFFFFFFE0000000075A30D1B9038A115, + 0xFFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFC, + 0xE87579C11079F43DD824993C2CEE5ED3, + ( + 0x161FF7528B899B2D0C28607CA52C5B86, + 0xCF5AC8395BAFEB13C02DA292DDED7A83 + ) + ), + "secp128r2": ( + 707, + 0xFFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFF, + 0x3FFFFFFF7FFFFFFFBE0024720613B5A3, + 0xD6031998D1B3BBFEBF59CC9BBFF9AEE1, + 0x5EEEFCA380D02919DC2C6558BB6D8A5D, + ( + 0x7B6AA5D85E572983E6FB32A7CDEBC140, + 0x27B6916A894D3AEE7106FE805FC34B44 + ) + ), + "secp160k1": ( + 708, + 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFAC73, + 0x0100000000000000000001B8FA16DFAB9ACA16B6B3, + 0, + 7, + ( + 0x3B4C382CE37AA192A4019E763036F4F5DD4D7EBB, + 0x938CF935318FDCED6BC28286531733C3F03C4FEE + ) + ), + "secp160r1": ( + 709, + 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFF, + 0x0100000000000000000001F4C8F927AED3CA752257, + 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFC, + 0x001C97BEFC54BD7A8B65ACF89F81D4D4ADC565FA45, + ( + 0x4A96B5688EF573284664698968C38BB913CBFC82, + 0x23A628553168947D59DCC912042351377AC5FB32 + ) + ), + "secp160r2": ( + 710, + 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFAC73, + 0x0100000000000000000000351EE786A818F3A1A16B, + 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFAC70, + 0x00B4E134D3FB59EB8BAB57274904664D5AF50388BA, + ( + 0x52DCB034293A117E1F4FF11B30F7199D3144CE6D, + 0xFEAFFEF2E331F296E071FA0DF9982CFEA7D43F2E + ) + ), + "secp192k1": ( + 711, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFEE37, + 0xFFFFFFFFFFFFFFFFFFFFFFFE26F2FC170F69466A74DEFD8D, + 0, + 3, + ( + 0xDB4FF10EC057E9AE26B07D0280B7F4341DA5D1B1EAE06C7D, + 0x9B2F2F6D9C5628A7844163D015BE86344082AA88D95E2F9D + ) + ), + "prime192v1": ( + 409, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFF, + 0xFFFFFFFFFFFFFFFFFFFFFFFF99DEF836146BC9B1B4D22831, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFC, + 0x64210519E59C80E70FA7E9AB72243049FEB8DEECC146B9B1, + ( + 0x188DA80EB03090F67CBF20EB43A18800F4FF0AFD82FF1012, + 0x07192B95FFC8DA78631011ED6B24CDD573F977A11E794811 + ) + ), + "secp224k1": ( + 712, + 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFE56D, + 0x010000000000000000000000000001DCE8D2EC6184CAF0A971769FB1F7, + 0, + 5, + ( + 0xA1455B334DF099DF30FC28A169A467E9E47075A90F7E650EB6B7A45C, + 0x7E089FED7FBA344282CAFBD6F7E319F7C0B0BD59E2CA4BDB556D61A5 + ) + ), + "secp224r1": ( + 713, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000001, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF16A2E0B8F03E13DD29455C5C2A3D, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFE, + 0xB4050A850C04B3ABF54132565044B0B7D7BFD8BA270B39432355FFB4, + ( + 0xB70E0CBD6BB4BF7F321390B94A03C1D356C21122343280D6115C1D21, + 0xBD376388B5F723FB4C22DFE6CD4375A05A07476444D5819985007E34 + ) + ), + "secp256k1": ( + 714, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141, + 0, + 7, + ( + 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, + 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8 + ) + ), + "prime256v1": ( + 715, + 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF, + 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551, + 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC, + 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B, + ( + 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296, + 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5 + ) + ), + "secp384r1": ( + 716, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC, + 0xB3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF, + ( + 0xAA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7, + 0x3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F + ) + ), + "secp521r1": ( + 717, + 0x01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, + 0x01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409, + 0x01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC, + 0x0051953EB9618E1C9A1F929A21A0B68540EEA2DA725B99B315F3B8B489918EF109E156193951EC7E937B1652C0BD3BB1BF073573DF883D2C34F1EF451FD46B503F00, + ( + 0x00C6858E06B70404E9CD9E3ECB662395B4429C648139053FB521F828AF606B4D3DBAA14B5E77EFE75928FE1DC127A2FFA8DE3348B3C1856A429BF97E7E31C2E5BD66, + 0x011839296A789A3BC0045C8A5FB42C7D1BD998F54449579B446817AFBD17273E662C97EE72995EF42640C550B9013FAD0761353C7086A272C24088BE94769FD16650 + ) + ) } + # pylint: enable=line-too-long def __init__(self, backend, aes): self._backend = backend @@ -44,8 +197,8 @@ class ECC: def get_curve(self, name): if name not in self.CURVES: raise ValueError("Unknown curve {}".format(name)) - nid = self.CURVES[name] - return EllipticCurve(self._backend(nid), self._aes, nid) + nid, p, n, a, b, g = self.CURVES[name] + return EllipticCurve(self._backend(p, n, a, b, g), self._aes, nid) def get_backend(self): diff --git a/src/lib/sslcrypto/fallback/ecc.py b/src/lib/sslcrypto/fallback/ecc.py index 3a438d4d..6ca9a498 100644 --- a/src/lib/sslcrypto/fallback/ecc.py +++ b/src/lib/sslcrypto/fallback/ecc.py @@ -6,185 +6,13 @@ from .aes import aes from ._util import int_to_bytes, bytes_to_int, inverse, square_root_mod_prime -# pylint: disable=line-too-long -CURVES = { - # nid: (p, n, a, b, (Gx, Gy)), - 704: ( - # secp112r1 - 0xDB7C2ABF62E35E668076BEAD208B, - 0xDB7C2ABF62E35E7628DFAC6561C5, - 0xDB7C2ABF62E35E668076BEAD2088, - 0x659EF8BA043916EEDE8911702B22, - ( - 0x09487239995A5EE76B55F9C2F098, - 0xA89CE5AF8724C0A23E0E0FF77500 - ) - ), - 705: ( - # secp112r2 - 0xDB7C2ABF62E35E668076BEAD208B, - 0x36DF0AAFD8B8D7597CA10520D04B, - 0x6127C24C05F38A0AAAF65C0EF02C, - 0x51DEF1815DB5ED74FCC34C85D709, - ( - 0x4BA30AB5E892B4E1649DD0928643, - 0xADCD46F5882E3747DEF36E956E97 - ) - ), - 706: ( - # secp128r1 - 0xFFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFF, - 0xFFFFFFFE0000000075A30D1B9038A115, - 0xFFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFC, - 0xE87579C11079F43DD824993C2CEE5ED3, - ( - 0x161FF7528B899B2D0C28607CA52C5B86, - 0xCF5AC8395BAFEB13C02DA292DDED7A83 - ) - ), - 707: ( - # secp128r2 - 0xFFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFF, - 0x3FFFFFFF7FFFFFFFBE0024720613B5A3, - 0xD6031998D1B3BBFEBF59CC9BBFF9AEE1, - 0x5EEEFCA380D02919DC2C6558BB6D8A5D, - ( - 0x7B6AA5D85E572983E6FB32A7CDEBC140, - 0x27B6916A894D3AEE7106FE805FC34B44 - ) - ), - 708: ( - # secp160k1 - 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFAC73, - 0x0100000000000000000001B8FA16DFAB9ACA16B6B3, - 0, - 7, - ( - 0x3B4C382CE37AA192A4019E763036F4F5DD4D7EBB, - 0x938CF935318FDCED6BC28286531733C3F03C4FEE - ) - ), - 709: ( - # secp160r1 - 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFF, - 0x0100000000000000000001F4C8F927AED3CA752257, - 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFC, - 0x001C97BEFC54BD7A8B65ACF89F81D4D4ADC565FA45, - ( - 0x4A96B5688EF573284664698968C38BB913CBFC82, - 0x23A628553168947D59DCC912042351377AC5FB32 - ) - ), - 710: ( - # secp160r2 - 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFAC73, - 0x0100000000000000000000351EE786A818F3A1A16B, - 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFAC70, - 0x00B4E134D3FB59EB8BAB57274904664D5AF50388BA, - ( - 0x52DCB034293A117E1F4FF11B30F7199D3144CE6D, - 0xFEAFFEF2E331F296E071FA0DF9982CFEA7D43F2E - ) - ), - 711: ( - # secp192k1 - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFEE37, - 0xFFFFFFFFFFFFFFFFFFFFFFFE26F2FC170F69466A74DEFD8D, - 0, - 3, - ( - 0xDB4FF10EC057E9AE26B07D0280B7F4341DA5D1B1EAE06C7D, - 0x9B2F2F6D9C5628A7844163D015BE86344082AA88D95E2F9D - ) - ), - 409: ( - # prime192v1 - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFF, - 0xFFFFFFFFFFFFFFFFFFFFFFFF99DEF836146BC9B1B4D22831, - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFC, - 0x64210519E59C80E70FA7E9AB72243049FEB8DEECC146B9B1, - ( - 0x188DA80EB03090F67CBF20EB43A18800F4FF0AFD82FF1012, - 0x07192B95FFC8DA78631011ED6B24CDD573F977A11E794811 - ) - ), - 712: ( - # secp224k1 - 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFE56D, - 0x010000000000000000000000000001DCE8D2EC6184CAF0A971769FB1F7, - 0, - 5, - ( - 0xA1455B334DF099DF30FC28A169A467E9E47075A90F7E650EB6B7A45C, - 0x7E089FED7FBA344282CAFBD6F7E319F7C0B0BD59E2CA4BDB556D61A5 - ) - ), - 713: ( - # secp224r1 - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000001, - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF16A2E0B8F03E13DD29455C5C2A3D, - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFE, - 0xB4050A850C04B3ABF54132565044B0B7D7BFD8BA270B39432355FFB4, - ( - 0xB70E0CBD6BB4BF7F321390B94A03C1D356C21122343280D6115C1D21, - 0xBD376388B5F723FB4C22DFE6CD4375A05A07476444D5819985007E34 - ) - ), - 714: ( - # secp256k1 - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F, - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141, - 0, - 7, - ( - 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, - 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8 - ) - ), - 415: ( - # prime256v1 - 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF, - 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551, - 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC, - 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B, - ( - 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296, - 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5 - ) - ), - 715: ( - # secp384r1 - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF, - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973, - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC, - 0xB3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF, - ( - 0xAA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7, - 0x3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F - ) - ), - 716: ( - # secp521r1 - 0x01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, - 0x01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409, - 0x01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC, - 0x0051953EB9618E1C9A1F929A21A0B68540EEA2DA725B99B315F3B8B489918EF109E156193951EC7E937B1652C0BD3BB1BF073573DF883D2C34F1EF451FD46B503F00, - ( - 0x00C6858E06B70404E9CD9E3ECB662395B4429C648139053FB521F828AF606B4D3DBAA14B5E77EFE75928FE1DC127A2FFA8DE3348B3C1856A429BF97E7E31C2E5BD66, - 0x011839296A789A3BC0045C8A5FB42C7D1BD998F54449579B446817AFBD17273E662C97EE72995EF42640C550B9013FAD0761353C7086A272C24088BE94769FD16650 - ) - ) -} -# pylint: enable=line-too-long - - class EllipticCurveBackend: - def __init__(self, nid): - self.p, self.n, self.a, self.b, self.g = CURVES[nid] - self.jacobian = JacobianCurve(*CURVES[nid]) + def __init__(self, p, n, a, b, g): + self.p, self.n, self.a, self.b, self.g = p, n, a, b, g + self.jacobian = JacobianCurve(p, n, a, b, g) - self.public_key_length = (len(bin(self.p).replace("0b", "")) + 7) // 8 - self.order_bitlength = len(bin(self.n).replace("0b", "")) + self.public_key_length = (len(bin(p).replace("0b", "")) + 7) // 8 + self.order_bitlength = len(bin(n).replace("0b", "")) def _int_to_bytes(self, raw, len=None): diff --git a/src/lib/sslcrypto/openssl/ecc.py b/src/lib/sslcrypto/openssl/ecc.py index 5b5f0bde..f9271e43 100644 --- a/src/lib/sslcrypto/openssl/ecc.py +++ b/src/lib/sslcrypto/openssl/ecc.py @@ -10,7 +10,7 @@ from .library import lib, openssl_backend lib.BN_new.restype = ctypes.POINTER(ctypes.c_char) lib.BN_bin2bn.restype = ctypes.POINTER(ctypes.c_char) lib.BN_CTX_new.restype = ctypes.POINTER(ctypes.c_char) -lib.EC_GROUP_new_by_curve_name.restype = ctypes.POINTER(ctypes.c_char) +lib.EC_GROUP_new_curve_GFp.restype = ctypes.POINTER(ctypes.c_char) lib.EC_KEY_new.restype = ctypes.POINTER(ctypes.c_char) lib.EC_POINT_new.restype = ctypes.POINTER(ctypes.c_char) lib.EC_KEY_get0_private_key.restype = ctypes.POINTER(ctypes.c_char) @@ -28,12 +28,12 @@ thread_local = threading.local() # 1.1.0) use global objects so they aren't thread safe. Fortunately we can check # the code to find out which functions are thread safe. # -# For example, EC_GROUP_new_by_curve_name checks global error code to initialize +# For example, EC_GROUP_new_curve_GFp checks global error code to initialize # the group, so if two errors happen at once or two threads read the error code, # or the codes are read in the wrong order, the group is initialized in a wrong # way. # -# EC_KEY_new_by_curve_name calls EC_GROUP_new_by_curve_name so it's not thread +# EC_KEY_new_by_curve_name calls EC_GROUP_new_curve_GFp so it's not thread # safe. We can't use the lock because it would be too slow; instead, we use # EC_KEY_new and then EC_KEY_set_group which calls EC_GROUP_copy instead which # is thread safe. @@ -68,14 +68,16 @@ class BN: if value is None: self.bn = lib.BN_new() self._free = True - elif isinstance(value, bytes): - self.bn = lib.BN_bin2bn(value, len(value), None) - self._free = True - else: + elif isinstance(value, int) and value < 256: self.bn = lib.BN_new() lib.BN_clear(self.bn) lib.BN_add_word(self.bn, value) self._free = True + else: + if isinstance(value, int): + value = value.to_bytes(128, "big") + self.bn = lib.BN_bin2bn(value, len(value), None) + self._free = True def __del__(self): @@ -201,21 +203,27 @@ class BN: class EllipticCurveBackend: - def __init__(self, nid): + def __init__(self, p, n, a, b, g): + bn_ctx = BN.Context.get() + self.lib = lib # For finalizer - self.nid = nid + + self.p = BN(p) + self.order = BN(n) + self.a = BN(a) + self.b = BN(b) + self.h = BN((p + n // 2) // n) + with lock: # Thread-safety - self.group = lib.EC_GROUP_new_by_curve_name(self.nid) + self.group = lib.EC_GROUP_new_curve_GFp(self.p.bn, self.a.bn, self.b.bn, bn_ctx) + if not self.group: + raise ValueError("Could not create group object") + generator = self._public_key_to_point(g) + lib.EC_GROUP_set_generator(self.group, generator, self.order.bn, self.h.bn) if not self.group: raise ValueError("The curve is not supported by OpenSSL") - self.order = BN() - self.p = BN() - bn_ctx = BN.Context.get() - lib.EC_GROUP_get_order(self.group, self.order.bn, bn_ctx) - lib.EC_GROUP_get_curve_GFp(self.group, self.p.bn, None, None, bn_ctx) - self.public_key_length = (len(self.p) + 7) // 8 self.is_supported_evp_pkey_ctx = hasattr(lib, "EVP_PKEY_CTX_new") From d3d18234df47bf3f667e495dee0d691b10884f98 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Mon, 16 Mar 2020 20:50:10 +0300 Subject: [PATCH 2368/2570] Upgrade gevent-ws to v2.0.5 --- src/lib/gevent_ws/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/lib/gevent_ws/__init__.py b/src/lib/gevent_ws/__init__.py index e295b41e..51542835 100644 --- a/src/lib/gevent_ws/__init__.py +++ b/src/lib/gevent_ws/__init__.py @@ -211,7 +211,7 @@ class WebSocketHandler(WSGIHandler): http_connection = [s.strip().lower() for s in self.environ.get("HTTP_CONNECTION", "").split(",")] - if "upgrade" not in http_connection or self.environ.get("HTTP_UPGRADE", "") != "websocket": + if "upgrade" not in http_connection or self.environ.get("HTTP_UPGRADE", "").lower() != "websocket": # Not my problem return super(WebSocketHandler, self).handle_one_response() @@ -259,3 +259,10 @@ class WebSocketHandler(WSGIHandler): def process_result(self): if "wsgi.websocket" not in self.environ: super(WebSocketHandler, self).process_result() + + @property + def version(self): + if not self.environ: + return None + + return self.environ.get('HTTP_SEC_WEBSOCKET_VERSION') From ba156bbdec26b8dcc5cc943e7b795f23a4789ad0 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Tue, 17 Mar 2020 07:54:56 +0300 Subject: [PATCH 2369/2570] Potential fix of BrokenPipeError --- src/lib/gevent_ws/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib/gevent_ws/__init__.py b/src/lib/gevent_ws/__init__.py index 51542835..8f0810bb 100644 --- a/src/lib/gevent_ws/__init__.py +++ b/src/lib/gevent_ws/__init__.py @@ -195,7 +195,10 @@ class WebSocket: def close(self, status=STATUS_OK): self.closed = True - self._send_frame(OPCODE_CLOSE, struct.pack("!H", status)) + try: + self._send_frame(OPCODE_CLOSE, struct.pack("!H", status)) + except (BrokenPipeError, ConnectionResetError): + pass self.socket.close() From 5fb342a8251d9ee57ad92d95db6a09607adaea42 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Mar 2020 14:48:24 +0100 Subject: [PATCH 2370/2570] Change to GPLv3 license Based on https://github.com/HelloZeroNet/ZeroNet/issues/2273 --- COPYING | 674 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ LICENSE | 351 +---------------------------- 2 files changed, 685 insertions(+), 340 deletions(-) create mode 100644 COPYING diff --git a/COPYING b/COPYING new file mode 100644 index 00000000..f288702d --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/LICENSE b/LICENSE index d6a93266..6f921e63 100644 --- a/LICENSE +++ b/LICENSE @@ -1,340 +1,11 @@ -GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - {description} - Copyright (C) {year} {fullname} - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - {signature of Ty Coon}, 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. - +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 3. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . From 2de3c9a54463f8200189bd3ebefaaa42d17c9431 Mon Sep 17 00:00:00 2001 From: Vadim Ushakov Date: Tue, 17 Mar 2020 23:09:40 +0700 Subject: [PATCH 2371/2570] Allow opening the sidebar while content.json is not loaded If one opens the sidebar of a site not being downloaded yet, the following error occurs: ``` Internal error: KeyError('content.json',): 'content.json' UiWebsocket.py line 79 > 235 > Sidebar/SidebarPlugin.py line 527 > 120 > ContentDbDict.py line 59 ``` Also, the sidebar is not visible. This fixes the both issues. For sites without peers, the only way to delete the site was to navigate to ZeroHellow, scroll the left panel to "Connecting sites", and delete the site from the list. Now those sites can be deleted from the sidebar. --- plugins/Sidebar/SidebarPlugin.py | 2 +- plugins/Sidebar/media/Sidebar.css | 2 +- plugins/Sidebar/media/all.css | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index e6087adf..ab2e9683 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -117,7 +117,7 @@ class UiWebsocketPlugin(object): 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["content.json"].get("domain", site.address), + site.content_manager.contents.get("content.json", {}).get("domain", site.address), ",".join(peer_ips) ) diff --git a/plugins/Sidebar/media/Sidebar.css b/plugins/Sidebar/media/Sidebar.css index cf56f061..2468eb0d 100644 --- a/plugins/Sidebar/media/Sidebar.css +++ b/plugins/Sidebar/media/Sidebar.css @@ -18,7 +18,7 @@ .sidebar .link-right:active { background-color: #444 } /* SIDEBAR */ -.sidebar-container { width: 100%; height: 100%; overflow: hidden; position: fixed; } +.sidebar-container { width: 100%; height: 100%; overflow: hidden; position: fixed; z-index: 2; } .sidebar { background-color: #212121; position: fixed; backface-visibility: hidden; right: -1200px; height: 100%; width: 1200px; } /*box-shadow: inset 0px 0px 10px #000*/ .sidebar .content { margin: 30px; font-family: "Segoe UI Light", "Segoe UI", "Helvetica Neue"; color: white; width: 375px; height: 300px; font-weight: 200; transition: all 1s; opacity: 0 } .sidebar-container.loaded .content { opacity: 1; transform: none } diff --git a/plugins/Sidebar/media/all.css b/plugins/Sidebar/media/all.css index 4921d57f..741169f3 100644 --- a/plugins/Sidebar/media/all.css +++ b/plugins/Sidebar/media/all.css @@ -130,7 +130,7 @@ .sidebar .link-right:active { background-color: #444 } /* SIDEBAR */ -.sidebar-container { width: 100%; height: 100%; overflow: hidden; position: fixed; } +.sidebar-container { width: 100%; height: 100%; overflow: hidden; position: fixed; z-index: 2; } .sidebar { background-color: #212121; position: fixed; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden ; right: -1200px; height: 100%; width: 1200px; } /*box-shadow: inset 0px 0px 10px #000*/ .sidebar .content { margin: 30px; font-family: "Segoe UI Light", "Segoe UI", "Helvetica Neue"; color: white; width: 375px; height: 300px; font-weight: 200; -webkit-transition: all 1s; -moz-transition: all 1s; -o-transition: all 1s; -ms-transition: all 1s; transition: all 1s ; opacity: 0 } .sidebar-container.loaded .content { opacity: 1; -webkit-transform: none ; -moz-transform: none ; -o-transform: none ; -ms-transform: none ; transform: none } From 66194ce43588bbef51a14bad3c4408e25d451d13 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Tue, 17 Mar 2020 23:48:36 +0300 Subject: [PATCH 2372/2570] Update gevent-ws to v2.0.7 to fix werkzeug --- src/lib/gevent_ws/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/lib/gevent_ws/__init__.py b/src/lib/gevent_ws/__init__.py index 8f0810bb..76564054 100644 --- a/src/lib/gevent_ws/__init__.py +++ b/src/lib/gevent_ws/__init__.py @@ -257,12 +257,18 @@ class WebSocketHandler(WSGIHandler): finally: self.time_finish = time.time() self.log_request() + self.close_connection = True def process_result(self): - if "wsgi.websocket" not in self.environ: + if "wsgi.websocket" in self.environ: + # Flushing result is required for werkzeug compatibility + for elem in self.result: + pass + else: super(WebSocketHandler, self).process_result() + @property def version(self): if not self.environ: From a5971adbe6d36ca737e00c063ed7bbf8a79bdacd Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 18 Mar 2020 03:19:01 +0100 Subject: [PATCH 2373/2570] Add data_dir to example UiConfig tracker list --- plugins/UiConfig/media/js/ConfigStorage.coffee | 2 +- plugins/UiConfig/media/js/all.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/UiConfig/media/js/ConfigStorage.coffee b/plugins/UiConfig/media/js/ConfigStorage.coffee index 9f35a91c..e5b37773 100644 --- a/plugins/UiConfig/media/js/ConfigStorage.coffee +++ b/plugins/UiConfig/media/js/ConfigStorage.coffee @@ -111,7 +111,7 @@ class ConfigStorage extends Class key: "trackers_file" type: "textarea" description: "Load additional list of torrent trackers dynamically, from a file" - placeholder: "Eg.: data/trackers.json" + placeholder: "Eg.: {data_dir}/trackers.json" value_pos: "fullwidth" section.items.push diff --git a/plugins/UiConfig/media/js/all.js b/plugins/UiConfig/media/js/all.js index 43a91bc8..1b8083c9 100644 --- a/plugins/UiConfig/media/js/all.js +++ b/plugins/UiConfig/media/js/all.js @@ -1426,7 +1426,7 @@ key: "trackers_file", type: "textarea", description: "Load additional list of torrent trackers dynamically, from a file", - placeholder: "Eg.: data/trackers.json", + placeholder: "Eg.: {data_dir}/trackers.json", value_pos: "fullwidth" }); section.items.push({ From ca94703fc3c7df95500d1c4dd8d2ac91f1267399 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 18 Mar 2020 03:21:00 +0100 Subject: [PATCH 2374/2570] Fix tray icon destroy overflow exception --- plugins/Trayicon/lib/notificationicon.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/Trayicon/lib/notificationicon.py b/plugins/Trayicon/lib/notificationicon.py index 57a4ddd6..8b913f83 100644 --- a/plugins/Trayicon/lib/notificationicon.py +++ b/plugins/Trayicon/lib/notificationicon.py @@ -566,10 +566,11 @@ class NotificationIcon(object): # print "NotificationIcon error", err, message message = MSG() time.sleep(0.125) - print("Icon thread stopped, removing icon...") + print("Icon thread stopped, removing icon (hicon: %s, hwnd: %s)..." % (self._hicon, self._hwnd)) Shell_NotifyIcon(NIM_DELETE, ctypes.cast(ctypes.pointer(iconinfo), ctypes.POINTER(NOTIFYICONDATA))) ctypes.windll.user32.DestroyWindow(self._hwnd) + ctypes.windll.user32.DestroyIcon.argtypes = [ctypes.wintypes.HICON] ctypes.windll.user32.DestroyIcon(self._hicon) From 723d1f4370f301aeefdf2391c79ea6ab880535e7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 18 Mar 2020 03:21:14 +0100 Subject: [PATCH 2375/2570] Rev4467 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 101f1277..bff10e93 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4462 + self.rev = 4467 self.argv = argv self.action = None self.test_parser = None From f41d02203862361cfc92e84eadeb0fc4c82065fb Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 20 Mar 2020 18:52:18 +0100 Subject: [PATCH 2376/2570] Log BrokenPipeError as warning --- src/Ui/UiServer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ui/UiServer.py b/src/Ui/UiServer.py index 188ff811..edeed59d 100644 --- a/src/Ui/UiServer.py +++ b/src/Ui/UiServer.py @@ -48,7 +48,7 @@ class UiWSGIHandler(WebSocketHandler): err_name = "UiWSGIHandler websocket" if "HTTP_UPGRADE" in self.environ else "UiWSGIHandler" try: super(UiWSGIHandler, self).run_application() - except (ConnectionAbortedError, ConnectionResetError) as err: + except (ConnectionAbortedError, ConnectionResetError, BrokenPipeError) as err: logging.warning("%s connection error: %s" % (err_name, err)) except Exception as err: logging.warning("%s error: %s" % (err_name, Debug.formatException(err))) From 70de3213d6cc36eaec81d2f537b848fd91713b76 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 20 Mar 2020 18:52:58 +0100 Subject: [PATCH 2377/2570] Fix peer save dictionary changed error --- plugins/PeerDb/PeerDbPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/PeerDb/PeerDbPlugin.py b/plugins/PeerDb/PeerDbPlugin.py index 2ce5e39f..a66b81cf 100644 --- a/plugins/PeerDb/PeerDbPlugin.py +++ b/plugins/PeerDb/PeerDbPlugin.py @@ -59,7 +59,7 @@ class ContentDbPlugin(object): def iteratePeers(self, site): site_id = self.site_ids.get(site.address) - for key, peer in site.peers.items(): + for key, peer in list(site.peers.items()): address, port = key.rsplit(":", 1) if peer.has_hashfield: hashfield = sqlite3.Binary(peer.hashfield.tobytes()) From 1eec38825211586aa840c38d564573e4b8236cde Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 20 Mar 2020 18:53:25 +0100 Subject: [PATCH 2378/2570] Rev4469 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index bff10e93..2b5656a3 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4467 + self.rev = 4469 self.argv = argv self.action = None self.test_parser = None From 31d43049153477e0a5545fe5f192b08b27f8a02f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Mar 2020 19:51:44 +0100 Subject: [PATCH 2379/2570] Rev4471, Allow files start with dot --- src/Config.py | 2 +- src/Content/ContentManager.py | 2 +- src/Test/TestContent.py | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index 2b5656a3..530d606e 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4469 + self.rev = 4471 self.argv = argv self.action = None self.test_parser = None diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 6256a8cd..27da402b 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -599,7 +599,7 @@ class ContentManager(object): return False elif len(relative_path) > 255: return False - elif relative_path[0] in (".", "/"): # Starts with + elif relative_path[0] in ("/", "\\"): # Starts with return False elif relative_path[-1] in (".", " "): # Ends with return False diff --git a/src/Test/TestContent.py b/src/Test/TestContent.py index d46abc1f..7e7ca1a5 100644 --- a/src/Test/TestContent.py +++ b/src/Test/TestContent.py @@ -258,11 +258,13 @@ class TestContent: assert not site.content_manager.isValidRelativePath("any\\hello.txt") # \ not allowed assert not site.content_manager.isValidRelativePath("/hello.txt") # Cannot start with / + assert not site.content_manager.isValidRelativePath("\\hello.txt") # Cannot start with \ assert not site.content_manager.isValidRelativePath("../hello.txt") # Not allowed .. in path assert not site.content_manager.isValidRelativePath("\0hello.txt") # NULL character assert not site.content_manager.isValidRelativePath("\31hello.txt") # 0-31 (ASCII control characters) assert not site.content_manager.isValidRelativePath("any/hello.txt ") # Cannot end with space assert not site.content_manager.isValidRelativePath("any/hello.txt.") # Cannot end with dot + assert site.content_manager.isValidRelativePath(".hello.txt") # Allow start with dot assert not site.content_manager.isValidRelativePath("any/CON") # Protected names on Windows assert not site.content_manager.isValidRelativePath("CON/any.txt") assert not site.content_manager.isValidRelativePath("any/lpt1.txt") From a4d91f7081c4f2b06c2c18bfacf04f8bdf964b06 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Sat, 21 Mar 2020 22:52:56 +0300 Subject: [PATCH 2380/2570] Import sslcrypto from lib --- src/Crypt/CryptBitcoin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Crypt/CryptBitcoin.py b/src/Crypt/CryptBitcoin.py index f54015dc..ae34141f 100644 --- a/src/Crypt/CryptBitcoin.py +++ b/src/Crypt/CryptBitcoin.py @@ -9,7 +9,7 @@ from Config import config lib_verify_best = "sslcrypto" -import sslcrypto +from lib import sslcrypto sslcurve_native = sslcrypto.ecc.get_curve("secp256k1") sslcurve_fallback = sslcrypto.fallback.ecc.get_curve("secp256k1") sslcurve = sslcurve_native From abde3d4cf7550fcde5894942695716a19db2a46e Mon Sep 17 00:00:00 2001 From: ZeroNet Date: Tue, 24 Mar 2020 01:58:33 +0100 Subject: [PATCH 2381/2570] Update Dockerfile --- Dockerfile | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index b85d44f1..abdbec71 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,11 +6,17 @@ ENV HOME /root COPY requirements.txt /root/requirements.txt #Install ZeroNet -RUN apk --no-cache --no-progress add python3 python3-dev gcc libffi-dev musl-dev make tor openssl \ +RUN apt-get update && apt-get install -yq python3 python3-dev openssl \ + && apt-get list \ + && apk --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 \ && echo "ControlPort 9051" >> /etc/tor/torrc \ - && echo "CookieAuthentication 1" >> /etc/tor/torrc + && echo "CookieAuthentication 1" >> /etc/tor/torrc \ + && python3 -V \ + && python3 -m pip list \ + && tor --version \ + && openssl version #Add Zeronet source COPY . /root From 740fe6535526080cdfc51b5ba88b0a606bd649fc Mon Sep 17 00:00:00 2001 From: ZeroNet Date: Tue, 24 Mar 2020 02:09:57 +0100 Subject: [PATCH 2382/2570] Update Dockerfile --- Dockerfile | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index abdbec71..85d42169 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.8 +FROM alpine:3.10 #Base settings ENV HOME /root @@ -6,14 +6,13 @@ ENV HOME /root COPY requirements.txt /root/requirements.txt #Install ZeroNet -RUN apt-get update && apt-get install -yq python3 python3-dev openssl \ - && apt-get list \ - && apk --no-cache --no-progress add python3 python3-dev gcc libffi-dev musl-dev make tor openssl \ +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 \ && echo "ControlPort 9051" >> /etc/tor/torrc \ - && echo "CookieAuthentication 1" >> /etc/tor/torrc \ - && python3 -V \ + && echo "CookieAuthentication 1" >> /etc/tor/torrc + +RUN python3 -V \ && python3 -m pip list \ && tor --version \ && openssl version From 108a3de433ce25db58475c488d1450645614b2ca Mon Sep 17 00:00:00 2001 From: ZeroNet Date: Tue, 24 Mar 2020 02:26:54 +0100 Subject: [PATCH 2383/2570] Update Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 85d42169..7839cfa0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.10 +FROM alpine:3.11 #Base settings ENV HOME /root From e1c0fd69849832ae61416dc2b61a028b4bc1ef52 Mon Sep 17 00:00:00 2001 From: Alfonso Montero Date: Tue, 24 Mar 2020 21:16:33 +0100 Subject: [PATCH 2384/2570] Readme: Add Docker image info and docker pulls badge --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a4ade769..a36ec44a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ZeroNet [![Build Status](https://travis-ci.org/HelloZeroNet/ZeroNet.svg?branch=py3)](https://travis-ci.org/HelloZeroNet/ZeroNet) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://zeronet.io/docs/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://zeronet.io/docs/help_zeronet/donate/) ![tests](https://github.com/HelloZeroNet/ZeroNet/workflows/tests/badge.svg) +# ZeroNet [![Build Status](https://travis-ci.org/HelloZeroNet/ZeroNet.svg?branch=py3)](https://travis-ci.org/HelloZeroNet/ZeroNet) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://zeronet.io/docs/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://zeronet.io/docs/help_zeronet/donate/) ![tests](https://github.com/HelloZeroNet/ZeroNet/workflows/tests/badge.svg) [![Docker Pulls](https://img.shields.io/docker/pulls/nofish/zeronet)](https://hub.docker.com/r/nofish/zeronet) Decentralized websites using Bitcoin crypto and the BitTorrent network - https://zeronet.io @@ -82,6 +82,9 @@ Decentralized websites using Bitcoin crypto and the BitTorrent network - https:/ __Tip:__ Start with `./ZeroNet.sh --ui_ip '*' --ui_restrict your.ip.address` to allow remote connections on the web interface. +#### Docker +There is an official image, built from source at: https://hub.docker.com/r/nofish/zeronet/ + ### Install from source - `wget https://github.com/HelloZeroNet/ZeroNet/archive/py3/ZeroNet-py3.tar.gz` From 1de748585846e935d9d88bc7f22c69c84f7b8252 Mon Sep 17 00:00:00 2001 From: canewsin Date: Wed, 25 Mar 2020 03:30:41 +0530 Subject: [PATCH 2385/2570] Update LICENSE --- LICENSE | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/LICENSE b/LICENSE index 6f921e63..0d17b72d 100644 --- a/LICENSE +++ b/LICENSE @@ -9,3 +9,19 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . + + +Additional Conditions : + +Contributing to this repo + This repo is governed by GPLv3, same is located at the root of the ZeroNet git repo, + unless specified separately all code is governed by that license, contributions to this repo + are divided into two key types, key contributions and non-key contributions, key contributions + are which, directly affects the code performance, quality and features of software, + non key contributions include things like translation datasets, image, graphic or video + contributions that does not affect the main usability of software but improves the existing + usability of certain thing or feature, these also include tests written with code, since their + purpose is to check, whether something is working or not as intended. All the non-key contributions + are governed by [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/), unless specified + above, a contribution is ruled by the type of contribution if there is a conflict between two + contributing parties of repo in any case. From 56acac8cd3ebb99e2c451b960d7e7bcfc6e92940 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 25 Mar 2020 04:13:16 +0100 Subject: [PATCH 2386/2570] Rev4473, Fix Merger site skipping content load to db for some seconds after new site added --- plugins/MergerSite/MergerSitePlugin.py | 7 ++----- src/Config.py | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/plugins/MergerSite/MergerSitePlugin.py b/plugins/MergerSite/MergerSitePlugin.py index d2c24398..2dccc6de 100644 --- a/plugins/MergerSite/MergerSitePlugin.py +++ b/plugins/MergerSite/MergerSitePlugin.py @@ -298,9 +298,6 @@ class SiteStoragePlugin(object): # Also notice merger sites on a merged site file change def onUpdated(self, inner_path, file=None): - if inner_path == "content.json": - site_manager.updateMergerSites() - super(SiteStoragePlugin, self).onUpdated(inner_path, file) merged_type = merged_db.get(self.site.address) @@ -397,6 +394,6 @@ class SiteManagerPlugin(object): super(SiteManagerPlugin, self).load(*args, **kwags) self.updateMergerSites() - def save(self, *args, **kwags): - super(SiteManagerPlugin, self).save(*args, **kwags) + def saveDelayed(self, *args, **kwags): + super(SiteManagerPlugin, self).saveDelayed(*args, **kwags) self.updateMergerSites() diff --git a/src/Config.py b/src/Config.py index 530d606e..2bd9c44b 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4471 + self.rev = 4473 self.argv = argv self.action = None self.test_parser = None From 0a9a9b5a575db6b30c279393f92c508e29d761db Mon Sep 17 00:00:00 2001 From: Ivanq Date: Mon, 30 Mar 2020 09:16:12 +0300 Subject: [PATCH 2387/2570] Support compressed keys --- plugins/CryptMessage/CryptMessage.py | 2 +- plugins/CryptMessage/CryptMessagePlugin.py | 6 ++--- src/Crypt/CryptBitcoin.py | 3 +-- src/lib/sslcrypto/_ecc.py | 31 +++++++++++++++++----- src/lib/sslcrypto/openssl/ecc.py | 2 +- 5 files changed, 31 insertions(+), 13 deletions(-) diff --git a/plugins/CryptMessage/CryptMessage.py b/plugins/CryptMessage/CryptMessage.py index 74659404..8349809c 100644 --- a/plugins/CryptMessage/CryptMessage.py +++ b/plugins/CryptMessage/CryptMessage.py @@ -32,7 +32,7 @@ def eciesDecryptMulti(encrypted_datas, privatekey): def eciesDecrypt(ciphertext, privatekey): - return curve.decrypt(base64.b64decode(ciphertext), curve.wif_to_private(privatekey), derivation="sha512") + return curve.decrypt(base64.b64decode(ciphertext), curve.wif_to_private(privatekey.encode()), derivation="sha512") def decodePubkey(pubkey): diff --git a/plugins/CryptMessage/CryptMessagePlugin.py b/plugins/CryptMessage/CryptMessagePlugin.py index 150bf8be..7c24f730 100644 --- a/plugins/CryptMessage/CryptMessagePlugin.py +++ b/plugins/CryptMessage/CryptMessagePlugin.py @@ -110,7 +110,7 @@ class UiWebsocketPlugin(object): # Gets the publickey of a given privatekey def actionEccPrivToPub(self, to, privatekey): - self.response(to, curve.private_to_public(curve.wif_to_private(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): @@ -149,8 +149,8 @@ class UserPlugin(object): index = param_index if "encrypt_publickey_%s" % index not in site_data: - privatekey = self.getEncryptPrivatekey(address, param_index) - publickey = curve.private_to_public(curve.wif_to_private(privatekey)) + 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] diff --git a/src/Crypt/CryptBitcoin.py b/src/Crypt/CryptBitcoin.py index ae34141f..c81999dd 100644 --- a/src/Crypt/CryptBitcoin.py +++ b/src/Crypt/CryptBitcoin.py @@ -60,7 +60,7 @@ def privatekeyToAddress(privatekey): # Return address from private key privatekey_bin = bytes.fromhex(privatekey) else: privatekey_bin = sslcurve.wif_to_private(privatekey.encode()) - return sslcurve.private_to_address(privatekey_bin, is_compressed=False).decode() + return sslcurve.private_to_address(privatekey_bin).decode() except Exception: # Invalid privatekey return False @@ -71,7 +71,6 @@ def sign(data, privatekey): # Return sign to data using private key return base64.b64encode(sslcurve.sign( data.encode(), sslcurve.wif_to_private(privatekey.encode()), - is_compressed=False, recoverable=True, hash=dbl_format )).decode() diff --git a/src/lib/sslcrypto/_ecc.py b/src/lib/sslcrypto/_ecc.py index 3dbc56cd..88e04576 100644 --- a/src/lib/sslcrypto/_ecc.py +++ b/src/lib/sslcrypto/_ecc.py @@ -296,11 +296,18 @@ class EllipticCurve: return x, y - def new_private_key(self): - return self._backend.new_private_key() + def new_private_key(self, is_compressed=False): + return self._backend.new_private_key() + (b"\x01" if is_compressed else b"") - def private_to_public(self, private_key, is_compressed=True): + def private_to_public(self, private_key): + if len(private_key) == self._backend.public_key_length: + is_compressed = False + elif len(private_key) == self._backend.public_key_length + 1 and private_key[-1] == 1: + is_compressed = True + private_key = private_key[:-1] + else: + raise ValueError("Private key has invalid length") x, y = self._backend.private_to_public(private_key) return self._encode_public_key(x, y, is_compressed=is_compressed) @@ -322,12 +329,16 @@ class EllipticCurve: return base58.b58encode_check(b"\x00" + hash160) - def private_to_address(self, private_key, is_compressed=True): + def private_to_address(self, private_key): # Kinda useless but left for quick migration from pybitcointools - return self.public_to_address(self.private_to_public(private_key, is_compressed=is_compressed)) + return self.public_to_address(self.private_to_public(private_key)) def derive(self, private_key, public_key): + if len(private_key) == self._backend.public_key_length + 1 and private_key[-1] == 1: + private_key = private_key[:-1] + if len(private_key) != self._backend.public_key_length: + raise ValueError("Private key has invalid length") if not isinstance(public_key, tuple): public_key = self._decode_public_key(public_key) return self._backend.ecdh(private_key, public_key) @@ -447,7 +458,15 @@ class EllipticCurve: return self._aes.decrypt(ciphertext, iv, k_enc, algo=algo) - def sign(self, data, private_key, hash="sha256", recoverable=False, is_compressed=True, entropy=None): + def sign(self, data, private_key, hash="sha256", recoverable=False, entropy=None): + if len(private_key) == self._backend.public_key_length: + is_compressed = False + elif len(private_key) == self._backend.public_key_length + 1 and private_key[-1] == 1: + is_compressed = True + private_key = private_key[:-1] + else: + raise ValueError("Private key has invalid length") + data = self._digest(data, hash) if not entropy: v = b"\x01" * len(data) diff --git a/src/lib/sslcrypto/openssl/ecc.py b/src/lib/sslcrypto/openssl/ecc.py index f9271e43..c667be8a 100644 --- a/src/lib/sslcrypto/openssl/ecc.py +++ b/src/lib/sslcrypto/openssl/ecc.py @@ -311,7 +311,7 @@ class EllipticCurveBackend: # To big integer private_key = BN(lib.EC_KEY_get0_private_key(eckey), link_only=True) # To binary - private_key_buf = private_key.bytes() + private_key_buf = private_key.bytes(self.public_key_length) # Cleanup lib.EC_KEY_free(eckey) return private_key_buf From 71001491df2d95716e4cf45875ba7b5a83b05aeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=A0?= Date: Wed, 8 Apr 2020 17:05:39 +0200 Subject: [PATCH 2388/2570] Use Gevent prerelease for Python 3.8 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 83f3c3ea..598a3625 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ gevent>=1.1.0; python_version < "3.8" -https://github.com/gevent/gevent/archive/master.zip; python_version >= "3.8" +gevent>=1.5.0a2; python_version >= "3.8" msgpack>=0.4.4 base58 merkletools From ad3920b26a50f4eaaa674d62531bd365c8ce1a0c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 11 Apr 2020 13:34:18 +0200 Subject: [PATCH 2389/2570] Rev4478, Skip slow updated files checking with large content.json --- src/Config.py | 2 +- src/Ui/UiWebsocket.py | 3 +++ src/Ui/media/Wrapper.coffee | 5 ++--- src/Ui/media/all.js | 7 ++++--- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Config.py b/src/Config.py index 2bd9c44b..0d378cdb 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4473 + self.rev = 4478 self.argv = argv self.action = None self.test_parser = None diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 7fce398b..cbc3b8fe 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -1041,6 +1041,9 @@ class UiWebsocket(object): inner_paths = [content_inner_path] + list(content.get("includes", {}).keys()) + list(content.get("files", {}).keys()) + if len(inner_paths) > 100: + return {"error": "Too many files in content.json"} + for relative_inner_path in inner_paths: inner_path = helper.getDirname(content_inner_path) + relative_inner_path try: diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index cecd1bc6..dceb4f57 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -623,7 +623,7 @@ class Wrapper updateModifiedPanel: => @ws.cmd "siteListModifiedFiles", [], (res) => - num = res.modified_files.length + num = res.modified_files?.length if num > 0 closed = @site_info.settings.modified_files_notification == false @infopanel.show(closed) @@ -642,8 +642,7 @@ class Wrapper @notifications.add "sign", "done", "content.json Signed!", 5000 @sitePublish("content.json") return false - - @log "siteListModifiedFiles", res + @log "siteListModifiedFiles", num, res setAnnouncerInfo: (announcer_info) -> status_db = {announcing: [], error: [], announced: []} diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index eb3dc905..f1eebe84 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -1856,8 +1856,8 @@ $.extend( $.easing, Wrapper.prototype.updateModifiedPanel = function() { return this.ws.cmd("siteListModifiedFiles", [], (function(_this) { return function(res) { - var closed, num; - num = res.modified_files.length; + var closed, num, ref; + num = (ref = res.modified_files) != null ? ref.length : void 0; if (num > 0) { closed = _this.site_info.settings.modified_files_notification === false; _this.infopanel.show(closed); @@ -1877,7 +1877,8 @@ $.extend( $.easing, return false; }); } - return _this.log("siteListModifiedFiles", res); + debugger; + return _this.log("siteListModifiedFiles", num, res); }; })(this)); }; From f3a839f422afd50f5bf8f17d83a8807571bc1dfe Mon Sep 17 00:00:00 2001 From: ZeroNet Date: Sun, 12 Apr 2020 12:15:10 +0200 Subject: [PATCH 2390/2570] Require final gevent 1.5.0 for Python 3.8 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 598a3625..f1f6068c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ gevent>=1.1.0; python_version < "3.8" -gevent>=1.5.0a2; python_version >= "3.8" +gevent>=1.5.0; python_version >= "3.8" msgpack>=0.4.4 base58 merkletools From a657afcd47bc3e0bc4ed7a474f71d3495b01b032 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Fri, 1 May 2020 18:24:47 +0100 Subject: [PATCH 2391/2570] Add missing seekable() class method to BigFile plugin --- plugins/Bigfile/BigfilePlugin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index 41506f13..6ab45372 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -597,6 +597,9 @@ class BigFile(object): whence = 0 return self.f.seek(pos, whence) + def seekable(self): + return self.f.seekable() + def tell(self): return self.f.tell() From 07faa3d6d3bd8f6c786d2345ed232740936cd32a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 3 May 2020 03:56:06 +0200 Subject: [PATCH 2392/2570] Move wrapper necessary check to separate function --- src/Ui/UiRequest.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 51730954..506f9661 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -334,6 +334,22 @@ class UiRequest(object): return template_rendered.encode("utf8") + def isWrapperNecessary(self, path): + match = re.match(r"/(?P
    [A-Za-z0-9\._-]+)(?P/.*|$)", path) + + if not match: + return True + + inner_path = match.group("inner_path").lstrip("/") + if not inner_path or path.endswith("/"): # It's a directory + content_type = self.getContentType("index.html") + else: # It's a file + content_type = self.getContentType(inner_path) + + is_html_file = "html" in content_type or "xhtml" in content_type + + return is_html_file + # - Actions - # Redirect to an url @@ -356,14 +372,7 @@ class UiRequest(object): address = match.group("address") inner_path = match.group("inner_path").lstrip("/") - if not inner_path or path.endswith("/"): # It's a directory - content_type = self.getContentType("index.html") - else: # It's a file - content_type = self.getContentType(inner_path) - - is_html_file = "html" in content_type or "xhtml" in content_type - - if not is_html_file: + if not self.isWrapperNecessary(path): return self.actionSiteMedia("/media" + path) # Serve non-html files without wrapper if self.isAjaxRequest(): From 439f8fc47613fcf4b8eee7dc9fd71cb3fe791be8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 3 May 2020 03:57:17 +0200 Subject: [PATCH 2393/2570] Fix UiPassword logout and session list url encoding --- plugins/disabled-UiPassword/UiPasswordPlugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/disabled-UiPassword/UiPasswordPlugin.py b/plugins/disabled-UiPassword/UiPasswordPlugin.py index 1962d5e6..bbc2df85 100644 --- a/plugins/disabled-UiPassword/UiPasswordPlugin.py +++ b/plugins/disabled-UiPassword/UiPasswordPlugin.py @@ -90,12 +90,14 @@ class UiRequestPlugin(object): del(cls.sessions[session_id]) # Action: Display sessions + @helper.encodeResponse def actionSessions(self): self.sendHeader() yield "
    "
             yield json.dumps(self.sessions, 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")
    
    From 36d96d484ed8dc3370c2dab9b50b714af702a2c4 Mon Sep 17 00:00:00 2001
    From: shortcutme 
    Date: Sun, 3 May 2020 03:59:09 +0200
    Subject: [PATCH 2394/2570] Workaround for UiPassword cookie issues with
     sandboxed iframes
    
    ---
     .../disabled-UiPassword/UiPasswordPlugin.py   | 65 +++++++++++++++----
     1 file changed, 53 insertions(+), 12 deletions(-)
    
    diff --git a/plugins/disabled-UiPassword/UiPasswordPlugin.py b/plugins/disabled-UiPassword/UiPasswordPlugin.py
    index bbc2df85..812c188b 100644
    --- a/plugins/disabled-UiPassword/UiPasswordPlugin.py
    +++ b/plugins/disabled-UiPassword/UiPasswordPlugin.py
    @@ -14,6 +14,7 @@ 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):
    @@ -28,8 +29,26 @@ def showPasswordAdvice(password):
     @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:
    @@ -42,10 +61,22 @@ class UiRequestPlugin(object):
                         self.cleanup()
                     # Validate session
                     session_id = self.getCookies().get("session_id")
    -                if session_id not in self.sessions:  # Invalid session id, display login
    +                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):
    @@ -53,13 +84,14 @@ class UiRequestPlugin(object):
             self.sendHeader()
             posted = self.getPosted()
             if posted:  # Validate http posted data
    -            if self.checkPassword(posted.get("password")):
    +            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", "")
    @@ -74,20 +106,26 @@ class UiRequestPlugin(object):
                     template = template.replace("{result}", "bad_password")
             yield template
     
    -    def checkPassword(self, password):
    -        return password == config.ui_password
    -
         def randomString(self, nchars):
             return ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(nchars))
     
    -    @classmethod
    -    def cleanup(cls):
    -        cls.last_cleanup = time.time()
    -        for session_id, session in list(cls.sessions.items()):
    +    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
    -                del(cls.sessions[session_id])
    +                self.sessionDelete(session_id)
                 elif not session["keep"] and time.time() - session["added"] > 60 * 60 * 24:  # Max 24h for non-keep sessions
    -                del(cls.sessions[session_id])
    +                self.sessionDelete(session_id)
     
         # Action: Display sessions
         @helper.encodeResponse
    @@ -95,6 +133,8 @@ class UiRequestPlugin(object):
             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
    @@ -103,7 +143,8 @@ class UiRequestPlugin(object):
             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:
    -                del self.sessions[session_id]
    +                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")
    
    From 38c1727b94e0a8b14c28c5400accb444eb840666 Mon Sep 17 00:00:00 2001
    From: shortcutme 
    Date: Sun, 3 May 2020 03:59:33 +0200
    Subject: [PATCH 2395/2570] Rev4485
    
    ---
     src/Config.py | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/src/Config.py b/src/Config.py
    index 0d378cdb..0811fb73 100644
    --- a/src/Config.py
    +++ b/src/Config.py
    @@ -13,7 +13,7 @@ class Config(object):
     
         def __init__(self, argv):
             self.version = "0.7.1"
    -        self.rev = 4478
    +        self.rev = 4485
             self.argv = argv
             self.action = None
             self.test_parser = None
    
    From cfef7ab0713c84f2d8c051f41db545cbd8fbb02e Mon Sep 17 00:00:00 2001
    From: Andrew Morgan 
    Date: Sun, 3 May 2020 14:31:20 +0100
    Subject: [PATCH 2396/2570] Fix pluralize translation function
    
    ---
     src/Translate/Translate.py | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/src/Translate/Translate.py b/src/Translate/Translate.py
    index 066133d1..e73f9be1 100644
    --- a/src/Translate/Translate.py
    +++ b/src/Translate/Translate.py
    @@ -94,9 +94,9 @@ class Translate(dict):
     
         def pluralize(self, value, single, multi):
             if value > 1:
    -            return self[single].format(value)
    -        else:
                 return self[multi].format(value)
    +        else:
    +            return self[single].format(value)
     
         def translateData(self, data, translate_table=None, mode="js"):
             if not translate_table:
    
    From 8db4344171ccda541a38013b85ea19ede1e14604 Mon Sep 17 00:00:00 2001
    From: shortcutme 
    Date: Mon, 4 May 2020 13:38:30 +0200
    Subject: [PATCH 2397/2570] Rev4486, Fix UiPassword cleanup error
    
    ---
     plugins/disabled-UiPassword/UiPasswordPlugin.py | 2 +-
     src/Config.py                                   | 2 +-
     2 files changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/plugins/disabled-UiPassword/UiPasswordPlugin.py b/plugins/disabled-UiPassword/UiPasswordPlugin.py
    index 812c188b..e8a4e4fe 100644
    --- a/plugins/disabled-UiPassword/UiPasswordPlugin.py
    +++ b/plugins/disabled-UiPassword/UiPasswordPlugin.py
    @@ -58,7 +58,7 @@ class UiRequestPlugin(object):
             else:
                 if config.ui_password:
                     if time.time() - self.last_cleanup > 60 * 60:  # Cleanup expired sessions every hour
    -                    self.cleanup()
    +                    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:
    diff --git a/src/Config.py b/src/Config.py
    index 0811fb73..9055270a 100644
    --- a/src/Config.py
    +++ b/src/Config.py
    @@ -13,7 +13,7 @@ class Config(object):
     
         def __init__(self, argv):
             self.version = "0.7.1"
    -        self.rev = 4485
    +        self.rev = 4486
             self.argv = argv
             self.action = None
             self.test_parser = None
    
    From 85733abadec2c548235627ea1d33edf49c389349 Mon Sep 17 00:00:00 2001
    From: Guilherme 
    Date: Sun, 10 May 2020 01:54:39 -0300
    Subject: [PATCH 2398/2570] Remove unnecessary debugger
    
    ---
     src/Ui/media/all.js | 1 -
     1 file changed, 1 deletion(-)
    
    diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js
    index f1eebe84..47d94f9c 100644
    --- a/src/Ui/media/all.js
    +++ b/src/Ui/media/all.js
    @@ -1877,7 +1877,6 @@ $.extend( $.easing,
                   return false;
                 });
               }
    -          debugger;
               return _this.log("siteListModifiedFiles", num, res);
             };
           })(this));
    
    From e4f42b8ce3e4239b2022a179822eda73d2a56f9a Mon Sep 17 00:00:00 2001
    From: Guilherme 
    Date: Mon, 11 May 2020 11:51:10 -0300
    Subject: [PATCH 2399/2570] Avoid iterating in uninitialized result
    
    ---
     src/lib/gevent_ws/__init__.py | 2 ++
     1 file changed, 2 insertions(+)
    
    diff --git a/src/lib/gevent_ws/__init__.py b/src/lib/gevent_ws/__init__.py
    index 76564054..a157e94c 100644
    --- a/src/lib/gevent_ws/__init__.py
    +++ b/src/lib/gevent_ws/__init__.py
    @@ -262,6 +262,8 @@ class WebSocketHandler(WSGIHandler):
     
         def process_result(self):
             if "wsgi.websocket" in self.environ:
    +            if self.result is None:
    +                return
                 # Flushing result is required for werkzeug compatibility
                 for elem in self.result:
                     pass
    
    From a02ed56c6978f130fc7928659f56cfb1aae20389 Mon Sep 17 00:00:00 2001
    From: canewsin 
    Date: Tue, 16 Jun 2020 10:33:21 +0530
    Subject: [PATCH 2400/2570] Added Android Play Store Link to Read Me
    
    ---
     README.md | 8 ++++++++
     1 file changed, 8 insertions(+)
    
    diff --git a/README.md b/README.md
    index a36ec44a..81a8a09a 100644
    --- a/README.md
    +++ b/README.md
    @@ -1,5 +1,9 @@
     # ZeroNet [![Build Status](https://travis-ci.org/HelloZeroNet/ZeroNet.svg?branch=py3)](https://travis-ci.org/HelloZeroNet/ZeroNet) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://zeronet.io/docs/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://zeronet.io/docs/help_zeronet/donate/) ![tests](https://github.com/HelloZeroNet/ZeroNet/workflows/tests/badge.svg) [![Docker Pulls](https://img.shields.io/docker/pulls/nofish/zeronet)](https://hub.docker.com/r/nofish/zeronet)
     
    +[Download from Google Play](https://play.google.com/store/apps/details?id=in.canews.zeronet)
    +
     Decentralized websites using Bitcoin crypto and the BitTorrent network - https://zeronet.io
     
     
    @@ -81,6 +85,10 @@ Decentralized websites using Bitcoin crypto and the BitTorrent network - https:/
      - Open the ZeroHello landing page in your browser by navigating to: http://127.0.0.1:43110/
      
      __Tip:__ Start with `./ZeroNet.sh --ui_ip '*' --ui_restrict your.ip.address` to allow remote connections on the web interface.
    + 
    + ### Android (arm, arm64, x86)
    + - minimum Android version supported 16 (JellyBean).
    + - Google Play Store Link https://play.google.com/store/apps/details?id=in.canews.zeronet
     
     #### Docker
     There is an official image, built from source at: https://hub.docker.com/r/nofish/zeronet/
    
    From 367745b5ea4e46c536d852e177602c91fd8b6fa0 Mon Sep 17 00:00:00 2001
    From: ZeroNet 
    Date: Tue, 16 Jun 2020 18:25:58 +0200
    Subject: [PATCH 2401/2570] Move Android Play store link next to download
     options
    
    ---
     README.md | 8 ++++----
     1 file changed, 4 insertions(+), 4 deletions(-)
    
    diff --git a/README.md b/README.md
    index 81a8a09a..5b02a2e9 100644
    --- a/README.md
    +++ b/README.md
    @@ -1,9 +1,5 @@
     # ZeroNet [![Build Status](https://travis-ci.org/HelloZeroNet/ZeroNet.svg?branch=py3)](https://travis-ci.org/HelloZeroNet/ZeroNet) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://zeronet.io/docs/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://zeronet.io/docs/help_zeronet/donate/) ![tests](https://github.com/HelloZeroNet/ZeroNet/workflows/tests/badge.svg) [![Docker Pulls](https://img.shields.io/docker/pulls/nofish/zeronet)](https://hub.docker.com/r/nofish/zeronet)
     
    -[Download from Google Play](https://play.google.com/store/apps/details?id=in.canews.zeronet)
    -
     Decentralized websites using Bitcoin crypto and the BitTorrent network - https://zeronet.io
     
     
    @@ -90,6 +86,10 @@ Decentralized websites using Bitcoin crypto and the BitTorrent network - https:/
      - minimum Android version supported 16 (JellyBean).
      - Google Play Store Link https://play.google.com/store/apps/details?id=in.canews.zeronet
     
    +[Download from Google Play](https://play.google.com/store/apps/details?id=in.canews.zeronet)
    +
     #### Docker
     There is an official image, built from source at: https://hub.docker.com/r/nofish/zeronet/
     
    
    From 179e5cb65107dba78d0dc837886fef1c31d4c995 Mon Sep 17 00:00:00 2001
    From: shortcutme 
    Date: Thu, 18 Jun 2020 17:21:43 +0200
    Subject: [PATCH 2402/2570] Fix portchecker.co
    
    ---
     src/Peer/PeerPortchecker.py | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/src/Peer/PeerPortchecker.py b/src/Peer/PeerPortchecker.py
    index bf73747b..ac4d650f 100644
    --- a/src/Peer/PeerPortchecker.py
    +++ b/src/Peer/PeerPortchecker.py
    @@ -86,7 +86,7 @@ class PeerPortchecker(object):
                 raise Exception("Invalid response: %s" % message)
     
         def checkPortchecker(self, port):
    -        data = urllib.request.urlopen("https://portchecker.co/check", b"port=%s" % str(port).encode("ascii"), timeout=20.0).read().decode("utf8")
    +        data = urllib.request.urlopen("https://portchecker.co", b"port=%s" % str(port).encode("ascii"), timeout=20.0).read().decode("utf8")
             message = re.match(r'.*
    (.*?)
    ', data, re.DOTALL).group(1) message = re.sub(r"<.*?>", "", message.replace("
    ", " ").replace(" ", " ").strip()) # Strip http tags From 97ad084c21df2fc838653492c7284077d845ad90 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 18 Jun 2020 17:22:08 +0200 Subject: [PATCH 2403/2570] Ignore ipv6 tests if not supported by os --- src/Test/conftest.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Test/conftest.py b/src/Test/conftest.py index 8f9dc3a5..c8739086 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -9,6 +9,7 @@ import gc import datetime import atexit import threading +import socket import pytest import mock @@ -320,6 +321,16 @@ def file_server4(request): @pytest.fixture def file_server6(request): + try: + sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + sock.connect(("::1", 80, 1, 1)) + has_ipv6 = True + except OSError: + has_ipv6 = False + if not has_ipv6: + pytest.skip("Ipv6 not supported") + + time.sleep(0.1) file_server6 = FileServer("::1", 1544) file_server6.ip_external = 'fca5:95d6:bfde:d902:8951:276e:1111:a22c' # Fake external ip From 79d26060b3e8184a1b16a8fd7baae6ef8710b441 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 18 Jun 2020 17:22:33 +0200 Subject: [PATCH 2404/2570] Add site address hash to site info websocket response --- src/Ui/UiWebsocket.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index cbc3b8fe..5040097a 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -268,6 +268,7 @@ class UiWebsocket(object): "cert_user_id": self.user.getCertUserId(site.address), "address": site.address, "address_short": site.address_short, + "address_hash": site.address_hash.hex(), "settings": settings, "content_updated": site.content_updated, "bad_files": len(site.bad_files), From ea6016d004eeb1c8f2927d171fae615d1afc7d8e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 18 Jun 2020 17:22:45 +0200 Subject: [PATCH 2405/2570] Fix latest gevent compatibility --- src/util/ThreadPool.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/util/ThreadPool.py b/src/util/ThreadPool.py index 8c759039..5b31ce37 100644 --- a/src/util/ThreadPool.py +++ b/src/util/ThreadPool.py @@ -1,5 +1,6 @@ import threading import time +import queue import gevent import gevent.monkey @@ -121,7 +122,7 @@ class Event: # Execute function calls in main loop from other threads class MainLoopCaller(): def __init__(self): - self.queue_call = gevent._threading.Queue() + self.queue_call = queue.Queue() self.pool = gevent.threadpool.ThreadPool(1) self.num_direct = 0 From 4eb50377c387743df2bf0c93fbd35d63a9661aea Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 18 Jun 2020 17:23:15 +0200 Subject: [PATCH 2406/2570] Warning about deleting private key for owned sites --- plugins/Sidebar/media/Sidebar.coffee | 33 ++++++++++-------- plugins/Sidebar/media/all.js | 50 +++++++++++++++++----------- 2 files changed, 50 insertions(+), 33 deletions(-) diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee index 47c6e7f8..2f398522 100644 --- a/plugins/Sidebar/media/Sidebar.coffee +++ b/plugins/Sidebar/media/Sidebar.coffee @@ -346,6 +346,25 @@ class Sidebar extends Class 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() @@ -417,19 +436,7 @@ class Sidebar extends Class # Delete site @tag.find("#button-delete").off("click touchend").on "click touchend", => - @wrapper.displayConfirm "Are you sure?", ["Delete this site", "Blacklist"], (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") - - + @handleSiteDeleteClick() return false # Owned checkbox diff --git a/plugins/Sidebar/media/all.js b/plugins/Sidebar/media/all.js index f83a5c5c..ff7e3cc3 100644 --- a/plugins/Sidebar/media/all.js +++ b/plugins/Sidebar/media/all.js @@ -55,7 +55,6 @@ }).call(this); - /* ---- Console.coffee ---- */ @@ -354,7 +353,6 @@ }).call(this); - /* ---- Menu.coffee ---- */ @@ -437,7 +435,6 @@ }).call(this); - /* ---- RateLimit.coffee ---- */ @@ -466,7 +463,6 @@ }).call(this); - /* ---- Scrollable.js ---- */ @@ -983,6 +979,35 @@ window.initScrollable = function () { })(this)); }; + Sidebar.prototype.handleSiteDeleteClick = function() { + var options, question; + if (this.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"]; + } + return this.wrapper.displayConfirm(question, options, (function(_this) { + return function(confirmed) { + if (confirmed === 1) { + _this.tag.find("#button-delete").addClass("loading"); + return _this.wrapper.ws.cmd("siteDelete", _this.wrapper.site_info.address, function() { + return document.location = $(".fixbutton-bg").attr("href"); + }); + } else if (confirmed === 2) { + return _this.wrapper.displayPrompt("Blacklist this site", "text", "Delete and Blacklist", "Reason", function(reason) { + _this.tag.find("#button-delete").addClass("loading"); + _this.wrapper.ws.cmd("siteblockAdd", [_this.wrapper.site_info.address, reason]); + return _this.wrapper.ws.cmd("siteDelete", _this.wrapper.site_info.address, function() { + return document.location = $(".fixbutton-bg").attr("href"); + }); + }); + } + }; + })(this)); + }; + Sidebar.prototype.onOpened = function() { var menu; this.log("Opened"); @@ -1073,22 +1098,7 @@ window.initScrollable = function () { })(this)); this.tag.find("#button-delete").off("click touchend").on("click touchend", (function(_this) { return function() { - _this.wrapper.displayConfirm("Are you sure?", ["Delete this site", "Blacklist"], function(confirmed) { - if (confirmed === 1) { - _this.tag.find("#button-delete").addClass("loading"); - return _this.wrapper.ws.cmd("siteDelete", _this.wrapper.site_info.address, function() { - return document.location = $(".fixbutton-bg").attr("href"); - }); - } else if (confirmed === 2) { - return _this.wrapper.displayPrompt("Blacklist this site", "text", "Delete and Blacklist", "Reason", function(reason) { - _this.tag.find("#button-delete").addClass("loading"); - _this.wrapper.ws.cmd("siteblockAdd", [_this.wrapper.site_info.address, reason]); - return _this.wrapper.ws.cmd("siteDelete", _this.wrapper.site_info.address, function() { - return document.location = $(".fixbutton-bg").attr("href"); - }); - }); - } - }); + _this.handleSiteDeleteClick(); return false; }; })(this)); From 14cbaf47c8988b12ed8aab3a2aea10a40149f7b1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 18 Jun 2020 17:28:56 +0200 Subject: [PATCH 2407/2570] Rev4493 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 9055270a..f580598b 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4486 + self.rev = 4493 self.argv = argv self.action = None self.test_parser = None From 6776dabdb30307b87ad4b0667f16b0bdcf07c19a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 30 Jun 2020 17:02:39 +0200 Subject: [PATCH 2408/2570] Fix piecemap downlad error when invalid piecemap got downloaded --- plugins/Bigfile/BigfilePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index 6ab45372..78a27b05 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -391,8 +391,8 @@ class ContentManagerPlugin(object): def verifyPiece(self, inner_path, pos, piece): try: piecemap = self.getPiecemap(inner_path) - except OSError as err: - raise VerifyError("Unable to download piecemap: %s" % err) + 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]: From 635c3b27cd195988dac1ca53a0de3821fa07d36b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 30 Jun 2020 17:03:06 +0200 Subject: [PATCH 2409/2570] Fix loading invalid site block list --- plugins/ContentFilter/ContentFilterStorage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ContentFilter/ContentFilterStorage.py b/plugins/ContentFilter/ContentFilterStorage.py index 70409d6b..289ec2a9 100644 --- a/plugins/ContentFilter/ContentFilterStorage.py +++ b/plugins/ContentFilter/ContentFilterStorage.py @@ -137,7 +137,7 @@ class ContentFilterStorage(object): if not include_site: continue content = include_site.storage.loadJson(include["inner_path"]) - details = content.get("siteblocks").get(address) + details = content.get("siteblocks", {}).get(address) if details: details["include"] = include break From ddbd5c7b19d4c13d6369b20a41b979cde36117fd Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 30 Jun 2020 17:04:09 +0200 Subject: [PATCH 2410/2570] Fix reset file server port with config web interface --- .../UiConfig/media/js/ConfigStorage.coffee | 9 ++++++- plugins/UiConfig/media/js/UiConfig.coffee | 8 +++--- plugins/UiConfig/media/js/all.js | 25 +++++++++++++------ src/File/FileServer.py | 1 + src/Ui/UiWebsocket.py | 2 ++ 5 files changed, 33 insertions(+), 12 deletions(-) diff --git a/plugins/UiConfig/media/js/ConfigStorage.coffee b/plugins/UiConfig/media/js/ConfigStorage.coffee index e5b37773..92327001 100644 --- a/plugins/UiConfig/media/js/ConfigStorage.coffee +++ b/plugins/UiConfig/media/js/ConfigStorage.coffee @@ -32,6 +32,13 @@ class ConfigStorage extends Class 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 @@ -68,7 +75,7 @@ class ConfigStorage extends Class title: "File server port" type: "text" valid_pattern: /[0-9]*/ - description: "Other peers will use this port to reach your served sites. (default: 15441)" + description: "Other peers will use this port to reach your served sites. (default: randomize)" section.items.push key: "ip_external" diff --git a/plugins/UiConfig/media/js/UiConfig.coffee b/plugins/UiConfig/media/js/UiConfig.coffee index 4ee3a1c6..bc1ac697 100644 --- a/plugins/UiConfig/media/js/UiConfig.coffee +++ b/plugins/UiConfig/media/js/UiConfig.coffee @@ -57,9 +57,8 @@ class UiConfig extends ZeroFrame for item, i in changed_values last = i == changed_values.length - 1 value = @config_storage.deformatValue(item.value, typeof(@config[item.key].default)) - value_same_as_default = JSON.stringify(@config[item.key].default) == JSON.stringify(value) - if value_same_as_default - value = null + 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) @@ -69,6 +68,9 @@ class UiConfig extends ZeroFrame cb(false) break + if value_same_as_default + value = null + @saveValue(item.key, value, if last then cb else null) saveValue: (key, value, cb) => diff --git a/plugins/UiConfig/media/js/all.js b/plugins/UiConfig/media/js/all.js index 1b8083c9..cdd2e799 100644 --- a/plugins/UiConfig/media/js/all.js +++ b/plugins/UiConfig/media/js/all.js @@ -1336,6 +1336,14 @@ } if (type === "boolean" && !value) { return false; + } else if (type === "number") { + if (typeof value === "number") { + return value.toString(); + } else if (!value) { + return "0"; + } else { + return value; + } } else { return value; } @@ -1379,7 +1387,7 @@ title: "File server port", type: "text", valid_pattern: /[0-9]*/, - description: "Other peers will use this port to reach your served sites. (default: 15441)" + description: "Other peers will use this port to reach your served sites. (default: randomize)" }); section.items.push({ key: "ip_external", @@ -1616,7 +1624,6 @@ }).call(this); - /* ---- ConfigView.coffee ---- */ @@ -1934,17 +1941,16 @@ }; UiConfig.prototype.saveValues = function(cb) { - var base, changed_values, i, item, j, last, len, match, message, results, value, value_same_as_default; + var base, changed_values, default_value, i, item, j, last, len, match, message, results, value, value_same_as_default; changed_values = this.getValuesChanged(); results = []; for (i = j = 0, len = changed_values.length; j < len; i = ++j) { item = changed_values[i]; last = i === changed_values.length - 1; value = this.config_storage.deformatValue(item.value, typeof this.config[item.key]["default"]); - value_same_as_default = JSON.stringify(this.config[item.key]["default"]) === JSON.stringify(value); - if (value_same_as_default) { - value = null; - } + default_value = this.config_storage.deformatValue(this.config[item.key]["default"], typeof this.config[item.key]["default"]); + this.log("default check:", JSON.stringify(default_value), "==", JSON.stringify(value)); + value_same_as_default = JSON.stringify(default_value) === JSON.stringify(value); if (this.config[item.key].item.valid_pattern && !(typeof (base = this.config[item.key].item).isHidden === "function" ? base.isHidden() : void 0)) { match = value.match(this.config[item.key].item.valid_pattern); if (!match || match[0] !== value) { @@ -1954,6 +1960,9 @@ break; } } + if (value_same_as_default) { + value = null; + } results.push(this.saveValue(item.key, value, last ? cb : null)); } return results; @@ -2054,4 +2063,4 @@ window.Page.createProjector(); -}).call(this); \ No newline at end of file +}).call(this); diff --git a/src/File/FileServer.py b/src/File/FileServer.py index 68be3a4b..8cf565a9 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -49,6 +49,7 @@ class FileServer(ConnectionServer): raise Exception("Can't find bindable port") if not config.tor == "always": config.saveValue("fileserver_port", port) # Save random port value for next restart + config.arguments.fileserver_port = port ConnectionServer.__init__(self, ip, port, self.handleRequest) self.log.debug("Supported IP types: %s" % self.supported_ip_types) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 5040097a..299ead14 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -1194,6 +1194,8 @@ class UiWebsocket(object): @flag.no_multiuser def actionConfigSet(self, to, key, value): import main + + self.log.debug("Changing config %s value to %r" % (key, value)) if key not in config.keys_api_change_allowed: self.response(to, {"error": "Forbidden: You cannot set this config key"}) return From 6bd49e8aff9c438e59a6fffa1bd1bc66541eb4c8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 30 Jun 2020 17:04:47 +0200 Subject: [PATCH 2411/2570] Fix killing greenlets gevent exception --- src/Debug/Debug.py | 14 ++++++++++++-- src/Worker/Worker.py | 4 ++-- src/util/GreenletManager.py | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/Debug/Debug.py b/src/Debug/Debug.py index 10a4d627..12f084bc 100644 --- a/src/Debug/Debug.py +++ b/src/Debug/Debug.py @@ -6,13 +6,19 @@ from Config import config # Non fatal exception class Notify(Exception): - def __init__(self, message): - self.message = message + def __init__(self, message=None): + if message: + self.message = message def __str__(self): return self.message +# Gevent greenlet.kill accept Exception type +def createNotifyType(message): + return type("Notify", (Notify, ), {"message": message}) + + def formatExceptionMessage(err): err_type = err.__class__.__name__ if err.args: @@ -101,6 +107,8 @@ import time num_block = 0 + + def testBlock(): global num_block logging.debug("Gevent block checker started") @@ -111,6 +119,8 @@ def testBlock(): logging.debug("Gevent block detected: %.3fs" % (time.time() - last_time - 1)) num_block += 1 last_time = time.time() + + gevent.spawn(testBlock) diff --git a/src/Worker/Worker.py b/src/Worker/Worker.py index 4cf04d97..b7111ba1 100644 --- a/src/Worker/Worker.py +++ b/src/Worker/Worker.py @@ -226,7 +226,7 @@ class Worker(object): def skip(self, reason="Unknown"): self.manager.log.debug("%s: Force skipping (reason: %s)" % (self.key, reason)) if self.thread: - self.thread.kill(exception=Debug.Notify("Worker skipping (reason: %s)" % reason)) + self.thread.kill(exception=Debug.createNotifyType("Worker skipping (reason: %s)" % reason)) self.start() # Force stop the worker @@ -234,6 +234,6 @@ class Worker(object): self.manager.log.debug("%s: Force stopping (reason: %s)" % (self.key, reason)) self.running = False if self.thread: - self.thread.kill(exception=Debug.Notify("Worker stopped (reason: %s)" % reason)) + self.thread.kill(exception=Debug.createNotifyType("Worker stopped (reason: %s)" % reason)) del self.thread self.manager.removeWorker(self) diff --git a/src/util/GreenletManager.py b/src/util/GreenletManager.py index 7245d05c..e024233d 100644 --- a/src/util/GreenletManager.py +++ b/src/util/GreenletManager.py @@ -20,5 +20,5 @@ class GreenletManager: def stopGreenlets(self, reason="Stopping all greenlets"): num = len(self.greenlets) - gevent.killall(list(self.greenlets), Debug.Notify(reason), block=False) + gevent.killall(list(self.greenlets), Debug.createNotifyType(reason), block=False) return num From 47ff6c68018d6bcff0379d7a1cb8d0d5ef1c6799 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 30 Jun 2020 17:04:55 +0200 Subject: [PATCH 2412/2570] Rev4496 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index f580598b..87c11553 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4493 + self.rev = 4496 self.argv = argv self.action = None self.test_parser = None From 29c3523353773efa4a3a52f7fc736e9ee6c0a88b Mon Sep 17 00:00:00 2001 From: SuperMan Date: Sat, 18 Jul 2020 17:45:32 +0530 Subject: [PATCH 2413/2570] arm64 arch docker image request #2568 --- Dockerfile.arm64v8 | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 Dockerfile.arm64v8 diff --git a/Dockerfile.arm64v8 b/Dockerfile.arm64v8 new file mode 100644 index 00000000..d27b7620 --- /dev/null +++ b/Dockerfile.arm64v8 @@ -0,0 +1,34 @@ +FROM alpine:3.12 + +#Base settings +ENV HOME /root + +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 \ + && 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 + From c17b8d53d3b3f10eb12a061cb25fa46f0a5f2a0d Mon Sep 17 00:00:00 2001 From: ZeroNet Date: Thu, 3 Sep 2020 16:56:41 +0200 Subject: [PATCH 2414/2570] Update changelog with 0.6.5, 0.7.0, 0.7.1 --- CHANGELOG.md | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 225e424a..b49b9ef6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,122 @@ +### ZeroNet 0.7.2 (2020-09-?) Rev4206? + + + +### ZeroNet 0.7.1 (2019-07-01) Rev4206 +### Added + - Built-in logging console in the web UI to see what's happening in the background. (pull down top-right 0 button to see it) + - Display database rebuild errors [Thanks to Lola] + - New plugin system that allows to install and manage builtin/third party extensions to the ZeroNet client using the web interface. + - Support multiple trackers_file + - Add OpenSSL 1.1 support to CryptMessage plugin based on Bitmessage modifications [Thanks to radfish] + - Display visual error message on startup errors + - Fix max opened files changing on Windows platform + - Display TLS1.3 compatibility on /Stats page + - Add fake SNI and ALPN to peer connections to make it more like standard https connections + - Hide and ignore tracker_proxy setting in Tor: Always mode as it's going to use Tor anyway. + - Deny websocket connections from unknown origins + - Restrict open_browser values to avoid RCE on sandbox escape + - Offer access web interface by IP address in case of unknown host + - Link to site's sidebar with "#ZeroNet:OpenSidebar" hash + +### Changed + - Allow .. in file names [Thanks to imachug] + - Change unstable trackers + - More clean errors on sites.json/users.json load error + - Various tweaks for tracker rating on unstable connections + - Use OpenSSL 1.1 dlls from default Python Windows distribution if possible + - Re-factor domain resolving for easier domain plugins + - Disable UDP connections if --proxy is used + - New, decorator-based Websocket API permission system to avoid future typo mistakes + +### Fixed + - Fix parsing config lines that have no value + - Fix start.py [Thanks to imachug] + - Allow multiple values of the same key in the config file [Thanks ssdifnskdjfnsdjk for reporting] + - Fix parsing config file lines that has % in the value [Thanks slrslr for reporting] + - Fix bootstrapper plugin hash reloads [Thanks geekless for reporting] + - Fix CryptMessage plugin OpenSSL dll loading on Windows (ZeroMail errors) [Thanks cxgreat2014 for reporting] + - Fix startup error when using OpenSSL 1.1 [Thanks to imachug] + - Fix a bug that did not loaded merged site data for 5 sec after the merged site got added + - Fix typo that allowed to add new plugins in public proxy mode. [Thanks styromaniac for reporting] + - Fix loading non-big files with "|all" postfix [Thanks to krzotr] + - Fix OpenSSL cert generation error crash by change Windows console encoding to utf8 + +#### Wrapper html injection vulnerability [Reported by ivanq] + +In ZeroNet before rev4188 the wrapper template variables was rendered incorrectly. + +Result: The opened site was able to gain WebSocket connection with unrestricted ADMIN/NOSANDBOX access, change configuration values and possible RCE on client's machine. + +Fix: Fixed the template rendering code, disallowed WebSocket connections from unknown locations, restricted open_browser configuration values to avoid possible RCE in case of sandbox escape. + +Note: The fix is also back ported to ZeroNet Py 2.x version (Rev3870) + + +### ZeroNet 0.7.0 (2019-06-12) Rev4106 (First release targeting Python 3.4+) +### Added + - 5-10x faster signature verification by using libsecp256k1 (Thanks to ZeroMux) + - Generated SSL certificate randomization to avoid protocol filters (Thanks to ValdikSS) + - Offline mode + - P2P source code update using ZeroNet protocol + - ecdsaSign/Verify commands to CryptMessage plugin (Thanks to imachug) + - Efficient file rename: change file names instead of re-downloading the file. + - Make redirect optional on site cloning (Thanks to Lola) + - EccPrivToPub / EccPubToPriv functions (Thanks to imachug) + - Detect and change dark/light theme based on OS setting (Thanks to filips123) + +### Changed + - Re-factored code to Python3 runtime (compatible with Python 3.4-3.8) + - More safe database sync mode + - Removed bundled third-party libraries where it's possible + - Use lang=en instead of lang={lang} in urls to avoid url encode problems + - Remove environment details from error page + - Don't push content.json updates larger than 10kb to significantly reduce bw usage for site with many files + +### Fixed + - Fix sending files with \0 characters + - Security fix: Escape error detail to avoid XSS (reported by krzotr) + - Fix signature verification using libsecp256k1 for compressed addresses (mostly certificates generated in the browser) + - Fix newsfeed if you have more than 1000 followed topic/post on one site. + - Fix site download as zip file + - Fix displaying sites with utf8 title + - Error message if dbRebuild fails (Thanks to Lola) + - Fix browser reopen if executing start.py again. (Thanks to imachug) + + +### ZeroNet 0.6.5 (2019-02-16) Rev3851 (Last release targeting Python 2.7.x) +### Added + - IPv6 support in peer exchange, bigfiles, optional file finding, tracker sharing, socket listening and connecting (based on tangdou1 modifications) + - New tracker database format with IPv6 support + - Display notification if there is an unpublished modification for your site + - Listen and shut down normally for SIGTERM (Thanks to blurHY) + - Support tilde `~` in filenames (by d14na) + - Support map for Namecoin subdomain names (Thanks to lola) + - Add log level to config page + - Support `{data}` for data dir variable in trackers_file value + - Quick check content.db on startup and rebuild if necessary + - Don't show meek proxy option if the tor client does not supports it + +### Changed + - Refactored port open checking with IPv6 support + - Consider non-local IPs as external even is the open port check fails (for CJDNS and Yggdrasil support) + - Add IPv6 tracker and change unstable tracker + - Don't correct sent local time with the calculated time correction + - Disable CSP for Edge + - Only support CREATE commands in dbschema indexes node and SELECT from storage.query + +### Fixed + - Check the length of master seed when executing cryptGetPrivatekey CLI command + - Only reload source code on file modification / creation + - Detection and issue warning for latest no-script plugin + - Fix atomic write of a non-existent file + - Fix sql queries with lots of variables and sites with lots of content.json + - Fix multi-line parsing of zeronet.conf + - Fix site deletion from users.json + - Fix site cloning before site downloaded (Reported by unsystemizer) + - Fix queryJson for non-list nodes (Reported by MingchenZhang) + + ## ZeroNet 0.6.4 (2018-10-20) Rev3660 ### Added - New plugin: UiConfig. A web interface that allows changing ZeroNet settings. From 4ad5c065f17d0a31c7e58ba711586ade8b49b6d9 Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Fri, 4 Sep 2020 17:16:47 +0200 Subject: [PATCH 2415/2570] Don't display gui error when running from cli on Windows --- zeronet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zeronet.py b/zeronet.py index c3378732..dacd2096 100755 --- a/zeronet.py +++ b/zeronet.py @@ -28,7 +28,7 @@ def main(): traceback.print_exc(file=open(error_log_path, "w")) print("---") print("Please report it: https://github.com/HelloZeroNet/ZeroNet/issues/new?assignees=&labels=&template=bug-report.md") - if sys.platform.startswith("win"): + if sys.platform.startswith("win") and "python.exe" not in sys.executable: displayErrorMessage(err, error_log_path) if main and (main.update_after_shutdown or main.restart_after_shutdown): # Updater From 6ff14d1bbdd0c0e840764c943fff4d5fabbee082 Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Fri, 4 Sep 2020 17:17:15 +0200 Subject: [PATCH 2416/2570] Fix plugin config error when running update.py --- update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/update.py b/update.py index ab39a602..cf9898f9 100644 --- a/update.py +++ b/update.py @@ -7,7 +7,7 @@ import shutil def update(): from Config import config - config.parse() + config.parse(silent=True) if getattr(sys, 'source_update_dir', False): if not os.path.isdir(sys.source_update_dir): From 0907edb6b17d1316533332f45e1a408f7fdd3b74 Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Fri, 4 Sep 2020 17:35:48 +0200 Subject: [PATCH 2417/2570] Remove obsolate auth_key generation --- src/Site/Site.py | 4 ---- src/Ui/UiWebsocket.py | 2 -- 2 files changed, 6 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 32f10abe..63304944 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -69,10 +69,6 @@ class Site(object): self.announcer = SiteAnnouncer(self) # Announce and get peer list from other nodes - if not self.settings.get("auth_key"): # To auth user in site (Obsolete, will be removed) - self.settings["auth_key"] = CryptHash.random() - self.log.debug("New auth key: %s" % self.settings["auth_key"]) - 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"]) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 299ead14..111c67cf 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -260,10 +260,8 @@ class UiWebsocket(object): settings = site.settings.copy() del settings["wrapper_key"] # Dont expose wrapper key - del settings["auth_key"] # Dont send auth key twice ret = { - "auth_key": self.site.settings["auth_key"], # Obsolete, will be removed "auth_address": self.user.getAuthAddress(site.address, create=create_user), "cert_user_id": self.user.getCertUserId(site.address), "address": site.address, From 6c1abf4004799a4fff955352a25246bd85da7346 Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Fri, 4 Sep 2020 17:49:23 +0200 Subject: [PATCH 2418/2570] Don't switch to libev for newer versions of gevent --- src/main.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main.py b/src/main.py index 4083ce88..7a0188e7 100644 --- a/src/main.py +++ b/src/main.py @@ -12,12 +12,12 @@ def startupError(msg): # Third party modules import gevent -try: - # Workaround for random crash when libuv used with threads - if "libev" not in str(gevent.config.loop): - gevent.config.loop = "libev-cext" -except Exception as err: - startupError("Unable to switch gevent loop to libev: %s" % err) +if gevent.version_info.major <= 1: # Workaround for random crash when libuv used with threads + try: + if "libev" not in str(gevent.config.loop): + gevent.config.loop = "libev-cext" + except Exception as err: + startupError("Unable to switch gevent loop to libev: %s" % err) import gevent.monkey gevent.monkey.patch_all(thread=False, subprocess=False) From 051e404a808b06368796938ff4318e4d8222366f Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Fri, 4 Sep 2020 17:52:26 +0200 Subject: [PATCH 2419/2570] Fix typo in Benchmark cli info success number --- plugins/Benchmark/BenchmarkPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Benchmark/BenchmarkPlugin.py b/plugins/Benchmark/BenchmarkPlugin.py index 8af140d8..39590622 100644 --- a/plugins/Benchmark/BenchmarkPlugin.py +++ b/plugins/Benchmark/BenchmarkPlugin.py @@ -193,7 +193,7 @@ class ActionsPlugin: 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"]) + num_success = len([res_key for res_key, res_val in res.items() if res_val == "ok"]) yield "* Result:\n" yield " - Total: %s tests\n" % len(res) yield " - Success: %s tests\n" % num_success From 8d964d1b8e40c2dd8dbce4fbfa9caf7d81961e4d Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Fri, 4 Sep 2020 17:55:41 +0200 Subject: [PATCH 2420/2570] Fix infopanel overflow on mobile devices --- src/Ui/media/Infopanel.coffee | 4 ++-- src/Ui/media/Wrapper.css | 5 +++-- src/Ui/template/wrapper.html | 14 ++++++++------ 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Ui/media/Infopanel.coffee b/src/Ui/media/Infopanel.coffee index eb17eae7..d728ccbe 100644 --- a/src/Ui/media/Infopanel.coffee +++ b/src/Ui/media/Infopanel.coffee @@ -3,7 +3,7 @@ class Infopanel @visible = false show: (closed=false) => - @elem.addClass("visible") + @elem.parent().addClass("visible") if closed @close() else @@ -23,7 +23,7 @@ class Infopanel @close() hide: => - @elem.removeClass("visible") + @elem.parent().removeClass("visible") close: => @elem.addClass("closed") diff --git a/src/Ui/media/Wrapper.css b/src/Ui/media/Wrapper.css index d633ff45..4ce0f61f 100644 --- a/src/Ui/media/Wrapper.css +++ b/src/Ui/media/Wrapper.css @@ -110,13 +110,14 @@ a { color: black } /* Infopanel */ +.infopanel-container { width: 100%; height: 100%; overflow: hidden; position: absolute; display: none; } +.infopanel-container.visible { display: block; } .infopanel { - position: absolute; z-index: 999; padding: 15px 15px; bottom: 25px; right: 50px; border: 1px solid #eff3fe; display: none; + position: absolute; z-index: 999; padding: 15px 15px; bottom: 25px; right: 50px; border: 1px solid #eff3fe; font-family: 'Lucida Grande', 'Segoe UI', Helvetica, Arial, sans-serif; box-shadow: 0px 10px 55px rgba(58, 39, 176, 0.17); background-color: white; border-left: 4px solid #9a61f8; border-top-left-radius: 4px; border-bottom-left-radius: 4px; transition: all 0.8s cubic-bezier(0.215, 0.61, 0.355, 1); } -.infopanel.visible { display: block; } .infopanel.closed { box-shadow: none; transform: translateX(100%); right: 0px; cursor: pointer; } .infopanel .message { font-size: 13px; line-height: 15px; display: inline-block; vertical-align: -9px; } .infopanel .message .line { max-width: 200px; display: inline-block; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } diff --git a/src/Ui/template/wrapper.html b/src/Ui/template/wrapper.html index e0270542..6cb27ba7 100644 --- a/src/Ui/template/wrapper.html +++ b/src/Ui/template/wrapper.html @@ -49,13 +49,15 @@ else if (window.opener && window.opener.location.toString()) {
    -
    - 8 -
    - 8 modified files
    content.json, data.json +
    +
    + 8 +
    + 8 modified files
    content.json, data.json +
    + Sign & Publish + ×
    - Sign & Publish - ×
    From f7874e1ca328440ddcff758ae62e5fb4c0f7b299 Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Fri, 4 Sep 2020 17:56:16 +0200 Subject: [PATCH 2421/2570] Fix loading bar hide bug --- src/Ui/media/Loading.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Ui/media/Loading.coffee b/src/Ui/media/Loading.coffee index 877087c6..8e35ce66 100644 --- a/src/Ui/media/Loading.coffee +++ b/src/Ui/media/Loading.coffee @@ -2,15 +2,18 @@ class Loading constructor: (@wrapper) -> if window.show_loadingscreen then @showScreen() @timer_hide = null + @timer_set = null setProgress: (percent) -> if @timer_hide clearInterval @timer_hide - RateLimit 500, -> + @timer_set = RateLimit 500, -> $(".progressbar").css("transform": "scaleX(#{parseInt(percent*100)/100})").css("opacity", "1").css("display", "block") hideProgress: -> @log "hideProgress" + if @timer_set + clearInterval @timer_set @timer_hide = setTimeout ( => $(".progressbar").css("transform": "scaleX(1)").css("opacity", "0").hideLater(1000) ), 300 From 501bd51bd1dffd793145fcf0c493f07035ebe7c7 Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Fri, 4 Sep 2020 17:57:34 +0200 Subject: [PATCH 2422/2570] Only set title from content.json if wrapperSetTitle has not been called --- src/Ui/media/Wrapper.coffee | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index dceb4f57..43cd033f 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -34,6 +34,7 @@ class Wrapper @opener_tested = false @announcer_line = null @web_notifications = {} + @is_title_changed = false @allowed_event_constructors = [window.MouseEvent, window.KeyboardEvent, window.PointerEvent] # Allowed event constructors @@ -171,7 +172,9 @@ class Wrapper else if cmd == "wrapperSetViewport" # Set the viewport @actionSetViewport(message) else if cmd == "wrapperSetTitle" + @log "wrapperSetTitle", message.params $("head title").text(message.params) + @is_title_changed = true else if cmd == "wrapperReload" # Reload current page @actionReload(message) else if cmd == "wrapperGetLocalStorage" @@ -499,8 +502,8 @@ class Wrapper #if not @site_error then @loading.hideScreen() # Hide loading screen if @ws.ws.readyState == 1 and not @site_info # Ws opened @reloadSiteInfo() - else if @site_info and @site_info.content?.title? - window.document.title = @site_info.content.title+" - ZeroNet" + else if @site_info and @site_info.content?.title? and not @is_title_changed + window.document.title = @site_info.content.title + " - ZeroNet" @log "Setting title to", window.document.title onWrapperLoad: => @@ -534,7 +537,7 @@ class Wrapper if res == "ok" @notifications.add("size_limit", "done", "Site storage limit modified!", 5000) - if site_info.content?.title? + if site_info.content?.title? and not @is_title_changed window.document.title = site_info.content.title + " - ZeroNet" @log "Setting title to", window.document.title @@ -551,7 +554,7 @@ class Wrapper if site_info.event[1] == window.file_inner_path # File downloaded we currently on @loading.hideScreen() if not @site_info then @reloadSiteInfo() - if site_info.content + if site_info.content and not @is_title_changed window.document.title = site_info.content.title + " - ZeroNet" @log "Required file #{window.file_inner_path} done, setting title to", window.document.title if not window.show_loadingscreen From 46fba195dae72eea0100f2a7c1a7c8e50d9a8560 Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Fri, 4 Sep 2020 17:57:56 +0200 Subject: [PATCH 2423/2570] Merge js, css --- src/Ui/media/all.css | 5 +++-- src/Ui/media/all.js | 23 +++++++++++++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Ui/media/all.css b/src/Ui/media/all.css index 6e4173c2..8f4b459c 100644 --- a/src/Ui/media/all.css +++ b/src/Ui/media/all.css @@ -123,13 +123,14 @@ a { color: black } /* Infopanel */ +.infopanel-container { width: 100%; height: 100%; overflow: hidden; position: absolute; display: none; } +.infopanel-container.visible { display: block; } .infopanel { - position: absolute; z-index: 999; padding: 15px 15px; bottom: 25px; right: 50px; border: 1px solid #eff3fe; display: none; + position: absolute; z-index: 999; padding: 15px 15px; bottom: 25px; right: 50px; border: 1px solid #eff3fe; font-family: 'Lucida Grande', 'Segoe UI', Helvetica, Arial, sans-serif; -webkit-box-shadow: 0px 10px 55px rgba(58, 39, 176, 0.17); -moz-box-shadow: 0px 10px 55px rgba(58, 39, 176, 0.17); -o-box-shadow: 0px 10px 55px rgba(58, 39, 176, 0.17); -ms-box-shadow: 0px 10px 55px rgba(58, 39, 176, 0.17); box-shadow: 0px 10px 55px rgba(58, 39, 176, 0.17) ; background-color: white; border-left: 4px solid #9a61f8; border-top-left-radius: 4px; border-bottom-left-radius: 4px; -webkit-transition: all 0.8s cubic-bezier(0.215, 0.61, 0.355, 1); -moz-transition: all 0.8s cubic-bezier(0.215, 0.61, 0.355, 1); -o-transition: all 0.8s cubic-bezier(0.215, 0.61, 0.355, 1); -ms-transition: all 0.8s cubic-bezier(0.215, 0.61, 0.355, 1); transition: all 0.8s cubic-bezier(0.215, 0.61, 0.355, 1) ; } -.infopanel.visible { display: block; } .infopanel.closed { -webkit-box-shadow: none; -moz-box-shadow: none; -o-box-shadow: none; -ms-box-shadow: none; box-shadow: none ; -webkit-transform: translateX(100%); -moz-transform: translateX(100%); -o-transform: translateX(100%); -ms-transform: translateX(100%); transform: translateX(100%) ; right: 0px; cursor: pointer; } .infopanel .message { font-size: 13px; line-height: 15px; display: inline-block; vertical-align: -9px; } .infopanel .message .line { max-width: 200px; display: inline-block; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index 47d94f9c..de9ecd79 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -556,7 +556,7 @@ $.extend( $.easing, if (closed == null) { closed = false; } - this.elem.addClass("visible"); + this.elem.parent().addClass("visible"); if (closed) { return this.close(); } else { @@ -585,7 +585,7 @@ $.extend( $.easing, }; Infopanel.prototype.hide = function() { - return this.elem.removeClass("visible"); + return this.elem.parent().removeClass("visible"); }; Infopanel.prototype.close = function() { @@ -635,13 +635,14 @@ $.extend( $.easing, this.showScreen(); } this.timer_hide = null; + this.timer_set = null; } Loading.prototype.setProgress = function(percent) { if (this.timer_hide) { clearInterval(this.timer_hide); } - return RateLimit(500, function() { + return this.timer_set = RateLimit(500, function() { return $(".progressbar").css({ "transform": "scaleX(" + (parseInt(percent * 100) / 100) + ")" }).css("opacity", "1").css("display", "block"); @@ -650,6 +651,9 @@ $.extend( $.easing, Loading.prototype.hideProgress = function() { this.log("hideProgress"); + if (this.timer_set) { + clearInterval(this.timer_set); + } return this.timer_hide = setTimeout(((function(_this) { return function() { return $(".progressbar").css({ @@ -775,6 +779,7 @@ $.extend( $.easing, }).call(this); + /* ---- Notifications.coffee ---- */ @@ -971,6 +976,7 @@ $.extend( $.easing, this.opener_tested = false; this.announcer_line = null; this.web_notifications = {}; + this.is_title_changed = false; this.allowed_event_constructors = [window.MouseEvent, window.KeyboardEvent, window.PointerEvent]; window.onload = this.onPageLoad; window.onhashchange = (function(_this) { @@ -1147,7 +1153,9 @@ $.extend( $.easing, } else if (cmd === "wrapperSetViewport") { return this.actionSetViewport(message); } else if (cmd === "wrapperSetTitle") { - return $("head title").text(message.params); + this.log("wrapperSetTitle", message.params); + $("head title").text(message.params); + return this.is_title_changed = true; } else if (cmd === "wrapperReload") { return this.actionReload(message); } else if (cmd === "wrapperGetLocalStorage") { @@ -1682,7 +1690,7 @@ $.extend( $.easing, } if (this.ws.ws.readyState === 1 && !this.site_info) { return this.reloadSiteInfo(); - } else if (this.site_info && (((ref = this.site_info.content) != null ? ref.title : void 0) != null)) { + } else if (this.site_info && (((ref = this.site_info.content) != null ? ref.title : void 0) != null) && !this.is_title_changed) { window.document.title = this.site_info.content.title + " - ZeroNet"; return this.log("Setting title to", window.document.title); } @@ -1724,7 +1732,7 @@ $.extend( $.easing, }); }); } - if (((ref = site_info.content) != null ? ref.title : void 0) != null) { + if ((((ref = site_info.content) != null ? ref.title : void 0) != null) && !_this.is_title_changed) { window.document.title = site_info.content.title + " - ZeroNet"; return _this.log("Setting title to", window.document.title); } @@ -1744,7 +1752,7 @@ $.extend( $.easing, if (!this.site_info) { this.reloadSiteInfo(); } - if (site_info.content) { + if (site_info.content && !this.is_title_changed) { window.document.title = site_info.content.title + " - ZeroNet"; this.log("Required file " + window.file_inner_path + " done, setting title to", window.document.title); } @@ -1996,7 +2004,6 @@ $.extend( $.easing, }).call(this); - /* ---- WrapperZeroFrame.coffee ---- */ From cafeebf1202ec10cda626d9cab639da4c1bbcec9 Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Fri, 4 Sep 2020 18:07:03 +0200 Subject: [PATCH 2424/2570] Fix wrapper_nonce adding to url --- src/Ui/UiRequest.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 506f9661..a94eab3f 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -480,12 +480,15 @@ class UiRequest(object): wrapper_nonce = self.getWrapperNonce() inner_query_string = self.processQueryString(site, self.env.get("QUERY_STRING", "")) - if inner_query_string: - inner_query_string = "?%s&wrapper_nonce=%s" % (inner_query_string, wrapper_nonce) - elif "?" in inner_path: - inner_query_string = "&wrapper_nonce=%s" % wrapper_nonce + if "?" in inner_path: + sep = "&" else: - inner_query_string = "?wrapper_nonce=%s" % wrapper_nonce + sep = "?" + + if inner_query_string: + inner_query_string = "%s%s&wrapper_nonce=%s" % (sep, inner_query_string, wrapper_nonce) + else: + inner_query_string = "%swrapper_nonce=%s" % (sep, wrapper_nonce) if self.isProxyRequest(): # Its a remote proxy request homepage = "http://zero/" + config.homepage From 9d198ff7f2aeb56d99df6477e198df69fa7e8dc7 Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Fri, 4 Sep 2020 18:07:29 +0200 Subject: [PATCH 2425/2570] Display full path in 404 error instead of inner_path --- src/Ui/UiRequest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index a94eab3f..66d53463 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -666,7 +666,7 @@ class UiRequest(object): return self.actionFile(file_path, header_length=header_length, header_noscript=header_noscript, header_allow_ajax=header_allow_ajax, file_size=file_size, path_parts=path_parts) else: self.log.debug("File not found: %s" % path_parts["inner_path"]) - return self.error404(path_parts["inner_path"]) + return self.error404(path) # Serve a media for ui def actionUiMedia(self, path): From 8a71bf65cd4fb53c95f0e3864fc0da5bc35d8b62 Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Fri, 4 Sep 2020 18:08:43 +0200 Subject: [PATCH 2426/2570] Don't leak local path on delete error --- 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 111c67cf..59f7ffe3 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -627,7 +627,7 @@ class UiWebsocket(object): self.site.storage.delete(inner_path) except Exception as err: self.log.error("File delete error: %s" % err) - return self.response(to, {"error": "Delete error: %s" % err}) + return self.response(to, {"error": "Delete error: %s" % Debug.formatExceptionMessage(err)}) self.response(to, "ok") From 0bc9374a7d7c119c7b8031c37ac6ff5c46fc4f7b Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Fri, 4 Sep 2020 18:14:22 +0200 Subject: [PATCH 2427/2570] Optional stats to dirList websocket API command --- src/Ui/UiWebsocket.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 59f7ffe3..a7cade76 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -6,6 +6,7 @@ import shutil import re import copy import logging +import stat import gevent @@ -655,9 +656,19 @@ class UiWebsocket(object): # List directories in a directory @flag.async_run - def actionDirList(self, to, inner_path): + def actionDirList(self, to, inner_path, stats=False): try: - return list(self.site.storage.list(inner_path)) + if stats: + back = [] + for file_name in self.site.storage.list(inner_path): + file_stats = os.stat(self.site.storage.getPath(inner_path + "/" + file_name)) + is_dir = stat.S_ISDIR(file_stats.st_mode) + back.append( + {"name": file_name, "size": file_stats.st_size, "is_dir": is_dir} + ) + return back + else: + return list(self.site.storage.list(inner_path)) except Exception as err: self.log.error("dirList %s error: %s" % (inner_path, Debug.formatException(err))) return {"error": Debug.formatExceptionMessage(err)} From 79f10ffe0c9f9898c5caad866ad9045d0f0a5dd5 Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Fri, 4 Sep 2020 18:15:16 +0200 Subject: [PATCH 2428/2570] Return error when fileGet binary file --- src/Ui/UiWebsocket.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index a7cade76..c4778508 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -708,7 +708,10 @@ class UiWebsocket(object): import base64 body = base64.b64encode(body).decode() else: - body = body.decode() + try: + body = body.decode() + except Exception as err: + self.response(to, {"error": "Error decoding text: %s" % err}) self.response(to, body) @flag.async_run From e14f5bf84704664e257ec72c81af8c64c0a2e7b5 Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Fri, 4 Sep 2020 18:15:56 +0200 Subject: [PATCH 2429/2570] Allow modified files query from non-admin sites --- src/Ui/UiWebsocket.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index c4778508..40e4bdad 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -1039,7 +1039,6 @@ class UiWebsocket(object): else: return {"error": "Invalid address"} - @flag.admin @flag.async_run def actionSiteListModifiedFiles(self, to, content_inner_path="content.json"): content = self.site.content_manager.contents[content_inner_path] From e97236201c756e41a64d81897dad91ffd3b92fe0 Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Fri, 4 Sep 2020 18:21:02 +0200 Subject: [PATCH 2430/2570] Try to recover site privatekey from master seed when site owned switch enabled --- plugins/Sidebar/SidebarPlugin.py | 27 ++++++++++++++++++++++++++- plugins/Sidebar/media/Sidebar.coffee | 13 +++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index ab2e9683..6b8ce7f5 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -737,10 +737,35 @@ class UiWebsocketPlugin(object): @flag.no_multiuser def actionSiteSetOwned(self, to, owned): if self.site.address == config.updatesite: - return self.response(to, "You can't change the ownership of the updater site") + 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.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 diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee index 2f398522..ac6b9ae0 100644 --- a/plugins/Sidebar/media/Sidebar.coffee +++ b/plugins/Sidebar/media/Sidebar.coffee @@ -441,9 +441,18 @@ class Sidebar extends Class # Owned checkbox @tag.find("#checkbox-owned").off("click touchend").on "click touchend", => - @wrapper.ws.cmd "siteSetOwned", [@tag.find("#checkbox-owned").is(":checked")] + 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") + else + @log "Unable to recover private key: #{res_recover.error}" - # Owned checkbox + + # Owned auto download checkbox @tag.find("#checkbox-autodownloadoptional").off("click touchend").on "click touchend", => @wrapper.ws.cmd "siteSetAutodownloadoptional", [@tag.find("#checkbox-autodownloadoptional").is(":checked")] From 91d0ce3a503d87073ee128d1d2a118e7048e3b8b Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Fri, 4 Sep 2020 18:21:52 +0200 Subject: [PATCH 2431/2570] Save users.json of private key change --- plugins/Sidebar/SidebarPlugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index 6b8ce7f5..96d04e22 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -773,6 +773,7 @@ class UiWebsocketPlugin(object): site_data = self.user.sites[self.site.address] site_data["privatekey"] = privatekey self.site.updateWebsocket(set_privatekey=bool(privatekey)) + self.user.save() return "ok" From 817ab04941732b881220b76744ade63f98ae9001 Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Fri, 4 Sep 2020 18:29:02 +0200 Subject: [PATCH 2432/2570] Fix private key recover typo --- plugins/Sidebar/SidebarPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index 96d04e22..4e79839d 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -752,7 +752,7 @@ class UiWebsocketPlugin(object): if site_data.get("privatekey"): return {"error": "This site already has saved privated key"} - address_index = self.site.content_manager.get("content.json", {}).get("address_index") + 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"} From 964545dd1f5cab1e58c901423296615abc9e7b9c Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Sun, 6 Sep 2020 17:01:59 +0200 Subject: [PATCH 2433/2570] Remove unnecessary logging --- plugins/UiConfig/media/js/all.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/UiConfig/media/js/all.js b/plugins/UiConfig/media/js/all.js index cdd2e799..cb3b553f 100644 --- a/plugins/UiConfig/media/js/all.js +++ b/plugins/UiConfig/media/js/all.js @@ -1624,6 +1624,7 @@ }).call(this); + /* ---- ConfigView.coffee ---- */ @@ -1949,7 +1950,6 @@ last = i === changed_values.length - 1; value = this.config_storage.deformatValue(item.value, typeof this.config[item.key]["default"]); default_value = this.config_storage.deformatValue(this.config[item.key]["default"], typeof this.config[item.key]["default"]); - this.log("default check:", JSON.stringify(default_value), "==", JSON.stringify(value)); value_same_as_default = JSON.stringify(default_value) === JSON.stringify(value); if (this.config[item.key].item.valid_pattern && !(typeof (base = this.config[item.key].item).isHidden === "function" ? base.isHidden() : void 0)) { match = value.match(this.config[item.key].item.valid_pattern); From a0dfbe31f6f6bf75514046f019861d68cd65ff38 Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Sun, 6 Sep 2020 17:06:26 +0200 Subject: [PATCH 2434/2570] Add timeout for private key recover message --- plugins/Sidebar/media/Sidebar.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee index ac6b9ae0..336c61bb 100644 --- a/plugins/Sidebar/media/Sidebar.coffee +++ b/plugins/Sidebar/media/Sidebar.coffee @@ -447,7 +447,7 @@ class Sidebar extends Class if owned @wrapper.ws.cmd "siteRecoverPrivatekey", [], (res_recover) => if res_recover == "ok" - @wrapper.notifications.add("recover", "done", "Private key recovered from master seed") + @wrapper.notifications.add("recover", "done", "Private key recovered from master seed", 5000) else @log "Unable to recover private key: #{res_recover.error}" From b7bc19701247d0c906f97e66437f2ea818a81075 Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Tue, 8 Sep 2020 19:26:18 +0200 Subject: [PATCH 2435/2570] Only try to get more peers for timeout task if site is recently added --- src/Site/Site.py | 3 +++ src/Worker/WorkerManager.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 63304944..c40f5195 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -150,6 +150,9 @@ class Site(object): return size_limit return 999999 + def isAddedRecently(self): + return time.time() - self.settings.get("added", 0) < 60 * 60 * 24 + # Download all file from content.json def downloadContent(self, inner_path, download_files=True, peer=None, check_modifications=False, diffs={}): s = time.time() diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 6cfd0d3d..f68e8410 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -84,7 +84,7 @@ class WorkerManager(object): len(task["peers"] or []), len(task["failed"]), len(self.asked_peers) ) ) - if not announced: + if not announced and task["site"].isAddedRecently(): task["site"].announce(mode="more") # Find more peers announced = True if task["optional_hash_id"]: From 5a226baaa510a31255f6cdda3a96798d4c5d3a4a Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Tue, 8 Sep 2020 19:28:04 +0200 Subject: [PATCH 2436/2570] Reduce announce number for not recently added sites --- src/Site/Site.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index c40f5195..166039a2 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -332,10 +332,15 @@ class Site(object): s = time.time() self.log.debug( - "Start downloading, bad_files: %s, check_size: %s, blind_includes: %s, called by: %s" % - (self.bad_files, check_size, blind_includes, Debug.formatStack()) + "Start downloading, bad_files: %s, check_size: %s, blind_includes: %s, isAddedRecently: %s" % + (self.bad_files, check_size, blind_includes, self.isAddedRecently()) ) - gevent.spawn(self.announce, force=True) + + if self.isAddedRecently(): + gevent.spawn(self.announce, mode="start", force=True) + else: + gevent.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 if not valid: @@ -437,7 +442,7 @@ class Site(object): # Wait for peers if not self.peers: - self.announce() + self.announce(mode="update") for wait in range(10): time.sleep(5 + wait) self.log.debug("CheckModifications: Waiting for peers...") @@ -494,7 +499,7 @@ class Site(object): self.checkBadFiles() if announce: - self.announce(force=True) + self.announce(mode="update", force=True) # Full update, we can reset bad files if check_files and since == 0: @@ -581,7 +586,7 @@ class Site(object): publishers = [] # Publisher threads if not self.peers: - self.announce() + self.announce(mode="more") if limit == "default": limit = 5 @@ -788,8 +793,9 @@ class Site(object): else: # Wait until file downloaded self.bad_files[inner_path] = self.bad_files.get(inner_path, 0) + 1 # Mark as bad file if not self.content_manager.contents.get("content.json"): # No content.json, download it first! - self.log.debug("Need content.json first") - gevent.spawn(self.announce) + self.log.debug("Need content.json first (inner_path: %s, priority: %s)" % (inner_path, priority)) + if priority > 0: + gevent.spawn(self.announce) if inner_path != "content.json": # Prevent double download task = self.worker_manager.addTask("content.json", peer) task["evt"].get() From 94765af0f37cd22fd40aa0de53dafc8807481970 Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Tue, 8 Sep 2020 19:28:41 +0200 Subject: [PATCH 2437/2570] Fix not downloaded site delete on startup --- src/Site/SiteManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index 44773b0d..684d69fc 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -79,7 +79,7 @@ class SiteManager(object): content_db = ContentDb.getContentDb() for row in content_db.execute("SELECT * FROM site").fetchall(): address = row["address"] - if address not in self.sites: + if address not in self.sites and address not in address_found: self.log.info("Deleting orphan site from content.db: %s" % address) try: From 8dc5aee8aab271ec98151792c6e3f8626a91078c Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Tue, 8 Sep 2020 19:32:10 +0200 Subject: [PATCH 2438/2570] Js based redirecting template formatting --- src/Ui/UiRequest.py | 15 ++++++++++++++- src/util/helper.py | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 66d53463..efac19be 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -350,12 +350,25 @@ class UiRequest(object): return is_html_file + @helper.encodeResponse + def formatRedirect(self, url): + return """ + + + Redirecting to {0} + + + + """.format(html.escape(url)) + # - Actions - # Redirect to an url def actionRedirect(self, url): self.start_response('301 Redirect', [('Location', str(url))]) - yield b"Location changed: " + url.encode("utf8") + yield self.formatRedirect(url) def actionIndex(self): return self.actionRedirect("/" + config.homepage) diff --git a/src/util/helper.py b/src/util/helper.py index 5383e5a3..61455b08 100644 --- a/src/util/helper.py +++ b/src/util/helper.py @@ -338,7 +338,7 @@ def cmp(a, b): return (a > b) - (a < b) -def encodeResponse(func): +def encodeResponse(func): # Encode returned data from utf8 to bytes def wrapper(*args, **kwargs): back = func(*args, **kwargs) if "__next__" in dir(back): From 1695571afaa7608d1df13741846cf6113a842576 Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Tue, 8 Sep 2020 19:32:45 +0200 Subject: [PATCH 2439/2570] Add browser-like header for port checker requests --- src/Peer/PeerPortchecker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Peer/PeerPortchecker.py b/src/Peer/PeerPortchecker.py index ac4d650f..6f487b5a 100644 --- a/src/Peer/PeerPortchecker.py +++ b/src/Peer/PeerPortchecker.py @@ -18,7 +18,9 @@ class PeerPortchecker(object): if type(post_data) is dict: post_data = urllib.parse.urlencode(post_data).encode("utf8") req = urllib.request.Request(url, post_data) - req.add_header('Referer', url) + req.add_header("Referer", url) + req.add_header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11") + req.add_header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") return urllib.request.urlopen(req, timeout=20.0) def portOpen(self, port): From 5b09f7af41f67710194ca9bbf44ae64cd9532cc2 Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Tue, 8 Sep 2020 19:35:23 +0200 Subject: [PATCH 2440/2570] New port checker: ipfingerprints.com, PortChecker minor rearranging --- src/Peer/PeerPortchecker.py | 102 +++++++++++++++++++++--------------- 1 file changed, 59 insertions(+), 43 deletions(-) diff --git a/src/Peer/PeerPortchecker.py b/src/Peer/PeerPortchecker.py index 6f487b5a..3c4daecf 100644 --- a/src/Peer/PeerPortchecker.py +++ b/src/Peer/PeerPortchecker.py @@ -9,6 +9,10 @@ from util import UpnpPunch class PeerPortchecker(object): + checker_functions = { + "ipv4": ["checkIpfingerprints", "checkCanyouseeme"], + "ipv6": ["checkMyaddr", "checkIpv6scanner"] + } def __init__(self, file_server): self.log = logging.getLogger("PeerPortchecker") self.upnp_port_opened = False @@ -39,10 +43,7 @@ class PeerPortchecker(object): return UpnpPunch.ask_to_close_port(port, protos=["TCP"]) def portCheck(self, port, ip_type="ipv4"): - if ip_type == "ipv6": - checker_functions = ["checkMyaddr", "checkIpv6scanner"] - else: - checker_functions = ["checkPortchecker", "checkCanyouseeme"] + checker_functions = self.checker_functions[ip_type] for func_name in checker_functions: func = getattr(self, func_name) @@ -51,13 +52,13 @@ class PeerPortchecker(object): res = func(port) if res: self.log.info( - "Checking port %s (%s) using %s result: %s in %.3fs" % + "Checked port %s (%s) using %s result: %s in %.3fs" % (port, ip_type, func_name, res, time.time() - s) ) time.sleep(0.1) if res["opened"] and not self.file_server.had_external_incoming: res["opened"] = False - self.log.warning("Port %s:%s, but no incoming connection" % (res["ip"], port)) + self.log.warning("Port %s:%s looks opened, but no incoming connection" % (res["ip"], port)) break except Exception as err: self.log.warning( @@ -87,41 +88,19 @@ class PeerPortchecker(object): else: raise Exception("Invalid response: %s" % message) - def checkPortchecker(self, port): - data = urllib.request.urlopen("https://portchecker.co", b"port=%s" % str(port).encode("ascii"), timeout=20.0).read().decode("utf8") - message = re.match(r'.*
    (.*?)
    ', data, re.DOTALL).group(1) - message = re.sub(r"<.*?>", "", message.replace("
    ", " ").replace(" ", " ").strip()) # Strip http tags + def checkIpfingerprints(self, port): + data = self.requestUrl("https://www.ipfingerprints.com/portscan.php").read().decode("utf8") + ip = re.match(r'.*name="remoteHost".*?value="(.*?)"', data, re.DOTALL).group(1) - match = re.match(r".*targetIP.*?value=\"(.*?)\"", data, re.DOTALL) - if match: - ip = match.group(1) - else: - raise Exception("Invalid response: %s" % message) + post_data = { + "remoteHost": ip, "start_port": port, "end_port": port, + "normalScan": "Yes", "scan_type": "connect2", "ping_type": "none" + } + message = self.requestUrl("https://www.ipfingerprints.com/scripts/getPortsInfo.php", post_data).read().decode("utf8") if "open" in message: return {"ip": ip, "opened": True} - elif "closed" in message: - return {"ip": ip, "opened": False} - else: - raise Exception("Invalid response: %s" % message) - - def checkSubnetonline(self, port): - url = "https://www.subnetonline.com/pages/ipv6-network-tools/online-ipv6-port-scanner.php" - - data = self.requestUrl(url).read().decode("utf8") - - ip = re.match(r'.*Your IP is.*?name="host".*?value="(.*?)"', data, re.DOTALL).group(1) - token = re.match(r'.*name="token".*?value="(.*?)"', data, re.DOTALL).group(1) - - post_data = {"host": ip, "port": port, "allow": "on", "token": token, "submit": "Scanning.."} - data = self.requestUrl(url, post_data).read().decode("utf8") - - message = re.match(r".*
    (.*?)
    ", data, re.DOTALL).group(1) - message = re.sub(r"<.*?>", "", message.replace("
    ", " ").replace(" ", " ").strip()) # Strip http tags - - if "online" in message: - return {"ip": ip, "opened": True} - elif "closed" in message: + elif "filtered" in message or "closed" in message: return {"ip": ip, "opened": False} else: raise Exception("Invalid response: %s" % message) @@ -165,9 +144,46 @@ class PeerPortchecker(object): else: raise Exception("Invalid response: %s" % message_text) -if __name__ == "__main__": - import time - peer_portchecker = PeerPortchecker() - for func_name in ["checkIpv6scanner", "checkMyaddr", "checkPortchecker", "checkCanyouseeme"]: - s = time.time() - print((func_name, getattr(peer_portchecker, func_name)(3894), "%.3fs" % (time.time() - s))) + def checkPortchecker(self, port): # Not working: Forbidden + data = self.requestUrl("https://portchecker.co").read().decode("utf8") + csrf = re.match(r'.*name="_csrf" value="(.*?)"', data, re.DOTALL).group(1) + + data = self.requestUrl("https://portchecker.co", {"port": port, "_csrf": csrf}).read().decode("utf8") + message = re.match(r'.*
    (.*?)
    ', data, re.DOTALL).group(1) + message = re.sub(r"<.*?>", "", message.replace("
    ", " ").replace(" ", " ").strip()) # Strip http tags + + match = re.match(r".*targetIP.*?value=\"(.*?)\"", data, re.DOTALL) + if match: + ip = match.group(1) + else: + raise Exception("Invalid response: %s" % message) + + if "open" in message: + return {"ip": ip, "opened": True} + elif "closed" in message: + return {"ip": ip, "opened": False} + else: + raise Exception("Invalid response: %s" % message) + + def checkSubnetonline(self, port): # Not working: Invalid response + url = "https://www.subnetonline.com/pages/ipv6-network-tools/online-ipv6-port-scanner.php" + + data = self.requestUrl(url).read().decode("utf8") + + ip = re.match(r'.*Your IP is.*?name="host".*?value="(.*?)"', data, re.DOTALL).group(1) + token = re.match(r'.*name="token".*?value="(.*?)"', data, re.DOTALL).group(1) + + post_data = {"host": ip, "port": port, "allow": "on", "token": token, "submit": "Scanning.."} + data = self.requestUrl(url, post_data).read().decode("utf8") + + print(post_data, data) + + message = re.match(r".*
    (.*?)
    ", data, re.DOTALL).group(1) + message = re.sub(r"<.*?>", "", message.replace("
    ", " ").replace(" ", " ").strip()) # Strip http tags + + if "online" in message: + return {"ip": ip, "opened": True} + elif "closed" in message: + return {"ip": ip, "opened": False} + else: + raise Exception("Invalid response: %s" % message) From 8c20927f68a7ce945d72dfc22e4b32a0ee2c93a6 Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Tue, 8 Sep 2020 19:35:58 +0200 Subject: [PATCH 2441/2570] Allow test port checker functions from CLI --- plugins/Benchmark/BenchmarkPlugin.py | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/plugins/Benchmark/BenchmarkPlugin.py b/plugins/Benchmark/BenchmarkPlugin.py index 39590622..b47da642 100644 --- a/plugins/Benchmark/BenchmarkPlugin.py +++ b/plugins/Benchmark/BenchmarkPlugin.py @@ -340,6 +340,32 @@ class ActionsPlugin: yield "." assert ok, "does not verify from %s" % address + 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) + s = time.time() + 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 @@ -361,4 +387,9 @@ class ConfigPlugin(object): '--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 From c4f8c0177ef849e5875f5c794d3811ffd7cdf238 Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Tue, 8 Sep 2020 19:36:54 +0200 Subject: [PATCH 2442/2570] Add mode to tracker announce logging --- plugins/AnnounceZero/AnnounceZeroPlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/AnnounceZero/AnnounceZeroPlugin.py b/plugins/AnnounceZero/AnnounceZeroPlugin.py index 7f31e052..623cd4b5 100644 --- a/plugins/AnnounceZero/AnnounceZeroPlugin.py +++ b/plugins/AnnounceZero/AnnounceZeroPlugin.py @@ -133,8 +133,8 @@ class SiteAnnouncerPlugin(object): 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) in %.3fs" % - (tracker_address, site_index, peers_added, add_types, time.time() - s) + "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 From 49f8e0bc3a7ac757feac7e5f59b5ea12dd9c5387 Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Wed, 9 Sep 2020 18:21:09 +0200 Subject: [PATCH 2443/2570] Allow link to console tabs --- plugins/Sidebar/media/Console.coffee | 10 +++++++--- plugins/Sidebar/media/Prototypes.coffee | 9 +++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 plugins/Sidebar/media/Prototypes.coffee diff --git a/plugins/Sidebar/media/Console.coffee b/plugins/Sidebar/media/Console.coffee index 724ebb94..d5a83346 100644 --- a/plugins/Sidebar/media/Console.coffee +++ b/plugins/Sidebar/media/Console.coffee @@ -20,10 +20,10 @@ class Console extends Class handleMessageWebsocket_original(message) $(window).on "hashchange", => - if window.top.location.hash == "#ZeroNet:Console" + if window.top.location.hash.startsWith("#ZeroNet:Console") @open() - if window.top.location.hash == "#ZeroNet:Console" + if window.top.location.hash.startsWith("#ZeroNet:Console") setTimeout (=> @open()), 10 createHtmltag: -> @@ -58,10 +58,13 @@ class Console extends Class @container.appendTo(document.body) @tag = @container.find(".console") for tab_type in @tab_types - tab = $("", {href: "#", "data-filter": tab_type.filter}).text(tab_type.title) + 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) => @@ -192,6 +195,7 @@ class Console extends Class $("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/Prototypes.coffee b/plugins/Sidebar/media/Prototypes.coffee new file mode 100644 index 00000000..a9edd255 --- /dev/null +++ b/plugins/Sidebar/media/Prototypes.coffee @@ -0,0 +1,9 @@ +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 From b9c65d75efd44d80ee81babcc536ed26a1cd54fe Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Wed, 9 Sep 2020 18:29:24 +0200 Subject: [PATCH 2444/2570] Move error log handler to config object to be able to catch plugin load errors --- src/Config.py | 18 ++++++++++++++++++ src/Ui/UiServer.py | 17 +++-------------- src/Ui/UiWebsocket.py | 2 +- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/Config.py b/src/Config.py index 87c11553..ea6f143e 100644 --- a/src/Config.py +++ b/src/Config.py @@ -7,6 +7,7 @@ import configparser import logging import logging.handlers import stat +import time class Config(object): @@ -647,9 +648,26 @@ class Config(object): logging.getLogger('').name = "-" # Remove root prefix + self.error_logger = ErrorLogHandler() + self.error_logger.setLevel(logging.getLevelName("ERROR")) + logging.getLogger('').addHandler(self.error_logger) + if console_logging: self.initConsoleLogger() if file_logging: self.initFileLogger() + +class ErrorLogHandler(logging.StreamHandler): + def __init__(self): + self.lines = [] + return super().__init__() + + def emit(self, record): + self.lines.append([time.time(), record.levelname, self.format(record)]) + + def onNewRecord(self, record): + pass + + config = Config(sys.argv) diff --git a/src/Ui/UiServer.py b/src/Ui/UiServer.py index edeed59d..9d93ccfd 100644 --- a/src/Ui/UiServer.py +++ b/src/Ui/UiServer.py @@ -14,17 +14,6 @@ from Debug import Debug import importlib -class LogDb(logging.StreamHandler): - def __init__(self, ui_server): - self.lines = [] - self.ui_server = ui_server - return super(LogDb, self).__init__() - - def emit(self, record): - self.ui_server.updateWebsocket(log_event=record.levelname) - self.lines.append([time.time(), record.levelname, self.format(record)]) - - # Skip websocket handler if not necessary class UiWSGIHandler(WebSocketHandler): @@ -93,10 +82,10 @@ class UiServer: self.site_manager = SiteManager.site_manager self.sites = SiteManager.site_manager.list() self.log = logging.getLogger(__name__) + config.error_logger.onNewRecord = self.handleErrorLogRecord - self.logdb_errors = LogDb(ui_server=self) - self.logdb_errors.setLevel(logging.getLevelName("ERROR")) - logging.getLogger('').addHandler(self.logdb_errors) + def handleErrorLogRecord(self, record): + self.updateWebsocket(log_event=record.levelname) # After WebUI started def afterStarted(self): diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 40e4bdad..599f6558 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -1123,7 +1123,7 @@ class UiWebsocket(object): @flag.admin @flag.no_multiuser def actionServerErrors(self, to): - return self.server.logdb_errors.lines + return config.error_logger.lines @flag.admin @flag.no_multiuser From e74fdc4036b2aee6317d2531e02cf314b5be91b5 Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Wed, 9 Sep 2020 18:29:53 +0200 Subject: [PATCH 2445/2570] Redirect homepage with / at the end --- src/Ui/UiRequest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index efac19be..afbe3226 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -371,7 +371,7 @@ class UiRequest(object): yield self.formatRedirect(url) def actionIndex(self): - return self.actionRedirect("/" + config.homepage) + return self.actionRedirect("/" + config.homepage + "/") # Render a file from media with iframe site wrapper def actionWrapper(self, path, extra_headers=None): From 0309b816956d7cb653f907cb00e2833302326da2 Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Fri, 18 Sep 2020 18:42:03 +0200 Subject: [PATCH 2446/2570] SiteListModifiedFiles: Give error instead of exception if content file does not exists --- src/Ui/UiWebsocket.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 599f6558..88e395d6 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -1041,7 +1041,10 @@ class UiWebsocket(object): @flag.async_run def actionSiteListModifiedFiles(self, to, content_inner_path="content.json"): - content = self.site.content_manager.contents[content_inner_path] + content = self.site.content_manager.contents.get(content_inner_path) + if not content: + return {"error": "content file not avaliable"} + min_mtime = content.get("modified", 0) site_path = self.site.storage.directory modified_files = [] From bf092b83abcfbda56748c457199c70ded2f09dcd Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Fri, 18 Sep 2020 18:43:25 +0200 Subject: [PATCH 2447/2570] Workaround for stuck iframe url in Firefox when using back button --- src/Ui/template/wrapper.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Ui/template/wrapper.html b/src/Ui/template/wrapper.html index 6cb27ba7..8e1c6fbc 100644 --- a/src/Ui/template/wrapper.html +++ b/src/Ui/template/wrapper.html @@ -76,7 +76,10 @@ else if (window.opener && window.opener.location.toString()) { + + + +
    + + + + \ No newline at end of file From 392350ff79238cf8f01c8f9432ce578d706bd766 Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Mon, 21 Sep 2020 18:25:38 +0200 Subject: [PATCH 2454/2570] Codemirror file editor for UiFileManager plugin --- .../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 + 57 files changed, 41027 insertions(+) create mode 100644 plugins/UiFileManager/media/codemirror/LICENSE create mode 100644 plugins/UiFileManager/media/codemirror/all.css create mode 100644 plugins/UiFileManager/media/codemirror/all.js create mode 100644 plugins/UiFileManager/media/codemirror/base/codemirror.css create mode 100644 plugins/UiFileManager/media/codemirror/base/codemirror.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/dialog/dialog.css create mode 100644 plugins/UiFileManager/media/codemirror/extension/dialog/dialog.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/edit/closebrackets.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/edit/closetag.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/edit/continuelist.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/edit/matchbrackets.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/edit/matchtags.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/edit/trailingspace.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/fold/brace-fold.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/fold/comment-fold.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/fold/foldcode.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/fold/foldgutter.css create mode 100644 plugins/UiFileManager/media/codemirror/extension/fold/foldgutter.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/fold/indent-fold.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/fold/markdown-fold.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/fold/xml-fold.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/hint/anyword-hint.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/hint/html-hint.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/hint/show-hint.css create mode 100644 plugins/UiFileManager/media/codemirror/extension/hint/show-hint.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/hint/sql-hint.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/hint/xml-hint.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/lint/json-lint.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/lint/jsonlint.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/lint/lint.css create mode 100644 plugins/UiFileManager/media/codemirror/extension/lint/lint.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/mdn-like-custom.css create mode 100644 plugins/UiFileManager/media/codemirror/extension/scroll/annotatescrollbar.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/scroll/scrollpastend.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/scroll/simplescrollbars.css create mode 100644 plugins/UiFileManager/media/codemirror/extension/scroll/simplescrollbars.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/search/jump-to-line.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/search/match-highlighter.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/search/matchesonscrollbar.css create mode 100644 plugins/UiFileManager/media/codemirror/extension/search/matchesonscrollbar.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/search/search.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/search/searchcursor.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/selection/active-line.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/selection/mark-selection.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/selection/selection-pointer.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/simple.js create mode 100644 plugins/UiFileManager/media/codemirror/extension/sublime.js create mode 100644 plugins/UiFileManager/media/codemirror/mode/coffeescript.js create mode 100644 plugins/UiFileManager/media/codemirror/mode/css.js create mode 100644 plugins/UiFileManager/media/codemirror/mode/go.js create mode 100644 plugins/UiFileManager/media/codemirror/mode/htmlembedded.js create mode 100644 plugins/UiFileManager/media/codemirror/mode/htmlmixed.js create mode 100644 plugins/UiFileManager/media/codemirror/mode/javascript.js create mode 100644 plugins/UiFileManager/media/codemirror/mode/markdown.js create mode 100644 plugins/UiFileManager/media/codemirror/mode/python.js create mode 100644 plugins/UiFileManager/media/codemirror/mode/rust.js create mode 100644 plugins/UiFileManager/media/codemirror/mode/xml.js diff --git a/plugins/UiFileManager/media/codemirror/LICENSE b/plugins/UiFileManager/media/codemirror/LICENSE new file mode 100644 index 00000000..ff7db4b9 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/LICENSE @@ -0,0 +1,21 @@ +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 new file mode 100644 index 00000000..86904409 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/all.css @@ -0,0 +1,678 @@ + +/* ---- 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 new file mode 100644 index 00000000..ef2a423a --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/all.js @@ -0,0 +1,19964 @@ + +/* ---- 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 new file mode 100644 index 00000000..56896500 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/base/codemirror.css @@ -0,0 +1,349 @@ +/* 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 new file mode 100644 index 00000000..06f0f868 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/base/codemirror.js @@ -0,0 +1,9778 @@ +// 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 new file mode 100644 index 00000000..677c0783 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/dialog/dialog.css @@ -0,0 +1,32 @@ +.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 new file mode 100644 index 00000000..5f1f4aa4 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/dialog/dialog.js @@ -0,0 +1,163 @@ +// 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 new file mode 100644 index 00000000..4415c393 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/edit/closebrackets.js @@ -0,0 +1,191 @@ +// 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 new file mode 100644 index 00000000..8689765e --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/edit/closetag.js @@ -0,0 +1,184 @@ +// 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 new file mode 100644 index 00000000..2e5625ad --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/edit/continuelist.js @@ -0,0 +1,101 @@ +// 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 new file mode 100644 index 00000000..2c47e070 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/edit/matchbrackets.js @@ -0,0 +1,158 @@ +// 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 new file mode 100644 index 00000000..2203d939 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/edit/matchtags.js @@ -0,0 +1,66 @@ +// 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 new file mode 100644 index 00000000..c39c310a --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/edit/trailingspace.js @@ -0,0 +1,27 @@ +// 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 new file mode 100644 index 00000000..654d1fb6 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/fold/brace-fold.js @@ -0,0 +1,105 @@ +// 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 new file mode 100644 index 00000000..836101d8 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/fold/comment-fold.js @@ -0,0 +1,59 @@ +// 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 new file mode 100644 index 00000000..887df3fe --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/fold/foldcode.js @@ -0,0 +1,157 @@ +// 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 new file mode 100644 index 00000000..ad19ae2d --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/fold/foldgutter.css @@ -0,0 +1,20 @@ +.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 new file mode 100644 index 00000000..7d46a609 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/fold/foldgutter.js @@ -0,0 +1,163 @@ +// 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 new file mode 100644 index 00000000..0cc11264 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/fold/indent-fold.js @@ -0,0 +1,48 @@ +// 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 new file mode 100644 index 00000000..6a551786 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/fold/markdown-fold.js @@ -0,0 +1,49 @@ +// 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 new file mode 100644 index 00000000..13bc3838 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/fold/xml-fold.js @@ -0,0 +1,184 @@ +// 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 new file mode 100644 index 00000000..d27a9ec0 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/hint/anyword-hint.js @@ -0,0 +1,41 @@ +// 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 new file mode 100644 index 00000000..d0cca4f6 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/hint/html-hint.js @@ -0,0 +1,350 @@ +// 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 new file mode 100644 index 00000000..5617ccca --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/hint/show-hint.css @@ -0,0 +1,36 @@ +.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 new file mode 100644 index 00000000..cd0d6a7b --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/hint/show-hint.js @@ -0,0 +1,479 @@ +// 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 new file mode 100644 index 00000000..de84707d --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/hint/sql-hint.js @@ -0,0 +1,304 @@ +// 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 new file mode 100644 index 00000000..7575b370 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/hint/xml-hint.js @@ -0,0 +1,123 @@ +// 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 new file mode 100644 index 00000000..ac1d6ec2 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/lint/json-lint.js @@ -0,0 +1,40 @@ +// 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 new file mode 100644 index 00000000..f34759e8 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/lint/jsonlint.js @@ -0,0 +1 @@ +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 new file mode 100644 index 00000000..f097cfe3 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/lint/lint.css @@ -0,0 +1,73 @@ +/* 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 new file mode 100644 index 00000000..5bc1af18 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/lint/lint.js @@ -0,0 +1,255 @@ +// 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 new file mode 100644 index 00000000..7a523119 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/mdn-like-custom.css @@ -0,0 +1,44 @@ +/* + 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 new file mode 100644 index 00000000..9fe61ec1 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/scroll/annotatescrollbar.js @@ -0,0 +1,122 @@ +// 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 new file mode 100644 index 00000000..2ed9d95e --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/scroll/scrollpastend.js @@ -0,0 +1,48 @@ +// 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 new file mode 100644 index 00000000..5eea7aa1 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/scroll/simplescrollbars.css @@ -0,0 +1,66 @@ +.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 new file mode 100644 index 00000000..750a2bd3 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/scroll/simplescrollbars.js @@ -0,0 +1,152 @@ +// 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 new file mode 100644 index 00000000..1f3526d2 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/search/jump-to-line.js @@ -0,0 +1,50 @@ +// 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 new file mode 100644 index 00000000..3a4a7ded --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/search/match-highlighter.js @@ -0,0 +1,167 @@ +// 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 new file mode 100644 index 00000000..77932cc9 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/search/matchesonscrollbar.css @@ -0,0 +1,8 @@ +.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 new file mode 100644 index 00000000..8a4a8275 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/search/matchesonscrollbar.js @@ -0,0 +1,97 @@ +// 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 new file mode 100644 index 00000000..cecdd52e --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/search/search.js @@ -0,0 +1,260 @@ +// 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 new file mode 100644 index 00000000..d5869578 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/search/searchcursor.js @@ -0,0 +1,296 @@ +// 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 new file mode 100644 index 00000000..c7b14ce0 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/selection/active-line.js @@ -0,0 +1,72 @@ +// 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 new file mode 100644 index 00000000..adfaa62d --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/selection/mark-selection.js @@ -0,0 +1,119 @@ +// 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 new file mode 100644 index 00000000..f0bd61a3 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/selection/selection-pointer.js @@ -0,0 +1,98 @@ +// 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 new file mode 100644 index 00000000..655f9914 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/simple.js @@ -0,0 +1,216 @@ +// 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 new file mode 100644 index 00000000..7edf172e --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/extension/sublime.js @@ -0,0 +1,714 @@ +// 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 new file mode 100644 index 00000000..a54e9d5e --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/mode/coffeescript.js @@ -0,0 +1,359 @@ +// 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 new file mode 100644 index 00000000..441ba4ab --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/mode/css.js @@ -0,0 +1,860 @@ +// 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 new file mode 100644 index 00000000..c005e42d --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/mode/go.js @@ -0,0 +1,187 @@ +// 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 new file mode 100644 index 00000000..439e63a4 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/mode/htmlembedded.js @@ -0,0 +1,37 @@ +// 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 new file mode 100644 index 00000000..8341ac82 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/mode/htmlmixed.js @@ -0,0 +1,152 @@ +// 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 new file mode 100644 index 00000000..9c751d23 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/mode/javascript.js @@ -0,0 +1,934 @@ +// 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 new file mode 100644 index 00000000..287f39b5 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/mode/markdown.js @@ -0,0 +1,886 @@ +// 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 new file mode 100644 index 00000000..de5fd38a --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/mode/python.js @@ -0,0 +1,399 @@ +// 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 new file mode 100644 index 00000000..f95f320d --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/mode/rust.js @@ -0,0 +1,72 @@ +// 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 new file mode 100644 index 00000000..73c6e0e0 --- /dev/null +++ b/plugins/UiFileManager/media/codemirror/mode/xml.js @@ -0,0 +1,413 @@ +// 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}); + +}); From 19bc0358b51340c38111aef4b9ed429932109902 Mon Sep 17 00:00:00 2001 From: Tamas Kocsis Date: Mon, 21 Sep 2020 18:25:53 +0200 Subject: [PATCH 2455/2570] Merge UiFileManager js, css --- plugins/UiFileManager/media/css/all.css | 211 ++ plugins/UiFileManager/media/js/all.js | 3013 +++++++++++++++++++++++ 2 files changed, 3224 insertions(+) create mode 100644 plugins/UiFileManager/media/css/all.css create mode 100644 plugins/UiFileManager/media/js/all.js diff --git a/plugins/UiFileManager/media/css/all.css b/plugins/UiFileManager/media/css/all.css new file mode 100644 index 00000000..cd9e16c6 --- /dev/null +++ b/plugins/UiFileManager/media/css/all.css @@ -0,0 +1,211 @@ + +/* ---- 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 - 100x); 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 - 73px); 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/js/all.js b/plugins/UiFileManager/media/js/all.js new file mode 100644 index 00000000..eae5fc11 --- /dev/null +++ b/plugins/UiFileManager/media/js/all.js @@ -0,0 +1,3013 @@ + +/* ---- 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 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,d09GRgABAAAAAGfcABIAAAAAx5wAAQABAAAAAAAAAAAAAAAAAAAAAAAAAABHREVGAAABlAAAAEcAAABYB30Hd0dQT1MAAAHcAAAH8AAAFLywggk9R1NVQgAACcwAAACmAAABFMK7zVBPUy8yAAAKdAAAAFYAAABgoKexpmNtYXAAAArMAAADZAAABnjIFMucY3Z0IAAADjAAAABMAAAATCRBBuVmcGdtAAAOfAAAATsAAAG8Z/Rcq2dhc3AAAA+4AAAADAAAAAwACAATZ2x5ZgAAD8QAAE7fAACZfgdaOmpoZG14AABepAAAAJoAAAGo8AnZfGhlYWQAAF9AAAAANgAAADb4RqsOaGhlYQAAX3gAAAAgAAAAJAq6BzxobXR4AABfmAAAA4cAAAZwzpCM0GxvY2EAAGMgAAADKQAAAzowggjbbWF4cAAAZkwAAAAgAAAAIAPMAvluYW1lAABmbAAAAJkAAAEQEG8sqXBvc3QAAGcIAAAAEwAAACD/bQBkcHJlcAAAZxwAAAC9AAAA23Sgj+x4AQXBsQFBMQAFwHvRZg0bgEpnDXukA4AWYBvqv9O/E1RAUQ3NxcJSNM3A2lpsbcXBQZydxdVdPH3Fz1/RZSyZ5Ss9lqEL+AB4AWSOA4ydQRgAZ7a2bdu2bdu2bduI07hubF2s2gxqxbX+p7anzO5nIZCfkawkZ8/eA0dSfsa65QupPWf5rAU0Xzht5WI6kxMgihAy2GawQwY7BzkXzFq+mPLZJSAkO0NyVuEchXPXzjMfTU3eEJqGpv4IV0LrMD70DITBYWTcyh0Wh6LhdEgLR8O5UD3+U0wNP+I0/cv4OIvjvRlpHZ+SYvx/0uKd2YlP+t+TJHnBuWz/XPKmJP97x2f4U5MsTpC8+Efi6iSn46Qi58KVhP73kQ3kpgAlqEUd6lKP+jShKS1oSVva04FOdKYf/RnIMIYzgtGMZxLnucAlLnON69zkNne4yz3u84CHPOIxT3jKM17wkle85g0f+cwXvvKN3/whEjWYx7zms4CFLGIxS1jKMpazvBWsaCUrW8WqVrO6DW1vRzvb1e72so/97O8ABzrIwQ5xqMMd6WinOcNZrnCVq13jWte70e3udLd73edBD3nEox7zuCc8iZSIqiKjo9cExlKYbdEZclKIknQjRik9xkmSNHEc/9fY01Nr27Zt27Zt294HZ9u2bWttjGc1OHXc70Wt+tQb9fl2dkZmRuTUdBL5ExrDewn1Mq6YsX+YYkWOU23sksZYFqe7WqaGWapYtXfEp90vh3pH2dlViVSvy7kkRSnM9lH5BXZ8pBn+l7XcKrOvhzbaTm2xe8RZOy1uwak2imNvGn0TyD9qT5MvZ+9pMD2HUfsWy2QlhntyQyXYV+KW3CWVU/s0mJEba4Y9SZcv6HI3Xd6hy9t6yr6jYlfOOSpMVSlSVdVcC51jIVX5Df2ffCT5OLIN1FCt1JVZY9vnjME4TKBDgprStxk9W6ig0lXQmSfXWcC4CGv5vh4bsZn5LuzBf9g7VD4rKBcVbKBq+vPUmEod7Ig6WZo6owu6oR8GYIilaqglawT+w/xm3EruMWo8iW+p8x2+xw/4ET9hHzKom4ksnMN5XMBFXKJONnKQizz4YZbmCA5CEGqpThjCEYFIS3aiEG0DnRg74sQyxjHGMyYw+jjjIj8KojCKojhKojTKojwqojKqorE/z+nO2BO9MUb5nXGYgMn0nYrpmInZmIuF3GMLdtB7J713830v/mvJctXYflBTO6Vmlq4Wdljpdpj/4g/OOEzAPEt3FpBbhLV8X4+N2Mx8F/bgP5yLp9LTVMqgytdU+ZoqTzvjMAELmC/CZuzCHvyHffGqaZlqgmSkIBVpluk0xiRMwTTMwCzMYb20IuRTLDpZsjqjC7phAP6Dm/EI64/icTyBS+SykYNc5PEOfHCRHwVRGEVRHCVRGmVRHhVRGVU56yi/wiSFq6y261m9r1/kMOulwRqmUfQtyt3S1Rld0A0D8B/cjEvIRg5ykccb9cFFfhREYRRFcZREaZRFeVREZVTlbLT68emHkREchKA7eqI3a2Hy2Xq5eAxPgndPvgmSkYJUpLG/MSZhCqZhBmZhDuuuuqu0eqE3+tlqDbLd8jOarXYEByHojp7ojcG22xmK4RiJ0ZwJCe/NrRSxN/pFFVdhyb60bMuyzXbJXrNVlq04e8TuVVBhp0VYsn0S5P6T3nhKrpKCrp9qP1gan7daSjD1/znsjDdmSMpvWQGrZAMyL3Nbwu5Qonx2j70vH+MzZCqKrD1nhe0/ds522Xbzkdlnx6+5e0pgd7x9bdaW2Vv2qf9pyeb4M+x7xj6WpHz6u0gEYRevq7vQjvtftzNXs5aNxvqbsNS/XcmmBmHfev8pgvEFlML3OHh1nfG4nRVhaVc+EwL+XnZek0m3k3Y341tKUpLttxNy5dq9ircaImsp9rnt432+ZB+y70rwVqlsGd7sB2wQWbwvwo56K6fpefU+3n7Fw8teH3ZehL2hGwrLvrGddvL6ftLfzb23f0E3FHazgguvny2+Mj8XsJ721786zgWE/Q8XFfh3uJB8lq6AsA3IuDLbF7Dq7Q8i6907+Ky4q7133XyzN34gr4t9aU9fsz5QwUWIGiiCR4rlceTjCZHLE6oKqqIwVVd9RauxWpLroE4qoi48xdWdp4T6qL9KaiBPWQ3lKafhGqny2srzB6PljBAAAEbh9+U6QJyybXPPWLJt27bdmK8SLpPtsd/zr/dcdaRzuX3weR9dvqmfrnUrfz1hoBxMsVIeNjioHk+81YkvvurBH3/1Ekig+ggmWP2EEaYBIojQIFFEaYgYYjRMHHEaIYEEjZJEisZII03LZJChFbLI0iqFFGqNYoq1Timl2qCccm1SSaW2qKZa29RSqx3qqdcujTRqj2aatU8rvTpgiCEdMcKIjhljTCdMMKlTplnRuZAJ87LVl/yp7D78f4KMZCjjr5kYyEKmMvuoDGWu19rpAlV6GACA8Lf19Xp/uf89XyA0hH1uM0wcJ5HGydnNxdVdTm80YAKznTm4GLGJrPgTxr9+h9F3+Bf8L47foQzSeKRSixbJMnkSverlDibRndmS3FmD9KnKIK9EbXrWI4U55Fmc0KJ7qDDvBUtLii3rOU3W6ZVuuFpDd39TO7dYekVhRi/sUvGPVHbSys0Y+ggXFJDmjbSPzVqlk8bV2V3Ogl4QocQUrEM9VnQOGMJ49FMU79z28lXnNcZgFbzF8Yf+6UVu4TnPf8vZIrdP7kzqZCd6CF4sqUIvzys9f/cam9eY9oKFOpUzW5/Vkip1L9bg7BC6O6agQJOKr2BysQi7vSdc5EV5eAFNizNiBAEYhb/3T+ykje1U08RsYtu2c5X4Nrv3Wo+a54eAErb4Qg+nH08UUUfe4vJCE21Lk1tN9K0tLzbhbmyuNTECySQCj81jx+M8j0X+w+31KU1Z7Hp4Pn9gIItuFocAwyEPkIdk0SD3p4wyWpjhCAGiCFGAIUz7OghSo4I8/ehXf/pH5KlcFWpUE3nBr8/jPGIYi5GmJmjiGCsIMZcC7Q8igwAAeAE1xTcBwlAABuEvvYhI0cDGxJYxqHg2mNhZ6RawggOE0Ntf7iTpMlrJyDbZhKj9OjkLMWL/XNSPuX6BHoZxHMx43HJ3QrGJdaIjpNPspNOJn5pGDpMAAHgBhdIDsCRJFIXhcxpjm7U5tm3bCK5tKzS2bdu2bdszNbb5mHveZq1CeyO+/tu3u6oAhAN5dMugqYDQXERCAwF8hbqIojiAtOiMqViIRdiC3TiCW3iMRKZnRhZiEZZlB77Pz9mZXTiEwzmNS/mENpQ7VCW0O3Q+dNGjV8fr5T33YkwWk8t4Jr+pbhqaX8xMM98sNMvMerMpfyZrodEuo13TtGsxtmIPjuI2nsAyAzOxMIuyHDvyA34R7JrKJdoVG8rx9y54tb2u3jPvhclscpg82lXtz10zzGyzQLvWmY1Ju0D7yt5ACbsdb9ltADJJWkkpySUK2ASxNqtNZiOJrxPv2fHQJH6ScDphd8Lu64Out7oeujb62gR/pD/MH+oP8n/3v/PrAH56SeWH/dDlxSD+O+/IZzJU5v/LA/nX6PEr/N9cdP6e4ziBkziF0ziDbjiMa7iOG7iJW7iN7uiBO7iLe7iv7+6JXniIR3iMJ3iKZ+iNPkhAIixBMoS+6McwI4wyGZOjPw5xFAbgCAayMquwKquxOmtgEGuyFmuzDuuyHuuzAQZjCBuyERuzCZuyGZvrfw5jC7ZkK7ZmG7bFcIzg+/yAH/MTfsrPcBTHcBbPqauHXdmN7/I9fsiPOAYrORrrkQaa8FG4aSvBgJI2EBYjnSUiUwMHZJoslI9lUeCgLJYt8r1slV1yXHYHuskeOSLn5GjgsByT03JNzshZ6S7n5JLckctyRXqKLzflodwK9Jbb8lheyJNAH3kqryRBXssb6Ssx7jmG1cRAf7EA00sKyeDgkJoxMEoySSHJKYUdDFCLODiiFpWyUkrKORiolpcqUlmqOhikVpO6UlPqSX0Ag9UG0kwaSnNp4a54tpR27jHbSwcAw9WO8n7w2gfyYfD4I/lUPpbP5HMAR9UvpLN7zC4ORqpDHIxShzsYrU6VaQDGqEtkKYBx6pNAf4l1cFaNc/BcjRfr9oVySE6A76q5JDfAD9UqDiaoux1MVM87mKpedDAd8CAEOEitLXUADlC7Si+A3dVnov3sq76QGPffTGbJAmCOmkNyAZin5hEPwEI1v4MlajWpDmCp2tDBcvUXByvUGQ7HqDMdrFRny3wAq9QFDkerCx2sV5c52KCuEz2HjWqSTQA2A/kzOdj6B09lNjIAKgCdAIAAigB4ANQAZABOAFoAhwBgAFYANAI8ALwAxAAAABT+YAAUApsAIAMhAAsEOgAUBI0AEAWwABQGGAAVAaYAEQbAAA4AAAAAeAFdjgUOE0EUhmeoW0IUqc1UkZk0LsQqu8Wh3nm4W4wD4E7tLP9Gt9Eep4fAVvCR5+/LD6bOIzUwDucbcvn393hXdFKRmzc0uBLCfmyB39I4oMBPSI2IEn1E6v2RqZJYiMXZewvRF49u30O0HnivcX9BLQE2No89OzESbcr/Du8TndKI+phogFmQB3gSAAIflFpfNWLqvECkMTBDg1dWHm2L8lIKG7uBwc7KSyKN+G+Nnn/++HCoNqEQP6GRDAljg3YejBaLMKtKvFos8osq/c53/+YuZ/8X2n8XEKnbLn81CDqvqjLvF6qyKj2FZGmk1PmxsT2JkjTSCjVbI6NQ91xWOU3+SSzGZttmUXbXTbJPE7Nltcj+KeVR9eDik3uQ/a6Rh8gptD+5gl0xTp1Z+S2rR/YW6R+/xokBAAABAAIACAAC//8AD3gBjHoHeBPHFu45s0WSC15JlmWqLQtLdAOybEhPXqhphBvqvfSSZzqG0LvB2DTTYgyhpoFNAsumAgnYN/QW0et1ICHd6Y1ijd/MykZap3wvXzyjmS3zn39OnQUkGAogNJFUEEAGC8RAHIzXYhSr1dZejVFUCPBW1luL3sYGQIUOvVWSVn8XafBQH30AbADKQ300kQB7UpNCnSnUmfVuV1TMr1pMaCZW71Si7KoT82vrNi6X1SVYEa0ouNCPLqFJ8AFyIIN+T/dgzE0iUIokGJTUO69KpuBMMvmulUwJ9if980h/ILC56jecrksQA2l/AS6aDaI5OFmKat7bdan+r300lAkD0LoNugWfkJ7RNiFeTvHgv7fG/vdo5qh27UZl4kui486bLR98sO/99wOBPNFG3DKAyDiqC6qQppEoQRchTTUFVEFRzQH2NsFt90m8QUejsbgE6/BWmkLX4fd5vAECkwHEswxtfUiCghDaGAYwpgatwgYKG4TlUKoH9digHpejYQwHP0NtmJaogVAjkyoG1IZ8r3gbHWBia+bwxWhFrRPgrS2gmhU1Xr8rIaCCoibqM404fhfD7va77C725xP4n8/h1v/cApslQXqrW0G3H9DSgVJs2L2gO5q7L+9+4ssON+52W74RzR3oLVxHh+O6fBy8GDfTgfxvMd2YT4cTNw4GQBhT1Vq0yuuhOQwPSW9hYllqBE5hgxQuI0mxcHotihoT4K3CW82O9wQiilY3PEpR1KQAbz281Zreu8KESvd4PR5/ekam3+dISHC40z3uFNkRnyCyQbxscrj97LIvPsHXNkPoPXft+Y/2b31x2973c7Mnz1qAbbY/e/y91XvO7l6Zm1OIk/8zy/fo6S2vnom/es1ZcXLp69PHDJ86ZPLGEcWn7Pv3W788tLhwFkiQVfWtlCMdhFioBx5Ih3YwJSSrwMQTamR1s4Gbycq1JyqgRqVpVrEaNp/TEsMjt6I2DLD9Zj+0ZuHphorW5t5I87t1jfSnaZmCm//KTGvdxp6e4Wub4GCCulM8fqcupd+f7mEMYHpGsn4lOfIC50byojNra86C17bOnVeyqHfXTr16ru5J7t+K8rattJLPdO7Zq0unPtSURQ5niUU5JdvzOs3funWx6elhg3t0eXr48O6Vp3OKty3ulFO8dbH8zLAhPbo+M3TIc788JmY/BgIMq6oQf5EOQCPwgg8W/IUeNGCDBjWKn8gGiVwpUhpwpdCaWRrwTkhpxjulWQrvrKFJe+iWuqEuwVqXE9FA0ZLwHk+uJKuuWoy8sJpwojK5mnC6uFqYMIMphcnp9sqMusZS20w0ca0R4p2ZGRkhooa98Nqgxw5sKzzQZ+xIfPzxrdMD5YO6Hn7+PKV4cdU0usG1dW3KpEmPtx36ZPeBuDBLfWHS8k6vf7BzQe8Xuz9DZ87bVLXt9oTHOnz6xDgsTpw+b9Iy4fOBy//VutdD/6fPWEB4XnRBUPc5SsjjSNUeh4HlPibomIsvSivocvwEEBbQZuRFeSRYwQJqnTRV1DffZst0ykQwKfYEp8njJQum/jjXs3KvBZf2eMGzYGoFeeZT3IzPdZw2jqbTz3rQWfRmycDxXXfgcwAIHvbOzFrvxHhCTN4Mm92fTog3M8FmI5kv/DTfu24v6b1hsHf+D5NJh0/o8/T1LuMn4U+YlnwGs7BRt/FdaAkdCggNyCChh6RCHUgO7bvIdlfU9z1QlwWSRNXCektaIlsqNVNi7jnVKdlNguDFrvRMK2xlWRuFTVvRk4dm7Hl7pnCx75px2Ju+Mqbo3/Sn/phMv/w3R/40rBTTxXchGuoBe5kKuvuQMWxfurtzuKxuK3N2Vh/ZiIV0xB46Agv3CLE7aTqe2InFgNCQlmM6XAUzOPmbNPFeEOEvBc6yV3ct8XJuVn/xnSG0vHPO4q0rhh3jOFJJEokl74LAOGQ7p2GkY2ILk1iaiF+RpDWAsJzFsUlwmnFdP8SMiTFj0p2hFH4qk0crBw9Xy9tn339/dvtBrR95pHWrhx4CBFtVjqDokdAODFpkKGRPOt3o27WJDNw4U24JQGACs8IoZoWxbL32oRWj2M1R7Oaws+I2GKVoVjR4pkgpFOJOIYJfsfna2uxe3S5MVt2dZIpR5RVfXxfLv/u2XNg9v2DZPJK/OH+BQEbTvfQA+tH3Bz6K7ehZeij224sXyumlihvnbgJCCQC5LL0Hcg0uiUGR/pxsgMQNQkzThLB1E4FPspzCbZX8qT5yeQ9dTGwNxdP52w4DIPQDEH1Maic8BcaAa3i3MyLSBDRBcfKVFEWzhOcVHps0h1MJrefyY41fYDGmse5GEF2ir7Ij3hrXY9GERWt3o3D5eAVLa6aRqwtI69mbemSv3LDk6K3zuy7Si7QPIPSvqhBuM3SemogRywDF1qCrywZ1OTqI1f0apGkfA/bTNgGO19L4rwGA2WqsQdNj9cwNFM0TJsnuAf58XUVtEGCtlhS5oT4mhhKSosYZ8kgpJjcORUkupNeNuYtzCqumFOwOfnTqm+kjpuRUAR1Oq/YUzspdtn7VYqEtyc1GyB//5udX/jtAa+FRZx/4ovzdCYuW5MzOI0DADyB2Y7oaBXWgizEChN0ClxUtIseKzAGGhWJZDvIsRzPL0XpCqd/EwTvcukmjD11Wk5B77NieYBZZcjA4Fw8m4Ndr6A7sPlr4qbI9OdYEENYxG2jJUDSEQSEMyJZFhiFMPrcAVDQxzJ4pFjkiU5pWLzwpmeqxSc62NcB3ID4M1sSjN/MTduZvBEapzRFPWDT2+hKq2XSnmEynupJvgm+1GJl3+JtfrpT9at1pXT5p7qpN86d2aEOukAvb6YSH6e3rN2jwwoczZ6svrdzlbwIE5jP8DaRdEA8u5vPCKlxbAr7/GCkBVEvgiFQUrUGkHjjcsmi6Bxf8fgVSBWbcjholEJ5JuVQF8RMO7/vst1OnaSX2wn+dGbA56eWpMwtWSLs2iLduzKe/nrtBf8ZHg51wJRZLwXHZPR9/+9r7LxbuBmQWCGIqY1+GtkY7D28Fxy4pkQYO1QaO6OYeVEwNvvZf0qeyQrgkdb7zvpRYBCDAOMZLHd3KXdC8Zm8d7IUO9vawsnH98locnAsvsyUv9ovcUqGel+tWnFffWUukmagORUuJJCtkJKEsKyKTEHimpfOFes7ZNoPRVjFhcPaCqsCZ4NzsQeMqykq/W/PSnTWrcuatpt+MXrigfMEiMX10Ses2H0z+8PqNDybta9O6ZNT7ly5Vbpm2rujWsgKx3sKJY/Pzy5cAEBhaVSXc0uVsDL0hXO7USGlnAzuXUrBzO+FpBAj6L7tBRQ1OXY2u5RF4BqRLxLXB6lBAcvuZl0hlLt5fk00LD923ZeCsvcPHnsi7dJuq9M3G3s9/p9/329B449RpqwvInA7PzbiRt/KbGfRD+nUG7UWnSuvFL+9kP9f13Zt7175YBlVVkMsi4GjxcfCA7XdAE4tnfwgTQInwhIk8kLE7m7Ko3IPd6WX3fCJMQBmUGAAlIsvW7wSEzvCRME3sCjIkROgYu8r8up5LoeRAPzrQTLIrTzG3NT94AKevxGkHOL9FWCBcET4GAUyQCsxgWOKgkxhp3ZpYK6rzlEK4UrlPeIz/Ca22BEs3AyDkwgHhmvhEGIsenDkWKaBKHIuOxC/UD44UelaWkEUo7KO5K+mCUiDwRNVvwiS214nggmf/InYls0Ey3+v6UthY6itchUUF/jZ+QSh+seCVmXkvfmWEPL+Jpbzh8ngYaftUznNjsobP2E0+e/fDsy+P7lJWXS2vm7zouYUDRmdNHvXvlw8f37WzZNSzRfSj6vIZCIyg98sXpDXgh8fg/4LaNpSbmBlis14BBbS4tmYOMS5Nk8xx/JdZ0dqTsL0F1LaKVj88wUrWZgG1WZrmDs/FKdojJFJvmd/y6sqbmWHjEjkFmeclNnCliMQk20Q+cuoJPrHbbCxoizaU9dwl086ZkI/FXHpnrz9jcddlK+1xU/dnPTunW7p91fglsp3uptpReuTt6Jjl6D3d950HUh86mXWHFr0VE1OOM364jUN33P25zrO9HxjbGFu1e+SFtfj7z/SrbT3+9dXJ11BY3fzh4IUvr7+NC7DoMM37/RZdVdbCPcHb9gZuxfpox/d+uE770uXLioYPsOAfDb/nLDYAkBpKKpggCjrWzp5rHxfIbCBzdbCIRPdfkVqrRemToZIffehmvXAyuDH/EGmxjbQ8GHwKf7iFM+h8dujSjdQjxSBAMYCYp2fuCZAEPQzxsnb2BHqEdKZpceElzXE8ieKRSAkrIRpdjc/qCmccshvZkCUjrlRXKE66ivHadz9MHDopn35FD+ODuS/RT2kppsxas6SA3pTUA6XDNzR37Z5z4DopDv66eBqa1s0aNWU0AMJkFhEuSQcYhx2MftKY67ITkrgAd4A2g3OsGzliSRNXLtGdDFZ/OtcacLo9TF0Iq6ZteuJ7qT698T2l9OgKjNr5FSY6y+puLXz/9CFt8/YGeOrLu5iNGUuOY/prNPj5jvX0x7tLv6NfrXgbiM7yIcZyNDig/T9wzJmLCaNirMbW4lG0OVnkFk2ClXltVtoTbzG+tA8bb8JN9PKBs8fK//j6gqRuo8eO9jtFj71OJNvdxRhf1eMW2gkA6kg66kiehrBG/Sk/ixZlvq3RBqcoKoZsTdHMBhdpdTmq/4TrwXzyv8ohwqpgSzKZbAlWbpDUjbRF9fppbH0LPPIPuq5ZiBhW74j1ZeOK7ur1TgQ3lAq5wfvIEJITnMnXqgMI05h2XGPakQSD/7+04+/qIa1RKLo2Sns7rlFSI9Lv7YcbPcM6rWEEmlRZ5A7H61eA7ZLTTVwpRKjWHB46xGtd6R+qRivWEPRhwk1MSCrNoOVlh/H6/lEv++lOouwfkbUV04/Pxi444usL6KI/0arJv9FPWrfHTutD3Elmfe96GPfOUOYZFMqwqyrwqoGTusmC2VqaBftFbKheXXFKfaz1SeayYEppKSkvY9s3QFKDy0g215/3WDNZr0Yb/sORsf4uH04uLZVU/pSfVUAn2M84aGXMZ8PBm+Nj4KRIA+CpvzWUfvlCxacQXXb39OWfS/PnTV6Fknr39umK8iMzlxQuhGp+JJ2ficbMM1x411Y041kyEJ6FPmLtCn1hBEyDRbAOSmAPmPtp7YGRJUuEX7dnyB3lnvJweZKcKxfKr8vvypZ+DKtJJw99iG5SX2PkLfwq+BEZ8QV5bTeNZxS2JoHgzMqz1VbQgCGVoMk/WQFE6hfXdB+OIFrl0rINzJ6qJZa76967j5FXw9YYlMAQo8Mn1Xw5BFE/4A91URCqvizEx+SyoxvtrMcteA2v3S610ZRV1G0vZXvwH/FVFk4yydC7w8Si4KbgUY4trK0WeFLDKG5Axk0JA6mtPQbz1IgEOiq944qFnGYMqai7rIx8sl8cfHcjA7JWfB4ITKqqkCzM6q2QBO2N9baRiFglslASaxVK8aTantNDGYTDq5+JmHSTtmVKluX0lvoG/X0VWYnRb+zE6OX7A3vfPS2c3b3nhECKL9CybcXY/lTWGXxsezHdf56ggA767e8j79IbGBeE6qhQqlfLdnhKi4rXS5YonsBBmILahZMWLeCfXbMQjm0cPaeIeSFW37uro6zXhVmlpO4PGEf/+IMWY591r75aQNeT+4IsLv169NznG1bkz1svAIHRVVGSzPhzQApDZXY3DuVtat1qVFYGxGrYP45KMFv5fVZDVGXZXrKRU5NkSpX/jtdkRivmTkUxh57s3O0etyrjtvTkvndOC6dxIuf2LP2454mpv9ru8VtCy84j+8/J+b1Dr1fzuw1APKpbhxMGaVKifrwi8S8k/2B0hgpbU0JplmJIs6J1y+Aak2AMR9WkyyZ0uLGGd7KflpThp7+jZVUO9jwVHIPeguItRfQKeSr4lqRev5B3rG2wMIZ8s3rGwuUIgNCNxa1sfl7EUIO3CVvL4O6NH45UmR+ZsFarE0boqaeHb4+hHKzHP6ew1ljj8hKQbcSfvqFw7a9xu+ke0vOPG2i/Vvjt3LJta5dtWoMjTw6hFV8WUuaMPnql6OVCkt/p46I3bkw8MXX+mplj+0wfPv3VsbvOTzgye/7aGRde4FK1ARDX6HluK6M4RvplxRDyA9XE8gi6hrbYT1uKwyXbne8l20ZAWMKYKmHvtMEDmmSPZzIb3aDhBMoQa7Q6BnORwWRKAS9z36FzEKtYgrTqmu8HepPs27HllTcltTLlFL2jECSfCtcrPRt37tgoXAVAnr+LQf28o50GJl7vGBM8g9MzujZAQfdpqXqy7iPs69qZ4M2S4Oenq8Rdd7qF/OiDAPJ3uox9DG7B6EANphnOB2oUOo4N4nQfL0RxbyqHuli9YwQ4M9HHGjvH4TVxMPhZg6aY/DLWbZL0aRndtJOeczrp0Z10cykeL31TuFVpVg8IN+90E1PHjr17leFDaA8gntLj70gjBWE8tZ2w8UgcUOTx1ZILhfA6vAsiC7nVU/nyWrlY3i2zKQFkjt0iQwi7HnD1/31kPvb7lKbjxZt0HS36DC9R3w1hHmkVbBVMIe2CR0g5OcM5jWNI9zKkZmhjRBrGY0AaBhdajwdCHxmGM67QqFIadY2cJ1crxwZvkCRhBX9/TwBxmh77Hoe/Tz4ifYoI3NHwcwcpPGmRTGwyFPv9/AzCge2FR+9eExpV/iD8sWHDcnHexqV8vZX0CImW54AJUoAhVk2182YhUttZ+ORZM4nev58uxKnSV7enFJne5+9pwr41tKv51kDSIm2JPci1o4lKBqqSeptnMRZ6BHP0VVP1uzFNJZH4VTQm7HZ+hsKSCQtOo7llZfKcW52L5Dy+7iPkshCv25DXYENhVQ9oaOLGwheRuFOornBL9r2BzWdjs+3iXtqIXAw2BQSxKksoAgAB6ke8pnZCJfHznKLKUcLqNWuAa694Ca9IFARwg4q8yMV+9z5foRI6WXo7jiQRwpM9vvyVTZR+wh7zgB43K4RvxKehETSBqZqzaTO9WFbU5Opo42QgnIm19d9QYROnnnlF845HePZ4ZK1ti3ZWx50kw7GeOzKH93h5vsx9uu/edwv94MdpjXc69NM9dzI/2muiRM19a/NJxK/fnjh+SO6eCQcn7T0nemh0r/XuFfSNicndc99ZXLy3x6AJQzs9u6b33ldpnRd7K0v7di4/3GswEN33JssAdaAuDNVs9epzbDZFFQLAvFI4s0w0er1a5xiSWdCTzRjeqTG1S3SnMX1gJz8mnmNnJNusXi6dycrdtZh8s/TkOEvJ7nG46Mbulfnvdevx9oLVxHqLnl0xU4bgR4vpBRqUPjxVQluUnAKE/7C9qmB71RC6aEqjJLZ0xNFbYu3cBiIzGiYfP2SLZ60RHqfWV4dBBKu/mnG3R98AxjZ5aMhq805p0sEx/6N3J15e/e5P5p3mgqylL63LmdK337ah6EVI2vh73pUdWQuPl7r3HuMaNYCh/FEGiIN6jOHE+g04RYkhhuU0w6moIZE3opeEGJ1hveMM2//2s589neW2TsavmysRCf0DgkwrF2JAxf59Y3eXWMYe+uC73UW56rP/eiOviHhuY9o8kn4HJuZh+i3T+4GN+NPaMxx7P4b9F8awg3GcpZl1jjl7LPcKw0usbQD1zMDvq5f29v56H9cj/WodhigRH7tCd5qNOZiUAv57J9quhITQSSCmyCaX3+MhT12jFdP/N/fsN0G3+NaiwXm+8Xn08rgiG2lkzotH188pW4IF9BsafGrzwW6P9T4tHHtlVZ2lLwHCAwDkmOxg0gzR4hK4FUZI0ShSwRMjQ3Ft+TjfaEiPYyOdpWoPML3i5zzsJF7/1OA0hRSIfwD7cvv2PSWPPByV5u87+Msvhe0FY3fssxZasgZnF1T2AAIDaU/hZ8Z4XWgMOVpKqofzk8KTQzDAC9tfYmT9a+ODGjcV0hsup/b/uHsP8CiO5H24umdmV1mbFwSKC1qSESjawiByjiYbBJIJJgsRDrCQwRiTBAibIJJE8JGxEWPSioyJ4mxEOM5gnI/D2RecpW193T0rNL3Ahef7PekvPTubd7t7qqqr3nqrNtzJQjcRHlHt/DlmniIFYYp7RJjSfAG8O03jojC5SqsVq6yvz17MCdzz242Zn7bKmrV/cVHOmVPflK1bfOC5gXsXU/nyoqbLZ1d+euOfowfnrF6/LHM+SvzX0etb0Peb+D6+HED6xABgpnocZLHy82JKEFB4wevjd8LonbDacJ/tWUF6M5OaFMMiXa67PKRHnfIuoMGSB43PeX5JvMcjHS0i+d4U/KeZU7N6VzE2Bwa2DY9TznO+WhvVEBpGP5m55kjPrHtEHnANScigCDCMjr420OO5rOHxcjqKfqpNm+effRZw9WnSAw2l3xcCDmbDnHV4mMK4ffAE00tPsA6wo4aAwe/2BNWk6B1hU2ycO0VzgSUmgdogepD7rZNjktu0s6alpNKxpMrpld3IZcuagA795eMoulkGHxYgtg5yiAHouGbqgiymIqLWPxmDCeAYiz0d/FGYcgii/qDv6UchmIuGoFoQJk1zCstmeDyjUL/PyDB0+w76aQ5ZaICqkbPQaPKsdxkg2AyABhrAD82Keiyaxc6EAdgcCwAMs/nuMUuVuWUTNewJBk5Qt5p52+gdW82devROPe6lB/AEuMKvSgMEcL0O836czDik+iRVo2ewG644doXSlVnlXzyX+tYf0GiDZ0L+i0uCyx4c6eCR02cvf7t3FlnsbYrLZ0zPG+dNxBe+3VT1tZxeo0t0VmborwZbrOKsxIkIm/ijEQZzz5k1CNZrldNfrVArw9zLOrWS05ds1qsVHRRgGEa9jGQ6qnCoBx3UkPqRPg6rVR/D+2+AqlVwfuuKjDC6dMAYctQUQQ1Hji/hsPxPCj9C5jmfvXGP/FC2a/mKnXuWL92N3VvIMvI+CS2pXI4SqwIP3f3okvrRXeYBkSw5io8tAqaoVm1/tjL8RtBBXRQqrJzFPxxUQkRf6DE7tegLMVFnkiA6Q1Gfn72Q69kTmHvl3S88m5fsHtB/32vF2PwLuZHv/UW5O3s5uUt+l4/eWuutXHOT+xkkS/rBN4+Jop/xH3YOLuQWYfX9PY7/6G6kMXjxEXfj6wtncgKoQ1d2/itP8Ws7Bg/ZvqgEx1ejxq9M/j0ey7NRy6qAsltvYEvhnzXZxUV0BqHQWZXDWKZRB/gLg/XbEbj/jHURV7CPh8CX07e8TlzUpOWRdp5D0rBdqfWlNcZNXpDT818PA8R9tONyb47VBGpYjXC6BeKjKtWvIcCGUhxeUGtJQCPrm0pjK+hRbSCSXhvUcBD8Ga88l69xTyScSx7s6PPZgWP3y155Ycy0Cci+v/+XngWXcz1KwbTx81B0j/7PDpjR97Vjp9b0nDKkS4eObQbNGfz6geE7sjInD2RxXfW3eJDSFuwwUg1zOEVEo46ehFDnUU6NRqBjoZ8ksFAC9FNldBoLs2Nm5tnw027nYQvzfMxocXl5aruYp7t1mvvyhQtKW/J7oTe7XbuQdbZ1y/CWQmQABEvout+jJsJErRXFMESMTBiWuN3oCdka6Qo/xgdoyAbD0SAmkFRApUaTrr91GHku3+rsKZ0478oFfMbb6ecSyVp5EQBBLIBUJqc/HgMSRK7OIxiQImBAlF0ZcpLMXUFmn6yUMiovMiuIoCmAcpPeDIEsVQkN8/98Ub5FyX9y6AXBEt9ktKugYN84OAbEhmK1JsndKzzkwjryWzWsIxeP/blqbbXUqvKilFz1Jzm96rbUBBA0BpDK6diCob8wKB3qU+ffoz5BMoek+NUj6I6VbeSSxNAd9MvfPyAlaPLt33//C5pMSm7jA6jA+5X3I7SWTMQu7AQEDtJDKqWjCadeEZjM/iul8wCF08KcIwhjuq8nUwDTU20M2OV2pzgZhYCO4/uqi6TXmHuuTokjxsc1Ji+Xo3CpaWU0+acUuk7uOWaK3BwQDAGQ3qEjETGgOv8HGFA6nlO1Aw/0HpKSi4qWSHU3vMoxFPIGLjG0hjrQUrXWjeAzD02guqgjhkUbWRZLqo2iDPzDOQqckuxKSUxJSWURk5myRCiL3OLEsw++c+sWPvBO/PVdu6T3yRuJ909c+tfr/6w4+lnS9A7kb+VfDH3+/vvku/ZsBAcoJ6zjE5mqiPlQHdeuJf80nGKvttLxTvONV9HGyyCPOpQxH8y9WTMdr5mO11I7XsVi5uN1plKmchods4nGFQ6aEU+yx7Et3Wi9ajx8+Hr8QRXdunX4QGU7FHTvwYDnvrqKIjpMT/zMc+OH1/9VfuLzRPb9r6I35B+kOHBCe9XMcwNQ68g4OOZUGs4DfVuC3paF+9uyYCYizAI3x8wiG7l9djipsKTIPxxf2nX+nu5Neg/Ydqyg5/LStpE9R0qBJXdS1jSYOAJvfb/ttiA8YyRgKCDr0Vi5F48fEnXxA1QwaE1QaaHkBTNtYdCc1WVlrjqLG/bufljxgvdXfqv09EUNiNYwBFMmajzEwnMqxLnYnGu90Dr+wLGxQg99BHHow8ZsNzvWYUe1nj8AYtBqLzAVJwuvzRBQkO6jKQpiuLjK887l8oOedWcMGgiy6dU5Q1++EvHV13Go/j3XLRQZ+/knzlvraqAQBMMAZBZdxcJctb7/uB+B9qNtPK6LTlBHRtM8d2E0ylVPR6NM/WwE+iGr9gmo0NS9NJrRAR4/Q+S0GWONsYwml5bipluVJOzFlAqKzga0wR+hyl97NUrEATu2Bv50+dTHp+fljF8QiDLwlHsbhxUXB76aFfBRMZIvfX/r4MS5G/NJVTEApufmvjJM/gfUgyaQoeKmzbR9qdRdAeL+ZapgMS4WUECKRbn99i+30Z0WT7XEncZ9mDSnkXG/nEZkczgSOamZc6HkPluuX9uyaEHBuKmrF6wueff8lrULi6aMLVxYlTX9/Ofnc3MvTM09P33qwgVLFq/YXP7+m0VL1s2es37pxjevnt+yagnOy7v1Ut7NvJduzpl9i2lVNIBMkyXgqMkBOOiwHUISs76/vxhulZqqEOKgEz4Ubo224sxSKxM2elQtWEcPZvpoZEc1DNfKZQXH5Bnv317D/ef/KAmPRZM+JCPQ02Q+mk/mnyWLGPKMniEj7klheLu3Rf6OueQUaj93Rz6uYOdgNbVgvbgFM0IdZsOERJWqIKkp1TXqEDDXcHVZWRk1+c6qr6TL+GfA8Dwxy3OolCZDR5ivujp1phNiVT4ptYgoLw9iH+UI4NU8DpOaoaO5OzJ8MFkYFUgBcWnh4ky6FiY1rfbByLQW/CuYkPAqIiFC0AjezJGJT0l7yPFujqlM+JJ+cq0X6ZCjcEOKHWu3nVw+5DllnbqSqr9OvdK5oOzQ5iU7V14/cibzSPsuKPjjL5Hs2V2wctvTi1H0ntx072fP9+jbI/U1VL9Z7wEF6MDJgS2XjN596elnct/DC4pmZg0d36ZFzqacsiH04Z2XP38vf9P0Fzr1bde3a/Yr++rUs47p1Llv++fMtjGdhkxm52Gs/Hf8g3IBKMgHkYyhqauWYNlOo0nTAh7PaRhFw5obY33sxbe1a2UYJSxS69fUZwRBgmG0kutvynmuac/AWtWd3oqThZnMsWOqT+Oa05PVvEZaU+mdVO7DpzbXSLeHwqVoCWeqQc1TeeI+4RAEmYLoA2FBEi9ewkLg8/CeWo9n3UpTaXa8tuyrOdVgWX/6uD8sOvs+knZDm4Xy9i2U/NXAxSiPNJMeQxPpPsaCPPKtkuKTpzdt3f/GyGEjJk0aMTzTi7YiK2qLLFtLyHfbtpJvt0w/jnqg+aj78UPk8MUL5PARPHDDtptHppTe/OPaUQOX5eXOXjZgzML95MOdO1HD/XtR3K4d5N7ecvT8pUtkZ/kFsvv6NTSEawx+Rwrna9kQJqlh8W42szDGjRfp2aocb9fqOlguB8t2nujgV2zXt1OVrt3mzcHscU7JkPSJjhj9AtUkOlJZooOtjltbK5rm0LIcTJbxhBBDz/mzFuzaP2lupz7b9i99bWME+WPTIfWn9h+Kz8bFD5r7Ys7s5MWpSSEvLihcRM5n98trVG8lykgaQfnIY6FIGi29A/FQ+jsBI5SijtUEEMxDs6RTUgwoEMGzbaiCGjaRHcfcHU4YPlXmzZMy0CwUsA1keJ5K3n26WmEQBcnQGvaoqW24yqcyN4IdrfzoEhkgfhCZVagorFdbLBjDfXjKGVbjNMZaHJXJOFMclcmUmDhfHeHpFJR5CFJMKfTR6FqhbBSdwt9rKk2oKE1IYAWXrbEuVheFLM3GaLa1Mqgws8vJxcwbc9pd8cnueLc7SSuecT3vL27TqUBu3YZsxcXkWy6Q6MwKZNuwZ/5LyPx6mGSaXrq565Deo5fhO34yd4nJ5B4Ut38fimUy+RN5W+r3an5eu8SNrQfFmxp4zFnyfNw+tVtrAASzlVipPbfnZuDFJpLI6Zbae1NxuRJbCBgWSGfwXHpugsEBCeLys3LVkAQ1EAt8G2F1uOhxnXXWwEk2x4K1E8atXj1u/Lrq1O7dU9N69JDPjNu8afyEdescXZ5J79FnUnfAkA0g/ST/C4IhHDqzajQxog40Pa7OrTRU4HsoYQa2eQYr9RScKdbA8YK0pWgSWbOLzEOv7ELtqk5KHaRBReQFVFKEiitD17OVao834X3KcXDAADWAo8lQGyoJBC0b272wUEgV5tC0Xg2ofTyMV/LYHMyR5YuNauuoWImqLRzH4n3ePajZ5LbP9uhSvAsFbJw4oBQV4k2TUMTYTi1b93xm2pp5U8ZN7PM6IGiDC/FGpQziYaka424kjk8opWLjg7phWinVkRyYB4UgZaoZgHKPhEM0JICklVSxARtxLXk6rK6PyRxfq1E2XlOlRmqfV5eaID0VXdtSxaoqnxQ8rKpyu1DggO5dMzo/06P4zblLN3duv3bvkoU7S/p06Nxt8xB5TOsWT6UnNX4hb864tGF1GxdOyH954lPPPpuUy9m6efIHuH5NThrTnDRGmRrAcohNBWcyB1GiOWqJl1ayyP3ZT8mPaxVC7rL3b6TI3vdyOligrxoq8GN0MK4Ql3JgxOJPg5J15CdjqHZGzQ6O1mnJQo5Fov7oxRmX2pTtCszcu7ofBXS9i9/cvF6Kqbw4fXE30lS5Cwg6AEhtOeetqYqDQ8RM2iOUcwQBGunPTI0Oc1lizXjRgL+RX1DQ31AoDiC3/1z9e18209V4IpojdYNAcKiSj22IEw4G0HF/UO8eV9GaEsvVWoklvsNqLBMyqGDADNIL7QWWy26nKuEmcZ1MfqDtIavBZaDGE3GI4qDR9xWlSEMLYjURcGvuVhqKDNmwtdDYZ3DbF2KS672RnTsxOaFZk8BFjJ+Mt6MfeEVkWxUx1OiJhZE2sTAS+xdGst3GSAsj0Q/FH6BRFrwdD31m/kwATL9Dldw8TxRBv0XSsF2JuU+iiVOD6kmaF6OaJCEDL/mZucdWlxtfOrFx04nj5E+n3swe0H9kdv9+WVgeVfLu2Z3dt5w7t8Mwetr0Mb1HTZuSDXxfXS/Nlg5DPBwMBTDCQTQB2OMDAZTXlbfADReqP8Tr6bWK6kAAMsJlfBsATOLy8JqhvgDKFf4eFb6FAP7e23g9MsJFKYq/R+CA8ffkACjfKcf55xfx91yWGCRghEvQEm+qeU8sfU8sfw9g6EjmSbNpfF4H4mCwGqixIgNZ1QDLONa+nsXnYIrlSNZ/qs8pjaW7tz77FiYZjdqqJhk054ZV7/C4PoWJL+6JGmcdC8YzJo/O9+DPjp6/vXVye1+1Dt49Yd4fzo5qOHl67rBtf7ryzlsHcnu/gVpTr/epZjxj+E8A42DOwbbALJGB92TKuGo2gIbFPJH6rwaDr1ZAyNYL+5PFAL56WilWcrHtycovKFYyDq5aEe7903ufS1Olo95eNtzbe8yBz/5+AF2ORtlki1K6njQu8n6HZuOPAMFQeF/6SB4FwfA0r58PDJF8hQJBgdzrlqVAdoWCZJ+kKxWqUQ7iL9KwGitCaQg5ETIiNBR1J8dmoW6o2yxyDHWfRQ6Tw/ReX9QnjxzkB1Kah/qRAwASZRa/SSt1vgUnxEBjGKvKTZpyjWTeLjvGV4gFXOJKRpg4vuliVzxmq8cpJJECQbMB+yA13p+IzGgvafG8LoVnTIwOq2JzsiQFNirJbuSopSTvezV75apTjDd7e82LK7YsxVXNXsDJY3dSarJkf9r74bA5D/nJz216cAaN688YtPk7qo+Tu6N+XCEtyaEk2tAjr1YVtmU0Wgw7AeRMKjeh4GCSz30DrXmHyLUUfVQEwb4CX5N2y0TPlcAMEwmYsYlatMr8FqvZx51FWci5+t4s8usX5PuyMmRfuXUrrVUiH44/9/K5B+QSvdnB+3HR7LwixLKyNFM4wWCBJpRvEtu0mWhNo4TSSf9tJsjKkd8wxapl8PT1ojHacy7+HIONGokVEzUbv90Whe01VAdt62ehtuYgmFFHz7WyQxfm9zgx6OqRfofjm7ZcnDIxt/vJwQXjhtyVB1d8886W/KudkkauWtJzi9qs/qaYZiOeS85avazf0GsDRkwkH4IEvau/NcyVe9P5pUBruKhiHjkwB6B5BTs+8zieWSS9EynSDvzRMhzJXZwQxcmzjpR6E3IthHoWTpFvE8LZIBHai9P5VWk6fXH6tXS6F8YKmt8Q1YYV2iubVrB8ZoJgB1OpLioxboMujIuvjeOcnMVj11g8aRSTrg3qHJzQwwCK70nlknafr9h14ouPPpkybvzyY/88Pr00MePt8Te+9DYyvr12zZyEtiVVgV1LEv86c/kEqe/0tWYcsch2aNCIt4qK3x44MW9KP2vh4f79+wwm1V9NLz3dM3rJnHXdU7/DU/r3ypSS9xVEL1wNgOFlVlFuaAaR0JT6x8ZmT2k4fWmjCqh1PKP8ExvhdY2+6kczv6XG6RBHUZCQhULu+opcZzzD75gsUeROcnOszhf+S8m/zfxg0eJ7c6Zee+XNOS1W3O12ZuHRZ344cLLbOBxbMPz17bvm529Q7ORX8mJmiXfVK58uWv3Vgmnvrlgz6tVhLbekFrwyuupfT7fudnrX8vOfH2N2rQvsl5+Sy+itUHBCb9WoMeWNPPIwMsDXr80F6/EU4nN7Dhpq/Z+DppoHHdoNX5iFHvpe5oe35KeqIqS/ebdqzph2xEOOoXTulbVpU0V4C4yMDA2xeYmyAI5xNlk85WDJPAIolZkRZUeXyAbwYyS4dG1iXDLfeDm6K+vRXbVuvXDu4zPGZg1PgJtaMz8x3AJbNaNr8Nnc1JRheZ8VThnRbe7Yd+d+umrcoO5zR7/nyUaD23RdthuPHUz2p7Uv2EUJBN6CJmve20jOlJClrrVX16K0czn4SMzdw0dyvH3rfugBDGspl8D9GK5fiD+b8v+eQWB+hEHg5gwCT+65xxAIjFu95Qv9GQSRAAqrIrWCEybq0iiPlInYeBkwy6iYbPwW8538qJSlEu9dpXD43Vj7sJOTpUwcpA9nPa9qO0PQC0scJ5l9Aa+CFy1ixUH0iD86W/UC/ogy/laurAJWzCbDShRHPkZx3pXnAMEmxgGS0/04QHWewAEqK9MyshsB5AyekR0nit5/yXMqxbyrl4HW4hkoHnPacI2FFAn0tlrNDkhX1YsMPh+fn60kjdp0emJZ2TC04hPyLPryK/QeSZLTSSoq9/7Le5ONLw5Arsd37WFiPzIxB4xCuO+G+FlAQn2nREenr4LX+qHxtiMcrOK4e0O7wkswjSlpdGDjkZH8xgrU6LpLPQbkD/BeK8avN8lvgrf7xoSDDADB0F3XmSbqkd4gctC/GxM1SRW+Skbeni3Nzoga2gAmlZSUrVpVJo1pndfa68BvpuWl4c8BwXbSQ/4Hl8/nVYPN/vg6kUfdNosfY7BU1vvyamgYr8O3hPlS1ZzpyImOKSm+IjX5H/s2t04Na9h6iTeJFgS+R5nz3t1llo1hFV3kCZXraNHaenkcW5vXSQ/p73R3j4BsNZRp/39kX/HFs/h300J1tDBOTxwXuSU+9pjDqRsup5BxUlZa6Iyr7xzDuzbRUbvaL83JP9CPSvzGtyuuVv34x2OW4tBz+JeC+a9V3aKyj2Fc9TfGQN6pwgWvq6hBQ37iTKURFYLQ6Vbx39b6lYaJPgeEcX8sQbUJ7oXjSS0uQvTuNIs22IaK3eZkC7PlD8uTFY1kxDsaGQOrStVp28lyVEC2z90rdWYVy6x6uXJ57tjJk946h9+1r0Ph+1DKfmQustEi5mJvVb0weWX4/Wvk0s1v2O6UXf2tEei5i4FmkAzrVENKqi97G1/Bji2E3UkgRgikW73Pxs6lMYj7XC35VWnLBDVMbwx1THnVpr0ygl/xIEKfDCp96uGG5nDyY41b5eT+6qNMuIY+Byt7zocrl15p3e781GtfexONf1x0Ynb3pT8tfi+jzaVF98ivnq0FS7duW7Z4u/zUqHUOHLYUu7eSpTNHj51Ovpmx98KklxdOHT0qF7UggUc/+Mv7R+7cvv3msoj8dUzetwLgBQY7z3ZLPNst0kVFIRH0jhGkU2vI0XbzVlS6vdUAZ6Oko/Lbe07ZVwZ/VJnlY6ArFi6b0TBMhZhYvqNW/Lv+UIoWsSsJfkE7CFKmiElhhTUMiE1hVYxG6rKlJtH7DCZ305AsliW9PeQLclb68cePdhS0TnCUfImao9Gbyde79nwcXnXtpg0NRZ1mGhFG9dMjCkOHkMXk4IAL5PSREqR8GHf3r4Cq/0p64BN0raIgV7VFx9Ah6nIrUXrrJbr9IsGFdxYUM+BB+imynGN4BcvERAhpjFozkZrCiekP195oT8JZV3dvbJ0YFtWhXZd9+/CBba0GOOKf3SdflfZVkl1HLatDxw2X5cLZu07YVwe9+xIAZn0ClWJDGjihIfSnaSG3z5OLq/g3xbpqeKjMfWnOWg7VnwEmHHFPrtxlqcwkk+JwGvX1u2b5Vx4sk5/XIhYr/31TVuYu8ls2OnXtJC/iPX1Vi5F3ozbXRt9A7fZvMr66kLzTev/PMsLIUVPIG4FQDUu1TGZZbxedk1Wzg1ZmB0XNF9v3GGSrz06EVIhRJ5tTrD9r1TcVo8OfvKrpLHNFry3p0nbdtW7UF/2Y/MOza0XBrj0Fy3ZzB3RZwOj55KOkZXsc1AlFSZWUx/qhx3T47l3Q6igNkQYMEdBTDdHtPhY6VItQcVrfHxpGoRE+ox/AToxYEmtnI7ZRQ2vAj9RXTs/ecvAc+vFmN12N5Z+Dl66+cT3E+/IlUuWQxVJLzvlTwuVVUBeyVCOvN4InUBEFP+yRiNcewNfdzqBz1cDvaBxrsfUTA7YFGqC9DU5RwldvLZVryYAdO0bKqw6tlquO61mBr2JX10mAqg+RHmiMnA6h0EgE3gUfQ7BtSNA3NGbv+lbJTL26Usr95L2qplGrWX29/FfJYAAIgGSt5o86RjQtYIw2UkdSkVnAWbdUYbVrND+A6LVs4ska/gzvBEZDmhRrkmTYsG7thp+nyt8H7d0bgkxcHuQv8M9KNQRATG2G81A4ikb0s0FGfMUq6PIy/yvJLrmklCR0Zt1WkltZrAzcG0S+R5YgQPCKfBV/oPwFQiBeDeRWnoN24RLKVANrs5jcEaZKwNc95mHuBH+wg/y4s6hnt859lL/MWb1mduc+vbuwGgP5ezROOUdHV0fFgcxZ9KMI6GgBK3wsgME1lRMwRz6E3Ya+EAg2aKJKdp67krQeyJJvGdUMI8rkD/IA2FLD8OL0KoWPjuscds8dNjwv71geOdyhZYuOHVomtlfmD575h/0vvTQooWP7Fzp1ZquZSPqgN+BpMEFzlYJJvioVwYlTlYcw+5FwU7QpwSRlslQCjfn5Nu3rQIZeTs/t3SI5tPPzQ19clPfUsEFdI+Y0Gzdo6MantWzRHamN8iU4oQ2fCj9Dh8IDogMwnwzvH8wkPVxA+G2196h5dYpsNg7GRGGOO7TJG9742eym9Runz52T6Xo6Kym66TPKvUmLbG1CM1oaJy63pVs6PgUYRsgVUjOlmrNoWjHo4EkpK7br8CZZD6MhNkwjfdJYk8+SkiQXzrxG/rVn8oW765Rqch0lkOsckyET0Z+rD/N8bTKbb9tgkExSjNRCaispmVqnk7aBLQLbBvYNzAqUqeAGoky2y0kmXmbl1CVtKT+mxvd5eXT3Li9kdev5wuDkzi1auBom/rNzdlaXzpkjOrno3QaJyYC8I+Q7ZI1hBoTxWnYq0IAyueTQL2QamGDMMMqZdEoq0uisoeDTOncqk5w0Xzta7wzUo/OwHsa1G3v3QvKdDUpUb/eEFwe27htM5dz7NNlOrNV/gABfn1GjTsCVGgH3Pq1J+E+agLM8ynZcIK+Q4qAznLkDPd9ryx5bhQuUK9pjC2Hs2LZMXrLklmi2wQoBEKsGBAaJUVEUE8pAnz/EYgZO7EtORWETMqVj2QZr13mrl8wYexkQtJAdqIsBhM/R+3Iq8EaO+r6qBsOG8ZnSUZQtO7ouWLVqwehLgKABuY9awWEIgCjf5/yn5qwrxg+TPKPI/W7z3vjD6DHldJ7j5Jb4OJ1TPOwJYLmlPagDzy09KzvwIgPQx/eGsMf3ogxgUtSA3MSj4We+xi18NWSM6qhQa2B59Ls1qSqVmWXQjcMpDugjeizLJje7Lt3g+eOkm2359UQqtQiWYSeOk64yNJ1mnMN9FvFgUG2eUujtvCxn+LBpU0Zk5kjy4KmTMxsOnpIzBBBMgg04RjoMBparUqjpMyo1XYQZNsAaZUYhvILcQe4VOJ5MRwut6DWePVmPw7T3cbmVjMCtH1tTZGe87wfITe6sRJgQ6TDJs5I8tBIVAqJ6PEWaoMSBBIHsnfyr0tzI+eY4fGncFNYCmq1yKl6Fjys7JJqxA8CrwCpm3/iigY7P2ZhGS7E8i6LDUR8BKRrX5SBF4wQVdGxAAZuoASaYejfm5LDGvvq2I+H2aHuCXcrUUwnrspQNT+frmz+ywMnCgjaGWvpTPflFYGOxgNIZK9nJQamW8ynt3SlvLzY8pH0a0HCyR0b90e2ONdzPTvlL8o/WkD+P5i8BhbEmDam+/vEuiKfrclAH5osOmB97Uux7aQpx+lA1zls+FG6LtuFMNrEGCQzyrJPgk2ObgA1GV1AIlVc28+ax9RMoBkppRKz7vMyDoXCkp981ZhiMGu/k9T3uwIiHXVrtHI9DPjwuhV4YHscubpeSlBLbMMmNUlzK4E/o3zlylrxw5g79O4P6ocLTVdmoVfZdbPsTuUV6zpqFPx0n7V+/Zj1rpcwu9CaWvVVYrqpYs2bN+iNVD7Yw/d1FPVeJrlw0NILtqkuruncxzFqgn+oWsMb7iqJ3ovw5z2JNXpRJJECryqMBkxpr4x5EbIK+dD2qpre7QyTmIl+1i9NX7ULp0i6NOuVM4theTSdehdASGFcy6tZ57suFtgeXrnjQnPLvbIVl5ZUvnCkoWLyQRli6opijJ7H3qlJ65ggykN/JGyuK1q/EVB93V38bwHpHx0MqMKs3WB7Ir5+hh8Z81VzghqbQAlIgHY5C7cLU15ck+jeUEiIAsZ7GZqrHAV6ftDFpSq1gMifTuwLK6+Yy15TDeTame0zmGnEitiiciWyZKYbB+ETJpij28cmMpaY+E+Xrcun7TQMjbWshuSR+4QpLH7Wy57j0pcWyi9XldKY1ZAeU5HYb5cWo/6Sz09eWJXxF/jnjwBKycMWBmeTn+wlHXp9+ZgoatGTbF6hB2iHy0o408quUsaMZ+c0zNKRxdNVXgw2RjVDHTKfTKd1C90iD9efWkyj0ObvQm+wRdK+q/Bz7IzubqBcdzjNv4fr9cnKAVQ4CKCU8LqgHo3WC+m/rRQUoUs8NVsw1sAXoY3o1nPNgSsPZrkAFjFeKupluIoaU03QavaICiMsO7JY9Y3LISQ9a6kFtcl9EHrzjLTn97GnyJuo5bzaqGkmDj4sURD8+82V8wNv73HnOThrJ+xSfBxcsVu085hV1TjRNrkAH103BigcKVhxYJMy0N5wdmVWKpvY7Ojo6IVrK1FGvmH2P5lxJhx9BvxbWAslngSxQU0dv5ARxqR+ZLx/aMWOsbfbsX8kXBpX+BaHIf01YbJs85Y8HDWgeY4vjyHdvxG2NQg1RyNyl+ciAoqO3u66eyF8KMrPWygmqPXUhClzQCI6J3QXFPsfB+kSf2qAR4ghdgjq1AeWjQQNTg5gGUqau9Ri3G/TpSPZ0pCkyJpJNvfbp2ApmaqbGolw1JlasaYjhBObIGle6PifLN+BZkwZsTdkjFvYCvjkwqai10yncBNldTiM9GGKRm64UW69EFEs7dKIdZy7SP1z34Dep374r4XP3J5LlqKPsnYzXZnj3oqH7vZW4+4ASsps1FJNaFI0o+nHh1KLEZkU/o6PJI4qGovuDmMQ0AZB+pSsXAWPFDV/c0uoKeBtilkMbcqnkZxzYVK3cEoclCNB8oI936KKzMlIz62ItudxsN49Noz1S6EEq/7at+Urz9ZafP0TffeH9Hv2Wv9nuPdkcW1v8TB4kSMWKpd/MEvWQ93wIHp+PJg4vORVQAghiqr+XI+gcomCF2BBNBBmsZkUDr2lExXqmghNl6mdVt8LntDhZUwwtoeLXv9lewdQhlM/Qwowgm6cisBOiFLPWmZIF9AbOFGGpkBR6YVXwdqOdXsypFnOKHIFXkV8O9J30I/07U0n/Tl2RpNE3yKWdFvx8jpqzgV7QUFI9XZ2+gV68H2NkQoFDfN31v6HWygnDVahTV9Rz/9o+cTsVay2DuAUAgQkSwt02O/O5HGDmtUMsK2nALNywAHWrcfUDpHhwyWpP4RbskZDxE4+UG0tWkLtHL3+ClBhvMi6PJT99cPECikST464A5hoq8SqUaJgspiLEhKmB1yizNJwiCJzB15jhUHhQNKP06wZs48/a6bMmdmpDxF63gu+jteBjalTbDa6KHDx9jf7hul8jC/ntn9TE9iEH0fObtu8uJJQVTb5D1pKlxfjO91f//AAtRfFvLJ9XjADBblwgfSMxD7yeLk/pYBAc8mM1f8MovrigiHe6GYkGww8MydHFVJpjd6it3FfGmTVR1cMg5sL4rvhgn21dJ88b3nPYO6Ctp/Qe739SF15VA7RePwFs/v9THxSepXosG4WL0v/fDiksQ1u+b9+1k1P3Refnzhr/0Ue4W1kZ7ZQy/HB5682JEyeOKKximV7ez0X6is7HAcN1QGeUWOIu7l/iMC3+rXCNgoNsYCZJqyLXhuZ6iJxTprzUYm7Pyw8eePbtQ2cOjkFNPcoo242JdGx0qH9461jr3xsBINgir0TrDK0gAELoGLVTJgTiTSe2kjwDDK36j8pZsqDXW8AYpfTwg2QHA6ToyE8O/xaSsoIeoZKWYsZdFWmknESKoD0A3ifFPJ4b7vBPotgFbrjNHsa5kGG2x1PE2Zf+99zwxzLDq3/CG+no4iFXHJb46xoaJXwu6+Z1ZD6sgq0gZfozwMFYwwDHIgPcj/qtRsazLMz/CQMcXf03DHDM/HZ8XLI/8osajn/zixr4Mb+oEWzw/0UNKkSxbkQjDrMR9504sZgsNaA528jCT8yo6YI9e8ZiA3Gg2PqAoJBanmAp7om/dyMFexfiuczeSFAit8VTDNNA4h07pold/msgsgxjH+NIYw6DyHhXtSMZuA8eiSWfKWpr1nj6GdAHRgJj8AcIqGEo9QCMeiZVXaOelG90GUVk7+FJQgdP3pu2YHTXjqOyO3cdPTCpgYsDfIZpx/7SOXtEty7DKcaX2LJBfGJydXXNr/xgA5g5UtQQQP4r589Gwtj/7hdsrsmIcjrYYYuMcnXrxmpoQeh1pviltErr+8ycvuk3baDHiJ6s6ze1dpe2b9e1/u5C/nbl41/QV7c/RRF4YxGeV9sDHG8kErL8lsl6gJPo/7fmgoD+SawHU12YANTREvJtgv8hMpESmD8Wzg52E8dM7EIAjypUbKpp8xoioER1tJ6kYj8bzcDTABTPJQ+EdlF793pQXfkGuS80jZJvFBUV6bqihkNPHSfmkU6R4UGYh3JiX0fOgzIwT0To7FTh4wrxBU/hfaOlvQ9O377NmqeSZg+ktKorUloR6lhSQk4Aqv6R9vuYqrSFSJguNEvQ7eBibw8haEM+DF8FBWXqx2EWFi6A+0yKj3jH3F/0/zV2FeBx3Ep4dN7TnYOGMzc5s8PwHEOYmZMyM1zytYFXZmbm1hSnjD6XufUXfFRmZmau69snjeRZ7WkLHyS2/N9/o9nRrDSSZpRhYA6QvIA8IHW9uUA+/bQ3G8hrr+l8IA9fnerUwQ+25OqHL2bcdVUlhci4ULW0bxaBWWwMq4eYP9lvsl9UFKcMQB/JniA0jYZkfx+6ntBNsD2AeyA30eWEbofNbILFPcAx0Lyb0An4VXAXpHFnOz90lMj4KfFfSp9oY8vYdOsTA/gPaKzeJ65Qn4AIiGt1rFy0H52aJSsoiPYabD+WPef+LNqxTkBkmmgfqnQJ3WwGxMx7A6QdG30kOy8APcCHnkHoJrgiAJ3FTXSE0AnYJNAFaegcTzvuOwJ3KkozUsnu3kz8FMNKhrU0HQCh5Qb6SKgjNF2PSXKFdj8VaJRdo5vcaQHcUa7QLwn0PpEIoRPuGk92QvcRsseU7CprOlrOP7TldLMJtt615WCuc7TKWm3xK1ijRtNBimRZNBh9JHs3AF3uQzcSugk+D0JzE11J6Hb4mE2y0BWm3LyH0AlWIrgL0tA1Qi9jtF4w0zOO1vG6p8Np/JHPTMZQdht9JHuY0HSoIZnnQ9cTugk2BXAXcAPNuwmdgB+80UroIiF7hZYdsw2jNJO1NOcQP6VESPbV0mAe2XBKoGfrkfcigEbT4f7ksEwLrbkPDEAPN9EcNJpD0+EBWGYyf0HY9oRjYUf4sJtJigS0AEBBGnoM+6FjvNQJSbIHfaINfoS+1idGCC3W+z6xD34CPZho/FK075maJXO5iva52oNNRQ+GGUhRM/O1HjeTZuiAbjKOmrHRR7IdA9ClJpoDolGPewdgmcm8mZgTcBHpxkNXCd2M0v5LppQ6JCxHxwXIPutC1+dhJD6sJbkKINRgYI8scX2+S2K5wrpPC6zYl1dY9F3Vrs0cZQr9qEDPDm8idMLdWaAL0tB9GfkulUEQLWaFspj9HEuWPMWu8vqhvlfqpyOk871PJXpQZjD6SLZ3AHqwieaAaHw6hwZgfXJ8Qdj2Ax0LG/dhN5MUCbjGe5KErhAaGaE1glnKUO7ddC+3ktx07zaZg3Lb6CPZzoSmNVQy10RzQDT2cl+bGbVNzJuJOQGXeJITulBIXqYlxzxaKMteWpYSAJ/PIskJvVmjOSR2Ina8ByCxBYK91JyN8K9o/rIGtrIpkJtWlqHfG8bIDz9InmjN6ihizctOwzQWmSMDiLkFfmANFnN/H/MrihnR1wKzuIcLNFbqSi3FSl35UASHBGx10L4h6chXYkUe84lkmPPm7GfkxUpxik/X1co1bqPkx3oLIvoPATXgDUrxT+ib0Mhq7zjQrWerQl8bRY0vWd+LDgddspqtlyW/fk+EbsU85amlmKd8JDTAJX+Wmpz2Ant/GSp+GZqD+6JqJdAZcgr+RsLyoSKNYYZ5tHGUL315rZm46M/Tl6fposbLZl45MBKUzbzMU9A5Oq95pHp2UGJzT1/f6BTnrqvqi0V2UrNjHAVb2C4Q8+/3JOP6zY1ZxXHMzNXoWhozahVK7xDi3oW4m+CZIG5ucHNAbhztkwOYmclcRMyt7K4A5grHlLoLmRW6JEDqShYsdTN8xHa1uMv+QOrmlcxiLtfMWCMNZ9ZDNHMrm2nNkko0s9h7DA/nIaiGeYh+KuOFcK74ufMbmfIrHpdxCvGP/GntvU/H346H1na+Lf+EKcGWitbOp8Xf710a3ycu4vv7Suw7olX+s5e37uC/0bpjDVzGFkCuMRMnT0Jv+QdpRrBmT/JRdBkojljNHCkm5hZ4gs20mAf6mF9BZoU+F5jFXebjdoi7la0LWFvlOubcpAu5FXoSPntrboJVN29NLcXacSVwlOX99Gl0XzbgHOsKtDpsWaxDiFR0NeTLrtfH8xX5XvJeqjGX7g99Nefme+P9+p69jPpzNLzPOwxL0eENgdShmKO+CkbCcWCfEMFXruwErRrwLgIec46SkJ3DcvAE9DBxGXbY08OEMQ32upNjnk3vrFLIYv8N7yoeqU3rU7Wdxr43iX3Gh3PXM6+X+7+W+tGX0j7VpRPaP3Z4PXV69e4OK/u6zExvH9qgktsHrMeb4TY207KZbB48923+J0u3GBrTWIEPvcVw7eO22Z6I1pCYwR6ZFyoftxNY88caH/NoYm6B79mukOtn7ijXowKZcQwt1OhTaAwRd0eNRBN3EXG3spsCpK5xDKlxDC3U6Fqw5R7RK3ePK2sSKm4QfottTLVR3y8nlk1sOOzql1DPcihKgE9shNbrtzTKqdYMRVBwXh6ZLtCLNHoQmw6ZICYfHTHF6D4AEDouMooiFe3uJDbHioJEVJ/dZoHeN/yZWhsguhxCVp8jTKHvF+hT+G/EvcadQp7UO1MU1pI0CfTB4fuRW6ErgfvQhQb6C4GeGSkm7hZ3FZtpcUc0+jmBHhp+GbkVejmAxa3RUJjalR0T7lDcwGHDR5mCozu1lB2KT3Cxat0usbcJvjMjDsnRCoMC4kJ9tc08IN5evwpPimhZESs0EiTLhWIevQArfy3G9iXsW2yvExZ5WqROsI9ST5CdwOo0O11iTMY4sstbB6HxaO3XK7Rb675irSNytCy39rjhMPZytLbIK9AiLxSW2g9H41Ldno3tG2TtQhx5Y3S8rJqNtWKbUT0nktfnx2HccZlGF7KrfJYyGFeoJIusi4jc6jtX43fu0uPKPP3Igu1uN7arOopJLYvEv+h0QZY/FoPM0qru5CFABkTuHM4VP3fGo3KqIP65Nx4dHRWzhLujYsYwOjpVlI7ufDvK1t2/T/SI6MnRjHX3Ph19WwKWRuXkQX5iaXSfqJw8SIpvBJTmDWYfWtmjPZu1BG0clATY3thzP43lcRTxO5L9yOp9HpWi1rTGTuEaW6H3CPA2MU+fsgaj4kZ9PoN6u6DHlbn+FQu212K7kqWeZGlmeazBehMMNP0KB1rvNx/PLEnyKZogsQ7J/ZS7bzgPuNyxMSKC31BEcA18yqZBri8iqGc5tBJ/kFbtaw6m2RZt/QzSWGSOZBFzC8tn4y3mch/zK8iMaGHBzOKO+7gbiHsjWxUQx6yO/iBut5n8LvFvhE8CYgjlmT90DNafwCqGaB/1+omfErDzUOzZR+g5tI+dFRruB/C9uyR/lraPW3pcWSFRcaMdHIB2sLLHlfn0kQXb3Z+xXclST7I0QxtrsGQZpO3jACHLfzkgC9rHy8ySJIcpLNY8ROYG3csLWaNleUN1LzHrPvZyF41eTr3UqfclOtPkbiTuJrg6iJsb3ByQG2chewQwM82cWiwrNSKzij22AkiO1GxZFUBxYPte7i8S3+MSXun7SNTrPj0u4Wk8BkjeDHey8Zbkw/9A8ua1LF1yiu6OFZJcjU++UX/jwfiNmT2uzP0v2ndV7bAZ28eKnhIee3QJgMSnFoeuNfDHwtfYjvua+DwbteTtAZ6kv5IcKw58wY8F+lZ2Zfg8isyXU6y9HZ5kE6w4fr5jRrm+oIhY+56O9daLMTOK/xUxr4EuikARc0euHOfE/CAxr9mb/A1lz8uRWJJ5ADG3wNdeBIp2d/N9zK8gs0KfD8zijvm4LyXuNraQTbf2HvI5RdoUP9+D+NvgY+hrRf5ijvY39B119B0b2Szc37D2TjqKvO9w+oVd+o6N8A76NCtuiZfL8H5h6nis21kKK8E7GbZD0LqLMjYVysQsnU6uPHnjX4F15KbV7s3mPG1BZRX3PO/063uXUEvzzSqfZVe8N3HdvmrZtN9KZt1BFdGzj5wJdK7wT9ItxcUv8az05eMf3PrTacfFBn9WDta4yfHfwy5L61Da1dTsjOe8NeFNxv1UWgJenDjIV7bCdVVlURyjE/WscjOrT5/z074X1qBA77KHRleSz6XcNMmBTKFxzwu5Jys0XBa058WN+DEHih83VREzxY9jJjPvJuYEdJF9evOlLIfsU1XjxDfoFP22OJtkodUSzbCwbgO+W/bW6LKAmH0/fLdobv4LcbeyIwK4sx2Tuwu5FTozgDubGdyReuJuhptZg8U9kBvcHJAbvf90ZjHrp6NyAeKe96mqj6HtdpSI9kcx8xiO77M0+jhAbtPkk9O0RjBLXuQkgT5d6+9Tdoov6ie5R2huzOyE2j5XoxusnR16k2uLHUcWOys0IsBiY1HDYpF7D4Vm5wfMhQbY3LqXjwTMs/Jsbo0uDhoNJjfvJu4EzvEL0uQu9vaMNf9m4k/gfmSBT3YcEx2D/mCXeRb8GrCO6IPyW/s7An0B2GMuO9NbUU41VpTN7nz3VXtnyovk8hUoyVitm2tZvbUWztaSYDU1lGS5Rt9pr2goar5DapXcg6FzLDewkwF3clKr5K4G7Q7fAFsBtZJqdx5B/GRsv8l5BAD7H5Z1YrD/2B7ewT2AtPgwafFG5wE2x9JipqlFfgayKPQCyLK0mOXzieXE3Q4XsQmWT+znmE/oC/KJ7WWOD0saV5VCnTu4tI9yOBk6YkYO6T+vATQwJk/1yX9yM2I62U6W7xScw/tjGcj+HP+MlxW474Bf/7Qq7xW95UPrsL4XlmOozatlXnUv545HVSVRWVQ09SuLPPTo76t7i4o6z3WPwnKiA2RxUcbFObnfb9GVRdXc+r/YV4z8Qw1sZxtCc1kEZkKreyBEoXP0YB3BzwFwRuOzH4bPeLt7eupktKGlPhvawE7QNrTUZ0MbYBO235razZmD+KEaPwH6yEiowH+P+Pm6nQP8H+dLiG0AeAFVyIlBAzEUA1EjafSd9F8ApbIGcr3Zw/Ja6+t6vm/3rCXJZSo7SApPEpDdC7SinPG3dkFRYg6DhDaArzJJLFdQ1LOZGNtEcjIz2RQ2QAUqt626tEoiK/ZSR5J9xMzc9zDQItDftdSC+w9Alz7xTheekvJReeozPUxQQQjjcqJ/+cSLT+XVHgI57X3miegMwgkKrPUDInsISgAAAAEAAAACAADiktOWXw889QAbCAAAAAAAxPARLgAAAADQ206a+hv91QkwCHMAAAAJAAIAAAAAAAB4AWNgZGBgz/nHw8DA6flL+p8XpwFQBAUwzgEAcBwFBXgBjZQDsCXJEoa/qsrq897atu2xbdu2bXum79iztm3btm3bu72ZEbcjTow74o+vXZWZf2ZI6U3p4f4Ck9+V8/0S5ss3jJOpDI1vM0D+oI/rQz9/N3P84xwTRnKQLKCpW87BvgxH+wNZGhqzh74/SnWlqouqq6qMar1qtqqJariqt/ueue4GjpfdqS+9WSunMDc8RqPCqQyM5fXff3FFLMO4WI0rJFUN1utRTIw3c4U/mdtkIGWi6P2mXJH8rc9uVk1nbNwJ4xDd++VyH83lUU6Pp5HGfTmosD9VolBBnmVXeZK2/lCWh/ocp/x/aE/1cDbiJ+jzjvr9FFI5jc4yi25ShS7+MSrrve7Sn9T9QIn7IrtPdlH+wNmFwCIZqO8vpZPYdynd/C3Kw5Tn8H8ZwPzwPocngRPDbxwfnmAfZXt9p7r7ieuUe8YRzNLzRdJdc30pneLNytc51H3FCvmcjrq/vkkDOoUVrAgP0FeGMi1pqPevZLz/h5lSlx7+O2qqqvqZTJL5rA9fUMvvwwqt6Wi9PzFcpLqfvlrPNkkZmicVGKZ7qV2YmP0otelg+ZM7uVQeZFHyAE3leqbKMurpvzrJ2ayK6znY/ckGGcV6acYR/niOiIu4UJ8vK1xA/0Jteri/OT/O03zdkX0cp9JHlmssS0nlJ+b7kN0cHuaKUEIaBjLD8uivYYI/gTPCo0zyf9PVd2Qq/NPVffdP+VidC5NqLHXr6K46za3hKP8y/f1bVPYP6PmNLPR9GazqoLFV0hjLWu6SNhyaLOWy/43l8kIvKiQnkspUusU3OVSO4AQZzWGxPl1iM71ezuU+aJ2H6vkiKrt/OM9ylefS/hlWs0RrdK71hnk9dlGpZC6Yv/w52c/m2S1KfWweLpY/OXtffXy98gvVq7l/N5Z5t1jmXfPnFmWeVb8Wy/2ZPap1W618TnV37tWNZT4tlvnUZDHYvzemxWXrbZHau3F/ulm8to9t0frbemyL1BxZ/2m+btM4zlHeqjxb+bXyRc3nfu6H7C/llckabgtvUmJzwnxns8L6VZpygfpuhfIKZTujn8fZYnyGs20Ny8/GlIHZ3VYPy9PGtFlj/V7KVqXsZfPHZsA2aR6yOVHMR/i/1dvqsL20+WYzxjxidcvnnM2ajWk9bz1uMVh/599uzPxflkObszbr8vrnzzbhBRqTaTB75O/mNf4PGySVPAB4ATzBAxBbWQAAwNi2bfw4ebyr7UFt27ZtY1Dbtm3btu1Rd1ksVsN/J7O2sAF7GQdxTnIecBVcwG3NncBdzT3IfcT9ySvH68E7zCf8/vzbgv8ErQW3haWEtYUdhOOFm4QXRRnRJbFe3EV8RCKXVJQMljyXxqVlpL2lZ6QfZMVk/WTn5Q75YPltRTlFF8UmxSMlVk5Q7lF+UdlUGVUNVX/VLNU2dVo9QX1fU1SzRPNN20W7VftWR3VTdKv1Fn1T/XqD0dDDsNHoNHY0bjE+MeVNfU37TN/M2FzNPMl81SKztLBcs1LrHOt2WwPbeHvOPt++2n7CMcQxy3HJaXa2dD5w8VwVXT1dM1zn3Xx3ZXdtd1f3ePdSj8TT1rPcG/D28j7zLfEb/S38VwMgMC2wNsgOlg+OCF4NZUObw1XDg8KPI5UiW6KmaOvogei7mCtWItY+Ni52OPY9/n+8U3xN/H78NyNmtEyBqc30ZUYyU5mTzJuELBFOkESVxJVk1xQvpUqdSWfSqzMVMquyweyA7LMcPxfKTcjdy/3IB/Pd8g8LwQItzPt7GVCBbuAiNMLecBJcCvfAy/ANEiM9ciOAKqNmqD+ahlaiA+gm+oCl2IMhroJb4gF4Ol6FD+Nb+COREQ8BpCppRbqRQWQmWUMOkdvkI5VSD8W0Kv1TEDzACAEFAADNNWTbtvltZHPItm3btm3btn22hjPeGwbmgs3gJHgEfoIEmA9Whq1gJzgUzoab4ElUAB1CN9EHFI4ycQlcH3PcB4/HB/B1/BaH4HRSjNQlG2lJ2oBy2peOp8voXnqFvqbfaRzLy0qzRkyxAWwyW8UOsjPsOnvHfrEwlslL8Cq8ARe8Hx/GJ/Hl/A5/wb/waJFLFBLlRFNhRG8xTiwRu8Ul8VqEiHRZTFaS9SSTveU4uVTukZfkPflKfpNBMlUVVuVVbdVcEdVLDVIz1Xp1TN1Rn1WUzq0r6Ja6kz5tipo6hpheZoxZavaYy+aVCTQptpCtaaHtbkfZhXaHPW+f2f82xRV2tRxyPdxoN90tduvdbnfJvXQBLsmP8Qv9Wr/TH/UX/d0sCRMZsgAAAAABAAABnACPABYAVAAFAAEAAAAAAA4AAAIAAhQABgABeAFdjjN7AwAYhN/a3evuZTAlW2x7im3+/VyM5zPvgCtynHFyfsMJ97DOT3lUtcrP9vrne/kF3zyv80teca3zRxIUidGT7zGWxahQY0KbAkNSVORHNDTp8omRX/4lBok8VtRbZuaDLz9Hf+qMJX0s/ElmS/nVpC8raVpR1WNITdM2DfUqdBlRkf0RwIsdJyHi8j8rFnNKFSE1AAAAeAFjYGYAg/9ZDCkMWAAAKh8B0QB4AdvAo72BQZthEyMfkzbjJn5GILmd38pAVVqAgUObYTujh7WeogiQuZ0pwsNCA8xiDnI2URUDsVjifG20JUEsVjMdJUl+EIutMNbNSBrEYp9YHmOlDGJx1KUHWEqBWJwhrmZq4iAWV1mCt5ksiMXdnOIHUcdzc1NXsg2IxSsiyMvJBmLx2RipywiCHLNJgIsd6FgF19pMCZdNBkKMxZs2iACJABHGkk0NIKJAhLF0E78MUCxfhrEUAOkaMm8AAAA=) 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,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/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,d09GRgABAAAAAEbcABAAAAAAfQwAAQABAAAAAAAAAAAAAAAAAAAAAAAAAABHUE9TAAABbAAABOQAAAv2MtQEeUdTVUIAAAZQAAAAQQAAAFCyIrRQT1MvMgAABpQAAABXAAAAYLorAUBjbWFwAAAG7AAAAI8AAADEj/6wZGN2dCAAAAd8AAAAMAAAADAX3wLxZnBnbQAAB6wAAAE/AAABvC/mTqtnYXNwAAAI7AAAAAwAAAAMAAgAE2dseWYAAAj4AAA2eQAAYlxNsqlBaGVhZAAAP3QAAAA0AAAANve2KKdoaGVhAAA/qAAAAB8AAAAkDRcHFmhtdHgAAD/IAAACPAAAA3CPSUvWbG9jYQAAQgQAAAG6AAABusPVqwRtYXhwAABDwAAAACAAAAAgAwkC3m5hbWUAAEPgAAAAtAAAAU4XNjG1cG9zdAAARJQAAAF3AAACF7VLITZwcmVwAABGDAAAAM8AAAEuQJ9pDngBpJUDrCVbE0ZX9znX1ti2bdu2bU/w89nm1di2bdu2jXjqfWO7V1ajUru2Otk4QCD5qIRbqUqtRoT2aj+oDynwApjhwNN34fbsPKAPobrrDjggvbggAz21cOiHFyjoKeIpwkH3sHvRve4pxWVnojPdve7MdZY7e53zrq+bzL3r5nDzuTXcfm6iJ587Wa5U/lMuekp5hHv9Ge568okijyiFQ0F8CCSITGQhK9nITh7yUkDxQhSmKMUpQSlKU4bq1KExzWlBK9rwCZ/yGZ/zBV/yNd/wLd/xM7/yG7/zB3+SyFKWs4GNbGYLh/BSnBhKkI5SJCVR5iXs3j4iZGqZyX6nKNFUsq1UsSNUldVkDdnADtNIz8Z2mmZ2geZ2llbyE7X5VH4mP5dfyC/lCNUYKUfJ0XKMHCvHq8YEOVFOkpPlLNWeLefIuXKeXKg+FsnFcolcqr6Wy1XK36SxbpUOLWzxg/tsXJoSxlcWgw9FlVPcTlLCLlHKtpAovYruU/SyIptJlH6ay0K13Upva8e/rYNal2OcjWGB/Y2XYGIoR6SyjtOOaBQhXJEQRS4qEvag51P4ktuuUEzGyjgZLxNkAD4kI1AGk1Ets6lVSjaQjI1ys9wig6iicVaV1WQN2UiOlxPkRDlJTparpIfqRNGUGFpIH8IsgQiZWm6SW6VGpMxiMlbGyXiZID1ksBk0tasa+REcgrWbjua9k1ACbC+aMyG2RGONorqd1Ey3KvsMmr9WKUGrtEHZP2iV5miVZrPN5uFQXa21FgShu/bK9V7HCz4/+M4nBcnA9ltfW25z7ZKNs3G89bp3io+47JSdtbHvkX+Ct+dcfK7+Bdtpf+h+/o1trsvLQPQzsat2+pW5F3jvS5U0lhdi522PtbA9L6zn5efGkM/y3LsGAHbD/g22Tyv213N1GtoduwmSRzWG2go7BIS/cix/ameH20SbZFOJQFgyAFto4y3STgLhds2m2LIn+dtsB9i2JxWyA9hJ9fuNXeLF+uvtiB0DCWES6wxgl+WMN6zPWQDCnu6j/sUmGs+LuV1spo2wdRZrE4gkiiiLfNTvJRtgJ9RHpMZ/WqP4FIBQVAv5Qp3L2hFe3GM7/qa/5BWxg2/Iv/NsW7UG7Bzvdb0p326+Inb0PesfeLf56q+7BkDEK/LaAQBJXldHI9X96Q6+dVSX3m8mGhvy7ZdDbXSCE0YEqcn86BTP/eQUL0oxdIZTEp3iVKIyVahGTepRnwY0RCc6LWlF61ee4rHEEU8CiYxgJKMYzRjGMp4JTGQSk5nJLGYzh7nMYynLHp34m9CZz1YO4ZKfMOEQIRxSC4fMwiWL8JBVeMkmfMgtfMkj/Mgr/CkgvBQUARQVgRQTvhQXQZQQwZQUIZQSoZQWYVQS4VQWEVQRkVQTUdQU0WjmujcQMTQUETQWSWguktJSJKOVSEprkZyvhYdv+A4ffhZefuVP3WPRaUeiCGUEYwlnvIhkApOJYqaIZhbziGGpSMoyEcFykZRNwmGrcDgkfHDkP4WQhQ3EQBDE9pmZ+m/pK4ovGh2DLW8Y/0wRrZ3sTlWy/Ut6kPnlj7St3vzVJ3/zxZ878t9iVrSeNZdng1ty+3Z0tRvzw/zamDuNWXr9V2Q8vEZPedSbe/UNmH3D1uu4Sr5k7uHPvuMCT5oZE7a0fYJ4AWNgZGBg4GKQY9BhYHRx8wlh4GBgYQCC///BMow5memJQDEGCA8oxwKmOYBYCESDxa4xMDH4MDACoScANIcG1QAAAHgBY2BmWcj4hYGVgYF1FqsxAwOjPIRmvsiQxsTAwADEUPCAgel9AINCNJCpAOK75+enAyne/385kv5eZWDgSGLSVmBgnO/PyMDAYsW6gUEBCJkA3C8QGAB4AWNgYGACYmYgFgGSjGCahWEDkNZgUACyOBh4GeoYTjCcZPjPaMgYzHSM6RbTHQURBSkFOQUlBSsFF4UShTVKQv//A3XwAnUsAKo8BVQZBFUprCChIANUaYlQ+f/r/8f/DzEI/T/4f8L/gr///r7+++rBlgcbH2x4sPbB9Ad9D+IfaNw7DHQLkQAAN6c0ewAAKgDDAJIAmACHAGgAjACqAAAAFf5gABUEOgAVBbAAFQSNABADIQALBhgAFQAAAAB4AV2OBc4bMRCF7f4UlCoohmyFE1sRQ0WB3ZTbcDxlJlEPUOaGzvJWuBHmODlEaaFsGJ5PD0ydR7RnHM5X5PLv7/Eu40R3bt7Q4EoI+7EFfkvjkAKvSY0dJbrYKXYHJk9iJmZn781EVzy6fQ+7xcB7jfszagiwoXns2ZGRaFLqd3if6JTGro/ZDTAz8gBPAkDgg1Ljq8aeOi+wU+qZvsErK4WmRSkphY1Nz2BjpSSRxv5vjZ5//vh4qPZAYb+mEQkJQ4NmCoxmszDLS7yazVKzPP3ON//mLmf/F5p/F7BTtF3+qhd0XuVlyi/kZV56CsnSiKrzQ2N7EiVpxBSO2hpxhWOeSyinzD+J2dCsm2yX3XUj7NPIrNnRne1TSiHvwcUn9zD7XSMPkVRofnIFu2KcY8xKrdmxna1F+gexEIitAAABAAIACAAC//8AD3gBfFcFfBu5sx5pyWkuyW5iO0md15yzzboUqilQZmZmTCllZpcZjvnKTGs3x8x851duj5mZIcob2fGL3T/499uJZyWP5ht9+kYBCncDkB2SCQIoUAImdB5m0iJHkKa2GR5xRHRECzqy2aD5sCuOd4aHiEy19DKTFBWXEF1za7rXTXb8jB/ytfDCX/2+AsC4HcRUOkRuCCIkQUE0roChBGtdXAs6Fu4IqkljoU0ljDEVDBo1WZVzLpE2aCTlT3oD+xYNj90KQLwTc3ZALmyMxk7BcCmYcz0AzDmUnBLJNLmoum1y32Q6OqTQZP5CKQqKAl/UecXxy3CThM1kNWipf4OumRo2U1RTDZupqpkeNi2qmRs2bWFTUc2csGkPm0Q1s8MmVU0HT1oX9Azd64w8bsHNH5seedBm6PTEh72O9PqcSOU/E63PkT4f9DnaJ/xd+bt/9zqy+MPyD8ndrJLcfT8p20P2snH82cNeup9V0lJSBvghMLm2QDTke6AFTIsiTkKQSTHEeejkccTZeUkcYLYaFEg9nCTVvCHMrcptMCNuKI/j4tbFbbBZ/RCC8hguw/B6fH6v22a323SPoefJNqs9Ex2rrNh0r2H4/W6r3d3SJ7hnrz1//tVTe08889OcCZWVM7adf/Pcg3vOfi7Sb7ZNnb2MrBg8p7Dba2cOX7Jee6fhjy+tvHnmqCFVJb1ePn3qzYznns1497K0c1kVAEgwqfZraYv0AqSAA5qCHypgEZilRWZ5UT2PYsgNdAxLlEcNYjwKajQGgw8Es+JcAwHH5qETLIgby1WDHhpXgAyPz93SbkOsep7hjeL0eqNVIP9lTHKRzEmHdu0+dGjn7sPHunfq0LV7h47daMbhnXWvenbo0ql7x47dmLCSvrRSvDNw6uSa3oETJwLthg9r37v9iBHt/3lj9amTgT5rTpwMtBsxtGOfdiNGtPujmzivGwjQpvZr8WesjxPZUAYhMK1F/0qJXHRyLXWOAx0H50dxboQfxapphKtHGVUGHf1gc6PC6GkIo0NCsYGDIdUo5n9yHFb8Uz0qpyqHT8qpyOmZI4w2c1RTC1d7tc4anqdBGhkdmshNVo7GA2MF8+opFMrXcvAt55yfJNbVj8SKVhCJpBCfz+vGL5mK0yVjQRtLLX1+osicbALyzY/jkdK22by5e7c3z+x5acqYSaSkScEL3Xs8T9l3/Qc8NvUqY+SjNsv87OFG3YpXpZYUzytzDe7coy/ZsiQ4Yuzd/U688NSmCXd17sZub3v7oC2fjfhCGltW8VnjxjpZZy+dWjwpIJwormzTK79/iW/wBAAgqGEiyZKzQISGiQpWr1h4SISYUkm57FNqBQIBVkr3y8NAQ+3D36A4IWQV/JmZqJw2NT1T0Q3QAqTsQblg41NPbiqQH2Iv035kK206mGysZG3YMSs7xtrMDAyhTcjWSC4axqy4LiZRQdFdvnTNq1KX320HjVawZx6SCzc8/UKgUH6QtKPt2PKac4MDleRlMsxKBpFXpq4ZVBNmKyIxHbSvMAF1NBWyAQPW6z3nEIpfMhe2fL8kuIX8TClDEQQX6cwueUmTlNNpRPey/31uR/D0LuH14ccWkqFs//wTw9hv00gu+7IyEr8T3Cw2Ex+EZHAAktOEiPrIJO5s8hWcNqema06vU3PT02QFW/8NW0tWfSM432N9SfA9chuP5WOfkxnwHUgggyki+HwUXGw8M+65u8v3uexl0v7FyJpdaRIdRN8AAdJ5nYKQIGi4CB1U8zNNoUnPR3X1LjTb4EsQYnsMWACwJO6xk7e4bT/99GX0N7R2ndAo0jMzAOfHN02cnKkT94fv09bvr5QLAD8UpuJ51ev0rCK6SgOc3gCn19OKL9lADWokUbkS0ldBzwNNU8HdEjRXVGu0qPKIei288y5jBN59h9Cfl8yfv3jp/PmLaAn7hF0izUgO6U0cpAW7wD7NP3vy5Fk2o/rUyQeieM4C0DcRjwS+aHYSJiRhdokFkVRTjNUkvr1gffj25dM3f2ZXqEN85awnGncAgOhB3A1hQDSuhqG06+MGs+MEg0I21x4BImqiqcGk+kF0sY1xoc8M45pOL4mpgk13GVCnJSTTKXr+KSPXFgybNz6w4msqEctn537ZcSt7XKC7j1Bp9YE+E9bvXiU/S5K+eGzlJwfYcRkI9MM9smOuzWDV/+9pGmaYlnq9hLYFMjf0Fje13Izl5ntACdyDxkxTg0pcymnYlcImJDTWkK0ZcHQO3nrRBvWETcbdrEfVuA6VHa2IuhjrtnyGTjYeWzR1zsyJK7+iMpFevcjmTVuxkH176VX2rUy/Wls1d+3ilceELgtnTJs/d5R85OMrL40+Xdyiev7Ln15+Uh6/ZNmc5Qsj/CwFEIfj/jeANOgFJknoJonXwOrVZBeho02iBmkcTDlsEq4XIUsyjQo+3p84FpvOj7aLuIlTcynCvocf/qlml0xn/1WziWySrVR5nj1BOt4mXPlnKO1Lm0d5sxb3wsB8cmFylDcEVyexVFLRSeV8JAmXnJAllfClLUX8xpYRRhu0x6VoUYM5CS4WP7Qol4xGbc5ACRJ8Pr8v3WalWOW2FIsc2wbl3kECqXmlRfO5Xd/44pfPn2a/S/TjFRPnLl42d9J4O90m5J9jt9zYlFL2x6eX2A/nn5Us0xftWbf+UPvWQGEBYukSOQMu6B+nMDE0VnSsHA0kECeUCrz7ItigIy5ra0J7xQK3tGcqRoQsNh92U8w/JhEZmLktBoMe7bO7rLB0epebg632jH3uY/bP+ffYx6T9mVGBvNsWTF8WkF5wOh7Pcnz4lOJvxb4//z77iJSSLGJH3RhW06N96dRHXn5ww7qD0f3pDCC6cX9ugKIoomQEkXw9VczkxNMLnBCUCoruT0/3oxKL7r/NJmk/p7m+evWfGuE78Vt2lRns9N13kx40+4fnAD8CjMf6NcP6ZYKOq42NrmfDJWy4Xj1P+cEsSLLxkhUklCwkOAq4oqQVOOpuIs64nGxq0JVQz7ij5o27pAixmy+WM/67KC2ZsngH++XyNfbLtqVTF/36ykt/vrFletWG9bNnbDTmjRwzc/aYUbPF4lnHCwofXvLa5cuvLXm4qMWx2c+eP//PkRkbN1TNWrWa/j1u+eJJExcvjpzFAYg3s44vfRL+t0nkS3xjCynWFA5OSSRLynVkyecXVH67ol5PpINovJ8YLr/dnoHXLW8MFxXW7i3ZMSj8I0l96SOSyi5/3XNvxxtbB5aMDNy4dsmE9UtPPfNIx46difLpNfI/7DL7kp1g37C3GjV6NCeL/NStbO2ps2c2bD4CALW10f4qDgYDNPymcCtU8R4uYw/H8WnY1+/HcReOEKGKyJDmBj5OcRwItIUhwnqhFpJw9xFg6CkFlTYXTfVqZdf/tfIcAE0d79/dG2EECYYQQBQCAgoialiVLVpbFypuAUXFWRzUvVBcrQv3nv11zxCpv9pqh6DW0Up3ta4uW6uWCra1So7/3b3wfBfR//rVcsl7+ZL73nffffs7HTFBR5D3WpvCDmUdIQb1I01myQTjoQl2MRpRl/r3hG4oVpCF83Vw+kdwei2j93o4WagRrjD/Nw7YgU6IrsgAfQGRcYCTLxUZur5kPuL/lYuuNgU1XoSa+ueEfPon+J1yrD1J7UCC+5VG3BHBHVHcEcUdlSGKO3nPyzABMdyNFOv48MTEyEXCyPp9KK85NAqGGrz6I7y65gckiwz3dgAI+xivtAIDOA3LqyxbS9V3By2ZYgWxj1KxdrMPUEhIZKJWxzrtdWqXG6lJNABmTO6TO6EgZ/pvgvDn0c+vb5z6WEvxzh24q2xeXq9VAwomDR8q2098/X7JuWGdhg3GY64xvHvgZPkLaR2wgixCI1vHWKJpbdGx3G7mDCO77O7d6Eeg+9T6IJEoXP9qW0dDeSvNbVsrcjvaUN5aC9pa0c2ZWrhMKvyhjOgmkGUyEsFkpRLVKsh0dyc2B5YQICBgIe/NBCIEGNktqHxMBISRCV+50v3qzz2L/GNX5i4ra+5/7cXJK/oKktUtLnpWmZsBf4zfwZ/i9d7NYU+YMLgiIyLr7Gi8AA/zaQ6/hPNgCdx2D3ukdEseEwlhjDkuaOZ8eO9b/PGA3n2za6oggAlxCaLjSGGvi6/CKXAHfhxvwhtxbhtLaVQsrIM2+DLywL6O+mUrO6a7GfRIcPf8hNHZAIBE7VQd8ASDAWfec3ESdiGTC5nSGsiiwiLUtMnjuEOk1kzFcI9JHoR5kz0Y+SwCsXdhGH0VKhzHp/+FzFeRz9+O7fCtL2Q4AL8u2e72RcFosiLP9wIgHmY+hxmEgGJg84/lVDxnGtpH+FMziw5T/GGx/Sx9V+NPbS1/uvSGcm/t5vGnTEK3rUG9y6yEYO1+tfpYOon3TSpILhmHhztfw/bCn2qhobiwdDW+fQN/CjstfKZ4Dj4A9dOWrFx2S7KdOD56V0TLD0s++Qptwe2eLpq+6O1Jo56aACCYSGT3GbIfW4Kuj9KLgIabbN50LDdy1C0P5CSL2U+190OAThfGG/zHkIjP1Tfgj2ByPUSwrYiu7925+a0D27bugj/KF/F1OBh6QhP0gEPxrZ/ljc/fsONrFTee28R4g67DL2Qd3IERJIOHLwGln4cGSUJdTxdyhgDi1AKL4NMYAdkLvyXzDscv4Os/X3r77Nm3JRt+Ef9xEdfgl8Wb97668d7lQzcAZDjMIDh4glxAaHWfDV1JZj/rSS1tOuz1hHmUcIAjHG+MklgeL6F9LCbnn+jtWIJ+rI8SzjpaowWoDFuPSrZKXAiAE5+ZjCY9wHwiifwfvmXsI9wJMhnuBBn3B5CRXWYPc85tcJTWCd84gtBCVOTYSOfNYvNOJnxzgfBNCMgDJG7zSAeR2NXUTWzOuYmcC5VObFq7NxloMKYVZwDIYliIk59EGoTQ8FMi1WHihc7472r8D34dZmIIYUsBXXXbuXHroZP7iteG4MvI91jOCtgbusEO5K+347Q8e+MPb+JPbT/Gt4ZtDjppKBnYmi4D3IJyT8WxGL/UbqKsmPH2vW7kQdLd4LSKMre9bogIAvLe7u0GiyvOul0mNypGuE2h989SwFg6lJAPH3RNyQJYyWiVDLWO6XV1aHWtQn/HIrSI4vwGGfYxf74lFwHn0WS/ZYX76uoIKFu35IbrwlVyYQCxLpa96kTTx3OvJq5zuRfv5Pnw7hyqq8P1Z75rABK6Pm/yyAWS7d6fZ34//7k8f/ry4ka6xjKbeygnyTXR9CbFOhNBTIUiJtZlQleZiHWo4RgPKCvqPoxRivhqEFpQ55fr6lbBkzDE8TtKxt+gmY6VhGRb0QTHkw6dul8oThJo+wjtwodgwulWsMINaHf91LqjZPMpvyPTOJQPmKOhI8f8PFG13EQvVGfduUdgdUUc7AqJkgqDxNrKgaMhs+eobTNFT+700efrUV5FO30KebG5Uc8EWtlONUbCMKgzknfwPPyXDJ+HyXX+Mu77L9xf9q8jy7JPHHm3L/wDzYL3tomF0LEaU3YHPO9P/D/xPpFcNlR9sDfKQ0VIyDvYAkWjZCRQzAmOFb5urd0QeRq30fSlk1sX8kKZEurossFEhcHnyoTDl8u1YiS69x3B9zwSWwMExpGYerP/TAzKwmQIe+FjUFIzXI7/xHfxIdgdStAT9q2tfHHfu+/uf+kjNJB8sB+OIDdl6AFH4n34L3Twt98O4jvvXP/tEFB10nkWhzCCLoBffFVBMRMFCoqJUu7Jo9qcQ5WQhel6UVXuFrihDj12C/rgmlv4Xfj4imeeWYHfRW0c30q2f05/8nfluilTqH6k9PKT+hJ6GYEFpCu4GMj0BlevUyth7YJ7K4qXwVBu5hBhkW1IDMiHUy53QO1z+HbC7IyHkG/FrwOur4fAz/Q/oGEDoWEgCAODHkFDdtGcXDTnCMq5zh4tAL0r8H4kpavGhqLpIBNRJVTz83QOvA09Zkyd91RIxN025kVT8WEYuGH50hX4HMp1PC/ZLpyZ9q+OkeWL52TMDTFb1nadMXVp5dSnJy9Q9tJwohNfko6pURM+HNWSXLSkiJtbsnyG2TXfxfFwS0N5+AN5LeLfk+CaalbRx3ANsgkVK167jf+BYVf/gGESurZtzbKynQeu38YXb/6EX5bQb+9sXLEFzhw+vX3GF6/ZfsL4bXnqqum5OZM7pl96/eA3tz6Xly0pAhAEAyCWMjs8lpcL/M4jdosEtVlJxXhgirkUP1GHnxBHE/PJKN6sVGi0nNDoFpObCZzc5HQCL2Jc1JAPCxfF+1idfOgj3sJVDXfxqbrX12+xS7b6DrXYAcVbQnV9h+07dmwXqum83gBIErOT0h6ti1Svgj5NhjuVyQPgGCjm2X0hcx7M1kRooc4DKgqUA2AuFBx3fnH8AwW4oHC0GH+3L9MPbQCQf2TPuZTjaH4+bo9y+oEPGxL9IFfbfYkSzHAPk61ylpwjE4wKyA1qmgtMS6QQLWHPpkMRHYZTpdFCH61HFGtTIrRCc6KRuj30nxUBCMOOwggIr9bgFy/iizK+cAm/VAOXIklse+9LnYfY9m5f0XTvOnueTgCIvzM9MZCzvDVYu64bu9CRCx3brjqoeDokgUJH8jwTKfoEd3emyyzq/2glwTUEZ8DP8AVcRf5dgafIVSthCwp0tHeEojDHRXQJfU7X1YvgdY3g5QZ6cnhpZn/AMhdEigqdGRClC7oCqqHAaIAYNrITG6pOLWguHAm9sa4We0NvdANV1WdjiPTC83TuIWTuaYynHgfcdA+1JewiQCzqxW0bu7vEwj/M0IinwRkTnIPu3PsFfeeIFu4ePbpNHFi5Qdk/S/FhFCSvBTrQmuaUyJS8Jc8JFaXYgdrxKOiFF/B4uE2q/ueVI7rPld8ykZxQQWNOCMVqtyP5KmUV0w008gZRM18weD0Rhy865yaANFUl8m6WjsuY0hgTKbXQ00qBl16S195pf0QeDCCIR+eEeMWP421XpZaC+eZCZJgOCp/C6Ndg1Ccv6GU9Ooe+cbSFuxMSGC5CQ6awjXnnQZr99YDpJtEo17b6ScLmDz5g3+srHkZm6TgQWX5HiRfY3yJDRTCIBYg47TQ3EguI536ZvstWkibUTqdDOh28yXA/rXTQWwwWY0Uhj6GeaEHmKuxAUC8ehqKsxkeh2AeEgGiwWcE2gGAboOcEjmscwUumaSUSSa34wOusF7ELa7zgtAz3Eq8yr71eb3mJxRXZXiO8iEdB7xAOrvFq8ELFtgBOj9h9A2RmQvMxZC8X7WKJUKJJLHRs5YNnVN+bw2mwVVE5gqeXj9DpX4WvvH3n+yNj8nJG/QZ1dZVHfm3u67iSu9H/o4mz+7XtE9lr3Jvbdr81YuDIvunyouMfVuDgrHnJb+Ym75vQPe1JgMAiQpME2R/4gGAwUKMtfbWiT8+rG16i0GSJiTelgngLhgXJdNQ9YHkGH0Vr6nz8lGBEwsWThZs7+Z+p67Q67/TFuukL+xWFBE/OWVgM/7mJL/fPXi37O17q1oPIn/pXqp/IwJ0zu5dvpTzUj/hQf4p91JiJYsfrtbKdZ0SWuhGqaWbNl47lZtcYt9XsR7Q4IgYJjeapCp5GttOHzr2AJNzwdk1DQ01lnYguzsh/trj4jQnZ8rYLMO5G2HUY/+Nb8tD5J7aEbT9G+S2H0FbgacuI5qslp57XMbyF+N/R1mhgQUdaSBWpROetTo9c8c9zLp0csspad8Y/bkPBiUt1Ty/oPSk09Kke82eiZlCAqd27oJx/fl3eKxuG3thi75IKv03J+uxltleGEtreEbOBH8E9T4O73nV7BAEdZeygWHtZEPGuS4LKSMkHZ1u7BNV0LmSXQgEhNzCTBJTJoqM8wQKmAuEQs4Xmn/pexTXQ+8x31xx5SF41b9TqzD6pp/YPm94MwTcmmGDMjTY3YCLEf18ukxY/3yFmb0IPYV/ZZClgXCmAIAoAdF6OAWYwABCWeJDuRnJhdH0qSmjIJwC9ubggrebyI0KSVbDRzapJptHE5dkXXqi0hT0RE+DbMSg7+8IFYXnFwgNHPT0Oi/KwAQsr6udSGg/APUU3xr/RYAxwRc2F4HpyofdwXgSSi0CKp54PAwby4oU8RZsm2CVRiSCw7A2LuzXFOgN+OFmw0ep/CuOb2f/uEZeyvvfSudZVw078UDdrQZ9JltBJPRfMIVyEYFpOnzX3jn/2U0z4B8Fh02ZMycwi3LT5QGYqPJ+c9flLAAJilot6sg+MVD+rvgO/CzihojXInKuh50RKgiIQw3zY9lR82KkJO/Nf/6hu7Nju08Lr6oQ3ew0494OjCG1eVJwcV/8rmZ7x9ToA4BJywXI2Gq2nd/VxkMEmqbVesraew1m2uISWLYqdoftXAKAGG+4J15Lf9SZPmcFJI43RQ5aP2xlEDvmoczRX56C2taxZHx+WMFn77outO4c08+lkSut+k858b8WBSjf3o5Ju4DBxDkMDQLAYADGF4KGn/K5OzFVO6h8d63FDSqznvw/zwCtFtbWF0Ae2wjuJbXEVnsORsn/9UriHpBTszLZR6c3Hx3ybjo8RkrJ1YvkvIM8geyMcjNY8h15r53Kblhej/DZRLsLIRRgz4vk9E0xtHTPjKLMLX/nyPAbzveL3TZi4LaLT85P/daRuxIg+T/mjuoL8HuNakeVY03vAyJHDxl7+0TEdrVk5dUB3bz8PRxZas2zGY3H1V8XOynMtBED0FPvQvcA9F/covAK7n5yjFyIXDlRR5xHNbRa/v/CVI3WF47pPbU1w25WT98k5xxD04txx6Yn1NQwZRT/FEVx8QBhIcsFGTR5TDerHW7bBfD1eIpnfTJ15HWHaSFrPaCZsm0jj+ZEEIx1RQ0uX/3xt6bJlS3/5ddnSurTUJSXpGRnpi0vS01DkrZ07d+6oNd3eQXzEuj1jRo8es8e0c0xhYeEOhuMiPJLiqNWhbIk5TuCkhwdvrPxP7RPK1+Ym7ZO4S8dz11rrPvGP21jw8eXaBfN7TQwJmdhn/jz4zw18qUuGo046/0yvvrgSO178IrMzNj+W+u/NjL54pFDvxL3/o+S7qvI9XLj4kYir0pyg/hDln7/OGnSsrtMzg5ny7zEuNHR890bl3+fJJXcjkJyaRpX/weQkeCch9auXnXsPvUPw9gbdAC82VEWkd42p6g022CjAKkbAKTSA6g71itCIdMpo5y5DO8d3HxFYd8nQdvEAvwiDMEJMSXQYxM67c/J1EoDUThfOkvkjQZnGItW7xm8EFr+pGCpMEIjZPVNYTl6U6qGKF5sdbEbu6ZsFkRf7oGbEWTA1g9NYcIenqJmL9dhCq+1DQ4kTIoQaQ1Fe09EfZ12Ha/SHJYETrYxp0JWRS46euHr4+DUS+hk7dEju4GVnjt069sVtGf0gLsrNHwsjknoEtd1a+syHlevkrJHZjz2WFRi1femGg9+ulvMHPaHICnPDdbRAygRm0E/jU1M6qIUsetcINl/YRG1cN+6BaXWTL5V4PtRMUfjFrLgcVKv5wDePHu3cwTfCJzB4UPvl2154QcrE/1Q4Xs16TCfbfYy7X0aDKqBOwW8ekR8eYmcmy3iGVrU37zloTa6m9Hq4ExGrEzGqaYVQ666xb1bV5uYNmRVa9+WeQXmXfkMrHLPWFqenCM3uHQcQhAAg/EnwcAddeCnGMS/v4iESE0etEalOtqIslINICfNI5IwrKdEZK7zTXDZ+cw8v+gIvvAcnDxmCztw73ijHwwGQqsmFASzmrAiNNqUXTdsBD5j5Is07sMBWhiedOQvSvINEyw6IL27vRWtW8nRFOsLTQbp2OppBJ7ds0FkqxxAWInU0nW40G61ikvzKNfztiasI/nQCf3vtDfn7cpgEBXjvOPrRw8PRUuzs8IDobwCBBQDhJnkOT1DM8RgnXR8VT3LXeTir9kC1PZy65WPp4EuHAWSgnwjVdCSRpmgZ5h3sIQ+TJ8rMTzdSM0IQ6IjEj6EZvw7z8Y3PPsO/wXzy3hedgE87rjku0speFIbMCu0NuKdQT3A2gWGcVNVUOel5VtNwAhWxRkrug0pIkSz8KEjQdON5kfIBwU7W2GGJNN74i798E3rgjOhdZa26hbTw6qDvkh3QBs+C7tD+FLp9L3TaPr0biTgMSx4lxgBIdBYQqihv8nvkPxKbKiWFSetRqOOa0OPo0b3om6odCn2S8Da0Xk4FrUBbQMtjQCxNiWa70doHMnC1gmadmyKjnVH4eJaHZzLBpInSo4LKF0aMGjXihcoOo/oNGjx4UL9ReFviH6+dHj/dPn3i6ddqEldbXp5/evz+mNj9Y0/Pf9lC8XgT18KBD611htTiG/jSS7hWfl/BuwXBe4YG71axNj+Ctx/FmwxaWW3Xmf0Y3uYEBV+GPlspiq/VFKqg36IgZ2he3tCcgg5HX8wfMyb/xaPfUTwn7GsXvX8SxXN1Ys1rpyeShxh/+rU/EhU8ZsAl4gUhFgSARGAzECSaqly2GfjqJxb7JTdtAXRHKva7oocjFffQaU1csC0bvD4ncUj7lAGvvr5i0Na+CYNikweh37d+mdm9fbtxT/ht+SSra4eooh6Kv1KGV8JSsTPzV6IYFVUxpqc6EFC7nBb1y5oKa01zVSn1UvBKoQrC60puxFNokCJAGJio8cU4ueUaM/GkG5iObmz0uO+xEG2ivTBV0zGQjuUtm4isKF0/LLjCuoL4+MqTQ+deQsIH6z/+6PTpjz7ecVBAlxoDLNLiMy2v/xoMIz8Pq4ZtQq583/KbLVJjoAUS7QjEiSTfEwoKwH0R4JpG0O4m8ih2i8SqZC2x2gwVLZGw0AIbe4CvhX7s62otmglX0S1oJYwXSSgcyRsDZrIvf5FiotBX9REesbHSczvdf608+5OIrhcNHDTKHS5DQ4r7b+t89KhXef7cyt/P3jxnlycULpn5e6Wy3nkNP0vZ4i1WsdoeECXPB1Uj+QLUmAe1Z6QuUik9TYxMdNpbiWa6jZVEoi+xGZvHxxGTF4mpvQ+NKXyn5+I1Kzpak+LXrVnbw1Yw0t5z/dpN1iRr7Kq19bNrXnu1pubV12ompXbJTF267tleB0YVHsreuG59Ykpq0qb1W/v8e0xBec8169G8QxhDdOgdCBqUPRQIgPg+2ft+YKqyJn7kEfy4TGIzrUFJVYm3UYi2Az3d2OQ9DfWSwWZk7Gfk61bkaqYa6VjeTHPfw5k0sJiUf6SlTvkHLegpmAW98dPQF++Go/HuOrwTFpK/YDwNGoQOaJEjofLpyps3yYBOsbV4hsivIqW/ka4F4KuM7FDZezDWLsmAvpNiK7ylYAnRsnCy/ajF+8zPP/+Ma4UW9T8LH6O/AAK5uLW4mvCqldjWs1hni+qb0t80u4c5c5Kp2tywOVWtjHexYe0dwpSuLK5Nyt4ysQO9G0Z788hYHt1kpTJXru5s1yMjTW6KvHkbzgLTyntzAgUXVw/tn9UV1/zyA/6UGLmvzp27evl7tT8P7p/VBRqv/g71JMe5ekHp0rlVt392fBLVJzwxfv7R+MdDElOegSfyVkZ1Wlnw1vFT52U4d/Lo3r2HJWW8++aw1e06rSp45dPLJ+XC5YW9Bw2K63KonUdAM9PAzkOHJxpMnn4DH+tboOyT58WfhDnOtWnFMjCwmppROrVc1VtHDH5E+YHsUon8CXNqa3HQrVviT2fOnKEZi8GkruEHqQq0JPomHsxQ+DSGLEVMI2tayYWV7juLeJ/HYkjht6hR15ZISmox1u4ZaVFaRu0GT5G8KzeKfIWeqFkgkXaTskI9ZvO6+BTO6vtwpV2H9e4ISvKfjeIgJNp27ztyZN/uchFtGjYsv7Awf9hQhzcc/OdtOBi/cvsv/OpcuAe2gZFwDy7A5/G3eBQaIG/d/eVbs974eu9mOX/gymmzn342Z+QyfAdvhROgG9TBcXg7yVknQxvui4/hKtwH2mkfAqoQfFiNWTR4i1Zf30+dUJ4tkWnqhg4hZKCKCFSz9IemXlYvs4phfaz9sp4UZQXrY/WouCJdn61HJJdyRn9Bf0NfrxfzKjz1LfSImI/6gMZ0iforzMmMaFzfDPcPI6ojrkT8EUG+BSIMEWjaQeVamHaQXodECMWEvk1lVCKbzqigkW4egmVKn1mlrzz3bPJjXZ54Acqvrl6+W98Mr7BOav5Mj5zO6KgpNjA2de7EKbOtaZlxsV7yqNK1y/Fx65Co0s5hEzLaR8coteujwAxhlrAJRIDqvy4BHaiGXRsuAQhK4EzhqBAOJNCccm25IPBZQponO/qxY5mQBWdC8TX2W86+NCTTqlwgqnzrCcygE0gGa/jMNl9j4i1y/q5Jw4MB3ibW8BtbUR1wJYDk3FqYvFlzEVmlFiTdZg1oQS+tseX+mm+F+luVNmFbdDWpvKZNSJ1FbVhCw6dGDf8qpR9+TZV+RDZ2JQ12Zdm5WoaGh7fCgK1vpianJeo8drqLWb32lHXN71NQis7xPAtTXHj6DfyW0H9ZSfKw4KCneia1zTQZTP2iErp3XZ6a+ERnpq9WSM2FfCZPDLSLievSpGuS72iLvpGa76Gyp0SwoVXSMUb/ni60d1flz1l3wugfuJ91RySF6U52ByBD08vBtwwrkQRNF1HJzqJJ27dPKtq56sk4a/fu1rgnxXcm7907efKOHZPjuz+ekNCjB5OJIxquCXWSB8HLG3SluoWL4hHF0WQXpV3ycle0l82LU6Z8eyUkI9pFl+IbvAOO/QaG1x8RsoSVJ/AMuOoEXHT3chWl41NoJ/pKOgECwRjXrgKVMm8B2ssAYLGS1Z1C34XQevFAzV5H1do2A/SQTj6CFWyqy4CkjtBXjv2wY0Yba0JqxttIfn39qp0FsxcjmI92rocg4fG27ZJSOsjj1pfO6DdzwmQZQDAKlaHrJCcdBT7URBoJ7uUy0liItFCCjoHqA10OJE/wViD1UwLJAwXTyyl0KKNDOh1q6AfZdGhQgOkzk2+Uh2qkZFQosyiiyP6LgsUHY6PSo7KjBPKVKMJK3lHBUURmXo6qiSIC8gNyq7ytZlv6to2i3w00KAHtTk0QRY1SaRsB4+H+zNTMtPh0SqPSza93T328Z8XmFYdk9Ha31Ixe3bvNE5+O7xAZ3y5UHjV71uTE4QH+I7pOnT9nqhxtjYtJSlyi2HuzST7/cWc+n+rCdJHab3RooEO2SLP5IqULeVdBE/VE3rxFPxpBB286XCYf2cD9fD6gpQACaxQw05Q+9EK45oh0XMb1bM4NJDYczOIAOeAh4XMuDuDhEizjC328XZtzNEEopkJYjBguHVMweErLusu6mFk9U0dH1JJQyqaXZqemCM3vHR8Un9AiCKdJ5xWapAEgTGU1ia01cdQHGhUQUFxwstVCAW2vsvigBTnXsAMK1+DjyA0Kn52F0t2+7Df3of5wg9BFkVNC7H1yKXYO3FBbi/r/ocxfhDPhSQLpDTowf9pNZdipLAwgcnHCZqLWl3AyS6RiGibCNM+MQa/u1qX17NY/REjw7N937Jxn28W0ay2tUuYajLbDLUQmSqAH3wf8P9j3XHewTeC82LD4cLjlwxKYjrajki1mJudmEXuknbMeNQOQFeREsL3Eg9ojdAghA033uB7p8D89p2HW4T17jhzevffIW0MG9h8yNGfAYHHmpvfe2zR986FDmweOGzdwes748TlMR08EW4VVAjE8wGd+AOjAZ3Aqu28DQLpMdHUkOA+Gom3k9XPoD4heAt+gdwEABo5aBB/lOzKQqhhsOHBr/C75zjkhmn6Hr2pk3ykm39klnWDfOcu+840wi3XNfQsMaCf9juposO8ABEbimcIXYmfWA9YDEEl9v/NL///p/JJZl5eye6xO+zaOdYPRQ03Q6yh9ct9h40f3m45+E+CfH35xfcO0pGDS+oV2r5ubm/1sTsGkXNb6dZi0fnUcPhjuvsZsKqUnSReKIkBr9mRZ0APmAndwwEsSxWjySCqMRYWZCT+CwymMwRWmuwpTBV6BQylMM1niYUarMMfB6/ApCuMtu/yOlwozESyHecCbzEVhaCzIi4hiLe5lKuwxmAEPUFiTRGFNylEwzLdp+AsA3WDJxnLJW7iqz0c1PwiiMxRkHyHAPJdOFrsnkJ2+CSCtMNpQpw3wLrTAl2vINGVgL6LueAodcslAO+gF8o/aB0b2By0k/Dy4fqE39ngHXyJ2wRXHXB/U2vGTL9p69yac00JS2rmO4fHHcAIchxZAoOwbnEr7nghdIgDdN3PhkYZ6cp/197C1bqOsNahqXGuZ0V+F6a7CVIESZR0NsguMlwozEQxvXCPZZY0avqC9HGzOdsqcDUuUOSUJNf7eGwCghTqLCjMTJCn85abCNJwjMHMZXgpMVUOagpebrMK8T2A2MrwUmIkNgQpeDIbWKUmN/ABaKzWzTN7Nf8QpC3ZBAk4WuExYoOKscFkgWjZdoL1PAlXFArUjhGABFZcjQSP9q12LdCSuL4haW4GN1S5q05bRonZtERvxyPbt91u3WmEHa966BAW0/lU0Q23hQutxR9bChfswmit9D2yfdXTus98b95nOSSul/0CXSGA6Ofe9H5xGYYIkDx4mQYWZCT+BUylMsCtMrgpTRaT0ZArTSnaBma3CHAdfwMXsd1xhQlWYieANWEzXLoTC2EIMtpbOtYOgN/hauCEuB55ExgYQx8K/QoBG2lEismMPdGykUSsjhIkQmiHUQdgbpuCqTTAZpmzCVWzAx+BTsAvssgW/zwb8/haYiT+gcwgEn/2kP+N3EADCCRUH8B0HfPywPR/ADtWGjNqH0sBbcGh7+tJWeYlmN5XWDVbER+ND1LdjiWdqJEDiyJmhEum2EFMhEvppGjr6b0wftKk0bwztSih47cn+m5b0GVjfM8wiwzux07vtexdV+ptk7BOZH9/Y59G69YaLA26XKW0KJAp5acD3i/Dd7BWxUBjWpt1vB1OLomD9wRYtfjvE+IfVsbO1SHLyhlnZs0bJna2XCmNRYWbCT5U96+cK012FqSJ6dCiDkV1gvFSYieBNZc8yGJsfkZSqvGf10GzOFOec65Q5vSSFrwECmwjMQtaXZQLZfBU+Z5raIfBwRhrdPegOp64d5OpAbO6urpuPVWlfoQU7Rh+ntQ9X/FULvfGt2r/q6v5aQf6TbPjXusqqWvwleReOA1eNHb+G8e0z5Fl3ysEgEgzSSBxfrhrFtbVGLzUaB/4avgrxkZh7SZqqXZrrGt1dky8wcQVPccQMbvRf4Nzav069+t1M2PX8sf6vRHRsOy8tLx+/t3BE+vApYrcrd//9xrSzaV3xTysrKkKDjgW0yeneC5rWD/y8Z9+CTcuUtWB1v9IVshZdnbpkMQika9FODmBrocJcVmFmwiQQQGFiXWBkyQkjg6oUM4Vor1MgwH0YiwpzPC2K/coDMNJpFWaifwvKRR0oDD1eK6ZaO19vFadj4DMwjULGyxQy3mBLdsoZAcQ1XJeXin1Ae/AY6AJOc9XNmkO9Hl3qLLBSZ3s6CKYrlh5bUZJelk4rntOJ3shOH5GOpim3iitq0hvIC1GeTRc624PYiy2dO6GGapk2fLdtrOaSRKut1bTztDNfH/rwCB5LcPB1o5p4HmwsIRWvLj2Tlfz15opjt375NG9Q3qRrSK49Oem1pPSXx3x9wzFEEFevGrWw35OPnaqflrWh7ZmiucOFjPHTPRA8OM40NKfHqAM79rzeffi4YZnN5TWHumSkZ+G7P62Rl+xv3/6FmF6Hnux4ZFS3zGz0S9kMqdWEUrbG/XAqrU0ma/e4065JY3YNq6uVvif3n3Dy4hLQgnJIiFPfqTBXVJiZsLPCr2EuMLLMYBgvpvlTiFCdAgFUGOmMCjMxMIhyT2sKY2ttsFkUPmugzbeljB8/cto9Y4HE7B7VXgFlAKAC6ZQTRgYzW4hai4bZT4cJTJ70B4NR7B4LQAxKp9o9+wnMTOmgCjMRO4AMvBmMq92TQvi/j3QTWAhX7wSkxJivPAgOIiaNV5BOqc637/Uil4AOJq8ges8Um2EONsWa0k3ZphGmKaYSU5lpr+kt0wcmT+IaBpkoTEis3dcUwvReiIm+AF/K+zQS1lbD1AavtvRDczBLGepcm9r8CAv6Aqf3TjUjCTpLkYnxEVSi0fwbDceQK2fh/uJRk/CX3/+IL0GfSwO3xon6/hn4dp/vLL0jew7Y1uVsH9x8wfaw9eMWbtwq6SfgG/86ewcfhwHVP0BzepyUvztlS9E82aeVvsqY1X560b3U6n1LO2RUPDvnTbpOrL6QyZ9+ivwZyuSPWSeq66TU/TH+6u/kwT0Kf7WWFSgV5rIKMxMOVORhpAuMLDEYxoNDmTyMeGAu2aLCHB/O8Il8EJ/TKszEeCYP21AYWxuDLZxxhEDwfFVMFA+ynI8nSOXPaFOsVLGaNeOowQRAT5aiXs9U2vvvxgd1w6k1S/7ExHq9cBsvpqly9PiXH1y8d/simY/gNZPUHh7m7Cq+1oQZWa52lcDbVa14u4pdqXaVkTCMakpRHlKNLOtD7Koc6H41fnTME+vGDx+F//6lw7CoJ9aNHT2+rmUrGUb4x7cqWQDrA/1lfNm3fUBJCYqshfFGnw1f9LhWZrqNP/FutuFs9z+29FnUBqIhnl4nd3ad2RY67G5uJ/Yoa8FquthaDHHyxm5FFphkN7ZiKswpFWYmHACYNPB3hfmDwTDeGIIYhI5BaOc6qMJMjGOSgMHY/Gk9gfJbrN6HzZfrnM9fmS9QNjXaUitJLDDtv+tj+U/ViTbdx5Km1InWdVozvOkyUd07jje6dOfrRNXnY3TIVehwl9EhUEeejgZ0zYz/IZXBrBaEr6XWN11LXUpLxBU5WthwXdeDnYMVTmxOEgvlDxhRQ6KPbjD35jxE+wgj9SppROAseUfz8768ojfzRcP+XEUJX0Nssaj9zdSxUE/ckNRiVpqq0/WoX5y7OAvXEx8oEwrd1mYLs+lJHPRUjnsF1sKO8YUd9x6o8PCEPaEH7ADdYS+9eyUurMRWX6LykmS3Tyrxp1WfAra3CU0QsZdCQQdiMc3WnJb1yMYQ/ribBGCk+iCBGEoJZQkoj3tmwB8aF1FNlUqM5k7HatW4UVpgmjZoIBeSVG0aadjiM5mZJxb9iv8mEmHxycyMD6fxLTL3xs0vLSkpWVyyQLjT2C0zetjwUTCuzkSkQuHw4YXaphkUuff4CVJ7ffLkTjhG7Z/ZSfLsKcS3dAOhLMuO+Cz7QW9dsC5WJ+Qpx3GSbIOORGytQkpl2dqPoFuZWO+/alXgHwoflooDUIR0geXNOrL8lKCWDKcL2c7yXe/7kWAiAhovms6OUeKVzhs6eM6cwUPnTU6OjkpKiopOlvwGFBcPGFhUNDC6c1JMTDKEyUpPgfi10E/6GxhBAmAlU9qZ3KtpqMtLe8ugXngprh1kk6s1XQwHod/sYd1fsEYmLJk1LOlAXESSVD1i+dDMmLD8VUMz2jM59xIqEn8WOhJL8KvzIMeaweJIqEhy3rOBsWMzKH5dhL/hcCLDJGDQ1GL6siZQo1UwhXV5blbKRfEALMQ73iPw3YQ7MF8Lz/Yqg4fKCaf59AvSIPwczK0CgM2B78Lh0Is/C5WIi+E7F6Zc9MVXoTv0IPhRXNDz5LcjwEkmc0/CJwEARpceDp3q7xJc0FsM/hSDPwX7MXjed/RQbbsuDWa0HYYCiXCDO8WEfRbO0JbYCAc8NzXla9iNjk/iT2HkT+fIGHsBKP4pbEBdhTvAi3CmXfAQol0j+c/MLhw7Z/bYwjmCJX/O7BG9R86YOYLmJ8FWZBUOApl8L4Bsa39ahRoG46EVpvz9Er4CQ15CEXgaXG6Ey+k8Awh8CxVeovBGaIJhRuEeDMFXXvr7b+EgnmvEc2EZXEfgY0CRME2KBAJ9KhDLjqJLjITmV+lhzUXsEGb2/OmogzCIyGQP0Ayk8/H8+31HdllydzbjeAoaycJYVSmq9XIelUkrnSKhVfCJFNCXpaVV2CrCMyer5NvC7G0221Q0w3EAPonw2/SZehK/4AqZOxqUgvsh/wfKsaIjSTlWbDQ7EI2zs/T8YQOAnupMYMhR53bvSHqcDhlskbyrZ6omd+jR5y1cjWeLSa1CZ3KQGGTsLw5om+os9J+wC8ftWPbY1DjfpHlpN/F3G8h/MOxmyvQs34RpSUu3wzM4Dp6BJ9HUV318jnkbYIuPUOWiSv1x2NrgfcJgPFDcrHKRwj97UJHwvdDx4Wf9Ct/T/DYqqlLWyx8A0cz6CFuAyY/qJNS2HjWpPfzJhf9/oseQqvkjL7xw9ewTa3PD02Y/XjT2q6/QuLo60muYW/llcMuTphYFBbmk17DRDugNgBAuWAjPGUA3Dc81d00lIHeRsh2KLYfajLzBeVarnnGeN8950Gz1idShA8XFH+DRHvDFD/EY4bysh6Hr16+fjoKwLEET8mW0H9XwJ7outANRYIsmz95cSznFHnsw726PCmymSZE7s+FqplxJkudpE+aPzpTbHw+GeeStNg3/n82ew3OPzp4zmQTQV4QegaCPpmai+QNnHf+vqyMs/4fqiIfURgwGAG4hOEogRiPTmzd1zjOZnmuXVFO4LIGr5mQsak5mJpzXmKNT8jb/Bbts07oAAAB4AWNgZGAAYen931bF89t8ZZDkYACBIx8E9UD0OZEzun+E/l7lLOKoBHI5GZhAogBOMQvyeAFjYGRg4Ej6e5WBgdPoj9B/I44FQBFUcAcAiWcGPQB4AW2RUxidTQwG52Szv22ztm3btm3btm3btm3bvqvd03y1LuaZrPGGngCA+RkSkWEyhHR6jhTag4r+DBX8n6QKFSOdLKaNrOBb15rftSEZQrtIJGPILCkY6jIjNr+KMd/IZ+QxkhjtjAZGRqNsMCYRGSr/UFW/JbX2oq9Go427QIyP/yWbj8I3/h9G+5+o5tMxWscbE6xdmVp+DqMlJzO1Bclt3mgtwOiPxcbmGI2o7KObO5lzmD+huI7lb9+ATv4Hvv74B6KY4+kdvtQ1FJG4dHCF+dH8hatOQjcCJwPszsXs7l1oo/HJa86vKSgqu4lmdQGjpXxPH/k1PEfj0DaoP7ptc7vQKphrtAksG81RySdb+NnazfUr/vEPiGj+1/jGKCizSSLCLPPvPi8Nn/39X/TWlnbvheT1IympZ/gt9Igueo8S+hcTPspAYdeXBu4c5bQmrYO/f9Z3nM7uM1prdkq7stRw5Sknc2miy+mn35BK0jFGvqGmJLS5k2ls66t99AVzPqpkHKWehigT/PuH+Lhj+E6QRZDDSyRneH+Qg/moscqXIcLLDN5FM5DTN7facniTZzlsY4Bepkvw5x/io7UkeJaDZfAm8lt4kfxGb/MKY6wuI8UbGbxNX9JrV7Pl8BZBDoPpFjjY6+MFVPw4OfndJYbLPNq5I7TxnZn8UVtmhEaSzsgYWK4ZN8gox83b6SL1qCFVKeBGENNNJbXmJLu2Z5RO4RfXnZyuEuVcQZsTn8LB3z0FW2/CPAAAAAAAAAAAAAAALABaANQBSgHaAo4CqgLUAv4DLgNUA2gDgAOaA7IEAgQuBIQFAgVKBbAGGgZQBsgHMAdAB1AHgAeuB94IOgjuCTgJpgn8Cj4KhgrCCygLggueC9QMHgxCDKYM9A1GDYwN6A5MDrIO3g8aD1IPuhAGEEQQfhCkELwQ4BECER4RWBHiEkASkBLuE1IToBQUFFoUhhTKFRIVLhWaFeAWMhaQFuwXLBewGAAYRBh+GOIZPBmSGcwaEBooGmwashqyGtobRBuqHA4ccByaHT4dYB30Ho4emh60HrwfZh98H8ggCiBoIQYhQCGQIboh0CIGIjwihiKSIqwixiLgIzgjSiNcI24jgCOWI6wkIiQuJEAkUiRoJHokjCSeJLQlIiU0JUYlWCVqJXwlkiXEJkImVCZmJngmjiagJu4nVCdmJ3gniiecJ7AnxiiOKJoorCi+KNAo5Cj2KQgpGikwKcop3CnuKgAqEiokKjgqcCrqKvwrDisgKzQrRiukK7gr1CxeLPItGC1YLZQtni2oLcAt2i3uLgYuHi4+Llouci6KLp4u3C9eL3Yv2DAcMKQw9jEcMS4AAAABAAAA3ACXABYAXwAFAAEAAAAAAA4AAAIAAeYAAwABeAF9zANyI2AYBuBnt+YBMsqwjkfpsLY9qmL7Bj1Hb1pbP7+X6HOmy7/uAf8EeJn/GxV4mbvEjL/M3R88Pabfsr0Cbl7mUQdu7am4VNFUEbQp5VpOS8melIyWogt1yyoqMopSkn+kkmIiouKOpNQ15FSUBUWFREWe1ISoWcE378e+mU99WU1NVUlhYZ2nHXKh6sKVrJSQirqMsKKcKyllDSkNYRtWzVu0Zd+iGTEhkXtU0y0IeAFswQOWQgEAAMDZv7Zt27ZtZddTZ+4udYFmBEC5qKCaEjWBQK069Ro0atKsRas27Tp06tKtR68+/QYMGjJsxKgx4yZMmjJtxqw58xYsWrJsxao16zZs2rJtx649+w4cOnLsxKkz5y5cunLtxq079x48evLsxas37z58+vLtx68//0LCIqJi4hKSUtIyshWC4GErEAAAAOAs/3NtI+tluy7Ztm3zZZ6z69yMBuVixBqU50icNMkK1ap48kySXdGy3biVKl+CcYeuFalz786DMo1mTWvy2hsZ3po3Y86yBYuWHHtvzYpVzT64kmnTug0fnTqX6LNPvvjmq+9K/PDLT7/98c9f/wU4EShYkBBhQvUoFSFcpChnLvTZ0qLVtgM72rTr0m1Ch06T4g0ZNvDk+ZMXLo08efk4RnZGDkZOhlQWv1AfH/bSvEwDA0cXEG1kYG7C4lpalM+Rll9apFdcWsBZklGUmgpisZeU54Pp/DwwHwBPQXTqAHgBLc4lXMVQFIDxe5+/Ke4uCXd3KLhLWsWdhvWynugFl7ieRu+dnsb5flD+V44+W03Pqkm96nSsSX3pwfbG8hyVafqKLY53NhRyi8/1/P8l1md6//6SRzsznWXcUiuTXQ3F3NJTfU3V3NRrJp2WrjUzN3sl06/thr54PYV7+IYaQ1++jlly8+AO2iz5W4IT8OEJIqi29NXrGHhwB65DLfxAtSN5HvgQQgRjjiSfQJDDoBz5e4AA3BwJtOVAHgtBBGGeRNsK5DYGd8IvM61XFAA=) 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/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 9bfcf96d..b067e13f 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.5" - self.rev = 4560 + self.version = "0.7.6" + self.rev = 4565 self.argv = argv self.action = None self.test_parser = 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))) 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/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/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/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/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 } 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 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 f498aedb96461ce7e87e73bb2aa24f020b606de1 Mon Sep 17 00:00:00 2001 From: canewsin Date: Wed, 2 Mar 2022 20:17:14 +0530 Subject: [PATCH 2517/2570] 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 2518/2570] 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 2519/2570] 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 2520/2570] 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 2521/2570] 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 2522/2570] 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 2523/2570] 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 2524/2570] 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 2525/2570] 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 2526/2570] 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 2527/2570] 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 2528/2570] 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 2529/2570] 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 2530/2570] 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 2531/2570] 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 2532/2570] 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 2533/2570] 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 2534/2570] 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 2535/2570] 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 2536/2570] 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 2537/2570] 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 2538/2570] 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 2539/2570] 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 2540/2570] 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 2541/2570] 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 2542/2570] 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 2543/2570] 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 2544/2570] 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 2545/2570] 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 2546/2570] 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 2547/2570] 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 2548/2570] 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 2549/2570] 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 2550/2570] 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 2551/2570] 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 2552/2570] 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 2553/2570] 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 2554/2570] 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 2555/2570] 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 2556/2570] 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 2557/2570] 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 2558/2570] 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 2559/2570] 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 2560/2570] 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 2561/2570] 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 2562/2570] 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 2563/2570] 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 2564/2570] 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 2565/2570] 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 2566/2570] 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 2567/2570] 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 2568/2570] 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 2569/2570] 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 2570/2570] 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