Line data Source code
1 : /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 : /*
3 : * Software License Agreement (BSD License)
4 : *
5 : * Copyright (c) 2006, ScalingWeb.com
6 : * All rights reserved.
7 : *
8 : * Redistribution and use of this software in source and binary forms, with or without modification, are
9 : * permitted provided that the following conditions are met:
10 : *
11 : * * Redistributions of source code must retain the above
12 : * copyright notice, this list of conditions and the
13 : * following disclaimer.
14 : *
15 : * * Redistributions in binary form must reproduce the above
16 : * copyright notice, this list of conditions and the
17 : * following disclaimer in the documentation and/or other
18 : * materials provided with the distribution.
19 : *
20 : * * Neither the name of ScalingWeb.com nor the names of its
21 : * contributors may be used to endorse or promote products
22 : * derived from this software without specific prior
23 : * written permission of ScalingWeb.com.
24 :
25 : * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
26 : * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
27 : * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
28 : * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
29 : * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
30 : * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
31 : * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
32 : * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33 : */
34 :
35 : #include "MorkParser.hxx"
36 : #include <boost/io/ios_state.hpp>
37 : #include <stdlib.h>
38 : #include <sstream>
39 : #include <string>
40 : #include <string.h>
41 : #include <stdexcept>
42 : #include <fstream>
43 : #include <iostream>
44 : #include <algorithm>
45 :
46 2 : std::string g_Empty = "";
47 :
48 : // Mork header of supported format version
49 : const char *MorkMagicHeader = "// <!-- <mdb:mork:z v=\"1.4\"/> -->";
50 :
51 : const char *MorkDictColumnMeta = "<(a=c)>";
52 :
53 :
54 12 : MorkParser::MorkParser( int DefaultScope ) :
55 : columns_(),
56 : values_(),
57 : mork_(),
58 : currentCells_(0),
59 : error_(NoError),
60 : morkData_(),
61 : morkPos_(0),
62 : nextAddValueId_(0x7fffffff),
63 : defaultScope_(DefaultScope),
64 : defaultListScope_(0x81),
65 : defaultTableId_(1),
66 12 : nowParsing_(NPValues)
67 : {
68 12 : }
69 :
70 6 : bool MorkParser::open( const std::string &path )
71 : {
72 6 : initVars();
73 6 : std::string line;
74 12 : std::ifstream infile(path.c_str(), std::ios_base::in);
75 6 : if(!infile.is_open())
76 : {
77 0 : error_ = FailedToOpen;
78 0 : return false;
79 : }
80 :
81 966 : while (getline(infile, line, '\n'))
82 : {
83 954 : morkData_.append(line);
84 954 : morkData_.append("\n");
85 : }
86 :
87 : // Parse mork
88 12 : return parse();
89 : }
90 :
91 : inline MorkErrors MorkParser::error()
92 : {
93 : return error_;
94 : }
95 :
96 6 : void MorkParser::initVars()
97 : {
98 6 : error_ = NoError;
99 6 : morkPos_ = 0;
100 6 : nowParsing_ = NPValues;
101 6 : currentCells_ = 0;
102 6 : nextAddValueId_ = 0x7fffffff;
103 6 : }
104 :
105 6 : bool MorkParser::parse()
106 : {
107 6 : bool Result = true;
108 :
109 : // Run over mork chars and parse each term
110 6 : char cur = nextChar();
111 :
112 6 : int i = 0;
113 :
114 612 : while ( Result && cur )
115 : {
116 600 : if ( !isWhiteSpace( cur ) )
117 : {
118 264 : i++;
119 : // Figure out what a term
120 264 : switch ( cur )
121 : {
122 : case '<':
123 : // Dict
124 48 : Result = parseDict();
125 48 : break;
126 : case '/':
127 : // Comment
128 6 : Result = parseComment();
129 6 : break;
130 : case '{':
131 12 : Result = parseTable();
132 : // Table
133 12 : break;
134 : case '[':
135 66 : Result = parseRow( 0, 0 );
136 : // Row
137 66 : break;
138 : case '@':
139 132 : Result = parseGroup();
140 : // Group
141 132 : break;
142 : default:
143 0 : error_ = DefectedFormat;
144 0 : Result = false;
145 0 : break;
146 : }
147 : }
148 :
149 : // Get next char
150 600 : cur = nextChar();
151 : }
152 :
153 6 : return Result;
154 : }
155 :
156 8736 : bool MorkParser::isWhiteSpace( char c )
157 : {
158 8736 : switch ( c )
159 : {
160 : case ' ':
161 : case '\t':
162 : case '\r':
163 : case '\n':
164 : case '\f':
165 2850 : return true;
166 : default:
167 5886 : return false;
168 : }
169 : }
170 :
171 43854 : inline char MorkParser::nextChar()
172 : {
173 43854 : char cur = 0;
174 :
175 :
176 43854 : if ( morkPos_ < morkData_.length() )
177 : {
178 43848 : cur = morkData_[ morkPos_ ];
179 43848 : morkPos_++;
180 : }
181 :
182 43854 : if ( !cur )
183 : {
184 6 : cur = 0;
185 : }
186 :
187 43854 : return cur;
188 : }
189 :
190 48 : bool MorkParser::parseDict()
191 : {
192 48 : char cur = nextChar();
193 48 : bool Result = true;
194 48 : nowParsing_ = NPValues;
195 :
196 1626 : while ( Result && cur != '>' && cur )
197 : {
198 1530 : if ( !isWhiteSpace( cur ) )
199 : {
200 882 : switch ( cur )
201 : {
202 : case '<':
203 : {
204 :
205 36 : if ( morkData_.substr( morkPos_ - 1, strlen( MorkDictColumnMeta ) ) == MorkDictColumnMeta )
206 : {
207 36 : nowParsing_ = NPColumns;
208 36 : morkPos_ += strlen( MorkDictColumnMeta ) - 1;
209 : }
210 :
211 :
212 36 : break;
213 : }
214 : case '(':
215 810 : Result = parseCell();
216 810 : break;
217 : case '/':
218 36 : Result = parseComment();
219 36 : break;
220 :
221 : }
222 : }
223 :
224 1530 : cur = nextChar();
225 : }
226 :
227 48 : return Result;
228 : }
229 :
230 42 : inline bool MorkParser::parseComment()
231 : {
232 42 : char cur = nextChar();
233 42 : if ( '/' != cur ) return false;
234 :
235 852 : while ( cur != '\r' && cur != '\n' && cur )
236 : {
237 768 : cur = nextChar();
238 : }
239 :
240 42 : return true;
241 : }
242 :
243 4854 : bool MorkParser::parseCell()
244 : {
245 4854 : bool Result = true;
246 4854 : bool bValueOid = false;
247 4854 : bool bColumn = true;
248 4854 : int Corners = 0;
249 :
250 : // Column = Value
251 4854 : std::string Column;
252 9708 : std::string Text;
253 4854 : Column.reserve( 4 );
254 4854 : Text.reserve( 32 );
255 :
256 4854 : char cur = nextChar();
257 :
258 : // Process cell start with column (bColumn == true)
259 37944 : while ( Result && cur != ')' && cur )
260 : {
261 28236 : switch ( cur )
262 : {
263 : case '^':
264 : // Oids
265 4548 : Corners++;
266 4548 : if ( 1 == Corners )
267 : {
268 : }
269 504 : else if ( 2 == Corners )
270 : {
271 504 : bColumn = false;
272 504 : bValueOid = true;
273 : }
274 : else
275 : {
276 0 : Text += cur;
277 : }
278 :
279 4548 : break;
280 : case '=':
281 : // From column to value
282 4350 : if ( bColumn )
283 : {
284 4350 : bColumn = false;
285 : }
286 : else
287 : {
288 0 : Text += cur;
289 : }
290 4350 : break;
291 : case '\\':
292 : {
293 : // Get next two chars
294 0 : char NextChar= nextChar();
295 0 : if ( '\r' != NextChar && '\n' != NextChar )
296 : {
297 0 : Text += NextChar;
298 : }
299 : else
300 : {
301 0 : (void)nextChar();
302 : }
303 : }
304 0 : break;
305 : case '$':
306 : {
307 : // Get next two chars
308 0 : std::string HexChar;
309 0 : HexChar += nextChar();
310 0 : HexChar += nextChar();
311 0 : Text += (char)strtoul(HexChar.c_str(), 0, 16);
312 : }
313 0 : break;
314 : default:
315 : // Just a char
316 19338 : if ( bColumn )
317 : {
318 9738 : Column += cur;
319 : }
320 : else
321 : {
322 9600 : Text += cur;
323 : }
324 19338 : break;
325 : }
326 :
327 28236 : cur = nextChar();
328 : }
329 :
330 : // Apply column and text
331 4854 : int ColumnId = strtoul(Column.c_str(), 0, 16);
332 :
333 4854 : if ( NPRows != nowParsing_ )
334 : {
335 : // Dicts
336 810 : if ( "" != Text )
337 : {
338 804 : if ( nowParsing_ == NPColumns )
339 : {
340 504 : columns_[ ColumnId ] = Text;
341 : }
342 : else
343 : {
344 300 : values_[ ColumnId ] = Text;
345 : }
346 : }
347 : }
348 : else
349 : {
350 4044 : if ( "" != Text )
351 : {
352 : // Rows
353 : //int ValueId = string( Text.c_str() ).toInt( 0, 16 );
354 1260 : int ValueId = strtoul(Text.c_str(), 0, 16);
355 :
356 1260 : if ( bValueOid )
357 : {
358 504 : ( *currentCells_ )[ ColumnId ] = ValueId;
359 : }
360 : else
361 : {
362 756 : nextAddValueId_--;
363 756 : values_[ nextAddValueId_ ] = Text;
364 756 : ( *currentCells_ )[ ColumnId ] = nextAddValueId_;
365 : }
366 : }
367 : }
368 :
369 9708 : return Result;
370 : }
371 :
372 12 : bool MorkParser::parseTable()
373 : {
374 12 : bool Result = true;
375 12 : std::string TextId;
376 12 : int Id = 0, Scope = 0;
377 :
378 12 : char cur = nextChar();
379 :
380 : // Get id
381 96 : while ( cur != '{' && cur != '[' && cur != '}' && cur )
382 : {
383 72 : if ( !isWhiteSpace( cur ) )
384 : {
385 60 : TextId += cur;
386 : }
387 :
388 72 : cur = nextChar();
389 : }
390 :
391 12 : parseScopeId( TextId, &Id, &Scope );
392 :
393 : // Parse the table
394 348 : while ( Result && cur != '}' && cur )
395 : {
396 324 : if ( !isWhiteSpace( cur ) )
397 : {
398 90 : switch ( cur )
399 : {
400 : case '{':
401 12 : Result = parseMeta( '}' );
402 12 : break;
403 : case '[':
404 78 : Result = parseRow( Id, Scope );
405 78 : break;
406 : case '-':
407 : case '+':
408 0 : break;
409 : default:
410 : {
411 0 : std::string JustId;
412 0 : while ( !isWhiteSpace( cur ) && cur )
413 : {
414 0 : JustId += cur;
415 0 : cur = nextChar();
416 :
417 0 : if ( cur == '}' )
418 : {
419 0 : return Result;
420 : }
421 : }
422 :
423 0 : int JustIdNum = 0, JustScopeNum = 0;
424 0 : parseScopeId( JustId, &JustIdNum, &JustScopeNum );
425 :
426 0 : setCurrentRow( Scope, Id, JustScopeNum, JustIdNum );
427 : }
428 0 : break;
429 : }
430 : }
431 :
432 324 : cur = nextChar();
433 : }
434 :
435 12 : return Result;
436 : }
437 :
438 156 : void MorkParser::parseScopeId( const std::string &TextId, int *Id, int *Scope )
439 : {
440 156 : int Pos = 0;
441 :
442 156 : if ( ( Pos = TextId.find( ':' ) ) >= 0 )
443 : {
444 96 : std::string tId = TextId.substr( 0, Pos );
445 192 : std::string tSc = TextId.substr( Pos + 1, TextId.length() - Pos );
446 :
447 96 : if ( tSc.length() > 1 && '^' == tSc[ 0 ] )
448 : {
449 : // Delete '^'
450 96 : tSc.erase( 0, 1 );
451 : }
452 :
453 96 : *Id = strtoul(tId.c_str(), 0, 16);
454 :
455 192 : *Scope = strtoul(tSc.c_str(), 0, 16);
456 : }
457 : else
458 : {
459 60 : *Id = strtoul(TextId.c_str(), 0, 16);
460 : }
461 156 : }
462 :
463 144 : inline void MorkParser::setCurrentRow( int TableScope, int TableId, int RowScope, int RowId )
464 : {
465 144 : if ( !RowScope )
466 : {
467 60 : RowScope = defaultScope_;
468 : }
469 :
470 144 : if ( !TableScope )
471 : {
472 66 : TableScope = defaultScope_;
473 : }
474 :
475 : // 01.08.2012 davido
476 : // TableId 0 is wrong here.
477 : // Straying rows (rows that defined outside the table) belong to the default scope and table is the last was seen: 1:^80
478 : // (at least i read so the specification)
479 144 : if (TableId)
480 : {
481 78 : defaultTableId_ = TableId;
482 : }
483 :
484 144 : if (!TableId)
485 : {
486 66 : TableId = defaultTableId_;
487 : }
488 :
489 144 : currentCells_ = &( mork_[ abs( TableScope ) ][ abs( TableId ) ][ abs( RowScope ) ][ abs( RowId ) ] );
490 144 : }
491 :
492 144 : bool MorkParser::parseRow( int TableId, int TableScope )
493 : {
494 144 : bool Result = true;
495 144 : std::string TextId;
496 144 : int Id = 0, Scope = 0;
497 144 : nowParsing_ = NPRows;
498 :
499 144 : char cur = nextChar();
500 :
501 : // Get id
502 834 : while ( cur != '(' && cur != ']' && cur != '[' && cur )
503 : {
504 546 : if ( !isWhiteSpace( cur ) )
505 : {
506 546 : TextId += cur;
507 : }
508 :
509 546 : cur = nextChar();
510 : }
511 :
512 144 : parseScopeId( TextId, &Id, &Scope );
513 144 : setCurrentRow( TableScope, TableId, Scope, Id );
514 :
515 : // Parse the row
516 5952 : while ( Result && cur != ']' && cur )
517 : {
518 5664 : if ( !isWhiteSpace( cur ) )
519 : {
520 4044 : switch ( cur )
521 : {
522 : case '(':
523 4044 : Result = parseCell();
524 4044 : break;
525 : case '[':
526 0 : Result = parseMeta( ']' );
527 0 : break;
528 : default:
529 0 : Result = false;
530 0 : break;
531 : }
532 : }
533 :
534 5664 : cur = nextChar();
535 : }
536 :
537 144 : return Result;
538 : }
539 :
540 132 : bool MorkParser::parseGroup()
541 : {
542 132 : return parseMeta( '@' );
543 : }
544 :
545 144 : bool MorkParser::parseMeta( char c )
546 : {
547 144 : char cur = nextChar();
548 :
549 1152 : while ( cur != c && cur )
550 : {
551 864 : cur = nextChar();
552 : }
553 :
554 144 : return true;
555 : }
556 :
557 174 : MorkTableMap *MorkParser::getTables( int TableScope )
558 : {
559 174 : TableScopeMap::iterator iter;
560 174 : iter = mork_.find( TableScope );
561 :
562 174 : if ( iter == mork_.end() )
563 : {
564 0 : return 0;
565 : }
566 :
567 174 : return &iter->second;
568 : }
569 :
570 168 : MorkRowMap *MorkParser::getRows( int RowScope, RowScopeMap *table )
571 : {
572 168 : RowScopeMap::iterator iter;
573 168 : iter = table->find( RowScope );
574 :
575 168 : if ( iter == table->end() )
576 : {
577 0 : return 0;
578 : }
579 :
580 168 : return &iter->second;
581 : }
582 :
583 728 : std::string &MorkParser::getValue( int oid )
584 : {
585 728 : MorkDict::iterator foundIter = values_.find( oid );
586 :
587 728 : if ( values_.end() == foundIter )
588 : {
589 0 : return g_Empty;
590 : }
591 :
592 728 : return foundIter->second;
593 : }
594 :
595 390 : std::string &MorkParser::getColumn( int oid )
596 : {
597 390 : MorkDict::iterator foundIter = columns_.find( oid );
598 :
599 390 : if ( columns_.end() == foundIter )
600 : {
601 0 : return g_Empty;
602 : }
603 :
604 390 : return foundIter->second;
605 : }
606 :
607 162 : void MorkParser::retrieveLists(std::set<std::string>& lists)
608 : {
609 : #ifdef VERBOSE
610 : boost::io::ios_all_saver ias(std::cout);
611 : std::cout << std::hex << std::uppercase;
612 : #endif
613 :
614 162 : MorkTableMap* tables = getTables(defaultScope_);
615 162 : if (!tables) return;
616 972 : for (MorkTableMap::iterator TableIter = tables->begin();
617 648 : TableIter != tables->end(); ++TableIter )
618 : {
619 : #ifdef VERBOSE
620 : std::cout << "\t Table:"
621 : << ( ( int ) TableIter->first < 0 ? "-" : " " )
622 : << TableIter->first << std::endl;
623 : #endif
624 162 : MorkRowMap* rows = getRows( defaultListScope_, &TableIter->second );
625 162 : if (!rows) return;
626 1458 : for ( MorkRowMap::iterator RowIter = rows->begin();
627 972 : RowIter != rows->end(); ++RowIter )
628 : {
629 : #ifdef VERBOSE
630 : std::cout << "\t\t\t Row Id:"
631 : << ( ( int ) RowIter->first < 0 ? "-" : " ")
632 : << RowIter->first << std::endl;
633 : std::cout << "\t\t\t\t Cells:\r\n";
634 : #endif
635 : // Get cells
636 3888 : for ( MorkCells::iterator cellsIter = RowIter->second.begin();
637 2592 : cellsIter != RowIter->second.end(); ++cellsIter )
638 : {
639 1296 : if (cellsIter->first == 0xC1)
640 : {
641 324 : lists.insert(getValue( cellsIter->second ));
642 324 : break;
643 : }
644 : }
645 : }
646 : }
647 : }
648 :
649 2 : void MorkParser::getRecordKeysForListTable(std::string& listName, std::set<int>& records)
650 : {
651 : #ifdef VERBOSE
652 : boost::io::ios_all_saver ias(std::cout);
653 : std::cout << std::hex << std::uppercase;
654 : #endif
655 :
656 2 : MorkTableMap* tables = getTables(defaultScope_);
657 2 : if (!tables) return;
658 12 : for (MorkTableMap::iterator TableIter = tables->begin();
659 8 : TableIter != tables->end(); ++TableIter )
660 : {
661 : #ifdef VERBOSE
662 : std::cout << "\t Table:"
663 : << ( ( int ) TableIter->first < 0 ? "-" : " " )
664 : << TableIter->first << std::endl;
665 : #endif
666 2 : MorkRowMap* rows = getRows( 0x81, &TableIter->second );
667 2 : if (!rows) return;
668 18 : for ( MorkRowMap::iterator RowIter = rows->begin();
669 12 : RowIter != rows->end(); ++RowIter )
670 : {
671 : #ifdef VERBOSE
672 : std::cout << "\t\t\t Row Id:"
673 : << ( ( int ) RowIter->first < 0 ? "-" : " ")
674 : << RowIter->first << std::endl;
675 : std::cout << "\t\t\t\t Cells:\r\n";
676 : #endif
677 : // Get cells
678 4 : bool listFound = false;
679 120 : for ( MorkCells::iterator cellsIter = RowIter->second.begin();
680 80 : cellsIter != RowIter->second.end(); ++cellsIter )
681 : {
682 36 : if (listFound)
683 : {
684 10 : if (cellsIter->first >= 0xC7)
685 : {
686 10 : std::string value = getValue(cellsIter->second);
687 10 : int id = strtoul(value.c_str(), 0, 16);
688 10 : records.insert(id);
689 : }
690 : }
691 30 : else if ((cellsIter->first == 0xC1) &&
692 4 : listName == getValue( cellsIter->second ))
693 : {
694 2 : listFound = true;
695 : }
696 : }
697 :
698 : }
699 : }
700 : }
701 :
702 0 : void MorkParser::dump()
703 : {
704 0 : boost::io::ios_all_saver ias(std::cout);
705 0 : std::cout << std::hex << std::uppercase;
706 :
707 0 : std::cout << "Column Dict:\r\n";
708 0 : std::cout << "=============================================\r\n\r\n";
709 :
710 : //// columns dict
711 0 : for ( MorkDict::iterator iter = columns_.begin();
712 0 : iter != columns_.end(); ++iter )
713 : {
714 0 : std::cout << iter->first
715 0 : << " : "
716 0 : << iter->second
717 0 : << std::endl;
718 : }
719 :
720 : //// values dict
721 0 : std::cout << "\r\nValues Dict:\r\n";
722 0 : std::cout << "=============================================\r\n\r\n";
723 :
724 0 : for ( MorkDict::iterator iter = values_.begin();
725 0 : iter != values_.end(); ++iter )
726 : {
727 0 : if (iter->first >= nextAddValueId_) {
728 0 : continue;
729 : }
730 :
731 0 : std::cout << iter->first
732 0 : << " : "
733 0 : << iter->second
734 0 : << "\r\n";
735 : }
736 :
737 0 : std::cout << std::endl << "Data:" << std::endl;
738 0 : std::cout << "============================================="
739 0 : << std::endl << std::endl;
740 :
741 : //// Mork data
742 0 : for ( TableScopeMap::iterator iter = mork_.begin();
743 0 : iter != mork_.end(); ++iter )
744 : {
745 0 : std::cout << "\r\n Scope:" << iter->first << std::endl;
746 :
747 0 : for ( MorkTableMap::iterator TableIter = iter->second.begin();
748 0 : TableIter != iter->second.end(); ++TableIter )
749 : {
750 0 : std::cout << "\t Table:"
751 0 : << ( ( int ) TableIter->first < 0 ? "-" : " " )
752 0 : << TableIter->first << std::endl;
753 :
754 0 : for (RowScopeMap::iterator RowScopeIter = TableIter->second.begin();
755 0 : RowScopeIter != TableIter->second.end(); ++RowScopeIter )
756 : {
757 0 : std::cout << "\t\t RowScope:"
758 0 : << RowScopeIter->first << std::endl;
759 :
760 0 : for (MorkRowMap::iterator RowIter = RowScopeIter->second.begin();
761 0 : RowIter != RowScopeIter->second.end(); ++RowIter )
762 : {
763 0 : std::cout << "\t\t\t Row Id:"
764 0 : << ((int) RowIter->first < 0 ? "-" : " ")
765 0 : << RowIter->first << std::endl;
766 0 : std::cout << "\t\t\t\t Cells:" << std::endl;
767 :
768 0 : for (MorkCells::iterator CellsIter = RowIter->second.begin();
769 0 : CellsIter != RowIter->second.end(); ++CellsIter )
770 : {
771 : // Write ids
772 0 : std::cout << "\t\t\t\t\t"
773 0 : << CellsIter->first
774 0 : << " : "
775 0 : << CellsIter->second
776 0 : << " => ";
777 :
778 0 : MorkDict::iterator FoundIter = values_.find( CellsIter->second );
779 0 : if ( FoundIter != values_.end() )
780 : {
781 : // Write string values
782 0 : std::cout << columns_[ CellsIter->first ].c_str()
783 0 : << " : "
784 0 : << FoundIter->second.c_str()
785 0 : << std::endl;
786 : }
787 : }
788 : }
789 : }
790 : }
791 0 : }
792 6 : }
793 :
794 : /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|