liquid_cms 0.2.0.11 → 0.2.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. data/CHANGELOG.rdoc +7 -0
  2. data/TODO.rdoc +1 -1
  3. data/app/controllers/cms/main_controller.rb +3 -2
  4. data/app/helpers/cms/common_helper.rb +9 -2
  5. data/app/helpers/cms/components_helper.rb +10 -4
  6. data/app/models/cms/component.rb +4 -0
  7. data/app/views/cms/assets/_list.html.erb +4 -4
  8. data/app/views/cms/components/_list.html.erb +5 -1
  9. data/app/views/cms/pages/_list.html.erb +4 -4
  10. data/app/views/cms/shared/_sidebar.html.erb +32 -8
  11. data/app/views/layouts/cms.html.erb +5 -2
  12. data/generators/liquid_cms/templates/config/locales/cms/en.yml +3 -2
  13. data/generators/liquid_cms/templates/public/cms/codemirror/LICENSE +0 -0
  14. data/generators/liquid_cms/templates/public/cms/codemirror/css/csscolors.css +0 -0
  15. data/generators/liquid_cms/templates/public/cms/codemirror/css/docs.css +17 -3
  16. data/generators/liquid_cms/templates/public/cms/codemirror/css/font.js +15 -0
  17. data/generators/liquid_cms/templates/public/cms/codemirror/css/jscolors.css +0 -0
  18. data/generators/liquid_cms/templates/public/cms/codemirror/css/sparqlcolors.css +0 -0
  19. data/generators/liquid_cms/templates/public/cms/codemirror/css/xmlcolors.css +0 -0
  20. data/generators/liquid_cms/templates/public/cms/codemirror/js/codemirror.js +59 -26
  21. data/generators/liquid_cms/templates/public/cms/codemirror/js/editor.js +149 -71
  22. data/generators/liquid_cms/templates/public/cms/codemirror/js/highlight.js +2 -2
  23. data/generators/liquid_cms/templates/public/cms/codemirror/js/mirrorframe.js +2 -2
  24. data/generators/liquid_cms/templates/public/cms/codemirror/js/parsecss.js +5 -3
  25. data/generators/liquid_cms/templates/public/cms/codemirror/js/parsedummy.js +0 -0
  26. data/generators/liquid_cms/templates/public/cms/codemirror/js/parsehtmlmixed.js +28 -9
  27. data/generators/liquid_cms/templates/public/cms/codemirror/js/parsejavascript.js +0 -0
  28. data/generators/liquid_cms/templates/public/cms/codemirror/js/parsesparql.js +0 -0
  29. data/generators/liquid_cms/templates/public/cms/codemirror/js/parsexml.js +6 -1
  30. data/generators/liquid_cms/templates/public/cms/codemirror/js/select.js +48 -21
  31. data/generators/liquid_cms/templates/public/cms/codemirror/js/stringstream.js +15 -1
  32. data/generators/liquid_cms/templates/public/cms/codemirror/js/tokenize.js +0 -0
  33. data/generators/liquid_cms/templates/public/cms/codemirror/js/tokenizejavascript.js +1 -1
  34. data/generators/liquid_cms/templates/public/cms/codemirror/js/undo.js +17 -14
  35. data/generators/liquid_cms/templates/public/cms/codemirror/js/unittests.js +44 -0
  36. data/generators/liquid_cms/templates/public/cms/codemirror/js/util.js +6 -3
  37. data/generators/liquid_cms/templates/public/cms/javascripts/cms.js +15 -1
  38. data/generators/liquid_cms/templates/public/cms/javascripts/livepipe.js +181 -0
  39. data/generators/liquid_cms/templates/public/cms/javascripts/tabs.js +149 -0
  40. data/generators/liquid_cms/templates/public/cms/stylesheets/ie9.css +4 -0
  41. data/generators/liquid_cms/templates/public/cms/stylesheets/sidebar.css +132 -0
  42. data/generators/liquid_cms/templates/public/cms/stylesheets/styles.css +1 -74
  43. data/generators/liquid_cms/templates/public/cms/stylesheets/themes/dark.css +2 -1
  44. data/lib/liquid_cms/context.rb +4 -0
  45. data/lib/liquid_cms/version.rb +1 -1
  46. data/liquid_cms.gemspec +1 -1
  47. data/test/functional/assets_controller_test.rb +3 -3
  48. data/test/rails_app/config/locales/cms/en.yml +8 -0
  49. metadata +11 -16
  50. data/generators/liquid_cms/templates/public/cms/codemirror/bigtest.html +0 -1296
  51. data/generators/liquid_cms/templates/public/cms/codemirror/css/people.jpg +0 -0
  52. data/generators/liquid_cms/templates/public/cms/codemirror/csstest.html +0 -60
  53. data/generators/liquid_cms/templates/public/cms/codemirror/highlight.html +0 -82
  54. data/generators/liquid_cms/templates/public/cms/codemirror/htmltest.html +0 -52
  55. data/generators/liquid_cms/templates/public/cms/codemirror/index.html +0 -245
  56. data/generators/liquid_cms/templates/public/cms/codemirror/jstest.html +0 -56
  57. data/generators/liquid_cms/templates/public/cms/codemirror/manual.html +0 -759
  58. data/generators/liquid_cms/templates/public/cms/codemirror/mixedtest.html +0 -52
  59. data/generators/liquid_cms/templates/public/cms/codemirror/sparqltest.html +0 -41
  60. data/generators/liquid_cms/templates/public/cms/codemirror/story.html +0 -671
@@ -83,64 +83,6 @@ a img {
83
83
  font-style: italic;
84
84
  }
85
85
 
86
- #sidebar {
87
- background-color: #CFCFCF;
88
- float: left;
89
- padding: 0 1em;
90
- margin-left: 1em;
91
- width: 18em;
92
- }
93
- #sidebar h2 {
94
- font-size: 12pt;
95
- padding: 0.2em;
96
- margin-top: 0.5em;
97
- text-align: center;
98
- }
99
- #sidebar #pages {
100
- margin-bottom: 1.5em;
101
- }
102
- #sidebar ul {
103
- list-style-type: none;
104
- margin: 0;
105
- padding: 0;
106
- }
107
- #sidebar #assets li {
108
- border: 1px solid #AAA;
109
- padding: 0.3em 0.4em;
110
- margin-bottom: 0.3em;
111
- }
112
- #sidebar #assets li:hover {
113
- background-color: #BFD6FF !important;
114
- }
115
- #sidebar #assets li.light {
116
- background-color: #DDD;
117
- }
118
- #sidebar .preview {
119
- margin-bottom: 0.5em;
120
- }
121
- #sidebar .preview a {
122
- font-size: 8pt;
123
- }
124
- #sidebar .asset_details {
125
- margin-top: 0.5em;
126
- }
127
- #sidebar .asset_image {
128
- margin: 0 1.5em 0;
129
- }
130
- #sidebar .asset_image {
131
- float: left;
132
- }
133
- #sidebar .asset_size {
134
- font-size: 8pt;
135
- float: right;
136
- }
137
- #sidebar #components form {
138
- margin-bottom: 0.5em;
139
- }
140
- #sidebar #components ul ul {
141
- margin-left: 0.5em;
142
- }
143
-
144
86
  #content p .notice {
145
87
  background-color: yellow;
146
88
  font-size: 12pt;
@@ -152,21 +94,6 @@ h2 p.message {
152
94
  margin: 0;
153
95
  }
154
96
 
155
- #cms ul.tree { list-style-type: none; background: url(/cms/images/tree/vline.png) repeat-y; margin: 0; padding: 0; }
156
- #cms ul.tree ul { margin-left: 10px; }
157
- #cms ul.tree li { margin: 0; padding: 0 12px; line-height: 20px; background: url(/cms/images/tree/node.png) no-repeat; color: #369; font-weight: bold; }
158
- #cms ul.tree li:last-child { background: url(/cms/images/tree/lastnode.png) no-repeat; }
159
-
160
- #cms ul.tree, #sidebar #assets ul {
161
- margin-bottom: 1em;
162
- }
163
- #cms ul.tree ul {
164
- margin-left: 15px;
165
- }
166
- #cms ul.tree li img.folder:hover {
167
- cursor: pointer;
168
- }
169
-
170
97
  .left {
171
98
  float: left;
172
99
  }
@@ -186,5 +113,5 @@ h2 p.message {
186
113
  background: url("/cms/images/icons/page.png") no-repeat scroll right 50% transparent;
187
114
  }
188
115
  .cms_assets #content h2 {
189
- background: url("/cms/images/icons/photo.png") no-repeat scroll right 50% transparent;
116
+ background: url("/cms/images/icons/picture.png") no-repeat scroll right 50% transparent;
190
117
  }
@@ -2,12 +2,13 @@ body {
2
2
  background: url("/cms/images/trentacinque.gif") repeat scroll 0 0 transparent;
3
3
  }
4
4
  #header, #sidebar h2 {
5
- background: #111;
5
+ background: #222;
6
6
  background: -moz-linear-gradient(270deg, #555555 0%, #222 40%, #000 100%) repeat scroll 0 0 transparent;
7
7
  background: -webkit-gradient(linear, left top, left bottom, color-stop(0,#555), color-stop(0.40,#222), color-stop(1.0,#000));
8
8
  }
9
9
  #content h2 {
10
10
  color: #AAA;
11
+ text-shadow: -1px -1px 2px #000000;
11
12
  }
12
13
  .cms_documentation #content {
13
14
  color: #EEE;
@@ -15,5 +15,9 @@ module Cms
15
15
  def assets
16
16
  @object ? @object.assets : Cms::Asset.scoped(nil)
17
17
  end
18
+
19
+ def components
20
+ Cms::Component.files(Cms::Component.full_path(self))
21
+ end
18
22
  end
19
23
  end
@@ -1,3 +1,3 @@
1
1
  module Cms
2
- VERSION = "0.2.0.11"
2
+ VERSION = "0.2.0.12"
3
3
  end
@@ -11,7 +11,7 @@ Gem::Specification.new do |s|
11
11
  s.summary = "A context aware Rails CMS engine using the Liquid template library."
12
12
  s.description = <<-TXT
13
13
  A context aware Rails CMS engine using the Liquid template library.
14
- Use the 0.3.x series for Rails 3 compatibility and the 0.2.x version for Rails 2 compatibility.
14
+ Use the 0.3.x version for Rails 3 compatibility and the 0.2.x version for Rails 2 compatibility.
15
15
  TXT
16
16
 
17
17
  s.required_rubygems_version = ">= 1.3.6"
@@ -11,7 +11,7 @@ class Cms::AssetsControllerTest < ActionController::TestCase
11
11
  should "show the new form" do
12
12
  get :new
13
13
  assert_select 'div.text', false
14
- #assert_select 'div.file .hint', 'Upload an asset file.'
14
+ assert_select 'div.file .hint', 'Upload an asset file.'
15
15
  end
16
16
  end
17
17
 
@@ -45,7 +45,7 @@ class Cms::AssetsControllerTest < ActionController::TestCase
45
45
  get :edit, :id => asset.id
46
46
  assert_select 'div.text', true
47
47
  assert_select 'p.break', :text => 'or edit the contents...', :count => 1
48
- #assert_select 'div.file .hint', 'An existing file has been uploaded. Upload a new file to replace it.'
48
+ assert_select 'div.file .hint', 'An existing file has been uploaded. Upload a new file to replace it.'
49
49
  end
50
50
 
51
51
  should "show form for an editable asset without a textarea" do
@@ -54,7 +54,7 @@ class Cms::AssetsControllerTest < ActionController::TestCase
54
54
  get :edit, :id => asset.id
55
55
  assert_select 'div.text', false
56
56
  assert_select 'p.break', :text => 'or edit the contents...', :count => 0
57
- #assert_select 'div.file .hint', 'An existing file has been uploaded. Upload a new file to replace it.'
57
+ assert_select 'div.file .hint', 'An existing file has been uploaded. Upload a new file to replace it.'
58
58
  end
59
59
  end
60
60
 
@@ -38,6 +38,7 @@ en:
38
38
  "yes": 'Yes'
39
39
  "no": 'No'
40
40
  buttons:
41
+ cancel: 'Cancel'
41
42
  create: 'Create %{model}'
42
43
  update: 'Update %{model}'
43
44
  submit: 'Submit %{model}'
@@ -60,3 +61,10 @@ en:
60
61
  content: 'To create a layout page, add the <em>{{ content_for_layout }}</em> tag which will act as a placeholder for the contents of another page. Use ctrl+s to save.'
61
62
  slug: 'The url path that will be used to access this page. Defaults to the page name if not provided. ie. /name.<br/> Ex. /home_page'
62
63
  layout_page_id: 'Optional page that will be used as this pages layout. Ie. This pages content will be inserted into the layout page where the <em>{{ content_for_layout }}</em> tag is specified.'
64
+ cms_component:
65
+ file_content: '<em>No liquid support for editing components.</em> Use ctrl+s to save.'
66
+ cms_asset:
67
+ asset: 'Upload an asset file.'
68
+ file_content: '<em>No liquid support for editing assets.</em> Use ctrl+s to save.'
69
+ edit:
70
+ asset: 'An existing file has been uploaded. Upload a new file to replace it.'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: liquid_cms
3
3
  version: !ruby/object:Gem::Version
4
- hash: 73
4
+ hash: 71
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 2
9
9
  - 0
10
- - 11
11
- version: 0.2.0.11
10
+ - 12
11
+ version: 0.2.0.12
12
12
  platform: ruby
13
13
  authors:
14
14
  - Andrew Kaspick
@@ -17,7 +17,7 @@ autorequire:
17
17
  bindir: bin
18
18
  cert_chain: []
19
19
 
20
- date: 2011-02-25 00:00:00 -06:00
20
+ date: 2011-03-03 00:00:00 -06:00
21
21
  default_executable:
22
22
  dependencies:
23
23
  - !ruby/object:Gem::Dependency
@@ -176,7 +176,7 @@ dependencies:
176
176
  version: "0"
177
177
  type: :development
178
178
  version_requirements: *id010
179
- description: " A context aware Rails CMS engine using the Liquid template library.\n Use the 0.3.x series for Rails 3 compatibility and the 0.2.x version for Rails 2 compatibility.\n"
179
+ description: " A context aware Rails CMS engine using the Liquid template library.\n Use the 0.3.x version for Rails 3 compatibility and the 0.2.x version for Rails 2 compatibility.\n"
180
180
  email:
181
181
  - andrew@redlinesoftware.com
182
182
  executables: []
@@ -256,17 +256,12 @@ files:
256
256
  - generators/liquid_cms/templates/config/locales/cms/en.yml
257
257
  - generators/liquid_cms/templates/migration.rb
258
258
  - generators/liquid_cms/templates/public/cms/codemirror/LICENSE
259
- - generators/liquid_cms/templates/public/cms/codemirror/bigtest.html
260
259
  - generators/liquid_cms/templates/public/cms/codemirror/css/csscolors.css
261
260
  - generators/liquid_cms/templates/public/cms/codemirror/css/docs.css
261
+ - generators/liquid_cms/templates/public/cms/codemirror/css/font.js
262
262
  - generators/liquid_cms/templates/public/cms/codemirror/css/jscolors.css
263
- - generators/liquid_cms/templates/public/cms/codemirror/css/people.jpg
264
263
  - generators/liquid_cms/templates/public/cms/codemirror/css/sparqlcolors.css
265
264
  - generators/liquid_cms/templates/public/cms/codemirror/css/xmlcolors.css
266
- - generators/liquid_cms/templates/public/cms/codemirror/csstest.html
267
- - generators/liquid_cms/templates/public/cms/codemirror/highlight.html
268
- - generators/liquid_cms/templates/public/cms/codemirror/htmltest.html
269
- - generators/liquid_cms/templates/public/cms/codemirror/index.html
270
265
  - generators/liquid_cms/templates/public/cms/codemirror/js/codemirror.js
271
266
  - generators/liquid_cms/templates/public/cms/codemirror/js/editor.js
272
267
  - generators/liquid_cms/templates/public/cms/codemirror/js/highlight.js
@@ -282,12 +277,8 @@ files:
282
277
  - generators/liquid_cms/templates/public/cms/codemirror/js/tokenize.js
283
278
  - generators/liquid_cms/templates/public/cms/codemirror/js/tokenizejavascript.js
284
279
  - generators/liquid_cms/templates/public/cms/codemirror/js/undo.js
280
+ - generators/liquid_cms/templates/public/cms/codemirror/js/unittests.js
285
281
  - generators/liquid_cms/templates/public/cms/codemirror/js/util.js
286
- - generators/liquid_cms/templates/public/cms/codemirror/jstest.html
287
- - generators/liquid_cms/templates/public/cms/codemirror/manual.html
288
- - generators/liquid_cms/templates/public/cms/codemirror/mixedtest.html
289
- - generators/liquid_cms/templates/public/cms/codemirror/sparqltest.html
290
- - generators/liquid_cms/templates/public/cms/codemirror/story.html
291
282
  - generators/liquid_cms/templates/public/cms/images/icons/accept.png
292
283
  - generators/liquid_cms/templates/public/cms/images/icons/add.png
293
284
  - generators/liquid_cms/templates/public/cms/images/icons/anchor.png
@@ -1315,12 +1306,16 @@ files:
1315
1306
  - generators/liquid_cms/templates/public/cms/javascripts/cms.js
1316
1307
  - generators/liquid_cms/templates/public/cms/javascripts/cookiejar.js
1317
1308
  - generators/liquid_cms/templates/public/cms/javascripts/humanmsg.js
1309
+ - generators/liquid_cms/templates/public/cms/javascripts/livepipe.js
1318
1310
  - generators/liquid_cms/templates/public/cms/javascripts/parseliquid.js
1311
+ - generators/liquid_cms/templates/public/cms/javascripts/tabs.js
1319
1312
  - generators/liquid_cms/templates/public/cms/stylesheets/clearfix.css
1320
1313
  - generators/liquid_cms/templates/public/cms/stylesheets/documentation.css
1321
1314
  - generators/liquid_cms/templates/public/cms/stylesheets/humanmsg.css
1322
1315
  - generators/liquid_cms/templates/public/cms/stylesheets/ie.css
1316
+ - generators/liquid_cms/templates/public/cms/stylesheets/ie9.css
1323
1317
  - generators/liquid_cms/templates/public/cms/stylesheets/liquidcolors.css
1318
+ - generators/liquid_cms/templates/public/cms/stylesheets/sidebar.css
1324
1319
  - generators/liquid_cms/templates/public/cms/stylesheets/simple_form.css
1325
1320
  - generators/liquid_cms/templates/public/cms/stylesheets/styles.css
1326
1321
  - generators/liquid_cms/templates/public/cms/stylesheets/themes/dark.css
@@ -1,1296 +0,0 @@
1
- <html xmlns="http://www.w3.org/1999/xhtml">
2
- <head>
3
- <script src="js/codemirror.js" type="text/javascript"></script>
4
- <title>CodeMirror: JavaScript demonstration</title>
5
- <link rel="stylesheet" type="text/css" href="css/docs.css"/>
6
- </head>
7
- <body style="padding: 20px;">
8
-
9
- <p>This page demonstrates <a href="index.html">CodeMirror</a>'s
10
- JavaScript parser. Note that the ugly buttons at the top are not are
11
- not part of CodeMirror proper -- they demonstrate the way it can be
12
- embedded in a web-application.</p>
13
-
14
- <div class="border">
15
- <textarea id="code" cols="120" rows="30">
16
- /* The Editor object manages the content of the editable frame. It
17
- * catches events, colours nodes, and indents lines. This file also
18
- * holds some functions for transforming arbitrary DOM structures into
19
- * plain sequences of &lt;span> and &lt;br> elements
20
- */
21
-
22
- // Make sure a string does not contain two consecutive 'collapseable'
23
- // whitespace characters.
24
- function makeWhiteSpace(n) {
25
- var buffer = [], nb = true;
26
- for (; n > 0; n--) {
27
- buffer.push((nb || n == 1) ? nbsp : " ");
28
- nb = !nb;
29
- }
30
- return buffer.join("");
31
- }
32
-
33
- // Create a set of white-space characters that will not be collapsed
34
- // by the browser, but will not break text-wrapping either.
35
- function fixSpaces(string) {
36
- if (string.charAt(0) == " ") string = nbsp + string.slice(1);
37
- return string.replace(/[\t \u00a0]{2,}/g, function(s) {return makeWhiteSpace(s.length);});
38
- }
39
-
40
- function cleanText(text) {
41
- return text.replace(/\u00a0/g, " ").replace(/\u200b/g, "");
42
- }
43
-
44
- // Create a SPAN node with the expected properties for document part
45
- // spans.
46
- function makePartSpan(value, doc) {
47
- var text = value;
48
- if (value.nodeType == 3) text = value.nodeValue;
49
- else value = doc.createTextNode(text);
50
-
51
- var span = doc.createElement("SPAN");
52
- span.isPart = true;
53
- span.appendChild(value);
54
- span.currentText = text;
55
- return span;
56
- }
57
-
58
- // On webkit, when the last BR of the document does not have text
59
- // behind it, the cursor can not be put on the line after it. This
60
- // makes pressing enter at the end of the document occasionally do
61
- // nothing (or at least seem to do nothing). To work around it, this
62
- // function makes sure the document ends with a span containing a
63
- // zero-width space character. The traverseDOM iterator filters such
64
- // character out again, so that the parsers won't see them. This
65
- // function is called from a few strategic places to make sure the
66
- // zwsp is restored after the highlighting process eats it.
67
- var webkitLastLineHack = webkit ?
68
- function(container) {
69
- var last = container.lastChild;
70
- if (!last || !last.isPart || last.textContent != "\u200b")
71
- container.appendChild(makePartSpan("\u200b", container.ownerDocument));
72
- } : function() {};
73
-
74
- var Editor = (function(){
75
- // The HTML elements whose content should be suffixed by a newline
76
- // when converting them to flat text.
77
- var newlineElements = {"P": true, "DIV": true, "LI": true};
78
-
79
- function asEditorLines(string) {
80
- return fixSpaces(string.replace(/\t/g, " ").replace(/\u00a0/g, " ")).replace(/\r\n?/g, "\n").split("\n");
81
- }
82
-
83
- // Helper function for traverseDOM. Flattens an arbitrary DOM node
84
- // into an array of textnodes and &lt;br> tags.
85
- function simplifyDOM(root) {
86
- var doc = root.ownerDocument;
87
- var result = [];
88
- var leaving = true;
89
-
90
- function simplifyNode(node) {
91
- if (node.nodeType == 3) {
92
- var text = node.nodeValue = fixSpaces(node.nodeValue.replace(/[\r\u200b]/g, "").replace(/\n/g, " "));
93
- if (text.length) leaving = false;
94
- result.push(node);
95
- }
96
- else if (node.nodeName == "BR" &amp;&amp; node.childNodes.length == 0) {
97
- leaving = true;
98
- result.push(node);
99
- }
100
- else {
101
- forEach(node.childNodes, simplifyNode);
102
- if (!leaving &amp;&amp; newlineElements.hasOwnProperty(node.nodeName)) {
103
- leaving = true;
104
- result.push(doc.createElement("BR"));
105
- }
106
- }
107
- }
108
-
109
- simplifyNode(root);
110
- return result;
111
- }
112
-
113
- // Creates a MochiKit-style iterator that goes over a series of DOM
114
- // nodes. The values it yields are strings, the textual content of
115
- // the nodes. It makes sure that all nodes up to and including the
116
- // one whose text is being yielded have been 'normalized' to be just
117
- // &lt;span> and &lt;br> elements.
118
- // See the story.html file for some short remarks about the use of
119
- // continuation-passing style in this iterator.
120
- function traverseDOM(start){
121
- function yield(value, c){cc = c; return value;}
122
- function push(fun, arg, c){return function(){return fun(arg, c);};}
123
- function stop(){cc = stop; throw StopIteration;};
124
- var cc = push(scanNode, start, stop);
125
- var owner = start.ownerDocument;
126
- var nodeQueue = [];
127
-
128
- // Create a function that can be used to insert nodes after the
129
- // one given as argument.
130
- function pointAt(node){
131
- var parent = node.parentNode;
132
- var next = node.nextSibling;
133
- return function(newnode) {
134
- parent.insertBefore(newnode, next);
135
- };
136
- }
137
- var point = null;
138
-
139
- // Insert a normalized node at the current point. If it is a text
140
- // node, wrap it in a &lt;span>, and give that span a currentText
141
- // property -- this is used to cache the nodeValue, because
142
- // directly accessing nodeValue is horribly slow on some browsers.
143
- // The dirty property is used by the highlighter to determine
144
- // which parts of the document have to be re-highlighted.
145
- function insertPart(part){
146
- var text = "\n";
147
- if (part.nodeType == 3) {
148
- select.snapshotChanged();
149
- part = makePartSpan(part, owner);
150
- text = part.currentText;
151
- }
152
- part.dirty = true;
153
- nodeQueue.push(part);
154
- point(part);
155
- return text;
156
- }
157
-
158
- // Extract the text and newlines from a DOM node, insert them into
159
- // the document, and yield the textual content. Used to replace
160
- // non-normalized nodes.
161
- function writeNode(node, c){
162
- var toYield = [];
163
- forEach(simplifyDOM(node), function(part) {
164
- toYield.push(insertPart(part));
165
- });
166
- return yield(toYield.join(""), c);
167
- }
168
-
169
- // Check whether a node is a normalized &lt;span> element.
170
- function partNode(node){
171
- if (node.isPart &amp;&amp; node.childNodes.length == 1 &amp;&amp; node.firstChild.nodeType == 3) {
172
- node.currentText = node.firstChild.nodeValue;
173
- return !/[\n\t\r]/.test(node.currentText);
174
- }
175
- return false;
176
- }
177
-
178
- // Handle a node. Add its successor to the continuation if there
179
- // is one, find out whether the node is normalized. If it is,
180
- // yield its content, otherwise, normalize it (writeNode will take
181
- // care of yielding).
182
- function scanNode(node, c){
183
- if (node.nextSibling)
184
- c = push(scanNode, node.nextSibling, c);
185
-
186
- if (partNode(node)){
187
- nodeQueue.push(node);
188
- return yield(node.currentText, c);
189
- }
190
- else if (node.nodeName == "BR") {
191
- nodeQueue.push(node);
192
- return yield("\n", c);
193
- }
194
- else {
195
- point = pointAt(node);
196
- removeElement(node);
197
- return writeNode(node, c);
198
- }
199
- }
200
-
201
- // MochiKit iterators are objects with a next function that
202
- // returns the next value or throws StopIteration when there are
203
- // no more values.
204
- return {next: function(){return cc();}, nodes: nodeQueue};
205
- }
206
-
207
- // Determine the text size of a processed node.
208
- function nodeSize(node) {
209
- if (node.nodeName == "BR")
210
- return 1;
211
- else
212
- return node.currentText.length;
213
- }
214
-
215
- // Search backwards through the top-level nodes until the next BR or
216
- // the start of the frame.
217
- function startOfLine(node) {
218
- while (node &amp;&amp; node.nodeName != "BR") node = node.previousSibling;
219
- return node;
220
- }
221
- function endOfLine(node, container) {
222
- if (!node) node = container.firstChild;
223
- else if (node.nodeName == "BR") node = node.nextSibling;
224
-
225
- while (node &amp;&amp; node.nodeName != "BR") node = node.nextSibling;
226
- return node;
227
- }
228
-
229
- // Replace all DOM nodes in the current selection with new ones.
230
- // Needed to prevent issues in IE where the old DOM nodes can be
231
- // pasted back into the document, still holding their old undo
232
- // information.
233
- function scrubPasted(container, start, start2) {
234
- var end = select.selectionTopNode(container, true),
235
- doc = container.ownerDocument;
236
- if (start != null &amp;&amp; start.parentNode != container) start = start2;
237
- if (start === false) start = null;
238
- if (start == end || !end || !container.firstChild) return;
239
-
240
- var clear = traverseDOM(start ? start.nextSibling : container.firstChild);
241
- while (end.parentNode == container) try{clear.next();}catch(e){break;}
242
- forEach(clear.nodes, function(node) {
243
- var newNode = node.nodeName == "BR" ? doc.createElement("BR") : makePartSpan(node.currentText, doc);
244
- container.replaceChild(newNode, node);
245
- });
246
- }
247
-
248
- // Client interface for searching the content of the editor. Create
249
- // these by calling CodeMirror.getSearchCursor. To use, call
250
- // findNext on the resulting object -- this returns a boolean
251
- // indicating whether anything was found, and can be called again to
252
- // skip to the next find. Use the select and replace methods to
253
- // actually do something with the found locations.
254
- function SearchCursor(editor, string, fromCursor) {
255
- this.editor = editor;
256
- this.history = editor.history;
257
- this.history.commit();
258
-
259
- // Are we currently at an occurrence of the search string?
260
- this.atOccurrence = false;
261
- // The object stores a set of nodes coming after its current
262
- // position, so that when the current point is taken out of the
263
- // DOM tree, we can still try to continue.
264
- this.fallbackSize = 15;
265
- var cursor;
266
- // Start from the cursor when specified and a cursor can be found.
267
- if (fromCursor &amp;&amp; (cursor = select.cursorPos(this.editor.container))) {
268
- this.line = cursor.node;
269
- this.offset = cursor.offset;
270
- }
271
- else {
272
- this.line = null;
273
- this.offset = 0;
274
- }
275
- this.valid = !!string;
276
-
277
- // Create a matcher function based on the kind of string we have.
278
- var target = string.split("\n"), self = this;;
279
- this.matches = (target.length == 1) ?
280
- // For one-line strings, searching can be done simply by calling
281
- // indexOf on the current line.
282
- function() {
283
- var match = cleanText(self.history.textAfter(self.line).slice(self.offset)).indexOf(string);
284
- if (match > -1)
285
- return {from: {node: self.line, offset: self.offset + match},
286
- to: {node: self.line, offset: self.offset + match + string.length}};
287
- } :
288
- // Multi-line strings require internal iteration over lines, and
289
- // some clunky checks to make sure the first match ends at the
290
- // end of the line and the last match starts at the start.
291
- function() {
292
- var firstLine = cleanText(self.history.textAfter(self.line).slice(self.offset));
293
- var match = firstLine.lastIndexOf(target[0]);
294
- if (match == -1 || match != firstLine.length - target[0].length)
295
- return false;
296
- var startOffset = self.offset + match;
297
-
298
- var line = self.history.nodeAfter(self.line);
299
- for (var i = 1; i &lt; target.length - 1; i++) {
300
- if (cleanText(self.history.textAfter(line)) != target[i])
301
- return false;
302
- line = self.history.nodeAfter(line);
303
- }
304
-
305
- if (cleanText(self.history.textAfter(line)).indexOf(target[target.length - 1]) != 0)
306
- return false;
307
-
308
- return {from: {node: self.line, offset: startOffset},
309
- to: {node: line, offset: target[target.length - 1].length}};
310
- };
311
- }
312
-
313
- SearchCursor.prototype = {
314
- findNext: function() {
315
- if (!this.valid) return false;
316
- this.atOccurrence = false;
317
- var self = this;
318
-
319
- // Go back to the start of the document if the current line is
320
- // no longer in the DOM tree.
321
- if (this.line &amp;&amp; !this.line.parentNode) {
322
- this.line = null;
323
- this.offset = 0;
324
- }
325
-
326
- // Set the cursor's position one character after the given
327
- // position.
328
- function saveAfter(pos) {
329
- if (self.history.textAfter(pos.node).length &lt; pos.offset) {
330
- self.line = pos.node;
331
- self.offset = pos.offset + 1;
332
- }
333
- else {
334
- self.line = self.history.nodeAfter(pos.node);
335
- self.offset = 0;
336
- }
337
- }
338
-
339
- while (true) {
340
- var match = this.matches();
341
- // Found the search string.
342
- if (match) {
343
- this.atOccurrence = match;
344
- saveAfter(match.from);
345
- return true;
346
- }
347
- this.line = this.history.nodeAfter(this.line);
348
- this.offset = 0;
349
- // End of document.
350
- if (!this.line) {
351
- this.valid = false;
352
- return false;
353
- }
354
- }
355
- },
356
-
357
- select: function() {
358
- if (this.atOccurrence) {
359
- select.setCursorPos(this.editor.container, this.atOccurrence.from, this.atOccurrence.to);
360
- select.scrollToCursor(this.editor.container);
361
- }
362
- },
363
-
364
- replace: function(string) {
365
- if (this.atOccurrence) {
366
- var end = this.editor.replaceRange(this.atOccurrence.from, this.atOccurrence.to, string);
367
- this.line = end.node;
368
- this.offset = end.offset;
369
- this.atOccurrence = false;
370
- }
371
- }
372
- };
373
-
374
- // The Editor object is the main inside-the-iframe interface.
375
- function Editor(options) {
376
- this.options = options;
377
- window.indentUnit = options.indentUnit;
378
- this.parent = parent;
379
- this.doc = document;
380
- var container = this.container = this.doc.body;
381
- this.win = window;
382
- this.history = new History(container, options.undoDepth, options.undoDelay,
383
- this, options.onChange);
384
- var self = this;
385
-
386
- if (!Editor.Parser)
387
- throw "No parser loaded.";
388
- if (options.parserConfig &amp;&amp; Editor.Parser.configure)
389
- Editor.Parser.configure(options.parserConfig);
390
-
391
- if (!options.readOnly)
392
- select.setCursorPos(container, {node: null, offset: 0});
393
-
394
- this.dirty = [];
395
- if (options.content)
396
- this.importCode(options.content);
397
- else // FF acts weird when the editable document is completely empty
398
- container.appendChild(this.doc.createElement("BR"));
399
-
400
- if (!options.readOnly) {
401
- if (options.continuousScanning !== false) {
402
- this.scanner = this.documentScanner(options.linesPerPass);
403
- this.delayScanning();
404
- }
405
-
406
- function setEditable() {
407
- // In IE, designMode frames can not run any scripts, so we use
408
- // contentEditable instead.
409
- if (document.body.contentEditable != undefined &amp;&amp; internetExplorer)
410
- document.body.contentEditable = "true";
411
- else
412
- document.designMode = "on";
413
-
414
- document.documentElement.style.borderWidth = "0";
415
- if (!options.textWrapping)
416
- container.style.whiteSpace = "nowrap";
417
- }
418
-
419
- // If setting the frame editable fails, try again when the user
420
- // focus it (happens when the frame is not visible on
421
- // initialisation, in Firefox).
422
- try {
423
- setEditable();
424
- }
425
- catch(e) {
426
- var focusEvent = addEventHandler(document, "focus", function() {
427
- focusEvent();
428
- setEditable();
429
- }, true);
430
- }
431
-
432
- addEventHandler(document, "keydown", method(this, "keyDown"));
433
- addEventHandler(document, "keypress", method(this, "keyPress"));
434
- addEventHandler(document, "keyup", method(this, "keyUp"));
435
-
436
- function cursorActivity() {self.cursorActivity(false);}
437
- addEventHandler(document.body, "mouseup", cursorActivity);
438
- addEventHandler(document.body, "paste", function(event) {
439
- cursorActivity();
440
- if (internetExplorer) {
441
- var text = null;
442
- try {text = window.clipboardData.getData("Text");}catch(e){}
443
- if (text != null) {
444
- self.replaceSelection(text);
445
- event.stop();
446
- }
447
- else {
448
- var start = select.selectionTopNode(self.container, true),
449
- start2 = start &amp;&amp; start.previousSibling;
450
- setTimeout(function(){scrubPasted(self.container, start, start2);}, 0);
451
- }
452
- }
453
- });
454
- addEventHandler(document.body, "cut", cursorActivity);
455
-
456
- if (this.options.autoMatchParens)
457
- addEventHandler(document.body, "click", method(this, "scheduleParenBlink"));
458
- }
459
- }
460
-
461
- function isSafeKey(code) {
462
- return (code >= 16 &amp;&amp; code &lt;= 18) || // shift, control, alt
463
- (code >= 33 &amp;&amp; code &lt;= 40); // arrows, home, end
464
- }
465
-
466
- Editor.prototype = {
467
- // Import a piece of code into the editor.
468
- importCode: function(code) {
469
- this.history.push(null, null, asEditorLines(code));
470
- this.history.reset();
471
- },
472
-
473
- // Extract the code from the editor.
474
- getCode: function() {
475
- if (!this.container.firstChild)
476
- return "";
477
-
478
- var accum = [];
479
- select.markSelection(this.win);
480
- forEach(traverseDOM(this.container.firstChild), method(accum, "push"));
481
- webkitLastLineHack(this.container);
482
- select.selectMarked();
483
- return cleanText(accum.join(""));
484
- },
485
-
486
- checkLine: function(node) {
487
- if (node === false || !(node == null || node.parentNode == this.container))
488
- throw parent.CodeMirror.InvalidLineHandle;
489
- },
490
-
491
- cursorPosition: function(start) {
492
- if (start == null) start = true;
493
- var pos = select.cursorPos(this.container, start);
494
- if (pos) return {line: pos.node, character: pos.offset};
495
- else return {line: null, character: 0};
496
- },
497
-
498
- firstLine: function() {
499
- return null;
500
- },
501
-
502
- lastLine: function() {
503
- if (this.container.lastChild) return startOfLine(this.container.lastChild);
504
- else return null;
505
- },
506
-
507
- nextLine: function(line) {
508
- this.checkLine(line);
509
- var end = endOfLine(line, this.container);
510
- return end || false;
511
- },
512
-
513
- prevLine: function(line) {
514
- this.checkLine(line);
515
- if (line == null) return false;
516
- return startOfLine(line.previousSibling);
517
- },
518
-
519
- selectLines: function(startLine, startOffset, endLine, endOffset) {
520
- this.checkLine(startLine);
521
- var start = {node: startLine, offset: startOffset}, end = null;
522
- if (endOffset !== undefined) {
523
- this.checkLine(endLine);
524
- end = {node: endLine, offset: endOffset};
525
- }
526
- select.setCursorPos(this.container, start, end);
527
- select.scrollToCursor(this.container);
528
- },
529
-
530
- lineContent: function(line) {
531
- this.checkLine(line);
532
- var accum = [];
533
- for (line = line ? line.nextSibling : this.container.firstChild;
534
- line &amp;&amp; line.nodeName != "BR"; line = line.nextSibling)
535
- accum.push(nodeText(line));
536
- return cleanText(accum.join(""));
537
- },
538
-
539
- setLineContent: function(line, content) {
540
- this.history.commit();
541
- this.replaceRange({node: line, offset: 0},
542
- {node: line, offset: this.history.textAfter(line).length},
543
- content);
544
- this.addDirtyNode(line);
545
- this.scheduleHighlight();
546
- },
547
-
548
- insertIntoLine: function(line, position, content) {
549
- var before = null;
550
- if (position == "end") {
551
- before = endOfLine(line, this.container);
552
- }
553
- else {
554
- for (var cur = line ? line.nextSibling : this.container.firstChild; cur; cur = cur.nextSibling) {
555
- if (position == 0) {
556
- before = cur;
557
- break;
558
- }
559
- var text = (cur.innerText || cur.textContent || cur.nodeValue || "");
560
- if (text.length > position) {
561
- before = cur.nextSibling;
562
- content = text.slice(0, position) + content + text.slice(position);
563
- removeElement(cur);
564
- break;
565
- }
566
- position -= text.length;
567
- }
568
- }
569
-
570
- var lines = asEditorLines(content), doc = this.container.ownerDocument;
571
- for (var i = 0; i &lt; lines.length; i++) {
572
- if (i > 0) this.container.insertBefore(doc.createElement("BR"), before);
573
- this.container.insertBefore(makePartSpan(lines[i], doc), before);
574
- }
575
- this.addDirtyNode(line);
576
- this.scheduleHighlight();
577
- },
578
-
579
- // Retrieve the selected text.
580
- selectedText: function() {
581
- var h = this.history;
582
- h.commit();
583
-
584
- var start = select.cursorPos(this.container, true),
585
- end = select.cursorPos(this.container, false);
586
- if (!start || !end) return "";
587
-
588
- if (start.node == end.node)
589
- return h.textAfter(start.node).slice(start.offset, end.offset);
590
-
591
- var text = [h.textAfter(start.node).slice(start.offset)];
592
- for (pos = h.nodeAfter(start.node); pos != end.node; pos = h.nodeAfter(pos))
593
- text.push(h.textAfter(pos));
594
- text.push(h.textAfter(end.node).slice(0, end.offset));
595
- return cleanText(text.join("\n"));
596
- },
597
-
598
- // Replace the selection with another piece of text.
599
- replaceSelection: function(text) {
600
- this.history.commit();
601
- var start = select.cursorPos(this.container, true),
602
- end = select.cursorPos(this.container, false);
603
- if (!start || !end) return;
604
-
605
- end = this.replaceRange(start, end, text);
606
- select.setCursorPos(this.container, start, end);
607
- },
608
-
609
- replaceRange: function(from, to, text) {
610
- var lines = asEditorLines(text);
611
- lines[0] = this.history.textAfter(from.node).slice(0, from.offset) + lines[0];
612
- var lastLine = lines[lines.length - 1];
613
- lines[lines.length - 1] = lastLine + this.history.textAfter(to.node).slice(to.offset);
614
- var end = this.history.nodeAfter(to.node);
615
- this.history.push(from.node, end, lines);
616
- return {node: this.history.nodeBefore(end),
617
- offset: lastLine.length};
618
- },
619
-
620
- getSearchCursor: function(string, fromCursor) {
621
- return new SearchCursor(this, string, fromCursor);
622
- },
623
-
624
- // Re-indent the whole buffer
625
- reindent: function() {
626
- if (this.container.firstChild)
627
- this.indentRegion(null, this.container.lastChild);
628
- },
629
-
630
- grabKeys: function(eventHandler, filter) {
631
- this.frozen = eventHandler;
632
- this.keyFilter = filter;
633
- },
634
- ungrabKeys: function() {
635
- this.frozen = "leave";
636
- this.keyFilter = null;
637
- },
638
-
639
- // Intercept enter and tab, and assign their new functions.
640
- keyDown: function(event) {
641
- if (this.frozen == "leave") this.frozen = null;
642
- if (this.frozen &amp;&amp; (!this.keyFilter || this.keyFilter(event.keyCode))) {
643
- event.stop();
644
- this.frozen(event);
645
- return;
646
- }
647
-
648
- var code = event.keyCode;
649
- // Don't scan when the user is typing.
650
- this.delayScanning();
651
- // Schedule a paren-highlight event, if configured.
652
- if (this.options.autoMatchParens)
653
- this.scheduleParenBlink();
654
-
655
- if (code == 13) { // enter
656
- if (event.ctrlKey) {
657
- this.reparseBuffer();
658
- }
659
- else {
660
- select.insertNewlineAtCursor(this.win);
661
- this.indentAtCursor();
662
- select.scrollToCursor(this.container);
663
- }
664
- event.stop();
665
- }
666
- else if (code == 9 &amp;&amp; this.options.tabMode != "default") { // tab
667
- this.handleTab(!event.ctrlKey &amp;&amp; !event.shiftKey);
668
- event.stop();
669
- }
670
- else if (code == 32 &amp;&amp; event.shiftKey &amp;&amp; this.options.tabMode == "default") { // space
671
- this.handleTab(true);
672
- event.stop();
673
- }
674
- else if ((code == 219 || code == 221) &amp;&amp; event.ctrlKey) {
675
- this.blinkParens(event.shiftKey);
676
- event.stop();
677
- }
678
- else if (event.metaKey &amp;&amp; (code == 37 || code == 39)) { // Meta-left/right
679
- var cursor = select.selectionTopNode(this.container);
680
- if (cursor === false || !this.container.firstChild) return;
681
-
682
- if (code == 37) select.focusAfterNode(startOfLine(cursor), this.container);
683
- else {
684
- end = endOfLine(cursor, this.container);
685
- select.focusAfterNode(end ? end.previousSibling : this.container.lastChild, this.container);
686
- }
687
- event.stop();
688
- }
689
- else if (event.ctrlKey || event.metaKey) {
690
- if ((event.shiftKey &amp;&amp; code == 90) || code == 89) { // shift-Z, Y
691
- select.scrollToNode(this.history.redo());
692
- event.stop();
693
- }
694
- else if (code == 90 || code == 8) { // Z, backspace
695
- select.scrollToNode(this.history.undo());
696
- event.stop();
697
- }
698
- else if (code == 83 &amp;&amp; this.options.saveFunction) { // S
699
- this.options.saveFunction();
700
- event.stop();
701
- }
702
- }
703
- },
704
-
705
- // Check for characters that should re-indent the current line,
706
- // and prevent Opera from handling enter and tab anyway.
707
- keyPress: function(event) {
708
- var electric = /indent|default/.test(this.options.tabMode) &amp;&amp; Editor.Parser.electricChars;
709
- // Hack for Opera, and Firefox on OS X, in which stopping a
710
- // keydown event does not prevent the associated keypress event
711
- // from happening, so we have to cancel enter and tab again
712
- // here.
713
- if ((this.frozen &amp;&amp; (!this.keyFilter || this.keyFilter(event.keyCode))) ||
714
- event.code == 13 || (event.code == 9 &amp;&amp; this.options.tabMode != "default") ||
715
- (event.keyCode == 32 &amp;&amp; event.shiftKey &amp;&amp; this.options.tabMode == "default"))
716
- event.stop();
717
- else if (electric &amp;&amp; electric.indexOf(event.character) != -1)
718
- this.parent.setTimeout(method(this, "indentAtCursor"), 0);
719
- },
720
-
721
- // Mark the node at the cursor dirty when a non-safe key is
722
- // released.
723
- keyUp: function(event) {
724
- this.cursorActivity(isSafeKey(event.keyCode));
725
- },
726
-
727
- // Indent the line following a given &lt;br>, or null for the first
728
- // line. If given a &lt;br> element, this must have been highlighted
729
- // so that it has an indentation method. Returns the whitespace
730
- // element that has been modified or created (if any).
731
- indentLineAfter: function(start, direction) {
732
- // whiteSpace is the whitespace span at the start of the line,
733
- // or null if there is no such node.
734
- var whiteSpace = start ? start.nextSibling : this.container.firstChild;
735
- if (whiteSpace &amp;&amp; !hasClass(whiteSpace, "whitespace"))
736
- whiteSpace = null;
737
-
738
- // Sometimes the start of the line can influence the correct
739
- // indentation, so we retrieve it.
740
- var firstText = whiteSpace ? whiteSpace.nextSibling : (start ? start.nextSibling : this.container.firstChild);
741
- var nextChars = (start &amp;&amp; firstText &amp;&amp; firstText.currentText) ? firstText.currentText : "";
742
-
743
- // Ask the lexical context for the correct indentation, and
744
- // compute how much this differs from the current indentation.
745
- var newIndent = 0, curIndent = whiteSpace ? whiteSpace.currentText.length : 0;
746
- if (direction != null &amp;&amp; this.options.tabMode == "shift")
747
- newIndent = direction ? curIndent + indentUnit : Math.max(0, curIndent - indentUnit)
748
- else if (start)
749
- newIndent = start.indentation(nextChars, curIndent, direction);
750
- else if (Editor.Parser.firstIndentation)
751
- newIndent = Editor.Parser.firstIndentation(nextChars, curIndent, direction);
752
- var indentDiff = newIndent - curIndent;
753
-
754
- // If there is too much, this is just a matter of shrinking a span.
755
- if (indentDiff &lt; 0) {
756
- if (newIndent == 0) {
757
- if (firstText) select.snapshotMove(whiteSpace.firstChild, firstText.firstChild, 0);
758
- removeElement(whiteSpace);
759
- whiteSpace = null;
760
- }
761
- else {
762
- select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true);
763
- whiteSpace.currentText = makeWhiteSpace(newIndent);
764
- whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
765
- }
766
- }
767
- // Not enough...
768
- else if (indentDiff > 0) {
769
- // If there is whitespace, we grow it.
770
- if (whiteSpace) {
771
- whiteSpace.currentText = makeWhiteSpace(newIndent);
772
- whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
773
- }
774
- // Otherwise, we have to add a new whitespace node.
775
- else {
776
- whiteSpace = makePartSpan(makeWhiteSpace(newIndent), this.doc);
777
- whiteSpace.className = "whitespace";
778
- if (start) insertAfter(whiteSpace, start);
779
- else this.container.insertBefore(whiteSpace, this.container.firstChild);
780
- }
781
- if (firstText) select.snapshotMove(firstText.firstChild, whiteSpace.firstChild, curIndent, false, true);
782
- }
783
- if (indentDiff != 0) this.addDirtyNode(start);
784
- return whiteSpace;
785
- },
786
-
787
- // Re-highlight the selected part of the document.
788
- highlightAtCursor: function() {
789
- var pos = select.selectionTopNode(this.container, true);
790
- var to = select.selectionTopNode(this.container, false);
791
- if (pos === false || to === false) return;
792
-
793
- select.markSelection(this.win);
794
- if (this.highlight(pos, endOfLine(to, this.container), true, 20) === false)
795
- return false;
796
- select.selectMarked();
797
- return true;
798
- },
799
-
800
- // When tab is pressed with text selected, the whole selection is
801
- // re-indented, when nothing is selected, the line with the cursor
802
- // is re-indented.
803
- handleTab: function(direction) {
804
- if (this.options.tabMode == "spaces") {
805
- select.insertTabAtCursor(this.win);
806
- }
807
- else if (!select.somethingSelected(this.win)) {
808
- this.indentAtCursor(direction);
809
- }
810
- else {
811
- var start = select.selectionTopNode(this.container, true),
812
- end = select.selectionTopNode(this.container, false);
813
- if (start === false || end === false) return;
814
- this.indentRegion(start, end, direction);
815
- }
816
- },
817
-
818
- // Delay (or initiate) the next paren blink event.
819
- scheduleParenBlink: function() {
820
- if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
821
- var self = this;
822
- this.parenEvent = this.parent.setTimeout(function(){self.blinkParens();}, 300);
823
- },
824
-
825
- // Take the token before the cursor. If it contains a character in
826
- // '()[]{}', search for the matching paren/brace/bracket, and
827
- // highlight them in green for a moment, or red if no proper match
828
- // was found.
829
- blinkParens: function(jump) {
830
- // Clear the event property.
831
- if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
832
- this.parenEvent = null;
833
-
834
- // Extract a 'paren' from a piece of text.
835
- function paren(node) {
836
- if (node.currentText) {
837
- var match = node.currentText.match(/^[\s\u00a0]*([\(\)\[\]{}])[\s\u00a0]*$/);
838
- return match &amp;&amp; match[1];
839
- }
840
- }
841
- // Determine the direction a paren is facing.
842
- function forward(ch) {
843
- return /[\(\[\{]/.test(ch);
844
- }
845
-
846
- var ch, self = this, cursor = select.selectionTopNode(this.container, true);
847
- if (!cursor || !this.highlightAtCursor()) return;
848
- cursor = select.selectionTopNode(this.container, true);
849
- if (!(cursor &amp;&amp; ((ch = paren(cursor)) || (cursor = cursor.nextSibling) &amp;&amp; (ch = paren(cursor)))))
850
- return;
851
- // We only look for tokens with the same className.
852
- var className = cursor.className, dir = forward(ch), match = matching[ch];
853
-
854
- // Since parts of the document might not have been properly
855
- // highlighted, and it is hard to know in advance which part we
856
- // have to scan, we just try, and when we find dirty nodes we
857
- // abort, parse them, and re-try.
858
- function tryFindMatch() {
859
- var stack = [], ch, ok = true;;
860
- for (var runner = cursor; runner; runner = dir ? runner.nextSibling : runner.previousSibling) {
861
- if (runner.className == className &amp;&amp; runner.nodeName == "SPAN" &amp;&amp; (ch = paren(runner))) {
862
- if (forward(ch) == dir)
863
- stack.push(ch);
864
- else if (!stack.length)
865
- ok = false;
866
- else if (stack.pop() != matching[ch])
867
- ok = false;
868
- if (!stack.length) break;
869
- }
870
- else if (runner.dirty || runner.nodeName != "SPAN" &amp;&amp; runner.nodeName != "BR") {
871
- return {node: runner, status: "dirty"};
872
- }
873
- }
874
- return {node: runner, status: runner &amp;&amp; ok};
875
- }
876
- // Temporarily give the relevant nodes a colour.
877
- function blink(node, ok) {
878
- node.style.fontWeight = "bold";
879
- node.style.color = ok ? "#8F8" : "#F88";
880
- self.parent.setTimeout(function() {node.style.fontWeight = ""; node.style.color = "";}, 500);
881
- }
882
-
883
- while (true) {
884
- var found = tryFindMatch();
885
- if (found.status == "dirty") {
886
- this.highlight(found.node, 1);
887
- // Needed because in some corner cases a highlight does not
888
- // reach a node.
889
- found.node.dirty = false;
890
- continue;
891
- }
892
- else {
893
- blink(cursor, found.status);
894
- if (found.node) {
895
- blink(found.node, found.status);
896
- if (jump) select.focusAfterNode(found.node.previousSibling, this.container);
897
- }
898
- break;
899
- }
900
- }
901
- },
902
-
903
- // Adjust the amount of whitespace at the start of the line that
904
- // the cursor is on so that it is indented properly.
905
- indentAtCursor: function(direction) {
906
- if (!this.container.firstChild) return;
907
- // The line has to have up-to-date lexical information, so we
908
- // highlight it first.
909
- if (!this.highlightAtCursor()) return;
910
- var cursor = select.selectionTopNode(this.container, false);
911
- // If we couldn't determine the place of the cursor,
912
- // there's nothing to indent.
913
- if (cursor === false)
914
- return;
915
- var lineStart = startOfLine(cursor);
916
- var whiteSpace = this.indentLineAfter(lineStart, direction);
917
- if (cursor == lineStart &amp;&amp; whiteSpace)
918
- cursor = whiteSpace;
919
- // This means the indentation has probably messed up the cursor.
920
- if (cursor == whiteSpace)
921
- select.focusAfterNode(cursor, this.container);
922
- },
923
-
924
- // Indent all lines whose start falls inside of the current
925
- // selection.
926
- indentRegion: function(start, end, direction) {
927
- var current = (start = startOfLine(start)), before = start &amp;&amp; startOfLine(start.previousSibling);
928
- if (end.nodeName != "BR") end = endOfLine(end, this.container);
929
-
930
- do {
931
- if (current) this.highlight(before, current, true);
932
- this.indentLineAfter(current, direction);
933
- before = current;
934
- current = endOfLine(current, this.container);
935
- } while (current != end);
936
- select.setCursorPos(this.container, {node: start, offset: 0}, {node: end, offset: 0});
937
- },
938
-
939
- // Find the node that the cursor is in, mark it as dirty, and make
940
- // sure a highlight pass is scheduled.
941
- cursorActivity: function(safe) {
942
- if (internetExplorer) {
943
- this.container.createTextRange().execCommand("unlink");
944
- this.selectionSnapshot = select.selectionCoords(this.win);
945
- }
946
-
947
- var activity = this.options.cursorActivity;
948
- if (!safe || activity) {
949
- var cursor = select.selectionTopNode(this.container, false);
950
- if (cursor === false || !this.container.firstChild) return;
951
- cursor = cursor || this.container.firstChild;
952
- if (activity) activity(cursor);
953
- if (!safe) {
954
- this.scheduleHighlight();
955
- this.addDirtyNode(cursor);
956
- }
957
- }
958
- },
959
-
960
- reparseBuffer: function() {
961
- forEach(this.container.childNodes, function(node) {node.dirty = true;});
962
- if (this.container.firstChild)
963
- this.addDirtyNode(this.container.firstChild);
964
- },
965
-
966
- // Add a node to the set of dirty nodes, if it isn't already in
967
- // there.
968
- addDirtyNode: function(node) {
969
- node = node || this.container.firstChild;
970
- if (!node) return;
971
-
972
- for (var i = 0; i &lt; this.dirty.length; i++)
973
- if (this.dirty[i] == node) return;
974
-
975
- if (node.nodeType != 3)
976
- node.dirty = true;
977
- this.dirty.push(node);
978
- },
979
-
980
- // Cause a highlight pass to happen in options.passDelay
981
- // milliseconds. Clear the existing timeout, if one exists. This
982
- // way, the passes do not happen while the user is typing, and
983
- // should as unobtrusive as possible.
984
- scheduleHighlight: function() {
985
- // Timeouts are routed through the parent window, because on
986
- // some browsers designMode windows do not fire timeouts.
987
- var self = this;
988
- this.parent.clearTimeout(this.highlightTimeout);
989
- this.highlightTimeout = this.parent.setTimeout(function(){self.highlightDirty();}, this.options.passDelay);
990
- },
991
-
992
- // Fetch one dirty node, and remove it from the dirty set.
993
- getDirtyNode: function() {
994
- while (this.dirty.length > 0) {
995
- var found = this.dirty.pop();
996
- // IE8 sometimes throws an unexplainable 'invalid argument'
997
- // exception for found.parentNode
998
- try {
999
- // If the node has been coloured in the meantime, or is no
1000
- // longer in the document, it should not be returned.
1001
- while (found &amp;&amp; found.parentNode != this.container)
1002
- found = found.parentNode
1003
- if (found &amp;&amp; (found.dirty || found.nodeType == 3))
1004
- return found;
1005
- } catch (e) {}
1006
- }
1007
- return null;
1008
- },
1009
-
1010
- // Pick dirty nodes, and highlight them, until
1011
- // options.linesPerPass lines have been highlighted. The highlight
1012
- // method will continue to next lines as long as it finds dirty
1013
- // nodes. It returns an object indicating the amount of lines
1014
- // left, and information about the place where it stopped. If
1015
- // there are dirty nodes left after this function has spent all
1016
- // its lines, it shedules another highlight to finish the job.
1017
- highlightDirty: function(force) {
1018
- // Prevent FF from raising an error when it is firing timeouts
1019
- // on a page that's no longer loaded.
1020
- if (!window.select) return;
1021
-
1022
- var lines = force ? Infinity : this.options.linesPerPass;
1023
- if (!this.options.readOnly) select.markSelection(this.win);
1024
- var start;
1025
- while (lines > 0 &amp;&amp; (start = this.getDirtyNode())){
1026
- var result = this.highlight(start, lines);
1027
- if (result) {
1028
- lines = result.left;
1029
- if (result.node &amp;&amp; result.dirty)
1030
- this.addDirtyNode(result.node);
1031
- }
1032
- }
1033
- if (!this.options.readOnly) select.selectMarked();
1034
- if (start)
1035
- this.scheduleHighlight();
1036
- return this.dirty.length == 0;
1037
- },
1038
-
1039
- // Creates a function that, when called through a timeout, will
1040
- // continuously re-parse the document.
1041
- documentScanner: function(linesPer) {
1042
- var self = this, pos = null;
1043
- return function() {
1044
- // If the current node is no longer in the document... oh
1045
- // well, we start over.
1046
- if (pos &amp;&amp; pos.parentNode != self.container)
1047
- pos = null;
1048
- select.markSelection(self.win);
1049
- var result = self.highlight(pos, linesPer, true);
1050
- select.selectMarked();
1051
- var newPos = result ? (result.node &amp;&amp; result.node.nextSibling) : null;
1052
- pos = (pos == newPos) ? null : newPos;
1053
- self.delayScanning();
1054
- };
1055
- },
1056
-
1057
- // Starts the continuous scanning process for this document after
1058
- // a given interval.
1059
- delayScanning: function() {
1060
- if (this.scanner) {
1061
- this.parent.clearTimeout(this.documentScan);
1062
- this.documentScan = this.parent.setTimeout(this.scanner, this.options.continuousScanning);
1063
- }
1064
- },
1065
-
1066
- // The function that does the actual highlighting/colouring (with
1067
- // help from the parser and the DOM normalizer). Its interface is
1068
- // rather overcomplicated, because it is used in different
1069
- // situations: ensuring that a certain line is highlighted, or
1070
- // highlighting up to X lines starting from a certain point. The
1071
- // 'from' argument gives the node at which it should start. If
1072
- // this is null, it will start at the beginning of the document.
1073
- // When a number of lines is given with the 'target' argument, it
1074
- // will highlight no more than that amount of lines. If this
1075
- // argument holds a DOM node, it will highlight until it reaches
1076
- // that node. If at any time it comes across a 'clean' line (no
1077
- // dirty nodes), it will stop, except when 'cleanLines' is true.
1078
- highlight: function(from, target, cleanLines, maxBacktrack){
1079
- var container = this.container, self = this, active = this.options.activeTokens;
1080
- var lines = (typeof target == "number" ? target : null);
1081
-
1082
- if (!container.firstChild)
1083
- return;
1084
- // Backtrack to the first node before from that has a partial
1085
- // parse stored.
1086
- while (from &amp;&amp; (!from.parserFromHere || from.dirty)) {
1087
- from = from.previousSibling;
1088
- if (maxBacktrack != null &amp;&amp; from.nodeName == "BR" &amp;&amp; (--maxBacktrack) &lt; 0)
1089
- return false;
1090
- }
1091
- // If we are at the end of the document, do nothing.
1092
- if (from &amp;&amp; !from.nextSibling)
1093
- return;
1094
-
1095
- // Check whether a part (&lt;span> node) and the corresponding token
1096
- // match.
1097
- function correctPart(token, part){
1098
- return !part.reduced &amp;&amp; part.currentText == token.value &amp;&amp; part.className == token.style;
1099
- }
1100
- // Shorten the text associated with a part by chopping off
1101
- // characters from the front. Note that only the currentText
1102
- // property gets changed. For efficiency reasons, we leave the
1103
- // nodeValue alone -- we set the reduced flag to indicate that
1104
- // this part must be replaced.
1105
- function shortenPart(part, minus){
1106
- part.currentText = part.currentText.substring(minus);
1107
- part.reduced = true;
1108
- }
1109
- // Create a part corresponding to a given token.
1110
- function tokenPart(token){
1111
- var part = makePartSpan(token.value, self.doc);
1112
- part.className = token.style;
1113
- return part;
1114
- }
1115
-
1116
- function maybeTouch(node) {
1117
- if (node) {
1118
- if (node.nextSibling != node.oldNextSibling) {
1119
- self.history.touch(node);
1120
- node.oldNextSibling = node.nextSibling;
1121
- }
1122
- }
1123
- else {
1124
- if (self.container.firstChild != self.container.oldFirstChild) {
1125
- self.history.touch(node);
1126
- self.container.oldFirstChild = self.container.firstChild;
1127
- }
1128
- }
1129
- }
1130
-
1131
- // Get the token stream. If from is null, we start with a new
1132
- // parser from the start of the frame, otherwise a partial parse
1133
- // is resumed.
1134
- var traversal = traverseDOM(from ? from.nextSibling : container.firstChild),
1135
- stream = stringStream(traversal),
1136
- parsed = from ? from.parserFromHere(stream) : Editor.Parser.make(stream);
1137
-
1138
- // parts is an interface to make it possible to 'delay' fetching
1139
- // the next DOM node until we are completely done with the one
1140
- // before it. This is necessary because often the next node is
1141
- // not yet available when we want to proceed past the current
1142
- // one.
1143
- var parts = {
1144
- current: null,
1145
- // Fetch current node.
1146
- get: function(){
1147
- if (!this.current)
1148
- this.current = traversal.nodes.shift();
1149
- return this.current;
1150
- },
1151
- // Advance to the next part (do not fetch it yet).
1152
- next: function(){
1153
- this.current = null;
1154
- },
1155
- // Remove the current part from the DOM tree, and move to the
1156
- // next.
1157
- remove: function(){
1158
- container.removeChild(this.get());
1159
- this.current = null;
1160
- },
1161
- // Advance to the next part that is not empty, discarding empty
1162
- // parts.
1163
- getNonEmpty: function(){
1164
- var part = this.get();
1165
- // Allow empty nodes when they are alone on a line, needed
1166
- // for the FF cursor bug workaround (see select.js,
1167
- // insertNewlineAtCursor).
1168
- while (part &amp;&amp; part.nodeName == "SPAN" &amp;&amp; part.currentText == "") {
1169
- var old = part;
1170
- this.remove();
1171
- part = this.get();
1172
- // Adjust selection information, if any. See select.js for details.
1173
- select.snapshotMove(old.firstChild, part &amp;&amp; (part.firstChild || part), 0);
1174
- }
1175
- return part;
1176
- }
1177
- };
1178
-
1179
- var lineDirty = false, prevLineDirty = true, lineNodes = 0;
1180
-
1181
- // This forEach loops over the tokens from the parsed stream, and
1182
- // at the same time uses the parts object to proceed through the
1183
- // corresponding DOM nodes.
1184
- forEach(parsed, function(token){
1185
- var part = parts.getNonEmpty();
1186
-
1187
- if (token.value == "\n"){
1188
- // The idea of the two streams actually staying synchronized
1189
- // is such a long shot that we explicitly check.
1190
- if (part.nodeName != "BR")
1191
- throw "Parser out of sync. Expected BR.";
1192
-
1193
- if (part.dirty || !part.indentation) lineDirty = true;
1194
- maybeTouch(from);
1195
- from = part;
1196
-
1197
- // Every &lt;br> gets a copy of the parser state and a lexical
1198
- // context assigned to it. The first is used to be able to
1199
- // later resume parsing from this point, the second is used
1200
- // for indentation.
1201
- part.parserFromHere = parsed.copy();
1202
- part.indentation = token.indentation;
1203
- part.dirty = false;
1204
-
1205
- // If the target argument wasn't an integer, go at least
1206
- // until that node.
1207
- if (lines == null &amp;&amp; part == target) throw StopIteration;
1208
-
1209
- // A clean line with more than one node means we are done.
1210
- // Throwing a StopIteration is the way to break out of a
1211
- // MochiKit forEach loop.
1212
- if ((lines != null &amp;&amp; --lines &lt;= 0) || (!lineDirty &amp;&amp; !prevLineDirty &amp;&amp; lineNodes > 1 &amp;&amp; !cleanLines))
1213
- throw StopIteration;
1214
- prevLineDirty = lineDirty; lineDirty = false; lineNodes = 0;
1215
- parts.next();
1216
- }
1217
- else {
1218
- if (part.nodeName != "SPAN")
1219
- throw "Parser out of sync. Expected SPAN.";
1220
- if (part.dirty)
1221
- lineDirty = true;
1222
- lineNodes++;
1223
-
1224
- // If the part matches the token, we can leave it alone.
1225
- if (correctPart(token, part)){
1226
- part.dirty = false;
1227
- parts.next();
1228
- }
1229
- // Otherwise, we have to fix it.
1230
- else {
1231
- lineDirty = true;
1232
- // Insert the correct part.
1233
- var newPart = tokenPart(token);
1234
- container.insertBefore(newPart, part);
1235
- if (active) active(newPart, token, self);
1236
- var tokensize = token.value.length;
1237
- var offset = 0;
1238
- // Eat up parts until the text for this token has been
1239
- // removed, adjusting the stored selection info (see
1240
- // select.js) in the process.
1241
- while (tokensize > 0) {
1242
- part = parts.get();
1243
- var partsize = part.currentText.length;
1244
- select.snapshotReplaceNode(part.firstChild, newPart.firstChild, tokensize, offset);
1245
- if (partsize > tokensize){
1246
- shortenPart(part, tokensize);
1247
- tokensize = 0;
1248
- }
1249
- else {
1250
- tokensize -= partsize;
1251
- offset += partsize;
1252
- parts.remove();
1253
- }
1254
- }
1255
- }
1256
- }
1257
- });
1258
- maybeTouch(from);
1259
- webkitLastLineHack(this.container);
1260
-
1261
- // The function returns some status information that is used by
1262
- // hightlightDirty to determine whether and where it has to
1263
- // continue.
1264
- return {left: lines,
1265
- node: parts.getNonEmpty(),
1266
- dirty: lineDirty};
1267
- }
1268
- };
1269
-
1270
- return Editor;
1271
- })();
1272
-
1273
- addEventHandler(window, "load", function() {
1274
- var CodeMirror = window.frameElement.CodeMirror;
1275
- CodeMirror.editor = new Editor(CodeMirror.options);
1276
- this.parent.setTimeout(method(CodeMirror, "init"), 0);
1277
- });
1278
- </textarea>
1279
- </div>
1280
-
1281
- <script type="text/javascript">
1282
- var textarea = document.getElementById('code');
1283
- var editor = new CodeMirror(CodeMirror.replace(textarea), {
1284
- height: "750px",
1285
- width: "100%",
1286
- content: textarea.value,
1287
- parserfile: ["tokenizejavascript.js", "parsejavascript.js"],
1288
- stylesheet: "css/jscolors.css",
1289
- path: "js/",
1290
- autoMatchParens: true,
1291
- initCallback: function(editor){editor.win.document.body.lastChild.scrollIntoView();}
1292
- });
1293
- </script>
1294
-
1295
- </body>
1296
- </html>