From 59fe50b31dcf6bee28fb4e52c8ee677ee58676ff Mon Sep 17 00:00:00 2001 From: Max Pozdeev Date: Tue, 13 Jul 2021 12:49:07 +0300 Subject: [PATCH] * add support of markdown in notes (using Parsedown library) --- src/ajax.php | 5 +- src/content/themes/default/index.php | 1 + src/content/themes/default/markdown.css | 123 ++ src/content/themes/default/style.css | 45 +- src/db/config.php.default | 3 + src/includes/common.php | 24 +- src/includes/markup.php | 47 + src/includes/mytinytodo.js | 16 +- src/includes/parsedown/LICENSE.txt | 20 + src/includes/parsedown/Parsedown.php | 1712 +++++++++++++++++++++++ 10 files changed, 1945 insertions(+), 51 deletions(-) create mode 100644 src/content/themes/default/markdown.css create mode 100644 src/includes/markup.php create mode 100644 src/includes/parsedown/LICENSE.txt create mode 100644 src/includes/parsedown/Parsedown.php diff --git a/src/ajax.php b/src/ajax.php index 5359f59..b9ab8bf 100644 --- a/src/ajax.php +++ b/src/ajax.php @@ -10,6 +10,7 @@ set_error_handler('myErrorHandler'); set_exception_handler('myExceptionHandler'); require_once('./init.php'); +require_once(MTTINC. 'markup.php'); $db = DBConnection::instance(); @@ -227,7 +228,7 @@ elseif(isset($_GET['editNote'])) $db->dq("UPDATE {$db->prefix}todolist SET note=?,d_edited=? WHERE id=$id", array($note, time()) ); $t = array(); $t['total'] = 1; - $t['list'][] = array('id'=>$id, 'note'=>mttMarkup_v1($note), 'noteText'=>(string)$note); + $t['list'][] = array('id'=>$id, 'note'=> noteMarkup($note), 'noteText'=>(string)$note); jsonExit($t); } elseif(isset($_GET['editTask'])) @@ -543,7 +544,7 @@ function prepareTaskRow($r) 'dateCompletedInlineTitle' => htmlarray(sprintf($lang->get('taskdate_inline_completed'), $dCompleted)), 'compl' => (int)$r['compl'], 'prio' => $r['prio'], - 'note' => mttMarkup_v1($r['note']), + 'note' => noteMarkup($r['note']), 'noteText' => (string)$r['note'], 'ow' => (int)$r['ow'], 'tags' => htmlarray($r['tags']), diff --git a/src/content/themes/default/index.php b/src/content/themes/default/index.php index 929c9b8..1692f18 100644 --- a/src/content/themes/default/index.php +++ b/src/content/themes/default/index.php @@ -7,6 +7,7 @@ <?php mttinfo('title'); ?> + diff --git a/src/content/themes/default/markdown.css b/src/content/themes/default/markdown.css new file mode 100644 index 0000000..8e057ac --- /dev/null +++ b/src/content/themes/default/markdown.css @@ -0,0 +1,123 @@ +/* Markdown notes */ + +.markdown-note { + line-height: 1.3; +} +/* +.markdown-note > *:first-child { margin-top: 0 !important; } +.markdown-note > *:last-child { margin-bottom: 0 !important; } +*/ + +.markdown-note h1 { font-size:2rem; } +.markdown-note h2 { font-size:1.5rem; } +.markdown-note h3 { font-size:1.2rem; } +.markdown-note h4 { font-size:1rem; } +.markdown-note h5 { font-size:1rem; } +.markdown-note h6 { font-size:1rem; } + +.markdown-note hr { + height: 1px; + border: 0; + background-color: #ccc; +} + +.markdown-note p, +.markdown-note blockquote, +.markdown-note ul, +.markdown-note ol, +.markdown-note dl, +.markdown-note table, +.markdown-note pre { + margin: 10px 0; +} +.markdown-note ul, +.markdown-note ol { + padding-left: 20px; +} +.markdown-note ul.no-list, +.markdown-note ol.no-list { + list-style-type: none; + padding: 0; +} + +.markdown-note blockquote { + margin:15px 0; + border-left: 4px solid #DDD; + padding: 0 15px; + color: #777; +} +.markdown-note blockquote > :first-child { + margin-top: 0px; +} +.markdown-note blockquote > :last-child { + margin-bottom: 0px; +} + +.markdown-note table { + width: 100%; + overflow: auto; + display: block; + border-spacing: 0; + border-collapse: collapse; +} +.markdown-note table th { + font-weight: bold; +} +.markdown-note table th, +.markdown-note table td { + border: 1px solid #ddd; + padding: 6px 13px; +} +.markdown-note table tr { + border-top: 1px solid #ccc; + background-color: #fff; +} +.markdown-note table tr:nth-child(2n) { + background-color: #f8f8f8; +} + + +.markdown-note code, /* inline code */ +.markdown-note tt { + font-size: 0.95rem; + margin: 0 2px; + padding: 0px 5px; + border: 1px solid #ddd; + background-color: #f8f8f8; + border-radius: 3px; +} +.markdown-note code { + white-space: nowrap; +} +.markdown-note pre { + font-size: 0.95rem; + line-height: 1.2rem; + padding: 6px 10px; + background-color: #f8f8f8; + border: 1px solid #ddd; + overflow: auto; + border-radius: 3px; +} +.markdown-note pre code, /* block of code */ +.markdown-note pre tt { + margin: 0; + padding: 0; + background-color: transparent; + border: none; +} +.markdown-note pre > code { + white-space: pre; +} + + +/* narrow screens */ +@media only screen and (max-width: 600px) { + + .markdown-note { padding-top: 0.5rem; } + + .markdown-note pre, + .markdown-note code, + .markdown-note tt { + font-size: 0.9rem; + } +} diff --git a/src/content/themes/default/style.css b/src/content/themes/default/style.css index 31ea543..7f0795f 100644 --- a/src/content/themes/default/style.css +++ b/src/content/themes/default/style.css @@ -157,25 +157,29 @@ a { color:#0000ff; cursor:pointer; text-decoration:underline; } } .task-toggle::after { content:'0'; color:transparent; } /* for baseline */ .task-toggle:hover { opacity:0.7; } -li.task-has-note .task-toggle { visibility:visible; } -li.task-expanded .task-toggle { transform:rotate(90deg); } +li.task-row.task-has-note .task-toggle { visibility:visible; } +li.task-row.task-expanded .task-toggle { transform:rotate(90deg); } /* #tasklist input[type="checkbox"] { vertical-align:-1px; } /* Chrome */ #tasklist { list-style-type: none; margin: 0; padding: 0;} #tasklist li.task-row { - padding:1px 2px; + padding:0px 2px; + border:1px solid transparent; /* allocate space for expanded border */ border-bottom:1px solid #DEDEDE; + margin-bottom: 4px; min-height:20px; background-color:#fff; position:relative; /* for z-index */ } #tasklist li.task-row:hover, #tasklist li.task-row.menu-active { - background-color:#f6f6f6; - /* box-shadow:1px 0 2px rgba(0,0,0,0.2), -1px 0 2px rgba(0,0,0,0.2); z-index: 1; */ + box-shadow: 1px 0 3px 1px rgba(0,0,0,0.2); + /* transition: box-shadow .1s ease-in; */ + z-index: 1; } #tasklist .task-container { display:flex; justify-content:flex-start; align-items:baseline; } +#tasklist li.task-row.task-expanded { border:1px solid #DEDEDE; border-radius: 3px; } .task-left { display:inline-block; } .task-middle { flex-grow:1; margin:0.6rem 0px; margin-left:5px; min-width:0; /*for long text*/ flex-basis:0; /* IE11 */} .task-middle-top { display:flex; align-items:baseline; } @@ -185,8 +189,8 @@ li.task-expanded .task-toggle { transform:rotate(90deg); } .task-date { color:#999999; font-size:0.8rem; margin-left:4px; display:none; } .task-date-completed { color:#999999; display:none; margin-left:5px; } .show-inline-date .task-date { display:inline; } -.show-inline-date li.task-completed .task-date-completed { display:inline; } -.show-inline-date li.task-completed .task-date { display:none; } +.show-inline-date li.task-row.task-completed .task-date-completed { display:inline; } +.show-inline-date li.task-row.task-completed .task-date { display:none; } .task-through { overflow:hidden; flex-grow:1; } .task-title a { color:#000000; } .task-title a:hover { color:#af0000; } @@ -196,15 +200,15 @@ li.task-expanded .task-toggle { transform:rotate(90deg); } .task-tags .tag { font-size:0.9rem; font-weight:bold; color:#333333; text-decoration:underline; } .duedate { color:#333333; padding:0px; padding-left:1px; margin-left:5px; white-space:nowrap; } .duedate:before { content:'\2192\20'; } -li.task-completed .duedate { /*font-size:0.8rem;*/ display:none; } +li.task-row.task-completed .duedate { /*font-size:0.8rem;*/ display:none; } #tasklist li.task-row.soon .duedate { color:#008000; } #tasklist li.task-row.today .duedate { color:#FF0000; } #tasklist li.task-row.past .duedate { color:#A52A2A; } -li.task-completed .task-middle { color:#777777;} -li.task-completed .task-through { text-decoration:line-through; } -li.task-completed .task-title a { color:#777777; } -#tasklist li.task-completed { opacity:0.6; } -#tasklist li.task-completed:hover { opacity:1.0; } +li.task-row.ask-completed .task-middle { color:#777777;} +li.task-row.task-completed .task-through { text-decoration:line-through; } +li.task-row.task-completed .task-title a { color:#777777; } +#tasklist li.task-row.task-completed { opacity:0.6; } +#tasklist li.task-row.task-completed:hover { opacity:1.0; } #tasklist li.task-row.not-in-tagpreview { opacity:0.1; } #tasklist .mtt-task-placeholder { min-height:0px; padding:0px; height:18px; line-height:18px; @@ -228,9 +232,9 @@ li.task-row:hover .taskactionbtn { visibility:visible; } #tasklist.filter-soon li.task-row.soon { display:block; } -#tasklist.filter-past li.task-completed, -#tasklist.filter-today li.task-completed, -#tasklist.filter-soon li.task-completed { +#tasklist.filter-past li.task-row.task-completed, +#tasklist.filter-today li.task-row.task-completed, +#tasklist.filter-soon li.task-row.task-completed { display:none; } @@ -246,9 +250,8 @@ li.task-row:hover .taskactionbtn { visibility:visible; } white-space:normal; word-wrap:break-word; } -li.task-expanded .task-note-block { display:block; } -/*li.task-expanded { box-shadow:1px 0 2px rgba(0,0,0,0.2), -1px 0 2px rgba(0,0,0,0.2); }*/ -li.task-completed .task-note-block .task-note { text-decoration:line-through; } +li.task-row.task-expanded .task-note-block { display:block; } +li.task-row.task-completed .task-note-block .task-note { text-decoration:line-through; } .task-note-area { display:none; margin-bottom:5px; } .task-note-area textarea { color:#999999; width:100%; display:block; height:65px; } .task-note-actions { font-size:0.8rem; } @@ -464,8 +467,8 @@ li.mtt-item-hidden { display:none; } #tasklist li.task-row .task-through { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } #tasklist li.task-row:hover { background-color:#ffffff; } - #tasklist li.task-expanded .task-note-block { display:none; } - #tasklist li.task-row.clicked { /*background-color:#f6f6f6;*/ box-shadow:1px 0 2px rgba(0,0,0,0.2), -1px 0 2px rgba(0,0,0,0.2); z-index: 1; } + #tasklist li.task-row.task-expanded .task-note-block { display:none; } + #tasklist li.task-row.clicked { border:1px solid #DEDEDE; border-radius: 3px; } #tasklist li.task-row.clicked .task-actions { display:block; } #tasklist li.task-row.clicked .task-through { white-space:normal; display:inline; } #tasklist li.task-row.clicked.task-has-note .task-note-block { display:block; } diff --git a/src/db/config.php.default b/src/db/config.php.default index 21b38ee..b8896a4 100644 --- a/src/db/config.php.default +++ b/src/db/config.php.default @@ -54,4 +54,7 @@ $config['dateformatshort'] = 'j M'; # Show task date in list $config['showdate'] = 0; +# Use Markdown syntax for notes. Set to 'v1' to use old v1.6 syntax. +$config['markup'] = 'markdown'; + ?> diff --git a/src/includes/common.php b/src/includes/common.php index 9425ee2..6ddb34e 100644 --- a/src/includes/common.php +++ b/src/includes/common.php @@ -115,6 +115,7 @@ class Config 'dateformatshort' => array('default'=>'j M', 'type'=>'s'), 'template' => array('default'=>'default', 'type'=>'s'), 'showdate' => array('default'=>0, 'type'=>'i'), + 'markup' => array('default'=>'markdown', 'type'=>'s'), ); public static $config; @@ -215,29 +216,6 @@ function url_dir($url, $onlyPath = 1) return '/'; } -// Convert note's raw text to html with allowed elements (b,i,u,s and raw urls) -function mttMarkup_v1($s) -{ - //hide allowed elements from escaping - $c1 = chr(1); - $c2 = chr(2); - $s = preg_replace("~([\s\S]*?)~i", "${c1}b${c2}\$1${c1}/b${c2}", $s); - $s = preg_replace("~([\s\S]*?)~i", "${c1}i${c2}\$1${c1}/i${c2}", $s); - $s = preg_replace("~([\s\S]*?)~i", "${c1}u${c2}\$1${c1}/u${c2}", $s); - $s = preg_replace("~([\s\S]*?)~i", "${c1}s${c2}\$1${c1}/s${c2}", $s); - $s = htmlspecialchars($s); //escape all elements, except above - $s = str_replace( [$c1, $c2], ['<','>'], $s ); //unhide - $s = nl2br($s); - - // make links from text starting with 'www.' - $s = preg_replace( "/(^|\s|>)(www\.([\w\#$%&~\/.\-\+;:=,\?\[\]@]+?))(,|\.|:|)?(?=\s|"|<|>|\"|<|>|$)/i" , '$1$2$4' , $s ); - - // make link from text starting with protocol like 'http://' - $s = preg_replace( "/(^|\s|>)([a-z]+:\/\/([\w\#$%&~\/.\-\+;:=,\?\[\]@]+?))(,|\.|:|)?(?=\s|"|<|>|\"|<|>|$)/i" , '$1$2$4' , $s); - - return $s; -} - /* found in comments on http://www.php.net/manual/en/function.uniqid.php#94959 */ function generateUUID() { diff --git a/src/includes/markup.php b/src/includes/markup.php new file mode 100644 index 0000000..082122e --- /dev/null +++ b/src/includes/markup.php @@ -0,0 +1,47 @@ +setSafeMode(true); + //$parser->setBreaksEnabled(true); + return $parser->text($s); +} + + +// Convert note's raw text to html with allowed elements (b,i,u,s and raw urls) +function mttMarkup_v1($s) +{ + //hide allowed elements from escaping + $c1 = chr(1); + $c2 = chr(2); + $s = preg_replace("~([\s\S]*?)~i", "${c1}b${c2}\$1${c1}/b${c2}", $s); + $s = preg_replace("~([\s\S]*?)~i", "${c1}i${c2}\$1${c1}/i${c2}", $s); + $s = preg_replace("~([\s\S]*?)~i", "${c1}u${c2}\$1${c1}/u${c2}", $s); + $s = preg_replace("~([\s\S]*?)~i", "${c1}s${c2}\$1${c1}/s${c2}", $s); + $s = htmlspecialchars($s); //escape all elements, except above + $s = str_replace( [$c1, $c2], ['<','>'], $s ); //unhide + $s = nl2br($s); + + // make links from text starting with 'www.' + $s = preg_replace( "/(^|\s|>)(www\.([\w\#$%&~\/.\-\+;:=,\?\[\]@]+?))(,|\.|:|)?(?=\s|"|<|>|\"|<|>|$)/i" , '$1$2$4' , $s ); + + // make link from text starting with protocol like 'http://' + $s = preg_replace( "/(^|\s|>)([a-z]+:\/\/([\w\#$%&~\/.\-\+;:=,\?\[\]@]+?))(,|\.|:|)?(?=\s|"|<|>|\"|<|>|$)/i" , '$1$2$4' , $s); + + return $s; +} + +?> \ No newline at end of file diff --git a/src/includes/mytinytodo.js b/src/includes/mytinytodo.js index 51b058b..9138c87 100644 --- a/src/includes/mytinytodo.js +++ b/src/includes/mytinytodo.js @@ -61,7 +61,8 @@ var mytinytodo = window.mytinytodo = _mtt = { firstdayofweek: 1, touchDevice: false, calendarIcon: 'calendar.png', // need templateUrl+icon - history: true + history: true, + markdown: true, }, timers: { @@ -125,7 +126,6 @@ var mytinytodo = window.mytinytodo = _mtt = { delete options.mttUrl; } if (options.hasOwnProperty('db')) { - this.db = new options.db(this); delete options.db; } if (options.hasOwnProperty('homeUrl')) { @@ -699,6 +699,10 @@ var mytinytodo = window.mytinytodo = _mtt = { //TODO: handle unknown list } + if (_mtt.options.markdown == true) { + $('#mtt_body').addClass('markdown-enabled'); + } + _mtt.options.openList = 0; $('#lists ul').html(ti); $('#lists').show(); @@ -970,6 +974,8 @@ _mtt.prepareTaskStr = prepareTaskStr; function prepareTaskBlocks(item) { var id = item.id; + var markdown = ''; + if (_mtt.options.markdown == true) markdown = 'markdown-note'; return ''+ '
' + '
' + @@ -990,7 +996,7 @@ function prepareTaskBlocks(item) '
' + '
' + - '
' + prepareTaskNoteInlineHtml(item.note) + '
' + + '
' + prepareTaskNoteInlineHtml(item.note, item.noteText) + '
' + '
'+ ''+_mtt.lang.get('actionNoteSave') + ' | '+_mtt.lang.get('actionNoteCancel')+'' + @@ -1009,7 +1015,7 @@ function prepareTaskTitleInlineHtml(s) } _mtt.prepareTaskTitleInlineHtml = prepareTaskTitleInlineHtml; -function prepareTaskNoteInlineHtml(s) +function prepareTaskNoteInlineHtml(s, rawText) { // Task note is already escaped on php back-end return s; @@ -1468,7 +1474,7 @@ function saveTaskNote(id) var item = json.list[0]; taskList[id].note = item.note; taskList[id].noteText = item.noteText; - $('#tasknote'+id).html(prepareTaskNoteInlineHtml(item.note)); + $('#tasknote'+id).html(prepareTaskNoteInlineHtml(item.note, item.noteText)); if(item.note == '') $('#taskrow_'+id).removeClass('task-has-note task-expanded'); else $('#taskrow_'+id).addClass('task-has-note task-expanded'); cancelTaskNote(id); diff --git a/src/includes/parsedown/LICENSE.txt b/src/includes/parsedown/LICENSE.txt new file mode 100644 index 0000000..8e7c764 --- /dev/null +++ b/src/includes/parsedown/LICENSE.txt @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2013-2018 Emanuil Rusev, erusev.com + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/includes/parsedown/Parsedown.php b/src/includes/parsedown/Parsedown.php new file mode 100644 index 0000000..1b9d6d5 --- /dev/null +++ b/src/includes/parsedown/Parsedown.php @@ -0,0 +1,1712 @@ +DefinitionData = array(); + + # standardize line breaks + $text = str_replace(array("\r\n", "\r"), "\n", $text); + + # remove surrounding line breaks + $text = trim($text, "\n"); + + # split text into lines + $lines = explode("\n", $text); + + # iterate through lines to identify blocks + $markup = $this->lines($lines); + + # trim line breaks + $markup = trim($markup, "\n"); + + return $markup; + } + + # + # Setters + # + + function setBreaksEnabled($breaksEnabled) + { + $this->breaksEnabled = $breaksEnabled; + + return $this; + } + + protected $breaksEnabled; + + function setMarkupEscaped($markupEscaped) + { + $this->markupEscaped = $markupEscaped; + + return $this; + } + + protected $markupEscaped; + + function setUrlsLinked($urlsLinked) + { + $this->urlsLinked = $urlsLinked; + + return $this; + } + + protected $urlsLinked = true; + + function setSafeMode($safeMode) + { + $this->safeMode = (bool) $safeMode; + + return $this; + } + + protected $safeMode; + + protected $safeLinksWhitelist = array( + 'http://', + 'https://', + 'ftp://', + 'ftps://', + 'mailto:', + 'data:image/png;base64,', + 'data:image/gif;base64,', + 'data:image/jpeg;base64,', + 'irc:', + 'ircs:', + 'git:', + 'ssh:', + 'news:', + 'steam:', + ); + + # + # Lines + # + + protected $BlockTypes = array( + '#' => array('Header'), + '*' => array('Rule', 'List'), + '+' => array('List'), + '-' => array('SetextHeader', 'Table', 'Rule', 'List'), + '0' => array('List'), + '1' => array('List'), + '2' => array('List'), + '3' => array('List'), + '4' => array('List'), + '5' => array('List'), + '6' => array('List'), + '7' => array('List'), + '8' => array('List'), + '9' => array('List'), + ':' => array('Table'), + '<' => array('Comment', 'Markup'), + '=' => array('SetextHeader'), + '>' => array('Quote'), + '[' => array('Reference'), + '_' => array('Rule'), + '`' => array('FencedCode'), + '|' => array('Table'), + '~' => array('FencedCode'), + ); + + # ~ + + protected $unmarkedBlockTypes = array( + 'Code', + ); + + # + # Blocks + # + + protected function lines(array $lines) + { + $CurrentBlock = null; + + foreach ($lines as $line) + { + if (chop($line) === '') + { + if (isset($CurrentBlock)) + { + $CurrentBlock['interrupted'] = true; + } + + continue; + } + + if (strpos($line, "\t") !== false) + { + $parts = explode("\t", $line); + + $line = $parts[0]; + + unset($parts[0]); + + foreach ($parts as $part) + { + $shortage = 4 - mb_strlen($line, 'utf-8') % 4; + + $line .= str_repeat(' ', $shortage); + $line .= $part; + } + } + + $indent = 0; + + while (isset($line[$indent]) and $line[$indent] === ' ') + { + $indent ++; + } + + $text = $indent > 0 ? substr($line, $indent) : $line; + + # ~ + + $Line = array('body' => $line, 'indent' => $indent, 'text' => $text); + + # ~ + + if (isset($CurrentBlock['continuable'])) + { + $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock); + + if (isset($Block)) + { + $CurrentBlock = $Block; + + continue; + } + else + { + if ($this->isBlockCompletable($CurrentBlock['type'])) + { + $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock); + } + } + } + + # ~ + + $marker = $text[0]; + + # ~ + + $blockTypes = $this->unmarkedBlockTypes; + + if (isset($this->BlockTypes[$marker])) + { + foreach ($this->BlockTypes[$marker] as $blockType) + { + $blockTypes []= $blockType; + } + } + + # + # ~ + + foreach ($blockTypes as $blockType) + { + $Block = $this->{'block'.$blockType}($Line, $CurrentBlock); + + if (isset($Block)) + { + $Block['type'] = $blockType; + + if ( ! isset($Block['identified'])) + { + $Blocks []= $CurrentBlock; + + $Block['identified'] = true; + } + + if ($this->isBlockContinuable($blockType)) + { + $Block['continuable'] = true; + } + + $CurrentBlock = $Block; + + continue 2; + } + } + + # ~ + + if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted'])) + { + $CurrentBlock['element']['text'] .= "\n".$text; + } + else + { + $Blocks []= $CurrentBlock; + + $CurrentBlock = $this->paragraph($Line); + + $CurrentBlock['identified'] = true; + } + } + + # ~ + + if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) + { + $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock); + } + + # ~ + + $Blocks []= $CurrentBlock; + + unset($Blocks[0]); + + # ~ + + $markup = ''; + + foreach ($Blocks as $Block) + { + if (isset($Block['hidden'])) + { + continue; + } + + $markup .= "\n"; + $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']); + } + + $markup .= "\n"; + + # ~ + + return $markup; + } + + protected function isBlockContinuable($Type) + { + return method_exists($this, 'block'.$Type.'Continue'); + } + + protected function isBlockCompletable($Type) + { + return method_exists($this, 'block'.$Type.'Complete'); + } + + # + # Code + + protected function blockCode($Line, $Block = null) + { + if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted'])) + { + return; + } + + if ($Line['indent'] >= 4) + { + $text = substr($Line['body'], 4); + + $Block = array( + 'element' => array( + 'name' => 'pre', + 'handler' => 'element', + 'text' => array( + 'name' => 'code', + 'text' => $text, + ), + ), + ); + + return $Block; + } + } + + protected function blockCodeContinue($Line, $Block) + { + if ($Line['indent'] >= 4) + { + if (isset($Block['interrupted'])) + { + $Block['element']['text']['text'] .= "\n"; + + unset($Block['interrupted']); + } + + $Block['element']['text']['text'] .= "\n"; + + $text = substr($Line['body'], 4); + + $Block['element']['text']['text'] .= $text; + + return $Block; + } + } + + protected function blockCodeComplete($Block) + { + $text = $Block['element']['text']['text']; + + $Block['element']['text']['text'] = $text; + + return $Block; + } + + # + # Comment + + protected function blockComment($Line) + { + if ($this->markupEscaped or $this->safeMode) + { + return; + } + + if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!') + { + $Block = array( + 'markup' => $Line['body'], + ); + + if (preg_match('/-->$/', $Line['text'])) + { + $Block['closed'] = true; + } + + return $Block; + } + } + + protected function blockCommentContinue($Line, array $Block) + { + if (isset($Block['closed'])) + { + return; + } + + $Block['markup'] .= "\n" . $Line['body']; + + if (preg_match('/-->$/', $Line['text'])) + { + $Block['closed'] = true; + } + + return $Block; + } + + # + # Fenced Code + + protected function blockFencedCode($Line) + { + if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([^`]+)?[ ]*$/', $Line['text'], $matches)) + { + $Element = array( + 'name' => 'code', + 'text' => '', + ); + + if (isset($matches[1])) + { + /** + * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes + * Every HTML element may have a class attribute specified. + * The attribute, if specified, must have a value that is a set + * of space-separated tokens representing the various classes + * that the element belongs to. + * [...] + * The space characters, for the purposes of this specification, + * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), + * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and + * U+000D CARRIAGE RETURN (CR). + */ + $language = substr($matches[1], 0, strcspn($matches[1], " \t\n\f\r")); + + $class = 'language-'.$language; + + $Element['attributes'] = array( + 'class' => $class, + ); + } + + $Block = array( + 'char' => $Line['text'][0], + 'element' => array( + 'name' => 'pre', + 'handler' => 'element', + 'text' => $Element, + ), + ); + + return $Block; + } + } + + protected function blockFencedCodeContinue($Line, $Block) + { + if (isset($Block['complete'])) + { + return; + } + + if (isset($Block['interrupted'])) + { + $Block['element']['text']['text'] .= "\n"; + + unset($Block['interrupted']); + } + + if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text'])) + { + $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1); + + $Block['complete'] = true; + + return $Block; + } + + $Block['element']['text']['text'] .= "\n".$Line['body']; + + return $Block; + } + + protected function blockFencedCodeComplete($Block) + { + $text = $Block['element']['text']['text']; + + $Block['element']['text']['text'] = $text; + + return $Block; + } + + # + # Header + + protected function blockHeader($Line) + { + if (isset($Line['text'][1])) + { + $level = 1; + + while (isset($Line['text'][$level]) and $Line['text'][$level] === '#') + { + $level ++; + } + + if ($level > 6) + { + return; + } + + $text = trim($Line['text'], '# '); + + $Block = array( + 'element' => array( + 'name' => 'h' . min(6, $level), + 'text' => $text, + 'handler' => 'line', + ), + ); + + return $Block; + } + } + + # + # List + + protected function blockList($Line) + { + list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]'); + + if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches)) + { + $Block = array( + 'indent' => $Line['indent'], + 'pattern' => $pattern, + 'element' => array( + 'name' => $name, + 'handler' => 'elements', + ), + ); + + if($name === 'ol') + { + $listStart = stristr($matches[0], '.', true); + + if($listStart !== '1') + { + $Block['element']['attributes'] = array('start' => $listStart); + } + } + + $Block['li'] = array( + 'name' => 'li', + 'handler' => 'li', + 'text' => array( + $matches[2], + ), + ); + + $Block['element']['text'] []= & $Block['li']; + + return $Block; + } + } + + protected function blockListContinue($Line, array $Block) + { + if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches)) + { + if (isset($Block['interrupted'])) + { + $Block['li']['text'] []= ''; + + $Block['loose'] = true; + + unset($Block['interrupted']); + } + + unset($Block['li']); + + $text = isset($matches[1]) ? $matches[1] : ''; + + $Block['li'] = array( + 'name' => 'li', + 'handler' => 'li', + 'text' => array( + $text, + ), + ); + + $Block['element']['text'] []= & $Block['li']; + + return $Block; + } + + if ($Line['text'][0] === '[' and $this->blockReference($Line)) + { + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); + + $Block['li']['text'] []= $text; + + return $Block; + } + + if ($Line['indent'] > 0) + { + $Block['li']['text'] []= ''; + + $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); + + $Block['li']['text'] []= $text; + + unset($Block['interrupted']); + + return $Block; + } + } + + protected function blockListComplete(array $Block) + { + if (isset($Block['loose'])) + { + foreach ($Block['element']['text'] as &$li) + { + if (end($li['text']) !== '') + { + $li['text'] []= ''; + } + } + } + + return $Block; + } + + # + # Quote + + protected function blockQuote($Line) + { + if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) + { + $Block = array( + 'element' => array( + 'name' => 'blockquote', + 'handler' => 'lines', + 'text' => (array) $matches[1], + ), + ); + + return $Block; + } + } + + protected function blockQuoteContinue($Line, array $Block) + { + if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) + { + if (isset($Block['interrupted'])) + { + $Block['element']['text'] []= ''; + + unset($Block['interrupted']); + } + + $Block['element']['text'] []= $matches[1]; + + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $Block['element']['text'] []= $Line['text']; + + return $Block; + } + } + + # + # Rule + + protected function blockRule($Line) + { + if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text'])) + { + $Block = array( + 'element' => array( + 'name' => 'hr' + ), + ); + + return $Block; + } + } + + # + # Setext + + protected function blockSetextHeader($Line, array $Block = null) + { + if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) + { + return; + } + + if (chop($Line['text'], $Line['text'][0]) === '') + { + $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; + + return $Block; + } + } + + # + # Markup + + protected function blockMarkup($Line) + { + if ($this->markupEscaped or $this->safeMode) + { + return; + } + + if (preg_match('/^<(\w[\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches)) + { + $element = strtolower($matches[1]); + + if (in_array($element, $this->textLevelElements)) + { + return; + } + + $Block = array( + 'name' => $matches[1], + 'depth' => 0, + 'markup' => $Line['text'], + ); + + $length = strlen($matches[0]); + + $remainder = substr($Line['text'], $length); + + if (trim($remainder) === '') + { + if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) + { + $Block['closed'] = true; + + $Block['void'] = true; + } + } + else + { + if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) + { + return; + } + + if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder)) + { + $Block['closed'] = true; + } + } + + return $Block; + } + } + + protected function blockMarkupContinue($Line, array $Block) + { + if (isset($Block['closed'])) + { + return; + } + + if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open + { + $Block['depth'] ++; + } + + if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close + { + if ($Block['depth'] > 0) + { + $Block['depth'] --; + } + else + { + $Block['closed'] = true; + } + } + + if (isset($Block['interrupted'])) + { + $Block['markup'] .= "\n"; + + unset($Block['interrupted']); + } + + $Block['markup'] .= "\n".$Line['body']; + + return $Block; + } + + # + # Reference + + protected function blockReference($Line) + { + if (preg_match('/^\[(.+?)\]:[ ]*?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches)) + { + $id = strtolower($matches[1]); + + $Data = array( + 'url' => $matches[2], + 'title' => null, + ); + + if (isset($matches[3])) + { + $Data['title'] = $matches[3]; + } + + $this->DefinitionData['Reference'][$id] = $Data; + + $Block = array( + 'hidden' => true, + ); + + return $Block; + } + } + + # + # Table + + protected function blockTable($Line, array $Block = null) + { + if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) + { + return; + } + + if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '') + { + $alignments = array(); + + $divider = $Line['text']; + + $divider = trim($divider); + $divider = trim($divider, '|'); + + $dividerCells = explode('|', $divider); + + foreach ($dividerCells as $dividerCell) + { + $dividerCell = trim($dividerCell); + + if ($dividerCell === '') + { + continue; + } + + $alignment = null; + + if ($dividerCell[0] === ':') + { + $alignment = 'left'; + } + + if (substr($dividerCell, - 1) === ':') + { + $alignment = $alignment === 'left' ? 'center' : 'right'; + } + + $alignments []= $alignment; + } + + # ~ + + $HeaderElements = array(); + + $header = $Block['element']['text']; + + $header = trim($header); + $header = trim($header, '|'); + + $headerCells = explode('|', $header); + + foreach ($headerCells as $index => $headerCell) + { + $headerCell = trim($headerCell); + + $HeaderElement = array( + 'name' => 'th', + 'text' => $headerCell, + 'handler' => 'line', + ); + + if (isset($alignments[$index])) + { + $alignment = $alignments[$index]; + + $HeaderElement['attributes'] = array( + 'style' => 'text-align: '.$alignment.';', + ); + } + + $HeaderElements []= $HeaderElement; + } + + # ~ + + $Block = array( + 'alignments' => $alignments, + 'identified' => true, + 'element' => array( + 'name' => 'table', + 'handler' => 'elements', + ), + ); + + $Block['element']['text'] []= array( + 'name' => 'thead', + 'handler' => 'elements', + ); + + $Block['element']['text'] []= array( + 'name' => 'tbody', + 'handler' => 'elements', + 'text' => array(), + ); + + $Block['element']['text'][0]['text'] []= array( + 'name' => 'tr', + 'handler' => 'elements', + 'text' => $HeaderElements, + ); + + return $Block; + } + } + + protected function blockTableContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + if ($Line['text'][0] === '|' or strpos($Line['text'], '|')) + { + $Elements = array(); + + $row = $Line['text']; + + $row = trim($row); + $row = trim($row, '|'); + + preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches); + + foreach ($matches[0] as $index => $cell) + { + $cell = trim($cell); + + $Element = array( + 'name' => 'td', + 'handler' => 'line', + 'text' => $cell, + ); + + if (isset($Block['alignments'][$index])) + { + $Element['attributes'] = array( + 'style' => 'text-align: '.$Block['alignments'][$index].';', + ); + } + + $Elements []= $Element; + } + + $Element = array( + 'name' => 'tr', + 'handler' => 'elements', + 'text' => $Elements, + ); + + $Block['element']['text'][1]['text'] []= $Element; + + return $Block; + } + } + + # + # ~ + # + + protected function paragraph($Line) + { + $Block = array( + 'element' => array( + 'name' => 'p', + 'text' => $Line['text'], + 'handler' => 'line', + ), + ); + + return $Block; + } + + # + # Inline Elements + # + + protected $InlineTypes = array( + '"' => array('SpecialCharacter'), + '!' => array('Image'), + '&' => array('SpecialCharacter'), + '*' => array('Emphasis'), + ':' => array('Url'), + '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'), + '>' => array('SpecialCharacter'), + '[' => array('Link'), + '_' => array('Emphasis'), + '`' => array('Code'), + '~' => array('Strikethrough'), + '\\' => array('EscapeSequence'), + ); + + # ~ + + protected $inlineMarkerList = '!"*_&[:<>`~\\'; + + # + # ~ + # + + public function line($text, $nonNestables=array()) + { + $markup = ''; + + # $excerpt is based on the first occurrence of a marker + + while ($excerpt = strpbrk($text, $this->inlineMarkerList)) + { + $marker = $excerpt[0]; + + $markerPosition = strpos($text, $marker); + + $Excerpt = array('text' => $excerpt, 'context' => $text); + + foreach ($this->InlineTypes[$marker] as $inlineType) + { + # check to see if the current inline type is nestable in the current context + + if ( ! empty($nonNestables) and in_array($inlineType, $nonNestables)) + { + continue; + } + + $Inline = $this->{'inline'.$inlineType}($Excerpt); + + if ( ! isset($Inline)) + { + continue; + } + + # makes sure that the inline belongs to "our" marker + + if (isset($Inline['position']) and $Inline['position'] > $markerPosition) + { + continue; + } + + # sets a default inline position + + if ( ! isset($Inline['position'])) + { + $Inline['position'] = $markerPosition; + } + + # cause the new element to 'inherit' our non nestables + + foreach ($nonNestables as $non_nestable) + { + $Inline['element']['nonNestables'][] = $non_nestable; + } + + # the text that comes before the inline + $unmarkedText = substr($text, 0, $Inline['position']); + + # compile the unmarked text + $markup .= $this->unmarkedText($unmarkedText); + + # compile the inline + $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']); + + # remove the examined text + $text = substr($text, $Inline['position'] + $Inline['extent']); + + continue 2; + } + + # the marker does not belong to an inline + + $unmarkedText = substr($text, 0, $markerPosition + 1); + + $markup .= $this->unmarkedText($unmarkedText); + + $text = substr($text, $markerPosition + 1); + } + + $markup .= $this->unmarkedText($text); + + return $markup; + } + + # + # ~ + # + + protected function inlineCode($Excerpt) + { + $marker = $Excerpt['text'][0]; + + if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(? strlen($matches[0]), + 'element' => array( + 'name' => 'code', + 'text' => $text, + ), + ); + } + } + + protected function inlineEmailTag($Excerpt) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches)) + { + $url = $matches[1]; + + if ( ! isset($matches[2])) + { + $url = 'mailto:' . $url; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $matches[1], + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + protected function inlineEmphasis($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + $marker = $Excerpt['text'][0]; + + if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) + { + $emphasis = 'strong'; + } + elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) + { + $emphasis = 'em'; + } + else + { + return; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => $emphasis, + 'handler' => 'line', + 'text' => $matches[1], + ), + ); + } + + protected function inlineEscapeSequence($Excerpt) + { + if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) + { + return array( + 'markup' => $Excerpt['text'][1], + 'extent' => 2, + ); + } + } + + protected function inlineImage($Excerpt) + { + if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') + { + return; + } + + $Excerpt['text']= substr($Excerpt['text'], 1); + + $Link = $this->inlineLink($Excerpt); + + if ($Link === null) + { + return; + } + + $Inline = array( + 'extent' => $Link['extent'] + 1, + 'element' => array( + 'name' => 'img', + 'attributes' => array( + 'src' => $Link['element']['attributes']['href'], + 'alt' => $Link['element']['text'], + ), + ), + ); + + $Inline['element']['attributes'] += $Link['element']['attributes']; + + unset($Inline['element']['attributes']['href']); + + return $Inline; + } + + protected function inlineLink($Excerpt) + { + $Element = array( + 'name' => 'a', + 'handler' => 'line', + 'nonNestables' => array('Url', 'Link'), + 'text' => null, + 'attributes' => array( + 'href' => null, + 'title' => null, + ), + ); + + $extent = 0; + + $remainder = $Excerpt['text']; + + if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) + { + $Element['text'] = $matches[1]; + + $extent += strlen($matches[0]); + + $remainder = substr($remainder, $extent); + } + else + { + return; + } + + if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*"|\'[^\']*\'))?\s*[)]/', $remainder, $matches)) + { + $Element['attributes']['href'] = $matches[1]; + + if (isset($matches[2])) + { + $Element['attributes']['title'] = substr($matches[2], 1, - 1); + } + + $extent += strlen($matches[0]); + } + else + { + if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) + { + $definition = strlen($matches[1]) ? $matches[1] : $Element['text']; + $definition = strtolower($definition); + + $extent += strlen($matches[0]); + } + else + { + $definition = strtolower($Element['text']); + } + + if ( ! isset($this->DefinitionData['Reference'][$definition])) + { + return; + } + + $Definition = $this->DefinitionData['Reference'][$definition]; + + $Element['attributes']['href'] = $Definition['url']; + $Element['attributes']['title'] = $Definition['title']; + } + + return array( + 'extent' => $extent, + 'element' => $Element, + ); + } + + protected function inlineMarkup($Excerpt) + { + if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) + { + return; + } + + if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*[ ]*>/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + } + + protected function inlineSpecialCharacter($Excerpt) + { + if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text'])) + { + return array( + 'markup' => '&', + 'extent' => 1, + ); + } + + $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot'); + + if (isset($SpecialCharacter[$Excerpt['text'][0]])) + { + return array( + 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';', + 'extent' => 1, + ); + } + } + + protected function inlineStrikethrough($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) + { + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'del', + 'text' => $matches[1], + 'handler' => 'line', + ), + ); + } + } + + protected function inlineUrl($Excerpt) + { + if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') + { + return; + } + + if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)) + { + $url = $matches[0][0]; + + $Inline = array( + 'extent' => strlen($matches[0][0]), + 'position' => $matches[0][1], + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + + return $Inline; + } + } + + protected function inlineUrlTag($Excerpt) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches)) + { + $url = $matches[1]; + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + # ~ + + protected function unmarkedText($text) + { + if ($this->breaksEnabled) + { + $text = preg_replace('/[ ]*\n/', "
\n", $text); + } + else + { + $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "
\n", $text); + $text = str_replace(" \n", "\n", $text); + } + + return $text; + } + + # + # Handlers + # + + protected function element(array $Element) + { + if ($this->safeMode) + { + $Element = $this->sanitiseElement($Element); + } + + $markup = '<'.$Element['name']; + + if (isset($Element['attributes'])) + { + foreach ($Element['attributes'] as $name => $value) + { + if ($value === null) + { + continue; + } + + $markup .= ' '.$name.'="'.self::escape($value).'"'; + } + } + + $permitRawHtml = false; + + if (isset($Element['text'])) + { + $text = $Element['text']; + } + // very strongly consider an alternative if you're writing an + // extension + elseif (isset($Element['rawHtml'])) + { + $text = $Element['rawHtml']; + $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode']; + $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode; + } + + if (isset($text)) + { + $markup .= '>'; + + if (!isset($Element['nonNestables'])) + { + $Element['nonNestables'] = array(); + } + + if (isset($Element['handler'])) + { + $markup .= $this->{$Element['handler']}($text, $Element['nonNestables']); + } + elseif (!$permitRawHtml) + { + $markup .= self::escape($text, true); + } + else + { + $markup .= $text; + } + + $markup .= ''; + } + else + { + $markup .= ' />'; + } + + return $markup; + } + + protected function elements(array $Elements) + { + $markup = ''; + + foreach ($Elements as $Element) + { + $markup .= "\n" . $this->element($Element); + } + + $markup .= "\n"; + + return $markup; + } + + # ~ + + protected function li($lines) + { + $markup = $this->lines($lines); + + $trimmedMarkup = trim($markup); + + if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '

') + { + $markup = $trimmedMarkup; + $markup = substr($markup, 3); + + $position = strpos($markup, "

"); + + $markup = substr_replace($markup, '', $position, 4); + } + + return $markup; + } + + # + # Deprecated Methods + # + + function parse($text) + { + $markup = $this->text($text); + + return $markup; + } + + protected function sanitiseElement(array $Element) + { + static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; + static $safeUrlNameToAtt = array( + 'a' => 'href', + 'img' => 'src', + ); + + if (isset($safeUrlNameToAtt[$Element['name']])) + { + $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]); + } + + if ( ! empty($Element['attributes'])) + { + foreach ($Element['attributes'] as $att => $val) + { + # filter out badly parsed attribute + if ( ! preg_match($goodAttribute, $att)) + { + unset($Element['attributes'][$att]); + } + # dump onevent attribute + elseif (self::striAtStart($att, 'on')) + { + unset($Element['attributes'][$att]); + } + } + } + + return $Element; + } + + protected function filterUnsafeUrlInAttribute(array $Element, $attribute) + { + foreach ($this->safeLinksWhitelist as $scheme) + { + if (self::striAtStart($Element['attributes'][$attribute], $scheme)) + { + return $Element; + } + } + + $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]); + + return $Element; + } + + # + # Static Methods + # + + protected static function escape($text, $allowQuotes = false) + { + return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8'); + } + + protected static function striAtStart($string, $needle) + { + $len = strlen($needle); + + if ($len > strlen($string)) + { + return false; + } + else + { + return strtolower(substr($string, 0, $len)) === strtolower($needle); + } + } + + static function instance($name = 'default') + { + if (isset(self::$instances[$name])) + { + return self::$instances[$name]; + } + + $instance = new static(); + + self::$instances[$name] = $instance; + + return $instance; + } + + private static $instances = array(); + + # + # Fields + # + + protected $DefinitionData; + + # + # Read-Only + + protected $specialCharacters = array( + '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', + ); + + protected $StrongRegex = array( + '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s', + '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us', + ); + + protected $EmRegex = array( + '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', + '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', + ); + + protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?'; + + protected $voidElements = array( + 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', + ); + + protected $textLevelElements = array( + 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', + 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', + 'i', 'rp', 'del', 'code', 'strike', 'marquee', + 'q', 'rt', 'ins', 'font', 'strong', + 's', 'tt', 'kbd', 'mark', + 'u', 'xm', 'sub', 'nobr', + 'sup', 'ruby', + 'var', 'span', + 'wbr', 'time', + ); +}