]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/HtmlParser.php
include [all] Include and file path should be devided with single space. File path...
[SourceForge/phpwiki.git] / lib / HtmlParser.php
1 <?php // -*-php-*-
2
3 /**
4  * HtmlParser Class: Conversion HTML => wikimarkup
5  * Requires XmlParser, XmlElement and the expat (or now the libxml) library. This is all in core.
6  */
7
8 /*
9  * Copyright (C) 2004 Reini Urban
10  *
11  * This file is part of PhpWiki.
12  *
13  * PhpWiki is free software; you can redistribute it and/or modify
14  * it under the terms of the GNU General Public License as published by
15  * the Free Software Foundation; either version 2 of the License, or
16  * (at your option) any later version.
17  *
18  * PhpWiki is distributed in the hope that it will be useful,
19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21  * GNU General Public License for more details.
22  *
23  * You should have received a copy of the GNU General Public License along
24  * with PhpWiki; if not, write to the Free Software Foundation, Inc.,
25  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
26  */
27
28 /**
29  * Base class to implement html => wikitext converters,
30  * extendable for various wiki syntax versions.
31  * This is needed to be able to use htmlarea-alike editors,
32  * and to import XML or HTML documents.
33  *
34  * See also php-html.sf.net for a php-only version, if
35  * you don't have the expat/libxml extension included.
36  * See also http://search.cpan.org/~diberri/HTML-WikiConverter/
37  *
38  */
39
40 // RssParser contains the XML (expat) and url-grabber methods
41 require_once 'lib/XmlParser.php';
42
43 class HtmlParser
44 extends XmlParser
45 {
46     var $dialect, $_handlers, $root;
47
48     /**
49      *  dialect: "PhpWiki2", "PhpWiki"
50      *  possible more dialects: MediaWiki, kwiki, c2
51      */
52     function HtmlParser($dialect = "PhpWiki2", $encoding = '') {
53         $classname = "HtmlParser_".$dialect;
54         if (class_exists($classname))
55             $this->dialect = new $classname;
56         else {
57             trigger_error(sprintf("unknown HtmlParser dialect %s",$dialect),E_USER_ERROR);
58         }
59         $this->_handlers =& $this->dialect->_handlers;
60         $this->XmlParser($encoding);
61     xml_parser_set_option($this->_parser, XML_OPTION_CASE_FOLDING, 0);
62     xml_parser_set_option($this->_parser, XML_OPTION_SKIP_WHITE, 1);
63     }
64
65     // The three callbacks, called on walking through the HTML tree.
66     // No extensions needed from XmlParser.
67     /*
68     function tag_open($parser, $name, $attrs='') {
69     }
70     function tag_close($parser, $name, $attrs='') {
71     }
72     function cdata($parser, $data) {
73     }
74     function parse_url($file, $debug=false)
75     */
76
77     function output () {
78         if (is_null($this->root))
79             $this->root = $GLOBALS['xml_parser_root'];
80         $output = $this->wikify( $this->root );
81         return $output;
82     }
83
84     function wikify ($node, $parent = null) {
85         $output = '';
86         if( isa($node, 'XmlElement')) {
87             $dialect =& $this->dialect;
88             $conv = $dialect->_handlers[$node->_tag];
89             if( is_string($conv) and method_exists($dialect, $conv)) {
90                 $output = $dialect->$conv($node);
91             } elseif( is_array($conv) ) {
92                 foreach ($node->getContent() as $n) {
93                     $output .= $this->wikify($n, $node);
94                 }
95                 $output = $conv[0] . $output . $conv[count($conv)-1];
96             } elseif( !empty($conv) ) {
97                 $output = $conv;
98                 foreach ($node->getContent() as $n) {
99                     $output .= $this->wikify($n, $node);
100                 }
101             } else {
102                 foreach ($node->getContent() as $n) {
103                     $output .= $this->wikify($n, $node);
104                 }
105             }
106         } else {
107             $output = $node;
108             if ($parent and $parent->_tag != 'pre')
109                 preg_replace("/ {2,}/"," ",$output);
110             if (trim($output) == '')
111                 $output = '';
112         }
113         return $output;
114     }
115
116     /** elem_contents()
117      *  $output = $parser->elem_contents( $elem );
118
119      * Returns a wikified version of the contents of the specified
120      * HTML element. This is done by passing each element of this
121      * element's content list through the C<wikify()> method, and
122      * returning the concatenated result.
123      */
124     function elem_contents($node) {
125         $output = '';
126         if (isa($node,'XmlElement')) {
127             foreach ($node->getContent() as $child) {
128                 $output .= $this->wikify($child, isset($node->parent) ? $node->parent : null);
129             }
130         } else {
131             $output = $this->wikify($content);
132         }
133         return $output;
134     }
135
136     //
137     // Private function: _elem_attr_str( $elem, @attrs )
138     //
139     // Returns a string containing a list of attribute names and
140     // values associated with the specified HTML element. Only
141     // attribute names included in @attrs will be added to the
142     // string of attributes that is returned. The return value
143     // is suitable for inserting into an HTML document, as
144     // attribute name/value pairs are specified in attr="value"
145     // format.
146     //
147     function _elem_attr_str($node, $attrs) {
148         $s = '';
149         foreach ($node->_attr as $attr => $val) {
150             $attr = strtolower($attr);
151             if (in_array($attr,$attrs))
152                 $s .= " $attr=\"$val\"";
153         }
154         return $s;
155     }
156
157     //
158     // Private function: _elem_has_ancestor( $elem, $tagname )
159     //
160     // Returns true if the specified HtmlElement has an ancestor element
161     // whose element tag equals $tag. This is useful for determining if
162     // an element belongs to the specified tag.
163     //
164     function _elem_has_ancestor($node, $tag) {
165         if (isset($node->parent)) {
166             if ($node->parent->_tag == $tag) return true;
167             return $this->_elem_has_ancestor($node->parent, $tag);
168         }
169         return false;
170     }
171
172     //
173     // Private function: _elem_is_image_div( $elem )
174     //
175     // Returns true $elem is a container element (P or DIV) meant only to
176     // lay out an IMG.
177     //
178     // More specifically, returns true if the given element is a DIV or P
179     // element and the only child it contains is an IMG tag or an IMG tag
180     // contained within a sole A tag (not counting child elements with
181     // whitespace text only).
182     //
183     function _elem_is_image_div( $node ) {
184         // Return false if node is undefined or isn't a DIV at all
185         if (!$node or !in_array($node->_tag,array("div","p")))
186             return false;
187         $contents = $node->getContent();
188         // Returns true if sole child is an IMG tag
189         if (count($contents) == 1 and isset($contents[0]) and $contents[0]->_tag == 'img')
190             return true;
191         // Check if child is a sole A tag that contains an IMG tag
192         if (count($contents) == 1 and isset($contents[0]) and $contents[0]->_tag == 'a') {
193             $children = $contents[0]->getContent();
194             if (count($children) == 1 and isset($children[0]) and $children[0]->_tag == 'img')
195                 return true;
196         }
197         return false;
198     }
199
200     /** preserves tags and content
201      */
202     function wikify_default($node) {
203         return $this->wikify_preserve($node);
204     }
205
206     /** preserves tags and content
207     */
208     function wikify_preserve($node) {
209         return $node->asXML();
210     }
211
212     function log($dummy) {}
213 }
214
215
216 class HtmlParser_PhpWiki2
217 extends HtmlParser
218 {
219     function HtmlParser_PhpWiki2() {
220         $this->_handlers =
221             array('html'   => '',
222                   'head'   => '',
223                   'title'  => '',
224                   'meta'   => '',
225                   'link'   => '',
226                   'script' => '',
227                   'body'   => '',
228
229                   'br'     => "<br>",
230                   'b'      => array( "*" ),
231                   'strong' => array( "*" ),
232                   'i'      => array( "_" ),
233                   'em'     => array( "_" ),
234                   'hr'     => "----\n\n",
235
236                   // PRE blocks are handled specially (see tidy_whitespace and
237                   // wikify methods)
238                   'pre'    => array( "<pre>", "</pre>" ),
239
240                   'dl'     => array( '', "\n\n" ),
241                   'dt'     => array( ';', '' ),
242                   'dd'     => array( ':', '' ),
243
244                   'p'      => array( "\n\n", "\n\n" ),
245                   'ul'     => array( '', "\n" ),
246                   'ol'     => array( '', "\n" ),
247
248                   'li'     => "wikify_list_item",
249                   'table'  => "wikify_table",
250                   'tr'     => "wikify_tr",
251                   'td'     => "wikify_td",
252                   'th'     => "wikify_td",
253                   'div'    => array( '', "\n\n" ),
254                   'img'    => "wikify_img",
255                   'a'      => "wikify_link",
256                   'span'   => array( '', '' ),
257
258                   'h1'     => "wikify_h",
259                   'h2'     => "wikify_h",
260                   'h3'     => "wikify_h",
261                   'h4'     => "wikify_h",
262                   'h5'     => "wikify_h",
263                   'h6'     => "wikify_h",
264
265                   'font'   => array( '', '' ),
266                   'sup'    => "wikify_default",
267                   'sub'    => "wikify_default",
268                   'nowiki' => "wikify_verbatim",
269                   'verbatim' => "wikify_default",
270                   'noinclude' => "wikify_noinclude",
271                   );
272     }
273
274     function wikify_table( $node ) {
275         $this->ident = '';
276         return "| \n" . $this->elem_contents($node) . "|\n\n";
277     }
278     function wikify_tr( $node ) {
279         return "\n| " . $this->elem_contents($node);
280     }
281     function wikify_th( $node ) {
282         $ident = empty($this->ident) ? '' : $this->ident;
283         $output = "$ident| ";
284         $content = $this->elem_contents($node);
285         preg_replace("s/^\s+/","",$content);
286         $output .= $content;
287         $this->ident .= '  ';
288         return "$output |\n";
289     }
290
291     function wikify_list_item( $node ) {
292         return ($this->_elem_has_ancestor($node, 'ol') ? '*' : '#') . " " . trim($this->elem_contents($node)). "\n";
293     }
294
295     function wikify_link( $node ) {
296         $url = $this->absolute_url( $node->getAttr('href') );
297         $title = $this->elem_contents($node);
298         if (empty($url))
299             $title = trim($title);
300
301         // Just return the link title if this tag is contained
302         // within an header tag
303         if (isset($node->parent) and preg_match('/^h\d$/',$node->parent->_tag))
304             return $title;
305
306         // Return if this is a link to an image contained within
307         if (isset($node->parent) and $this->_elem_is_image_div($node->parent))
308             return $title;
309
310         // If HREF is the same as the link title, then
311         // just return the URL (it'll be converted into
312         // a clickable link by the wiki engine)
313         if ($url == $title) return $url;
314         return "[ $url | $title ]";
315     }
316
317     function wikify_h( $node ) {
318         $level = substr($node->_tag,1);
319         if ($level < 4) {
320             $markup = str_repeat('!',4 - $level);
321         } else {
322             $markup = '!';
323         }
324         return $markup.' '.trim($this->elem_contents($node))."\n\n";
325     }
326
327     function wikify_verbatim( $node ) {
328         $contents = $this->elem_contents( $node );
329         return "\n<verbatim>\n$contents\n</verbatim>";
330     }
331
332     function wikify_noinclude( $node ) {
333         return $this->elem_contents( $node );
334     }
335
336     function wikify_img( $node ) {
337         $image_url = $this->absolute_url( $node->getAttr('src') );
338         $file = basename( $image_url );
339         $alignment = $node->getAttr('align');
340         $this->log( "Processing IMG tag for SRC: ".$image_url."..." );
341         //
342         // Grab attributes to be added to the [ Image ] markup (since 1.3.10)
343         //
344         if (!$alignment) {
345             if ($this->_elem_is_image_div( $node->parent ))
346                 $image_div = $node->parent;
347             elseif (isset($node->parent) and $this->_elem_is_image_div( $node->parent->parent ))
348                 $image_div = $node->parent->parent;
349         }
350         if ( !$alignment and $image_div ) {
351             $css_style = $image_div->getAttr('style');
352             $css_class = $image_div->getAttr('class');
353
354             // float => align: Check for float attribute; if it's there,
355             //                 then we'll add it to the [Image] syntax
356             if (!$alignment and preg_match("/float\:\s*(right|left)/i",$css_style,$m))
357                 $alignment = $m[1];
358             if (!$alignment and preg_match("/float(right|left)/i",$css_class,$m));
359                 $alignment = $m[1];
360             if( $alignment ) {
361                 $attrs[] = "align=$alignment";
362                 $this->log( "  Image is contained within a DIV that specifies $alignment alignment" );
363                 $this->log( "  Adding '$alignment' to [Image] markup attributes" );
364             } else {
365                 $this->log( "  Image is not contained within a DIV for alignment" );
366             }
367         } else {
368             $this->log( "  Image is not contained within a DIV" );
369         }
370         if ($alignment)
371             $attrs[] = "align=$alignment";
372         //
373         // Check if we need to request a thumbnail of this
374         // image; it's needed if the specified width attribute
375         // differs from the default size of the image
376         //
377         if( $width = $node->getAttr('width') ) {
378             $this->log( "  Image has WIDTH attribute of $width" );
379             $this->log( "  Checking whether resulting [Image] markup should specify a thumbnail..." );
380
381             // Download the image from the network and store
382             $abs_url = $this->absolute_url( $node->getAttr('src') );
383             $this->log( "    Fetching image '$abs_url' from the network" );
384             list( $actual_w, $actual_h, $flag, $attr_str) = getimagesize( $abs_url );
385
386             // If the WIDTH attribute of the IMG tag is not equal
387             // to the actual width of the image, then we need to
388             // create a thumbnail
389             if( preg_match("/^\d+$/",$width) and $width != $actual_w ) {
390                 $this->log( "    IMG tag's WIDTH attribute ($width) differs from actual width of image ($actual_w)" );
391                 $this->log( "      -- that means we're going to need a thumbnail" );
392                 $this->log( "    Adding 'width' to list of attributes for [Image] markup" );
393                 $attrs[] = "width=$width";
394                 $width_added = true;
395             }
396             $height = $node->getAttr('height');
397             if( preg_match("/^\d+$/",$height) and $height != $height_h ) {
398                 $this->log( "    IMG tag's HEIGHT attribute ($height) differs from actual height of image ($actual_h)" );
399                 $this->log( "      -- that means we're going to need a thumbnail" );
400                 $this->log( "    Adding 'height' to list of attributes for [Image] markup" );
401                 if (isset($width_added))
402                     $attrs[count($attr)-1] = "size=".$width."x".$height;
403                 else
404                     $attrs[] = "height=$height";
405             }
406         }
407         if ($alt = $node->getAttr('alt')) {
408             $this->log( "  Adding alternate text '$alt' to [Image] markup" );
409             $attrs[] = "alt=$alt";
410         }
411         $attr_str = join(' ', $attrs);
412         $this->log( "...done processing IMG tag\n" );
413         return "[ $file $attr_str ]";
414     }
415 }
416
417 // Local Variables:
418 // mode: php
419 // tab-width: 8
420 // c-basic-offset: 4
421 // c-hanging-comment-ender-p: nil
422 // indent-tabs-mode: nil
423 // End: