├── .output └── Surge Policy.box ├── README.md ├── assets ├── done.png ├── error.png ├── icon.png ├── loading.gif ├── thankyou.jpg └── thankyou2.jpg ├── config.json ├── main.js ├── prefs.json ├── scripts ├── app.js └── diff.js └── strings ├── en.strings └── zh-Hans.strings /.output/Surge Policy.box: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fndroid/surge-gen-jsbox/d8cf91dac0495cba6aff76f57271e52fbd431112/.output/Surge Policy.box -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## surge-gen-jsbox 2 | 3 | Surge3+配置管理器 4 | 5 | ### 安装 6 | 7 | [点击安装](https://xteko.com/redir?name=Surge%20Policy&url=https%3A%2F%2Fgithub.com%2FFndroid%2Fsurge-gen-jsbox%2Fblob%2Fmaster%2F.output%2FSurge%2520Policy.box%3Fraw%3Dtrue) 8 | 9 | ### 特性 10 | 1. 支持多服务商订阅 11 | 2. 便捷的策略编辑操作 12 | 3. 第三方规则可视化更新 13 | 14 | ### 使用 15 | 16 | 1. 点击界面最上方“铅笔”按钮,导入基础配置文件,删除配置文件中不需要的节点([Proxy]下) 17 | 2. 退回主界面,此时主界面会根据策略组进行刷新 18 | 3. 点击主界面最上方“云朵”按钮,添加服务商提供的托管地址,用于获取节点信息 19 | 4. 退回主界面,点击最上方“刷新”按钮,拉取节点信息 20 | 5. 根据自己的需求安排策略组,点击PROXIES下节点进行移除,点击MORE下节点进行添加 21 | 6. 点击Generate按钮尝试生成,如果无法生成,脚本会自动导航到节点不存在的策略组中,修复所有问题即可生成 22 | 23 | 24 | ### 更新配置 25 | 26 | 由于Surge3+支持RULE-SET可以远程下载规则,所以多数时间我们并不需要使用脚本修改规则 27 | 28 | 但是脚本中提供了一个DIFF工具,使用这个工具,可以将当前的配置文件和规则维护者的配置文件进行比较,并且选择需要的更新进行合并 29 | 30 | #### 具体操作 31 | 32 | 1. 点击界面最上方“铅笔”按钮,打开基础文件编辑界面 33 | 2. 点击界面底部左边的按钮,输入远端配置文件地址,点击确定 34 | 3. 在打开的界面中,根据提示进行修改 35 | 36 | 提示类型: 37 | - 绿色:本地文件增加内容 38 | - 红色:远端文件多余内容 39 | 40 | 当点击提示时,对应的内容会被删除,当所有内容检查完毕后,点击Save按钮即可保存配置文件 41 | 42 | > 此处操做类似于git diff 43 | -------------------------------------------------------------------------------- /assets/done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fndroid/surge-gen-jsbox/d8cf91dac0495cba6aff76f57271e52fbd431112/assets/done.png -------------------------------------------------------------------------------- /assets/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fndroid/surge-gen-jsbox/d8cf91dac0495cba6aff76f57271e52fbd431112/assets/error.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fndroid/surge-gen-jsbox/d8cf91dac0495cba6aff76f57271e52fbd431112/assets/icon.png -------------------------------------------------------------------------------- /assets/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fndroid/surge-gen-jsbox/d8cf91dac0495cba6aff76f57271e52fbd431112/assets/loading.gif -------------------------------------------------------------------------------- /assets/thankyou.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fndroid/surge-gen-jsbox/d8cf91dac0495cba6aff76f57271e52fbd431112/assets/thankyou.jpg -------------------------------------------------------------------------------- /assets/thankyou2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fndroid/surge-gen-jsbox/d8cf91dac0495cba6aff76f57271e52fbd431112/assets/thankyou2.jpg -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "name": "Surge Policy", 4 | "url": "", 5 | "version": "1.0.0", 6 | "author": "", 7 | "website": "", 8 | "types": 0 9 | }, 10 | "settings": { 11 | "minSDKVer": "1.0.0", 12 | "minOSVer": "10.0.0", 13 | "idleTimerDisabled": false, 14 | "autoKeyboardEnabled": false, 15 | "keyboardToolbarEnabled": false, 16 | "rotateDisabled": false 17 | }, 18 | "widget": { 19 | "tintColor": "", 20 | "iconColor": "" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | // $app.autoKeyboardEnabled = true 2 | 3 | var app = require('scripts/app'); 4 | 5 | app.renderUI() -------------------------------------------------------------------------------- /prefs.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "SETTINGS", 3 | "groups": [ 4 | { 5 | "title": "General", 6 | "items": [ 7 | { 8 | "title": "File Name", 9 | "type": "string", 10 | "key": "filename", 11 | "value": "Fndroid.conf" 12 | }, 13 | { 14 | "title": "Export With", 15 | "type": "list", 16 | "key": "export", 17 | "items": ["Share Sheet", "URL Scheme"], 18 | "value": 0 19 | }, 20 | { 21 | "title": "URL Scheme", 22 | "type": "list", 23 | "key": "scheme", 24 | "items": ["surge3", "surge", "surge-enterprise"], 25 | "value": 0 26 | }, 27 | { 28 | "title": "Update Timeout", 29 | "type": "integer", 30 | "key": "updateTimeout", 31 | "value": 10 32 | }, 33 | { 34 | "title": "Notify New Proxies", 35 | "type": "list", 36 | "key": "notificationType", 37 | "items": ["None", "Notification", "Dialog"], 38 | "value": 1 39 | } 40 | ] 41 | }, 42 | { 43 | "title": "Donate", 44 | "items": [ 45 | { 46 | "title": "WeChat", 47 | "type": "script", 48 | "value": "$quicklook.open({image: $file.read('assets/thankyou.jpg').image})" 49 | } 50 | ] 51 | }, 52 | { 53 | "title": "Info", 54 | "items": [ 55 | { 56 | "title": "Author", 57 | "type": "info", 58 | "value": "Fndroid" 59 | } 60 | ] 61 | } 62 | ] 63 | } -------------------------------------------------------------------------------- /scripts/app.js: -------------------------------------------------------------------------------- 1 | const Diff = require('./diff') 2 | 3 | const TEXT = 'conf' 4 | const SUBS = 'subs' 5 | const PROXIES = 'proxies' 6 | const POLICIES = 'policies' 7 | const TEXT_PROXIES = 'conf_proxies' 8 | const DIFF_URL = "diff_url" 9 | 10 | 11 | $define({ 12 | type: "KeyboardObserver: NSObject", 13 | props: ["height"], 14 | events: { 15 | "init": () => { 16 | self = self.$super().$init(); 17 | 18 | const center = $objc("NSNotificationCenter").$defaultCenter(); 19 | const observer = self; 20 | const register = (selector, name) => { 21 | center.$addObserver_selector_name_object(observer, selector, name, null); 22 | } 23 | 24 | register("show:", "UIKeyboardWillShowNotification"); 25 | register("show:", "UIKeyboardDidShowNotification"); 26 | register("hide:", "UIKeyboardWillHideNotification"); 27 | return self; 28 | }, 29 | "show": notification => { 30 | const info = notification.$userInfo(); 31 | const frame = info.$objectForKey("UIKeyboardFrameEndUserInfoKey"); 32 | const rect = frame.$CGRectValue(); 33 | const height = rect.height; 34 | self.$setHeight(height); 35 | self.$notifyHeightChange(); 36 | }, 37 | "hide": notification => { 38 | self.$setHeight(0); 39 | self.$notifyHeightChange(); 40 | }, 41 | "notifyHeightChange": () => { 42 | const height = self.$height(); 43 | console.log('height:', height) 44 | let v = $("basicConfView") 45 | if (v) { 46 | let f = v.frame 47 | v.remakeLayout(make => { 48 | make.size.equalTo($size(f.width, $('mainView').frame.height - height + 30)) 49 | }) 50 | } 51 | } 52 | } 53 | }); 54 | 55 | const observer = $objc("KeyboardObserver").$new(); 56 | $objc_retain(observer); 57 | 58 | $app.listen({ 59 | exit: () => $objc_release(observer) 60 | }); 61 | 62 | const EMPTY_NAV_BUTTON = [{ 63 | title: "" 64 | }] 65 | 66 | let renderUI = async () => { 67 | $ui.render({ 68 | props: { 69 | title: 'Surge Policy', 70 | navButtons: EMPTY_NAV_BUTTON 71 | }, 72 | events: { 73 | appeared: handleMainViewAppeared 74 | }, 75 | views: [{ 76 | type: 'view', 77 | props: { 78 | id: 'mainView', 79 | }, 80 | layout: (make, view) => { 81 | make.height.width.equalTo(view.super).offset(-30) 82 | make.top.left.equalTo(view.super).offset(15) 83 | }, 84 | events: {}, 85 | views: [{ 86 | type: 'stack', 87 | props: { 88 | id: '', 89 | spacing: 10, 90 | distribution: $stackViewDistribution.equalCentering, 91 | stack: { 92 | views: [{ 93 | type: 'image', 94 | props: { 95 | id: '', 96 | icon: $icon("030", $color("tint"), $size(30, 30)), 97 | }, 98 | events: { 99 | tapped: renderTextEditUI 100 | }, 101 | views: [] 102 | }, { 103 | type: 'image', 104 | props: { 105 | id: '', 106 | icon: $icon("091", $color("tint"), $size(30, 30)), 107 | }, 108 | events: { 109 | tapped: renderSubscrptionUI 110 | }, 111 | views: [] 112 | }, { 113 | type: 'image', 114 | props: { 115 | id: '', 116 | icon: $icon("162", $color("tint"), $size(30, 30)), 117 | }, 118 | events: { 119 | tapped: handleSubsUpdate 120 | }, 121 | views: [] 122 | }, { 123 | type: 'image', 124 | props: { 125 | id: '', 126 | icon: $icon("002", $color("tint"), $size(30, 30)), 127 | }, 128 | events: { 129 | tapped: () => { 130 | $prefs.open() 131 | } 132 | }, 133 | views: [] 134 | },], 135 | } 136 | }, 137 | layout: (make, view) => { 138 | make.height.equalTo(30) 139 | make.width.equalTo(view.super).offset(-40) 140 | make.left.equalTo(view.super).offset(20) 141 | }, 142 | }, { 143 | type: 'menu', 144 | props: { 145 | id: 'policyMenuView', 146 | }, 147 | layout: (make, view) => { 148 | make.top.equalTo(view.prev.bottom).offset(10) 149 | make.height.equalTo(40) 150 | make.width.equalTo(view.super) 151 | }, 152 | events: { 153 | changed: handleMenuChange 154 | }, 155 | views: [] 156 | }, { 157 | type: 'label', 158 | props: { 159 | id: 'labelTop', 160 | align: $align.center, 161 | font: $font("bold", 17), 162 | text: 'PROXIES' 163 | }, 164 | layout: (make, view) => { 165 | make.height.equalTo(30) 166 | make.width.equalTo(view.super) 167 | make.top.equalTo(view.prev.bottom).offset(10) 168 | }, 169 | events: {}, 170 | views: [] 171 | }, { 172 | type: 'view', 173 | props: { 174 | id: 'line1', 175 | bgcolor: $color('#f1f3f4') 176 | }, 177 | layout: (make, view) => { 178 | make.height.equalTo(2) 179 | make.width.equalTo($("mainView")).offset(30) 180 | make.left.equalTo(view.super).offset(-15) 181 | make.top.equalTo(view.prev.bottom).offset(0) 182 | }, 183 | events: {}, 184 | views: [] 185 | }, { 186 | type: 'list', 187 | props: { 188 | id: 'existListView', 189 | reorder: true, 190 | rowHeight: 35, 191 | template: { 192 | views: [{ 193 | type: 'label', 194 | props: { 195 | id: 'name', 196 | font: $font(16), 197 | }, 198 | layout: $layout.fill, 199 | },] 200 | } 201 | }, 202 | layout: (make, view) => { 203 | make.width.equalTo(view.super) 204 | make.height.equalTo(view.super).multipliedBy(1 / 2).offset(-125) 205 | make.top.equalTo(view.prev.bottom).offset(0) 206 | }, 207 | events: { 208 | didSelect: (sender, indexPath, data) => { 209 | let idx = $('policyMenuView').index 210 | let od = $cache.get(POLICIES) 211 | od[idx].proxies.splice(indexPath.row, 1) 212 | $cache.set(POLICIES, od) 213 | saveConf(od) 214 | 215 | sender.delete(indexPath) 216 | if (isPolicyExist(data.name.text)) { 217 | let ed = $('allListView').data 218 | data.name.color = $color("black") 219 | ed.push(data) 220 | $('allListView').data = ed 221 | } 222 | }, 223 | reorderFinished: data => { 224 | let idx = $('policyMenuView').index 225 | let od = $cache.get(POLICIES) 226 | od[idx].proxies = data.map(i => i.name.text) 227 | $cache.set(POLICIES, od) 228 | saveConf(od) 229 | } 230 | }, 231 | views: [] 232 | }, { 233 | type: 'view', 234 | props: { 235 | id: 'line2', 236 | bgcolor: $color('#f1f3f4') 237 | }, 238 | layout: (make, view) => { 239 | make.height.equalTo(2) 240 | make.width.equalTo($("mainView")).offset(30) 241 | make.left.equalTo(view.super).offset(-15) 242 | make.top.equalTo(view.prev.bottom).offset(0) 243 | }, 244 | events: {}, 245 | views: [] 246 | }, { 247 | type: 'label', 248 | props: { 249 | id: 'labelBottom', 250 | align: $align.center, 251 | font: $font("bold", 17), 252 | text: 'MORE' 253 | }, 254 | layout: (make, view) => { 255 | make.height.equalTo(30) 256 | make.width.equalTo(view.super) 257 | make.top.equalTo(view.prev.bottom).offset(10) 258 | }, 259 | events: {}, 260 | views: [] 261 | }, { 262 | type: 'view', 263 | props: { 264 | id: 'line3', 265 | bgcolor: $color('#f1f3f4') 266 | }, 267 | layout: (make, view) => { 268 | make.height.equalTo(2) 269 | make.width.equalTo($("mainView")).offset(30) 270 | make.left.equalTo(view.super).offset(-15) 271 | make.top.equalTo(view.prev.bottom).offset(0) 272 | }, 273 | events: {}, 274 | views: [] 275 | }, { 276 | type: 'list', 277 | props: { 278 | id: 'allListView', 279 | rowHeight: 35, 280 | template: { 281 | views: [{ 282 | type: 'label', 283 | props: { 284 | id: 'name', 285 | font: $font(16), 286 | }, 287 | layout: $layout.fill, 288 | },] 289 | } 290 | }, 291 | layout: (make, view) => { 292 | make.width.equalTo(view.super) 293 | make.height.equalTo(view.super).multipliedBy(1 / 2).offset(-125) 294 | make.top.equalTo(view.prev.bottom) 295 | }, 296 | events: { 297 | didSelect: (sender, indexPath, data) => { 298 | let idx = $('policyMenuView').index 299 | let od = $cache.get(POLICIES) 300 | od[idx].proxies.push(data.name.text) 301 | $cache.set(POLICIES, od) 302 | 303 | let reg = filePartReg('Proxy Group') 304 | let text = $cache.get(TEXT) 305 | text = text.replace(reg, `$1\n${policyStringify(od)}\n\n$3`) 306 | $cache.set(TEXT, text) 307 | 308 | sender.delete(indexPath) 309 | let ed = $('existListView').data 310 | data.name.color = $color('black') 311 | ed.push(data) 312 | $('existListView').data = ed 313 | } 314 | }, 315 | views: [] 316 | }, { 317 | type: 'view', 318 | props: { 319 | id: 'line4', 320 | bgcolor: $color('#f1f3f4') 321 | }, 322 | layout: (make, view) => { 323 | make.height.equalTo(2) 324 | make.width.equalTo($("mainView")).offset(30) 325 | make.left.equalTo(view.super).offset(-15) 326 | make.top.equalTo(view.prev.bottom).offset(0) 327 | }, 328 | events: {}, 329 | views: [] 330 | }, { 331 | type: 'button', 332 | props: { 333 | title: 'Generate', 334 | id: 'genButtonView' 335 | }, 336 | layout: (make, view) => { 337 | make.top.equalTo(view.prev.bottom).offset(10) 338 | make.height.equalTo(50) 339 | make.width.equalTo(view.super) 340 | }, 341 | events: { 342 | tapped: generateConf 343 | }, 344 | views: [] 345 | }, { 346 | type: 'label', 347 | props: { 348 | id: 'hint', 349 | hidden: true, 350 | text: "Edit in Text Mode", 351 | font: $font("bold", 25), 352 | textColor: $color("#a6a5a4") 353 | }, 354 | layout: $layout.center, 355 | events: { 356 | tapped: renderTextEditUI 357 | }, 358 | views: [] 359 | },] 360 | }, { 361 | type: 'blur', 362 | props: { 363 | id: 'downloadingView', 364 | style: 2, 365 | hidden: true 366 | }, 367 | layout: $layout.fill, 368 | events: {}, 369 | views: [{ 370 | type: 'list', 371 | props: { 372 | id: 'downloadingListView', 373 | radius: 10, 374 | template: { 375 | views: [{ 376 | type: 'label', 377 | props: { 378 | id: 'domainView', 379 | font: $font("bold", 17) 380 | }, 381 | layout: (make, view) => { 382 | make.centerY.equalTo(view.super) 383 | make.width.equalTo(view.super).offset(-75) 384 | make.left.equalTo(view.super).offset(15) 385 | }, 386 | events: {}, 387 | views: [] 388 | }, { 389 | type: 'image', 390 | props: { 391 | id: 'statusView', 392 | }, 393 | layout: (make, view) => { 394 | make.centerY.equalTo(view.super) 395 | make.size.equalTo($size(25, 25)); 396 | make.right.equalTo(view.super).offset(-15) 397 | }, 398 | events: {}, 399 | views: [] 400 | },] 401 | } 402 | }, 403 | layout: (make, view) => { 404 | make.center.equalTo(view.super) 405 | make.width.equalTo(view.super).offset(-60) 406 | make.height.equalTo(view.super).multipliedBy(0.5) 407 | }, 408 | events: {}, 409 | views: [] 410 | }, { 411 | type: 'button', 412 | props: { 413 | id: '', 414 | bgcolor: $color("#000"), 415 | radius: 20, 416 | icon: $icon('225', $color('#fff'), $size(40, 40)) 417 | }, 418 | layout: (make, view) => { 419 | make.centerX.equalTo(view.super) 420 | make.top.equalTo(view.prev.bottom).offset(10) 421 | }, 422 | events: { 423 | tapped: _ => { 424 | $('downloadingView').hidden = true 425 | } 426 | }, 427 | views: [] 428 | },] 429 | }] 430 | }) 431 | } 432 | 433 | let renderTextEditUI = () => { 434 | $ui.push({ 435 | props: { 436 | title: "BASIC CONF", 437 | navButtons: EMPTY_NAV_BUTTON, 438 | }, 439 | views: [{ 440 | type: 'text', 441 | props: { 442 | placeholder: "Surge Configuration", 443 | text: $cache.get(TEXT) || '', 444 | font: $font(14), 445 | id: 'basicConfView' 446 | }, 447 | layout: (make, view) => { 448 | make.width.left.top.equalTo(view.super) 449 | make.height.equalTo(view.super) 450 | }, 451 | events: { 452 | didChange: (sender) => { 453 | $cache.set(TEXT, sender.text) 454 | } 455 | }, 456 | views: [] 457 | }, { 458 | type: 'button', 459 | props: { 460 | id: '', 461 | radius: 20, 462 | bgcolor: $color("tint"), 463 | icon: $icon('165', $color('white'), $size(25, 25)) 464 | }, 465 | layout: (make, view) => { 466 | make.size.equalTo($size(40, 40)) 467 | make.right.equalTo(view.super).offset(-30) 468 | make.bottom.equalTo(view.prev).offset(-30) 469 | }, 470 | events: { 471 | tapped: async sender => { 472 | let t = await $input.text({ 473 | placeholder: "URL, will overwrite current profile", 474 | }) 475 | if (!t) return 476 | let resp = await $http.get(t) 477 | $('basicConfView').text = resp.data 478 | $cache.set(TEXT, resp.data) 479 | } 480 | }, 481 | views: [] 482 | }, { 483 | type: 'button', 484 | props: { 485 | id: '', 486 | radius: 20, 487 | bgcolor: $color("tint"), 488 | icon: $icon('045', $color('white'), $size(25, 25)) 489 | }, 490 | layout: (make, view) => { 491 | make.size.equalTo($size(40, 40)) 492 | make.right.equalTo(view.super).offset(-80) 493 | make.bottom.equalTo(view.prev.prev).offset(-30) 494 | }, 495 | events: { 496 | tapped: async sender => { 497 | let t = await $input.text({ 498 | placeholder: "URL, profile to show diff", 499 | text: $cache.get(DIFF_URL) || '' 500 | }) 501 | if (!t) return 502 | $cache.set(DIFF_URL, t) 503 | let resp = await $http.get(t) 504 | let diff = new Diff() 505 | let d = diff.main(resp.data.replace(/\r\n/g, '\n'), $cache.get(TEXT).replace(/\r\n/g, '\n')) 506 | diff.cleanupSemantic(d) 507 | // console.log(diff.prettyHtml(d)) 508 | $ui.push({ 509 | props: { 510 | title: "DIFF", 511 | navButtons: EMPTY_NAV_BUTTON 512 | }, 513 | views: [{ 514 | type: 'web', 515 | props: { 516 | id: '', 517 | script: () => { 518 | let btn = document.getElementsByClassName('button')[0] 519 | let content = document.getElementsByClassName('content')[0] 520 | btn.onclick = () => { 521 | $notify("save", content.innerText) 522 | return false 523 | } 524 | }, 525 | html: ` 526 | 527 | 528 | 561 | 562 | 563 |
564 | ${diff.prettyHtml(d)} 565 |
566 | 567 |
Save
568 | 569 | 590 | 591 | ` 592 | }, 593 | layout: $layout.fill, 594 | events: { 595 | save: content => { 596 | $ui.alert({ 597 | title: "Warning", 598 | message: "Are you sure to overwrite the profile?", 599 | actions: [{ 600 | title: "Yes", 601 | handler: () => { 602 | $('basicConfView').text = content 603 | $cache.set(TEXT, content) 604 | $ui.pop() 605 | } 606 | }, { 607 | title: "No" 608 | }] 609 | }) 610 | } 611 | } 612 | },] 613 | }) 614 | } 615 | }, 616 | views: [] 617 | },] 618 | }) 619 | } 620 | 621 | let renderSubscrptionUI = () => { 622 | $ui.push({ 623 | props: { 624 | title: "SUBSCRIPTIONS", 625 | navButtons: EMPTY_NAV_BUTTON, 626 | }, 627 | views: [{ 628 | type: 'list', 629 | props: { 630 | id: 'subscriptionListView', 631 | data: $cache.get(SUBS) || [], 632 | reorder: true, 633 | actions: [{ 634 | title: 'delete', 635 | handler: (sender, indexPath) => { 636 | // sender.delete(indexPath) 637 | $cache.set(SUBS, $('subscriptionListView').data) 638 | } 639 | }] 640 | }, 641 | layout: $layout.fill, 642 | events: { 643 | reorderFinished: data => { 644 | $cache.set(SUBS, data) 645 | }, 646 | didSelect: async (sender, indexPath, data) => { 647 | let t = await $input.text({ 648 | text: data 649 | }) 650 | if (!t) return 651 | 652 | let d = sender.data 653 | d[indexPath.row] = t 654 | sender.data = d 655 | $cache.set(SUBS, d) 656 | } 657 | }, 658 | views: [] 659 | }, { 660 | type: 'button', 661 | props: { 662 | id: '', 663 | bgcolor: $color("#fff"), 664 | radius: 20, 665 | icon: $icon('104', $color('tint'), $size(40, 40)) 666 | }, 667 | layout: (make, view) => { 668 | make.right.equalTo(view.super).offset(-30) 669 | make.bottom.equalTo(view.super).offset(-50) 670 | }, 671 | events: { 672 | tapped: async sender => { 673 | let t = await $input.text({ 674 | placeholder: "URL" 675 | }) 676 | if (!t) return 677 | let d = $('subscriptionListView').data 678 | d.push(t) 679 | $cache.set(SUBS, d) 680 | $('subscriptionListView').data = d 681 | } 682 | }, 683 | views: [] 684 | },] 685 | }) 686 | } 687 | 688 | let handleMainViewAppeared = () => { 689 | let t = $cache.get(TEXT) 690 | if (!t) return 691 | let lines = t.split('\n').filter(l => !/^\s*(#|\/\/)/.test(l)) 692 | let policies = [] 693 | lines.forEach(l => { 694 | if (/(.+?)=\s*(url-test|select|fallback|ssid)\s*,(.+)/.test(l)) { 695 | let name = RegExp.$1.trim() 696 | let type = RegExp.$2 697 | let others = RegExp.$3 698 | if (/policy\-path\s*=\s*http/.test(others) || type === "ssid") { 699 | policies.push({ 700 | name, 701 | type, 702 | raw: l 703 | }) 704 | } else if (type === 'select') { 705 | policies.push({ 706 | name, 707 | type, 708 | proxies: others.split(',').map(i => i.trim()).filter(i => i !== "") 709 | }) 710 | } else if (type === 'fallback' || type === 'url-test') { 711 | let os = others.split(',').map(i => i.trim()).filter(i => i !== "") 712 | let proxies = [] 713 | let url = 'http://www.gstatic.com/generate_204' 714 | let interval = 200 715 | os.forEach(o => { 716 | if (/url\s*=\s*(http.+)/.test(o)) { 717 | url = RegExp.$1.trim() 718 | } else if (/interval\s*=\s*(\d+)/.test(o)) { 719 | interval = RegExp.$1 * 1 720 | } else { 721 | proxies.push(o) 722 | } 723 | }) 724 | policies.push({ 725 | name, 726 | type, 727 | proxies, 728 | url, 729 | interval 730 | }) 731 | } 732 | } 733 | }) 734 | $('policyMenuView').items = policies.map(p => p.name) 735 | $cache.set(POLICIES, policies) 736 | $cache.set(TEXT_PROXIES, parseProxies(t, true).map(p => p.name)) 737 | handleMenuChange({ index: $('policyMenuView').index }) 738 | 739 | } 740 | 741 | let handleMenuChange = async sender => { 742 | let idx = sender.index 743 | let policies = $cache.get(POLICIES) 744 | 745 | let editable = policies[idx].hasOwnProperty('proxies') 746 | 747 | let viewGroup = ['labelTop', 'labelBottom', 'line1', 'line2', 'line3', 'line4', 'existListView', 'allListView'] 748 | viewGroup.forEach(v => { 749 | $('mainView').get(v).hidden = !editable 750 | }) 751 | $("hint").hidden = editable 752 | 753 | if (!editable) { 754 | $('existListView').data = [] 755 | $('allListView').data = [] 756 | return 757 | } 758 | 759 | let existProxies = policies[idx].proxies 760 | let psns = getAllProxyNames() 761 | 762 | $('existListView').data = existProxies.map(ep => { 763 | return { 764 | name: { 765 | text: ep, 766 | textColor: isPolicyExist(ep) ? $color('black') : $color("red") 767 | } 768 | } 769 | }) 770 | 771 | $('allListView').data = psns 772 | .filter(p => !existProxies.includes(p)) 773 | .map(p => { 774 | return { 775 | name: { 776 | text: p 777 | } 778 | } 779 | }) 780 | } 781 | 782 | let getAllProxyNames = () => { 783 | let ps = $cache.get(PROXIES) || [] 784 | let policies = $cache.get(POLICIES) 785 | let originNames = $cache.get(TEXT_PROXIES) 786 | return originNames.concat(ps.map(p => p.name).concat(policies.map(p => p.name)).concat(['DIRECT', 'REJECT', 'REJECT-TINYGIF'])) 787 | } 788 | 789 | let isPolicyExist = (name) => { 790 | return getAllProxyNames().includes(name) 791 | } 792 | 793 | let handleSubsUpdate = async () => { 794 | let subs = $cache.get(SUBS) 795 | 796 | let httpGetCount = async (url, timeout, counter = () => { }) => { 797 | try { 798 | let resp = await $http.get({ 799 | url, 800 | timeout 801 | }) 802 | counter({ url, success: !resp.error && resp.response.statusCode === 200 }) 803 | return resp 804 | } catch (e) { 805 | counter({ url, success: false }) 806 | return { data: '' } 807 | } 808 | } 809 | 810 | let hostFromURL = (url) => { 811 | if (/https?:\/\/(.+?)(?:\/|$|\?)/.test(url)) { 812 | return RegExp.$1.trim() 813 | } 814 | return url 815 | } 816 | 817 | let convertListData = (ds) => { 818 | return ds.map(d => { 819 | let src = "assets/loading.gif" 820 | if (d.status === 1) { 821 | src = "assets/done.png" 822 | } else if (d.status === 2) { 823 | src = "assets/error.png" 824 | } 825 | return { domainView: { text: hostFromURL(d.url) }, statusView: { src } } 826 | }) 827 | } 828 | 829 | let data = subs.map(s => ({ url: s, status: 0 })) 830 | $('downloadingListView').data = convertListData(data) 831 | $('downloadingView').hidden = false 832 | 833 | let failedUrls = [] 834 | let resps = await Promise.all(subs.map(s => httpGetCount(s, $prefs.get('updateTimeout'), (res) => { 835 | if (!res.success) { 836 | failedUrls.push(res.url) 837 | } 838 | data.find(d => d.url === res.url).status = res.success ? 1 : 2 839 | $('downloadingListView').data = convertListData(data) 840 | }))) 841 | let proxies = resps.map(resp => parseProxies(resp.data)).reduce((prev, cur) => prev.concat(cur), []) 842 | let oldProxies = $cache.get(PROXIES) || [] 843 | let newProxies = proxies.filter(p => !oldProxies.map(o => o.name).includes(p.name)) 844 | if (newProxies.length > 0) { 845 | let notificationType = $prefs.get('notificationType') 846 | console.log('notificationType:', notificationType) 847 | let title = 'New Proxies' 848 | let body = newProxies.map(p => p.name).join(', ') 849 | if (notificationType === 1) { 850 | $push.schedule({ 851 | title, 852 | body, 853 | delay: 1 854 | }) 855 | } else if (notificationType === 2) { 856 | $ui.alert({ 857 | title, 858 | message: body, 859 | actions: [{ title: "OK", handler: () => {} }] 860 | }) 861 | } 862 | } 863 | $cache.set(PROXIES, proxies) 864 | handleMenuChange({ index: $('policyMenuView').index }) 865 | } 866 | 867 | let parseProxies = (raw, alias = false) => { 868 | let proxies = [] 869 | let lines = raw.split('\n').filter(l => !/^\s*(#|\/\/)/.test(l)) 870 | lines.forEach(l => { 871 | if (/(.+?)=\s*(ss|custom|http|sorcks5|snell|vmess)\s*,/.test(l)) { 872 | proxies.push({ 873 | name: RegExp.$1.trim(), 874 | raw: l 875 | }) 876 | } else if (alias && /(.+?)=\s*(direct|reject|reject-tinygif)\s*$/.test(l)) { 877 | proxies.push({ 878 | name: RegExp.$1.trim(), 879 | raw: l 880 | }) 881 | } 882 | }) 883 | return proxies 884 | } 885 | 886 | let policyStringify = (policies) => { 887 | let lines = [] 888 | policies.forEach(ps => { 889 | if (ps.hasOwnProperty('raw')) { 890 | lines.push(ps.raw) 891 | } else if (ps.type === 'select') { 892 | lines.push(`${ps.name} = select, ${ps.proxies.join(', ')}`) 893 | } else { 894 | lines.push(`${ps.name} = ${ps.type}, ${ps.proxies.join(', ')}, interval=${ps.interval}, url=${ps.url}`) 895 | } 896 | }) 897 | return lines.join('\n\n') 898 | } 899 | 900 | let filePartReg = name => { 901 | let reg = `(\\[${name}\\])([\\S\\s]*?)(\\[General\\]|\\[Replica\\]|\\[Proxy\\]|\\[Proxy Group\\]|\\[Rule\\]|\\[Host\\]|\\[URL Rewrite\\]|\\[Header Rewrite\\]|\\[SSID Setting\\]|\\[MITM\\]|\\[Script\\]|$)` 902 | return new RegExp(reg) 903 | } 904 | 905 | let saveConf = (od) => { 906 | let reg = filePartReg('Proxy Group') 907 | let text = $cache.get(TEXT) 908 | text = text.replace(reg, `$1\n${policyStringify(od)}\n\n$3`) 909 | $cache.set(TEXT, text) 910 | } 911 | 912 | let generateConf = async _ => { 913 | // Check before generate 914 | let policies = $cache.get(POLICIES) 915 | for (let i = 0; i < policies.length; i++) { 916 | if (policies[i].hasOwnProperty('raw')) continue 917 | for (let j = 0; j < policies[i].proxies.length; j++) { 918 | let node = policies[i].proxies[j] 919 | if (!isPolicyExist(node)) { 920 | $ui.toast('Proxy not exist') 921 | $('policyMenuView').index = i 922 | handleMenuChange({ index: i }) 923 | $('existListView').scrollTo({ 924 | animated: true, 925 | indexPath: $indexPath(0, j) 926 | }) 927 | $device.taptic(2) 928 | return 929 | } 930 | } 931 | } 932 | 933 | const filename = $prefs.get('filename') 934 | let proxies = $cache.get(PROXIES) 935 | let raw = proxies.map(p => p.raw).join('\n\n') 936 | let t = $cache.get(TEXT) 937 | t = t.replace(filePartReg('Proxy'), `$1$2\n${raw}\n$3`) 938 | 939 | let exportWith = $prefs.get('export') 940 | if (exportWith === 0) { 941 | $share.sheet([filename, $data({ string: t })]) 942 | } else { 943 | const SCHEMES = ["surge3", "surge", "surge-enterprise"] 944 | const schemeIdx = $prefs.get("scheme") 945 | let server = $server.new() 946 | server.addHandler({ 947 | response: _ => { 948 | return { 949 | type: "data", 950 | props: { 951 | text: t 952 | } 953 | } 954 | } 955 | }) 956 | let randomPort = Math.round(Math.random() * 30000 + 30000) 957 | server.start({ port: randomPort }) 958 | let serverUrl = `http://127.0.0.1:${randomPort}/${filename}` 959 | let testResp = await $http.get(serverUrl) 960 | if (testResp.response.statusCode === 200) { 961 | let surgeScheme = `${SCHEMES[schemeIdx]}:///install-config?url=${encodeURIComponent(serverUrl)}` 962 | $app.openURL(surgeScheme) 963 | $app.listen({ 964 | resume: () => { 965 | server && server.stop() 966 | } 967 | }) 968 | } else { 969 | $ui.alert('Server Error, please try again or use Share Sheet to export!') 970 | } 971 | 972 | $delay(10, () => { 973 | server && server.stop() 974 | }) 975 | } 976 | } 977 | 978 | module.exports = { 979 | renderUI 980 | } -------------------------------------------------------------------------------- /scripts/diff.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This library was modified by Harrison Liddiard. The source code to this 3 | * modified version can be found at https://github.com/liddiard/google-diff/. 4 | * The original source code can be found at 5 | * http://code.google.com/p/google-diff-match-patch/. This unofficial fork is 6 | * not maintained by or affiliated with Google Inc. The original attribution 7 | * and licensing information follows. 8 | */ 9 | 10 | /** 11 | * Diff Match and Patch 12 | * 13 | * Copyright 2006 Google Inc. 14 | * http://code.google.com/p/google-diff-match-patch/ 15 | * 16 | * Licensed under the Apache License, Version 2.0 (the "License"); 17 | * you may not use this file except in compliance with the License. 18 | * You may obtain a copy of the License at 19 | * 20 | * http://www.apache.org/licenses/LICENSE-2.0 21 | * 22 | * Unless required by applicable law or agreed to in writing, software 23 | * distributed under the License is distributed on an "AS IS" BASIS, 24 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 25 | * See the License for the specific language governing permissions and 26 | * limitations under the License. 27 | */ 28 | 29 | /** 30 | * @fileoverview Computes the difference between two texts to create a patch. 31 | * Applies the patch onto another text, allowing for errors. 32 | * @author fraser@google.com (Neil Fraser) 33 | */ 34 | 35 | /** 36 | * Class containing the diff. 37 | * @constructor 38 | */ 39 | function diff(options) { 40 | var options = options || {}; 41 | 42 | // Defaults. 43 | // Redefine these in your program to override the defaults. 44 | 45 | // Number of seconds to map a diff before giving up (0 for infinity). 46 | this.Timeout = options.timeout || 1.0; 47 | // Cost of an empty edit operation in terms of edit characters. 48 | this.EditCost = options.editCost || 4; 49 | } 50 | 51 | 52 | // DIFF FUNCTIONS 53 | 54 | 55 | /** 56 | * The data structure representing a diff is an array of tuples: 57 | * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] 58 | * which means: delete 'Hello', add 'Goodbye' and keep ' world.' 59 | */ 60 | var DIFF_DELETE = -1; 61 | var DIFF_INSERT = 1; 62 | var DIFF_EQUAL = 0; 63 | 64 | /** @typedef {{0: number, 1: string}} */ 65 | diff.Diff; 66 | 67 | 68 | /** 69 | * Find the differences between two texts. Simplifies the problem by stripping 70 | * any common prefix or suffix off the texts before diffing. 71 | * @param {string} text1 Old string to be diffed. 72 | * @param {string} text2 New string to be diffed. 73 | * @param {boolean=} opt_checklines Optional speedup flag. If present and false, 74 | * then don't run a line-level diff first to identify the changed areas. 75 | * Defaults to true, which does a faster, slightly less optimal diff. 76 | * @param {number} opt_deadline Optional time when the diff should be complete 77 | * by. Used internally for recursive calls. Users should set DiffTimeout 78 | * instead. 79 | * @return {!Array.} Array of diff tuples. 80 | */ 81 | diff.prototype.main = function(text1, text2, opt_checklines, 82 | opt_deadline) { 83 | // Set a deadline by which time the diff must be complete. 84 | if (typeof opt_deadline == 'undefined') { 85 | if (this.Timeout <= 0) { 86 | opt_deadline = Number.MAX_VALUE; 87 | } else { 88 | opt_deadline = (new Date).getTime() + this.Timeout * 1000; 89 | } 90 | } 91 | var deadline = opt_deadline; 92 | 93 | // Check for null inputs. 94 | if (text1 == null || text2 == null) { 95 | throw new Error('Null input. (diff_main)'); 96 | } 97 | 98 | // Check for equality (speedup). 99 | if (text1 == text2) { 100 | if (text1) { 101 | return [[DIFF_EQUAL, text1]]; 102 | } 103 | return []; 104 | } 105 | 106 | if (typeof opt_checklines == 'undefined') { 107 | opt_checklines = true; 108 | } 109 | var checklines = opt_checklines; 110 | 111 | // Trim off common prefix (speedup). 112 | var commonlength = this.commonPrefix(text1, text2); 113 | var commonprefix = text1.substring(0, commonlength); 114 | text1 = text1.substring(commonlength); 115 | text2 = text2.substring(commonlength); 116 | 117 | // Trim off common suffix (speedup). 118 | commonlength = this.commonSuffix(text1, text2); 119 | var commonsuffix = text1.substring(text1.length - commonlength); 120 | text1 = text1.substring(0, text1.length - commonlength); 121 | text2 = text2.substring(0, text2.length - commonlength); 122 | 123 | // Compute the diff on the middle block. 124 | var diffs = this.compute_(text1, text2, checklines, deadline); 125 | 126 | // Restore the prefix and suffix. 127 | if (commonprefix) { 128 | diffs.unshift([DIFF_EQUAL, commonprefix]); 129 | } 130 | if (commonsuffix) { 131 | diffs.push([DIFF_EQUAL, commonsuffix]); 132 | } 133 | this.cleanupMerge(diffs); 134 | return diffs; 135 | }; 136 | 137 | 138 | /** 139 | * Find the differences between two texts. Assumes that the texts do not 140 | * have any common prefix or suffix. 141 | * @param {string} text1 Old string to be diffed. 142 | * @param {string} text2 New string to be diffed. 143 | * @param {boolean} checklines Speedup flag. If false, then don't run a 144 | * line-level diff first to identify the changed areas. 145 | * If true, then run a faster, slightly less optimal diff. 146 | * @param {number} deadline Time when the diff should be complete by. 147 | * @return {!Array.} Array of diff tuples. 148 | * @private 149 | */ 150 | diff.prototype.compute_ = function(text1, text2, checklines, 151 | deadline) { 152 | var diffs; 153 | 154 | if (!text1) { 155 | // Just add some text (speedup). 156 | return [[DIFF_INSERT, text2]]; 157 | } 158 | 159 | if (!text2) { 160 | // Just delete some text (speedup). 161 | return [[DIFF_DELETE, text1]]; 162 | } 163 | 164 | var longtext = text1.length > text2.length ? text1 : text2; 165 | var shorttext = text1.length > text2.length ? text2 : text1; 166 | var i = longtext.indexOf(shorttext); 167 | if (i != -1) { 168 | // Shorter text is inside the longer text (speedup). 169 | diffs = [[DIFF_INSERT, longtext.substring(0, i)], 170 | [DIFF_EQUAL, shorttext], 171 | [DIFF_INSERT, longtext.substring(i + shorttext.length)]]; 172 | // Swap insertions for deletions if diff is reversed. 173 | if (text1.length > text2.length) { 174 | diffs[0][0] = diffs[2][0] = DIFF_DELETE; 175 | } 176 | return diffs; 177 | } 178 | 179 | if (shorttext.length == 1) { 180 | // Single character string. 181 | // After the previous speedup, the character can't be an equality. 182 | return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]]; 183 | } 184 | 185 | // Check to see if the problem can be split in two. 186 | var hm = this.halfMatch_(text1, text2); 187 | if (hm) { 188 | // A half-match was found, sort out the return data. 189 | var text1_a = hm[0]; 190 | var text1_b = hm[1]; 191 | var text2_a = hm[2]; 192 | var text2_b = hm[3]; 193 | var mid_common = hm[4]; 194 | // Send both pairs off for separate processing. 195 | var diffs_a = this.main(text1_a, text2_a, checklines, deadline); 196 | var diffs_b = this.main(text1_b, text2_b, checklines, deadline); 197 | // Merge the results. 198 | return diffs_a.concat([[DIFF_EQUAL, mid_common]], diffs_b); 199 | } 200 | 201 | if (checklines && text1.length > 100 && text2.length > 100) { 202 | return this.lineMode_(text1, text2, deadline); 203 | } 204 | 205 | return this.bisect_(text1, text2, deadline); 206 | }; 207 | 208 | 209 | /** 210 | * Do a quick line-level diff on both strings, then rediff the parts for 211 | * greater accuracy. 212 | * This speedup can produce non-minimal diffs. 213 | * @param {string} text1 Old string to be diffed. 214 | * @param {string} text2 New string to be diffed. 215 | * @param {number} deadline Time when the diff should be complete by. 216 | * @return {!Array.} Array of diff tuples. 217 | * @private 218 | */ 219 | diff.prototype.lineMode_ = function(text1, text2, deadline) { 220 | // Scan the text on a line-by-line basis first. 221 | var a = this.linesToChars_(text1, text2); 222 | text1 = a.chars1; 223 | text2 = a.chars2; 224 | var linearray = a.lineArray; 225 | 226 | var diffs = this.main(text1, text2, false, deadline); 227 | 228 | // Convert the diff back to original text. 229 | this.charsToLines_(diffs, linearray); 230 | // Eliminate freak matches (e.g. blank lines) 231 | this.cleanupSemantic(diffs); 232 | 233 | // Rediff any replacement blocks, this time character-by-character. 234 | // Add a dummy entry at the end. 235 | diffs.push([DIFF_EQUAL, '']); 236 | var pointer = 0; 237 | var count_delete = 0; 238 | var count_insert = 0; 239 | var text_delete = ''; 240 | var text_insert = ''; 241 | while (pointer < diffs.length) { 242 | switch (diffs[pointer][0]) { 243 | case DIFF_INSERT: 244 | count_insert++; 245 | text_insert += diffs[pointer][1]; 246 | break; 247 | case DIFF_DELETE: 248 | count_delete++; 249 | text_delete += diffs[pointer][1]; 250 | break; 251 | case DIFF_EQUAL: 252 | // Upon reaching an equality, check for prior redundancies. 253 | if (count_delete >= 1 && count_insert >= 1) { 254 | // Delete the offending records and add the merged ones. 255 | diffs.splice(pointer - count_delete - count_insert, 256 | count_delete + count_insert); 257 | pointer = pointer - count_delete - count_insert; 258 | var a = this.main(text_delete, text_insert, false, deadline); 259 | for (var j = a.length - 1; j >= 0; j--) { 260 | diffs.splice(pointer, 0, a[j]); 261 | } 262 | pointer = pointer + a.length; 263 | } 264 | count_insert = 0; 265 | count_delete = 0; 266 | text_delete = ''; 267 | text_insert = ''; 268 | break; 269 | } 270 | pointer++; 271 | } 272 | diffs.pop(); // Remove the dummy entry at the end. 273 | 274 | return diffs; 275 | }; 276 | 277 | 278 | /** 279 | * Find the 'middle snake' of a diff, split the problem in two 280 | * and return the recursively constructed diff. 281 | * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. 282 | * @param {string} text1 Old string to be diffed. 283 | * @param {string} text2 New string to be diffed. 284 | * @param {number} deadline Time at which to bail if not yet complete. 285 | * @return {!Array.} Array of diff tuples. 286 | * @private 287 | */ 288 | diff.prototype.bisect_ = function(text1, text2, deadline) { 289 | // Cache the text lengths to prevent multiple calls. 290 | var text1_length = text1.length; 291 | var text2_length = text2.length; 292 | var max_d = Math.ceil((text1_length + text2_length) / 2); 293 | var v_offset = max_d; 294 | var v_length = 2 * max_d; 295 | var v1 = new Array(v_length); 296 | var v2 = new Array(v_length); 297 | // Setting all elements to -1 is faster in Chrome & Firefox than mixing 298 | // integers and undefined. 299 | for (var x = 0; x < v_length; x++) { 300 | v1[x] = -1; 301 | v2[x] = -1; 302 | } 303 | v1[v_offset + 1] = 0; 304 | v2[v_offset + 1] = 0; 305 | var delta = text1_length - text2_length; 306 | // If the total number of characters is odd, then the front path will collide 307 | // with the reverse path. 308 | var front = (delta % 2 != 0); 309 | // Offsets for start and end of k loop. 310 | // Prevents mapping of space beyond the grid. 311 | var k1start = 0; 312 | var k1end = 0; 313 | var k2start = 0; 314 | var k2end = 0; 315 | for (var d = 0; d < max_d; d++) { 316 | // Bail out if deadline is reached. 317 | if ((new Date()).getTime() > deadline) { 318 | break; 319 | } 320 | 321 | // Walk the front path one step. 322 | for (var k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { 323 | var k1_offset = v_offset + k1; 324 | var x1; 325 | if (k1 == -d || (k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1])) { 326 | x1 = v1[k1_offset + 1]; 327 | } else { 328 | x1 = v1[k1_offset - 1] + 1; 329 | } 330 | var y1 = x1 - k1; 331 | while (x1 < text1_length && y1 < text2_length && 332 | text1.charAt(x1) == text2.charAt(y1)) { 333 | x1++; 334 | y1++; 335 | } 336 | v1[k1_offset] = x1; 337 | if (x1 > text1_length) { 338 | // Ran off the right of the graph. 339 | k1end += 2; 340 | } else if (y1 > text2_length) { 341 | // Ran off the bottom of the graph. 342 | k1start += 2; 343 | } else if (front) { 344 | var k2_offset = v_offset + delta - k1; 345 | if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) { 346 | // Mirror x2 onto top-left coordinate system. 347 | var x2 = text1_length - v2[k2_offset]; 348 | if (x1 >= x2) { 349 | // Overlap detected. 350 | return this.bisectSplit_(text1, text2, x1, y1, deadline); 351 | } 352 | } 353 | } 354 | } 355 | 356 | // Walk the reverse path one step. 357 | for (var k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { 358 | var k2_offset = v_offset + k2; 359 | var x2; 360 | if (k2 == -d || (k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1])) { 361 | x2 = v2[k2_offset + 1]; 362 | } else { 363 | x2 = v2[k2_offset - 1] + 1; 364 | } 365 | var y2 = x2 - k2; 366 | while (x2 < text1_length && y2 < text2_length && 367 | text1.charAt(text1_length - x2 - 1) == 368 | text2.charAt(text2_length - y2 - 1)) { 369 | x2++; 370 | y2++; 371 | } 372 | v2[k2_offset] = x2; 373 | if (x2 > text1_length) { 374 | // Ran off the left of the graph. 375 | k2end += 2; 376 | } else if (y2 > text2_length) { 377 | // Ran off the top of the graph. 378 | k2start += 2; 379 | } else if (!front) { 380 | var k1_offset = v_offset + delta - k2; 381 | if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) { 382 | var x1 = v1[k1_offset]; 383 | var y1 = v_offset + x1 - k1_offset; 384 | // Mirror x2 onto top-left coordinate system. 385 | x2 = text1_length - x2; 386 | if (x1 >= x2) { 387 | // Overlap detected. 388 | return this.bisectSplit_(text1, text2, x1, y1, deadline); 389 | } 390 | } 391 | } 392 | } 393 | } 394 | // Diff took too long and hit the deadline or 395 | // number of diffs equals number of characters, no commonality at all. 396 | return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]]; 397 | }; 398 | 399 | 400 | /** 401 | * Given the location of the 'middle snake', split the diff in two parts 402 | * and recurse. 403 | * @param {string} text1 Old string to be diffed. 404 | * @param {string} text2 New string to be diffed. 405 | * @param {number} x Index of split point in text1. 406 | * @param {number} y Index of split point in text2. 407 | * @param {number} deadline Time at which to bail if not yet complete. 408 | * @return {!Array.} Array of diff tuples. 409 | * @private 410 | */ 411 | diff.prototype.bisectSplit_ = function(text1, text2, x, y, 412 | deadline) { 413 | var text1a = text1.substring(0, x); 414 | var text2a = text2.substring(0, y); 415 | var text1b = text1.substring(x); 416 | var text2b = text2.substring(y); 417 | 418 | // Compute both diffs serially. 419 | var diffs = this.main(text1a, text2a, false, deadline); 420 | var diffsb = this.main(text1b, text2b, false, deadline); 421 | 422 | return diffs.concat(diffsb); 423 | }; 424 | 425 | 426 | /** 427 | * Split two texts into an array of strings. Reduce the texts to a string of 428 | * hashes where each Unicode character represents one line. 429 | * @param {string} text1 First string. 430 | * @param {string} text2 Second string. 431 | * @return {{chars1: string, chars2: string, lineArray: !Array.}} 432 | * An object containing the encoded text1, the encoded text2 and 433 | * the array of unique strings. 434 | * The zeroth element of the array of unique strings is intentionally blank. 435 | * @private 436 | */ 437 | diff.prototype.linesToChars_ = function(text1, text2) { 438 | var lineArray = []; // e.g. lineArray[4] == 'Hello\n' 439 | var lineHash = {}; // e.g. lineHash['Hello\n'] == 4 440 | 441 | // '\x00' is a valid character, but various debuggers don't like it. 442 | // So we'll insert a junk entry to avoid generating a null character. 443 | lineArray[0] = ''; 444 | 445 | /** 446 | * Split a text into an array of strings. Reduce the texts to a string of 447 | * hashes where each Unicode character represents one line. 448 | * Modifies linearray and linehash through being a closure. 449 | * @param {string} text String to encode. 450 | * @return {string} Encoded string. 451 | * @private 452 | */ 453 | function diff_linesToCharsMunge_(text) { 454 | var chars = ''; 455 | // Walk the text, pulling out a substring for each line. 456 | // text.split('\n') would would temporarily double our memory footprint. 457 | // Modifying text would create many large strings to garbage collect. 458 | var lineStart = 0; 459 | var lineEnd = -1; 460 | // Keeping our own length variable is faster than looking it up. 461 | var lineArrayLength = lineArray.length; 462 | while (lineEnd < text.length - 1) { 463 | lineEnd = text.indexOf('\n', lineStart); 464 | if (lineEnd == -1) { 465 | lineEnd = text.length - 1; 466 | } 467 | var line = text.substring(lineStart, lineEnd + 1); 468 | lineStart = lineEnd + 1; 469 | 470 | if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) : 471 | (lineHash[line] !== undefined)) { 472 | chars += String.fromCharCode(lineHash[line]); 473 | } else { 474 | chars += String.fromCharCode(lineArrayLength); 475 | lineHash[line] = lineArrayLength; 476 | lineArray[lineArrayLength++] = line; 477 | } 478 | } 479 | return chars; 480 | } 481 | 482 | var chars1 = diff_linesToCharsMunge_(text1); 483 | var chars2 = diff_linesToCharsMunge_(text2); 484 | return {chars1: chars1, chars2: chars2, lineArray: lineArray}; 485 | }; 486 | 487 | 488 | /** 489 | * Rehydrate the text in a diff from a string of line hashes to real lines of 490 | * text. 491 | * @param {!Array.} diffs Array of diff tuples. 492 | * @param {!Array.} lineArray Array of unique strings. 493 | * @private 494 | */ 495 | diff.prototype.charsToLines_ = function(diffs, lineArray) { 496 | for (var x = 0; x < diffs.length; x++) { 497 | var chars = diffs[x][1]; 498 | var text = []; 499 | for (var y = 0; y < chars.length; y++) { 500 | text[y] = lineArray[chars.charCodeAt(y)]; 501 | } 502 | diffs[x][1] = text.join(''); 503 | } 504 | }; 505 | 506 | 507 | /** 508 | * Determine the common prefix of two strings. 509 | * @param {string} text1 First string. 510 | * @param {string} text2 Second string. 511 | * @return {number} The number of characters common to the start of each 512 | * string. 513 | */ 514 | diff.prototype.commonPrefix = function(text1, text2) { 515 | // Quick check for common null cases. 516 | if (!text1 || !text2 || text1.charAt(0) != text2.charAt(0)) { 517 | return 0; 518 | } 519 | // Binary search. 520 | // Performance analysis: http://neil.fraser.name/news/2007/10/09/ 521 | var pointermin = 0; 522 | var pointermax = Math.min(text1.length, text2.length); 523 | var pointermid = pointermax; 524 | var pointerstart = 0; 525 | while (pointermin < pointermid) { 526 | if (text1.substring(pointerstart, pointermid) == 527 | text2.substring(pointerstart, pointermid)) { 528 | pointermin = pointermid; 529 | pointerstart = pointermin; 530 | } else { 531 | pointermax = pointermid; 532 | } 533 | pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); 534 | } 535 | return pointermid; 536 | }; 537 | 538 | 539 | /** 540 | * Determine the common suffix of two strings. 541 | * @param {string} text1 First string. 542 | * @param {string} text2 Second string. 543 | * @return {number} The number of characters common to the end of each string. 544 | */ 545 | diff.prototype.commonSuffix = function(text1, text2) { 546 | // Quick check for common null cases. 547 | if (!text1 || !text2 || 548 | text1.charAt(text1.length - 1) != text2.charAt(text2.length - 1)) { 549 | return 0; 550 | } 551 | // Binary search. 552 | // Performance analysis: http://neil.fraser.name/news/2007/10/09/ 553 | var pointermin = 0; 554 | var pointermax = Math.min(text1.length, text2.length); 555 | var pointermid = pointermax; 556 | var pointerend = 0; 557 | while (pointermin < pointermid) { 558 | if (text1.substring(text1.length - pointermid, text1.length - pointerend) == 559 | text2.substring(text2.length - pointermid, text2.length - pointerend)) { 560 | pointermin = pointermid; 561 | pointerend = pointermin; 562 | } else { 563 | pointermax = pointermid; 564 | } 565 | pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); 566 | } 567 | return pointermid; 568 | }; 569 | 570 | 571 | /** 572 | * Determine if the suffix of one string is the prefix of another. 573 | * @param {string} text1 First string. 574 | * @param {string} text2 Second string. 575 | * @return {number} The number of characters common to the end of the first 576 | * string and the start of the second string. 577 | * @private 578 | */ 579 | diff.prototype.commonOverlap_ = function(text1, text2) { 580 | // Cache the text lengths to prevent multiple calls. 581 | var text1_length = text1.length; 582 | var text2_length = text2.length; 583 | // Eliminate the null case. 584 | if (text1_length == 0 || text2_length == 0) { 585 | return 0; 586 | } 587 | // Truncate the longer string. 588 | if (text1_length > text2_length) { 589 | text1 = text1.substring(text1_length - text2_length); 590 | } else if (text1_length < text2_length) { 591 | text2 = text2.substring(0, text1_length); 592 | } 593 | var text_length = Math.min(text1_length, text2_length); 594 | // Quick check for the worst case. 595 | if (text1 == text2) { 596 | return text_length; 597 | } 598 | 599 | // Start by looking for a single character match 600 | // and increase length until no match is found. 601 | // Performance analysis: http://neil.fraser.name/news/2010/11/04/ 602 | var best = 0; 603 | var length = 1; 604 | while (true) { 605 | var pattern = text1.substring(text_length - length); 606 | var found = text2.indexOf(pattern); 607 | if (found == -1) { 608 | return best; 609 | } 610 | length += found; 611 | if (found == 0 || text1.substring(text_length - length) == 612 | text2.substring(0, length)) { 613 | best = length; 614 | length++; 615 | } 616 | } 617 | }; 618 | 619 | 620 | /** 621 | * Do the two texts share a substring which is at least half the length of the 622 | * longer text? 623 | * This speedup can produce non-minimal diffs. 624 | * @param {string} text1 First string. 625 | * @param {string} text2 Second string. 626 | * @return {Array.} Five element Array, containing the prefix of 627 | * text1, the suffix of text1, the prefix of text2, the suffix of 628 | * text2 and the common middle. Or null if there was no match. 629 | * @private 630 | */ 631 | diff.prototype.halfMatch_ = function(text1, text2) { 632 | if (this.Timeout <= 0) { 633 | // Don't risk returning a non-optimal diff if we have unlimited time. 634 | return null; 635 | } 636 | var longtext = text1.length > text2.length ? text1 : text2; 637 | var shorttext = text1.length > text2.length ? text2 : text1; 638 | if (longtext.length < 4 || shorttext.length * 2 < longtext.length) { 639 | return null; // Pointless. 640 | } 641 | var dmp = this; // 'this' becomes 'window' in a closure. 642 | 643 | /** 644 | * Does a substring of shorttext exist within longtext such that the substring 645 | * is at least half the length of longtext? 646 | * Closure, but does not reference any external variables. 647 | * @param {string} longtext Longer string. 648 | * @param {string} shorttext Shorter string. 649 | * @param {number} i Start index of quarter length substring within longtext. 650 | * @return {Array.} Five element Array, containing the prefix of 651 | * longtext, the suffix of longtext, the prefix of shorttext, the suffix 652 | * of shorttext and the common middle. Or null if there was no match. 653 | * @private 654 | */ 655 | function diff_halfMatchI_(longtext, shorttext, i) { 656 | // Start with a 1/4 length substring at position i as a seed. 657 | var seed = longtext.substring(i, i + Math.floor(longtext.length / 4)); 658 | var j = -1; 659 | var best_common = ''; 660 | var best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b; 661 | while ((j = shorttext.indexOf(seed, j + 1)) != -1) { 662 | var prefixLength = dmp.commonPrefix(longtext.substring(i), 663 | shorttext.substring(j)); 664 | var suffixLength = dmp.commonSuffix(longtext.substring(0, i), 665 | shorttext.substring(0, j)); 666 | if (best_common.length < suffixLength + prefixLength) { 667 | best_common = shorttext.substring(j - suffixLength, j) + 668 | shorttext.substring(j, j + prefixLength); 669 | best_longtext_a = longtext.substring(0, i - suffixLength); 670 | best_longtext_b = longtext.substring(i + prefixLength); 671 | best_shorttext_a = shorttext.substring(0, j - suffixLength); 672 | best_shorttext_b = shorttext.substring(j + prefixLength); 673 | } 674 | } 675 | if (best_common.length * 2 >= longtext.length) { 676 | return [best_longtext_a, best_longtext_b, 677 | best_shorttext_a, best_shorttext_b, best_common]; 678 | } else { 679 | return null; 680 | } 681 | } 682 | 683 | // First check if the second quarter is the seed for a half-match. 684 | var hm1 = diff_halfMatchI_(longtext, shorttext, 685 | Math.ceil(longtext.length / 4)); 686 | // Check again based on the third quarter. 687 | var hm2 = diff_halfMatchI_(longtext, shorttext, 688 | Math.ceil(longtext.length / 2)); 689 | var hm; 690 | if (!hm1 && !hm2) { 691 | return null; 692 | } else if (!hm2) { 693 | hm = hm1; 694 | } else if (!hm1) { 695 | hm = hm2; 696 | } else { 697 | // Both matched. Select the longest. 698 | hm = hm1[4].length > hm2[4].length ? hm1 : hm2; 699 | } 700 | 701 | // A half-match was found, sort out the return data. 702 | var text1_a, text1_b, text2_a, text2_b; 703 | if (text1.length > text2.length) { 704 | text1_a = hm[0]; 705 | text1_b = hm[1]; 706 | text2_a = hm[2]; 707 | text2_b = hm[3]; 708 | } else { 709 | text2_a = hm[0]; 710 | text2_b = hm[1]; 711 | text1_a = hm[2]; 712 | text1_b = hm[3]; 713 | } 714 | var mid_common = hm[4]; 715 | return [text1_a, text1_b, text2_a, text2_b, mid_common]; 716 | }; 717 | 718 | 719 | /** 720 | * Reduce the number of edits by eliminating semantically trivial equalities. 721 | * @param {!Array.} diffs Array of diff tuples. 722 | */ 723 | diff.prototype.cleanupSemantic = function(diffs) { 724 | var changes = false; 725 | var equalities = []; // Stack of indices where equalities are found. 726 | var equalitiesLength = 0; // Keeping our own length var is faster in JS. 727 | /** @type {?string} */ 728 | var lastequality = null; 729 | // Always equal to diffs[equalities[equalitiesLength - 1]][1] 730 | var pointer = 0; // Index of current position. 731 | // Number of characters that changed prior to the equality. 732 | var length_insertions1 = 0; 733 | var length_deletions1 = 0; 734 | // Number of characters that changed after the equality. 735 | var length_insertions2 = 0; 736 | var length_deletions2 = 0; 737 | while (pointer < diffs.length) { 738 | if (diffs[pointer][0] == DIFF_EQUAL) { // Equality found. 739 | equalities[equalitiesLength++] = pointer; 740 | length_insertions1 = length_insertions2; 741 | length_deletions1 = length_deletions2; 742 | length_insertions2 = 0; 743 | length_deletions2 = 0; 744 | lastequality = diffs[pointer][1]; 745 | } else { // An insertion or deletion. 746 | if (diffs[pointer][0] == DIFF_INSERT) { 747 | length_insertions2 += diffs[pointer][1].length; 748 | } else { 749 | length_deletions2 += diffs[pointer][1].length; 750 | } 751 | // Eliminate an equality that is smaller or equal to the edits on both 752 | // sides of it. 753 | if (lastequality && (lastequality.length <= 754 | Math.max(length_insertions1, length_deletions1)) && 755 | (lastequality.length <= Math.max(length_insertions2, 756 | length_deletions2))) { 757 | // Duplicate record. 758 | diffs.splice(equalities[equalitiesLength - 1], 0, 759 | [DIFF_DELETE, lastequality]); 760 | // Change second copy to insert. 761 | diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; 762 | // Throw away the equality we just deleted. 763 | equalitiesLength--; 764 | // Throw away the previous equality (it needs to be reevaluated). 765 | equalitiesLength--; 766 | pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1; 767 | length_insertions1 = 0; // Reset the counters. 768 | length_deletions1 = 0; 769 | length_insertions2 = 0; 770 | length_deletions2 = 0; 771 | lastequality = null; 772 | changes = true; 773 | } 774 | } 775 | pointer++; 776 | } 777 | 778 | // Normalize the diff. 779 | if (changes) { 780 | this.cleanupMerge(diffs); 781 | } 782 | this.cleanupSemanticLossless(diffs); 783 | 784 | // Find any overlaps between deletions and insertions. 785 | // e.g: abcxxxxxxdef 786 | // -> abcxxxdef 787 | // e.g: xxxabcdefxxx 788 | // -> defxxxabc 789 | // Only extract an overlap if it is as big as the edit ahead or behind it. 790 | pointer = 1; 791 | while (pointer < diffs.length) { 792 | if (diffs[pointer - 1][0] == DIFF_DELETE && 793 | diffs[pointer][0] == DIFF_INSERT) { 794 | var deletion = diffs[pointer - 1][1]; 795 | var insertion = diffs[pointer][1]; 796 | var overlap_length1 = this.commonOverlap_(deletion, insertion); 797 | var overlap_length2 = this.commonOverlap_(insertion, deletion); 798 | if (overlap_length1 >= overlap_length2) { 799 | if (overlap_length1 >= deletion.length / 2 || 800 | overlap_length1 >= insertion.length / 2) { 801 | // Overlap found. Insert an equality and trim the surrounding edits. 802 | diffs.splice(pointer, 0, 803 | [DIFF_EQUAL, insertion.substring(0, overlap_length1)]); 804 | diffs[pointer - 1][1] = 805 | deletion.substring(0, deletion.length - overlap_length1); 806 | diffs[pointer + 1][1] = insertion.substring(overlap_length1); 807 | pointer++; 808 | } 809 | } else { 810 | if (overlap_length2 >= deletion.length / 2 || 811 | overlap_length2 >= insertion.length / 2) { 812 | // Reverse overlap found. 813 | // Insert an equality and swap and trim the surrounding edits. 814 | diffs.splice(pointer, 0, 815 | [DIFF_EQUAL, deletion.substring(0, overlap_length2)]); 816 | diffs[pointer - 1][0] = DIFF_INSERT; 817 | diffs[pointer - 1][1] = 818 | insertion.substring(0, insertion.length - overlap_length2); 819 | diffs[pointer + 1][0] = DIFF_DELETE; 820 | diffs[pointer + 1][1] = 821 | deletion.substring(overlap_length2); 822 | pointer++; 823 | } 824 | } 825 | pointer++; 826 | } 827 | pointer++; 828 | } 829 | }; 830 | 831 | 832 | /** 833 | * Look for single edits surrounded on both sides by equalities 834 | * which can be shifted sideways to align the edit to a word boundary. 835 | * e.g: The cat came. -> The cat came. 836 | * @param {!Array.} diffs Array of diff tuples. 837 | */ 838 | diff.prototype.cleanupSemanticLossless = function(diffs) { 839 | /** 840 | * Given two strings, compute a score representing whether the internal 841 | * boundary falls on logical boundaries. 842 | * Scores range from 6 (best) to 0 (worst). 843 | * Closure, but does not reference any external variables. 844 | * @param {string} one First string. 845 | * @param {string} two Second string. 846 | * @return {number} The score. 847 | * @private 848 | */ 849 | function diff_cleanupSemanticScore_(one, two) { 850 | if (!one || !two) { 851 | // Edges are the best. 852 | return 6; 853 | } 854 | 855 | // Each port of this function behaves slightly differently due to 856 | // subtle differences in each language's definition of things like 857 | // 'whitespace'. Since this function's purpose is largely cosmetic, 858 | // the choice has been made to use each language's native features 859 | // rather than force total conformity. 860 | var char1 = one.charAt(one.length - 1); 861 | var char2 = two.charAt(0); 862 | var nonAlphaNumeric1 = char1.match(diff.nonAlphaNumericRegex_); 863 | var nonAlphaNumeric2 = char2.match(diff.nonAlphaNumericRegex_); 864 | var whitespace1 = nonAlphaNumeric1 && 865 | char1.match(diff.whitespaceRegex_); 866 | var whitespace2 = nonAlphaNumeric2 && 867 | char2.match(diff.whitespaceRegex_); 868 | var lineBreak1 = whitespace1 && 869 | char1.match(diff.linebreakRegex_); 870 | var lineBreak2 = whitespace2 && 871 | char2.match(diff.linebreakRegex_); 872 | var blankLine1 = lineBreak1 && 873 | one.match(diff.blanklineEndRegex_); 874 | var blankLine2 = lineBreak2 && 875 | two.match(diff.blanklineStartRegex_); 876 | 877 | if (blankLine1 || blankLine2) { 878 | // Five points for blank lines. 879 | return 5; 880 | } else if (lineBreak1 || lineBreak2) { 881 | // Four points for line breaks. 882 | return 4; 883 | } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) { 884 | // Three points for end of sentences. 885 | return 3; 886 | } else if (whitespace1 || whitespace2) { 887 | // Two points for whitespace. 888 | return 2; 889 | } else if (nonAlphaNumeric1 || nonAlphaNumeric2) { 890 | // One point for non-alphanumeric. 891 | return 1; 892 | } 893 | return 0; 894 | } 895 | 896 | var pointer = 1; 897 | // Intentionally ignore the first and last element (don't need checking). 898 | while (pointer < diffs.length - 1) { 899 | if (diffs[pointer - 1][0] == DIFF_EQUAL && 900 | diffs[pointer + 1][0] == DIFF_EQUAL) { 901 | // This is a single edit surrounded by equalities. 902 | var equality1 = diffs[pointer - 1][1]; 903 | var edit = diffs[pointer][1]; 904 | var equality2 = diffs[pointer + 1][1]; 905 | 906 | // First, shift the edit as far left as possible. 907 | var commonOffset = this.commonSuffix(equality1, edit); 908 | if (commonOffset) { 909 | var commonString = edit.substring(edit.length - commonOffset); 910 | equality1 = equality1.substring(0, equality1.length - commonOffset); 911 | edit = commonString + edit.substring(0, edit.length - commonOffset); 912 | equality2 = commonString + equality2; 913 | } 914 | 915 | // Second, step character by character right, looking for the best fit. 916 | var bestEquality1 = equality1; 917 | var bestEdit = edit; 918 | var bestEquality2 = equality2; 919 | var bestScore = diff_cleanupSemanticScore_(equality1, edit) + 920 | diff_cleanupSemanticScore_(edit, equality2); 921 | while (edit.charAt(0) === equality2.charAt(0)) { 922 | equality1 += edit.charAt(0); 923 | edit = edit.substring(1) + equality2.charAt(0); 924 | equality2 = equality2.substring(1); 925 | var score = diff_cleanupSemanticScore_(equality1, edit) + 926 | diff_cleanupSemanticScore_(edit, equality2); 927 | // The >= encourages trailing rather than leading whitespace on edits. 928 | if (score >= bestScore) { 929 | bestScore = score; 930 | bestEquality1 = equality1; 931 | bestEdit = edit; 932 | bestEquality2 = equality2; 933 | } 934 | } 935 | 936 | if (diffs[pointer - 1][1] != bestEquality1) { 937 | // We have an improvement, save it back to the diff. 938 | if (bestEquality1) { 939 | diffs[pointer - 1][1] = bestEquality1; 940 | } else { 941 | diffs.splice(pointer - 1, 1); 942 | pointer--; 943 | } 944 | diffs[pointer][1] = bestEdit; 945 | if (bestEquality2) { 946 | diffs[pointer + 1][1] = bestEquality2; 947 | } else { 948 | diffs.splice(pointer + 1, 1); 949 | pointer--; 950 | } 951 | } 952 | } 953 | pointer++; 954 | } 955 | }; 956 | 957 | // Define some regex patterns for matching boundaries. 958 | diff.nonAlphaNumericRegex_ = /[^a-zA-Z0-9]/; 959 | diff.whitespaceRegex_ = /\s/; 960 | diff.linebreakRegex_ = /[\r\n]/; 961 | diff.blanklineEndRegex_ = /\n\r?\n$/; 962 | diff.blanklineStartRegex_ = /^\r?\n\r?\n/; 963 | 964 | /** 965 | * Reduce the number of edits by eliminating operationally trivial equalities. 966 | * @param {!Array.} diffs Array of diff tuples. 967 | */ 968 | diff.prototype.cleanupEfficiency = function(diffs) { 969 | var changes = false; 970 | var equalities = []; // Stack of indices where equalities are found. 971 | var equalitiesLength = 0; // Keeping our own length var is faster in JS. 972 | /** @type {?string} */ 973 | var lastequality = null; 974 | // Always equal to diffs[equalities[equalitiesLength - 1]][1] 975 | var pointer = 0; // Index of current position. 976 | // Is there an insertion operation before the last equality. 977 | var pre_ins = false; 978 | // Is there a deletion operation before the last equality. 979 | var pre_del = false; 980 | // Is there an insertion operation after the last equality. 981 | var post_ins = false; 982 | // Is there a deletion operation after the last equality. 983 | var post_del = false; 984 | while (pointer < diffs.length) { 985 | if (diffs[pointer][0] == DIFF_EQUAL) { // Equality found. 986 | if (diffs[pointer][1].length < this.EditCost && 987 | (post_ins || post_del)) { 988 | // Candidate found. 989 | equalities[equalitiesLength++] = pointer; 990 | pre_ins = post_ins; 991 | pre_del = post_del; 992 | lastequality = diffs[pointer][1]; 993 | } else { 994 | // Not a candidate, and can never become one. 995 | equalitiesLength = 0; 996 | lastequality = null; 997 | } 998 | post_ins = post_del = false; 999 | } else { // An insertion or deletion. 1000 | if (diffs[pointer][0] == DIFF_DELETE) { 1001 | post_del = true; 1002 | } else { 1003 | post_ins = true; 1004 | } 1005 | /* 1006 | * Five types to be split: 1007 | * ABXYCD 1008 | * AXCD 1009 | * ABXC 1010 | * AXCD 1011 | * ABXC 1012 | */ 1013 | if (lastequality && ((pre_ins && pre_del && post_ins && post_del) || 1014 | ((lastequality.length < this.EditCost / 2) && 1015 | (pre_ins + pre_del + post_ins + post_del) == 3))) { 1016 | // Duplicate record. 1017 | diffs.splice(equalities[equalitiesLength - 1], 0, 1018 | [DIFF_DELETE, lastequality]); 1019 | // Change second copy to insert. 1020 | diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; 1021 | equalitiesLength--; // Throw away the equality we just deleted; 1022 | lastequality = null; 1023 | if (pre_ins && pre_del) { 1024 | // No changes made which could affect previous entry, keep going. 1025 | post_ins = post_del = true; 1026 | equalitiesLength = 0; 1027 | } else { 1028 | equalitiesLength--; // Throw away the previous equality. 1029 | pointer = equalitiesLength > 0 ? 1030 | equalities[equalitiesLength - 1] : -1; 1031 | post_ins = post_del = false; 1032 | } 1033 | changes = true; 1034 | } 1035 | } 1036 | pointer++; 1037 | } 1038 | 1039 | if (changes) { 1040 | this.cleanupMerge(diffs); 1041 | } 1042 | }; 1043 | 1044 | 1045 | /** 1046 | * Reorder and merge like edit sections. Merge equalities. 1047 | * Any edit section can move as long as it doesn't cross an equality. 1048 | * @param {!Array.} diffs Array of diff tuples. 1049 | */ 1050 | diff.prototype.cleanupMerge = function(diffs) { 1051 | diffs.push([DIFF_EQUAL, '']); // Add a dummy entry at the end. 1052 | var pointer = 0; 1053 | var count_delete = 0; 1054 | var count_insert = 0; 1055 | var text_delete = ''; 1056 | var text_insert = ''; 1057 | var commonlength; 1058 | while (pointer < diffs.length) { 1059 | switch (diffs[pointer][0]) { 1060 | case DIFF_INSERT: 1061 | count_insert++; 1062 | text_insert += diffs[pointer][1]; 1063 | pointer++; 1064 | break; 1065 | case DIFF_DELETE: 1066 | count_delete++; 1067 | text_delete += diffs[pointer][1]; 1068 | pointer++; 1069 | break; 1070 | case DIFF_EQUAL: 1071 | // Upon reaching an equality, check for prior redundancies. 1072 | if (count_delete + count_insert > 1) { 1073 | if (count_delete !== 0 && count_insert !== 0) { 1074 | // Factor out any common prefixies. 1075 | commonlength = this.commonPrefix(text_insert, text_delete); 1076 | if (commonlength !== 0) { 1077 | if ((pointer - count_delete - count_insert) > 0 && 1078 | diffs[pointer - count_delete - count_insert - 1][0] == 1079 | DIFF_EQUAL) { 1080 | diffs[pointer - count_delete - count_insert - 1][1] += 1081 | text_insert.substring(0, commonlength); 1082 | } else { 1083 | diffs.splice(0, 0, [DIFF_EQUAL, 1084 | text_insert.substring(0, commonlength)]); 1085 | pointer++; 1086 | } 1087 | text_insert = text_insert.substring(commonlength); 1088 | text_delete = text_delete.substring(commonlength); 1089 | } 1090 | // Factor out any common suffixies. 1091 | commonlength = this.commonSuffix(text_insert, text_delete); 1092 | if (commonlength !== 0) { 1093 | diffs[pointer][1] = text_insert.substring(text_insert.length - 1094 | commonlength) + diffs[pointer][1]; 1095 | text_insert = text_insert.substring(0, text_insert.length - 1096 | commonlength); 1097 | text_delete = text_delete.substring(0, text_delete.length - 1098 | commonlength); 1099 | } 1100 | } 1101 | // Delete the offending records and add the merged ones. 1102 | if (count_delete === 0) { 1103 | diffs.splice(pointer - count_insert, 1104 | count_delete + count_insert, [DIFF_INSERT, text_insert]); 1105 | } else if (count_insert === 0) { 1106 | diffs.splice(pointer - count_delete, 1107 | count_delete + count_insert, [DIFF_DELETE, text_delete]); 1108 | } else { 1109 | diffs.splice(pointer - count_delete - count_insert, 1110 | count_delete + count_insert, [DIFF_DELETE, text_delete], 1111 | [DIFF_INSERT, text_insert]); 1112 | } 1113 | pointer = pointer - count_delete - count_insert + 1114 | (count_delete ? 1 : 0) + (count_insert ? 1 : 0) + 1; 1115 | } else if (pointer !== 0 && diffs[pointer - 1][0] == DIFF_EQUAL) { 1116 | // Merge this equality with the previous one. 1117 | diffs[pointer - 1][1] += diffs[pointer][1]; 1118 | diffs.splice(pointer, 1); 1119 | } else { 1120 | pointer++; 1121 | } 1122 | count_insert = 0; 1123 | count_delete = 0; 1124 | text_delete = ''; 1125 | text_insert = ''; 1126 | break; 1127 | } 1128 | } 1129 | if (diffs[diffs.length - 1][1] === '') { 1130 | diffs.pop(); // Remove the dummy entry at the end. 1131 | } 1132 | 1133 | // Second pass: look for single edits surrounded on both sides by equalities 1134 | // which can be shifted sideways to eliminate an equality. 1135 | // e.g: ABAC -> ABAC 1136 | var changes = false; 1137 | pointer = 1; 1138 | // Intentionally ignore the first and last element (don't need checking). 1139 | while (pointer < diffs.length - 1) { 1140 | if (diffs[pointer - 1][0] == DIFF_EQUAL && 1141 | diffs[pointer + 1][0] == DIFF_EQUAL) { 1142 | // This is a single edit surrounded by equalities. 1143 | if (diffs[pointer][1].substring(diffs[pointer][1].length - 1144 | diffs[pointer - 1][1].length) == diffs[pointer - 1][1]) { 1145 | // Shift the edit over the previous equality. 1146 | diffs[pointer][1] = diffs[pointer - 1][1] + 1147 | diffs[pointer][1].substring(0, diffs[pointer][1].length - 1148 | diffs[pointer - 1][1].length); 1149 | diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1]; 1150 | diffs.splice(pointer - 1, 1); 1151 | changes = true; 1152 | } else if (diffs[pointer][1].substring(0, diffs[pointer + 1][1].length) == 1153 | diffs[pointer + 1][1]) { 1154 | // Shift the edit over the next equality. 1155 | diffs[pointer - 1][1] += diffs[pointer + 1][1]; 1156 | diffs[pointer][1] = 1157 | diffs[pointer][1].substring(diffs[pointer + 1][1].length) + 1158 | diffs[pointer + 1][1]; 1159 | diffs.splice(pointer + 1, 1); 1160 | changes = true; 1161 | } 1162 | } 1163 | pointer++; 1164 | } 1165 | // If shifts were made, the diff needs reordering and another shift sweep. 1166 | if (changes) { 1167 | this.cleanupMerge(diffs); 1168 | } 1169 | }; 1170 | 1171 | 1172 | /** 1173 | * loc is a location in text1, compute and return the equivalent location in 1174 | * text2. 1175 | * e.g. 'The cat' vs 'The big cat', 1->1, 5->8 1176 | * @param {!Array.} diffs Array of diff tuples. 1177 | * @param {number} loc Location within text1. 1178 | * @return {number} Location within text2. 1179 | */ 1180 | diff.prototype.xIndex = function(diffs, loc) { 1181 | var chars1 = 0; 1182 | var chars2 = 0; 1183 | var last_chars1 = 0; 1184 | var last_chars2 = 0; 1185 | var x; 1186 | for (x = 0; x < diffs.length; x++) { 1187 | if (diffs[x][0] !== DIFF_INSERT) { // Equality or deletion. 1188 | chars1 += diffs[x][1].length; 1189 | } 1190 | if (diffs[x][0] !== DIFF_DELETE) { // Equality or insertion. 1191 | chars2 += diffs[x][1].length; 1192 | } 1193 | if (chars1 > loc) { // Overshot the location. 1194 | break; 1195 | } 1196 | last_chars1 = chars1; 1197 | last_chars2 = chars2; 1198 | } 1199 | // Was the location was deleted? 1200 | if (diffs.length != x && diffs[x][0] === DIFF_DELETE) { 1201 | return last_chars2; 1202 | } 1203 | // Add the remaining character length. 1204 | return last_chars2 + (loc - last_chars1); 1205 | }; 1206 | 1207 | 1208 | /** 1209 | * Convert a diff array into a pretty HTML report. 1210 | * @param {!Array.} diffs Array of diff tuples. 1211 | * @return {string} HTML representation. 1212 | */ 1213 | diff.prototype.prettyHtml = function(diffs) { 1214 | var html = []; 1215 | var pattern_amp = /&/g; 1216 | var pattern_lt = //g; 1218 | var pattern_br = /\n/g; 1219 | for (var x = 0; x < diffs.length; x++) { 1220 | var op = diffs[x][0]; // Operation (insert, delete, equal) 1221 | var data = diffs[x][1]; // Text of change. 1222 | var text = data.replace(pattern_amp, '&').replace(pattern_lt, '<') 1223 | .replace(pattern_gt, '>').replace(pattern_br, '
'); 1224 | switch (op) { 1225 | case DIFF_INSERT: 1226 | html[x] = '' + text + ''; 1227 | break; 1228 | case DIFF_DELETE: 1229 | html[x] = '' + text + ''; 1230 | break; 1231 | case DIFF_EQUAL: 1232 | html[x] = '' + text + ''; 1233 | break; 1234 | } 1235 | } 1236 | return html.join(''); 1237 | }; 1238 | 1239 | 1240 | /** 1241 | * Compute and return the source text (all equalities and deletions). 1242 | * @param {!Array.} diffs Array of diff tuples. 1243 | * @return {string} Source text. 1244 | */ 1245 | diff.prototype.text1 = function(diffs) { 1246 | var text = []; 1247 | for (var x = 0; x < diffs.length; x++) { 1248 | if (diffs[x][0] !== DIFF_INSERT) { 1249 | text[x] = diffs[x][1]; 1250 | } 1251 | } 1252 | return text.join(''); 1253 | }; 1254 | 1255 | 1256 | /** 1257 | * Compute and return the destination text (all equalities and insertions). 1258 | * @param {!Array.} diffs Array of diff tuples. 1259 | * @return {string} Destination text. 1260 | */ 1261 | diff.prototype.text2 = function(diffs) { 1262 | var text = []; 1263 | for (var x = 0; x < diffs.length; x++) { 1264 | if (diffs[x][0] !== DIFF_DELETE) { 1265 | text[x] = diffs[x][1]; 1266 | } 1267 | } 1268 | return text.join(''); 1269 | }; 1270 | 1271 | 1272 | /** 1273 | * Compute the Levenshtein distance; the number of inserted, deleted or 1274 | * substituted characters. 1275 | * @param {!Array.} diffs Array of diff tuples. 1276 | * @return {number} Number of changes. 1277 | */ 1278 | diff.prototype.levenshtein = function(diffs) { 1279 | var levenshtein = 0; 1280 | var insertions = 0; 1281 | var deletions = 0; 1282 | for (var x = 0; x < diffs.length; x++) { 1283 | var op = diffs[x][0]; 1284 | var data = diffs[x][1]; 1285 | switch (op) { 1286 | case DIFF_INSERT: 1287 | insertions += data.length; 1288 | break; 1289 | case DIFF_DELETE: 1290 | deletions += data.length; 1291 | break; 1292 | case DIFF_EQUAL: 1293 | // A deletion and an insertion is one substitution. 1294 | levenshtein += Math.max(insertions, deletions); 1295 | insertions = 0; 1296 | deletions = 0; 1297 | break; 1298 | } 1299 | } 1300 | levenshtein += Math.max(insertions, deletions); 1301 | return levenshtein; 1302 | }; 1303 | 1304 | 1305 | /** 1306 | * Crush the diff into an encoded string which describes the operations 1307 | * required to transform text1 into text2. 1308 | * E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. 1309 | * Operations are tab-separated. Inserted text is escaped using %xx notation. 1310 | * @param {!Array.} diffs Array of diff tuples. 1311 | * @return {string} Delta text. 1312 | */ 1313 | diff.prototype.toDelta = function(diffs) { 1314 | var text = []; 1315 | for (var x = 0; x < diffs.length; x++) { 1316 | switch (diffs[x][0]) { 1317 | case DIFF_INSERT: 1318 | text[x] = '+' + encodeURI(diffs[x][1]); 1319 | break; 1320 | case DIFF_DELETE: 1321 | text[x] = '-' + diffs[x][1].length; 1322 | break; 1323 | case DIFF_EQUAL: 1324 | text[x] = '=' + diffs[x][1].length; 1325 | break; 1326 | } 1327 | } 1328 | return text.join('\t').replace(/%20/g, ' '); 1329 | }; 1330 | 1331 | 1332 | /** 1333 | * Given the original text1, and an encoded string which describes the 1334 | * operations required to transform text1 into text2, compute the full diff. 1335 | * @param {string} text1 Source string for the diff. 1336 | * @param {string} delta Delta text. 1337 | * @return {!Array.} Array of diff tuples. 1338 | * @throws {!Error} If invalid input. 1339 | */ 1340 | diff.prototype.fromDelta = function(text1, delta) { 1341 | var diffs = []; 1342 | var diffsLength = 0; // Keeping our own length var is faster in JS. 1343 | var pointer = 0; // Cursor in text1 1344 | var tokens = delta.split(/\t/g); 1345 | for (var x = 0; x < tokens.length; x++) { 1346 | // Each token begins with a one character parameter which specifies the 1347 | // operation of this token (delete, insert, equality). 1348 | var param = tokens[x].substring(1); 1349 | switch (tokens[x].charAt(0)) { 1350 | case '+': 1351 | try { 1352 | diffs[diffsLength++] = [DIFF_INSERT, decodeURI(param)]; 1353 | } catch (ex) { 1354 | // Malformed URI sequence. 1355 | throw new Error('Illegal escape in diff_fromDelta: ' + param); 1356 | } 1357 | break; 1358 | case '-': 1359 | // Fall through. 1360 | case '=': 1361 | var n = parseInt(param, 10); 1362 | if (isNaN(n) || n < 0) { 1363 | throw new Error('Invalid number in diff_fromDelta: ' + param); 1364 | } 1365 | var text = text1.substring(pointer, pointer += n); 1366 | if (tokens[x].charAt(0) == '=') { 1367 | diffs[diffsLength++] = [DIFF_EQUAL, text]; 1368 | } else { 1369 | diffs[diffsLength++] = [DIFF_DELETE, text]; 1370 | } 1371 | break; 1372 | default: 1373 | // Blank tokens are ok (from a trailing \t). 1374 | // Anything else is an error. 1375 | if (tokens[x]) { 1376 | throw new Error('Invalid diff operation in diff_fromDelta: ' + 1377 | tokens[x]); 1378 | } 1379 | } 1380 | } 1381 | if (pointer != text1.length) { 1382 | throw new Error('Delta length (' + pointer + 1383 | ') does not equal source text length (' + text1.length + ').'); 1384 | } 1385 | return diffs; 1386 | }; 1387 | 1388 | 1389 | // Export these global variables so that they survive Google's JS compiler. 1390 | // In a browser, 'this' will be 'window'. 1391 | // Users of node.js should 'require' the uncompressed version since Google's 1392 | // JS compiler may break the following exports for non-browser environments. 1393 | this['diff'] = diff; 1394 | this['DIFF_DELETE'] = DIFF_DELETE; 1395 | this['DIFF_INSERT'] = DIFF_INSERT; 1396 | this['DIFF_EQUAL'] = DIFF_EQUAL; 1397 | 1398 | module.exports = diff; -------------------------------------------------------------------------------- /strings/en.strings: -------------------------------------------------------------------------------- 1 | "OK" = "OK"; 2 | "DONE" = "Done"; 3 | "HELLO_WORLD" = "Hello, World!"; 4 | -------------------------------------------------------------------------------- /strings/zh-Hans.strings: -------------------------------------------------------------------------------- 1 | "OK" = "好的"; 2 | "DONE" = "完成"; 3 | "HELLO_WORLD" = "你好,世界!"; 4 | --------------------------------------------------------------------------------