Make WordPress Core

Ticket #14481: tagcodes.php

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