Overview
  • Namespace
  • Class

Namespaces

  • apemsel
    • AttributedString

Classes

  • apemsel\AttributedString\AttributedString
  • apemsel\AttributedString\MutableAttributedString
  • apemsel\AttributedString\TokenizedAttributedString
  1 <?php
  2 namespace apemsel\AttributedString;
  3 
  4 /**
  5  * Basic class to work with attributed strings.
  6  *
  7  * Attributed strings are strings that can have multiple attributes per character of the string
  8  *
  9  * @author Adrian Pemsel <apemsel@gmail.com>
 10  */
 11 class AttributedString implements \Countable
 12 {
 13   protected $string;
 14   protected $attributes;
 15   protected $length;
 16   protected $byteToChar;
 17   
 18   /**
 19    * @param string|AttributedString $string Either a simple string or another AttributedString to init the AttributedString
 20    */
 21   public function __construct($string) {
 22     if (is_string($string)) {
 23       $this->string = $string;
 24       $this->length = mb_strlen($string, "utf-8");
 25     }
 26     elseif ($string instanceof AttributedString) {
 27       $this->string = $string->string;
 28       $this->attributes = $string->attributes;
 29       $this->lenght = $string->length;
 30       $this->byteToChar = $string->byteToChar;
 31     }
 32     else {
 33       throw new \InvalidArgumentException();
 34     }
 35   }
 36   
 37   /**
 38    * Returns the native string
 39    *
 40    * @return string The native string representation of the AttributedString without attributes
 41    */
 42   public function __toString() {
 43     return $this->string;
 44   }
 45   
 46   /**
 47    * Creates a new attribute layer
 48    *
 49    * @param string $attribute The name of the new attribute
 50    * @throws InvalidArgumentException if the attribute already exists
 51    */
 52   public function createAttribute($attribute) {
 53     if ($this->hasAttribute($attribute)) {
 54       throw new \InvalidArgumentException();
 55     }
 56     
 57     $this->attributes[$attribute] = array_fill(0, $this->length, false);
 58   }
 59   
 60   /**
 61    * Check if the given attribute exists
 62    *
 63    * @param string $attribute The name of the attribute to check
 64    * @return bool
 65    */
 66   public function hasAttribute($attribute) {
 67     return isset($this->attributes[$attribute]);
 68   }
 69   
 70   public function deleteAttribute($attribute) {
 71     if (isset($this->attributes[$attribute])) {
 72       unset($this->attributes[$attribute]);
 73     }
 74   }
 75   
 76   /**
 77    * Set given range of the string to an attribute and state
 78    *
 79    * @param int $from start offset
 80    * @param int $to end offset
 81    * @param string $attribute name of the attribute to be set
 82    * @param bool $state set state to true (default) or false
 83    */
 84   public function setRange($from, $to, $attribute, $state = true) {
 85     // Ensure correct range
 86     $from = min($from, $this->length);
 87     $from = max($from, 0);
 88     $to = min($to, $this->length);
 89     $to = max($to, 0);
 90     
 91     // Be kind and swap from and to if mixed up
 92     if ($from>$to) {
 93       list($from, $to) = [$to, $from];
 94     }
 95     
 96     // Create attribute if it does not exist
 97     if (!$this->hasAttribute($attribute)) {
 98       $this->createAttribute($attribute);
 99     }
100 
101     // Set attribute state for given range
102     $this->attributes[$attribute] = array_replace($this->attributes[$attribute], array_fill($from, $to-$from+1, $state));
103   }
104   
105   /**
106    * Set given length of the string to an attribute and state
107    *
108    * @param int $from start offset
109    * @param int $length length to be set
110    * @param string $attribute name of the attribute to be set
111    * @param bool $state set state to true (default) or false
112    */
113   public function setLength($from, $length, $attribute, $state = true) {
114     return $this->setRange($from, $from + $length - 1, $attribute, $state);
115   }
116   
117   /**
118    * Set parts of the string matching a given regex to an attribute and state
119    *
120    * @param string $pattern regex pattern
121    * @param string $attribute name of the attribute to be set
122    * @param bool $state set state to true (default) or false
123    * @return int number of matches
124    */
125   public function setPattern($pattern, $attribute, $state = true) {
126     if ($ret = preg_match_all($pattern, $this->string, $matches, PREG_OFFSET_CAPTURE)) {
127       foreach($matches[0] as $match)
128       {
129         $match[1] = $this->byteToCharOffset($match[1]);
130         $this->setRange($match[1], $match[1]+mb_strlen($match[0], "utf-8")-1, $attribute, $state);
131       }
132 
133       return $ret;
134     }
135   }
136   
137   /**
138    * Set given substring to an attribute and state
139    *
140    * @param string $substring the substring to search
141    * @param string $attribute name of the attribute to be set
142    * @param bool $all set first or all occurences of the substring
143    * @param bool $matchCase match or ignore case
144    * @param bool $state set state to true (default) or false
145    */
146   public function setSubstring($substring, $attribute, $all = true, $matchCase = true, $state = true) {
147     $offset = 0;
148     $length = mb_strlen($substring, "utf-8");
149     $func = $matchCase ? "mb_strpos" : "mb_stripos";
150     
151     while (false !== $pos = $func($this->string, $substring, $offset, "utf-8")) {
152       $this->setRange($pos, $pos + $length - 1, $attribute, $state);
153       if (!$all) {
154         return;
155       }
156       $offset = $pos + $length;
157     }
158   }
159   
160   /**
161    * Search inside the string for ranges with the given attribute
162    *
163    * @param string $attribute name of the attribute to search
164    * @param int $offset start offset
165    * @param bool $returnLength if true (default is false), return an array with position and length of the found range
166    * @param bool $state the state to look for (default is true)
167    * @param bool $strict perform strict comparison during search
168    * @return int|int[] either position or position and lenght in an array
169    */
170   public function searchAttribute($attribute, $offset = 0, $returnLength = false, $state = true, $strict = true) {
171     if (!$this->hasAttribute($attribute)) {
172       return false;
173     }
174     
175     $a = $this->attributes[$attribute];
176 
177     if ($offset) {
178       $a = array_slice($a, $offset, NULL, true);
179     }
180     
181     $pos = array_search($state, $a, $strict);
182     
183     if ($returnLength) {
184       if (false === $pos) {
185         return false;
186       }
187       
188       $a = array_slice($a, $pos - $offset);
189       $length = array_search(!$state, $a, $strict);
190       $length = $length ? $length : $this->length - $pos;
191 
192       return [$pos, $length];
193     } else {
194       return $pos;
195     }
196   }
197   
198   /**
199    * Check for given attribute at a offset
200    *
201    * @param string $attribute name of the attribute to check
202    * @param int $pos offset to check
203    * @return bool true if string has the attribute at the given position
204    */
205   public function is($attribute, $pos) {
206     return (isset($this->attributes[$attribute][$pos]) and $this->attributes[$attribute][$pos]);
207   }
208   
209   /**
210    * Return an array of substrings that have a given attribute
211    *
212    * @param string $attribute name of the attribute
213    * @param int $pos offset
214    * @param bool $state the state to look for (default is true)
215    * @param bool $strict perform strict comparison during search
216    * @return string[] array of strings with given attribute
217    */
218   public function substrings($attribute, $offset = 0, $state = true, $strict = true)
219   {
220     $substrings = [];
221     while (false !== $pl = $this->searchAttribute($attribute, $offset, true, $state, $strict))
222     {
223       //var_dump($pl);
224       $substring = mb_substr($this->string, $pl[0], $pl[1], "UTF-8");
225       $substrings[] = $substring;
226       $offset = $pl[0] + $pl[1];
227     }
228     
229     return $substrings;
230   }
231   
232   /**
233    * Return all parts of the string that have a given attribute as new string
234    *
235    * @param string $attribute name of the attribute
236    * @param int $pos offset
237    * @param bool $state the state to look for (default is true)
238    * @param bool $strict perform strict comparison during search
239    * @param string $glue glue that is inserted between the parts, default is nothing ("")
240    * @return string combined filtered string
241    */
242   public function filter($attribute, $offset = 0, $state = true, $strict = true, $glue = "")
243   {
244     return implode($glue, $this->substrings($attribute, $offset, $state, $strict));
245   }
246   
247   /**
248    * Return all attributes at a given offset
249    *
250    * @param int $pos offset
251    * @return string[] attributes at the given offset
252    */
253   public function attributesAt($pos) {
254     $attributes = [];
255 
256     foreach ($this->attributes as $attribute => &$map) {
257       if ($map[$pos]) {
258         $attributes[] = $attribute;
259       }
260     }
261 
262     return $attributes;
263   }
264   
265   /**
266    * Convert to HTML, using a given class to mark attribute spans
267    *
268    * @param string $tag HTML tag to use for the spans (defaults is "<span>")
269    * @param string $classPrefix Optional prefix used to convert the attribute names to class names
270    * @return string HTML
271    * @throws Exception if the AttributedString cannot be converted to HTML due to improper nesting
272    */
273   public function toHtml($tag = "span", $classPrefix = "") {
274     foreach($this->attributes as $attribute => $map) $state[$attribute] = false;
275 
276     $html = "";
277     $stack = [];
278     $lastPos = 0;
279 
280     for ($i=0; $i<$this->length; $i++)
281     {
282       foreach($this->attributes as $attribute => &$map)
283       {
284         if ($this->attributes[$attribute][$i] != $state[$attribute])
285         {
286           $state[$attribute] = $this->attributes[$attribute][$i];
287 
288           $html .= mb_substr($this->string, $lastPos, $i-$lastPos, "utf-8");
289           $lastPos = $i;
290 
291           if ($state[$attribute])
292           {
293             $html .= "<$tag class=\"$classPrefix$attribute\">";
294             $stack[] = $attribute;
295           }
296           else
297           {
298             if ($attribute != array_pop($stack))
299             {
300               throw new Exception("Attributes are not properly nested for HTML conversion");
301             }
302             $html .= "</$tag>";
303           }
304         }
305       }
306     }
307 
308     $html .= mb_substr($this->string, $lastPos, $this->length-$lastPos, 'utf-8');
309 
310     // Close all spans that remained open
311     $html .= str_repeat("</$tag>", count($stack));
312 
313     return $html;
314   }
315   
316   /**
317    * Combine attributes with the given boolean operation
318    *
319    * @param string $op one of or|xor|and|not
320    * @param string $attribute1 name of the first attribute
321    * @param string $attribute2 Name of the second attribute. Ignored for "not" operation.
322    * @param string $to optional name of the attribute to copy the result to
323    * @throws InvalidArgumentException if one of the attributes does not exist or an unkown operation is given
324    */
325   public function combineAttributes($op, $attribute1, $attribute2 = false, $to = false)
326   {
327     $to = isset($to) ? $to : $attribute1;
328     $op = strtolower($op);
329     
330     if ($op == "not") {
331       $attribute2 = $attribute1;
332     }
333 
334     if (!$this->hasAttribute($attribute1) or !$this->hasAttribute($attribute2)) {
335       throw new \InvalidArgumentException("Attribute does not exist");
336     }
337     
338     if (!isset($this->attributes[$to])) {
339       $this->attributes[$to] = []; // No need to init because array is created below
340     }
341     
342     // Switch outside the loops for speed
343     switch ($op) {
344       case 'or':
345         for($i = 0; $i < $this->length; $i++) {
346           $this->attributes[$to][$i] = $this->attributes[$attribute1][$i] || $this->attributes[$attribute2][$i];
347         }
348       break;
349       
350       case 'xor':
351         for($i = 0; $i < $this->length; $i++) {
352           $this->attributes[$to][$i] = ($this->attributes[$attribute1][$i] xor $this->attributes[$attribute2][$i]);
353         }
354       break;
355 
356       case 'and':
357         for($i = 0; $i < $this->length; $i++) {
358           $this->attributes[$to][$i] = $this->attributes[$attribute1][$i] && $this->attributes[$attribute2][$i];
359         }
360       break;
361 
362       case 'not':
363         for($i = 0; $i < $this->length; $i++) {
364           $this->attributes[$to][$i] = !$this->attributes[$attribute1][$i];
365         }
366       break;
367       
368       default:
369         throw new \InvalidArgumentException("Unknown operation");
370     }
371   }
372   
373   /**
374    * Enable and fill cache for byte to char offset conversion
375    *
376    * May improve performance if setPattern is used extensively
377    */
378   public function enablebyteToCharCache() {
379     $this->byteToChar = [];
380     $char = 0;
381     for ($i = 0; $i < strlen($this->string); ) {
382       $char++;
383       $byte = $this->string[$i];
384       $cl = self::utf8CharLen($byte);
385       $i += $cl;
386       
387       $this->byteToChar[$i] = $char;
388     }
389   }
390   
391   protected function byteToCharOffset($boff) {
392     if (isset($this->byteToChar[$boff])) return $this->byteToChar[$boff];
393     
394     return $this->byteToChar[$boff] = self::byteToCharOffsetString($this->string, $boff);
395   }
396 
397   protected function charToByteOffset($char) {
398     $byte = strlen(mb_substr($this->string, 0, $char, "utf-8"));
399     if (!isset($this->byteToChar[$byte])) $this->byteToChar[$byte] = $char;
400     
401     return $byte;
402   }
403   
404   protected static function byteToCharOffsetString($string, $boff) {
405     $result = 0;
406     
407     for ($i = 0; $i < $boff; ) {
408       $result++;
409       $byte = $string[$i];
410       $cl = self::utf8CharLen($byte);
411       $i += $cl;
412     }
413     
414     return $result;
415   }
416   
417   protected static function utf8CharLen($byte) {
418     $base2 = str_pad(base_convert((string) ord($byte), 10, 2), 8, "0", STR_PAD_LEFT);
419     $p = strpos($base2, "0");
420     
421     if ($p == 0) {
422       return 1;
423     } elseif ($p <= 4) {
424       return $p;
425     } else {
426       throw new \InvalidArgumentException();
427     }
428   }
429   
430   /**
431    * Return string length (number of UTF-8 chars, not strlen())
432    *
433    * @return int string length
434    */
435   public function count() {
436     return $this->length;
437   }
438 }
439 
API documentation generated by ApiGen