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 'Episode.php';
17 : require_once 'Questions.php';
18 : require_once 'Mapping.php';
19 : require_once 'Differences.php';
20 :
21 : /**
22 : * Reading an episode from a loaded blog message
23 : *
24 : * @category Rdr
25 : * @package Edit
26 : * @author Michel Corne <mcorne@yahoo.com>
27 : * @copyright 2010 Michel Corne
28 : * @license http://www.opensource.org/licenses/bsd-license.php The BSD License
29 : */
30 :
31 : class Read extends Episode
32 : {
33 : /**
34 : * The error message reported when the number sequence is invalid
35 : */
36 : const ERR_NUMBER_SEQUENCE = 'bad number sequence';
37 :
38 : /**
39 : * The error message reported when the episode cannot be parsed
40 : */
41 : const ERR_PARSE_EPISODE = 'cannot parse episode';
42 :
43 : /**
44 : * The error message reported when the links cannot be parsed
45 : */
46 : const ERR_PARSE_LINKS = 'cannot parse links';
47 :
48 : /**
49 : * The error message reported when a list cannot be parsed
50 : */
51 : const ERR_PARSE_LIST = 'cannot parse list';
52 :
53 : /**
54 : * The message reported when the episode is read
55 : */
56 : const MSG_EPISODE_READ = 'Episode read into: %s';
57 :
58 : /**
59 : * The message reported when there is no change before and after the episode processing
60 : */
61 : const MSG_NO_CHANGE = 'There are no changes.';
62 :
63 : /**
64 : * Compare the episode input and output files
65 : *
66 : * @return string the differences episode input and output files
67 : */
68 : public function compareInAndOut()
69 : {
70 1 : $in = $this->includeFile($this->workingFile);
71 1 : $out = $this->getEpisode($this->outputFile);
72 :
73 1 : $difference = $this->arraysMap('mixedDiffToString', $out, $in);
74 1 : $result = $this->arrayToString($difference, true, " ===>\n\n") or
75 1 : $result = self::MSG_NO_CHANGE;
76 :
77 1 : return $result;
78 : }
79 :
80 : /**
81 : * Reads and parses the episode input or output file
82 : *
83 : * @param string $filename the file name
84 : * @return array the parsed episode
85 : */
86 : public function getEpisode($filename)
87 : {
88 3 : $content = $this->readFile($filename);
89 3 : $content = $this->tidyString($content);
90 :
91 3 : return $this->parseEpisode($content);
92 : }
93 :
94 : /**
95 : * Parses the HTML episode content
96 : *
97 : * @param string $episodeContent the HTML episode content
98 : * @return array the parsed episode
99 : */
100 : public function parseEpisode($episodeContent)
101 : {
102 4 : $pattern = $this->completePattern(Episode::EPISODE_TPL);
103 4 : $episode = $this->match($pattern, $episodeContent, self::ERR_PARSE_EPISODE,
104 4 : array_keys($this->episodeDefault));
105 :
106 4 : $episode = $this->arrayMapKeys('parseLinks', $episode, array(
107 4 : 'links-top',
108 4 : 'links-bottom',
109 4 : ));
110 4 : $episode = $this->arrayMapKeys('parseList', $episode, array(
111 4 : 'questions',
112 4 : 'mapping',
113 4 : 'differences',
114 4 : ));
115 4 : $episode = $this->arrayMapKeys('parseFirstLine', $episode, array(
116 4 : 'trans-first-line',
117 4 : 'fro-first-line',
118 4 : ));
119 4 : $episode = $this->arrayMapKeys('parseLines', $episode, array(
120 4 : 'trans-text',
121 4 : 'fro-text',
122 4 : 'blog-numbers',
123 4 : 'meon-numbers',
124 4 : 'meon-text',
125 4 : 'martin-chapters',
126 4 : 'martin-numbers',
127 4 : 'martin-text',
128 4 : 'fro-numbers',
129 4 : ));
130 :
131 4 : return $episode;
132 : }
133 :
134 : /**
135 : * Extracts the first line of a text
136 : *
137 : * @param string $lines the lines of the text in HTML
138 : * @return mixed the line-breaks before the first line and the first line
139 : */
140 : public function parseFirstLine($lines)
141 : {
142 5 : $lines = $this->parseLines($lines);
143 5 : $firstLine = array_pop($lines);
144 :
145 5 : return array('breaks' => $lines, 'text' => $firstLine);
146 : }
147 :
148 : /**
149 : * Explodes the lines of a text
150 : *
151 : * @param string $lines the lines of the text in HTML
152 : * @return array the lines of the text
153 : */
154 : public function parseLines($lines)
155 : {
156 6 : return explode(Episode::LINE_SEPARATOR, $lines);
157 : }
158 :
159 : /**
160 : * Extracts the links of the episode
161 : *
162 : * @param string $links the link section in HTML
163 : * @return array the links details
164 : */
165 : public function parseLinks($links)
166 : {
167 5 : $pattern = $this->completePattern(Episode::LINKS_TPL);
168 :
169 5 : return $this->match($pattern, $links, self::ERR_PARSE_LINKS, array_keys($this->linksDefault));
170 : }
171 :
172 : /**
173 : * Parses a list
174 : *
175 : * @param string $list the list section in HTML
176 : * @return array the list details
177 : */
178 : public function parseList($list)
179 : {
180 5 : $pattern = $this->completePattern(Episode::LIST_TPL);
181 :
182 5 : return $this->extractAll($pattern, $list, self::ERR_PARSE_LIST);
183 : }
184 :
185 : /**
186 : * Completes the line numbers
187 : *
188 : * Only line numbers multiple of 4 are written in a blog message.
189 : *
190 : * @param array $episode the episode details
191 : * @return array the episode details with the completed line numbers
192 : * @throws Exception an exception is thrown by abort() if the original line numbers
193 : * do not match the rebuilt line numbers
194 : */
195 : public function prepNumbers($episode)
196 : {
197 : // adds an empty line number corresponding to the first line
198 3 : array_unshift($episode['blog-numbers'], '');
199 : // removes empty line numbers
200 3 : $withNumbers = array_filter($episode['blog-numbers']);
201 :
202 : // completes tne line numbers
203 3 : list($idx, $lineNumber) = each($withNumbers);
204 3 : $firstLineNumber = $lineNumber - $idx;
205 3 : $lastLineNumber = $firstLineNumber + count($episode['blog-numbers']) - 1;
206 3 : $episode['blog-numbers'] = range($firstLineNumber, $lastLineNumber);
207 :
208 : // verifies the original line numbers fit into the rebuilt line numbers
209 3 : array_diff_assoc($withNumbers, $episode['blog-numbers']) and
210 1 : $this->abort(self::ERR_NUMBER_SEQUENCE);
211 :
212 3 : return $episode;
213 : }
214 :
215 : /**
216 : * Adds the first line of a text to the text
217 : *
218 : * Adds the first letter to the first line if missing.
219 : * The first letter is coming from the letter image file name.
220 : *
221 : * @param array $episode the episode details
222 : * @param string $firstLineKey the key of the first line
223 : * @param string $textKey the key or name of the text to process
224 : * @param boolean $addFirstLetter adds the first letter if true, no letter added if false
225 : * @return array the episode details with the updated text
226 : */
227 : public function prepText($episode, $firstLineKey, $textKey, $addFirstLetter = false)
228 : {
229 : // adds the first letter coming from the letter image file name if applicable
230 3 : $firstLine = $addFirstLetter? basename($episode['letter-src'], '.gif') : '';
231 3 : $firstLine .= $episode[$firstLineKey]['text'];
232 : // adds the first line to the text
233 3 : array_unshift($episode[$textKey], $firstLine);
234 :
235 : // adds empty lines in the text according to the actual lines numbers
236 3 : return $this->shiftLines($episode, $textKey);
237 : }
238 :
239 : /**
240 : * Reads the episode from the HTML input file into the PHP working file and worksheet
241 : *
242 : * @return string a message reporting that the episode is read
243 : */
244 : public function readEpisode()
245 : {
246 : // reads the episode
247 1 : $episode = $this->getEpisode($this->inputFile);
248 :
249 : // writes the episode details into the working file
250 1 : $this->writeFile($this->workingFile, $episode, true);
251 :
252 : // writes the episode details in the worksheet
253 1 : $this->writeEpisode($episode);
254 : // writes the worksheet into a file
255 1 : $this->writeSheet();
256 :
257 1 : return sprintf(self::MSG_EPISODE_READ, basename($this->worksheetFile));
258 : }
259 :
260 : /**
261 : * Adds empty lines to a text
262 : *
263 : * @param array $episode the episode details
264 : * @param string $textKey the key or name of the text to process
265 : * @return array the episode details with the updated text
266 : */
267 : public function shiftLines($episode, $textKey)
268 : {
269 : // indexes the text lines with numbers according to the blog message
270 4 : $lines = array_combine($episode['blog-numbers'], $episode[$textKey]);
271 :
272 4 : $episode[$textKey] = array();
273 :
274 4 : foreach($episode['fro-numbers'] as $number) {
275 : // adds an empty line for each number that does not have a corresponding line of text
276 4 : $episode[$textKey][] = $number? $lines[$number] : '';
277 4 : }
278 :
279 4 : return $episode;
280 : }
281 :
282 : /**
283 : * Writes the episode in the worksheet
284 : *
285 : * @param array $episode the episode details
286 : * @return void
287 : */
288 : public function writeEpisode($episode)
289 : {
290 2 : $this->initLines(count($episode['fro-numbers']));
291 :
292 2 : $episode = $this->prepNumbers($episode);
293 2 : $episode = $this->prepText($episode, 'fro-first-line', 'fro-text', true);
294 2 : $episode = $this->prepText($episode, 'trans-first-line', 'trans-text');
295 :
296 : $columnNames = array(
297 2 : 'trans-text',
298 2 : 'fro-text',
299 2 : 'fro-numbers',
300 2 : 'meon-numbers',
301 2 : 'meon-text',
302 2 : 'martin-chapters',
303 2 : 'martin-numbers',
304 2 : 'martin-text',
305 2 : );
306 2 : $this->writeColumns($episode, array_intersect_key($this->columnNamesToNumbers, array_flip($columnNames)));
307 :
308 2 : $questions = $this->writeList($episode, 'Questions', 'questions');
309 2 : $mapping = $this->writeList($episode, 'Mapping', 'mapping');
310 2 : $differences = $this->writeList($episode, 'Differences', 'differences');
311 2 : }
312 :
313 : /**
314 : * Writes a list in the worksheet
315 : *
316 : * @param array $episode the episode details
317 : * @param string $class the name of the class to process the list with its toCsv() method
318 : * @param string $listKey the key or name of the list to write
319 : * @return void
320 : */
321 : public function writeList($episode, $class, $listKey)
322 : {
323 : // creates rows for each list item and row cells for each list item details
324 3 : $arrays = array_map(array($class, 'toCsv'), $episode[$listKey]);
325 3 : $rows = array();
326 :
327 : // updates the worksheet rows with the list items details (partial rows at this point)
328 3 : foreach($arrays as $array) {
329 : // finds corresponding worksheet line number to the text line number
330 3 : $lineNumber = array_search($array[Episode::COL_FRO_NUMBERS], $episode['fro-numbers']);
331 : // removes the line number from the row details
332 3 : unset($array[Episode::COL_FRO_NUMBERS]);
333 : // adds the list details to the row
334 3 : $rows[$lineNumber] = $array;
335 3 : }
336 :
337 : // write the rows in the spreadsheet
338 3 : $this->writeRows($rows);
339 3 : }
|