1<?php
2/**
3 * DokuWiki plugin Typography; helper component
4 *
5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author Satoshi Sahara <sahara.satoshi@gmail.com>
7 */
8// must be run within Dokuwiki
9if (!defined('DOKU_INC')) die();
10
11class helper_plugin_typography_parser extends DokuWiki_Plugin
12{
13    protected $properties, $specifications;
14
15    function __construct() {
16        // allowable parameters and relevant CSS properties
17        $this->properties = array(
18            'wf' => 'wf',           // exceptional class="wf-webfont"
19            'ff' => 'font-family',
20            'fc' => 'color',
21            'bg' => 'background-color',
22            'fs' => 'font-size',
23            'fw' => 'font-weight',
24            'fv' => 'font-variant',
25            'lh' => 'line-height',
26            'ls' => 'letter-spacing',
27            'ws' => 'word-spacing',
28            'va' => 'vertical-align',
29            'sp' => 'white-space',
30              0  => 'text-shadow',
31              1  => 'text-transform',
32        );
33
34        // valid patterns of css properties
35        $this->specifications = array(
36            'wf' => '/^[a-zA-Z_-]+$/',
37            'ff' => '/^((\'[^,]+?\'|[^ ,]+?) *,? *)+$/',
38            'fc' => '/(^\#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$)|'
39                   .'(^rgb\((\d{1,3}%?,){2}\d{1,3}%?\)$)|'
40                   .'(^[a-zA-Z]+$)/',
41            'bg' => '/(^\#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$)|'
42                   .'(^rgb\((\d{1,3}%?,){2}\d{1,3}%?\)$)|'
43                   .'(^rgba\((\d{1,3}%?,){3}[\d.]+\)$)|'
44                   .'(^[a-zA-Z]+$)/',
45            'font-size' =>
46                 '/^(?:\d+(?:\.\d+)?(?:px|em|ex|pt|%)'
47                .'|(?:x{1,2}-)?small|medium|(?:x{1,2}-)?large|smaller|larger)$/',
48            'font-weight' =>
49                 '/^(?:\d00|normal|bold|bolder|lighter)$/',
50            'font-variant' =>
51                 '/^(?:normal|small-?caps)$/',
52            'line-height' =>
53                 '/^\d+(?:\.\d+)?(?:px|em|ex|pt|%)?$/',
54            'letter-spacing' =>
55                 '/^-?\d+(?:\.\d+)?(?:px|em|ex|pt|%)$/',
56            'word-spacing' =>
57                 '/^-?\d+(?:\.\d+)?(?:px|em|ex|pt|%)$/',
58            'vertical-align' =>
59                 '/^-?\d+(?:\.\d+)?(?:px|em|ex|pt|%)$|'
60                .'^(?:baseline|sub|super|top|text-top|middle|bottom|text-bottom|inherit)$/',
61            'white-space' =>
62                 '/^(?:normal|nowrap|pre|pre-line|pre-wrap)$/',
63        );
64    }
65
66    /**
67     * Get allowed CSS properties
68     *
69     * @return  array
70     */
71    public function getAllowedProperties()
72    {
73        return $this->properties;
74    }
75
76    /**
77     * Set allowed CSS properties
78     *
79     * @param array $props  allowable CSS property name
80     * @return  bool
81     */
82    public function setAllowedProperties(array $properties)
83    {
84        $this->properties = $properties;
85        return true;
86    }
87
88    /**
89     * validation of CSS property short name
90     *
91     * @param   string $name  short name of CSS property
92     * @return  bool  true if defined
93     */
94    public function is_short_property($name)
95    {
96        return isset($this->properties[$name]);
97    }
98
99    /**
100     * parse style attribute of an element
101     *
102     * @param   string $style  style attribute of an element
103     * @param   bool $filter   allow only CSS properties defined in $this->props
104     * @return  array  an associative array holds 'declarations' and 'classes'
105     */
106    public function parse_inlineCSS($style, $filter=true)
107    {
108        if (empty($style)) return array();
109
110        $elem = array(
111          //'tag'          => 'span',
112            'declarations' => array(), // css property:value pairs of style attribute
113            'classes'      => array(), // splitted class attribute
114        );
115
116        $tokens = explode(';', $style);
117
118        foreach ($tokens as $declaration) {
119            $item = array_map('trim', explode(':', $declaration, 2));
120            if (!isset($item[1])) continue;
121
122            // check CSS property name
123            if (isset($this->properties[$item[0]])) {
124                $name = $this->properties[$item[0]];
125            } elseif (in_array($item[0], $this->properties)) {
126                $name = $item[0];
127            } elseif ($filter === false) {
128                $name = $item[0];  // assume as CSS property
129            } else {
130                continue;          // ignore unknown property
131            }
132
133            // check CSS property value
134            if (isset($this->specifications[$name])) {
135                if (preg_match($this->specifications[$name], $item[1], $matches)) {
136                    $value = $item[1];
137                } else {
138                    continue; // ignore invalid property value
139                }
140                if (($name == 'font-variant') && ($value == 'smallcaps')) {
141                    $value = 'small-caps';
142                }
143            } else {
144                $value = htmlspecialchars($item[1], ENT_COMPAT, 'UTF-8');
145            }
146
147            if ($name == 'wf') {
148                // webfont : wf: webfont_class_without_prefix;
149                $elem['classes'] += array('webfont' => 'wf-'.$value);
150            } else {
151                // declaration : CSS property-value pairs
152                $elem['declarations'] += array($name => $value);
153            }
154        }
155
156        // unset empty attributes of an element
157        foreach (array_keys($elem) as $key) {
158           if (empty($elem[$key])) unset($elem[$key]);
159        }
160        return $elem;
161    }
162
163    /**
164     * build inline CSS for style attribute of an element
165     *
166     * @param   array $declarations  CSS property-value pairs
167     * @return  string  inline CSS for style attribute
168     */
169    public function build_inlineCSS(array $declarations)
170    {
171        $css = array();
172        foreach ($declarations as $name => $value) {
173            $css[] = $name.':'.$value.';';
174        }
175        return implode(' ', $css);
176    }
177
178    /**
179     * build style and class attribute of an element
180     *
181     * @param   array $elem  holds 'declarations' and 'classes'
182     * @param   array $addClasses  class items to be added
183     * @return  string  attributes of an element
184     */
185    public function build_attributes(array $elem, array $addClasses=array())
186    {
187        $attr = $css = $item = array();
188
189        if (isset($elem['declarations'])) {
190            foreach ($elem['declarations'] as $name => $value) {
191                $css[] = $name.':'.$value.';';
192            }
193            $attr['style'] = implode(' ', $css);
194        }
195
196        if (!empty($addClasses)) {
197            $elem['classes'] = isset($elem['classes']) ?: array();
198            $elem['classes'] = array_unique($elem['classes'] + $addClasses);
199        }
200
201        if (isset($elem['classes'])) {
202            $attr['class'] = implode(' ', $elem['classes']);
203        }
204
205        foreach ($attr as $key => $value) {
206            $item[] = $key.'="'.$value.'"';
207        }
208        $out = empty($item) ? '' : ' '.implode(' ', $item);
209        return $out;
210    }
211
212}
213