|
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 Twig to provide some useful functionalities: |
|
20
|
|
|
* |
|
21
|
|
|
* <li> |
|
22
|
|
|
* <b>Use configuration options in book contents</b> (instead of only in templates). |
|
23
|
|
|
* For example, in "chapter1.md" you can write "The title is {{ book.title }}". |
|
24
|
|
|
* This is mainly useful with user-defined configuration options. |
|
25
|
|
|
* |
|
26
|
|
|
* <li> |
|
27
|
|
|
* <b>itemtoc()</b> function automatically generates the Table of Contents of the current item. |
|
28
|
|
|
* The deep of the itemtoc can be tweaked by the configuration option <i>edition.itemtoc.deep</i>, |
|
29
|
|
|
* which defaults to one less than the main TOC deep <i>edition.toc.deep</i>. |
|
30
|
|
|
* It uses de <i>itemtoc.twig</i> template that must be available either as local or global template. |
|
31
|
|
|
* |
|
32
|
|
|
* <li> |
|
33
|
|
|
* <b>file()</b> function (deprecated, use fragment()) works like PHP's <i>include()</i>, |
|
34
|
|
|
* allowing the inclusion of another file into the current item. |
|
35
|
|
|
* The syntax is <i>file(filename, variables, options)</i> where "variables" and "options" are |
|
36
|
|
|
* optional hash tables where you can pass variables {'variable': 'value'} or |
|
37
|
|
|
* options {'nopagebreak': true} to the included file. |
|
38
|
|
|
* |
|
39
|
|
|
* <li> |
|
40
|
|
|
* <b>fragment()</b> function works like PHP's <i>include()</i>, allowing the inclusion of |
|
41
|
|
|
* another file into the current item. |
|
42
|
|
|
* |
|
43
|
|
|
* is a simplified version of <i>file()</i> without any variables or options. |
|
44
|
|
|
* The syntax is <i>fragment(filename)</i>. No page break will be inserted after the file contents. |
|
45
|
|
|
* |
|
46
|
|
|
* |
|
47
|
|
|
*/ |
|
48
|
|
|
class TwigExtensionPlugin extends BasePlugin implements EventSubscriberInterface |
|
49
|
|
|
{ |
|
50
|
1 |
|
public static function getSubscribedEvents() |
|
51
|
|
|
{ |
|
52
|
1 |
|
return array( |
|
53
|
1 |
|
EasybookEvents::PRE_PARSE => 'onItemPreParse', |
|
54
|
1 |
|
EasybookEvents::POST_PARSE => array('onItemPostParse', -1010) |
|
55
|
1 |
|
); |
|
56
|
|
|
} |
|
57
|
|
|
|
|
58
|
1 |
|
public function onItemPreParse(ParseEvent $event) |
|
59
|
|
|
{ |
|
60
|
1 |
|
$this->init($event); |
|
61
|
|
|
|
|
62
|
1 |
|
$content = $event->getItemProperty('original'); |
|
63
|
|
|
|
|
64
|
|
|
// replace "{#" to avoid problems with markdown extra syntax for ids in headers |
|
65
|
1 |
|
$content = str_replace('{#', '@%@&', $content); |
|
66
|
|
|
|
|
67
|
|
|
// replace configuration options on PreParse to take care of normal replacements |
|
68
|
|
|
// and the first pass of "itemtoc()" |
|
69
|
1 |
|
$content = $this->renderString($content); |
|
70
|
|
|
|
|
71
|
|
|
# replace back "{#" |
|
72
|
1 |
|
$content = str_replace('@%@&', '{#', $content); |
|
73
|
|
|
|
|
74
|
1 |
|
$event->setItemProperty('original', $content); |
|
75
|
1 |
|
} |
|
76
|
|
|
|
|
77
|
1 |
|
public function onItemPostParse(ParseEvent $event) |
|
78
|
|
|
{ |
|
79
|
1 |
|
$this->init($event); |
|
80
|
|
|
|
|
81
|
1 |
|
$content = $event->getItemProperty('content'); |
|
82
|
|
|
|
|
83
|
|
|
// ensure the Twig function call is not enclosed into a '<p>..</p>' tag |
|
84
|
|
|
// as it will result in epub checking erros |
|
85
|
1 |
|
$content = preg_replace('/<p>\s*{{/', '{{', $content); |
|
86
|
1 |
|
$content = preg_replace('/}}\s*<\/p>/', '}}', $content); |
|
87
|
|
|
|
|
88
|
|
|
// replace "{#" to avoid problems with markdown extra syntax for ids in headers |
|
89
|
1 |
|
$content = str_replace('{#', '@%@&', $content); |
|
90
|
|
|
|
|
91
|
|
|
// replace also in PostParse to process the second pass of "itemtoc()" ("itemtoc_internal()") |
|
92
|
1 |
|
$content = $this->renderString($content); |
|
93
|
|
|
|
|
94
|
|
|
# replace back "{#" |
|
95
|
1 |
|
$content = str_replace('@%@&', '{#', $content); |
|
96
|
|
|
|
|
97
|
1 |
|
$event->setItemProperty('content', $content); |
|
98
|
1 |
|
} |
|
99
|
|
|
|
|
100
|
1 |
|
protected function renderString($string, $variables = array()) |
|
101
|
|
|
{ |
|
102
|
|
|
// we need a new Twig String Renderer environment |
|
103
|
1 |
|
$twig = new \Twig_Environment(new \Twig_Loader_String(), $this->app['twig.options']); |
|
104
|
|
|
|
|
105
|
1 |
|
$this->addTwigGlobals($twig); |
|
106
|
1 |
|
$this->registerExtensions($twig); |
|
107
|
|
|
|
|
108
|
1 |
|
return $twig->render($string, $variables); |
|
109
|
|
|
} |
|
110
|
|
|
|
|
111
|
1 |
|
protected function addTwigGlobals(\Twig_Environment $twig) |
|
112
|
|
|
{ |
|
113
|
1 |
|
$twig->addGlobal('app', $this->app); |
|
114
|
|
|
|
|
115
|
1 |
|
if (null !== $this->app['publishing.book.config']['book']) { |
|
116
|
1 |
|
$twig->addGlobal('book', $this->app['publishing.book.config']['book']); |
|
117
|
|
|
|
|
118
|
1 |
|
$publishingEdition = $this->edition; |
|
119
|
1 |
|
$editions = $this->app->book('editions'); |
|
120
|
1 |
|
$twig->addGlobal('edition', $editions[$publishingEdition]); |
|
121
|
1 |
|
} |
|
122
|
|
|
|
|
123
|
1 |
|
$twig->addGlobal('item', $this->item); |
|
124
|
1 |
|
} |
|
125
|
|
|
|
|
126
|
|
|
/** |
|
127
|
|
|
* Register all the extensions here. |
|
128
|
|
|
*/ |
|
129
|
1 |
|
protected function registerExtensions(\Twig_Environment $twig) |
|
130
|
|
|
{ |
|
131
|
|
|
// file() |
|
132
|
1 |
|
$twig->addFunction(new \Twig_SimpleFunction('file', array($this, 'fileFunction'))); |
|
133
|
|
|
|
|
134
|
|
|
// fragment() |
|
135
|
1 |
|
$twig->addFunction(new \Twig_SimpleFunction('fragment', array($this, 'fragmentFunction'))); |
|
136
|
|
|
|
|
137
|
|
|
// itemtoc() and its internal counterpart |
|
138
|
1 |
|
$twig->addFunction(new \Twig_SimpleFunction('itemtoc', array($this, 'itemTocFunction'))); |
|
139
|
1 |
|
$twig->addFunction( |
|
140
|
1 |
|
new \Twig_SimpleFunction('_itemtoc_internal', array($this, 'itemTocInternalFunction')) |
|
141
|
1 |
|
); |
|
142
|
1 |
|
} |
|
143
|
|
|
|
|
144
|
|
|
/** |
|
145
|
|
|
* Twig function: <b>file(filename, variables, options)</b> |
|
146
|
|
|
* |
|
147
|
|
|
* @deprecated |
|
148
|
|
|
* |
|
149
|
|
|
* @param string $filename to be included (relative to book Contents dir) |
|
150
|
|
|
* @param array $variables to be passed to the template |
|
151
|
|
|
* @param array $options (default: 'nopagebreak: false' => add a page break after the included text) |
|
152
|
|
|
* |
|
153
|
|
|
* @return string included text with all the replacements done. |
|
154
|
|
|
*/ |
|
155
|
|
|
public function fileFunction($filename, $variables = array(), $options = array()) |
|
156
|
|
|
{ |
|
157
|
|
|
$dir = $this->app['publishing.dir.contents']; |
|
158
|
|
|
$file = $dir . '/' . $filename; |
|
159
|
|
|
|
|
160
|
|
View Code Duplication |
if (!file_exists($file)) { |
|
161
|
|
|
$this->writeLn( |
|
162
|
|
|
sprintf('Included content file "%s" not found in "%s"', $filename, $this->item['config']['content']), |
|
163
|
|
|
'error' |
|
164
|
|
|
); |
|
165
|
|
|
|
|
166
|
|
|
return $filename; |
|
167
|
|
|
} |
|
168
|
|
|
|
|
169
|
|
|
$rendered = $this->renderString(file_get_contents($file), $variables); |
|
170
|
|
|
|
|
171
|
|
|
// pagebreak is added by default |
|
172
|
|
|
$addPageBreak = !isset($options['nopagebreak']) || (isset($options['nopagebreak']) && !$options['nopagebreak']); |
|
173
|
|
|
if ($addPageBreak) { |
|
174
|
|
|
$rendered .= '<div class="page-break"></div>'; |
|
175
|
|
|
} |
|
176
|
|
|
|
|
177
|
|
|
return $rendered; |
|
178
|
|
|
} |
|
179
|
|
|
|
|
180
|
|
|
/** |
|
181
|
|
|
* Twig function: <b>fragment(filename)</b> |
|
182
|
|
|
* |
|
183
|
|
|
* @param string $filename to be included (relative to book Contents dir) |
|
184
|
|
|
* |
|
185
|
|
|
* @param array $variables to be passed to the template (example: {'name': 'John'} ) |
|
186
|
|
|
* @param array $options (example {'pagebreak' => true} will add a page break after the included text) |
|
187
|
|
|
* |
|
188
|
|
|
* @return string included text with all the replacements done. |
|
189
|
|
|
*/ |
|
190
|
1 |
|
public function fragmentFunction($filename, $variables = array(), $options = array()) |
|
191
|
|
|
{ |
|
192
|
1 |
|
$dir = $this->app['publishing.dir.contents']; |
|
193
|
1 |
|
$file = $dir . '/' . $filename; |
|
194
|
|
|
|
|
195
|
1 |
View Code Duplication |
if (!file_exists($file)) { |
|
196
|
|
|
$this->writeLn( |
|
197
|
|
|
sprintf('Fragment file "%s" not found in "%s"', $filename, $this->item['config']['content']), |
|
198
|
|
|
'error' |
|
199
|
|
|
); |
|
200
|
|
|
|
|
201
|
|
|
return $filename; |
|
202
|
|
|
} |
|
203
|
|
|
|
|
204
|
1 |
|
$rendered = $this->renderString(file_get_contents($file), $variables); |
|
205
|
|
|
|
|
206
|
|
|
// surround contents with the (optional) tag and class |
|
207
|
1 |
|
$tag = isset($options['tag']) ? $options['tag'] : ''; |
|
208
|
1 |
|
$attributes = array('markdown' => 1); |
|
209
|
1 |
|
$attributes['class'] = isset($options['class']) ? $options['class'] : ''; |
|
210
|
1 |
|
if ($attributes['class'] && empty($tag)) { |
|
211
|
1 |
|
$tag = 'div'; |
|
212
|
1 |
|
} |
|
213
|
|
|
|
|
214
|
1 |
|
if ($tag) { |
|
215
|
1 |
|
$attributes['class'] = 'fragment ' . $attributes['class']; |
|
216
|
1 |
|
$rendered = Toolkit::renderHTMLTag($tag, $rendered, $attributes); |
|
217
|
1 |
|
} |
|
218
|
|
|
|
|
219
|
|
|
// add pagebreak if asked |
|
220
|
1 |
|
if (isset($options['pagebreak']) && $options['pagebreak']) { |
|
221
|
|
|
$rendered .= '<div class="page-break"></div>'; |
|
222
|
|
|
} |
|
223
|
|
|
|
|
224
|
1 |
|
return $rendered; |
|
225
|
|
|
} |
|
226
|
|
|
|
|
227
|
|
|
/** |
|
228
|
|
|
* Twig function: <b>itemtoc()</b> |
|
229
|
|
|
* |
|
230
|
|
|
* @return string The _itemtoc_internal() function call |
|
231
|
|
|
* |
|
232
|
|
|
* Generating the item toc requires two phases to ensure that included files got parsed after being |
|
233
|
|
|
* included (with file()). The second phase does the actual rendering of the itemtoc |
|
234
|
|
|
* template. |
|
235
|
|
|
*/ |
|
236
|
1 |
|
public function itemTocFunction() |
|
237
|
|
|
{ |
|
238
|
1 |
|
return '{{ _itemtoc_internal() }}'; |
|
239
|
|
|
} |
|
240
|
|
|
|
|
241
|
|
|
/** |
|
242
|
|
|
* Twig function: <b>_itemtoc_internal()</b> |
|
243
|
|
|
* <b>This is an internal function so is not to be used directly in content files.</b> |
|
244
|
|
|
* It will be invoked on itemPostParse, after all the file() functions have been resolved and all |
|
245
|
|
|
* the included item contents have been parsed. |
|
246
|
|
|
* |
|
247
|
|
|
* @uses Configuration option <i>edition.plugins.TwigExtension.itemtoc.deep</i> |
|
248
|
|
|
* (default: <i>edition.toc.deep + 1 </i> |
|
249
|
|
|
* |
|
250
|
|
|
* @return string The item toc rendered |
|
251
|
|
|
* |
|
252
|
|
|
*/ |
|
253
|
1 |
|
public function itemTocInternalFunction() |
|
254
|
|
|
{ |
|
255
|
1 |
|
$template = 'itemtoc.twig'; |
|
256
|
|
|
|
|
257
|
|
|
// note that we need to use the normal Twig template renderer, not our Twig string renderer |
|
258
|
1 |
|
$twig = $this->app['twig']; |
|
259
|
1 |
|
$this->addTwigGlobals($twig); |
|
260
|
|
|
|
|
261
|
1 |
|
$itemtoc_deep = $this->getEditionOption( |
|
262
|
1 |
|
'plugins.options.TwigExtension.itemtoc.deep', |
|
263
|
1 |
|
$this->getEditionOption('toc.deep') + 1 |
|
264
|
1 |
|
); |
|
265
|
|
|
|
|
266
|
1 |
|
$rendered = $twig->render($template, array('itemtoc_deep' => $itemtoc_deep)); |
|
267
|
|
|
|
|
268
|
1 |
|
return $rendered; |
|
269
|
|
|
} |
|
270
|
|
|
} |
|
271
|
|
|
|