Make WordPress Core

Ticket #14481: tagcodes.php

File tagcodes.php, 12.4 KB (added by deadowl, 14 years ago)

Took the suggestion of using filters as an interface, fixed some parsing bugs, put a leading underscore in front of internal functions, changed function names to use "tagcode" instead of "shortcode"

Line 
1<?php
2/*
3Plugin Name: Tagcode Interpreter
4Description: Can be used to replace buggy shortcodes
5Version: 0.9
6Author: Jacob Beauregard
7*/
8
9/**
10 * global index
11*/
12$_tagcode_register = array();
13
14/**
15 * tagcode public interface
16*/
17function add_tagcode($name,$handler,$priority=10) {
18        global $_tagcode_register;
19        $_tagcode_register[$name] = true;
20        return add_filter(_tagcode_ref_name($name),$handler,$priority,3);
21}
22function apply_tagcodes($name,$content,$attrs) {
23        $ref = array($name,$content,$attrs);
24        return apply_filters_ref_array(_tagcode_ref_name($name),$ref);
25}
26function eval_tagcodes($content) {
27        return _tagcode_eval($content);
28}
29function has_tagcode($name,$handler=false) {
30        return has_filter(_tagcode_ref_name($name),$handler);
31}
32function remove_all_tagcodes($name,$priority=false) {
33        global $_tagcode_register;
34        $value = remove_all_filters(_tagcode_ref_name($name),$priority);
35        if (!has_tagcode($name)) {
36                $_tagcode_register[$name] = false;
37        }
38        return $value;
39}
40function remove_tagcode($name,$handler,$priority=10) {
41        global $_tagcode_register;
42        $value = remove_filter(_tagcode_ref_name($name),$handler,$priority,3);
43        if (!has_tagcode($name)) {
44                $_tagcode_register[$name] = false;
45        }
46        return $value;
47}
48
49/**
50 * alias to use in filter for given tag name
51*/
52function _tagcode_ref_name($name) {
53        return "_tagcode_".strtolower($args[0]);
54}
55
56/**
57 * types of tagcode expressions
58*/
59function _tagcode_types() {
60        return array(
61                'esc_lsqbr',
62                'esc_rsqbr',
63                'tag_inline',
64                'tag_open',
65                'tag_close',
66                'attr',
67                'literal',
68                'entity_ref',
69                'char_ref',
70                'char_str',
71                'name',
72                'text'
73        );
74}
75
76/*
77 * tagcode expressions that are subtypes of other tagcode expressions
78*/
79function _tagcode_sub_root() {
80        //top level types
81        return array('esc_rsqbr','esc_lsqbr','tag_inline','tag_open','tag_close','text');
82}
83function _tagcode_sub_tag_inline() {
84        return array('attr','name');
85}
86function _tagcode_sub_tag_open() {
87        return array('attr','name');
88}
89function _tagcode_sub_tag_close() {
90        return array('name');
91}
92function _tagcode_sub_text() {
93        return null;
94}
95function _tagcode_sub_name() {
96        return null;
97}
98function _tagcode_sub_attr() {
99        return array('name','literal');
100}
101function _tagcode_sub_literal() {
102        return null;
103        //return array('char_ref','entity_ref','char_str');
104}
105function _tagcode_sub_char_str() {
106        return null;
107}
108function _tagcode_sub_char_ref() {
109        return null;
110}
111function _tagcode_sub_entity_ref() {
112        return null;
113}
114function _tagcode_sub_esc_lsqbr() {
115        return null;
116}
117function _tagcode_sub_esc_rsqbr() {
118        return null;
119}
120
121/**
122 * returns subtypes of expression type
123*/
124function _tagcode_sub($type='root') {
125        if (in_array($type,_tagcode_types()) || $type == 'root') {
126                $func = "_tagcode_sub_{$type}";
127                $subtypes = call_user_func($func);
128        }
129        return $subtypes;
130}
131
132/**
133 * regular expressions to match valid expressions
134*/
135function _tagcode_re_name($named=false) {
136        $expr = '[A-Za-z][-A-Za-z0-9_:.]*';
137        return $named?"(?P<name>{$expr})":$expr;
138}
139function _tagcode_re_registered_name($named=false) {
140        global $_tagcode_register;
141        $tags = join("|",array_map('preg_quote',array_keys($_tagcode_register)));
142        if (strlen($tags) === 0) {
143                //to guarantee failure in the instance there are no registered tagcodes
144                $expr = '$^';
145        } else {
146                $name = _tagcode_re_name();
147                $expr = "{$name}(?<={$tags})";
148        }
149        return $named?"(?P<name>{$expr})":$expr;
150}
151function _tagcode_re_char_str($named=false) {
152        $expr = "[^%&]+";
153        return $named?"(?P<char_str>{$expr})":$expr;
154}
155function _tagcode_re_char_str_sq($named=false) {
156        //single quote version for shortcode_re_char_str
157        $expr =  "[^%&']+";
158        return $named?"(?P<char_str>{$expr})":$expr;
159}
160function _tagcode_re_char_str_dq($named=false) {
161        //double quote version for shortcode_re_char_str
162        $expr = '[^%&"]+';
163        return $named?"(?P<char_str>{$expr})":$expr;
164}
165function _tagcode_re_char_ref($named=false) {
166        $expr = "&#(?:[0-9]+|x[0-9a-fA-F]+);";
167        return $named?"(?P<char_ref>{$expr})":$expr;
168}
169function _tagcode_re_entity_ref($named=false) {
170        $name = _tagcode_re_name();
171        $expr = "&{$name};";
172        return $named?"(?P<entity_ref>{$expr})":$expr;
173}
174function _tagcode_re_literal($named=false) {
175        $nq = _tagcode_re_literal_nq();
176        $sq = _tagcode_re_literal_sq();
177        $dq = _tagcode_re_literal_dq();
178        $expr = "(?:{$nq}|{$sq}|{$dq})";
179        return $named?"(?P<literal>{$expr})":$expr;
180}
181function _tagcode_re_literal_nq() {
182        //literal without quotes
183        $expr = "[-a-zA-Z0-9_:.]+";
184        return $expr;
185}
186function _tagcode_re_literal_sq() {
187        //literal with single quotes
188        $char_str = _tagcode_re_char_str_sq();
189        $char_ref = _tagcode_re_char_ref();
190        $entity_ref = _tagcode_re_entity_ref();
191        $expr = "'(?:{$char_str}|{$char_ref}|{$entity_ref})*'";
192        return $expr;
193}
194function _tagcode_re_literal_dq() {
195        //literal with double quotes
196        $char_str = _tagcode_re_char_str_dq();
197        $char_ref = _tagcode_re_char_ref();
198        $entity_ref = _tagcode_re_entity_ref();
199        $expr =  "\"(?:{$char_str}|{$char_ref}|{$entity_ref})*\"";
200        return $expr;
201}
202function _tagcode_re_attr($named=false) {
203        $name  = _tagcode_re_name();
204        $literal = _tagcode_re_literal();
205        $expr = "(?<!^){$name}(?:={$literal})?";
206        return $named?"(?P<attr>{$expr})":$expr;
207}
208function _tagcode_re_esc_lsqbr($named=false) {
209        $expr = "\\[\\[";
210        return $named?"(?P<esc_lsqbr>{$expr})":$expr;
211}
212function _tagcode_re_esc_rsqbr($named=false) {
213        //two right square brackets not immediately followed by an odd number of sequential right square brackets
214        $expr = '\]\](?=(?:\]\])*(?!\]))';
215        return $named?"(?P<esc_rsqbr>{$expr})":$expr;
216}
217function _tagcode_re_tag_inline($named=false) {
218        $name = _tagcode_re_registered_name();
219        $attr = _tagcode_re_attr();
220        $expr = "{$name}(?:\\s+{$attr})*";
221        return $named?"\\[(?P<tag_inline>{$expr})\\s*\\/\\]":"\\[{$expr}\\s*\\/\\]";
222}
223function _tagcode_re_tag_open($named=false) {
224        $name = _tagcode_re_registered_name();
225        $attr = _tagcode_re_attr();
226        $expr = "{$name}(?:\\s+{$attr})*";
227        return $named?"\\[(?P<tag_open>{$expr})\\s*\\]":"\\[{$expr}\\s*\\]";
228}
229function _tagcode_re_tag_close($named=false) {
230        $name = _tagcode_re_registered_name();
231        $expr = $name;
232        return $named?"\\[\\/(?P<tag_close>{$expr})\\s*\\]":"\\[\\/{$expr}\\s*\\]";
233}
234function _tagcode_re_text($named=false) {
235        $expr = '(?s:.[^\[\]]*)';
236        return $named?"(?P<text>{$expr})":$expr;
237}
238
239/**
240 * combines shortcode regex of types specified in parameters, returns delimited regex
241*/
242function _tagcode_re_combine() {
243        $types = func_get_args();
244
245        $regexps = array();
246        foreach ($types as $type) {
247                $re_func = "_tagcode_re_{$type}";
248                array_push($regexps,call_user_func($re_func,1));
249        }
250        $statement = '/' . join('|',$regexps) . '/';
251        return $statement;
252}
253
254/**
255 * returns regular expression to match subtypes of an expression (ex. attr => {$name}={$literal})
256*/
257function _tagcode_re_subtypes($type) {
258        $subtypes = _tagcode_sub($type);
259        $re = null;
260        if (count($subtypes)) {
261                $re = call_user_func_array('_tagcode_re_combine',$subtypes);
262        }
263        return $re;
264}
265
266/**
267 * evaluation of different tagcode expressions
268*/
269function _tagcode_eval_name($name) {
270        return strtolower($name['expression']);
271}
272function _tagcode_eval_char_str($char_str) {
273        return $char_str['expression'];
274}
275function _tagcode_eval_char_ref($char_ref) {
276        //Not sure if this is the proper way to handle char_refs.
277        //I'm doing this rather than running html_entity_decode on a full string
278        //because I don't understand html_entity_decode's argument for a
279        //character set. Which also means that this code is probably incorrect.
280        $expr = $char_ref['expression'];
281        if ($expr[0] == 'x') {
282                $expr = hexdec($expr);
283        }
284        $expr = chr($expr);
285        return $expr;
286}
287function _tagcode_eval_entity_ref($entity_ref) {
288        $expr = strtolower($entity_ref['expression']);
289        $lookup = array_flip(get_html_translation_table(HTML_ENTITIES));
290        return $lookup[$expr];
291}
292function _tagcode_eval_literal($literal) {
293        //$expr = "";
294        //foreach ($literal['children'] as $child) {
295        //      $eval_func = "_tagcode_eval_{$child['type']}";
296        //      $expr .= call_user_func($eval_func,$child);
297        //}
298        $expr = $literal['expression'];
299        $expr = html_entity_decode($expr);
300        if ($expr[0] == "'" || $expr[0] == '"') {
301                $expr = substr($expr,1,strlen($expr)-2);
302        }
303        return $expr;
304}
305function _tagcode_eval_attr($attr) {
306        $children = $attr['children'];
307        $key = _tagcode_eval_name($children[0]);
308        if (isset($children[1])) {
309                $value = _tagcode_eval_literal($children[1]);
310        } else {
311                $value = $key;
312        }
313        return array('key' => $key, 'value' => $value);
314}
315function _tagcode_eval_esc_lsqbr($esc_lsqbr,&$stack) {
316        $node = array_pop($stack);
317        $node['content'] .= "[";
318        array_push($stack,$node);
319        return $node;
320}
321function _tagcode_eval_esc_rsqbr($esc_rsqbr,&$stack) {
322        $node = array_pop($stack);
323        $node['content'] .= "]";
324        array_push($stack,$node);
325        return $node;
326}
327function _tagcode_eval_tag_inline($tag_inline,&$stack) {
328        $children = $tag_inline['children'];
329        $name = _tagcode_eval_name($children[0]);
330        $attrs = array();
331        foreach ($children as $child) {
332                if ($child['type'] == 'attr') {
333                        $attr = _tagcode_eval_attr($child);
334                        $key = $attr['key'];
335                        $value = $attr['value'];
336                        $attrs[$key] = $value;
337                }
338        }
339        $content = apply_tagcodes($name,'',$attrs);
340        // adds evaluated content to next on the stack
341        $node = array_pop($stack);
342        $node['content'] .= $content;
343        array_push($stack,$node);
344        return $node;
345}
346function _tagcode_eval_tag_open($tag_open,&$stack,&$refs) {
347        $children = $tag_open['children'];
348        $name = _tagcode_eval_name($children[0]);
349        $attrs = array();
350        foreach ($children as $child) {
351                if ($child['type'] == 'attr') {
352                        $attr = _tagcode_eval_attr($child);
353                        $key = $attr['key'];
354                        $value = $attr['value'];
355                        $attrs[$key] = $value;
356                }
357        }
358        $data = array('name' => $name, 'attrs' => $attrs);
359        $node = array('node' => $data, 'content' => '');
360        //throws itself on the stack with attributes, etc.
361        array_push($stack,$node);
362        $refs[$name] += 1;
363        return $node;
364}
365function _tagcode_eval_tag_close($tag_close,&$stack,&$refs) {
366        $value = null;
367        $name = _tagcode_eval_name($tag_close['children'][0]);
368        if ($refs[$name] > 0) {
369                do {
370                        $child = array_pop($stack);
371                        $child_node  = $child['node'];
372                        $child_name  = $child['node']['name'];
373                        $child_attrs = $child['node']['attrs'];
374                        $parent = array_pop($stack);
375                        $parent['content'] .= apply_tagcodes($name,$child['content'],$child_attrs);
376                        $refs[$name] -= 1;
377                        array_push($stack,$parent);
378                } while ($child_name != $name);
379        }
380        return array('name' => $name);
381}
382function _tagcode_eval_text($text,&$stack) {
383        $node = array_pop($stack);
384        $node['content'] .= $text['expression'];
385        array_push($stack,$node);
386        return $text['expression'];
387}
388function _tagcode_eval($expr) {
389        $stack = array();
390        $refs  = array();
391        //build parse tree
392        $root = _tagcode_build_tree($expr);
393
394        //create root content node, throw on stack
395        $root_node = array('node' => 'root', 'content' => '');
396        array_push($stack,$root_node);
397        //evaluate immediate children
398        $subs = _tagcode_sub();
399        foreach ($root['children'] as $child) {
400                if (in_array($child['type'],$subs)) {
401                        $func = "_tagcode_eval_{$child['type']}";
402                        switch ($child['type']) {
403                        case 'tag_inline':
404                                _tagcode_eval_tag_inline($child,$stack,$refs);
405                                break;
406                        case 'tag_open':
407                                _tagcode_eval_tag_open($child,$stack,$refs);
408                                break;
409                        case 'tag_close':
410                                _tagcode_eval_tag_close($child,$stack,$refs);
411                                break;
412                        case 'text':
413                                _tagcode_eval_text($child,$stack);
414                                break;
415                        case 'esc_lsqbr':
416                                _tagcode_eval_esc_lsqbr($child,$stack);
417                                break;
418                        case 'esc_rsqbr':
419                                _tagcode_eval_esc_rsqbr($child,$stack);
420                                break;
421                        default:
422                                call_user_func($func,$child);
423                        }
424                }
425        }
426        //evaluate children remaining on stack
427        $child = array_pop($stack);
428        while ($child['node'] != 'root') {
429                $parent = array_pop($stack);
430                $child_node  = $child['node'];
431                $child_name  = $child['node']['name'];
432                $child_attrs = $child['node']['attrs'];
433                $parent['content'] .= apply_tagcodes($child_name,'',$child_attrs);
434                $parent['content'] .= $child['content'];
435                $child = $parent;
436        }
437        return $child['content'];
438}
439
440/**
441* builds an expression tree from a tagcode expression
442*/
443function _tagcode_build_tree($parent) {
444        if (is_string($parent)) {
445                $parent = array('type' => 'root', 'expression' => $parent);
446        }
447        $parent_type = $parent['type'];
448        $parent_expression = $parent['expression'];
449        $re_subtypes = _tagcode_re_subtypes($parent_type);
450        $nodes = array();
451        if ($re_subtypes) {
452                $match_set = array();
453                preg_match_all($re_subtypes,$parent_expression,$match_set,PREG_SET_ORDER);
454                foreach ($match_set as $index => $match) {
455                        foreach (_tagcode_types() as $type) {
456                                if (strlen($match[$type]) > 0) {
457                                        $nodes[$index] = array('type' => $type, 'expression' => $match[$type]);
458                                }
459                        }
460                }
461        }
462        $parent['children'] = array_map('_tagcode_build_tree',$nodes);
463        return $parent;
464}
465
466add_filter('the_content','eval_tagcodes',11);