From 365a3898b9078a34632d10ce7779db19cccd1704 Mon Sep 17 00:00:00 2001 From: Keith Solomon Date: Tue, 11 Aug 2026 16:10:19 -0500 Subject: [PATCH] Track PUC's bundled vendor/ in git (formerly ignored) --- .gitignore | 2 + .../vendor/Parsedown.php | 4 + .../vendor/ParsedownModern.php | 1538 +++++++++++++++++ .../vendor/PucReadmeParser.php | 352 ++++ 4 files changed, 1896 insertions(+) create mode 100644 includes/plugin-update-checker/vendor/Parsedown.php create mode 100644 includes/plugin-update-checker/vendor/ParsedownModern.php create mode 100644 includes/plugin-update-checker/vendor/PucReadmeParser.php diff --git a/.gitignore b/.gitignore index 7a58261..8f22cf9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ notes/ vendor/ phpunit.xml .phpunit.result.cache +# But DO track the vendor/ that ships with the bundled plugin-update-checker library. +!/includes/plugin-update-checker/vendor/ diff --git a/includes/plugin-update-checker/vendor/Parsedown.php b/includes/plugin-update-checker/vendor/Parsedown.php new file mode 100644 index 0000000..11a212f --- /dev/null +++ b/includes/plugin-update-checker/vendor/Parsedown.php @@ -0,0 +1,4 @@ +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; + + # + # 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']; + + $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8'); + + $Block['element']['text']['text'] = $text; + + return $Block; + } + + # + # Comment + + protected function blockComment($Line) + { + if ($this->markupEscaped) + { + 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,}[ ]*([\w-]+)?[ ]*$/', $Line['text'], $matches)) + { + $Element = array( + 'name' => 'code', + 'text' => '', + ); + + if (isset($matches[1])) + { + $class = 'language-'.$matches[1]; + + $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']; + + $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8'); + + $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', + ), + ); + + $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'] []= ''; + + 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; + } + } + + # + # 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, $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) + { + return; + } + + if (preg_match('/^<(\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, $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) + { + $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) + { + $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; + } + + # 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', + '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('/^[(]((?:[^ ()]|[(][^ )]+[)])+)(?:[ ]+("[^"]*"|\'[^\']*\'))?[)]/', $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']; + } + + $Element['attributes']['href'] = str_replace(array('&', '<'), array('&', '<'), $Element['attributes']['href']); + + return array( + 'extent' => $extent, + 'element' => $Element, + ); + } + + protected function inlineMarkup($Excerpt) + { + if ($this->markupEscaped or strpos($Excerpt['text'], '>') === false) + { + return; + } + + if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\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*(?:[ ]*'.$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)) + { + $Inline = array( + 'extent' => strlen($matches[0][0]), + 'position' => $matches[0][1], + 'element' => array( + 'name' => 'a', + 'text' => $matches[0][0], + 'attributes' => array( + 'href' => $matches[0][0], + ), + ), + ); + + return $Inline; + } + } + + protected function inlineUrlTag($Excerpt) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches)) + { + $url = str_replace(array('&', '<'), array('&', '<'), $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) + { + $markup = '<'.$Element['name']; + + if (isset($Element['attributes'])) + { + foreach ($Element['attributes'] as $name => $value) + { + if ($value === null) + { + continue; + } + + $markup .= ' '.$name.'="'.$value.'"'; + } + } + + if (isset($Element['text'])) + { + $markup .= '>'; + + if (isset($Element['handler'])) + { + $markup .= $this->{$Element['handler']}($Element['text']); + } + else + { + $markup .= $Element['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; + } + + # + # Static Methods + # + + 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', 'sub', 'mark', + 'u', 'xm', 'sup', 'nobr', + 'var', 'ruby', + 'wbr', 'span', + 'time', + ); +} \ No newline at end of file diff --git a/includes/plugin-update-checker/vendor/PucReadmeParser.php b/includes/plugin-update-checker/vendor/PucReadmeParser.php new file mode 100644 index 0000000..a794c49 --- /dev/null +++ b/includes/plugin-update-checker/vendor/PucReadmeParser.php @@ -0,0 +1,352 @@ +parse_readme_contents( $file_contents ); + } + + function parse_readme_contents( $file_contents ) { + $file_contents = str_replace(array("\r\n", "\r"), "\n", $file_contents); + $file_contents = trim($file_contents); + if ( 0 === strpos( $file_contents, "\xEF\xBB\xBF" ) ) + $file_contents = substr( $file_contents, 3 ); + + // Markdown transformations + $file_contents = preg_replace( "|^###([^#]+)#*?\s*?\n|im", '=$1='."\n", $file_contents ); + $file_contents = preg_replace( "|^##([^#]+)#*?\s*?\n|im", '==$1=='."\n", $file_contents ); + $file_contents = preg_replace( "|^#([^#]+)#*?\s*?\n|im", '===$1==='."\n", $file_contents ); + + // === Plugin Name === + // Must be the very first thing. + if ( !preg_match('|^===(.*)===|', $file_contents, $_name) ) + return array(); // require a name + $name = trim($_name[1], '='); + $name = $this->sanitize_text( $name ); + + $file_contents = $this->chop_string( $file_contents, $_name[0] ); + + + // Requires at least: 1.5 + if ( preg_match('|Requires at least:(.*)|i', $file_contents, $_requires_at_least) ) + $requires_at_least = $this->sanitize_text($_requires_at_least[1]); + else + $requires_at_least = NULL; + + + // Tested up to: 2.1 + if ( preg_match('|Tested up to:(.*)|i', $file_contents, $_tested_up_to) ) + $tested_up_to = $this->sanitize_text( $_tested_up_to[1] ); + else + $tested_up_to = NULL; + + // Requires PHP: 5.2.4 + if ( preg_match('|Requires PHP:(.*)|i', $file_contents, $_requires_php) ) { + $requires_php = $this->sanitize_text( $_requires_php[1] ); + } else { + $requires_php = null; + } + + // Stable tag: 10.4-ride-the-fire-eagle-danger-day + if ( preg_match('|Stable tag:(.*)|i', $file_contents, $_stable_tag) ) + $stable_tag = $this->sanitize_text( $_stable_tag[1] ); + else + $stable_tag = NULL; // we assume trunk, but don't set it here to tell the difference between specified trunk and default trunk + + + // Tags: some tag, another tag, we like tags + if ( preg_match('|Tags:(.*)|i', $file_contents, $_tags) ) { + $tags = preg_split('|,[\s]*?|', trim($_tags[1])); + foreach ( array_keys($tags) as $t ) + $tags[$t] = $this->sanitize_text( $tags[$t] ); + } else { + $tags = array(); + } + + + // Contributors: markjaquith, mdawaffe, zefrank + $contributors = array(); + if ( preg_match('|Contributors:(.*)|i', $file_contents, $_contributors) ) { + $temp_contributors = preg_split('|,[\s]*|', trim($_contributors[1])); + foreach ( array_keys($temp_contributors) as $c ) { + $tmp_sanitized = $this->user_sanitize( $temp_contributors[$c] ); + if ( strlen(trim($tmp_sanitized)) > 0 ) + $contributors[$c] = $tmp_sanitized; + unset($tmp_sanitized); + } + } + + + // Donate Link: URL + if ( preg_match('|Donate link:(.*)|i', $file_contents, $_donate_link) ) + $donate_link = esc_url( $_donate_link[1] ); + else + $donate_link = NULL; + + + // togs, conts, etc are optional and order shouldn't matter. So we chop them only after we've grabbed their values. + foreach ( array('tags', 'contributors', 'requires_at_least', 'tested_up_to', 'stable_tag', 'donate_link') as $chop ) { + if ( $$chop ) { + $_chop = '_' . $chop; + $file_contents = $this->chop_string( $file_contents, ${$_chop}[0] ); + } + } + + $file_contents = trim($file_contents); + + + // short-description fu + if ( !preg_match('/(^(.*?))^[\s]*=+?[\s]*.+?[\s]*=+?/ms', $file_contents, $_short_description) ) + $_short_description = array( 1 => &$file_contents, 2 => &$file_contents ); + $short_desc_filtered = $this->sanitize_text( $_short_description[2] ); + $short_desc_length = strlen($short_desc_filtered); + $short_description = substr($short_desc_filtered, 0, 150); + if ( $short_desc_length > strlen($short_description) ) + $truncated = true; + else + $truncated = false; + if ( $_short_description[1] ) + $file_contents = $this->chop_string( $file_contents, $_short_description[1] ); // yes, the [1] is intentional + + // == Section == + // Break into sections + // $_sections[0] will be the title of the first section, $_sections[1] will be the content of the first section + // the array alternates from there: title2, content2, title3, content3... and so forth + $_sections = preg_split('/^[\s]*==[\s]*(.+?)[\s]*==/m', $file_contents, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY); + + $sections = array(); + for ( $i=0; $i < count($_sections); $i +=2 ) { + $title = $this->sanitize_text( $_sections[$i] ); + if ( isset($_sections[$i+1]) ) { + $content = preg_replace('/(^[\s]*)=[\s]+(.+?)[\s]+=/m', '$1

$2

', $_sections[$i+1]); + $content = $this->filter_text( $content, true ); + } else { + $content = ''; + } + $sections[str_replace(' ', '_', strtolower($title))] = array('title' => $title, 'content' => $content); + } + + + // Special sections + // This is where we nab our special sections, so we can enforce their order and treat them differently, if needed + // upgrade_notice is not a section, but parse it like it is for now + $final_sections = array(); + foreach ( array('description', 'installation', 'frequently_asked_questions', 'screenshots', 'changelog', 'change_log', 'upgrade_notice') as $special_section ) { + if ( isset($sections[$special_section]) ) { + $final_sections[$special_section] = $sections[$special_section]['content']; + unset($sections[$special_section]); + } + } + if ( isset($final_sections['change_log']) && empty($final_sections['changelog']) ) + $final_sections['changelog'] = $final_sections['change_log']; + + + $final_screenshots = array(); + if ( isset($final_sections['screenshots']) ) { + preg_match_all('|
  • (.*?)
  • |s', $final_sections['screenshots'], $screenshots, PREG_SET_ORDER); + if ( $screenshots ) { + foreach ( (array) $screenshots as $ss ) + $final_screenshots[] = $ss[1]; + } + } + + // Parse the upgrade_notice section specially: + // 1.0 => blah, 1.1 => fnord + $upgrade_notice = array(); + if ( isset($final_sections['upgrade_notice']) ) { + $split = preg_split( '#

    (.*?)

    #', $final_sections['upgrade_notice'], -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ); + if ( count($split) >= 2 ) { + for ( $i = 0; $i < count( $split ); $i += 2 ) { + $upgrade_notice[$this->sanitize_text( $split[$i] )] = substr( $this->sanitize_text( $split[$i + 1] ), 0, 300 ); + } + } + unset( $final_sections['upgrade_notice'] ); + } + + // No description? + // No problem... we'll just fall back to the old style of description + // We'll even let you use markup this time! + $excerpt = false; + if ( !isset($final_sections['description']) ) { + $final_sections = array_merge(array('description' => $this->filter_text( $_short_description[2], true )), $final_sections); + $excerpt = true; + } + + + // dump the non-special sections into $remaining_content + // their order will be determined by their original order in the readme.txt + $remaining_content = ''; + foreach ( $sections as $s_name => $s_data ) { + $remaining_content .= "\n

    {$s_data['title']}

    \n{$s_data['content']}"; + } + $remaining_content = trim($remaining_content); + + + // All done! + // $r['tags'] and $r['contributors'] are simple arrays + // $r['sections'] is an array with named elements + $r = array( + 'name' => $name, + 'tags' => $tags, + 'requires_at_least' => $requires_at_least, + 'tested_up_to' => $tested_up_to, + 'requires_php' => $requires_php, + 'stable_tag' => $stable_tag, + 'contributors' => $contributors, + 'donate_link' => $donate_link, + 'short_description' => $short_description, + 'screenshots' => $final_screenshots, + 'is_excerpt' => $excerpt, + 'is_truncated' => $truncated, + 'sections' => $final_sections, + 'remaining_content' => $remaining_content, + 'upgrade_notice' => $upgrade_notice + ); + + return $r; + } + + function chop_string( $string, $chop ) { // chop a "prefix" from a string: Agressive! uses strstr not 0 === strpos + if ( $_string = strstr($string, $chop) ) { + $_string = substr($_string, strlen($chop)); + return trim($_string); + } else { + return trim($string); + } + } + + function user_sanitize( $text, $strict = false ) { // whitelisted chars + if ( function_exists('user_sanitize') ) // bbPress native + return user_sanitize( $text, $strict ); + + if ( $strict ) { + $text = preg_replace('/[^a-z0-9-]/i', '', $text); + $text = preg_replace('|-+|', '-', $text); + } else { + $text = preg_replace('/[^a-z0-9_-]/i', '', $text); + } + return $text; + } + + function sanitize_text( $text ) { // not fancy + $text = function_exists('wp_strip_all_tags') + ? wp_strip_all_tags($text) + //phpcs:ignore WordPressVIPMinimum.Functions.StripTags.StripTagsOneParameter -- Using wp_strip_all_tags() if available + : strip_tags($text); + + $text = esc_html($text); + $text = trim($text); + return $text; + } + + function filter_text( $text, $markdown = false ) { // fancy, Markdown + $text = trim($text); + + $text = call_user_func( array( __CLASS__, 'code_trick' ), $text, $markdown ); // A better parser than Markdown's for: backticks -> CODE + + if ( $markdown ) { // Parse markdown. + if ( !class_exists('Parsedown', false) ) { + /** @noinspection PhpIncludeInspection */ + require_once(dirname(__FILE__) . '/Parsedown' . (version_compare(PHP_VERSION, '5.3.0', '>=') ? '' : 'Legacy') . '.php'); + } + $instance = Parsedown::instance(); + $text = $instance->text($text); + } + + $allowed = array( + 'a' => array( + 'href' => array(), + 'title' => array(), + 'rel' => array()), + 'blockquote' => array('cite' => array()), + 'br' => array(), + 'p' => array(), + 'code' => array(), + 'pre' => array(), + 'em' => array(), + 'strong' => array(), + 'ul' => array(), + 'ol' => array(), + 'li' => array(), + 'h3' => array(), + 'h4' => array() + ); + + $text = balanceTags($text); + + $text = wp_kses( $text, $allowed ); + $text = trim($text); + return $text; + } + + function code_trick( $text, $markdown ) { // Don't use bbPress native function - it's incompatible with Markdown + // If doing markdown, first take any user formatted code blocks and turn them into backticks so that + // markdown will preserve things like underscores in code blocks + if ( $markdown ) + $text = preg_replace_callback("!(
    |)(.*?)(
    |)!s", array( __CLASS__,'decodeit'), $text); + + $text = str_replace(array("\r\n", "\r"), "\n", $text); + if ( !$markdown ) { + // This gets the "inline" code blocks, but can't be used with Markdown. + $text = preg_replace_callback("|(`)(.*?)`|", array( __CLASS__, 'encodeit'), $text); + // This gets the "block level" code blocks and converts them to PRE CODE + $text = preg_replace_callback("!(^|\n)`(.*?)`!s", array( __CLASS__, 'encodeit'), $text); + } else { + // Markdown can do inline code, we convert bbPress style block level code to Markdown style + $text = preg_replace_callback("!(^|\n)([ \t]*?)`(.*?)`!s", array( __CLASS__, 'indent'), $text); + } + return $text; + } + + function indent( $matches ) { + $text = $matches[3]; + $text = preg_replace('|^|m', $matches[2] . ' ', $text); + return $matches[1] . $text; + } + + function encodeit( $matches ) { + if ( function_exists('encodeit') ) // bbPress native + return encodeit( $matches ); + + $text = trim($matches[2]); + $text = htmlspecialchars($text, ENT_QUOTES); + $text = str_replace(array("\r\n", "\r"), "\n", $text); + $text = preg_replace("|\n\n\n+|", "\n\n", $text); + $text = str_replace('&lt;', '<', $text); + $text = str_replace('&gt;', '>', $text); + $text = "$text"; + if ( "`" != $matches[1] ) + $text = "
    $text
    "; + return $text; + } + + function decodeit( $matches ) { + if ( function_exists('decodeit') ) // bbPress native + return decodeit( $matches ); + + $text = $matches[2]; + $trans_table = array_flip(get_html_translation_table(HTML_ENTITIES)); + $text = strtr($text, $trans_table); + $text = str_replace('
    ', '', $text); + $text = str_replace('&', '&', $text); + $text = str_replace(''', "'", $text); + if ( '
    ' == $matches[1] )
    +			$text = "\n$text\n";
    +		return "`$text`";
    +	}
    +
    +} // end class
    +
    +endif;