diff --git a/admin/js/hyperdown.js b/admin/js/hyperdown.js index ad0a787f..12a09354 100644 --- a/admin/js/hyperdown.js +++ b/admin/js/hyperdown.js @@ -101,15 +101,34 @@ }; this.hooks = {}; this.html = false; + this.blockParsers = [['code', 10], ['shtml', 20], ['ahtml', 30], ['list', 40], ['math', 50], ['pre', 60], ['html', 70], ['footnote', 80], ['definition', 90], ['quote', 100], ['table', 110], ['sh', 120], ['mh', 130], ['hr', 140], ['default', 9999]]; + this.parsers = {}; } Parser.prototype.makeHtml = function(text) { - var html; + var html, j, len, name, parser, ref; this.footnotes = []; this.definitions = {}; this.holders = {}; this.uniqid = (Math.ceil(Math.random() * 10000000)) + (Math.ceil(Math.random() * 10000000)); this.id = 0; + this.blockParsers.sort(function(a, b) { + if (a[1] < b[1]) { + return -1; + } else { + return 1; + } + }); + ref = this.blockParsers; + for (j = 0, len = ref.length; j < len; j++) { + parser = ref[j]; + name = parser[0]; + if (parser[2] !== void 0) { + this.parsers[name] = parser[2]; + } else { + this.parsers[name] = this['parseBlock' + (ucfirst(name))].bind(this); + } + } text = this.initText(text); html = this.parse(text); html = this.makeFootnotes(html); @@ -403,7 +422,7 @@ }; Parser.prototype.parseBlock = function(text, lines) { - var align, aligns, autoHtml, block, emptyCount, head, htmlTagAllRegExp, htmlTagRegExp, indent, isAfterList, j, key, l, lastMatch, len, len1, len2, line, m, matches, n, num, ref, row, rows, space, special, tag; + var block, j, key, l, len, len1, line, name, parser, pass, ref, ref1, state; ref = text.split("\n"); for (j = 0, len = ref.length; j < len; j++) { line = ref[j]; @@ -412,266 +431,363 @@ this.blocks = []; this.current = 'normal'; this.pos = -1; - special = (array_keys(this.specialWhiteList)).join('|'); - emptyCount = 0; - autoHtml = false; + state = { + special: (array_keys(this.specialWhiteList)).join('|'), + empty: 0, + html: false + }; for (key = l = 0, len1 = lines.length; l < len1; key = ++l) { line = lines[key]; block = this.getBlock(); if (block != null) { block = block.slice(0); } - if (!!(matches = line.match(/^(\s*)((?:[0-9]+\.)|(?:[a-z]\.?)|\-|\+|\*)\s+/i))) { - space = matches[1].length; - emptyCount = 0; - if (this.isBlock('list')) { - this.setBlock(key, space); - } else { - this.startBlock('list', key, space); - } - continue; - } else if (this.isBlock('list')) { - if ((emptyCount === 0) && !!(matches = line.match(/^(\s+)/)) && matches[1].length > block[3]) { - this.setBlock(key); + if (this.current !== 'normal') { + pass = this.parsers[this.current](block, key, line, state, lines); + if (!pass) { continue; } } - if (!!(matches = line.match(/^(\s*)(~{3,}|`{3,})([^`~]*)$/i))) { - if (this.isBlock('code')) { - isAfterList = block[3][2]; - if (isAfterList) { - this.combineBlock().setBlock(key); - } else { - (this.setBlock(key)).endBlock(); + ref1 = this.parsers; + for (name in ref1) { + parser = ref1[name]; + if (name !== this.current) { + pass = parser(block, key, line, state, lines); + if (!pass) { + break; } - } else { - isAfterList = false; - if (this.isBlock('list')) { - space = block[3]; - isAfterList = (space > 0 && matches[1].length >= space) || matches[1].length > space; - } - this.startBlock('code', key, [matches[1], matches[3], isAfterList]); } - continue; - } else if (this.isBlock('code')) { - this.setBlock(key); - continue; - } - if (this.html) { - if (!autoHtml && !!(matches = line.match(/^(\s*)!!!(\s*)$/))) { - if (this.isBlock('shtml')) { - this.setBlock(key).endBlock(); - } else { - this.startBlock('shtml', key); - } - continue; - } else if (this.isBlock('shtml')) { - this.setBlock(key); - continue; - } - htmlTagRegExp = new RegExp("^\\s*<(" + this.blockHtmlTags + ")(\\s+[^>]*)?>", 'i'); - if (matches = line.match(htmlTagRegExp)) { - if (this.isBlock('ahtml')) { - this.setBlock(key); - continue; - } else if (matches[2] === void 0 || matches[2] !== '/') { - this.startBlock('ahtml', key); - htmlTagAllRegExp = new RegExp("\\s*<(" + this.blockHtmlTags + ")(\\s+[^>]*)?>", 'ig'); - while (true) { - m = htmlTagAllRegExp.exec(line); - if (!m) { - break; - } - lastMatch = m[1]; - } - if (0 <= line.indexOf("")) { - this.endBlock(); - } else { - autoHtml = lastMatch; - } - continue; - } - } else if (!!autoHtml && 0 <= line.indexOf("")) { - this.setBlock(key).endBlock(); - autoHtml = false; - continue; - } else if (this.isBlock('ahtml')) { - this.setBlock(key); - continue; - } else if (!!(matches = line.match(/^\s*\s*$/))) { - this.startBlock('ahtml', key).endBlock(); - continue; - } - } - if (!!(matches = line.match(/^(\s*)\$\$(\s*)$/))) { - if (this.isBlock('math')) { - this.setBlock(key).endBlock(); - } else { - this.startBlock('math', key); - } - continue; - } else if (this.isBlock('math')) { - this.setBlock(key); - continue; - } - if (!!(line.match(/^ {4}/))) { - emptyCount = 0; - if ((this.isBlock('pre')) || this.isBlock('list')) { - this.setBlock(key); - } else { - this.startBlock('pre', key); - } - continue; - } else if (this.isBlock('pre')) { - if (line.match(/^\s*$/)) { - if (emptyCount > 0) { - this.startBlock('normal', key); - } else { - this.setBlock(key); - } - emptyCount += 1; - } else { - this.startBlock('normal', key); - } - continue; - } - if (!!(matches = line.match(new RegExp("^\\s*<(" + special + ")(\\s+[^>]*)?>", 'i')))) { - tag = matches[1].toLowerCase(); - if (!(this.isBlock('html', tag)) && !(this.isBlock('pre'))) { - this.startBlock('html', key, tag); - } - continue; - } else if (!!(matches = line.match(new RegExp("\\s*$", 'i')))) { - tag = matches[1].toLowerCase(); - if (this.isBlock('html', tag)) { - this.setBlock(key).endBlock(); - } - continue; - } else if (this.isBlock('html')) { - this.setBlock(key); - continue; - } - switch (true) { - case !!(matches = line.match(/^\[\^((?:[^\]]|\\\]|\\\[)+?)\]:/)): - space = matches[0].length - 1; - this.startBlock('footnote', key, [space, matches[1]]); - break; - case !!(matches = line.match(/^\s*\[((?:[^\]]|\\\]|\\\[)+?)\]:\s*(.+)$/)): - this.definitions[matches[1]] = this.cleanUrl(matches[2]); - this.startBlock('definition', key).endBlock(); - break; - case !!(matches = line.match(/^(\s*)>/)): - if ((this.isBlock('list')) && matches[1].length > 0) { - this.setBlock(key); - } else if (this.isBlock('quote')) { - this.setBlock(key); - } else { - this.startBlock('quote', key); - } - break; - case !!(matches = line.match(/^((?:(?:(?:\||\+)(?:[ :]*\-+[ :]*)(?:\||\+))|(?:(?:[ :]*\-+[ :]*)(?:\||\+)(?:[ :]*\-+[ :]*))|(?:(?:[ :]*\-+[ :]*)(?:\||\+))|(?:(?:\||\+)(?:[ :]*\-+[ :]*)))+)$/)): - if (this.isBlock('table')) { - block[3][0].push(block[3][2]); - block[3][2] += 1; - this.setBlock(key, block[3]); - } else { - head = 0; - if ((block == null) || block[0] !== 'normal' || lines[block[2]].match(/^\s*$/)) { - this.startBlock('table', key); - } else { - head = 1; - this.backBlock(1, 'table'); - } - if (matches[1][0] === '|') { - matches[1] = matches[1].substring(1); - if (matches[1][matches[1].length - 1] === '|') { - matches[1] = matches[1].substring(0, matches[1].length - 1); - } - } - rows = matches[1].split(/\+|\|/); - aligns = []; - for (n = 0, len2 = rows.length; n < len2; n++) { - row = rows[n]; - align = 'none'; - if (!!(matches = row.match(/^\s*(:?)\-+(:?)\s*$/))) { - if (!!matches[1] && !!matches[2]) { - align = 'center'; - } else if (!!matches[1]) { - align = 'left'; - } else if (!!matches[2]) { - align = 'right'; - } - } - aligns.push(align); - } - this.setBlock(key, [[head], aligns, head + 1]); - } - break; - case !!(matches = line.match(/^(#+)(.*)$/)): - num = Math.min(matches[1].length, 6); - this.startBlock('sh', key, num).endBlock(); - break; - case !!(matches = line.match(/^\s*((=|-){2,})\s*$/)) && ((block != null) && block[0] === 'normal' && !lines[block[2]].match(/^\s*$/)): - if (this.isBlock('normal')) { - this.backBlock(1, 'mh', matches[1][0] === '=' ? 1 : 2).setBlock(key).endBlock(); - } else { - this.startBlock('normal', key); - } - break; - case !!(line.match(/^[-\*]{3,}\s*$/)): - this.startBlock('hr', key).endBlock(); - break; - default: - if (this.isBlock('list')) { - if (!!(matches = line.match(/^(\s*)/))) { - indent = matches[1].length > 0; - if (emptyCount > 0 && !indent) { - this.startBlock('normal', key); - } else { - this.setBlock(key); - } - if (indent) { - emptyCount = 0; - } else { - emptyCount += 1; - } - } else if (emptyCount === 0) { - this.setBlock(key); - } else { - this.startBlock('normal', key); - } - } else if (this.isBlock('footnote')) { - matches = line.match(/^(\s*)/); - if (matches[1].length >= block[3][0]) { - this.setBlock(key); - } else { - this.startBlock('normal', key); - } - } else if (this.isBlock('table')) { - if (0 <= line.indexOf('|')) { - block[3][2] += 1; - this.setBlock(key, block[3]); - } else { - this.startBlock('normal', key); - } - } else if (this.isBlock('quote')) { - if (!line.match(/^(\s*)$/)) { - this.setBlock(key); - } else { - this.startBlock('normal', key); - } - } else { - if ((block == null) || block[0] !== 'normal') { - this.startBlock('normal', key); - } else { - this.setBlock(key); - } - } } } return this.optimizeBlocks(this.blocks, lines); }; + Parser.prototype.parseBlockList = function(block, key, line, state) { + var matches, space; + if (!!(matches = line.match(/^(\s*)((?:[0-9]+\.)|(?:[a-z]\.?)|\-|\+|\*)\s+/i))) { + space = matches[1].length; + state.empty = 0; + if (this.isBlock('list')) { + this.setBlock(key, space); + } else { + this.startBlock('list', key, space); + } + return false; + } else if (this.isBlock('list')) { + if ((state.empty === 0) && !!(matches = line.match(/^(\s+)/)) && matches[1].length > block[3]) { + this.setBlock(key); + return false; + } + } + return true; + }; + + Parser.prototype.parseBlockCode = function(block, key, line) { + var isAfterList, matches, space; + if (!!(matches = line.match(/^(\s*)(~{3,}|`{3,})([^`~]*)$/i))) { + if (this.isBlock('code')) { + isAfterList = block[3][2]; + if (isAfterList) { + this.combineBlock().setBlock(key); + } else { + (this.setBlock(key)).endBlock(); + } + } else { + isAfterList = false; + if (this.isBlock('list')) { + space = block[3]; + isAfterList = (space > 0 && matches[1].length >= space) || matches[1].length > space; + } + this.startBlock('code', key, [matches[1], matches[3], isAfterList]); + } + return false; + } else if (this.isBlock('code')) { + this.setBlock(key); + return false; + } + return true; + }; + + Parser.prototype.parseBlockShtml = function(block, key, line, state) { + var matches; + if (this.html) { + if (!!(matches = line.match(/^(\s*)!!!(\s*)$/))) { + if (this.isBlock('shtml')) { + this.setBlock(key).endBlock(); + } else { + this.startBlock('shtml', key); + } + return false; + } else if (this.isBlock('shtml')) { + this.setBlock(key); + return false; + } + } + return true; + }; + + Parser.prototype.parseBlockAhtml = function(block, key, line, state) { + var htmlTagAllRegExp, htmlTagRegExp, lastMatch, m, matches; + if (this.html) { + htmlTagRegExp = new RegExp("^\\s*<(" + this.blockHtmlTags + ")(\\s+[^>]*)?>", 'i'); + if (matches = line.match(htmlTagRegExp)) { + if (this.isBlock('ahtml')) { + this.setBlock(key); + return false; + } else if (matches[2] === void 0 || matches[2] !== '/') { + this.startBlock('ahtml', key); + htmlTagAllRegExp = new RegExp("\\s*<(" + this.blockHtmlTags + ")(\\s+[^>]*)?>", 'ig'); + while (true) { + m = htmlTagAllRegExp.exec(line); + if (!m) { + break; + } + lastMatch = m[1]; + } + if (0 <= line.indexOf("")) { + this.endBlock(); + } else { + state.html = lastMatch; + } + return false; + } + } else if (!!state.html && 0 <= line.indexOf("")) { + this.setBlock(key).endBlock(); + state.html = false; + return false; + } else if (this.isBlock('ahtml')) { + this.setBlock(key); + return false; + } else if (!!(matches = line.match(/^\s*\s*$/))) { + this.startBlock('ahtml', key).endBlock(); + return false; + } + } + return true; + }; + + Parser.prototype.parseBlockMath = function(block, key, line) { + var matches; + if (!!(matches = line.match(/^(\s*)\$\$(\s*)$/))) { + if (this.isBlock('math')) { + this.setBlock(key).endBlock(); + } else { + this.startBlock('math', key); + } + return false; + } else if (this.isBlock('math')) { + this.setBlock(key); + return false; + } + return true; + }; + + Parser.prototype.parseBlockPre = function(block, key, line, state) { + if (!!(line.match(/^ {4}/))) { + state.empty = 0; + if (this.isBlock('pre')) { + this.setBlock(key); + } else { + this.startBlock('pre', key); + } + return false; + } else if (this.isBlock('pre')) { + if (line.match(/^\s*$/)) { + if (state.empty > 0) { + this.startBlock('normal', key); + } else { + this.setBlock(key); + } + state.empty += 1; + } else { + this.startBlock('normal', key); + } + return false; + } + return true; + }; + + Parser.prototype.parseBlockHtml = function(block, key, line, state) { + var matches, tag; + if (!!(matches = line.match(new RegExp("^\\s*<(" + state.special + ")(\\s+[^>]*)?>", 'i')))) { + tag = matches[1].toLowerCase(); + if (!(this.isBlock('html', tag)) && !(this.isBlock('pre'))) { + this.startBlock('html', key, tag); + } + return false; + } else if (!!(matches = line.match(new RegExp("\\s*$", 'i')))) { + tag = matches[1].toLowerCase(); + if (this.isBlock('html', tag)) { + this.setBlock(key).endBlock(); + } + return false; + } else if (this.isBlock('html')) { + this.setBlock(key); + return false; + } + return true; + }; + + Parser.prototype.parseBlockFootnote = function(block, key, line) { + var matches, space; + if (!!(matches = line.match(/^\[\^((?:[^\]]|\\\]|\\\[)+?)\]:/))) { + space = matches[0].length - 1; + this.startBlock('footnote', key, [space, matches[1]]); + return false; + } + return true; + }; + + Parser.prototype.parseBlockDefinition = function(block, key, line) { + var matches; + if (!!(matches = line.match(/^\s*\[((?:[^\]]|\\\]|\\\[)+?)\]:\s*(.+)$/))) { + this.definitions[matches[1]] = this.cleanUrl(matches[2]); + this.startBlock('definition', key).endBlock(); + return false; + } + return true; + }; + + Parser.prototype.parseBlockQuote = function(block, key, line) { + var matches; + if (!!(matches = line.match(/^(\s*)>/))) { + if ((this.isBlock('list')) && matches[1].length > 0) { + this.setBlock(key); + } else if (this.isBlock('quote')) { + this.setBlock(key); + } else { + this.startBlock('quote', key); + } + return false; + } + return true; + }; + + Parser.prototype.parseBlockTable = function(block, key, line, state, lines) { + var align, aligns, head, j, len, matches, row, rows; + if (!!(matches = line.match(/^((?:(?:(?:\||\+)(?:[ :]*\-+[ :]*)(?:\||\+))|(?:(?:[ :]*\-+[ :]*)(?:\||\+)(?:[ :]*\-+[ :]*))|(?:(?:[ :]*\-+[ :]*)(?:\||\+))|(?:(?:\||\+)(?:[ :]*\-+[ :]*)))+)$/))) { + if (this.isBlock('table')) { + block[3][0].push(block[3][2]); + block[3][2] += 1; + this.setBlock(key, block[3]); + } else { + head = 0; + if ((block == null) || block[0] !== 'normal' || lines[block[2]].match(/^\s*$/)) { + this.startBlock('table', key); + } else { + head = 1; + this.backBlock(1, 'table'); + } + if (matches[1][0] === '|') { + matches[1] = matches[1].substring(1); + if (matches[1][matches[1].length - 1] === '|') { + matches[1] = matches[1].substring(0, matches[1].length - 1); + } + } + rows = matches[1].split(/\+|\|/); + aligns = []; + for (j = 0, len = rows.length; j < len; j++) { + row = rows[j]; + align = 'none'; + if (!!(matches = row.match(/^\s*(:?)\-+(:?)\s*$/))) { + if (!!matches[1] && !!matches[2]) { + align = 'center'; + } else if (!!matches[1]) { + align = 'left'; + } else if (!!matches[2]) { + align = 'right'; + } + } + aligns.push(align); + } + this.setBlock(key, [[head], aligns, head + 1]); + } + return false; + } + return true; + }; + + Parser.prototype.parseBlockSh = function(block, key, line) { + var matches, num; + if (!!(matches = line.match(/^(#+)(.*)$/))) { + num = Math.min(matches[1].length, 6); + this.startBlock('sh', key, num).endBlock(); + return false; + } + return true; + }; + + Parser.prototype.parseBlockMh = function(block, key, line, state, lines) { + var matches; + if (!!(matches = line.match(/^\s*((=|-){2,})\s*$/)) && ((block != null) && block[0] === 'normal' && !lines[block[2]].match(/^\s*$/))) { + if (this.isBlock('normal')) { + this.backBlock(1, 'mh', matches[1][0] === '=' ? 1 : 2).setBlock(key).endBlock(); + } else { + this.startBlock('normal', key); + } + return false; + } + return true; + }; + + Parser.prototype.parseBlockHr = function(block, key, line) { + if (!!(line.match(/^[-\*]{3,}\s*$/))) { + this.startBlock('hr', key).endBlock(); + return false; + } + return true; + }; + + Parser.prototype.parseBlockDefault = function(block, key, line, state) { + var indent, matches; + if (this.isBlock('list')) { + if (!!(matches = line.match(/^(\s*)/))) { + indent = matches[1].length > 0; + if (state.empty > 0 && !indent) { + this.startBlock('normal', key); + } else { + this.setBlock(key); + } + if (indent) { + state.empty = 0; + } else { + state.empty += 1; + } + } else if (state.empty === 0) { + this.setBlock(key); + } else { + this.startBlock('normal', key); + } + } else if (this.isBlock('footnote')) { + matches = line.match(/^(\s*)/); + if (matches[1].length >= block[3][0]) { + this.setBlock(key); + } else { + this.startBlock('normal', key); + } + } else if (this.isBlock('table')) { + if (0 <= line.indexOf('|')) { + block[3][2] += 1; + this.setBlock(key, block[3]); + } else { + this.startBlock('normal', key); + } + } else if (this.isBlock('quote')) { + if (!line.match(/^(\s*)$/)) { + this.setBlock(key); + } else { + this.startBlock('normal', key); + } + } else { + if ((block == null) || block[0] !== 'normal') { + this.startBlock('normal', key); + } else { + this.setBlock(key); + } + } + return true; + }; + Parser.prototype.optimizeBlocks = function(_blocks, _lines) { var block, blocks, from, isEmpty, key, lines, moved, nextBlock, prevBlock, to, type, types; blocks = _blocks.slice(0); @@ -879,7 +995,7 @@ } rows = line.split('|').map(function(row) { if (row.match(/^\s+$/)) { - return ''; + return ' '; } else { return trim(row); } diff --git a/var/HyperDown.php b/var/HyperDown.php index 2e122d3c..fce9f10b 100644 --- a/var/HyperDown.php +++ b/var/HyperDown.php @@ -45,6 +45,27 @@ class HyperDown */ public $_html = false; + /** + * @var array + */ + public $blockParsers = array( + array('code', 10), + array('shtml', 20), + array('ahtml', 30), + array('list', 40), + array('math', 50), + array('pre', 60), + array('html', 70), + array('footnote', 80), + array('definition', 90), + array('quote', 100), + array('table', 110), + array('sh', 120), + array('mh', 130), + array('hr', 140), + array('default', 9999) + ); + /** * _blocks * @@ -93,6 +114,11 @@ class HyperDown */ private $_id; + /** + * @var array + */ + private $_parsers = array(); + /** * makeHtml * @@ -107,6 +133,20 @@ class HyperDown $this->_uniqid = md5(uniqid()); $this->_id = 0; + usort($this->blockParsers, function ($a, $b) { + return $a[1] < $b[1] ? -1 : 1; + }); + + foreach ($this->blockParsers as $parser) { + list ($name) = $parser; + + if (isset($parser[2])) { + $this->_parsers[$name] = $parser[2]; + } else { + $this->_parsers[$name] = array($this, 'parseBlock' . ucfirst($name)); + } + } + $text = $this->initText($text); $html = $this->parse($text); $html = $this->makeFootnotes($html); @@ -538,333 +578,527 @@ class HyperDown $this->_blocks = array(); $this->_current = 'normal'; $this->_pos = -1; - $special = implode("|", array_keys($this->_specialWhiteList)); - $emptyCount = 0; - $autoHtml = false; + + $state = array( + 'special' => implode("|", array_keys($this->_specialWhiteList)), + 'empty' => 0, + 'html' => false + ); // analyze by line foreach ($lines as $key => $line) { $block = $this->getBlock(); + $args = array($block, $key, $line, &$state, $lines); - // list block - if (preg_match("/^(\s*)((?:[0-9]+\.)|(?:[a-z]\.?)|\-|\+|\*)\s+/i", $line, $matches)) { - $space = strlen($matches[1]); - $emptyCount = 0; + if ($this->_current != 'normal') { + $pass = call_user_func_array($this->_parsers[$this->_current], $args); - // opened - if ($this->isBlock('list')) { - $this->setBlock($key, $space); - } else { - $this->startBlock('list', $key, $space); - } - - continue; - } else if ($this->isBlock('list')) { - if ($emptyCount == 0 - && preg_match("/^(\s+)/", $line, $matches) - && strlen($matches[1]) > $block[3]) { - $this->setBlock($key); + if (!$pass) { continue; } } - // code block - if (preg_match("/^(\s*)(~{3,}|`{3,})([^`~]*)$/i", $line, $matches)) { - if ($this->isBlock('code')) { - $isAfterList = $block[3][2]; + foreach ($this->_parsers as $name => $parser) { + if ($name != $this->_current) { + $pass = call_user_func_array($parser, $args); - if ($isAfterList) { - $this->combineBlock() - ->setBlock($key); - } else { - $this->setBlock($key) - ->endBlock(); + if (!$pass) { + break; } - } else { - $isAfterList = false; - - if ($this->isBlock('list')) { - $space = $block[3]; - - $isAfterList = ($space > 0 && strlen($matches[1]) >= $space) - || strlen($matches[1]) > $space; - } - - $this->startBlock('code', $key, array( - $matches[1], $matches[3], $isAfterList - )); } - - continue; - } else if ($this->isBlock('code')) { - $this->setBlock($key); - continue; - } - - // super html mode - if ($this->_html) { - if (!$autoHtml && preg_match("/^(\s*)!!!(\s*)$/", $line, $matches)) { - if ($this->isBlock('shtml')) { - $this->setBlock($key)->endBlock(); - } else { - $this->startBlock('shtml', $key); - } - - continue; - } else if ($this->isBlock('shtml')) { - $this->setBlock($key); - continue; - } - - // auto html - if (preg_match("/^\s*<({$this->_blockHtmlTags})(\s+[^>]*)?>/i", $line, $matches)) { - if ($this->isBlock('ahtml')) { - $this->setBlock($key); - continue; - } else if (empty($matches[2]) || $matches[2] != '/') { - $this->startBlock('ahtml', $key); - preg_match_all("/<({$this->_blockHtmlTags})(\s+[^>]*)?>/i", $line, $allMatches); - $lastMatch = $allMatches[1][count($allMatches[0]) - 1]; - - if (strpos($line, "") !== false) { - $this->endBlock(); - } else { - $autoHtml = $lastMatch; - } - continue; - } - } else if (!!$autoHtml && strpos($line, "") !== false) { - $this->setBlock($key)->endBlock(); - $autoHtml = false; - continue; - } else if ($this->isBlock('ahtml')) { - $this->setBlock($key); - continue; - } else if (preg_match("/^\s*\s*$/", $line, $matches)) { - $this->startBlock('ahtml', $key)->endBlock(); - continue; - } - } - - // mathjax mode - if (preg_match("/^(\s*)\\$\\$(\s*)$/", $line, $matches)) { - if ($this->isBlock('math')) { - $this->setBlock($key)->endBlock(); - } else { - $this->startBlock('math', $key); - } - - continue; - } else if ($this->isBlock('math')) { - $this->setBlock($key); - continue; - } - - // pre block - if (preg_match("/^ {4}/", $line)) { - $emptyCount = 0; - - if ($this->isBlock('pre') || $this->isBlock('list')) { - $this->setBlock($key); - } else { - $this->startBlock('pre', $key); - } - - continue; - } else if ($this->isBlock('pre')) { - if (preg_match("/^\s*$/", $line)) { - if ($emptyCount > 0) { - $this->startBlock('normal', $key); - } else { - $this->setBlock($key); - } - - $emptyCount ++; - } else { - $this->startBlock('normal', $key); - } - - continue; - } - - // html block is special too - if (preg_match("/^\s*<({$special})(\s+[^>]*)?>/i", $line, $matches)) { - $tag = strtolower($matches[1]); - if (!$this->isBlock('html', $tag) && !$this->isBlock('pre')) { - $this->startBlock('html', $key, $tag); - } - - continue; - } else if (preg_match("/<\/({$special})>\s*$/i", $line, $matches)) { - $tag = strtolower($matches[1]); - - if ($this->isBlock('html', $tag)) { - $this->setBlock($key) - ->endBlock(); - } - - continue; - } else if ($this->isBlock('html')) { - $this->setBlock($key); - continue; - } - - switch (true) { - // footnote - case preg_match("/^\[\^((?:[^\]]|\\]|\\[)+?)\]:/", $line, $matches): - $space = strlen($matches[0]) - 1; - $this->startBlock('footnote', $key, array( - $space, $matches[1] - )); - break; - - // definition - case preg_match("/^\s*\[((?:[^\]]|\\]|\\[)+?)\]:\s*(.+)$/", $line, $matches): - $this->_definitions[$matches[1]] = $this->cleanUrl($matches[2]); - $this->startBlock('definition', $key) - ->endBlock(); - break; - - // block quote - case preg_match("/^(\s*)>/", $line, $matches): - if ($this->isBlock('list') && strlen($matches[1]) > 0) { - $this->setBlock($key); - } else if ($this->isBlock('quote')) { - $this->setBlock($key); - } else { - $this->startBlock('quote', $key); - } - break; - - // table - case preg_match("/^((?:(?:(?:\||\+)(?:[ :]*\-+[ :]*)(?:\||\+))|(?:(?:[ :]*\-+[ :]*)(?:\||\+)(?:[ :]*\-+[ :]*))|(?:(?:[ :]*\-+[ :]*)(?:\||\+))|(?:(?:\||\+)(?:[ :]*\-+[ :]*)))+)$/", $line, $matches): - if ($this->isBlock('table')) { - $block[3][0][] = $block[3][2]; - $block[3][2] ++; - $this->setBlock($key, $block[3]); - } else { - $head = 0; - - if (empty($block) || - $block[0] != 'normal' || - preg_match("/^\s*$/", $lines[$block[2]])) { - $this->startBlock('table', $key); - } else { - $head = 1; - $this->backBlock(1, 'table'); - } - - if ($matches[1][0] == '|') { - $matches[1] = substr($matches[1], 1); - - if ($matches[1][strlen($matches[1]) - 1] == '|') { - $matches[1] = substr($matches[1], 0, -1); - } - } - - $rows = preg_split("/(\+|\|)/", $matches[1]); - $aligns = array(); - foreach ($rows as $row) { - $align = 'none'; - - if (preg_match("/^\s*(:?)\-+(:?)\s*$/", $row, $matches)) { - if (!empty($matches[1]) && !empty($matches[2])) { - $align = 'center'; - } else if (!empty($matches[1])) { - $align = 'left'; - } else if (!empty($matches[2])) { - $align = 'right'; - } - } - - $aligns[] = $align; - } - - $this->setBlock($key, array(array($head), $aligns, $head + 1)); - } - break; - - // single heading - case preg_match("/^(#+)(.*)$/", $line, $matches): - $num = min(strlen($matches[1]), 6); - $this->startBlock('sh', $key, $num) - ->endBlock(); - break; - - // multi heading - case preg_match("/^\s*((=|-){2,})\s*$/", $line, $matches) - && ($block && $block[0] == "normal" && !preg_match("/^\s*$/", $lines[$block[2]])): // check if last line isn't empty - if ($this->isBlock('normal')) { - $this->backBlock(1, 'mh', $matches[1][0] == '=' ? 1 : 2) - ->setBlock($key) - ->endBlock(); - } else { - $this->startBlock('normal', $key); - } - break; - - // hr - case preg_match("/^[-\*]{3,}\s*$/", $line): - $this->startBlock('hr', $key) - ->endBlock(); - break; - - // normal - default: - if ($this->isBlock('list')) { - if (preg_match("/^(\s*)/", $line, $matches)) { // empty line - $indent = strlen($matches[1]) > 0; - - if ($emptyCount > 0 && !$indent) { - $this->startBlock('normal', $key); - } else { - $this->setBlock($key); - } - - if ($indent) { - $emptyCount = 0; - } else { - $emptyCount ++; - } - } else if ($emptyCount == 0) { - $this->setBlock($key); - } else { - $this->startBlock('normal', $key); - } - } else if ($this->isBlock('footnote')) { - preg_match("/^(\s*)/", $line, $matches); - if (strlen($matches[1]) >= $block[3][0]) { - $this->setBlock($key); - } else { - $this->startBlock('normal', $key); - } - } else if ($this->isBlock('table')) { - if (false !== strpos($line, '|')) { - $block[3][2] ++; - $this->setBlock($key, $block[3]); - } else { - $this->startBlock('normal', $key); - } - } else if ($this->isBlock('quote')) { - if (!preg_match("/^(\s*)$/", $line)) { // empty line - $this->setBlock($key); - } else { - $this->startBlock('normal', $key); - } - } else { - if (empty($block) || $block[0] != 'normal') { - $this->startBlock('normal', $key); - } else { - $this->setBlock($key); - } - } - break; } } return $this->optimizeBlocks($this->_blocks, $lines); } + /** + * @param $block + * @param $key + * @param $line + * @param $state + * @return bool + */ + private function parseBlockList($block, $key, $line, &$state) + { + if (preg_match("/^(\s*)((?:[0-9]+\.)|(?:[a-z]\.?)|\-|\+|\*)\s+/i", $line, $matches)) { + $space = strlen($matches[1]); + $state['empty'] = 0; + + // opened + if ($this->isBlock('list')) { + $this->setBlock($key, $space); + } else { + $this->startBlock('list', $key, $space); + } + + return false; + } else if ($this->isBlock('list')) { + if ($state['empty'] == 0 + && preg_match("/^(\s+)/", $line, $matches) + && strlen($matches[1]) > $block[3]) { + $this->setBlock($key); + return false; + } + } + + return true; + } + + /** + * @param $block + * @param $key + * @param $line + * @return bool + */ + private function parseBlockCode($block, $key, $line) + { + if (preg_match("/^(\s*)(~{3,}|`{3,})([^`~]*)$/i", $line, $matches)) { + if ($this->isBlock('code')) { + $isAfterList = $block[3][2]; + + if ($isAfterList) { + $this->combineBlock() + ->setBlock($key); + } else { + $this->setBlock($key) + ->endBlock(); + } + } else { + $isAfterList = false; + + if ($this->isBlock('list')) { + $space = $block[3]; + + $isAfterList = ($space > 0 && strlen($matches[1]) >= $space) + || strlen($matches[1]) > $space; + } + + $this->startBlock('code', $key, array( + $matches[1], $matches[3], $isAfterList + )); + } + + return false; + } else if ($this->isBlock('code')) { + $this->setBlock($key); + return false; + } + + return true; + } + + /** + * @param $block + * @param $key + * @param $line + * @param $state + * @return bool + */ + private function parseBlockShtml($block, $key, $line, &$state) + { + if ($this->_html) { + if (preg_match("/^(\s*)!!!(\s*)$/", $line, $matches)) { + if ($this->isBlock('shtml')) { + $this->setBlock($key)->endBlock(); + } else { + $this->startBlock('shtml', $key); + } + + return false; + } else if ($this->isBlock('shtml')) { + $this->setBlock($key); + return false; + } + } + + return true; + } + + /** + * @param $block + * @param $key + * @param $line + * @param $state + * @return bool + */ + private function parseBlockAhtml($block, $key, $line, &$state) + { + if ($this->_html) { + if (preg_match("/^\s*<({$this->_blockHtmlTags})(\s+[^>]*)?>/i", $line, $matches)) { + if ($this->isBlock('ahtml')) { + $this->setBlock($key); + return false; + } else if (empty($matches[2]) || $matches[2] != '/') { + $this->startBlock('ahtml', $key); + preg_match_all("/<({$this->_blockHtmlTags})(\s+[^>]*)?>/i", $line, $allMatches); + $lastMatch = $allMatches[1][count($allMatches[0]) - 1]; + + if (strpos($line, "") !== false) { + $this->endBlock(); + } else { + $state['html'] = $lastMatch; + } + return false; + } + } else if (!!$state['html'] && strpos($line, "") !== false) { + $this->setBlock($key)->endBlock(); + $state['html'] = false; + return false; + } else if ($this->isBlock('ahtml')) { + $this->setBlock($key); + return false; + } else if (preg_match("/^\s*\s*$/", $line, $matches)) { + $this->startBlock('ahtml', $key)->endBlock(); + return false; + } + } + + return true; + } + + /** + * @param $block + * @param $key + * @param $line + * @return bool + */ + private function parseBlockMath($block, $key, $line) + { + if (preg_match("/^(\s*)\\$\\$(\s*)$/", $line, $matches)) { + if ($this->isBlock('math')) { + $this->setBlock($key)->endBlock(); + } else { + $this->startBlock('math', $key); + } + + return false; + } else if ($this->isBlock('math')) { + $this->setBlock($key); + return false; + } + + return true; + } + + /** + * @param $block + * @param $key + * @param $line + * @param $state + * @return bool + */ + private function parseBlockPre($block, $key, $line, &$state) + { + if (preg_match("/^ {4}/", $line)) { + $state['empty'] = 0; + + if ($this->isBlock('pre')) { + $this->setBlock($key); + } else { + $this->startBlock('pre', $key); + } + + return false; + } else if ($this->isBlock('pre')) { + if (preg_match("/^\s*$/", $line)) { + if ($state['empty'] > 0) { + $this->startBlock('normal', $key); + } else { + $this->setBlock($key); + } + + $state['empty'] ++; + } else { + $this->startBlock('normal', $key); + } + + return false; + } + + return true; + } + + /** + * @param $block + * @param $key + * @param $line + * @param $state + * @return bool + */ + private function parseBlockHtml($block, $key, $line, &$state) + { + if (preg_match("/^\s*<({$state['special']})(\s+[^>]*)?>/i", $line, $matches)) { + $tag = strtolower($matches[1]); + if (!$this->isBlock('html', $tag) && !$this->isBlock('pre')) { + $this->startBlock('html', $key, $tag); + } + + return false; + } else if (preg_match("/<\/({$state['special']})>\s*$/i", $line, $matches)) { + $tag = strtolower($matches[1]); + + if ($this->isBlock('html', $tag)) { + $this->setBlock($key) + ->endBlock(); + } + + return false; + } else if ($this->isBlock('html')) { + $this->setBlock($key); + return false; + } + + return true; + } + + /** + * @param $block + * @param $key + * @param $line + * @return bool + */ + private function parseBlockFootnote($block, $key, $line) + { + if (preg_match("/^\[\^((?:[^\]]|\\]|\\[)+?)\]:/", $line, $matches)) { + $space = strlen($matches[0]) - 1; + $this->startBlock('footnote', $key, array( + $space, $matches[1] + )); + + return false; + } + + return true; + } + + /** + * @param $block + * @param $key + * @param $line + * @return bool + */ + private function parseBlockDefinition($block, $key, $line) + { + if (preg_match("/^\s*\[((?:[^\]]|\\]|\\[)+?)\]:\s*(.+)$/", $line, $matches)) { + $this->_definitions[$matches[1]] = $this->cleanUrl($matches[2]); + $this->startBlock('definition', $key) + ->endBlock(); + + return false; + } + + return true; + } + + /** + * @param $block + * @param $key + * @param $line + * @return bool + */ + private function parseBlockQuote($block, $key, $line) + { + if (preg_match("/^(\s*)>/", $line, $matches)) { + if ($this->isBlock('list') && strlen($matches[1]) > 0) { + $this->setBlock($key); + } else if ($this->isBlock('quote')) { + $this->setBlock($key); + } else { + $this->startBlock('quote', $key); + } + + return false; + } + + return true; + } + + /** + * @param $block + * @param $key + * @param $line + * @param $state + * @param $lines + * @return bool + */ + private function parseBlockTable($block, $key, $line, &$state, $lines) + { + if (preg_match("/^((?:(?:(?:\||\+)(?:[ :]*\-+[ :]*)(?:\||\+))|(?:(?:[ :]*\-+[ :]*)(?:\||\+)(?:[ :]*\-+[ :]*))|(?:(?:[ :]*\-+[ :]*)(?:\||\+))|(?:(?:\||\+)(?:[ :]*\-+[ :]*)))+)$/", $line, $matches)) { + if ($this->isBlock('table')) { + $block[3][0][] = $block[3][2]; + $block[3][2]++; + $this->setBlock($key, $block[3]); + } else { + $head = 0; + + if (empty($block) || + $block[0] != 'normal' || + preg_match("/^\s*$/", $lines[$block[2]])) { + $this->startBlock('table', $key); + } else { + $head = 1; + $this->backBlock(1, 'table'); + } + + if ($matches[1][0] == '|') { + $matches[1] = substr($matches[1], 1); + + if ($matches[1][strlen($matches[1]) - 1] == '|') { + $matches[1] = substr($matches[1], 0, -1); + } + } + + $rows = preg_split("/(\+|\|)/", $matches[1]); + $aligns = array(); + foreach ($rows as $row) { + $align = 'none'; + + if (preg_match("/^\s*(:?)\-+(:?)\s*$/", $row, $matches)) { + if (!empty($matches[1]) && !empty($matches[2])) { + $align = 'center'; + } else if (!empty($matches[1])) { + $align = 'left'; + } else if (!empty($matches[2])) { + $align = 'right'; + } + } + + $aligns[] = $align; + } + + $this->setBlock($key, array(array($head), $aligns, $head + 1)); + } + + return false; + } + + return true; + } + + /** + * @param $block + * @param $key + * @param $line + * @return bool + */ + private function parseBlockSh($block, $key, $line) + { + if (preg_match("/^(#+)(.*)$/", $line, $matches)) { + $num = min(strlen($matches[1]), 6); + $this->startBlock('sh', $key, $num) + ->endBlock(); + + return false; + } + + return true; + } + + /** + * @param $block + * @param $key + * @param $line + * @param $state + * @param $lines + * @return bool + */ + private function parseBlockMh($block, $key, $line, &$state, $lines) + { + if (preg_match("/^\s*((=|-){2,})\s*$/", $line, $matches) + && ($block && $block[0] == "normal" && !preg_match("/^\s*$/", $lines[$block[2]]))) { // check if last line isn't empty + if ($this->isBlock('normal')) { + $this->backBlock(1, 'mh', $matches[1][0] == '=' ? 1 : 2) + ->setBlock($key) + ->endBlock(); + } else { + $this->startBlock('normal', $key); + } + + return false; + } + + return true; + } + + /** + * @param $block + * @param $key + * @param $line + * @return bool + */ + private function parseBlockHr($block, $key, $line) + { + if (preg_match("/^[-\*]{3,}\s*$/", $line)) { + $this->startBlock('hr', $key) + ->endBlock(); + + return false; + } + + return true; + } + + /** + * @param $block + * @param $key + * @param $line + * @param $state + * @return bool + */ + private function parseBlockDefault($block, $key, $line, &$state) + { + if ($this->isBlock('list')) { + if (preg_match("/^(\s*)/", $line, $matches)) { // empty line + $indent = strlen($matches[1]) > 0; + + if ($state['empty'] > 0 && !$indent) { + $this->startBlock('normal', $key); + } else { + $this->setBlock($key); + } + + if ($indent) { + $state['empty'] = 0; + } else { + $state['empty'] ++; + } + } else if ($state['empty'] == 0) { + $this->setBlock($key); + } else { + $this->startBlock('normal', $key); + } + } else if ($this->isBlock('footnote')) { + preg_match("/^(\s*)/", $line, $matches); + if (strlen($matches[1]) >= $block[3][0]) { + $this->setBlock($key); + } else { + $this->startBlock('normal', $key); + } + } else if ($this->isBlock('table')) { + if (false !== strpos($line, '|')) { + $block[3][2] ++; + $this->setBlock($key, $block[3]); + } else { + $this->startBlock('normal', $key); + } + } else if ($this->isBlock('quote')) { + if (!preg_match("/^(\s*)$/", $line)) { // empty line + $this->setBlock($key); + } else { + $this->startBlock('normal', $key); + } + } else { + if (empty($block) || $block[0] != 'normal') { + $this->startBlock('normal', $key); + } else { + $this->setBlock($key); + } + } + + return true; + } + /** * @param array $blocks * @param array $lines