1 : <?php
2 : /**
3 : * Roman de Renart
4 : *
5 : * PHP version 5
6 : *
7 : * @category Rdr
8 : * @package Edit
9 : * @author Michel Corne <mcorne@yahoo.com>
10 : * @copyright 2010 Michel Corne
11 : * @license http://www.opensource.org/licenses/bsd-license.php The BSD License
12 : * @link http://roman-de-renart.blogspot.com/
13 : * @version SVN: $Id$
14 : */
15 :
16 : require_once 'InputOutput.php';
17 :
18 : /**
19 : * Loading, reading and writing the table of contents
20 : *
21 : * @category Rdr
22 : * @package Edit
23 : * @author Michel Corne <mcorne@yahoo.com>
24 : * @copyright 2010 Michel Corne
25 : * @license http://www.opensource.org/licenses/bsd-license.php The BSD License
26 : */
27 :
28 : class TableContents extends InputOutput
29 : {
30 : /**
31 : * The episode identifier separator
32 : */
33 : const EPISODE_ID_SEPARATOR = '.';
34 :
35 : /**
36 : * The format of an item of the episode list
37 : */
38 : const EPISODE_LIST_FMT = '%s.%s -- %s';
39 :
40 : /**
41 : * The error message reported when the table of contents cannot be extracted
42 : */
43 : const ERR_EXTRACT_TABLE = 'cannot extract table of contents';
44 :
45 : /**
46 : * The error message reported when the episode number is invalid
47 : */
48 : const ERR_INVALID_ID = 'invalid episode ID: %s';
49 :
50 : /**
51 : * The error message reported when the episode name is invalid
52 : */
53 : const ERR_INVALID_NAME = 'cannot find episode by name';
54 :
55 : /**
56 : * The error message reported when the episode details cannot be parsed
57 : */
58 : const ERR_PARSE_EPISODE = 'cannot parse episode';
59 :
60 : /**
61 : * The error message reported when the story details cannot be parsed
62 : */
63 : const ERR_PARSE_STORY = 'cannot parse story';
64 :
65 : /**
66 : * The error message reported when the table of contents cannot be parsed
67 : */
68 : const ERR_PARSE_TABLE = 'cannot parse table of contents';
69 :
70 : /**
71 : * The name of the HTML input file
72 : */
73 : const INPUT_FILE = 'table-contents-in.html';
74 :
75 : /**
76 : * The message reported when the table of contents is loaded
77 : */
78 : const MSG_TABLE_LOADED = 'Table of contents loaded into: %s';
79 :
80 : /**
81 : * The message reported when the table of contents is read
82 : */
83 : const MSG_TABLE_READ = 'Table of contents read into: %s';
84 :
85 : /**
86 : * The message reported when the table of contents is written
87 : */
88 : const MSG_TABLE_WRITTEN = 'Table of contents written into: %s';
89 :
90 : /**
91 : * The name of HTML output file
92 : */
93 : const OUTPUT_FILE = 'table-contents-out.html';
94 :
95 : /**
96 : * The name of the PHP working file
97 : */
98 : const WORKING_FILE = 'table-contents.php';
99 :
100 : /**
101 : * The template of the container of the table of contents
102 : */
103 : const CONTAINER_TPL =
104 : '
105 : <!-- BEGIN TABLE OF CONTENTS -->
106 : %s
107 : <!-- END TABLE OF CONTENTS -->
108 : ';
109 :
110 : /**
111 : * The template of the table of contents
112 : */
113 : const TABLE_CONTENTS_TPL =
114 : '
115 : <table class="rdr-table">
116 : %s
117 : </table>
118 : ';
119 :
120 : /**
121 : * The template of the story details
122 : */
123 : const STORY_TPL =
124 : '
125 : <tr>
126 : <td class="rdr-list">%s</td>
127 : <td>
128 : <a href="javascript: displayORhideElt(\'%s\')">%s</a>
129 : <div id="%s" class="translate rdr-table" style="display:none">
130 : %s
131 : </div>
132 : </td>
133 : </tr>
134 : ';
135 :
136 : /**
137 : * The template of the episode details
138 : */
139 : const EPISODE_TPL =
140 : '
141 : <p class="rdr-table">
142 : <span class="rdr-list">•</span>
143 : <a href="http://roman-de-renart.blogspot.com/%s">%s</a>
144 : </p>
145 : ';
146 :
147 : /**
148 : * The episode details keys
149 : * @var array
150 : */
151 : private $episodeKeys = array('path', 'title');
152 :
153 : /**
154 : * The name of the HTML input file
155 : * @var string
156 : */
157 : public $inputFile;
158 :
159 : /**
160 : * The name of the HTML output file
161 : * @var string
162 : */
163 : public $outputFile;
164 :
165 : /**
166 : * The name of the PHP working file
167 : * @var string
168 : */
169 : public $workingFile;
170 :
171 : /**
172 : * The stories details aka the table of contents
173 : * @var array
174 : */
175 : private $stories = array();
176 :
177 : /**
178 : * The story details keys
179 : * @var array
180 : */
181 : private $storyKeys = array('number', 'id1', 'title', 'id2', 'episodes');
182 :
183 : /**
184 : * Constructor
185 : *
186 : * @param array $config the configuration directives
187 : * @param bool $setStories sets the stories array if true, no setting if false
188 : * @param boolean $setFileNames sets the names of the working files if true, no setting if false
189 : * @return void
190 : */
191 : public function __construct($config, $setStories = true, $setFileNames = true)
192 : {
193 20 : parent::__construct($config);
194 :
195 20 : $setFileNames and $this->setFileNames();
196 20 : $setStories and $this->setStories();
197 20 : }
198 :
199 : /**
200 : * Displays the table of contents
201 : *
202 : * @return string the table of contents
203 : */
204 : public function display()
205 : {
206 1 : $episodes = array();
207 :
208 1 : foreach($this->stories as $storyNumber => $story) {
209 1 : foreach($story['episodes'] as $episodeNumber => $episode) {
210 1 : $episodes[] = sprintf(self::EPISODE_LIST_FMT,
211 1 : $storyNumber + 1, $episodeNumber + 1, $episode['title']);
212 1 : }
213 1 : }
214 :
215 1 : return implode("\n", $episodes);
216 : }
217 :
218 : /**
219 : * Finds the episode details
220 : *
221 : * Identifies the last episode or the last story if no identifier is passed.
222 : * Identifies the last episode of a story if only the story number is passed.
223 : *
224 : * @param string $episodeID the episode identifier, format: <story-number>.<episode-number>
225 : * @return array the episode path, title and identifier
226 : * @throws Exception an exception is thrown by abort() if the episode identifier is invalid
227 : */
228 : public function findEpisodeById($episodeID)
229 : {
230 : // extracts the story and episode numbers
231 4 : $parts = explode(self::EPISODE_ID_SEPARATOR, $episodeID);
232 : // defaults to the last story
233 4 : $storyNumber = empty($parts[0])? count($this->stories) : $parts[0];
234 4 : isset($this->stories[$storyNumber-1]) or
235 2 : $this->abort(sprintf(self::ERR_INVALID_ID, $episodeID));
236 :
237 : // defaults to the last episode
238 4 : $episodeNumber = empty($parts[1])? count($this->stories[$storyNumber-1]['episodes']) : $parts[1];
239 4 : isset($this->stories[$storyNumber-1]['episodes'][$episodeNumber-1]) or
240 1 : $this->abort(sprintf(self::ERR_INVALID_ID, $episodeID));
241 :
242 : // returns the episode details
243 4 : $episode = $this->stories[$storyNumber-1]['episodes'][$episodeNumber-1];
244 : // updates the episode identifier
245 4 : $episode['id'] = $storyNumber . self::EPISODE_ID_SEPARATOR . $episodeNumber;
246 :
247 4 : return $episode;
248 : }
249 :
250 : /**
251 : * Finds the identifier of an episode
252 : *
253 : * @param string $episodeName the name of the episode
254 : * @return array the story and the episode numbers
255 : * @throws Exception an exception is thrown by abort() if the episode name is invalid
256 : */
257 : public function findEpisodeId($episodeName)
258 : {
259 8 : foreach($this->stories as $storyNumber => $story) {
260 8 : foreach($story['episodes'] as $episodeNumber => $episode) {
261 8 : if (basename($episode['path'], '.html') == $episodeName) {
262 : // the episode is found
263 8 : return array($storyNumber, $episodeNumber);
264 : }
265 8 : }
266 8 : }
267 :
268 : // the episode was not found
269 : // returns an error if an episode name was given
270 8 : $episodeName and $this->abort(self::ERR_INVALID_NAME);
271 :
272 : // return the last episode otherwise
273 3 : return array($storyNumber, $episodeNumber);
274 : }
275 :
276 : /**
277 : * Finds the next episode
278 : *
279 : * @param string $episodeName the name of the episode
280 : * @return array the path of the next episode
281 : */
282 : public function findNextEpisode($episodeName)
283 : {
284 3 : list($storyNumber, $episodeNumber) = $this->findEpisodeId($episodeName);
285 :
286 3 : isset($this->stories[$storyNumber]['episodes'][++$episodeNumber]) or
287 3 : isset($this->stories[++$storyNumber]['episodes'][$episodeNumber = 0]) or
288 3 : $storyNumber = 0;
289 :
290 3 : return $this->stories[$storyNumber]['episodes'][$episodeNumber]['path'];
291 : }
292 :
293 : /**
294 : * Finds the next story
295 : *
296 : * @param string $episodeName the name of the episode
297 : * @return array the title of the next story and the path of its first episode
298 : */
299 : public function findNextStory($episodeName)
300 : {
301 3 : list($storyNumber) = $this->findEpisodeId($episodeName);
302 3 : isset($this->stories[++$storyNumber]) or $storyNumber = 0;
303 :
304 : return array(
305 3 : $this->stories[$storyNumber]['title'],
306 3 : $this->stories[$storyNumber]['episodes'][0]['path']);
307 : }
308 :
309 : /**
310 : * Finds the previous episode
311 : *
312 : * @param string $episodeName the name of the episode
313 : * @return array the path of the previous episode
314 : */
315 : public function findPreviousEpisode($episodeName)
316 : {
317 3 : list($storyNumber, $episodeNumber) = $this->findEpisodeId($episodeName);
318 :
319 3 : if (!isset($this->stories[$storyNumber]['episodes'][--$episodeNumber])) {
320 2 : isset($this->stories[--$storyNumber]) or $storyNumber = count($this->stories) - 1;
321 2 : $episodeNumber = count($this->stories[$storyNumber]['episodes']) - 1;
322 2 : }
323 :
324 3 : return $this->stories[$storyNumber]['episodes'][$episodeNumber]['path'];
325 : }
326 :
327 : /**
328 : * Finds the previous story
329 : *
330 : * @param string $episodeName the name of the episode
331 : * @return array the title of the previous story and the path of its first episode
332 : */
333 : public function findPreviousStory($episodeName)
334 : {
335 3 : list($storyNumber) = $this->findEpisodeId($episodeName);
336 3 : isset($this->stories[--$storyNumber]) or $storyNumber = count($this->stories) - 1;
337 :
338 : return array(
339 3 : $this->stories[$storyNumber]['title'],
340 3 : $this->stories[$storyNumber]['episodes'][0]['path']);
341 : }
342 :
343 : /**
344 : * Finds the current episode
345 : *
346 : * @param string $episodeName the name of the episode
347 : * @return array the title of the current story and the path of the episode
348 : */
349 : public function findCurrentStory($episodeName)
350 : {
351 3 : list($storyNumber) = $this->findEpisodeId($episodeName);
352 :
353 : return array(
354 3 : $this->stories[$storyNumber]['title'],
355 3 : $this->stories[$storyNumber]['episodes'][0]['path']);
356 : }
357 : /**
358 : * Returns the stories array
359 : *
360 : * @return array the stories array
361 : */
362 : public function getStories()
363 : {
364 1 : return $this->stories;
365 : }
366 :
367 : /**
368 : * Loads the table of contents from the blog into the HTML input file
369 : *
370 : * @return string a message reporting that the table of contents is loaded
371 : */
372 : public function load()
373 : {
374 : // the path and domain of the blog message may be passed for testing purposes
375 1 : ($path = @func_get_arg(0)) === false and $path = null;
376 1 : ($domain = @func_get_arg(1)) === false and $domain = null;
377 1 : $blogContent = $this->readBlog($path, $domain);
378 :
379 : // extracts the table of contents HTML
380 1 : $pattern = $this->completePattern(self::CONTAINER_TPL);
381 1 : $tableContents = $this->extract($pattern, $blogContent, self::ERR_EXTRACT_TABLE);
382 :
383 : // writes the table of contents into the input file
384 1 : $this->writeFile($this->inputFile, $tableContents);
385 :
386 1 : return sprintf(self::MSG_TABLE_LOADED, self::INPUT_FILE);
387 : }
388 :
389 : /**
390 : * Parses the episodes details
391 : *
392 : * @param array $story the story details
393 : * @return array the story details updated with the episode details
394 : */
395 : public function parseEpisodes($story)
396 : {
397 3 : static $pattern;
398 3 : isset($pattern) or $pattern = $this->completePattern(self::EPISODE_TPL);
399 :
400 3 : $story['episodes'] = $this->matchAll(
401 3 : $pattern, $story['episodes'], self::ERR_PARSE_EPISODE, $this->episodeKeys);
402 :
403 3 : return $story;
404 : }
405 :
406 : /**
407 : * Parses the stories details
408 : *
409 : * @param array $story the story details
410 : * @return mixed the stories details updated with their episodes details
411 : */
412 : public function parseStories($stories)
413 : {
414 2 : $pattern = $this->completePattern(self::STORY_TPL);
415 :
416 2 : $stories = $this->matchAll(
417 2 : $pattern, $stories, self::ERR_PARSE_STORY, $this->storyKeys);
418 :
419 2 : return $this->arrayMap('parseEpisodes', $stories);
420 : }
421 :
422 : /**
423 : * Reads the table of contents from the HTML input file into the PHP working file
424 : *
425 : * @return string a message reporting that the table of contents is read
426 : */
427 : public function read()
428 : {
429 1 : $pattern = $this->completePattern(self::TABLE_CONTENTS_TPL);
430 :
431 1 : $tableContents = $this->readFile($this->inputFile);
432 :
433 1 : list($stories) = $this->match($pattern, $tableContents, self::ERR_PARSE_TABLE);
434 1 : $stories = $this->tidyString($stories);
435 1 : $stories = $this->parseStories($stories);
436 :
437 1 : $this->writeFile($this->workingFile, $stories, true);
438 1 : $this->stories = $stories;
439 :
440 1 : return sprintf(self::MSG_TABLE_READ, self::WORKING_FILE);
441 : }
442 :
443 : /**
444 : * Sets the names of the working files
445 : *
446 : * @return void
447 : */
448 : public function setFileNames()
449 : {
450 : // sets the working files with the basename
451 20 : $this->inputFile = $this->makeFilePath(self::INPUT_FILE, 'wip-dir' );
452 20 : $this->outputFile = $this->makeFilePath(self::OUTPUT_FILE, 'wip-dir');
453 20 : $this->workingFile = $this->makeFilePath(self::WORKING_FILE, 'wip-dir');
454 20 : }
455 :
456 : /**
457 : * Sets the stories array
458 : *
459 : * @return void
460 : */
461 : public function setStories()
462 : {
463 16 : $this->stories = $this->includeFile($this->workingFile);
464 16 : }
465 :
466 : /**
467 : * Writes the table of contents from the PHP working file into the HTML output file
468 : *
469 : * @return string a message reporting that the table of contents is written
470 : */
471 : public function write()
472 : {
473 1 : $tableContents = $this->writeTableContents($this->stories);
474 1 : $tableContents = sprintf(self::CONTAINER_TPL, $tableContents);
475 :
476 1 : $tableContents = $this->addNbsp($tableContents);
477 1 : $this->writeFile($this->outputFile, $tableContents);
478 :
479 1 : return sprintf(self::MSG_TABLE_WRITTEN, self::OUTPUT_FILE);
480 : }
481 :
482 : /**
483 : * Writes the episode details in HTML
484 : *
485 : * Typically used as callback of array_reduce().
486 : *
487 : * @param string $episodes the concatenated episodes details in HTML
488 : * @param array $episode the current episode details
489 : * @return string the concatenated episodes details in HTML
490 : */
491 : public function writeEpisode($episodes, $episode)
492 : {
493 4 : return $episodes . vsprintf(self::EPISODE_TPL, $episode);
494 : }
495 :
496 : /**
497 : * Writes the story details into HTML
498 : *
499 : * Typically used as callback of array_reduce().
500 : *
501 : * @param string $stories the concatenated stories details in HTML
502 : * @param array $story the current story details
503 : * @return array the concatenated stories details in HTML
504 : */
505 : public function writeStory($stories, $story)
506 : {
507 3 : $story['episodes'] = array_reduce($story['episodes'], array($this, 'writeEpisode'));
508 :
509 3 : return $stories . vsprintf(self::STORY_TPL, $story);
510 : }
511 :
512 : /**
513 : * Writes the table of contents into HTML
514 : *
515 : * @param array $stories the stories details
516 : * @return string the table of contents in HTML
517 : */
518 : public function writeTableContents($stories)
519 : {
520 2 : $stories = array_reduce($stories, array($this, 'writeStory'));
521 :
522 2 : return sprintf(self::TABLE_CONTENTS_TPL, $stories);
523 : }
|