├── blog ├── 20070808.markdown ├── 20070809.markdown └── 20070810.markdown ├── favicon.ico ├── templates ├── textblock.html ├── blogpost.html ├── textblock_edit.html ├── blogpost_edit.html ├── blogpost_create.html └── template.html ├── texts ├── resources.markdown ├── about.markdown ├── index.markdown ├── contact_sent.markdown ├── contact_error.markdown ├── about_friends.markdown ├── 404.markdown ├── login.markdown └── contact.markdown ├── 404.php ├── index.php ├── style ├── leman_swiss_flag.jpg └── style.css ├── resources.php ├── sidebar ├── external_profiles.markdown └── blog_feed.markdown ├── about_friends.php ├── about.php ├── blog.php ├── blog_new_post.php ├── include ├── config.php ├── email.php ├── logic.php └── markdown.php ├── contact.php ├── logout.php ├── login.php ├── rss.php └── README.md /blog/20070808.markdown: -------------------------------------------------------------------------------- 1 | xxx 2 | August 8, 2007 3 | asd -------------------------------------------------------------------------------- /blog/20070809.markdown: -------------------------------------------------------------------------------- 1 | yyy 2 | August 9, 2007 3 | sdf -------------------------------------------------------------------------------- /blog/20070810.markdown: -------------------------------------------------------------------------------- 1 | zzz 2 | August 10, 2007 3 | dfg -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/phpMyFlatSite/master/favicon.ico -------------------------------------------------------------------------------- /templates/textblock.html: -------------------------------------------------------------------------------- 1 | {{ text }} 2 | 3 |
{{ admin }}
4 | -------------------------------------------------------------------------------- /texts/resources.markdown: -------------------------------------------------------------------------------- 1 | Resources 2 | 3 | ## Resources 4 | 5 | Your content here. 6 | -------------------------------------------------------------------------------- /texts/about.markdown: -------------------------------------------------------------------------------- 1 | About 2 | 3 | ### My Super Cool Website 4 | 5 | Blah blah blah. 6 | -------------------------------------------------------------------------------- /texts/index.markdown: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/phpMyFlatSite/master/texts/index.markdown -------------------------------------------------------------------------------- /404.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /style/leman_swiss_flag.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/phpMyFlatSite/master/style/leman_swiss_flag.jpg -------------------------------------------------------------------------------- /resources.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sidebar/external_profiles.markdown: -------------------------------------------------------------------------------- 1 | #### External profiles 2 | 3 | - [LinkedIn](http://www.linkedin.com/) 4 | -------------------------------------------------------------------------------- /sidebar/blog_feed.markdown: -------------------------------------------------------------------------------- 1 | #### Subscribe 2 | 3 | - RSS Feed 4 | -------------------------------------------------------------------------------- /about_friends.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /about.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blog.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blog_new_post.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /texts/contact_sent.markdown: -------------------------------------------------------------------------------- 1 | Contact Message Sent 2 | 3 | ### Contact Message Sent 4 | 5 | Your message was sent. 6 | 7 | Thank you. 8 | -------------------------------------------------------------------------------- /templates/blogpost.html: -------------------------------------------------------------------------------- 1 |
{{ title }}
2 | 3 |
{{ date }}
4 |
5 | 6 | {{ text }} 7 | 8 | {{ admin }} 9 | -------------------------------------------------------------------------------- /texts/contact_error.markdown: -------------------------------------------------------------------------------- 1 | Contact Error 2 | 3 | ### Contact Error 4 | 5 | Error, the message could not be sent. 6 | 7 | Please check that you have filled all fields and computed the human check properly. 8 | -------------------------------------------------------------------------------- /texts/about_friends.markdown: -------------------------------------------------------------------------------- 1 | About My Friends 2 | 3 | ### Friends 4 | 5 | Here are my friends: 6 | 7 | [Steve Jobs](http://homepage.mac.com/steve/) 8 | 9 | [Bill Gates](http://www.microsoft.com/billgates/) 10 | -------------------------------------------------------------------------------- /texts/404.markdown: -------------------------------------------------------------------------------- 1 | Error 2 | 3 | ### Error: page not found 4 | 5 | A "HTTP 404 error" occurred. 6 | 7 | It means the link you followed is either wrong or outdated. 8 | 9 | Please use the left menu to find what you want. 10 | -------------------------------------------------------------------------------- /include/config.php: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /contact.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /logout.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /texts/login.markdown: -------------------------------------------------------------------------------- 1 | Login 2 | 3 |

Login

4 | 5 |
6 |

User:

7 |

Password:

8 |

9 |
10 | -------------------------------------------------------------------------------- /templates/textblock_edit.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Page title:

4 | 5 |

6 |
7 | -------------------------------------------------------------------------------- /templates/blogpost_edit.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Title:

4 |

Date:

5 | 6 |

7 |
8 | -------------------------------------------------------------------------------- /templates/blogpost_create.html: -------------------------------------------------------------------------------- 1 |

Blog New Post

2 | 3 |
4 |

File:

5 |

Title:

6 |

Date:

7 | 8 |

9 |
10 | -------------------------------------------------------------------------------- /texts/contact.markdown: -------------------------------------------------------------------------------- 1 | Contact 2 | 3 |

Contact

4 | 5 |
6 |

Name:

7 |

Email:

8 |


9 |

Human check: 2+2 =

10 |

11 |
12 | -------------------------------------------------------------------------------- /login.php: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /include/email.php: -------------------------------------------------------------------------------- 1 | \n"; 16 | $headers .= "Reply-To: ".$thename." <$email>\n"; 17 | $headers .= "X-Mailer: PHP/seriot.ch"; 18 | $headers .= "Date: " . date("r"); 19 | 20 | $browser = $_SERVER['HTTP_USER_AGENT']; 21 | $ip = $_SERVER['REMOTE_ADDR']; 22 | 23 | $texte = "$message 24 | 25 | -- 26 | Browser: $browser 27 | IP Address: $ip"; 28 | 29 | mail($GLOBALS['email'],"Message on ".$GLOBALS['site_name']." from $thename",wordwrap(stripslashes($texte)),"$headers"); 30 | 31 | header("Location: ../contact.php?message_sent=yes"); 32 | 33 | ?> 34 | -------------------------------------------------------------------------------- /rss.php: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | seriot.ch 13 | http://seriot.ch 14 | Nicolas Seriot\'s Blog 15 | '; 16 | 17 | $items = array(); 18 | foreach($posts as $p) { 19 | $i = ' 20 | '.$p->title.' 21 | http://seriot.ch/'.$p->permalink_url().' 22 | '.htmlentities(Markdown($p->text)).' 23 | http://seriot.ch/'.$p->permalink_url().' 24 | '; 25 | array_push($items, $i); 26 | } 27 | $items_string = implode('', $items); 28 | 29 | $footer=' 30 | '; 31 | 32 | echo $header.$items_string.$footer; 33 | 34 | ?> -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | phpMyFlatSite 2 | ============= 3 | 4 | phpMyFlatSite (MFS) is a minimal framework to run a classic, single user, dynamic web site. 5 | 6 | I had been looking for such a framework for a long time. 7 | 8 | Although there are plenty of them out there, I never found one that did suit my taste. 9 | 10 | So I wrote mine in 2007. I was inspired by what I think is close to the perfect website : [exubero.com](http://exubero.com). 11 | 12 | #### Features for Users 13 | 14 | - install as simple as drag and drop 15 | - runs anywhere where php4 is installed 16 | - uses a flat file database, no DB engine required 17 | - online pages edition 18 | - a blog with rss feed and archives 19 | - a contact page 20 | - uses the [markdown syntax](http://daringfireball.net/projects/markdown/syntax) 21 | - strict XHTML 1.0, CSS 2.0 compliant 22 | - basic and simple template system 23 | - really easy to tweak 24 | 25 | #### Features for Programmers 26 | 27 | - logic.php runs the whole site with only 450 lines of code, or 1000 words 28 | 29 | #### Limitations 30 | 31 | - can't create pages online 32 | - can't upload files 33 | - can't post comments 34 | 35 | #### Live Demo 36 | 37 | phpMyWebSite is running on [seriot.ch](http://www.seriot.ch). 38 | 39 | #### Install 40 | 41 | Put the `mfs` folder in your web server. 42 | 43 | Point you web brower to `http://host/path_to_mfs/` and you're in. 44 | 45 | Hint: you can log in by clicking the copyright name. Use `admin`/`password`. 46 | 47 | You can change the default settings in `include/config.php`. 48 | 49 | If necessary, allow the PHP process owner to write and edit files: 50 | 51 | $ chmod 777 blog 52 | $ chmod 646 blog/*.markdown 53 | $ chmod 646 texts/*.markdown 54 | -------------------------------------------------------------------------------- /templates/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 | 18 | {{ site_name }} - {{ page_title }} 19 | 20 | 21 | 22 | 23 |
24 | 25 | 29 | 30 |
{{ page_content }}
31 | 32 | 33 | 34 |
35 | 36 | 37 | 38 |
39 | 40 |

41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /style/style.css: -------------------------------------------------------------------------------- 1 | /* adapted from www.exubero.com */ 2 | 3 | body { 4 | font-family: Verdana, Helvetica, Arial, sans-serif; 5 | padding: 0; 6 | margin: 0; 7 | color: white; 8 | /*background-color: #173150;*/ 9 | background-color: grey; 10 | text-align: center; 11 | } 12 | 13 | .left { 14 | float: left; 15 | padding: 1em; 16 | /* 17 | padding-top: 1em; 18 | padding-bottom: 1em; 19 | */ 20 | } 21 | 22 | .right { 23 | float: right; 24 | padding: 1em; 25 | /* 26 | padding-top: 1em; 27 | padding-bottom: 1em; 28 | */ 29 | } 30 | 31 | .text_left { 32 | float: left; 33 | } 34 | 35 | .text_right { 36 | float: right; 37 | } 38 | 39 | a img { 40 | border: none; 41 | } 42 | 43 | #wrap { 44 | padding: 0; 45 | margin: 0px auto; 46 | width: 55em; 47 | color: black; 48 | background-color: white; 49 | position: relative; 50 | text-align: left; 51 | } 52 | 53 | #header { 54 | font-family: Helvetica, Arial, sans-serif; 55 | padding: 0; 56 | margin: 0; 57 | height: 180px; 58 | background: url(leman_swiss_flag.jpg) #6699cc no-repeat center top; 59 | border-bottom: #404040 2px solid; 60 | } 61 | 62 | #header p { 63 | padding: 0.25em; 64 | margin: 0; 65 | } 66 | 67 | #site_name { 68 | font-size: 250%; 69 | } 70 | 71 | #quotation { 72 | /*font-weight: bold;*/ 73 | font-size: 80%; 74 | float: right; 75 | color: white; 76 | text-align: right; 77 | font-family: "Courier"; 78 | } 79 | 80 | #quotation a { 81 | text-decoration: none; 82 | color: white; 83 | } 84 | 85 | #content { 86 | padding-left: 1em; 87 | padding-right: 1em; 88 | padding-bottom: 1em; 89 | float: right; 90 | width: 48em; 91 | font-size:10pt; 92 | } 93 | 94 | /* 95 | #content h2, #content h3, #content h4, #content h5, #content h6 { 96 | color: #404040; 97 | } 98 | */ 99 | 100 | #content h2 { 101 | padding: 0.1em; 102 | border: #173150 2px solid; 103 | /*border-left: #3e7035 2px solid;*/ 104 | /*border-bottom: #3e7035 2px solid;*/ 105 | margin-bottom: 0; 106 | font-variant: small-caps; 107 | clear: both; 108 | } 109 | 110 | #content h3 { 111 | border-bottom: #173150 2px solid; 112 | margin-bottom: 0; 113 | } 114 | 115 | #content .date, #content .updated { 116 | padding: 0.25em; 117 | font-size: smaller; 118 | float: right; 119 | margin: 0.25em; 120 | color: #3e7035; 121 | font-style: italic; 122 | } 123 | 124 | #content .author { 125 | font-size: small; 126 | float: right; 127 | color: #3e7035; 128 | font-style: italic; 129 | } 130 | 131 | #content blockquote { 132 | font-style: italic; 133 | } 134 | 135 | #content pre { 136 | border: 1px solid black; 137 | padding: 0.5em; 138 | margin: 0.5em; 139 | background-color: #eee; 140 | } 141 | 142 | #content code { 143 | /*border: 1px solid black;*/ 144 | /*padding: 0.5em;*/ 145 | /*margin: 0.5em;*/ 146 | background-color: #eee; 147 | } 148 | 149 | #footer { 150 | padding: 0.25em; 151 | clear: both; 152 | /*font-weight: bold;*/ 153 | font-size: smaller; 154 | color: white; 155 | /*font-style: italic;*/ 156 | background-color: #666; 157 | text-align: center; 158 | } 159 | 160 | #footer a { 161 | color: white; 162 | text-decoration: none; 163 | } 164 | 165 | ul#nav { 166 | width: 13em; 167 | border-bottom: black 1px solid; 168 | } 169 | 170 | ul#nav, ul#nav ul { 171 | padding: 0; 172 | margin: 0px 0px 1em; 173 | border-right: black 1px solid; 174 | font-size: medium; 175 | list-style-type: none; 176 | background-color: #6699cc; 177 | } 178 | 179 | ul#nav ul { 180 | font-size: small; 181 | margin: 0; 182 | border-style: none; 183 | background-color: #99ccff; 184 | } 185 | 186 | ul#nav li { 187 | padding: 0 0.25em; 188 | font-size: larger; 189 | margin: 0; 190 | border-left: black 1px solid; 191 | border-top: black 1px solid; 192 | } 193 | 194 | ul#nav li:hover, ul#nav li.navselected { 195 | background-color: white; 196 | } 197 | 198 | ul#nav a { 199 | padding: 0; 200 | margin: 0; 201 | display: block; 202 | width: 100%; 203 | text-decoration: none; 204 | } 205 | 206 | ul#nav a:link, ul#nav a:visited { 207 | color: black; 208 | } 209 | 210 | ul#nav a:hover { 211 | color: #ff0000; 212 | } 213 | 214 | #sidebar { 215 | padding: 0 1em; 216 | margin: 0px; 217 | width: 12em; 218 | } 219 | #sidebar h3, #sidebar h4, #sidebar h5, #sidebar p, #sidebar ul, #sidebar ol { 220 | font-size: small; 221 | } 222 | 223 | #sidebar h3, #sidebar h4, #sidebar h5 { 224 | margin-top: 1.5em; 225 | margin-bottom: 0; 226 | } 227 | 228 | #sidebar ul { 229 | padding: 0; 230 | margin:0; 231 | list-style-type: none 232 | } 233 | 234 | #sidebar li { 235 | padding-left: 0.5em; 236 | } 237 | 238 | /* ---[ Entries ]----------------------------------- */ 239 | 240 | #content dl.entries { 241 | margin: 0; 242 | padding: 0; 243 | } 244 | #content dl.entries dt { 245 | font-size: 120%; 246 | line-height: 1.5em; 247 | font-weight: bold; 248 | border-bottom: 1px solid #eaeaea; 249 | padding-top: 1em; 250 | } 251 | #content dl.entries dt a { 252 | text-decoration: none; 253 | color: #40659B; 254 | } 255 | #content dl.entries dt a:hover { 256 | color: #036; 257 | text-decoration: underline; 258 | } 259 | #content dl.entries dd { 260 | margin: 0; 261 | padding: 0; 262 | font-size: 100%; 263 | } 264 | #content dl.entries dd .more { 265 | font-size: 90%; 266 | } 267 | #content dl.entries dd.posted { 268 | margin-bottom: 0.25em; 269 | font-size: 90%; 270 | color: #999; 271 | } 272 | #content dl.entries dd.permalink { 273 | margin: 0; 274 | font-size: 90%; 275 | float: right; 276 | } 277 | #content dl.entries dd.permalink a { 278 | color: #999; 279 | } 280 | #content dl.entries dd p { 281 | margin: 1em 0 1em 0; 282 | } 283 | 284 | /* ---[ dl's inside entries ]----------------------------------- */ 285 | 286 | #content dl.entries dl dt { 287 | font-size: 100%; 288 | line-height: 1em; 289 | border-bottom: none; 290 | padding-top: 0.5em; 291 | } 292 | 293 | #content dl.entries dl dd { 294 | margin-left: 1em; 295 | margin-bottom: 0.5em; 296 | } 297 | 298 | 299 | #content img.border { 300 | display: block; 301 | padding:5px; 302 | background:#FFF; 303 | border:1px solid; 304 | border-color: #ccc #666 #666 #ccc 305 | } 306 | 307 | /* ---[ XML buttons ]----------------------------------- */ 308 | /* Styles taken from http://www.jschreiber.com/archives/2004/09/creating_pure_c.html */ 309 | 310 | .btn { 311 | display: inline; 312 | font: x-small/200% Arial, sans-serif; 313 | padding: 1px 0; 314 | border: 1px solid #666; 315 | margin: 0; 316 | text-decoration: none; 317 | background-color: #fff; 318 | } 319 | .btnFront { 320 | display: inline; 321 | background-color: #f90; 322 | color: #fff; 323 | padding: 0 2px; 324 | border: 1px solid #fff; 325 | border-top: 0; 326 | border-bottom: 0; 327 | margin: 0px; 328 | } 329 | .btnText { 330 | display: inline; 331 | background-color: #898e79; 332 | color: #fff; 333 | padding: 0 2px; 334 | border: 0; 335 | border-right: 1px solid #fff; 336 | margin: 0px; 337 | } 338 | 339 | 340 | -------------------------------------------------------------------------------- /include/logic.php: -------------------------------------------------------------------------------- 1 | = 7) { 12 | set_error_handler(function ($errno, $errstr) { 13 | return strpos($errstr, 'Declaration of') === 0; 14 | }, E_WARNING); 15 | } 16 | */ 17 | 18 | $GLOBALS['texts_folder'] = 'texts'; 19 | $GLOBALS['blog_folder'] = 'blog'; 20 | $GLOBALS['sidebar_folder'] = 'sidebar'; 21 | $GLOBALS['templates_folder'] = 'templates'; 22 | $GLOBALS['markup_ext'] = 'markdown'; 23 | 24 | require_once('include/markdown.php'); 25 | include "include/config.php"; 26 | ini_set('url_rewriter.tags', ''); 27 | ini_set('session.save_handler', 'files'); 28 | session_start(); 29 | 30 | class TextItem { 31 | var $id; 32 | var $title; 33 | var $text; 34 | 35 | function exists($name) { 36 | return file_exists($GLOBALS['texts_folder']."/".$name.".".$GLOBALS['markup_ext']); 37 | } 38 | 39 | function TextItem($id) { 40 | $this->id = remove_file_extension($id); 41 | $this->read(); 42 | } 43 | 44 | function file_path() { 45 | return $GLOBALS['texts_folder'].'/'.$this->id.'.'.$GLOBALS['markup_ext']; 46 | } 47 | 48 | function allTexts() { 49 | $ids = array_reverse(files_in_dir($GLOBALS['texts_folder'])); 50 | $a = array(); 51 | foreach($ids as $i) { 52 | array_push($a, new TextItem($i)); 53 | } 54 | return $a; 55 | } 56 | 57 | function read() { 58 | if(!$this->exists($this->id)) { 59 | return; 60 | } 61 | 62 | $lines = file($this->file_path()); 63 | $this->title = $lines[0]; 64 | $this->text = implode('', array_slice($lines, 1)); 65 | } 66 | 67 | function display() { 68 | $is_logged_in = $_SESSION['is_logged_in'] == True; 69 | $edit_state = $is_logged_in && $_GET['edit'] == '1'; 70 | $d = array(); 71 | $d['id'] = $this->id; 72 | $d['title'] = $this->title; 73 | $d['text'] = $edit_state ? $this->text : Markdown($this->text); 74 | $d['admin'] = ''; 75 | if($is_logged_in) { 76 | if(is_writable($this->file_path())) { 77 | $d['admin'] .= '[edit] '; 78 | } 79 | $d['admin'] .= '[logout]'; 80 | } 81 | $template = $edit_state ? 'textblock_edit.html' : 'textblock.html'; 82 | return evaluate_template_keys($d, $template); 83 | } 84 | 85 | function updateTitleText($title, $text) { 86 | $this->title = stripslashes($title); 87 | $this->text = stripslashes($text); 88 | $this->save(); 89 | } 90 | 91 | function save() { 92 | $f = fopen($this->file_path(), "w") or die ("can't open or write file"); 93 | fwrite($f, "$this->title\n"); 94 | fwrite($f, "$this->text"); 95 | fclose($f); 96 | } 97 | } 98 | 99 | class SidebarItem extends TextItem { 100 | function display() { 101 | return Markdown($this->text); 102 | } 103 | 104 | function exists($name) { 105 | return file_exists($GLOBALS['sidebar_folder']."/".$name.".".$GLOBALS['markup_ext']); 106 | } 107 | 108 | function file_path() { 109 | return $GLOBALS['sidebar_folder'].'/'.$this->id.'.'.$GLOBALS['markup_ext']; 110 | } 111 | 112 | function read() { 113 | if(!$this->exists($this->id)) { 114 | return; 115 | } 116 | $this->text = file_get_contents($this->file_path()); 117 | } 118 | } 119 | 120 | class BlogPost extends TextItem { 121 | var $date; 122 | 123 | function exists($name) { 124 | return file_exists($GLOBALS['blog_folder']."/".$name.".".$GLOBALS['markup_ext']); 125 | } 126 | 127 | function file_path() { 128 | return $GLOBALS['blog_folder'].'/'.$this->id.'.'.$GLOBALS['markup_ext']; 129 | } 130 | 131 | function all_posts($limit = False) { 132 | $ids = array_reverse(files_in_dir($GLOBALS['blog_folder'])); 133 | $a = array(); 134 | foreach($ids as $i) { 135 | array_push($a, new BlogPost($i)); 136 | } 137 | if($limit) { 138 | return array_slice($a, 0, $limit); 139 | } 140 | return $a; 141 | } 142 | 143 | function all_posts_display($limit=False) { 144 | $a = array(); 145 | $head = '
'; 146 | 147 | if($_SESSION['is_logged_in']) { 148 | $head .= '

[new post]

'; 149 | } 150 | foreach(BlogPost::all_posts($limit) as $p) { 151 | array_push($a, $p->display()); 152 | } 153 | $foot = '
'; 154 | return $head.implode('', $a).$foot; 155 | } 156 | 157 | function history_link() { 158 | return ''.$this->title.''; 159 | } 160 | 161 | function histories_links($current_id) { 162 | $a = array(); 163 | $head = '

History

'; 169 | return $head.implode('', $a).$foot; 170 | } 171 | 172 | function read() { 173 | if(!$this->exists($this->id)) { 174 | return; 175 | } 176 | $lines = file($this->file_path()); 177 | $this->title = $lines[0]; 178 | $this->date = $lines[1]; 179 | $this->text = implode('', array_slice($lines, 2)); 180 | } 181 | 182 | function display() { 183 | $is_logged_in = $_SESSION['is_logged_in'] == True; 184 | $edit_state = $is_logged_in && $_GET['edit'] == '1'; 185 | $is_writable = is_writable($this->file_path()); 186 | $remove_state = $_GET['remove']; 187 | $d = array(); 188 | $d['title'] = $this->title; 189 | $d['date'] = $this->date; 190 | $d['text'] = $edit_state ? $this->text : Markdown($this->text); 191 | $d['permalink'] = $this->permalink(); 192 | if($is_logged_in) { 193 | $d['admin'] = '
'; 194 | if($is_writable) { 195 | $d['admin'] .= $this->edit_link().' '.$this->remove_link().' '; 196 | } 197 | $d['admin'] .= '[logout]
'; 198 | 199 | if($remove_state) { 200 | $d['admin'] .= '
[confirm removal]'; 201 | $d['admin'] .= ' [cancel removal]
'; 202 | } 203 | } 204 | $template = $edit_state ? 'blogpost_edit.html' : 'blogpost.html'; 205 | return evaluate_template_keys($d, $template); 206 | } 207 | 208 | function updateTitleDateText($title, $date, $text) { 209 | $this->title = stripslashes($title); 210 | $this->date = stripslashes($date); 211 | $this->text = stripslashes($text); 212 | $this->save(); 213 | } 214 | 215 | function save() { 216 | $f = fopen($this->file_path(), "w") or die ("can't open or write file"); 217 | fwrite($f, "$this->title\n"); 218 | fwrite($f, "$this->date\n"); 219 | fwrite($f, "$this->text"); 220 | fclose($f); 221 | } 222 | 223 | function remove() { 224 | if($_SESSION['article_to_remove'] == $this->id && is_writable($this->file_path())) { 225 | unlink($this->file_path()); 226 | unset($_SESSION['article_to_remove']); 227 | } 228 | } 229 | 230 | function edit_link() { 231 | return '[edit]'; 232 | } 233 | 234 | function remove_link() { 235 | return '[x]'; 236 | } 237 | 238 | function permalink_url() { 239 | return 'blog.php?article='.$this->id; 240 | } 241 | 242 | function permalink() { 243 | return 'permanent link'; 244 | } 245 | } 246 | 247 | function create_unique_blog_file($file_name, $file_content) { 248 | $file_name = remove_file_extension($file_name).".".$GLOBALS['markup_ext']; // ensure right extension schema 249 | 250 | while(in_array($file_name, files_in_dir($GLOBALS['blog_folder']))) { 251 | $file_name = remove_file_extension($file_name).'_'.'.'.$GLOBALS['markup_ext']; // ensure uniqueness 252 | } 253 | 254 | $file_handle = fopen($GLOBALS['blog_folder'].'/'.$file_name,"w+b") or die ("can't open or write file ".$GLOBALS['blog_folder'].'/'.$file_name.", try to $ chmod 777 ".$GLOBALS['blog_folder']); 255 | fwrite($file_handle, stripslashes($file_content)); 256 | fclose($file_handle); 257 | 258 | return $file_name; 259 | } 260 | 261 | function sidebar($id) { 262 | $s = new SidebarItem($id); 263 | return $s->display(); 264 | } 265 | 266 | function current_file() { 267 | $current_file = explode("?", basename($_SERVER['PHP_SELF'])); 268 | return $current_file[0]; 269 | } 270 | 271 | function build_text_page($id, $sidebar=False) { 272 | $ti = new TextItem($id); 273 | 274 | if($_POST['title'] && $_POST['text']) { 275 | $ti->updateTitleText($_POST['title'], $_POST['text']); 276 | header("Location: ".current_file()."?edit=0"); 277 | } 278 | 279 | $d = array(); 280 | $d['menu'] = build_menu(); 281 | $d['page_title'] = $ti->title; 282 | $d['page_sidebar'] = $sidebar ? sidebar($sidebar) : ''; 283 | $d['page_content'] = $ti->display(); 284 | 285 | return evaluate_template_keys($d, 'template.html'); 286 | } 287 | 288 | function build_create_blogpost_page($id, $sidebar=False) { 289 | if(!$_SESSION['is_logged_in']) { 290 | header("Location: login.php"); 291 | } 292 | 293 | if($_POST['file'] && $_POST['title'] && $_POST['date'] && $_POST['text']) { 294 | $file_content = $_POST['title'].'\n'.$_POST['date'].'\n'.$_POST['text']; 295 | $file = create_unique_blog_file($_POST['file'], $file_content); 296 | $b = new BlogPost($file); 297 | $b->updateTitleDateText($_POST['title'], $_POST['date'], $_POST['text']); 298 | $b->save(); 299 | header("Location: ".$b->permalink_url()); 300 | } 301 | 302 | $d = array(); 303 | $d['today_compact'] = date("Ymd"); 304 | $d['today_full'] = date("F j, Y"); 305 | $content = evaluate_template_keys($d, 'blogpost_create.html'); 306 | 307 | $d = array(); 308 | $d['menu'] = build_menu(); 309 | $d['page_title'] = 'Create New Post'; 310 | $d['page_sidebar'] = $sidebar ? sidebar($sidebar) : ''; 311 | $d['page_content'] = $content; 312 | 313 | return evaluate_template_keys($d, 'template.html'); 314 | } 315 | 316 | function build_blog_page($id, $sidebar=False, $history=False) { 317 | $d = array(); 318 | $d['menu'] = build_menu(); 319 | 320 | $blog_content = array(); 321 | $files_in_blog_dir = files_in_dir('blog'); 322 | 323 | $is_logged_in = $_SESSION['is_logged_in'] == True; 324 | $single_article_state = isset($_GET['article']); 325 | $remove_article_state = $_GET['remove'] == '1'; 326 | $remove_confirmed_article_state = $_GET['remove_confirmed'] == '1'; 327 | 328 | if($single_article_state) { 329 | if(!BlogPost::exists($_GET['article'])) { 330 | header("Location: blog.php"); 331 | } 332 | 333 | $b = new BlogPost($_GET['article']); 334 | 335 | if($is_logged_in) { 336 | if($remove_article_state) { 337 | $_SESSION['article_to_remove'] = $_GET['article']; 338 | } else if ($remove_confirmed_article_state) { 339 | $b->remove(); 340 | header("Location: blog.php"); 341 | } 342 | } 343 | 344 | if($_POST['title'] && $_POST['date'] && $_POST['text']) { 345 | $b->updateTitleDateText($_POST['title'], $_POST['date'], $_POST['text']); 346 | header("Location: ".current_file()); 347 | } 348 | 349 | $d['page_title'] = $b->title; 350 | $d['page_content'] = '
'.$b->display().'
'; 351 | } else { 352 | unset($_SESSION['article_to_remove']); 353 | $d['page_title'] = 'Blog'; 354 | echo $EMAIL; 355 | $d['page_content'] = BlogPost::all_posts_display($GLOBALS['blog_posts_per_page']); 356 | } 357 | 358 | $d['page_sidebar'] = $sidebar ? sidebar($sidebar) : ''; 359 | $d['page_sidebar'] .= $sidebar && $history ? BlogPost::histories_links($b->id) : ''; 360 | 361 | echo evaluate_template_keys($d, 'template.html'); 362 | } 363 | 364 | function evaluate_template_keys($d, $template) { 365 | echo $SITE_DOMAIN; 366 | if(!isset($d['site_name'])) { 367 | $d['site_name'] = $GLOBALS['site_name']; 368 | } 369 | 370 | $template_content = file_get_contents($GLOBALS['templates_folder'].'/'.$template); 371 | 372 | preg_match_all("{{ [A-Za-z0-9_]* }}", $template_content, $matches, PREG_PATTERN_ORDER); 373 | 374 | foreach ($matches[0] as $token) { 375 | $token = substr($token, 2, -2); 376 | $template_content = str_replace("{{ ".$token." }}", $d[$token], $template_content); 377 | } 378 | return $template_content; 379 | } 380 | 381 | function files_in_dir($dir) { 382 | $array = array(); 383 | if ($handle = opendir($dir)) { 384 | while ($file = readdir($handle)) { 385 | if ($file[0] != "." && !is_dir($file)) { 386 | array_push($array, $file); 387 | } 388 | } 389 | closedir($handle); 390 | } 391 | sort($array); 392 | return $array; 393 | } 394 | 395 | function remove_file_extension($strName) { 396 | $ext = strrchr($strName, '.'); 397 | 398 | if($ext !== false) { 399 | $strName = substr($strName, 0, -strlen($ext)); 400 | } 401 | return $strName; 402 | } 403 | 404 | class MenuItem { 405 | var $name; 406 | var $link; 407 | var $submenu; 408 | 409 | function MenuItem($aName, $aLink, $aSubmenu=array()) { 410 | $this->name = $aName; 411 | $this->link = $aLink; 412 | $this->submenu = $aSubmenu; 413 | } 414 | 415 | function myclass($myparameter) { 416 | $this->myvar = $myparameter; 417 | } 418 | 419 | function get_menu() { 420 | $current_page = basename($_SERVER['SCRIPT_NAME']); 421 | 422 | $s = ""; 423 | 424 | if($current_page === $this->link) { 425 | $s .= '
  • '.$this->name.''; 428 | } 429 | 430 | $display_submenu = $current_page === $this->link; 431 | 432 | for($i=0; $isubmenu); $i++) { 433 | if($this->submenu[$i]->link === $current_page) { 434 | $display_submenu = True; 435 | } 436 | } 437 | 438 | if($display_submenu) { 439 | for($i=0; $isubmenu); $i++) { 440 | $s .= "
      ".$this->submenu[$i]->get_menu()."
    \n"; 441 | } 442 | } 443 | return $s.'
  • '; 444 | } 445 | } 446 | 447 | function build_menu() { 448 | $menu = array(new MenuItem("Home","index.php"), 449 | 450 | /* new MenuItem("Blog","blog.php"), */ 451 | 452 | /* new MenuItem("Software","software.php"), */ 453 | 454 | /* 455 | new MenuItem("Resources","resources.php",array(new MenuItem("Home Made Maps","maps.php"), 456 | new MenuItem("Hello Mach-O","hello_macho.php"), 457 | new MenuItem("Abusing Twitter API","abusing_twitter_api.php"), 458 | new MenuItem("Data Visualization","visualization.php"), 459 | new MenuItem("Talks and Papers","resources_talks_papers.php"))), 460 | */ 461 | 462 | new MenuItem("Home Made Maps","maps.php"), 463 | new MenuItem("Hello Mach-O","hello_macho.php"), 464 | new MenuItem("A Tiny NTP Client","ntp.php"), 465 | new MenuItem("Abusing Twitter API","abusing_twitter_api.php"), 466 | new MenuItem("Parsing JSON","parsing_json.php"), 467 | new MenuItem("Data Visualization","visualization.php"), 468 | 469 | new MenuItem("Trail","trail.php",array(new MenuItem("Trail du Salève","trail_saleve_2016.php"), new MenuItem("Eiger Ultra Trail","trail_e101_2018.php"), new MenuItem("X-Alpine","trail_xalpine_2019.php"))), 470 | 471 | new MenuItem("Talks and Papers","resources_talks_papers.php"), 472 | 473 | 474 | new MenuItem("Contact","contact.php")); 475 | 476 | $s = ""; 477 | 478 | for($i=0; $iget_menu(); 480 | } 481 | 482 | return $s; 483 | } 484 | 485 | ?> 486 | -------------------------------------------------------------------------------- /include/markdown.php: -------------------------------------------------------------------------------- 1 | 8 | # 9 | # Original Markdown 10 | # Copyright (c) 2004-2006 John Gruber 11 | # 12 | # 13 | 14 | 15 | define( 'MARKDOWN_VERSION', "1.0.1g" ); # Tue 3 Jul 2007 16 | define( 'MARKDOWNEXTRA_VERSION', "1.1.3" ); # Tue 3 Jul 2007 17 | 18 | 19 | # 20 | # Global default settings: 21 | # 22 | 23 | # Change to ">" for HTML output 24 | define( 'MARKDOWN_EMPTY_ELEMENT_SUFFIX', " />"); 25 | 26 | # Define the width of a tab for code blocks. 27 | define( 'MARKDOWN_TAB_WIDTH', 4 ); 28 | 29 | # Optional title attribute for footnote links and backlinks. 30 | define( 'MARKDOWN_FN_LINK_TITLE', "" ); 31 | define( 'MARKDOWN_FN_BACKLINK_TITLE', "" ); 32 | 33 | # Optional class attribute for footnote links and backlinks. 34 | define( 'MARKDOWN_FN_LINK_CLASS', "" ); 35 | define( 'MARKDOWN_FN_BACKLINK_CLASS', "" ); 36 | 37 | 38 | # 39 | # WordPress settings: 40 | # 41 | 42 | # Change to false to remove Markdown from posts and/or comments. 43 | define( 'MARKDOWN_WP_POSTS', true ); 44 | define( 'MARKDOWN_WP_COMMENTS', true ); 45 | 46 | 47 | 48 | ### Standard Function Interface ### 49 | 50 | define( 'MARKDOWN_PARSER_CLASS', 'MarkdownExtra_Parser' ); 51 | 52 | function Markdown($text) { 53 | # 54 | # Initialize the parser and return the result of its transform method. 55 | # 56 | # Setup static parser variable. 57 | static $parser; 58 | if (!isset($parser)) { 59 | $parser_class = MARKDOWN_PARSER_CLASS; 60 | $parser = new $parser_class; 61 | } 62 | 63 | # Transform text using parser. 64 | return $parser->transform($text); 65 | } 66 | 67 | 68 | ### WordPress Plugin Interface ### 69 | 70 | /* 71 | Plugin Name: Markdown Extra 72 | Plugin URI: http://www.michelf.com/projects/php-markdown/ 73 | Description: Markdown syntax allows you to write using an easy-to-read, easy-to-write plain text format. Based on the original Perl version by John Gruber. More... 74 | Version: 1.1.3 75 | Author: Michel Fortin 76 | Author URI: http://www.michelf.com/ 77 | */ 78 | 79 | if (isset($wp_version)) { 80 | # More details about how it works here: 81 | # 82 | 83 | # Post content and excerpts 84 | # - Remove WordPress paragraph generator. 85 | # - Run Markdown on excerpt, then remove all tags. 86 | # - Add paragraph tag around the excerpt, but remove it for the excerpt rss. 87 | if (MARKDOWN_WP_POSTS) { 88 | remove_filter('the_content', 'wpautop'); 89 | remove_filter('the_content_rss', 'wpautop'); 90 | remove_filter('the_excerpt', 'wpautop'); 91 | add_filter('the_content', 'Markdown', 6); 92 | add_filter('the_content_rss', 'Markdown', 6); 93 | add_filter('get_the_excerpt', 'Markdown', 6); 94 | add_filter('get_the_excerpt', 'trim', 7); 95 | add_filter('the_excerpt', 'mdwp_add_p'); 96 | add_filter('the_excerpt_rss', 'mdwp_strip_p'); 97 | 98 | remove_filter('content_save_pre', 'balanceTags', 50); 99 | remove_filter('excerpt_save_pre', 'balanceTags', 50); 100 | add_filter('the_content', 'balanceTags', 50); 101 | add_filter('get_the_excerpt', 'balanceTags', 9); 102 | } 103 | 104 | # Comments 105 | # - Remove WordPress paragraph generator. 106 | # - Remove WordPress auto-link generator. 107 | # - Scramble important tags before passing them to the kses filter. 108 | # - Run Markdown on excerpt then remove paragraph tags. 109 | if (MARKDOWN_WP_COMMENTS) { 110 | remove_filter('comment_text', 'wpautop', 30); 111 | remove_filter('comment_text', 'make_clickable'); 112 | add_filter('pre_comment_content', 'Markdown', 6); 113 | add_filter('pre_comment_content', 'mdwp_hide_tags', 8); 114 | add_filter('pre_comment_content', 'mdwp_show_tags', 12); 115 | add_filter('get_comment_text', 'Markdown', 6); 116 | add_filter('get_comment_excerpt', 'Markdown', 6); 117 | add_filter('get_comment_excerpt', 'mdwp_strip_p', 7); 118 | 119 | global $wp_markdown_hidden; 120 | $wp_markdown_hidden[1] = 121 | '

     
  • '; 122 | $wp_markdown_hidden[2] = explode(' ', str_rot13( 123 | 'pEj07ZbbBZ U1kqgh4w4p pre2zmeN6K QTi31t9pre ol0MP1jzJR '. 124 | 'ML5IjmbRol ulANi1NsGY J7zRLJqPul liA8ctl16T K9nhooUHli')); 125 | } 126 | 127 | function mdwp_add_p($text) { 128 | if (!preg_match('{^$|^<(p|ul|ol|dl|pre|blockquote)>}i', $text)) { 129 | $text = '

    '.$text.'

    '; 130 | $text = preg_replace('{\n{2,}}', "

    \n\n

    ", $text); 131 | } 132 | return $text; 133 | } 134 | 135 | function mdwp_strip_p($t) { return preg_replace('{}i', '', $t); } 136 | 137 | function mdwp_hide_tags($text) { 138 | global $wp_markdown_hidden; 139 | return str_replace(explode($wp_markdown_hidden), 140 | explode($wp_markdown_hidden), $text); 141 | } 142 | function mdwp_show_tags($text) { 143 | global $markdown_hidden_tags; 144 | return str_replace(array_values($markdown_hidden_tags), 145 | array_keys($markdown_hidden_tags), $text); 146 | } 147 | } 148 | 149 | 150 | ### bBlog Plugin Info ### 151 | 152 | function identify_modifier_markdown() { 153 | return array( 154 | 'name' => 'markdown', 155 | 'type' => 'modifier', 156 | 'nicename' => 'PHP Markdown Extra', 157 | 'description' => 'A text-to-HTML conversion tool for web writers', 158 | 'authors' => 'Michel Fortin and John Gruber', 159 | 'licence' => 'GPL', 160 | 'version' => MARKDOWNEXTRA_VERSION, 161 | 'help' => 'Markdown syntax allows you to write using an easy-to-read, easy-to-write plain text format. Based on the original Perl version by John Gruber. More...', 162 | ); 163 | } 164 | 165 | 166 | ### Smarty Modifier Interface ### 167 | 168 | function smarty_modifier_markdown($text) { 169 | return Markdown($text); 170 | } 171 | 172 | 173 | ### Textile Compatibility Mode ### 174 | 175 | # Rename this file to "classTextile.php" and it can replace Textile everywhere. 176 | 177 | if (strcasecmp(substr(__FILE__, -16), "classTextile.php") == 0) { 178 | # Try to include PHP SmartyPants. Should be in the same directory. 179 | @include_once 'smartypants.php'; 180 | # Fake Textile class. It calls Markdown instead. 181 | class Textile { 182 | function TextileThis($text, $lite='', $encode='') { 183 | if ($lite == '' && $encode == '') $text = Markdown($text); 184 | if (function_exists('SmartyPants')) $text = SmartyPants($text); 185 | return $text; 186 | } 187 | # Fake restricted version: restrictions are not supported for now. 188 | function TextileRestricted($text, $lite='', $noimage='') { 189 | return $this->TextileThis($text, $lite); 190 | } 191 | # Workaround to ensure compatibility with TextPattern 4.0.3. 192 | function blockLite($text) { return $text; } 193 | } 194 | } 195 | 196 | 197 | 198 | # 199 | # Markdown Parser Class 200 | # 201 | 202 | class Markdown_Parser { 203 | 204 | # Regex to match balanced [brackets]. 205 | # Needed to insert a maximum bracked depth while converting to PHP. 206 | var $nested_brackets_depth = 6; 207 | var $nested_brackets; 208 | 209 | var $nested_url_parenthesis_depth = 4; 210 | var $nested_url_parenthesis; 211 | 212 | # Table of hash values for escaped characters: 213 | var $escape_chars = '\`*_{}[]()>#+-.!'; 214 | var $escape_table = array(); 215 | var $backslash_escape_table = array(); 216 | 217 | # Change to ">" for HTML output. 218 | var $empty_element_suffix = MARKDOWN_EMPTY_ELEMENT_SUFFIX; 219 | var $tab_width = MARKDOWN_TAB_WIDTH; 220 | 221 | 222 | function Markdown_Parser() { 223 | # 224 | # Constructor function. Initialize appropriate member variables. 225 | # 226 | $this->_initDetab(); 227 | 228 | $this->nested_brackets = 229 | str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth). 230 | str_repeat('\])*', $this->nested_brackets_depth); 231 | 232 | $this->nested_url_parenthesis = 233 | str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth). 234 | str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth); 235 | 236 | # Create an identical table but for escaped characters. 237 | foreach (preg_split('/(?!^|$)/', $this->escape_chars) as $char) { 238 | $entity = "&#". ord($char). ";"; 239 | $this->escape_table[$char] = $entity; 240 | $this->backslash_escape_table["\\$char"] = $entity; 241 | } 242 | 243 | # Sort document, block, and span gamut in ascendent priority order. 244 | asort($this->document_gamut); 245 | asort($this->block_gamut); 246 | asort($this->span_gamut); 247 | } 248 | 249 | 250 | # Internal hashes used during transformation. 251 | var $urls = array(); 252 | var $titles = array(); 253 | var $html_blocks = array(); 254 | var $html_hashes = array(); # Contains both blocks and span hashes. 255 | 256 | # Status flag to avoid invalid nesting. 257 | var $in_anchor = false; 258 | 259 | 260 | function transform($text) { 261 | # 262 | # Main function. The order in which other subs are called here is 263 | # essential. Link and image substitutions need to happen before 264 | # _EscapeSpecialCharsWithinTagAttributes(), so that any *'s or _'s in the 265 | # and tags get encoded. 266 | # 267 | # Clear the global hashes. If we don't clear these, you get conflicts 268 | # from other articles when generating a page which contains more than 269 | # one article (e.g. an index page that shows the N most recent 270 | # articles): 271 | $this->urls = array(); 272 | $this->titles = array(); 273 | $this->html_blocks = array(); 274 | $this->html_hashes = array(); 275 | 276 | # Standardize line endings: 277 | # DOS to Unix and Mac to Unix 278 | $text = str_replace(array("\r\n", "\r"), "\n", $text); 279 | 280 | # Make sure $text ends with a couple of newlines: 281 | $text .= "\n\n"; 282 | 283 | # Convert all tabs to spaces. 284 | $text = $this->detab($text); 285 | 286 | # Turn block-level HTML blocks into hash entries 287 | $text = $this->hashHTMLBlocks($text); 288 | 289 | # Strip any lines consisting only of spaces and tabs. 290 | # This makes subsequent regexen easier to write, because we can 291 | # match consecutive blank lines with /\n+/ instead of something 292 | # contorted like /[ ]*\n+/ . 293 | $text = preg_replace('/^[ ]+$/m', '', $text); 294 | 295 | # Run document gamut methods. 296 | foreach ($this->document_gamut as $method => $priority) { 297 | $text = $this->$method($text); 298 | } 299 | 300 | return $text . "\n"; 301 | } 302 | 303 | var $document_gamut = array( 304 | # Strip link definitions, store in hashes. 305 | "stripLinkDefinitions" => 20, 306 | 307 | "runBasicBlockGamut" => 30, 308 | ); 309 | 310 | 311 | function stripLinkDefinitions($text) { 312 | # 313 | # Strips link definitions from text, stores the URLs and titles in 314 | # hash references. 315 | # 316 | $less_than_tab = $this->tab_width - 1; 317 | 318 | # Link defs are in the form: ^[id]: url "optional title" 319 | $text = preg_replace_callback('{ 320 | ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1 321 | [ ]* 322 | \n? # maybe *one* newline 323 | [ ]* 324 | ? # url = $2 325 | [ ]* 326 | \n? # maybe one newline 327 | [ ]* 328 | (?: 329 | (?<=\s) # lookbehind for whitespace 330 | ["(] 331 | (.*?) # title = $3 332 | [")] 333 | [ ]* 334 | )? # title is optional 335 | (?:\n+|\Z) 336 | }xm', 337 | array(&$this, '_stripLinkDefinitions_callback'), 338 | $text); 339 | return $text; 340 | } 341 | function _stripLinkDefinitions_callback($matches) { 342 | $link_id = strtolower($matches[1]); 343 | $this->urls[$link_id] = $this->encodeAmpsAndAngles($matches[2]); 344 | if (isset($matches[3])) 345 | $this->titles[$link_id] = str_replace('"', '"', $matches[3]); 346 | return ''; # String that will replace the block 347 | } 348 | 349 | 350 | function hashHTMLBlocks($text) { 351 | $less_than_tab = $this->tab_width - 1; 352 | 353 | # Hashify HTML blocks: 354 | # We only want to do this for block-level HTML tags, such as headers, 355 | # lists, and tables. That's because we still want to wrap

    s around 356 | # "paragraphs" that are wrapped in non-block-level tags, such as anchors, 357 | # phrase emphasis, and spans. The list of tags we're looking for is 358 | # hard-coded: 359 | $block_tags_a = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'. 360 | 'script|noscript|form|fieldset|iframe|math|ins|del'; 361 | $block_tags_b = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'. 362 | 'script|noscript|form|fieldset|iframe|math'; 363 | 364 | # Regular expression for the content of a block tag. 365 | $nested_tags_level = 4; 366 | $attr = ' 367 | (?> # optional tag attributes 368 | \s # starts with whitespace 369 | (?> 370 | [^>"/]+ # text outside quotes 371 | | 372 | /+(?!>) # slash not followed by ">" 373 | | 374 | "[^"]*" # text inside double quotes (tolerate ">") 375 | | 376 | \'[^\']*\' # text inside single quotes (tolerate ">") 377 | )* 378 | )? 379 | '; 380 | $content = 381 | str_repeat(' 382 | (?> 383 | [^<]+ # content without tag 384 | | 385 | <\2 # nested opening tag 386 | '.$attr.' # attributes 387 | (?: 388 | /> 389 | | 390 | >', $nested_tags_level). # end of opening tag 391 | '.*?'. # last level nested tag content 392 | str_repeat(' 393 | # closing nested tag 394 | ) 395 | | 396 | <(?!/\2\s*> # other tags with a different name 397 | ) 398 | )*', 399 | $nested_tags_level); 400 | 401 | # First, look for nested blocks, e.g.: 402 | #

    403 | #
    404 | # tags for inner block must be indented. 405 | #
    406 | #
    407 | # 408 | # The outermost tags must start at the left margin for this to match, and 409 | # the inner nested divs must be indented. 410 | # We need to do this before the next, more liberal match, because the next 411 | # match will start at the first `
    ` and stop at the first `
    `. 412 | $text = preg_replace_callback('{ 413 | ( # save in $1 414 | ^ # start of line (with /m) 415 | <('.$block_tags_a.')# start tag = $2 416 | '.$attr.'>\n # attributes followed by > and \n 417 | '.$content.' # content, support nesting 418 | # the matching end tag 419 | [ ]* # trailing spaces/tabs 420 | (?=\n+|\Z) # followed by a newline or end of document 421 | ) 422 | }xmi', 423 | array(&$this, '_hashHTMLBlocks_callback'), 424 | $text); 425 | 426 | # 427 | # Match from `\n` to `\n`, handling nested tags in between. 428 | # 429 | $text = preg_replace_callback('{ 430 | ( # save in $1 431 | ^ # start of line (with /m) 432 | <('.$block_tags_b.')# start tag = $2 433 | '.$attr.'> # attributes followed by > 434 | '.$content.' # content, support nesting 435 | # the matching end tag 436 | [ ]* # trailing spaces/tabs 437 | (?=\n+|\Z) # followed by a newline or end of document 438 | ) 439 | }xmi', 440 | array(&$this, '_hashHTMLBlocks_callback'), 441 | $text); 442 | 443 | # Special case just for
    . It was easier to make a special case than 444 | # to make the other regex more complicated. 445 | $text = preg_replace_callback('{ 446 | (?: 447 | (?<=\n\n) # Starting after a blank line 448 | | # or 449 | \A\n? # the beginning of the doc 450 | ) 451 | ( # save in $1 452 | [ ]{0,'.$less_than_tab.'} 453 | <(hr) # start tag = $2 454 | \b # word break 455 | ([^<>])*? # 456 | /?> # the matching end tag 457 | [ ]* 458 | (?=\n{2,}|\Z) # followed by a blank line or end of document 459 | ) 460 | }xi', 461 | array(&$this, '_hashHTMLBlocks_callback'), 462 | $text); 463 | 464 | # Special case for standalone HTML comments: 465 | $text = preg_replace_callback('{ 466 | (?: 467 | (?<=\n\n) # Starting after a blank line 468 | | # or 469 | \A\n? # the beginning of the doc 470 | ) 471 | ( # save in $1 472 | [ ]{0,'.$less_than_tab.'} 473 | (?s: 474 | 475 | ) 476 | [ ]* 477 | (?=\n{2,}|\Z) # followed by a blank line or end of document 478 | ) 479 | }x', 480 | array(&$this, '_hashHTMLBlocks_callback'), 481 | $text); 482 | 483 | # PHP and ASP-style processor instructions ( 496 | ) 497 | [ ]* 498 | (?=\n{2,}|\Z) # followed by a blank line or end of document 499 | ) 500 | }x', 501 | array(&$this, '_hashHTMLBlocks_callback'), 502 | $text); 503 | 504 | return $text; 505 | } 506 | function _hashHTMLBlocks_callback($matches) { 507 | $text = $matches[1]; 508 | $key = $this->hashBlock($text); 509 | return "\n\n$key\n\n"; 510 | } 511 | 512 | 513 | function hashBlock($text) { 514 | # 515 | # Called whenever a tag must be hashed when a function insert a block-level 516 | # tag in $text, it pass through this function and is automaticaly escaped, 517 | # which remove the need to call _HashHTMLBlocks at every step. 518 | # 519 | # Swap back any tag hash found in $text so we do not have to `unhash` 520 | # multiple times at the end. 521 | $text = $this->unhash($text); 522 | 523 | # Then hash the block. 524 | $key = "B\x1A". md5($text); 525 | $this->html_hashes[$key] = $text; 526 | $this->html_blocks[$key] = $text; 527 | return $key; # String that will replace the tag. 528 | } 529 | 530 | 531 | function hashSpan($text, $word_separator = false) { 532 | # 533 | # Called whenever a tag must be hashed when a function insert a span-level 534 | # element in $text, it pass through this function and is automaticaly 535 | # escaped, blocking invalid nested overlap. If optional argument 536 | # $word_separator is true, surround the hash value by spaces. 537 | # 538 | # Swap back any tag hash found in $text so we do not have to `unhash` 539 | # multiple times at the end. 540 | $text = $this->unhash($text); 541 | 542 | # Then hash the span. 543 | $key = "S\x1A". md5($text); 544 | if ($word_separator) $key = ":$key:"; 545 | 546 | $this->html_hashes[$key] = $text; 547 | return $key; # String that will replace the span tag. 548 | } 549 | 550 | 551 | var $block_gamut = array( 552 | # 553 | # These are all the transformations that form block-level 554 | # tags like paragraphs, headers, and list items. 555 | # 556 | "doHeaders" => 10, 557 | "doHorizontalRules" => 20, 558 | 559 | "doLists" => 40, 560 | "doCodeBlocks" => 50, 561 | "doBlockQuotes" => 60, 562 | ); 563 | 564 | function runBlockGamut($text) { 565 | # 566 | # Run block gamut tranformations. 567 | # 568 | # We need to escape raw HTML in Markdown source before doing anything 569 | # else. This need to be done for each block, and not only at the 570 | # begining in the Markdown function since hashed blocks can be part of 571 | # list items and could have been indented. Indented blocks would have 572 | # been seen as a code block in a previous pass of hashHTMLBlocks. 573 | $text = $this->hashHTMLBlocks($text); 574 | 575 | return $this->runBasicBlockGamut($text); 576 | } 577 | 578 | function runBasicBlockGamut($text) { 579 | # 580 | # Run block gamut tranformations, without hashing HTML blocks. This is 581 | # useful when HTML blocks are known to be already hashed, like in the first 582 | # whole-document pass. 583 | # 584 | foreach ($this->block_gamut as $method => $priority) { 585 | $text = $this->$method($text); 586 | } 587 | 588 | # Finally form paragraph and restore hashed blocks. 589 | $text = $this->formParagraphs($text); 590 | 591 | return $text; 592 | } 593 | 594 | 595 | function doHorizontalRules($text) { 596 | # Do Horizontal Rules: 597 | return preg_replace( 598 | array('{^[ ]{0,2}([ ]?\*[ ]?){3,}[ ]*$}mx', 599 | '{^[ ]{0,2}([ ]? -[ ]?){3,}[ ]*$}mx', 600 | '{^[ ]{0,2}([ ]? _[ ]?){3,}[ ]*$}mx'), 601 | "\n".$this->hashBlock("empty_element_suffix")."\n", 602 | $text); 603 | } 604 | 605 | 606 | var $span_gamut = array( 607 | # 608 | # These are all the transformations that occur *within* block-level 609 | # tags like paragraphs, headers, and list items. 610 | # 611 | "escapeSpecialCharsWithinTagAttributes" => -20, 612 | "doCodeSpans" => -10, 613 | "encodeBackslashEscapes" => -5, 614 | 615 | # Process anchor and image tags. Images must come first, 616 | # because ![foo][f] looks like an anchor. 617 | "doImages" => 10, 618 | "doAnchors" => 20, 619 | 620 | # Make links out of things like `` 621 | # Must come after doAnchors, because you can use < and > 622 | # delimiters in inline links like [this](). 623 | "doAutoLinks" => 30, 624 | "encodeAmpsAndAngles" => 40, 625 | 626 | "doItalicsAndBold" => 50, 627 | "doHardBreaks" => 60, 628 | ); 629 | 630 | function runSpanGamut($text) { 631 | # 632 | # Run span gamut tranformations. 633 | # 634 | foreach ($this->span_gamut as $method => $priority) { 635 | $text = $this->$method($text); 636 | } 637 | 638 | return $text; 639 | } 640 | 641 | 642 | function doHardBreaks($text) { 643 | # Do hard breaks: 644 | $br_tag = $this->hashSpan("empty_element_suffix\n"); 645 | return preg_replace('/ {2,}\n/', $br_tag, $text); 646 | } 647 | 648 | 649 | function escapeSpecialCharsWithinTagAttributes($text) { 650 | # 651 | # Within tags -- meaning between < and > -- encode [\ ` * _] so they 652 | # don't conflict with their use in Markdown for code, italics and strong. 653 | # We're replacing each such character with its corresponding MD5 checksum 654 | # value; this is likely overkill, but it should prevent us from colliding 655 | # with the escape values by accident. 656 | # 657 | $tokens = $this->tokenizeHTML($text); 658 | $text = ''; # rebuild $text from the tokens 659 | 660 | foreach ($tokens as $cur_token) { 661 | if ($cur_token[0] == 'tag') { 662 | $cur_token[1] = str_replace('\\', $this->escape_table['\\'], $cur_token[1]); 663 | $cur_token[1] = str_replace('`', $this->escape_table['`'], $cur_token[1]); 664 | $cur_token[1] = str_replace('*', $this->escape_table['*'], $cur_token[1]); 665 | $cur_token[1] = str_replace('_', $this->escape_table['_'], $cur_token[1]); 666 | } 667 | $text .= $cur_token[1]; 668 | } 669 | return $text; 670 | } 671 | 672 | 673 | function doAnchors($text) { 674 | # 675 | # Turn Markdown link shortcuts into XHTML
    tags. 676 | # 677 | if ($this->in_anchor) return $text; 678 | $this->in_anchor = true; 679 | 680 | # 681 | # First, handle reference-style links: [link text] [id] 682 | # 683 | $text = preg_replace_callback('{ 684 | ( # wrap whole match in $1 685 | \[ 686 | ('.$this->nested_brackets.') # link text = $2 687 | \] 688 | 689 | [ ]? # one optional space 690 | (?:\n[ ]*)? # one optional newline followed by spaces 691 | 692 | \[ 693 | (.*?) # id = $3 694 | \] 695 | ) 696 | }xs', 697 | array(&$this, '_doAnchors_reference_callback'), $text); 698 | 699 | # 700 | # Next, inline-style links: [link text](url "optional title") 701 | # 702 | $text = preg_replace_callback('{ 703 | ( # wrap whole match in $1 704 | \[ 705 | ('.$this->nested_brackets.') # link text = $2 706 | \] 707 | \( # literal paren 708 | [ ]* 709 | (?: 710 | <(\S*)> # href = $3 711 | | 712 | ('.$this->nested_url_parenthesis.') # href = $4 713 | ) 714 | [ ]* 715 | ( # $5 716 | ([\'"]) # quote char = $6 717 | (.*?) # Title = $7 718 | \6 # matching quote 719 | [ ]* # ignore any spaces/tabs between closing quote and ) 720 | )? # title is optional 721 | \) 722 | ) 723 | }xs', 724 | array(&$this, '_DoAnchors_inline_callback'), $text); 725 | 726 | # 727 | # Last, handle reference-style shortcuts: [link text] 728 | # These must come last in case you've also got [link test][1] 729 | # or [link test](/foo) 730 | # 731 | // $text = preg_replace_callback('{ 732 | // ( # wrap whole match in $1 733 | // \[ 734 | // ([^\[\]]+) # link text = $2; can\'t contain [ or ] 735 | // \] 736 | // ) 737 | // }xs', 738 | // array(&$this, '_doAnchors_reference_callback'), $text); 739 | 740 | $this->in_anchor = false; 741 | return $text; 742 | } 743 | function _doAnchors_reference_callback($matches) { 744 | $whole_match = $matches[1]; 745 | $link_text = $matches[2]; 746 | $link_id =& $matches[3]; 747 | 748 | if ($link_id == "") { 749 | # for shortcut links like [this][] or [this]. 750 | $link_id = $link_text; 751 | } 752 | 753 | # lower-case and turn embedded newlines into spaces 754 | $link_id = strtolower($link_id); 755 | $link_id = preg_replace('{[ ]?\n}', ' ', $link_id); 756 | 757 | if (isset($this->urls[$link_id])) { 758 | $url = $this->urls[$link_id]; 759 | $url = $this->encodeAmpsAndAngles($url); 760 | 761 | $result = "titles[$link_id] ) ) { 763 | $title = $this->titles[$link_id]; 764 | $title = $this->encodeAmpsAndAngles($title); 765 | $result .= " title=\"$title\""; 766 | } 767 | 768 | $link_text = $this->runSpanGamut($link_text); 769 | $result .= ">$link_text"; 770 | $result = $this->hashSpan($result); 771 | } 772 | else { 773 | $result = $whole_match; 774 | } 775 | return $result; 776 | } 777 | function _doAnchors_inline_callback($matches) { 778 | $whole_match = $matches[1]; 779 | $link_text = $this->runSpanGamut($matches[2]); 780 | $url = $matches[3] == '' ? $matches[4] : $matches[3]; 781 | $title =& $matches[7]; 782 | 783 | $url = $this->encodeAmpsAndAngles($url); 784 | 785 | $result = "encodeAmpsAndAngles($title); 789 | $result .= " title=\"$title\""; 790 | } 791 | 792 | $link_text = $this->runSpanGamut($link_text); 793 | $result .= ">$link_text"; 794 | 795 | return $this->hashSpan($result); 796 | } 797 | 798 | 799 | function doImages($text) { 800 | # 801 | # Turn Markdown image shortcuts into tags. 802 | # 803 | # 804 | # First, handle reference-style labeled images: ![alt text][id] 805 | # 806 | $text = preg_replace_callback('{ 807 | ( # wrap whole match in $1 808 | !\[ 809 | ('.$this->nested_brackets.') # alt text = $2 810 | \] 811 | 812 | [ ]? # one optional space 813 | (?:\n[ ]*)? # one optional newline followed by spaces 814 | 815 | \[ 816 | (.*?) # id = $3 817 | \] 818 | 819 | ) 820 | }xs', 821 | array(&$this, '_doImages_reference_callback'), $text); 822 | 823 | # 824 | # Next, handle inline images: ![alt text](url "optional title") 825 | # Don't forget: encode * and _ 826 | # 827 | $text = preg_replace_callback('{ 828 | ( # wrap whole match in $1 829 | !\[ 830 | ('.$this->nested_brackets.') # alt text = $2 831 | \] 832 | \s? # One optional whitespace character 833 | \( # literal paren 834 | [ ]* 835 | (?: 836 | <(\S*)> # src url = $3 837 | | 838 | ('.$this->nested_url_parenthesis.') # src url = $4 839 | ) 840 | [ ]* 841 | ( # $5 842 | ([\'"]) # quote char = $6 843 | (.*?) # title = $7 844 | \6 # matching quote 845 | [ ]* 846 | )? # title is optional 847 | \) 848 | ) 849 | }xs', 850 | array(&$this, '_doImages_inline_callback'), $text); 851 | 852 | return $text; 853 | } 854 | function _doImages_reference_callback($matches) { 855 | $whole_match = $matches[1]; 856 | $alt_text = $matches[2]; 857 | $link_id = strtolower($matches[3]); 858 | 859 | if ($link_id == "") { 860 | $link_id = strtolower($alt_text); # for shortcut links like ![this][]. 861 | } 862 | 863 | $alt_text = str_replace('"', '"', $alt_text); 864 | if (isset($this->urls[$link_id])) { 865 | $url = $this->urls[$link_id]; 866 | $result = "\"$alt_text\"";titles[$link_id])) { 868 | $title = $this->titles[$link_id]; 869 | $result .= " title=\"$title\""; 870 | } 871 | $result .= $this->empty_element_suffix; 872 | $result = $this->hashSpan($result); 873 | } 874 | else { 875 | # If there's no such link ID, leave intact: 876 | $result = $whole_match; 877 | } 878 | 879 | return $result; 880 | } 881 | function _doImages_inline_callback($matches) { 882 | $whole_match = $matches[1]; 883 | $alt_text = $matches[2]; 884 | $url = $matches[3] == '' ? $matches[4] : $matches[3]; 885 | $title =& $matches[7]; 886 | 887 | $alt_text = str_replace('"', '"', $alt_text); 888 | $result = "\"$alt_text\"";empty_element_suffix; 894 | 895 | return $this->hashSpan($result); 896 | } 897 | 898 | 899 | function doHeaders($text) { 900 | # Setext-style headers: 901 | # Header 1 902 | # ======== 903 | # 904 | # Header 2 905 | # -------- 906 | # 907 | $text = preg_replace_callback('{ ^(.+?)[ ]*\n=+[ ]*\n+ }mx', 908 | array(&$this, '_doHeaders_callback_setext_h1'), $text); 909 | $text = preg_replace_callback('{ ^(.+?)[ ]*\n-+[ ]*\n+ }mx', 910 | array(&$this, '_doHeaders_callback_setext_h2'), $text); 911 | 912 | # atx-style headers: 913 | # # Header 1 914 | # ## Header 2 915 | # ## Header 2 with closing hashes ## 916 | # ... 917 | # ###### Header 6 918 | # 919 | $text = preg_replace_callback('{ 920 | ^(\#{1,6}) # $1 = string of #\'s 921 | [ ]* 922 | (.+?) # $2 = Header text 923 | [ ]* 924 | \#* # optional closing #\'s (not counted) 925 | \n+ 926 | }xm', 927 | array(&$this, '_doHeaders_callback_atx'), $text); 928 | 929 | return $text; 930 | } 931 | function _doHeaders_callback_setext_h1($matches) { 932 | $block = "

    ".$this->runSpanGamut($matches[1])."

    "; 933 | return "\n" . $this->hashBlock($block) . "\n\n"; 934 | } 935 | function _doHeaders_callback_setext_h2($matches) { 936 | $block = "

    ".$this->runSpanGamut($matches[1])."

    "; 937 | return "\n" . $this->hashBlock($block) . "\n\n"; 938 | } 939 | function _doHeaders_callback_atx($matches) { 940 | $level = strlen($matches[1]); 941 | $block = "".$this->runSpanGamut($matches[2]).""; 942 | return "\n" . $this->hashBlock($block) . "\n\n"; 943 | } 944 | 945 | 946 | function doLists($text) { 947 | # 948 | # Form HTML ordered (numbered) and unordered (bulleted) lists. 949 | # 950 | $less_than_tab = $this->tab_width - 1; 951 | 952 | # Re-usable patterns to match list item bullets and number markers: 953 | $marker_ul = '[*+-]'; 954 | $marker_ol = '\d+[.]'; 955 | $marker_any = "(?:$marker_ul|$marker_ol)"; 956 | 957 | $markers = array($marker_ul, $marker_ol); 958 | 959 | foreach ($markers as $marker) { 960 | # Re-usable pattern to match any entirel ul or ol list: 961 | $whole_list = ' 962 | ( # $1 = whole list 963 | ( # $2 964 | [ ]{0,'.$less_than_tab.'} 965 | ('.$marker.') # $3 = first list item marker 966 | [ ]+ 967 | ) 968 | (?s:.+?) 969 | ( # $4 970 | \z 971 | | 972 | \n{2,} 973 | (?=\S) 974 | (?! # Negative lookahead for another list item marker 975 | [ ]* 976 | '.$marker.'[ ]+ 977 | ) 978 | ) 979 | ) 980 | '; // mx 981 | 982 | # We use a different prefix before nested lists than top-level lists. 983 | # See extended comment in _ProcessListItems(). 984 | 985 | if ($this->list_level) { 986 | $text = preg_replace_callback('{ 987 | ^ 988 | '.$whole_list.' 989 | }mx', 990 | array(&$this, '_doLists_callback'), $text); 991 | } 992 | else { 993 | $text = preg_replace_callback('{ 994 | (?:(?<=\n)\n|\A\n?) # Must eat the newline 995 | '.$whole_list.' 996 | }mx', 997 | array(&$this, '_doLists_callback'), $text); 998 | } 999 | } 1000 | 1001 | return $text; 1002 | } 1003 | function _doLists_callback($matches) { 1004 | # Re-usable patterns to match list item bullets and number markers: 1005 | $marker_ul = '[*+-]'; 1006 | $marker_ol = '\d+[.]'; 1007 | $marker_any = "(?:$marker_ul|$marker_ol)"; 1008 | 1009 | $list = $matches[1]; 1010 | $list_type = preg_match("/$marker_ul/", $matches[3]) ? "ul" : "ol"; 1011 | 1012 | $marker_any = ( $list_type == "ul" ? $marker_ul : $marker_ol ); 1013 | 1014 | $list .= "\n"; 1015 | $result = $this->processListItems($list, $marker_any); 1016 | 1017 | $result = $this->hashBlock("<$list_type>\n" . $result . ""); 1018 | return "\n". $result ."\n\n"; 1019 | } 1020 | 1021 | var $list_level = 0; 1022 | 1023 | function processListItems($list_str, $marker_any) { 1024 | # 1025 | # Process the contents of a single ordered or unordered list, splitting it 1026 | # into individual list items. 1027 | # 1028 | # The $this->list_level global keeps track of when we're inside a list. 1029 | # Each time we enter a list, we increment it; when we leave a list, 1030 | # we decrement. If it's zero, we're not in a list anymore. 1031 | # 1032 | # We do this because when we're not inside a list, we want to treat 1033 | # something like this: 1034 | # 1035 | # I recommend upgrading to version 1036 | # 8. Oops, now this line is treated 1037 | # as a sub-list. 1038 | # 1039 | # As a single paragraph, despite the fact that the second line starts 1040 | # with a digit-period-space sequence. 1041 | # 1042 | # Whereas when we're inside a list (or sub-list), that line will be 1043 | # treated as the start of a sub-list. What a kludge, huh? This is 1044 | # an aspect of Markdown's syntax that's hard to parse perfectly 1045 | # without resorting to mind-reading. Perhaps the solution is to 1046 | # change the syntax rules such that sub-lists must start with a 1047 | # starting cardinal number; e.g. "1." or "a.". 1048 | 1049 | $this->list_level++; 1050 | 1051 | # trim trailing blank lines: 1052 | $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); 1053 | 1054 | $list_str = preg_replace_callback('{ 1055 | (\n)? # leading line = $1 1056 | (^[ ]*) # leading whitespace = $2 1057 | ('.$marker_any.') [ ]+ # list marker = $3 1058 | ((?s:.+?)) # list item text = $4 1059 | (?:(\n+(?=\n))|\n) # tailing blank line = $5 1060 | (?= \n* (\z | \2 ('.$marker_any.') [ ]+)) 1061 | }xm', 1062 | array(&$this, '_processListItems_callback'), $list_str); 1063 | 1064 | $this->list_level--; 1065 | return $list_str; 1066 | } 1067 | function _processListItems_callback($matches) { 1068 | $item = $matches[4]; 1069 | $leading_line =& $matches[1]; 1070 | $leading_space =& $matches[2]; 1071 | $tailing_blank_line =& $matches[5]; 1072 | 1073 | if ($leading_line || $tailing_blank_line || 1074 | preg_match('/\n{2,}/', $item)) 1075 | { 1076 | $item = $this->runBlockGamut($this->outdent($item)."\n"); 1077 | } 1078 | else { 1079 | # Recursion for sub-lists: 1080 | $item = $this->doLists($this->outdent($item)); 1081 | $item = preg_replace('/\n+$/', '', $item); 1082 | $item = $this->runSpanGamut($item); 1083 | } 1084 | 1085 | return "
  • " . $item . "
  • \n"; 1086 | } 1087 | 1088 | 1089 | function doCodeBlocks($text) { 1090 | # 1091 | # Process Markdown `
    ` blocks.
    1092 | 	#
    1093 | 		$text = preg_replace_callback('{
    1094 | 				(?:\n\n|\A)
    1095 | 				(	            # $1 = the code block -- one or more lines, starting with a space/tab
    1096 | 				  (?:
    1097 | 					(?:[ ]{'.$this->tab_width.'} | \t)  # Lines must start with a tab or a tab-width of spaces
    1098 | 					.*\n+
    1099 | 				  )+
    1100 | 				)
    1101 | 				((?=^[ ]{0,'.$this->tab_width.'}\S)|\Z)	# Lookahead for non-space at line-start, or end of doc
    1102 | 			}xm',
    1103 | 			array(&$this, '_doCodeBlocks_callback'), $text);
    1104 | 
    1105 | 		return $text;
    1106 | 	}
    1107 | 	function _doCodeBlocks_callback($matches) {
    1108 | 		$codeblock = $matches[1];
    1109 | 
    1110 | 		$codeblock = $this->encodeCode($this->outdent($codeblock));
    1111 | //		$codeblock = $this->detab($codeblock);
    1112 | 		# trim leading newlines and trailing whitespace
    1113 | 		$codeblock = preg_replace(array('/\A\n+/', '/\n+\z/'), '', $codeblock);
    1114 | 
    1115 | 		$result = "\n\n".$this->hashBlock("
    " . $codeblock . "\n
    ")."\n\n"; 1116 | 1117 | return $result; 1118 | } 1119 | 1120 | 1121 | function doCodeSpans($text) { 1122 | # 1123 | # * Backtick quotes are used for spans. 1124 | # 1125 | # * You can use multiple backticks as the delimiters if you want to 1126 | # include literal backticks in the code span. So, this input: 1127 | # 1128 | # Just type ``foo `bar` baz`` at the prompt. 1129 | # 1130 | # Will translate to: 1131 | # 1132 | #

    Just type foo `bar` baz at the prompt.

    1133 | # 1134 | # There's no arbitrary limit to the number of backticks you 1135 | # can use as delimters. If you need three consecutive backticks 1136 | # in your code, use four for delimiters, etc. 1137 | # 1138 | # * You can use spaces to get literal backticks at the edges: 1139 | # 1140 | # ... type `` `bar` `` ... 1141 | # 1142 | # Turns to: 1143 | # 1144 | # ... type `bar` ... 1145 | # 1146 | $text = preg_replace_callback('@ 1147 | (?encodeCode($c); 1163 | return $this->hashSpan("$c"); 1164 | } 1165 | 1166 | 1167 | function encodeCode($_) { 1168 | # 1169 | # Encode/escape certain characters inside Markdown code runs. 1170 | # The point is that in code, these characters are literals, 1171 | # and lose their special Markdown meanings. 1172 | # 1173 | # Encode all ampersands; HTML entities are not 1174 | # entities within a Markdown code span. 1175 | $_ = str_replace('&', '&', $_); 1176 | 1177 | # Do the angle bracket song and dance: 1178 | $_ = str_replace(array('<', '>'), 1179 | array('<', '>'), $_); 1180 | 1181 | # Now, escape characters that are magic in Markdown: 1182 | // $_ = str_replace(array_keys($this->escape_table), 1183 | // array_values($this->escape_table), $_); 1184 | 1185 | return $_; 1186 | } 1187 | 1188 | 1189 | function doItalicsAndBold($text) { 1190 | # must go first: 1191 | $text = preg_replace_callback('{ 1192 | ( # $1: Marker 1193 | (? 1201 | [^*_]+? # Anthing not em markers. 1202 | | 1203 | # Balence any regular emphasis inside. 1204 | \1 (?=\S) .+? (?<=\S) \1 1205 | | 1206 | . # Allow unbalenced * and _. 1207 | )+? 1208 | ) 1209 | (?<=\S) \1\1 # End mark not preceded by whitespace. 1210 | }sx', 1211 | array(&$this, '_doItalicAndBold_strong_callback'), $text); 1212 | # Then : 1213 | $text = preg_replace_callback( 1214 | '{ ( (?runSpanGamut($text); 1222 | return $this->hashSpan("$text"); 1223 | } 1224 | function _doItalicAndBold_strong_callback($matches) { 1225 | $text = $matches[2]; 1226 | $text = $this->runSpanGamut($text); 1227 | return $this->hashSpan("$text"); 1228 | } 1229 | 1230 | 1231 | function doBlockQuotes($text) { 1232 | $text = preg_replace_callback('/ 1233 | ( # Wrap whole match in $1 1234 | ( 1235 | ^[ ]*>[ ]? # ">" at the start of a line 1236 | .+\n # rest of the first line 1237 | (.+\n)* # subsequent consecutive lines 1238 | \n* # blanks 1239 | )+ 1240 | ) 1241 | /xm', 1242 | array(&$this, '_doBlockQuotes_callback'), $text); 1243 | 1244 | return $text; 1245 | } 1246 | function _doBlockQuotes_callback($matches) { 1247 | $bq = $matches[1]; 1248 | # trim one level of quoting - trim whitespace-only lines 1249 | $bq = preg_replace(array('/^[ ]*>[ ]?/m', '/^[ ]+$/m'), '', $bq); 1250 | $bq = $this->runBlockGamut($bq); # recurse 1251 | 1252 | $bq = preg_replace('/^/m', " ", $bq); 1253 | # These leading spaces cause problem with
     content, 
    1254 | 		# so we need to fix that:
    1255 | 		$bq = preg_replace_callback('{(\s*
    .+?
    )}sx', 1256 | array(&$this, '_DoBlockQuotes_callback2'), $bq); 1257 | 1258 | return "\n". $this->hashBlock("
    \n$bq\n
    ")."\n\n"; 1259 | } 1260 | function _doBlockQuotes_callback2($matches) { 1261 | $pre = $matches[1]; 1262 | $pre = preg_replace('/^ /m', '', $pre); 1263 | return $pre; 1264 | } 1265 | 1266 | 1267 | function formParagraphs($text) { 1268 | # 1269 | # Params: 1270 | # $text - string to process with html

    tags 1271 | # 1272 | # Strip leading and trailing lines: 1273 | $text = preg_replace(array('/\A\n+/', '/\n+\z/'), '', $text); 1274 | 1275 | $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); 1276 | 1277 | # 1278 | # Wrap

    tags. 1279 | # 1280 | foreach ($grafs as $key => $value) { 1281 | if (!isset( $this->html_blocks[$value] )) { 1282 | $value = $this->runSpanGamut($value); 1283 | $value = preg_replace('/^([ ]*)/', "

    ", $value); 1284 | $value .= "

    "; 1285 | $grafs[$key] = $this->unhash($value); 1286 | } 1287 | } 1288 | 1289 | # 1290 | # Unhashify HTML blocks 1291 | # 1292 | foreach ($grafs as $key => $graf) { 1293 | # Modify elements of @grafs in-place... 1294 | if (isset($this->html_blocks[$graf])) { 1295 | $block = $this->html_blocks[$graf]; 1296 | $graf = $block; 1297 | // if (preg_match('{ 1298 | // \A 1299 | // ( # $1 =
    tag 1300 | //
    ]* 1302 | // \b 1303 | // markdown\s*=\s* ([\'"]) # $2 = attr quote char 1304 | // 1 1305 | // \2 1306 | // [^>]* 1307 | // > 1308 | // ) 1309 | // ( # $3 = contents 1310 | // .* 1311 | // ) 1312 | // (
    ) # $4 = closing tag 1313 | // \z 1314 | // }xs', $block, $matches)) 1315 | // { 1316 | // list(, $div_open, , $div_content, $div_close) = $matches; 1317 | // 1318 | // # We can't call Markdown(), because that resets the hash; 1319 | // # that initialization code should be pulled into its own sub, though. 1320 | // $div_content = $this->hashHTMLBlocks($div_content); 1321 | // 1322 | // # Run document gamut methods on the content. 1323 | // foreach ($this->document_gamut as $method => $priority) { 1324 | // $div_content = $this->$method($div_content); 1325 | // } 1326 | // 1327 | // $div_open = preg_replace( 1328 | // '{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open); 1329 | // 1330 | // $graf = $div_open . "\n" . $div_content . "\n" . $div_close; 1331 | // } 1332 | $grafs[$key] = $graf; 1333 | } 1334 | } 1335 | 1336 | return implode("\n\n", $grafs); 1337 | } 1338 | 1339 | 1340 | function encodeAmpsAndAngles($text) { 1341 | # Smart processing for ampersands and angle brackets that need to be encoded. 1342 | 1343 | # Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: 1344 | # http://bumppo.net/projects/amputator/ 1345 | $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/', 1346 | '&', $text);; 1347 | 1348 | # Encode naked <'s 1349 | $text = preg_replace('{<(?![a-z/?\$!%])}i', '<', $text); 1350 | 1351 | return $text; 1352 | } 1353 | 1354 | 1355 | function encodeBackslashEscapes($text) { 1356 | # 1357 | # Parameter: String. 1358 | # Returns: The string, with after processing the following backslash 1359 | # escape sequences. 1360 | # 1361 | # Must process escaped backslashes first. 1362 | return str_replace(array_keys($this->backslash_escape_table), 1363 | array_values($this->backslash_escape_table), $text); 1364 | } 1365 | 1366 | 1367 | function doAutoLinks($text) { 1368 | $text = preg_replace_callback('{<((https?|ftp|dict):[^\'">\s]+)>}', 1369 | array(&$this, '_doAutoLinks_url_callback'), $text); 1370 | 1371 | # Email addresses: 1372 | $text = preg_replace_callback('{ 1373 | < 1374 | (?:mailto:)? 1375 | ( 1376 | [-.\w\x80-\xFF]+ 1377 | \@ 1378 | [-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+ 1379 | ) 1380 | > 1381 | }xi', 1382 | array(&$this, '_doAutoLinks_email_callback'), $text); 1383 | 1384 | return $text; 1385 | } 1386 | function _doAutoLinks_url_callback($matches) { 1387 | $url = $this->encodeAmpsAndAngles($matches[1]); 1388 | $link = "$url"; 1389 | return $this->hashSpan($link); 1390 | } 1391 | function _doAutoLinks_email_callback($matches) { 1392 | $address = $matches[1]; 1393 | $link = $this->encodeEmailAddress($address); 1394 | return $this->hashSpan($link); 1395 | } 1396 | 1397 | 1398 | function encodeEmailAddress($addr) { 1399 | # 1400 | # Input: an email address, e.g. "foo@example.com" 1401 | # 1402 | # Output: the email address as a mailto link, with each character 1403 | # of the address encoded as either a decimal or hex entity, in 1404 | # the hopes of foiling most address harvesting spam bots. E.g.: 1405 | # 1406 | #

    foo@exampl 1409 | # e.com

    1410 | # 1411 | # Based by a filter by Matthew Wickline, posted to BBEdit-Talk. 1412 | # With some optimizations by Milian Wolff. 1413 | # 1414 | $addr = "mailto:" . $addr; 1415 | $chars = preg_split('/(? $char) { 1419 | $ord = ord($char); 1420 | # Ignore non-ascii chars. 1421 | if ($ord < 128) { 1422 | $r = ($seed * (1 + $key)) % 100; # Pseudo-random function. 1423 | # roughly 10% raw, 45% hex, 45% dec 1424 | # '@' *must* be encoded. I insist. 1425 | if ($r > 90 && $char != '@') /* do nothing */; 1426 | else if ($r < 45) $chars[$key] = '&#x'.dechex($ord).';'; 1427 | else $chars[$key] = '&#'.$ord.';'; 1428 | } 1429 | } 1430 | 1431 | $addr = implode('', $chars); 1432 | $text = implode('', array_slice($chars, 7)); # text without `mailto:` 1433 | $addr = "$text"; 1434 | 1435 | return $addr; 1436 | } 1437 | 1438 | 1439 | function tokenizeHTML($str) { 1440 | # 1441 | # Parameter: String containing HTML + Markdown markup. 1442 | # Returns: An array of the tokens comprising the input 1443 | # string. Each token is either a tag or a run of text 1444 | # between tags. Each element of the array is a 1445 | # two-element array; the first is either 'tag' or 'text'; 1446 | # the second is the actual value. 1447 | # Note: Markdown code spans are taken into account: no tag token is 1448 | # generated within a code span. 1449 | # 1450 | $tokens = array(); 1451 | 1452 | while ($str != "") { 1453 | # 1454 | # Each loop iteration seach for either the next tag or the next 1455 | # openning code span marker. If a code span marker is found, the 1456 | # code span is extracted in entierty and will result in an extra 1457 | # text token. 1458 | # 1459 | $parts = preg_split('{ 1460 | ( 1461 | (? # comment 1465 | | 1466 | <\?.*?\?> | <%.*?%> # processing instruction 1467 | | 1468 | <[/!$]?[-a-zA-Z0-9:]+ # regular tags 1469 | (?: 1470 | \s 1471 | (?>[^"\'>]+|"[^"]*"|\'[^\']*\')* 1472 | )? 1473 | > 1474 | ) 1475 | }xs', $str, 2, PREG_SPLIT_DELIM_CAPTURE); 1476 | 1477 | # Create token from text preceding tag. 1478 | if ($parts[0] != "") { 1479 | $tokens[] = array('text', $parts[0]); 1480 | } 1481 | 1482 | # Check if we reach the end. 1483 | if (count($parts) < 3) { 1484 | break; 1485 | } 1486 | 1487 | # Create token from tag or code span. 1488 | if ($parts[1]{0} == "`") { 1489 | $tokens[] = array('text', $parts[1]); 1490 | $str = $parts[2]; 1491 | 1492 | # Skip the whole code span, pass as text token. 1493 | if (preg_match('/^(.*(?tab_width})/m", "", $text); 1514 | } 1515 | 1516 | 1517 | # String length function for detab. `_initDetab` will create a function to 1518 | # hanlde UTF-8 if the default function does not exist. 1519 | var $utf8_strlen = 'mb_strlen'; 1520 | 1521 | function detab($text) { 1522 | # 1523 | # Replace tabs with the appropriate amount of space. 1524 | # 1525 | # For each line we separate the line in blocks delemited by 1526 | # tab characters. Then we reconstruct every line by adding the 1527 | # appropriate number of space between each blocks. 1528 | 1529 | $strlen = $this->utf8_strlen; # strlen function for UTF-8. 1530 | $lines = explode("\n", $text); 1531 | $text = ""; 1532 | 1533 | foreach ($lines as $line) { 1534 | # Split in blocks. 1535 | $blocks = explode("\t", $line); 1536 | # Add each blocks to the line. 1537 | $line = $blocks[0]; 1538 | unset($blocks[0]); # Do not add first block twice. 1539 | foreach ($blocks as $block) { 1540 | # Calculate amount of space, insert spaces, insert block. 1541 | $amount = $this->tab_width - 1542 | $strlen($line, 'UTF-8') % $this->tab_width; 1543 | $line .= str_repeat(" ", $amount) . $block; 1544 | } 1545 | $text .= "$line\n"; 1546 | } 1547 | return $text; 1548 | } 1549 | function _initDetab() { 1550 | # 1551 | # Check for the availability of the function in the `utf8_strlen` property 1552 | # (initially `mb_strlen`). If the function is not available, create a 1553 | # function that will loosely count the number of UTF-8 characters with a 1554 | # regular expression. 1555 | # 1556 | if (function_exists($this->utf8_strlen)) return; 1557 | $this->utf8_strlen = create_function('$text', 'return preg_match_all( 1558 | "/[\\\\x00-\\\\xBF]|[\\\\xC0-\\\\xFF][\\\\x80-\\\\xBF]*/", 1559 | $text, $m);'); 1560 | } 1561 | 1562 | 1563 | function unhash($text) { 1564 | # 1565 | # Swap back in all the tags hashed by _HashHTMLBlocks. 1566 | # 1567 | return str_replace(array_keys($this->html_hashes), 1568 | array_values($this->html_hashes), $text); 1569 | } 1570 | 1571 | } 1572 | 1573 | 1574 | # 1575 | # Markdown Extra Parser Class 1576 | # 1577 | 1578 | class MarkdownExtra_Parser extends Markdown_Parser { 1579 | 1580 | # Prefix for footnote ids. 1581 | var $fn_id_prefix = ""; 1582 | 1583 | # Optional title attribute for footnote links and backlinks. 1584 | var $fn_link_title = MARKDOWN_FN_LINK_TITLE; 1585 | var $fn_backlink_title = MARKDOWN_FN_BACKLINK_TITLE; 1586 | 1587 | # Optional class attribute for footnote links and backlinks. 1588 | var $fn_link_class = MARKDOWN_FN_LINK_CLASS; 1589 | var $fn_backlink_class = MARKDOWN_FN_BACKLINK_CLASS; 1590 | 1591 | 1592 | function MarkdownExtra_Parser() { 1593 | # 1594 | # Constructor function. Initialize the parser object. 1595 | # 1596 | # Add extra escapable characters before parent constructor 1597 | # initialize the table. 1598 | $this->escape_chars .= ':|'; 1599 | 1600 | # Insert extra document, block, and span transformations. 1601 | # Parent constructor will do the sorting. 1602 | $this->document_gamut += array( 1603 | "stripFootnotes" => 15, 1604 | "stripAbbreviations" => 25, 1605 | "appendFootnotes" => 50, 1606 | ); 1607 | $this->block_gamut += array( 1608 | "doTables" => 15, 1609 | "doDefLists" => 45, 1610 | ); 1611 | $this->span_gamut += array( 1612 | "doFootnotes" => 5, 1613 | "doAbbreviations" => 70, 1614 | ); 1615 | 1616 | parent::Markdown_Parser(); 1617 | } 1618 | 1619 | 1620 | # Extra hashes used during extra transformations. 1621 | var $footnotes = array(); 1622 | var $footnotes_ordered = array(); 1623 | var $abbr_desciptions = array(); 1624 | var $abbr_matches = array(); 1625 | var $html_cleans = array(); 1626 | 1627 | # Status flag to avoid invalid nesting. 1628 | var $in_footnote = false; 1629 | 1630 | 1631 | function transform($text) { 1632 | # 1633 | # Added clear to the new $html_hashes, reordered `hashHTMLBlocks` before 1634 | # blank line stripping and added extra parameter to `runBlockGamut`. 1635 | # 1636 | # Clear the global hashes. If we don't clear these, you get conflicts 1637 | # from other articles when generating a page which contains more than 1638 | # one article (e.g. an index page that shows the N most recent 1639 | # articles): 1640 | $this->footnotes = array(); 1641 | $this->footnotes_ordered = array(); 1642 | $this->abbr_desciptions = array(); 1643 | $this->abbr_matches = array(); 1644 | $this->html_cleans = array(); 1645 | 1646 | return parent::transform($text); 1647 | } 1648 | 1649 | 1650 | ### HTML Block Parser ### 1651 | 1652 | # Tags that are always treated as block tags: 1653 | var $block_tags = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend'; 1654 | 1655 | # Tags treated as block tags only if the opening tag is alone on it's line: 1656 | var $context_block_tags = 'script|noscript|math|ins|del'; 1657 | 1658 | # Tags where markdown="1" default to span mode: 1659 | var $contain_span_tags = 'p|h[1-6]|li|dd|dt|td|th|legend|address'; 1660 | 1661 | # Tags which must not have their contents modified, no matter where 1662 | # they appear: 1663 | var $clean_tags = 'script|math'; 1664 | 1665 | # Tags that do not need to be closed. 1666 | var $auto_close_tags = 'hr|img'; 1667 | 1668 | 1669 | function hashHTMLBlocks($text) { 1670 | # 1671 | # Hashify HTML Blocks and "clean tags". 1672 | # 1673 | # We only want to do this for block-level HTML tags, such as headers, 1674 | # lists, and tables. That's because we still want to wrap

    s around 1675 | # "paragraphs" that are wrapped in non-block-level tags, such as anchors, 1676 | # phrase emphasis, and spans. The list of tags we're looking for is 1677 | # hard-coded. 1678 | # 1679 | # This works by calling _HashHTMLBlocks_InMarkdown, which then calls 1680 | # _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1" 1681 | # attribute is found whitin a tag, _HashHTMLBlocks_InHTML calls back 1682 | # _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag. 1683 | # These two functions are calling each other. It's recursive! 1684 | # 1685 | # 1686 | # Call the HTML-in-Markdown hasher. 1687 | # 1688 | list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text); 1689 | 1690 | return $text; 1691 | } 1692 | function _hashHTMLBlocks_inMarkdown($text, $indent = 0, 1693 | $enclosing_tag = '', $span = false) 1694 | { 1695 | # 1696 | # Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags. 1697 | # 1698 | # * $indent is the number of space to be ignored when checking for code 1699 | # blocks. This is important because if we don't take the indent into 1700 | # account, something like this (which looks right) won't work as expected: 1701 | # 1702 | #

    1703 | #
    1704 | # Hello World. <-- Is this a Markdown code block or text? 1705 | #
    <-- Is this a Markdown code block or a real tag? 1706 | #
    1707 | # 1708 | # If you don't like this, just don't indent the tag on which 1709 | # you apply the markdown="1" attribute. 1710 | # 1711 | # * If $enclosing_tag is not empty, stops at the first unmatched closing 1712 | # tag with that name. Nested tags supported. 1713 | # 1714 | # * If $span is true, text inside must treated as span. So any double 1715 | # newline will be replaced by a single newline so that it does not create 1716 | # paragraphs. 1717 | # 1718 | # Returns an array of that form: ( processed text , remaining text ) 1719 | # 1720 | if ($text === '') return array('', ''); 1721 | 1722 | # Regex to check for the presense of newlines around a block tag. 1723 | $newline_match_before = '/(?:^\n?|\n\n)*$/'; 1724 | $newline_match_after = 1725 | '{ 1726 | ^ # Start of text following the tag. 1727 | (?:[ ]*)? # Optional comment. 1728 | [ ]*\n # Must be followed by newline. 1729 | }xs'; 1730 | 1731 | # Regex to match any tag. 1732 | $block_tag_match = 1733 | '{ 1734 | ( # $2: Capture hole tag. 1735 | block_tags.' | 1738 | '.$this->context_block_tags.' | 1739 | '.$this->clean_tags.' | 1740 | (?!\s)'.$enclosing_tag.' 1741 | ) 1742 | \s* # Whitespace. 1743 | (?> 1744 | ".*?" | # Double quotes (can contain `>`) 1745 | \'.*?\' | # Single quotes (can contain `>`) 1746 | .+? # Anything but quotes and `>`. 1747 | )*? 1748 | > # End of tag. 1749 | | 1750 | # HTML Comment 1751 | | 1752 | <\?.*?\?> | <%.*?%> # Processing instruction 1753 | | 1754 | # CData Block 1755 | ) 1756 | }xs'; 1757 | 1758 | 1759 | $depth = 0; # Current depth inside the tag tree. 1760 | $parsed = ""; # Parsed text that will be returned. 1761 | 1762 | # 1763 | # Loop through every tag until we find the closing tag of the parent 1764 | # or loop until reaching the end of text if no parent tag specified. 1765 | # 1766 | do { 1767 | # 1768 | # Split the text using the first $tag_match pattern found. 1769 | # Text before pattern will be first in the array, text after 1770 | # pattern will be at the end, and between will be any catches made 1771 | # by the pattern. 1772 | # 1773 | $parts = preg_split($block_tag_match, $text, 2, 1774 | PREG_SPLIT_DELIM_CAPTURE); 1775 | 1776 | # If in Markdown span mode, add a empty-string span-level hash 1777 | # after each newline to prevent triggering any block element. 1778 | if ($span) { 1779 | $void = $this->hashSpan("", true) ; 1780 | $newline = $this->hashSpan("", true) . "\n"; 1781 | $parts[0] = $void . str_replace("\n", $newline, $parts[0]) . $void; 1782 | } 1783 | 1784 | $parsed .= $parts[0]; # Text before current tag. 1785 | 1786 | # If end of $text has been reached. Stop loop. 1787 | if (count($parts) < 3) { 1788 | $text = ""; 1789 | break; 1790 | } 1791 | 1792 | $tag = $parts[1]; # Tag to handle. 1793 | $text = $parts[2]; # Remaining text after current tag. 1794 | 1795 | # 1796 | # Check for: Tag inside code block or span 1797 | # 1798 | if (# Find current paragraph 1799 | preg_match('/(?>^\n?|\n\n)((?>.\n?)+?)$/', $parsed, $matches) && 1800 | ( 1801 | # Then match in it either a code block... 1802 | preg_match('/^ {'.($indent+4).'}.*(?>\n {'.($indent+4).'}.*)*'. 1803 | '(?!\n)$/', $matches[1], $x) || 1804 | # ...or unbalenced code span markers. (the regex matches balenced) 1805 | !preg_match('/^(?>[^`]+|(`+)(?>[^`]+|(?!\1[^`])`)*?\1(?!`))*$/s', 1806 | $matches[1]) 1807 | )) 1808 | { 1809 | # Tag is in code block or span and may not be a tag at all. So we 1810 | # simply skip the first char (should be a `<`). 1811 | $parsed .= $tag{0}; 1812 | $text = substr($tag, 1) . $text; # Put back $tag minus first char. 1813 | } 1814 | # 1815 | # Check for: Opening Block level tag or 1816 | # Opening Content Block tag (like ins and del) 1817 | # used as a block tag (tag is alone on it's line). 1818 | # 1819 | else if (preg_match("{^<(?:$this->block_tags)\b}", $tag) || 1820 | ( preg_match("{^<(?:$this->context_block_tags)\b}", $tag) && 1821 | preg_match($newline_match_before, $parsed) && 1822 | preg_match($newline_match_after, $text) ) 1823 | ) 1824 | { 1825 | # Need to parse tag and following text using the HTML parser. 1826 | list($block_text, $text) = 1827 | $this->_hashHTMLBlocks_inHTML($tag . $text, "hashBlock", true); 1828 | 1829 | # Make sure it stays outside of any paragraph by adding newlines. 1830 | $parsed .= "\n\n$block_text\n\n"; 1831 | } 1832 | # 1833 | # Check for: Clean tag (like script, math) 1834 | # HTML Comments, processing instructions. 1835 | # 1836 | else if (preg_match("{^<(?:$this->clean_tags)\b}", $tag) || 1837 | $tag{1} == '!' || $tag{1} == '?') 1838 | { 1839 | # Need to parse tag and following text using the HTML parser. 1840 | # (don't check for markdown attribute) 1841 | list($block_text, $text) = 1842 | $this->_hashHTMLBlocks_inHTML($tag . $text, "hashClean", false); 1843 | 1844 | $parsed .= $block_text; 1845 | } 1846 | # 1847 | # Check for: Tag with same name as enclosing tag. 1848 | # 1849 | else if ($enclosing_tag !== '' && 1850 | # Same name as enclosing tag. 1851 | preg_match("{^= 0); 1874 | 1875 | return array($parsed, $text); 1876 | } 1877 | function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr) { 1878 | # 1879 | # Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags. 1880 | # 1881 | # * Calls $hash_method to convert any blocks. 1882 | # * Stops when the first opening tag closes. 1883 | # * $md_attr indicate if the use of the `markdown="1"` attribute is allowed. 1884 | # (it is not inside clean tags) 1885 | # 1886 | # Returns an array of that form: ( processed text , remaining text ) 1887 | # 1888 | if ($text === '') return array('', ''); 1889 | 1890 | # Regex to match `markdown` attribute inside of a tag. 1891 | $markdown_attr_match = ' 1892 | { 1893 | \s* # Eat whitespace before the `markdown` attribute 1894 | markdown 1895 | \s*=\s* 1896 | (?: 1897 | (["\']) # $1: quote delimiter 1898 | (.*?) # $2: attribute value 1899 | \1 # matching delimiter 1900 | | 1901 | ([^\s>]*) # $3: unquoted attribute value 1902 | ) 1903 | () # $4: make $3 always defined (avoid warnings) 1904 | }xs'; 1905 | 1906 | # Regex to match any tag. 1907 | $tag_match = '{ 1908 | ( # $2: Capture hole tag. 1909 | 1913 | ".*?" | # Double quotes (can contain `>`) 1914 | \'.*?\' | # Single quotes (can contain `>`) 1915 | .+? # Anything but quotes and `>`. 1916 | )*? 1917 | > # End of tag. 1918 | | 1919 | # HTML Comment 1920 | | 1921 | <\?.*?\?> | <%.*?%> # Processing instruction 1922 | | 1923 | # CData Block 1924 | ) 1925 | }xs'; 1926 | 1927 | $original_text = $text; # Save original text in case of faliure. 1928 | 1929 | $depth = 0; # Current depth inside the tag tree. 1930 | $block_text = ""; # Temporary text holder for current text. 1931 | $parsed = ""; # Parsed text that will be returned. 1932 | 1933 | # 1934 | # Get the name of the starting tag. 1935 | # 1936 | if (preg_match("/^<([\w:$]*)\b/", $text, $matches)) 1937 | $base_tag_name = $matches[1]; 1938 | 1939 | # 1940 | # Loop through every tag until we find the corresponding closing tag. 1941 | # 1942 | do { 1943 | # 1944 | # Split the text using the first $tag_match pattern found. 1945 | # Text before pattern will be first in the array, text after 1946 | # pattern will be at the end, and between will be any catches made 1947 | # by the pattern. 1948 | # 1949 | $parts = preg_split($tag_match, $text, 2, PREG_SPLIT_DELIM_CAPTURE); 1950 | 1951 | if (count($parts) < 3) { 1952 | # 1953 | # End of $text reached with unbalenced tag(s). 1954 | # In that case, we return original text unchanged and pass the 1955 | # first character as filtered to prevent an infinite loop in the 1956 | # parent function. 1957 | # 1958 | return array($original_text{0}, substr($original_text, 1)); 1959 | } 1960 | 1961 | $block_text .= $parts[0]; # Text before current tag. 1962 | $tag = $parts[1]; # Tag to handle. 1963 | $text = $parts[2]; # Remaining text after current tag. 1964 | 1965 | # 1966 | # Check for: Auto-close tag (like
    ) 1967 | # Comments and Processing Instructions. 1968 | # 1969 | if (preg_match("{^auto_close_tags)\b}", $tag) || 1970 | $tag{1} == '!' || $tag{1} == '?') 1971 | { 1972 | # Just add the tag to the block as if it was text. 1973 | $block_text .= $tag; 1974 | } 1975 | else { 1976 | # 1977 | # Increase/decrease nested tag count. Only do so if 1978 | # the tag's name match base tag's. 1979 | # 1980 | if (preg_match("{^mode = $attr_m[2] . $attr_m[3]; 1997 | $span_mode = $this->mode == 'span' || $this->mode != 'block' && 1998 | preg_match("{^<(?:$this->contain_span_tags)\b}", $tag); 1999 | 2000 | # Calculate indent before tag. 2001 | preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches); 2002 | $indent = strlen($matches[1]); 2003 | 2004 | # End preceding block with this tag. 2005 | $block_text .= $tag; 2006 | $parsed .= $this->$hash_method($block_text); 2007 | 2008 | # Get enclosing tag name for the ParseMarkdown function. 2009 | preg_match('/^<([\w:$]*)\b/', $tag, $matches); 2010 | $tag_name = $matches[1]; 2011 | 2012 | # Parse the content using the HTML-in-Markdown parser. 2013 | list ($block_text, $text) 2014 | = $this->_hashHTMLBlocks_inMarkdown($text, $indent, 2015 | $tag_name, $span_mode); 2016 | 2017 | # Outdent markdown text. 2018 | if ($indent > 0) { 2019 | $block_text = preg_replace("/^[ ]{1,$indent}/m", "", 2020 | $block_text); 2021 | } 2022 | 2023 | # Append tag content to parsed text. 2024 | if (!$span_mode) $parsed .= "\n\n$block_text\n\n"; 2025 | else $parsed .= "$block_text"; 2026 | 2027 | # Start over a new block. 2028 | $block_text = ""; 2029 | } 2030 | else $block_text .= $tag; 2031 | } 2032 | 2033 | } while ($depth > 0); 2034 | 2035 | # 2036 | # Hash last block text that wasn't processed inside the loop. 2037 | # 2038 | $parsed .= $this->$hash_method($block_text); 2039 | 2040 | return array($parsed, $text); 2041 | } 2042 | 2043 | 2044 | function hashClean($text) { 2045 | # 2046 | # Called whenever a tag must be hashed when a function insert a "clean" tag 2047 | # in $text, it pass through this function and is automaticaly escaped, 2048 | # blocking invalid nested overlap. 2049 | # 2050 | # Swap back any tag hash found in $text so we do not have to `unhash` 2051 | # multiple times at the end. 2052 | $text = $this->unhash($text); 2053 | 2054 | # Then hash the tag. 2055 | $key = "C\x1A". md5($text); 2056 | $this->html_cleans[$key] = $text; 2057 | $this->html_hashes[$key] = $text; 2058 | return $key; # String that will replace the clean tag. 2059 | } 2060 | 2061 | 2062 | function doHeaders($text) { 2063 | # 2064 | # Redefined to add id attribute support. 2065 | # 2066 | # Setext-style headers: 2067 | # Header 1 {#header1} 2068 | # ======== 2069 | # 2070 | # Header 2 {#header2} 2071 | # -------- 2072 | # 2073 | $text = preg_replace_callback( 2074 | '{ (^.+?) (?:[ ]+\{\#([-_:a-zA-Z0-9]+)\})? [ ]*\n=+[ ]*\n+ }mx', 2075 | array(&$this, '_doHeaders_callback_setext_h1'), $text); 2076 | $text = preg_replace_callback( 2077 | '{ (^.+?) (?:[ ]+\{\#([-_:a-zA-Z0-9]+)\})? [ ]*\n-+[ ]*\n+ }mx', 2078 | array(&$this, '_doHeaders_callback_setext_h2'), $text); 2079 | 2080 | # atx-style headers: 2081 | # # Header 1 {#header1} 2082 | # ## Header 2 {#header2} 2083 | # ## Header 2 with closing hashes ## {#header3} 2084 | # ... 2085 | # ###### Header 6 {#header2} 2086 | # 2087 | $text = preg_replace_callback('{ 2088 | ^(\#{1,6}) # $1 = string of #\'s 2089 | [ ]* 2090 | (.+?) # $2 = Header text 2091 | [ ]* 2092 | \#* # optional closing #\'s (not counted) 2093 | (?:[ ]+\{\#([-_:a-zA-Z0-9]+)\})? # id attribute 2094 | [ ]* 2095 | \n+ 2096 | }xm', 2097 | array(&$this, '_doHeaders_callback_atx'), $text); 2098 | 2099 | return $text; 2100 | } 2101 | function _doHeaders_attr($attr) { 2102 | if (empty($attr)) return ""; 2103 | return " id=\"$attr\""; 2104 | } 2105 | function _doHeaders_callback_setext_h1($matches) { 2106 | $attr = $this->_doHeaders_attr($id =& $matches[2]); 2107 | $block = "".$this->runSpanGamut($matches[1]).""; 2108 | return "\n" . $this->hashBlock($block) . "\n\n"; 2109 | } 2110 | function _doHeaders_callback_setext_h2($matches) { 2111 | $attr = $this->_doHeaders_attr($id =& $matches[2]); 2112 | $block = "".$this->runSpanGamut($matches[1]).""; 2113 | return "\n" . $this->hashBlock($block) . "\n\n"; 2114 | } 2115 | function _doHeaders_callback_atx($matches) { 2116 | $level = strlen($matches[1]); 2117 | $attr = $this->_doHeaders_attr($id =& $matches[3]); 2118 | $block = "".$this->runSpanGamut($matches[2]).""; 2119 | return "\n" . $this->hashBlock($block) . "\n\n"; 2120 | } 2121 | 2122 | 2123 | function doTables($text) { 2124 | # 2125 | # Form HTML tables. 2126 | # 2127 | $less_than_tab = $this->tab_width - 1; 2128 | # 2129 | # Find tables with leading pipe. 2130 | # 2131 | # | Header 1 | Header 2 2132 | # | -------- | -------- 2133 | # | Cell 1 | Cell 2 2134 | # | Cell 3 | Cell 4 2135 | # 2136 | $text = preg_replace_callback(' 2137 | { 2138 | ^ # Start of a line 2139 | [ ]{0,'.$less_than_tab.'} # Allowed whitespace. 2140 | [|] # Optional leading pipe (present) 2141 | (.+) \n # $1: Header row (at least one pipe) 2142 | 2143 | [ ]{0,'.$less_than_tab.'} # Allowed whitespace. 2144 | [|] ([ ]*[-:]+[-| :]*) \n # $2: Header underline 2145 | 2146 | ( # $3: Cells 2147 | (?: 2148 | [ ]* # Allowed whitespace. 2149 | [|] .* \n # Row content. 2150 | )* 2151 | ) 2152 | (?=\n|\Z) # Stop at final double newline. 2153 | }xm', 2154 | array(&$this, '_doTable_leadingPipe_callback'), $text); 2155 | 2156 | # 2157 | # Find tables without leading pipe. 2158 | # 2159 | # Header 1 | Header 2 2160 | # -------- | -------- 2161 | # Cell 1 | Cell 2 2162 | # Cell 3 | Cell 4 2163 | # 2164 | $text = preg_replace_callback(' 2165 | { 2166 | ^ # Start of a line 2167 | [ ]{0,'.$less_than_tab.'} # Allowed whitespace. 2168 | (\S.*[|].*) \n # $1: Header row (at least one pipe) 2169 | 2170 | [ ]{0,'.$less_than_tab.'} # Allowed whitespace. 2171 | ([-:]+[ ]*[|][-| :]*) \n # $2: Header underline 2172 | 2173 | ( # $3: Cells 2174 | (?: 2175 | .* [|] .* \n # Row content 2176 | )* 2177 | ) 2178 | (?=\n|\Z) # Stop at final double newline. 2179 | }xm', 2180 | array(&$this, '_DoTable_callback'), $text); 2181 | 2182 | return $text; 2183 | } 2184 | function _doTable_leadingPipe_callback($matches) { 2185 | $head = $matches[1]; 2186 | $underline = $matches[2]; 2187 | $content = $matches[3]; 2188 | 2189 | # Remove leading pipe for each row. 2190 | $content = preg_replace('/^ *[|]/m', '', $content); 2191 | 2192 | return $this->_doTable_callback(array($matches[0], $head, $underline, $content)); 2193 | } 2194 | function _doTable_callback($matches) { 2195 | $head = $matches[1]; 2196 | $underline = $matches[2]; 2197 | $content = $matches[3]; 2198 | 2199 | # Remove any tailing pipes for each line. 2200 | $head = preg_replace('/[|] *$/m', '', $head); 2201 | $underline = preg_replace('/[|] *$/m', '', $underline); 2202 | $content = preg_replace('/[|] *$/m', '', $content); 2203 | 2204 | # Reading alignement from header underline. 2205 | $separators = preg_split('/ *[|] */', $underline); 2206 | foreach ($separators as $n => $s) { 2207 | if (preg_match('/^ *-+: *$/', $s)) $attr[$n] = ' align="right"'; 2208 | else if (preg_match('/^ *:-+: *$/', $s))$attr[$n] = ' align="center"'; 2209 | else if (preg_match('/^ *:-+ *$/', $s)) $attr[$n] = ' align="left"'; 2210 | else $attr[$n] = ''; 2211 | } 2212 | 2213 | # Creating code spans before splitting the row is an easy way to 2214 | # handle a code span containg pipes. 2215 | $head = $this->doCodeSpans($head); 2216 | $headers = preg_split('/ *[|] */', $head); 2217 | $col_count = count($headers); 2218 | 2219 | # Write column headers. 2220 | $text = "\n"; 2221 | $text .= "\n"; 2222 | $text .= "\n"; 2223 | foreach ($headers as $n => $header) 2224 | $text .= " ".$this->runSpanGamut(trim($header))."\n"; 2225 | $text .= "\n"; 2226 | $text .= "\n"; 2227 | 2228 | # Split content by row. 2229 | $rows = explode("\n", trim($content, "\n")); 2230 | 2231 | $text .= "\n"; 2232 | foreach ($rows as $row) { 2233 | # Creating code spans before splitting the row is an easy way to 2234 | # handle a code span containg pipes. 2235 | $row = $this->doCodeSpans($row); 2236 | 2237 | # Split row by cell. 2238 | $row_cells = preg_split('/ *[|] */', $row, $col_count); 2239 | $row_cells = array_pad($row_cells, $col_count, ''); 2240 | 2241 | $text .= "\n"; 2242 | foreach ($row_cells as $n => $cell) 2243 | $text .= " ".$this->runSpanGamut(trim($cell))."\n"; 2244 | $text .= "\n"; 2245 | } 2246 | $text .= "\n"; 2247 | $text .= "
    "; 2248 | 2249 | return $this->hashBlock($text) . "\n"; 2250 | } 2251 | 2252 | 2253 | function doDefLists($text) { 2254 | # 2255 | # Form HTML definition lists. 2256 | # 2257 | $less_than_tab = $this->tab_width - 1; 2258 | 2259 | # Re-usable pattern to match any entire dl list: 2260 | $whole_list = ' 2261 | ( # $1 = whole list 2262 | ( # $2 2263 | [ ]{0,'.$less_than_tab.'} 2264 | ((?>.*\S.*\n)+) # $3 = defined term 2265 | \n? 2266 | [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition 2267 | ) 2268 | (?s:.+?) 2269 | ( # $4 2270 | \z 2271 | | 2272 | \n{2,} 2273 | (?=\S) 2274 | (?! # Negative lookahead for another term 2275 | [ ]{0,'.$less_than_tab.'} 2276 | (?: \S.*\n )+? # defined term 2277 | \n? 2278 | [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition 2279 | ) 2280 | (?! # Negative lookahead for another definition 2281 | [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition 2282 | ) 2283 | ) 2284 | ) 2285 | '; // mx 2286 | 2287 | $text = preg_replace_callback('{ 2288 | (?:(?<=\n\n)|\A\n?) 2289 | '.$whole_list.' 2290 | }mx', 2291 | array(&$this, '_doDefLists_callback'), $text); 2292 | 2293 | return $text; 2294 | } 2295 | function _doDefLists_callback($matches) { 2296 | # Re-usable patterns to match list item bullets and number markers: 2297 | $list = $matches[1]; 2298 | 2299 | # Turn double returns into triple returns, so that we can make a 2300 | # paragraph for the last item in a list, if necessary: 2301 | $result = trim($this->processDefListItems($list)); 2302 | $result = "
    \n" . $result . "\n
    "; 2303 | return $this->hashBlock($result) . "\n\n"; 2304 | } 2305 | 2306 | 2307 | function processDefListItems($list_str) { 2308 | # 2309 | # Process the contents of a single definition list, splitting it 2310 | # into individual term and definition list items. 2311 | # 2312 | $less_than_tab = $this->tab_width - 1; 2313 | 2314 | # trim trailing blank lines: 2315 | $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); 2316 | 2317 | # Process definition terms. 2318 | $list_str = preg_replace_callback('{ 2319 | (?:\n\n+|\A\n?) # leading line 2320 | ( # definition terms = $1 2321 | [ ]{0,'.$less_than_tab.'} # leading whitespace 2322 | (?![:][ ]|[ ]) # negative lookahead for a definition 2323 | # mark (colon) or more whitespace. 2324 | (?: \S.* \n)+? # actual term (not whitespace). 2325 | ) 2326 | (?=\n?[ ]{0,3}:[ ]) # lookahead for following line feed 2327 | # with a definition mark. 2328 | }xm', 2329 | array(&$this, '_processDefListItems_callback_dt'), $list_str); 2330 | 2331 | # Process actual definitions. 2332 | $list_str = preg_replace_callback('{ 2333 | \n(\n+)? # leading line = $1 2334 | [ ]{0,'.$less_than_tab.'} # whitespace before colon 2335 | [:][ ]+ # definition mark (colon) 2336 | ((?s:.+?)) # definition text = $2 2337 | (?= \n+ # stop at next definition mark, 2338 | (?: # next term or end of text 2339 | [ ]{0,'.$less_than_tab.'} [:][ ] | 2340 |
    | \z 2341 | ) 2342 | ) 2343 | }xm', 2344 | array(&$this, '_processDefListItems_callback_dd'), $list_str); 2345 | 2346 | return $list_str; 2347 | } 2348 | function _processDefListItems_callback_dt($matches) { 2349 | $terms = explode("\n", trim($matches[1])); 2350 | $text = ''; 2351 | foreach ($terms as $term) { 2352 | $term = $this->runSpanGamut(trim($term)); 2353 | $text .= "\n
    " . $term . "
    "; 2354 | } 2355 | return $text . "\n"; 2356 | } 2357 | function _processDefListItems_callback_dd($matches) { 2358 | $leading_line = $matches[1]; 2359 | $def = $matches[2]; 2360 | 2361 | if ($leading_line || preg_match('/\n{2,}/', $def)) { 2362 | $def = $this->runBlockGamut($this->outdent($def . "\n\n")); 2363 | $def = "\n". $def ."\n"; 2364 | } 2365 | else { 2366 | $def = rtrim($def); 2367 | $def = $this->runSpanGamut($this->outdent($def)); 2368 | } 2369 | 2370 | return "\n
    " . $def . "
    \n"; 2371 | } 2372 | 2373 | 2374 | function doItalicsAndBold($text) { 2375 | # 2376 | # Redefined to change emphasis by underscore behaviour so that it does not 2377 | # work in the middle of a word. 2378 | # 2379 | # must go first: 2380 | $text = preg_replace_callback(array( 2381 | '{ 2382 | ( # $1: Marker 2383 | (? 2391 | [^_]+? # Anthing not em markers. 2392 | | 2393 | # Balence any regular _ emphasis inside. 2394 | (? 2410 | [^*]+? # Anthing not em markers. 2411 | | 2412 | # Balence any regular * emphasis inside. 2413 | \* (?=\S) (.+?) (?<=\S) \* 2414 | | 2415 | \* # Allow unbalenced as last resort. 2416 | )+? 2417 | ) 2418 | (?<=\S) \*\* # End mark not preceded by whitespace. 2419 | }sx', 2420 | ), 2421 | array(&$this, '_doItalicAndBold_strong_callback'), $text); 2422 | # Then : 2423 | $text = preg_replace_callback(array( 2424 | '{ ( (? tags 2437 | # 2438 | # Strip leading and trailing lines: 2439 | $text = preg_replace(array('/\A\n+/', '/\n+\z/'), '', $text); 2440 | 2441 | $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); 2442 | 2443 | # 2444 | # Wrap

    tags and unhashify HTML blocks 2445 | # 2446 | foreach ($grafs as $key => $value) { 2447 | $value = trim($this->runSpanGamut($value)); 2448 | 2449 | # Check if this should be enclosed in a paragraph. 2450 | # Clean tag hashes & block tag hashes are left alone. 2451 | $clean_key = $value; 2452 | $block_key = substr($value, 0, 34); 2453 | 2454 | $is_p = (!isset($this->html_blocks[$block_key]) && 2455 | !isset($this->html_cleans[$clean_key])); 2456 | 2457 | if ($is_p) { 2458 | $value = "

    $value

    "; 2459 | } 2460 | $grafs[$key] = $value; 2461 | } 2462 | 2463 | # Join grafs in one text, then unhash HTML tags. 2464 | $text = implode("\n\n", $grafs); 2465 | 2466 | # Finish by removing any tag hashes still present in $text. 2467 | $text = $this->unhash($text); 2468 | 2469 | return $text; 2470 | } 2471 | 2472 | 2473 | ### Footnotes 2474 | 2475 | function stripFootnotes($text) { 2476 | # 2477 | # Strips link definitions from text, stores the URLs and titles in 2478 | # hash references. 2479 | # 2480 | $less_than_tab = $this->tab_width - 1; 2481 | 2482 | # Link defs are in the form: [^id]: url "optional title" 2483 | $text = preg_replace_callback('{ 2484 | ^[ ]{0,'.$less_than_tab.'}\[\^(.+?)\][ ]?: # note_id = $1 2485 | [ ]* 2486 | \n? # maybe *one* newline 2487 | ( # text = $2 (no blank lines allowed) 2488 | (?: 2489 | .+ # actual text 2490 | | 2491 | \n # newlines but 2492 | (?!\[\^.+?\]:\s)# negative lookahead for footnote marker. 2493 | (?!\n+[ ]{0,3}\S)# ensure line is not blank and followed 2494 | # by non-indented content 2495 | )* 2496 | ) 2497 | }xm', 2498 | array(&$this, '_stripFootnotes_callback'), 2499 | $text); 2500 | return $text; 2501 | } 2502 | function _stripFootnotes_callback($matches) { 2503 | $note_id = $this->fn_id_prefix . $matches[1]; 2504 | $this->footnotes[$note_id] = $this->outdent($matches[2]); 2505 | return ''; # String that will replace the block 2506 | } 2507 | 2508 | 2509 | function doFootnotes($text) { 2510 | # 2511 | # Replace footnote references in $text [^id] with a special text-token 2512 | # which will be can be 2513 | # 2514 | if (!$this->in_footnote && !$this->in_anchor) { 2515 | $text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text); 2516 | } 2517 | return $text; 2518 | } 2519 | 2520 | 2521 | function appendFootnotes($text) { 2522 | # 2523 | # Append footnote list to text. 2524 | # 2525 | 2526 | $text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', 2527 | array(&$this, '_appendFootnotes_callback'), $text); 2528 | 2529 | if (!empty($this->footnotes_ordered)) { 2530 | $text .= "\n\n"; 2531 | $text .= "
    \n"; 2532 | $text .= "fn_backlink_class != "") { 2537 | $class = $this->fn_backlink_class; 2538 | $class = $this->encodeAmpsAndAngles($class); 2539 | $class = str_replace('"', '"', $class); 2540 | $attr .= " class=\"$class\""; 2541 | } 2542 | if ($this->fn_backlink_title != "") { 2543 | $title = $this->fn_backlink_title; 2544 | $title = $this->encodeAmpsAndAngles($title); 2545 | $title = str_replace('"', '"', $title); 2546 | $attr .= " title=\"$title\""; 2547 | } 2548 | $num = 0; 2549 | 2550 | $this->in_footnote = true; 2551 | 2552 | foreach ($this->footnotes_ordered as $note_id => $footnote) { 2553 | $footnote .= "\n"; # Need to append newline before parsing. 2554 | $footnote = $this->runBlockGamut("$footnote\n"); 2555 | 2556 | $attr2 = str_replace("%%", ++$num, $attr); 2557 | 2558 | # Add backlink to last paragraph; create new paragraph if needed. 2559 | $backlink = ""; 2560 | if (preg_match('{

    $}', $footnote)) { 2561 | $footnote = substr($footnote, 0, -4) . " $backlink

    "; 2562 | } else { 2563 | $footnote .= "\n\n

    $backlink

    "; 2564 | } 2565 | 2566 | $text .= "
  • \n"; 2567 | $text .= $footnote . "\n"; 2568 | $text .= "
  • \n\n"; 2569 | } 2570 | 2571 | $this->in_footnote = false; 2572 | 2573 | $text .= "\n"; 2574 | $text .= "
    "; 2575 | } 2576 | return $text; 2577 | } 2578 | function _appendFootnotes_callback($matches) { 2579 | $node_id = $this->fn_id_prefix . $matches[1]; 2580 | 2581 | # Create footnote marker only if it has a corresponding footnote *and* 2582 | # the footnote hasn't been used by another marker. 2583 | if (isset($this->footnotes[$node_id])) { 2584 | # Transfert footnote content to the ordered list. 2585 | $this->footnotes_ordered[$node_id] = $this->footnotes[$node_id]; 2586 | unset($this->footnotes[$node_id]); 2587 | 2588 | $num = count($this->footnotes_ordered); 2589 | $attr = " rel=\"footnote\""; 2590 | if ($this->fn_link_class != "") { 2591 | $class = $this->fn_link_class; 2592 | $class = $this->encodeAmpsAndAngles($class); 2593 | $class = str_replace('"', '"', $class); 2594 | $attr .= " class=\"$class\""; 2595 | } 2596 | if ($this->fn_link_title != "") { 2597 | $title = $this->fn_link_title; 2598 | $title = $this->encodeAmpsAndAngles($title); 2599 | $title = str_replace('"', '"', $title); 2600 | $attr .= " title=\"$title\""; 2601 | } 2602 | $attr = str_replace("%%", $num, $attr); 2603 | 2604 | return 2605 | "". 2606 | "$num". 2607 | ""; 2608 | } 2609 | 2610 | return "[^".$matches[1]."]"; 2611 | } 2612 | 2613 | 2614 | ### Abbreviations ### 2615 | 2616 | function stripAbbreviations($text) { 2617 | # 2618 | # Strips abbreviations from text, stores titles in hash references. 2619 | # 2620 | $less_than_tab = $this->tab_width - 1; 2621 | 2622 | # Link defs are in the form: [id]*: url "optional title" 2623 | $text = preg_replace_callback('{ 2624 | ^[ ]{0,'.$less_than_tab.'}\*\[(.+?)\][ ]?: # abbr_id = $1 2625 | (.*) # text = $2 (no blank lines allowed) 2626 | }xm', 2627 | array(&$this, '_stripAbbreviations_callback'), 2628 | $text); 2629 | return $text; 2630 | } 2631 | function _stripAbbreviations_callback($matches) { 2632 | $abbr_word = $matches[1]; 2633 | $abbr_desc = $matches[2]; 2634 | $this->abbr_matches[] = preg_quote($abbr_word); 2635 | $this->abbr_desciptions[$abbr_word] = trim($abbr_desc); 2636 | return ''; # String that will replace the block 2637 | } 2638 | 2639 | 2640 | function doAbbreviations($text) { 2641 | # 2642 | # Find defined abbreviations in text and wrap them in elements. 2643 | # 2644 | if ($this->abbr_matches) { 2645 | // cannot use the /x modifier because abbr_matches may 2646 | // contain spaces: 2647 | $text = preg_replace_callback('{'. 2648 | '(?abbr_matches) .')'. 2650 | '(?![\w\x1A])'. 2651 | '}', 2652 | array(&$this, '_doAbbreviations_callback'), $text); 2653 | } 2654 | return $text; 2655 | } 2656 | function _doAbbreviations_callback($matches) { 2657 | $abbr = $matches[0]; 2658 | if (isset($this->abbr_desciptions[$abbr])) { 2659 | $desc = $this->abbr_desciptions[$abbr]; 2660 | if (empty($desc)) { 2661 | return $this->hashSpan("$abbr"); 2662 | } else { 2663 | $desc = $this->escapeSpecialCharsWithinTagAttributes($desc); 2664 | return $this->hashSpan("$abbr"); 2665 | } 2666 | } else { 2667 | return $matches[0]; 2668 | } 2669 | } 2670 | 2671 | } 2672 | 2673 | 2674 | /* 2675 | 2676 | PHP Markdown Extra 2677 | ================== 2678 | 2679 | Description 2680 | ----------- 2681 | 2682 | This is a PHP port of the original Markdown formatter written in Perl 2683 | by John Gruber. This special "Extra" version of PHP Markdown features 2684 | further enhancements to the syntax for making additional constructs 2685 | such as tables and definition list. 2686 | 2687 | Markdown is a text-to-HTML filter; it translates an easy-to-read / 2688 | easy-to-write structured text format into HTML. Markdown's text format 2689 | is most similar to that of plain text email, and supports features such 2690 | as headers, *emphasis*, code blocks, blockquotes, and links. 2691 | 2692 | Markdown's syntax is designed not as a generic markup language, but 2693 | specifically to serve as a front-end to (X)HTML. You can use span-level 2694 | HTML tags anywhere in a Markdown document, and you can use block level 2695 | HTML tags (like
    and as well). 2696 | 2697 | For more information about Markdown's syntax, see: 2698 | 2699 | 2700 | 2701 | 2702 | Bugs 2703 | ---- 2704 | 2705 | To file bug reports please send email to: 2706 | 2707 | 2708 | 2709 | Please include with your report: (1) the example input; (2) the output you 2710 | expected; (3) the output Markdown actually produced. 2711 | 2712 | 2713 | Version History 2714 | --------------- 2715 | 2716 | See Readme file for details. 2717 | 2718 | Extra 1.1.3 (3 Jul 2007): 2719 | 2720 | Extra 1.1.2 (7 Feb 2007) 2721 | 2722 | Extra 1.1.1 (28 Dec 2006) 2723 | 2724 | Extra 1.1 (1 Dec 2006) 2725 | 2726 | Extra 1.0.1 (9 Dec 2005) 2727 | 2728 | Extra 1.0 (5 Sep 2005) 2729 | 2730 | 2731 | Copyright and License 2732 | --------------------- 2733 | 2734 | PHP Markdown & Extra 2735 | Copyright (c) 2004-2007 Michel Fortin 2736 | 2737 | All rights reserved. 2738 | 2739 | Based on Markdown 2740 | Copyright (c) 2003-2006 John Gruber 2741 | 2742 | All rights reserved. 2743 | 2744 | Redistribution and use in source and binary forms, with or without 2745 | modification, are permitted provided that the following conditions are 2746 | met: 2747 | 2748 | * Redistributions of source code must retain the above copyright notice, 2749 | this list of conditions and the following disclaimer. 2750 | 2751 | * Redistributions in binary form must reproduce the above copyright 2752 | notice, this list of conditions and the following disclaimer in the 2753 | documentation and/or other materials provided with the distribution. 2754 | 2755 | * Neither the name "Markdown" nor the names of its contributors may 2756 | be used to endorse or promote products derived from this software 2757 | without specific prior written permission. 2758 | 2759 | This software is provided by the copyright holders and contributors "as 2760 | is" and any express or implied warranties, including, but not limited 2761 | to, the implied warranties of merchantability and fitness for a 2762 | particular purpose are disclaimed. In no event shall the copyright owner 2763 | or contributors be liable for any direct, indirect, incidental, special, 2764 | exemplary, or consequential damages (including, but not limited to, 2765 | procurement of substitute goods or services; loss of use, data, or 2766 | profits; or business interruption) however caused and on any theory of 2767 | liability, whether in contract, strict liability, or tort (including 2768 | negligence or otherwise) arising in any way out of the use of this 2769 | software, even if advised of the possibility of such damage. 2770 | 2771 | */ 2772 | ?> --------------------------------------------------------------------------------