Update template after it has been created and populated and stuff.
16 | It automatically updates all its instances with the new template.
18 | Try editing the template. Add "{id}" to the end and see what happens.
20 | That node and all of its children will be used as a new template for whatever key you add after it. EG: SubtleTemplate whatever_key
16 | .
7 | You can only use this stylesheet to run MooTools JSSpec Runner.
8 | */
9 |
10 | /* @group Reset */
11 |
12 | * {
13 | margin: 0;
14 | padding: 0;
15 | }
16 |
17 | ul {
18 | list-style: none;
19 | }
20 |
21 | /* @end */
22 |
23 | /* @group Base */
24 |
25 | html {
26 | overflow: hidden;
27 | }
28 |
29 | body {
30 | font: 11px/1.5 Lucida Grande, Helvetica, Arial, sans-serif;
31 | background: #F3F1F1;
32 | color: #41464D;
33 | }
34 |
35 | body, #container {
36 | width: 100%;
37 | height: 100%;
38 | overflow: hidden;
39 | }
40 |
41 | a {
42 | text-decoration:none;
43 | }
44 |
45 | #title {
46 | position: absolute;
47 | top: 0;
48 | left: 0;
49 | width: 100%;
50 | padding: 5px 0;
51 | background: #aaa;
52 | background: #41464D;
53 | color: #F3F1F1;
54 | height: 30px;
55 | }
56 |
57 | #list {
58 | position: absolute;
59 | width: 30%;
60 | overflow-y: auto;
61 | overflow-x: hidden;
62 | top: 40px;
63 | left: 0;
64 | bottom: 0;
65 | /* height: expression(document.body.clientHeight-40);*/
66 | }
67 |
68 | #log {
69 | position: absolute;
70 | top: 40px;
71 | right: 0;
72 | bottom: 0;
73 | overflow-y: auto;
74 | overflow-x: hidden;
75 | width: 70%;
76 | /* height: expression(document.body.clientHeight-40);*/
77 | }
78 |
79 | span.spc {
80 | display: block;
81 | height: 16px;
82 | }
83 |
84 | #log-wrapper, #list-wrapper {
85 | overflow: hidden;
86 | padding: 4px 4px 0;
87 | background: #fff;
88 | }
89 |
90 | #log-wrapper {
91 | margin: 16px 16px 0 8px;
92 | }
93 |
94 | #list-wrapper {
95 | margin: 16px 8px 0 16px;
96 | }
97 |
98 | .success a:link, .success a:visited {
99 | color: #657528;
100 | }
101 |
102 | .exception a:link, .exception a:visited {
103 | color: #B33F3F;
104 | }
105 |
106 | a:link, a:visited {
107 | color: #528CE0;
108 | }
109 |
110 | a:hover, a:active {
111 | color: #41464D !important;
112 | cursor: pointer !important;
113 | }
114 |
115 | #title h1 {
116 | font: 25px/1.1 Arial, sans-serif;
117 | font-weight: bolder;
118 | float: left;
119 | margin: 1px 0 2px 20px;
120 | text-shadow: 0 2px 2px rgba(0,0,0,0.4);
121 | }
122 |
123 | #title h1 span {
124 | color: #D2E0E6;
125 | }
126 |
127 | #title ul li {
128 | font-weight: bold;
129 | font-size: 12px;
130 | float: right;
131 | margin: 10px 5px 0;
132 | }
133 |
134 | #title ul {
135 | margin-right: 20px;
136 | }
137 |
138 | h2 {
139 | font-size: 14px;
140 | background: #D0C8C8;
141 | color: #8A7575;
142 | margin-bottom: 4px;
143 | padding: 2px 5px;
144 | }
145 |
146 | h2#runner {
147 | cursor: pointer;
148 | background-color: #CFE773;
149 | color: #657528;
150 | }
151 |
152 | h2#runner:hover {
153 | color: #41464D;
154 | }
155 |
156 | h2#runner.disabled {
157 | color: #fff;
158 | background: #C8CBD0;
159 | cursor: default;
160 | }
161 |
162 | br {
163 | display: none;
164 | }
165 |
166 | h3 {
167 | font-size: 12px;
168 | padding: 3px 5px 1px;
169 | }
170 |
171 | h4 {
172 | font-size: 11px;
173 | background: #C8CBD0;
174 | padding: 3px 5px 1px;
175 | margin-bottom: 4px;
176 | }
177 |
178 | h3, h4 {
179 | cursor: default;
180 | }
181 |
182 | #list h3 {
183 | background: #D2E0E6;
184 | color: #528CE0;
185 | margin-bottom: 4px;
186 | }
187 |
188 | #log h3 {
189 | background: #D0C8C8;
190 | color: #8A7575;
191 | margin-bottom: 4px;
192 | }
193 |
194 | #log li div {
195 | overflow: hidden;
196 | padding: 0 4px 4px;
197 | }
198 |
199 | p {
200 | color: #575d67;
201 | }
202 |
203 | p.uri {
204 | float: right;
205 | margin-top: -15px;
206 | font-size: 10px;
207 | }
208 |
209 | p.left, p.uri {
210 | font-family: Monaco, Courier New, monospace;
211 | }
212 |
213 | p#footer {
214 | text-align: right;
215 | padding: 10px 16px 0 0;
216 | }
217 |
218 | /* @end */
219 |
220 | /* @group Success/Failure Colors */
221 |
222 | #log .exception h3 {
223 | color: #B33F3F;
224 | background: #EE9A9A;
225 | }
226 |
227 | #log .exception h4 {
228 | color: #B33F3F;
229 | background: #eed8d8;
230 | cursor: pointer;
231 | }
232 |
233 | #log .success h3 {
234 | background-color: #CFE773;
235 | color: #657528;
236 | }
237 |
238 | #log .success h4 {
239 | background-color: #e2e5d2;
240 | color: #657528;
241 | cursor: default;
242 | }
243 |
244 | #log .stub h4 {
245 | background-color: #e5e5e1;
246 | color: #a1a87e;
247 | cursor: default;
248 | }
249 |
250 | #list .exception h3 {
251 | color: #B33F3F;
252 | background: #eed8d8;
253 | }
254 |
255 | #list .success h3 {
256 | background-color: #e2e5d2;
257 | color: #657528;
258 | }
259 |
260 | a.rerun {
261 | font-size: 11px;
262 | color: #fff !important;
263 | }
264 |
265 | /* @end */
266 |
267 | /* @group Code */
268 |
269 | .number_value {
270 | color: red;
271 | }
272 |
273 | .string_value {
274 | color: green;
275 | }
276 |
277 | .regexp_value {
278 | color: olive;
279 | }
280 |
281 | .boolean_value {
282 | color: red;
283 | }
284 |
285 | .dom_value {
286 | color: purple;
287 | }
288 |
289 | .undefined_value, .null_value {
290 | color: gray;
291 | }
292 |
293 | /* @end */
--------------------------------------------------------------------------------
/Demos/SubtleTemplate.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 | SubtleTemplate
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | {data1}
15 | {data2}
16 | {data3}
17 |
18 |
19 |
20 |
21 |
22 |
23 | {data1}
24 | {data2}
25 | {data3}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
52 |
53 |
54 |
55 |
56 |
57 |
91 |
92 |
93 |
94 |
95 |
98 |
99 |
146 |
147 |
148 |
149 |
150 |
151 |
162 |
163 |
164 |
165 |
166 |
167 |
176 |
177 |
178 |
179 |
180 |
181 |
192 |
193 |
194 |
--------------------------------------------------------------------------------
/mootools-subtle-templates.tmproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | currentDocument
6 | Specs/Plugins/SubtleTemplate.js
7 | documents
8 |
9 |
10 | expanded
11 |
12 | name
13 | mootools-subtle-templates
14 | regexFolderFilter
15 | !.*/(\.[^/]*|CVS|_darcs|_MTN|\{arch\}|blib|.*~\.nib|.*\.(framework|app|pbproj|pbxproj|xcode(proj)?|bundle))$
16 | sourceDirectory
17 |
18 |
19 |
20 | fileHierarchyDrawerWidth
21 | 220
22 | metaData
23 |
24 | Demos/demo.css
25 |
26 | caret
27 |
28 | column
29 | 11
30 | line
31 | 3
32 |
33 | firstVisibleColumn
34 | 0
35 | firstVisibleLine
36 | 0
37 |
38 | Demos/demo.html
39 |
40 | caret
41 |
42 | column
43 | 0
44 | line
45 | 38
46 |
47 | firstVisibleColumn
48 | 0
49 | firstVisibleLine
50 | 0
51 |
52 | Demos/demo1.html
53 |
54 | caret
55 |
56 | column
57 | 0
58 | line
59 | 18
60 |
61 | firstVisibleColumn
62 | 0
63 | firstVisibleLine
64 | 0
65 |
66 | Demos/demo2.html
67 |
68 | caret
69 |
70 | column
71 | 0
72 | line
73 | 58
74 |
75 | firstVisibleColumn
76 | 0
77 | firstVisibleLine
78 | 0
79 |
80 | Demos/demo3.html
81 |
82 | caret
83 |
84 | column
85 | 0
86 | line
87 | 37
88 |
89 | columnSelection
90 |
91 | firstVisibleColumn
92 | 0
93 | firstVisibleLine
94 | 0
95 | selectFrom
96 |
97 | column
98 | 0
99 | line
100 | 32
101 |
102 | selectTo
103 |
104 | column
105 | 0
106 | line
107 | 37
108 |
109 |
110 | Demos/demo4.html
111 |
112 | caret
113 |
114 | column
115 | 21
116 | line
117 | 41
118 |
119 | firstVisibleColumn
120 | 0
121 | firstVisibleLine
122 | 0
123 |
124 | Demos/demo5.html
125 |
126 | caret
127 |
128 | column
129 | 5
130 | line
131 | 27
132 |
133 | firstVisibleColumn
134 | 0
135 | firstVisibleLine
136 | 0
137 |
138 | Demos/demo6.html
139 |
140 | caret
141 |
142 | column
143 | 109
144 | line
145 | 49
146 |
147 | columnSelection
148 |
149 | firstVisibleColumn
150 | 0
151 | firstVisibleLine
152 | 0
153 | selectFrom
154 |
155 | column
156 | 104
157 | line
158 | 49
159 |
160 | selectTo
161 |
162 | column
163 | 111
164 | line
165 | 49
166 |
167 |
168 | Demos/demo7.html
169 |
170 | caret
171 |
172 | column
173 | 0
174 | line
175 | 44
176 |
177 | firstVisibleColumn
178 | 0
179 | firstVisibleLine
180 | 0
181 |
182 | Demos/demo8.html
183 |
184 | caret
185 |
186 | column
187 | 1
188 | line
189 | 76
190 |
191 | firstVisibleColumn
192 | 0
193 | firstVisibleLine
194 | 4
195 |
196 | Source/Plugins/SubtleTemplate.js
197 |
198 | caret
199 |
200 | column
201 | 20
202 | line
203 | 97
204 |
205 | firstVisibleColumn
206 | 0
207 | firstVisibleLine
208 | 0
209 |
210 | Specs/Plugins/SubtleTemplate.js
211 |
212 | caret
213 |
214 | column
215 | 53
216 | line
217 | 216
218 |
219 | firstVisibleColumn
220 | 0
221 | firstVisibleLine
222 | 142
223 |
224 |
225 | openDocuments
226 |
227 | Demos/demo.html
228 | Demos/demo.css
229 | Demos/demo1.html
230 | Demos/demo2.html
231 | Demos/demo3.html
232 | Demos/demo4.html
233 | Demos/demo5.html
234 | Demos/demo6.html
235 | Demos/demo7.html
236 | Source/Plugins/SubtleTemplate.js
237 | Specs/Plugins/SubtleTemplate.js
238 | Demos/demo8.html
239 |
240 | showFileHierarchyDrawer
241 |
242 | showFileHierarchyPanel
243 |
244 | treeState
245 |
246 | mootools-subtle-templates
247 |
248 | isExpanded
249 |
250 | subItems
251 |
252 |
253 |
254 | windowFrame
255 | {{524, 69}, {1396, 1109}}
256 |
257 |
258 |
--------------------------------------------------------------------------------
/Source/Plugins/SubtleTemplate.js:
--------------------------------------------------------------------------------
1 | /*
2 | ---
3 | provides: [SubtleTemplate]
4 | description: "Define templates in HTML. From Elements or Strings. Populate, re-populate, again and again super-fast. Modify templates AFTER you use them! Update all instances instantly."
5 |
6 | license: MIT-style
7 | author: Thomas Aylott
8 | copyright: Copyright © 2006–2009 Thomas Aylott
9 |
10 | requires:
11 | - core:1.2/Utils
12 | - core:1.2/Class
13 | - core:1.2/Events
14 | - core:1.2/Options
15 | - core:1.2/Element
16 | ...
17 | */
18 | var SubtleTemplate = new Class({
19 |
20 | Implements: Options,
21 |
22 | options:{
23 | subTemplateClass:'SubtleTemplate',
24 | tag: 'div',
25 | id: '',
26 | 'class': '',
27 | html: '{html}'
28 | },
29 |
30 | kids:[],
31 |
32 | /*
33 | initialize
34 | accepts elements or an object of options
35 | */
36 | initialize:function(options){
37 | if($type(options)=='element')
38 | this.setElementOptions(options);
39 | else
40 | this.setOptions(options);
41 |
42 | this.TemplateClass = new Class({
43 |
44 | Extends: SubtleTemplate.Template,
45 |
46 | data: {}
47 |
48 | });
49 |
50 | this.TemplateClass.instance = this;
51 | this.TemplateClass.kids = this.kids;
52 | this.TemplateClass.options = this.options;
53 | this.TemplateClass.updateTemplate = this.updateTemplate.bind(this);
54 |
55 | return this.TemplateClass;
56 | },
57 |
58 | setElementOptions: function(element){
59 | if(!element) return this.fireEvent('error');
60 | element = $(element);
61 |
62 | this.parseSubTemplates(element);
63 |
64 | if(Browser.Engine.trident)element.getElements('*').each(function(el){
65 | // Edit this list to include
66 | // all attribute names whose value you may set in your html templates
67 | // which you will then update to some value which contains a space
68 | // 'id class name value for title alt src rel cite'.split(' ')
69 | ['abbr', 'above', 'accept', 'accesskey', 'action', 'align', 'alink', 'alt', 'archive', 'autostart', 'axis', 'background', 'balance', 'behavior', 'below', 'bgcolor', 'bgproperties', 'border', 'bordercolor', 'bordercolordark', 'bordercolorlight', 'bottommargin', 'cabbase', 'cellpadding', 'cellspacing', 'charset', 'checked', 'cite', 'class', 'classid', 'clear', 'clip', 'code', 'codebase', 'codetype', 'color', 'cols', 'colspan', 'compact', 'content', 'controls', 'coords', 'data', 'datapagesize', 'datetime', 'declare', 'defer', 'delay', 'dir', 'direction', 'disabled', 'dynsrc', 'enctype', 'face', 'for', 'frame', 'frameborder', 'framespacing', 'gutter', 'headers', 'height', 'hidden', 'href', 'hreflang', 'hspace', /*'http-equiv',*/ 'id', 'ismap', 'label', 'lang', 'language', 'left', 'leftmargin', 'link', 'longdesc', 'loop', 'lowsrc', 'marginheight', 'marginwidth', 'maxlength', 'mayscript', 'media', 'method', 'multiple', 'name', 'noexternaldata', 'noresize', 'noshade', 'nowrap', /*'onblur', 'onchange', 'onclick', 'ondblclick', 'onerror', 'onfocus', 'onkeydown', 'onkeypress', 'onkeyup', 'onload', 'onmousedown', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup', 'onreset', 'onresize', 'onselect', 'onsubmit', 'onunload',*/ 'pagex', 'pagey', 'pointsize', 'readonly', 'rel', 'rev', 'rightmargin', 'rows', 'rowspan', 'rules', 'runat', 'scope', 'scrollamount', 'scrolldelay', 'scrolling', 'selected', 'shape', 'size', 'span', 'src', 'standby', 'start', 'style', 'summary', 'tabindex', 'target', 'title', 'top', 'topmargin', 'truespeed', 'type', 'usemap', 'valign', 'value', 'valuetype', 'visibility', 'vlink', 'volume', 'vspace', 'width', 'wrap', 'xmlns']
70 | .each(function(property){try{
71 | var val = el.getProperty(property);
72 | if(val && $type(val)=='string')
73 | el.setProperty(property, el.getProperty(property) + ' HACKED_FOR_IE');
74 | }catch(e){};});
75 | });
76 |
77 | this.setOptions({
78 | parent: element.parentNode ? $(element.parentNode) : null,
79 | tag: element.get('tag'),
80 | id: element.get('id'),
81 | 'class':element.get('class'),
82 | html: element.get('html').replace(/ ?HACKED_FOR_IE/g,'')
83 | });
84 |
85 | element.dispose();
86 | element = null;
87 |
88 | return this;
89 | },
90 |
91 | subTemplates:{},
92 | parseSubTemplates: function(element){
93 | var subTemplateElements = element.getElements('.'+this.options.subTemplateClass);
94 | subTemplateElements.each(function(subTemplateElement){
95 | var key = subTemplateElement.removeClass(this.options.subTemplateClass).get('class');
96 | if (!key) return;
97 | new Element('span',{ 'class':this.options.subTemplateClass+'-'+key.camelCase() }).inject(subTemplateElement, 'before');
98 | this.subTemplates[key] = new SubtleTemplate(subTemplateElement);
99 | },this);
100 | },
101 |
102 | updateTemplate: function(fn){
103 | try{console.log( "updateTemplate", fn );}catch(e){};
104 | if($type(fn) != 'function') return this;
105 | var element = new Element(this.options.tag, { 'class': this.options['class'], html: this.options.html });
106 |
107 | this.setElementOptions(fn.run(this,element) || element);
108 |
109 | try{console.log( pp(this.kids) );}catch(e){};
110 | this.kids.each(function(kid){ if (kid.populate) kid.populate({}, this.options); },this);
111 |
112 | return this;
113 | }
114 |
115 | });
116 | SubtleTemplate.Template = new Class({
117 |
118 | Implements: Events,
119 |
120 | data:{},
121 |
122 | initialize: function(data, options){
123 | if($type(data)=='array' && /^(object|hash)$/.test($type(data[0]))){
124 | var instances = [];
125 | data.each(function(dataSet,index){
126 | instances[index] = new this.constructor(dataSet, options);
127 | }, this);
128 | return instances;
129 | }
130 |
131 | this.constructor.kids.push(this);
132 |
133 | if(options){
134 | this.element = options.element; delete options.element;
135 | this.constructor.instance.setOptions(options);
136 | }
137 | if(data) this.data = $merge(this.data, data);
138 |
139 | this.element = this.element || new Element(this.constructor.instance.options.tag);
140 | this.populate();
141 |
142 | return this.fireEvent("initialize");
143 | },
144 |
145 | populate: function(data, options){
146 | // try{console.log( data, options );}catch(e){};
147 | this.constructor.instance.setOptions(options);
148 | if(data) this.data = $merge(this.data, data);
149 |
150 | this.element.set({
151 | 'html': this.constructor.instance.options.html.substitute(this.data),
152 | 'class':(this.data.html_class||this.constructor.instance.options['class']||'').substitute(this.data),
153 | 'id': (this.data.html_id||'').substitute(this.data)
154 | });
155 |
156 | this.populateSubTemplates(this.data);
157 |
158 | return this.fireEvent("populate");
159 | },
160 |
161 | populateSubTemplates: function(data){
162 | var self = this;
163 | Hash.each(this.constructor.instance.subTemplates, function(subTemplate,key){
164 |
165 | try{console.log( self.constructor.instance.options.subTemplateClass+'-'+key.camelCase() );}catch(e){};
166 | var elementForKey = self.element.getElement('.'+ self.constructor.instance.options.subTemplateClass+'-'+key.camelCase() )
167 |
168 | if ($type(data[key])=='array') data[key].each(function(dataForKey){
169 |
170 | new subTemplate(dataForKey).element.inject(elementForKey, 'before');
171 |
172 | });
173 |
174 | elementForKey.dispose();
175 | });
176 | },
177 |
178 | toElement: function(){
179 | return this.element;
180 | },
181 |
182 | inject: function(parent){
183 | this.element.inject( parent || this.parent || this.constructor.instance.options.parent );
184 | return this.fireEvent("inject");
185 | }
186 |
187 | });
188 |
--------------------------------------------------------------------------------
/Specs/Plugins/SubtleTemplate.js:
--------------------------------------------------------------------------------
1 | describe('SubtleTemplate', {
2 |
3 | 'before all': function(){
4 | demo = new Element('div', {id:'subtletemplate_demo'}).inject(document.body);
5 |
6 | template = new Element('div',{
7 | id:'subtletemplate',
8 | 'class':'super duper {extraclass}',
9 | html:'\
10 | {data1} \
11 | {data2} \
12 | {data3} \
13 | '
14 | }).inject(demo);
15 |
16 | MyDiv = new SubtleTemplate(template);
17 |
18 |
19 | new Element('div',{
20 | id:'mydiv_with_attributes',
21 | html:''
26 | }).inject(demo);
27 |
28 | MyDiv_with_attributes = new SubtleTemplate($('mydiv_with_attributes'));
29 | }
30 |
31 | ,'after all': function(){
32 | template.destroy();
33 | demo.destroy();
34 | MyDiv_with_attributes=
35 | demo=
36 | MyDiv=
37 | null;
38 | }
39 |
40 | ,after: function(){
41 | demo.empty();
42 | }
43 |
44 | ,'should be created and injected and match the data': function(){
45 | var fred = new MyDiv({ data1:'fred' }).inject( demo );
46 | value_of( fred.element.get('text') ).should_match( 'fred' );
47 | }
48 |
49 | ,'should allow updating the data later': function(){
50 | var fred = new MyDiv({ data1:'fred' }).inject( demo );
51 | value_of( fred.element.get('text') ).should_match( 'fred' );
52 |
53 | fred.populate({ data1:'updated' });
54 | value_of( fred.element.get('text') ).should_match( 'updated' );
55 | }
56 |
57 | ,'should not break when you have spaces in your values': function(){
58 | // This is really to check for IE goofing up your html since it'll unquote attribute values in its view of the HTML, EG:
59 | // howdy
60 | // In IE becomes:
61 | // howdy
62 | // Then, setting the class to "something else altogether" would result in this:
63 | // howdy
64 |
65 | var fred = new MyDiv_with_attributes({ data1:'fred is my name' }).inject( demo );
66 | value_of( fred.element.getElement('input').get('value') ).should_be( 'fred is my name' );
67 |
68 | fred.populate({ data1:'derf is not my name' });
69 | value_of( fred.element.getElement('input').get('value') ).should_match( 'derf is not my name' );
70 | }
71 |
72 | ,'should allow you to update the html of the template and rebuild everything': function(){
73 |
74 | var fred = new MyDiv({ data1:'fred' }).inject( demo );
75 | value_of( fred.element.get('text') ).should_match( 'fred' );
76 |
77 | var fred1 = new MyDiv({ data1:'fred1' }).inject( demo );
78 | value_of( fred1.element.get('text') ).should_match( 'fred1' );
79 |
80 | MyDiv.updateTemplate(function(templateClassInstance){
81 | this.getElement('li').set('html','{data1}{data1}');
82 | });
83 | value_of( fred.element.get('text') ).should_match( 'fredfred' );
84 | value_of( fred1.element.get('text') ).should_match( 'fred1fred1' );
85 |
86 | }
87 |
88 | ,'should allow you to update the html to any random tag, even a new one': function(){
89 | var MyBlankDiv = new SubtleTemplate();
90 |
91 | var fred = new MyBlankDiv({ html:'fred' }).inject( demo );
92 | value_of( fred.element.get('text') ).should_match( 'fred' );
93 | value_of( fred.element.getElement('*') ).should_be(null);
94 |
95 | var fred1 = new MyBlankDiv({ html:'fred1' }).inject( demo );
96 | value_of( fred1.element.get('text') ).should_match( 'fred1' );
97 | value_of( fred1.element.getElement('*') ).should_be(null);
98 |
99 | fred.populate({
100 | data1:'fred'
101 | });
102 | fred1.populate({
103 | data1:'fred1'
104 | });
105 |
106 | MyBlankDiv.updateTemplate(function(templateClassInstance){
107 | return new Element('div',{html:'{data1}{data1}
'})
108 | });
109 | value_of( fred.element.getElement('*').get('tag') ).should_be( 'p' );
110 | value_of( fred.element.get('text') ).should_match( 'fredfred' );
111 |
112 | value_of( fred.element.getElement('*').get('tag') ).should_be( 'p' );
113 | value_of( fred1.element.get('text') ).should_match( 'fred1fred1' );
114 |
115 | }
116 |
117 | ,'should be able to implement stuff on the template': function(){
118 |
119 | var MySuperDiv = new SubtleTemplate();
120 |
121 | my_div = new MySuperDiv({});
122 |
123 | MySuperDiv.implement({
124 | something: 'something'
125 | });
126 |
127 | value_of( new MySuperDiv({}).something ).should_be( 'something' );
128 | value_of( my_div.something ).should_be( 'something' );
129 |
130 | }
131 |
132 | ,'should keep its class when used as a template': function(){
133 |
134 | var fred = new MyDiv({ data1:'fred' }).inject( demo );
135 | value_of( fred.element.get('text') ).should_match( 'fred' );
136 |
137 | value_of( template.get('class') ).should_be( 'super duper {extraclass}' )
138 | value_of( fred.element.get('class') ).should_be( 'super duper ' )
139 | }
140 |
141 | ,'should substitute class and id': function(){
142 |
143 | var fred = new MyDiv({ data1:'fred', extraclass:'myextraclass', html_id:'subtletemplate{id}', id:'flarm' }).inject( demo );
144 | value_of( fred.element.get('text') ).should_match( 'fred' );
145 |
146 | value_of( template.get('class') ).should_be( 'super duper {extraclass}' )
147 | value_of( fred.element.get('class') ).should_be( 'super duper myextraclass' )
148 |
149 | value_of( template.get('id') ).should_be( 'subtletemplate' )
150 | value_of( fred.element.get('id') ).should_be( 'subtletemplateflarm' )
151 |
152 | fred.populate({ data1:'fred2', extraclass:'myextraclass2', html_id:'subtletemplate{id}2', id:'flarm2' });
153 | value_of( fred.element.get('text') ).should_match( 'fred2' );
154 | value_of( fred.element.get('class') ).should_be( 'super duper myextraclass2' )
155 | value_of( fred.element.get('id') ).should_be( 'subtletemplateflarm22' )
156 | }
157 |
158 | ,"shouldn't use the original id attribute": function(){
159 |
160 | var fred = new MyDiv({ data1:'fred', extraclass:'myextraclass' }).inject( demo );
161 | value_of( fred.element.get('text') ).should_match( 'fred' );
162 |
163 | value_of( template.get('id') ).should_be( 'subtletemplate' )
164 | value_of( fred.element.get('id') ).should_not_be( 'subtletemplate' )
165 | }
166 | /*
167 | ,"should keep additional classes when repopulated": function(){
168 |
169 | // Start with class="one{one} two{two} three{three}"
170 | // data = {one:1,two:2,three:3}
171 | // populate
172 | // class should be "one1 two2 three3"
173 | // Add class "four4"
174 | // populate
175 | // class should be "one1 two2 three3 four4"
176 | // Change data to {one:4,two:5,three:6}
177 | // populate
178 | // class should be "one4 two5 three6 four4"
179 |
180 | }
181 | */
182 | ,"should bless an existing element if provided": function(){
183 |
184 | // Make a new normal html element
185 | var myExistingElement = new Element('div',{ html:'myExistingElement' });
186 | value_of( myExistingElement.get('html') ).should_be( 'myExistingElement' );
187 |
188 | // Blessing it should populate() just like creating a new one
189 | var fred = new MyDiv({ data1:'myExistingElement is populated!' }, { element:myExistingElement });
190 | value_of( myExistingElement.get('html') ).should_match( 'myExistingElement is populated!' );
191 |
192 | }
193 | ,"should create sub templates when children of the template element include an that match the subtemplate classname": function(){
194 | var DATA = {
195 | masterName:'masterName',
196 | children:[
197 | { childName:'childName1' },
198 | { childName:'childName2' },
199 | { childName:'childName3' }
200 | ]
201 | };
202 | var masterTemplateElement = new Element('div',{html:'{masterName} {childName}
'}).inject(demo);
203 | var MasterTemplate = new SubtleTemplate(masterTemplateElement);
204 | var myMasterTemplate = new MasterTemplate(DATA).inject(demo);
205 |
206 | value_of( demo.getElements('p').length ).should_be(3);
207 |
208 | DATA.children.push({ childName:'childName4' });
209 | myMasterTemplate.populate(DATA);
210 |
211 | value_of( demo.getElements('p').length ).should_be(4);
212 |
213 | delete DATA.children
214 | myMasterTemplate.populate(DATA);
215 |
216 | value_of( DATA.children ).should_be_undefined();
217 | value_of( demo.getElements('p').length ).should_be(0);
218 | }
219 | });
220 |
221 |
222 |
223 |
--------------------------------------------------------------------------------
/Specs/Assets/Scripts/JSSpec.js:
--------------------------------------------------------------------------------
1 | /**
2 | * JSSpec
3 | *
4 | * Copyright 2007 Alan Kang
5 | * - mailto:jania902@gmail.com
6 | * - http://jania.pe.kr
7 | *
8 | * http://jania.pe.kr/aw/moin.cgi/JSSpec
9 | *
10 | * Dependencies:
11 | * - diff_match_patch.js ( http://code.google.com/p/google-diff-match-patch )
12 | *
13 | * This library is free software; you can redistribute it and/or
14 | * modify it under the terms of the GNU Lesser General Public
15 | * License as published by the Free Software Foundation; either
16 | * version 2.1 of the License, or (at your option) any later version.
17 | *
18 | * This library is distributed in the hope that it will be useful,
19 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 | * Lesser General Public License for more details.
22 | *
23 | * You should have received a copy of the GNU Lesser General Public
24 | * License along with this library; if not, write to the Free Software
25 | * Foundation, Inc, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
26 | */
27 |
28 | /**
29 | * Namespace
30 | */
31 |
32 | var JSSpec = {
33 | specs: [],
34 |
35 | EMPTY_FUNCTION: function() {},
36 |
37 | Browser: {
38 | Trident: navigator.appName == "Microsoft Internet Explorer",
39 | Webkit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
40 | Gecko: navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1,
41 | Presto: navigator.appName == "Opera"
42 | }
43 | };
44 |
45 |
46 |
47 | /**
48 | * Executor
49 | */
50 | JSSpec.Executor = function(target, onSuccess, onException) {
51 | this.target = target;
52 | this.onSuccess = typeof onSuccess == 'function' ? onSuccess : JSSpec.EMPTY_FUNCTION;
53 | this.onException = typeof onException == 'function' ? onException : JSSpec.EMPTY_FUNCTION;
54 |
55 | if(JSSpec.Browser.Trident) {
56 | // Exception handler for Trident. It helps to collect exact line number where exception occured.
57 | window.onerror = function(message, fileName, lineNumber) {
58 | var self = window._curExecutor;
59 | var ex = {message:message, fileName:fileName, lineNumber:lineNumber};
60 |
61 | if(JSSpec._secondPass) {
62 | ex = self.mergeExceptions(JSSpec._assertionFailure, ex);
63 | delete JSSpec._secondPass;
64 | delete JSSpec._assertionFailure;
65 |
66 | ex.type = "failure";
67 | self.onException(self, ex);
68 | } else if(JSSpec._assertionFailure) {
69 | JSSpec._secondPass = true;
70 | self.run();
71 | } else {
72 | self.onException(self, ex);
73 | }
74 |
75 | return true;
76 | };
77 | }
78 | };
79 | JSSpec.Executor.prototype.mergeExceptions = function(assertionFailure, normalException) {
80 | var merged = {
81 | message:assertionFailure.message,
82 | fileName:normalException.fileName,
83 | lineNumber:normalException.lineNumber
84 | };
85 |
86 | return merged;
87 | };
88 |
89 | JSSpec.Executor.prototype.run = function() {
90 | var self = this;
91 | var target = this.target;
92 | var onSuccess = this.onSuccess;
93 | var onException = this.onException;
94 |
95 | window.setTimeout(
96 | function() {
97 | var result;
98 | if(JSSpec.Browser.Trident) {
99 | window._curExecutor = self;
100 |
101 | result = self.target();
102 | self.onSuccess(self, result);
103 | } else {
104 | try {
105 | result = self.target();
106 | self.onSuccess(self, result);
107 | } catch(ex) {
108 | if(JSSpec.Browser.Webkit) ex = {message:ex.message, fileName:ex.sourceURL, lineNumber:ex.line};
109 |
110 | if(JSSpec._secondPass) {
111 | ex = self.mergeExceptions(JSSpec._assertionFailure, ex);
112 | delete JSSpec._secondPass;
113 | delete JSSpec._assertionFailure;
114 |
115 | ex.type = "failure";
116 | self.onException(self, ex);
117 | } else if(JSSpec._assertionFailure) {
118 | JSSpec._secondPass = true;
119 | self.run();
120 | } else {
121 | self.onException(self, ex);
122 | }
123 | }
124 | }
125 | },
126 | 0
127 | );
128 | };
129 |
130 |
131 |
132 | /**
133 | * CompositeExecutor composites one or more executors and execute them sequencially.
134 | */
135 | JSSpec.CompositeExecutor = function(onSuccess, onException, continueOnException) {
136 | this.queue = [];
137 | this.onSuccess = typeof onSuccess == 'function' ? onSuccess : JSSpec.EMPTY_FUNCTION;
138 | this.onException = typeof onException == 'function' ? onException : JSSpec.EMPTY_FUNCTION;
139 | this.continueOnException = !!continueOnException;
140 | };
141 |
142 | JSSpec.CompositeExecutor.prototype.addFunction = function(func) {
143 | this.addExecutor(new JSSpec.Executor(func));
144 | };
145 |
146 | JSSpec.CompositeExecutor.prototype.addExecutor = function(executor) {
147 | var last = this.queue.length == 0 ? null : this.queue[this.queue.length - 1];
148 | if(last) {
149 | last.next = executor;
150 | }
151 |
152 | executor.parent = this;
153 | executor.onSuccessBackup = executor.onSuccess;
154 | executor.onSuccess = function(result) {
155 | this.onSuccessBackup(result);
156 | if(this.next) {
157 | this.next.run();
158 | } else {
159 | this.parent.onSuccess();
160 | }
161 | };
162 | executor.onExceptionBackup = executor.onException;
163 | executor.onException = function(executor, ex) {
164 | this.onExceptionBackup(executor, ex);
165 |
166 | if(this.parent.continueOnException) {
167 | if(this.next) {
168 | this.next.run();
169 | } else {
170 | this.parent.onSuccess();
171 | }
172 | } else {
173 | this.parent.onException(executor, ex);
174 | }
175 | };
176 |
177 | this.queue.push(executor);
178 | };
179 |
180 | JSSpec.CompositeExecutor.prototype.run = function() {
181 | if(this.queue.length > 0) {
182 | this.queue[0].run();
183 | }
184 | };
185 |
186 | /**
187 | * Spec is a set of Examples in a specific context
188 | */
189 | JSSpec.Spec = function(context, entries) {
190 | this.id = JSSpec.Spec.id++;
191 | this.context = context;
192 | this.url = location.href;
193 |
194 | this.filterEntriesByEmbeddedExpressions(entries);
195 | this.extractOutSpecialEntries(entries);
196 | this.examples = this.makeExamplesFromEntries(entries);
197 | this.examplesMap = this.makeMapFromExamples(this.examples);
198 | };
199 |
200 | JSSpec.Spec.id = 0;
201 | JSSpec.Spec.prototype.getExamples = function() {
202 | return this.examples;
203 | };
204 |
205 | JSSpec.Spec.prototype.hasException = function() {
206 | return this.getTotalFailures() > 0 || this.getTotalErrors() > 0;
207 | };
208 |
209 | JSSpec.Spec.prototype.getTotalFailures = function() {
210 | var examples = this.examples;
211 | var failures = 0;
212 | for(var i = 0; i < examples.length; i++) {
213 | if(examples[i].isFailure()) failures++;
214 | }
215 | return failures;
216 | };
217 |
218 | JSSpec.Spec.prototype.getTotalErrors = function() {
219 | var examples = this.examples;
220 | var errors = 0;
221 | for(var i = 0; i < examples.length; i++) {
222 | if(examples[i].isError()) errors++;
223 | }
224 | return errors;
225 | };
226 |
227 | JSSpec.Spec.prototype.filterEntriesByEmbeddedExpressions = function(entries) {
228 | var isTrue;
229 | for(name in entries) {
230 | var m = name.match(/\[\[(.+)\]\]/);
231 | if(m && m[1]) {
232 | eval("isTrue = (" + m[1] + ")");
233 | if(!isTrue) delete entries[name];
234 | }
235 | }
236 | };
237 |
238 | JSSpec.Spec.prototype.extractOutSpecialEntries = function(entries) {
239 | this.beforeEach = JSSpec.EMPTY_FUNCTION;
240 | this.beforeAll = JSSpec.EMPTY_FUNCTION;
241 | this.afterEach = JSSpec.EMPTY_FUNCTION;
242 | this.afterAll = JSSpec.EMPTY_FUNCTION;
243 |
244 | for(name in entries) {
245 | if(name == 'before' || name == 'before each' || name == 'before_each') {
246 | this.beforeEach = entries[name];
247 | } else if(name == 'before all' || name == 'before_all') {
248 | this.beforeAll = entries[name];
249 | } else if(name == 'after' || name == 'after each' || name == 'after_each') {
250 | this.afterEach = entries[name];
251 | } else if(name == 'after all' || name == 'after_all') {
252 | this.afterAll = entries[name];
253 | }
254 | }
255 |
256 | delete entries['before'];
257 | delete entries['before each'];
258 | delete entries['before_each'];
259 | delete entries['before all'];
260 | delete entries['before_all'];
261 | delete entries['after'];
262 | delete entries['after each'];
263 | delete entries['after_each'];
264 | delete entries['after all'];
265 | delete entries['after_all'];
266 | };
267 |
268 | JSSpec.Spec.prototype.makeExamplesFromEntries = function(entries) {
269 | var examples = [];
270 | for(name in entries) {
271 | examples.push(new JSSpec.Example(name, entries[name], this.beforeEach, this.afterEach));
272 | }
273 | return examples;
274 | };
275 |
276 | JSSpec.Spec.prototype.makeMapFromExamples = function(examples) {
277 | var map = {};
278 | for(var i = 0; i < examples.length; i++) {
279 | var example = examples[i];
280 | map[example.id] = examples[i];
281 | }
282 | return map;
283 | };
284 |
285 | JSSpec.Spec.prototype.getExampleById = function(id) {
286 | return this.examplesMap[id];
287 | };
288 |
289 | JSSpec.Spec.prototype.getExecutor = function() {
290 | var self = this;
291 | var onException = function(executor, ex) {
292 | self.exception = ex;
293 | };
294 |
295 | var composite = new JSSpec.CompositeExecutor();
296 | composite.addFunction(function() {JSSpec.log.onSpecStart(self);});
297 | composite.addExecutor(new JSSpec.Executor(this.beforeAll, null, function(exec, ex) {
298 | self.exception = ex;
299 | JSSpec.log.onSpecEnd(self);
300 | }));
301 |
302 | var exampleAndAfter = new JSSpec.CompositeExecutor(null,null,true);
303 | for(var i = 0; i < this.examples.length; i++) {
304 | exampleAndAfter.addExecutor(this.examples[i].getExecutor());
305 | }
306 | exampleAndAfter.addExecutor(new JSSpec.Executor(this.afterAll, null, onException));
307 | exampleAndAfter.addFunction(function() {JSSpec.log.onSpecEnd(self);});
308 | composite.addExecutor(exampleAndAfter);
309 |
310 | return composite;
311 | };
312 |
313 | /**
314 | * Example
315 | */
316 | JSSpec.Example = function(name, target, before, after) {
317 | this.id = JSSpec.Example.id++;
318 | this.name = name;
319 | this.target = target;
320 | this.before = before;
321 | this.after = after;
322 | };
323 |
324 | JSSpec.Example.id = 0;
325 | JSSpec.Example.prototype.isFailure = function() {
326 | return this.exception && this.exception.type == "failure";
327 | };
328 |
329 | JSSpec.Example.prototype.isError = function() {
330 | return this.exception && !this.exception.type;
331 | };
332 |
333 | JSSpec.Example.prototype.getExecutor = function() {
334 | var self = this;
335 | var onException = function(executor, ex) {
336 | self.exception = ex;
337 | };
338 |
339 | var composite = new JSSpec.CompositeExecutor();
340 | composite.addFunction(function() {JSSpec.log.onExampleStart(self);});
341 | composite.addExecutor(new JSSpec.Executor(this.before, null, function(exec, ex) {
342 | self.exception = ex;
343 | JSSpec.log.onExampleEnd(self);
344 | }));
345 |
346 | var targetAndAfter = new JSSpec.CompositeExecutor(null,null,true);
347 |
348 | targetAndAfter.addExecutor(new JSSpec.Executor(this.target, null, onException));
349 | targetAndAfter.addExecutor(new JSSpec.Executor(this.after, null, onException));
350 | targetAndAfter.addFunction(function() {JSSpec.log.onExampleEnd(self);});
351 |
352 | composite.addExecutor(targetAndAfter);
353 |
354 | return composite;
355 | };
356 |
357 | /**
358 | * Runner
359 | */
360 | JSSpec.Runner = function(specs, logger) {
361 | JSSpec.log = logger;
362 |
363 | this.totalExamples = 0;
364 | this.specs = [];
365 | this.specsMap = {};
366 | this.addAllSpecs(specs);
367 | };
368 |
369 | JSSpec.Runner.prototype.addAllSpecs = function(specs) {
370 | for(var i = 0; i < specs.length; i++) {
371 | this.addSpec(specs[i]);
372 | }
373 | };
374 |
375 | JSSpec.Runner.prototype.addSpec = function(spec) {
376 | this.specs.push(spec);
377 | this.specsMap[spec.id] = spec;
378 | this.totalExamples += spec.getExamples().length;
379 | };
380 |
381 | JSSpec.Runner.prototype.getSpecById = function(id) {
382 | return this.specsMap[id];
383 | };
384 |
385 | JSSpec.Runner.prototype.getSpecByContext = function(context) {
386 | for(var i = 0; i < this.specs.length; i++) {
387 | if(this.specs[i].context == context) return this.specs[i];
388 | }
389 | return null;
390 | };
391 |
392 | JSSpec.Runner.prototype.getSpecs = function() {
393 | return this.specs;
394 | };
395 |
396 | JSSpec.Runner.prototype.hasException = function() {
397 | return this.getTotalFailures() > 0 || this.getTotalErrors() > 0;
398 | };
399 |
400 | JSSpec.Runner.prototype.getTotalFailures = function() {
401 | var specs = this.specs;
402 | var failures = 0;
403 | for(var i = 0; i < specs.length; i++) {
404 | failures += specs[i].getTotalFailures();
405 | }
406 | return failures;
407 | };
408 |
409 | JSSpec.Runner.prototype.getTotalErrors = function() {
410 | var specs = this.specs;
411 | var errors = 0;
412 | for(var i = 0; i < specs.length; i++) {
413 | errors += specs[i].getTotalErrors();
414 | }
415 | return errors;
416 | };
417 |
418 |
419 | JSSpec.Runner.prototype.run = function() {
420 | JSSpec.log.onRunnerStart();
421 | var executor = new JSSpec.CompositeExecutor(function() {JSSpec.log.onRunnerEnd()},null,true);
422 | for(var i = 0; i < this.specs.length; i++) {
423 | executor.addExecutor(this.specs[i].getExecutor());
424 | }
425 | executor.run();
426 | };
427 |
428 |
429 | JSSpec.Runner.prototype.rerun = function(context) {
430 | JSSpec.runner = new JSSpec.Runner([this.getSpecByContext(context)], JSSpec.log);
431 | JSSpec.runner.run();
432 | };
433 |
434 | /**
435 | * Logger
436 | */
437 | JSSpec.Logger = function() {
438 | this.finishedExamples = 0;
439 | this.startedAt = null;
440 | };
441 |
442 | JSSpec.Logger.prototype.onRunnerStart = function() {
443 | this._title = document.title;
444 |
445 | this.startedAt = new Date();
446 | var container = document.getElementById('jsspec_container');
447 | if(container) {
448 | container.innerHTML = "";
449 | } else {
450 | container = document.createElement("DIV");
451 | container.id = "jsspec_container";
452 | document.body.appendChild(container);
453 | }
454 |
455 | var title = document.createElement("DIV");
456 | title.id = "title";
457 | title.innerHTML = [
458 | 'JSSpec ',
459 | '',
460 | JSSpec.options.rerun ? '[X ] ' + JSSpec.util.escapeTags(decodeURIComponent(JSSpec.options.rerun)) + ' ' : '',
461 | ' ' + JSSpec.runner.totalExamples + ' examples ',
462 | ' 0 failures ',
463 | ' 0 errors ',
464 | ' 0 % done ',
465 | ' 0 secs ',
466 | ' ',
467 | 'JSSpec homepage
',
468 | ].join("");
469 | container.appendChild(title);
470 |
471 | var list = document.createElement("DIV");
472 | list.id = "list";
473 | list.innerHTML = [
474 | 'List ',
475 | '',
476 | function() {
477 | var specs = JSSpec.runner.getSpecs();
478 | var sb = [];
479 | for(var i = 0; i < specs.length; i++) {
480 | var spec = specs[i];
481 | sb.push(' ');
482 | }
483 | return sb.join("");
484 | }(),
485 | ' '
486 | ].join("");
487 | container.appendChild(list);
488 |
489 | var log = document.createElement("DIV");
490 | log.id = "log";
491 | log.innerHTML = [
492 | 'Log ',
493 | '',
494 | function() {
495 | var specs = JSSpec.runner.getSpecs();
496 | var sb = [];
497 | for(var i = 0; i < specs.length; i++) {
498 | var spec = specs[i];
499 | sb.push(' ');
500 | sb.push(' ' + JSSpec.util.escapeTags(specs[i].context) + ' [rerun ] ');
501 | sb.push(' ');
502 | for(var j = 0; j < spec.examples.length; j++) {
503 | var example = spec.examples[j];
504 | sb.push(' ')
505 | sb.push(' ' + JSSpec.util.escapeTags(example.name) + ' ')
506 | sb.push(' ')
507 | }
508 | sb.push(' ');
509 | sb.push(' ');
510 | }
511 | return sb.join("");
512 | }(),
513 | ' '
514 | ].join("");
515 |
516 | container.appendChild(log);
517 |
518 | // add event handler for toggling
519 | var specs = JSSpec.runner.getSpecs();
520 | var sb = [];
521 | for(var i = 0; i < specs.length; i++) {
522 | var spec = document.getElementById("spec_" + specs[i].id);
523 | var title = spec.getElementsByTagName("H3")[0];
524 | title.onclick = function(e) {
525 | var target = document.getElementById(this.parentNode.id + "_examples");
526 | target.style.display = target.style.display == "none" ? "block" : "none";
527 | return true;
528 | }
529 | }
530 | };
531 |
532 | JSSpec.Logger.prototype.onRunnerEnd = function() {
533 | if(JSSpec.runner.hasException()) {
534 | var times = 4;
535 | var title1 = "*" + this._title;
536 | var title2 = "*F" + JSSpec.runner.getTotalFailures() + " E" + JSSpec.runner.getTotalErrors() + "* " + this._title;
537 | } else {
538 | var times = 2;
539 | var title1 = this._title;
540 | var title2 = "Success";
541 | }
542 | this.blinkTitle(times,title1,title2);
543 | };
544 |
545 | JSSpec.Logger.prototype.blinkTitle = function(times, title1, title2) {
546 | var times = times * 2;
547 | var mode = true;
548 |
549 | var f = function() {
550 | if(times > 0) {
551 | document.title = mode ? title1 : title2;
552 | mode = !mode;
553 | times--;
554 | window.setTimeout(f, 500);
555 | } else {
556 | document.title = title1;
557 | }
558 | };
559 |
560 | f();
561 | };
562 |
563 | JSSpec.Logger.prototype.onSpecStart = function(spec) {
564 | var spec_list = document.getElementById("spec_" + spec.id + "_list");
565 | var spec_log = document.getElementById("spec_" + spec.id);
566 |
567 | spec_list.className = "ongoing";
568 | spec_log.className = "ongoing";
569 | };
570 |
571 | JSSpec.Logger.prototype.onSpecEnd = function(spec) {
572 | var spec_list = document.getElementById("spec_" + spec.id + "_list");
573 | var spec_log = document.getElementById("spec_" + spec.id);
574 | var examples = document.getElementById("spec_" + spec.id + "_examples");
575 | var className = spec.hasException() ? "exception" : "success";
576 |
577 | spec_list.className = className;
578 | spec_log.className = className;
579 |
580 | if(JSSpec.options.autocollapse && !spec.hasException()) examples.style.display = "none";
581 |
582 | if(spec.exception) {
583 | heading.appendChild(document.createTextNode(" - " + spec.exception.message));
584 | }
585 | };
586 |
587 | JSSpec.Logger.prototype.onExampleStart = function(example) {
588 | var li = document.getElementById("example_" + example.id);
589 | li.className = "ongoing";
590 | };
591 |
592 | JSSpec.Logger.prototype.onExampleEnd = function(example) {
593 | var li = document.getElementById("example_" + example.id);
594 | li.className = example.exception ? "exception" : "success";
595 |
596 | if(example.exception) {
597 | var div = document.createElement("DIV");
598 | div.innerHTML = example.exception.message + " " + " at " + example.exception.fileName + ", line " + example.exception.lineNumber + "
";
599 | li.appendChild(div);
600 | }
601 |
602 | var title = document.getElementById("title");
603 | var runner = JSSpec.runner;
604 |
605 | title.className = runner.hasException() ? "exception" : "success";
606 |
607 | this.finishedExamples++;
608 | document.getElementById("total_failures").innerHTML = runner.getTotalFailures();
609 | document.getElementById("total_errors").innerHTML = runner.getTotalErrors();
610 | var progress = parseInt(this.finishedExamples / runner.totalExamples * 100);
611 | document.getElementById("progress").innerHTML = progress;
612 | document.getElementById("total_elapsed").innerHTML = (new Date().getTime() - this.startedAt.getTime()) / 1000;
613 |
614 | document.title = progress + "%: " + this._title;
615 | };
616 |
617 | /**
618 | * IncludeMatcher
619 | */
620 | JSSpec.IncludeMatcher = function(actual, expected, condition) {
621 | this.actual = actual;
622 | this.expected = expected;
623 | this.condition = condition;
624 | this.match = false;
625 | this.explaination = this.makeExplain();
626 | };
627 |
628 | JSSpec.IncludeMatcher.createInstance = function(actual, expected, condition) {
629 | return new JSSpec.IncludeMatcher(actual, expected, condition);
630 | };
631 |
632 | JSSpec.IncludeMatcher.prototype.matches = function() {
633 | return this.match;
634 | };
635 |
636 | JSSpec.IncludeMatcher.prototype.explain = function() {
637 | return this.explaination;
638 | };
639 |
640 | JSSpec.IncludeMatcher.prototype.makeExplain = function() {
641 | if(typeof this.actual.length == 'undefined') {
642 | return this.makeExplainForNotArray();
643 | } else {
644 | return this.makeExplainForArray();
645 | }
646 | };
647 |
648 | JSSpec.IncludeMatcher.prototype.makeExplainForNotArray = function() {
649 | if(this.condition) {
650 | this.match = !!this.actual[this.expected];
651 | } else {
652 | this.match = !this.actual[this.expected];
653 | }
654 |
655 | var sb = [];
656 | sb.push('actual value:
');
657 | sb.push('' + JSSpec.util.inspect(this.actual, false, this.expected) + '
');
658 | sb.push('should ' + (this.condition ? '' : 'not') + ' include:
');
659 | sb.push('' + JSSpec.util.inspect(this.expected) + '
');
660 | return sb.join("");
661 | }
662 | ;
663 | JSSpec.IncludeMatcher.prototype.makeExplainForArray = function() {
664 | var matches;
665 | if(this.condition) {
666 | for(var i = 0; i < this.actual.length; i++) {
667 | matches = JSSpec.EqualityMatcher.createInstance(this.expected, this.actual[i]).matches();
668 | if(matches) {
669 | this.match = true;
670 | break;
671 | }
672 | }
673 | } else {
674 | for(var i = 0; i < this.actual.length; i++) {
675 | matches = JSSpec.EqualityMatcher.createInstance(this.expected, this.actual[i]).matches();
676 | if(matches) {
677 | this.match = false;
678 | break;
679 | }
680 | }
681 | }
682 |
683 | if(this.match) return "";
684 |
685 | var sb = [];
686 | sb.push('actual value:
');
687 | sb.push('' + JSSpec.util.inspect(this.actual, false, this.condition ? null : i) + '
');
688 | sb.push('should ' + (this.condition ? '' : 'not') + ' include:
');
689 | sb.push('' + JSSpec.util.inspect(this.expected) + '
');
690 | return sb.join("");
691 | };
692 |
693 | /**
694 | * PropertyLengthMatcher
695 | */
696 | JSSpec.PropertyLengthMatcher = function(num, property, o, condition) {
697 | this.num = num;
698 | this.o = o;
699 | this.property = property;
700 | if((property == 'characters' || property == 'items') && typeof o.length != 'undefined') {
701 | this.property = 'length';
702 | }
703 |
704 | this.condition = condition;
705 | this.conditionMet = function(x) {
706 | if(condition == 'exactly') return x.length == num;
707 | if(condition == 'at least') return x.length >= num;
708 | if(condition == 'at most') return x.length <= num;
709 |
710 | throw "Unknown condition '" + condition + "'";
711 | };
712 | this.match = false;
713 | this.explaination = this.makeExplain();
714 | };
715 |
716 | JSSpec.PropertyLengthMatcher.prototype.makeExplain = function() {
717 | if(this.o._type == 'String' && this.property == 'length') {
718 | this.match = this.conditionMet(this.o);
719 | return this.match ? '' : this.makeExplainForString();
720 | } else if(typeof this.o.length != 'undefined' && this.property == "length") {
721 | this.match = this.conditionMet(this.o);
722 | return this.match ? '' : this.makeExplainForArray();
723 | } else if(typeof this.o[this.property] != 'undefined' && this.o[this.property] != null) {
724 | this.match = this.conditionMet(this.o[this.property]);
725 | return this.match ? '' : this.makeExplainForObject();
726 | } else if(typeof this.o[this.property] == 'undefined' || this.o[this.property] == null) {
727 | this.match = false;
728 | return this.makeExplainForNoProperty();
729 | }
730 |
731 | this.match = true;
732 | };
733 |
734 | JSSpec.PropertyLengthMatcher.prototype.makeExplainForString = function() {
735 | var sb = [];
736 |
737 | var exp = this.num == 0 ?
738 | 'be an empty string ' :
739 | 'have ' + this.condition + ' ' + this.num + ' characters ';
740 |
741 | sb.push('actual value has ' + this.o.length + ' characters :
');
742 | sb.push('' + JSSpec.util.inspect(this.o) + '
');
743 | sb.push('but it should ' + exp + '.
');
744 |
745 | return sb.join("");
746 | };
747 |
748 | JSSpec.PropertyLengthMatcher.prototype.makeExplainForArray = function() {
749 | var sb = [];
750 |
751 | var exp = this.num == 0 ?
752 | 'be an empty array ' :
753 | 'have ' + this.condition + ' ' + this.num + ' items ';
754 |
755 | sb.push('actual value has ' + this.o.length + ' items :
');
756 | sb.push('' + JSSpec.util.inspect(this.o) + '
');
757 | sb.push('but it should ' + exp + '.
');
758 |
759 | return sb.join("");
760 | };
761 |
762 | JSSpec.PropertyLengthMatcher.prototype.makeExplainForObject = function() {
763 | var sb = [];
764 |
765 | var exp = this.num == 0 ?
766 | 'be empty ' :
767 | 'have ' + this.condition + ' ' + this.num + ' ' + this.property + '. ';
768 |
769 | sb.push('actual value has ' + this.o[this.property].length + ' ' + this.property + ' :
');
770 | sb.push('' + JSSpec.util.inspect(this.o, false, this.property) + '
');
771 | sb.push('but it should ' + exp + '.
');
772 |
773 | return sb.join("");
774 | };
775 |
776 | JSSpec.PropertyLengthMatcher.prototype.makeExplainForNoProperty = function() {
777 | var sb = [];
778 |
779 | sb.push('actual value:
');
780 | sb.push('' + JSSpec.util.inspect(this.o) + '
');
781 | sb.push('should have ' + this.condition + ' ' + this.num + ' ' + this.property + ' but there\'s no such property.
');
782 |
783 | return sb.join("");
784 | };
785 |
786 | JSSpec.PropertyLengthMatcher.prototype.matches = function() {
787 | return this.match;
788 | };
789 |
790 | JSSpec.PropertyLengthMatcher.prototype.explain = function() {
791 | return this.explaination;
792 | };
793 |
794 | JSSpec.PropertyLengthMatcher.createInstance = function(num, property, o, condition) {
795 | return new JSSpec.PropertyLengthMatcher(num, property, o, condition);
796 | };
797 |
798 | /**
799 | * EqualityMatcher
800 | */
801 | JSSpec.EqualityMatcher = {};
802 |
803 | JSSpec.EqualityMatcher.createInstance = function(expected, actual) {
804 | if(expected == null || actual == null) {
805 | return new JSSpec.NullEqualityMatcher(expected, actual);
806 | } else if(expected._type && expected._type == actual._type) {
807 | if(expected._type == "String") {
808 | return new JSSpec.StringEqualityMatcher(expected, actual);
809 | } else if(expected._type == "Date") {
810 | return new JSSpec.DateEqualityMatcher(expected, actual);
811 | } else if(expected._type == "Number") {
812 | return new JSSpec.NumberEqualityMatcher(expected, actual);
813 | } else if(expected._type == "Array") {
814 | return new JSSpec.ArrayEqualityMatcher(expected, actual);
815 | } else if(expected._type == "Boolean") {
816 | return new JSSpec.BooleanEqualityMatcher(expected, actual);
817 | }
818 | }
819 |
820 | return new JSSpec.ObjectEqualityMatcher(expected, actual);
821 | };
822 |
823 | JSSpec.EqualityMatcher.basicExplain = function(expected, actual, expectedDesc, actualDesc) {
824 | var sb = [];
825 |
826 | sb.push(actualDesc || 'actual value:
');
827 | sb.push('' + JSSpec.util.inspect(actual) + '
');
828 | sb.push(expectedDesc || 'should be:
');
829 | sb.push('' + JSSpec.util.inspect(expected) + '
');
830 |
831 | return sb.join("");
832 | };
833 |
834 | JSSpec.EqualityMatcher.diffExplain = function(expected, actual) {
835 | var sb = [];
836 |
837 | sb.push('diff:
');
838 | sb.push('');
839 |
840 | var dmp = new diff_match_patch();
841 | var diff = dmp.diff_main(expected, actual);
842 | dmp.diff_cleanupEfficiency(diff);
843 |
844 | sb.push(JSSpec.util.inspect(dmp.diff_prettyHtml(diff), true));
845 |
846 | sb.push('
');
847 |
848 | return sb.join("");
849 | };
850 |
851 | /**
852 | * BooleanEqualityMatcher
853 | */
854 | JSSpec.BooleanEqualityMatcher = function(expected, actual) {
855 | this.expected = expected;
856 | this.actual = actual;
857 | };
858 |
859 | JSSpec.BooleanEqualityMatcher.prototype.explain = function() {
860 | var sb = [];
861 |
862 | sb.push('actual value:
');
863 | sb.push('' + JSSpec.util.inspect(this.actual) + '
');
864 | sb.push('should be:
');
865 | sb.push('' + JSSpec.util.inspect(this.expected) + '
');
866 |
867 | return sb.join("");
868 | };
869 |
870 | JSSpec.BooleanEqualityMatcher.prototype.matches = function() {
871 | return this.expected == this.actual;
872 | };
873 |
874 | /**
875 | * NullEqualityMatcher
876 | */
877 | JSSpec.NullEqualityMatcher = function(expected, actual) {
878 | this.expected = expected;
879 | this.actual = actual;
880 | };
881 |
882 | JSSpec.NullEqualityMatcher.prototype.matches = function() {
883 | return this.expected == this.actual && typeof this.expected == typeof this.actual;
884 | };
885 |
886 | JSSpec.NullEqualityMatcher.prototype.explain = function() {
887 | return JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual);
888 | };
889 |
890 | JSSpec.DateEqualityMatcher = function(expected, actual) {
891 | this.expected = expected;
892 | this.actual = actual;
893 | };
894 |
895 | JSSpec.DateEqualityMatcher.prototype.matches = function() {
896 | return this.expected.getTime() == this.actual.getTime();
897 | };
898 |
899 | JSSpec.DateEqualityMatcher.prototype.explain = function() {
900 | var sb = [];
901 |
902 | sb.push(JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual));
903 | sb.push(JSSpec.EqualityMatcher.diffExplain(this.expected.toString(), this.actual.toString()));
904 |
905 | return sb.join("");
906 | };
907 |
908 | /**
909 | * ObjectEqualityMatcher
910 | */
911 | JSSpec.ObjectEqualityMatcher = function(expected, actual) {
912 | this.expected = expected;
913 | this.actual = actual;
914 | this.match = this.expected == this.actual;
915 | this.explaination = this.makeExplain();
916 | };
917 |
918 | JSSpec.ObjectEqualityMatcher.prototype.matches = function() {return this.match};
919 |
920 | JSSpec.ObjectEqualityMatcher.prototype.explain = function() {return this.explaination};
921 |
922 | JSSpec.ObjectEqualityMatcher.prototype.makeExplain = function() {
923 | if(this.expected == this.actual) {
924 | this.match = true;
925 | return "";
926 | }
927 |
928 | if(JSSpec.util.isDomNode(this.expected)) {
929 | return this.makeExplainForDomNode();
930 | }
931 |
932 | var key, expectedHasItem, actualHasItem;
933 |
934 | for(key in this.expected) {
935 | expectedHasItem = this.expected[key] != null && typeof this.expected[key] != 'undefined';
936 | actualHasItem = this.actual[key] != null && typeof this.actual[key] != 'undefined';
937 | if(expectedHasItem && !actualHasItem) return this.makeExplainForMissingItem(key);
938 | }
939 | for(key in this.actual) {
940 | expectedHasItem = this.expected[key] != null && typeof this.expected[key] != 'undefined';
941 | actualHasItem = this.actual[key] != null && typeof this.actual[key] != 'undefined';
942 | if(actualHasItem && !expectedHasItem) return this.makeExplainForUnknownItem(key);
943 | }
944 |
945 | for(key in this.expected) {
946 | var matcher = JSSpec.EqualityMatcher.createInstance(this.expected[key], this.actual[key]);
947 | if(!matcher.matches()) return this.makeExplainForItemMismatch(key);
948 | }
949 |
950 | this.match = true;
951 | };
952 |
953 | JSSpec.ObjectEqualityMatcher.prototype.makeExplainForDomNode = function(key) {
954 | var sb = [];
955 |
956 | sb.push(JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual));
957 |
958 | return sb.join("");
959 | };
960 |
961 | JSSpec.ObjectEqualityMatcher.prototype.makeExplainForMissingItem = function(key) {
962 | var sb = [];
963 |
964 | sb.push('actual value has no item named ' + JSSpec.util.inspect(key) + '
');
965 | sb.push('' + JSSpec.util.inspect(this.actual, false, key) + '
');
966 | sb.push('but it should have the item whose value is ' + JSSpec.util.inspect(this.expected[key]) + '
');
967 | sb.push('' + JSSpec.util.inspect(this.expected, false, key) + '
');
968 |
969 | return sb.join("");
970 | };
971 |
972 | JSSpec.ObjectEqualityMatcher.prototype.makeExplainForUnknownItem = function(key) {
973 | var sb = [];
974 |
975 | sb.push('actual value has item named ' + JSSpec.util.inspect(key) + '
');
976 | sb.push('' + JSSpec.util.inspect(this.actual, false, key) + '
');
977 | sb.push('but there should be no such item
');
978 | sb.push('' + JSSpec.util.inspect(this.expected, false, key) + '
');
979 |
980 | return sb.join("");
981 | };
982 |
983 | JSSpec.ObjectEqualityMatcher.prototype.makeExplainForItemMismatch = function(key) {
984 | var sb = [];
985 |
986 | sb.push('actual value has an item named ' + JSSpec.util.inspect(key) + ' whose value is ' + JSSpec.util.inspect(this.actual[key]) + '
');
987 | sb.push('' + JSSpec.util.inspect(this.actual, false, key) + '
');
988 | sb.push('but it\'s value should be ' + JSSpec.util.inspect(this.expected[key]) + '
');
989 | sb.push('' + JSSpec.util.inspect(this.expected, false, key) + '
');
990 |
991 | return sb.join("");
992 | };
993 |
994 |
995 |
996 |
997 | /**
998 | * ArrayEqualityMatcher
999 | */
1000 | JSSpec.ArrayEqualityMatcher = function(expected, actual) {
1001 | this.expected = expected;
1002 | this.actual = actual;
1003 | this.match = this.expected == this.actual;
1004 | this.explaination = this.makeExplain();
1005 | };
1006 |
1007 | JSSpec.ArrayEqualityMatcher.prototype.matches = function() {return this.match};
1008 |
1009 | JSSpec.ArrayEqualityMatcher.prototype.explain = function() {return this.explaination};
1010 |
1011 | JSSpec.ArrayEqualityMatcher.prototype.makeExplain = function() {
1012 | if(this.expected.length != this.actual.length) return this.makeExplainForLengthMismatch();
1013 |
1014 | for(var i = 0; i < this.expected.length; i++) {
1015 | var matcher = JSSpec.EqualityMatcher.createInstance(this.expected[i], this.actual[i]);
1016 | if(!matcher.matches()) return this.makeExplainForItemMismatch(i);
1017 | }
1018 |
1019 | this.match = true;
1020 | };
1021 |
1022 | JSSpec.ArrayEqualityMatcher.prototype.makeExplainForLengthMismatch = function() {
1023 | return JSSpec.EqualityMatcher.basicExplain(
1024 | this.expected,
1025 | this.actual,
1026 | 'but it should be ' + this.expected.length + '
',
1027 | 'actual value has ' + this.actual.length + ' items
'
1028 | );
1029 | };
1030 |
1031 | JSSpec.ArrayEqualityMatcher.prototype.makeExplainForItemMismatch = function(index) {
1032 | var postfix = ["th", "st", "nd", "rd", "th"][Math.min((index + 1) % 10,4)];
1033 |
1034 | var sb = [];
1035 |
1036 | sb.push('' + (index + 1) + postfix + ' item (index ' + index + ') of actual value is ' + JSSpec.util.inspect(this.actual[index]) + ' :
');
1037 | sb.push('' + JSSpec.util.inspect(this.actual, false, index) + '
');
1038 | sb.push('but it should be ' + JSSpec.util.inspect(this.expected[index]) + ' :
');
1039 | sb.push('' + JSSpec.util.inspect(this.expected, false, index) + '
');
1040 |
1041 | return sb.join("");
1042 | };
1043 |
1044 | /**
1045 | * NumberEqualityMatcher
1046 | */
1047 | JSSpec.NumberEqualityMatcher = function(expected, actual) {
1048 | this.expected = expected;
1049 | this.actual = actual;
1050 | };
1051 |
1052 | JSSpec.NumberEqualityMatcher.prototype.matches = function() {
1053 | if(this.expected == this.actual) return true;
1054 | };
1055 |
1056 | JSSpec.NumberEqualityMatcher.prototype.explain = function() {
1057 | return JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual);
1058 | };
1059 |
1060 | /**
1061 | * StringEqualityMatcher
1062 | */
1063 | JSSpec.StringEqualityMatcher = function(expected, actual) {
1064 | this.expected = expected;
1065 | this.actual = actual;
1066 | };
1067 |
1068 | JSSpec.StringEqualityMatcher.prototype.matches = function() {
1069 | if(this.expected == this.actual) return true;
1070 | };
1071 |
1072 | JSSpec.StringEqualityMatcher.prototype.explain = function() {
1073 | var sb = [];
1074 |
1075 | sb.push(JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual));
1076 | sb.push(JSSpec.EqualityMatcher.diffExplain(this.expected, this.actual));
1077 | return sb.join("");
1078 | };
1079 |
1080 | /**
1081 | * PatternMatcher
1082 | */
1083 | JSSpec.PatternMatcher = function(actual, pattern, condition) {
1084 | this.actual = actual;
1085 | this.pattern = pattern;
1086 | this.condition = condition;
1087 | this.match = false;
1088 | this.explaination = this.makeExplain();
1089 | };
1090 |
1091 | JSSpec.PatternMatcher.createInstance = function(actual, pattern, condition) {
1092 | return new JSSpec.PatternMatcher(actual, pattern, condition);
1093 | };
1094 |
1095 | JSSpec.PatternMatcher.prototype.makeExplain = function() {
1096 | var sb;
1097 | if(this.actual == null || this.actual._type != 'String') {
1098 | sb = [];
1099 | sb.push('actual value:
');
1100 | sb.push('' + JSSpec.util.inspect(this.actual) + '
');
1101 | sb.push('should ' + (this.condition ? '' : 'not') + ' match with pattern:
');
1102 | sb.push('' + JSSpec.util.inspect(this.pattern) + '
');
1103 | sb.push('but pattern matching cannot be performed.
');
1104 | return sb.join("");
1105 | } else {
1106 | this.match = this.condition == !!this.actual.match(this.pattern);
1107 | if(this.match) return "";
1108 |
1109 | sb = [];
1110 | sb.push('actual value:
');
1111 | sb.push('' + JSSpec.util.inspect(this.actual) + '
');
1112 | sb.push('should ' + (this.condition ? '' : 'not') + ' match with pattern:
');
1113 | sb.push('' + JSSpec.util.inspect(this.pattern) + '
');
1114 | return sb.join("");
1115 | }
1116 | };
1117 |
1118 | JSSpec.PatternMatcher.prototype.matches = function() {
1119 | return this.match;
1120 | };
1121 |
1122 | JSSpec.PatternMatcher.prototype.explain = function() {
1123 | return this.explaination;
1124 | };
1125 |
1126 | /**
1127 | * Domain Specific Languages
1128 | */
1129 | JSSpec.DSL = {};
1130 |
1131 | JSSpec.DSL.forString = {
1132 | normalizeHtml: function() {
1133 | var html = this;
1134 |
1135 | // Uniformize quotation, turn tag names and attribute names into lower case
1136 | html = html.replace(/<(\/?)(\w+)([^>]*?)>/img, function(str, closingMark, tagName, attrs) {
1137 | var sortedAttrs = JSSpec.util.sortHtmlAttrs(JSSpec.util.correctHtmlAttrQuotation(attrs).toLowerCase())
1138 | return "<" + closingMark + tagName.toLowerCase() + sortedAttrs + ">"
1139 | });
1140 |
1141 | // validation self-closing tags
1142 | html = html.replace(/<(br|hr|img)([^>]*?)>/mg, function(str, tag, attrs) {
1143 | return "<" + tag + attrs + " />";
1144 | });
1145 |
1146 | // append semi-colon at the end of style value
1147 | html = html.replace(/style="(.*?)"/mg, function(str, styleStr) {
1148 | styleStr = JSSpec.util.sortStyleEntries(styleStr.strip()); // for Safari
1149 | if(styleStr.charAt(styleStr.length - 1) != ';') styleStr += ";"
1150 |
1151 | return 'style="' + styleStr + '"'
1152 | });
1153 |
1154 | // sort style entries
1155 |
1156 | // remove empty style attributes
1157 | html = html.replace(/ style=";"/mg, "");
1158 |
1159 | // remove new-lines
1160 | html = html.replace(/\r/mg, '');
1161 | html = html.replace(/\n/mg, '');
1162 |
1163 | return html;
1164 | }
1165 | };
1166 |
1167 |
1168 |
1169 | JSSpec.DSL.describe = function(context, entries) {
1170 | JSSpec.specs.push(new JSSpec.Spec(context, entries));
1171 | };
1172 |
1173 | JSSpec.DSL.value_of = function(target) {
1174 | if(JSSpec._secondPass) return {};
1175 |
1176 | var subject = new JSSpec.DSL.Subject(target);
1177 | return subject;
1178 | };
1179 |
1180 | JSSpec.DSL.Subject = function(target) {
1181 | this.target = target;
1182 | };
1183 |
1184 | JSSpec.DSL.Subject.prototype._type = 'Subject';
1185 |
1186 | JSSpec.DSL.Subject.prototype.should_fail = function(message) {
1187 | JSSpec._assertionFailure = {message:message};
1188 | throw JSSpec._assertionFailure;
1189 | };
1190 |
1191 | JSSpec.DSL.Subject.prototype.should_be = function(expected) {
1192 | var matcher = JSSpec.EqualityMatcher.createInstance(expected, this.target);
1193 | if(!matcher.matches()) {
1194 | JSSpec._assertionFailure = {message:matcher.explain()};
1195 | throw JSSpec._assertionFailure;
1196 | }
1197 | };
1198 |
1199 | JSSpec.DSL.Subject.prototype.should_not_be = function(expected) {
1200 | // TODO JSSpec.EqualityMatcher should support 'condition'
1201 | var matcher = JSSpec.EqualityMatcher.createInstance(expected, this.target);
1202 | if(matcher.matches()) {
1203 | JSSpec._assertionFailure = {message:"'" + this.target + "' should not be '" + expected + "'"};
1204 | throw JSSpec._assertionFailure;
1205 | }
1206 | };
1207 |
1208 | JSSpec.DSL.Subject.prototype.should_be_empty = function() {
1209 | this.should_have(0, this.getType() == 'String' ? 'characters' : 'items');
1210 | };
1211 |
1212 | JSSpec.DSL.Subject.prototype.should_not_be_empty = function() {
1213 | this.should_have_at_least(1, this.getType() == 'String' ? 'characters' : 'items');
1214 | };
1215 |
1216 | JSSpec.DSL.Subject.prototype.should_be_true = function() {
1217 | this.should_be(true);
1218 | };
1219 |
1220 | JSSpec.DSL.Subject.prototype.should_be_false = function() {
1221 | this.should_be(false);
1222 | };
1223 |
1224 | JSSpec.DSL.Subject.prototype.should_be_null = function() {
1225 | this.should_be(null);
1226 | };
1227 |
1228 | JSSpec.DSL.Subject.prototype.should_be_undefined = function() {
1229 | this.should_be(undefined);
1230 | };
1231 |
1232 | JSSpec.DSL.Subject.prototype.should_not_be_null = function() {
1233 | this.should_not_be(null);
1234 | };
1235 |
1236 | JSSpec.DSL.Subject.prototype.should_not_be_undefined = function() {
1237 | this.should_not_be(undefined);
1238 | };
1239 |
1240 | JSSpec.DSL.Subject.prototype._should_have = function(num, property, condition) {
1241 | var matcher = JSSpec.PropertyLengthMatcher.createInstance(num, property, this.target, condition);
1242 | if(!matcher.matches()) {
1243 | JSSpec._assertionFailure = {message:matcher.explain()};
1244 | throw JSSpec._assertionFailure;
1245 | }
1246 | };
1247 |
1248 | JSSpec.DSL.Subject.prototype.should_have = function(num, property) {
1249 | this._should_have(num, property, "exactly");
1250 | };
1251 |
1252 | JSSpec.DSL.Subject.prototype.should_have_exactly = function(num, property) {
1253 | this._should_have(num, property, "exactly");
1254 | };
1255 |
1256 | JSSpec.DSL.Subject.prototype.should_have_at_least = function(num, property) {
1257 | this._should_have(num, property, "at least");
1258 | };
1259 |
1260 | JSSpec.DSL.Subject.prototype.should_have_at_most = function(num, property) {
1261 | this._should_have(num, property, "at most");
1262 | };
1263 |
1264 | JSSpec.DSL.Subject.prototype.should_include = function(expected) {
1265 | var matcher = JSSpec.IncludeMatcher.createInstance(this.target, expected, true);
1266 | if(!matcher.matches()) {
1267 | JSSpec._assertionFailure = {message:matcher.explain()};
1268 | throw JSSpec._assertionFailure;
1269 | }
1270 | };
1271 |
1272 | JSSpec.DSL.Subject.prototype.should_not_include = function(expected) {
1273 | var matcher = JSSpec.IncludeMatcher.createInstance(this.target, expected, false);
1274 | if(!matcher.matches()) {
1275 | JSSpec._assertionFailure = {message:matcher.explain()};
1276 | throw JSSpec._assertionFailure;
1277 | }
1278 | };
1279 |
1280 | JSSpec.DSL.Subject.prototype.should_match = function(pattern) {
1281 | var matcher = JSSpec.PatternMatcher.createInstance(this.target, pattern, true);
1282 | if(!matcher.matches()) {
1283 | JSSpec._assertionFailure = {message:matcher.explain()};
1284 | throw JSSpec._assertionFailure;
1285 | }
1286 | }
1287 | JSSpec.DSL.Subject.prototype.should_not_match = function(pattern) {
1288 | var matcher = JSSpec.PatternMatcher.createInstance(this.target, pattern, false);
1289 | if(!matcher.matches()) {
1290 | JSSpec._assertionFailure = {message:matcher.explain()};
1291 | throw JSSpec._assertionFailure;
1292 | }
1293 | };
1294 |
1295 | JSSpec.DSL.Subject.prototype.getType = function() {
1296 | if(typeof this.target == 'undefined') {
1297 | return 'undefined';
1298 | } else if(this.target == null) {
1299 | return 'null';
1300 | } else if(this.target._type) {
1301 | return this.target._type;
1302 | } else if(JSSpec.util.isDomNode(this.target)) {
1303 | return 'DomNode';
1304 | } else {
1305 | return 'object';
1306 | }
1307 | };
1308 |
1309 | /**
1310 | * Utilities
1311 | */
1312 | JSSpec.util = {
1313 | escapeTags: function(string) {
1314 | return string.replace(//img, '>');
1315 | },
1316 | parseOptions: function(defaults) {
1317 | var options = defaults;
1318 |
1319 | var url = location.href;
1320 | var queryIndex = url.indexOf('?');
1321 | if(queryIndex == -1) return options;
1322 |
1323 | var query = url.substring(queryIndex + 1);
1324 | var pairs = query.split('&');
1325 | for(var i = 0; i < pairs.length; i++) {
1326 | var tokens = pairs[i].split('=');
1327 | options[tokens[0]] = tokens[1];
1328 | }
1329 |
1330 | return options;
1331 | },
1332 | correctHtmlAttrQuotation: function(html) {
1333 | html = html.replace(/(\w+)=['"]([^'"]+)['"]/mg,function (str, name, value) {return name + '=' + '"' + value + '"';});
1334 | html = html.replace(/(\w+)=([^ '"]+)/mg,function (str, name, value) {return name + '=' + '"' + value + '"';});
1335 | html = html.replace(/'/mg, '"');
1336 |
1337 | return html;
1338 | },
1339 | sortHtmlAttrs: function(html) {
1340 | var attrs = [];
1341 | html.replace(/((\w+)="[^"]+")/mg, function(str, matched) {
1342 | attrs.push(matched);
1343 | });
1344 | return attrs.length == 0 ? "" : " " + attrs.sort().join(" ");
1345 | },
1346 | sortStyleEntries: function(styleText) {
1347 | var entries = styleText.split(/; /);
1348 | return entries.sort().join("; ");
1349 | },
1350 | escapeHtml: function(str) {
1351 | if(!this._div) {
1352 | this._div = document.createElement("DIV");
1353 | this._text = document.createTextNode('');
1354 | this._div.appendChild(this._text);
1355 | }
1356 | this._text.data = str;
1357 | return this._div.innerHTML;
1358 | },
1359 | isDomNode: function(o) {
1360 | // TODO: make it more stricter
1361 | return (typeof o.nodeName == 'string') && (typeof o.nodeType == 'number');
1362 | },
1363 | inspectDomPath: function(o) {
1364 | var sb = [];
1365 | while(o && o.nodeName != '#document' && o.parent) {
1366 | var siblings = o.parentNode.childNodes;
1367 | for(var i = 0; i < siblings.length; i++) {
1368 | if(siblings[i] == o) {
1369 | sb.push(o.nodeName + (i == 0 ? '' : '[' + i + ']'));
1370 | break;
1371 | }
1372 | }
1373 | o = o.parentNode;
1374 | }
1375 | return sb.join(" > ");
1376 | },
1377 | inspectDomNode: function(o) {
1378 | if(o.nodeType == 1) {
1379 | var nodeName = o.nodeName.toLowerCase();
1380 | var sb = [];
1381 | sb.push('');
1382 | sb.push("<");
1383 | sb.push(nodeName);
1384 |
1385 | var attrs = o.attributes;
1386 | for(var i = 0; i < attrs.length; i++) {
1387 | if(
1388 | attrs[i].nodeValue &&
1389 | attrs[i].nodeName != 'contentEditable' &&
1390 | attrs[i].nodeName != 'style' &&
1391 | typeof attrs[i].nodeValue != 'function'
1392 | ) sb.push(' ' + attrs[i].nodeName.toLowerCase() + ' ="' + attrs[i].nodeValue + '" ');
1393 | }
1394 | if(o.style && o.style.cssText) {
1395 | sb.push(' style ="' + o.style.cssText + '" ');
1396 | }
1397 | sb.push('>');
1398 | sb.push(JSSpec.util.escapeHtml(o.innerHTML));
1399 | sb.push('</' + nodeName + '>');
1400 | sb.push(' (' + JSSpec.util.inspectDomPath(o) + ') ' );
1401 | sb.push(' ');
1402 | return sb.join("");
1403 | } else if(o.nodeType == 3) {
1404 | return '#text ' + o.nodeValue + ' ';
1405 | } else {
1406 | return 'UnknownDomNode ';
1407 | }
1408 | },
1409 | inspect: function(o, dontEscape, emphasisKey) {
1410 | var sb, inspected;
1411 |
1412 | if(typeof o == 'undefined') return 'undefined ';
1413 | if(o == null) return 'null ';
1414 | if(o._type == 'String') return '"' + (dontEscape ? o : JSSpec.util.escapeHtml(o)) + '" ';
1415 |
1416 | if(o._type == 'Date') {
1417 | return '"' + o.toString() + '" ';
1418 | }
1419 |
1420 | if(o._type == 'Number') return '' + (dontEscape ? o : JSSpec.util.escapeHtml(o)) + ' ';
1421 |
1422 | if(o._type == 'Boolean') return '' + o + ' ';
1423 |
1424 | if(o._type == 'RegExp') return '' + JSSpec.util.escapeHtml(o.toString()) + ' ';
1425 |
1426 | if(JSSpec.util.isDomNode(o)) return JSSpec.util.inspectDomNode(o);
1427 |
1428 | if(o._type == 'Array' || typeof o.length != 'undefined') {
1429 | sb = [];
1430 | for(var i = 0; i < o.length; i++) {
1431 | inspected = JSSpec.util.inspect(o[i]);
1432 | sb.push(i == emphasisKey ? ('' + inspected + ' ') : inspected);
1433 | }
1434 | return '[' + sb.join(', ') + '] ';
1435 | }
1436 |
1437 | // object
1438 | sb = [];
1439 | for(var key in o) {
1440 | if(key == 'should') continue;
1441 |
1442 | inspected = JSSpec.util.inspect(key) + ":" + JSSpec.util.inspect(o[key]);
1443 | sb.push(key == emphasisKey ? ('' + inspected + ' ') : inspected);
1444 | }
1445 | return '{' + sb.join(', ') + '} ';
1446 | }
1447 | };
1448 |
1449 | describe = JSSpec.DSL.describe;
1450 | behavior_of = JSSpec.DSL.describe;
1451 | value_of = JSSpec.DSL.value_of;
1452 | expect = JSSpec.DSL.value_of; // @deprecated
1453 |
1454 | String.prototype._type = "String";
1455 | Number.prototype._type = "Number";
1456 | Date.prototype._type = "Date";
1457 | Array.prototype._type = "Array";
1458 | Boolean.prototype._type = "Boolean";
1459 | RegExp.prototype._type = "RegExp";
1460 |
1461 | var targets = [Array.prototype, Date.prototype, Number.prototype, String.prototype, Boolean.prototype, RegExp.prototype];
1462 |
1463 | String.prototype.normalizeHtml = JSSpec.DSL.forString.normalizeHtml;
1464 | String.prototype.asHtml = String.prototype.normalizeHtml; //@deprecated
1465 |
1466 |
1467 |
1468 | /**
1469 | * Main
1470 | */
1471 | JSSpec.defaultOptions = {
1472 | autorun: 1,
1473 | specIdBeginsWith: 0,
1474 | exampleIdBeginsWith: 0,
1475 | autocollapse: 1
1476 | };
1477 | JSSpec.options = JSSpec.util.parseOptions(JSSpec.defaultOptions);
1478 |
1479 | JSSpec.Spec.id = JSSpec.options.specIdBeginsWith;
1480 | JSSpec.Example.id = JSSpec.options.exampleIdBeginsWith;
1481 |
1482 |
1483 |
1484 | window.onload = function() {
1485 | if(JSSpec.specs.length > 0) {
1486 | if(!JSSpec.options.inSuite) {
1487 | JSSpec.runner = new JSSpec.Runner(JSSpec.specs, new JSSpec.Logger());
1488 | if(JSSpec.options.rerun) {
1489 | JSSpec.runner.rerun(decodeURIComponent(JSSpec.options.rerun));
1490 | } else {
1491 | JSSpec.runner.run();
1492 | }
1493 | } else {
1494 | // in suite, send all specs to parent
1495 | var parentWindow = window.frames.parent.window;
1496 | for(var i = 0; i < JSSpec.specs.length; i++) {
1497 | parentWindow.JSSpec.specs.push(JSSpec.specs[i]);
1498 | }
1499 | }
1500 | } else {
1501 | var links = document.getElementById('list').getElementsByTagName('A');
1502 | var frameContainer = document.createElement('DIV');
1503 | frameContainer.style.display = 'none';
1504 | document.body.appendChild(frameContainer);
1505 |
1506 | for(var i = 0; i < links.length; i++) {
1507 | var frame = document.createElement('IFRAME');
1508 | frame.src = links[i].href + '?inSuite=0&specIdBeginsWith=' + (i * 10000) + '&exampleIdBeginsWith=' + (i * 10000);
1509 | frameContainer.appendChild(frame);
1510 | }
1511 | }
1512 | }
--------------------------------------------------------------------------------
/Specs/Assets/Scripts/DiffMatchPatch.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Diff Match and Patch
3 | *
4 | * Copyright 2006 Google Inc.
5 | * http://code.google.com/p/google-diff-match-patch/
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | */
19 |
20 | /**
21 | * @fileoverview Computes the difference between two texts to create a patch.
22 | * Applies the patch onto another text, allowing for errors.
23 | * @author fraser@google.com (Neil Fraser)
24 | */
25 |
26 | /**
27 | * Class containing the diff, match and patch methods.
28 | * @constructor
29 | */
30 | function diff_match_patch() {
31 |
32 | // Defaults.
33 | // Redefine these in your program to override the defaults.
34 |
35 | // Number of seconds to map a diff before giving up. (0 for infinity)
36 | this.Diff_Timeout = 1.0;
37 | // Cost of an empty edit operation in terms of edit characters.
38 | this.Diff_EditCost = 4;
39 | // The size beyond which the double-ended diff activates.
40 | // Double-ending is twice as fast, but less accurate.
41 | this.Diff_DualThreshold = 32;
42 | // Tweak the relative importance (0.0 = accuracy, 1.0 = proximity)
43 | this.Match_Balance = 0.5;
44 | // At what point is no match declared (0.0 = perfection, 1.0 = very loose)
45 | this.Match_Threshold = 0.5;
46 | // The min and max cutoffs used when computing text lengths.
47 | this.Match_MinLength = 100;
48 | this.Match_MaxLength = 1000;
49 | // Chunk size for context length.
50 | this.Patch_Margin = 4;
51 |
52 | /**
53 | * Compute the number of bits in an int.
54 | * The normal answer for JavaScript is 32.
55 | * @return {number} Max bits
56 | */
57 | function getMaxBits() {
58 | var maxbits = 0;
59 | var oldi = 1;
60 | var newi = 2;
61 | while (oldi != newi) {
62 | maxbits++;
63 | oldi = newi;
64 | newi = newi << 1;
65 | }
66 | return maxbits;
67 | }
68 | // How many bits in a number?
69 | this.Match_MaxBits = getMaxBits();
70 | }
71 |
72 |
73 | // DIFF FUNCTIONS
74 |
75 |
76 | /**
77 | * The data structure representing a diff is an array of tuples:
78 | * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']]
79 | * which means: delete 'Hello', add 'Goodbye' and keep ' world.'
80 | */
81 | var DIFF_DELETE = -1;
82 | var DIFF_INSERT = 1;
83 | var DIFF_EQUAL = 0;
84 |
85 |
86 | /**
87 | * Find the differences between two texts. Simplifies the problem by stripping
88 | * any common prefix or suffix off the texts before diffing.
89 | * @param {string} text1 Old string to be diffed
90 | * @param {string} text2 New string to be diffed
91 | * @param {boolean} opt_checklines Optional speedup flag. If present and false,
92 | * then don't run a line-level diff first to identify the changed areas.
93 | * Defaults to true, which does a faster, slightly less optimal diff
94 | * @return {Array.>} Array of diff tuples
95 | */
96 | diff_match_patch.prototype.diff_main = function(text1, text2, opt_checklines) {
97 | // Check for equality (speedup)
98 | if (text1 == text2) {
99 | return [[DIFF_EQUAL, text1]];
100 | }
101 |
102 | if (typeof opt_checklines == 'undefined') {
103 | opt_checklines = true;
104 | }
105 | var checklines = opt_checklines;
106 |
107 | // Trim off common prefix (speedup)
108 | var commonlength = this.diff_commonPrefix(text1, text2);
109 | var commonprefix = text1.substring(0, commonlength);
110 | text1 = text1.substring(commonlength);
111 | text2 = text2.substring(commonlength);
112 |
113 | // Trim off common suffix (speedup)
114 | commonlength = this.diff_commonSuffix(text1, text2);
115 | var commonsuffix = text1.substring(text1.length - commonlength);
116 | text1 = text1.substring(0, text1.length - commonlength);
117 | text2 = text2.substring(0, text2.length - commonlength);
118 |
119 | // Compute the diff on the middle block
120 | var diffs = this.diff_compute(text1, text2, checklines);
121 |
122 | // Restore the prefix and suffix
123 | if (commonprefix) {
124 | diffs.unshift([DIFF_EQUAL, commonprefix]);
125 | }
126 | if (commonsuffix) {
127 | diffs.push([DIFF_EQUAL, commonsuffix]);
128 | }
129 | this.diff_cleanupMerge(diffs);
130 | return diffs;
131 | };
132 |
133 |
134 | /**
135 | * Find the differences between two texts. Assumes that the texts do not
136 | * have any common prefix or suffix.
137 | * @param {string} text1 Old string to be diffed
138 | * @param {string} text2 New string to be diffed
139 | * @param {boolean} checklines Speedup flag. If false, then don't run a
140 | * line-level diff first to identify the changed areas.
141 | * If true, then run a faster, slightly less optimal diff
142 | * @return {Array.>} Array of diff tuples
143 | * @private
144 | */
145 | diff_match_patch.prototype.diff_compute = function(text1, text2, checklines) {
146 | var diffs;
147 |
148 | if (!text1) {
149 | // Just add some text (speedup)
150 | return [[DIFF_INSERT, text2]];
151 | }
152 |
153 | if (!text2) {
154 | // Just delete some text (speedup)
155 | return [[DIFF_DELETE, text1]];
156 | }
157 |
158 | var longtext = text1.length > text2.length ? text1 : text2;
159 | var shorttext = text1.length > text2.length ? text2 : text1;
160 | var i = longtext.indexOf(shorttext);
161 | if (i != -1) {
162 | // Shorter text is inside the longer text (speedup)
163 | diffs = [[DIFF_INSERT, longtext.substring(0, i)],
164 | [DIFF_EQUAL, shorttext],
165 | [DIFF_INSERT, longtext.substring(i + shorttext.length)]];
166 | // Swap insertions for deletions if diff is reversed.
167 | if (text1.length > text2.length) {
168 | diffs[0][0] = diffs[2][0] = DIFF_DELETE;
169 | }
170 | return diffs;
171 | }
172 | longtext = shorttext = null; // Garbage collect
173 |
174 | // Check to see if the problem can be split in two.
175 | var hm = this.diff_halfMatch(text1, text2);
176 | if (hm) {
177 | // A half-match was found, sort out the return data.
178 | var text1_a = hm[0];
179 | var text1_b = hm[1];
180 | var text2_a = hm[2];
181 | var text2_b = hm[3];
182 | var mid_common = hm[4];
183 | // Send both pairs off for separate processing.
184 | var diffs_a = this.diff_main(text1_a, text2_a, checklines);
185 | var diffs_b = this.diff_main(text1_b, text2_b, checklines);
186 | // Merge the results.
187 | return diffs_a.concat([[DIFF_EQUAL, mid_common]], diffs_b);
188 | }
189 |
190 | // Perform a real diff.
191 | if (checklines && (text1.length < 100 || text2.length < 100)) {
192 | // Too trivial for the overhead.
193 | checklines = false;
194 | }
195 | var linearray;
196 | if (checklines) {
197 | // Scan the text on a line-by-line basis first.
198 | var a = this.diff_linesToChars(text1, text2);
199 | text1 = a[0];
200 | text2 = a[1];
201 | linearray = a[2];
202 | }
203 | diffs = this.diff_map(text1, text2);
204 | if (!diffs) {
205 | // No acceptable result.
206 | diffs = [[DIFF_DELETE, text1], [DIFF_INSERT, text2]];
207 | }
208 | if (checklines) {
209 | // Convert the diff back to original text.
210 | this.diff_charsToLines(diffs, linearray);
211 | // Eliminate freak matches (e.g. blank lines)
212 | this.diff_cleanupSemantic(diffs);
213 |
214 | // Rediff any replacement blocks, this time character-by-character.
215 | // Add a dummy entry at the end.
216 | diffs.push([DIFF_EQUAL, '']);
217 | var pointer = 0;
218 | var count_delete = 0;
219 | var count_insert = 0;
220 | var text_delete = '';
221 | var text_insert = '';
222 | while (pointer < diffs.length) {
223 | switch (diffs[pointer][0]) {
224 | case DIFF_INSERT:
225 | count_insert++;
226 | text_insert += diffs[pointer][1];
227 | break;
228 | case DIFF_DELETE:
229 | count_delete++;
230 | text_delete += diffs[pointer][1];
231 | break;
232 | case DIFF_EQUAL:
233 | // Upon reaching an equality, check for prior redundancies.
234 | if (count_delete >= 1 && count_insert >= 1) {
235 | // Delete the offending records and add the merged ones.
236 | var a = this.diff_main(text_delete, text_insert, false);
237 | diffs.splice(pointer - count_delete - count_insert,
238 | count_delete + count_insert);
239 | pointer = pointer - count_delete - count_insert;
240 | for (var j = a.length - 1; j >= 0; j--) {
241 | diffs.splice(pointer, 0, a[j]);
242 | }
243 | pointer = pointer + a.length;
244 | }
245 | count_insert = 0;
246 | count_delete = 0;
247 | text_delete = '';
248 | text_insert = '';
249 | break;
250 | }
251 | pointer++;
252 | }
253 | diffs.pop(); // Remove the dummy entry at the end.
254 | }
255 | return diffs;
256 | };
257 |
258 |
259 | /**
260 | * Split two texts into an array of strings. Reduce the texts to a string of
261 | * hashes where each Unicode character represents one line.
262 | * @param {string} text1 First string
263 | * @param {string} text2 Second string
264 | * @return {Array.>} Three element Array, containing the
265 | * encoded text1, the encoded text2 and the array of unique strings. The
266 | * zeroth element of the array of unique strings is intentionally blank.
267 | * @private
268 | */
269 | diff_match_patch.prototype.diff_linesToChars = function(text1, text2) {
270 | var lineArray = []; // e.g. lineArray[4] == 'Hello\n'
271 | var lineHash = {}; // e.g. lineHash['Hello\n'] == 4
272 |
273 | // '\x00' is a valid character, but various debuggers don't like it.
274 | // So we'll insert a junk entry to avoid generating a null character.
275 | lineArray[0] = '';
276 |
277 | /**
278 | * Split a text into an array of strings. Reduce the texts to a string of
279 | * hashes where each Unicode character represents one line.
280 | * Modifies linearray and linehash through being a closure.
281 | * @param {string} text String to encode
282 | * @return {string} Encoded string
283 | * @private
284 | */
285 | function diff_linesToCharsMunge(text) {
286 | var chars = '';
287 | // Walk the text, pulling out a substring for each line.
288 | // text.split('\n') would would temporarily double our memory footprint.
289 | // Modifying text would create many large strings to garbage collect.
290 | var lineStart = 0;
291 | var lineEnd = -1;
292 | // Keeping our own length variable is faster than looking it up.
293 | var lineArrayLength = lineArray.length;
294 | while (lineEnd < text.length - 1) {
295 | lineEnd = text.indexOf('\n', lineStart);
296 | if (lineEnd == -1) {
297 | lineEnd = text.length - 1;
298 | }
299 | var line = text.substring(lineStart, lineEnd + 1);
300 | lineStart = lineEnd + 1;
301 |
302 | if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) :
303 | (lineHash[line] !== undefined)) {
304 | chars += String.fromCharCode(lineHash[line]);
305 | } else {
306 | chars += String.fromCharCode(lineArrayLength);
307 | lineHash[line] = lineArrayLength;
308 | lineArray[lineArrayLength++] = line;
309 | }
310 | }
311 | return chars;
312 | }
313 |
314 | var chars1 = diff_linesToCharsMunge(text1);
315 | var chars2 = diff_linesToCharsMunge(text2);
316 | return [chars1, chars2, lineArray];
317 | };
318 |
319 |
320 | /**
321 | * Rehydrate the text in a diff from a string of line hashes to real lines of
322 | * text.
323 | * @param {Array.>} diffs Array of diff tuples
324 | * @param {Array.} lineArray Array of unique strings
325 | * @private
326 | */
327 | diff_match_patch.prototype.diff_charsToLines = function(diffs, lineArray) {
328 | for (var x = 0; x < diffs.length; x++) {
329 | var chars = diffs[x][1];
330 | var text = [];
331 | for (var y = 0; y < chars.length; y++) {
332 | text[y] = lineArray[chars.charCodeAt(y)];
333 | }
334 | diffs[x][1] = text.join('');
335 | }
336 | };
337 |
338 |
339 | /**
340 | * Explore the intersection points between the two texts.
341 | * @param {string} text1 Old string to be diffed
342 | * @param {string} text2 New string to be diffed
343 | * @return {Array.>?} Array of diff tuples or null if no diff
344 | * available
345 | * @private
346 | */
347 | diff_match_patch.prototype.diff_map = function(text1, text2) {
348 | // Don't run for too long.
349 | var ms_end = (new Date()).getTime() + this.Diff_Timeout * 1000;
350 | var max_d = text1.length + text2.length - 1;
351 | var doubleEnd = this.Diff_DualThreshold * 2 < max_d;
352 | var v_map1 = [];
353 | var v_map2 = [];
354 | var v1 = {};
355 | var v2 = {};
356 | v1[1] = 0;
357 | v2[1] = 0;
358 | var x, y;
359 | var footstep; // Used to track overlapping paths.
360 | var footsteps = {};
361 | var done = false;
362 | // Safari 1.x doesn't have hasOwnProperty
363 | var hasOwnProperty = !!(footsteps.hasOwnProperty);
364 | // If the total number of characters is odd, then the front path will collide
365 | // with the reverse path.
366 | var front = (text1.length + text2.length) % 2;
367 | for (var d = 0; d < max_d; d++) {
368 | // Bail out if timeout reached.
369 | if (this.Diff_Timeout > 0 && (new Date()).getTime() > ms_end) {
370 | return null;
371 | }
372 |
373 | // Walk the front path one step.
374 | v_map1[d] = {};
375 | for (var k = -d; k <= d; k += 2) {
376 | if (k == -d || k != d && v1[k - 1] < v1[k + 1]) {
377 | x = v1[k + 1];
378 | } else {
379 | x = v1[k - 1] + 1;
380 | }
381 | y = x - k;
382 | if (doubleEnd) {
383 | footstep = x + ',' + y;
384 | if (front && (hasOwnProperty ? footsteps.hasOwnProperty(footstep) :
385 | (footsteps[footstep] !== undefined))) {
386 | done = true;
387 | }
388 | if (!front) {
389 | footsteps[footstep] = d;
390 | }
391 | }
392 | while (!done && x < text1.length && y < text2.length &&
393 | text1.charAt(x) == text2.charAt(y)) {
394 | x++;
395 | y++;
396 | if (doubleEnd) {
397 | footstep = x + ',' + y;
398 | if (front && (hasOwnProperty ? footsteps.hasOwnProperty(footstep) :
399 | (footsteps[footstep] !== undefined))) {
400 | done = true;
401 | }
402 | if (!front) {
403 | footsteps[footstep] = d;
404 | }
405 | }
406 | }
407 | v1[k] = x;
408 | v_map1[d][x + ',' + y] = true;
409 | if (x == text1.length && y == text2.length) {
410 | // Reached the end in single-path mode.
411 | return this.diff_path1(v_map1, text1, text2);
412 | } else if (done) {
413 | // Front path ran over reverse path.
414 | v_map2 = v_map2.slice(0, footsteps[footstep] + 1);
415 | var a = this.diff_path1(v_map1, text1.substring(0, x),
416 | text2.substring(0, y));
417 | return a.concat(this.diff_path2(v_map2, text1.substring(x),
418 | text2.substring(y)));
419 | }
420 | }
421 |
422 | if (doubleEnd) {
423 | // Walk the reverse path one step.
424 | v_map2[d] = {};
425 | for (var k = -d; k <= d; k += 2) {
426 | if (k == -d || k != d && v2[k - 1] < v2[k + 1]) {
427 | x = v2[k + 1];
428 | } else {
429 | x = v2[k - 1] + 1;
430 | }
431 | y = x - k;
432 | footstep = (text1.length - x) + ',' + (text2.length - y);
433 | if (!front && (hasOwnProperty ? footsteps.hasOwnProperty(footstep) :
434 | (footsteps[footstep] !== undefined))) {
435 | done = true;
436 | }
437 | if (front) {
438 | footsteps[footstep] = d;
439 | }
440 | while (!done && x < text1.length && y < text2.length &&
441 | text1.charAt(text1.length - x - 1) ==
442 | text2.charAt(text2.length - y - 1)) {
443 | x++;
444 | y++;
445 | footstep = (text1.length - x) + ',' + (text2.length - y);
446 | if (!front && (hasOwnProperty ? footsteps.hasOwnProperty(footstep) :
447 | (footsteps[footstep] !== undefined))) {
448 | done = true;
449 | }
450 | if (front) {
451 | footsteps[footstep] = d;
452 | }
453 | }
454 | v2[k] = x;
455 | v_map2[d][x + ',' + y] = true;
456 | if (done) {
457 | // Reverse path ran over front path.
458 | v_map1 = v_map1.slice(0, footsteps[footstep] + 1);
459 | var a = this.diff_path1(v_map1, text1.substring(0, text1.length - x),
460 | text2.substring(0, text2.length - y));
461 | return a.concat(this.diff_path2(v_map2,
462 | text1.substring(text1.length - x),
463 | text2.substring(text2.length - y)));
464 | }
465 | }
466 | }
467 | }
468 | // Number of diffs equals number of characters, no commonality at all.
469 | return null;
470 | };
471 |
472 |
473 | /**
474 | * Work from the middle back to the start to determine the path.
475 | * @param {Array.} v_map Array of paths.
476 | * @param {string} text1 Old string fragment to be diffed
477 | * @param {string} text2 New string fragment to be diffed
478 | * @return {Array.>} Array of diff tuples
479 | * @private
480 | */
481 | diff_match_patch.prototype.diff_path1 = function(v_map, text1, text2) {
482 | var path = [];
483 | var x = text1.length;
484 | var y = text2.length;
485 | /** @type {number?} */
486 | var last_op = null;
487 | for (var d = v_map.length - 2; d >= 0; d--) {
488 | while (1) {
489 | if (v_map[d].hasOwnProperty ? v_map[d].hasOwnProperty((x - 1) + ',' + y) :
490 | (v_map[d][(x - 1) + ',' + y] !== undefined)) {
491 | x--;
492 | if (last_op === DIFF_DELETE) {
493 | path[0][1] = text1.charAt(x) + path[0][1];
494 | } else {
495 | path.unshift([DIFF_DELETE, text1.charAt(x)]);
496 | }
497 | last_op = DIFF_DELETE;
498 | break;
499 | } else if (v_map[d].hasOwnProperty ?
500 | v_map[d].hasOwnProperty(x + ',' + (y - 1)) :
501 | (v_map[d][x + ',' + (y - 1)] !== undefined)) {
502 | y--;
503 | if (last_op === DIFF_INSERT) {
504 | path[0][1] = text2.charAt(y) + path[0][1];
505 | } else {
506 | path.unshift([DIFF_INSERT, text2.charAt(y)]);
507 | }
508 | last_op = DIFF_INSERT;
509 | break;
510 | } else {
511 | x--;
512 | y--;
513 | //if (text1.charAt(x) != text2.charAt(y)) {
514 | // throw new Error('No diagonal. Can\'t happen. (diff_path1)');
515 | //}
516 | if (last_op === DIFF_EQUAL) {
517 | path[0][1] = text1.charAt(x) + path[0][1];
518 | } else {
519 | path.unshift([DIFF_EQUAL, text1.charAt(x)]);
520 | }
521 | last_op = DIFF_EQUAL;
522 | }
523 | }
524 | }
525 | return path;
526 | };
527 |
528 |
529 | /**
530 | * Work from the middle back to the end to determine the path.
531 | * @param {Array.} v_map Array of paths.
532 | * @param {string} text1 Old string fragment to be diffed
533 | * @param {string} text2 New string fragment to be diffed
534 | * @return {Array.>} Array of diff tuples
535 | * @private
536 | */
537 | diff_match_patch.prototype.diff_path2 = function(v_map, text1, text2) {
538 | var path = [];
539 | var pathLength = 0;
540 | var x = text1.length;
541 | var y = text2.length;
542 | /** @type {number?} */
543 | var last_op = null;
544 | for (var d = v_map.length - 2; d >= 0; d--) {
545 | while (1) {
546 | if (v_map[d].hasOwnProperty ? v_map[d].hasOwnProperty((x - 1) + ',' + y) :
547 | (v_map[d][(x - 1) + ',' + y] !== undefined)) {
548 | x--;
549 | if (last_op === DIFF_DELETE) {
550 | path[pathLength - 1][1] += text1.charAt(text1.length - x - 1);
551 | } else {
552 | path[pathLength++] =
553 | [DIFF_DELETE, text1.charAt(text1.length - x - 1)];
554 | }
555 | last_op = DIFF_DELETE;
556 | break;
557 | } else if (v_map[d].hasOwnProperty ?
558 | v_map[d].hasOwnProperty(x + ',' + (y - 1)) :
559 | (v_map[d][x + ',' + (y - 1)] !== undefined)) {
560 | y--;
561 | if (last_op === DIFF_INSERT) {
562 | path[pathLength - 1][1] += text2.charAt(text2.length - y - 1);
563 | } else {
564 | path[pathLength++] =
565 | [DIFF_INSERT, text2.charAt(text2.length - y - 1)];
566 | }
567 | last_op = DIFF_INSERT;
568 | break;
569 | } else {
570 | x--;
571 | y--;
572 | //if (text1.charAt(text1.length - x - 1) !=
573 | // text2.charAt(text2.length - y - 1)) {
574 | // throw new Error('No diagonal. Can\'t happen. (diff_path2)');
575 | //}
576 | if (last_op === DIFF_EQUAL) {
577 | path[pathLength - 1][1] += text1.charAt(text1.length - x - 1);
578 | } else {
579 | path[pathLength++] =
580 | [DIFF_EQUAL, text1.charAt(text1.length - x - 1)];
581 | }
582 | last_op = DIFF_EQUAL;
583 | }
584 | }
585 | }
586 | return path;
587 | };
588 |
589 |
590 | /**
591 | * Determine the common prefix of two strings
592 | * @param {string} text1 First string
593 | * @param {string} text2 Second string
594 | * @return {number} The number of characters common to the start of each
595 | * string.
596 | */
597 | diff_match_patch.prototype.diff_commonPrefix = function(text1, text2) {
598 | // Quick check for common null cases.
599 | if (!text1 || !text2 || text1.charCodeAt(0) !== text2.charCodeAt(0)) {
600 | return 0;
601 | }
602 | // Binary search.
603 | // Performance analysis: http://neil.fraser.name/news/2007/10/09/
604 | var pointermin = 0;
605 | var pointermax = Math.min(text1.length, text2.length);
606 | var pointermid = pointermax;
607 | var pointerstart = 0;
608 | while (pointermin < pointermid) {
609 | if (text1.substring(pointerstart, pointermid) ==
610 | text2.substring(pointerstart, pointermid)) {
611 | pointermin = pointermid;
612 | pointerstart = pointermin;
613 | } else {
614 | pointermax = pointermid;
615 | }
616 | pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin);
617 | }
618 | return pointermid;
619 | };
620 |
621 |
622 | /**
623 | * Determine the common suffix of two strings
624 | * @param {string} text1 First string
625 | * @param {string} text2 Second string
626 | * @return {number} The number of characters common to the end of each string.
627 | */
628 | diff_match_patch.prototype.diff_commonSuffix = function(text1, text2) {
629 | // Quick check for common null cases.
630 | if (!text1 || !text2 || text1.charCodeAt(text1.length - 1) !==
631 | text2.charCodeAt(text2.length - 1)) {
632 | return 0;
633 | }
634 | // Binary search.
635 | // Performance analysis: http://neil.fraser.name/news/2007/10/09/
636 | var pointermin = 0;
637 | var pointermax = Math.min(text1.length, text2.length);
638 | var pointermid = pointermax;
639 | var pointerend = 0;
640 | while (pointermin < pointermid) {
641 | if (text1.substring(text1.length - pointermid, text1.length - pointerend) ==
642 | text2.substring(text2.length - pointermid, text2.length - pointerend)) {
643 | pointermin = pointermid;
644 | pointerend = pointermin;
645 | } else {
646 | pointermax = pointermid;
647 | }
648 | pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin);
649 | }
650 | return pointermid;
651 | };
652 |
653 |
654 | /**
655 | * Do the two texts share a substring which is at least half the length of the
656 | * longer text?
657 | * @param {string} text1 First string
658 | * @param {string} text2 Second string
659 | * @return {Array.?} Five element Array, containing the prefix of
660 | * text1, the suffix of text1, the prefix of text2, the suffix of
661 | * text2 and the common middle. Or null if there was no match.
662 | */
663 | diff_match_patch.prototype.diff_halfMatch = function(text1, text2) {
664 | var longtext = text1.length > text2.length ? text1 : text2;
665 | var shorttext = text1.length > text2.length ? text2 : text1;
666 | if (longtext.length < 10 || shorttext.length < 1) {
667 | return null; // Pointless.
668 | }
669 | var dmp = this; // 'this' becomes 'window' in a closure.
670 |
671 | /**
672 | * Does a substring of shorttext exist within longtext such that the substring
673 | * is at least half the length of longtext?
674 | * Closure, but does not reference any external variables.
675 | * @param {string} longtext Longer string
676 | * @param {string} shorttext Shorter string
677 | * @param {number} i Start index of quarter length substring within longtext
678 | * @return {Array.?} Five element Array, containing the prefix of
679 | * longtext, the suffix of longtext, the prefix of shorttext, the suffix
680 | * of shorttext and the common middle. Or null if there was no match.
681 | * @private
682 | */
683 | function diff_halfMatchI(longtext, shorttext, i) {
684 | // Start with a 1/4 length substring at position i as a seed.
685 | var seed = longtext.substring(i, i + Math.floor(longtext.length / 4));
686 | var j = -1;
687 | var best_common = '';
688 | var best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b;
689 | while ((j = shorttext.indexOf(seed, j + 1)) != -1) {
690 | var prefixLength = dmp.diff_commonPrefix(longtext.substring(i),
691 | shorttext.substring(j));
692 | var suffixLength = dmp.diff_commonSuffix(longtext.substring(0, i),
693 | shorttext.substring(0, j));
694 | if (best_common.length < suffixLength + prefixLength) {
695 | best_common = shorttext.substring(j - suffixLength, j) +
696 | shorttext.substring(j, j + prefixLength);
697 | best_longtext_a = longtext.substring(0, i - suffixLength);
698 | best_longtext_b = longtext.substring(i + prefixLength);
699 | best_shorttext_a = shorttext.substring(0, j - suffixLength);
700 | best_shorttext_b = shorttext.substring(j + prefixLength);
701 | }
702 | }
703 | if (best_common.length >= longtext.length / 2) {
704 | return [best_longtext_a, best_longtext_b,
705 | best_shorttext_a, best_shorttext_b, best_common];
706 | } else {
707 | return null;
708 | }
709 | }
710 |
711 | // First check if the second quarter is the seed for a half-match.
712 | var hm1 = diff_halfMatchI(longtext, shorttext,
713 | Math.ceil(longtext.length / 4));
714 | // Check again based on the third quarter.
715 | var hm2 = diff_halfMatchI(longtext, shorttext,
716 | Math.ceil(longtext.length / 2));
717 | var hm;
718 | if (!hm1 && !hm2) {
719 | return null;
720 | } else if (!hm2) {
721 | hm = hm1;
722 | } else if (!hm1) {
723 | hm = hm2;
724 | } else {
725 | // Both matched. Select the longest.
726 | hm = hm1[4].length > hm2[4].length ? hm1 : hm2;
727 | }
728 |
729 | // A half-match was found, sort out the return data.
730 | var text1_a, text1_b, text2_a, text2_b;
731 | if (text1.length > text2.length) {
732 | text1_a = hm[0];
733 | text1_b = hm[1];
734 | text2_a = hm[2];
735 | text2_b = hm[3];
736 | } else {
737 | text2_a = hm[0];
738 | text2_b = hm[1];
739 | text1_a = hm[2];
740 | text1_b = hm[3];
741 | }
742 | var mid_common = hm[4];
743 | return [text1_a, text1_b, text2_a, text2_b, mid_common];
744 | };
745 |
746 |
747 | /**
748 | * Reduce the number of edits by eliminating semantically trivial equalities.
749 | * @param {Array.>} diffs Array of diff tuples
750 | */
751 | diff_match_patch.prototype.diff_cleanupSemantic = function(diffs) {
752 | var changes = false;
753 | var equalities = []; // Stack of indices where equalities are found.
754 | var equalitiesLength = 0; // Keeping our own length var is faster in JS.
755 | var lastequality = null; // Always equal to equalities[equalitiesLength-1][1]
756 | var pointer = 0; // Index of current position.
757 | // Number of characters that changed prior to the equality.
758 | var length_changes1 = 0;
759 | // Number of characters that changed after the equality.
760 | var length_changes2 = 0;
761 | while (pointer < diffs.length) {
762 | if (diffs[pointer][0] == DIFF_EQUAL) { // equality found
763 | equalities[equalitiesLength++] = pointer;
764 | length_changes1 = length_changes2;
765 | length_changes2 = 0;
766 | lastequality = diffs[pointer][1];
767 | } else { // an insertion or deletion
768 | length_changes2 += diffs[pointer][1].length;
769 | if (lastequality !== null && (lastequality.length <= length_changes1) &&
770 | (lastequality.length <= length_changes2)) {
771 | // Duplicate record
772 | diffs.splice(equalities[equalitiesLength - 1], 0,
773 | [DIFF_DELETE, lastequality]);
774 | // Change second copy to insert.
775 | diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT;
776 | // Throw away the equality we just deleted.
777 | equalitiesLength--;
778 | // Throw away the previous equality (it needs to be reevaluated).
779 | equalitiesLength--;
780 | pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1;
781 | length_changes1 = 0; // Reset the counters.
782 | length_changes2 = 0;
783 | lastequality = null;
784 | changes = true;
785 | }
786 | }
787 | pointer++;
788 | }
789 | if (changes) {
790 | this.diff_cleanupMerge(diffs);
791 | }
792 | this.diff_cleanupSemanticLossless(diffs);
793 | };
794 |
795 |
796 | /**
797 | * Look for single edits surrounded on both sides by equalities
798 | * which can be shifted sideways to align the edit to a word boundary.
799 | * e.g: The cat c ame. -> The cat came.
800 | * @param {Array.>} diffs Array of diff tuples
801 | */
802 | diff_match_patch.prototype.diff_cleanupSemanticLossless = function(diffs) {
803 | // Define some regex patterns for matching boundaries.
804 | var punctuation = /[^a-zA-Z0-9]/;
805 | var whitespace = /\s/;
806 | var linebreak = /[\r\n]/;
807 | var blanklineEnd = /\n\r?\n$/;
808 | var blanklineStart = /^\r?\n\r?\n/;
809 |
810 | /**
811 | * Given two strings, compute a score representing whether the internal
812 | * boundary falls on logical boundaries.
813 | * Scores range from 5 (best) to 0 (worst).
814 | * Closure, makes reference to regex patterns defined above.
815 | * @param {string} one First string
816 | * @param {string} two Second string
817 | * @return {number} The score.
818 | */
819 | function diff_cleanupSemanticScore(one, two) {
820 | if (!one || !two) {
821 | // Edges are the best.
822 | return 5;
823 | }
824 |
825 | // Each port of this function behaves slightly differently due to
826 | // subtle differences in each language's definition of things like
827 | // 'whitespace'. Since this function's purpose is largely cosmetic,
828 | // the choice has been made to use each language's native features
829 | // rather than force total conformity.
830 | var score = 0;
831 | // One point for non-alphanumeric.
832 | if (one.charAt(one.length - 1).match(punctuation) ||
833 | two.charAt(0).match(punctuation)) {
834 | score++;
835 | // Two points for whitespace.
836 | if (one.charAt(one.length - 1).match(whitespace) ||
837 | two.charAt(0).match(whitespace)) {
838 | score++;
839 | // Three points for line breaks.
840 | if (one.charAt(one.length - 1).match(linebreak) ||
841 | two.charAt(0).match(linebreak)) {
842 | score++;
843 | // Four points for blank lines.
844 | if (one.match(blanklineEnd) || two.match(blanklineStart)) {
845 | score++;
846 | }
847 | }
848 | }
849 | }
850 | return score;
851 | }
852 |
853 | var pointer = 1;
854 | // Intentionally ignore the first and last element (don't need checking).
855 | while (pointer < diffs.length - 1) {
856 | if (diffs[pointer - 1][0] == DIFF_EQUAL &&
857 | diffs[pointer + 1][0] == DIFF_EQUAL) {
858 | // This is a single edit surrounded by equalities.
859 | var equality1 = diffs[pointer - 1][1];
860 | var edit = diffs[pointer][1];
861 | var equality2 = diffs[pointer + 1][1];
862 |
863 | // First, shift the edit as far left as possible.
864 | var commonOffset = this.diff_commonSuffix(equality1, edit);
865 | if (commonOffset) {
866 | var commonString = edit.substring(edit.length - commonOffset);
867 | equality1 = equality1.substring(0, equality1.length - commonOffset);
868 | edit = commonString + edit.substring(0, edit.length - commonOffset);
869 | equality2 = commonString + equality2;
870 | }
871 |
872 | // Second, step character by character right, looking for the best fit.
873 | var bestEquality1 = equality1;
874 | var bestEdit = edit;
875 | var bestEquality2 = equality2;
876 | var bestScore = diff_cleanupSemanticScore(equality1, edit) +
877 | diff_cleanupSemanticScore(edit, equality2);
878 | while (edit.charAt(0) === equality2.charAt(0)) {
879 | equality1 += edit.charAt(0);
880 | edit = edit.substring(1) + equality2.charAt(0);
881 | equality2 = equality2.substring(1);
882 | var score = diff_cleanupSemanticScore(equality1, edit) +
883 | diff_cleanupSemanticScore(edit, equality2);
884 | // The >= encourages trailing rather than leading whitespace on edits.
885 | if (score >= bestScore) {
886 | bestScore = score;
887 | bestEquality1 = equality1;
888 | bestEdit = edit;
889 | bestEquality2 = equality2;
890 | }
891 | }
892 |
893 | if (diffs[pointer - 1][1] != bestEquality1) {
894 | // We have an improvement, save it back to the diff.
895 | if (bestEquality1) {
896 | diffs[pointer - 1][1] = bestEquality1;
897 | } else {
898 | diffs.splice(pointer - 1, 1);
899 | pointer--;
900 | }
901 | diffs[pointer][1] = bestEdit;
902 | if (bestEquality2) {
903 | diffs[pointer + 1][1] = bestEquality2;
904 | } else {
905 | diffs.splice(pointer + 1, 1);
906 | pointer--;
907 | }
908 | }
909 | }
910 | pointer++;
911 | }
912 | };
913 |
914 |
915 | /**
916 | * Reduce the number of edits by eliminating operationally trivial equalities.
917 | * @param {Array.>} diffs Array of diff tuples
918 | */
919 | diff_match_patch.prototype.diff_cleanupEfficiency = function(diffs) {
920 | var changes = false;
921 | var equalities = []; // Stack of indices where equalities are found.
922 | var equalitiesLength = 0; // Keeping our own length var is faster in JS.
923 | var lastequality = ''; // Always equal to equalities[equalitiesLength-1][1]
924 | var pointer = 0; // Index of current position.
925 | // Is there an insertion operation before the last equality.
926 | var pre_ins = false;
927 | // Is there a deletion operation before the last equality.
928 | var pre_del = false;
929 | // Is there an insertion operation after the last equality.
930 | var post_ins = false;
931 | // Is there a deletion operation after the last equality.
932 | var post_del = false;
933 | while (pointer < diffs.length) {
934 | if (diffs[pointer][0] == DIFF_EQUAL) { // equality found
935 | if (diffs[pointer][1].length < this.Diff_EditCost &&
936 | (post_ins || post_del)) {
937 | // Candidate found.
938 | equalities[equalitiesLength++] = pointer;
939 | pre_ins = post_ins;
940 | pre_del = post_del;
941 | lastequality = diffs[pointer][1];
942 | } else {
943 | // Not a candidate, and can never become one.
944 | equalitiesLength = 0;
945 | lastequality = '';
946 | }
947 | post_ins = post_del = false;
948 | } else { // an insertion or deletion
949 | if (diffs[pointer][0] == DIFF_DELETE) {
950 | post_del = true;
951 | } else {
952 | post_ins = true;
953 | }
954 | /*
955 | * Five types to be split:
956 | * A BXYC D
957 | * A XC D
958 | * A BXC
959 | * AXC D
960 | * A BXC
961 | */
962 | if (lastequality && ((pre_ins && pre_del && post_ins && post_del) ||
963 | ((lastequality.length < this.Diff_EditCost / 2) &&
964 | (pre_ins + pre_del + post_ins + post_del) == 3))) {
965 | // Duplicate record
966 | diffs.splice(equalities[equalitiesLength - 1], 0,
967 | [DIFF_DELETE, lastequality]);
968 | // Change second copy to insert.
969 | diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT;
970 | equalitiesLength--; // Throw away the equality we just deleted;
971 | lastequality = '';
972 | if (pre_ins && pre_del) {
973 | // No changes made which could affect previous entry, keep going.
974 | post_ins = post_del = true;
975 | equalitiesLength = 0;
976 | } else {
977 | equalitiesLength--; // Throw away the previous equality;
978 | pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1;
979 | post_ins = post_del = false;
980 | }
981 | changes = true;
982 | }
983 | }
984 | pointer++;
985 | }
986 |
987 | if (changes) {
988 | this.diff_cleanupMerge(diffs);
989 | }
990 | };
991 |
992 |
993 | /**
994 | * Reorder and merge like edit sections. Merge equalities.
995 | * Any edit section can move as long as it doesn't cross an equality.
996 | * @param {Array.>} diffs Array of diff tuples
997 | */
998 | diff_match_patch.prototype.diff_cleanupMerge = function(diffs) {
999 | diffs.push([DIFF_EQUAL, '']); // Add a dummy entry at the end.
1000 | var pointer = 0;
1001 | var count_delete = 0;
1002 | var count_insert = 0;
1003 | var text_delete = '';
1004 | var text_insert = '';
1005 | var commonlength;
1006 | while (pointer < diffs.length) {
1007 | switch (diffs[pointer][0]) {
1008 | case DIFF_INSERT:
1009 | count_insert++;
1010 | text_insert += diffs[pointer][1];
1011 | pointer++;
1012 | break;
1013 | case DIFF_DELETE:
1014 | count_delete++;
1015 | text_delete += diffs[pointer][1];
1016 | pointer++;
1017 | break;
1018 | case DIFF_EQUAL:
1019 | // Upon reaching an equality, check for prior redundancies.
1020 | if (count_delete !== 0 || count_insert !== 0) {
1021 | if (count_delete !== 0 && count_insert !== 0) {
1022 | // Factor out any common prefixies.
1023 | commonlength = this.diff_commonPrefix(text_insert, text_delete);
1024 | if (commonlength !== 0) {
1025 | if ((pointer - count_delete - count_insert) > 0 &&
1026 | diffs[pointer - count_delete - count_insert - 1][0] ==
1027 | DIFF_EQUAL) {
1028 | diffs[pointer - count_delete - count_insert - 1][1] +=
1029 | text_insert.substring(0, commonlength);
1030 | } else {
1031 | diffs.splice(0, 0, [DIFF_EQUAL,
1032 | text_insert.substring(0, commonlength)]);
1033 | pointer++;
1034 | }
1035 | text_insert = text_insert.substring(commonlength);
1036 | text_delete = text_delete.substring(commonlength);
1037 | }
1038 | // Factor out any common suffixies.
1039 | commonlength = this.diff_commonSuffix(text_insert, text_delete);
1040 | if (commonlength !== 0) {
1041 | diffs[pointer][1] = text_insert.substring(text_insert.length -
1042 | commonlength) + diffs[pointer][1];
1043 | text_insert = text_insert.substring(0, text_insert.length -
1044 | commonlength);
1045 | text_delete = text_delete.substring(0, text_delete.length -
1046 | commonlength);
1047 | }
1048 | }
1049 | // Delete the offending records and add the merged ones.
1050 | if (count_delete === 0) {
1051 | diffs.splice(pointer - count_delete - count_insert,
1052 | count_delete + count_insert, [DIFF_INSERT, text_insert]);
1053 | } else if (count_insert === 0) {
1054 | diffs.splice(pointer - count_delete - count_insert,
1055 | count_delete + count_insert, [DIFF_DELETE, text_delete]);
1056 | } else {
1057 | diffs.splice(pointer - count_delete - count_insert,
1058 | count_delete + count_insert, [DIFF_DELETE, text_delete],
1059 | [DIFF_INSERT, text_insert]);
1060 | }
1061 | pointer = pointer - count_delete - count_insert +
1062 | (count_delete ? 1 : 0) + (count_insert ? 1 : 0) + 1;
1063 | } else if (pointer !== 0 && diffs[pointer - 1][0] == DIFF_EQUAL) {
1064 | // Merge this equality with the previous one.
1065 | diffs[pointer - 1][1] += diffs[pointer][1];
1066 | diffs.splice(pointer, 1);
1067 | } else {
1068 | pointer++;
1069 | }
1070 | count_insert = 0;
1071 | count_delete = 0;
1072 | text_delete = '';
1073 | text_insert = '';
1074 | break;
1075 | }
1076 | }
1077 | if (diffs[diffs.length - 1][1] === '') {
1078 | diffs.pop(); // Remove the dummy entry at the end.
1079 | }
1080 |
1081 | // Second pass: look for single edits surrounded on both sides by equalities
1082 | // which can be shifted sideways to eliminate an equality.
1083 | // e.g: ABA C -> AB AC
1084 | var changes = false;
1085 | pointer = 1;
1086 | // Intentionally ignore the first and last element (don't need checking).
1087 | while (pointer < diffs.length - 1) {
1088 | if (diffs[pointer - 1][0] == DIFF_EQUAL &&
1089 | diffs[pointer + 1][0] == DIFF_EQUAL) {
1090 | // This is a single edit surrounded by equalities.
1091 | if (diffs[pointer][1].substring(diffs[pointer][1].length -
1092 | diffs[pointer - 1][1].length) == diffs[pointer - 1][1]) {
1093 | // Shift the edit over the previous equality.
1094 | diffs[pointer][1] = diffs[pointer - 1][1] +
1095 | diffs[pointer][1].substring(0, diffs[pointer][1].length -
1096 | diffs[pointer - 1][1].length);
1097 | diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1];
1098 | diffs.splice(pointer - 1, 1);
1099 | changes = true;
1100 | } else if (diffs[pointer][1].substring(0, diffs[pointer + 1][1].length)
1101 | == diffs[pointer + 1][1]) {
1102 | // Shift the edit over the next equality.
1103 | diffs[pointer - 1][1] += diffs[pointer + 1][1];
1104 | diffs[pointer][1] =
1105 | diffs[pointer][1].substring(diffs[pointer + 1][1].length) +
1106 | diffs[pointer + 1][1];
1107 | diffs.splice(pointer + 1, 1);
1108 | changes = true;
1109 | }
1110 | }
1111 | pointer++;
1112 | }
1113 | // If shifts were made, the diff needs reordering and another shift sweep.
1114 | if (changes) {
1115 | this.diff_cleanupMerge(diffs);
1116 | }
1117 | };
1118 |
1119 |
1120 | /**
1121 | * loc is a location in text1, compute and return the equivalent location in
1122 | * text2.
1123 | * e.g. 'The cat' vs 'The big cat', 1->1, 5->8
1124 | * @param {Array.>} diffs Array of diff tuples
1125 | * @param {number} loc Location within text1
1126 | * @return {number} Location within text2
1127 | */
1128 | diff_match_patch.prototype.diff_xIndex = function(diffs, loc) {
1129 | var chars1 = 0;
1130 | var chars2 = 0;
1131 | var last_chars1 = 0;
1132 | var last_chars2 = 0;
1133 | var x;
1134 | for (x = 0; x < diffs.length; x++) {
1135 | if (diffs[x][0] !== DIFF_INSERT) { // Equality or deletion.
1136 | chars1 += diffs[x][1].length;
1137 | }
1138 | if (diffs[x][0] !== DIFF_DELETE) { // Equality or insertion.
1139 | chars2 += diffs[x][1].length;
1140 | }
1141 | if (chars1 > loc) { // Overshot the location.
1142 | break;
1143 | }
1144 | last_chars1 = chars1;
1145 | last_chars2 = chars2;
1146 | }
1147 | // Was the location was deleted?
1148 | if (diffs.length != x && diffs[x][0] === DIFF_DELETE) {
1149 | return last_chars2;
1150 | }
1151 | // Add the remaining character length.
1152 | return last_chars2 + (loc - last_chars1);
1153 | };
1154 |
1155 |
1156 | /**
1157 | * Convert a diff array into a pretty HTML report.
1158 | * @param {Array.>} diffs Array of diff tuples
1159 | * @return {string} HTML representation
1160 | */
1161 | diff_match_patch.prototype.diff_prettyHtml = function(diffs) {
1162 | var html = [];
1163 | var i = 0;
1164 | for (var x = 0; x < diffs.length; x++) {
1165 | var op = diffs[x][0]; // Operation (insert, delete, equal)
1166 | var data = diffs[x][1]; // Text of change.
1167 | var text = data.replace(/&/g, '&').replace(//g, '>').replace(/\n/g, '¶ ');
1169 | switch (op) {
1170 | case DIFF_INSERT:
1171 | html[x] = '' +
1172 | text + ' ';
1173 | break;
1174 | case DIFF_DELETE:
1175 | html[x] = '' +
1176 | text + '';
1177 | break;
1178 | case DIFF_EQUAL:
1179 | html[x] = '' + text + ' ';
1180 | break;
1181 | }
1182 | if (op !== DIFF_DELETE) {
1183 | i += data.length;
1184 | }
1185 | }
1186 | return html.join('');
1187 | };
1188 |
1189 |
1190 | /**
1191 | * Compute and return the source text (all equalities and deletions).
1192 | * @param {Array.>} diffs Array of diff tuples
1193 | * @return {string} Source text
1194 | */
1195 | diff_match_patch.prototype.diff_text1 = function(diffs) {
1196 | var txt = [];
1197 | for (var x = 0; x < diffs.length; x++) {
1198 | if (diffs[x][0] !== DIFF_INSERT) {
1199 | txt[x] = diffs[x][1];
1200 | }
1201 | }
1202 | return txt.join('');
1203 | };
1204 |
1205 |
1206 | /**
1207 | * Compute and return the destination text (all equalities and insertions).
1208 | * @param {Array.>} diffs Array of diff tuples
1209 | * @return {string} Destination text
1210 | */
1211 | diff_match_patch.prototype.diff_text2 = function(diffs) {
1212 | var txt = [];
1213 | for (var x = 0; x < diffs.length; x++) {
1214 | if (diffs[x][0] !== DIFF_DELETE) {
1215 | txt[x] = diffs[x][1];
1216 | }
1217 | }
1218 | return txt.join('');
1219 | };
1220 |
1221 |
1222 | /**
1223 | * Crush the diff into an encoded string which describes the operations
1224 | * required to transform text1 into text2.
1225 | * E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'.
1226 | * Operations are tab-separated. Inserted text is escaped using %xx notation.
1227 | * @param {Array.>} diffs Array of diff tuples
1228 | * @return {string} Delta text
1229 | */
1230 | diff_match_patch.prototype.diff_toDelta = function(diffs) {
1231 | var txt = [];
1232 | for (var x = 0; x < diffs.length; x++) {
1233 | switch (diffs[x][0]) {
1234 | case DIFF_INSERT:
1235 | txt[x] = '+' + encodeURI(diffs[x][1]);
1236 | break;
1237 | case DIFF_DELETE:
1238 | txt[x] = '-' + diffs[x][1].length;
1239 | break;
1240 | case DIFF_EQUAL:
1241 | txt[x] = '=' + diffs[x][1].length;
1242 | break;
1243 | }
1244 | }
1245 | // Opera doesn't know how to encode char 0.
1246 | return txt.join('\t').replace(/\0/g, '%00').replace(/%20/g, ' ');
1247 | };
1248 |
1249 |
1250 | /**
1251 | * Given the original text1, and an encoded string which describes the
1252 | * operations required to transform text1 into text2, compute the full diff.
1253 | * @param {string} text1 Source string for the diff
1254 | * @param {string} delta Delta text
1255 | * @return {Array.>} Array of diff tuples
1256 | * @throws {Error} If invalid input
1257 | */
1258 | diff_match_patch.prototype.diff_fromDelta = function(text1, delta) {
1259 | var diffs = [];
1260 | var diffsLength = 0; // Keeping our own length var is faster in JS.
1261 | var pointer = 0; // Cursor in text1
1262 | // Opera doesn't know how to decode char 0.
1263 | delta = delta.replace(/%00/g, '\0');
1264 | var tokens = delta.split(/\t/g);
1265 | for (var x = 0; x < tokens.length; x++) {
1266 | // Each token begins with a one character parameter which specifies the
1267 | // operation of this token (delete, insert, equality).
1268 | var param = tokens[x].substring(1);
1269 | switch (tokens[x].charAt(0)) {
1270 | case '+':
1271 | try {
1272 | diffs[diffsLength++] = [DIFF_INSERT, decodeURI(param)];
1273 | } catch (ex) {
1274 | // Malformed URI sequence.
1275 | throw new Error('Illegal escape in diff_fromDelta: ' + param);
1276 | }
1277 | break;
1278 | case '-':
1279 | // Fall through.
1280 | case '=':
1281 | var n = parseInt(param, 10);
1282 | if (isNaN(n) || n < 0) {
1283 | throw new Error('Invalid number in diff_fromDelta: ' + param);
1284 | }
1285 | var text = text1.substring(pointer, pointer += n);
1286 | if (tokens[x].charAt(0) == '=') {
1287 | diffs[diffsLength++] = [DIFF_EQUAL, text];
1288 | } else {
1289 | diffs[diffsLength++] = [DIFF_DELETE, text];
1290 | }
1291 | break;
1292 | default:
1293 | // Blank tokens are ok (from a trailing \t).
1294 | // Anything else is an error.
1295 | if (tokens[x]) {
1296 | throw new Error('Invalid diff operation in diff_fromDelta: ' +
1297 | tokens[x]);
1298 | }
1299 | }
1300 | }
1301 | if (pointer != text1.length) {
1302 | throw new Error('Delta length (' + pointer +
1303 | ') does not equal source text length (' + text1.length + ').');
1304 | }
1305 | return diffs;
1306 | };
1307 |
1308 |
1309 | // MATCH FUNCTIONS
1310 |
1311 |
1312 | /**
1313 | * Locate the best instance of 'pattern' in 'text' near 'loc'.
1314 | * @param {string} text The text to search
1315 | * @param {string} pattern The pattern to search for
1316 | * @param {number} loc The location to search around
1317 | * @return {number?} Best match index or null
1318 | */
1319 | diff_match_patch.prototype.match_main = function(text, pattern, loc) {
1320 | loc = Math.max(0, Math.min(loc, text.length - pattern.length));
1321 | if (text == pattern) {
1322 | // Shortcut (potentially not guaranteed by the algorithm)
1323 | return 0;
1324 | } else if (text.length === 0) {
1325 | // Nothing to match.
1326 | return null;
1327 | } else if (text.substring(loc, loc + pattern.length) == pattern) {
1328 | // Perfect match at the perfect spot! (Includes case of null pattern)
1329 | return loc;
1330 | } else {
1331 | // Do a fuzzy compare.
1332 | return this.match_bitap(text, pattern, loc);
1333 | }
1334 | };
1335 |
1336 |
1337 | /**
1338 | * Locate the best instance of 'pattern' in 'text' near 'loc' using the
1339 | * Bitap algorithm.
1340 | * @param {string} text The text to search
1341 | * @param {string} pattern The pattern to search for
1342 | * @param {number} loc The location to search around
1343 | * @return {number?} Best match index or null
1344 | * @private
1345 | */
1346 | diff_match_patch.prototype.match_bitap = function(text, pattern, loc) {
1347 | if (pattern.length > this.Match_MaxBits) {
1348 | throw new Error('Pattern too long for this browser.');
1349 | }
1350 |
1351 | // Initialise the alphabet.
1352 | var s = this.match_alphabet(pattern);
1353 |
1354 | var score_text_length = text.length;
1355 | // Coerce the text length between reasonable maximums and minimums.
1356 | score_text_length = Math.max(score_text_length, this.Match_MinLength);
1357 | score_text_length = Math.min(score_text_length, this.Match_MaxLength);
1358 |
1359 | var dmp = this; // 'this' becomes 'window' in a closure.
1360 |
1361 | /**
1362 | * Compute and return the score for a match with e errors and x location.
1363 | * Accesses loc, score_text_length and pattern through being a closure.
1364 | * @param {number} e Number of errors in match
1365 | * @param {number} x Location of match
1366 | * @return {number} Overall score for match
1367 | * @private
1368 | */
1369 | function match_bitapScore(e, x) {
1370 | var d = Math.abs(loc - x);
1371 | return (e / pattern.length / dmp.Match_Balance) +
1372 | (d / score_text_length / (1.0 - dmp.Match_Balance));
1373 | }
1374 |
1375 | // Highest score beyond which we give up.
1376 | var score_threshold = this.Match_Threshold;
1377 | // Is there a nearby exact match? (speedup)
1378 | var best_loc = text.indexOf(pattern, loc);
1379 | if (best_loc != -1) {
1380 | score_threshold = Math.min(match_bitapScore(0, best_loc), score_threshold);
1381 | }
1382 | // What about in the other direction? (speedup)
1383 | best_loc = text.lastIndexOf(pattern, loc + pattern.length);
1384 | if (best_loc != -1) {
1385 | score_threshold = Math.min(match_bitapScore(0, best_loc), score_threshold);
1386 | }
1387 |
1388 | // Initialise the bit arrays.
1389 | var matchmask = 1 << (pattern.length - 1);
1390 | best_loc = null;
1391 |
1392 | var bin_min, bin_mid;
1393 | var bin_max = Math.max(loc + loc, text.length);
1394 | var last_rd;
1395 | for (var d = 0; d < pattern.length; d++) {
1396 | // Scan for the best match; each iteration allows for one more error.
1397 | var rd = Array(text.length);
1398 |
1399 | // Run a binary search to determine how far from 'loc' we can stray at this
1400 | // error level.
1401 | bin_min = loc;
1402 | bin_mid = bin_max;
1403 | while (bin_min < bin_mid) {
1404 | if (match_bitapScore(d, bin_mid) < score_threshold) {
1405 | bin_min = bin_mid;
1406 | } else {
1407 | bin_max = bin_mid;
1408 | }
1409 | bin_mid = Math.floor((bin_max - bin_min) / 2 + bin_min);
1410 | }
1411 | // Use the result from this iteration as the maximum for the next.
1412 | bin_max = bin_mid;
1413 | var start = Math.max(0, loc - (bin_mid - loc) - 1);
1414 | var finish = Math.min(text.length - 1, pattern.length + bin_mid);
1415 |
1416 | if (text.charAt(finish) == pattern.charAt(pattern.length - 1)) {
1417 | rd[finish] = (1 << (d + 1)) - 1;
1418 | } else {
1419 | rd[finish] = (1 << d) - 1;
1420 | }
1421 | for (var j = finish - 1; j >= start; j--) {
1422 | // The alphabet (s) is a sparse hash, so the following lines generate
1423 | // warnings.
1424 | if (d === 0) { // First pass: exact match.
1425 | rd[j] = ((rd[j + 1] << 1) | 1) & s[text.charAt(j)];
1426 | } else { // Subsequent passes: fuzzy match.
1427 | rd[j] = ((rd[j + 1] << 1) | 1) & s[text.charAt(j)] |
1428 | ((last_rd[j + 1] << 1) | 1) | ((last_rd[j] << 1) | 1) |
1429 | last_rd[j + 1];
1430 | }
1431 | if (rd[j] & matchmask) {
1432 | var score = match_bitapScore(d, j);
1433 | // This match will almost certainly be better than any existing match.
1434 | // But check anyway.
1435 | if (score <= score_threshold) {
1436 | // Told you so.
1437 | score_threshold = score;
1438 | best_loc = j;
1439 | if (j > loc) {
1440 | // When passing loc, don't exceed our current distance from loc.
1441 | start = Math.max(0, loc - (j - loc));
1442 | } else {
1443 | // Already passed loc, downhill from here on in.
1444 | break;
1445 | }
1446 | }
1447 | }
1448 | }
1449 | // No hope for a (better) match at greater error levels.
1450 | if (match_bitapScore(d + 1, loc) > score_threshold) {
1451 | break;
1452 | }
1453 | last_rd = rd;
1454 | }
1455 | return best_loc;
1456 | };
1457 |
1458 |
1459 | /**
1460 | * Initialise the alphabet for the Bitap algorithm.
1461 | * @param {string} pattern The text to encode
1462 | * @return {Object} Hash of character locations
1463 | * @private
1464 | */
1465 | diff_match_patch.prototype.match_alphabet = function(pattern) {
1466 | var s = {};
1467 | for (var i = 0; i < pattern.length; i++) {
1468 | s[pattern.charAt(i)] = 0;
1469 | }
1470 | for (var i = 0; i < pattern.length; i++) {
1471 | s[pattern.charAt(i)] |= 1 << (pattern.length - i - 1);
1472 | }
1473 | return s;
1474 | };
1475 |
1476 |
1477 | // PATCH FUNCTIONS
1478 |
1479 |
1480 | /**
1481 | * Increase the context until it is unique,
1482 | * but don't let the pattern expand beyond Match_MaxBits.
1483 | * @param {patch_obj} patch The patch to grow
1484 | * @param {string} text Source text
1485 | * @private
1486 | */
1487 | diff_match_patch.prototype.patch_addContext = function(patch, text) {
1488 | var pattern = text.substring(patch.start2, patch.start2 + patch.length1);
1489 | var padding = 0;
1490 | while (text.indexOf(pattern) != text.lastIndexOf(pattern) &&
1491 | pattern.length < this.Match_MaxBits - this.Patch_Margin
1492 | - this.Patch_Margin) {
1493 | padding += this.Patch_Margin;
1494 | pattern = text.substring(patch.start2 - padding,
1495 | patch.start2 + patch.length1 + padding);
1496 | }
1497 | // Add one chunk for good luck.
1498 | padding += this.Patch_Margin;
1499 | // Add the prefix.
1500 | var prefix = text.substring(patch.start2 - padding, patch.start2);
1501 | if (prefix !== '') {
1502 | patch.diffs.unshift([DIFF_EQUAL, prefix]);
1503 | }
1504 | // Add the suffix.
1505 | var suffix = text.substring(patch.start2 + patch.length1,
1506 | patch.start2 + patch.length1 + padding);
1507 | if (suffix !== '') {
1508 | patch.diffs.push([DIFF_EQUAL, suffix]);
1509 | }
1510 |
1511 | // Roll back the start points.
1512 | patch.start1 -= prefix.length;
1513 | patch.start2 -= prefix.length;
1514 | // Extend the lengths.
1515 | patch.length1 += prefix.length + suffix.length;
1516 | patch.length2 += prefix.length + suffix.length;
1517 | };
1518 |
1519 |
1520 | /**
1521 | * Compute a list of patches to turn text1 into text2.
1522 | * Use diffs if provided, otherwise compute it ourselves.
1523 | * There are two ways to call this function:
1524 | * Method 1:
1525 | * a = Old text, b = New text, c = array of diff tuplle for a to b
1526 | * Method 2:
1527 | * a = Array of diff tuples for text 1 to text 2, b and c undefined
1528 | * @param {string|Array.>} a Old text (method 1) or Array of diff
1529 | * tuples for text1 to text2 (method 2)
1530 | * @param {string?} b New text (method 1)
1531 | * @param {Array.>} c Optional array of diff tuples for text1 to text2
1532 | * (method 1)
1533 | * @return {Array.} Array of patch objects
1534 | */
1535 | diff_match_patch.prototype.patch_make = function(a, b, c) {
1536 | var text1, text2, diffs;
1537 | if (typeof b == 'undefined') {
1538 | diffs = a;
1539 | text1 = this.diff_text1(diffs);
1540 | text2 = ''; // text2 is not actually used.
1541 | } else {
1542 | text1 = a;
1543 | text2 = b;
1544 | if (typeof c != 'undefined') {
1545 | diffs = c;
1546 | } else {
1547 | diffs = this.diff_main(text1, text2, true);
1548 | if (diffs.length > 2) {
1549 | this.diff_cleanupSemantic(diffs);
1550 | this.diff_cleanupEfficiency(diffs);
1551 | }
1552 | }
1553 | }
1554 |
1555 | if (diffs.length === 0) {
1556 | return []; // Get rid of the null case.
1557 | }
1558 | var patches = [];
1559 | var patch = new patch_obj();
1560 | var patchDiffLength = 0; // Keeping our own length var is faster in JS.
1561 | var char_count1 = 0; // Number of characters into the text1 string.
1562 | var char_count2 = 0; // Number of characters into the text2 string.
1563 | var prepatch_text = text1; // Recreate the patches to determine context info.
1564 | var postpatch_text = text1;
1565 | for (var x = 0; x < diffs.length; x++) {
1566 | var diff_type = diffs[x][0];
1567 | var diff_text = diffs[x][1];
1568 |
1569 | if (!patchDiffLength && diff_type !== DIFF_EQUAL) {
1570 | // A new patch starts here.
1571 | patch.start1 = char_count1;
1572 | patch.start2 = char_count2;
1573 | }
1574 |
1575 | switch (diff_type) {
1576 | case DIFF_INSERT:
1577 | patch.diffs[patchDiffLength++] = diffs[x];
1578 | patch.length2 += diff_text.length;
1579 | postpatch_text = postpatch_text.substring(0, char_count2) + diff_text +
1580 | postpatch_text.substring(char_count2);
1581 | break;
1582 | case DIFF_DELETE:
1583 | patch.length1 += diff_text.length;
1584 | patch.diffs[patchDiffLength++] = diffs[x];
1585 | postpatch_text = postpatch_text.substring(0, char_count2) +
1586 | postpatch_text.substring(char_count2 + diff_text.length);
1587 | break;
1588 | case DIFF_EQUAL:
1589 | if (diff_text.length <= 2 * this.Patch_Margin &&
1590 | patchDiffLength && diffs.length != x + 1) {
1591 | // Small equality inside a patch.
1592 | patch.diffs[patchDiffLength++] = diffs[x];
1593 | patch.length1 += diff_text.length;
1594 | patch.length2 += diff_text.length;
1595 | } else if (diff_text.length >= 2 * this.Patch_Margin) {
1596 | // Time for a new patch.
1597 | if (patchDiffLength) {
1598 | this.patch_addContext(patch, prepatch_text);
1599 | patches.push(patch);
1600 | patch = new patch_obj();
1601 | patchDiffLength = 0;
1602 | prepatch_text = postpatch_text;
1603 | }
1604 | }
1605 | break;
1606 | }
1607 |
1608 | // Update the current character count.
1609 | if (diff_type !== DIFF_INSERT) {
1610 | char_count1 += diff_text.length;
1611 | }
1612 | if (diff_type !== DIFF_DELETE) {
1613 | char_count2 += diff_text.length;
1614 | }
1615 | }
1616 | // Pick up the leftover patch if not empty.
1617 | if (patchDiffLength) {
1618 | this.patch_addContext(patch, prepatch_text);
1619 | patches.push(patch);
1620 | }
1621 |
1622 | return patches;
1623 | };
1624 |
1625 |
1626 | /**
1627 | * Merge a set of patches onto the text. Return a patched text, as well
1628 | * as a list of true/false values indicating which patches were applied.
1629 | * @param {Array.} patches Array of patch objects
1630 | * @param {string} text Old text
1631 | * @return {Array.>} Two element Array, containing the
1632 | * new text and an array of boolean values
1633 | */
1634 | diff_match_patch.prototype.patch_apply = function(patches, text) {
1635 | if (patches.length == 0) {
1636 | return [text, []];
1637 | }
1638 |
1639 | // Deep copy the patches so that no changes are made to originals.
1640 | var patchesCopy = [];
1641 | for (var x = 0; x < patches.length; x++) {
1642 | var patch = patches[x];
1643 | var patchCopy = new patch_obj();
1644 | patchCopy.diffs = patch.diffs.slice();
1645 | patchCopy.start1 = patch.start1;
1646 | patchCopy.start2 = patch.start2;
1647 | patchCopy.length1 = patch.length1;
1648 | patchCopy.length2 = patch.length2;
1649 | patchesCopy[x] = patchCopy;
1650 | }
1651 | patches = patchesCopy;
1652 |
1653 | var nullPadding = this.patch_addPadding(patches);
1654 | text = nullPadding + text + nullPadding;
1655 |
1656 | this.patch_splitMax(patches);
1657 | // delta keeps track of the offset between the expected and actual location
1658 | // of the previous patch. If there are patches expected at positions 10 and
1659 | // 20, but the first patch was found at 12, delta is 2 and the second patch
1660 | // has an effective expected position of 22.
1661 | var delta = 0;
1662 | var results = [];
1663 | for (var x = 0; x < patches.length; x++) {
1664 | var expected_loc = patches[x].start2 + delta;
1665 | var text1 = this.diff_text1(patches[x].diffs);
1666 | var start_loc = this.match_main(text, text1, expected_loc);
1667 | if (start_loc === null) {
1668 | // No match found. :(
1669 | results[x] = false;
1670 | } else {
1671 | // Found a match. :)
1672 | results[x] = true;
1673 | delta = start_loc - expected_loc;
1674 | var text2 = text.substring(start_loc, start_loc + text1.length);
1675 | if (text1 == text2) {
1676 | // Perfect match, just shove the replacement text in.
1677 | text = text.substring(0, start_loc) +
1678 | this.diff_text2(patches[x].diffs) +
1679 | text.substring(start_loc + text1.length);
1680 | } else {
1681 | // Imperfect match. Run a diff to get a framework of equivalent
1682 | // indicies.
1683 | var diffs = this.diff_main(text1, text2, false);
1684 | this.diff_cleanupSemanticLossless(diffs);
1685 | var index1 = 0;
1686 | var index2;
1687 | for (var y = 0; y < patches[x].diffs.length; y++) {
1688 | var mod = patches[x].diffs[y];
1689 | if (mod[0] !== DIFF_EQUAL) {
1690 | index2 = this.diff_xIndex(diffs, index1);
1691 | }
1692 | if (mod[0] === DIFF_INSERT) { // Insertion
1693 | text = text.substring(0, start_loc + index2) + mod[1] +
1694 | text.substring(start_loc + index2);
1695 | } else if (mod[0] === DIFF_DELETE) { // Deletion
1696 | text = text.substring(0, start_loc + index2) +
1697 | text.substring(start_loc + this.diff_xIndex(diffs,
1698 | index1 + mod[1].length));
1699 | }
1700 | if (mod[0] !== DIFF_DELETE) {
1701 | index1 += mod[1].length;
1702 | }
1703 | }
1704 | }
1705 | }
1706 | }
1707 | // Strip the padding off.
1708 | text = text.substring(nullPadding.length, text.length - nullPadding.length);
1709 | return [text, results];
1710 | };
1711 |
1712 |
1713 | /**
1714 | * Add some padding on text start and end so that edges can match something.
1715 | * @param {Array.} patches Array of patch objects
1716 | * @return {string} The padding string added to each side.
1717 | * @private
1718 | */
1719 | diff_match_patch.prototype.patch_addPadding = function(patches) {
1720 | var nullPadding = '';
1721 | for (var x = 0; x < this.Patch_Margin; x++) {
1722 | nullPadding += String.fromCharCode(x);
1723 | }
1724 |
1725 | // Bump all the patches forward.
1726 | for (var x = 0; x < patches.length; x++) {
1727 | patches[x].start1 += nullPadding.length;
1728 | patches[x].start2 += nullPadding.length;
1729 | }
1730 |
1731 | // Add some padding on start of first diff.
1732 | var patch = patches[0];
1733 | var diffs = patch.diffs;
1734 | if (diffs.length == 0 || diffs[0][0] != DIFF_EQUAL) {
1735 | // Add nullPadding equality.
1736 | diffs.unshift([DIFF_EQUAL, nullPadding]);
1737 | patch.start1 -= nullPadding.length; // Should be 0.
1738 | patch.start2 -= nullPadding.length; // Should be 0.
1739 | patch.length1 += nullPadding.length;
1740 | patch.length2 += nullPadding.length;
1741 | } else if (nullPadding.length > diffs[0][1].length) {
1742 | // Grow first equality.
1743 | var extraLength = nullPadding.length - diffs[0][1].length;
1744 | diffs[0][1] = nullPadding.substring(diffs[0][1].length) + diffs[0][1];
1745 | patch.start1 -= extraLength;
1746 | patch.start2 -= extraLength;
1747 | patch.length1 += extraLength;
1748 | patch.length2 += extraLength;
1749 | }
1750 |
1751 | // Add some padding on end of last diff.
1752 | patch = patches[patches.length - 1];
1753 | diffs = patch.diffs;
1754 | if (diffs.length == 0 || diffs[diffs.length - 1][0] != DIFF_EQUAL) {
1755 | // Add nullPadding equality.
1756 | diffs.push([DIFF_EQUAL, nullPadding]);
1757 | patch.length1 += nullPadding.length;
1758 | patch.length2 += nullPadding.length;
1759 | } else if (nullPadding.length > diffs[diffs.length - 1][1].length) {
1760 | // Grow last equality.
1761 | var extraLength = nullPadding.length - diffs[diffs.length - 1][1].length;
1762 | diffs[diffs.length - 1][1] += nullPadding.substring(0, extraLength);
1763 | patch.length1 += extraLength;
1764 | patch.length2 += extraLength;
1765 | }
1766 |
1767 | return nullPadding;
1768 | };
1769 |
1770 |
1771 | /**
1772 | * Look through the patches and break up any which are longer than the maximum
1773 | * limit of the match algorithm.
1774 | * @param {Array.} patches Array of patch objects
1775 | */
1776 | diff_match_patch.prototype.patch_splitMax = function(patches) {
1777 | for (var x = 0; x < patches.length; x++) {
1778 | if (patches[x].length1 > this.Match_MaxBits) {
1779 | var bigpatch = patches[x];
1780 | // Remove the big old patch.
1781 | patches.splice(x, 1);
1782 | var patch_size = this.Match_MaxBits;
1783 | var start1 = bigpatch.start1;
1784 | var start2 = bigpatch.start2;
1785 | var precontext = '';
1786 | while (bigpatch.diffs.length !== 0) {
1787 | // Create one of several smaller patches.
1788 | var patch = new patch_obj();
1789 | var empty = true;
1790 | patch.start1 = start1 - precontext.length;
1791 | patch.start2 = start2 - precontext.length;
1792 | if (precontext !== '') {
1793 | patch.length1 = patch.length2 = precontext.length;
1794 | patch.diffs.push([DIFF_EQUAL, precontext]);
1795 | }
1796 | while (bigpatch.diffs.length !== 0 &&
1797 | patch.length1 < patch_size - this.Patch_Margin) {
1798 | var diff_type = bigpatch.diffs[0][0];
1799 | var diff_text = bigpatch.diffs[0][1];
1800 | if (diff_type === DIFF_INSERT) {
1801 | // Insertions are harmless.
1802 | patch.length2 += diff_text.length;
1803 | start2 += diff_text.length;
1804 | patch.diffs.push(bigpatch.diffs.shift());
1805 | empty = false;
1806 | } else {
1807 | // Deletion or equality. Only take as much as we can stomach.
1808 | diff_text = diff_text.substring(0, patch_size - patch.length1 -
1809 | this.Patch_Margin);
1810 | patch.length1 += diff_text.length;
1811 | start1 += diff_text.length;
1812 | if (diff_type === DIFF_EQUAL) {
1813 | patch.length2 += diff_text.length;
1814 | start2 += diff_text.length;
1815 | } else {
1816 | empty = false;
1817 | }
1818 | patch.diffs.push([diff_type, diff_text]);
1819 | if (diff_text == bigpatch.diffs[0][1]) {
1820 | bigpatch.diffs.shift();
1821 | } else {
1822 | bigpatch.diffs[0][1] =
1823 | bigpatch.diffs[0][1].substring(diff_text.length);
1824 | }
1825 | }
1826 | }
1827 | // Compute the head context for the next patch.
1828 | precontext = this.diff_text2(patch.diffs);
1829 | precontext =
1830 | precontext.substring(precontext.length - this.Patch_Margin);
1831 | // Append the end context for this patch.
1832 | var postcontext = this.diff_text1(bigpatch.diffs)
1833 | .substring(0, this.Patch_Margin);
1834 | if (postcontext !== '') {
1835 | patch.length1 += postcontext.length;
1836 | patch.length2 += postcontext.length;
1837 | if (patch.diffs.length !== 0 &&
1838 | patch.diffs[patch.diffs.length - 1][0] === DIFF_EQUAL) {
1839 | patch.diffs[patch.diffs.length - 1][1] += postcontext;
1840 | } else {
1841 | patch.diffs.push([DIFF_EQUAL, postcontext]);
1842 | }
1843 | }
1844 | if (!empty) {
1845 | patches.splice(x++, 0, patch);
1846 | }
1847 | }
1848 | }
1849 | }
1850 | };
1851 |
1852 |
1853 | /**
1854 | * Take a list of patches and return a textual representation.
1855 | * @param {Array.} patches Array of patch objects
1856 | * @return {string} Text representation of patches
1857 | */
1858 | diff_match_patch.prototype.patch_toText = function(patches) {
1859 | var text = [];
1860 | for (var x = 0; x < patches.length; x++) {
1861 | text[x] = patches[x];
1862 | }
1863 | return text.join('');
1864 | };
1865 |
1866 |
1867 | /**
1868 | * Parse a textual representation of patches and return a list of patch objects.
1869 | * @param {string} textline Text representation of patches
1870 | * @return {Array.} Array of patch objects
1871 | * @throws {Error} If invalid input
1872 | */
1873 | diff_match_patch.prototype.patch_fromText = function(textline) {
1874 | var patches = [];
1875 | if (!textline) {
1876 | return patches;
1877 | }
1878 | // Opera doesn't know how to decode char 0.
1879 | textline = textline.replace(/%00/g, '\0');
1880 | var text = textline.split('\n');
1881 | var textPointer = 0;
1882 | while (textPointer < text.length) {
1883 | var m = text[textPointer].match(/^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$/);
1884 | if (!m) {
1885 | throw new Error('Invalid patch string: ' + text[textPointer]);
1886 | }
1887 | var patch = new patch_obj();
1888 | patches.push(patch);
1889 | patch.start1 = parseInt(m[1], 10);
1890 | if (m[2] === '') {
1891 | patch.start1--;
1892 | patch.length1 = 1;
1893 | } else if (m[2] == '0') {
1894 | patch.length1 = 0;
1895 | } else {
1896 | patch.start1--;
1897 | patch.length1 = parseInt(m[2], 10);
1898 | }
1899 |
1900 | patch.start2 = parseInt(m[3], 10);
1901 | if (m[4] === '') {
1902 | patch.start2--;
1903 | patch.length2 = 1;
1904 | } else if (m[4] == '0') {
1905 | patch.length2 = 0;
1906 | } else {
1907 | patch.start2--;
1908 | patch.length2 = parseInt(m[4], 10);
1909 | }
1910 | textPointer++;
1911 |
1912 | while (textPointer < text.length) {
1913 | var sign = text[textPointer].charAt(0);
1914 | try {
1915 | var line = decodeURI(text[textPointer].substring(1));
1916 | } catch (ex) {
1917 | // Malformed URI sequence.
1918 | throw new Error('Illegal escape in patch_fromText: ' + line);
1919 | }
1920 | if (sign == '-') {
1921 | // Deletion.
1922 | patch.diffs.push([DIFF_DELETE, line]);
1923 | } else if (sign == '+') {
1924 | // Insertion.
1925 | patch.diffs.push([DIFF_INSERT, line]);
1926 | } else if (sign == ' ') {
1927 | // Minor equality.
1928 | patch.diffs.push([DIFF_EQUAL, line]);
1929 | } else if (sign == '@') {
1930 | // Start of next patch.
1931 | break;
1932 | } else if (sign === '') {
1933 | // Blank line? Whatever.
1934 | } else {
1935 | // WTF?
1936 | throw new Error('Invalid patch mode "' + sign + '" in: ' + line);
1937 | }
1938 | textPointer++;
1939 | }
1940 | }
1941 | return patches;
1942 | };
1943 |
1944 |
1945 | /**
1946 | * Class representing one patch operation.
1947 | * @constructor
1948 | */
1949 | function patch_obj() {
1950 | this.diffs = [];
1951 | /** @type {number?} */
1952 | this.start1 = null;
1953 | /** @type {number?} */
1954 | this.start2 = null;
1955 | this.length1 = 0;
1956 | this.length2 = 0;
1957 | }
1958 |
1959 |
1960 | /**
1961 | * Emmulate GNU diff's format.
1962 | * Header: @@ -382,8 +481,9 @@
1963 | * Indicies are printed as 1-based, not 0-based.
1964 | * @return {string} The GNU diff string
1965 | */
1966 | patch_obj.prototype.toString = function() {
1967 | var coords1, coords2;
1968 | if (this.length1 === 0) {
1969 | coords1 = this.start1 + ',0';
1970 | } else if (this.length1 == 1) {
1971 | coords1 = this.start1 + 1;
1972 | } else {
1973 | coords1 = (this.start1 + 1) + ',' + this.length1;
1974 | }
1975 | if (this.length2 === 0) {
1976 | coords2 = this.start2 + ',0';
1977 | } else if (this.length2 == 1) {
1978 | coords2 = this.start2 + 1;
1979 | } else {
1980 | coords2 = (this.start2 + 1) + ',' + this.length2;
1981 | }
1982 | var txt = ['@@ -' + coords1 + ' +' + coords2 + ' @@\n'];
1983 | var op;
1984 | // Escape the body of the patch with %xx notation.
1985 | for (var x = 0; x < this.diffs.length; x++) {
1986 | switch (this.diffs[x][0]) {
1987 | case DIFF_INSERT:
1988 | op = '+';
1989 | break;
1990 | case DIFF_DELETE:
1991 | op = '-';
1992 | break;
1993 | case DIFF_EQUAL:
1994 | op = ' ';
1995 | break;
1996 | }
1997 | txt[x + 1] = op + encodeURI(this.diffs[x][1]) + '\n';
1998 | }
1999 | // Opera doesn't know how to encode char 0.
2000 | return txt.join('').replace(/\0/g, '%00').replace(/%20/g, ' ');
2001 | };
2002 |
2003 |
--------------------------------------------------------------------------------