Completed
Push — master ( 605748...35a41c )
by Kevin
05:21
created

BasePremailer::loadDocument()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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