GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — develop ( b62a26...606a76 )
by Lonnie
9s
created
myth/Docs/Builder.php 1 patch
Indentation   +718 added lines, -718 removed lines patch added patch discarded remove patch
@@ -42,725 +42,725 @@
 block discarded – undo
42 42
 class Builder implements DocBuilderInterface
43 43
 {
44 44
 
45
-    protected $docs_ext = '.md';
46
-
47
-    protected $ignore_files = ['_404.md'];
48
-
49
-    protected $doc_folders = [];
50
-
51
-    /**
52
-     * Stores the current folder alias,
53
-     * once the file has been found.
54
-     *
55
-     * @var null
56
-     */
57
-    protected $current_folder = null;
58
-
59
-    protected $table_classes = 'table table-hover';
60
-
61
-    protected $apppath = '';
62
-
63
-    protected $formatters = [];
64
-
65
-    protected $page_title = null;
66
-
67
-    //--------------------------------------------------------------------
68
-
69
-    public function __construct($config = array())
70
-    {
71
-        $this->apppath = ! empty($config['apppath']) ? rtrim($config['apppath'], '/') . '/' : '';
72
-    }
73
-
74
-    //--------------------------------------------------------------------
75
-
76
-    public function pageTitle()
77
-    {
78
-        return $this->page_title;
79
-    }
80
-
81
-    //--------------------------------------------------------------------
82
-
83
-
84
-
85
-    /**
86
-     * Does the actual work of reading in and parsing the help file.
87
-     * If a folder Nickname (see addDocFolder() ) is passed as the second parameter,
88
-     * it will limit it's search to that single folder. If nothing is passed, it will
89
-     * search through all of the folders in the order they were given to the library,
90
-     * until it finds the first one.
91
-     *
92
-     * @param string $path The 'path' of the file (relative to the docs
93
-     *                                 folder. Usually from the URI)
94
-     * @param string $restrictToFolder (Optional) The folder nickname
95
-     *
96
-     * @return string
97
-     */
98
-    public function readPage($path, $restrictToFolder = null)
99
-    {
100
-        // Clean up our path
101
-        $path = trim($path, '/ ');
102
-
103
-        $content = $this->locateAndReadFile($path, $restrictToFolder);
104
-
105
-        $content = $this->parse($content);
106
-
107
-        return $content;
108
-    }
109
-
110
-    //--------------------------------------------------------------------
111
-
112
-    /**
113
-     * Parses the contents. Currently runs through the Markdown Extended
114
-     * parser to convert to HTML.
115
-     *
116
-     * @param $str
117
-     * @return mixed
118
-     */
119
-    public function parse($str)
120
-    {
121
-        return $this->format($str);
122
-    }
123
-
124
-    //--------------------------------------------------------------------
125
-
126
-    /**
127
-     * Perform a few housekeeping tasks on a page, like rewriting URLs to full
128
-     * URLs, not relative, ensuring they link correctly, etc.
129
-     *
130
-     * @param      $content
131
-     * @param null $site_url
132
-     * @param null $current_url
133
-     * @return string   The post-processed HTML.
134
-     */
135
-    public function postProcess($content, $site_url = null, $current_url = null)
136
-    {
137
-        if (empty($content)) {
138
-            return $content;
139
-        }
140
-
141
-        try {
142
-            $xml = new \SimpleXMLElement('<?xml version="1.0" standalone="yes"?><div>' . $content . '</div>');
143
-        } catch (\Exception $e) {
144
-            // SimpleXML barfed on us, so send back the un-modified content
145
-            return $content;
146
-        }
147
-
148
-        // Prepare some things and cleanup others
149
-        $groups = array_keys($this->doc_folders);
150
-        $site_url = rtrim($site_url, '/') . '/';
151
-        $current_url = rtrim($current_url, '#/');
152
-
153
-        // Try to determine the current_url if one isn't set.
154
-        if (empty($this->current_folder)) {
155
-            $this->current_folder = $this->detectCurrentFolder($current_url, $groups);
156
-        }
157
-
158
-        /*
45
+	protected $docs_ext = '.md';
46
+
47
+	protected $ignore_files = ['_404.md'];
48
+
49
+	protected $doc_folders = [];
50
+
51
+	/**
52
+	 * Stores the current folder alias,
53
+	 * once the file has been found.
54
+	 *
55
+	 * @var null
56
+	 */
57
+	protected $current_folder = null;
58
+
59
+	protected $table_classes = 'table table-hover';
60
+
61
+	protected $apppath = '';
62
+
63
+	protected $formatters = [];
64
+
65
+	protected $page_title = null;
66
+
67
+	//--------------------------------------------------------------------
68
+
69
+	public function __construct($config = array())
70
+	{
71
+		$this->apppath = ! empty($config['apppath']) ? rtrim($config['apppath'], '/') . '/' : '';
72
+	}
73
+
74
+	//--------------------------------------------------------------------
75
+
76
+	public function pageTitle()
77
+	{
78
+		return $this->page_title;
79
+	}
80
+
81
+	//--------------------------------------------------------------------
82
+
83
+
84
+
85
+	/**
86
+	 * Does the actual work of reading in and parsing the help file.
87
+	 * If a folder Nickname (see addDocFolder() ) is passed as the second parameter,
88
+	 * it will limit it's search to that single folder. If nothing is passed, it will
89
+	 * search through all of the folders in the order they were given to the library,
90
+	 * until it finds the first one.
91
+	 *
92
+	 * @param string $path The 'path' of the file (relative to the docs
93
+	 *                                 folder. Usually from the URI)
94
+	 * @param string $restrictToFolder (Optional) The folder nickname
95
+	 *
96
+	 * @return string
97
+	 */
98
+	public function readPage($path, $restrictToFolder = null)
99
+	{
100
+		// Clean up our path
101
+		$path = trim($path, '/ ');
102
+
103
+		$content = $this->locateAndReadFile($path, $restrictToFolder);
104
+
105
+		$content = $this->parse($content);
106
+
107
+		return $content;
108
+	}
109
+
110
+	//--------------------------------------------------------------------
111
+
112
+	/**
113
+	 * Parses the contents. Currently runs through the Markdown Extended
114
+	 * parser to convert to HTML.
115
+	 *
116
+	 * @param $str
117
+	 * @return mixed
118
+	 */
119
+	public function parse($str)
120
+	{
121
+		return $this->format($str);
122
+	}
123
+
124
+	//--------------------------------------------------------------------
125
+
126
+	/**
127
+	 * Perform a few housekeeping tasks on a page, like rewriting URLs to full
128
+	 * URLs, not relative, ensuring they link correctly, etc.
129
+	 *
130
+	 * @param      $content
131
+	 * @param null $site_url
132
+	 * @param null $current_url
133
+	 * @return string   The post-processed HTML.
134
+	 */
135
+	public function postProcess($content, $site_url = null, $current_url = null)
136
+	{
137
+		if (empty($content)) {
138
+			return $content;
139
+		}
140
+
141
+		try {
142
+			$xml = new \SimpleXMLElement('<?xml version="1.0" standalone="yes"?><div>' . $content . '</div>');
143
+		} catch (\Exception $e) {
144
+			// SimpleXML barfed on us, so send back the un-modified content
145
+			return $content;
146
+		}
147
+
148
+		// Prepare some things and cleanup others
149
+		$groups = array_keys($this->doc_folders);
150
+		$site_url = rtrim($site_url, '/') . '/';
151
+		$current_url = rtrim($current_url, '#/');
152
+
153
+		// Try to determine the current_url if one isn't set.
154
+		if (empty($this->current_folder)) {
155
+			$this->current_folder = $this->detectCurrentFolder($current_url, $groups);
156
+		}
157
+
158
+		/*
159 159
          * Rewrite the URLs
160 160
          */
161
-        foreach ($xml->xpath('//a') as $link) {
162
-            $link = $this->reformatAnchor($link, $groups, $current_url, $site_url);
163
-        }
164
-
165
-        $content = $xml->asXML();
166
-        $content = trim(str_replace('<?xml version="1.0" standalone="yes"?>', '', $content));
167
-
168
-        // Clean up and style the tables
169
-        $content = str_replace('<table>', '<table class="' . $this->table_classes . '">', $content);
170
-
171
-        return $content;
172
-    }
173
-    //--------------------------------------------------------------------
174
-
175
-    /**
176
-     * Allows users to define the classes that are attached to
177
-     * generated tables.
178
-     *
179
-     * @param null $classes
180
-     * @return $this
181
-     */
182
-    public function setTableClasses($classes = null)
183
-    {
184
-        $this->table_classes = $classes;
185
-
186
-        return $this;
187
-    }
188
-
189
-    //--------------------------------------------------------------------
190
-
191
-    /**
192
-     * Given the contents to render, will build a list of links for the sidebar
193
-     * out of the headings in the file.
194
-     *
195
-     * Note: Will ONLY use h2 and h3 to build the links from.
196
-     *
197
-     * Note: The $content passed in WILL be modified by adding named anchors
198
-     * that match up with the locations.
199
-     *
200
-     * @param string $content The HTML to analyse for headings.
201
-     * @return string
202
-     */
203
-    public function buildDocumentMap(&$content)
204
-    {
205
-        if (empty($content)) {
206
-            return $content;
207
-        }
208
-
209
-        // If $content already has a wrapping <div> and </div> tags, remove them,
210
-        // since we'll replace them just below.
211
-        if (strpos($content, '<div>') === 0) {
212
-            $content = substr($content, 5);
213
-
214
-            // Trailing div also?
215
-            if (substr($content, -6) == '</div>') {
216
-                $content = substr($content, 0, -6);
217
-            }
218
-        }
219
-
220
-        try {
221
-            $xml = new \SimpleXMLElement('<?xml version="1.0" standalone="yes"?><div>' . $content . '</div>');
222
-        } catch (\Exception $e) {
223
-            // SimpleXML barfed on us, so send back the un-modified content
224
-            return [];
225
-        }
226
-
227
-        $map = [];
228
-        list($map, $content) = $this->extractDocMapAndAddAnchors($content, $xml, $map);
229
-
230
-        return $map;
231
-    }
232
-
233
-    //--------------------------------------------------------------------
234
-
235
-    /**
236
-     * Stores the name of the callback method to run to convert the source
237
-     * files to viewable files. By default, this should be used to register
238
-     * a Mardown Extended formatter with the system, but could be used to
239
-     * extend the
240
-     *
241
-     * @param string $callback
242
-     * @param bool $cascade // If FALSE the formatting of a component ends here. If TRUE, will be passed to next formatter.
243
-     * @return $this
244
-     */
245
-    public function registerFormatter($callback = null, $cascade = false)
246
-    {
247
-        if (empty($callback)) return;
248
-
249
-        $this->formatters[] = [
250
-            'callable' => $callback,
251
-            'cascade'  => (bool)$cascade
252
-        ];
253
-
254
-        return $this;
255
-    }
256
-
257
-    //--------------------------------------------------------------------
258
-
259
-    /**
260
-     * Runs the text through the registered formatters.
261
-     *
262
-     * @param $str
263
-     * @return mixed
264
-     */
265
-    public function format($str)
266
-    {
267
-        if (! is_array($this->formatters)) return $str;
268
-
269
-        foreach ($this->formatters as $formatter) {
270
-            $method = $formatter['callable'];
271
-            $cascade = $formatter['cascade'];
272
-
273
-            $str = call_user_func($method, $str);
274
-
275
-            if (! $cascade) return $str;
276
-        }
277
-
278
-        return $str;
279
-    }
280
-
281
-    //--------------------------------------------------------------------
282
-
283
-    //--------------------------------------------------------------------
284
-    // Table of Contents methods
285
-    //--------------------------------------------------------------------
286
-
287
-    /**
288
-     * Retrieves the list of files in a folder and preps the name and filename
289
-     * so it's ready for creating the HTML.
290
-     *
291
-     * @param  String $folder The path to the folder to retrieve.
292
-     *
293
-     * @return Array  An associative array @see parse_ini_file for format
294
-     * details.
295
-     */
296
-    public function buildTOC($folder)
297
-    {
298
-        // If the toc file exists in the folder, use it to build the links.
299
-        if (is_file("{$folder}/_toc.ini")) {
300
-            $toc = parse_ini_file("{$folder}/_toc.ini", true);
301
-            return $this->columnizeTOC($toc);
302
-        }
303
-
304
-        // If the toc file does not exist, build the links by listing the files
305
-        // in the directory (and any sub-directories)
306
-        $map = $this->directory_map($folder);
307
-
308
-        // If directory_map can not open the directory or find any files inside
309
-        // the directory, return an empty array.
310
-        if (empty($map)) {
311
-            return [];
312
-        }
313
-
314
-        // If these docs are located in the /application/docs or /bonfire/docs
315
-        // directory, just use $this->current_group for the root.
316
-        // Module docs need $this->current_group and $type.
317
-        $tocRoot = $this->current_folder;
318
-        if ($this->current_folder != strtolower($folder)) {
319
-            $tocRoot .= '/' . strtolower($folder);
320
-        }
321
-
322
-        $toc = [];
323
-        foreach ($map as $files) {
324
-            // If $files isn't an array, then make it one so that all situations
325
-            // may be dealt with cleanly.
326
-            if (! is_array($files)) {
327
-                $files = [$files];
328
-            }
329
-
330
-            foreach ($files as $file) {
331
-                if (in_array($file, $this->ignore_files)) {
332
-                    continue;
333
-                }
334
-
335
-                // The title for the index is the passed $type. Otherwise,
336
-                // build the title from the file's name.
337
-                if (strpos($file, 'index') === false) {
338
-                    $title = str_replace($this->docs_ext, '', $file);
339
-                    $title = str_replace('_', ' ', $title);
340
-                    $title = ucwords($title);
341
-
342
-                    $toc["{$tocRoot}/{$file}"] = $title;
343
-                } else {
344
-                    $toc[$tocRoot] = $type;
345
-                }
346
-            }
347
-        }
348
-
349
-        $toc = $this->columnizeTOC($toc);
350
-
351
-        return $toc;
352
-    }
353
-
354
-    //--------------------------------------------------------------------
355
-
356
-    /**
357
-     * Sorts the passed TOC array into columns of as close to equal length
358
-     * as we can get it.
359
-     *
360
-     * @param $toc
361
-     * @return array
362
-     */
363
-    protected function columnizeTOC($toc)
364
-    {
365
-        $section_count = count($toc);
366
-
367
-        // First - determine the size of each 'section'.
368
-        $sizes = [];
369
-
370
-        foreach ($toc as $section => $chapters) {
371
-            $sizes[] = count($chapters);
372
-        }
373
-
374
-        $column_avg = (int)round(array_sum($sizes) / $section_count);
375
-
376
-        // Split things into 4 columns of approximately equal size.
377
-        // If we only have 4 columns (or less), then make sure to
378
-        // deal with that also.
379
-        $columns = [];
380
-
381
-        $current_column = 0;
382
-        $current_column_count = 0;
383
-        $keys = array_keys($toc);
384
-
385
-        for ($i = 0; $i <= $section_count; $i++) {
386
-            if (! isset($keys[$i])) {
387
-                continue;
388
-            }
389
-
390
-            $section = array_shift($toc);
391
-
392
-            // Can we stay in this column?
393
-            if ($current_column_count <= $column_avg && $section_count > 4) {
394
-                // Don't forget to account for the heading also.
395
-                $current_column_count += count($section) + 1;
396
-            } else {
397
-                $current_column_count = 0;
398
-                $current_column++;
399
-            }
400
-
401
-            $columns[$current_column][$keys[$i]] = $section;
402
-        }
403
-
404
-        return $columns;
405
-    }
406
-
407
-    //--------------------------------------------------------------------
408
-
409
-    //--------------------------------------------------------------------
410
-    // Folder Methods
411
-    //--------------------------------------------------------------------
412
-
413
-    /**
414
-     * Returns the current docFolders array.
415
-     *
416
-     * @return array
417
-     */
418
-    public function docFolders()
419
-    {
420
-        return $this->doc_folders;
421
-    }
422
-
423
-    //--------------------------------------------------------------------
424
-
425
-    /**
426
-     * Registers a path to be used when searching for documentation files.
427
-     *
428
-     * @param $name     A nickname to reference it by later.
429
-     * @param $path     The server path to the folder.
430
-     * @return $this
431
-     */
432
-    public function addDocFolder($name, $path)
433
-    {
434
-        // Standardize the path
435
-        $path = realpath($path) . '/';
436
-
437
-        // realpath will return FALSE if the path doesn't exist
438
-        // or the script doesn't have access to it.
439
-        if (! $path || $path == '/') {
440
-            return $this;
441
-        }
442
-
443
-        $name = strtolower($name);
444
-
445
-        $this->doc_folders[$name] = $path;
446
-
447
-        return $this;
448
-    }
449
-
450
-    //--------------------------------------------------------------------
451
-
452
-    /**
453
-     * Removes a folder from the folders we scan for documentation files
454
-     * within.
455
-     *
456
-     * @param $name
457
-     * @return $this
458
-     */
459
-    public function removeDocFolder($name)
460
-    {
461
-        $name = strtolower($name);
462
-
463
-        if (isset($this->doc_folders[$name])) {
464
-            unset($this->doc_folders[$name]);
465
-        }
466
-
467
-        return $this;
468
-    }
469
-
470
-    //--------------------------------------------------------------------
471
-
472
-    //--------------------------------------------------------------------
473
-    // Private Methods
474
-    //--------------------------------------------------------------------
475
-
476
-    /**
477
-     * Analyzes the passed in current url string and checks against
478
-     * a list of groups to determine what the current group is.
479
-     *
480
-     * @param $current_url
481
-     * @param $groups
482
-     * @return string
483
-     */
484
-    protected function detectCurrentFolder($current_url, $groups = [])
485
-    {
486
-        if (! is_array($groups)) {
487
-            return null;
488
-        }
489
-
490
-        $segments = explode('/', $current_url);
491
-
492
-        // We start from the back of the array since
493
-        // that's most likely to be close to the end.
494
-        $segments = array_reverse($segments);
495
-
496
-        foreach ($segments as $segment) {
497
-            foreach ($groups as $group) {
498
-                if (strtolower($group) == strtolower($segment)) {
499
-                    return $group;
500
-                }
501
-            }
502
-        }
503
-
504
-        // Nothing found?
505
-        return null;
506
-    }
507
-
508
-    //--------------------------------------------------------------------
509
-
510
-    //--------------------------------------------------------------------
511
-    // Private Methods
512
-    //--------------------------------------------------------------------
513
-
514
-    /**
515
-     * Locates the file on disk and reads the contents into a single string.
516
-     *
517
-     * If a folder Nickname (see addDocFolder() ) is passed as the second parameter,
518
-     * it will limit it's search to that single folder. If nothing is passed, it will
519
-     * search through all of the folders in the order they were given to the library,
520
-     * until it finds the first one.
521
-     *
522
-     * @param string $path The 'path' of the file (relative to the docs
523
-     *                                 folder. Usually from the URI)
524
-     * @param string $restrictToFolder (Optional) The nickname of one of the
525
-     *                                 folders to restrict the search to.
526
-     *
527
-     * @throws RuntimeException
528
-     * @return null|string
529
-     */
530
-    private function locateAndReadFile($path, $restrictToFolder = null)
531
-    {
532
-        $folders = $this->doc_folders;
533
-
534
-        if (! is_null($restrictToFolder)) {
535
-            // Make sure the folder exists
536
-            if (! is_null($restrictToFolder) && ! isset($this->doc_folders[$restrictToFolder])) {
537
-                throw new \RuntimeException('You must add the docs folder that you wish to find docs from.');
538
-            }
539
-
540
-            $folders = [$this->doc_folders[$restrictToFolder]];
541
-        }
542
-
543
-        foreach ($folders as $alias => $folder) {
544
-            if (file_exists($folder . $path . $this->docs_ext)) {
545
-                // Store the alias so we know which folder we're in.
546
-                $this->current_folder = $alias;
547
-
548
-                return file_get_contents($folder . $path . $this->docs_ext);
549
-            }
550
-        }
551
-
552
-        return null;
553
-    }
554
-
555
-    //--------------------------------------------------------------------
556
-
557
-    /**
558
-     * Re-formats the passed in link.
559
-     *
560
-     * @param $link
561
-     * @param $current_url
562
-     * @param $site_url
563
-     * @return mixed
564
-     */
565
-    private function reformatAnchor($link, $groups, $current_url, $site_url)
566
-    {
567
-        // Grab the href value.
568
-        $href = $link->attributes()->href;
569
-
570
-        // If the href is null, it's probably a named anchor with no content.
571
-        if (! $href) {
572
-            // Make sure it has an href, else the XML will not close this
573
-            // tag correctly.
574
-            $link['href'] = ' ';
575
-
576
-            return $link;
577
-        }
578
-
579
-        // Remove any trailing # signs
580
-        $href = rtrim($href, '# ');
581
-
582
-        // If the href starts with #, then attach the current_url to it
583
-        if ($href != '' && substr_compare($href, '#', 0, 1) === 0) {
584
-            $link['href'] = $current_url . $href;
585
-
586
-            return $link;
587
-        }
588
-
589
-        // If it's a full external path, go on...
590
-        if ((strpos($href, 'http://') !== false || strpos($href, 'https://') !== false) &&
591
-            strpos($href, $site_url) === false
592
-        ) {
593
-            $link['target'] = "_blank";
594
-            return $link;
595
-        }
596
-
597
-        // If it's a full local path, get rid of it.
598
-        if ($site_url !== "/" && strpos($href, $site_url) !== false) {
599
-            $href = str_replace($site_url, '', $href);
600
-        }
601
-
602
-        // Strip out some unnecessary items, just in case they're there.
603
-        if (substr($href, 0, strlen('docs/')) == 'docs/') {
604
-            $href = substr($href, strlen('docs/'));
605
-        }
606
-
607
-        // This includes 'bonfire/' if it was missed during the conversion.
608
-        if (substr($href, 0, strlen('bonfire/')) == 'bonfire/') {
609
-            $href = substr($href, strlen('bonfire/'));
610
-        }
611
-
612
-        // If another 'group' is not already defined at the head of the link
613
-        // then add the current group to it.
614
-        $group_found = false;
615
-
616
-        foreach ($groups as $group) {
617
-            if (strpos($href, $group) === 0) {
618
-                $group_found = true;
619
-            }
620
-        }
621
-
622
-        if (! $group_found) {
623
-            $href = $this->current_folder . '/' . $href;
624
-        }
625
-
626
-        // Convert to full site_url
627
-        if (strpos($href, 'http') !== 0) {
628
-            $href = $site_url . 'docs/' . ltrim($href, '/ ');
629
-        }
630
-
631
-        // Save the corrected href
632
-        $link['href'] = $href;
633
-
634
-        return $link;
635
-    }
636
-
637
-    //--------------------------------------------------------------------
638
-
639
-    /**
640
-     * Creates a Document Map based on <h2> and <h3> tags.
641
-     * Also adds named anchors into the $content so the map
642
-     * can link to the content properly.
643
-     *
644
-     * @param $content
645
-     * @param $xml
646
-     * @param $map
647
-     * @return array
648
-     */
649
-    protected function extractDocMapAndAddAnchors(&$content, $xml, $map)
650
-    {
651
-        // Holds the current h2 we're processing
652
-        $current_obj = [];
653
-
654
-        $currentChild = 0;
655
-
656
-        foreach ($xml->children() as $childType => $line) {
657
-            $currentChild++;
658
-
659
-            // If it's an h1 - take the first and make it
660
-            // our page title.
661
-            if ($childType == 'h1' && empty($this->page_title))
662
-            {
663
-                $this->page_title = (string)$line;
664
-            }
665
-
666
-            // Make sure that our current object is
667
-            // stored and reset.
668
-            if ($childType == 'h1' || $childType == 'h2') {
669
-                if (count($current_obj)) {
670
-                    $map[] = $current_obj;
671
-                    $current_obj = [];
672
-                }
673
-            }
674
-
675
-            if ($childType == 'h2') {
676
-                $name = (string)$line;
677
-                $link = strtolower(str_replace(' ', '_', (string)$line));
678
-
679
-                $current_obj['name'] = $name;
680
-                $current_obj['link'] = '#' . $link;
681
-                $current_obj['items'] = [];
682
-
683
-                // Insert a named anchor into the $content
684
-                $anchor = '<a name="' . $link . '" id="' . $link . '" ></a>';
685
-
686
-                $search = "<h2>{$name}</h2>";
687
-
688
-                $content = str_replace($search, $anchor . $search, $content);
689
-            } elseif ($childType == 'h3') {
690
-                // Make sure we have some place to store the items.
691
-                if (! isset($current_obj['items'])) {
692
-                    $current_obj['items'] = [];
693
-                }
694
-
695
-                $link = strtolower(str_replace(' ', '_', (string)$line));
696
-                $name = (string)$line;
697
-
698
-                $current_obj['items'][] = [
699
-                    'name' => $name,
700
-                    'link' => '#' . $link
701
-                ];
702
-
703
-                // Insert a named anchor into the $content
704
-                $anchor = '<a name="' . $link . '" id="' . $link . '" ></a>';
705
-
706
-                $search = "<h3>{$name}</h3>";
707
-
708
-                $content = str_replace($search, $anchor . $search, $content);
709
-            }
710
-
711
-            // Is this the last element? Then close out our current object.
712
-            if (count($xml) == $currentChild) {
713
-                if (count($current_obj)) {
714
-                    $map[] = $current_obj;
715
-                }
716
-            }
717
-        }
718
-        return [$map, $content];
719
-    }
720
-    //--------------------------------------------------------------------
721
-
722
-    /**
723
-     * Create a Directory Map
724
-     *
725
-     * Reads the specified directory and builds an array
726
-     * representation of it. Sub-folders contained with the
727
-     * directory will be mapped as well.
728
-     *
729
-     * @param    string $source_dir Path to source
730
-     * @param    int $directory_depth Depth of directories to traverse
731
-     *                        (0 = fully recursive, 1 = current dir, etc)
732
-     * @param    bool $hidden Whether to show hidden files
733
-     * @return    array
734
-     */
735
-    protected function directory_map($source_dir, $directory_depth = 0, $hidden = FALSE)
736
-    {
737
-        if ($fp = @opendir($source_dir)) {
738
-            $filedata = array();
739
-            $new_depth = $directory_depth - 1;
740
-            $source_dir = rtrim($source_dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
741
-
742
-            while (FALSE !== ($file = readdir($fp))) {
743
-                // Remove '.', '..', and hidden files [optional]
744
-                if ($file === '.' OR $file === '..' OR ($hidden === FALSE && $file[0] === '.')) {
745
-                    continue;
746
-                }
747
-
748
-                is_dir($source_dir . $file) && $file .= DIRECTORY_SEPARATOR;
749
-
750
-                if (($directory_depth < 1 OR $new_depth > 0) && is_dir($source_dir . $file)) {
751
-                    $filedata[$file] = directory_map($source_dir . $file, $new_depth, $hidden);
752
-                } else {
753
-                    $filedata[] = $file;
754
-                }
755
-            }
756
-
757
-            closedir($fp);
758
-            return $filedata;
759
-        }
760
-
761
-        return FALSE;
762
-    }
763
-
764
-    //--------------------------------------------------------------------
161
+		foreach ($xml->xpath('//a') as $link) {
162
+			$link = $this->reformatAnchor($link, $groups, $current_url, $site_url);
163
+		}
164
+
165
+		$content = $xml->asXML();
166
+		$content = trim(str_replace('<?xml version="1.0" standalone="yes"?>', '', $content));
167
+
168
+		// Clean up and style the tables
169
+		$content = str_replace('<table>', '<table class="' . $this->table_classes . '">', $content);
170
+
171
+		return $content;
172
+	}
173
+	//--------------------------------------------------------------------
174
+
175
+	/**
176
+	 * Allows users to define the classes that are attached to
177
+	 * generated tables.
178
+	 *
179
+	 * @param null $classes
180
+	 * @return $this
181
+	 */
182
+	public function setTableClasses($classes = null)
183
+	{
184
+		$this->table_classes = $classes;
185
+
186
+		return $this;
187
+	}
188
+
189
+	//--------------------------------------------------------------------
190
+
191
+	/**
192
+	 * Given the contents to render, will build a list of links for the sidebar
193
+	 * out of the headings in the file.
194
+	 *
195
+	 * Note: Will ONLY use h2 and h3 to build the links from.
196
+	 *
197
+	 * Note: The $content passed in WILL be modified by adding named anchors
198
+	 * that match up with the locations.
199
+	 *
200
+	 * @param string $content The HTML to analyse for headings.
201
+	 * @return string
202
+	 */
203
+	public function buildDocumentMap(&$content)
204
+	{
205
+		if (empty($content)) {
206
+			return $content;
207
+		}
208
+
209
+		// If $content already has a wrapping <div> and </div> tags, remove them,
210
+		// since we'll replace them just below.
211
+		if (strpos($content, '<div>') === 0) {
212
+			$content = substr($content, 5);
213
+
214
+			// Trailing div also?
215
+			if (substr($content, -6) == '</div>') {
216
+				$content = substr($content, 0, -6);
217
+			}
218
+		}
219
+
220
+		try {
221
+			$xml = new \SimpleXMLElement('<?xml version="1.0" standalone="yes"?><div>' . $content . '</div>');
222
+		} catch (\Exception $e) {
223
+			// SimpleXML barfed on us, so send back the un-modified content
224
+			return [];
225
+		}
226
+
227
+		$map = [];
228
+		list($map, $content) = $this->extractDocMapAndAddAnchors($content, $xml, $map);
229
+
230
+		return $map;
231
+	}
232
+
233
+	//--------------------------------------------------------------------
234
+
235
+	/**
236
+	 * Stores the name of the callback method to run to convert the source
237
+	 * files to viewable files. By default, this should be used to register
238
+	 * a Mardown Extended formatter with the system, but could be used to
239
+	 * extend the
240
+	 *
241
+	 * @param string $callback
242
+	 * @param bool $cascade // If FALSE the formatting of a component ends here. If TRUE, will be passed to next formatter.
243
+	 * @return $this
244
+	 */
245
+	public function registerFormatter($callback = null, $cascade = false)
246
+	{
247
+		if (empty($callback)) return;
248
+
249
+		$this->formatters[] = [
250
+			'callable' => $callback,
251
+			'cascade'  => (bool)$cascade
252
+		];
253
+
254
+		return $this;
255
+	}
256
+
257
+	//--------------------------------------------------------------------
258
+
259
+	/**
260
+	 * Runs the text through the registered formatters.
261
+	 *
262
+	 * @param $str
263
+	 * @return mixed
264
+	 */
265
+	public function format($str)
266
+	{
267
+		if (! is_array($this->formatters)) return $str;
268
+
269
+		foreach ($this->formatters as $formatter) {
270
+			$method = $formatter['callable'];
271
+			$cascade = $formatter['cascade'];
272
+
273
+			$str = call_user_func($method, $str);
274
+
275
+			if (! $cascade) return $str;
276
+		}
277
+
278
+		return $str;
279
+	}
280
+
281
+	//--------------------------------------------------------------------
282
+
283
+	//--------------------------------------------------------------------
284
+	// Table of Contents methods
285
+	//--------------------------------------------------------------------
286
+
287
+	/**
288
+	 * Retrieves the list of files in a folder and preps the name and filename
289
+	 * so it's ready for creating the HTML.
290
+	 *
291
+	 * @param  String $folder The path to the folder to retrieve.
292
+	 *
293
+	 * @return Array  An associative array @see parse_ini_file for format
294
+	 * details.
295
+	 */
296
+	public function buildTOC($folder)
297
+	{
298
+		// If the toc file exists in the folder, use it to build the links.
299
+		if (is_file("{$folder}/_toc.ini")) {
300
+			$toc = parse_ini_file("{$folder}/_toc.ini", true);
301
+			return $this->columnizeTOC($toc);
302
+		}
303
+
304
+		// If the toc file does not exist, build the links by listing the files
305
+		// in the directory (and any sub-directories)
306
+		$map = $this->directory_map($folder);
307
+
308
+		// If directory_map can not open the directory or find any files inside
309
+		// the directory, return an empty array.
310
+		if (empty($map)) {
311
+			return [];
312
+		}
313
+
314
+		// If these docs are located in the /application/docs or /bonfire/docs
315
+		// directory, just use $this->current_group for the root.
316
+		// Module docs need $this->current_group and $type.
317
+		$tocRoot = $this->current_folder;
318
+		if ($this->current_folder != strtolower($folder)) {
319
+			$tocRoot .= '/' . strtolower($folder);
320
+		}
321
+
322
+		$toc = [];
323
+		foreach ($map as $files) {
324
+			// If $files isn't an array, then make it one so that all situations
325
+			// may be dealt with cleanly.
326
+			if (! is_array($files)) {
327
+				$files = [$files];
328
+			}
329
+
330
+			foreach ($files as $file) {
331
+				if (in_array($file, $this->ignore_files)) {
332
+					continue;
333
+				}
334
+
335
+				// The title for the index is the passed $type. Otherwise,
336
+				// build the title from the file's name.
337
+				if (strpos($file, 'index') === false) {
338
+					$title = str_replace($this->docs_ext, '', $file);
339
+					$title = str_replace('_', ' ', $title);
340
+					$title = ucwords($title);
341
+
342
+					$toc["{$tocRoot}/{$file}"] = $title;
343
+				} else {
344
+					$toc[$tocRoot] = $type;
345
+				}
346
+			}
347
+		}
348
+
349
+		$toc = $this->columnizeTOC($toc);
350
+
351
+		return $toc;
352
+	}
353
+
354
+	//--------------------------------------------------------------------
355
+
356
+	/**
357
+	 * Sorts the passed TOC array into columns of as close to equal length
358
+	 * as we can get it.
359
+	 *
360
+	 * @param $toc
361
+	 * @return array
362
+	 */
363
+	protected function columnizeTOC($toc)
364
+	{
365
+		$section_count = count($toc);
366
+
367
+		// First - determine the size of each 'section'.
368
+		$sizes = [];
369
+
370
+		foreach ($toc as $section => $chapters) {
371
+			$sizes[] = count($chapters);
372
+		}
373
+
374
+		$column_avg = (int)round(array_sum($sizes) / $section_count);
375
+
376
+		// Split things into 4 columns of approximately equal size.
377
+		// If we only have 4 columns (or less), then make sure to
378
+		// deal with that also.
379
+		$columns = [];
380
+
381
+		$current_column = 0;
382
+		$current_column_count = 0;
383
+		$keys = array_keys($toc);
384
+
385
+		for ($i = 0; $i <= $section_count; $i++) {
386
+			if (! isset($keys[$i])) {
387
+				continue;
388
+			}
389
+
390
+			$section = array_shift($toc);
391
+
392
+			// Can we stay in this column?
393
+			if ($current_column_count <= $column_avg && $section_count > 4) {
394
+				// Don't forget to account for the heading also.
395
+				$current_column_count += count($section) + 1;
396
+			} else {
397
+				$current_column_count = 0;
398
+				$current_column++;
399
+			}
400
+
401
+			$columns[$current_column][$keys[$i]] = $section;
402
+		}
403
+
404
+		return $columns;
405
+	}
406
+
407
+	//--------------------------------------------------------------------
408
+
409
+	//--------------------------------------------------------------------
410
+	// Folder Methods
411
+	//--------------------------------------------------------------------
412
+
413
+	/**
414
+	 * Returns the current docFolders array.
415
+	 *
416
+	 * @return array
417
+	 */
418
+	public function docFolders()
419
+	{
420
+		return $this->doc_folders;
421
+	}
422
+
423
+	//--------------------------------------------------------------------
424
+
425
+	/**
426
+	 * Registers a path to be used when searching for documentation files.
427
+	 *
428
+	 * @param $name     A nickname to reference it by later.
429
+	 * @param $path     The server path to the folder.
430
+	 * @return $this
431
+	 */
432
+	public function addDocFolder($name, $path)
433
+	{
434
+		// Standardize the path
435
+		$path = realpath($path) . '/';
436
+
437
+		// realpath will return FALSE if the path doesn't exist
438
+		// or the script doesn't have access to it.
439
+		if (! $path || $path == '/') {
440
+			return $this;
441
+		}
442
+
443
+		$name = strtolower($name);
444
+
445
+		$this->doc_folders[$name] = $path;
446
+
447
+		return $this;
448
+	}
449
+
450
+	//--------------------------------------------------------------------
451
+
452
+	/**
453
+	 * Removes a folder from the folders we scan for documentation files
454
+	 * within.
455
+	 *
456
+	 * @param $name
457
+	 * @return $this
458
+	 */
459
+	public function removeDocFolder($name)
460
+	{
461
+		$name = strtolower($name);
462
+
463
+		if (isset($this->doc_folders[$name])) {
464
+			unset($this->doc_folders[$name]);
465
+		}
466
+
467
+		return $this;
468
+	}
469
+
470
+	//--------------------------------------------------------------------
471
+
472
+	//--------------------------------------------------------------------
473
+	// Private Methods
474
+	//--------------------------------------------------------------------
475
+
476
+	/**
477
+	 * Analyzes the passed in current url string and checks against
478
+	 * a list of groups to determine what the current group is.
479
+	 *
480
+	 * @param $current_url
481
+	 * @param $groups
482
+	 * @return string
483
+	 */
484
+	protected function detectCurrentFolder($current_url, $groups = [])
485
+	{
486
+		if (! is_array($groups)) {
487
+			return null;
488
+		}
489
+
490
+		$segments = explode('/', $current_url);
491
+
492
+		// We start from the back of the array since
493
+		// that's most likely to be close to the end.
494
+		$segments = array_reverse($segments);
495
+
496
+		foreach ($segments as $segment) {
497
+			foreach ($groups as $group) {
498
+				if (strtolower($group) == strtolower($segment)) {
499
+					return $group;
500
+				}
501
+			}
502
+		}
503
+
504
+		// Nothing found?
505
+		return null;
506
+	}
507
+
508
+	//--------------------------------------------------------------------
509
+
510
+	//--------------------------------------------------------------------
511
+	// Private Methods
512
+	//--------------------------------------------------------------------
513
+
514
+	/**
515
+	 * Locates the file on disk and reads the contents into a single string.
516
+	 *
517
+	 * If a folder Nickname (see addDocFolder() ) is passed as the second parameter,
518
+	 * it will limit it's search to that single folder. If nothing is passed, it will
519
+	 * search through all of the folders in the order they were given to the library,
520
+	 * until it finds the first one.
521
+	 *
522
+	 * @param string $path The 'path' of the file (relative to the docs
523
+	 *                                 folder. Usually from the URI)
524
+	 * @param string $restrictToFolder (Optional) The nickname of one of the
525
+	 *                                 folders to restrict the search to.
526
+	 *
527
+	 * @throws RuntimeException
528
+	 * @return null|string
529
+	 */
530
+	private function locateAndReadFile($path, $restrictToFolder = null)
531
+	{
532
+		$folders = $this->doc_folders;
533
+
534
+		if (! is_null($restrictToFolder)) {
535
+			// Make sure the folder exists
536
+			if (! is_null($restrictToFolder) && ! isset($this->doc_folders[$restrictToFolder])) {
537
+				throw new \RuntimeException('You must add the docs folder that you wish to find docs from.');
538
+			}
539
+
540
+			$folders = [$this->doc_folders[$restrictToFolder]];
541
+		}
542
+
543
+		foreach ($folders as $alias => $folder) {
544
+			if (file_exists($folder . $path . $this->docs_ext)) {
545
+				// Store the alias so we know which folder we're in.
546
+				$this->current_folder = $alias;
547
+
548
+				return file_get_contents($folder . $path . $this->docs_ext);
549
+			}
550
+		}
551
+
552
+		return null;
553
+	}
554
+
555
+	//--------------------------------------------------------------------
556
+
557
+	/**
558
+	 * Re-formats the passed in link.
559
+	 *
560
+	 * @param $link
561
+	 * @param $current_url
562
+	 * @param $site_url
563
+	 * @return mixed
564
+	 */
565
+	private function reformatAnchor($link, $groups, $current_url, $site_url)
566
+	{
567
+		// Grab the href value.
568
+		$href = $link->attributes()->href;
569
+
570
+		// If the href is null, it's probably a named anchor with no content.
571
+		if (! $href) {
572
+			// Make sure it has an href, else the XML will not close this
573
+			// tag correctly.
574
+			$link['href'] = ' ';
575
+
576
+			return $link;
577
+		}
578
+
579
+		// Remove any trailing # signs
580
+		$href = rtrim($href, '# ');
581
+
582
+		// If the href starts with #, then attach the current_url to it
583
+		if ($href != '' && substr_compare($href, '#', 0, 1) === 0) {
584
+			$link['href'] = $current_url . $href;
585
+
586
+			return $link;
587
+		}
588
+
589
+		// If it's a full external path, go on...
590
+		if ((strpos($href, 'http://') !== false || strpos($href, 'https://') !== false) &&
591
+			strpos($href, $site_url) === false
592
+		) {
593
+			$link['target'] = "_blank";
594
+			return $link;
595
+		}
596
+
597
+		// If it's a full local path, get rid of it.
598
+		if ($site_url !== "/" && strpos($href, $site_url) !== false) {
599
+			$href = str_replace($site_url, '', $href);
600
+		}
601
+
602
+		// Strip out some unnecessary items, just in case they're there.
603
+		if (substr($href, 0, strlen('docs/')) == 'docs/') {
604
+			$href = substr($href, strlen('docs/'));
605
+		}
606
+
607
+		// This includes 'bonfire/' if it was missed during the conversion.
608
+		if (substr($href, 0, strlen('bonfire/')) == 'bonfire/') {
609
+			$href = substr($href, strlen('bonfire/'));
610
+		}
611
+
612
+		// If another 'group' is not already defined at the head of the link
613
+		// then add the current group to it.
614
+		$group_found = false;
615
+
616
+		foreach ($groups as $group) {
617
+			if (strpos($href, $group) === 0) {
618
+				$group_found = true;
619
+			}
620
+		}
621
+
622
+		if (! $group_found) {
623
+			$href = $this->current_folder . '/' . $href;
624
+		}
625
+
626
+		// Convert to full site_url
627
+		if (strpos($href, 'http') !== 0) {
628
+			$href = $site_url . 'docs/' . ltrim($href, '/ ');
629
+		}
630
+
631
+		// Save the corrected href
632
+		$link['href'] = $href;
633
+
634
+		return $link;
635
+	}
636
+
637
+	//--------------------------------------------------------------------
638
+
639
+	/**
640
+	 * Creates a Document Map based on <h2> and <h3> tags.
641
+	 * Also adds named anchors into the $content so the map
642
+	 * can link to the content properly.
643
+	 *
644
+	 * @param $content
645
+	 * @param $xml
646
+	 * @param $map
647
+	 * @return array
648
+	 */
649
+	protected function extractDocMapAndAddAnchors(&$content, $xml, $map)
650
+	{
651
+		// Holds the current h2 we're processing
652
+		$current_obj = [];
653
+
654
+		$currentChild = 0;
655
+
656
+		foreach ($xml->children() as $childType => $line) {
657
+			$currentChild++;
658
+
659
+			// If it's an h1 - take the first and make it
660
+			// our page title.
661
+			if ($childType == 'h1' && empty($this->page_title))
662
+			{
663
+				$this->page_title = (string)$line;
664
+			}
665
+
666
+			// Make sure that our current object is
667
+			// stored and reset.
668
+			if ($childType == 'h1' || $childType == 'h2') {
669
+				if (count($current_obj)) {
670
+					$map[] = $current_obj;
671
+					$current_obj = [];
672
+				}
673
+			}
674
+
675
+			if ($childType == 'h2') {
676
+				$name = (string)$line;
677
+				$link = strtolower(str_replace(' ', '_', (string)$line));
678
+
679
+				$current_obj['name'] = $name;
680
+				$current_obj['link'] = '#' . $link;
681
+				$current_obj['items'] = [];
682
+
683
+				// Insert a named anchor into the $content
684
+				$anchor = '<a name="' . $link . '" id="' . $link . '" ></a>';
685
+
686
+				$search = "<h2>{$name}</h2>";
687
+
688
+				$content = str_replace($search, $anchor . $search, $content);
689
+			} elseif ($childType == 'h3') {
690
+				// Make sure we have some place to store the items.
691
+				if (! isset($current_obj['items'])) {
692
+					$current_obj['items'] = [];
693
+				}
694
+
695
+				$link = strtolower(str_replace(' ', '_', (string)$line));
696
+				$name = (string)$line;
697
+
698
+				$current_obj['items'][] = [
699
+					'name' => $name,
700
+					'link' => '#' . $link
701
+				];
702
+
703
+				// Insert a named anchor into the $content
704
+				$anchor = '<a name="' . $link . '" id="' . $link . '" ></a>';
705
+
706
+				$search = "<h3>{$name}</h3>";
707
+
708
+				$content = str_replace($search, $anchor . $search, $content);
709
+			}
710
+
711
+			// Is this the last element? Then close out our current object.
712
+			if (count($xml) == $currentChild) {
713
+				if (count($current_obj)) {
714
+					$map[] = $current_obj;
715
+				}
716
+			}
717
+		}
718
+		return [$map, $content];
719
+	}
720
+	//--------------------------------------------------------------------
721
+
722
+	/**
723
+	 * Create a Directory Map
724
+	 *
725
+	 * Reads the specified directory and builds an array
726
+	 * representation of it. Sub-folders contained with the
727
+	 * directory will be mapped as well.
728
+	 *
729
+	 * @param    string $source_dir Path to source
730
+	 * @param    int $directory_depth Depth of directories to traverse
731
+	 *                        (0 = fully recursive, 1 = current dir, etc)
732
+	 * @param    bool $hidden Whether to show hidden files
733
+	 * @return    array
734
+	 */
735
+	protected function directory_map($source_dir, $directory_depth = 0, $hidden = FALSE)
736
+	{
737
+		if ($fp = @opendir($source_dir)) {
738
+			$filedata = array();
739
+			$new_depth = $directory_depth - 1;
740
+			$source_dir = rtrim($source_dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
741
+
742
+			while (FALSE !== ($file = readdir($fp))) {
743
+				// Remove '.', '..', and hidden files [optional]
744
+				if ($file === '.' OR $file === '..' OR ($hidden === FALSE && $file[0] === '.')) {
745
+					continue;
746
+				}
747
+
748
+				is_dir($source_dir . $file) && $file .= DIRECTORY_SEPARATOR;
749
+
750
+				if (($directory_depth < 1 OR $new_depth > 0) && is_dir($source_dir . $file)) {
751
+					$filedata[$file] = directory_map($source_dir . $file, $new_depth, $hidden);
752
+				} else {
753
+					$filedata[] = $file;
754
+				}
755
+			}
756
+
757
+			closedir($fp);
758
+			return $filedata;
759
+		}
760
+
761
+		return FALSE;
762
+	}
763
+
764
+	//--------------------------------------------------------------------
765 765
 
766 766
 }
Please login to merge, or discard this patch.