1<?php
2
3/**
4 * @todo Unit test
5 */
6class HTMLPurifier_ContentSets
7{
8
9    /**
10     * List of content set strings (pipe separators) indexed by name.
11     * @type array
12     */
13    public $info = array();
14
15    /**
16     * List of content set lookups (element => true) indexed by name.
17     * @type array
18     * @note This is in HTMLPurifier_HTMLDefinition->info_content_sets
19     */
20    public $lookup = array();
21
22    /**
23     * Synchronized list of defined content sets (keys of info).
24     * @type array
25     */
26    protected $keys = array();
27    /**
28     * Synchronized list of defined content values (values of info).
29     * @type array
30     */
31    protected $values = array();
32
33    /**
34     * Merges in module's content sets, expands identifiers in the content
35     * sets and populates the keys, values and lookup member variables.
36     * @param HTMLPurifier_HTMLModule[] $modules List of HTMLPurifier_HTMLModule
37     */
38    public function __construct($modules)
39    {
40        if (!is_array($modules)) {
41            $modules = array($modules);
42        }
43        // populate content_sets based on module hints
44        // sorry, no way of overloading
45        foreach ($modules as $module) {
46            foreach ($module->content_sets as $key => $value) {
47                $temp = $this->convertToLookup($value);
48                if (isset($this->lookup[$key])) {
49                    // add it into the existing content set
50                    $this->lookup[$key] = array_merge($this->lookup[$key], $temp);
51                } else {
52                    $this->lookup[$key] = $temp;
53                }
54            }
55        }
56        $old_lookup = false;
57        while ($old_lookup !== $this->lookup) {
58            $old_lookup = $this->lookup;
59            foreach ($this->lookup as $i => $set) {
60                $add = array();
61                foreach ($set as $element => $x) {
62                    if (isset($this->lookup[$element])) {
63                        $add += $this->lookup[$element];
64                        unset($this->lookup[$i][$element]);
65                    }
66                }
67                $this->lookup[$i] += $add;
68            }
69        }
70
71        foreach ($this->lookup as $key => $lookup) {
72            $this->info[$key] = implode(' | ', array_keys($lookup));
73        }
74        $this->keys   = array_keys($this->info);
75        $this->values = array_values($this->info);
76    }
77
78    /**
79     * Accepts a definition; generates and assigns a ChildDef for it
80     * @param HTMLPurifier_ElementDef $def HTMLPurifier_ElementDef reference
81     * @param HTMLPurifier_HTMLModule $module Module that defined the ElementDef
82     */
83    public function generateChildDef(&$def, $module)
84    {
85        if (!empty($def->child)) { // already done!
86            return;
87        }
88        $content_model = $def->content_model;
89        if (is_string($content_model)) {
90            // Assume that $this->keys is alphanumeric
91            $def->content_model = preg_replace_callback(
92                '/\b(' . implode('|', $this->keys) . ')\b/',
93                array($this, 'generateChildDefCallback'),
94                $content_model
95            );
96            //$def->content_model = str_replace(
97            //    $this->keys, $this->values, $content_model);
98        }
99        $def->child = $this->getChildDef($def, $module);
100    }
101
102    public function generateChildDefCallback($matches)
103    {
104        return $this->info[$matches[0]];
105    }
106
107    /**
108     * Instantiates a ChildDef based on content_model and content_model_type
109     * member variables in HTMLPurifier_ElementDef
110     * @note This will also defer to modules for custom HTMLPurifier_ChildDef
111     *       subclasses that need content set expansion
112     * @param HTMLPurifier_ElementDef $def HTMLPurifier_ElementDef to have ChildDef extracted
113     * @param HTMLPurifier_HTMLModule $module Module that defined the ElementDef
114     * @return HTMLPurifier_ChildDef corresponding to ElementDef
115     */
116    public function getChildDef($def, $module)
117    {
118        $value = $def->content_model;
119        if (is_object($value)) {
120            trigger_error(
121                'Literal object child definitions should be stored in '.
122                'ElementDef->child not ElementDef->content_model',
123                E_USER_NOTICE
124            );
125            return $value;
126        }
127        switch ($def->content_model_type) {
128            case 'required':
129                return new HTMLPurifier_ChildDef_Required($value);
130            case 'optional':
131                return new HTMLPurifier_ChildDef_Optional($value);
132            case 'empty':
133                return new HTMLPurifier_ChildDef_Empty();
134            case 'custom':
135                return new HTMLPurifier_ChildDef_Custom($value);
136        }
137        // defer to its module
138        $return = false;
139        if ($module->defines_child_def) { // save a func call
140            $return = $module->getChildDef($def);
141        }
142        if ($return !== false) {
143            return $return;
144        }
145        // error-out
146        trigger_error(
147            'Could not determine which ChildDef class to instantiate',
148            E_USER_ERROR
149        );
150        return false;
151    }
152
153    /**
154     * Converts a string list of elements separated by pipes into
155     * a lookup array.
156     * @param string $string List of elements
157     * @return array Lookup array of elements
158     */
159    protected function convertToLookup($string)
160    {
161        $array = explode('|', str_replace(' ', '', $string));
162        $ret = array();
163        foreach ($array as $k) {
164            $ret[$k] = true;
165        }
166        return $ret;
167    }
168}
169
170// vim: et sw=4 sts=4
171