Completed
Push — master ( 56c2c4...605748 )
by Kevin
02:18
created

BasePremailer::getStyleNodes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 11
rs 9.4285
cc 2
eloc 5
nc 2
nop 1
1
<?php namespace Luminaire\Premailer;
2
3
/**
4
 * Created by Sublime Text 3
5
 *
6
 * @user     Kevin Tanjung
7
 * @website  http://kevintanjung.github.io
8
 * @email    [email protected]
9
 * @date     02/08/2016
10
 * @time     11:05
11
 */
12
13
use Crossjoin\Css\Format\Rule\AtMedia\MediaQuery;
14
use Crossjoin\Css\Format\Rule\AtMedia\MediaRule;
15
use Crossjoin\Css\Format\Rule\RuleAbstract;
16
use Crossjoin\Css\Format\Rule\Style\StyleDeclaration;
17
use Crossjoin\Css\Format\Rule\Style\StyleRuleSet;
18
use Crossjoin\Css\Format\Rule\Style\StyleSelector;
19
use Crossjoin\Css\Reader\CssString;
20
use Crossjoin\Css\Writer\WriterAbstract;
21
use Symfony\Component\CssSelector\CssSelectorConverter as CssSelector;
22
use DOMDocument, DOMElement, DOMXPath;
23
use LengthException, RuntimeException, InvalidArgumentException;
24
25
/**
26
 * The base Premailer class
27
 *
28
 * @package  \Luminaire\Premailer
29
 */
30
abstract class BasePremailer
31
{
32
33
    const OPTION_STYLE_TAG                  = 'styleTag';
34
    const OPTION_STYLE_TAG_BODY             = 1;
35
    const OPTION_STYLE_TAG_HEAD             = 2;
36
    const OPTION_STYLE_TAG_REMOVE           = 3;
37
38
    const OPTION_HTML_COMMENTS              = 'htmlComments';
39
    const OPTION_HTML_COMMENTS_KEEP         = 1;
40
    const OPTION_HTML_COMMENTS_REMOVE       = 2;
41
42
    const OPTION_HTML_CLASSES               = 'htmlClasses';
43
    const OPTION_HTML_CLASSES_KEEP          = 1;
44
    const OPTION_HTML_CLASSES_REMOVE        = 2;
45
46
    const OPTION_TEXT_LINE_WIDTH            = 'textLineWidth';
47
48
    const OPTION_CSS_WRITER_CLASS           = 'cssWriterClass';
49
    const OPTION_CSS_WRITER_CLASS_COMPACT   = '\Crossjoin\Css\Writer\Compact';
50
    const OPTION_CSS_WRITER_CLASS_PRETTY    = '\Crossjoin\Css\Writer\Pretty';
51
52
    /**
53
     * The options for HTML/text generation
54
     *
55
     * @var array
56
     */
57
    protected $options = [
58
        self::OPTION_STYLE_TAG        => self::OPTION_STYLE_TAG_BODY,
59
        self::OPTION_HTML_CLASSES     => self::OPTION_HTML_CLASSES_KEEP,
60
        self::OPTION_HTML_COMMENTS    => self::OPTION_HTML_COMMENTS_REMOVE,
61
        self::OPTION_CSS_WRITER_CLASS => self::OPTION_CSS_WRITER_CLASS_COMPACT,
62
        self::OPTION_TEXT_LINE_WIDTH  => 75,
63
    ];
64
65
    /**
66
     * The pseudo classes that can be set in a style attribute and that are
67
     * supported by the Symfony CssSelector (doesn't support CSS4 yet).
68
     *
69
     * @var array
70
     */
71
    protected $allowed_pseudo_classes = [
72
        StyleSelector::PSEUDO_CLASS_FIRST_CHILD,
73
        StyleSelector::PSEUDO_CLASS_ROOT,
74
        StyleSelector::PSEUDO_CLASS_NTH_CHILD,
75
        StyleSelector::PSEUDO_CLASS_NTH_LAST_CHILD,
76
        StyleSelector::PSEUDO_CLASS_NTH_OF_TYPE,
77
        StyleSelector::PSEUDO_CLASS_NTH_LAST_OF_TYPE,
78
        StyleSelector::PSEUDO_CLASS_LAST_CHILD,
79
        StyleSelector::PSEUDO_CLASS_FIRST_OF_TYPE,
80
        StyleSelector::PSEUDO_CLASS_LAST_OF_TYPE,
81
        StyleSelector::PSEUDO_CLASS_ONLY_CHILD,
82
        StyleSelector::PSEUDO_CLASS_ONLY_OF_TYPE,
83
        StyleSelector::PSEUDO_CLASS_EMPTY,
84
        StyleSelector::PSEUDO_CLASS_NOT,
85
    ];
86
87
    /**
88
     * The charset for HTML/text output
89
     *
90
     * @var string
91
     */
92
    protected $charset = "UTF-8";
93
94
    /**
95
     * The prepared HTML content
96
     *
97
     * @var string
98
     */
99
    protected $html;
100
101
    /**
102
     * The prepared text content
103
     *
104
     * @var string
105
     */
106
    protected $text;
107
108
    /**
109
     * Sets the charset used in the HTML document and used for the output.
110
     *
111
     * @param  string  $charset
112
     * @return $this
113
     */
114
    public function setCharset($charset)
115
    {
116
        $this->charset = $charset;
117
118
        return $this;
119
    }
120
121
    /**
122
     * Gets the charset used in the HTML document and used for the output.
123
     *
124
     * @return string
125
     */
126
    public function getCharset()
127
    {
128
        return $this->charset;
129
    }
130
131
    /**
132
     * Sets an option for the generation of the mail.
133
     *
134
     * @param string $name
135
     * @param mixed $value
136
     */
137
    public function setOption($name, $value)
138
    {
139
        if ( ! is_string($name))
140
        {
141
            throw new InvalidArgumentException('The argument 0 of [setOption] method is expected to be a [string], but [' . gettype($name) . '] is given.');
142
        }
143
144
        if ( ! isset($this->options[$name]))
145
        {
146
            throw new InvalidArgumentException("An option with the name [{$name}] doesn't exist.");
147
        }
148
149
        $this->validateScalarOptionValue($name, $value);
150
        $this->validatePossibleOptionValue($name, $value);
151
152
        $this->options[$name] = $value;
153
    }
154
155
    /**
156
     * Gets an option for the generation of the mail.
157
     *
158
     * @param  string|null  $name
159
     * @return mixed
160
     *
161
     * @throws \InvalidArgumentException
162
     */
163
    public function getOption($name = null)
164
    {
165
        if (is_null($name))
166
        {
167
            return $this->options;
168
        }
169
170
        if ( ! is_string($name))
171
        {
172
            throw new InvalidArgumentException('The argument 0 of [setOption] method is expected to be a [string], but [' . gettype($name) . '] is given.');
173
        }
174
175
        if ( ! isset($this->options[$name]))
176
        {
177
            throw new InvalidArgumentException("An option with the name [{$name}] doesn't exist.");
178
        }
179
180
        return $this->options[$name];
181
    }
182
183
    /**
184
     * Gets the prepared HTML version of the mail.
185
     *
186
     * @return string
187
     */
188
    public function getHtml()
189
    {
190
        if ($this->html === null)
191
        {
192
            $this->prepareContent();
193
        }
194
195
        return $this->html;
196
    }
197
198
    /**
199
     * Gets the prepared text version of the mail.
200
     *
201
     * @return string
202
     */
203
    public function getText()
204
    {
205
        if ($this->text === null)
206
        {
207
            $this->prepareContent();
208
        }
209
210
        return $this->text;
211
    }
212
213
    /**
214
     * Gets the HTML content from the preferred source.
215
     *
216
     * @return string
217
     */
218
    abstract protected function getHtmlContent();
219
220
    /**
221
     * Prepares the mail HTML/text content.
222
     *
223
     * @return void
224
     */
225
    protected function prepareContent()
226
    {
227
        if ( ! class_exists('\DOMDocument'))
228
        {
229
            throw new RuntimeException("Required extension 'dom' seems to be missing.");
230
        }
231
232
        $doc = new DOMDocument();
233
        $doc->loadHTML($this->getHtmlContent());
234
235
        $xpath = new DOMXPath($doc);
236
237
        $reader = new CssString((new StyleSheetExtractor($doc))->extract());
238
        $reader->setEnvironmentEncoding($this->getCharset());
239
240
        $rules = $reader->getStyleSheet()->getRules();
241
242
        // Extract all relevant style declarations
243
        $selectors = [];
244
245
        foreach ($this->getRelevantStyleRules($rules) as $styleRule)
246
        {
247
            foreach ($styleRule->getSelectors() as $selector) {
248
                // Check if the selector contains pseudo classes/elements that cannot
249
                // be mapped to elements
250
                $skip = false;
251
                foreach($selector->getPseudoClasses() as $pseudoClass) {
252
                    if (!in_array($pseudoClass, $this->allowed_pseudo_classes)) {
253
                        $skip = true;
254
                        break;
255
                    }
256
                }
257
                if ($skip === false) {
258
                    $specificity = $selector->getSpecificity();
259
                    if (!isset($selectors[$specificity])) {
260
                        $selectors[$specificity] = [];
261
                    }
262
                    $selectorString = $selector->getValue();
263
                    if (!isset($selectors[$specificity][$selectorString])) {
264
                        $selectors[$specificity][$selectorString] = [];
265
                    }
266
                    foreach ($styleRule->getDeclarations() as $declaration) {
267
                        $selectors[$specificity][$selectorString][] = $declaration;
268
                    }
269
                }
270
            }
271
        }
272
273
        // Get all specificity values (to process the declarations in the correct order,
274
        // without sorting the array by key, which perhaps could result in a changed
275
        // order of selectors within the specificity).
276
        $specificityKeys = array_keys($selectors);
277
        sort($specificityKeys);
278
279
        // Temporary remove all existing style attributes, because they always have the highest priority
280
        // and are added again after all styles have been applied to the elements
281
        $elements = $xpath->query("descendant-or-self::*[@style]");
282
        /** @var \DOMElement $element */
283
        foreach ($elements as $element) {
284
            if ($element->attributes !== null) {
285
                $styleAttribute = $element->attributes->getNamedItem("style");
286
287
                $styleValue = "";
288
                if ($styleAttribute !== null) {
289
                    $styleValue = (string)$styleAttribute->nodeValue;
290
                }
291
292
                if ($styleValue !== "") {
293
                    $element->setAttribute('data-pre-mailer-original-style', $styleValue);
294
                    $element->removeAttribute('style');
295
                }
296
            }
297
        }
298
299
        // Process all style declarations in the correct order
300
        foreach ($specificityKeys as $specificityKey) {
301
            /** @var StyleDeclaration[] $declarations */
302
            foreach ($selectors[$specificityKey] as $selectorString => $declarations) {
303
                $xpathQuery = (new CssSelector())->toXPath($selectorString);
304
                $elements = $xpath->query($xpathQuery);
305
                /** @var \DOMElement $element */
306
                foreach ($elements as $element) {
307
                    if ($element->attributes !== null) {
308
                        $styleAttribute = $element->attributes->getNamedItem("style");
309
310
                        $styleValue = "";
311
                        if ($styleAttribute !== null) {
312
                            $styleValue = (string)$styleAttribute->nodeValue;
313
                        }
314
315
                        $concat = ($styleValue === "") ? "" : ";";
316
                        foreach ($declarations as $declaration) {
317
                            $styleValue .= $concat . $declaration->getProperty() . ":" . $declaration->getValue();
318
                            $concat = ";";
319
                        }
320
321
                        $element->setAttribute('style', $styleValue);
322
                    }
323
                }
324
            }
325
        }
326
327
        // Add temporarily removed style attributes again, after all styles have been applied to the elements
328
        $elements = $xpath->query("descendant-or-self::*[@data-pre-mailer-original-style]");
329
        /** @var \DOMElement $element */
330
        foreach ($elements as $element) {
331
            if ($element->attributes !== null) {
332
                $styleAttribute = $element->attributes->getNamedItem("style");
333
                $styleValue = "";
334
                if ($styleAttribute !== null) {
335
                    $styleValue = (string)$styleAttribute->nodeValue;
336
                }
337
338
                $originalStyleAttribute = $element->attributes->getNamedItem("data-pre-mailer-original-style");
339
                $originalStyleValue = "";
340
                if ($originalStyleAttribute !== null) {
341
                    $originalStyleValue = (string)$originalStyleAttribute->nodeValue;
342
                }
343
344
                if ($styleValue !== "" || $originalStyleValue !== "") {
345
                    $styleValue = ($styleValue !== "" ? $styleValue . ";" : "") . $originalStyleValue;
346
                    $element->setAttribute('style', $styleValue);
347
                    $element->removeAttribute('data-pre-mailer-original-style');
348
                }
349
            }
350
        }
351
352
        // Optionally remove class attributes in HTML tags
353
        $optionHtmlClasses = $this->getOption(self::OPTION_HTML_CLASSES);
354
        if ($optionHtmlClasses === self::OPTION_HTML_CLASSES_REMOVE) {
355
            $nodesWithClass = [];
356
            foreach ($xpath->query('descendant-or-self::*[@class]') as $nodeWithClass) {
357
                $nodesWithClass[] = $nodeWithClass;
358
            }
359
            /** @var \DOMElement $nodeWithClass */
360
            foreach ($nodesWithClass as $nodeWithClass) {
361
                $nodeWithClass->removeAttribute('class');
362
            }
363
        }
364
365
        // Optionally remove HTML comments
366
        $optionHtmlComments = $this->getOption(self::OPTION_HTML_COMMENTS);
367
        if ($optionHtmlComments === self::OPTION_HTML_COMMENTS_REMOVE) {
368
            $commentNodes = [];
369
            foreach ($xpath->query('//comment()') as $comment) {
370
                $commentNodes[] = $comment;
371
            }
372
            foreach ($commentNodes as $commentNode) {
373
                $commentNode->parentNode->removeChild($commentNode);
374
            }
375
        }
376
377
        // Write XPath document back to DOM document
378
        $newDoc = $xpath->document;
379
380
        // Generate text version (before adding the styles again)
381
        $this->text = $this->prepareText($newDoc);
382
383
        // Optionally add styles tag to the HEAD or the BODY of the document
384
        $optionStyleTag = $this->getOption(self::OPTION_STYLE_TAG);
385
        if ($optionStyleTag === self::OPTION_STYLE_TAG_BODY || $optionStyleTag === self::OPTION_STYLE_TAG_HEAD) {
386
            $cssWriterClass = $this->getOption(self::OPTION_CSS_WRITER_CLASS);
387
            /** @var WriterAbstract $cssWriter */
388
            $cssWriter = new $cssWriterClass($reader->getStyleSheet());
389
            $styleNode = $newDoc->createElement("style");
390
            $styleNode->nodeValue = $cssWriter->getContent();
391
392
            if ($optionStyleTag === self::OPTION_STYLE_TAG_BODY) {
393
                /** @var \DOMNode $bodyNode */
394
                foreach($newDoc->getElementsByTagName('body') as $bodyNode) {
395
                    $bodyNode->insertBefore($styleNode, $bodyNode->firstChild);
396
                    break;
397
                }
398
            } elseif ($optionStyleTag === self::OPTION_STYLE_TAG_HEAD) {
399
                /** @var \DOMNode $headNode */
400
                foreach($newDoc->getElementsByTagName('head') as $headNode) {
401
                    $headNode->appendChild($styleNode);
402
                    break;
403
                }
404
            }
405
        }
406
407
        // Generate HTML version
408
        $this->html = $newDoc->saveHTML();
409
    }
410
411
    /**
412
     * Prepares the mail text content.
413
     *
414
     * @param \DOMDocument $doc
415
     * @return string
416
     */
417
    protected function prepareText(\DOMDocument $doc)
418
    {
419
        $text = $this->convertHtmlToText($doc->childNodes);
420
        $charset = $this->getCharset();
421
        $textLineMaxLength = $this->getOption(self::OPTION_TEXT_LINE_WIDTH);
422
423
        $text = preg_replace_callback('/^([^\n]+)$/m', function($match) use ($charset, $textLineMaxLength) {
424
                $break = "\n";
425
                $parts = preg_split('/((?:\(\t[^\t]+\t\))|[^\p{L}\p{N}])/', $match[0], -1, PREG_SPLIT_DELIM_CAPTURE);
426
427
                $return = "";
428
                $brLength = mb_strlen(trim($break, "\r\n"), $charset);
429
430
                $lineLength = $brLength;
431
                foreach ($parts as $part) {
432
                    // Replace character before/after links with a zero width space,
433
                    // and mark links as non-breakable
434
                    $breakLongLines = true;
435
                    if (strpos($part, "\t")) {
436
                        $part = str_replace("\t", mb_convert_encoding("\xE2\x80\x8C", $charset, "UTF-8"), $part);
437
                        $breakLongLines = false;
438
                    }
439
440
                    // Get part length
441
                    $partLength = mb_strlen($part, $charset);
442
443
                    // Ignore trailing space characters if this would cause the line break
444
                    if (($lineLength + $partLength) === ($textLineMaxLength + 1)) {
445
                        $lastChar = mb_substr($part, -1, 1, $charset);
446
                        if ($lastChar === " ") {
447
                            $part = mb_substr($part, 0, -1, $charset);
448
                            $partLength--;
449
                        }
450
                    }
451
452
                    // Check if enough chars left to add the part
453
                    if (($lineLength + $partLength) <= $textLineMaxLength) {
454
                        $return .= $part;
455
                        $lineLength += $partLength;
456
                    // Check if the part is longer than the line (so that we need to break it)
457
                    } elseif ($partLength > ($textLineMaxLength - $brLength)) {
458
                        if ($breakLongLines === true) {
459
                            $addPart = mb_substr($part, 0, ($textLineMaxLength - $lineLength), $charset);
460
                            $return .= $addPart;
461
                            $lineLength = $brLength;
462
463
                            for ($i = mb_strlen($addPart, $charset), $j = $partLength; $i < $j; $i+=($textLineMaxLength - $brLength)) {
464
                                $addPart = $break . mb_substr($part, $i, ($textLineMaxLength - $brLength), $charset);
465
                                $return .= $addPart;
466
                                $lineLength = mb_strlen($addPart, $charset) - 1;
467
                            }
468
                        } else {
469
                            $return .= $break . trim($part) . $break;
470
                            $lineLength = $brLength;
471
                        }
472
                    // Add a break to add the part in the next line
473
                    } else {
474
                        $return .= $break . rtrim($part);
475
                        $lineLength = $brLength + $partLength;
476
                    }
477
                }
478
                return $return;
479
            }, $text);
480
481
        $text = preg_replace('/^\s+|\s+$/', '', $text);
482
483
        return $text;
484
    }
485
486
    /**
487
     * Converts HTML tags to text, to create a text version of an HTML document.
488
     *
489
     * @param \DOMNodeList $nodes
490
     * @return string
491
     */
492
    protected function convertHtmlToText(\DOMNodeList $nodes)
493
    {
494
        $text = "";
495
496
        /** @var \DOMElement $node */
497
        foreach ($nodes as $node) {
498
            $lineBreaksBefore = 0;
499
            $lineBreaksAfter = 0;
500
            $lineCharBefore = "";
501
            $lineCharAfter = "";
502
            $prefix = "";
503
            $suffix = "";
504
505
            if (in_array($node->nodeName, ["h1", "h2", "h3", "h4", "h5", "h6", "h"])) {
506
                $lineCharAfter = "=";
507
                $lineBreaksAfter = 2;
508
            } elseif (in_array($node->nodeName, ["p", "td"])) {
509
                $lineBreaksAfter = 2;
510
            } elseif (in_array($node->nodeName, ["div"])) {
511
                $lineBreaksAfter = 1;
512
            }
513
514
            if ($node->nodeName === "h1") {
515
                $lineCharBefore = "*";
516
                $lineCharAfter = "*";
517
            } elseif ($node->nodeName === "h2") {
518
                $lineCharBefore = "=";
519
            }
520
521
            if ($node->nodeName === '#text') {
522
                $textContent = html_entity_decode($node->textContent, ENT_COMPAT | ENT_HTML401, $this->getCharset());
523
524
                // Replace tabs (used to mark links below) and other control characters
525
                $textContent = preg_replace("/[\r\n\f\v\t]+/", "", $textContent);
526
527
                if ($textContent !== "") {
528
                    $text .= $textContent;
529
                }
530
            } elseif ($node->nodeName === 'a') {
531
                $href = "";
532
                if ($node->attributes !== null) {
533
                    $hrefAttribute = $node->attributes->getNamedItem("href");
534
                    if ($hrefAttribute !== null) {
535
                        $href = (string)$hrefAttribute->nodeValue;
536
                    }
537
                }
538
                if ($href !== "") {
539
                    $suffix = " (\t" . $href . "\t)";
540
                }
541
            } elseif ($node->nodeName === 'b' || $node->nodeName === 'strong') {
542
                $prefix = "*";
543
                $suffix = "*";
544
            } elseif ($node->nodeName === 'hr') {
545
                $text .= str_repeat('-', 75) . "\n\n";
546
            }
547
548
            if ($node->hasChildNodes()) {
549
                $text .= str_repeat("\n", $lineBreaksBefore);
550
551
                $addText = $this->convertHtmlToText($node->childNodes);
552
553
                $text .= $prefix;
554
555
                $text .= $lineCharBefore ? str_repeat($lineCharBefore, 75) . "\n" : "";
556
                $text .= $addText;
557
                $text .= $lineCharAfter ? "\n" . str_repeat($lineCharAfter, 75) . "\n" : "";
558
559
                $text .= $suffix;
560
561
                $text .= str_repeat("\n", $lineBreaksAfter);
562
            }
563
        }
564
565
        // Remove unnecessary white spaces at he beginning/end of lines
566
        $text = preg_replace("/(?:^[ \t\f]+([^ \t\f])|([^ \t\f])[ \t\f]+$)/m", "\\1\\2", $text);
567
568
        // Replace multiple line-breaks
569
        $text = preg_replace("/[\r\n]{2,}/", "\n\n", $text);
570
571
        return $text;
572
    }
573
574
    /**
575
     * Gets all generally relevant style rules.
576
     * The selectors/declarations are checked in detail in prepareContent().
577
     *
578
     * @param $rules RuleAbstract[]
579
     * @return StyleRuleSet[]
580
     */
581
    protected function getRelevantStyleRules(array $rules)
582
    {
583
        $styleRules = [];
584
585
        foreach ($rules as $rule) {
586
            if ($rule instanceof StyleRuleSet) {
587
                $styleRules[] = $rule;
588
            } else if ($rule instanceof MediaRule) {
589
                foreach ($rule->getQueries() as $mediaQuery) {
590
                    // Only add styles in media rules, if the media rule is valid for "all" and "screen" media types
591
                    // @note: http://premailer.dialect.ca/ also supports "handheld", but this is really useless
592
                    $type = $mediaQuery->getType();
593
                    if ($type === MediaQuery::TYPE_ALL || $type === MediaQuery::TYPE_SCREEN) {
594
                        // ...and only if there are no additional conditions (like screen width etc.)
595
                        // which are dynamic and therefore need to be ignored.
596
                        $conditionCount = count($mediaQuery->getConditions());
597
                        if ($conditionCount === 0) {
598
                            foreach ($this->getRelevantStyleRules($rule->getRules()) as $styleRule) {
599
                                $styleRules[] = $styleRule;
600
                            }
601
                            break;
602
                        }
603
                    }
604
                }
605
            }
606
        }
607
608
        return $styleRules;
609
    }
610
611
    /**
612
     * Validate the scalar value of the passed option
613
     *
614
     * @param  string      $name
615
     * @param  string|int  $value
616
     * @return void
617
     *
618
     * @throws \InvalidArgumentException
619
     */
620
    protected function validateScalarOptionValue($name, $value)
621
    {
622
        switch ($name)
623
        {
624
            case self::OPTION_STYLE_TAG:
625
            case self::OPTION_HTML_CLASSES:
626
            case self::OPTION_HTML_COMMENTS:
627
            case self::OPTION_TEXT_LINE_WIDTH:
628
                if ( ! is_int($value))
629
                {
630
                    throw new InvalidArgumentException("The argument 1 of [setOption] method is expected to be a [integer] for option [{name}], but [" . gettype($value) . '] is given.');
631
                }
632
633
                break;
634
635
            case self::OPTION_CSS_WRITER_CLASS:
636
                if ( ! is_string($value))
637
                {
638
                    throw new InvalidArgumentException("The argument 1 of [setOption] method is expected to be a [string] for option [{name}], but [" . gettype($value) . '] is given.');
639
                }
640
641
                break;
642
        }
643
    }
644
645
    /**
646
     * Validate the possible value of the passed option
647
     *
648
     * @param  string      $name
649
     * @param  string|int  $value
650
     * @return void
651
     *
652
     * @throws \InvalidArgumentException
653
     */
654
    protected function validatePossibleOptionValue($name, $value)
655
    {
656
        switch ($name)
657
        {
658
            case self::OPTION_STYLE_TAG:
659
                if ( ! in_array($value, [self::OPTION_STYLE_TAG_BODY, self::OPTION_STYLE_TAG_HEAD, self::OPTION_STYLE_TAG_REMOVE]))
660
                {
661
                    throw new InvalidArgumentException("Invalid value [$value] for option [$name].");
662
                }
663
                break;
664
665
            case self::OPTION_HTML_CLASSES:
666
                if ( ! in_array($value, [self::OPTION_HTML_CLASSES_REMOVE, self::OPTION_HTML_CLASSES_KEEP]))
667
                {
668
                    throw new InvalidArgumentException("Invalid value [$value] for option [$name].");
669
                }
670
                break;
671
672
            case self::OPTION_HTML_COMMENTS:
673
                if ( ! in_array($value, [self::OPTION_HTML_COMMENTS_REMOVE, self::OPTION_HTML_COMMENTS_KEEP]))
674
                {
675
                    throw new InvalidArgumentException("Invalid value [$value] for option [$name].");
676
                }
677
                break;
678
679
            case self::OPTION_TEXT_LINE_WIDTH:
680
                if ($value <= 0)
681
                {
682
                    throw new LengthException("Value '" . gettype($value) . "' for option '$name' is to small.");
683
                }
684
                break;
685
686
            case self::OPTION_CSS_WRITER_CLASS:
687
                if (is_subclass_of($value, '\Crossjoin\Css\Writer\WriterAbstract', true) === false) {
688
                    throw new \InvalidArgumentException(
689
                        "Invalid value '$value' for option '$name'. " .
690
                        "The given class has to be a subclass of \\Crossjoin\\Css\\Writer\\WriterAbstract."
691
                    );
692
                }
693
        }
694
    }
695
696
}
697