├── .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 |
--------------------------------------------------------------------------------