WordPress.org

Make WordPress Core

Ticket #14481: content_tags.php

File content_tags.php, 14.0 KB (added by deadowl, 11 years ago)

Refactored previous content_tags.php, in particular the eval-geared functions. Also restructured the 'node' structure used for parsing and interpreting

Line 
1<?php
2/*
3Plugin Name: Content Tag Interpreter
4Description: Can be used to replace buggy shortcodes
5Version: 0.97
6Author: Jacob Beauregard
7*/
8
9/**
10 * content_tag public interface
11*/
12function add_content_tag_handler($tag,$handler,$priority=10) {
13        return add_filter(_content_tag_ref_name($tag),$handler,$priority,3);
14}
15function apply_content_tag_handlers($tag,$content,$attrs) {
16        return apply_filters_ref_array(_content_tag_ref_name($tag),array($tag,$content,$attrs));
17}
18function eval_content_tags($content) {
19        return _content_tag_eval($content);
20}
21function has_content_tag_handler($tag,$handler=false) {
22        return has_filter(_content_tag_ref_name($tag),$handler);
23}
24function remove_all_content_tag_handlers($tag,$priority=false) {
25        return remove_all_filters(_content_tag_ref_name($tag),$priority);
26}
27function remove_content_tag_handler($tag,$handler,$priority=10) {
28        return remove_filter(_content_tag_ref_name($tag),$handler,$priority,3);
29}
30
31/**
32 * alias to use in filter for given tag name
33*/
34function _content_tag_ref_name($tag) {
35        return "_content_tag_".strtolower($tag);
36}
37
38/**
39 * types of content_tag expressions
40*/
41function _content_tag_types() {
42        return array(
43                'esc_lsqbr',
44                'esc_rsqbr',
45                'tag_inline',
46                'tag_open',
47                'tag_close',
48                'attr',
49                'literal',
50                'name',
51                'text'
52        );
53}
54function _content_tag_type_aliases($type) {
55        $table = array(
56                array('name','tag_name')
57        );
58        $aliases = array($type);
59        foreach ($table as $entry) {
60                if (in_array($type,$entry,true)) {
61                        $aliases = $entry;
62                }
63        }
64        return $aliases;
65}
66
67/*
68 * content_tag expressions that are subtypes of other content_tag expressions
69*/
70function _content_tag_sub_root() {
71        //top level types
72        return array('esc_lsqbr','esc_rsqbr','tag_inline','tag_open','tag_close','text');
73}
74function _content_tag_sub_tag_inline() {
75        return array('tag_name','attr');
76}
77function _content_tag_sub_tag_open() {
78        return array('tag_name','attr');
79}
80function _content_tag_sub_tag_close() {
81        return array('tag_name');
82}
83function _content_tag_sub_text() {
84        return array();
85}
86function _content_tag_sub_name() {
87        return array();
88}
89function _content_tag_sub_attr() {
90        return array('name','literal');
91}
92function _content_tag_sub_literal() {
93        return array();
94}
95function _content_tag_sub_char_str() {
96        return array();
97}
98function _content_tag_sub_char_ref() {
99        return array();
100}
101function _content_tag_sub_entity_ref() {
102        return array();
103}
104function _content_tag_sub_esc_lsqbr() {
105        return array();
106}
107function _content_tag_sub_esc_rsqbr() {
108        return array();
109}
110
111/**
112 * returns subtypes of expression type
113*/
114function _content_tag_sub($type='root') {
115        if (in_array($type,_content_tag_types(),true) || $type == 'root') {
116                $func = "_content_tag_sub_{$type}";
117                $subtypes = call_user_func($func);
118        }
119        return $subtypes;
120}
121
122/**
123 * regular expressions to match valid expressions
124*/
125function _content_tag_re_lsqbr() {
126        return '\[';
127}
128function _content_tag_re_rsqbr() {
129        return '\]';
130}
131function _content_tag_re_fslash() {
132        return '\/';
133}
134function _content_tag_re_eq() {
135        return '=';
136}
137function _content_tag_re_ws() {
138        return '\s';
139}
140function _content_tag_re_name($named=false) {
141        $expr = '[A-Za-z][-A-Za-z0-9_:.]*';
142        return $named?"(?P<name>{$expr})":$expr;
143}
144function _content_tag_re_tag_name($named=false) {
145        $name = _content_tag_re_name($named);
146        return "^{$name}";
147}
148function _content_tag_re_char_str($named=false) {
149        $expr = "[^%&]+";
150        return $named?"(?P<char_str>{$expr})":$expr;
151}
152function _content_tag_re_char_str_sq($named=false) {
153        //single quote version for shortcode_re_char_str
154        $expr =  "[^%&']+";
155        return $named?"(?P<char_str>{$expr})":$expr;
156}
157function _content_tag_re_char_str_dq($named=false) {
158        //double quote version for shortcode_re_char_str
159        $expr = '[^%&"]+';
160        return $named?"(?P<char_str>{$expr})":$expr;
161}
162function _content_tag_re_char_ref($named=false) {
163        $expr = "&#(?:[0-9]+|x[0-9a-fA-F]+);";
164        return $named?"(?P<char_ref>{$expr})":$expr;
165}
166function _content_tag_re_entity_ref($named=false) {
167        $name = _content_tag_re_name();
168        $expr = "&{$name};";
169        return $named?"(?P<entity_ref>{$expr})":$expr;
170}
171function _content_tag_re_literal($named=false) {
172        $nq = _content_tag_re_literal_nq();
173        $sq = _content_tag_re_literal_sq();
174        $dq = _content_tag_re_literal_dq();
175        $expr = "(?:{$nq}|{$sq}|{$dq})";
176        return $named?"(?P<literal>{$expr})":$expr;
177}
178function _content_tag_re_literal_nq() {
179        //literal without quotes
180        $expr = "[-a-zA-Z0-9_:.]+";
181        return $expr;
182}
183function _content_tag_re_literal_sq() {
184        //literal with single quotes
185        $char_str = _content_tag_re_char_str_sq();
186        $char_ref = _content_tag_re_char_ref();
187        $entity_ref = _content_tag_re_entity_ref();
188        $expr = "'(?:{$char_str}|{$char_ref}|{$entity_ref})*'";
189        return $expr;
190}
191function _content_tag_re_literal_dq() {
192        //literal with double quotes
193        $char_str = _content_tag_re_char_str_dq();
194        $char_ref = _content_tag_re_char_ref();
195        $entity_ref = _content_tag_re_entity_ref();
196        $expr =  "\"(?:{$char_str}|{$char_ref}|{$entity_ref})*\"";
197        return $expr;
198}
199function _content_tag_re_attr($named=false) {
200        $name  = _content_tag_re_name();
201        $literal = _content_tag_re_literal();
202        $eq = _content_tag_re_eq();
203        $ws = _content_tag_re_ws();
204        $expr = "{$name}(?:{$ws}*{$eq}{$ws}*{$literal})?";
205        return $named?"(?P<attr>{$expr})":$expr;
206}
207function _content_tag_re_attr_list() {
208        $ws = _content_tag_re_ws();
209        $attr = _content_tag_re_attr();
210        return "(?:{$ws}+{$attr})*";
211}
212function _content_tag_re_esc_lsqbr($named=false) {
213        $lsqbr = _content_tag_re_lsqbr();
214        $expr = "{$lsqbr}{$lsqbr}";
215        return $named?"(?P<esc_lsqbr>{$expr})":$expr;
216}
217function _content_tag_re_esc_rsqbr($named=false) {
218        $rsqbr = _content_tag_re_rsqbr();
219        $expr = "{$rsqbr}{$rsqbr}";
220        return $named?"(?P<esc_rsqbr>{$expr})":$expr;
221}
222function _content_tag_re_tag_inline($named=false) {
223        $lsqbr = _content_tag_re_lsqbr();
224        $rsqbr = _content_tag_re_rsqbr();
225        $fslash = _content_tag_re_fslash();
226        $ws = _content_tag_re_ws();
227        $name = _content_tag_re_name();
228        $attr = _content_tag_re_attr();
229        $attr_list = _content_tag_re_attr_list();
230        $expr = "{$lsqbr}{$ws}*{$name}{$attr_list}{$ws}*{$fslash}{$ws}*{$rsqbr}";
231        return $named?"(?P<tag_inline>{$expr})":$expr;
232}
233function _content_tag_re_tag_open($named=false) {
234        $lsqbr = _content_tag_re_lsqbr();
235        $rsqbr = _content_tag_re_rsqbr();
236        $ws = _content_tag_re_ws();
237        $name = _content_tag_re_name();
238        $attr = _content_tag_re_attr();
239        $attr_list = _content_tag_re_attr_list();
240        $expr = "{$lsqbr}{$ws}*{$name}{$attr_list}{$ws}*{$rsqbr}";
241        return $named?"(?P<tag_open>{$expr})":$expr;
242}
243function _content_tag_re_tag_close($named=false) {
244        $lsqbr = _content_tag_re_lsqbr();
245        $rsqbr = _content_tag_re_rsqbr();
246        $fslash = _content_tag_re_fslash();
247        $ws = _content_tag_re_ws();
248        $name = _content_tag_re_name();
249        $expr = "{$lsqbr}{$ws}*{$fslash}{$ws}*{$name}{$ws}*{$rsqbr}";
250        return $named?"(?P<tag_close>{$expr})":$expr;
251}
252function _content_tag_re_text($named=false) {
253        $lsqbr = _content_tag_re_lsqbr();
254        $rsqbr = _content_tag_re_rsqbr();
255        //for speed, not the largest chunk
256        $expr = "(?s:.[^{$lsqbr}{$rsqbr}]*)";
257        return $named?"(?P<text>{$expr})":$expr;
258}
259
260/**
261 * cleans tags to prepare for faster parsing (specifically for re_tag_name)
262*/
263function _content_tag_clean_tag_inline($expr) {
264        $lsqbr = _content_tag_re_lsqbr();
265        $rsqbr = _content_tag_re_rsqbr();
266        $ws = _content_tag_re_ws();
267        return preg_replace("/^{$lsqbr}{$ws}*/","",$expr);
268}
269function _content_tag_clean_tag_open($expr) {
270        $lsqbr = _content_tag_re_lsqbr();
271        $rsqbr = _content_tag_re_rsqbr();
272        $ws = _content_tag_re_ws();
273        return preg_replace("/^{$lsqbr}{$ws}*/","",$expr);
274}
275function _content_tag_clean_tag_close($expr) {
276        $lsqbr = _content_tag_re_lsqbr();
277        $rsqbr = _content_tag_re_rsqbr();
278        $fslash = _content_tag_re_fslash();
279        $ws = _content_tag_re_ws();
280        return preg_replace("/^{$lsqbr}{$ws}*{$fslash}{$ws}*/","",$expr);
281}
282function _content_tag_clean_literal($expr) {
283        if ($expr[0] == "'" || $expr[0] == '"') {
284                $expr = substr($expr,1,strlen($expr)-2);
285        }
286        return $expr;
287}
288
289/**
290 * returns a cleaned version of the expression for type
291*/
292function _content_tag_clean($type,$expr) {
293        $func = "_content_tag_clean_{$type}";
294        if (is_callable($func)) {
295                $expr = call_user_func($func,$expr);
296        }
297        return $expr;
298}
299
300/**
301 * combines shortcode regex of types specified in parameters, returns delimited regex
302*/
303function _content_tag_re_combine() {
304        $types = func_get_args();
305        $regexps = array();
306        foreach ($types as $type) {
307                $re_func = "_content_tag_re_{$type}";
308                array_push($regexps,call_user_func($re_func,1));
309        }
310        $statement = '/' . join('|',$regexps) . '/';
311        return $statement;
312}
313
314/**
315 * returns regular expression to match subtypes of an expression (ex. attr => {$name}={$literal})
316*/
317function _content_tag_re_subtypes($type) {
318        $subtypes = _content_tag_sub($type);
319        $re = null;
320        if (count($subtypes)) {
321                $re = call_user_func_array('_content_tag_re_combine',$subtypes);
322        }
323        return $re;
324}
325
326/**
327 * returns a node structure with specified type and expression
328*/
329function _content_tag_init_node($type,$expr) {
330        return array(
331                'type' => $type,
332                'expression' => $expr,
333                'name' => null,
334                'attrs' => array(),
335                'content' => '',
336                'children' => array()
337        );
338} 
339
340/**
341* builds an expression tree from a content_tag expression
342*/
343function _content_tag_build_tree($expr) {
344        $node = is_string($expr)?_content_tag_init_node('root',$expr):$expr;
345        $parse_expr = _content_tag_clean($node['type'],$node['expression']);
346        $subtypes = _content_tag_sub($node['type']);
347        if (count($subtypes)) {
348                $re_subtypes = _content_tag_re_subtypes($node['type']);
349                preg_match_all($re_subtypes,$parse_expr,$matches,PREG_SET_ORDER);
350                foreach ($matches as $match) {
351                        foreach ($match as $type => $expr) {
352                                $aliases = _content_tag_type_aliases($type);
353                                if ($expr && count(array_intersect($aliases,$subtypes))) {
354                                        array_push($node['children'],_content_tag_init_node($type,$expr));
355                                }
356                        }
357                }
358        }
359        $node['children'] = array_map('_content_tag_build_tree',$node['children']);
360        return $node;
361}
362
363/**
364 * adds a tag to the stack to have content added to
365*/
366function _content_tag_stack_push($tag,&$stack,&$refs) {
367        array_push($stack,$tag);
368        if (isset($refs[$tag['name']])) {
369                $refs[$tag['name']] += 1;
370        } else {
371                $refs[$tag['name']] = 1;
372        }
373}
374
375/**
376 * evaluates all stack members above specified member as though they were inline
377 * evaluates specified member with own content and content of members above it
378 * evaluates nothing if specified member not on stack
379*/
380function _content_tag_stack_fold($name,&$stack,&$refs) {
381        if ($name === null || (isset($refs[$name]) && $refs[$name] > 0)) {
382                while (count($stack) > 1 && (!isset($tag) || $tag['name'] != $name)) {
383                        $tag = array_pop($stack);
384                        if ($tag['name'] === $name) {
385                                //include intermediate content for matched tag
386                                $expr = apply_content_tag_handlers($tag['name'],$tag['content'],$tag['attrs']);
387                        } else {
388                                //implicitly inline if the tag doesn't match.
389                                $expr = apply_content_tag_handlers($tag['name'],'',$tag['attrs']).$tag['content'];
390                        }
391                        _content_tag_eval_text(array('expression'=>$expr),$stack);
392                        $refs[$tag['name']] -= 1;
393                }
394        }
395}
396
397/**
398 * evaluation of different content_tag expressions
399*/
400function _content_tag_eval_name($name) {
401        return strtolower($name['expression']);
402}
403function _content_tag_eval_char_str($char_str) {
404        return $char_str['expression'];
405}
406function _content_tag_eval_literal($literal) {
407        //this could alternatively be broken down to
408        //entity reference (with subexpression name)
409        //character reference
410        //character
411        $expr = $literal['expression'];
412        $expr = _content_tag_clean_literal($expr);
413        $expr = html_entity_decode($expr);
414        return $expr;
415}
416function _content_tag_eval_attr($attr) {
417        $children = $attr['children'];
418        $key = _content_tag_eval_name($children[0]);
419        if (isset($children[1])) {
420                $value = _content_tag_eval_literal($children[1]);
421        } else {
422                $value = $key;
423        }
424        return array('key' => $key, 'value' => $value);
425}
426function _content_tag_eval_esc_lsqbr($esc_lsqbr,&$stack) {
427        _content_tag_eval_text('[',$stack);
428}
429function _content_tag_eval_esc_rsqbr($esc_rsqbr,&$stack) {
430        _content_tag_eval_text(']',$stack);
431}
432function _content_tag_eval_tag($tag) {
433        $tag['name'] = _content_tag_eval_name($tag['children'][0]);
434        foreach ($tag['children'] as $child) {
435                if ($child['type'] == 'attr') {
436                        $attr = _content_tag_eval_attr($child);
437                        $tag['attrs'][$attr['key']] = $attr['value'];
438                }
439        }
440        return $tag;
441}
442function _content_tag_eval_tag_inline($tag,&$stack) {
443        $tag = _content_tag_eval_tag($tag);
444        if (has_content_tag_handler($tag['name'])) {
445                $expr = apply_content_tag_handlers($tag['name'],$tag['content'],$tag['attrs']);
446        } else {
447                $expr = $tag['expression'];
448        }
449        _content_tag_eval_text(array('expression' => $expr),$stack);
450}
451function _content_tag_eval_tag_open($tag,&$stack,&$refs) {
452        $tag = _content_tag_eval_tag($tag);
453        if (has_content_tag_handler($tag['name'])) {
454                _content_tag_stack_push($tag,$stack,$refs);
455        } else {
456                _content_tag_eval_text($tag,$stack);
457        }
458}
459function _content_tag_eval_tag_close($tag,&$stack,&$refs) {
460        $tag['name'] = _content_tag_eval_name($tag['children'][0]);
461        if (has_content_tag_handler($tag['name'])) {
462                _content_tag_stack_fold($tag['name'],$stack,$refs);
463        } else {
464                _content_tag_eval_text($tag,$stack);
465        }
466}
467function _content_tag_eval_text($text,&$stack) {
468        $node = array_pop($stack);
469        $node['content'] .= $text['expression'];
470        array_push($stack,$node);
471}
472function _content_tag_eval($expr) {
473        $stack = array();
474        $refs  = array();
475        $root = _content_tag_build_tree($expr);
476        array_push($stack,$root);
477        $subtypes = _content_tag_sub('root');
478        foreach ($root['children'] as $child) {
479                $func = "_content_tag_eval_{$child['type']}";
480                if ($child['type'] == 'tag_inline') {
481                        _content_tag_eval_tag_inline($child,$stack,$refs);
482                } elseif ($child['type'] == 'tag_open') {
483                        _content_tag_eval_tag_open($child,$stack,$refs);
484                } elseif ($child['type'] == 'tag_close') {
485                        _content_tag_eval_tag_close($child,$stack,$refs);
486                } elseif ($child['type'] == 'text') {
487                        _content_tag_eval_text($child,$stack);
488                } elseif ($child['type'] == 'esc_lsqbr') {
489                        _content_tag_eval_esc_lsqbr($child,$stack);
490                } elseif ($child['type'] == 'esc_rsqbr') {
491                        _content_tag_eval_esc_rsqbr($child,$stack);
492                } elseif (in_array($child['type'],$subtypes,true)) {
493                        call_user_func($func,$child);
494                }
495        }
496        _content_tag_stack_fold(null,$stack,$refs);
497        return $stack[0]['content'];
498}
499
500add_filter('the_content','eval_content_tags',11);