Make WordPress Core

Ticket #14481: content_tags.php

File content_tags.php, 14.0 KB (added by deadowl, 16 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);