~ubuntu-branches/ubuntu/utopic/codemirror-js/utopic

« back to all changes in this revision

Viewing changes to mode/xmlpure/xmlpure.js

  • Committer: Package Import Robot
  • Author(s): David Paleino
  • Date: 2012-04-12 12:25:28 UTC
  • Revision ID: package-import@ubuntu.com-20120412122528-8xp5a8frj4h1d3ee
Tags: upstream-2.23
ImportĀ upstreamĀ versionĀ 2.23

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/**
 
2
 * xmlpure.js
 
3
 * 
 
4
 * Building upon and improving the CodeMirror 2 XML parser
 
5
 * @author: Dror BG (deebug.dev@gmail.com)
 
6
 * @date: August, 2011
 
7
 */
 
8
 
 
9
CodeMirror.defineMode("xmlpure", function(config, parserConfig) {
 
10
    // constants
 
11
    var STYLE_ERROR = "error";
 
12
    var STYLE_INSTRUCTION = "comment";
 
13
    var STYLE_COMMENT = "comment";
 
14
    var STYLE_ELEMENT_NAME = "tag";
 
15
    var STYLE_ATTRIBUTE = "attribute";
 
16
    var STYLE_WORD = "string";
 
17
    var STYLE_TEXT = "atom";
 
18
    var STYLE_ENTITIES = "string";
 
19
 
 
20
    var TAG_INSTRUCTION = "!instruction";
 
21
    var TAG_CDATA = "!cdata";
 
22
    var TAG_COMMENT = "!comment";
 
23
    var TAG_TEXT = "!text";
 
24
    
 
25
    var doNotIndent = {
 
26
        "!cdata": true,
 
27
        "!comment": true,
 
28
        "!text": true,
 
29
        "!instruction": true
 
30
    };
 
31
 
 
32
    // options
 
33
    var indentUnit = config.indentUnit;
 
34
 
 
35
    ///////////////////////////////////////////////////////////////////////////
 
36
    // helper functions
 
37
    
 
38
    // chain a parser to another parser
 
39
    function chain(stream, state, parser) {
 
40
        state.tokenize = parser;
 
41
        return parser(stream, state);
 
42
    }
 
43
    
 
44
    // parse a block (comment, CDATA or text)
 
45
    function inBlock(style, terminator, nextTokenize) {
 
46
        return function(stream, state) {
 
47
            while (!stream.eol()) {
 
48
                if (stream.match(terminator)) {
 
49
                    popContext(state);
 
50
                    state.tokenize = nextTokenize;
 
51
                    break;
 
52
                }
 
53
                stream.next();
 
54
            }
 
55
            return style;
 
56
        };
 
57
    }
 
58
    
 
59
    // go down a level in the document
 
60
    // (hint: look at who calls this function to know what the contexts are)
 
61
    function pushContext(state, tagName) {
 
62
        var noIndent = doNotIndent.hasOwnProperty(tagName) || (state.context && state.context.doIndent);
 
63
        var newContext = {
 
64
            tagName: tagName,
 
65
            prev: state.context,
 
66
            indent: state.context ? state.context.indent + indentUnit : 0,
 
67
            lineNumber: state.lineNumber,
 
68
            indented: state.indented,
 
69
            noIndent: noIndent
 
70
        };
 
71
        state.context = newContext;
 
72
    }
 
73
    
 
74
    // go up a level in the document
 
75
    function popContext(state) {
 
76
        if (state.context) {
 
77
            var oldContext = state.context;
 
78
            state.context = oldContext.prev;
 
79
            return oldContext;
 
80
        }
 
81
        
 
82
        // we shouldn't be here - it means we didn't have a context to pop
 
83
        return null;
 
84
    }
 
85
    
 
86
    // return true if the current token is seperated from the tokens before it
 
87
    // which means either this is the start of the line, or there is at least
 
88
    // one space or tab character behind the token
 
89
    // otherwise returns false
 
90
    function isTokenSeparated(stream) {
 
91
        return stream.sol() ||
 
92
            stream.string.charAt(stream.start - 1) == " " ||
 
93
            stream.string.charAt(stream.start - 1) == "\t";
 
94
    }
 
95
    
 
96
    ///////////////////////////////////////////////////////////////////////////
 
97
    // context: document
 
98
    // 
 
99
    // an XML document can contain:
 
100
    // - a single declaration (if defined, it must be the very first line)
 
101
    // - exactly one root element
 
102
    // @todo try to actually limit the number of root elements to 1
 
103
    // - zero or more comments
 
104
    function parseDocument(stream, state) {
 
105
        if(stream.eat("<")) {
 
106
            if(stream.eat("?")) {
 
107
                // processing instruction
 
108
                pushContext(state, TAG_INSTRUCTION);
 
109
                state.tokenize = parseProcessingInstructionStartTag;
 
110
                return STYLE_INSTRUCTION;
 
111
            } else if(stream.match("!--")) {
 
112
                // new context: comment
 
113
                pushContext(state, TAG_COMMENT);
 
114
                return chain(stream, state, inBlock(STYLE_COMMENT, "-->", parseDocument));
 
115
            } else if(stream.eatSpace() || stream.eol() ) {
 
116
                stream.skipToEnd();
 
117
                return STYLE_ERROR;
 
118
            } else {
 
119
                // element
 
120
                state.tokenize = parseElementTagName;
 
121
                return STYLE_ELEMENT_NAME;
 
122
            }
 
123
        }
 
124
        
 
125
        // error on line
 
126
        stream.skipToEnd();
 
127
        return STYLE_ERROR;
 
128
    }
 
129
 
 
130
    ///////////////////////////////////////////////////////////////////////////
 
131
    // context: XML element start-tag or end-tag
 
132
    //
 
133
    // - element start-tag can contain attributes
 
134
    // - element start-tag may self-close (or start an element block if it doesn't)
 
135
    // - element end-tag can contain only the tag name
 
136
    function parseElementTagName(stream, state) {
 
137
        // get the name of the tag
 
138
        var startPos = stream.pos;
 
139
        if(stream.match(/^[a-zA-Z_:][-a-zA-Z0-9_:.]*/)) {
 
140
            // element start-tag
 
141
            var tagName = stream.string.substring(startPos, stream.pos);
 
142
            pushContext(state, tagName);
 
143
            state.tokenize = parseElement;
 
144
            return STYLE_ELEMENT_NAME;
 
145
        } else if(stream.match(/^\/[a-zA-Z_:][-a-zA-Z0-9_:.]*( )*>/)) {
 
146
            // element end-tag
 
147
            var endTagName = stream.string.substring(startPos + 1, stream.pos - 1).trim();
 
148
            var oldContext = popContext(state);
 
149
            state.tokenize = state.context == null ? parseDocument : parseElementBlock;
 
150
            if(oldContext == null || endTagName != oldContext.tagName) {
 
151
                // the start and end tag names should match - error
 
152
                return STYLE_ERROR;
 
153
            }
 
154
            return STYLE_ELEMENT_NAME;
 
155
        } else {
 
156
            // no tag name - error
 
157
            state.tokenize = state.context == null ? parseDocument : parseElementBlock;
 
158
            stream.eatWhile(/[^>]/);
 
159
            stream.eat(">");
 
160
            return STYLE_ERROR;
 
161
        }
 
162
        
 
163
        stream.skipToEnd();
 
164
        return null;
 
165
    }
 
166
    
 
167
    function parseElement(stream, state) {
 
168
        if(stream.match(/^\/>/)) {
 
169
            // self-closing tag
 
170
            popContext(state);
 
171
            state.tokenize = state.context == null ? parseDocument : parseElementBlock;
 
172
            return STYLE_ELEMENT_NAME;
 
173
        } else if(stream.eat(/^>/)) {
 
174
            state.tokenize = parseElementBlock;
 
175
            return STYLE_ELEMENT_NAME;
 
176
        } else if(isTokenSeparated(stream) && stream.match(/^[a-zA-Z_:][-a-zA-Z0-9_:.]*( )*=/)) {
 
177
            // attribute
 
178
            state.tokenize = parseAttribute;
 
179
            return STYLE_ATTRIBUTE;
 
180
        }
 
181
        
 
182
        // no other options - this is an error
 
183
        state.tokenize = state.context == null ? parseDocument : parseDocument;
 
184
        stream.eatWhile(/[^>]/);
 
185
        stream.eat(">");
 
186
        return STYLE_ERROR;
 
187
    }
 
188
    
 
189
    ///////////////////////////////////////////////////////////////////////////
 
190
    // context: attribute
 
191
    // 
 
192
    // attribute values may contain everything, except:
 
193
    // - the ending quote (with ' or ") - this marks the end of the value
 
194
    // - the character "<" - should never appear
 
195
    // - ampersand ("&") - unless it starts a reference: a string that ends with a semi-colon (";")
 
196
    // ---> note: this parser is lax in what may be put into a reference string,
 
197
    // ---> consult http://www.w3.org/TR/REC-xml/#NT-Reference if you want to make it tighter
 
198
    function parseAttribute(stream, state) {
 
199
        var quote = stream.next();
 
200
        if(quote != "\"" && quote != "'") {
 
201
            // attribute must be quoted
 
202
            stream.skipToEnd();
 
203
            state.tokenize = parseElement;
 
204
            return STYLE_ERROR;
 
205
        }
 
206
        
 
207
        state.tokParams.quote = quote;    
 
208
        state.tokenize = parseAttributeValue;
 
209
        return STYLE_WORD;
 
210
    }
 
211
 
 
212
    // @todo: find out whether this attribute value spans multiple lines,
 
213
    //        and if so, push a context for it in order not to indent it
 
214
    //        (or something of the sort..)
 
215
    function parseAttributeValue(stream, state) {
 
216
        var ch = "";
 
217
        while(!stream.eol()) {
 
218
            ch = stream.next();
 
219
            if(ch == state.tokParams.quote) {
 
220
                // end quote found
 
221
                state.tokenize = parseElement;
 
222
                return STYLE_WORD;
 
223
            } else if(ch == "<") {
 
224
                // can't have less-than signs in an attribute value, ever
 
225
                stream.skipToEnd()
 
226
                state.tokenize = parseElement;
 
227
                return STYLE_ERROR;
 
228
            } else if(ch == "&") {
 
229
                // reference - look for a semi-colon, or return error if none found
 
230
                ch = stream.next();
 
231
                
 
232
                // make sure that semi-colon isn't right after the ampersand
 
233
                if(ch == ';') {
 
234
                    stream.skipToEnd()
 
235
                    state.tokenize = parseElement;
 
236
                    return STYLE_ERROR;
 
237
                }
 
238
                
 
239
                // make sure no less-than characters slipped in
 
240
                while(!stream.eol() && ch != ";") {
 
241
                    if(ch == "<") {
 
242
                        // can't have less-than signs in an attribute value, ever
 
243
                        stream.skipToEnd()
 
244
                        state.tokenize = parseElement;
 
245
                        return STYLE_ERROR;
 
246
                    }
 
247
                    ch = stream.next();
 
248
                }
 
249
                if(stream.eol() && ch != ";") {
 
250
                    // no ampersand found - error
 
251
                    stream.skipToEnd();
 
252
                    state.tokenize = parseElement;
 
253
                    return STYLE_ERROR;
 
254
                }                
 
255
            }
 
256
        }
 
257
        
 
258
        // attribute value continues to next line
 
259
        return STYLE_WORD;
 
260
    }
 
261
    
 
262
    ///////////////////////////////////////////////////////////////////////////
 
263
    // context: element block
 
264
    //
 
265
    // a block can contain:
 
266
    // - elements
 
267
    // - text
 
268
    // - CDATA sections
 
269
    // - comments
 
270
    function parseElementBlock(stream, state) {
 
271
        if(stream.eat("<")) {
 
272
            if(stream.match("?")) {
 
273
                pushContext(state, TAG_INSTRUCTION);
 
274
                state.tokenize = parseProcessingInstructionStartTag;
 
275
                return STYLE_INSTRUCTION;
 
276
            } else if(stream.match("!--")) {
 
277
                // new context: comment
 
278
                pushContext(state, TAG_COMMENT);
 
279
                return chain(stream, state, inBlock(STYLE_COMMENT, "-->",
 
280
                    state.context == null ? parseDocument : parseElementBlock));
 
281
            } else if(stream.match("![CDATA[")) {
 
282
                // new context: CDATA section
 
283
                pushContext(state, TAG_CDATA);
 
284
                return chain(stream, state, inBlock(STYLE_TEXT, "]]>",
 
285
                    state.context == null ? parseDocument : parseElementBlock));
 
286
            } else if(stream.eatSpace() || stream.eol() ) {
 
287
                stream.skipToEnd();
 
288
                return STYLE_ERROR;
 
289
            } else {
 
290
                // element
 
291
                state.tokenize = parseElementTagName;
 
292
                return STYLE_ELEMENT_NAME;
 
293
            }
 
294
        } else if(stream.eat("&")) {
 
295
            stream.eatWhile(/[^;]/);
 
296
            stream.eat(";");
 
297
            return STYLE_ENTITIES;
 
298
        } else {
 
299
            // new context: text
 
300
            pushContext(state, TAG_TEXT);
 
301
            state.tokenize = parseText;
 
302
            return null;
 
303
        }
 
304
        
 
305
        state.tokenize = state.context == null ? parseDocument : parseElementBlock;
 
306
        stream.skipToEnd();
 
307
        return null;
 
308
    }
 
309
    
 
310
    function parseText(stream, state) {
 
311
        stream.eatWhile(/[^<]/);
 
312
        if(!stream.eol()) {
 
313
            // we cannot possibly be in the document context,
 
314
            // just inside an element block
 
315
            popContext(state);
 
316
            state.tokenize = parseElementBlock;
 
317
        }
 
318
        return STYLE_TEXT;
 
319
    }
 
320
 
 
321
    ///////////////////////////////////////////////////////////////////////////
 
322
    // context: XML processing instructions
 
323
    //
 
324
    // XML processing instructions (PIs) allow documents to contain instructions for applications.
 
325
    // PI format: <?name data?>
 
326
    // - 'name' can be anything other than 'xml' (case-insensitive)
 
327
    // - 'data' can be anything which doesn't contain '?>'
 
328
    // XML declaration is a special PI (see XML declaration context below)
 
329
    function parseProcessingInstructionStartTag(stream, state) {
 
330
        if(stream.match("xml", true, true)) {
 
331
            // xml declaration
 
332
            if(state.lineNumber > 1 || stream.pos > 5) {
 
333
                state.tokenize = parseDocument;
 
334
                stream.skipToEnd();
 
335
                return STYLE_ERROR;
 
336
            } else {
 
337
                state.tokenize = parseDeclarationVersion;
 
338
                return STYLE_INSTRUCTION;
 
339
            }
 
340
        }
 
341
 
 
342
        // regular processing instruction
 
343
        if(isTokenSeparated(stream) || stream.match("?>")) {
 
344
            // we have a space after the start-tag, or nothing but the end-tag
 
345
            // either way - error!
 
346
            state.tokenize = parseDocument;
 
347
            stream.skipToEnd();
 
348
            return STYLE_ERROR;
 
349
        }
 
350
 
 
351
        state.tokenize = parseProcessingInstructionBody;
 
352
        return STYLE_INSTRUCTION;
 
353
    }
 
354
 
 
355
    function parseProcessingInstructionBody(stream, state) {
 
356
        stream.eatWhile(/[^?]/);
 
357
        if(stream.eat("?")) {
 
358
            if(stream.eat(">")) {
 
359
                popContext(state);
 
360
                state.tokenize = state.context == null ? parseDocument : parseElementBlock;
 
361
            }
 
362
        }
 
363
        return STYLE_INSTRUCTION;
 
364
    }
 
365
 
 
366
    
 
367
    ///////////////////////////////////////////////////////////////////////////
 
368
    // context: XML declaration
 
369
    //
 
370
    // XML declaration is of the following format:
 
371
    // <?xml version="1.0" encoding="UTF-8" standalone="no" ?>
 
372
    // - must start at the first character of the first line
 
373
    // - may span multiple lines
 
374
    // - must include 'version'
 
375
    // - may include 'encoding' and 'standalone' (in that order after 'version')
 
376
    // - attribute names must be lowercase
 
377
    // - cannot contain anything else on the line
 
378
    function parseDeclarationVersion(stream, state) {
 
379
        state.tokenize = parseDeclarationEncoding;
 
380
        
 
381
        if(isTokenSeparated(stream) && stream.match(/^version( )*=( )*"([a-zA-Z0-9_.:]|\-)+"/)) {
 
382
            return STYLE_INSTRUCTION;
 
383
        }
 
384
        stream.skipToEnd();
 
385
        return STYLE_ERROR;
 
386
    }
 
387
 
 
388
    function parseDeclarationEncoding(stream, state) {
 
389
        state.tokenize = parseDeclarationStandalone;
 
390
        
 
391
        if(isTokenSeparated(stream) && stream.match(/^encoding( )*=( )*"[A-Za-z]([A-Za-z0-9._]|\-)*"/)) {
 
392
            return STYLE_INSTRUCTION;
 
393
        }
 
394
        return null;
 
395
    }
 
396
 
 
397
    function parseDeclarationStandalone(stream, state) {
 
398
        state.tokenize = parseDeclarationEndTag;
 
399
        
 
400
        if(isTokenSeparated(stream) && stream.match(/^standalone( )*=( )*"(yes|no)"/)) {
 
401
            return STYLE_INSTRUCTION;
 
402
        }
 
403
        return null;
 
404
    }
 
405
 
 
406
    function parseDeclarationEndTag(stream, state) {
 
407
        state.tokenize = parseDocument;
 
408
        
 
409
        if(stream.match("?>") && stream.eol()) {
 
410
            popContext(state);
 
411
            return STYLE_INSTRUCTION;
 
412
        }
 
413
        stream.skipToEnd();
 
414
        return STYLE_ERROR;
 
415
    }
 
416
 
 
417
    ///////////////////////////////////////////////////////////////////////////
 
418
    // returned object
 
419
    return {
 
420
        electricChars: "/[",
 
421
        
 
422
        startState: function() {
 
423
            return {
 
424
                tokenize: parseDocument,
 
425
                tokParams: {},
 
426
                lineNumber: 0,
 
427
                lineError: false,
 
428
                context: null,
 
429
                indented: 0
 
430
            };
 
431
        },
 
432
 
 
433
        token: function(stream, state) {
 
434
            if(stream.sol()) {
 
435
                // initialize a new line
 
436
                state.lineNumber++;
 
437
                state.lineError = false;
 
438
                state.indented = stream.indentation();
 
439
            }
 
440
 
 
441
            // eat all (the spaces) you can
 
442
            if(stream.eatSpace()) return null;
 
443
 
 
444
            // run the current tokenize function, according to the state
 
445
            var style = state.tokenize(stream, state);
 
446
            
 
447
            // is there an error somewhere in the line?
 
448
            state.lineError = (state.lineError || style == "error");
 
449
 
 
450
            return style;
 
451
        },
 
452
        
 
453
        blankLine: function(state) {
 
454
            // blank lines are lines too!
 
455
            state.lineNumber++;
 
456
            state.lineError = false;
 
457
        },
 
458
        
 
459
        indent: function(state, textAfter) {
 
460
            if(state.context) {
 
461
                if(state.context.noIndent == true) {
 
462
                    // do not indent - no return value at all
 
463
                    return;
 
464
                }
 
465
                if(textAfter.match(/^<\/.*/)) {
 
466
                    // end-tag - indent back to last context
 
467
                    return state.context.indent;
 
468
                }
 
469
                if(textAfter.match(/^<!\[CDATA\[/)) {
 
470
                    // a stand-alone CDATA start-tag - indent back to column 0
 
471
                    return 0;                
 
472
                }
 
473
                // indent to last context + regular indent unit
 
474
                return state.context.indent + indentUnit;
 
475
            }
 
476
            return 0;
 
477
        },
 
478
        
 
479
        compareStates: function(a, b) {
 
480
            if (a.indented != b.indented) return false;
 
481
            for (var ca = a.context, cb = b.context; ; ca = ca.prev, cb = cb.prev) {
 
482
                if (!ca || !cb) return ca == cb;
 
483
                if (ca.tagName != cb.tagName) return false;
 
484
            }
 
485
        }
 
486
    };
 
487
});
 
488
 
 
489
CodeMirror.defineMIME("application/xml", "purexml");
 
490
CodeMirror.defineMIME("text/xml", "purexml");