|
1
|
|
|
<?php |
|
2
|
|
|
/* |
|
3
|
|
|
* This file is part of the trefoil application. |
|
4
|
|
|
* |
|
5
|
|
|
* (c) Miguel Angel Gabriel <[email protected]> |
|
6
|
|
|
* |
|
7
|
|
|
* For the full copyright and license information, please view the LICENSE |
|
8
|
|
|
* file that was distributed with this source code. |
|
9
|
|
|
*/ |
|
10
|
|
|
namespace Trefoil\Plugins\Optional; |
|
11
|
|
|
|
|
12
|
|
|
use Easybook\Events\EasybookEvents; |
|
13
|
|
|
use Easybook\Events\ParseEvent; |
|
14
|
|
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface; |
|
15
|
|
|
use Trefoil\Plugins\BasePlugin; |
|
16
|
|
|
use Trefoil\Util\Toolkit; |
|
17
|
|
|
|
|
18
|
|
|
/** |
|
19
|
|
|
* This plugin extends footnotes to support several formats. |
|
20
|
|
|
* |
|
21
|
|
|
* - type 'end': This is the normal Markdown-rendered footnotes. |
|
22
|
|
|
* They will be at the end of each book item, separated by a <hr/> tag. |
|
23
|
|
|
* This is the default. |
|
24
|
|
|
* |
|
25
|
|
|
* - type 'inject: This is a variant of thpe 'end', where each item's |
|
26
|
|
|
* footnotes will be injected to a certain injection point. |
|
27
|
|
|
* Just write '<div class="footnotes"></div>' anywhere in each item |
|
28
|
|
|
* where the footnotes should be injected. |
|
29
|
|
|
* |
|
30
|
|
|
* - type 'item': All the footnotes in the book will be collected and |
|
31
|
|
|
* rendered in a separated item called 'footnotes' that need to |
|
32
|
|
|
* exist in the book. |
|
33
|
|
|
* |
|
34
|
|
|
* - type 'inline: PrinceXML support inline footnotes, where the text |
|
35
|
|
|
* of the note must be inlined into the text, instead of just a |
|
36
|
|
|
* reference. Prince will manage the numbering. |
|
37
|
|
|
*/ |
|
38
|
|
|
class FootnotesExtendPlugin extends BasePlugin implements EventSubscriberInterface |
|
39
|
|
|
{ |
|
40
|
|
|
const FOOTNOTES_TYPE_END = 'end'; |
|
41
|
|
|
const FOOTNOTES_TYPE_ITEM = 'item'; |
|
42
|
|
|
const FOOTNOTES_TYPE_INJECT = 'inject'; |
|
43
|
|
|
const FOOTNOTES_TYPE_INLINE = 'inline'; |
|
44
|
|
|
|
|
45
|
|
|
/** |
|
46
|
|
|
* @var string Type of footnotes to generate |
|
47
|
|
|
*/ |
|
48
|
|
|
protected $footnotesType = ''; |
|
49
|
|
|
|
|
50
|
|
|
/** |
|
51
|
|
|
* @var array The extracted footnotes the current book item |
|
52
|
|
|
*/ |
|
53
|
|
|
protected $footnotesCurrentItem = array(); |
|
54
|
|
|
|
|
55
|
|
|
/** |
|
56
|
|
|
* @var string The current item footnotes (as text) |
|
57
|
|
|
*/ |
|
58
|
|
|
protected $itemFootnotesText = ''; |
|
59
|
|
|
|
|
60
|
|
|
/* ******************************************************************************** |
|
61
|
|
|
* Event handlers |
|
62
|
|
|
* ******************************************************************************** |
|
63
|
|
|
*/ |
|
64
|
4 |
|
public static function getSubscribedEvents() |
|
65
|
|
|
{ |
|
66
|
|
|
return array( |
|
67
|
4 |
|
EasybookEvents::POST_PARSE => array('onItemPostParse') |
|
68
|
4 |
|
); |
|
69
|
|
|
} |
|
70
|
|
|
|
|
71
|
4 |
|
public function onItemPostParse(ParseEvent $event) |
|
72
|
|
|
{ |
|
73
|
4 |
|
$this->init($event); |
|
74
|
|
|
|
|
75
|
4 |
|
$this->processItem(); |
|
76
|
|
|
|
|
77
|
|
|
// reload changed item |
|
78
|
4 |
|
$event->setItem($this->item); |
|
79
|
4 |
|
} |
|
80
|
|
|
|
|
81
|
|
|
/* ******************************************************************************** |
|
82
|
|
|
* Implementation |
|
83
|
|
|
* ******************************************************************************** |
|
84
|
|
|
*/ |
|
85
|
|
|
|
|
86
|
|
|
/** |
|
87
|
|
|
* Process a content item |
|
88
|
|
|
*/ |
|
89
|
4 |
|
protected function processItem() |
|
90
|
|
|
{ |
|
91
|
|
|
// lazy initialize |
|
92
|
4 |
|
if (!isset($this->app['publishing.footnotes.items'])) { |
|
93
|
4 |
|
$this->app['publishing.footnotes.items'] = array(); |
|
94
|
4 |
|
} |
|
95
|
|
|
|
|
96
|
4 |
|
$this->footnotesCurrentItem = array(); |
|
97
|
|
|
|
|
98
|
|
|
// options |
|
99
|
4 |
|
$this->footnotesType = $this->getEditionOption('plugins.options.FootnotesExtend.type', 'end'); |
|
100
|
|
|
|
|
101
|
4 |
|
$this->fixFootnotes(); |
|
102
|
|
|
|
|
103
|
4 |
|
switch ($this->footnotesType) { |
|
104
|
4 |
|
case self::FOOTNOTES_TYPE_END: |
|
105
|
|
|
// nothing else to do |
|
106
|
1 |
|
break; |
|
107
|
|
|
|
|
108
|
3 |
|
case self::FOOTNOTES_TYPE_INLINE: |
|
109
|
|
|
|
|
110
|
1 |
|
$this->extractFootnotes(); |
|
111
|
1 |
|
$this->inlineFootnotes(); |
|
112
|
1 |
|
break; |
|
113
|
|
|
|
|
114
|
2 |
|
case self::FOOTNOTES_TYPE_ITEM: |
|
115
|
|
|
|
|
116
|
1 |
|
$this->extractFootnotes(); |
|
117
|
1 |
|
$this->renumberReferences(); |
|
118
|
1 |
|
break; |
|
119
|
|
|
|
|
120
|
1 |
|
case self::FOOTNOTES_TYPE_INJECT: |
|
121
|
|
|
|
|
122
|
1 |
|
$this->saveInjectionTarget(); |
|
123
|
1 |
|
$this->extractFootnotes(); |
|
124
|
1 |
|
$this->restoreInjectionTarget(); |
|
125
|
1 |
|
$this->injectFootnotes(); |
|
126
|
1 |
|
break; |
|
127
|
4 |
|
} |
|
128
|
|
|
|
|
129
|
|
|
// look if we need to remove the footnotes book item |
|
130
|
4 |
|
$this->removeUnneededFootnotesItem(); |
|
131
|
4 |
|
} |
|
132
|
|
|
|
|
133
|
|
|
/** |
|
134
|
|
|
* Replace character ':' by '-' in footnotes ids because epubcheck does not like it. |
|
135
|
|
|
*/ |
|
136
|
4 |
View Code Duplication |
protected function fixFootnotes() |
|
|
|
|
|
|
137
|
|
|
{ |
|
138
|
4 |
|
$content = $this->item['content']; |
|
139
|
|
|
|
|
140
|
|
|
// fix footnotes ref in text |
|
141
|
4 |
|
$content = preg_replace('/id="fnref(\d*):/', 'id="fnref$1-', $content); |
|
142
|
4 |
|
$content = str_replace('href="#fn:', 'href="#fn-', $content); |
|
143
|
|
|
|
|
144
|
|
|
// fix footnotes |
|
145
|
4 |
|
$content = str_replace('id="fn:', 'id="fn-', $content); |
|
146
|
4 |
|
$content = preg_replace('/href="#fnref(\d*):/', 'href="#fnref$1-', $content); |
|
147
|
|
|
|
|
148
|
|
|
// fix return sign used |
|
149
|
4 |
|
$content = str_replace('↩', '[↵]', $content); |
|
150
|
|
|
|
|
151
|
4 |
|
$this->item['content'] = $content; |
|
152
|
4 |
|
} |
|
153
|
|
|
|
|
154
|
|
|
/** |
|
155
|
|
|
* Extracts anc collects all footnotes in the item. |
|
156
|
|
|
*/ |
|
157
|
3 |
|
protected function extractFootnotes() |
|
158
|
|
|
{ |
|
159
|
3 |
|
$content = $this->item['content']; |
|
160
|
|
|
|
|
161
|
3 |
|
$this->itemFootnotesText = ''; |
|
162
|
|
|
|
|
163
|
3 |
|
$regExp = '/'; |
|
164
|
3 |
|
$regExp .= '<div class="footnotes">.*<ol>(?<fns>.*)<\/ol>.*<\/div>'; |
|
165
|
3 |
|
$regExp .= '/Ums'; // Ungreedy, multiline, dotall |
|
166
|
|
|
|
|
167
|
|
|
// PHP 5.3 compat |
|
168
|
3 |
|
$me = $this; |
|
169
|
|
|
|
|
170
|
3 |
|
$content = preg_replace_callback( |
|
171
|
3 |
|
$regExp, |
|
172
|
|
|
function ($matches) use ($me) { |
|
173
|
|
|
|
|
174
|
3 |
|
$this->itemFootnotesText = $matches[0]; |
|
175
|
|
|
|
|
176
|
3 |
|
$regExp2 = '/'; |
|
177
|
3 |
|
$regExp2 .= '<li.*id="(?<id>.*)">.*'; |
|
178
|
3 |
|
$regExp2 .= '<p>(?<text>.*) <a .*href="#(?<backref>.*)"'; |
|
179
|
3 |
|
$regExp2 .= '/Ums'; // Ungreedy, multiline, dotall |
|
180
|
|
|
|
|
181
|
3 |
|
preg_match_all($regExp2, $matches[0], $matches2, PREG_SET_ORDER); |
|
182
|
|
|
|
|
183
|
3 |
|
if ($matches2) { |
|
184
|
3 |
|
foreach ($matches2 as $match2) { |
|
185
|
|
|
$footnote = array( |
|
186
|
3 |
|
'item' => $this->item['toc'][0]['slug'], |
|
187
|
3 |
|
'text' => $match2['text'], |
|
188
|
3 |
|
'id' => $match2['id'], |
|
189
|
3 |
|
'text' => $match2['text'], |
|
190
|
3 |
|
'backref' => $match2['backref'], |
|
191
|
3 |
|
'new_number' => count($this->app['publishing.footnotes.items']) + 1 |
|
192
|
3 |
|
); |
|
193
|
|
|
|
|
194
|
|
|
// save for current item |
|
195
|
3 |
|
$this->footnotesCurrentItem[$match2['id']] = $footnote; |
|
196
|
|
|
|
|
197
|
|
|
// save for all items |
|
198
|
3 |
|
$footnotes = $this->app['publishing.footnotes.items']; |
|
199
|
3 |
|
$footnotes[$match2['id']] = $footnote; |
|
200
|
3 |
|
$this->app['publishing.footnotes.items'] = $footnotes; |
|
201
|
3 |
|
} |
|
202
|
3 |
|
} |
|
203
|
|
|
|
|
204
|
3 |
|
return ''; |
|
205
|
3 |
|
}, |
|
206
|
|
|
$content |
|
207
|
3 |
|
); |
|
208
|
|
|
|
|
209
|
3 |
|
$this->item['content'] = $content; |
|
210
|
3 |
|
} |
|
211
|
|
|
|
|
212
|
|
|
/** |
|
213
|
|
|
* Inline footnotes in the text, after the note reference. |
|
214
|
|
|
* |
|
215
|
|
|
* This is only useful for renderers that support automatic |
|
216
|
|
|
* inline footnotes, like PrinceXML. |
|
217
|
|
|
*/ |
|
218
|
1 |
View Code Duplication |
protected function inlineFootnotes() |
|
|
|
|
|
|
219
|
|
|
{ |
|
220
|
1 |
|
$content = $this->item['content']; |
|
221
|
|
|
|
|
222
|
1 |
|
$regExp = '/'; |
|
223
|
1 |
|
$regExp .= '<sup id="(?<supid>fnref.?-.*)">'; |
|
224
|
1 |
|
$regExp .= '<a(?<prev>.*)href="#(?<href>fn-.*)"(?<post>.*)>(?<number>.*)<\/a><\/sup>'; |
|
225
|
1 |
|
$regExp .= '/Ums'; // Ungreedy, multiline, dotall |
|
226
|
|
|
|
|
227
|
|
|
// PHP 5.3 compat |
|
228
|
1 |
|
$me = $this; |
|
229
|
|
|
|
|
230
|
1 |
|
$content = preg_replace_callback( |
|
231
|
1 |
|
$regExp, |
|
232
|
|
|
function ($matches) use ($me) { |
|
233
|
1 |
|
$footnotes = $me->footnotesCurrentItem; |
|
234
|
1 |
|
$footnote = $footnotes[$matches['href']]; |
|
235
|
1 |
|
$text = $footnote['text']; |
|
236
|
|
|
|
|
237
|
|
|
// replace <p>...</p> with <br/> because no block elements are |
|
238
|
|
|
// allowed inside a <span>. |
|
239
|
|
|
// The paragraph contents are also put inside a fake paragraph <span> |
|
240
|
|
|
// so they can be styled. |
|
241
|
|
|
|
|
242
|
1 |
|
$text = str_replace( |
|
243
|
1 |
|
['<p>', '</p>'], |
|
244
|
1 |
|
['<span class="p">', '<br/></span>'], |
|
245
|
|
|
$text |
|
246
|
1 |
|
); |
|
247
|
1 |
|
$text = '<span class="p" >' . $text . '</span>'; |
|
248
|
|
|
|
|
249
|
1 |
|
$html = sprintf( |
|
250
|
1 |
|
'<span class="fn">%s</span>', |
|
251
|
|
|
$text |
|
252
|
1 |
|
); |
|
253
|
|
|
|
|
254
|
1 |
|
return $html; |
|
255
|
1 |
|
}, |
|
256
|
|
|
$content |
|
257
|
1 |
|
); |
|
258
|
|
|
|
|
259
|
1 |
|
$this->item['content'] = $content; |
|
260
|
1 |
|
} |
|
261
|
|
|
|
|
262
|
|
|
/** |
|
263
|
|
|
* Renumber all footnotes references to be correlative for the whole book. |
|
264
|
|
|
*/ |
|
265
|
1 |
|
protected function renumberReferences() |
|
266
|
|
|
{ |
|
267
|
1 |
|
$content = $this->item['content']; |
|
268
|
|
|
|
|
269
|
1 |
|
$regExp = '/'; |
|
270
|
1 |
|
$regExp .= '<sup id="(?<supid>fnref.?-.*)">'; |
|
271
|
1 |
|
$regExp .= '<a(?<prev>.*)href="#(?<href>fn-.*)"(?<post>.*)>(?<number>.*)<\/a>'; |
|
272
|
1 |
|
$regExp .= '/Ums'; // Ungreedy, multiline, dotall |
|
273
|
|
|
|
|
274
|
|
|
// PHP 5.3 compat |
|
275
|
1 |
|
$me = $this; |
|
276
|
|
|
|
|
277
|
1 |
|
$content = preg_replace_callback( |
|
278
|
1 |
|
$regExp, |
|
279
|
|
|
function ($matches) use ($me) { |
|
280
|
1 |
|
$newNumber = $this->app['publishing.footnotes.items'][$matches['href']]['new_number']; |
|
281
|
|
|
|
|
282
|
1 |
|
$html = sprintf( |
|
283
|
1 |
|
'<sup id="%s"><a%shref="#%s"%s>%s</a>', |
|
284
|
1 |
|
$matches['supid'], |
|
285
|
1 |
|
$matches['prev'], |
|
286
|
1 |
|
$matches['href'], |
|
287
|
1 |
|
$matches['post'], |
|
288
|
|
|
$newNumber |
|
289
|
1 |
|
); |
|
290
|
|
|
|
|
291
|
1 |
|
return $html; |
|
292
|
1 |
|
}, |
|
293
|
|
|
$content |
|
294
|
1 |
|
); |
|
295
|
|
|
|
|
296
|
1 |
|
$this->item['content'] = $content; |
|
297
|
1 |
|
} |
|
298
|
|
|
|
|
299
|
|
|
/** |
|
300
|
|
|
* Replace the footnotes injection target to keep extractFootnotes() from removing it. |
|
301
|
|
|
*/ |
|
302
|
1 |
View Code Duplication |
protected function saveInjectionTarget() |
|
|
|
|
|
|
303
|
|
|
{ |
|
304
|
1 |
|
$content = $this->item['content']; |
|
305
|
|
|
|
|
306
|
1 |
|
$content = preg_replace('/<div class="footnotes">\s*<\/div>/', '<div class="__footnotes"></div>', $content); |
|
307
|
|
|
|
|
308
|
1 |
|
$this->item['content'] = $content; |
|
309
|
1 |
|
} |
|
310
|
|
|
|
|
311
|
|
|
/** |
|
312
|
|
|
* Restore the injection target |
|
313
|
|
|
*/ |
|
314
|
1 |
View Code Duplication |
protected function restoreInjectionTarget() |
|
|
|
|
|
|
315
|
|
|
{ |
|
316
|
1 |
|
$content = $this->item['content']; |
|
317
|
|
|
|
|
318
|
1 |
|
$content = preg_replace('/<div class="__footnotes">\s*<\/div>/', '<div class="footnotes"></div>', $content); |
|
319
|
|
|
|
|
320
|
1 |
|
$this->item['content'] = $content; |
|
321
|
1 |
|
} |
|
322
|
|
|
|
|
323
|
|
|
/** |
|
324
|
|
|
* Inject footnotes at the injection target. |
|
325
|
|
|
* |
|
326
|
|
|
* The injection target is a '<div class="footnotes"></div>' placed anywhere in the item text. |
|
327
|
|
|
*/ |
|
328
|
1 |
|
protected function injectFootnotes() |
|
329
|
|
|
{ |
|
330
|
1 |
|
$content = $this->item['content']; |
|
331
|
|
|
|
|
332
|
1 |
|
$regExp = '/'; |
|
333
|
1 |
|
$regExp .= '<div class="footnotes">\s*<\/div>'; |
|
334
|
1 |
|
$regExp .= '/Ums'; // Ungreedy, multiline, dotall |
|
335
|
|
|
|
|
336
|
|
|
// PHP 5.3 compat |
|
337
|
1 |
|
$me = $this; |
|
338
|
|
|
|
|
339
|
1 |
|
$content = preg_replace_callback( |
|
340
|
1 |
|
$regExp, |
|
341
|
1 |
|
function ($matches) use ($me) { |
|
|
|
|
|
|
342
|
|
|
|
|
343
|
1 |
|
$footnotes = $this->app->render( |
|
344
|
1 |
|
'_footnotes.twig', |
|
345
|
1 |
|
array('footnotes' => $this->footnotesCurrentItem) |
|
346
|
1 |
|
); |
|
347
|
|
|
|
|
348
|
1 |
|
return Toolkit::renderHTMLTag( |
|
349
|
1 |
|
'div', |
|
350
|
1 |
|
$footnotes, |
|
351
|
1 |
|
array('class' => 'footnotes') |
|
352
|
1 |
|
); |
|
353
|
1 |
|
}, |
|
354
|
|
|
$content |
|
355
|
1 |
|
); |
|
356
|
|
|
|
|
357
|
1 |
|
$this->item['content'] = $content; |
|
358
|
1 |
|
} |
|
359
|
|
|
|
|
360
|
|
|
/** |
|
361
|
|
|
* Ensure the footnotes item is removed from the book if it is not needed. |
|
362
|
|
|
*/ |
|
363
|
4 |
|
protected function removeUnneededFootnotesItem() |
|
364
|
|
|
{ |
|
365
|
|
|
// only for footnotes item |
|
366
|
4 |
|
if ($this->item['config']['element'] !== 'footnotes') { |
|
367
|
4 |
|
return; |
|
368
|
|
|
} |
|
369
|
|
|
|
|
370
|
|
|
// instruct the publisher to remove 'footnotes' item from book |
|
371
|
|
|
// if footnotes type is not 'item' |
|
372
|
4 |
|
if ($this->footnotesType !== self::FOOTNOTES_TYPE_ITEM) { |
|
373
|
|
|
|
|
374
|
3 |
|
$this->item['remove'] = true; |
|
375
|
|
|
|
|
376
|
3 |
|
return; |
|
377
|
|
|
} |
|
378
|
|
|
|
|
379
|
|
|
// instruct the publisher to remove 'footnotes' item from book |
|
380
|
|
|
// if footnotes type is 'item' but not footnotes |
|
381
|
1 |
|
if (count($this->app['publishing.footnotes.items']) == 0) { |
|
382
|
|
|
$this->item['remove'] = true; |
|
383
|
|
|
$this->writeLn("No footnotes found in text.", 'info'); |
|
384
|
|
|
} |
|
385
|
|
|
|
|
386
|
1 |
|
} |
|
387
|
|
|
} |
|
388
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.