Add latest changes from icehrm pro

This commit is contained in:
gamonoid
2018-05-21 00:23:56 +02:00
parent 9c56b8acd1
commit 861e94cf9d
1375 changed files with 175006 additions and 2662 deletions

951
core/lib/Mail/RFC822.php Normal file
View File

@@ -0,0 +1,951 @@
<?php
/**
* RFC 822 Email address list validation Utility
*
* PHP versions 4 and 5
*
* LICENSE:
*
* Copyright (c) 2001-2010, Richard Heyes
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* o Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* o Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* o The names of the authors may not be used to endorse or promote
* products derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @category Mail
* @package Mail
* @author Richard Heyes <richard@phpguru.org>
* @author Chuck Hagenbuch <chuck@horde.org
* @copyright 2001-2010 Richard Heyes
* @license http://opensource.org/licenses/bsd-license.php New BSD License
* @version CVS: $Id: RFC822.php 294749 2010-02-08 08:22:25Z clockwerx $
* @link http://pear.php.net/package/Mail/
*/
/**
* RFC 822 Email address list validation Utility
*
* What is it?
*
* This class will take an address string, and parse it into it's consituent
* parts, be that either addresses, groups, or combinations. Nested groups
* are not supported. The structure it returns is pretty straight forward,
* and is similar to that provided by the imap_rfc822_parse_adrlist(). Use
* print_r() to view the structure.
*
* How do I use it?
*
* $address_string = 'My Group: "Richard" <richard@localhost> (A comment), ted@example.com (Ted Bloggs), Barney;';
* $structure = Mail_RFC822::parseAddressList($address_string, 'example.com', true)
* print_r($structure);
*
* @author Richard Heyes <richard@phpguru.org>
* @author Chuck Hagenbuch <chuck@horde.org>
* @version $Revision: 294749 $
* @license BSD
* @package Mail
*/
class Mail_RFC822 {
/**
* The address being parsed by the RFC822 object.
* @var string $address
*/
var $address = '';
/**
* The default domain to use for unqualified addresses.
* @var string $default_domain
*/
var $default_domain = 'localhost';
/**
* Should we return a nested array showing groups, or flatten everything?
* @var boolean $nestGroups
*/
var $nestGroups = true;
/**
* Whether or not to validate atoms for non-ascii characters.
* @var boolean $validate
*/
var $validate = true;
/**
* The array of raw addresses built up as we parse.
* @var array $addresses
*/
var $addresses = array();
/**
* The final array of parsed address information that we build up.
* @var array $structure
*/
var $structure = array();
/**
* The current error message, if any.
* @var string $error
*/
var $error = null;
/**
* An internal counter/pointer.
* @var integer $index
*/
var $index = null;
/**
* The number of groups that have been found in the address list.
* @var integer $num_groups
* @access public
*/
var $num_groups = 0;
/**
* A variable so that we can tell whether or not we're inside a
* Mail_RFC822 object.
* @var boolean $mailRFC822
*/
var $mailRFC822 = true;
/**
* A limit after which processing stops
* @var int $limit
*/
var $limit = null;
/**
* Sets up the object. The address must either be set here or when
* calling parseAddressList(). One or the other.
*
* @access public
* @param string $address The address(es) to validate.
* @param string $default_domain Default domain/host etc. If not supplied, will be set to localhost.
* @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing.
* @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance.
*
* @return object Mail_RFC822 A new Mail_RFC822 object.
*/
function Mail_RFC822($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null)
{
if (isset($address)) $this->address = $address;
if (isset($default_domain)) $this->default_domain = $default_domain;
if (isset($nest_groups)) $this->nestGroups = $nest_groups;
if (isset($validate)) $this->validate = $validate;
if (isset($limit)) $this->limit = $limit;
}
/**
* Starts the whole process. The address must either be set here
* or when creating the object. One or the other.
*
* @access public
* @param string $address The address(es) to validate.
* @param string $default_domain Default domain/host etc.
* @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing.
* @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance.
*
* @return array A structured array of addresses.
*/
function parseAddressList($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null)
{
if (!isset($this) || !isset($this->mailRFC822)) {
$obj = new Mail_RFC822($address, $default_domain, $nest_groups, $validate, $limit);
return $obj->parseAddressList();
}
if (isset($address)) $this->address = $address;
if (isset($default_domain)) $this->default_domain = $default_domain;
if (isset($nest_groups)) $this->nestGroups = $nest_groups;
if (isset($validate)) $this->validate = $validate;
if (isset($limit)) $this->limit = $limit;
$this->structure = array();
$this->addresses = array();
$this->error = null;
$this->index = null;
// Unfold any long lines in $this->address.
$this->address = preg_replace('/\r?\n/', "\r\n", $this->address);
$this->address = preg_replace('/\r\n(\t| )+/', ' ', $this->address);
while ($this->address = $this->_splitAddresses($this->address));
if ($this->address === false || isset($this->error)) {
require_once 'PEAR.php';
return PEAR::raiseError($this->error);
}
// Validate each address individually. If we encounter an invalid
// address, stop iterating and return an error immediately.
foreach ($this->addresses as $address) {
$valid = $this->_validateAddress($address);
if ($valid === false || isset($this->error)) {
require_once 'PEAR.php';
return PEAR::raiseError($this->error);
}
if (!$this->nestGroups) {
$this->structure = array_merge($this->structure, $valid);
} else {
$this->structure[] = $valid;
}
}
return $this->structure;
}
/**
* Splits an address into separate addresses.
*
* @access private
* @param string $address The addresses to split.
* @return boolean Success or failure.
*/
function _splitAddresses($address)
{
if (!empty($this->limit) && count($this->addresses) == $this->limit) {
return '';
}
if ($this->_isGroup($address) && !isset($this->error)) {
$split_char = ';';
$is_group = true;
} elseif (!isset($this->error)) {
$split_char = ',';
$is_group = false;
} elseif (isset($this->error)) {
return false;
}
// Split the string based on the above ten or so lines.
$parts = explode($split_char, $address);
$string = $this->_splitCheck($parts, $split_char);
// If a group...
if ($is_group) {
// If $string does not contain a colon outside of
// brackets/quotes etc then something's fubar.
// First check there's a colon at all:
if (strpos($string, ':') === false) {
$this->error = 'Invalid address: ' . $string;
return false;
}
// Now check it's outside of brackets/quotes:
if (!$this->_splitCheck(explode(':', $string), ':')) {
return false;
}
// We must have a group at this point, so increase the counter:
$this->num_groups++;
}
// $string now contains the first full address/group.
// Add to the addresses array.
$this->addresses[] = array(
'address' => trim($string),
'group' => $is_group
);
// Remove the now stored address from the initial line, the +1
// is to account for the explode character.
$address = trim(substr($address, strlen($string) + 1));
// If the next char is a comma and this was a group, then
// there are more addresses, otherwise, if there are any more
// chars, then there is another address.
if ($is_group && substr($address, 0, 1) == ','){
$address = trim(substr($address, 1));
return $address;
} elseif (strlen($address) > 0) {
return $address;
} else {
return '';
}
// If you got here then something's off
return false;
}
/**
* Checks for a group at the start of the string.
*
* @access private
* @param string $address The address to check.
* @return boolean Whether or not there is a group at the start of the string.
*/
function _isGroup($address)
{
// First comma not in quotes, angles or escaped:
$parts = explode(',', $address);
$string = $this->_splitCheck($parts, ',');
// Now we have the first address, we can reliably check for a
// group by searching for a colon that's not escaped or in
// quotes or angle brackets.
if (count($parts = explode(':', $string)) > 1) {
$string2 = $this->_splitCheck($parts, ':');
return ($string2 !== $string);
} else {
return false;
}
}
/**
* A common function that will check an exploded string.
*
* @access private
* @param array $parts The exloded string.
* @param string $char The char that was exploded on.
* @return mixed False if the string contains unclosed quotes/brackets, or the string on success.
*/
function _splitCheck($parts, $char)
{
$string = $parts[0];
for ($i = 0; $i < count($parts); $i++) {
if ($this->_hasUnclosedQuotes($string)
|| $this->_hasUnclosedBrackets($string, '<>')
|| $this->_hasUnclosedBrackets($string, '[]')
|| $this->_hasUnclosedBrackets($string, '()')
|| substr($string, -1) == '\\') {
if (isset($parts[$i + 1])) {
$string = $string . $char . $parts[$i + 1];
} else {
$this->error = 'Invalid address spec. Unclosed bracket or quotes';
return false;
}
} else {
$this->index = $i;
break;
}
}
return $string;
}
/**
* Checks if a string has unclosed quotes or not.
*
* @access private
* @param string $string The string to check.
* @return boolean True if there are unclosed quotes inside the string,
* false otherwise.
*/
function _hasUnclosedQuotes($string)
{
$string = trim($string);
$iMax = strlen($string);
$in_quote = false;
$i = $slashes = 0;
for (; $i < $iMax; ++$i) {
switch ($string[$i]) {
case '\\':
++$slashes;
break;
case '"':
if ($slashes % 2 == 0) {
$in_quote = !$in_quote;
}
// Fall through to default action below.
default:
$slashes = 0;
break;
}
}
return $in_quote;
}
/**
* Checks if a string has an unclosed brackets or not. IMPORTANT:
* This function handles both angle brackets and square brackets;
*
* @access private
* @param string $string The string to check.
* @param string $chars The characters to check for.
* @return boolean True if there are unclosed brackets inside the string, false otherwise.
*/
function _hasUnclosedBrackets($string, $chars)
{
$num_angle_start = substr_count($string, $chars[0]);
$num_angle_end = substr_count($string, $chars[1]);
$this->_hasUnclosedBracketsSub($string, $num_angle_start, $chars[0]);
$this->_hasUnclosedBracketsSub($string, $num_angle_end, $chars[1]);
if ($num_angle_start < $num_angle_end) {
$this->error = 'Invalid address spec. Unmatched quote or bracket (' . $chars . ')';
return false;
} else {
return ($num_angle_start > $num_angle_end);
}
}
/**
* Sub function that is used only by hasUnclosedBrackets().
*
* @access private
* @param string $string The string to check.
* @param integer &$num The number of occurences.
* @param string $char The character to count.
* @return integer The number of occurences of $char in $string, adjusted for backslashes.
*/
function _hasUnclosedBracketsSub($string, &$num, $char)
{
$parts = explode($char, $string);
for ($i = 0; $i < count($parts); $i++){
if (substr($parts[$i], -1) == '\\' || $this->_hasUnclosedQuotes($parts[$i]))
$num--;
if (isset($parts[$i + 1]))
$parts[$i + 1] = $parts[$i] . $char . $parts[$i + 1];
}
return $num;
}
/**
* Function to begin checking the address.
*
* @access private
* @param string $address The address to validate.
* @return mixed False on failure, or a structured array of address information on success.
*/
function _validateAddress($address)
{
$is_group = false;
$addresses = array();
if ($address['group']) {
$is_group = true;
// Get the group part of the name
$parts = explode(':', $address['address']);
$groupname = $this->_splitCheck($parts, ':');
$structure = array();
// And validate the group part of the name.
if (!$this->_validatePhrase($groupname)){
$this->error = 'Group name did not validate.';
return false;
} else {
// Don't include groups if we are not nesting
// them. This avoids returning invalid addresses.
if ($this->nestGroups) {
$structure = new stdClass;
$structure->groupname = $groupname;
}
}
$address['address'] = ltrim(substr($address['address'], strlen($groupname . ':')));
}
// If a group then split on comma and put into an array.
// Otherwise, Just put the whole address in an array.
if ($is_group) {
while (strlen($address['address']) > 0) {
$parts = explode(',', $address['address']);
$addresses[] = $this->_splitCheck($parts, ',');
$address['address'] = trim(substr($address['address'], strlen(end($addresses) . ',')));
}
} else {
$addresses[] = $address['address'];
}
// Check that $addresses is set, if address like this:
// Groupname:;
// Then errors were appearing.
if (!count($addresses)){
$this->error = 'Empty group.';
return false;
}
// Trim the whitespace from all of the address strings.
array_map('trim', $addresses);
// Validate each mailbox.
// Format could be one of: name <geezer@domain.com>
// geezer@domain.com
// geezer
// ... or any other format valid by RFC 822.
for ($i = 0; $i < count($addresses); $i++) {
if (!$this->validateMailbox($addresses[$i])) {
if (empty($this->error)) {
$this->error = 'Validation failed for: ' . $addresses[$i];
}
return false;
}
}
// Nested format
if ($this->nestGroups) {
if ($is_group) {
$structure->addresses = $addresses;
} else {
$structure = $addresses[0];
}
// Flat format
} else {
if ($is_group) {
$structure = array_merge($structure, $addresses);
} else {
$structure = $addresses;
}
}
return $structure;
}
/**
* Function to validate a phrase.
*
* @access private
* @param string $phrase The phrase to check.
* @return boolean Success or failure.
*/
function _validatePhrase($phrase)
{
// Splits on one or more Tab or space.
$parts = preg_split('/[ \\x09]+/', $phrase, -1, PREG_SPLIT_NO_EMPTY);
$phrase_parts = array();
while (count($parts) > 0){
$phrase_parts[] = $this->_splitCheck($parts, ' ');
for ($i = 0; $i < $this->index + 1; $i++)
array_shift($parts);
}
foreach ($phrase_parts as $part) {
// If quoted string:
if (substr($part, 0, 1) == '"') {
if (!$this->_validateQuotedString($part)) {
return false;
}
continue;
}
// Otherwise it's an atom:
if (!$this->_validateAtom($part)) return false;
}
return true;
}
/**
* Function to validate an atom which from rfc822 is:
* atom = 1*<any CHAR except specials, SPACE and CTLs>
*
* If validation ($this->validate) has been turned off, then
* validateAtom() doesn't actually check anything. This is so that you
* can split a list of addresses up before encoding personal names
* (umlauts, etc.), for example.
*
* @access private
* @param string $atom The string to check.
* @return boolean Success or failure.
*/
function _validateAtom($atom)
{
if (!$this->validate) {
// Validation has been turned off; assume the atom is okay.
return true;
}
// Check for any char from ASCII 0 - ASCII 127
if (!preg_match('/^[\\x00-\\x7E]+$/i', $atom, $matches)) {
return false;
}
// Check for specials:
if (preg_match('/[][()<>@,;\\:". ]/', $atom)) {
return false;
}
// Check for control characters (ASCII 0-31):
if (preg_match('/[\\x00-\\x1F]+/', $atom)) {
return false;
}
return true;
}
/**
* Function to validate quoted string, which is:
* quoted-string = <"> *(qtext/quoted-pair) <">
*
* @access private
* @param string $qstring The string to check
* @return boolean Success or failure.
*/
function _validateQuotedString($qstring)
{
// Leading and trailing "
$qstring = substr($qstring, 1, -1);
// Perform check, removing quoted characters first.
return !preg_match('/[\x0D\\\\"]/', preg_replace('/\\\\./', '', $qstring));
}
/**
* Function to validate a mailbox, which is:
* mailbox = addr-spec ; simple address
* / phrase route-addr ; name and route-addr
*
* @access public
* @param string &$mailbox The string to check.
* @return boolean Success or failure.
*/
function validateMailbox(&$mailbox)
{
// A couple of defaults.
$phrase = '';
$comment = '';
$comments = array();
// Catch any RFC822 comments and store them separately.
$_mailbox = $mailbox;
while (strlen(trim($_mailbox)) > 0) {
$parts = explode('(', $_mailbox);
$before_comment = $this->_splitCheck($parts, '(');
if ($before_comment != $_mailbox) {
// First char should be a (.
$comment = substr(str_replace($before_comment, '', $_mailbox), 1);
$parts = explode(')', $comment);
$comment = $this->_splitCheck($parts, ')');
$comments[] = $comment;
// +2 is for the brackets
$_mailbox = substr($_mailbox, strpos($_mailbox, '('.$comment)+strlen($comment)+2);
} else {
break;
}
}
foreach ($comments as $comment) {
$mailbox = str_replace("($comment)", '', $mailbox);
}
$mailbox = trim($mailbox);
// Check for name + route-addr
if (substr($mailbox, -1) == '>' && substr($mailbox, 0, 1) != '<') {
$parts = explode('<', $mailbox);
$name = $this->_splitCheck($parts, '<');
$phrase = trim($name);
$route_addr = trim(substr($mailbox, strlen($name.'<'), -1));
if ($this->_validatePhrase($phrase) === false || ($route_addr = $this->_validateRouteAddr($route_addr)) === false) {
return false;
}
// Only got addr-spec
} else {
// First snip angle brackets if present.
if (substr($mailbox, 0, 1) == '<' && substr($mailbox, -1) == '>') {
$addr_spec = substr($mailbox, 1, -1);
} else {
$addr_spec = $mailbox;
}
if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
return false;
}
}
// Construct the object that will be returned.
$mbox = new stdClass();
// Add the phrase (even if empty) and comments
$mbox->personal = $phrase;
$mbox->comment = isset($comments) ? $comments : array();
if (isset($route_addr)) {
$mbox->mailbox = $route_addr['local_part'];
$mbox->host = $route_addr['domain'];
$route_addr['adl'] !== '' ? $mbox->adl = $route_addr['adl'] : '';
} else {
$mbox->mailbox = $addr_spec['local_part'];
$mbox->host = $addr_spec['domain'];
}
$mailbox = $mbox;
return true;
}
/**
* This function validates a route-addr which is:
* route-addr = "<" [route] addr-spec ">"
*
* Angle brackets have already been removed at the point of
* getting to this function.
*
* @access private
* @param string $route_addr The string to check.
* @return mixed False on failure, or an array containing validated address/route information on success.
*/
function _validateRouteAddr($route_addr)
{
// Check for colon.
if (strpos($route_addr, ':') !== false) {
$parts = explode(':', $route_addr);
$route = $this->_splitCheck($parts, ':');
} else {
$route = $route_addr;
}
// If $route is same as $route_addr then the colon was in
// quotes or brackets or, of course, non existent.
if ($route === $route_addr){
unset($route);
$addr_spec = $route_addr;
if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
return false;
}
} else {
// Validate route part.
if (($route = $this->_validateRoute($route)) === false) {
return false;
}
$addr_spec = substr($route_addr, strlen($route . ':'));
// Validate addr-spec part.
if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
return false;
}
}
if (isset($route)) {
$return['adl'] = $route;
} else {
$return['adl'] = '';
}
$return = array_merge($return, $addr_spec);
return $return;
}
/**
* Function to validate a route, which is:
* route = 1#("@" domain) ":"
*
* @access private
* @param string $route The string to check.
* @return mixed False on failure, or the validated $route on success.
*/
function _validateRoute($route)
{
// Split on comma.
$domains = explode(',', trim($route));
foreach ($domains as $domain) {
$domain = str_replace('@', '', trim($domain));
if (!$this->_validateDomain($domain)) return false;
}
return $route;
}
/**
* Function to validate a domain, though this is not quite what
* you expect of a strict internet domain.
*
* domain = sub-domain *("." sub-domain)
*
* @access private
* @param string $domain The string to check.
* @return mixed False on failure, or the validated domain on success.
*/
function _validateDomain($domain)
{
// Note the different use of $subdomains and $sub_domains
$subdomains = explode('.', $domain);
while (count($subdomains) > 0) {
$sub_domains[] = $this->_splitCheck($subdomains, '.');
for ($i = 0; $i < $this->index + 1; $i++)
array_shift($subdomains);
}
foreach ($sub_domains as $sub_domain) {
if (!$this->_validateSubdomain(trim($sub_domain)))
return false;
}
// Managed to get here, so return input.
return $domain;
}
/**
* Function to validate a subdomain:
* subdomain = domain-ref / domain-literal
*
* @access private
* @param string $subdomain The string to check.
* @return boolean Success or failure.
*/
function _validateSubdomain($subdomain)
{
if (preg_match('|^\[(.*)]$|', $subdomain, $arr)){
if (!$this->_validateDliteral($arr[1])) return false;
} else {
if (!$this->_validateAtom($subdomain)) return false;
}
// Got here, so return successful.
return true;
}
/**
* Function to validate a domain literal:
* domain-literal = "[" *(dtext / quoted-pair) "]"
*
* @access private
* @param string $dliteral The string to check.
* @return boolean Success or failure.
*/
function _validateDliteral($dliteral)
{
return !preg_match('/(.)[][\x0D\\\\]/', $dliteral, $matches) && $matches[1] != '\\';
}
/**
* Function to validate an addr-spec.
*
* addr-spec = local-part "@" domain
*
* @access private
* @param string $addr_spec The string to check.
* @return mixed False on failure, or the validated addr-spec on success.
*/
function _validateAddrSpec($addr_spec)
{
$addr_spec = trim($addr_spec);
// Split on @ sign if there is one.
if (strpos($addr_spec, '@') !== false) {
$parts = explode('@', $addr_spec);
$local_part = $this->_splitCheck($parts, '@');
$domain = substr($addr_spec, strlen($local_part . '@'));
// No @ sign so assume the default domain.
} else {
$local_part = $addr_spec;
$domain = $this->default_domain;
}
if (($local_part = $this->_validateLocalPart($local_part)) === false) return false;
if (($domain = $this->_validateDomain($domain)) === false) return false;
// Got here so return successful.
return array('local_part' => $local_part, 'domain' => $domain);
}
/**
* Function to validate the local part of an address:
* local-part = word *("." word)
*
* @access private
* @param string $local_part
* @return mixed False on failure, or the validated local part on success.
*/
function _validateLocalPart($local_part)
{
$parts = explode('.', $local_part);
$words = array();
// Split the local_part into words.
while (count($parts) > 0){
$words[] = $this->_splitCheck($parts, '.');
for ($i = 0; $i < $this->index + 1; $i++) {
array_shift($parts);
}
}
// Validate each word.
foreach ($words as $word) {
// If this word contains an unquoted space, it is invalid. (6.2.4)
if (strpos($word, ' ') && $word[0] !== '"')
{
return false;
}
if ($this->_validatePhrase(trim($word)) === false) return false;
}
// Managed to get here, so return the input.
return $local_part;
}
/**
* Returns an approximate count of how many addresses are in the
* given string. This is APPROXIMATE as it only splits based on a
* comma which has no preceding backslash. Could be useful as
* large amounts of addresses will end up producing *large*
* structures when used with parseAddressList().
*
* @param string $data Addresses to count
* @return int Approximate count
*/
function approximateCount($data)
{
return count(preg_split('/(?<!\\\\),/', $data));
}
/**
* This is a email validating function separate to the rest of the
* class. It simply validates whether an email is of the common
* internet form: <user>@<domain>. This can be sufficient for most
* people. Optional stricter mode can be utilised which restricts
* mailbox characters allowed to alphanumeric, full stop, hyphen
* and underscore.
*
* @param string $data Address to check
* @param boolean $strict Optional stricter mode
* @return mixed False if it fails, an indexed array
* username/domain if it matches
*/
function isValidInetAddress($data, $strict = false)
{
$regex = $strict ? '/^([.0-9a-z_+-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i' : '/^([*+!.&#$|\'\\%\/0-9a-z^_`{}=?~:-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i';
if (preg_match($regex, trim($data), $matches)) {
return array($matches[1], $matches[2]);
} else {
return false;
}
}
}

168
core/lib/Mail/mail.php Normal file
View File

@@ -0,0 +1,168 @@
<?php
/**
* internal PHP-mail() implementation of the PEAR Mail:: interface.
*
* PHP versions 4 and 5
*
* LICENSE:
*
* Copyright (c) 2010 Chuck Hagenbuch
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* o Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* o Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* o The names of the authors may not be used to endorse or promote
* products derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @category Mail
* @package Mail
* @author Chuck Hagenbuch <chuck@horde.org>
* @copyright 2010 Chuck Hagenbuch
* @license http://opensource.org/licenses/bsd-license.php New BSD License
* @version CVS: $Id: mail.php 294747 2010-02-08 08:18:33Z clockwerx $
* @link http://pear.php.net/package/Mail/
*/
/**
* internal PHP-mail() implementation of the PEAR Mail:: interface.
* @package Mail
* @version $Revision: 294747 $
*/
class Mail_mail extends Mail {
/**
* Any arguments to pass to the mail() function.
* @var string
*/
var $_params = '';
/**
* Constructor.
*
* Instantiates a new Mail_mail:: object based on the parameters
* passed in.
*
* @param array $params Extra arguments for the mail() function.
*/
function Mail_mail($params = null)
{
// The other mail implementations accept parameters as arrays.
// In the interest of being consistent, explode an array into
// a string of parameter arguments.
if (is_array($params)) {
$this->_params = join(' ', $params);
} else {
$this->_params = $params;
}
/* Because the mail() function may pass headers as command
* line arguments, we can't guarantee the use of the standard
* "\r\n" separator. Instead, we use the system's native line
* separator. */
if (defined('PHP_EOL')) {
$this->sep = PHP_EOL;
} else {
$this->sep = (strpos(PHP_OS, 'WIN') === false) ? "\n" : "\r\n";
}
}
/**
* Implements Mail_mail::send() function using php's built-in mail()
* command.
*
* @param mixed $recipients Either a comma-seperated list of recipients
* (RFC822 compliant), or an array of recipients,
* each RFC822 valid. This may contain recipients not
* specified in the headers, for Bcc:, resending
* messages, etc.
*
* @param array $headers The array of headers to send with the mail, in an
* associative array, where the array key is the
* header name (ie, 'Subject'), and the array value
* is the header value (ie, 'test'). The header
* produced from those values would be 'Subject:
* test'.
*
* @param string $body The full text of the message body, including any
* Mime parts, etc.
*
* @return mixed Returns true on success, or a PEAR_Error
* containing a descriptive error message on
* failure.
*
* @access public
*/
function send($recipients, $headers, $body)
{
if (!is_array($headers)) {
return PEAR::raiseError('$headers must be an array');
}
$result = $this->_sanitizeHeaders($headers);
if (is_a($result, 'PEAR_Error')) {
return $result;
}
// If we're passed an array of recipients, implode it.
if (is_array($recipients)) {
$recipients = implode(', ', $recipients);
}
// Get the Subject out of the headers array so that we can
// pass it as a seperate argument to mail().
$subject = '';
if (isset($headers['Subject'])) {
$subject = $headers['Subject'];
unset($headers['Subject']);
}
// Also remove the To: header. The mail() function will add its own
// To: header based on the contents of $recipients.
unset($headers['To']);
// Flatten the headers out.
$headerElements = $this->prepareHeaders($headers);
if (is_a($headerElements, 'PEAR_Error')) {
return $headerElements;
}
list(, $text_headers) = $headerElements;
// We only use mail()'s optional fifth parameter if the additional
// parameters have been provided and we're not running in safe mode.
if (empty($this->_params) || ini_get('safe_mode')) {
$result = mail($recipients, $subject, $body, $text_headers);
} else {
$result = mail($recipients, $subject, $body, $text_headers,
$this->_params);
}
// If the mail() function returned failure, we need to create a
// PEAR_Error object and return it instead of the boolean result.
if ($result === false) {
$result = PEAR::raiseError('mail() returned failure');
}
return $result;
}
}

143
core/lib/Mail/mock.php Normal file
View File

@@ -0,0 +1,143 @@
<?php
/**
* Mock implementation
*
* PHP versions 4 and 5
*
* LICENSE:
*
* Copyright (c) 2010 Chuck Hagenbuch
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* o Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* o Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* o The names of the authors may not be used to endorse or promote
* products derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @category Mail
* @package Mail
* @author Chuck Hagenbuch <chuck@horde.org>
* @copyright 2010 Chuck Hagenbuch
* @license http://opensource.org/licenses/bsd-license.php New BSD License
* @version CVS: $Id: mock.php 294747 2010-02-08 08:18:33Z clockwerx $
* @link http://pear.php.net/package/Mail/
*/
/**
* Mock implementation of the PEAR Mail:: interface for testing.
* @access public
* @package Mail
* @version $Revision: 294747 $
*/
class Mail_mock extends Mail {
/**
* Array of messages that have been sent with the mock.
*
* @var array
* @access public
*/
var $sentMessages = array();
/**
* Callback before sending mail.
*
* @var callback
*/
var $_preSendCallback;
/**
* Callback after sending mai.
*
* @var callback
*/
var $_postSendCallback;
/**
* Constructor.
*
* Instantiates a new Mail_mock:: object based on the parameters
* passed in. It looks for the following parameters, both optional:
* preSendCallback Called before an email would be sent.
* postSendCallback Called after an email would have been sent.
*
* @param array Hash containing any parameters.
* @access public
*/
function Mail_mock($params)
{
if (isset($params['preSendCallback']) &&
is_callable($params['preSendCallback'])) {
$this->_preSendCallback = $params['preSendCallback'];
}
if (isset($params['postSendCallback']) &&
is_callable($params['postSendCallback'])) {
$this->_postSendCallback = $params['postSendCallback'];
}
}
/**
* Implements Mail_mock::send() function. Silently discards all
* mail.
*
* @param mixed $recipients Either a comma-seperated list of recipients
* (RFC822 compliant), or an array of recipients,
* each RFC822 valid. This may contain recipients not
* specified in the headers, for Bcc:, resending
* messages, etc.
*
* @param array $headers The array of headers to send with the mail, in an
* associative array, where the array key is the
* header name (ie, 'Subject'), and the array value
* is the header value (ie, 'test'). The header
* produced from those values would be 'Subject:
* test'.
*
* @param string $body The full text of the message body, including any
* Mime parts, etc.
*
* @return mixed Returns true on success, or a PEAR_Error
* containing a descriptive error message on
* failure.
* @access public
*/
function send($recipients, $headers, $body)
{
if ($this->_preSendCallback) {
call_user_func_array($this->_preSendCallback,
array(&$this, $recipients, $headers, $body));
}
$entry = array('recipients' => $recipients, 'headers' => $headers, 'body' => $body);
$this->sentMessages[] = $entry;
if ($this->_postSendCallback) {
call_user_func_array($this->_postSendCallback,
array(&$this, $recipients, $headers, $body));
}
return true;
}
}

84
core/lib/Mail/null.php Normal file
View File

@@ -0,0 +1,84 @@
<?php
/**
* Null implementation of the PEAR Mail interface
*
* PHP versions 4 and 5
*
* LICENSE:
*
* Copyright (c) 2010 Phil Kernick
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* o Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* o Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* o The names of the authors may not be used to endorse or promote
* products derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @category Mail
* @package Mail
* @author Phil Kernick <philk@rotfl.com.au>
* @copyright 2010 Phil Kernick
* @license http://opensource.org/licenses/bsd-license.php New BSD License
* @version CVS: $Id: null.php 294747 2010-02-08 08:18:33Z clockwerx $
* @link http://pear.php.net/package/Mail/
*/
/**
* Null implementation of the PEAR Mail:: interface.
* @access public
* @package Mail
* @version $Revision: 294747 $
*/
class Mail_null extends Mail {
/**
* Implements Mail_null::send() function. Silently discards all
* mail.
*
* @param mixed $recipients Either a comma-seperated list of recipients
* (RFC822 compliant), or an array of recipients,
* each RFC822 valid. This may contain recipients not
* specified in the headers, for Bcc:, resending
* messages, etc.
*
* @param array $headers The array of headers to send with the mail, in an
* associative array, where the array key is the
* header name (ie, 'Subject'), and the array value
* is the header value (ie, 'test'). The header
* produced from those values would be 'Subject:
* test'.
*
* @param string $body The full text of the message body, including any
* Mime parts, etc.
*
* @return mixed Returns true on success, or a PEAR_Error
* containing a descriptive error message on
* failure.
* @access public
*/
function send($recipients, $headers, $body)
{
return true;
}
}

171
core/lib/Mail/sendmail.php Normal file
View File

@@ -0,0 +1,171 @@
<?php
//
// +----------------------------------------------------------------------+
// | PHP Version 4 |
// +----------------------------------------------------------------------+
// | Copyright (c) 1997-2003 The PHP Group |
// +----------------------------------------------------------------------+
// | This source file is subject to version 2.02 of the PHP license, |
// | that is bundled with this package in the file LICENSE, and is |
// | available at through the world-wide-web at |
// | http://www.php.net/license/2_02.txt. |
// | If you did not receive a copy of the PHP license and are unable to |
// | obtain it through the world-wide-web, please send a note to |
// | license@php.net so we can mail you a copy immediately. |
// +----------------------------------------------------------------------+
// | Author: Chuck Hagenbuch <chuck@horde.org> |
// +----------------------------------------------------------------------+
/**
* Sendmail implementation of the PEAR Mail:: interface.
* @access public
* @package Mail
* @version $Revision: 294744 $
*/
class Mail_sendmail extends Mail {
/**
* The location of the sendmail or sendmail wrapper binary on the
* filesystem.
* @var string
*/
var $sendmail_path = '/usr/sbin/sendmail';
/**
* Any extra command-line parameters to pass to the sendmail or
* sendmail wrapper binary.
* @var string
*/
var $sendmail_args = '-i';
/**
* Constructor.
*
* Instantiates a new Mail_sendmail:: object based on the parameters
* passed in. It looks for the following parameters:
* sendmail_path The location of the sendmail binary on the
* filesystem. Defaults to '/usr/sbin/sendmail'.
*
* sendmail_args Any extra parameters to pass to the sendmail
* or sendmail wrapper binary.
*
* If a parameter is present in the $params array, it replaces the
* default.
*
* @param array $params Hash containing any parameters different from the
* defaults.
* @access public
*/
function Mail_sendmail($params)
{
if (isset($params['sendmail_path'])) {
$this->sendmail_path = $params['sendmail_path'];
}
if (isset($params['sendmail_args'])) {
$this->sendmail_args = $params['sendmail_args'];
}
/*
* Because we need to pass message headers to the sendmail program on
* the commandline, we can't guarantee the use of the standard "\r\n"
* separator. Instead, we use the system's native line separator.
*/
if (defined('PHP_EOL')) {
$this->sep = PHP_EOL;
} else {
$this->sep = (strpos(PHP_OS, 'WIN') === false) ? "\n" : "\r\n";
}
}
/**
* Implements Mail::send() function using the sendmail
* command-line binary.
*
* @param mixed $recipients Either a comma-seperated list of recipients
* (RFC822 compliant), or an array of recipients,
* each RFC822 valid. This may contain recipients not
* specified in the headers, for Bcc:, resending
* messages, etc.
*
* @param array $headers The array of headers to send with the mail, in an
* associative array, where the array key is the
* header name (ie, 'Subject'), and the array value
* is the header value (ie, 'test'). The header
* produced from those values would be 'Subject:
* test'.
*
* @param string $body The full text of the message body, including any
* Mime parts, etc.
*
* @return mixed Returns true on success, or a PEAR_Error
* containing a descriptive error message on
* failure.
* @access public
*/
function send($recipients, $headers, $body)
{
if (!is_array($headers)) {
return PEAR::raiseError('$headers must be an array');
}
$result = $this->_sanitizeHeaders($headers);
if (is_a($result, 'PEAR_Error')) {
return $result;
}
$recipients = $this->parseRecipients($recipients);
if (is_a($recipients, 'PEAR_Error')) {
return $recipients;
}
$recipients = implode(' ', array_map('escapeshellarg', $recipients));
$headerElements = $this->prepareHeaders($headers);
if (is_a($headerElements, 'PEAR_Error')) {
return $headerElements;
}
list($from, $text_headers) = $headerElements;
/* Since few MTAs are going to allow this header to be forged
* unless it's in the MAIL FROM: exchange, we'll use
* Return-Path instead of From: if it's set. */
if (!empty($headers['Return-Path'])) {
$from = $headers['Return-Path'];
}
if (!isset($from)) {
return PEAR::raiseError('No from address given.');
} elseif (strpos($from, ' ') !== false ||
strpos($from, ';') !== false ||
strpos($from, '&') !== false ||
strpos($from, '`') !== false) {
return PEAR::raiseError('From address specified with dangerous characters.');
}
$from = escapeshellarg($from); // Security bug #16200
$mail = @popen($this->sendmail_path . (!empty($this->sendmail_args) ? ' ' . $this->sendmail_args : '') . " -f$from -- $recipients", 'w');
if (!$mail) {
return PEAR::raiseError('Failed to open sendmail [' . $this->sendmail_path . '] for execution.');
}
// Write the headers following by two newlines: one to end the headers
// section and a second to separate the headers block from the body.
fputs($mail, $text_headers . $this->sep . $this->sep);
fputs($mail, $body);
$result = pclose($mail);
if (version_compare(phpversion(), '4.2.3') == -1) {
// With older php versions, we need to shift the pclose
// result to get the exit code.
$result = $result >> 8 & 0xFF;
}
if ($result != 0) {
return PEAR::raiseError('sendmail returned error code ' . $result,
$result);
}
return true;
}
}

444
core/lib/Mail/smtp.php Normal file
View File

@@ -0,0 +1,444 @@
<?php
/**
* SMTP implementation of the PEAR Mail interface. Requires the Net_SMTP class.
*
* PHP versions 4 and 5
*
* LICENSE:
*
* Copyright (c) 2010, Chuck Hagenbuch
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* o Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* o Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* o The names of the authors may not be used to endorse or promote
* products derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @category HTTP
* @package HTTP_Request
* @author Jon Parise <jon@php.net>
* @author Chuck Hagenbuch <chuck@horde.org>
* @copyright 2010 Chuck Hagenbuch
* @license http://opensource.org/licenses/bsd-license.php New BSD License
* @version CVS: $Id: smtp.php 294747 2010-02-08 08:18:33Z clockwerx $
* @link http://pear.php.net/package/Mail/
*/
/** Error: Failed to create a Net_SMTP object */
define('PEAR_MAIL_SMTP_ERROR_CREATE', 10000);
/** Error: Failed to connect to SMTP server */
define('PEAR_MAIL_SMTP_ERROR_CONNECT', 10001);
/** Error: SMTP authentication failure */
define('PEAR_MAIL_SMTP_ERROR_AUTH', 10002);
/** Error: No From: address has been provided */
define('PEAR_MAIL_SMTP_ERROR_FROM', 10003);
/** Error: Failed to set sender */
define('PEAR_MAIL_SMTP_ERROR_SENDER', 10004);
/** Error: Failed to add recipient */
define('PEAR_MAIL_SMTP_ERROR_RECIPIENT', 10005);
/** Error: Failed to send data */
define('PEAR_MAIL_SMTP_ERROR_DATA', 10006);
/**
* SMTP implementation of the PEAR Mail interface. Requires the Net_SMTP class.
* @access public
* @package Mail
* @version $Revision: 294747 $
*/
class Mail_smtp extends Mail {
/**
* SMTP connection object.
*
* @var object
* @access private
*/
var $_smtp = null;
/**
* The list of service extension parameters to pass to the Net_SMTP
* mailFrom() command.
* @var array
*/
var $_extparams = array();
/**
* The SMTP host to connect to.
* @var string
*/
var $host = 'localhost';
/**
* The port the SMTP server is on.
* @var integer
*/
var $port = 25;
/**
* Should SMTP authentication be used?
*
* This value may be set to true, false or the name of a specific
* authentication method.
*
* If the value is set to true, the Net_SMTP package will attempt to use
* the best authentication method advertised by the remote SMTP server.
*
* @var mixed
*/
var $auth = false;
/**
* The username to use if the SMTP server requires authentication.
* @var string
*/
var $username = '';
/**
* The password to use if the SMTP server requires authentication.
* @var string
*/
var $password = '';
/**
* Hostname or domain that will be sent to the remote SMTP server in the
* HELO / EHLO message.
*
* @var string
*/
var $localhost = 'localhost';
/**
* SMTP connection timeout value. NULL indicates no timeout.
*
* @var integer
*/
var $timeout = null;
/**
* Turn on Net_SMTP debugging?
*
* @var boolean $debug
*/
var $debug = false;
/**
* Indicates whether or not the SMTP connection should persist over
* multiple calls to the send() method.
*
* @var boolean
*/
var $persist = false;
/**
* Use SMTP command pipelining (specified in RFC 2920) if the SMTP server
* supports it. This speeds up delivery over high-latency connections. By
* default, use the default value supplied by Net_SMTP.
* @var bool
*/
var $pipelining;
/**
* Constructor.
*
* Instantiates a new Mail_smtp:: object based on the parameters
* passed in. It looks for the following parameters:
* host The server to connect to. Defaults to localhost.
* port The port to connect to. Defaults to 25.
* auth SMTP authentication. Defaults to none.
* username The username to use for SMTP auth. No default.
* password The password to use for SMTP auth. No default.
* localhost The local hostname / domain. Defaults to localhost.
* timeout The SMTP connection timeout. Defaults to none.
* verp Whether to use VERP or not. Defaults to false.
* DEPRECATED as of 1.2.0 (use setMailParams()).
* debug Activate SMTP debug mode? Defaults to false.
* persist Should the SMTP connection persist?
* pipelining Use SMTP command pipelining
*
* If a parameter is present in the $params array, it replaces the
* default.
*
* @param array Hash containing any parameters different from the
* defaults.
* @access public
*/
function Mail_smtp($params)
{
if (isset($params['host'])) $this->host = $params['host'];
if (isset($params['port'])) $this->port = $params['port'];
if (isset($params['auth'])) $this->auth = $params['auth'];
if (isset($params['username'])) $this->username = $params['username'];
if (isset($params['password'])) $this->password = $params['password'];
if (isset($params['localhost'])) $this->localhost = $params['localhost'];
if (isset($params['timeout'])) $this->timeout = $params['timeout'];
if (isset($params['debug'])) $this->debug = (bool)$params['debug'];
if (isset($params['persist'])) $this->persist = (bool)$params['persist'];
if (isset($params['pipelining'])) $this->pipelining = (bool)$params['pipelining'];
// Deprecated options
if (isset($params['verp'])) {
$this->addServiceExtensionParameter('XVERP', is_bool($params['verp']) ? null : $params['verp']);
}
register_shutdown_function(array(&$this, '_Mail_smtp'));
}
/**
* Destructor implementation to ensure that we disconnect from any
* potentially-alive persistent SMTP connections.
*/
function _Mail_smtp()
{
$this->disconnect();
}
/**
* Implements Mail::send() function using SMTP.
*
* @param mixed $recipients Either a comma-seperated list of recipients
* (RFC822 compliant), or an array of recipients,
* each RFC822 valid. This may contain recipients not
* specified in the headers, for Bcc:, resending
* messages, etc.
*
* @param array $headers The array of headers to send with the mail, in an
* associative array, where the array key is the
* header name (e.g., 'Subject'), and the array value
* is the header value (e.g., 'test'). The header
* produced from those values would be 'Subject:
* test'.
*
* @param string $body The full text of the message body, including any
* MIME parts, etc.
*
* @return mixed Returns true on success, or a PEAR_Error
* containing a descriptive error message on
* failure.
* @access public
*/
function send($recipients, $headers, $body)
{
/* If we don't already have an SMTP object, create one. */
$result = &$this->getSMTPObject();
if (PEAR::isError($result)) {
return $result;
}
if (!is_array($headers)) {
return PEAR::raiseError('$headers must be an array');
}
$this->_sanitizeHeaders($headers);
$headerElements = $this->prepareHeaders($headers);
if (is_a($headerElements, 'PEAR_Error')) {
$this->_smtp->rset();
return $headerElements;
}
list($from, $textHeaders) = $headerElements;
/* Since few MTAs are going to allow this header to be forged
* unless it's in the MAIL FROM: exchange, we'll use
* Return-Path instead of From: if it's set. */
if (!empty($headers['Return-Path'])) {
$from = $headers['Return-Path'];
}
if (!isset($from)) {
$this->_smtp->rset();
return PEAR::raiseError('No From: address has been provided',
PEAR_MAIL_SMTP_ERROR_FROM);
}
$params = null;
if (!empty($this->_extparams)) {
foreach ($this->_extparams as $key => $val) {
$params .= ' ' . $key . (is_null($val) ? '' : '=' . $val);
}
}
if (PEAR::isError($res = $this->_smtp->mailFrom($from, ltrim($params)))) {
$error = $this->_error("Failed to set sender: $from", $res);
$this->_smtp->rset();
return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_SENDER);
}
$recipients = $this->parseRecipients($recipients);
if (is_a($recipients, 'PEAR_Error')) {
$this->_smtp->rset();
return $recipients;
}
foreach ($recipients as $recipient) {
$res = $this->_smtp->rcptTo($recipient);
if (is_a($res, 'PEAR_Error')) {
$error = $this->_error("Failed to add recipient: $recipient", $res);
$this->_smtp->rset();
return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_RECIPIENT);
}
}
/* Send the message's headers and the body as SMTP data. */
$res = $this->_smtp->data($textHeaders . "\r\n\r\n" . $body);
list(,$args) = $this->_smtp->getResponse();
if (preg_match("/Ok: queued as (.*)/", $args, $queued)) {
$this->queued_as = $queued[1];
}
/* we need the greeting; from it we can extract the authorative name of the mail server we've really connected to.
* ideal if we're connecting to a round-robin of relay servers and need to track which exact one took the email */
$this->greeting = $this->_smtp->getGreeting();
if (is_a($res, 'PEAR_Error')) {
$error = $this->_error('Failed to send data', $res);
$this->_smtp->rset();
return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_DATA);
}
/* If persistent connections are disabled, destroy our SMTP object. */
if ($this->persist === false) {
$this->disconnect();
}
return true;
}
/**
* Connect to the SMTP server by instantiating a Net_SMTP object.
*
* @return mixed Returns a reference to the Net_SMTP object on success, or
* a PEAR_Error containing a descriptive error message on
* failure.
*
* @since 1.2.0
* @access public
*/
function &getSMTPObject()
{
if (is_object($this->_smtp) !== false) {
return $this->_smtp;
}
include_once 'Net/SMTP.php';
$this->_smtp = &new Net_SMTP($this->host,
$this->port,
$this->localhost);
/* If we still don't have an SMTP object at this point, fail. */
if (is_object($this->_smtp) === false) {
return PEAR::raiseError('Failed to create a Net_SMTP object',
PEAR_MAIL_SMTP_ERROR_CREATE);
}
/* Configure the SMTP connection. */
if ($this->debug) {
$this->_smtp->setDebug(true);
}
/* Attempt to connect to the configured SMTP server. */
if (PEAR::isError($res = $this->_smtp->connect($this->timeout))) {
$error = $this->_error('Failed to connect to ' .
$this->host . ':' . $this->port,
$res);
return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_CONNECT);
}
/* Attempt to authenticate if authentication has been enabled. */
if ($this->auth) {
$method = is_string($this->auth) ? $this->auth : '';
if (PEAR::isError($res = $this->_smtp->auth($this->username,
$this->password,
$method))) {
$error = $this->_error("$method authentication failure",
$res);
$this->_smtp->rset();
return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_AUTH);
}
}
return $this->_smtp;
}
/**
* Add parameter associated with a SMTP service extension.
*
* @param string Extension keyword.
* @param string Any value the keyword needs.
*
* @since 1.2.0
* @access public
*/
function addServiceExtensionParameter($keyword, $value = null)
{
$this->_extparams[$keyword] = $value;
}
/**
* Disconnect and destroy the current SMTP connection.
*
* @return boolean True if the SMTP connection no longer exists.
*
* @since 1.1.9
* @access public
*/
function disconnect()
{
/* If we have an SMTP object, disconnect and destroy it. */
if (is_object($this->_smtp) && $this->_smtp->disconnect()) {
$this->_smtp = null;
}
/* We are disconnected if we no longer have an SMTP object. */
return ($this->_smtp === null);
}
/**
* Build a standardized string describing the current SMTP error.
*
* @param string $text Custom string describing the error context.
* @param object $error Reference to the current PEAR_Error object.
*
* @return string A string describing the current SMTP error.
*
* @since 1.1.7
* @access private
*/
function _error($text, &$error)
{
/* Split the SMTP response into a code and a response string. */
list($code, $response) = $this->_smtp->getResponse();
/* Build our standardized error string. */
return $text
. ' [SMTP: ' . $error->getMessage()
. " (code: $code, response: $response)]";
}
}

502
core/lib/Mail/smtpmx.php Normal file
View File

@@ -0,0 +1,502 @@
<?PHP
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
/**
* SMTP MX
*
* SMTP MX implementation of the PEAR Mail interface. Requires the Net_SMTP class.
*
* PHP versions 4 and 5
*
* LICENSE:
*
* Copyright (c) 2010, gERD Schaufelberger
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* o Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* o Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* o The names of the authors may not be used to endorse or promote
* products derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @category Mail
* @package Mail_smtpmx
* @author gERD Schaufelberger <gerd@php-tools.net>
* @copyright 2010 gERD Schaufelberger
* @license http://opensource.org/licenses/bsd-license.php New BSD License
* @version CVS: $Id: smtpmx.php 294747 2010-02-08 08:18:33Z clockwerx $
* @link http://pear.php.net/package/Mail/
*/
require_once 'Net/SMTP.php';
/**
* SMTP MX implementation of the PEAR Mail interface. Requires the Net_SMTP class.
*
*
* @access public
* @author gERD Schaufelberger <gerd@php-tools.net>
* @package Mail
* @version $Revision: 294747 $
*/
class Mail_smtpmx extends Mail {
/**
* SMTP connection object.
*
* @var object
* @access private
*/
var $_smtp = null;
/**
* The port the SMTP server is on.
* @var integer
* @see getservicebyname()
*/
var $port = 25;
/**
* Hostname or domain that will be sent to the remote SMTP server in the
* HELO / EHLO message.
*
* @var string
* @see posix_uname()
*/
var $mailname = 'localhost';
/**
* SMTP connection timeout value. NULL indicates no timeout.
*
* @var integer
*/
var $timeout = 10;
/**
* use either PEAR:Net_DNS or getmxrr
*
* @var boolean
*/
var $withNetDns = true;
/**
* PEAR:Net_DNS_Resolver
*
* @var object
*/
var $resolver;
/**
* Whether to use VERP or not. If not a boolean, the string value
* will be used as the VERP separators.
*
* @var mixed boolean or string
*/
var $verp = false;
/**
* Whether to use VRFY or not.
*
* @var boolean $vrfy
*/
var $vrfy = false;
/**
* Switch to test mode - don't send emails for real
*
* @var boolean $debug
*/
var $test = false;
/**
* Turn on Net_SMTP debugging?
*
* @var boolean $peardebug
*/
var $debug = false;
/**
* internal error codes
*
* translate internal error identifier to PEAR-Error codes and human
* readable messages.
*
* @var boolean $debug
* @todo as I need unique error-codes to identify what exactly went wrond
* I did not use intergers as it should be. Instead I added a "namespace"
* for each code. This avoids conflicts with error codes from different
* classes. How can I use unique error codes and stay conform with PEAR?
*/
var $errorCode = array(
'not_connected' => array(
'code' => 1,
'msg' => 'Could not connect to any mail server ({HOST}) at port {PORT} to send mail to {RCPT}.'
),
'failed_vrfy_rcpt' => array(
'code' => 2,
'msg' => 'Recipient "{RCPT}" could not be veryfied.'
),
'failed_set_from' => array(
'code' => 3,
'msg' => 'Failed to set sender: {FROM}.'
),
'failed_set_rcpt' => array(
'code' => 4,
'msg' => 'Failed to set recipient: {RCPT}.'
),
'failed_send_data' => array(
'code' => 5,
'msg' => 'Failed to send mail to: {RCPT}.'
),
'no_from' => array(
'code' => 5,
'msg' => 'No from address has be provided.'
),
'send_data' => array(
'code' => 7,
'msg' => 'Failed to create Net_SMTP object.'
),
'no_mx' => array(
'code' => 8,
'msg' => 'No MX-record for {RCPT} found.'
),
'no_resolver' => array(
'code' => 9,
'msg' => 'Could not start resolver! Install PEAR:Net_DNS or switch off "netdns"'
),
'failed_rset' => array(
'code' => 10,
'msg' => 'RSET command failed, SMTP-connection corrupt.'
),
);
/**
* Constructor.
*
* Instantiates a new Mail_smtp:: object based on the parameters
* passed in. It looks for the following parameters:
* mailname The name of the local mail system (a valid hostname which matches the reverse lookup)
* port smtp-port - the default comes from getservicebyname() and should work fine
* timeout The SMTP connection timeout. Defaults to 30 seconds.
* vrfy Whether to use VRFY or not. Defaults to false.
* verp Whether to use VERP or not. Defaults to false.
* test Activate test mode? Defaults to false.
* debug Activate SMTP and Net_DNS debug mode? Defaults to false.
* netdns whether to use PEAR:Net_DNS or the PHP build in function getmxrr, default is true
*
* If a parameter is present in the $params array, it replaces the
* default.
*
* @access public
* @param array Hash containing any parameters different from the
* defaults.
* @see _Mail_smtpmx()
*/
function __construct($params)
{
if (isset($params['mailname'])) {
$this->mailname = $params['mailname'];
} else {
// try to find a valid mailname
if (function_exists('posix_uname')) {
$uname = posix_uname();
$this->mailname = $uname['nodename'];
}
}
// port number
if (isset($params['port'])) {
$this->_port = $params['port'];
} else {
$this->_port = getservbyname('smtp', 'tcp');
}
if (isset($params['timeout'])) $this->timeout = $params['timeout'];
if (isset($params['verp'])) $this->verp = $params['verp'];
if (isset($params['test'])) $this->test = $params['test'];
if (isset($params['peardebug'])) $this->test = $params['peardebug'];
if (isset($params['netdns'])) $this->withNetDns = $params['netdns'];
}
/**
* Constructor wrapper for PHP4
*
* @access public
* @param array Hash containing any parameters different from the defaults
* @see __construct()
*/
function Mail_smtpmx($params)
{
$this->__construct($params);
register_shutdown_function(array(&$this, '__destruct'));
}
/**
* Destructor implementation to ensure that we disconnect from any
* potentially-alive persistent SMTP connections.
*/
function __destruct()
{
if (is_object($this->_smtp)) {
$this->_smtp->disconnect();
$this->_smtp = null;
}
}
/**
* Implements Mail::send() function using SMTP direct delivery
*
* @access public
* @param mixed $recipients in RFC822 style or array
* @param array $headers The array of headers to send with the mail.
* @param string $body The full text of the message body,
* @return mixed Returns true on success, or a PEAR_Error
*/
function send($recipients, $headers, $body)
{
if (!is_array($headers)) {
return PEAR::raiseError('$headers must be an array');
}
$result = $this->_sanitizeHeaders($headers);
if (is_a($result, 'PEAR_Error')) {
return $result;
}
// Prepare headers
$headerElements = $this->prepareHeaders($headers);
if (is_a($headerElements, 'PEAR_Error')) {
return $headerElements;
}
list($from, $textHeaders) = $headerElements;
// use 'Return-Path' if possible
if (!empty($headers['Return-Path'])) {
$from = $headers['Return-Path'];
}
if (!isset($from)) {
return $this->_raiseError('no_from');
}
// Prepare recipients
$recipients = $this->parseRecipients($recipients);
if (is_a($recipients, 'PEAR_Error')) {
return $recipients;
}
foreach ($recipients as $rcpt) {
list($user, $host) = explode('@', $rcpt);
$mx = $this->_getMx($host);
if (is_a($mx, 'PEAR_Error')) {
return $mx;
}
if (empty($mx)) {
$info = array('rcpt' => $rcpt);
return $this->_raiseError('no_mx', $info);
}
$connected = false;
foreach ($mx as $mserver => $mpriority) {
$this->_smtp = new Net_SMTP($mserver, $this->port, $this->mailname);
// configure the SMTP connection.
if ($this->debug) {
$this->_smtp->setDebug(true);
}
// attempt to connect to the configured SMTP server.
$res = $this->_smtp->connect($this->timeout);
if (is_a($res, 'PEAR_Error')) {
$this->_smtp = null;
continue;
}
// connection established
if ($res) {
$connected = true;
break;
}
}
if (!$connected) {
$info = array(
'host' => implode(', ', array_keys($mx)),
'port' => $this->port,
'rcpt' => $rcpt,
);
return $this->_raiseError('not_connected', $info);
}
// Verify recipient
if ($this->vrfy) {
$res = $this->_smtp->vrfy($rcpt);
if (is_a($res, 'PEAR_Error')) {
$info = array('rcpt' => $rcpt);
return $this->_raiseError('failed_vrfy_rcpt', $info);
}
}
// mail from:
$args['verp'] = $this->verp;
$res = $this->_smtp->mailFrom($from, $args);
if (is_a($res, 'PEAR_Error')) {
$info = array('from' => $from);
return $this->_raiseError('failed_set_from', $info);
}
// rcpt to:
$res = $this->_smtp->rcptTo($rcpt);
if (is_a($res, 'PEAR_Error')) {
$info = array('rcpt' => $rcpt);
return $this->_raiseError('failed_set_rcpt', $info);
}
// Don't send anything in test mode
if ($this->test) {
$result = $this->_smtp->rset();
$res = $this->_smtp->rset();
if (is_a($res, 'PEAR_Error')) {
return $this->_raiseError('failed_rset');
}
$this->_smtp->disconnect();
$this->_smtp = null;
return true;
}
// Send data
$res = $this->_smtp->data("$textHeaders\r\n$body");
if (is_a($res, 'PEAR_Error')) {
$info = array('rcpt' => $rcpt);
return $this->_raiseError('failed_send_data', $info);
}
$this->_smtp->disconnect();
$this->_smtp = null;
}
return true;
}
/**
* Recieve mx rexords for a spciefied host
*
* The MX records
*
* @access private
* @param string $host mail host
* @return mixed sorted
*/
function _getMx($host)
{
$mx = array();
if ($this->withNetDns) {
$res = $this->_loadNetDns();
if (is_a($res, 'PEAR_Error')) {
return $res;
}
$response = $this->resolver->query($host, 'MX');
if (!$response) {
return false;
}
foreach ($response->answer as $rr) {
if ($rr->type == 'MX') {
$mx[$rr->exchange] = $rr->preference;
}
}
} else {
$mxHost = array();
$mxWeight = array();
if (!getmxrr($host, $mxHost, $mxWeight)) {
return false;
}
for ($i = 0; $i < count($mxHost); ++$i) {
$mx[$mxHost[$i]] = $mxWeight[$i];
}
}
asort($mx);
return $mx;
}
/**
* initialize PEAR:Net_DNS_Resolver
*
* @access private
* @return boolean true on success
*/
function _loadNetDns()
{
if (is_object($this->resolver)) {
return true;
}
if (!include_once 'Net/DNS.php') {
return $this->_raiseError('no_resolver');
}
$this->resolver = new Net_DNS_Resolver();
if ($this->debug) {
$this->resolver->test = 1;
}
return true;
}
/**
* raise standardized error
*
* include additional information in error message
*
* @access private
* @param string $id maps error ids to codes and message
* @param array $info optional information in associative array
* @see _errorCode
*/
function _raiseError($id, $info = array())
{
$code = $this->errorCode[$id]['code'];
$msg = $this->errorCode[$id]['msg'];
// include info to messages
if (!empty($info)) {
$search = array();
$replace = array();
foreach ($info as $key => $value) {
array_push($search, '{' . strtoupper($key) . '}');
array_push($replace, $value);
}
$msg = str_replace($search, $replace, $msg);
}
return PEAR::raiseError($msg, $code);
}
}

View File

@@ -7,7 +7,10 @@
"filp/whoops": "~2.1",
"swiftmailer/swiftmailer": "^6.0",
"pear/net_smtp": "^1.7",
"pear/mail": "^1.4"
"pear/mail": "^1.4",
"spomky-labs/base64url": "^1.0",
"cebe/markdown": "^1.2",
"neitanod/forceutf8": "^2.0"
},
"require-dev": {
"phpunit/phpunit": "~6"

View File

@@ -4,9 +4,68 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "3a99d8f1889b03e3d3d346d1c53e284a",
"content-hash": "0d37f31f02f0af3f426b668e57096b14",
"content-hash": "45cdd8adec569a3ef6dfed9c8b1fab41",
"packages": [
{
"name": "cebe/markdown",
"version": "1.2.1",
"source": {
"type": "git",
"url": "https://github.com/cebe/markdown.git",
"reference": "9bac5e971dd391e2802dca5400bbeacbaea9eb86"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cebe/markdown/zipball/9bac5e971dd391e2802dca5400bbeacbaea9eb86",
"reference": "9bac5e971dd391e2802dca5400bbeacbaea9eb86",
"shasum": ""
},
"require": {
"lib-pcre": "*",
"php": ">=5.4.0"
},
"require-dev": {
"cebe/indent": "*",
"facebook/xhprof": "*@dev",
"phpunit/phpunit": "4.1.*"
},
"bin": [
"bin/markdown"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.2.x-dev"
}
},
"autoload": {
"psr-4": {
"cebe\\markdown\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Carsten Brandt",
"email": "mail@cebe.cc",
"homepage": "http://cebe.cc/",
"role": "Creator"
}
],
"description": "A super fast, highly extensible markdown parser for PHP",
"homepage": "https://github.com/cebe/markdown#readme",
"keywords": [
"extensible",
"fast",
"gfm",
"markdown",
"markdown-extra"
],
"time": "2018-03-26T11:24:36+00:00"
},
{
"name": "consolidation/annotated-command",
"version": "2.8.2",
@@ -56,7 +115,7 @@
}
],
"description": "Initialize Symfony Console commands from annotated command class methods.",
"time": "2017-11-29 16:23:23"
"time": "2017-11-29T16:23:23+00:00"
},
{
"name": "consolidation/config",
@@ -110,7 +169,7 @@
}
],
"description": "Provide configuration services for a commandline tool.",
"time": "2017-12-22 17:28:19"
"time": "2017-12-22T17:28:19+00:00"
},
{
"name": "consolidation/log",
@@ -158,7 +217,7 @@
}
],
"description": "Improved Psr-3 / Psr\\Log logger based on Symfony Console components.",
"time": "2017-11-29 01:44:16"
"time": "2017-11-29T01:44:16+00:00"
},
{
"name": "consolidation/output-formatters",
@@ -207,7 +266,7 @@
}
],
"description": "Format text by applying transformations provided by plug-in formatters.",
"time": "2017-11-29 15:25:38"
"time": "2017-11-29T15:25:38+00:00"
},
{
"name": "consolidation/robo",
@@ -284,7 +343,7 @@
}
],
"description": "Modern task runner",
"time": "2017-12-29 06:48:35"
"time": "2017-12-29T06:48:35+00:00"
},
{
"name": "container-interop/container-interop",
@@ -315,7 +374,7 @@
],
"description": "Promoting the interoperability of container objects (DIC, SL, etc.)",
"homepage": "https://github.com/container-interop/container-interop",
"time": "2017-02-14 19:40:03"
"time": "2017-02-14T19:40:03+00:00"
},
{
"name": "dflydev/dot-access-data",
@@ -374,7 +433,7 @@
"dot",
"notation"
],
"time": "2017-01-20 21:14:22"
"time": "2017-01-20T21:14:22+00:00"
},
{
"name": "doctrine/lexer",
@@ -428,7 +487,7 @@
"lexer",
"parser"
],
"time": "2014-09-09 13:34:57"
"time": "2014-09-09T13:34:57+00:00"
},
{
"name": "egulias/email-validator",
@@ -485,7 +544,7 @@
"validation",
"validator"
],
"time": "2017-11-15 23:40:40"
"time": "2017-11-15T23:40:40+00:00"
},
{
"name": "filp/whoops",
@@ -546,7 +605,7 @@
"throwable",
"whoops"
],
"time": "2017-11-23 18:22:44"
"time": "2017-11-23T18:22:44+00:00"
},
{
"name": "gettext/gettext",
@@ -606,7 +665,7 @@
"po",
"translation"
],
"time": "2016-06-15 18:14:14"
"time": "2016-06-15T18:14:14+00:00"
},
{
"name": "gettext/languages",
@@ -667,7 +726,7 @@
"translations",
"unicode"
],
"time": "2017-03-23 17:02:28"
"time": "2017-03-23T17:02:28+00:00"
},
{
"name": "grasmash/expander",
@@ -714,7 +773,7 @@
}
],
"description": "Expands internal property references in PHP arrays file.",
"time": "2017-12-21 22:14:55"
"time": "2017-12-21T22:14:55+00:00"
},
{
"name": "grasmash/yaml-expander",
@@ -762,7 +821,7 @@
}
],
"description": "Expands internal property references in a yaml file.",
"time": "2017-12-16 16:06:03"
"time": "2017-12-16T16:06:03+00:00"
},
{
"name": "league/container",
@@ -827,7 +886,7 @@
"provider",
"service"
],
"time": "2017-05-10 09:20:27"
"time": "2017-05-10T09:20:27+00:00"
},
{
"name": "monolog/monolog",
@@ -900,7 +959,41 @@
"logging",
"psr-3"
],
"time": "2015-03-09 09:58:04"
"time": "2015-03-09T09:58:04+00:00"
},
{
"name": "neitanod/forceutf8",
"version": "v2.0.1",
"source": {
"type": "git",
"url": "https://github.com/neitanod/forceutf8.git",
"reference": "47c883ab2739e7938a8bb0bfd1c29d48c88858de"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/neitanod/forceutf8/zipball/47c883ab2739e7938a8bb0bfd1c29d48c88858de",
"reference": "47c883ab2739e7938a8bb0bfd1c29d48c88858de",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"autoload": {
"psr-0": {
"ForceUTF8\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"authors": [
{
"name": "Sebastián Grignoli",
"email": "grignoli@gmail.com"
}
],
"description": "PHP Class Encoding featuring popular Encoding::toUTF8() function --formerly known as forceUTF8()-- that fixes mixed encoded strings.",
"homepage": "https://github.com/neitanod/forceutf8",
"time": "2017-05-22T18:50:57+00:00"
},
{
"name": "pear/console_getopt",
@@ -947,7 +1040,7 @@
}
],
"description": "More info available on: http://pear.php.net/package/Console_Getopt",
"time": "2015-07-20 20:28:12"
"time": "2015-07-20T20:28:12+00:00"
},
{
"name": "pear/mail",
@@ -1005,7 +1098,7 @@
],
"description": "Class that provides multiple interfaces for sending emails.",
"homepage": "http://pear.php.net/package/Mail",
"time": "2017-04-11 17:27:29"
"time": "2017-04-11T17:27:29+00:00"
},
{
"name": "pear/net_smtp",
@@ -1065,7 +1158,7 @@
"mail",
"smtp"
],
"time": "2017-01-14 18:19:55"
"time": "2017-01-14T18:19:55+00:00"
},
{
"name": "pear/net_socket",
@@ -1119,7 +1212,7 @@
}
],
"description": "More info available on: http://pear.php.net/package/Net_Socket",
"time": "2017-04-06 15:16:38"
"time": "2017-04-06T15:16:38+00:00"
},
{
"name": "pear/pear-core-minimal",
@@ -1163,7 +1256,7 @@
}
],
"description": "Minimal set of PEAR core files to be used as composer dependency",
"time": "2017-02-28 16:46:11"
"time": "2017-02-28T16:46:11+00:00"
},
{
"name": "pear/pear_exception",
@@ -1218,7 +1311,7 @@
"keywords": [
"exception"
],
"time": "2015-02-10 20:07:52"
"time": "2015-02-10T20:07:52+00:00"
},
{
"name": "psr/container",
@@ -1267,7 +1360,7 @@
"container-interop",
"psr"
],
"time": "2017-02-14 16:28:37"
"time": "2017-02-14T16:28:37+00:00"
},
{
"name": "psr/log",
@@ -1314,7 +1407,59 @@
"psr",
"psr-3"
],
"time": "2016-10-10 12:19:37"
"time": "2016-10-10T12:19:37+00:00"
},
{
"name": "spomky-labs/base64url",
"version": "v1.0.2",
"source": {
"type": "git",
"url": "https://github.com/Spomky-Labs/base64url.git",
"reference": "ef6d5fb93894063d9cee996022259fd08d6646ea"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Spomky-Labs/base64url/zipball/ef6d5fb93894063d9cee996022259fd08d6646ea",
"reference": "ef6d5fb93894063d9cee996022259fd08d6646ea",
"shasum": ""
},
"require": {
"php": "^5.3|^7.0"
},
"require-dev": {
"phpunit/phpunit": "^4.0|^5.0",
"satooshi/php-coveralls": "^1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Base64Url\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Florent Morselli",
"homepage": "https://github.com/Spomky-Labs/base64url/contributors"
}
],
"description": "Base 64 URL Safe Encoding/decoding PHP Library",
"homepage": "https://github.com/Spomky-Labs/base64url",
"keywords": [
"base64",
"rfc4648",
"safe",
"url"
],
"time": "2016-01-21T19:50:30+00:00"
},
{
"name": "swiftmailer/swiftmailer",
@@ -1369,7 +1514,7 @@
"mail",
"mailer"
],
"time": "2017-09-30 22:39:41"
"time": "2017-09-30T22:39:41+00:00"
},
{
"name": "symfony/console",
@@ -1438,7 +1583,7 @@
],
"description": "Symfony Console Component",
"homepage": "https://symfony.com",
"time": "2018-01-03 07:37:34"
"time": "2018-01-03T07:37:34+00:00"
},
{
"name": "symfony/debug",
@@ -1494,7 +1639,7 @@
],
"description": "Symfony Debug Component",
"homepage": "https://symfony.com",
"time": "2018-01-03 17:14:19"
"time": "2018-01-03T17:14:19+00:00"
},
{
"name": "symfony/event-dispatcher",
@@ -1557,7 +1702,7 @@
],
"description": "Symfony EventDispatcher Component",
"homepage": "https://symfony.com",
"time": "2018-01-03 07:37:34"
"time": "2018-01-03T07:37:34+00:00"
},
{
"name": "symfony/filesystem",
@@ -1606,7 +1751,7 @@
],
"description": "Symfony Filesystem Component",
"homepage": "https://symfony.com",
"time": "2018-01-03 07:37:34"
"time": "2018-01-03T07:37:34+00:00"
},
{
"name": "symfony/finder",
@@ -1655,7 +1800,7 @@
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
"time": "2018-01-03 07:37:34"
"time": "2018-01-03T07:37:34+00:00"
},
{
"name": "symfony/polyfill-mbstring",
@@ -1714,7 +1859,7 @@
"portable",
"shim"
],
"time": "2017-10-11 12:05:26"
"time": "2017-10-11T12:05:26+00:00"
},
{
"name": "symfony/process",
@@ -1763,7 +1908,7 @@
],
"description": "Symfony Process Component",
"homepage": "https://symfony.com",
"time": "2018-01-03 07:37:34"
"time": "2018-01-03T07:37:34+00:00"
},
{
"name": "symfony/yaml",
@@ -1821,7 +1966,7 @@
],
"description": "Symfony Yaml Component",
"homepage": "https://symfony.com",
"time": "2018-01-03 07:37:34"
"time": "2018-01-03T07:37:34+00:00"
},
{
"name": "twig/twig",
@@ -1882,7 +2027,7 @@
"keywords": [
"templating"
],
"time": "2016-01-11 14:02:19"
"time": "2016-01-11T14:02:19+00:00"
}
],
"packages-dev": [
@@ -1938,7 +2083,7 @@
"constructor",
"instantiate"
],
"time": "2015-06-14 21:17:01"
"time": "2015-06-14T21:17:01+00:00"
},
{
"name": "myclabs/deep-copy",
@@ -1983,7 +2128,7 @@
"object",
"object graph"
],
"time": "2017-10-19 19:58:43"
"time": "2017-10-19T19:58:43+00:00"
},
{
"name": "phar-io/manifest",
@@ -2038,7 +2183,7 @@
}
],
"description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
"time": "2017-03-05 18:14:27"
"time": "2017-03-05T18:14:27+00:00"
},
{
"name": "phar-io/version",
@@ -2085,7 +2230,7 @@
}
],
"description": "Library for handling version information and constraints",
"time": "2017-03-05 17:38:23"
"time": "2017-03-05T17:38:23+00:00"
},
{
"name": "phpdocumentor/reflection-common",
@@ -2139,7 +2284,7 @@
"reflection",
"static analysis"
],
"time": "2017-09-11 18:02:19"
"time": "2017-09-11T18:02:19+00:00"
},
{
"name": "phpdocumentor/reflection-docblock",
@@ -2190,7 +2335,7 @@
}
],
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
"time": "2017-11-27 17:38:31"
"time": "2017-11-27T17:38:31+00:00"
},
{
"name": "phpdocumentor/type-resolver",
@@ -2237,7 +2382,7 @@
"email": "me@mikevanriel.com"
}
],
"time": "2017-07-14 14:27:02"
"time": "2017-07-14T14:27:02+00:00"
},
{
"name": "phpspec/prophecy",
@@ -2300,7 +2445,7 @@
"spy",
"stub"
],
"time": "2017-11-24 13:59:53"
"time": "2017-11-24T13:59:53+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -2363,7 +2508,7 @@
"testing",
"xunit"
],
"time": "2017-12-06 09:29:45"
"time": "2017-12-06T09:29:45+00:00"
},
{
"name": "phpunit/php-file-iterator",
@@ -2410,7 +2555,7 @@
"filesystem",
"iterator"
],
"time": "2017-11-27 13:52:08"
"time": "2017-11-27T13:52:08+00:00"
},
{
"name": "phpunit/php-text-template",
@@ -2451,7 +2596,7 @@
"keywords": [
"template"
],
"time": "2015-06-21 13:50:34"
"time": "2015-06-21T13:50:34+00:00"
},
{
"name": "phpunit/php-timer",
@@ -2500,7 +2645,7 @@
"keywords": [
"timer"
],
"time": "2017-02-26 11:10:40"
"time": "2017-02-26T11:10:40+00:00"
},
{
"name": "phpunit/php-token-stream",
@@ -2549,7 +2694,7 @@
"keywords": [
"tokenizer"
],
"time": "2017-11-27 05:48:46"
"time": "2017-11-27T05:48:46+00:00"
},
{
"name": "phpunit/phpunit",
@@ -2633,7 +2778,7 @@
"testing",
"xunit"
],
"time": "2017-12-17 06:31:19"
"time": "2017-12-17T06:31:19+00:00"
},
{
"name": "phpunit/phpunit-mock-objects",
@@ -2692,7 +2837,7 @@
"mock",
"xunit"
],
"time": "2018-01-06 05:45:45"
"time": "2018-01-06T05:45:45+00:00"
},
{
"name": "sebastian/code-unit-reverse-lookup",
@@ -2737,7 +2882,7 @@
],
"description": "Looks up which function or method a line of code belongs to",
"homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
"time": "2017-03-04 06:30:41"
"time": "2017-03-04T06:30:41+00:00"
},
{
"name": "sebastian/comparator",
@@ -2801,7 +2946,7 @@
"compare",
"equality"
],
"time": "2017-12-22 14:50:35"
"time": "2017-12-22T14:50:35+00:00"
},
{
"name": "sebastian/diff",
@@ -2853,7 +2998,7 @@
"keywords": [
"diff"
],
"time": "2017-08-03 08:09:46"
"time": "2017-08-03T08:09:46+00:00"
},
{
"name": "sebastian/environment",
@@ -2903,7 +3048,7 @@
"environment",
"hhvm"
],
"time": "2017-07-01 08:51:00"
"time": "2017-07-01T08:51:00+00:00"
},
{
"name": "sebastian/exporter",
@@ -2970,7 +3115,7 @@
"export",
"exporter"
],
"time": "2017-04-03 13:19:02"
"time": "2017-04-03T13:19:02+00:00"
},
{
"name": "sebastian/global-state",
@@ -3021,7 +3166,7 @@
"keywords": [
"global state"
],
"time": "2017-04-27 15:39:26"
"time": "2017-04-27T15:39:26+00:00"
},
{
"name": "sebastian/object-enumerator",
@@ -3068,7 +3213,7 @@
],
"description": "Traverses array structures and object graphs to enumerate all referenced objects",
"homepage": "https://github.com/sebastianbergmann/object-enumerator/",
"time": "2017-08-03 12:35:26"
"time": "2017-08-03T12:35:26+00:00"
},
{
"name": "sebastian/object-reflector",
@@ -3113,7 +3258,7 @@
],
"description": "Allows reflection of object attributes, including inherited and non-public ones",
"homepage": "https://github.com/sebastianbergmann/object-reflector/",
"time": "2017-03-29 09:07:27"
"time": "2017-03-29T09:07:27+00:00"
},
{
"name": "sebastian/recursion-context",
@@ -3166,7 +3311,7 @@
],
"description": "Provides functionality to recursively process PHP variables",
"homepage": "http://www.github.com/sebastianbergmann/recursion-context",
"time": "2017-03-03 06:23:57"
"time": "2017-03-03T06:23:57+00:00"
},
{
"name": "sebastian/resource-operations",
@@ -3208,7 +3353,7 @@
],
"description": "Provides a list of PHP built-in functions that operate on resources",
"homepage": "https://www.github.com/sebastianbergmann/resource-operations",
"time": "2015-07-28 20:34:47"
"time": "2015-07-28T20:34:47+00:00"
},
{
"name": "sebastian/version",
@@ -3251,7 +3396,7 @@
],
"description": "Library that helps with managing the version number of Git-hosted PHP projects",
"homepage": "https://github.com/sebastianbergmann/version",
"time": "2016-10-03 07:35:21"
"time": "2016-10-03T07:35:21+00:00"
},
{
"name": "theseer/tokenizer",
@@ -3291,7 +3436,7 @@
}
],
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
"time": "2017-04-07 12:08:54"
"time": "2017-04-07T12:08:54+00:00"
},
{
"name": "webmozart/assert",
@@ -3341,7 +3486,7 @@
"check",
"validate"
],
"time": "2016-11-23 20:04:58"
"time": "2016-11-23T20:04:58+00:00"
}
],
"aliases": [],

View File

@@ -2,6 +2,6 @@
// autoload.php @generated by Composer
require_once __DIR__ . '/composer' . '/autoload_real.php';
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit6d4a28cd96a5bc5d5b97781c062572d9::getLoader();

View File

@@ -1 +0,0 @@
../gettext/languages/bin/export-plural-rules

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env php
<?php
include 'export-plural-rules.php';

View File

@@ -1 +0,0 @@
../gettext/languages/bin/export-plural-rules.php

View File

@@ -0,0 +1,234 @@
<?php
use Gettext\Languages\Exporter\Exporter;
use Gettext\Languages\Language;
// Let's start by imposing that we don't accept any error or warning.
// This is a really life-saving approach.
error_reporting(E_ALL);
set_error_handler(function ($errno, $errstr, $errfile, $errline) {
Enviro::echoErr("$errstr\nFile: $errfile\nLine: $errline\nCode: $errno\n");
die(5);
});
require_once dirname(__DIR__).'/src/autoloader.php';
// Parse the command line options
Enviro::initialize();
try {
if (isset(Enviro::$languages)) {
$languages = array();
foreach (Enviro::$languages as $languageId) {
$language = Language::getById($languageId);
if (!isset($language)) {
throw new Exception("Unable to find the language with id '$languageId'");
}
$languages[] = $language;
}
} else {
$languages = Language::getAll();
}
if (Enviro::$reduce) {
$languages = Enviro::reduce($languages);
}
if (isset(Enviro::$outputFilename)) {
echo call_user_func(array(Exporter::getExporterClassName(Enviro::$outputFormat), 'toFile'), $languages, Enviro::$outputFilename, array('us-ascii' => Enviro::$outputUSAscii));
} else {
echo call_user_func(array(Exporter::getExporterClassName(Enviro::$outputFormat), 'toString'), $languages, array('us-ascii' => Enviro::$outputUSAscii));
}
} catch (Exception $x) {
Enviro::echoErr($x->getMessage()."\n");
Enviro::echoErr("Trace:\n");
Enviro::echoErr($x->getTraceAsString()."\n");
die(4);
}
die(0);
/**
* Helper class to handle command line options.
*/
class Enviro
{
/**
* Shall the output contain only US-ASCII characters?
* @var bool
*/
public static $outputUSAscii;
/**
* The output format.
* @var string
*/
public static $outputFormat;
/**
* Output file name.
* @var string
*/
public static $outputFilename;
/**
* List of wanted language IDs; it not set: all languages will be returned.
* @var array|null
*/
public static $languages;
/**
* Reduce the language list to the minimum common denominator.
* @var bool
*/
public static $reduce;
/**
* Parse the command line options.
*/
public static function initialize()
{
global $argv;
self::$outputUSAscii = false;
self::$outputFormat = null;
self::$outputFilename = null;
self::$languages = null;
self::$reduce = null;
$exporters = Exporter::getExporters();
if (isset($argv) && is_array($argv)) {
foreach ($argv as $argi => $arg) {
if ($argi === 0) {
continue;
}
if (is_string($arg)) {
$argLC = trim(strtolower($arg));
switch ($argLC) {
case '--us-ascii':
self::$outputUSAscii = true;
break;
case '--reduce=yes':
self::$reduce = true;
break;
case '--reduce=no':
self::$reduce = false;
break;
default:
if (preg_match('/^--output=.+$/', $argLC)) {
if (isset(self::$outputFilename)) {
self::echoErr("The output file name has been specified more than once!\n");
self::showSyntax();
die(3);
}
list(, self::$outputFilename) = explode('=', $arg, 2);
self::$outputFilename = trim(self::$outputFilename);
} elseif (preg_match('/^--languages?=.+$/', $argLC)) {
list(, $s) = explode('=', $arg, 2);
$list = explode(',', $s);
if (is_array(self::$languages)) {
self::$languages = array_merge(self::$languages, $list);
} else {
self::$languages = $list;
}
} elseif (isset($exporters[$argLC])) {
if (isset(self::$outputFormat)) {
self::echoErr("The output format has been specified more than once!\n");
self::showSyntax();
die(3);
}
self::$outputFormat = $argLC;
} else {
self::echoErr("Unknown option: $arg\n");
self::showSyntax();
die(2);
}
break;
}
}
}
}
if (!isset(self::$outputFormat)) {
self::showSyntax();
die(1);
}
if (isset(self::$languages)) {
self::$languages = array_values(array_unique(self::$languages));
}
if (!isset(self::$reduce)) {
self::$reduce = isset(self::$languages) ? false : true;
}
}
/**
* Write out the syntax.
*/
public static function showSyntax()
{
$exporters = array_keys(Exporter::getExporters(true));
self::echoErr("Syntax: php ".basename(__FILE__)." [--us-ascii] [--languages=<LanguageId>[,<LanguageId>,...]] [--reduce=yes|no] [--output=<file name>] <".implode('|', $exporters).">\n");
self::echoErr("Where:\n");
self::echoErr("--us-ascii : if specified, the output will contain only US-ASCII characters.\n");
self::echoErr("--languages: (or --language) export only the specified language codes.\n");
self::echoErr(" Separate languages with commas; you can also use this argument\n");
self::echoErr(" more than once; it's case insensitive and accepts both '_' and\n");
self::echoErr(" '-' as locale chunks separator (eg we accept 'it_IT' as well as\n");
self::echoErr(" 'it-it').\n");
self::echoErr("--reduce : if set to yes the output won't contain languages with the same\n");
self::echoErr(" base language and rules.\n For instance nl_BE ('Flemish') will be\n");
self::echoErr(" omitted because it's the same as nl ('Dutch').\n");
self::echoErr(" Defaults to 'no' --languages is specified, to 'yes' otherwise.\n");
self::echoErr("--output : if specified, the output will be saved to <file name>. If not\n");
self::echoErr(" specified we'll output to standard output.\n");
self::echoErr("Output formats\n");
$len = max(array_map('strlen', $exporters));
foreach ($exporters as $exporter) {
self::echoErr(str_pad($exporter, $len).": ".Exporter::getExporterDescription($exporter)."\n");
}
}
/**
* Print a string to stderr.
* @param string $str The string to be printed out.
*/
public static function echoErr($str)
{
$hStdErr = @fopen('php://stderr', 'a');
if ($hStdErr === false) {
echo $str;
} else {
fwrite($hStdErr, $str);
fclose($hStdErr);
}
}
/**
* Reduce a language list to the minimum common denominator.
* @param Language[] $languages
* @return Language[]
*/
public static function reduce($languages)
{
for ($numChunks = 3; $numChunks >= 2; $numChunks--) {
$filtered = array();
foreach ($languages as $language) {
$chunks = explode('_', $language->id);
$compatibleFound = false;
if (count($chunks) === $numChunks) {
$categoriesHash = serialize($language->categories);
$otherIds = array();
$otherIds[] = $chunks[0];
for ($k = 2; $k < $numChunks; $k++) {
$otherIds[] = $chunks[0].'_'.$chunks[$numChunks - 1];
}
foreach ($languages as $check) {
foreach ($otherIds as $otherId) {
if (($check->id === $otherId) && ($check->formula === $language->formula) && (serialize($check->categories) === $categoriesHash)) {
$compatibleFound = true;
break;
}
}
if ($compatibleFound === true) {
break;
}
}
}
if (!$compatibleFound) {
$filtered[] = $language;
}
}
$languages = $filtered;
}
return $languages;
}
}

170
core/lib/composer/vendor/bin/markdown vendored Executable file
View File

@@ -0,0 +1,170 @@
#!/usr/bin/env php
<?php
/**
* @copyright Copyright (c) 2014 Carsten Brandt
* @license https://github.com/cebe/markdown/blob/master/LICENSE
* @link https://github.com/cebe/markdown#readme
*/
$composerAutoload = [
__DIR__ . '/../vendor/autoload.php', // standalone with "composer install" run
__DIR__ . '/../../../autoload.php', // script is installed as a composer binary
];
foreach ($composerAutoload as $autoload) {
if (file_exists($autoload)) {
require($autoload);
break;
}
}
// Send all errors to stderr
ini_set('display_errors', 'stderr');
$flavor = 'cebe\\markdown\\Markdown';
$flavors = [
'gfm' => ['cebe\\markdown\\GithubMarkdown', __DIR__ . '/../GithubMarkdown.php'],
'extra' => ['cebe\\markdown\\MarkdownExtra', __DIR__ . '/../MarkdownExtra.php'],
];
$full = false;
$src = [];
foreach($argv as $k => $arg) {
if ($k == 0) {
continue;
}
if ($arg[0] == '-') {
$arg = explode('=', $arg);
switch($arg[0]) {
case '--flavor':
if (isset($arg[1])) {
if (isset($flavors[$arg[1]])) {
require($flavors[$arg[1]][1]);
$flavor = $flavors[$arg[1]][0];
} else {
error("Unknown flavor: " . $arg[1], "usage");
}
} else {
error("Incomplete argument --flavor!", "usage");
}
break;
case '--full':
$full = true;
break;
case '-h':
case '--help':
echo "PHP Markdown to HTML converter\n";
echo "------------------------------\n\n";
echo "by Carsten Brandt <mail@cebe.cc>\n\n";
usage();
break;
default:
error("Unknown argument " . $arg[0], "usage");
}
} else {
$src[] = $arg;
}
}
if (empty($src)) {
$markdown = file_get_contents("php://stdin");
} elseif (count($src) == 1) {
$file = reset($src);
if (!file_exists($file)) {
error("File does not exist:" . $file);
}
$markdown = file_get_contents($file);
} else {
error("Converting multiple files is not yet supported.", "usage");
}
/** @var cebe\markdown\Parser $md */
$md = new $flavor();
$markup = $md->parse($markdown);
if ($full) {
echo <<<HTML
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<style>
body { font-family: Arial, sans-serif; }
code { background: #eeeeff; padding: 2px; }
li { margin-bottom: 5px; }
img { max-width: 1200px; }
table, td, th { border: solid 1px #ccc; border-collapse: collapse; }
</style>
</head>
<body>
$markup
</body>
</html>
HTML;
} else {
echo $markup;
}
// functions
/**
* Display usage information
*/
function usage() {
global $argv;
$cmd = $argv[0];
echo <<<EOF
Usage:
$cmd [--flavor=<flavor>] [--full] [file.md]
--flavor specifies the markdown flavor to use. If omitted the original markdown by John Gruber [1] will be used.
Available flavors:
gfm - Github flavored markdown [2]
extra - Markdown Extra [3]
--full ouput a full HTML page with head and body. If not given, only the parsed markdown will be output.
--help shows this usage information.
If no file is specified input will be read from STDIN.
Examples:
Render a file with original markdown:
$cmd README.md > README.html
Render a file using gihtub flavored markdown:
$cmd --flavor=gfm README.md > README.html
Convert the original markdown description to html using STDIN:
curl http://daringfireball.net/projects/markdown/syntax.text | $cmd > md.html
[1] http://daringfireball.net/projects/markdown/syntax
[2] https://help.github.com/articles/github-flavored-markdown
[3] http://michelf.ca/projects/php-markdown/extra/
EOF;
exit(1);
}
/**
* Send custom error message to stderr
* @param $message string
* @param $callback mixed called before script exit
* @return void
*/
function error($message, $callback = null) {
$fe = fopen("php://stderr", "w");
fwrite($fe, "Error: " . $message . "\n");
if (is_callable($callback)) {
call_user_func($callback);
}
exit(1);
}

View File

@@ -1 +0,0 @@
../phpunit/phpunit/phpunit

53
core/lib/composer/vendor/bin/phpunit vendored Executable file
View File

@@ -0,0 +1,53 @@
#!/usr/bin/env php
<?php
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (version_compare('7.0.0', PHP_VERSION, '>')) {
fwrite(
STDERR,
sprintf(
'This version of PHPUnit is supported on PHP 7.0 and PHP 7.1.' . PHP_EOL .
'You are using PHP %s (%s).' . PHP_EOL,
PHP_VERSION,
PHP_BINARY
)
);
die(1);
}
if (!ini_get('date.timezone')) {
ini_set('date.timezone', 'UTC');
}
foreach (array(__DIR__ . '/../../autoload.php', __DIR__ . '/../vendor/autoload.php', __DIR__ . '/vendor/autoload.php') as $file) {
if (file_exists($file)) {
define('PHPUNIT_COMPOSER_INSTALL', $file);
break;
}
}
unset($file);
if (!defined('PHPUNIT_COMPOSER_INSTALL')) {
fwrite(
STDERR,
'You need to set up the project dependencies using Composer:' . PHP_EOL . PHP_EOL .
' composer install' . PHP_EOL . PHP_EOL .
'You can learn all about Composer on https://getcomposer.org/.' . PHP_EOL
);
die(1);
}
require PHPUNIT_COMPOSER_INSTALL;
PHPUnit\TextUI\Command::main();

View File

@@ -1 +0,0 @@
../consolidation/robo/robo

22
core/lib/composer/vendor/bin/robo vendored Executable file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env php
<?php
/**
* if we're running from phar load the phar autoload,
* else let the script 'robo' search for the autoloader
*/
if (strpos(basename(__FILE__), 'phar')) {
require_once 'phar://robo.phar/vendor/autoload.php';
} else {
if (file_exists(__DIR__.'/vendor/autoload.php')) {
require_once __DIR__.'/vendor/autoload.php';
} elseif (file_exists(__DIR__.'/../../autoload.php')) {
require_once __DIR__ . '/../../autoload.php';
} else {
require_once 'phar://robo.phar/vendor/autoload.php';
}
}
$runner = new \Robo\Runner();
$runner->setSelfUpdateRepository('consolidation/robo');
$statusCode = $runner->execute($_SERVER['argv']);
exit($statusCode);

View File

@@ -0,0 +1,6 @@
* text=auto
/.gitattributes
/.gitignore
/.scrutinizer.yml
/.travis.yml

View File

@@ -0,0 +1,4 @@
/.idea/
composer.lock
/vendor
README.html

View File

@@ -0,0 +1,6 @@
imports:
- php
tools:
external_code_coverage:
timeout: 600 # Timeout in seconds.

View File

@@ -0,0 +1,42 @@
language: php
php:
- 5.4
- 5.5
- 5.6
- 7.0
- 7.1
- 7.2
- nightly
- hhvm
# faster builds on new travis setup not using sudo
sudo: false
# travis does not support HHVM on other platforms, choosing trusty
dist: trusty
# cache composer cache
cache:
directories:
- $HOME/.composer/cache
# run build against hhvm but allow them to fail
# http://docs.travis-ci.com/user/build-configuration/#Rows-That-are-Allowed-To-Fail
matrix:
fast_finish: true
allow_failures:
- php: nightly
install:
- composer self-update && composer --version
- composer install --prefer-dist
script:
- vendor/bin/phpunit --verbose --coverage-clover=coverage.clover
# test against standard markdown spec
# - git clone https://github.com/jgm/stmd && cd stmd && perl runtests.pl spec.txt ../bin/markdown
after_script:
- wget https://scrutinizer-ci.com/ocular.phar
- php ocular.phar code-coverage:upload --format=php-clover coverage.clover

View File

@@ -0,0 +1,104 @@
CHANGELOG
=========
Version 1.2.1 on 26. Mar. 2018
------------------------------
- Improved handling of inline HTML with URL and email tags.
- Improved handling of custom syntax with `[[`, references should not use `[` as the first character in the reference name.
Version 1.2.0 on 14. Mar. 2018
------------------------------
- #50 Do not render empty emphs.
- #69 Improve ABSY for tables, make column and row information directly available in absy (@NathanBaulch)
- #89 Lists should be separated by a HR (@bieleckim)
- #95 Added `TableTrait::composeTable($head, $body)`, for easier overriding of table layout (@maximal, @cebe)
- #111 Improve rendering of successive strongs (@wogsland)
- #132 Improve detection and rendering of fenced code blocks in lists.
- #134 Fix Emph and Strong to allow escaping `*` or `_` inside them.
- #135 GithubMarkdown was not parsing inline code when there are square brackets around it.
- #151 Fixed table rendering for lines begining with | for GFM (@GenaBitu)
- Improved table rendering, allow single column tables.
Version 1.1.2 on 16. Jul 2017
-----------------------------
- #126 Fixed crash on empty lines that extend a lazy list
- #128 Fix table renderer which including default alignment (@tanakahisateru)
- #129 Use given encoded URL if decoded URL text looks insecure, e.g. uses broken UTF-8 (@tanakahisateru)
- Added a workaround for a [PHP bug](https://bugs.php.net/bug.php?id=45735) which exists in versions `<` 7.0, where `preg_match()` causes a segfault
on [catastropic backtracking][] in emph/strong parsing.
[catastropic backtracking]: http://www.regular-expressions.info/catastrophic.html
Version 1.1.1 on 14. Sep 2016
-----------------------------
- #112 Fixed parsing for custom self-closing HTML tags
- #113 improve extensibility by making `prepareMarkers()` protected and add `parseBlock()` method
- #114 better handling of continued inline HTML in paragraphs
Version 1.1.0 on 06. Mar. 2015
------------------------------
- improve compatibility with github flavored markdown
- #64 fixed some rendering issue with emph and strong
- #56 trailing and leading spaces in a link are now ignored
- fixed various issues with table rendering
- #98 Fix PHP fatal error when maximumNestingLevel was reached (@tanakahisateru)
- refactored nested and lazy list handling, improved overall list rendering consistency
- Lines containing "0" where skipped or considered empty in some cases (@tanakahisateru)
- #54 escape characters are now also considered inside of urls
Version 1.0.1 on 25. Oct. 2014
------------------------------
- Fixed the `bin/markdown` script to work with composer autoloader (c497bada0e15f61873ba6b2e29f4bb8b3ef2a489)
- #74 fixed a bug that caused a bunch of broken characters when non-ASCII input was given. Parser now handles UTF-8 input correctly. Other encodings are currently untested, UTF-8 is recommended.
Version 1.0.0 on 12. Oct. 2014
------------------------------
This is the first stable release of version 1.0 which is incompatible to the 0.9.x branch regarding the internal API which is used when extending the Markdown parser. The external API has no breaking changes. The rendered Markdown however has changed in some edge cases and some rendering issues have been fixed.
The parser got a bit slower compared to earlier versions but is able to parse Markdown more accurately and uses an abstract syntax tree as the internal representation of the parsed text which allows extensions to work with the parsed Markdown in many ways including rendering as other formats than HTML.
For more details about the changes see the [release message of 1.0.0-rc](https://github.com/cebe/markdown/releases/tag/1.0.0-rc).
You can try it out on the website: <http://markdown.cebe.cc/try>
The parser is now also regsitered on the [Babelmark 2 page](http://johnmacfarlane.net/babelmark2/?normalize=1&text=Hello+**World**!) by [John MacFarlane](http://johnmacfarlane.net/) which you can use to compare Markdown output of different parsers.
Version 1.0.0-rc on 10. Oct. 2014
---------------------------------
- #21 speed up inline parsing using [strpbrk](http://www.php.net/manual/de/function.strpbrk.php) about 20% speedup compared to parsing before.
- #24 CLI script now sends all error output to stderr instead of stdout
- #25 Added partial support for the Markdown Extra flavor
- #10 GithubMarkdown is now fully supported including tables
- #67 All Markdown classes are now composed out of php traits
- #67 The way to extend markdown has changed due to the introduction of an abstract syntax tree. See https://github.com/cebe/markdown/commit/dd2d0faa71b630e982d6651476872469b927db6d for how it changes or read the new README.
- Introduced an abstract syntax tree as an intermediate representation between parsing steps.
This not only fixes some issues with nested block elements but also allows manipulation of the markdown
before rendering.
- This version also fixes serveral rendering issues.
Version 0.9.2 on 18. Feb. 2014
------------------------------
- #27 Fixed some rendering problems with block elements not separated by newlines
Version 0.9.1 on 18. Feb. 2014
------------------------------
Fixed an issue with inline markers that begin with the same character e.g. `[` and `[[`.
Version 0.9.0 on 18. Feb. 2014
------------------------------
The initial release.
- Complete implementation of the original Markdown spec
- GFM without tables
- a command line tool for markdown parsing

View File

@@ -0,0 +1,36 @@
Contributing
============
First of all, **thank you** for contributing, **you are awesome**! :)
If you have an idea or found a bug, please [open an issue](https://github.com/cebe/markdown/issues/new) on github.
If you want to contribute code, there a few rules to follow:
- I am following a code style that is basically [PSR-2](http://www.php-fig.org/psr/2/) but with TABS indentation (yes, I really do that ;) ).
I am not going to nit-pick on all the details about the code style but indentation is a must. The important part is that code is readable.
Methods should be documented using phpdoc style.
- All code must be covered by tests so if you fix a bug or add a feature, please include a test case for it. See below on how that works.
- If you add a feature it should be documented.
- Also, while creating your Pull Request on GitHub, please write a description
which gives the context and/or explains why you are creating it.
Thank you very much!
Running the tests
-----------------
The Markdown parser classes are tested with [PHPUnit](https://phpunit.de/). For each test case there is a set of files in
the subfolders of the `/tests` folder. The result of the parser is tested with an input and an output file respectively
where the input file contains the Markdown and the output file contains the expected HTML.
You can run the tests after initializing the lib with composer(`composer install`) with the following command:
vendor/bin/phpunit
To create a new test case, create a `.md` file a`.html` with the same base name in the subfolders of
the `/tests` directory. See existing files for examples.

View File

@@ -0,0 +1,114 @@
<?php
/**
* @copyright Copyright (c) 2014 Carsten Brandt
* @license https://github.com/cebe/markdown/blob/master/LICENSE
* @link https://github.com/cebe/markdown#readme
*/
namespace cebe\markdown;
/**
* Markdown parser for github flavored markdown.
*
* @author Carsten Brandt <mail@cebe.cc>
*/
class GithubMarkdown extends Markdown
{
// include block element parsing using traits
use block\TableTrait;
use block\FencedCodeTrait;
// include inline element parsing using traits
use inline\StrikeoutTrait;
use inline\UrlLinkTrait;
/**
* @var boolean whether to interpret newlines as `<br />`-tags.
* This feature is useful for comments where newlines are often meant to be real new lines.
*/
public $enableNewlines = false;
/**
* @inheritDoc
*/
protected $escapeCharacters = [
// from Markdown
'\\', // backslash
'`', // backtick
'*', // asterisk
'_', // underscore
'{', '}', // curly braces
'[', ']', // square brackets
'(', ')', // parentheses
'#', // hash mark
'+', // plus sign
'-', // minus sign (hyphen)
'.', // dot
'!', // exclamation mark
'<', '>',
// added by GithubMarkdown
':', // colon
'|', // pipe
];
/**
* Consume lines for a paragraph
*
* Allow headlines, lists and code to break paragraphs
*/
protected function consumeParagraph($lines, $current)
{
// consume until newline
$content = [];
for ($i = $current, $count = count($lines); $i < $count; $i++) {
$line = $lines[$i];
if ($line === ''
|| ltrim($line) === ''
|| !ctype_alpha($line[0]) && (
$this->identifyQuote($line, $lines, $i) ||
$this->identifyFencedCode($line, $lines, $i) ||
$this->identifyUl($line, $lines, $i) ||
$this->identifyOl($line, $lines, $i) ||
$this->identifyHr($line, $lines, $i)
)
|| $this->identifyHeadline($line, $lines, $i))
{
break;
} elseif ($this->identifyCode($line, $lines, $i)) {
// possible beginning of a code block
// but check for continued inline HTML
// e.g. <img src="file.jpg"
// alt="some alt aligned with src attribute" title="some text" />
if (preg_match('~<\w+([^>]+)$~s', implode("\n", $content))) {
$content[] = $line;
} else {
break;
}
} else {
$content[] = $line;
}
}
$block = [
'paragraph',
'content' => $this->parseInline(implode("\n", $content)),
];
return [$block, --$i];
}
/**
* @inheritdocs
*
* Parses a newline indicated by two spaces on the end of a markdown line.
*/
protected function renderText($text)
{
if ($this->enableNewlines) {
$br = $this->html5 ? "<br>\n" : "<br />\n";
return strtr($text[1], [" \n" => $br, "\n" => $br]);
} else {
return parent::renderText($text);
}
}
}

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Carsten Brandt
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.

View File

@@ -0,0 +1,128 @@
<?php
/**
* @copyright Copyright (c) 2014 Carsten Brandt
* @license https://github.com/cebe/markdown/blob/master/LICENSE
* @link https://github.com/cebe/markdown#readme
*/
namespace cebe\markdown;
/**
* Markdown parser for the [initial markdown spec](http://daringfireball.net/projects/markdown/syntax).
*
* @author Carsten Brandt <mail@cebe.cc>
*/
class Markdown extends Parser
{
// include block element parsing using traits
use block\CodeTrait;
use block\HeadlineTrait;
use block\HtmlTrait {
parseInlineHtml as private;
}
use block\ListTrait {
// Check Ul List before headline
identifyUl as protected identifyBUl;
consumeUl as protected consumeBUl;
}
use block\QuoteTrait;
use block\RuleTrait {
// Check Hr before checking lists
identifyHr as protected identifyAHr;
consumeHr as protected consumeAHr;
}
// include inline element parsing using traits
use inline\CodeTrait;
use inline\EmphStrongTrait;
use inline\LinkTrait;
/**
* @var boolean whether to format markup according to HTML5 spec.
* Defaults to `false` which means that markup is formatted as HTML4.
*/
public $html5 = false;
/**
* @var array these are "escapeable" characters. When using one of these prefixed with a
* backslash, the character will be outputted without the backslash and is not interpreted
* as markdown.
*/
protected $escapeCharacters = [
'\\', // backslash
'`', // backtick
'*', // asterisk
'_', // underscore
'{', '}', // curly braces
'[', ']', // square brackets
'(', ')', // parentheses
'#', // hash mark
'+', // plus sign
'-', // minus sign (hyphen)
'.', // dot
'!', // exclamation mark
'<', '>',
];
/**
* @inheritDoc
*/
protected function prepare()
{
// reset references
$this->references = [];
}
/**
* Consume lines for a paragraph
*
* Allow headlines and code to break paragraphs
*/
protected function consumeParagraph($lines, $current)
{
// consume until newline
$content = [];
for ($i = $current, $count = count($lines); $i < $count; $i++) {
$line = $lines[$i];
// a list may break a paragraph when it is inside of a list
if (isset($this->context[1]) && $this->context[1] === 'list' && !ctype_alpha($line[0]) && (
$this->identifyUl($line, $lines, $i) || $this->identifyOl($line, $lines, $i))) {
break;
}
if ($line === '' || ltrim($line) === '' || $this->identifyHeadline($line, $lines, $i)) {
break;
} elseif ($line[0] === "\t" || $line[0] === " " && strncmp($line, ' ', 4) === 0) {
// possible beginning of a code block
// but check for continued inline HTML
// e.g. <img src="file.jpg"
// alt="some alt aligned with src attribute" title="some text" />
if (preg_match('~<\w+([^>]+)$~s', implode("\n", $content))) {
$content[] = $line;
} else {
break;
}
} else {
$content[] = $line;
}
}
$block = [
'paragraph',
'content' => $this->parseInline(implode("\n", $content)),
];
return [$block, --$i];
}
/**
* @inheritdocs
*
* Parses a newline indicated by two spaces on the end of a markdown line.
*/
protected function renderText($text)
{
return str_replace(" \n", $this->html5 ? "<br>\n" : "<br />\n", $text[1]);
}
}

View File

@@ -0,0 +1,256 @@
<?php
namespace cebe\markdown;
use cebe\markdown\block\TableTrait;
// work around https://github.com/facebook/hhvm/issues/1120
defined('ENT_HTML401') || define('ENT_HTML401', 0);
/**
* Markdown parser for the [markdown extra](http://michelf.ca/projects/php-markdown/extra/) flavor.
*
* @author Carsten Brandt <mail@cebe.cc>
* @license https://github.com/cebe/markdown/blob/master/LICENSE
* @link https://github.com/cebe/markdown#readme
*/
class MarkdownExtra extends Markdown
{
// include block element parsing using traits
use block\TableTrait;
use block\FencedCodeTrait;
// include inline element parsing using traits
// TODO
/**
* @var bool whether special attributes on code blocks should be applied on the `<pre>` element.
* The default behavior is to put them on the `<code>` element.
*/
public $codeAttributesOnPre = false;
/**
* @inheritDoc
*/
protected $escapeCharacters = [
// from Markdown
'\\', // backslash
'`', // backtick
'*', // asterisk
'_', // underscore
'{', '}', // curly braces
'[', ']', // square brackets
'(', ')', // parentheses
'#', // hash mark
'+', // plus sign
'-', // minus sign (hyphen)
'.', // dot
'!', // exclamation mark
'<', '>',
// added by MarkdownExtra
':', // colon
'|', // pipe
];
private $_specialAttributesRegex = '\{(([#\.][A-z0-9-_]+\s*)+)\}';
// TODO allow HTML intended 3 spaces
// TODO add markdown inside HTML blocks
// TODO implement definition lists
// TODO implement footnotes
// TODO implement Abbreviations
// block parsing
protected function identifyReference($line)
{
return ($line[0] === ' ' || $line[0] === '[') && preg_match('/^ {0,3}\[[^\[](.*?)\]:\s*([^\s]+?)(?:\s+[\'"](.+?)[\'"])?\s*('.$this->_specialAttributesRegex.')?\s*$/', $line);
}
/**
* Consume link references
*/
protected function consumeReference($lines, $current)
{
while (isset($lines[$current]) && preg_match('/^ {0,3}\[(.+?)\]:\s*(.+?)(?:\s+[\(\'"](.+?)[\)\'"])?\s*('.$this->_specialAttributesRegex.')?\s*$/', $lines[$current], $matches)) {
$label = strtolower($matches[1]);
$this->references[$label] = [
'url' => $this->replaceEscape($matches[2]),
];
if (isset($matches[3])) {
$this->references[$label]['title'] = $matches[3];
} else {
// title may be on the next line
if (isset($lines[$current + 1]) && preg_match('/^\s+[\(\'"](.+?)[\)\'"]\s*$/', $lines[$current + 1], $matches)) {
$this->references[$label]['title'] = $matches[1];
$current++;
}
}
if (isset($matches[5])) {
$this->references[$label]['attributes'] = $matches[5];
}
$current++;
}
return [false, --$current];
}
/**
* Consume lines for a fenced code block
*/
protected function consumeFencedCode($lines, $current)
{
// consume until ```
$block = [
'code',
];
$line = trim($lines[$current]);
if (($pos = strrpos($line, '`')) === false) {
$pos = strrpos($line, '~');
}
$fence = substr($line, 0, $pos + 1);
$block['attributes'] = substr($line, $pos);
$content = [];
for($i = $current + 1, $count = count($lines); $i < $count; $i++) {
if (($pos = strpos($line = $lines[$i], $fence)) === false || $pos > 3) {
$content[] = $line;
} else {
break;
}
}
$block['content'] = implode("\n", $content);
return [$block, $i];
}
protected function renderCode($block)
{
$attributes = $this->renderAttributes($block);
return ($this->codeAttributesOnPre ? "<pre$attributes><code>" : "<pre><code$attributes>")
. htmlspecialchars($block['content'] . "\n", ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8')
. "</code></pre>\n";
}
/**
* Renders a headline
*/
protected function renderHeadline($block)
{
foreach($block['content'] as $i => $element) {
if ($element[0] === 'specialAttributes') {
unset($block['content'][$i]);
$block['attributes'] = $element[1];
}
}
$tag = 'h' . $block['level'];
$attributes = $this->renderAttributes($block);
return "<$tag$attributes>" . rtrim($this->renderAbsy($block['content']), "# \t") . "</$tag>\n";
}
protected function renderAttributes($block)
{
$html = [];
if (isset($block['attributes'])) {
$attributes = preg_split('/\s+/', $block['attributes'], -1, PREG_SPLIT_NO_EMPTY);
foreach($attributes as $attribute) {
if ($attribute[0] === '#') {
$html['id'] = substr($attribute, 1);
} else {
$html['class'][] = substr($attribute, 1);
}
}
}
$result = '';
foreach($html as $attr => $value) {
if (is_array($value)) {
$value = trim(implode(' ', $value));
}
if (!empty($value)) {
$result .= " $attr=\"$value\"";
}
}
return $result;
}
// inline parsing
/**
* @marker {
*/
protected function parseSpecialAttributes($text)
{
if (preg_match("~$this->_specialAttributesRegex~", $text, $matches)) {
return [['specialAttributes', $matches[1]], strlen($matches[0])];
}
return [['text', '{'], 1];
}
protected function renderSpecialAttributes($block)
{
return '{' . $block[1] . '}';
}
protected function parseInline($text)
{
$elements = parent::parseInline($text);
// merge special attribute elements to links and images as they are not part of the final absy later
$relatedElement = null;
foreach($elements as $i => $element) {
if ($element[0] === 'link' || $element[0] === 'image') {
$relatedElement = $i;
} elseif ($element[0] === 'specialAttributes') {
if ($relatedElement !== null) {
$elements[$relatedElement]['attributes'] = $element[1];
unset($elements[$i]);
}
$relatedElement = null;
} else {
$relatedElement = null;
}
}
return $elements;
}
protected function renderLink($block)
{
if (isset($block['refkey'])) {
if (($ref = $this->lookupReference($block['refkey'])) !== false) {
$block = array_merge($block, $ref);
} else {
if (strncmp($block['orig'], '[', 1) === 0) {
return '[' . $this->renderAbsy($this->parseInline(substr($block['orig'], 1)));
}
return $block['orig'];
}
}
$attributes = $this->renderAttributes($block);
return '<a href="' . htmlspecialchars($block['url'], ENT_COMPAT | ENT_HTML401, 'UTF-8') . '"'
. (empty($block['title']) ? '' : ' title="' . htmlspecialchars($block['title'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"')
. $attributes . '>' . $this->renderAbsy($block['text']) . '</a>';
}
protected function renderImage($block)
{
if (isset($block['refkey'])) {
if (($ref = $this->lookupReference($block['refkey'])) !== false) {
$block = array_merge($block, $ref);
} else {
if (strncmp($block['orig'], '![', 2) === 0) {
return '![' . $this->renderAbsy($this->parseInline(substr($block['orig'], 2)));
}
return $block['orig'];
}
}
$attributes = $this->renderAttributes($block);
return '<img src="' . htmlspecialchars($block['url'], ENT_COMPAT | ENT_HTML401, 'UTF-8') . '"'
. ' alt="' . htmlspecialchars($block['text'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"'
. (empty($block['title']) ? '' : ' title="' . htmlspecialchars($block['title'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"')
. $attributes . ($this->html5 ? '>' : ' />');
}
}

View File

@@ -0,0 +1,389 @@
<?php
/**
* @copyright Copyright (c) 2014 Carsten Brandt
* @license https://github.com/cebe/markdown/blob/master/LICENSE
* @link https://github.com/cebe/markdown#readme
*/
namespace cebe\markdown;
use ReflectionMethod;
/**
* A generic parser for markdown-like languages.
*
* @author Carsten Brandt <mail@cebe.cc>
*/
abstract class Parser
{
/**
* @var integer the maximum nesting level for language elements.
*/
public $maximumNestingLevel = 32;
/**
* @var array the current context the parser is in.
* TODO remove in favor of absy
*/
protected $context = [];
/**
* @var array these are "escapeable" characters. When using one of these prefixed with a
* backslash, the character will be outputted without the backslash and is not interpreted
* as markdown.
*/
protected $escapeCharacters = [
'\\', // backslash
];
private $_depth = 0;
/**
* Parses the given text considering the full language.
*
* This includes parsing block elements as well as inline elements.
*
* @param string $text the text to parse
* @return string parsed markup
*/
public function parse($text)
{
$this->prepare();
if (ltrim($text) === '') {
return '';
}
$text = str_replace(["\r\n", "\n\r", "\r"], "\n", $text);
$this->prepareMarkers($text);
$absy = $this->parseBlocks(explode("\n", $text));
$markup = $this->renderAbsy($absy);
$this->cleanup();
return $markup;
}
/**
* Parses a paragraph without block elements (block elements are ignored).
*
* @param string $text the text to parse
* @return string parsed markup
*/
public function parseParagraph($text)
{
$this->prepare();
if (ltrim($text) === '') {
return '';
}
$text = str_replace(["\r\n", "\n\r", "\r"], "\n", $text);
$this->prepareMarkers($text);
$absy = $this->parseInline($text);
$markup = $this->renderAbsy($absy);
$this->cleanup();
return $markup;
}
/**
* This method will be called before `parse()` and `parseParagraph()`.
* You can override it to do some initialization work.
*/
protected function prepare()
{
}
/**
* This method will be called after `parse()` and `parseParagraph()`.
* You can override it to do cleanup.
*/
protected function cleanup()
{
}
// block parsing
private $_blockTypes;
/**
* @return array a list of block element types available.
*/
protected function blockTypes()
{
if ($this->_blockTypes === null) {
// detect block types via "identify" functions
$reflection = new \ReflectionClass($this);
$this->_blockTypes = array_filter(array_map(function($method) {
$name = $method->getName();
return strncmp($name, 'identify', 8) === 0 ? strtolower(substr($name, 8)) : false;
}, $reflection->getMethods(ReflectionMethod::IS_PROTECTED)));
sort($this->_blockTypes);
}
return $this->_blockTypes;
}
/**
* Given a set of lines and an index of a current line it uses the registed block types to
* detect the type of this line.
* @param array $lines
* @param integer $current
* @return string name of the block type in lower case
*/
protected function detectLineType($lines, $current)
{
$line = $lines[$current];
$blockTypes = $this->blockTypes();
foreach($blockTypes as $blockType) {
if ($this->{'identify' . $blockType}($line, $lines, $current)) {
return $blockType;
}
}
// consider the line a normal paragraph if no other block type matches
return 'paragraph';
}
/**
* Parse block elements by calling `detectLineType()` to identify them
* and call consume function afterwards.
*/
protected function parseBlocks($lines)
{
if ($this->_depth >= $this->maximumNestingLevel) {
// maximum depth is reached, do not parse input
return [['text', implode("\n", $lines)]];
}
$this->_depth++;
$blocks = [];
// convert lines to blocks
for ($i = 0, $count = count($lines); $i < $count; $i++) {
$line = $lines[$i];
if ($line !== '' && rtrim($line) !== '') { // skip empty lines
// identify a blocks beginning and parse the content
list($block, $i) = $this->parseBlock($lines, $i);
if ($block !== false) {
$blocks[] = $block;
}
}
}
$this->_depth--;
return $blocks;
}
/**
* Parses the block at current line by identifying the block type and parsing the content
* @param $lines
* @param $current
* @return array Array of two elements, the first element contains the block,
* the second contains the next line index to be parsed.
*/
protected function parseBlock($lines, $current)
{
// identify block type for this line
$blockType = $this->detectLineType($lines, $current);
// call consume method for the detected block type to consume further lines
return $this->{'consume' . $blockType}($lines, $current);
}
protected function renderAbsy($blocks)
{
$output = '';
foreach ($blocks as $block) {
array_unshift($this->context, $block[0]);
$output .= $this->{'render' . $block[0]}($block);
array_shift($this->context);
}
return $output;
}
/**
* Consume lines for a paragraph
*
* @param $lines
* @param $current
* @return array
*/
protected function consumeParagraph($lines, $current)
{
// consume until newline
$content = [];
for ($i = $current, $count = count($lines); $i < $count; $i++) {
if (ltrim($lines[$i]) !== '') {
$content[] = $lines[$i];
} else {
break;
}
}
$block = [
'paragraph',
'content' => $this->parseInline(implode("\n", $content)),
];
return [$block, --$i];
}
/**
* Render a paragraph block
*
* @param $block
* @return string
*/
protected function renderParagraph($block)
{
return '<p>' . $this->renderAbsy($block['content']) . "</p>\n";
}
// inline parsing
/**
* @var array the set of inline markers to use in different contexts.
*/
private $_inlineMarkers = [];
/**
* Returns a map of inline markers to the corresponding parser methods.
*
* This array defines handler methods for inline markdown markers.
* When a marker is found in the text, the handler method is called with the text
* starting at the position of the marker.
*
* Note that markers starting with whitespace may slow down the parser,
* you may want to use [[renderText]] to deal with them.
*
* You may override this method to define a set of markers and parsing methods.
* The default implementation looks for protected methods starting with `parse` that
* also have an `@marker` annotation in PHPDoc.
*
* @return array a map of markers to parser methods
*/
protected function inlineMarkers()
{
$markers = [];
// detect "parse" functions
$reflection = new \ReflectionClass($this);
foreach($reflection->getMethods(ReflectionMethod::IS_PROTECTED) as $method) {
$methodName = $method->getName();
if (strncmp($methodName, 'parse', 5) === 0) {
preg_match_all('/@marker ([^\s]+)/', $method->getDocComment(), $matches);
foreach($matches[1] as $match) {
$markers[$match] = $methodName;
}
}
}
return $markers;
}
/**
* Prepare markers that are used in the text to parse
*
* Add all markers that are present in markdown.
* Check is done to avoid iterations in parseInline(), good for huge markdown files
* @param string $text
*/
protected function prepareMarkers($text)
{
$this->_inlineMarkers = [];
foreach ($this->inlineMarkers() as $marker => $method) {
if (strpos($text, $marker) !== false) {
$m = $marker[0];
// put the longest marker first
if (isset($this->_inlineMarkers[$m])) {
reset($this->_inlineMarkers[$m]);
if (strlen($marker) > strlen(key($this->_inlineMarkers[$m]))) {
$this->_inlineMarkers[$m] = array_merge([$marker => $method], $this->_inlineMarkers[$m]);
continue;
}
}
$this->_inlineMarkers[$m][$marker] = $method;
}
}
}
/**
* Parses inline elements of the language.
*
* @param string $text the inline text to parse.
* @return array
*/
protected function parseInline($text)
{
if ($this->_depth >= $this->maximumNestingLevel) {
// maximum depth is reached, do not parse input
return [['text', $text]];
}
$this->_depth++;
$markers = implode('', array_keys($this->_inlineMarkers));
$paragraph = [];
while (!empty($markers) && ($found = strpbrk($text, $markers)) !== false) {
$pos = strpos($text, $found);
// add the text up to next marker to the paragraph
if ($pos !== 0) {
$paragraph[] = ['text', substr($text, 0, $pos)];
}
$text = $found;
$parsed = false;
foreach ($this->_inlineMarkers[$text[0]] as $marker => $method) {
if (strncmp($text, $marker, strlen($marker)) === 0) {
// parse the marker
array_unshift($this->context, $method);
list($output, $offset) = $this->$method($text);
array_shift($this->context);
$paragraph[] = $output;
$text = substr($text, $offset);
$parsed = true;
break;
}
}
if (!$parsed) {
$paragraph[] = ['text', substr($text, 0, 1)];
$text = substr($text, 1);
}
}
$paragraph[] = ['text', $text];
$this->_depth--;
return $paragraph;
}
/**
* Parses escaped special characters.
* @marker \
*/
protected function parseEscape($text)
{
if (isset($text[1]) && in_array($text[1], $this->escapeCharacters)) {
return [['text', $text[1]], 2];
}
return [['text', $text[0]], 1];
}
/**
* This function renders plain text sections in the markdown text.
* It can be used to work on normal text sections for example to highlight keywords or
* do special escaping.
*/
protected function renderText($block)
{
return $block[1];
}
}

View File

@@ -0,0 +1,521 @@
A super fast, highly extensible markdown parser for PHP
=======================================================
[![Latest Stable Version](https://poser.pugx.org/cebe/markdown/v/stable.png)](https://packagist.org/packages/cebe/markdown)
[![Total Downloads](https://poser.pugx.org/cebe/markdown/downloads.png)](https://packagist.org/packages/cebe/markdown)
[![Build Status](https://travis-ci.org/cebe/markdown.svg?branch=master)](http://travis-ci.org/cebe/markdown)
[![Code Coverage](https://scrutinizer-ci.com/g/cebe/markdown/badges/coverage.png?s=db6af342d55bea649307ef311fbd536abb9bab76)](https://scrutinizer-ci.com/g/cebe/markdown/)
[![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/cebe/markdown/badges/quality-score.png?s=17448ca4d140429fd687c58ff747baeb6568d528)](https://scrutinizer-ci.com/g/cebe/markdown/)
What is this? <a name="what"></a>
-------------
A set of [PHP][] classes, each representing a [Markdown][] flavor, and a command line tool
for converting markdown files to HTML files.
The implementation focus is to be **fast** (see [benchmark][]) and **extensible**.
Parsing Markdown to HTML is as simple as calling a single method (see [Usage](#usage)) providing a solid implementation
that gives most expected results even in non-trivial edge cases.
Extending the Markdown language with new elements is as simple as adding a new method to the class that converts the
markdown text to the expected output in HTML. This is possible without dealing with complex and error prone regular expressions.
It is also possible to hook into the markdown structure and add elements or read meta information using the internal representation
of the Markdown text as an abstract syntax tree (see [Extending the language](#extend)).
Currently the following markdown flavors are supported:
- **Traditional Markdown** according to <http://daringfireball.net/projects/markdown/syntax> ([try it!](http://markdown.cebe.cc/try?flavor=default)).
- **Github flavored Markdown** according to <https://help.github.com/articles/github-flavored-markdown> ([try it!](http://markdown.cebe.cc/try?flavor=gfm)).
- **Markdown Extra** according to <http://michelf.ca/projects/php-markdown/extra/> (currently not fully supported WIP see [#25][], [try it!](http://markdown.cebe.cc/try?flavor=extra))
- Any mixed Markdown flavor you like because of its highly extensible structure (See documentation below).
Future plans are to support:
- Smarty Pants <http://daringfireball.net/projects/smartypants/>
- ... (Feel free to [suggest](https://github.com/cebe/markdown/issues/new) further additions!)
[PHP]: http://php.net/ "PHP is a popular general-purpose scripting language that is especially suited to web development."
[Markdown]: http://en.wikipedia.org/wiki/Markdown "Markdown on Wikipedia"
[#25]: https://github.com/cebe/markdown/issues/25 "issue #25"
[benchmark]: https://github.com/kzykhys/Markbench#readme "kzykhys/Markbench on github"
### Who is using it?
- It powers the [API-docs and the definitive guide](http://www.yiiframework.com/doc-2.0/) for the [Yii Framework][] [2.0](https://github.com/yiisoft/yii2).
[Yii Framework]: http://www.yiiframework.com/ "The Yii PHP Framework"
Installation <a name="installation"></a>
------------
[PHP 5.4 or higher](http://www.php.net/downloads.php) is required to use it.
It will also run on facebook's [hhvm](http://hhvm.com/).
The library uses PHPDoc annotations to determine the markdown elements that should be parsed.
So in case you are using PHP `opcache`, make sure
[it does not strip comments](http://php.net/manual/en/opcache.configuration.php#ini.opcache.save-comments).
Installation is recommended to be done via [composer][] by running:
composer require cebe/markdown "~1.2.0"
Alternatively you can add the following to the `require` section in your `composer.json` manually:
```json
"cebe/markdown": "~1.2.0"
```
Run `composer update` afterwards.
[composer]: https://getcomposer.org/ "The PHP package manager"
> Note: If you have configured PHP with opcache you need to enable the
> [opcache.save_comments](http://php.net/manual/en/opcache.configuration.php#ini.opcache.save-comments) option because inline element parsing relies on PHPdoc annotations to find declared elements.
Usage <a name="usage"></a>
-----
### In your PHP project
To parse your markdown you need only two lines of code. The first one is to choose the markdown flavor as
one of the following:
- Traditional Markdown: `$parser = new \cebe\markdown\Markdown();`
- Github Flavored Markdown: `$parser = new \cebe\markdown\GithubMarkdown();`
- Markdown Extra: `$parser = new \cebe\markdown\MarkdownExtra();`
The next step is to call the `parse()`-method for parsing the text using the full markdown language
or calling the `parseParagraph()`-method to parse only inline elements.
Here are some examples:
```php
// traditional markdown and parse full text
$parser = new \cebe\markdown\Markdown();
echo $parser->parse($markdown);
// use github markdown
$parser = new \cebe\markdown\GithubMarkdown();
echo $parser->parse($markdown);
// use markdown extra
$parser = new \cebe\markdown\MarkdownExtra();
echo $parser->parse($markdown);
// parse only inline elements (useful for one-line descriptions)
$parser = new \cebe\markdown\GithubMarkdown();
echo $parser->parseParagraph($markdown);
```
You may optionally set one of the following options on the parser object:
For all Markdown Flavors:
- `$parser->html5 = true` to enable HTML5 output instead of HTML4.
- `$parser->keepListStartNumber = true` to enable keeping the numbers of ordered lists as specified in the markdown.
The default behavior is to always start from 1 and increment by one regardless of the number in markdown.
For GithubMarkdown:
- `$parser->enableNewlines = true` to convert all newlines to `<br/>`-tags. By default only newlines with two preceding spaces are converted to `<br/>`-tags.
It is recommended to use UTF-8 encoding for the input strings. Other encodings may work, but are currently untested.
### The command line script
You can use it to render this readme:
bin/markdown README.md > README.html
Using github flavored markdown:
bin/markdown --flavor=gfm README.md > README.html
or convert the original markdown description to html using the unix pipe:
curl http://daringfireball.net/projects/markdown/syntax.text | bin/markdown > md.html
Here is the full Help output you will see when running `bin/markdown --help`:
PHP Markdown to HTML converter
------------------------------
by Carsten Brandt <mail@cebe.cc>
Usage:
bin/markdown [--flavor=<flavor>] [--full] [file.md]
--flavor specifies the markdown flavor to use. If omitted the original markdown by John Gruber [1] will be used.
Available flavors:
gfm - Github flavored markdown [2]
extra - Markdown Extra [3]
--full ouput a full HTML page with head and body. If not given, only the parsed markdown will be output.
--help shows this usage information.
If no file is specified input will be read from STDIN.
Examples:
Render a file with original markdown:
bin/markdown README.md > README.html
Render a file using gihtub flavored markdown:
bin/markdown --flavor=gfm README.md > README.html
Convert the original markdown description to html using STDIN:
curl http://daringfireball.net/projects/markdown/syntax.text | bin/markdown > md.html
[1] http://daringfireball.net/projects/markdown/syntax
[2] https://help.github.com/articles/github-flavored-markdown
[3] http://michelf.ca/projects/php-markdown/extra/
Extensions
----------
Here are some extensions to this library:
- [Bogardo/markdown-codepen](https://github.com/Bogardo/markdown-codepen) - shortcode to embed codepens from http://codepen.io/ in markdown.
- [kartik-v/yii2-markdown](https://github.com/kartik-v/yii2-markdown) - Advanced Markdown editing and conversion utilities for Yii Framework 2.0.
- [cebe/markdown-latex](https://github.com/cebe/markdown-latex) - Convert Markdown to LaTeX and PDF
- [softark/creole](https://github.com/softark/creole) - A creole markup parser
- [hyn/frontmatter](https://github.com/hyn/frontmatter) - Frontmatter Metadata Support (JSON, TOML, YAML)
- ... [add yours!](https://github.com/cebe/markdown/edit/master/README.md#L186)
Extending the language <a name="extend"></a>
----------------------
Markdown consists of two types of language elements, I'll call them block and inline elements simlar to what you have in
HTML with `<div>` and `<span>`. Block elements are normally spreads over several lines and are separated by blank lines.
The most basic block element is a paragraph (`<p>`).
Inline elements are elements that are added inside of block elements i.e. inside of text.
This markdown parser allows you to extend the markdown language by changing existing elements behavior and also adding
new block and inline elements. You do this by extending from the parser class and adding/overriding class methods and
properties. For the different element types there are different ways to extend them as you will see in the following sections.
### Adding block elements
The markdown is parsed line by line to identify each non-empty line as one of the block element types.
To identify a line as the beginning of a block element it calls all protected class methods who's name begins with `identify`.
An identify function returns true if it has identified the block element it is responsible for or false if not.
In the following example we will implement support for [fenced code blocks][] which are part of the github flavored markdown.
[fenced code blocks]: https://help.github.com/articles/github-flavored-markdown#fenced-code-blocks
"Fenced code block feature of github flavored markdown"
```php
<?php
class MyMarkdown extends \cebe\markdown\Markdown
{
protected function identifyFencedCode($line, $lines, $current)
{
// if a line starts with at least 3 backticks it is identified as a fenced code block
if (strncmp($line, '```', 3) === 0) {
return true;
}
return false;
}
// ...
}
```
In the above, `$line` is a string containing the content of the current line and is equal to `$lines[$current]`.
You may use `$lines` and `$current` to check other lines than the current line. In most cases you can ignore these parameters.
Parsing of a block element is done in two steps:
1. **Consuming** all the lines belonging to it. In most cases this is iterating over the lines starting from the identified
line until a blank line occurs. This step is implemented by a method named `consume{blockName}()` where `{blockName}`
is the same name as used for the identify function above. The consume method also takes the lines array
and the number of the current line. It will return two arguments: an array representing the block element in the abstract syntax tree
of the markdown document and the line number to parse next. In the abstract syntax array the first element refers to the name of
the element, all other array elements can be freely defined by yourself.
In our example we will implement it like this:
```php
protected function consumeFencedCode($lines, $current)
{
// create block array
$block = [
'fencedCode',
'content' => [],
];
$line = rtrim($lines[$current]);
// detect language and fence length (can be more than 3 backticks)
$fence = substr($line, 0, $pos = strrpos($line, '`') + 1);
$language = substr($line, $pos);
if (!empty($language)) {
$block['language'] = $language;
}
// consume all lines until ```
for($i = $current + 1, $count = count($lines); $i < $count; $i++) {
if (rtrim($line = $lines[$i]) !== $fence) {
$block['content'][] = $line;
} else {
// stop consuming when code block is over
break;
}
}
return [$block, $i];
}
```
2. **Rendering** the element. After all blocks have been consumed, they are being rendered using the
`render{elementName}()`-method where `elementName` refers to the name of the element in the abstract syntax tree:
```php
protected function renderFencedCode($block)
{
$class = isset($block['language']) ? ' class="language-' . $block['language'] . '"' : '';
return "<pre><code$class>" . htmlspecialchars(implode("\n", $block['content']) . "\n", ENT_NOQUOTES, 'UTF-8') . '</code></pre>';
}
```
You may also add code highlighting here. In general it would also be possible to render ouput in a different language than
HTML for example LaTeX.
### Adding inline elements
Adding inline elements is different from block elements as they are parsed using markers in the text.
An inline element is identified by a marker that marks the beginning of an inline element (e.g. `[` will mark a possible
beginning of a link or `` ` `` will mark inline code).
Parsing methods for inline elements are also protected and identified by the prefix `parse`. Additionally a `@marker` annotation
in PHPDoc is needed to register the parse function for one or multiple markers.
The method will then be called when a marker is found in the text. As an argument it takes the text starting at the position of the marker.
The parser method will return an array containing the element of the abstract sytnax tree and an offset of text it has
parsed from the input markdown. All text up to this offset will be removed from the markdown before the next marker will be searched.
As an example, we will add support for the [strikethrough][] feature of github flavored markdown:
[strikethrough]: https://help.github.com/articles/github-flavored-markdown#strikethrough "Strikethrough feature of github flavored markdown"
```php
<?php
class MyMarkdown extends \cebe\markdown\Markdown
{
/**
* @marker ~~
*/
protected function parseStrike($markdown)
{
// check whether the marker really represents a strikethrough (i.e. there is a closing ~~)
if (preg_match('/^~~(.+?)~~/', $markdown, $matches)) {
return [
// return the parsed tag as an element of the abstract syntax tree and call `parseInline()` to allow
// other inline markdown elements inside this tag
['strike', $this->parseInline($matches[1])],
// return the offset of the parsed text
strlen($matches[0])
];
}
// in case we did not find a closing ~~ we just return the marker and skip 2 characters
return [['text', '~~'], 2];
}
// rendering is the same as for block elements, we turn the abstract syntax array into a string.
protected function renderStrike($element)
{
return '<del>' . $this->renderAbsy($element[1]) . '</del>';
}
}
```
### Composing your own Markdown flavor
This markdown library is composed of traits so it is very easy to create your own markdown flavor by adding and/or removing
the single feature traits.
Designing your Markdown flavor consists of four steps:
1. Select a base class
2. Select language feature traits
3. Define escapeable characters
4. Optionally add custom rendering behavior
#### Select a base class
If you want to extend from a flavor and only add features you can use one of the existing classes
(`Markdown`, `GithubMarkdown` or `MarkdownExtra`) as your flavors base class.
If you want to define a subset of the markdown language, i.e. remove some of the features, you have to
extend your class from `Parser`.
#### Select language feature traits
The following shows the trait selection for traditional Markdown.
```php
class MyMarkdown extends Parser
{
// include block element parsing using traits
use block\CodeTrait;
use block\HeadlineTrait;
use block\HtmlTrait {
parseInlineHtml as private;
}
use block\ListTrait {
// Check Ul List before headline
identifyUl as protected identifyBUl;
consumeUl as protected consumeBUl;
}
use block\QuoteTrait;
use block\RuleTrait {
// Check Hr before checking lists
identifyHr as protected identifyAHr;
consumeHr as protected consumeAHr;
}
// include inline element parsing using traits
use inline\CodeTrait;
use inline\EmphStrongTrait;
use inline\LinkTrait;
/**
* @var boolean whether to format markup according to HTML5 spec.
* Defaults to `false` which means that markup is formatted as HTML4.
*/
public $html5 = false;
protected function prepare()
{
// reset references
$this->references = [];
}
// ...
}
```
In general, just adding the trait with `use` is enough, however in some cases some fine tuning is desired
to get most expected parsing results. Elements are detected in alphabetical order of their identification
function. This means that if a line starting with `-` could be a list or a horizontal rule, the preference has to be set
by renaming the identification function. This is what is done with renaming `identifyHr` to `identifyAHr`
and `identifyBUl` to `identifyBUl`. The consume function always has to have the same name as the identification function
so this has to be renamed too.
There is also a conflict for parsing of the `<` character. This could either be a link/email enclosed in `<` and `>`
or an inline HTML tag. In order to resolve this conflict when adding the `LinkTrait`, we need to hide the `parseInlineHtml`
method of the `HtmlTrait`.
If you use any trait that uses the `$html5` property to adjust its output you also need to define this property.
If you use the link trait it may be useful to implement `prepare()` as shown above to reset references before
parsing to ensure you get a reusable object.
#### Define escapeable characters
Depending on the language features you have chosen there is a different set of characters that can be escaped
using `\`. The following is the set of escapeable characters for traditional markdown, you can copy it to your class
as is.
```php
/**
* @var array these are "escapeable" characters. When using one of these prefixed with a
* backslash, the character will be outputted without the backslash and is not interpreted
* as markdown.
*/
protected $escapeCharacters = [
'\\', // backslash
'`', // backtick
'*', // asterisk
'_', // underscore
'{', '}', // curly braces
'[', ']', // square brackets
'(', ')', // parentheses
'#', // hash mark
'+', // plus sign
'-', // minus sign (hyphen)
'.', // dot
'!', // exclamation mark
'<', '>',
];
```
#### Add custom rendering behavior
Optionally you may also want to adjust rendering behavior by overriding some methods.
You may refer to the `consumeParagraph()` method of the `Markdown` and `GithubMarkdown` classes for some inspiration
which define different rules for which elements are allowed to interrupt a paragraph.
Acknowledgements <a name="ack"></a>
----------------
I'd like to thank [@erusev][] for creating [Parsedown][] which heavily influenced this work and provided
the idea of the line based parsing approach.
[@erusev]: https://github.com/erusev "Emanuil Rusev"
[Parsedown]: http://parsedown.org/ "The Parsedown PHP Markdown parser"
FAQ <a name="faq"></a>
---
### Why another markdown parser?
While reviewing PHP markdown parsers for choosing one to use bundled with the [Yii framework 2.0][]
I found that most of the implementations use regex to replace patterns instead
of doing real parsing. This way extending them with new language elements is quite hard
as you have to come up with a complex regex, that matches your addition but does not mess
with other elements. Such additions are very common as you see on github which supports referencing
issues, users and commits in the comments.
A [real parser][] should use context aware methods that walk trough the text and
parse the tokens as they find them. The only implentation that I have found that uses
this approach is [Parsedown][] which also shows that this implementation is [much faster][benchmark]
than the regex way. Parsedown however is an implementation that focuses on speed and implements
its own flavor (mainly github flavored markdown) in one class and at the time of this writing was
not easily extensible.
Given the situation above I decided to start my own implementation using the parsing approach
from Parsedown and making it extensible creating a class for each markdown flavor that extend each
other in the way that also the markdown languages extend each other.
This allows you to choose between markdown language flavors and also provides a way to compose your
own flavor picking the best things from all.
I chose this approach as it is easier to implement and also more intuitive approach compared
to using callbacks to inject functionallity into the parser.
[real parser]: http://en.wikipedia.org/wiki/Parsing#Types_of_parser
[Parsedown]: http://parsedown.org/ "The Parsedown PHP Markdown parser"
[Yii framework 2.0]: https://github.com/yiisoft/yii2
### Where do I report bugs or rendering issues?
Just [open an issue][] on github, post your markdown code and describe the problem. You may also attach screenshots of the rendered HTML result to describe your problem.
[open an issue]: https://github.com/cebe/markdown/issues/new
### How can I contribute to this library?
Check the [CONTRIBUTING.md](CONTRIBUTING.md) file for more info.
### Am I free to use this?
This library is open source and licensed under the [MIT License][]. This means that you can do whatever you want
with it as long as you mention my name and include the [license file][license]. Check the [license][] for details.
[MIT License]: http://opensource.org/licenses/MIT
[license]: https://github.com/cebe/markdown/blob/master/LICENSE
Contact
-------
Feel free to contact me using [email](mailto:mail@cebe.cc) or [twitter](https://twitter.com/cebe_cc).

View File

@@ -0,0 +1,170 @@
#!/usr/bin/env php
<?php
/**
* @copyright Copyright (c) 2014 Carsten Brandt
* @license https://github.com/cebe/markdown/blob/master/LICENSE
* @link https://github.com/cebe/markdown#readme
*/
$composerAutoload = [
__DIR__ . '/../vendor/autoload.php', // standalone with "composer install" run
__DIR__ . '/../../../autoload.php', // script is installed as a composer binary
];
foreach ($composerAutoload as $autoload) {
if (file_exists($autoload)) {
require($autoload);
break;
}
}
// Send all errors to stderr
ini_set('display_errors', 'stderr');
$flavor = 'cebe\\markdown\\Markdown';
$flavors = [
'gfm' => ['cebe\\markdown\\GithubMarkdown', __DIR__ . '/../GithubMarkdown.php'],
'extra' => ['cebe\\markdown\\MarkdownExtra', __DIR__ . '/../MarkdownExtra.php'],
];
$full = false;
$src = [];
foreach($argv as $k => $arg) {
if ($k == 0) {
continue;
}
if ($arg[0] == '-') {
$arg = explode('=', $arg);
switch($arg[0]) {
case '--flavor':
if (isset($arg[1])) {
if (isset($flavors[$arg[1]])) {
require($flavors[$arg[1]][1]);
$flavor = $flavors[$arg[1]][0];
} else {
error("Unknown flavor: " . $arg[1], "usage");
}
} else {
error("Incomplete argument --flavor!", "usage");
}
break;
case '--full':
$full = true;
break;
case '-h':
case '--help':
echo "PHP Markdown to HTML converter\n";
echo "------------------------------\n\n";
echo "by Carsten Brandt <mail@cebe.cc>\n\n";
usage();
break;
default:
error("Unknown argument " . $arg[0], "usage");
}
} else {
$src[] = $arg;
}
}
if (empty($src)) {
$markdown = file_get_contents("php://stdin");
} elseif (count($src) == 1) {
$file = reset($src);
if (!file_exists($file)) {
error("File does not exist:" . $file);
}
$markdown = file_get_contents($file);
} else {
error("Converting multiple files is not yet supported.", "usage");
}
/** @var cebe\markdown\Parser $md */
$md = new $flavor();
$markup = $md->parse($markdown);
if ($full) {
echo <<<HTML
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<style>
body { font-family: Arial, sans-serif; }
code { background: #eeeeff; padding: 2px; }
li { margin-bottom: 5px; }
img { max-width: 1200px; }
table, td, th { border: solid 1px #ccc; border-collapse: collapse; }
</style>
</head>
<body>
$markup
</body>
</html>
HTML;
} else {
echo $markup;
}
// functions
/**
* Display usage information
*/
function usage() {
global $argv;
$cmd = $argv[0];
echo <<<EOF
Usage:
$cmd [--flavor=<flavor>] [--full] [file.md]
--flavor specifies the markdown flavor to use. If omitted the original markdown by John Gruber [1] will be used.
Available flavors:
gfm - Github flavored markdown [2]
extra - Markdown Extra [3]
--full ouput a full HTML page with head and body. If not given, only the parsed markdown will be output.
--help shows this usage information.
If no file is specified input will be read from STDIN.
Examples:
Render a file with original markdown:
$cmd README.md > README.html
Render a file using gihtub flavored markdown:
$cmd --flavor=gfm README.md > README.html
Convert the original markdown description to html using STDIN:
curl http://daringfireball.net/projects/markdown/syntax.text | $cmd > md.html
[1] http://daringfireball.net/projects/markdown/syntax
[2] https://help.github.com/articles/github-flavored-markdown
[3] http://michelf.ca/projects/php-markdown/extra/
EOF;
exit(1);
}
/**
* Send custom error message to stderr
* @param $message string
* @param $callback mixed called before script exit
* @return void
*/
function error($message, $callback = null) {
$fe = fopen("php://stderr", "w");
fwrite($fe, "Error: " . $message . "\n");
if (is_callable($callback)) {
call_user_func($callback);
}
exit(1);
}

View File

@@ -0,0 +1,66 @@
<?php
/**
* @copyright Copyright (c) 2014 Carsten Brandt
* @license https://github.com/cebe/markdown/blob/master/LICENSE
* @link https://github.com/cebe/markdown#readme
*/
namespace cebe\markdown\block;
/**
* Adds the 4 space indented code blocks
*/
trait CodeTrait
{
/**
* identify a line as the beginning of a code block.
*/
protected function identifyCode($line)
{
// indentation >= 4 or one tab is code
return ($l = $line[0]) === ' ' && $line[1] === ' ' && $line[2] === ' ' && $line[3] === ' ' || $l === "\t";
}
/**
* Consume lines for a code block element
*/
protected function consumeCode($lines, $current)
{
// consume until newline
$content = [];
for ($i = $current, $count = count($lines); $i < $count; $i++) {
$line = $lines[$i];
// a line is considered to belong to this code block as long as it is intended by 4 spaces or a tab
if (isset($line[0]) && ($line[0] === "\t" || strncmp($line, ' ', 4) === 0)) {
$line = $line[0] === "\t" ? substr($line, 1) : substr($line, 4);
$content[] = $line;
// but also if it is empty and the next line is intended by 4 spaces or a tab
} elseif (($line === '' || rtrim($line) === '') && isset($lines[$i + 1][0]) &&
($lines[$i + 1][0] === "\t" || strncmp($lines[$i + 1], ' ', 4) === 0)) {
if ($line !== '') {
$line = $line[0] === "\t" ? substr($line, 1) : substr($line, 4);
}
$content[] = $line;
} else {
break;
}
}
$block = [
'code',
'content' => implode("\n", $content),
];
return [$block, --$i];
}
/**
* Renders a code block
*/
protected function renderCode($block)
{
$class = isset($block['language']) ? ' class="language-' . $block['language'] . '"' : '';
return "<pre><code$class>" . htmlspecialchars($block['content'] . "\n", ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8') . "</code></pre>\n";
}
}

View File

@@ -0,0 +1,58 @@
<?php
/**
* @copyright Copyright (c) 2014 Carsten Brandt
* @license https://github.com/cebe/markdown/blob/master/LICENSE
* @link https://github.com/cebe/markdown#readme
*/
namespace cebe\markdown\block;
/**
* Adds the fenced code blocks
*
* automatically included 4 space indented code blocks
*/
trait FencedCodeTrait
{
use CodeTrait;
/**
* identify a line as the beginning of a fenced code block.
*/
protected function identifyFencedCode($line)
{
return ($line[0] === '`' && strncmp($line, '```', 3) === 0) ||
($line[0] === '~' && strncmp($line, '~~~', 3) === 0) ||
(isset($line[3]) && (
($line[3] === '`' && strncmp(ltrim($line), '```', 3) === 0) ||
($line[3] === '~' && strncmp(ltrim($line), '~~~', 3) === 0)
));
}
/**
* Consume lines for a fenced code block
*/
protected function consumeFencedCode($lines, $current)
{
$line = ltrim($lines[$current]);
$fence = substr($line, 0, $pos = strrpos($line, $line[0]) + 1);
$language = rtrim(substr($line, $pos));
// consume until end fence
$content = [];
for ($i = $current + 1, $count = count($lines); $i < $count; $i++) {
if (($pos = strpos($line = $lines[$i], $fence)) === false || $pos > 3) {
$content[] = $line;
} else {
break;
}
}
$block = [
'code',
'content' => implode("\n", $content),
];
if (!empty($language)) {
$block['language'] = $language;
}
return [$block, $i];
}
}

View File

@@ -0,0 +1,70 @@
<?php
/**
* @copyright Copyright (c) 2014 Carsten Brandt
* @license https://github.com/cebe/markdown/blob/master/LICENSE
* @link https://github.com/cebe/markdown#readme
*/
namespace cebe\markdown\block;
/**
* Adds the headline blocks
*/
trait HeadlineTrait
{
/**
* identify a line as a headline
*/
protected function identifyHeadline($line, $lines, $current)
{
return (
// heading with #
$line[0] === '#' && !preg_match('/^#\d+/', $line)
||
// underlined headline
!empty($lines[$current + 1]) &&
(($l = $lines[$current + 1][0]) === '=' || $l === '-') &&
preg_match('/^(\-+|=+)\s*$/', $lines[$current + 1])
);
}
/**
* Consume lines for a headline
*/
protected function consumeHeadline($lines, $current)
{
if ($lines[$current][0] === '#') {
// ATX headline
$level = 1;
while (isset($lines[$current][$level]) && $lines[$current][$level] === '#' && $level < 6) {
$level++;
}
$block = [
'headline',
'content' => $this->parseInline(trim($lines[$current], "# \t")),
'level' => $level,
];
return [$block, $current];
} else {
// underlined headline
$block = [
'headline',
'content' => $this->parseInline($lines[$current]),
'level' => $lines[$current + 1][0] === '=' ? 1 : 2,
];
return [$block, $current + 1];
}
}
/**
* Renders a headline
*/
protected function renderHeadline($block)
{
$tag = 'h' . $block['level'];
return "<$tag>" . $this->renderAbsy($block['content']) . "</$tag>\n";
}
abstract protected function parseInline($text);
abstract protected function renderAbsy($absy);
}

View File

@@ -0,0 +1,168 @@
<?php
/**
* @copyright Copyright (c) 2014 Carsten Brandt
* @license https://github.com/cebe/markdown/blob/master/LICENSE
* @link https://github.com/cebe/markdown#readme
*/
namespace cebe\markdown\block;
/**
* Adds inline and block HTML support
*/
trait HtmlTrait
{
/**
* @var array HTML elements considered as inline elements.
* @see http://www.w3.org/wiki/HTML/Elements#Text-level_semantics
*/
protected $inlineHtmlElements = [
'a', 'abbr', 'acronym',
'b', 'basefont', 'bdo', 'big', 'br', 'button', 'blink',
'cite', 'code',
'del', 'dfn',
'em',
'font',
'i', 'img', 'ins', 'input', 'iframe',
'kbd',
'label', 'listing',
'map', 'mark',
'nobr',
'object',
'q',
'rp', 'rt', 'ruby',
's', 'samp', 'script', 'select', 'small', 'spacer', 'span', 'strong', 'sub', 'sup',
'tt', 'var',
'u',
'wbr',
'time',
];
/**
* @var array HTML elements known to be self-closing.
*/
protected $selfClosingHtmlElements = [
'br', 'hr', 'img', 'input', 'nobr',
];
/**
* identify a line as the beginning of a HTML block.
*/
protected function identifyHtml($line, $lines, $current)
{
if ($line[0] !== '<' || isset($line[1]) && $line[1] == ' ') {
return false; // no html tag
}
if (strncmp($line, '<!--', 4) === 0) {
return true; // a html comment
}
$gtPos = strpos($lines[$current], '>');
$spacePos = strpos($lines[$current], ' ');
if ($gtPos === false && $spacePos === false) {
return false; // no html tag
} elseif ($spacePos === false) {
$tag = rtrim(substr($line, 1, $gtPos - 1), '/');
} else {
$tag = rtrim(substr($line, 1, min($gtPos, $spacePos) - 1), '/');
}
if (!ctype_alnum($tag) || in_array(strtolower($tag), $this->inlineHtmlElements)) {
return false; // no html tag or inline html tag
}
return true;
}
/**
* Consume lines for an HTML block
*/
protected function consumeHtml($lines, $current)
{
$content = [];
if (strncmp($lines[$current], '<!--', 4) === 0) { // html comment
for ($i = $current, $count = count($lines); $i < $count; $i++) {
$line = $lines[$i];
$content[] = $line;
if (strpos($line, '-->') !== false) {
break;
}
}
} else {
$tag = rtrim(substr($lines[$current], 1, min(strpos($lines[$current], '>'), strpos($lines[$current] . ' ', ' ')) - 1), '/');
$level = 0;
if (in_array($tag, $this->selfClosingHtmlElements)) {
$level--;
}
for ($i = $current, $count = count($lines); $i < $count; $i++) {
$line = $lines[$i];
$content[] = $line;
$level += substr_count($line, "<$tag") - substr_count($line, "</$tag>") - substr_count($line, "/>");
if ($level <= 0) {
break;
}
}
}
$block = [
'html',
'content' => implode("\n", $content),
];
return [$block, $i];
}
/**
* Renders an HTML block
*/
protected function renderHtml($block)
{
return $block['content'] . "\n";
}
/**
* Parses an & or a html entity definition.
* @marker &
*/
protected function parseEntity($text)
{
// html entities e.g. &copy; &#169; &#x00A9;
if (preg_match('/^&#?[\w\d]+;/', $text, $matches)) {
return [['inlineHtml', $matches[0]], strlen($matches[0])];
} else {
return [['text', '&amp;'], 1];
}
}
/**
* renders a html entity.
*/
protected function renderInlineHtml($block)
{
return $block[1];
}
/**
* Parses inline HTML.
* @marker <
*/
protected function parseInlineHtml($text)
{
if (strpos($text, '>') !== false) {
if (preg_match('~^</?(\w+\d?)( .*?)?>~s', $text, $matches)) {
// HTML tags
return [['inlineHtml', $matches[0]], strlen($matches[0])];
} elseif (preg_match('~^<!--.*?-->~s', $text, $matches)) {
// HTML comments
return [['inlineHtml', $matches[0]], strlen($matches[0])];
}
}
return [['text', '&lt;'], 1];
}
/**
* Escapes `>` characters.
* @marker >
*/
protected function parseGt($text)
{
return [['text', '&gt;'], 1];
}
}

View File

@@ -0,0 +1,202 @@
<?php
/**
* @copyright Copyright (c) 2014 Carsten Brandt
* @license https://github.com/cebe/markdown/blob/master/LICENSE
* @link https://github.com/cebe/markdown#readme
*/
namespace cebe\markdown\block;
/**
* Adds the list blocks
*/
trait ListTrait
{
/**
* @var bool enable support `start` attribute of ordered lists. This means that lists
* will start with the number you actually type in markdown and not the HTML generated one.
* Defaults to `false` which means that numeration of all ordered lists(<ol>) starts with 1.
*/
public $keepListStartNumber = false;
/**
* identify a line as the beginning of an ordered list.
*/
protected function identifyOl($line)
{
return (($l = $line[0]) > '0' && $l <= '9' || $l === ' ') && preg_match('/^ {0,3}\d+\.[ \t]/', $line);
}
/**
* identify a line as the beginning of an unordered list.
*/
protected function identifyUl($line)
{
$l = $line[0];
return ($l === '-' || $l === '+' || $l === '*') && (isset($line[1]) && (($l1 = $line[1]) === ' ' || $l1 === "\t")) ||
($l === ' ' && preg_match('/^ {0,3}[\-\+\*][ \t]/', $line));
}
/**
* Consume lines for an ordered list
*/
protected function consumeOl($lines, $current)
{
// consume until newline
$block = [
'list',
'list' => 'ol',
'attr' => [],
'items' => [],
];
return $this->consumeList($lines, $current, $block, 'ol');
}
/**
* Consume lines for an unordered list
*/
protected function consumeUl($lines, $current)
{
// consume until newline
$block = [
'list',
'list' => 'ul',
'items' => [],
];
return $this->consumeList($lines, $current, $block, 'ul');
}
private function consumeList($lines, $current, $block, $type)
{
$item = 0;
$indent = '';
$len = 0;
$lastLineEmpty = false;
// track the indentation of list markers, if indented more than previous element
// a list marker is considered to be long to a lower level
$leadSpace = 3;
$marker = $type === 'ul' ? ltrim($lines[$current])[0] : '';
for ($i = $current, $count = count($lines); $i < $count; $i++) {
$line = $lines[$i];
// match list marker on the beginning of the line
$pattern = ($type === 'ol') ? '/^( {0,'.$leadSpace.'})(\d+)\.[ \t]+/' : '/^( {0,'.$leadSpace.'})\\'.$marker.'[ \t]+/';
if (preg_match($pattern, $line, $matches)) {
if (($len = substr_count($matches[0], "\t")) > 0) {
$indent = str_repeat("\t", $len);
$line = substr($line, strlen($matches[0]));
} else {
$len = strlen($matches[0]);
$indent = str_repeat(' ', $len);
$line = substr($line, $len);
}
if ($i === $current) {
$leadSpace = strlen($matches[1]) + 1;
}
if ($type === 'ol' && $this->keepListStartNumber) {
// attr `start` for ol
if (!isset($block['attr']['start']) && isset($matches[2])) {
$block['attr']['start'] = $matches[2];
}
}
$block['items'][++$item][] = $line;
$block['lazyItems'][$item] = $lastLineEmpty;
$lastLineEmpty = false;
} elseif (ltrim($line) === '') {
// line is empty, may be a lazy list
$lastLineEmpty = true;
// two empty lines will end the list
if (!isset($lines[$i + 1][0])) {
break;
// next item is the continuation of this list -> lazy list
} elseif (preg_match($pattern, $lines[$i + 1])) {
$block['items'][$item][] = $line;
$block['lazyItems'][$item] = true;
// next item is indented as much as this list -> lazy list if it is not a reference
} elseif (strncmp($lines[$i + 1], $indent, $len) === 0 || !empty($lines[$i + 1]) && $lines[$i + 1][0] == "\t") {
$block['items'][$item][] = $line;
$nextLine = $lines[$i + 1][0] === "\t" ? substr($lines[$i + 1], 1) : substr($lines[$i + 1], $len);
$block['lazyItems'][$item] = empty($nextLine) || !method_exists($this, 'identifyReference') || !$this->identifyReference($nextLine);
// everything else ends the list
} else {
break;
}
} else {
if ($line[0] === "\t") {
$line = substr($line, 1);
} elseif (strncmp($line, $indent, $len) === 0) {
$line = substr($line, $len);
}
$block['items'][$item][] = $line;
$lastLineEmpty = false;
}
// if next line is <hr>, end the list
if (!empty($lines[$i + 1]) && method_exists($this, 'identifyHr') && $this->identifyHr($lines[$i + 1])) {
break;
}
}
foreach($block['items'] as $itemId => $itemLines) {
$content = [];
if (!$block['lazyItems'][$itemId]) {
$firstPar = [];
while (!empty($itemLines) && rtrim($itemLines[0]) !== '' && $this->detectLineType($itemLines, 0) === 'paragraph') {
$firstPar[] = array_shift($itemLines);
}
$content = $this->parseInline(implode("\n", $firstPar));
}
if (!empty($itemLines)) {
$content = array_merge($content, $this->parseBlocks($itemLines));
}
$block['items'][$itemId] = $content;
}
return [$block, $i];
}
/**
* Renders a list
*/
protected function renderList($block)
{
$type = $block['list'];
if (!empty($block['attr'])) {
$output = "<$type " . $this->generateHtmlAttributes($block['attr']) . ">\n";
} else {
$output = "<$type>\n";
}
foreach ($block['items'] as $item => $itemLines) {
$output .= '<li>' . $this->renderAbsy($itemLines). "</li>\n";
}
return $output . "</$type>\n";
}
/**
* Return html attributes string from [attrName => attrValue] list
* @param array $attributes the attribute name-value pairs.
* @return string
*/
private function generateHtmlAttributes($attributes)
{
foreach ($attributes as $name => $value) {
$attributes[$name] = "$name=\"$value\"";
}
return implode(' ', $attributes);
}
abstract protected function parseBlocks($lines);
abstract protected function parseInline($text);
abstract protected function renderAbsy($absy);
abstract protected function detectLineType($lines, $current);
}

View File

@@ -0,0 +1,63 @@
<?php
/**
* @copyright Copyright (c) 2014 Carsten Brandt
* @license https://github.com/cebe/markdown/blob/master/LICENSE
* @link https://github.com/cebe/markdown#readme
*/
namespace cebe\markdown\block;
/**
* Adds the block quote elements
*/
trait QuoteTrait
{
/**
* identify a line as the beginning of a block quote.
*/
protected function identifyQuote($line)
{
return $line[0] === '>' && (!isset($line[1]) || ($l1 = $line[1]) === ' ' || $l1 === "\t");
}
/**
* Consume lines for a blockquote element
*/
protected function consumeQuote($lines, $current)
{
// consume until newline
$content = [];
for ($i = $current, $count = count($lines); $i < $count; $i++) {
$line = $lines[$i];
if (ltrim($line) !== '') {
if ($line[0] == '>' && !isset($line[1])) {
$line = '';
} elseif (strncmp($line, '> ', 2) === 0) {
$line = substr($line, 2);
}
$content[] = $line;
} else {
break;
}
}
$block = [
'quote',
'content' => $this->parseBlocks($content),
'simple' => true,
];
return [$block, $i];
}
/**
* Renders a blockquote
*/
protected function renderQuote($block)
{
return '<blockquote>' . $this->renderAbsy($block['content']) . "</blockquote>\n";
}
abstract protected function parseBlocks($lines);
abstract protected function renderAbsy($absy);
}

View File

@@ -0,0 +1,40 @@
<?php
/**
* @copyright Copyright (c) 2014 Carsten Brandt
* @license https://github.com/cebe/markdown/blob/master/LICENSE
* @link https://github.com/cebe/markdown#readme
*/
namespace cebe\markdown\block;
/**
* Adds horizontal rules
*/
trait RuleTrait
{
/**
* identify a line as a horizontal rule.
*/
protected function identifyHr($line)
{
// at least 3 of -, * or _ on one line make a hr
return (($l = $line[0]) === ' ' || $l === '-' || $l === '*' || $l === '_') && preg_match('/^ {0,3}([\-\*_])\s*\1\s*\1(\1|\s)*$/', $line);
}
/**
* Consume a horizontal rule
*/
protected function consumeHr($lines, $current)
{
return [['hr'], $current];
}
/**
* Renders a horizontal rule
*/
protected function renderHr($block)
{
return $this->html5 ? "<hr>\n" : "<hr />\n";
}
}

View File

@@ -0,0 +1,156 @@
<?php
/**
* @copyright Copyright (c) 2014 Carsten Brandt
* @license https://github.com/cebe/markdown/blob/master/LICENSE
* @link https://github.com/cebe/markdown#readme
*/
namespace cebe\markdown\block;
/**
* Adds the table blocks
*/
trait TableTrait
{
/**
* identify a line as the beginning of a table block.
*/
protected function identifyTable($line, $lines, $current)
{
return strpos($line, '|') !== false && isset($lines[$current + 1])
&& preg_match('~^\\s*\\|?(\\s*:?-[\\-\\s]*:?\\s*\\|?)*\\s*$~', $lines[$current + 1])
&& strpos($lines[$current + 1], '|') !== false
&& isset($lines[$current + 2]) && trim($lines[$current + 1]) !== '';
}
/**
* Consume lines for a table
*/
protected function consumeTable($lines, $current)
{
// consume until newline
$block = [
'table',
'cols' => [],
'rows' => [],
];
for ($i = $current, $count = count($lines); $i < $count; $i++) {
$line = trim($lines[$i]);
// extract alignment from second line
if ($i == $current+1) {
$cols = explode('|', trim($line, ' |'));
foreach($cols as $col) {
$col = trim($col);
if (empty($col)) {
$block['cols'][] = '';
continue;
}
$l = ($col[0] === ':');
$r = (substr($col, -1, 1) === ':');
if ($l && $r) {
$block['cols'][] = 'center';
} elseif ($l) {
$block['cols'][] = 'left';
} elseif ($r) {
$block['cols'][] = 'right';
} else {
$block['cols'][] = '';
}
}
continue;
}
if ($line === '' || substr($lines[$i], 0, 4) === ' ') {
break;
}
if ($line[0] === '|') {
$line = substr($line, 1);
}
if (substr($line, -1, 1) === '|' && (substr($line, -2, 2) !== '\\|' || substr($line, -3, 3) === '\\\\|')) {
$line = substr($line, 0, -1);
}
array_unshift($this->context, 'table');
$row = $this->parseInline($line);
array_shift($this->context);
$r = count($block['rows']);
$c = 0;
$block['rows'][] = [];
foreach ($row as $absy) {
if (!isset($block['rows'][$r][$c])) {
$block['rows'][$r][] = [];
}
if ($absy[0] === 'tableBoundary') {
$c++;
} else {
$block['rows'][$r][$c][] = $absy;
}
}
}
return [$block, --$i];
}
/**
* render a table block
*/
protected function renderTable($block)
{
$head = '';
$body = '';
$cols = $block['cols'];
$first = true;
foreach($block['rows'] as $row) {
$cellTag = $first ? 'th' : 'td';
$tds = '';
foreach ($row as $c => $cell) {
$align = empty($cols[$c]) ? '' : ' align="' . $cols[$c] . '"';
$tds .= "<$cellTag$align>" . trim($this->renderAbsy($cell)) . "</$cellTag>";
}
if ($first) {
$head .= "<tr>$tds</tr>\n";
} else {
$body .= "<tr>$tds</tr>\n";
}
$first = false;
}
return $this->composeTable($head, $body);
}
/**
* This method composes a table from parsed body and head HTML.
*
* You may override this method to customize the table rendering, for example by
* adding a `class` to the table tag:
*
* ```php
* return "<table class="table table-striped">\n<thead>\n$head</thead>\n<tbody>\n$body</tbody>\n</table>\n"
* ```
*
* @param string $head table head HTML.
* @param string $body table body HTML.
* @return string the complete table HTML.
* @since 1.2.0
*/
protected function composeTable($head, $body)
{
return "<table>\n<thead>\n$head</thead>\n<tbody>\n$body</tbody>\n</table>\n";
}
/**
* @marker |
*/
protected function parseTd($markdown)
{
if (isset($this->context[1]) && $this->context[1] === 'table') {
return [['tableBoundary'], isset($markdown[1]) && $markdown[1] === ' ' ? 2 : 1];
}
return [['text', $markdown[0]], 1];
}
abstract protected function parseInline($text);
abstract protected function renderAbsy($absy);
}

View File

@@ -0,0 +1,42 @@
{
"name": "cebe/markdown",
"description": "A super fast, highly extensible markdown parser for PHP",
"keywords": ["markdown", "gfm", "markdown-extra", "fast", "extensible"],
"homepage": "https://github.com/cebe/markdown#readme",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Carsten Brandt",
"email": "mail@cebe.cc",
"homepage": "http://cebe.cc/",
"role": "Creator"
}
],
"support": {
"issues": "https://github.com/cebe/markdown/issues",
"source": "https://github.com/cebe/markdown"
},
"require": {
"php": ">=5.4.0",
"lib-pcre": "*"
},
"require-dev": {
"phpunit/phpunit": "4.1.*",
"facebook/xhprof": "*@dev",
"cebe/indent": "*"
},
"autoload": {
"psr-4": {
"cebe\\markdown\\": ""
}
},
"bin": [
"bin/markdown"
],
"extra": {
"branch-alias": {
"dev-master": "1.2.x-dev"
}
}
}

View File

@@ -0,0 +1,45 @@
<?php
/**
* @copyright Copyright (c) 2014 Carsten Brandt
* @license https://github.com/cebe/markdown/blob/master/LICENSE
* @link https://github.com/cebe/markdown#readme
*/
namespace cebe\markdown\inline;
/**
* Adds inline code elements
*/
trait CodeTrait
{
/**
* Parses an inline code span `` ` ``.
* @marker `
*/
protected function parseInlineCode($text)
{
if (preg_match('/^(``+)\s(.+?)\s\1/s', $text, $matches)) { // code with enclosed backtick
return [
[
'inlineCode',
$matches[2],
],
strlen($matches[0])
];
} elseif (preg_match('/^`(.+?)`/s', $text, $matches)) {
return [
[
'inlineCode',
$matches[1],
],
strlen($matches[0])
];
}
return [['text', $text[0]], 1];
}
protected function renderInlineCode($block)
{
return '<code>' . htmlspecialchars($block[1], ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</code>';
}
}

View File

@@ -0,0 +1,87 @@
<?php
/**
* @copyright Copyright (c) 2014 Carsten Brandt
* @license https://github.com/cebe/markdown/blob/master/LICENSE
* @link https://github.com/cebe/markdown#readme
*/
namespace cebe\markdown\inline;
/**
* Adds inline emphasizes and strong elements
*/
trait EmphStrongTrait
{
/**
* Parses emphasized and strong elements.
* @marker _
* @marker *
*/
protected function parseEmphStrong($text)
{
$marker = $text[0];
if (!isset($text[1])) {
return [['text', $text[0]], 1];
}
if ($marker == $text[1]) { // strong
// work around a PHP bug that crashes with a segfault on too much regex backtrack
// check whether the end marker exists in the text
// https://github.com/erusev/parsedown/issues/443
// https://bugs.php.net/bug.php?id=45735
if (strpos($text, $marker . $marker, 2) === false) {
return [['text', $text[0] . $text[1]], 2];
}
if ($marker === '*' && preg_match('/^[*]{2}((?>\\\\[*]|[^*]|[*][^*]*[*])+?)[*]{2}/s', $text, $matches) ||
$marker === '_' && preg_match('/^__((?>\\\\_|[^_]|_[^_]*_)+?)__/us', $text, $matches)) {
return [
[
'strong',
$this->parseInline($matches[1]),
],
strlen($matches[0])
];
}
} else { // emph
// work around a PHP bug that crashes with a segfault on too much regex backtrack
// check whether the end marker exists in the text
// https://github.com/erusev/parsedown/issues/443
// https://bugs.php.net/bug.php?id=45735
if (strpos($text, $marker, 1) === false) {
return [['text', $text[0]], 1];
}
if ($marker === '*' && preg_match('/^[*]((?>\\\\[*]|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*][^*])/s', $text, $matches) ||
$marker === '_' && preg_match('/^_((?>\\\\_|[^_]|__[^_]*__)+?)_(?!_[^_])\b/us', $text, $matches)) {
// if only a single whitespace or nothing is contained in an emphasis, do not consider it valid
if ($matches[1] === '' || $matches[1] === ' ') {
return [['text', $text[0]], 1];
}
return [
[
'emph',
$this->parseInline($matches[1]),
],
strlen($matches[0])
];
}
}
return [['text', $text[0]], 1];
}
protected function renderStrong($block)
{
return '<strong>' . $this->renderAbsy($block[1]) . '</strong>';
}
protected function renderEmph($block)
{
return '<em>' . $this->renderAbsy($block[1]) . '</em>';
}
abstract protected function parseInline($text);
abstract protected function renderAbsy($blocks);
}

View File

@@ -0,0 +1,287 @@
<?php
/**
* @copyright Copyright (c) 2014 Carsten Brandt
* @license https://github.com/cebe/markdown/blob/master/LICENSE
* @link https://github.com/cebe/markdown#readme
*/
namespace cebe\markdown\inline;
// work around https://github.com/facebook/hhvm/issues/1120
defined('ENT_HTML401') || define('ENT_HTML401', 0);
/**
* Addes links and images as well as url markers.
*
* This trait conflicts with the HtmlTrait. If both are used together,
* you have to define a resolution, by defining the HtmlTrait::parseInlineHtml
* as private so it is not used directly:
*
* ```php
* use block\HtmlTrait {
* parseInlineHtml as private parseInlineHtml;
* }
* ```
*
* If the method exists it is called internally by this trait.
*
* Also make sure to reset references on prepare():
*
* ```php
* protected function prepare()
* {
* // reset references
* $this->references = [];
* }
* ```
*/
trait LinkTrait
{
/**
* @var array a list of defined references in this document.
*/
protected $references = [];
/**
* Remove backslash from escaped characters
* @param $text
* @return string
*/
protected function replaceEscape($text)
{
$strtr = [];
foreach($this->escapeCharacters as $char) {
$strtr["\\$char"] = $char;
}
return strtr($text, $strtr);
}
/**
* Parses a link indicated by `[`.
* @marker [
*/
protected function parseLink($markdown)
{
if (!in_array('parseLink', array_slice($this->context, 1)) && ($parts = $this->parseLinkOrImage($markdown)) !== false) {
list($text, $url, $title, $offset, $key) = $parts;
return [
[
'link',
'text' => $this->parseInline($text),
'url' => $url,
'title' => $title,
'refkey' => $key,
'orig' => substr($markdown, 0, $offset),
],
$offset
];
} else {
// remove all starting [ markers to avoid next one to be parsed as link
$result = '[';
$i = 1;
while (isset($markdown[$i]) && $markdown[$i] === '[') {
$result .= '[';
$i++;
}
return [['text', $result], $i];
}
}
/**
* Parses an image indicated by `![`.
* @marker ![
*/
protected function parseImage($markdown)
{
if (($parts = $this->parseLinkOrImage(substr($markdown, 1))) !== false) {
list($text, $url, $title, $offset, $key) = $parts;
return [
[
'image',
'text' => $text,
'url' => $url,
'title' => $title,
'refkey' => $key,
'orig' => substr($markdown, 0, $offset + 1),
],
$offset + 1
];
} else {
// remove all starting [ markers to avoid next one to be parsed as link
$result = '!';
$i = 1;
while (isset($markdown[$i]) && $markdown[$i] === '[') {
$result .= '[';
$i++;
}
return [['text', $result], $i];
}
}
protected function parseLinkOrImage($markdown)
{
if (strpos($markdown, ']') !== false && preg_match('/\[((?>[^\]\[]+|(?R))*)\]/', $markdown, $textMatches)) { // TODO improve bracket regex
$text = $textMatches[1];
$offset = strlen($textMatches[0]);
$markdown = substr($markdown, $offset);
$pattern = <<<REGEXP
/(?(R) # in case of recursion match parentheses
\(((?>[^\s()]+)|(?R))*\)
| # else match a link with title
^\(\s*(((?>[^\s()]+)|(?R))*)(\s+"(.*?)")?\s*\)
)/x
REGEXP;
if (preg_match($pattern, $markdown, $refMatches)) {
// inline link
return [
$text,
isset($refMatches[2]) ? $this->replaceEscape($refMatches[2]) : '', // url
empty($refMatches[5]) ? null: $refMatches[5], // title
$offset + strlen($refMatches[0]), // offset
null, // reference key
];
} elseif (preg_match('/^([ \n]?\[(.*?)\])?/s', $markdown, $refMatches)) {
// reference style link
if (empty($refMatches[2])) {
$key = strtolower($text);
} else {
$key = strtolower($refMatches[2]);
}
return [
$text,
null, // url
null, // title
$offset + strlen($refMatches[0]), // offset
$key,
];
}
}
return false;
}
/**
* Parses inline HTML.
* @marker <
*/
protected function parseLt($text)
{
if (strpos($text, '>') !== false) {
if (!in_array('parseLink', $this->context)) { // do not allow links in links
if (preg_match('/^<([^\s>]*?@[^\s]*?\.\w+?)>/', $text, $matches)) {
// email address
return [
['email', $this->replaceEscape($matches[1])],
strlen($matches[0])
];
} elseif (preg_match('/^<([a-z]{3,}:\/\/[^\s]+?)>/', $text, $matches)) {
// URL
return [
['url', $this->replaceEscape($matches[1])],
strlen($matches[0])
];
}
}
// try inline HTML if it was neither a URL nor email if HtmlTrait is included.
if (method_exists($this, 'parseInlineHtml')) {
return $this->parseInlineHtml($text);
}
}
return [['text', '&lt;'], 1];
}
protected function renderEmail($block)
{
$email = htmlspecialchars($block[1], ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8');
return "<a href=\"mailto:$email\">$email</a>";
}
protected function renderUrl($block)
{
$url = htmlspecialchars($block[1], ENT_COMPAT | ENT_HTML401, 'UTF-8');
$decodedUrl = urldecode($block[1]);
$secureUrlText = preg_match('//u', $decodedUrl) ? $decodedUrl : $block[1];
$text = htmlspecialchars($secureUrlText, ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8');
return "<a href=\"$url\">$text</a>";
}
protected function lookupReference($key)
{
$normalizedKey = preg_replace('/\s+/', ' ', $key);
if (isset($this->references[$key]) || isset($this->references[$key = $normalizedKey])) {
return $this->references[$key];
}
return false;
}
protected function renderLink($block)
{
if (isset($block['refkey'])) {
if (($ref = $this->lookupReference($block['refkey'])) !== false) {
$block = array_merge($block, $ref);
} else {
if (strncmp($block['orig'], '[', 1) === 0) {
return '[' . $this->renderAbsy($this->parseInline(substr($block['orig'], 1)));
}
return $block['orig'];
}
}
return '<a href="' . htmlspecialchars($block['url'], ENT_COMPAT | ENT_HTML401, 'UTF-8') . '"'
. (empty($block['title']) ? '' : ' title="' . htmlspecialchars($block['title'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"')
. '>' . $this->renderAbsy($block['text']) . '</a>';
}
protected function renderImage($block)
{
if (isset($block['refkey'])) {
if (($ref = $this->lookupReference($block['refkey'])) !== false) {
$block = array_merge($block, $ref);
} else {
if (strncmp($block['orig'], '![', 2) === 0) {
return '![' . $this->renderAbsy($this->parseInline(substr($block['orig'], 2)));
}
return $block['orig'];
}
}
return '<img src="' . htmlspecialchars($block['url'], ENT_COMPAT | ENT_HTML401, 'UTF-8') . '"'
. ' alt="' . htmlspecialchars($block['text'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"'
. (empty($block['title']) ? '' : ' title="' . htmlspecialchars($block['title'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"')
. ($this->html5 ? '>' : ' />');
}
// references
protected function identifyReference($line)
{
return isset($line[0]) && ($line[0] === ' ' || $line[0] === '[') && preg_match('/^ {0,3}\[[^\[](.*?)\]:\s*([^\s]+?)(?:\s+[\'"](.+?)[\'"])?\s*$/', $line);
}
/**
* Consume link references
*/
protected function consumeReference($lines, $current)
{
while (isset($lines[$current]) && preg_match('/^ {0,3}\[(.+?)\]:\s*(.+?)(?:\s+[\(\'"](.+?)[\)\'"])?\s*$/', $lines[$current], $matches)) {
$label = strtolower($matches[1]);
$this->references[$label] = [
'url' => $this->replaceEscape($matches[2]),
];
if (isset($matches[3])) {
$this->references[$label]['title'] = $matches[3];
} else {
// title may be on the next line
if (isset($lines[$current + 1]) && preg_match('/^\s+[\(\'"](.+?)[\)\'"]\s*$/', $lines[$current + 1], $matches)) {
$this->references[$label]['title'] = $matches[1];
$current++;
}
}
$current++;
}
return [false, --$current];
}
abstract protected function parseInline($text);
abstract protected function renderAbsy($blocks);
}

View File

@@ -0,0 +1,40 @@
<?php
/**
* @copyright Copyright (c) 2014 Carsten Brandt
* @license https://github.com/cebe/markdown/blob/master/LICENSE
* @link https://github.com/cebe/markdown#readme
*/
namespace cebe\markdown\inline;
/**
* Adds strikeout inline elements
*/
trait StrikeoutTrait
{
/**
* Parses the strikethrough feature.
* @marker ~~
*/
protected function parseStrike($markdown)
{
if (preg_match('/^~~(.+?)~~/', $markdown, $matches)) {
return [
[
'strike',
$this->parseInline($matches[1])
],
strlen($matches[0])
];
}
return [['text', $markdown[0] . $markdown[1]], 2];
}
protected function renderStrike($block)
{
return '<del>' . $this->renderAbsy($block[1]) . '</del>';
}
abstract protected function parseInline($text);
abstract protected function renderAbsy($blocks);
}

View File

@@ -0,0 +1,50 @@
<?php
/**
* @copyright Copyright (c) 2014 Carsten Brandt
* @license https://github.com/cebe/markdown/blob/master/LICENSE
* @link https://github.com/cebe/markdown#readme
*/
namespace cebe\markdown\inline;
// work around https://github.com/facebook/hhvm/issues/1120
defined('ENT_HTML401') || define('ENT_HTML401', 0);
/**
* Adds auto linking for URLs
*/
trait UrlLinkTrait
{
/**
* Parses urls and adds auto linking feature.
* @marker http
* @marker ftp
*/
protected function parseUrl($markdown)
{
$pattern = <<<REGEXP
/(?(R) # in case of recursion match parentheses
\(((?>[^\s()]+)|(?R))*\)
| # else match a link with title
^(https?|ftp):\/\/(([^\s<>()]+)|(?R))+(?<![\.,:;\'"!\?\s])
)/x
REGEXP;
if (!in_array('parseLink', $this->context) && preg_match($pattern, $markdown, $matches)) {
return [
['autoUrl', $matches[0]],
strlen($matches[0])
];
}
return [['text', substr($markdown, 0, 4)], 4];
}
protected function renderAutoUrl($block)
{
$href = htmlspecialchars($block[1], ENT_COMPAT | ENT_HTML401, 'UTF-8');
$decodedUrl = urldecode($block[1]);
$secureUrlText = preg_match('//u', $decodedUrl) ? $decodedUrl : $block[1];
$text = htmlspecialchars($secureUrlText, ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8');
return "<a href=\"$href\">$text</a>";
}
}

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<phpunit bootstrap="./tests/bootstrap.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
stopOnFailure="false">
<testsuites>
<testsuite name="Markdown Test Suite">
<file>./tests/ParserTest.php</file>
<file>./tests/MarkdownTest.php</file>
<file>./tests/MarkdownOLStartNumTest.php</file>
<file>./tests/GithubMarkdownTest.php</file>
<file>./tests/MarkdownExtraTest.php</file>
</testsuite>
</testsuites>
<filter>
<blacklist>
<directory>./vendor</directory>
<directory>./tests</directory>
</blacklist>
</filter>
</phpunit>

View File

@@ -0,0 +1,115 @@
<?php
/**
* @copyright Copyright (c) 2014 Carsten Brandt
* @license https://github.com/cebe/markdown/blob/master/LICENSE
* @link https://github.com/cebe/markdown#readme
*/
namespace cebe\markdown\tests;
use cebe\markdown\Parser;
/**
* Base class for all Test cases.
*
* @author Carsten Brandt <mail@cebe.cc>
*/
abstract class BaseMarkdownTest extends \PHPUnit_Framework_TestCase
{
protected $outputFileExtension = '.html';
abstract public function getDataPaths();
/**
* @return Parser
*/
abstract public function createMarkdown();
/**
* @dataProvider dataFiles
*/
public function testParse($path, $file)
{
list($markdown, $html) = $this->getTestData($path, $file);
// Different OS line endings should not affect test
$html = str_replace(["\r\n", "\n\r", "\r"], "\n", $html);
$m = $this->createMarkdown();
$this->assertEquals($html, $m->parse($markdown));
}
public function testUtf8()
{
$this->assertSame("<p>абвгдеёжзийклмнопрстуфхцчшщъыьэюя</p>\n", $this->createMarkdown()->parse('абвгдеёжзийклмнопрстуфхцчшщъыьэюя'));
$this->assertSame("<p>there is a charater, 配</p>\n", $this->createMarkdown()->parse('there is a charater, 配'));
$this->assertSame("<p>Arabic Latter \"م (M)\"</p>\n", $this->createMarkdown()->parse('Arabic Latter "م (M)"'));
$this->assertSame("<p>電腦</p>\n", $this->createMarkdown()->parse('電腦'));
$this->assertSame('абвгдеёжзийклмнопрстуфхцчшщъыьэюя', $this->createMarkdown()->parseParagraph('абвгдеёжзийклмнопрстуфхцчшщъыьэюя'));
$this->assertSame('there is a charater, 配', $this->createMarkdown()->parseParagraph('there is a charater, 配'));
$this->assertSame('Arabic Latter "م (M)"', $this->createMarkdown()->parseParagraph('Arabic Latter "م (M)"'));
$this->assertSame('電腦', $this->createMarkdown()->parseParagraph('電腦'));
}
public function testInvalidUtf8()
{
$m = $this->createMarkdown();
$this->assertEquals("<p><code><3E></code></p>\n", $m->parse("`\x80`"));
$this->assertEquals('<code><3E></code>', $m->parseParagraph("`\x80`"));
}
public function pregData()
{
// http://en.wikipedia.org/wiki/Newline#Representations
return [
["a\r\nb", "a\nb"],
["a\n\rb", "a\nb"], // Acorn BBC and RISC OS spooled text output :)
["a\nb", "a\nb"],
["a\rb", "a\nb"],
["a\n\nb", "a\n\nb", "a</p>\n<p>b"],
["a\r\rb", "a\n\nb", "a</p>\n<p>b"],
["a\n\r\n\rb", "a\n\nb", "a</p>\n<p>b"], // Acorn BBC and RISC OS spooled text output :)
["a\r\n\r\nb", "a\n\nb", "a</p>\n<p>b"],
];
}
/**
* @dataProvider pregData
*/
public function testPregReplaceR($input, $exptected, $pexpect = null)
{
$this->assertSame($exptected, $this->createMarkdown()->parseParagraph($input));
$this->assertSame($pexpect === null ? "<p>$exptected</p>\n" : "<p>$pexpect</p>\n", $this->createMarkdown()->parse($input));
}
public function getTestData($path, $file)
{
return [
file_get_contents($this->getDataPaths()[$path] . '/' . $file . '.md'),
file_get_contents($this->getDataPaths()[$path] . '/' . $file . $this->outputFileExtension),
];
}
public function dataFiles()
{
$files = [];
foreach ($this->getDataPaths() as $name => $src) {
$handle = opendir($src);
if ($handle === false) {
throw new \Exception('Unable to open directory: ' . $src);
}
while (($file = readdir($handle)) !== false) {
if ($file === '.' || $file === '..') {
continue;
}
if (substr($file, -3, 3) === '.md' && file_exists($src . '/' . substr($file, 0, -3) . $this->outputFileExtension)) {
$files[] = [$name, substr($file, 0, -3)];
}
}
closedir($handle);
}
return $files;
}
}

View File

@@ -0,0 +1,80 @@
<?php
/**
* @copyright Copyright (c) 2014 Carsten Brandt
* @license https://github.com/cebe/markdown/blob/master/LICENSE
* @link https://github.com/cebe/markdown#readme
*/
namespace cebe\markdown\tests;
use cebe\markdown\GithubMarkdown;
/**
* Test case for the github flavored markdown.
*
* @author Carsten Brandt <mail@cebe.cc>
* @group github
*/
class GithubMarkdownTest extends BaseMarkdownTest
{
public function createMarkdown()
{
return new GithubMarkdown();
}
public function getDataPaths()
{
return [
'markdown-data' => __DIR__ . '/markdown-data',
'github-data' => __DIR__ . '/github-data',
];
}
public function testNewlines()
{
$markdown = $this->createMarkdown();
$this->assertEquals("This is text<br />\nnewline\nnewline.", $markdown->parseParagraph("This is text \nnewline\nnewline."));
$markdown->enableNewlines = true;
$this->assertEquals("This is text<br />\nnewline<br />\nnewline.", $markdown->parseParagraph("This is text \nnewline\nnewline."));
$this->assertEquals("<p>This is text</p>\n<p>newline<br />\nnewline.</p>\n", $markdown->parse("This is text\n\nnewline\nnewline."));
}
public function dataFiles()
{
$files = parent::dataFiles();
foreach($files as $i => $f) {
// skip files that are different in github MD
if ($f[0] === 'markdown-data' && (
$f[1] === 'list-marker-in-paragraph' ||
$f[1] === 'dense-block-markers'
)) {
unset($files[$i]);
}
}
return $files;
}
public function testKeepZeroAlive()
{
$parser = $this->createMarkdown();
$this->assertEquals("0", $parser->parseParagraph("0"));
$this->assertEquals("<p>0</p>\n", $parser->parse("0"));
}
public function testAutoLinkLabelingWithEncodedUrl()
{
$parser = $this->createMarkdown();
$utfText = "\xe3\x81\x82\xe3\x81\x84\xe3\x81\x86\xe3\x81\x88\xe3\x81\x8a";
$utfNaturalUrl = "http://example.com/" . $utfText;
$utfEncodedUrl = "http://example.com/" . urlencode($utfText);
$eucEncodedUrl = "http://example.com/" . urlencode(mb_convert_encoding($utfText, 'EUC-JP', 'UTF-8'));
$this->assertStringEndsWith(">{$utfNaturalUrl}</a>", $parser->parseParagraph($utfNaturalUrl), "Natural UTF-8 URL needs no conversion.");
$this->assertStringEndsWith(">{$utfNaturalUrl}</a>", $parser->parseParagraph($utfEncodedUrl), "Encoded UTF-8 URL will be converted to readable format.");
$this->assertStringEndsWith(">{$eucEncodedUrl}</a>", $parser->parseParagraph($eucEncodedUrl), "Non UTF-8 URL should never be converted.");
// See: \cebe\markdown\inline\UrlLinkTrait::renderAutoUrl
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace cebe\markdown\tests;
use cebe\markdown\MarkdownExtra;
/**
* @author Carsten Brandt <mail@cebe.cc>
* @group extra
*/
class MarkdownExtraTest extends BaseMarkdownTest
{
public function createMarkdown()
{
return new MarkdownExtra();
}
public function getDataPaths()
{
return [
'markdown-data' => __DIR__ . '/markdown-data',
'extra-data' => __DIR__ . '/extra-data',
];
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* @copyright Copyright (c) 2014 Carsten Brandt
* @license https://github.com/cebe/markdown/blob/master/LICENSE
* @link https://github.com/cebe/markdown#readme
*/
namespace cebe\markdown\tests;
use cebe\markdown\Markdown;
/**
* Test support ordered lists at arbitrary number(`start` html attribute)
* @author Maxim Hodyrew <maximkou@gmail.com>
* @group default
*/
class MarkdownOLStartNumTest extends BaseMarkdownTest
{
public function createMarkdown()
{
$markdown = new Markdown();
$markdown->keepListStartNumber = true;
return $markdown;
}
public function getDataPaths()
{
return [
'markdown-data' => __DIR__ . '/markdown-ol-start-num-data',
];
}
}

View File

@@ -0,0 +1,60 @@
<?php
/**
* @copyright Copyright (c) 2014 Carsten Brandt
* @license https://github.com/cebe/markdown/blob/master/LICENSE
* @link https://github.com/cebe/markdown#readme
*/
namespace cebe\markdown\tests;
use cebe\markdown\Markdown;
/**
* Test case for traditional markdown.
*
* @author Carsten Brandt <mail@cebe.cc>
* @group default
*/
class MarkdownTest extends BaseMarkdownTest
{
public function createMarkdown()
{
return new Markdown();
}
public function getDataPaths()
{
return [
'markdown-data' => __DIR__ . '/markdown-data',
];
}
public function testEdgeCases()
{
$this->assertEquals("<p>&amp;</p>\n", $this->createMarkdown()->parse('&'));
$this->assertEquals("<p>&lt;</p>\n", $this->createMarkdown()->parse('<'));
}
public function testKeepZeroAlive()
{
$parser = $this->createMarkdown();
$this->assertEquals("0", $parser->parseParagraph("0"));
$this->assertEquals("<p>0</p>\n", $parser->parse("0"));
}
public function testAutoLinkLabelingWithEncodedUrl()
{
$parser = $this->createMarkdown();
$utfText = "\xe3\x81\x82\xe3\x81\x84\xe3\x81\x86\xe3\x81\x88\xe3\x81\x8a";
$utfNaturalUrl = "http://example.com/" . $utfText;
$utfEncodedUrl = "http://example.com/" . urlencode($utfText);
$eucEncodedUrl = "http://example.com/" . urlencode(mb_convert_encoding($utfText, 'EUC-JP', 'UTF-8'));
$this->assertStringEndsWith(">{$utfNaturalUrl}</a>", $parser->parseParagraph("<{$utfNaturalUrl}>"), "Natural UTF-8 URL needs no conversion.");
$this->assertStringEndsWith(">{$utfNaturalUrl}</a>", $parser->parseParagraph("<{$utfEncodedUrl}>"), "Encoded UTF-8 URL will be converted to readable format.");
$this->assertStringEndsWith(">{$eucEncodedUrl}</a>", $parser->parseParagraph("<{$eucEncodedUrl}>"), "Non UTF-8 URL should never be converted.");
// See: \cebe\markdown\inline\LinkTrait::renderUrl
}
}

View File

@@ -0,0 +1,94 @@
<?php
/**
* @copyright Copyright (c) 2014 Carsten Brandt
* @license https://github.com/cebe/markdown/blob/master/LICENSE
* @link https://github.com/cebe/markdown#readme
*/
namespace cebe\markdown\tests;
use cebe\markdown\Parser;
/**
* Test case for the parser base class.
*
* @author Carsten Brandt <mail@cebe.cc>
* @group default
*/
class ParserTest extends \PHPUnit_Framework_TestCase
{
public function testMarkerOrder()
{
$parser = new TestParser();
$parser->markers = [
'[' => 'parseMarkerA',
'[[' => 'parseMarkerB',
];
$this->assertEquals("<p>Result is A</p>\n", $parser->parse('Result is [abc]'));
$this->assertEquals("<p>Result is B</p>\n", $parser->parse('Result is [[abc]]'));
$this->assertEquals('Result is A', $parser->parseParagraph('Result is [abc]'));
$this->assertEquals('Result is B', $parser->parseParagraph('Result is [[abc]]'));
$parser = new TestParser();
$parser->markers = [
'[[' => 'parseMarkerB',
'[' => 'parseMarkerA',
];
$this->assertEquals("<p>Result is A</p>\n", $parser->parse('Result is [abc]'));
$this->assertEquals("<p>Result is B</p>\n", $parser->parse('Result is [[abc]]'));
$this->assertEquals('Result is A', $parser->parseParagraph('Result is [abc]'));
$this->assertEquals('Result is B', $parser->parseParagraph('Result is [[abc]]'));
}
public function testMaxNestingLevel()
{
$parser = new TestParser();
$parser->markers = [
'[' => 'parseMarkerC',
];
$parser->maximumNestingLevel = 3;
$this->assertEquals("(C-a(C-b(C-c)))", $parser->parseParagraph('[a[b[c]]]'));
$parser->maximumNestingLevel = 2;
$this->assertEquals("(C-a(C-b[c]))", $parser->parseParagraph('[a[b[c]]]'));
$parser->maximumNestingLevel = 1;
$this->assertEquals("(C-a[b[c]])", $parser->parseParagraph('[a[b[c]]]'));
}
public function testKeepZeroAlive()
{
$parser = new TestParser();
$this->assertEquals("0", $parser->parseParagraph("0"));
$this->assertEquals("<p>0</p>\n", $parser->parse("0"));
}
}
class TestParser extends Parser
{
public $markers = [];
protected function inlineMarkers()
{
return $this->markers;
}
protected function parseMarkerA($text)
{
return [['text', 'A'], strrpos($text, ']') + 1];
}
protected function parseMarkerB($text)
{
return [['text', 'B'], strrpos($text, ']') + 1];
}
protected function parseMarkerC($text)
{
$terminatingMarkerPos = strrpos($text, ']');
$inside = $this->parseInline(substr($text, 1, $terminatingMarkerPos - 1));
return [['text', '(C-' . $this->renderAbsy($inside) . ')'], $terminatingMarkerPos + 1];
}
}

View File

@@ -0,0 +1,5 @@
<?php
if (file_exists(__DIR__ . '/../vendor/autoload.php')) {
require(__DIR__ . '/../vendor/autoload.php');
}

View File

@@ -0,0 +1,82 @@
<ol>
<li><p>foo</p>
<pre><code>```
bar
blah
foo
```
</code></pre>
</li>
</ol>
<hr />
<ol>
<li><p>foo</p>
<pre><code> bar
blah
foo
</code></pre>
</li>
</ol>
<hr />
<ol>
<li><p>foo</p>
<pre><code> bar
blah
foo
</code></pre>
</li>
</ol>
<hr />
<ol>
<li><p>foo</p>
<pre><code> bar
blah
foo
</code></pre>
</li>
</ol>
<hr />
<ol>
<li><p>foo</p>
<pre><code>bar
blah
foo
</code></pre>
</li>
</ol>
<pre><code>foo
bar
</code></pre>
<pre><code>foo
bar
</code></pre>
<pre><code>foo
bar
</code></pre>
<pre><code>foo
bar
</code></pre>
<pre><code>foo
bar
</code></pre>
<pre><code>foo
bar
```
</code></pre>

View File

@@ -0,0 +1,94 @@
1. foo
```
bar
blah
foo
```
----
1. foo
```
bar
blah
foo
```
----
1. foo
```
bar
blah
foo
```
----
1. foo
```
bar
blah
foo
```
----
1. foo
```
bar
blah
foo
```
```
foo
bar
```
```
foo
bar
```
```
foo
bar
```
```
foo
bar
```
```
foo
bar
```
```
foo
bar
```

View File

@@ -0,0 +1,17 @@
<pre><code>
fenced code block
</code></pre>
<pre><code>
fenced with tildes
</code></pre>
<pre><code>long fence
```
code about code
```
</code></pre>
<pre><code class="html" id="test">fenced code block
</code></pre>

View File

@@ -0,0 +1,24 @@
```
fenced code block
```
~~~
fenced with tildes
~~~
``````````
long fence
```
code about code
```
``````````
``` .html #test
fenced code block
```

View File

@@ -0,0 +1,8 @@
<h2>Non-tables</h2>
<p>This line contains two pipes but is not a table. [[yii\widgets\DetailView|DetailView]] widget displays the details of a single data [[yii\widgets\DetailView::$model|model]].</p>
<p>the line above contains a space.</p>
<p>looks | like | head
-:</p>
<p>looks | like | head
-:
a</p>

View File

@@ -0,0 +1,13 @@
Non-tables
----------
This line contains two pipes but is not a table. [[yii\widgets\DetailView|DetailView]] widget displays the details of a single data [[yii\widgets\DetailView::$model|model]].
the line above contains a space.
looks | like | head
-:
looks | like | head
-:
a

View File

@@ -0,0 +1,12 @@
<h1 id="header1">Header 1</h1>
<h2 id="header2">Header 2</h2>
<h2 class="main">The Site</h2>
<h2 class="main shine" id="the-site">The Site</h2>
<p><a href="url" id="id1" class="class">link</a>
<img src="url" alt="img" id="id2" class="class" /></p>
<p><a href="http://url.de/" title="optional title" id="id" class="class">link</a> or <a href="http://url.de/" title="optional title" id="id" class="class">linkref</a>
<img src="http://url.de/" alt="img" title="optional title" id="id" class="class" /></p>
<p>this is just normal text {.main .shine #the-site}</p>
<p>some { brackets</p>
<p>some } brackets</p>
<p>some { } brackets</p>

View File

@@ -0,0 +1,25 @@
Header 1 {#header1}
========
## Header 2 ## {#header2}
## The Site ## {.main}
## The Site ## {.main .shine #the-site}
[link](url){#id1 .class}
![img](url){#id2 .class}
[link][linkref] or [linkref]
![img][linkref]
[linkref]: http://url.de/ "optional title" {#id .class}
this is just normal text {.main .shine #the-site}
some { brackets
some } brackets
some { } brackets

View File

@@ -0,0 +1,219 @@
<h2>Tables</h2>
<table>
<thead>
<tr><th>First Header</th><th>Second Header</th></tr>
</thead>
<tbody>
<tr><td>Content Cell</td><td>Content Cell</td></tr>
<tr><td>Content Cell</td><td>Content Cell</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>First Header</th><th>Second Header</th></tr>
</thead>
<tbody>
<tr><td>Content Cell</td><td>Content Cell</td></tr>
<tr><td>Content Cell</td><td>Content Cell</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>Name</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td>Help</td><td>Display the help window.</td></tr>
<tr><td>Close</td><td>Closes a window</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>Name</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td>Help</td><td><strong>Display the</strong> help window.</td></tr>
<tr><td>Close</td><td><em>Closes</em> a window</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>Default-Align</th><th align="left">Left-Aligned</th><th align="center">Center Aligned</th><th align="right">Right Aligned</th></tr>
</thead>
<tbody>
<tr><td>1</td><td align="left">col 3 is</td><td align="center">some wordy text</td><td align="right">$1600</td></tr>
<tr><td>2</td><td align="left">col 2 is</td><td align="center">centered</td><td align="right">$12</td></tr>
<tr><td>3</td><td align="left">zebra stripes</td><td align="center">are neat</td><td align="right">$1</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>Simple</th><th>Table</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>2</td></tr>
<tr><td>3</td><td>4</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>Simple</th><th>Table</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>2</td></tr>
<tr><td>3</td><td>4</td></tr>
<tr><td>3</td><td>4 |</td></tr>
<tr><td>3</td><td>4 \</td></tr>
</tbody>
</table>
<p>Check https://github.com/erusev/parsedown/issues/184 for the following:</p>
<table>
<thead>
<tr><th>Foo</th><th>Bar</th><th>State</th></tr>
</thead>
<tbody>
<tr><td><code>Code | Pipe</code></td><td>Broken</td><td>Blank</td></tr>
<tr><td><code>Escaped Code \| Pipe</code></td><td>Broken</td><td>Blank</td></tr>
<tr><td>Escaped | Pipe</td><td>Broken</td><td>Blank</td></tr>
<tr><td>Escaped \</td><td>Pipe</td><td>Broken</td><td>Blank</td></tr>
<tr><td>Escaped \</td><td>Pipe</td><td>Broken</td><td>Blank</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th align="left">Simple</th><th>Table</th></tr>
</thead>
<tbody>
<tr><td align="left">3</td><td>4</td></tr>
<tr><td align="left">3</td><td>4</td></tr>
<tr><td align="left">5</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>Mixed</th><th>Table</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>2</td></tr>
<tr><td>3</td><td>4</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>Mixed</th><th>Table</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>2</td></tr>
<tr><td>3</td><td>4</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>Mixed</th><th>Table</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>2</td></tr>
<tr><td>3</td><td>4</td></tr>
</tbody>
</table>
<p>some text</p>
<table>
<thead>
<tr><th>single col</th></tr>
</thead>
<tbody>
<tr><td>1</td></tr>
<tr><td>2</td></tr>
<tr><td>3</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>Table</th><th>With</th><th>Empty</th><th>Cells</th></tr>
</thead>
<tbody>
<tr><td></td><td></td><td></td><td></td></tr>
<tr><td>a</td><td></td><td>b</td><td></td></tr>
<tr><td></td><td>a</td><td></td><td>b</td></tr>
<tr><td>a</td><td></td><td></td><td>b</td></tr>
<tr><td></td><td>a</td><td>b</td><td></td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th></th></tr>
</thead>
<tbody>
<tr><td></td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th></th><th></th></tr>
</thead>
<tbody>
<tr><td></td><td></td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>Table</th><th>Indentation</th></tr>
</thead>
<tbody>
<tr><td>A</td><td>B</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>Table</th><th>Indentation</th></tr>
</thead>
<tbody>
<tr><td>A</td><td>B</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>Table</th><th>Indentation</th></tr>
</thead>
<tbody>
<tr><td>A</td><td>B</td></tr>
</tbody>
</table>
<pre><code>| Table | Indentation |
</code></pre>
<p> | ----- | ---- |
| A | B |</p>
<table>
<thead>
<tr><th align="left">Table</th><th>Indentation</th></tr>
</thead>
<tbody>
</tbody>
</table>
<pre><code>| A | B |
</code></pre>
<table>
<thead>
<tr><th>Item</th><th align="right">Value</th></tr>
</thead>
<tbody>
<tr><td>Computer</td><td align="right">$1600</td></tr>
<tr><td>Phone</td><td align="right">$12</td></tr>
<tr><td>Pipe</td><td align="right">$1</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th align="center">a</th><th align="center">b</th><th align="center">c</th></tr>
</thead>
<tbody>
<tr><td align="center">1</td><td align="center">2</td><td align="center">3</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th align="left">a</th><th align="center">b</th><th align="left">c</th><th align="center">d</th></tr>
</thead>
<tbody>
<tr><td align="left">1</td><td align="center">2</td><td align="left">3</td><td align="center">4</td></tr>
</tbody>
</table>

View File

@@ -0,0 +1,130 @@
Tables
------
First Header | Second Header
------------- | -------------
Content Cell | Content Cell
Content Cell | Content Cell
| First Header | Second Header |
| ------------- | ------------- |
| Content Cell | Content Cell |
| Content Cell | Content Cell |
| Name | Description |
| ------------- | ----------- |
| Help | Display the help window.|
| Close | Closes a window |
| Name | Description |
| ------------- | ----------- |
| Help | **Display the** help window.|
| Close | _Closes_ a window |
| Default-Align | Left-Aligned | Center Aligned | Right Aligned |
| ------------- | :------------ |:---------------:| -----:|
| 1 | col 3 is | some wordy text | $1600 |
| 2 | col 2 is | centered | $12 |
| 3 | zebra stripes | are neat | $1 |
Simple | Table
------ | -----
1 | 2
3 | 4
| Simple | Table |
| ------ | ----- |
| 1 | 2 |
| 3 | 4 |
| 3 | 4 \|
| 3 | 4 \\|
Check https://github.com/erusev/parsedown/issues/184 for the following:
Foo | Bar | State
------ | ------ | -----
`Code | Pipe` | Broken | Blank
`Escaped Code \| Pipe` | Broken | Blank
Escaped \| Pipe | Broken | Blank
Escaped \\| Pipe | Broken | Blank
Escaped \\ | Pipe | Broken | Blank
| Simple | Table |
| :----- | ----- |
| 3 | 4 |
3 | 4
5
Mixed | Table
------ | -----
| 1 | 2
3 | 4
| Mixed | Table
------ | -----
| 1 | 2
3 | 4
Mixed | Table
|------ | ----- |
1 | 2
| 3 | 4 |
some text
| single col |
| -- | -- |
| 1 |
2
3
| Table | With | Empty | Cells |
| ----- | ---- | ----- | ----- |
| | | | |
| a | | b | |
| | a | | b |
| a | | | b |
| | a | b | |
|
-- | --
|
| | |
| - | - |
| | |
| Table | Indentation |
| ----- | ---- |
| A | B |
| Table | Indentation |
| ----- | ---- |
| A | B |
| Table | Indentation |
| ----- | ---- |
| A | B |
| Table | Indentation |
| ----- | ---- |
| A | B |
| Table | Indentation |
| :----- | ---- |
| A | B |
| Item | Value |
| --------- | -----:|
| Computer | $1600 |
| Phone | $12 |
| Pipe | $1 |
| a | b | c |
|:-:|:-:|:-:|
| 1 | 2 | 3 |
| a | b | c | d |
|:--|:-:|:--|:-:|
| 1 | 2 | 3 | 4 |

View File

@@ -0,0 +1,8 @@
<p>Not a headline but a code block:</p>
<pre><code>---
</code></pre>
<p>Not a headline but two HR:</p>
<hr />
<hr />
<hr />
<hr />

View File

@@ -0,0 +1,14 @@
Not a headline but a code block:
```
---
```
Not a headline but two HR:
***
---
---
***

View File

@@ -0,0 +1,82 @@
<ol>
<li><p>foo</p>
<pre><code>```
bar
blah
foo
```
</code></pre>
</li>
</ol>
<hr />
<ol>
<li><p>foo</p>
<pre><code> bar
blah
foo
</code></pre>
</li>
</ol>
<hr />
<ol>
<li><p>foo</p>
<pre><code> bar
blah
foo
</code></pre>
</li>
</ol>
<hr />
<ol>
<li><p>foo</p>
<pre><code> bar
blah
foo
</code></pre>
</li>
</ol>
<hr />
<ol>
<li><p>foo</p>
<pre><code>bar
blah
foo
</code></pre>
</li>
</ol>
<pre><code>foo
bar
</code></pre>
<pre><code>foo
bar
</code></pre>
<pre><code>foo
bar
</code></pre>
<pre><code>foo
bar
</code></pre>
<pre><code>foo
bar
</code></pre>
<pre><code>foo
bar
```
</code></pre>

View File

@@ -0,0 +1,94 @@
1. foo
```
bar
blah
foo
```
----
1. foo
```
bar
blah
foo
```
----
1. foo
```
bar
blah
foo
```
----
1. foo
```
bar
blah
foo
```
----
1. foo
```
bar
blah
foo
```
```
foo
bar
```
```
foo
bar
```
```
foo
bar
```
```
foo
bar
```
```
foo
bar
```
```
foo
bar
```

View File

@@ -0,0 +1,5 @@
<p>this is <del>striked out</del> after</p>
<p><del>striked out</del></p>
<p>a line with ~~ in it ...</p>
<p>~~</p>
<p>~</p>

View File

@@ -0,0 +1,9 @@
this is ~~striked out~~ after
~~striked out~~
a line with ~~ in it ...
~~
~

View File

@@ -0,0 +1,52 @@
<h1>this is to test dense blocks (no newlines between them)</h1>
<hr />
<h2>what is Markdown?</h2>
<p>see <a href="http://en.wikipedia.org/wiki/Markdown">Wikipedia</a></p>
<h2>a h2</h2>
<p>paragraph</p>
<p>this is a paragraph, not a headline or list
next line</p>
<ul>
<li>whoo</li>
</ul>
<p>par</p>
<pre><code>code
code
</code></pre>
<p>par</p>
<h3>Tasks list</h3>
<ul>
<li>list items</li>
</ul>
<h2>headline1</h2>
<blockquote><p>quote
quote</p>
</blockquote>
<h2>headline2</h2>
<hr />
<h1>h1</h1>
<h2>h2</h2>
<hr />
<h3>h3</h3>
<ol>
<li>ol1</li>
<li>ol2</li>
</ol>
<h4>h4</h4>
<ul>
<li>listA</li>
<li>listB</li>
</ul>
<h5>h5</h5>
<h6>h6</h6>
<hr />
<hr />
<h2>changelog 1</h2>
<ul>
<li>17-Feb-2013 re-design</li>
</ul>
<hr />
<h2>changelog 2</h2>
<ul>
<li>17-Feb-2013 re-design</li>
</ul>

View File

@@ -0,0 +1,56 @@
# this is to test dense blocks (no newlines between them)
----
## what is Markdown?
see [Wikipedia][]
a h2
----
paragraph
this is a paragraph, not a headline or list
next line
- whoo
par
code
code
par
### Tasks list
- list items
headline1
---------
> quote
> quote
[Wikipedia]: http://en.wikipedia.org/wiki/Markdown
headline2
---------
----
# h1
## h2
---
### h3
1. ol1
2. ol2
#### h4
- listA
- listB
##### h5
###### h6
--------
----
## changelog 1
* 17-Feb-2013 re-design
----
## changelog 2
* 17-Feb-2013 re-design

View File

@@ -0,0 +1,23 @@
<p>Now we need to set:</p>
<pre><code class="language-php">'session' =&gt; [
'cookieParams' =&gt; [
'path' =&gt; '/path1/',
]
],
</code></pre>
<p>and</p>
<pre><code class="language-php">'session' =&gt; [
'cookieParams' =&gt; [
'path' =&gt; '/path2/',
]
],
</code></pre>
<p>In the following starts a Blockquote:</p>
<blockquote><p>this is a blockquote</p>
</blockquote>
<p>par</p>
<hr />
<p>par</p>
<p>This is some text</p>
<h1>Headline1</h1>
<p>more text</p>

View File

@@ -0,0 +1,27 @@
Now we need to set:
```php
'session' => [
'cookieParams' => [
'path' => '/path1/',
]
],
```
and
```php
'session' => [
'cookieParams' => [
'path' => '/path2/',
]
],
```
In the following starts a Blockquote:
> this is a blockquote
par
***
par
This is some text
# Headline1
more text

View File

@@ -0,0 +1,19 @@
<h1>GitHub Flavored Markdown</h1>
<h2>Multiple underscores in words</h2>
<p>do_this_and_do_that_and_another_thing</p>
<h2>URL autolinking</h2>
<p><a href="http://example.com">http://example.com</a></p>
<h2>Strikethrough</h2>
<p><del>Mistaken text.</del></p>
<h2>Fenced code blocks</h2>
<pre><code>function test() {
console.log("notice the blank line before this function?");
}
</code></pre>
<h2>Syntax highlighting</h2>
<pre><code class="language-ruby">require 'redcarpet'
markdown = Redcarpet.new("Hello World!")
puts markdown.to_html
</code></pre>
<pre><code>this is also code
</code></pre>

View File

@@ -0,0 +1,40 @@
GitHub Flavored Markdown
========================
Multiple underscores in words
-----------------------------
do_this_and_do_that_and_another_thing
URL autolinking
---------------
http://example.com
Strikethrough
-------------
~~Mistaken text.~~
Fenced code blocks
------------------
```
function test() {
console.log("notice the blank line before this function?");
}
```
Syntax highlighting
-------------------
```ruby
require 'redcarpet'
markdown = Redcarpet.new("Hello World!")
puts markdown.to_html
```
~~~
this is also code
~~~

View File

@@ -0,0 +1,12 @@
<ol>
<li>Item one.</li>
<li><p>Item two with some code:</p>
<pre><code>code one
</code></pre>
</li>
<li><p>Item three with code:</p>
<pre><code>code two
</code></pre>
</li>
</ol>
<p>Paragraph.</p>

View File

@@ -0,0 +1,14 @@
1. Item one.
2. Item two with some code:
```
code one
```
3. Item three with code:
```
code two
```
Paragraph.

View File

@@ -0,0 +1,117 @@
<h1>GitHub Flavored Markdown</h1>
<p><em>View the <a href="http://github.github.com/github-flavored-markdown/sample_content.html">source of this content</a>.</em></p>
<p>Let's get the whole "linebreak" thing out of the way. The next paragraph contains two phrases separated by a single newline character:</p>
<p>Roses are red
Violets are blue</p>
<p>The next paragraph has the same phrases, but now they are separated by two spaces and a newline character:</p>
<p>Roses are red<br />
Violets are blue</p>
<p>Oh, and one thing I cannot stand is the mangling of words with multiple underscores in them like perform_complicated_task or do_this_and_do_that_and_another_thing.</p>
<h2>A bit of the GitHub spice</h2>
<p>In addition to the changes in the previous section, certain references are auto-linked:</p>
<ul>
<li>SHA: be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2</li>
<li>User@SHA ref: mojombo@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2</li>
<li>User/Project@SHA: mojombo/god@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2</li>
<li>#Num: #1</li>
<li>User/#Num: mojombo#1</li>
<li>User/Project#Num: mojombo/god#1</li>
</ul>
<p>These are dangerous goodies though, and we need to make sure email addresses don't get mangled:</p>
<p>My email addy is tom@github.com.</p>
<h2>Math is hard, let's go shopping</h2>
<p>In first grade I learned that 5 &gt; 3 and 2 &lt; 7. Maybe some arrows. 1 -&gt; 2 -&gt; 3. 9 &lt;- 8 &lt;- 7.</p>
<p>Triangles man! a^2 + b^2 = c^2</p>
<h2>We all like making lists</h2>
<p>The above header should be an H2 tag. Now, for a list of fruits:</p>
<ul>
<li>Red Apples</li>
<li>Purple Grapes</li>
<li>Green Kiwifruits</li>
</ul>
<p>Let's get crazy:</p>
<ol>
<li><p>This is a list item with two paragraphs. Lorem ipsum dolor
sit amet, consectetuer adipiscing elit. Aliquam hendrerit
mi posuere lectus.</p>
<p>Vestibulum enim wisi, viverra nec, fringilla in, laoreet
vitae, risus. Donec sit amet nisl. Aliquam semper ipsum
sit amet velit.</p>
</li>
<li><p>Suspendisse id sem consectetuer libero luctus adipiscing.</p>
</li>
</ol>
<p>What about some code <strong>in</strong> a list? That's insane, right?</p>
<ol>
<li><p>In Ruby you can map like this:</p>
<pre><code> ['a', 'b'].map { |x| x.uppercase }
</code></pre>
</li>
<li><p>In Rails, you can do a shortcut:</p>
<pre><code> ['a', 'b'].map(&amp;:uppercase)
</code></pre>
</li>
</ol>
<p>Some people seem to like definition lists</p>
<dl>
<dt>Lower cost</dt>
<dd>The new version of this product costs significantly less than the previous one!</dd>
<dt>Easier to use</dt>
<dd>We've changed the product so that it's much easier to use!</dd>
</dl>
<h2>I am a robot</h2>
<p>Maybe you want to print <code>robot</code> to the console 1000 times. Why not?</p>
<pre><code>def robot_invasion
puts("robot " * 1000)
end
</code></pre>
<p>You see, that was formatted as code because it's been indented by four spaces.</p>
<p>How about we throw some angle braces and ampersands in there?</p>
<pre><code>&lt;div class="footer"&gt;
&amp;copy; 2004 Foo Corporation
&lt;/div&gt;
</code></pre>
<h2>Set in stone</h2>
<p>Preformatted blocks are useful for ASCII art:</p>
<pre>
,-.
, ,-. ,-.
/ \ ( )-( )
\ | ,.>-( )-<
\|,' ( )-( )
Y ___`-' `-'
|/__/ `-'
|
|
| -hrr-
___|_____________
</pre>
<h2>Playing the blame game</h2>
<p>If you need to blame someone, the best way to do so is by quoting them:</p>
<blockquote><p>I, at any rate, am convinced that He does not throw dice.</p>
</blockquote>
<p>Or perhaps someone a little less eloquent:</p>
<blockquote><p>I wish you'd have given me this written question ahead of time so I
could plan for it... I'm sure something will pop into my head here in
the midst of this press conference, with all the pressure of trying to
come up with answer, but it hadn't yet...</p>
<p>I don't want to sound like
I have made no mistakes. I'm confident I have. I just haven't - you
just put me under the spot here, and maybe I'm not as quick on my feet
as I should be in coming up with one.</p>
</blockquote>
<h2>Table for two</h2>
<table>
<tr>
<th>ID</th><th>Name</th><th>Rank</th>
</tr>
<tr>
<td>1</td><td>Tom Preston-Werner</td><td>Awesome</td>
</tr>
<tr>
<td>2</td><td>Albert Einstein</td><td>Nearly as awesome</td>
</tr>
</table>
<h2>Crazy linking action</h2>
<p>I get 10 times more traffic from <a href="http://google.com/" title="Google">Google</a> than from
<a href="http://search.yahoo.com/" title="Yahoo Search">Yahoo</a> or <a href="http://search.msn.com/" title="MSN Search">MSN</a>.</p>

View File

@@ -0,0 +1,159 @@
GitHub Flavored Markdown
================================
*View the [source of this content](http://github.github.com/github-flavored-markdown/sample_content.html).*
Let's get the whole "linebreak" thing out of the way. The next paragraph contains two phrases separated by a single newline character:
Roses are red
Violets are blue
The next paragraph has the same phrases, but now they are separated by two spaces and a newline character:
Roses are red
Violets are blue
Oh, and one thing I cannot stand is the mangling of words with multiple underscores in them like perform_complicated_task or do_this_and_do_that_and_another_thing.
A bit of the GitHub spice
-------------------------
In addition to the changes in the previous section, certain references are auto-linked:
* SHA: be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2
* User@SHA ref: mojombo@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2
* User/Project@SHA: mojombo/god@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2
* \#Num: #1
* User/#Num: mojombo#1
* User/Project#Num: mojombo/god#1
These are dangerous goodies though, and we need to make sure email addresses don't get mangled:
My email addy is tom@github.com.
Math is hard, let's go shopping
-------------------------------
In first grade I learned that 5 > 3 and 2 < 7. Maybe some arrows. 1 -> 2 -> 3. 9 <- 8 <- 7.
Triangles man! a^2 + b^2 = c^2
We all like making lists
------------------------
The above header should be an H2 tag. Now, for a list of fruits:
* Red Apples
* Purple Grapes
* Green Kiwifruits
Let's get crazy:
1. This is a list item with two paragraphs. Lorem ipsum dolor
sit amet, consectetuer adipiscing elit. Aliquam hendrerit
mi posuere lectus.
Vestibulum enim wisi, viverra nec, fringilla in, laoreet
vitae, risus. Donec sit amet nisl. Aliquam semper ipsum
sit amet velit.
2. Suspendisse id sem consectetuer libero luctus adipiscing.
What about some code **in** a list? That's insane, right?
1. In Ruby you can map like this:
['a', 'b'].map { |x| x.uppercase }
2. In Rails, you can do a shortcut:
['a', 'b'].map(&:uppercase)
Some people seem to like definition lists
<dl>
<dt>Lower cost</dt>
<dd>The new version of this product costs significantly less than the previous one!</dd>
<dt>Easier to use</dt>
<dd>We've changed the product so that it's much easier to use!</dd>
</dl>
I am a robot
------------
Maybe you want to print `robot` to the console 1000 times. Why not?
def robot_invasion
puts("robot " * 1000)
end
You see, that was formatted as code because it's been indented by four spaces.
How about we throw some angle braces and ampersands in there?
<div class="footer">
&copy; 2004 Foo Corporation
</div>
Set in stone
------------
Preformatted blocks are useful for ASCII art:
<pre>
,-.
, ,-. ,-.
/ \ ( )-( )
\ | ,.>-( )-<
\|,' ( )-( )
Y ___`-' `-'
|/__/ `-'
|
|
| -hrr-
___|_____________
</pre>
Playing the blame game
----------------------
If you need to blame someone, the best way to do so is by quoting them:
> I, at any rate, am convinced that He does not throw dice.
Or perhaps someone a little less eloquent:
> I wish you'd have given me this written question ahead of time so I
> could plan for it... I'm sure something will pop into my head here in
> the midst of this press conference, with all the pressure of trying to
> come up with answer, but it hadn't yet...
>
> I don't want to sound like
> I have made no mistakes. I'm confident I have. I just haven't - you
> just put me under the spot here, and maybe I'm not as quick on my feet
> as I should be in coming up with one.
Table for two
-------------
<table>
<tr>
<th>ID</th><th>Name</th><th>Rank</th>
</tr>
<tr>
<td>1</td><td>Tom Preston-Werner</td><td>Awesome</td>
</tr>
<tr>
<td>2</td><td>Albert Einstein</td><td>Nearly as awesome</td>
</tr>
</table>
Crazy linking action
--------------------
I get 10 times more traffic from [Google] [1] than from
[Yahoo] [2] or [MSN] [3].
[1]: http://google.com/ "Google"
[2]: http://search.yahoo.com/ "Yahoo Search"
[3]: http://search.msn.com/ "MSN Search"

View File

@@ -0,0 +1,5 @@
<pre><code>hey, check [this].
[this]: https://github.com/cebe/markdown
</code></pre>
<p>is a vaild reference.</p>

View File

@@ -0,0 +1,6 @@
```
hey, check [this].
[this]: https://github.com/cebe/markdown
```
is a vaild reference.

View File

@@ -0,0 +1,21 @@
<blockquote><p>some text
`<code>`
// some code
\</code>``</p>
</blockquote>
<blockquote><p>some text</p>
<pre><code>// some code
</code></pre>
</blockquote>
<blockquote><p>some text</p>
<pre><code>// some code
</code></pre>
</blockquote>
<blockquote><p>some text</p>
<pre><code>// some code
</code></pre>
</blockquote>
<blockquote><p>some text</p>
</blockquote>
<pre><code>// some code
</code></pre>

View File

@@ -0,0 +1,26 @@
> some text
\```
// some code
\```
> some text
```
// some code
```
> some text
> ```
// some code
```
> some text
>
> ```
// some code
```
> some text
```
// some code
```

View File

@@ -0,0 +1,9 @@
<p>table of cronjobs should not result in empty ems:</p>
<table>
<thead>
<tr><th>cron</th></tr>
</thead>
<tbody>
<tr><td>* * * * *</td></tr>
</tbody>
</table>

View File

@@ -0,0 +1,5 @@
table of cronjobs should not result in empty ems:
| cron |
| ------- |
| * * * * * |

View File

@@ -0,0 +1,16 @@
<p>Text before list:</p>
<ul>
<li>item 1,</li>
<li>item 2,</li>
<li>item 3.</li>
</ul>
<p>Text after list.</p>
<ul>
<li>test</li>
<li>test<ul>
<li>test</li>
<li>test</li>
</ul>
</li>
<li>test</li>
</ul>

View File

@@ -0,0 +1,12 @@
Text before list:
* item 1,
* item 2,
* item 3.
Text after list.
- test
- test
- test
- test
- test

View File

@@ -0,0 +1,8 @@
<h2>Non-tables</h2>
<p>This line contains two pipes but is not a table. [[yii\widgets\DetailView|DetailView]] widget displays the details of a single data [[yii\widgets\DetailView::$model|model]].</p>
<p>the line above contains a space.</p>
<p>looks | like | head
-:</p>
<p>looks | like | head
-:
a</p>

View File

@@ -0,0 +1,13 @@
Non-tables
----------
This line contains two pipes but is not a table. [[yii\widgets\DetailView|DetailView]] widget displays the details of a single data [[yii\widgets\DetailView::$model|model]].
the line above contains a space.
looks | like | head
-:
looks | like | head
-:
a

View File

@@ -0,0 +1,203 @@
<h2>Tables</h2>
<table>
<thead>
<tr><th>First Header</th><th>Second Header</th></tr>
</thead>
<tbody>
<tr><td>Content Cell</td><td>Content Cell</td></tr>
<tr><td>Content Cell</td><td>Content Cell</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>First Header</th><th>Second Header</th></tr>
</thead>
<tbody>
<tr><td>Content Cell</td><td>Content Cell</td></tr>
<tr><td>Content Cell</td><td>Content Cell</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>Name</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td>Help</td><td>Display the help window.</td></tr>
<tr><td>Close</td><td>Closes a window</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>Name</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td>Help</td><td><strong>Display the</strong> help window.</td></tr>
<tr><td>Close</td><td><em>Closes</em> a window</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>Default-Align</th><th align="left">Left-Aligned</th><th align="center">Center Aligned</th><th align="right">Right Aligned</th></tr>
</thead>
<tbody>
<tr><td>1</td><td align="left">col 3 is</td><td align="center">some wordy text</td><td align="right">$1600</td></tr>
<tr><td>2</td><td align="left">col 2 is</td><td align="center">centered</td><td align="right">$12</td></tr>
<tr><td>3</td><td align="left">zebra stripes</td><td align="center">are neat</td><td align="right">$1</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>Simple</th><th>Table</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>2</td></tr>
<tr><td>3</td><td>4</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>Simple</th><th>Table</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>2</td></tr>
<tr><td>3</td><td>4</td></tr>
<tr><td>3</td><td>4 |</td></tr>
<tr><td>3</td><td>4 \</td></tr>
</tbody>
</table>
<p>Check <a href="https://github.com/erusev/parsedown/issues/184">https://github.com/erusev/parsedown/issues/184</a> for the following:</p>
<table>
<thead>
<tr><th>Foo</th><th>Bar</th><th>State</th></tr>
</thead>
<tbody>
<tr><td><code>Code | Pipe</code></td><td>Broken</td><td>Blank</td></tr>
<tr><td><code>Escaped Code \| Pipe</code></td><td>Broken</td><td>Blank</td></tr>
<tr><td>Escaped | Pipe</td><td>Broken</td><td>Blank</td></tr>
<tr><td>Escaped \</td><td>Pipe</td><td>Broken</td><td>Blank</td></tr>
<tr><td>Escaped \</td><td>Pipe</td><td>Broken</td><td>Blank</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th align="left">Simple</th><th>Table</th></tr>
</thead>
<tbody>
<tr><td align="left">3</td><td>4</td></tr>
<tr><td align="left">3</td><td>4</td></tr>
<tr><td align="left">5</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>Mixed</th><th>Table</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>2</td></tr>
<tr><td>3</td><td>4</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>Mixed</th><th>Table</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>2</td></tr>
<tr><td>3</td><td>4</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>Mixed</th><th>Table</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>2</td></tr>
<tr><td>3</td><td>4</td></tr>
</tbody>
</table>
<p>some text</p>
<table>
<thead>
<tr><th>single col</th></tr>
</thead>
<tbody>
<tr><td>1</td></tr>
<tr><td>2</td></tr>
<tr><td>3</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>Table</th><th>With</th><th>Empty</th><th>Cells</th></tr>
</thead>
<tbody>
<tr><td></td><td></td><td></td><td></td></tr>
<tr><td>a</td><td></td><td>b</td><td></td></tr>
<tr><td></td><td>a</td><td></td><td>b</td></tr>
<tr><td>a</td><td></td><td></td><td>b</td></tr>
<tr><td></td><td>a</td><td>b</td><td></td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th></th></tr>
</thead>
<tbody>
<tr><td></td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th></th><th></th></tr>
</thead>
<tbody>
<tr><td></td><td></td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>Table</th><th>Indentation</th></tr>
</thead>
<tbody>
<tr><td>A</td><td>B</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>Table</th><th>Indentation</th></tr>
</thead>
<tbody>
<tr><td>A</td><td>B</td></tr>
</tbody>
</table>
<table>
<thead>
<tr><th>Table</th><th>Indentation</th></tr>
</thead>
<tbody>
<tr><td>A</td><td>B</td></tr>
</tbody>
</table>
<pre><code>| Table | Indentation |
</code></pre>
<p> | ----- | ---- |
| A | B |</p>
<table>
<thead>
<tr><th align="left">Table</th><th>Indentation</th></tr>
</thead>
<tbody>
</tbody>
</table>
<pre><code>| A | B |
</code></pre>
<table>
<thead>
<tr><th>Item</th><th align="right">Value</th></tr>
</thead>
<tbody>
<tr><td>Computer</td><td align="right">$1600</td></tr>
<tr><td>Phone</td><td align="right">$12</td></tr>
<tr><td>Pipe</td><td align="right">$1</td></tr>
</tbody>
</table>

View File

@@ -0,0 +1,122 @@
Tables
------
First Header | Second Header
------------- | -------------
Content Cell | Content Cell
Content Cell | Content Cell
| First Header | Second Header |
| ------------- | ------------- |
| Content Cell | Content Cell |
| Content Cell | Content Cell |
| Name | Description |
| ------------- | ----------- |
| Help | Display the help window.|
| Close | Closes a window |
| Name | Description |
| ------------- | ----------- |
| Help | **Display the** help window.|
| Close | _Closes_ a window |
| Default-Align | Left-Aligned | Center Aligned | Right Aligned |
| ------------- | :------------ |:---------------:| -----:|
| 1 | col 3 is | some wordy text | $1600 |
| 2 | col 2 is | centered | $12 |
| 3 | zebra stripes | are neat | $1 |
Simple | Table
------ | -----
1 | 2
3 | 4
| Simple | Table |
| ------ | ----- |
| 1 | 2 |
| 3 | 4 |
| 3 | 4 \|
| 3 | 4 \\|
Check https://github.com/erusev/parsedown/issues/184 for the following:
Foo | Bar | State
------ | ------ | -----
`Code | Pipe` | Broken | Blank
`Escaped Code \| Pipe` | Broken | Blank
Escaped \| Pipe | Broken | Blank
Escaped \\| Pipe | Broken | Blank
Escaped \\ | Pipe | Broken | Blank
| Simple | Table |
| :----- | ----- |
| 3 | 4 |
3 | 4
5
Mixed | Table
------ | -----
| 1 | 2
3 | 4
| Mixed | Table
------ | -----
| 1 | 2
3 | 4
Mixed | Table
|------ | ----- |
1 | 2
| 3 | 4 |
some text
| single col |
| -- | -- |
| 1 |
2
3
| Table | With | Empty | Cells |
| ----- | ---- | ----- | ----- |
| | | | |
| a | | b | |
| | a | | b |
| a | | | b |
| | a | b | |
|
-- | --
|
| | |
| - | - |
| | |
| Table | Indentation |
| ----- | ---- |
| A | B |
| Table | Indentation |
| ----- | ---- |
| A | B |
| Table | Indentation |
| ----- | ---- |
| A | B |
| Table | Indentation |
| ----- | ---- |
| A | B |
| Table | Indentation |
| :----- | ---- |
| A | B |
| Item | Value |
| --------- | -----:|
| Computer | $1600 |
| Phone | $12 |
| Pipe | $1 |

View File

@@ -0,0 +1,8 @@
<p>Not a headline but a code block:</p>
<pre><code>---
</code></pre>
<p>Not a headline but two HR:</p>
<hr />
<hr />
<hr />
<hr />

View File

@@ -0,0 +1,14 @@
Not a headline but a code block:
```
---
```
Not a headline but two HR:
***
---
---
***

View File

@@ -0,0 +1,17 @@
<p>here is the url: <a href="http://www.cebe.cc/">http://www.cebe.cc/</a></p>
<p>here is the url: <a href="http://www.cebe.cc">http://www.cebe.cc</a></p>
<p>here is the url: <a href="http://www.cebe.cc/">http://www.cebe.cc/</a> and some text</p>
<p>using http is cool and http:// is the beginning of an url.</p>
<p>link should be url decoded: <a href="http://en.wikipedia.org/wiki/Mase_%28disambiguation%29">http://en.wikipedia.org/wiki/Mase_(disambiguation)</a></p>
<p>link in the end of the sentence: See this <a href="http://example.com/">http://example.com/</a>.</p>
<p>this one is in parenthesis (<a href="http://example.com/">http://example.com/</a>).</p>
<p>this one is in parenthesis (<a href="http://example.com:80/?id=1,2,3">http://example.com:80/?id=1,2,3</a>).</p>
<p>... (see <a href="http://en.wikipedia.org/wiki/Port_(computer_networking)">http://en.wikipedia.org/wiki/Port_(computer_networking)</a>).</p>
<p>... (see <a href="https://en.wikipedia.org/wiki/Port_(computer_networking)_more">https://en.wikipedia.org/wiki/Port_(computer_networking)_more</a>).</p>
<p>... (see <a href="https://en.wikipedia.org/wiki/Port_(computer_networking)_more">https://en.wikipedia.org/wiki/Port_(computer_networking)_more</a>). ... (see <a href="http://en.wikipedia.org/wiki/Port_(computer_networking)">http://en.wikipedia.org/wiki/Port_(computer_networking)</a>).</p>
<p>... (see <a href="https://en.wikipedia.org/wiki/Port_(computer_networking)_more">https://en.wikipedia.org/wiki/Port_(computer_networking)_more</a>)....(<a href="http://en.wikipedia.org/wiki/Port_(computer_networking)">http://en.wikipedia.org/wiki/Port_(computer_networking)</a>).</p>
<p>... (see <a href="http://en.wikipedia.org/wiki/Port">http://en.wikipedia.org/wiki/Port</a>)</p>
<p>... (see <a href="http://en.wikipedia.org/wiki/Port">http://en.wikipedia.org/wiki/Port</a>).</p>
<p><a href="http://www.cebe.cc">http://www.cebe.cc</a>, <a href="http://www.cebe.net">http://www.cebe.net</a>, and so on</p>
<p><a href="http://www.google.com/">link to http://www.google.com/</a></p>
<p>contact me at <strong><a href="http://cebe.cc/contact">http://cebe.cc/contact</a></strong>.</p>

View File

@@ -0,0 +1,33 @@
here is the url: http://www.cebe.cc/
here is the url: http://www.cebe.cc
here is the url: http://www.cebe.cc/ and some text
using http is cool and http:// is the beginning of an url.
link should be url decoded: http://en.wikipedia.org/wiki/Mase_%28disambiguation%29
link in the end of the sentence: See this http://example.com/.
this one is in parenthesis (http://example.com/).
this one is in parenthesis (http://example.com:80/?id=1,2,3).
... (see http://en.wikipedia.org/wiki/Port_(computer_networking)).
... (see https://en.wikipedia.org/wiki/Port_(computer_networking)_more).
... (see https://en.wikipedia.org/wiki/Port_(computer_networking)_more). ... (see http://en.wikipedia.org/wiki/Port_(computer_networking)).
... (see https://en.wikipedia.org/wiki/Port_(computer_networking)_more)....(http://en.wikipedia.org/wiki/Port_(computer_networking)).
... (see http://en.wikipedia.org/wiki/Port)
... (see http://en.wikipedia.org/wiki/Port).
http://www.cebe.cc, http://www.cebe.net, and so on
[link to http://www.google.com/](http://www.google.com/)
contact me at <strong>http://cebe.cc/contact</strong>.

View File

@@ -0,0 +1 @@
All tests prefixed with `md1_` are taken from http://daringfireball.net/projects/downloads/MarkdownTest_1.0.zip

View File

@@ -0,0 +1,11 @@
<blockquote><h2>This is a header.</h2>
<ol>
<li>This is the first list item.</li>
<li>This is the second list item.</li>
</ol>
<p>Here's some example code:</p>
<pre><code>return shell_exec("echo $input | $markdown_script");
</code></pre>
<blockquote><p>quote here</p>
</blockquote>
</blockquote>

View File

@@ -0,0 +1,10 @@
> ## This is a header.
>
> 1. This is the first list item.
> 2. This is the second list item.
>
> Here's some example code:
>
> return shell_exec("echo $input | $markdown_script");
>
> > quote here

View File

@@ -0,0 +1,9 @@
<blockquote><p>test test
test</p>
</blockquote>
<blockquote><p>test
test
test</p>
</blockquote>
<p>test</p>
<p>&gt;this is not a quote</p>

Some files were not shown because too many files have changed in this diff Show More