Failed Conditions
Push — psr2-config ( c6639e )
by Andreas
06:39 queued 03:33
created

Doku_Renderer_metadata   D

Complexity

Total Complexity 95

Size/Duplication

Total Lines 648
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
dl 0
loc 648
rs 4.675
c 0
b 0
f 0
wmc 95
lcom 1
cbo 1

44 Methods

Rating   Name   Duplication   Size   Complexity  
A getFormat() 0 3 1
A document_start() 0 18 4
B document_end() 0 22 4
A cdata() 0 8 3
A toc_additem() 0 15 3
A header() 0 10 2
A p_open() 0 3 1
A p_close() 0 3 1
A linebreak() 0 3 1
A hr() 0 3 1
A footnote_open() 0 7 2
A footnote_close() 0 7 2
A listu_open() 0 3 1
A listo_open() 0 3 1
A listitem_open() 0 3 1
A listitem_close() 0 3 1
A preformatted() 0 3 1
A quote_open() 0 3 1
A quote_close() 0 3 1
A file() 0 3 1
A code() 0 3 1
A acronym() 0 3 1
A smiley() 0 3 1
A entity() 0 3 1
A multiplyentity() 0 3 1
A singlequoteopening() 0 4 1
A singlequoteclosing() 0 4 1
A apostrophe() 0 4 1
A doublequoteopening() 0 4 1
A doublequoteclosing() 0 4 1
A camelcaselink() 0 3 1
A locallink() 0 6 3
B internallink() 0 30 5
A externallink() 0 10 4
A interwikilink() 0 12 4
B windowssharelink() 0 11 5
B emaillink() 0 11 5
A internalmedia() 0 6 3
A externalmedia() 0 5 3
A rss() 0 8 2
A _simpleTitle() 0 15 3
B _getLinkTitle() 0 17 8
A _firstimage() 0 12 4
A _recordMediaUsage() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like Doku_Renderer_metadata often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Doku_Renderer_metadata, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * The MetaData Renderer
4
 *
5
 * Metadata is additional information about a DokuWiki page that gets extracted mainly from the page's content
6
 * but also it's own filesystem data (like the creation time). All metadata is stored in the fields $meta and
7
 * $persistent.
8
 *
9
 * Some simplified rendering to $doc is done to gather the page's (text-only) abstract.
10
 *
11
 * @author Esther Brunner <[email protected]>
12
 */
13
class Doku_Renderer_metadata extends Doku_Renderer {
14
    /** the approximate byte lenght to capture for the abstract */
15
    const ABSTRACT_LEN = 250;
16
17
    /** the maximum UTF8 character length for the abstract */
18
    const ABSTRACT_MAX = 500;
19
20
    /** @var array transient meta data, will be reset on each rendering */
21
    public $meta = array();
22
23
    /** @var array persistent meta data, will be kept until explicitly deleted */
24
    public $persistent = array();
25
26
    /** @var array the list of headers used to create unique link ids */
27
    protected $headers = array();
28
29
    /** @var string temporary $doc store */
30
    protected $store = '';
31
32
    /** @var string keeps the first image reference */
33
    protected $firstimage = '';
34
35
    /** @var bool determines if enough data for the abstract was collected, yet */
36
    public $capture = true;
37
38
    /** @var int number of bytes captured for abstract */
39
    protected $captured = 0;
40
41
    /**
42
     * Returns the format produced by this renderer.
43
     *
44
     * @return string always 'metadata'
45
     */
46
    public function getFormat() {
47
        return 'metadata';
48
    }
49
50
    /**
51
     * Initialize the document
52
     *
53
     * Sets up some of the persistent info about the page if it doesn't exist, yet.
54
     */
55
    public function document_start() {
56
        global $ID;
57
58
        $this->headers = array();
59
60
        // external pages are missing create date
61
        if(!$this->persistent['date']['created']) {
62
            $this->persistent['date']['created'] = filectime(wikiFN($ID));
63
        }
64
        if(!isset($this->persistent['user'])) {
65
            $this->persistent['user'] = '';
66
        }
67
        if(!isset($this->persistent['creator'])) {
68
            $this->persistent['creator'] = '';
69
        }
70
        // reset metadata to persistent values
71
        $this->meta = $this->persistent;
72
    }
73
74
    /**
75
     * Finalize the document
76
     *
77
     * Stores collected data in the metadata
78
     */
79
    public function document_end() {
80
        global $ID;
81
82
        // store internal info in metadata (notoc,nocache)
83
        $this->meta['internal'] = $this->info;
84
85
        if(!isset($this->meta['description']['abstract'])) {
86
            // cut off too long abstracts
87
            $this->doc = trim($this->doc);
88
            if(strlen($this->doc) > self::ABSTRACT_MAX) {
89
                $this->doc = utf8_substr($this->doc, 0, self::ABSTRACT_MAX).'…';
90
            }
91
            $this->meta['description']['abstract'] = $this->doc;
92
        }
93
94
        $this->meta['relation']['firstimage'] = $this->firstimage;
95
96
        if(!isset($this->meta['date']['modified'])) {
97
            $this->meta['date']['modified'] = filemtime(wikiFN($ID));
98
        }
99
100
    }
101
102
    /**
103
     * Render plain text data
104
     *
105
     * This function takes care of the amount captured data and will stop capturing when
106
     * enough abstract data is available
107
     *
108
     * @param $text
109
     */
110
    public function cdata($text) {
111
        if(!$this->capture) return;
112
113
        $this->doc .= $text;
114
115
        $this->captured += strlen($text);
116
        if($this->captured > self::ABSTRACT_LEN) $this->capture = false;
117
    }
118
119
    /**
120
     * Add an item to the TOC
121
     *
122
     * @param string $id       the hash link
123
     * @param string $text     the text to display
124
     * @param int    $level    the nesting level
125
     */
126
    public function toc_additem($id, $text, $level) {
127
        global $conf;
128
129
        //only add items within configured levels
130
        if($level >= $conf['toptoclevel'] && $level <= $conf['maxtoclevel']) {
131
            // the TOC is one of our standard ul list arrays ;-)
132
            $this->meta['description']['tableofcontents'][] = array(
133
                'hid'   => $id,
134
                'title' => $text,
135
                'type'  => 'ul',
136
                'level' => $level - $conf['toptoclevel'] + 1
137
            );
138
        }
139
140
    }
141
142
    /**
143
     * Render a heading
144
     *
145
     * @param string $text  the text to display
146
     * @param int    $level header level
147
     * @param int    $pos   byte position in the original source
148
     */
149
    public function header($text, $level, $pos) {
150
        if(!isset($this->meta['title'])) $this->meta['title'] = $text;
151
152
        // add the header to the TOC
153
        $hid = $this->_headerToLink($text, true);
154
        $this->toc_additem($hid, $text, $level);
155
156
        // add to summary
157
        $this->cdata(DOKU_LF.$text.DOKU_LF);
158
    }
159
160
    /**
161
     * Open a paragraph
162
     */
163
    public function p_open() {
164
        $this->cdata(DOKU_LF);
165
    }
166
167
    /**
168
     * Close a paragraph
169
     */
170
    public function p_close() {
171
        $this->cdata(DOKU_LF);
172
    }
173
174
    /**
175
     * Create a line break
176
     */
177
    public function linebreak() {
178
        $this->cdata(DOKU_LF);
179
    }
180
181
    /**
182
     * Create a horizontal line
183
     */
184
    public function hr() {
185
        $this->cdata(DOKU_LF.'----------'.DOKU_LF);
186
    }
187
188
    /**
189
     * Callback for footnote start syntax
190
     *
191
     * All following content will go to the footnote instead of
192
     * the document. To achieve this the previous rendered content
193
     * is moved to $store and $doc is cleared
194
     *
195
     * @author Andreas Gohr <[email protected]>
196
     */
197
    public function footnote_open() {
198
        if($this->capture) {
199
            // move current content to store and record footnote
200
            $this->store = $this->doc;
201
            $this->doc   = '';
202
        }
203
    }
204
205
    /**
206
     * Callback for footnote end syntax
207
     *
208
     * All rendered content is moved to the $footnotes array and the old
209
     * content is restored from $store again
210
     *
211
     * @author Andreas Gohr
212
     */
213
    public function footnote_close() {
214
        if($this->capture) {
215
            // restore old content
216
            $this->doc   = $this->store;
217
            $this->store = '';
218
        }
219
    }
220
221
    /**
222
     * Open an unordered list
223
     */
224
    public function listu_open() {
225
        $this->cdata(DOKU_LF);
226
    }
227
228
    /**
229
     * Open an ordered list
230
     */
231
    public function listo_open() {
232
        $this->cdata(DOKU_LF);
233
    }
234
235
    /**
236
     * Open a list item
237
     *
238
     * @param int $level the nesting level
239
     * @param bool $node true when a node; false when a leaf
240
     */
241
    public function listitem_open($level,$node=false) {
242
        $this->cdata(str_repeat(DOKU_TAB, $level).'* ');
243
    }
244
245
    /**
246
     * Close a list item
247
     */
248
    public function listitem_close() {
249
        $this->cdata(DOKU_LF);
250
    }
251
252
    /**
253
     * Output preformatted text
254
     *
255
     * @param string $text
256
     */
257
    public function preformatted($text) {
258
        $this->cdata($text);
259
    }
260
261
    /**
262
     * Start a block quote
263
     */
264
    public function quote_open() {
265
        $this->cdata(DOKU_LF.DOKU_TAB.'"');
266
    }
267
268
    /**
269
     * Stop a block quote
270
     */
271
    public function quote_close() {
272
        $this->cdata('"'.DOKU_LF);
273
    }
274
275
    /**
276
     * Display text as file content, optionally syntax highlighted
277
     *
278
     * @param string $text text to show
279
     * @param string $lang programming language to use for syntax highlighting
280
     * @param string $file file path label
281
     */
282
    public function file($text, $lang = null, $file = null) {
283
        $this->cdata(DOKU_LF.$text.DOKU_LF);
284
    }
285
286
    /**
287
     * Display text as code content, optionally syntax highlighted
288
     *
289
     * @param string $text     text to show
290
     * @param string $language programming language to use for syntax highlighting
291
     * @param string $file     file path label
292
     */
293
    public function code($text, $language = null, $file = null) {
294
        $this->cdata(DOKU_LF.$text.DOKU_LF);
295
    }
296
297
    /**
298
     * Format an acronym
299
     *
300
     * Uses $this->acronyms
301
     *
302
     * @param string $acronym
303
     */
304
    public function acronym($acronym) {
305
        $this->cdata($acronym);
306
    }
307
308
    /**
309
     * Format a smiley
310
     *
311
     * Uses $this->smiley
312
     *
313
     * @param string $smiley
314
     */
315
    public function smiley($smiley) {
316
        $this->cdata($smiley);
317
    }
318
319
    /**
320
     * Format an entity
321
     *
322
     * Entities are basically small text replacements
323
     *
324
     * Uses $this->entities
325
     *
326
     * @param string $entity
327
     */
328
    public function entity($entity) {
329
        $this->cdata($entity);
330
    }
331
332
    /**
333
     * Typographically format a multiply sign
334
     *
335
     * Example: ($x=640, $y=480) should result in "640×480"
336
     *
337
     * @param string|int $x first value
338
     * @param string|int $y second value
339
     */
340
    public function multiplyentity($x, $y) {
341
        $this->cdata($x.'×'.$y);
342
    }
343
344
    /**
345
     * Render an opening single quote char (language specific)
346
     */
347
    public function singlequoteopening() {
348
        global $lang;
349
        $this->cdata($lang['singlequoteopening']);
350
    }
351
352
    /**
353
     * Render a closing single quote char (language specific)
354
     */
355
    public function singlequoteclosing() {
356
        global $lang;
357
        $this->cdata($lang['singlequoteclosing']);
358
    }
359
360
    /**
361
     * Render an apostrophe char (language specific)
362
     */
363
    public function apostrophe() {
364
        global $lang;
365
        $this->cdata($lang['apostrophe']);
366
    }
367
368
    /**
369
     * Render an opening double quote char (language specific)
370
     */
371
    public function doublequoteopening() {
372
        global $lang;
373
        $this->cdata($lang['doublequoteopening']);
374
    }
375
376
    /**
377
     * Render an closinging double quote char (language specific)
378
     */
379
    public function doublequoteclosing() {
380
        global $lang;
381
        $this->cdata($lang['doublequoteclosing']);
382
    }
383
384
    /**
385
     * Render a CamelCase link
386
     *
387
     * @param string $link The link name
388
     * @see http://en.wikipedia.org/wiki/CamelCase
389
     */
390
    public function camelcaselink($link) {
391
        $this->internallink($link, $link);
392
    }
393
394
    /**
395
     * Render a page local link
396
     *
397
     * @param string $hash hash link identifier
398
     * @param string $name name for the link
399
     */
400
    public function locallink($hash, $name = null) {
401
        if(is_array($name)) {
402
            $this->_firstimage($name['src']);
403
            if($name['type'] == 'internalmedia') $this->_recordMediaUsage($name['src']);
404
        }
405
    }
406
407
    /**
408
     * keep track of internal links in $this->meta['relation']['references']
409
     *
410
     * @param string            $id   page ID to link to. eg. 'wiki:syntax'
411
     * @param string|array|null $name name for the link, array for media file
412
     */
413
    public function internallink($id, $name = null) {
414
        global $ID;
415
416
        if(is_array($name)) {
417
            $this->_firstimage($name['src']);
418
            if($name['type'] == 'internalmedia') $this->_recordMediaUsage($name['src']);
419
        }
420
421
        $parts = explode('?', $id, 2);
422
        if(count($parts) === 2) {
423
            $id = $parts[0];
424
        }
425
426
        $default = $this->_simpleTitle($id);
427
428
        // first resolve and clean up the $id
429
        resolve_pageid(getNS($ID), $id, $exists);
0 ignored issues
show
Security Bug introduced by
It seems like getNS($ID) targeting getNS() can also be of type false; however, resolve_pageid() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
430
        @list($page) = explode('#', $id, 2);
1 ignored issue
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
431
432
        // set metadata
433
        $this->meta['relation']['references'][$page] = $exists;
434
        // $data = array('relation' => array('isreferencedby' => array($ID => true)));
435
        // p_set_metadata($id, $data);
436
437
        // add link title to summary
438
        if($this->capture) {
439
            $name = $this->_getLinkTitle($name, $default, $id);
440
            $this->doc .= $name;
441
        }
442
    }
443
444
    /**
445
     * Render an external link
446
     *
447
     * @param string            $url  full URL with scheme
448
     * @param string|array|null $name name for the link, array for media file
449
     */
450
    public function externallink($url, $name = null) {
451
        if(is_array($name)) {
452
            $this->_firstimage($name['src']);
453
            if($name['type'] == 'internalmedia') $this->_recordMediaUsage($name['src']);
454
        }
455
456
        if($this->capture) {
457
            $this->doc .= $this->_getLinkTitle($name, '<'.$url.'>');
458
        }
459
    }
460
461
    /**
462
     * Render an interwiki link
463
     *
464
     * You may want to use $this->_resolveInterWiki() here
465
     *
466
     * @param string       $match     original link - probably not much use
467
     * @param string|array $name      name for the link, array for media file
468
     * @param string       $wikiName  indentifier (shortcut) for the remote wiki
469
     * @param string       $wikiUri   the fragment parsed from the original link
470
     */
471
    public function interwikilink($match, $name, $wikiName, $wikiUri) {
472
        if(is_array($name)) {
473
            $this->_firstimage($name['src']);
474
            if($name['type'] == 'internalmedia') $this->_recordMediaUsage($name['src']);
475
        }
476
477
        if($this->capture) {
478
            list($wikiUri) = explode('#', $wikiUri, 2);
479
            $name = $this->_getLinkTitle($name, $wikiUri);
480
            $this->doc .= $name;
481
        }
482
    }
483
484
    /**
485
     * Link to windows share
486
     *
487
     * @param string       $url  the link
488
     * @param string|array $name name for the link, array for media file
489
     */
490
    public function windowssharelink($url, $name = null) {
491
        if(is_array($name)) {
492
            $this->_firstimage($name['src']);
493
            if($name['type'] == 'internalmedia') $this->_recordMediaUsage($name['src']);
494
        }
495
496
        if($this->capture) {
497
            if($name) $this->doc .= $name;
498
            else $this->doc .= '<'.$url.'>';
499
        }
500
    }
501
502
    /**
503
     * Render a linked E-Mail Address
504
     *
505
     * Should honor $conf['mailguard'] setting
506
     *
507
     * @param string       $address Email-Address
508
     * @param string|array $name    name for the link, array for media file
509
     */
510
    public function emaillink($address, $name = null) {
511
        if(is_array($name)) {
512
            $this->_firstimage($name['src']);
513
            if($name['type'] == 'internalmedia') $this->_recordMediaUsage($name['src']);
514
        }
515
516
        if($this->capture) {
517
            if($name) $this->doc .= $name;
518
            else $this->doc .= '<'.$address.'>';
519
        }
520
    }
521
522
    /**
523
     * Render an internal media file
524
     *
525
     * @param string $src     media ID
526
     * @param string $title   descriptive text
527
     * @param string $align   left|center|right
528
     * @param int    $width   width of media in pixel
529
     * @param int    $height  height of media in pixel
530
     * @param string $cache   cache|recache|nocache
531
     * @param string $linking linkonly|detail|nolink
532
     */
533
    public function internalmedia($src, $title = null, $align = null, $width = null,
534
                           $height = null, $cache = null, $linking = null) {
535
        if($this->capture && $title) $this->doc .= '['.$title.']';
0 ignored issues
show
Bug Best Practice introduced by
The expression $title of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
536
        $this->_firstimage($src);
537
        $this->_recordMediaUsage($src);
538
    }
539
540
    /**
541
     * Render an external media file
542
     *
543
     * @param string $src     full media URL
544
     * @param string $title   descriptive text
545
     * @param string $align   left|center|right
546
     * @param int    $width   width of media in pixel
547
     * @param int    $height  height of media in pixel
548
     * @param string $cache   cache|recache|nocache
549
     * @param string $linking linkonly|detail|nolink
550
     */
551
    public function externalmedia($src, $title = null, $align = null, $width = null,
552
                           $height = null, $cache = null, $linking = null) {
553
        if($this->capture && $title) $this->doc .= '['.$title.']';
0 ignored issues
show
Bug Best Practice introduced by
The expression $title of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
554
        $this->_firstimage($src);
555
    }
556
557
    /**
558
     * Render the output of an RSS feed
559
     *
560
     * @param string $url    URL of the feed
561
     * @param array  $params Finetuning of the output
562
     */
563
    public function rss($url, $params) {
564
        $this->meta['relation']['haspart'][$url] = true;
565
566
        $this->meta['date']['valid']['age'] =
567
            isset($this->meta['date']['valid']['age']) ?
568
                min($this->meta['date']['valid']['age'], $params['refresh']) :
569
                $params['refresh'];
570
    }
571
572
    #region Utils
573
574
    /**
575
     * Removes any Namespace from the given name but keeps
576
     * casing and special chars
577
     *
578
     * @author Andreas Gohr <[email protected]>
579
     *
580
     * @param string $name
581
     *
582
     * @return mixed|string
583
     */
584
    public function _simpleTitle($name) {
585
        global $conf;
586
587
        if(is_array($name)) return '';
588
589
        if($conf['useslash']) {
590
            $nssep = '[:;/]';
591
        } else {
592
            $nssep = '[:;]';
593
        }
594
        $name = preg_replace('!.*'.$nssep.'!', '', $name);
595
        //if there is a hash we use the anchor name only
596
        $name = preg_replace('!.*#!', '', $name);
597
        return $name;
598
    }
599
600
    /**
601
     * Construct a title and handle images in titles
602
     *
603
     * @author Harry Fuecks <[email protected]>
604
     * @param string|array|null $title    either string title or media array
605
     * @param string            $default  default title if nothing else is found
606
     * @param null|string       $id       linked page id (used to extract title from first heading)
607
     * @return string title text
608
     */
609
    public function _getLinkTitle($title, $default, $id = null) {
610
        if(is_array($title)) {
611
            if($title['title']) {
612
                return '['.$title['title'].']';
613
            } else {
614
                return $default;
615
            }
616
        } else if(is_null($title) || trim($title) == '') {
617
            if(useHeading('content') && $id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
618
                $heading = p_get_first_heading($id, METADATA_DONT_RENDER);
619
                if($heading) return $heading;
0 ignored issues
show
Bug Best Practice introduced by
The expression $heading of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
620
            }
621
            return $default;
622
        } else {
623
            return $title;
624
        }
625
    }
626
627
    /**
628
     * Remember first image
629
     *
630
     * @param string $src image URL or ID
631
     */
632
    protected function _firstimage($src) {
633
        if($this->firstimage) return;
634
        global $ID;
635
636
        list($src) = explode('#', $src, 2);
637
        if(!media_isexternal($src)) {
638
            resolve_mediaid(getNS($ID), $src, $exists);
0 ignored issues
show
Security Bug introduced by
It seems like getNS($ID) targeting getNS() can also be of type false; however, resolve_mediaid() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
639
        }
640
        if(preg_match('/.(jpe?g|gif|png)$/i', $src)) {
641
            $this->firstimage = $src;
642
        }
643
    }
644
645
    /**
646
     * Store list of used media files in metadata
647
     *
648
     * @param string $src media ID
649
     */
650
    protected function _recordMediaUsage($src) {
651
        global $ID;
652
653
        list ($src) = explode('#', $src, 2);
654
        if(media_isexternal($src)) return;
655
        resolve_mediaid(getNS($ID), $src, $exists);
0 ignored issues
show
Security Bug introduced by
It seems like getNS($ID) targeting getNS() can also be of type false; however, resolve_mediaid() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
656
        $this->meta['relation']['media'][$src] = $exists;
657
    }
658
659
    #endregion
660
}
661
662
//Setup VIM: ex: et ts=4 :
663