1: | <?php |
2: | |
3: | /** |
4: | * A zipper is a purely-functional data structure which contains |
5: | * a focus that can be efficiently manipulated. It is known as |
6: | * a "one-hole context". This mutable variant implements a zipper |
7: | * for a list as a pair of two arrays, laid out as follows: |
8: | * |
9: | * Base list: 1 2 3 4 [ ] 6 7 8 9 |
10: | * Front list: 1 2 3 4 |
11: | * Back list: 9 8 7 6 |
12: | * |
13: | * User is expected to keep track of the "current element" and properly |
14: | * fill it back in as necessary. (ToDo: Maybe it's more user friendly |
15: | * to implicitly track the current element?) |
16: | * |
17: | * Nota bene: the current class gets confused if you try to store NULLs |
18: | * in the list. |
19: | */ |
20: | |
21: | class HTMLPurifier_Zipper |
22: | { |
23: | public $front, $back; |
24: | |
25: | public function __construct($front, $back) { |
26: | $this->front = $front; |
27: | $this->back = $back; |
28: | } |
29: | |
30: | /** |
31: | * Creates a zipper from an array, with a hole in the |
32: | * 0-index position. |
33: | * @param Array to zipper-ify. |
34: | * @return Tuple of zipper and element of first position. |
35: | */ |
36: | static public function fromArray($array) { |
37: | $z = new self(array(), array_reverse($array)); |
38: | $t = $z->delete(); // delete the "dummy hole" |
39: | return array($z, $t); |
40: | } |
41: | |
42: | /** |
43: | * Convert zipper back into a normal array, optionally filling in |
44: | * the hole with a value. (Usually you should supply a $t, unless you |
45: | * are at the end of the array.) |
46: | */ |
47: | public function toArray($t = NULL) { |
48: | $a = $this->front; |
49: | if ($t !== NULL) $a[] = $t; |
50: | for ($i = count($this->back)-1; $i >= 0; $i--) { |
51: | $a[] = $this->back[$i]; |
52: | } |
53: | return $a; |
54: | } |
55: | |
56: | /** |
57: | * Move hole to the next element. |
58: | * @param $t Element to fill hole with |
59: | * @return Original contents of new hole. |
60: | */ |
61: | public function next($t) { |
62: | if ($t !== NULL) array_push($this->front, $t); |
63: | return empty($this->back) ? NULL : array_pop($this->back); |
64: | } |
65: | |
66: | /** |
67: | * Iterated hole advancement. |
68: | * @param $t Element to fill hole with |
69: | * @param $i How many forward to advance hole |
70: | * @return Original contents of new hole, i away |
71: | */ |
72: | public function advance($t, $n) { |
73: | for ($i = 0; $i < $n; $i++) { |
74: | $t = $this->next($t); |
75: | } |
76: | return $t; |
77: | } |
78: | |
79: | /** |
80: | * Move hole to the previous element |
81: | * @param $t Element to fill hole with |
82: | * @return Original contents of new hole. |
83: | */ |
84: | public function prev($t) { |
85: | if ($t !== NULL) array_push($this->back, $t); |
86: | return empty($this->front) ? NULL : array_pop($this->front); |
87: | } |
88: | |
89: | /** |
90: | * Delete contents of current hole, shifting hole to |
91: | * next element. |
92: | * @return Original contents of new hole. |
93: | */ |
94: | public function delete() { |
95: | return empty($this->back) ? NULL : array_pop($this->back); |
96: | } |
97: | |
98: | /** |
99: | * Returns true if we are at the end of the list. |
100: | * @return bool |
101: | */ |
102: | public function done() { |
103: | return empty($this->back); |
104: | } |
105: | |
106: | /** |
107: | * Insert element before hole. |
108: | * @param Element to insert |
109: | */ |
110: | public function insertBefore($t) { |
111: | if ($t !== NULL) array_push($this->front, $t); |
112: | } |
113: | |
114: | /** |
115: | * Insert element after hole. |
116: | * @param Element to insert |
117: | */ |
118: | public function insertAfter($t) { |
119: | if ($t !== NULL) array_push($this->back, $t); |
120: | } |
121: | |
122: | /** |
123: | * Splice in multiple elements at hole. Functional specification |
124: | * in terms of array_splice: |
125: | * |
126: | * $arr1 = $arr; |
127: | * $old1 = array_splice($arr1, $i, $delete, $replacement); |
128: | * |
129: | * list($z, $t) = HTMLPurifier_Zipper::fromArray($arr); |
130: | * $t = $z->advance($t, $i); |
131: | * list($old2, $t) = $z->splice($t, $delete, $replacement); |
132: | * $arr2 = $z->toArray($t); |
133: | * |
134: | * assert($old1 === $old2); |
135: | * assert($arr1 === $arr2); |
136: | * |
137: | * NB: the absolute index location after this operation is |
138: | * *unchanged!* |
139: | * |
140: | * @param Current contents of hole. |
141: | */ |
142: | public function splice($t, $delete, $replacement) { |
143: | // delete |
144: | $old = array(); |
145: | $r = $t; |
146: | for ($i = $delete; $i > 0; $i--) { |
147: | $old[] = $r; |
148: | $r = $this->delete(); |
149: | } |
150: | // insert |
151: | for ($i = count($replacement)-1; $i >= 0; $i--) { |
152: | $this->insertAfter($r); |
153: | $r = $replacement[$i]; |
154: | } |
155: | return array($old, $r); |
156: | } |
157: | } |
158: |