95a80993e0cd003e04189dde2eb25d88a86923ca
[redakcja.git] / project / static / js / codemirror / parsexml.js
1 /* This file defines an XML parser, with a few kludges to make it
2  * useable for HTML. autoSelfClosers defines a set of tag names that
3  * are expected to not have a closing tag, and doNotIndent specifies
4  * the tags inside of which no indentation should happen (see Config
5  * object). These can be disabled by passing the editor an object like
6  * {useHTMLKludges: false} as parserConfig option.
7  */
8
9 var XMLParser = Editor.Parser = (function() {
10   var Kludges = {
11     autoSelfClosers: {"br": true, "img": true, "hr": true, "link": true, "input": true,
12                       "meta": true, "col": true, "frame": true, "base": true, "area": true},
13     doNotIndent: {"pre": true, "!cdata": true}
14   };
15   var NoKludges = {autoSelfClosers: {}, doNotIndent: {"!cdata": true}};
16   var UseKludges = Kludges;
17   var alignCDATA = false;
18
19   // Simple stateful tokenizer for XML documents. Returns a
20   // MochiKit-style iterator, with a state property that contains a
21   // function encapsulating the current state. See tokenize.js.
22   var tokenizeXML = (function() {
23     function inText(source, setState) {
24       var ch = source.next();
25       if (ch == "<") {
26         if (source.equals("!")) {
27           source.next();
28           if (source.equals("[")) {
29             if (source.lookAhead("[CDATA[", true)) {
30               setState(inBlock("xml-cdata", "]]>"));
31               return null;
32             }
33             else {
34               return "xml-text";
35             }
36           }
37           else if (source.lookAhead("--", true)) {
38             setState(inBlock("xml-comment", "-->"));
39             return null;
40           }
41           else {
42             return "xml-text";
43           }
44         }
45         else if (source.equals("?")) {
46           source.next();
47           source.nextWhileMatches(/[\w\._\-]/);
48           setState(inBlock("xml-processing", "?>"));
49           return "xml-processing";
50         }
51         else {
52           if (source.equals("/")) source.next();
53           setState(inTag);
54           return "xml-punctuation";
55         }
56       }
57       else if (ch == "&") {
58         while (!source.endOfLine()) {
59           if (source.next() == ";")
60             break;
61         }
62         return "xml-entity";
63       }
64       else {
65         source.nextWhileMatches(/[^&<\n]/);
66         return "xml-text";
67       }
68     }
69
70     function inTag(source, setState) {
71       var ch = source.next();
72       if (ch == ">") {
73         setState(inText);
74         return "xml-punctuation";
75       }
76       else if (/[?\/]/.test(ch) && source.equals(">")) {
77         source.next();
78         setState(inText);
79         return "xml-punctuation";
80       }
81       else if (ch == "=") {
82         return "xml-punctuation";
83       }
84       else if (/[\'\"]/.test(ch)) {
85         setState(inAttribute(ch));
86         return null;
87       }
88       else {
89         source.nextWhileMatches(/[^\s\u00a0=<>\"\'\/?]/);
90         return "xml-name";
91       }
92     }
93
94     function inAttribute(quote) {
95       return function(source, setState) {
96         while (!source.endOfLine()) {
97           if (source.next() == quote) {
98             setState(inTag);
99             break;
100           }
101         }
102         return "xml-attribute";
103       };
104     }
105
106     function inBlock(style, terminator) {
107       return function(source, setState) {
108         while (!source.endOfLine()) {
109           if (source.lookAhead(terminator, true)) {
110             setState(inText);
111             break;
112           }
113           source.next();
114         }
115         return style;
116       };
117     }
118
119     return function(source, startState) {
120       return tokenizer(source, startState || inText);
121     };
122   })();
123
124   // The parser. The structure of this function largely follows that of
125   // parseJavaScript in parsejavascript.js (there is actually a bit more
126   // shared code than I'd like), but it is quite a bit simpler.
127   function parseXML(source) {
128     var tokens = tokenizeXML(source);
129     var cc = [base];
130     var tokenNr = 0, indented = 0;
131     var currentTag = null, context = null;
132     var consume, marked;
133     
134     function push(fs) {
135       for (var i = fs.length - 1; i >= 0; i--)
136         cc.push(fs[i]);
137     }
138     function cont() {
139       push(arguments);
140       consume = true;
141     }
142     function pass() {
143       push(arguments);
144       consume = false;
145     }
146
147     function mark(style) {
148       marked = style;
149     }
150     function expect(text) {
151       return function(style, content) {
152         if (content == text) cont();
153         else mark("xml-error") || cont(arguments.callee);
154       };
155     }
156
157     function pushContext(tagname, startOfLine) {
158       var noIndent = UseKludges.doNotIndent.hasOwnProperty(tagname) || (context && context.noIndent);
159       context = {prev: context, name: tagname, indent: indented, startOfLine: startOfLine, noIndent: noIndent};
160     }
161     function popContext() {
162       context = context.prev;
163     }
164     function computeIndentation(baseContext) {
165       return function(nextChars, current) {
166         var context = baseContext;
167         if (context && context.noIndent)
168           return current;
169         if (alignCDATA && /<!\[CDATA\[/.test(nextChars))
170           return 0;
171         if (context && /^<\//.test(nextChars))
172           context = context.prev;
173         while (context && !context.startOfLine)
174           context = context.prev;
175         if (context)
176           return context.indent + indentUnit;
177         else
178           return 0;
179       };
180     }
181
182     function base() {
183       return pass(element, base);
184     }
185     var harmlessTokens = {"xml-text": true, "xml-entity": true, "xml-comment": true, "xml-processing": true};
186     function element(style, content) {
187       if (content == "<") cont(tagname, attributes, endtag(tokenNr == 1));
188       else if (content == "</") cont(closetagname, expect(">"));
189       else if (style == "xml-cdata") {
190         if (!context || context.name != "!cdata") pushContext("!cdata");
191         if (/\]\]>$/.test(content)) popContext();
192         cont();
193       }
194       else if (harmlessTokens.hasOwnProperty(style)) cont();
195       else mark("xml-error") || cont();
196     }
197     function tagname(style, content) {
198       if (style == "xml-name") {
199         currentTag = content.toLowerCase();
200         mark("xml-tagname");
201         cont();
202       }
203       else {
204         currentTag = null;
205         pass();
206       }
207     }
208     function closetagname(style, content) {
209       if (style == "xml-name" && context && content.toLowerCase() == context.name) {
210         popContext();
211         mark("xml-tagname");
212       }
213       else {
214         mark("xml-error");
215       }
216       cont();
217     }
218     function endtag(startOfLine) {
219       return function(style, content) {
220         if (content == "/>" || (content == ">" && UseKludges.autoSelfClosers.hasOwnProperty(currentTag))) cont();
221         else if (content == ">") pushContext(currentTag, startOfLine) || cont();
222         else mark("xml-error") || cont(arguments.callee);
223       };
224     }
225     function attributes(style) {
226       if (style == "xml-name") mark("xml-attname") || cont(attribute, attributes);
227       else pass();
228     }
229     function attribute(style, content) {
230       if (content == "=") cont(value);
231       else if (content == ">" || content == "/>") pass(endtag);
232       else pass();
233     }
234     function value(style) {
235       if (style == "xml-attribute") cont(value);
236       else pass();
237     }
238
239     return {
240       indentation: function() {return indented;},
241
242       next: function(){
243         var token = tokens.next();
244         if (token.style == "whitespace" && tokenNr == 0)
245           indented = token.value.length;
246         else
247           tokenNr++;
248         if (token.content == "\n") {
249           indented = tokenNr = 0;
250           token.indentation = computeIndentation(context);
251         }
252
253         if (token.style == "whitespace" || token.type == "xml-comment")
254           return token;
255
256         while(true){
257           consume = marked = false;
258           cc.pop()(token.style, token.content);
259           if (consume){
260             if (marked)
261               token.style = marked;
262             return token;
263           }
264         }
265       },
266
267       copy: function(){
268         var _cc = cc.concat([]), _tokenState = tokens.state, _context = context;
269         var parser = this;
270         
271         return function(input){
272           cc = _cc.concat([]);
273           tokenNr = indented = 0;
274           context = _context;
275           tokens = tokenizeXML(input, _tokenState);
276           return parser;
277         };
278       }
279     };
280   }
281
282   return {
283     make: parseXML,
284     electricChars: "/",
285     configure: function(config) {
286       if (config.useHTMLKludges != null)
287         UseKludges = config.useHTMLKludges ? Kludges : NoKludges;
288       if (config.alignCDATA)
289         alignCDATA = config.alignCDATA;
290     }
291   };
292 })();